diff --git a/BlueMapBukkit/src/main/java/de/bluecolored/bluemap/bukkit/BukkitPlayer.java b/BlueMapBukkit/src/main/java/de/bluecolored/bluemap/bukkit/BukkitPlayer.java new file mode 100644 index 00000000..277833ff --- /dev/null +++ b/BlueMapBukkit/src/main/java/de/bluecolored/bluemap/bukkit/BukkitPlayer.java @@ -0,0 +1,138 @@ +/* + * This file is part of BlueMap, licensed under the MIT License (MIT). + * + * Copyright (c) Blue (Lukas Rieger) + * Copyright (c) contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package de.bluecolored.bluemap.bukkit; + +import java.lang.ref.WeakReference; +import java.util.EnumMap; +import java.util.Map; +import java.util.UUID; + +import org.bukkit.Bukkit; +import org.bukkit.GameMode; +import org.bukkit.Location; +import org.bukkit.potion.PotionEffectType; + +import com.flowpowered.math.vector.Vector3d; + +import de.bluecolored.bluemap.common.plugin.serverinterface.Gamemode; +import de.bluecolored.bluemap.common.plugin.serverinterface.Player; +import de.bluecolored.bluemap.common.plugin.text.Text; + +public class BukkitPlayer implements Player { + + private static final Map GAMEMODE_MAP = new EnumMap<>(GameMode.class); + static { + GAMEMODE_MAP.put(GameMode.ADVENTURE, Gamemode.ADVENTURE); + GAMEMODE_MAP.put(GameMode.SURVIVAL, Gamemode.SURVIVAL); + GAMEMODE_MAP.put(GameMode.CREATIVE, Gamemode.CREATIVE); + GAMEMODE_MAP.put(GameMode.SPECTATOR, Gamemode.SPECTATOR); + } + + private UUID uuid; + private Text name; + private UUID world; + private Vector3d position; + private boolean online; + private boolean sneaking; + private boolean invisible; + private Gamemode gamemode; + + private WeakReference delegate; + + public BukkitPlayer(org.bukkit.entity.Player delegate) { + this.uuid = delegate.getUniqueId(); + this.delegate = new WeakReference<>(delegate); + update(); + } + + @Override + public UUID getUuid() { + return this.uuid; + } + + @Override + public Text getName() { + return this.name; + } + + @Override + public UUID getWorld() { + return this.world; + } + + @Override + public Vector3d getPosition() { + return this.position; + } + + @Override + public boolean isOnline() { + return this.online; + } + + @Override + public boolean isSneaking() { + return this.sneaking; + } + + @Override + public boolean isInvisible() { + return this.invisible; + } + + @Override + public Gamemode getGamemode() { + return this.gamemode; + } + + /** + * API access, only call on server thread! + */ + public void update() { + org.bukkit.entity.Player player = delegate.get(); + if (player == null) { + player = Bukkit.getPlayer(uuid); + if (player == null) { + this.online = false; + return; + } + + delegate = new WeakReference<>(player); + } + + this.gamemode = GAMEMODE_MAP.get(player.getGameMode()); + + this.invisible = player.hasPotionEffect(PotionEffectType.INVISIBILITY); + + this.name = Text.of(player.getName()); + this.online = player.isOnline(); + + Location location = player.getLocation(); + this.position = new Vector3d(location.getX(), location.getY(), location.getZ()); + this.sneaking = player.isSneaking(); + this.world = player.getWorld().getUID(); + } + +} diff --git a/BlueMapBukkit/src/main/java/de/bluecolored/bluemap/bukkit/BukkitPlugin.java b/BlueMapBukkit/src/main/java/de/bluecolored/bluemap/bukkit/BukkitPlugin.java index 3c3637b2..dc7d2cb8 100644 --- a/BlueMapBukkit/src/main/java/de/bluecolored/bluemap/bukkit/BukkitPlugin.java +++ b/BlueMapBukkit/src/main/java/de/bluecolored/bluemap/bukkit/BukkitPlugin.java @@ -27,8 +27,15 @@ import java.io.File; import java.io.IOException; import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Optional; import java.util.UUID; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutionException; import java.util.concurrent.Future; @@ -37,14 +44,20 @@ import org.bukkit.World; import org.bukkit.command.CommandMap; import org.bukkit.command.defaults.BukkitCommand; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.Listener; +import org.bukkit.event.player.PlayerJoinEvent; +import org.bukkit.event.player.PlayerQuitEvent; import org.bukkit.plugin.java.JavaPlugin; import de.bluecolored.bluemap.common.plugin.Plugin; +import de.bluecolored.bluemap.common.plugin.serverinterface.Player; import de.bluecolored.bluemap.common.plugin.serverinterface.ServerEventListener; import de.bluecolored.bluemap.common.plugin.serverinterface.ServerInterface; import de.bluecolored.bluemap.core.logger.Logger; -public class BukkitPlugin extends JavaPlugin implements ServerInterface { +public class BukkitPlugin extends JavaPlugin implements ServerInterface, Listener { private static BukkitPlugin instance; @@ -52,8 +65,15 @@ public class BukkitPlugin extends JavaPlugin implements ServerInterface { private EventForwarder eventForwarder; private BukkitCommands commands; + private int playerUpdateIndex = 0; + private Map onlinePlayerMap; + private List onlinePlayerList; + public BukkitPlugin() { Logger.global = new JavaLogger(getLogger()); + + this.onlinePlayerMap = new ConcurrentHashMap<>(); + this.onlinePlayerList = Collections.synchronizedList(new ArrayList<>()); this.eventForwarder = new EventForwarder(); this.bluemap = new Plugin("bukkit", this); @@ -73,6 +93,7 @@ public void onEnable() { } //register events + getServer().getPluginManager().registerEvents(this, this); getServer().getPluginManager().registerEvents(eventForwarder, this); //register commands @@ -92,6 +113,9 @@ public void onEnable() { //tab completions getServer().getPluginManager().registerEvents(commands, this); + //start updating players + getServer().getScheduler().runTaskTimer(this, this::updateSomePlayers, 1, 1); + //load bluemap getServer().getScheduler().runTaskAsynchronously(this, () -> { try { @@ -107,6 +131,7 @@ public void onEnable() { @Override public void onDisable() { Logger.global.logInfo("Stopping..."); + getServer().getScheduler().cancelTasks(this); bluemap.unload(); Logger.global.logInfo("Saved and stopped!"); } @@ -179,4 +204,51 @@ public static BukkitPlugin getInstance() { return instance; } + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void onPlayerJoin(PlayerJoinEvent evt) { + BukkitPlayer player = new BukkitPlayer(evt.getPlayer()); + onlinePlayerMap.put(evt.getPlayer().getUniqueId(), player); + onlinePlayerList.add(player); + } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void onPlayerLeave(PlayerQuitEvent evt) { + UUID playerUUID = evt.getPlayer().getUniqueId(); + onlinePlayerMap.remove(playerUUID); + synchronized (onlinePlayerList) { + onlinePlayerList.removeIf(p -> p.getUuid().equals(playerUUID)); + } + } + + @Override + public Collection getOnlinePlayers() { + return onlinePlayerMap.values(); + } + + @Override + public Optional getPlayer(UUID uuid) { + return Optional.ofNullable(onlinePlayerMap.get(uuid)); + } + + /** + * Only update some of the online players each tick to minimize performance impact on the server-thread. + * Only call this method on the server-thread. + */ + private void updateSomePlayers() { + int onlinePlayerCount = onlinePlayerList.size(); + if (onlinePlayerCount == 0) return; + + int playersToBeUpdated = onlinePlayerCount / 20; //with 20 tps, each player is updated once a second + if (playersToBeUpdated == 0) playersToBeUpdated = 1; + + for (int i = 0; i < playersToBeUpdated; i++) { + playerUpdateIndex++; + if (playerUpdateIndex >= 20 && playerUpdateIndex >= onlinePlayerCount) playerUpdateIndex = 0; + + if (playerUpdateIndex < onlinePlayerCount) { + onlinePlayerList.get(i).update(); + } + } + } + } diff --git a/BlueMapBukkit/src/main/java/de/bluecolored/bluemap/bukkit/EventForwarder.java b/BlueMapBukkit/src/main/java/de/bluecolored/bluemap/bukkit/EventForwarder.java index a307437c..b800eaa5 100644 --- a/BlueMapBukkit/src/main/java/de/bluecolored/bluemap/bukkit/EventForwarder.java +++ b/BlueMapBukkit/src/main/java/de/bluecolored/bluemap/bukkit/EventForwarder.java @@ -42,6 +42,9 @@ import org.bukkit.event.block.BlockGrowEvent; import org.bukkit.event.block.BlockPlaceEvent; import org.bukkit.event.block.BlockSpreadEvent; +import org.bukkit.event.player.AsyncPlayerChatEvent; +import org.bukkit.event.player.PlayerJoinEvent; +import org.bukkit.event.player.PlayerQuitEvent; import org.bukkit.event.world.ChunkPopulateEvent; import org.bukkit.event.world.WorldSaveEvent; @@ -49,6 +52,7 @@ import com.flowpowered.math.vector.Vector3i; import de.bluecolored.bluemap.common.plugin.serverinterface.ServerEventListener; +import de.bluecolored.bluemap.common.plugin.text.Text; public class EventForwarder implements Listener { @@ -68,7 +72,7 @@ public synchronized void removeAllListeners() { @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) public synchronized void onWorldSaveToDisk(WorldSaveEvent evt) { - listeners.forEach(l -> l.onWorldSaveToDisk(evt.getWorld().getUID())); + for (ServerEventListener listener : listeners) listener.onWorldSaveToDisk(evt.getWorld().getUID()); } @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) @@ -119,7 +123,7 @@ public void onBlockChange(BlockFertilizeEvent evt) { private synchronized void onBlockChange(Location loc) { UUID world = loc.getWorld().getUID(); Vector3i pos = new Vector3i(loc.getBlockX(), loc.getBlockY(), loc.getBlockZ()); - listeners.forEach(l -> l.onBlockChange(world, pos)); + for (ServerEventListener listener : listeners) listener.onBlockChange(world, pos); } @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) @@ -127,7 +131,23 @@ public synchronized void onChunkFinishedGeneration(ChunkPopulateEvent evt) { Chunk chunk = evt.getChunk(); UUID world = chunk.getWorld().getUID(); Vector2i chunkPos = new Vector2i(chunk.getX(), chunk.getZ()); - listeners.forEach(l -> l.onChunkFinishedGeneration(world, chunkPos)); + for (ServerEventListener listener : listeners) listener.onChunkFinishedGeneration(world, chunkPos); + } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public synchronized void onPlayerJoin(PlayerJoinEvent evt) { + for (ServerEventListener listener : listeners) listener.onPlayerJoin(evt.getPlayer().getUniqueId()); + } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public synchronized void onPlayerLeave(PlayerQuitEvent evt) { + for (ServerEventListener listener : listeners) listener.onPlayerJoin(evt.getPlayer().getUniqueId()); + } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public synchronized void onPlayerChat(AsyncPlayerChatEvent evt) { + String message = String.format(evt.getFormat(), evt.getPlayer().getDisplayName(), evt.getMessage()); + for (ServerEventListener listener : listeners) listener.onChatMessage(Text.of(message)); } } diff --git a/BlueMapBukkit/src/main/resources/bluemap-bukkit-defaults.conf b/BlueMapBukkit/src/main/resources/bluemap-bukkit-defaults.conf index c6f20621..98e58691 100644 --- a/BlueMapBukkit/src/main/resources/bluemap-bukkit-defaults.conf +++ b/BlueMapBukkit/src/main/resources/bluemap-bukkit-defaults.conf @@ -9,3 +9,9 @@ webserver { port: 8100 maxConnectionCount: 100 } +liveUpdates { + enabled: true + hiddenGameModes: [] + hideInvisible: true + hideSneaking: false +} diff --git a/BlueMapBukkit/src/main/resources/bluemap-bukkit.conf b/BlueMapBukkit/src/main/resources/bluemap-bukkit.conf index c3a60ee9..bf44441c 100644 --- a/BlueMapBukkit/src/main/resources/bluemap-bukkit.conf +++ b/BlueMapBukkit/src/main/resources/bluemap-bukkit.conf @@ -38,6 +38,7 @@ webroot: "bluemap/web" #webdata: "path/to/data/folder" # If the web-application should use cookies to save the configurations of a user. +# Default is true useCookies: true webserver { @@ -165,3 +166,23 @@ maps: [ } ] + +liveUpdates { + # If the server should send live-updates and player-positions. + # Default is true + enabled: true + + # A list of gamemodes that will prevent a player from appearing on the map. + # Possible values are: survival, creative, spectator, adventure + hiddenGameModes: [ + "spectator" + ] + + # If this is true, players that have an invisibility (potion-)effect will be hidden on the map. + # Default is true + hideInvisible: true + + # If this is true, players that are sneaking will be hidden on the map. + # Default is false + hideSneaking: false +} diff --git a/BlueMapCLI/src/main/java/de/bluecolored/bluemap/cli/BlueMapCLI.java b/BlueMapCLI/src/main/java/de/bluecolored/bluemap/cli/BlueMapCLI.java index 93506a45..d3d9f5a4 100644 --- a/BlueMapCLI/src/main/java/de/bluecolored/bluemap/cli/BlueMapCLI.java +++ b/BlueMapCLI/src/main/java/de/bluecolored/bluemap/cli/BlueMapCLI.java @@ -57,6 +57,7 @@ import com.flowpowered.math.vector.Vector2i; import com.google.common.base.Preconditions; +import de.bluecolored.bluemap.common.BlueMapWebServer; import de.bluecolored.bluemap.common.MapType; import de.bluecolored.bluemap.common.RenderManager; import de.bluecolored.bluemap.common.RenderTask; @@ -72,7 +73,6 @@ import de.bluecolored.bluemap.core.render.lowres.LowresModelManager; import de.bluecolored.bluemap.core.resourcepack.ParseResourceException; import de.bluecolored.bluemap.core.resourcepack.ResourcePack; -import de.bluecolored.bluemap.core.web.BlueMapWebServer; import de.bluecolored.bluemap.core.web.WebFilesManager; import de.bluecolored.bluemap.core.web.WebSettings; import de.bluecolored.bluemap.core.world.SlicedWorld; diff --git a/BlueMapCore/src/main/java/de/bluecolored/bluemap/core/web/BlueMapWebServer.java b/BlueMapCommon/src/main/java/de/bluecolored/bluemap/common/BlueMapWebServer.java similarity index 68% rename from BlueMapCore/src/main/java/de/bluecolored/bluemap/core/web/BlueMapWebServer.java rename to BlueMapCommon/src/main/java/de/bluecolored/bluemap/common/BlueMapWebServer.java index 79db02cc..bdfd1fe9 100644 --- a/BlueMapCore/src/main/java/de/bluecolored/bluemap/core/web/BlueMapWebServer.java +++ b/BlueMapCommon/src/main/java/de/bluecolored/bluemap/common/BlueMapWebServer.java @@ -22,23 +22,41 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ -package de.bluecolored.bluemap.core.web; +package de.bluecolored.bluemap.common; import java.io.IOException; +import de.bluecolored.bluemap.common.live.LiveAPIRequestHandler; +import de.bluecolored.bluemap.common.plugin.serverinterface.ServerInterface; +import de.bluecolored.bluemap.core.config.LiveAPISettings; import de.bluecolored.bluemap.core.logger.Logger; +import de.bluecolored.bluemap.core.web.FileRequestHandler; +import de.bluecolored.bluemap.core.web.WebFilesManager; +import de.bluecolored.bluemap.core.web.WebServerConfig; import de.bluecolored.bluemap.core.webserver.WebServer; public class BlueMapWebServer extends WebServer { private WebFilesManager webFilesManager; + public BlueMapWebServer(WebServerConfig config) { super( config.getWebserverPort(), config.getWebserverMaxConnections(), config.getWebserverBindAdress(), - new BlueMapWebRequestHandler(config.getWebRoot()) + new FileRequestHandler(config.getWebRoot(), "BlueMap/Webserver") + ); + + this.webFilesManager = new WebFilesManager(config.getWebRoot()); + } + + public BlueMapWebServer(WebServerConfig config, LiveAPISettings liveSettings, ServerInterface server) { + super( + config.getWebserverPort(), + config.getWebserverMaxConnections(), + config.getWebserverBindAdress(), + new LiveAPIRequestHandler(server, liveSettings, new FileRequestHandler(config.getWebRoot(), "BlueMap/Webserver")) ); this.webFilesManager = new WebFilesManager(config.getWebRoot()); diff --git a/BlueMapCommon/src/main/java/de/bluecolored/bluemap/common/live/LiveAPIRequestHandler.java b/BlueMapCommon/src/main/java/de/bluecolored/bluemap/common/live/LiveAPIRequestHandler.java new file mode 100644 index 00000000..f61c611e --- /dev/null +++ b/BlueMapCommon/src/main/java/de/bluecolored/bluemap/common/live/LiveAPIRequestHandler.java @@ -0,0 +1,118 @@ +/* + * This file is part of BlueMap, licensed under the MIT License (MIT). + * + * Copyright (c) Blue (Lukas Rieger) + * Copyright (c) contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package de.bluecolored.bluemap.common.live; + +import java.io.IOException; +import java.io.StringWriter; +import java.util.HashMap; +import java.util.Map; + +import com.google.gson.stream.JsonWriter; + +import de.bluecolored.bluemap.common.plugin.serverinterface.Player; +import de.bluecolored.bluemap.common.plugin.serverinterface.ServerInterface; +import de.bluecolored.bluemap.core.config.LiveAPISettings; +import de.bluecolored.bluemap.core.webserver.HttpRequest; +import de.bluecolored.bluemap.core.webserver.HttpRequestHandler; +import de.bluecolored.bluemap.core.webserver.HttpResponse; +import de.bluecolored.bluemap.core.webserver.HttpStatusCode; + +public class LiveAPIRequestHandler implements HttpRequestHandler { + + private HttpRequestHandler notFoundHandler; + private Map liveAPIRequests; + private ServerInterface server; + + private LiveAPISettings config; + + public LiveAPIRequestHandler(ServerInterface server, LiveAPISettings config, HttpRequestHandler notFoundHandler) { + this.server = server; + this.notFoundHandler = notFoundHandler; + + this.liveAPIRequests = new HashMap<>(); + + this.liveAPIRequests.put("live", this::handleLivePingRequest); + this.liveAPIRequests.put("live/players", this::handlePlayersRequest); + + this.config = config; + } + + @Override + public HttpResponse handle(HttpRequest request) { + if (!config.isLiveUpdatesEnabled()) return this.notFoundHandler.handle(request); + + HttpRequestHandler handler = liveAPIRequests.get(request.getPath()); + if (handler != null) return handler.handle(request); + + return this.notFoundHandler.handle(request); + } + + public HttpResponse handleLivePingRequest(HttpRequest request) { + HttpResponse response = new HttpResponse(HttpStatusCode.OK); + response.setData("{\"status\":\"OK\"}"); + return response; + } + + public HttpResponse handlePlayersRequest(HttpRequest request) { + if (!request.getMethod().equalsIgnoreCase("GET")) return new HttpResponse(HttpStatusCode.BAD_REQUEST); + + try ( + StringWriter data = new StringWriter(); + JsonWriter json = new JsonWriter(data); + ){ + + json.beginObject(); + json.name("players").beginArray(); + for (Player player : server.getOnlinePlayers()) { + + if (config.isHideInvisible() && player.isInvisible()) continue; + if (config.isHideSneaking() && player.isSneaking()) continue; + if (config.getHiddenGameModes().contains(player.getGamemode().getId())) continue; + + json.beginObject(); + json.name("uuid").value(player.getUuid().toString()); + json.name("name").value(player.getName().toPlainString()); + json.name("world").value(player.getWorld().toString()); + json.name("position").beginObject(); + json.name("x").value(player.getPosition().getX()); + json.name("y").value(player.getPosition().getY()); + json.name("z").value(player.getPosition().getZ()); + json.endObject(); + json.endObject(); + } + json.endArray(); + json.endObject(); + + HttpResponse response = new HttpResponse(HttpStatusCode.OK); + response.setData(data.toString()); + + return response; + + } catch (IOException ex) { + return new HttpResponse(HttpStatusCode.INTERNAL_SERVER_ERROR); + } + } + +} diff --git a/BlueMapCommon/src/main/java/de/bluecolored/bluemap/common/plugin/Plugin.java b/BlueMapCommon/src/main/java/de/bluecolored/bluemap/common/plugin/Plugin.java index 17daded0..4c99bdac 100644 --- a/BlueMapCommon/src/main/java/de/bluecolored/bluemap/common/plugin/Plugin.java +++ b/BlueMapCommon/src/main/java/de/bluecolored/bluemap/common/plugin/Plugin.java @@ -46,10 +46,12 @@ import com.flowpowered.math.vector.Vector2i; +import de.bluecolored.bluemap.common.BlueMapWebServer; import de.bluecolored.bluemap.common.MapType; import de.bluecolored.bluemap.common.RenderManager; import de.bluecolored.bluemap.common.api.BlueMapAPIImpl; import de.bluecolored.bluemap.common.plugin.serverinterface.ServerInterface; +import de.bluecolored.bluemap.common.plugin.skins.PlayerSkinUpdater; import de.bluecolored.bluemap.core.config.ConfigManager; import de.bluecolored.bluemap.core.config.MainConfig; import de.bluecolored.bluemap.core.config.MainConfig.MapConfig; @@ -62,7 +64,6 @@ import de.bluecolored.bluemap.core.render.lowres.LowresModelManager; import de.bluecolored.bluemap.core.resourcepack.ParseResourceException; import de.bluecolored.bluemap.core.resourcepack.ResourcePack; -import de.bluecolored.bluemap.core.web.BlueMapWebServer; import de.bluecolored.bluemap.core.web.WebFilesManager; import de.bluecolored.bluemap.core.web.WebSettings; import de.bluecolored.bluemap.core.world.SlicedWorld; @@ -86,6 +87,7 @@ public class Plugin { private Map maps; private MapUpdateHandler updateHandler; + private PlayerSkinUpdater skinUpdater; private RenderManager renderManager; private BlueMapWebServer webServer; @@ -269,6 +271,12 @@ public synchronized void load() throws IOException, ParseResourceException { this.updateHandler = new MapUpdateHandler(this); serverInterface.registerListener(updateHandler); + //start skin updater + if (config.isLiveUpdatesEnabled()) { + this.skinUpdater = new PlayerSkinUpdater(config.getWebRoot().resolve("assets").resolve("playerheads").toFile()); + serverInterface.registerListener(skinUpdater); + } + //create/update webfiles WebFilesManager webFilesManager = new WebFilesManager(config.getWebRoot()); if (webFilesManager.needsUpdate()) { @@ -293,7 +301,7 @@ public synchronized void load() throws IOException, ParseResourceException { //start webserver if (config.isWebserverEnabled()) { - webServer = new BlueMapWebServer(config); + webServer = new BlueMapWebServer(config, config, serverInterface); webServer.updateWebfiles(); webServer.start(); } diff --git a/BlueMapCommon/src/main/java/de/bluecolored/bluemap/common/plugin/serverinterface/Gamemode.java b/BlueMapCommon/src/main/java/de/bluecolored/bluemap/common/plugin/serverinterface/Gamemode.java new file mode 100644 index 00000000..2ed0e03a --- /dev/null +++ b/BlueMapCommon/src/main/java/de/bluecolored/bluemap/common/plugin/serverinterface/Gamemode.java @@ -0,0 +1,54 @@ +/* + * This file is part of BlueMap, licensed under the MIT License (MIT). + * + * Copyright (c) Blue (Lukas Rieger) + * Copyright (c) contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package de.bluecolored.bluemap.common.plugin.serverinterface; + +public enum Gamemode { + + SURVIVAL ("survival"), + CREATIVE ("creative"), + ADVENTURE ("adventure"), + SPECTATOR ("spectator"); + + private final String id; + + Gamemode(String id){ + this.id = id; + } + + public String getId() { + return id; + } + + public static Gamemode getById(String id) { + if (id == null) throw new NullPointerException("id cannot be null"); + + for (Gamemode gamemode : values()) { + if (gamemode.id.equals(id)) return gamemode; + } + + throw new IllegalArgumentException("There is no Gamemode with id: '" + id + "'"); + } + +} diff --git a/BlueMapCommon/src/main/java/de/bluecolored/bluemap/common/plugin/serverinterface/Player.java b/BlueMapCommon/src/main/java/de/bluecolored/bluemap/common/plugin/serverinterface/Player.java new file mode 100644 index 00000000..91f4c60e --- /dev/null +++ b/BlueMapCommon/src/main/java/de/bluecolored/bluemap/common/plugin/serverinterface/Player.java @@ -0,0 +1,64 @@ +/* + * This file is part of BlueMap, licensed under the MIT License (MIT). + * + * Copyright (c) Blue (Lukas Rieger) + * Copyright (c) contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package de.bluecolored.bluemap.common.plugin.serverinterface; + +import java.util.UUID; + +import com.flowpowered.math.vector.Vector3d; + +import de.bluecolored.bluemap.common.plugin.text.Text; + +public interface Player { + + public UUID getUuid(); + + public Text getName(); + + public UUID getWorld(); + + public Vector3d getPosition(); + + public boolean isOnline(); + + /** + * Return true if the player is sneaking. + *

If the player is offline the value of this method is undetermined.

+ * @return + */ + public boolean isSneaking(); + + /** + * Returns true if the player has an invisibillity effect + *

If the player is offline the value of this method is undetermined.

+ */ + public boolean isInvisible(); + + /** + * Returns the {@link Gamemode} this player is in + *

If the player is offline the value of this method is undetermined.

+ */ + public Gamemode getGamemode(); + +} diff --git a/BlueMapCommon/src/main/java/de/bluecolored/bluemap/common/plugin/serverinterface/ServerEventListener.java b/BlueMapCommon/src/main/java/de/bluecolored/bluemap/common/plugin/serverinterface/ServerEventListener.java index 85c3edad..cbb2a97f 100644 --- a/BlueMapCommon/src/main/java/de/bluecolored/bluemap/common/plugin/serverinterface/ServerEventListener.java +++ b/BlueMapCommon/src/main/java/de/bluecolored/bluemap/common/plugin/serverinterface/ServerEventListener.java @@ -29,12 +29,20 @@ import com.flowpowered.math.vector.Vector2i; import com.flowpowered.math.vector.Vector3i; +import de.bluecolored.bluemap.common.plugin.text.Text; + public interface ServerEventListener { - void onWorldSaveToDisk(UUID world); + default void onWorldSaveToDisk(UUID world) {}; - void onBlockChange(UUID world, Vector3i blockPos); + default void onBlockChange(UUID world, Vector3i blockPos) {}; - void onChunkFinishedGeneration(UUID world, Vector2i chunkPos); + default void onChunkFinishedGeneration(UUID world, Vector2i chunkPos) {}; + + default void onPlayerJoin(UUID playerUuid) {}; + + default void onPlayerLeave(UUID playerUuid) {}; + + default void onChatMessage(Text message) {}; } diff --git a/BlueMapCommon/src/main/java/de/bluecolored/bluemap/common/plugin/serverinterface/ServerInterface.java b/BlueMapCommon/src/main/java/de/bluecolored/bluemap/common/plugin/serverinterface/ServerInterface.java index 73327761..92ca74c6 100644 --- a/BlueMapCommon/src/main/java/de/bluecolored/bluemap/common/plugin/serverinterface/ServerInterface.java +++ b/BlueMapCommon/src/main/java/de/bluecolored/bluemap/common/plugin/serverinterface/ServerInterface.java @@ -26,6 +26,8 @@ import java.io.File; import java.io.IOException; +import java.util.Collection; +import java.util.Optional; import java.util.UUID; public interface ServerInterface { @@ -73,5 +75,16 @@ default boolean isMetricsEnabled(boolean configValue) { return configValue; } + /** + * Returns a collection of the states of players that are currently online + */ + Collection getOnlinePlayers(); + + /** + * Returns the state of the player with that UUID if present
+ * this method is only guaranteed to return a {@link PlayerState} if the player is currently online. + */ + Optional getPlayer(UUID uuid); + } diff --git a/BlueMapCommon/src/main/java/de/bluecolored/bluemap/common/plugin/skins/PlayerSkin.java b/BlueMapCommon/src/main/java/de/bluecolored/bluemap/common/plugin/skins/PlayerSkin.java new file mode 100644 index 00000000..5201f8dc --- /dev/null +++ b/BlueMapCommon/src/main/java/de/bluecolored/bluemap/common/plugin/skins/PlayerSkin.java @@ -0,0 +1,152 @@ +/* + * This file is part of BlueMap, licensed under the MIT License (MIT). + * + * Copyright (c) Blue (Lukas Rieger) + * Copyright (c) contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package de.bluecolored.bluemap.common.plugin.skins; + +import java.awt.Graphics2D; +import java.awt.image.BufferedImage; +import java.io.File; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.Reader; +import java.net.URL; +import java.util.Base64; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +import javax.imageio.ImageIO; + +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonParser; + +import de.bluecolored.bluemap.core.logger.Logger; + +public class PlayerSkin { + + private final UUID uuid; + private long lastUpdate; + + public PlayerSkin(UUID uuid) { + this.uuid = uuid; + this.lastUpdate = -1; + } + + public void update(File storageFolder) { + long now = System.currentTimeMillis(); + if (lastUpdate > 0 && lastUpdate + 600000 > now) return; // only update if skin is older than 10 minutes + + lastUpdate = now; + + new Thread(() -> { + try { + Future futureSkin = loadSkin(); + BufferedImage skin = futureSkin.get(10, TimeUnit.SECONDS); + BufferedImage head = createHead(skin); + ImageIO.write(head, "png", new File(storageFolder, uuid.toString() + ".png")); + } catch (ExecutionException | TimeoutException e) { + Logger.global.logWarning("Failed to load player-skin from mojang-servers: " + e); + } catch (IOException e) { + Logger.global.logError("Failed to write player-head image!", e); + } catch (InterruptedException ignore) {} + }).start(); + } + + public BufferedImage createHead(BufferedImage skinTexture) { + BufferedImage head = new BufferedImage(8, 8, skinTexture.getType()); + + BufferedImage layer1 = skinTexture.getSubimage(8, 8, 8, 8); + BufferedImage layer2 = skinTexture.getSubimage(40, 8, 8, 8); + + try { + Graphics2D g = head.createGraphics(); + g.drawImage(layer1, 0, 0, null); + g.drawImage(layer2, 0, 0, null); + } catch (Throwable t) { // There might be problems with headless servers when loading the graphics class + Logger.global.noFloodWarning("headless-graphics-fail", + "Could not access Graphics2D to render player-skin texture. Try adding '-Djava.awt.headless=true' to your startup flags or ignore this warning."); + + layer1.copyData(head.getRaster()); + } + + return head; + } + + public Future loadSkin() { + CompletableFuture image = new CompletableFuture<>(); + + new Thread(() -> { + try { + JsonParser parser = new JsonParser(); + try (Reader reader = requestProfileJson()) { + String textureInfoJson = readTextureInfoJson(parser.parse(reader)); + String textureUrl = readTextureUrl(parser.parse(textureInfoJson)); + image.complete(ImageIO.read(new URL(textureUrl))); + } + } catch (IOException e) { + image.completeExceptionally(e); + } + }).start(); + + return image; + } + + private Reader requestProfileJson() throws IOException { + URL url = new URL("https://sessionserver.mojang.com/session/minecraft/profile/" + this.uuid); + return new InputStreamReader(url.openStream()); + } + + private String readTextureInfoJson(JsonElement json) throws IOException { + try { + JsonArray properties = json.getAsJsonObject().getAsJsonArray("properties"); + + for (JsonElement element : properties) { + if (element.getAsJsonObject().get("name").getAsString().equals("textures")) { + return new String(Base64.getDecoder().decode(element.getAsJsonObject().get("value").getAsString().getBytes())); + } + } + + throw new IOException("No texture info found!"); + } catch (IllegalStateException | ClassCastException e) { + throw new IOException(e); + } + + } + + private String readTextureUrl(JsonElement json) throws IOException { + try { + return json.getAsJsonObject() + .getAsJsonObject("textures") + .getAsJsonObject("SKIN") + .get("url").getAsString(); + } catch (IllegalStateException | ClassCastException e) { + throw new IOException(e); + } + } + +} diff --git a/BlueMapCommon/src/main/java/de/bluecolored/bluemap/common/plugin/skins/PlayerSkinUpdater.java b/BlueMapCommon/src/main/java/de/bluecolored/bluemap/common/plugin/skins/PlayerSkinUpdater.java new file mode 100644 index 00000000..2332a3be --- /dev/null +++ b/BlueMapCommon/src/main/java/de/bluecolored/bluemap/common/plugin/skins/PlayerSkinUpdater.java @@ -0,0 +1,63 @@ +/* + * This file is part of BlueMap, licensed under the MIT License (MIT). + * + * Copyright (c) Blue (Lukas Rieger) + * Copyright (c) contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package de.bluecolored.bluemap.common.plugin.skins; + +import java.io.File; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; + +import de.bluecolored.bluemap.common.plugin.serverinterface.ServerEventListener; + +public class PlayerSkinUpdater implements ServerEventListener { + + private File storageFolder; + + private Map skins; + + public PlayerSkinUpdater(File storageFolder) { + this.storageFolder = storageFolder; + this.skins = new ConcurrentHashMap<>(); + + this.storageFolder.mkdirs(); + } + + public void updateSkin(UUID playerUuid) { + PlayerSkin skin = skins.get(playerUuid); + + if (skin == null) { + skin = new PlayerSkin(playerUuid); + skins.put(playerUuid, skin); + } + + skin.update(storageFolder); + } + + @Override + public void onPlayerJoin(UUID playerUuid) { + updateSkin(playerUuid); + } + +} diff --git a/BlueMapCore/src/main/java/de/bluecolored/bluemap/core/config/LiveAPISettings.java b/BlueMapCore/src/main/java/de/bluecolored/bluemap/core/config/LiveAPISettings.java new file mode 100644 index 00000000..121008e9 --- /dev/null +++ b/BlueMapCore/src/main/java/de/bluecolored/bluemap/core/config/LiveAPISettings.java @@ -0,0 +1,39 @@ +/* + * This file is part of BlueMap, licensed under the MIT License (MIT). + * + * Copyright (c) Blue (Lukas Rieger) + * Copyright (c) contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package de.bluecolored.bluemap.core.config; + +import java.util.Collection; + +public interface LiveAPISettings { + + boolean isLiveUpdatesEnabled(); + + Collection getHiddenGameModes(); + + boolean isHideInvisible(); + + boolean isHideSneaking(); + +} diff --git a/BlueMapCore/src/main/java/de/bluecolored/bluemap/core/config/MainConfig.java b/BlueMapCore/src/main/java/de/bluecolored/bluemap/core/config/MainConfig.java index 1e1b820e..c7da1a5d 100644 --- a/BlueMapCore/src/main/java/de/bluecolored/bluemap/core/config/MainConfig.java +++ b/BlueMapCore/src/main/java/de/bluecolored/bluemap/core/config/MainConfig.java @@ -31,6 +31,8 @@ import java.nio.file.Path; import java.nio.file.Paths; import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; import java.util.List; import java.util.regex.Pattern; @@ -43,7 +45,7 @@ import de.bluecolored.bluemap.core.web.WebServerConfig; import ninja.leaping.configurate.ConfigurationNode; -public class MainConfig implements WebServerConfig { +public class MainConfig implements WebServerConfig, LiveAPISettings { private static final Pattern VALID_ID_PATTERN = Pattern.compile("[a-zA-Z0-9_]+"); private boolean downloadAccepted = false; @@ -64,6 +66,11 @@ public class MainConfig implements WebServerConfig { private List mapConfigs = new ArrayList<>(); + private boolean liveUpdatesEnabled = false; + private Collection hiddenGameModes = Collections.emptyList(); + private boolean hideInvisible = false; + private boolean hideSneaking = false; + public MainConfig(ConfigurationNode node) throws OutdatedConfigException, IOException { checkOutdated(node); @@ -101,6 +108,19 @@ public MainConfig(ConfigurationNode node) throws OutdatedConfigException, IOExce //maps loadMapConfigs(node.getNode("maps")); + + //live-updates + ConfigurationNode liveNode = node.getNode("liveUpdates"); + liveUpdatesEnabled = liveNode.getNode("enabled").getBoolean(true); + + hiddenGameModes = new ArrayList<>(); + for (ConfigurationNode gameModeNode : liveNode.getNode("hiddenGameModes").getChildrenList()) { + hiddenGameModes.add(gameModeNode.getString()); + } + hiddenGameModes = Collections.unmodifiableCollection(hiddenGameModes); + + hideInvisible = liveNode.getNode("hideInvisible").getBoolean(true); + hideSneaking = liveNode.getNode("hideSneaking").getBoolean(false); } private void loadWebConfig(ConfigurationNode node) throws IOException { @@ -196,6 +216,26 @@ public List getMapConfigs(){ return mapConfigs; } + @Override + public boolean isLiveUpdatesEnabled() { + return this.liveUpdatesEnabled; + } + + @Override + public Collection getHiddenGameModes() { + return this.hiddenGameModes; + } + + @Override + public boolean isHideInvisible() { + return this.hideInvisible; + } + + @Override + public boolean isHideSneaking() { + return this.hideSneaking; + } + public class MapConfig implements RenderSettings { private String id; diff --git a/BlueMapCore/src/main/java/de/bluecolored/bluemap/core/resourcepack/TextureGallery.java b/BlueMapCore/src/main/java/de/bluecolored/bluemap/core/resourcepack/TextureGallery.java index 4aa93004..4c6f5afd 100644 --- a/BlueMapCore/src/main/java/de/bluecolored/bluemap/core/resourcepack/TextureGallery.java +++ b/BlueMapCore/src/main/java/de/bluecolored/bluemap/core/resourcepack/TextureGallery.java @@ -198,9 +198,7 @@ public synchronized Texture loadTexture(FileAccess fileAccess, String path) thro //crop off animation frames if (image.getHeight() > image.getWidth()){ - BufferedImage cropped = new BufferedImage(image.getWidth(), image.getWidth(), image.getType()); - image.copyData(cropped.getRaster()); - image = cropped; + image = image.getSubimage(0, 0, image.getWidth(), image.getWidth()); } //check halfTransparency diff --git a/BlueMapCore/src/main/java/de/bluecolored/bluemap/core/web/BlueMapWebRequestHandler.java b/BlueMapCore/src/main/java/de/bluecolored/bluemap/core/web/FileRequestHandler.java similarity index 90% rename from BlueMapCore/src/main/java/de/bluecolored/bluemap/core/web/BlueMapWebRequestHandler.java rename to BlueMapCore/src/main/java/de/bluecolored/bluemap/core/web/FileRequestHandler.java index 28116e61..ca77973d 100644 --- a/BlueMapCore/src/main/java/de/bluecolored/bluemap/core/web/BlueMapWebRequestHandler.java +++ b/BlueMapCore/src/main/java/de/bluecolored/bluemap/core/web/FileRequestHandler.java @@ -33,9 +33,7 @@ import java.nio.file.InvalidPathException; import java.nio.file.Path; import java.util.GregorianCalendar; -import java.util.HashMap; import java.util.Locale; -import java.util.Map; import java.util.Set; import java.util.TimeZone; import java.util.concurrent.TimeUnit; @@ -50,16 +48,18 @@ import de.bluecolored.bluemap.core.webserver.HttpResponse; import de.bluecolored.bluemap.core.webserver.HttpStatusCode; -public class BlueMapWebRequestHandler implements HttpRequestHandler { +public class FileRequestHandler implements HttpRequestHandler { private static final long DEFLATE_MIN_SIZE = 10L * 1024L; private static final long DEFLATE_MAX_SIZE = 10L * 1024L * 1024L; private static final long INFLATE_MAX_SIZE = 10L * 1024L * 1024L; private Path webRoot; + private String serverName; - public BlueMapWebRequestHandler(Path webRoot) { + public FileRequestHandler(Path webRoot, String serverName) { this.webRoot = webRoot; + this.serverName = serverName; } @Override @@ -70,11 +70,11 @@ public HttpResponse handle(HttpRequest request) { ) return new HttpResponse(HttpStatusCode.NOT_IMPLEMENTED); HttpResponse response = generateResponse(request); - response.addHeader("Server", "BlueMap/WebServer"); + response.addHeader("Server", this.serverName); HttpStatusCode status = response.getStatusCode(); if (status.getCode() >= 400){ - response.setData(status.getCode() + " - " + status.getMessage() + "\nBlueMap/Webserver"); + response.setData(status.getCode() + " - " + status.getMessage() + "\n" + this.serverName); } return response; @@ -82,23 +82,7 @@ public HttpResponse handle(HttpRequest request) { @SuppressWarnings ("resource") private HttpResponse generateResponse(HttpRequest request) { - String adress = request.getPath(); - if (adress.isEmpty()) adress = "/"; - String[] adressParts = adress.split("\\?", 2); - String path = adressParts[0]; - String getParamString = adressParts.length > 1 ? adressParts[1] : ""; - - Map getParams = new HashMap<>(); - for (String getParam : getParamString.split("&")){ - if (getParam.isEmpty()) continue; - String[] kv = getParam.split("=", 2); - String key = kv[0]; - String value = kv.length > 1 ? kv[1] : ""; - getParams.put(key, value); - } - - if (path.startsWith("/")) path = path.substring(1); - if (path.endsWith("/")) path = path.substring(0, path.length() - 1); + String path = request.getPath(); Path filePath = webRoot; try { diff --git a/BlueMapCore/src/main/java/de/bluecolored/bluemap/core/web/WebSettings.java b/BlueMapCore/src/main/java/de/bluecolored/bluemap/core/web/WebSettings.java index 5914eba8..fc33c212 100644 --- a/BlueMapCore/src/main/java/de/bluecolored/bluemap/core/web/WebSettings.java +++ b/BlueMapCore/src/main/java/de/bluecolored/bluemap/core/web/WebSettings.java @@ -136,6 +136,7 @@ public void setFrom(TileRenderer tileRenderer, String mapId) { public void setFrom(World world, String mapId) { set(world.getSpawnPoint().getX(), "maps", mapId, "startPos", "x"); set(world.getSpawnPoint().getZ(), "maps", mapId, "startPos", "z"); + set(world.getUUID().toString(), "maps", mapId, "world"); } public void setFrom(MapConfig mapConfig, String mapId) { diff --git a/BlueMapCore/src/main/java/de/bluecolored/bluemap/core/webserver/HttpRequest.java b/BlueMapCore/src/main/java/de/bluecolored/bluemap/core/webserver/HttpRequest.java index a8597c51..9142546b 100644 --- a/BlueMapCore/src/main/java/de/bluecolored/bluemap/core/webserver/HttpRequest.java +++ b/BlueMapCore/src/main/java/de/bluecolored/bluemap/core/webserver/HttpRequest.java @@ -50,15 +50,18 @@ public class HttpRequest { private static final Pattern REQUEST_PATTERN = Pattern.compile("^(\\w+) (\\S+) (.+)$"); private String method; - private String path; + private String adress; private String version; private Map> header; private Map> headerLC; private byte[] data; - public HttpRequest(String method, String path, String version, Map> header) { + private String path = null; + private Map getParams = null; + + public HttpRequest(String method, String adress, String version, Map> header) { this.method = method; - this.path = path; + this.adress = adress; this.version = version; this.header = header; this.headerLC = new HashMap<>(); @@ -79,8 +82,8 @@ public String getMethod() { return method; } - public String getPath(){ - return path; + public String getAdress(){ + return adress; } public String getVersion() { @@ -92,7 +95,7 @@ public Map> getHeader() { } public Map> getLowercaseHeader() { - return header; + return headerLC; } public Set getHeader(String key){ @@ -107,6 +110,40 @@ public Set getLowercaseHeader(String key){ return headerValues; } + public String getPath() { + if (path == null) parseAdress(); + return path; + } + + public Map getGETParams() { + if (getParams == null) parseAdress(); + return Collections.unmodifiableMap(getParams); + } + + private void parseAdress() { + String adress = this.adress; + if (adress.isEmpty()) adress = "/"; + String[] adressParts = adress.split("\\?", 2); + String path = adressParts[0]; + String getParamString = adressParts.length > 1 ? adressParts[1] : ""; + + Map getParams = new HashMap<>(); + for (String getParam : getParamString.split("&")){ + if (getParam.isEmpty()) continue; + String[] kv = getParam.split("=", 2); + String key = kv[0]; + String value = kv.length > 1 ? kv[1] : ""; + getParams.put(key, value); + } + + //normalize path + if (path.startsWith("/")) path = path.substring(1); + if (path.endsWith("/")) path = path.substring(0, path.length() - 1); + + this.path = path; + this.getParams = getParams; + } + public InputStream getData(){ return new ByteArrayInputStream(data); } diff --git a/BlueMapCore/src/main/java/de/bluecolored/bluemap/core/webserver/HttpRequestHandler.java b/BlueMapCore/src/main/java/de/bluecolored/bluemap/core/webserver/HttpRequestHandler.java index 23f887ef..e5ce2f31 100644 --- a/BlueMapCore/src/main/java/de/bluecolored/bluemap/core/webserver/HttpRequestHandler.java +++ b/BlueMapCore/src/main/java/de/bluecolored/bluemap/core/webserver/HttpRequestHandler.java @@ -24,6 +24,7 @@ */ package de.bluecolored.bluemap.core.webserver; +@FunctionalInterface public interface HttpRequestHandler { HttpResponse handle(HttpRequest request); diff --git a/BlueMapCore/src/main/webroot/assets/playerheads/alex.png b/BlueMapCore/src/main/webroot/assets/playerheads/alex.png new file mode 100644 index 00000000..a49a5217 Binary files /dev/null and b/BlueMapCore/src/main/webroot/assets/playerheads/alex.png differ diff --git a/BlueMapCore/src/main/webroot/assets/playerheads/steve.png b/BlueMapCore/src/main/webroot/assets/playerheads/steve.png new file mode 100644 index 00000000..6037770b Binary files /dev/null and b/BlueMapCore/src/main/webroot/assets/playerheads/steve.png differ diff --git a/BlueMapCore/src/main/webroot/js/libs/BlueMap.js b/BlueMapCore/src/main/webroot/js/libs/BlueMap.js index e736a919..d91d423b 100644 --- a/BlueMapCore/src/main/webroot/js/libs/BlueMap.js +++ b/BlueMapCore/src/main/webroot/js/libs/BlueMap.js @@ -60,9 +60,10 @@ import { stringToImage, pathFromCoords } from './utils.js'; import {cachePreventionNr, getCookie, setCookie} from "./utils"; export default class BlueMap { - constructor(element, dataRoot) { + constructor(element, dataRoot = "data/", liveApiRoot = "live/") { this.element = $('
').appendTo(element)[0]; this.dataRoot = dataRoot; + this.liveApiRoot = liveApiRoot; this.locationHash = ''; this.cacheSuffix = ''; diff --git a/BlueMapCore/src/main/webroot/js/libs/hud/MarkerManager.js b/BlueMapCore/src/main/webroot/js/libs/hud/MarkerManager.js index ea722a8e..ec15a1d2 100644 --- a/BlueMapCore/src/main/webroot/js/libs/hud/MarkerManager.js +++ b/BlueMapCore/src/main/webroot/js/libs/hud/MarkerManager.js @@ -3,6 +3,7 @@ import $ from "jquery"; import ToggleButton from "../ui/ToggleButton"; import Label from "../ui/Label"; import {cachePreventionNr} from "../utils"; +import PlayerMarkerSet from "./PlayerMarkerSet"; export default class MarkerManager { @@ -10,14 +11,23 @@ export default class MarkerManager { this.blueMap = blueMap; this.ui = ui; + this.markerData = null; + this.liveData = null; this.markerSets = []; + this.playerMarkerSet = null; + this.readyPromise = - this.loadMarkerData() - .catch(ignore => { - if (this.blueMap.debugInfo) console.debug("Failed load markers:", ignore); - }) - .then(this.loadMarkers); + Promise.all([ + this.loadMarkerData() + .catch(ignore => { + if (this.blueMap.debugInfo) console.debug("Failed load markers:", ignore); + }), + this.checkLiveAPI() + .then(this.initializePlayerMarkers) + ]) + .then(this.loadMarkers) + .then(this.updatePlayerMarkerLoop); $(document).on('bluemap-map-change', this.onBlueMapMapChange); } @@ -41,6 +51,25 @@ export default class MarkerManager { }); } + checkLiveAPI() { + return new Promise((resolve, reject) => { + this.blueMap.fileLoader.load(this.blueMap.liveApiRoot + 'players?' + cachePreventionNr(), + liveData => { + try { + this.liveData = JSON.parse(liveData); + resolve(); + } catch (e){ + reject(e); + } + }, + xhr => {}, + error => { + reject(); + } + ); + }); + } + loadMarkers = () => { if (this.markerData && this.markerData.markerSets) { this.markerData.markerSets.forEach(setData => { @@ -49,12 +78,27 @@ export default class MarkerManager { } }; + initializePlayerMarkers = () => { + if (this.liveData){ + this.playerMarkerSet = new PlayerMarkerSet(this.blueMap); + this.markerSets.push(this.playerMarkerSet); + } + }; + update(){ this.markerSets.forEach(markerSet => { markerSet.update(); }); } + updatePlayerMarkerLoop = () => { + if (this.playerMarkerSet){ + this.playerMarkerSet.updateLive(); + } + + setTimeout(this.updatePlayerMarkerLoop, 2000); + }; + addMenuElements(menu){ let addedLabel = false; this.markerSets.forEach(markerSet => { diff --git a/BlueMapCore/src/main/webroot/js/libs/hud/POIMarker.js b/BlueMapCore/src/main/webroot/js/libs/hud/POIMarker.js index 3a8cdfd4..216f5581 100644 --- a/BlueMapCore/src/main/webroot/js/libs/hud/POIMarker.js +++ b/BlueMapCore/src/main/webroot/js/libs/hud/POIMarker.js @@ -1,7 +1,6 @@ import $ from 'jquery'; import Marker from "./Marker"; import {CSS2DObject} from "./CSS2DRenderer"; -import {Vector3} from "three"; import POI from "../../../assets/poi.svg"; diff --git a/BlueMapCore/src/main/webroot/js/libs/hud/PlayerMarker.js b/BlueMapCore/src/main/webroot/js/libs/hud/PlayerMarker.js new file mode 100644 index 00000000..1129fa5b --- /dev/null +++ b/BlueMapCore/src/main/webroot/js/libs/hud/PlayerMarker.js @@ -0,0 +1,90 @@ +import $ from 'jquery'; +import Marker from "./Marker"; +import {CSS2DObject} from "./CSS2DRenderer"; + +export default class PlayerMarker extends Marker { + + constructor(blueMap, markerSet, markerData, playerUuid, worldUuid) { + super(blueMap, markerSet, markerData); + + this.online = false; + this.player = playerUuid; + this.world = worldUuid; + + this.animationRunning = false; + this.lastFrame = -1; + } + + setVisible(visible){ + this.visible = visible && this.online && this.world === this.blueMap.settings.maps[this.blueMap.map].world; + + this.blueMap.updateFrame = true; + + if (!this.renderObject){ + let iconElement = $(`
${this.label}
`); + iconElement.find("img").click(this.onClick); + + this.renderObject = new CSS2DObject(iconElement[0]); + this.renderObject.position.copy(this.position); + this.renderObject.onBeforeRender = (renderer, scene, camera) => { + let distanceSquared = this.position.distanceToSquared(camera.position); + if (distanceSquared > 1000000) { + iconElement.addClass("distant"); + } else { + iconElement.removeClass("distant"); + } + + this.updateRenderObject(this.renderObject, scene, camera); + }; + } + + if (this.visible) { + this.blueMap.hudScene.add(this.renderObject); + } else { + this.blueMap.hudScene.remove(this.renderObject); + } + } + + updatePosition = () => { + if (this.renderObject && !this.renderObject.position.equals(this.position)) { + if (this.visible) { + if (!this.animationRunning) { + this.animationRunning = true; + requestAnimationFrame(this.moveAnimation); + } + } else { + this.renderObject.position.copy(this.position); + } + } + }; + + moveAnimation = (time) => { + let delta = time - this.lastFrame; + if (this.lastFrame === -1){ + delta = 20; + } + this.lastFrame = time; + + if (this.renderObject && !this.renderObject.position.equals(this.position)) { + this.renderObject.position.x += (this.position.x - this.renderObject.position.x) * 0.01 * delta; + this.renderObject.position.y += (this.position.y - this.renderObject.position.y) * 0.01 * delta; + this.renderObject.position.z += (this.position.z - this.renderObject.position.z) * 0.01 * delta; + + if (this.renderObject.position.distanceToSquared(this.position) < 0.001) { + this.renderObject.position.copy(this.position); + } + + this.blueMap.updateFrame = true; + + requestAnimationFrame(this.moveAnimation); + } else { + this.animationRunning = false; + this.lastFrame = -1; + } + }; + + onClick = () => { + + } + +} \ No newline at end of file diff --git a/BlueMapCore/src/main/webroot/js/libs/hud/PlayerMarkerSet.js b/BlueMapCore/src/main/webroot/js/libs/hud/PlayerMarkerSet.js new file mode 100644 index 00000000..9668af24 --- /dev/null +++ b/BlueMapCore/src/main/webroot/js/libs/hud/PlayerMarkerSet.js @@ -0,0 +1,85 @@ +import POIMarker from "./POIMarker"; +import ShapeMarker from "./ShapeMarker"; +import {cachePreventionNr} from "../utils"; +import PlayerMarker from "./PlayerMarker"; +import {Vector3} from "three"; + +export default class PlayerMarkerSet { + + constructor(blueMap) { + this.blueMap = blueMap; + this.id = "bluemap-live-players"; + this.label = "players"; + this.toggleable = true; + this.defaultHide = false; + this.marker = []; + this.markerMap = {}; + + this.visible = true; + } + + update() { + this.marker.forEach(marker => { + marker.setVisible(this.visible); + }); + } + + async updateLive(){ + await new Promise((resolve, reject) => { + this.blueMap.fileLoader.load(this.blueMap.liveApiRoot + 'players?' + cachePreventionNr(), + liveData => { + try { + liveData = JSON.parse(liveData); + resolve(liveData); + } catch (e){ + reject(e); + } + }, + xhr => {}, + error => { + reject(error); + } + ); + }).then((liveData) => { + this.updateWith(liveData) + }).catch((e) => { + console.error("Failed to update player-markers!", e); + }); + } + + updateWith(liveData){ + this.marker.forEach(marker => { + marker.nowOnline = false; + }); + + for(let i = 0; i < liveData.players.length; i++){ + let player = liveData.players[i]; + let marker = this.markerMap[player.uuid]; + + if (!marker){ + marker = new PlayerMarker(this.blueMap, this, { + type: "playermarker", + map: null, + position: player.position, + label: player.name, + link: null, + newTab: false + }, player.uuid, player.world); + + this.markerMap[player.uuid] = marker; + this.marker.push(marker); + } + + marker.nowOnline = true; + marker.position = new Vector3(player.position.x, player.position.y + 1.5, player.position.z); + marker.updatePosition(); + } + + this.marker.forEach(marker => { + if (marker.nowOnline !== marker.online){ + marker.online = marker.nowOnline; + marker.setVisible(this.visible); + } + }); + } +} \ No newline at end of file diff --git a/BlueMapCore/src/main/webroot/style/modules/hudInfo.scss b/BlueMapCore/src/main/webroot/style/modules/hudInfo.scss index 997cd8ca..cfdf07e7 100644 --- a/BlueMapCore/src/main/webroot/style/modules/hudInfo.scss +++ b/BlueMapCore/src/main/webroot/style/modules/hudInfo.scss @@ -76,7 +76,7 @@ } } -.bluemap-container .marker-poi { +.bluemap-container .marker-poi, .bluemap-container .marker-player { pointer-events: none; > * { @@ -86,4 +86,39 @@ > img { filter: drop-shadow(1px 1px 3px #0008); } +} + +.bluemap-container .marker-player { + img { + width: 32px; + height: 32px; + + image-rendering: pixelated; + image-rendering: crisp-edges; + + transition: all 0.3s; + } + + .nameplate { + position: absolute; + left: 50%; + top: 0; + transform: translate(-50%, -110%); + background: rgba(50, 50, 50, 0.75); + padding: 0.2em 0.3em; + color: white; + + transition: all 0.3s; + } + + &.distant { + img { + width: 16px; + height: 16px; + } + + .nameplate { + opacity: 0; + } + } } \ No newline at end of file diff --git a/BlueMapFabric/src/main/java/de/bluecolored/bluemap/fabric/FabricEventForwarder.java b/BlueMapFabric/src/main/java/de/bluecolored/bluemap/fabric/FabricEventForwarder.java index cc3f74c0..c96846e6 100644 --- a/BlueMapFabric/src/main/java/de/bluecolored/bluemap/fabric/FabricEventForwarder.java +++ b/BlueMapFabric/src/main/java/de/bluecolored/bluemap/fabric/FabricEventForwarder.java @@ -35,10 +35,14 @@ import de.bluecolored.bluemap.common.plugin.serverinterface.ServerEventListener; import de.bluecolored.bluemap.core.logger.Logger; import de.bluecolored.bluemap.fabric.events.ChunkFinalizeCallback; +import de.bluecolored.bluemap.fabric.events.PlayerJoinCallback; +import de.bluecolored.bluemap.fabric.events.PlayerLeaveCallback; import de.bluecolored.bluemap.fabric.events.WorldSaveCallback; import net.fabricmc.fabric.api.event.player.AttackBlockCallback; import net.fabricmc.fabric.api.event.player.UseBlockCallback; import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.network.ServerPlayerEntity; import net.minecraft.server.world.ServerWorld; import net.minecraft.util.ActionResult; import net.minecraft.util.Hand; @@ -60,13 +64,16 @@ public FabricEventForwarder(FabricMod mod) { ChunkFinalizeCallback.EVENT.register(this::onChunkFinalize); AttackBlockCallback.EVENT.register(this::onBlockAttack); UseBlockCallback.EVENT.register(this::onBlockUse); + + PlayerJoinCallback.EVENT.register(this::onPlayerJoin); + PlayerLeaveCallback.EVENT.register(this::onPlayerLeave); } - public void addEventListener(ServerEventListener listener) { + public synchronized void addEventListener(ServerEventListener listener) { this.eventListeners.add(listener); } - public void removeAllListeners() { + public synchronized void removeAllListeners() { this.eventListeners.clear(); } @@ -87,33 +94,47 @@ public ActionResult onBlockAttack(PlayerEntity player, World world, Hand hand, B return ActionResult.PASS; } - public void onBlockChange(ServerWorld world, BlockPos blockPos) { + public synchronized void onBlockChange(ServerWorld world, BlockPos blockPos) { Vector3i position = new Vector3i(blockPos.getX(), blockPos.getY(), blockPos.getZ()); try { UUID uuid = mod.getUUIDForWorld(world); - eventListeners.forEach(e -> e.onBlockChange(uuid, position)); + for (ServerEventListener listener : eventListeners) listener.onBlockChange(uuid, position); } catch (IOException e) { - Logger.global.logError("Failed to get UUID for world: " + world, e); + Logger.global.noFloodError("Failed to get the UUID for a world!", e); } } - public void onWorldSave(ServerWorld world) { + public synchronized void onWorldSave(ServerWorld world) { try { UUID uuid = mod.getUUIDForWorld(world); - eventListeners.forEach(e -> e.onWorldSaveToDisk(uuid)); + for (ServerEventListener listener : eventListeners) listener.onWorldSaveToDisk(uuid); } catch (IOException e) { - Logger.global.logError("Failed to get UUID for world: " + world, e); + Logger.global.noFloodError("Failed to get the UUID for a world!", e); } } - public void onChunkFinalize(ServerWorld world, Vector2i chunkPos) { + public synchronized void onChunkFinalize(ServerWorld world, Vector2i chunkPos) { try { UUID uuid = mod.getUUIDForWorld(world); - eventListeners.forEach(e -> e.onChunkFinishedGeneration(uuid, chunkPos)); + for (ServerEventListener listener : eventListeners) listener.onChunkFinishedGeneration(uuid, chunkPos); } catch (IOException e) { - Logger.global.logError("Failed to get UUID for world: " + world, e); + Logger.global.noFloodError("Failed to get the UUID for a world!", e); } } + public synchronized void onPlayerJoin(MinecraftServer server, ServerPlayerEntity player) { + if (this.mod.getServer() != server) return; + + UUID uuid = player.getUuid(); + for (ServerEventListener listener : eventListeners) listener.onPlayerJoin(uuid); + } + + public synchronized void onPlayerLeave(MinecraftServer server, ServerPlayerEntity player) { + if (this.mod.getServer() != server) return; + + UUID uuid = player.getUuid(); + for (ServerEventListener listener : eventListeners) listener.onPlayerLeave(uuid); + } + } diff --git a/BlueMapFabric/src/main/java/de/bluecolored/bluemap/fabric/FabricMod.java b/BlueMapFabric/src/main/java/de/bluecolored/bluemap/fabric/FabricMod.java index 3cac786e..5f24fef4 100644 --- a/BlueMapFabric/src/main/java/de/bluecolored/bluemap/fabric/FabricMod.java +++ b/BlueMapFabric/src/main/java/de/bluecolored/bluemap/fabric/FabricMod.java @@ -26,7 +26,12 @@ import java.io.File; import java.io.IOException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutionException; @@ -39,32 +44,45 @@ import de.bluecolored.bluemap.common.plugin.Plugin; import de.bluecolored.bluemap.common.plugin.commands.Commands; +import de.bluecolored.bluemap.common.plugin.serverinterface.Player; import de.bluecolored.bluemap.common.plugin.serverinterface.ServerEventListener; import de.bluecolored.bluemap.common.plugin.serverinterface.ServerInterface; import de.bluecolored.bluemap.core.logger.Logger; import de.bluecolored.bluemap.core.resourcepack.ParseResourceException; +import de.bluecolored.bluemap.fabric.events.PlayerJoinCallback; +import de.bluecolored.bluemap.fabric.events.PlayerLeaveCallback; import net.fabricmc.api.ModInitializer; import net.fabricmc.fabric.api.command.v1.CommandRegistrationCallback; import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents; +import net.fabricmc.fabric.api.event.lifecycle.v1.ServerTickEvents; import net.minecraft.server.MinecraftServer; +import net.minecraft.server.network.ServerPlayerEntity; import net.minecraft.server.world.ServerWorld; import net.minecraft.world.dimension.DimensionType; public class FabricMod implements ModInitializer, ServerInterface { private Plugin pluginInstance = null; + private MinecraftServer serverInstance = null; - private Map worldUuids; + private Map worldUUIDs; private FabricEventForwarder eventForwarder; private LoadingCache worldUuidCache; + + private int playerUpdateIndex = 0; + private Map onlinePlayerMap; + private List onlinePlayerList; public FabricMod() { Logger.global = new Log4jLogger(LogManager.getLogger(Plugin.PLUGIN_NAME)); + this.onlinePlayerMap = new ConcurrentHashMap<>(); + this.onlinePlayerList = Collections.synchronizedList(new ArrayList<>()); + pluginInstance = new Plugin("fabric", this); - this.worldUuids = new ConcurrentHashMap<>(); + this.worldUUIDs = new ConcurrentHashMap<>(); this.eventForwarder = new FabricEventForwarder(this); this.worldUuidCache = CacheBuilder.newBuilder() .weakKeys() @@ -86,12 +104,14 @@ public void onInitialize() { }); ServerLifecycleEvents.SERVER_STARTED.register((MinecraftServer server) -> { + this.serverInstance = server; + new Thread(()->{ - Logger.global.logInfo("Loading BlueMap..."); + Logger.global.logInfo("Loading BlueMap..."); try { pluginInstance.load(); - Logger.global.logInfo("BlueMap loaded!"); + if (pluginInstance.isLoaded()) Logger.global.logInfo("BlueMap loaded!"); } catch (IOException | ParseResourceException e) { Logger.global.logError("Failed to load bluemap!", e); } @@ -102,6 +122,13 @@ public void onInitialize() { pluginInstance.unload(); Logger.global.logInfo("BlueMap unloaded!"); }); + + PlayerJoinCallback.EVENT.register(this::onPlayerJoin); + PlayerLeaveCallback.EVENT.register(this::onPlayerLeave); + + ServerTickEvents.END_SERVER_TICK.register((MinecraftServer server) -> { + if (server == this.serverInstance) this.updateSomePlayers(); + }); } @Override @@ -118,10 +145,10 @@ public void unregisterAllListeners() { public UUID getUUIDForWorld(File worldFolder) throws IOException { worldFolder = worldFolder.getCanonicalFile(); - UUID uuid = worldUuids.get(worldFolder); + UUID uuid = worldUUIDs.get(worldFolder); if (uuid == null) { uuid = UUID.randomUUID(); - worldUuids.put(worldFolder, uuid); + worldUUIDs.put(worldFolder, uuid); } return uuid; @@ -150,5 +177,58 @@ private UUID loadUUIDForWorld(ServerWorld world) throws IOException { public File getConfigFolder() { return new File("config/bluemap"); } + + public void onPlayerJoin(MinecraftServer server, ServerPlayerEntity playerInstance) { + if (this.serverInstance != server) return; + + FabricPlayer player = new FabricPlayer(this, playerInstance); + onlinePlayerMap.put(player.getUuid(), player); + onlinePlayerList.add(player); + } + + public void onPlayerLeave(MinecraftServer server, ServerPlayerEntity player) { + if (this.serverInstance != server) return; + + UUID playerUUID = player.getUuid(); + onlinePlayerMap.remove(playerUUID); + synchronized (onlinePlayerList) { + onlinePlayerList.removeIf(p -> p.getUuid().equals(playerUUID)); + } + } + + public MinecraftServer getServer() { + return this.serverInstance; + } + + @Override + public Collection getOnlinePlayers() { + return onlinePlayerMap.values(); + } + + @Override + public Optional getPlayer(UUID uuid) { + return Optional.ofNullable(onlinePlayerMap.get(uuid)); + } + + /** + * Only update some of the online players each tick to minimize performance impact on the server-thread. + * Only call this method on the server-thread. + */ + private void updateSomePlayers() { + int onlinePlayerCount = onlinePlayerList.size(); + if (onlinePlayerCount == 0) return; + + int playersToBeUpdated = onlinePlayerCount / 20; //with 20 tps, each player is updated once a second + if (playersToBeUpdated == 0) playersToBeUpdated = 1; + + for (int i = 0; i < playersToBeUpdated; i++) { + playerUpdateIndex++; + if (playerUpdateIndex >= 20 && playerUpdateIndex >= onlinePlayerCount) playerUpdateIndex = 0; + + if (playerUpdateIndex < onlinePlayerCount) { + onlinePlayerList.get(i).update(); + } + } + } } diff --git a/BlueMapFabric/src/main/java/de/bluecolored/bluemap/fabric/FabricPlayer.java b/BlueMapFabric/src/main/java/de/bluecolored/bluemap/fabric/FabricPlayer.java new file mode 100644 index 00000000..7c8ce5b6 --- /dev/null +++ b/BlueMapFabric/src/main/java/de/bluecolored/bluemap/fabric/FabricPlayer.java @@ -0,0 +1,156 @@ +/* + * This file is part of BlueMap, licensed under the MIT License (MIT). + * + * Copyright (c) Blue (Lukas Rieger) + * Copyright (c) contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package de.bluecolored.bluemap.fabric; + +import java.io.IOException; +import java.lang.ref.WeakReference; +import java.util.EnumMap; +import java.util.Map; +import java.util.UUID; + +import com.flowpowered.math.vector.Vector3d; + +import de.bluecolored.bluemap.common.plugin.serverinterface.Gamemode; +import de.bluecolored.bluemap.common.plugin.serverinterface.Player; +import de.bluecolored.bluemap.common.plugin.text.Text; +import net.minecraft.entity.effect.StatusEffectInstance; +import net.minecraft.entity.effect.StatusEffects; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.network.ServerPlayerEntity; +import net.minecraft.util.math.Vec3d; +import net.minecraft.world.GameMode; + +public class FabricPlayer implements Player { + + private static final UUID UNKNOWN_WORLD_UUID = UUID.randomUUID(); + + private static final Map GAMEMODE_MAP = new EnumMap<>(GameMode.class); + static { + GAMEMODE_MAP.put(GameMode.ADVENTURE, Gamemode.ADVENTURE); + GAMEMODE_MAP.put(GameMode.SURVIVAL, Gamemode.SURVIVAL); + GAMEMODE_MAP.put(GameMode.CREATIVE, Gamemode.CREATIVE); + GAMEMODE_MAP.put(GameMode.SPECTATOR, Gamemode.SPECTATOR); + GAMEMODE_MAP.put(GameMode.NOT_SET, Gamemode.SURVIVAL); + } + + private UUID uuid; + private Text name; + private UUID world; + private Vector3d position; + private boolean online; + private boolean sneaking; + private boolean invisible; + private Gamemode gamemode; + + private FabricMod mod; + private WeakReference delegate; + + public FabricPlayer(FabricMod mod, ServerPlayerEntity delegate) { + this.uuid = delegate.getUuid(); + this.mod = mod; + this.delegate = new WeakReference<>(delegate); + + update(); + } + + @Override + public UUID getUuid() { + return this.uuid; + } + + @Override + public Text getName() { + return this.name; + } + + @Override + public UUID getWorld() { + return this.world; + } + + @Override + public Vector3d getPosition() { + return this.position; + } + + @Override + public boolean isOnline() { + return this.online; + } + + @Override + public boolean isSneaking() { + return this.sneaking; + } + + @Override + public boolean isInvisible() { + return this.invisible; + } + + @Override + public Gamemode getGamemode() { + return this.gamemode; + } + + /** + * Only call on server thread! + */ + public void update() { + ServerPlayerEntity player = delegate.get(); + if (player == null) { + MinecraftServer server = mod.getServer(); + if (server != null) { + player = server.getPlayerManager().getPlayer(uuid); + } + + if (player == null) { + this.online = false; + return; + } + + delegate = new WeakReference<>(player); + } + + this.gamemode = GAMEMODE_MAP.get(player.interactionManager.getGameMode()); + + StatusEffectInstance invis = player.getStatusEffect(StatusEffects.INVISIBILITY); + this.invisible = invis != null && invis.getDuration() > 0; + + this.name = Text.of(player.getName().getString()); + this.online = true; + + Vec3d pos = player.getPos(); + this.position = new Vector3d(pos.getX(), pos.getY(), pos.getZ()); + this.sneaking = player.isSneaking(); + + try { + this.world = mod.getUUIDForWorld(player.getServerWorld()); + } catch (IOException e) { + this.world = UNKNOWN_WORLD_UUID; + } + } + +} diff --git a/BlueMapFabric/src/main/java/de/bluecolored/bluemap/fabric/events/PlayerJoinCallback.java b/BlueMapFabric/src/main/java/de/bluecolored/bluemap/fabric/events/PlayerJoinCallback.java new file mode 100644 index 00000000..d564012c --- /dev/null +++ b/BlueMapFabric/src/main/java/de/bluecolored/bluemap/fabric/events/PlayerJoinCallback.java @@ -0,0 +1,42 @@ +/* + * This file is part of BlueMap, licensed under the MIT License (MIT). + * + * Copyright (c) Blue (Lukas Rieger) + * Copyright (c) contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package de.bluecolored.bluemap.fabric.events; + +import net.fabricmc.fabric.api.event.Event; +import net.fabricmc.fabric.api.event.EventFactory; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.network.ServerPlayerEntity; + +public interface PlayerJoinCallback { + Event EVENT = EventFactory.createArrayBacked(PlayerJoinCallback.class, + (listeners) -> (server, player) -> { + for (PlayerJoinCallback event : listeners) { + event.onPlayerJoin(server, player); + } + } + ); + + void onPlayerJoin(MinecraftServer server, ServerPlayerEntity player); +} diff --git a/BlueMapFabric/src/main/java/de/bluecolored/bluemap/fabric/events/PlayerLeaveCallback.java b/BlueMapFabric/src/main/java/de/bluecolored/bluemap/fabric/events/PlayerLeaveCallback.java new file mode 100644 index 00000000..f1ce2922 --- /dev/null +++ b/BlueMapFabric/src/main/java/de/bluecolored/bluemap/fabric/events/PlayerLeaveCallback.java @@ -0,0 +1,42 @@ +/* + * This file is part of BlueMap, licensed under the MIT License (MIT). + * + * Copyright (c) Blue (Lukas Rieger) + * Copyright (c) contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package de.bluecolored.bluemap.fabric.events; + +import net.fabricmc.fabric.api.event.Event; +import net.fabricmc.fabric.api.event.EventFactory; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.network.ServerPlayerEntity; + +public interface PlayerLeaveCallback { + Event EVENT = EventFactory.createArrayBacked(PlayerLeaveCallback.class, + (listeners) -> (server, player) -> { + for (PlayerLeaveCallback event : listeners) { + event.onPlayerLeave(server, player); + } + } + ); + + void onPlayerLeave(MinecraftServer server, ServerPlayerEntity player); +} diff --git a/BlueMapFabric/src/main/java/de/bluecolored/bluemap/fabric/mixin/MixinPlayerManager.java b/BlueMapFabric/src/main/java/de/bluecolored/bluemap/fabric/mixin/MixinPlayerManager.java new file mode 100644 index 00000000..b7109357 --- /dev/null +++ b/BlueMapFabric/src/main/java/de/bluecolored/bluemap/fabric/mixin/MixinPlayerManager.java @@ -0,0 +1,56 @@ +/* + * This file is part of BlueMap, licensed under the MIT License (MIT). + * + * Copyright (c) Blue (Lukas Rieger) + * Copyright (c) contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package de.bluecolored.bluemap.fabric.mixin; + +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +import de.bluecolored.bluemap.fabric.events.PlayerJoinCallback; +import de.bluecolored.bluemap.fabric.events.PlayerLeaveCallback; +import net.minecraft.network.ClientConnection; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.PlayerManager; +import net.minecraft.server.network.ServerPlayerEntity; + +@Mixin(PlayerManager.class) +public abstract class MixinPlayerManager { + + @Shadow + public abstract MinecraftServer getServer(); + + @Inject(at = @At("RETURN"), method = "onPlayerConnect") + public void onPlayerConnect(ClientConnection connection, ServerPlayerEntity player, CallbackInfo ci) { + PlayerJoinCallback.EVENT.invoker().onPlayerJoin(this.getServer(), player); + } + + @Inject(at = @At("HEAD"), method = "remove") + public void remove(ServerPlayerEntity player, CallbackInfo ci) { + PlayerLeaveCallback.EVENT.invoker().onPlayerLeave(this.getServer(), player); + } + +} diff --git a/BlueMapFabric/src/main/resources/bluemap-fabric-defaults.conf b/BlueMapFabric/src/main/resources/bluemap-fabric-defaults.conf index c6f20621..98e58691 100644 --- a/BlueMapFabric/src/main/resources/bluemap-fabric-defaults.conf +++ b/BlueMapFabric/src/main/resources/bluemap-fabric-defaults.conf @@ -9,3 +9,9 @@ webserver { port: 8100 maxConnectionCount: 100 } +liveUpdates { + enabled: true + hiddenGameModes: [] + hideInvisible: true + hideSneaking: false +} diff --git a/BlueMapFabric/src/main/resources/bluemap-fabric.conf b/BlueMapFabric/src/main/resources/bluemap-fabric.conf index af9edf92..c56e51a9 100644 --- a/BlueMapFabric/src/main/resources/bluemap-fabric.conf +++ b/BlueMapFabric/src/main/resources/bluemap-fabric.conf @@ -38,6 +38,7 @@ webroot: "bluemap/web" #webdata: "path/to/data/folder" # If the web-application should use cookies to save the configurations of a user. +# Default is true useCookies: true webserver { @@ -165,3 +166,23 @@ maps: [ } ] + +liveUpdates { + # If the server should send live-updates and player-positions. + # Default is true + enabled: true + + # A list of gamemodes that will prevent a player from appearing on the map. + # Possible values are: survival, creative, spectator, adventure + hiddenGameModes: [ + "spectator" + ] + + # If this is true, players that have an invisibility (potion-)effect will be hidden on the map. + # Default is true + hideInvisible: true + + # If this is true, players that are sneaking will be hidden on the map. + # Default is false + hideSneaking: false +} diff --git a/BlueMapFabric/src/main/resources/bluemap.mixins.json b/BlueMapFabric/src/main/resources/bluemap.mixins.json index e8c98a84..84d08a94 100644 --- a/BlueMapFabric/src/main/resources/bluemap.mixins.json +++ b/BlueMapFabric/src/main/resources/bluemap.mixins.json @@ -6,8 +6,9 @@ "mixins": [], "client": [], "server": [ - "MixinServerWorld", - "MixinChunkGenerator" + "MixinChunkGenerator", + "MixinPlayerManager", + "MixinServerWorld" ], "injectors": { "defaultRequire": 1 diff --git a/BlueMapForge/src/main/java/de/bluecolored/bluemap/forge/ForgeEventForwarder.java b/BlueMapForge/src/main/java/de/bluecolored/bluemap/forge/ForgeEventForwarder.java new file mode 100644 index 00000000..4c0aa278 --- /dev/null +++ b/BlueMapForge/src/main/java/de/bluecolored/bluemap/forge/ForgeEventForwarder.java @@ -0,0 +1,116 @@ +/* + * This file is part of BlueMap, licensed under the MIT License (MIT). + * + * Copyright (c) Blue (Lukas Rieger) + * Copyright (c) contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package de.bluecolored.bluemap.forge; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.UUID; + +import com.flowpowered.math.vector.Vector3i; + +import de.bluecolored.bluemap.common.plugin.serverinterface.ServerEventListener; +import de.bluecolored.bluemap.core.logger.Logger; +import net.minecraft.world.server.ServerWorld; +import net.minecraftforge.common.MinecraftForge; +import net.minecraftforge.event.entity.player.PlayerEvent.PlayerLoggedInEvent; +import net.minecraftforge.event.entity.player.PlayerEvent.PlayerLoggedOutEvent; +import net.minecraftforge.event.world.BlockEvent; +import net.minecraftforge.event.world.WorldEvent; +import net.minecraftforge.eventbus.api.SubscribeEvent; + +public class ForgeEventForwarder { + + private ForgeMod mod; + private Collection eventListeners; + + public ForgeEventForwarder(ForgeMod mod) { + this.mod = mod; + this.eventListeners = new ArrayList<>(1); + + MinecraftForge.EVENT_BUS.register(this); + } + + public synchronized void addEventListener(ServerEventListener listener) { + this.eventListeners.add(listener); + } + + public synchronized void removeAllListeners() { + this.eventListeners.clear(); + } + + @SubscribeEvent + public void onBlockBreak(BlockEvent.BreakEvent evt) { + onBlockChange(evt); + } + + @SubscribeEvent + public void onBlockPlace(BlockEvent.EntityPlaceEvent evt) { + onBlockChange(evt); + } + + private synchronized void onBlockChange(BlockEvent evt) { + if (!(evt.getWorld() instanceof ServerWorld)) return; + + try { + UUID world = mod.getUUIDForWorld((ServerWorld) evt.getWorld()); + Vector3i position = new Vector3i( + evt.getPos().getX(), + evt.getPos().getY(), + evt.getPos().getZ() + ); + + for (ServerEventListener listener : eventListeners) listener.onBlockChange(world, position); + + } catch (IOException e) { + Logger.global.noFloodError("Failed to get the UUID for a world!", e); + } + } + + @SubscribeEvent + public synchronized void onWorldSave(WorldEvent.Save evt) { + if (!(evt.getWorld() instanceof ServerWorld)) return; + + try { + UUID world = mod.getUUIDForWorld((ServerWorld) evt.getWorld()); + for (ServerEventListener listener : eventListeners) listener.onWorldSaveToDisk(world); + } catch (IOException e) { + Logger.global.noFloodError("Failed to get the UUID for a world!", e); + } + } + + @SubscribeEvent + public synchronized void onPlayerJoin(PlayerLoggedInEvent evt) { + UUID uuid = evt.getPlayer().getUniqueID(); + for (ServerEventListener listener : eventListeners) listener.onPlayerJoin(uuid); + } + + @SubscribeEvent + public synchronized void onPlayerLeave(PlayerLoggedOutEvent evt) { + UUID uuid = evt.getPlayer().getUniqueID(); + for (ServerEventListener listener : eventListeners) listener.onPlayerLeave(uuid); + } + +} diff --git a/BlueMapForge/src/main/java/de/bluecolored/bluemap/forge/ForgeMod.java b/BlueMapForge/src/main/java/de/bluecolored/bluemap/forge/ForgeMod.java index 43b3c029..c025bcac 100644 --- a/BlueMapForge/src/main/java/de/bluecolored/bluemap/forge/ForgeMod.java +++ b/BlueMapForge/src/main/java/de/bluecolored/bluemap/forge/ForgeMod.java @@ -28,30 +28,36 @@ import java.io.IOException; import java.util.ArrayList; import java.util.Collection; -import java.util.HashMap; +import java.util.Collections; +import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutionException; import org.apache.logging.log4j.LogManager; -import com.flowpowered.math.vector.Vector3i; import com.google.common.cache.CacheBuilder; import com.google.common.cache.CacheLoader; import com.google.common.cache.LoadingCache; import de.bluecolored.bluemap.common.plugin.Plugin; import de.bluecolored.bluemap.common.plugin.commands.Commands; +import de.bluecolored.bluemap.common.plugin.serverinterface.Player; import de.bluecolored.bluemap.common.plugin.serverinterface.ServerEventListener; import de.bluecolored.bluemap.common.plugin.serverinterface.ServerInterface; import de.bluecolored.bluemap.core.logger.Logger; -import net.minecraft.command.CommandSource; +import de.bluecolored.bluemap.core.resourcepack.ParseResourceException; +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.entity.player.ServerPlayerEntity; import net.minecraft.server.MinecraftServer; import net.minecraft.world.DimensionType; import net.minecraft.world.server.ServerWorld; import net.minecraftforge.common.MinecraftForge; -import net.minecraftforge.event.world.BlockEvent; -import net.minecraftforge.event.world.WorldEvent; +import net.minecraftforge.event.TickEvent.ServerTickEvent; +import net.minecraftforge.event.entity.player.PlayerEvent.PlayerLoggedInEvent; +import net.minecraftforge.event.entity.player.PlayerEvent.PlayerLoggedOutEvent; import net.minecraftforge.eventbus.api.SubscribeEvent; import net.minecraftforge.fml.common.Mod; import net.minecraftforge.fml.event.server.FMLServerStartingEvent; @@ -59,20 +65,29 @@ @Mod(Plugin.PLUGIN_ID) public class ForgeMod implements ServerInterface { + + private Plugin pluginInstance = null; + private MinecraftServer serverInstance = null; - private Plugin bluemap; - private Commands commands; - private Map worldUUIDs; - private Collection eventListeners; + private Map worldUUIDs; + private ForgeEventForwarder eventForwarder; private LoadingCache worldUuidCache; + private int playerUpdateIndex = 0; + private Map onlinePlayerMap; + private List onlinePlayerList; + public ForgeMod() { Logger.global = new Log4jLogger(LogManager.getLogger(Plugin.PLUGIN_NAME)); - this.bluemap = new Plugin("forge", this); - this.worldUUIDs = new HashMap<>(); - this.eventListeners = new ArrayList<>(1); + this.onlinePlayerMap = new ConcurrentHashMap<>(); + this.onlinePlayerList = Collections.synchronizedList(new ArrayList<>()); + + this.pluginInstance = new Plugin("forge", this); + + this.worldUUIDs = new ConcurrentHashMap<>(); + this.eventForwarder = new ForgeEventForwarder(this); this.worldUuidCache = CacheBuilder.newBuilder() .weakKeys() .maximumSize(1000) @@ -82,113 +97,61 @@ public UUID load(ServerWorld key) throws Exception { return loadUUIDForWorld(key); } }); - + MinecraftForge.EVENT_BUS.register(this); } @SubscribeEvent public void onServerStarting(FMLServerStartingEvent event) { this.worldUUIDs.clear(); - - for (ServerWorld world : event.getServer().getWorlds()) { - try { - registerWorld(world); - } catch (IOException e) { - Logger.global.logError("Failed to register world: " + world.getProviderName(), e); - } - - try { - world.save(null, false, false); - } catch (Throwable t) { - Logger.global.logError("Failed to save world: " + world.getProviderName(), t); - } - } //register commands - this.commands = new Commands<>(bluemap, event.getServer().getCommandManager().getDispatcher(), forgeSource -> new ForgeCommandSource(this, bluemap, forgeSource)); + new Commands<>(pluginInstance, event.getServer().getCommandManager().getDispatcher(), forgeSource -> new ForgeCommandSource(this, pluginInstance, forgeSource)); new Thread(() -> { + Logger.global.logInfo("Loading..."); + try { - Logger.global.logInfo("Loading..."); - bluemap.load(); - if (bluemap.isLoaded()) Logger.global.logInfo("Loaded!"); - } catch (Throwable t) { - Logger.global.logError("Failed to load!", t); + pluginInstance.load(); + if (pluginInstance.isLoaded()) Logger.global.logInfo("Loaded!"); + } catch (IOException | ParseResourceException e) { + Logger.global.logError("Failed to load bluemap!", e); } }).start(); } - private void registerWorld(ServerWorld world) throws IOException { - getUUIDForWorld(world); - } - @SubscribeEvent public void onServerStopping(FMLServerStoppingEvent event) { - Logger.global.logInfo("Stopping..."); - bluemap.unload(); - Logger.global.logInfo("Saved and stopped!"); + pluginInstance.unload(); + Logger.global.logInfo("BlueMap unloaded!"); } + + @SubscribeEvent + public void onTick(ServerTickEvent evt) { + updateSomePlayers(); + } @Override public void registerListener(ServerEventListener listener) { - eventListeners.add(listener); + eventForwarder.addEventListener(listener); } @Override public void unregisterAllListeners() { - eventListeners.clear(); - } - - @SubscribeEvent - public void onBlockBreak(BlockEvent.BreakEvent evt) { - onBlockChange(evt); - } - - @SubscribeEvent - public void onBlockPlace(BlockEvent.EntityPlaceEvent evt) { - onBlockChange(evt); - } - - private void onBlockChange(BlockEvent evt) { - if (!(evt.getWorld() instanceof ServerWorld)) return; - - try { - UUID world = getUUIDForWorld((ServerWorld) evt.getWorld()); - Vector3i position = new Vector3i( - evt.getPos().getX(), - evt.getPos().getY(), - evt.getPos().getZ() - ); - - for (ServerEventListener listener : eventListeners) listener.onBlockChange(world, position); - - } catch (IOException ignore) {} - } - - @SubscribeEvent - public void onWorldSave(WorldEvent.Save evt) { - if (!(evt.getWorld() instanceof ServerWorld)) return; - - try { - UUID world = getUUIDForWorld((ServerWorld) evt.getWorld()); - - for (ServerEventListener listener : eventListeners) listener.onWorldSaveToDisk(world); - - } catch (IOException ignore) {} + eventForwarder.removeAllListeners(); } @Override public UUID getUUIDForWorld(File worldFolder) throws IOException { - synchronized (worldUUIDs) { - String key = worldFolder.getCanonicalPath(); - - UUID uuid = worldUUIDs.get(key); - if (uuid == null) { - throw new IOException("There is no world with this folder loaded: " + worldFolder.getPath()); - } - - return uuid; + worldFolder = worldFolder.getCanonicalFile(); + + UUID uuid = worldUUIDs.get(worldFolder); + if (uuid == null) { + uuid = UUID.randomUUID(); + worldUUIDs.put(worldFolder, uuid); } + + return uuid; } public UUID getUUIDForWorld(ServerWorld world) throws IOException { @@ -202,17 +165,15 @@ public UUID getUUIDForWorld(ServerWorld world) throws IOException { } private UUID loadUUIDForWorld(ServerWorld world) throws IOException { - synchronized (worldUUIDs) { - String key = getFolderForWorld(world).getPath(); - - UUID uuid = worldUUIDs.get(key); - if (uuid == null) { - uuid = UUID.randomUUID(); - worldUUIDs.put(key, uuid); - } - - return uuid; + File key = getFolderForWorld(world); + + UUID uuid = worldUUIDs.get(key); + if (uuid == null) { + uuid = UUID.randomUUID(); + worldUUIDs.put(key, uuid); } + + return uuid; } private File getFolderForWorld(ServerWorld world) throws IOException { @@ -227,9 +188,60 @@ private File getFolderForWorld(ServerWorld world) throws IOException { public File getConfigFolder() { return new File("config/bluemap"); } + + public void onPlayerJoin(PlayerLoggedInEvent evt) { + PlayerEntity playerInstance = evt.getPlayer(); + if (!(playerInstance instanceof ServerPlayerEntity)) return; + + ForgePlayer player = new ForgePlayer(this, (ServerPlayerEntity) playerInstance); + onlinePlayerMap.put(player.getUuid(), player); + onlinePlayerList.add(player); + } + + public void onPlayerLeave(PlayerLoggedOutEvent evt) { + PlayerEntity player = evt.getPlayer(); + if (!(player instanceof ServerPlayerEntity)) return; + + UUID playerUUID = player.getUniqueID(); + onlinePlayerMap.remove(playerUUID); + synchronized (onlinePlayerList) { + onlinePlayerList.removeIf(p -> p.getUuid().equals(playerUUID)); + } + } - public Commands getCommands() { - return commands; + public MinecraftServer getServer() { + return this.serverInstance; + } + + @Override + public Collection getOnlinePlayers() { + return onlinePlayerMap.values(); + } + + @Override + public Optional getPlayer(UUID uuid) { + return Optional.ofNullable(onlinePlayerMap.get(uuid)); + } + + /** + * Only update some of the online players each tick to minimize performance impact on the server-thread. + * Only call this method on the server-thread. + */ + private void updateSomePlayers() { + int onlinePlayerCount = onlinePlayerList.size(); + if (onlinePlayerCount == 0) return; + + int playersToBeUpdated = onlinePlayerCount / 20; //with 20 tps, each player is updated once a second + if (playersToBeUpdated == 0) playersToBeUpdated = 1; + + for (int i = 0; i < playersToBeUpdated; i++) { + playerUpdateIndex++; + if (playerUpdateIndex >= 20 && playerUpdateIndex >= onlinePlayerCount) playerUpdateIndex = 0; + + if (playerUpdateIndex < onlinePlayerCount) { + onlinePlayerList.get(i).update(); + } + } } } diff --git a/BlueMapForge/src/main/java/de/bluecolored/bluemap/forge/ForgePlayer.java b/BlueMapForge/src/main/java/de/bluecolored/bluemap/forge/ForgePlayer.java new file mode 100644 index 00000000..a5c5f4fa --- /dev/null +++ b/BlueMapForge/src/main/java/de/bluecolored/bluemap/forge/ForgePlayer.java @@ -0,0 +1,155 @@ +/* + * This file is part of BlueMap, licensed under the MIT License (MIT). + * + * Copyright (c) Blue (Lukas Rieger) + * Copyright (c) contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package de.bluecolored.bluemap.forge; + +import java.io.IOException; +import java.lang.ref.WeakReference; +import java.util.EnumMap; +import java.util.Map; +import java.util.UUID; + +import com.flowpowered.math.vector.Vector3d; + +import de.bluecolored.bluemap.common.plugin.serverinterface.Gamemode; +import de.bluecolored.bluemap.common.plugin.serverinterface.Player; +import de.bluecolored.bluemap.common.plugin.text.Text; +import net.minecraft.entity.player.ServerPlayerEntity; +import net.minecraft.potion.EffectInstance; +import net.minecraft.potion.Effects; +import net.minecraft.server.MinecraftServer; +import net.minecraft.world.GameType; + +public class ForgePlayer implements Player { + + private static final UUID UNKNOWN_WORLD_UUID = UUID.randomUUID(); + + private static final Map GAMEMODE_MAP = new EnumMap<>(GameType.class); + static { + GAMEMODE_MAP.put(GameType.ADVENTURE, Gamemode.ADVENTURE); + GAMEMODE_MAP.put(GameType.SURVIVAL, Gamemode.SURVIVAL); + GAMEMODE_MAP.put(GameType.CREATIVE, Gamemode.CREATIVE); + GAMEMODE_MAP.put(GameType.SPECTATOR, Gamemode.SPECTATOR); + GAMEMODE_MAP.put(GameType.NOT_SET, Gamemode.SURVIVAL); + } + + private UUID uuid; + private Text name; + private UUID world; + private Vector3d position; + private boolean online; + private boolean sneaking; + private boolean invisible; + private Gamemode gamemode; + + private ForgeMod mod; + private WeakReference delegate; + + public ForgePlayer(ForgeMod mod, ServerPlayerEntity delegate) { + this.uuid = delegate.getUniqueID(); + this.mod = mod; + this.delegate = new WeakReference<>(delegate); + + update(); + } + + @Override + public UUID getUuid() { + return this.uuid; + } + + @Override + public Text getName() { + return this.name; + } + + @Override + public UUID getWorld() { + return this.world; + } + + @Override + public Vector3d getPosition() { + return this.position; + } + + @Override + public boolean isOnline() { + return this.online; + } + + @Override + public boolean isSneaking() { + return this.sneaking; + } + + @Override + public boolean isInvisible() { + return this.invisible; + } + + @Override + public Gamemode getGamemode() { + return this.gamemode; + } + + /** + * Only call on server thread! + */ + public void update() { + ServerPlayerEntity player = delegate.get(); + if (player == null) { + MinecraftServer server = mod.getServer(); + if (server != null) { + player = server.getPlayerList().getPlayerByUUID(uuid); + } + + if (player == null) { + this.online = false; + return; + } + + delegate = new WeakReference<>(player); + } + + this.gamemode = GAMEMODE_MAP.get(player.interactionManager.getGameType()); + + EffectInstance invis = player.getActivePotionEffect(Effects.INVISIBILITY); + this.invisible = invis != null && invis.getDuration() > 0; + + this.name = Text.of(player.getName().getString()); + this.online = true; + + net.minecraft.util.math.vector.Vector3d pos = player.getPositionVec(); + this.position = new Vector3d(pos.getX(), pos.getY(), pos.getZ()); + this.sneaking = player.isSneaking(); + + try { + this.world = mod.getUUIDForWorld(player.getServerWorld()); + } catch (IOException e) { + this.world = UNKNOWN_WORLD_UUID; + } + } + +} diff --git a/BlueMapForge/src/main/resources/bluemap-forge-defaults.conf b/BlueMapForge/src/main/resources/bluemap-forge-defaults.conf index c6f20621..98e58691 100644 --- a/BlueMapForge/src/main/resources/bluemap-forge-defaults.conf +++ b/BlueMapForge/src/main/resources/bluemap-forge-defaults.conf @@ -9,3 +9,9 @@ webserver { port: 8100 maxConnectionCount: 100 } +liveUpdates { + enabled: true + hiddenGameModes: [] + hideInvisible: true + hideSneaking: false +} diff --git a/BlueMapForge/src/main/resources/bluemap-forge.conf b/BlueMapForge/src/main/resources/bluemap-forge.conf index af9edf92..c56e51a9 100644 --- a/BlueMapForge/src/main/resources/bluemap-forge.conf +++ b/BlueMapForge/src/main/resources/bluemap-forge.conf @@ -38,6 +38,7 @@ webroot: "bluemap/web" #webdata: "path/to/data/folder" # If the web-application should use cookies to save the configurations of a user. +# Default is true useCookies: true webserver { @@ -165,3 +166,23 @@ maps: [ } ] + +liveUpdates { + # If the server should send live-updates and player-positions. + # Default is true + enabled: true + + # A list of gamemodes that will prevent a player from appearing on the map. + # Possible values are: survival, creative, spectator, adventure + hiddenGameModes: [ + "spectator" + ] + + # If this is true, players that have an invisibility (potion-)effect will be hidden on the map. + # Default is true + hideInvisible: true + + # If this is true, players that are sneaking will be hidden on the map. + # Default is false + hideSneaking: false +} diff --git a/BlueMapSponge/src/main/java/de/bluecolored/bluemap/sponge/EventForwarder.java b/BlueMapSponge/src/main/java/de/bluecolored/bluemap/sponge/EventForwarder.java index 06bd3022..5b58ba2f 100644 --- a/BlueMapSponge/src/main/java/de/bluecolored/bluemap/sponge/EventForwarder.java +++ b/BlueMapSponge/src/main/java/de/bluecolored/bluemap/sponge/EventForwarder.java @@ -32,6 +32,8 @@ import org.spongepowered.api.event.Order; import org.spongepowered.api.event.block.ChangeBlockEvent; import org.spongepowered.api.event.filter.type.Exclude; +import org.spongepowered.api.event.message.MessageChannelEvent; +import org.spongepowered.api.event.network.ClientConnectionEvent; import org.spongepowered.api.event.world.SaveWorldEvent; import org.spongepowered.api.event.world.chunk.PopulateChunkEvent; import org.spongepowered.api.world.Location; @@ -40,6 +42,7 @@ import com.flowpowered.math.vector.Vector3i; import de.bluecolored.bluemap.common.plugin.serverinterface.ServerEventListener; +import de.bluecolored.bluemap.common.plugin.text.Text; public class EventForwarder { @@ -73,4 +76,19 @@ public void onChunkFinishedGeneration(PopulateChunkEvent.Post evt) { listener.onChunkFinishedGeneration(evt.getTargetChunk().getWorld().getUniqueId(), new Vector2i(chunkPos.getX(), chunkPos.getZ())); } + @Listener(order = Order.POST) + public void onPlayerJoin(ClientConnectionEvent.Join evt) { + listener.onPlayerJoin(evt.getTargetEntity().getUniqueId()); + } + + @Listener(order = Order.POST) + public void onPlayerLeave(ClientConnectionEvent.Disconnect evt) { + listener.onPlayerJoin(evt.getTargetEntity().getUniqueId()); + } + + @Listener(order = Order.POST) + public void onPlayerChat(MessageChannelEvent.Chat evt) { + listener.onChatMessage(Text.of(evt.getMessage().toPlain())); + } + } diff --git a/BlueMapSponge/src/main/java/de/bluecolored/bluemap/sponge/SpongePlayer.java b/BlueMapSponge/src/main/java/de/bluecolored/bluemap/sponge/SpongePlayer.java new file mode 100644 index 00000000..5f3dcb60 --- /dev/null +++ b/BlueMapSponge/src/main/java/de/bluecolored/bluemap/sponge/SpongePlayer.java @@ -0,0 +1,151 @@ +/* + * This file is part of BlueMap, licensed under the MIT License (MIT). + * + * Copyright (c) Blue (Lukas Rieger) + * Copyright (c) contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package de.bluecolored.bluemap.sponge; + +import java.lang.ref.WeakReference; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.UUID; + +import org.spongepowered.api.Sponge; +import org.spongepowered.api.data.key.Keys; +import org.spongepowered.api.effect.potion.PotionEffect; +import org.spongepowered.api.effect.potion.PotionEffectTypes; +import org.spongepowered.api.entity.living.player.gamemode.GameMode; +import org.spongepowered.api.entity.living.player.gamemode.GameModes; + +import com.flowpowered.math.vector.Vector3d; + +import de.bluecolored.bluemap.common.plugin.serverinterface.Gamemode; +import de.bluecolored.bluemap.common.plugin.serverinterface.Player; +import de.bluecolored.bluemap.common.plugin.text.Text; + +public class SpongePlayer implements Player { + + private static final Map GAMEMODE_MAP = new HashMap<>(5); + static { + GAMEMODE_MAP.put(GameModes.ADVENTURE, Gamemode.ADVENTURE); + GAMEMODE_MAP.put(GameModes.SURVIVAL, Gamemode.SURVIVAL); + GAMEMODE_MAP.put(GameModes.CREATIVE, Gamemode.CREATIVE); + GAMEMODE_MAP.put(GameModes.SPECTATOR, Gamemode.SPECTATOR); + GAMEMODE_MAP.put(GameModes.NOT_SET, Gamemode.SURVIVAL); + } + + private UUID uuid; + private Text name; + private UUID world; + private Vector3d position; + private boolean online; + private boolean sneaking; + private boolean invisible; + private Gamemode gamemode; + + private WeakReference delegate; + + public SpongePlayer(org.spongepowered.api.entity.living.player.Player delegate) { + this.uuid = delegate.getUniqueId(); + this.delegate = new WeakReference<>(delegate); + update(); + } + + @Override + public UUID getUuid() { + return this.uuid; + } + + @Override + public Text getName() { + return this.name; + } + + @Override + public UUID getWorld() { + return this.world; + } + + @Override + public Vector3d getPosition() { + return this.position; + } + + @Override + public boolean isOnline() { + return this.online; + } + + @Override + public boolean isSneaking() { + return this.sneaking; + } + + @Override + public boolean isInvisible() { + return this.invisible; + } + + @Override + public Gamemode getGamemode() { + return this.gamemode; + } + + /** + * API access, only call on server thread! + */ + public void update() { + org.spongepowered.api.entity.living.player.Player player = delegate.get(); + if (player == null) { + player = Sponge.getServer().getPlayer(uuid).orElse(null); + if (player == null) { + this.online = false; + return; + } + + delegate = new WeakReference<>(player); + } + + this.gamemode = GAMEMODE_MAP.get(player.get(Keys.GAME_MODE).orElse(GameModes.NOT_SET)); + + boolean invis = player.get(Keys.VANISH).orElse(false); + if (!invis && player.get(Keys.INVISIBLE).orElse(false)) invis = true; + if (!invis) { + Optional> effects = player.get(Keys.POTION_EFFECTS); + if (effects.isPresent()) { + for (PotionEffect effect : effects.get()) { + if (effect.getType().equals(PotionEffectTypes.INVISIBILITY) && effect.getDuration() > 0) invis = true; + } + } + } + this.invisible = invis; + + this.name = Text.of(player.getName()); + this.online = player.isOnline(); + this.position = player.getPosition(); + this.sneaking = player.get(Keys.IS_SNEAKING).orElse(false); + this.world = player.getWorld().getUniqueId(); + } + +} diff --git a/BlueMapSponge/src/main/java/de/bluecolored/bluemap/sponge/SpongePlugin.java b/BlueMapSponge/src/main/java/de/bluecolored/bluemap/sponge/SpongePlugin.java index 5ddabf19..607cd7f1 100644 --- a/BlueMapSponge/src/main/java/de/bluecolored/bluemap/sponge/SpongePlugin.java +++ b/BlueMapSponge/src/main/java/de/bluecolored/bluemap/sponge/SpongePlugin.java @@ -27,8 +27,14 @@ import java.io.File; import java.io.IOException; import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Map; import java.util.Optional; import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; import javax.inject.Inject; @@ -39,13 +45,16 @@ import org.spongepowered.api.event.game.GameReloadEvent; import org.spongepowered.api.event.game.state.GameStartingServerEvent; import org.spongepowered.api.event.game.state.GameStoppingEvent; +import org.spongepowered.api.event.network.ClientConnectionEvent; import org.spongepowered.api.plugin.PluginContainer; import org.spongepowered.api.scheduler.SpongeExecutorService; +import org.spongepowered.api.scheduler.Task; import org.spongepowered.api.util.Tristate; import org.spongepowered.api.world.World; import org.spongepowered.api.world.storage.WorldProperties; import de.bluecolored.bluemap.common.plugin.Plugin; +import de.bluecolored.bluemap.common.plugin.serverinterface.Player; import de.bluecolored.bluemap.common.plugin.serverinterface.ServerEventListener; import de.bluecolored.bluemap.common.plugin.serverinterface.ServerInterface; import de.bluecolored.bluemap.core.logger.Logger; @@ -53,7 +62,7 @@ import net.querz.nbt.CompoundTag; import net.querz.nbt.NBTUtil; -@org.spongepowered.api.plugin.Plugin( +@org.spongepowered.api.plugin.Plugin ( id = Plugin.PLUGIN_ID, name = Plugin.PLUGIN_NAME, authors = { "Blue (Lukas Rieger)" }, @@ -71,13 +80,20 @@ public class SpongePlugin implements ServerInterface { private Plugin bluemap; private SpongeCommands commands; - + private SpongeExecutorService asyncExecutor; + private int playerUpdateIndex = 0; + private Map onlinePlayerMap; + private List onlinePlayerList; + @Inject public SpongePlugin(org.slf4j.Logger logger) { Logger.global = new Slf4jLogger(logger); + this.onlinePlayerMap = new ConcurrentHashMap<>(); + this.onlinePlayerList = Collections.synchronizedList(new ArrayList<>()); + this.bluemap = new Plugin("sponge", this); this.commands = new SpongeCommands(bluemap); } @@ -96,6 +112,12 @@ public void onServerStart(GameStartingServerEvent evt) { Sponge.getCommandManager().register(this, command, command.getLabel()); } + //start updating players + Task.builder() + .intervalTicks(1) + .execute(this::updateSomePlayers) + .submit(this); + asyncExecutor.execute(() -> { try { Logger.global.logInfo("Loading..."); @@ -110,6 +132,7 @@ public void onServerStart(GameStartingServerEvent evt) { @Listener public void onServerStop(GameStoppingEvent evt) { Logger.global.logInfo("Stopping..."); + Sponge.getScheduler().getScheduledTasks(this).forEach(t -> t.cancel()); bluemap.unload(); Logger.global.logInfo("Saved and stopped!"); } @@ -127,6 +150,23 @@ public void onServerReload(GameReloadEvent evt) { }); } + + @Listener + public void onPlayerJoin(ClientConnectionEvent.Join evt) { + SpongePlayer player = new SpongePlayer(evt.getTargetEntity()); + onlinePlayerMap.put(evt.getTargetEntity().getUniqueId(), player); + onlinePlayerList.add(player); + } + + @Listener + public void onPlayerLeave(ClientConnectionEvent.Disconnect evt) { + UUID playerUUID = evt.getTargetEntity().getUniqueId(); + onlinePlayerMap.remove(playerUUID); + synchronized (onlinePlayerList) { + onlinePlayerList.removeIf(p -> p.getUuid().equals(playerUUID)); + } + } + @Override public void registerListener(ServerEventListener listener) { Sponge.getEventManager().registerListeners(this, new EventForwarder(listener)); @@ -163,6 +203,16 @@ public String getWorldName(UUID worldUUID) { public File getConfigFolder() { return configurationDir.toFile(); } + + @Override + public Collection getOnlinePlayers() { + return onlinePlayerMap.values(); + } + + @Override + public Optional getPlayer(UUID uuid) { + return Optional.ofNullable(onlinePlayerMap.get(uuid)); + } @Override public boolean isMetricsEnabled(boolean configValue) { @@ -177,4 +227,25 @@ public boolean isMetricsEnabled(boolean configValue) { return Sponge.getMetricsConfigManager().getGlobalCollectionState().asBoolean(); } + /** + * Only update some of the online players each tick to minimize performance impact on the server-thread. + * Only call this method on the server-thread. + */ + private void updateSomePlayers() { + int onlinePlayerCount = onlinePlayerList.size(); + if (onlinePlayerCount == 0) return; + + int playersToBeUpdated = onlinePlayerCount / 20; //with 20 tps, each player is updated once a second + if (playersToBeUpdated == 0) playersToBeUpdated = 1; + + for (int i = 0; i < playersToBeUpdated; i++) { + playerUpdateIndex++; + if (playerUpdateIndex >= 20 && playerUpdateIndex >= onlinePlayerCount) playerUpdateIndex = 0; + + if (playerUpdateIndex < onlinePlayerCount) { + onlinePlayerList.get(i).update(); + } + } + } + } diff --git a/BlueMapSponge/src/main/resources/bluemap-sponge-defaults.conf b/BlueMapSponge/src/main/resources/bluemap-sponge-defaults.conf index f0a3bdb0..cf42dfc8 100644 --- a/BlueMapSponge/src/main/resources/bluemap-sponge-defaults.conf +++ b/BlueMapSponge/src/main/resources/bluemap-sponge-defaults.conf @@ -9,3 +9,9 @@ webserver { port: 8100 maxConnectionCount: 100 } +liveUpdates { + enabled: true + hiddenGameModes: [] + hideInvisible: true + hideSneaking: false +} diff --git a/BlueMapSponge/src/main/resources/bluemap-sponge.conf b/BlueMapSponge/src/main/resources/bluemap-sponge.conf index c97e6ea8..c126f710 100644 --- a/BlueMapSponge/src/main/resources/bluemap-sponge.conf +++ b/BlueMapSponge/src/main/resources/bluemap-sponge.conf @@ -160,3 +160,23 @@ maps: [ } ] + +liveUpdates { + # If the server should send live-updates and player-positions. + # Default is true + enabled: true + + # A list of gamemodes that will prevent a player from appearing on the map. + # Possible values are: survival, creative, spectator, adventure + hiddenGameModes: [ + "spectator" + ] + + # If this is true, players that have an invisibility (potion-)effect will be hidden on the map. + # Default is true + hideInvisible: true + + # If this is true, players that are sneaking will be hidden on the map. + # Default is false + hideSneaking: false +}