Merge branch 'base'

This commit is contained in:
Blue (Lukas Rieger) 2020-08-16 15:18:55 +02:00
commit 465a38080a
49 changed files with 2285 additions and 175 deletions

View File

@ -0,0 +1,138 @@
/*
* This file is part of BlueMap, licensed under the MIT License (MIT).
*
* Copyright (c) Blue (Lukas Rieger) <https://bluecolored.de>
* 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, Gamemode> 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<org.bukkit.entity.Player> 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();
}
}

View File

@ -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<UUID, Player> onlinePlayerMap;
private List<BukkitPlayer> 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<Player> getOnlinePlayers() {
return onlinePlayerMap.values();
}
@Override
public Optional<Player> 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();
}
}
}
}

View File

@ -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));
}
}

View File

@ -9,3 +9,9 @@ webserver {
port: 8100
maxConnectionCount: 100
}
liveUpdates {
enabled: true
hiddenGameModes: []
hideInvisible: true
hideSneaking: false
}

View File

@ -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
}

View File

@ -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;

View File

@ -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());

View File

@ -0,0 +1,118 @@
/*
* This file is part of BlueMap, licensed under the MIT License (MIT).
*
* Copyright (c) Blue (Lukas Rieger) <https://bluecolored.de>
* 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<String, HttpRequestHandler> 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);
}
}
}

View File

@ -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<String, MapType> 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();
}

View File

@ -0,0 +1,54 @@
/*
* This file is part of BlueMap, licensed under the MIT License (MIT).
*
* Copyright (c) Blue (Lukas Rieger) <https://bluecolored.de>
* 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 + "'");
}
}

View File

@ -0,0 +1,64 @@
/*
* This file is part of BlueMap, licensed under the MIT License (MIT).
*
* Copyright (c) Blue (Lukas Rieger) <https://bluecolored.de>
* 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 <code>true</code> if the player is sneaking.
* <p><i>If the player is offline the value of this method is undetermined.</i></p>
* @return
*/
public boolean isSneaking();
/**
* Returns <code>true</code> if the player has an invisibillity effect
* <p><i>If the player is offline the value of this method is undetermined.</i></p>
*/
public boolean isInvisible();
/**
* Returns the {@link Gamemode} this player is in
* <p><i>If the player is offline the value of this method is undetermined.</i></p>
*/
public Gamemode getGamemode();
}

View File

@ -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) {};
}

View File

@ -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<Player> getOnlinePlayers();
/**
* Returns the state of the player with that UUID if present<br>
* this method is only guaranteed to return a {@link PlayerState} if the player is currently online.
*/
Optional<Player> getPlayer(UUID uuid);
}

View File

@ -0,0 +1,152 @@
/*
* This file is part of BlueMap, licensed under the MIT License (MIT).
*
* Copyright (c) Blue (Lukas Rieger) <https://bluecolored.de>
* 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<BufferedImage> 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<BufferedImage> loadSkin() {
CompletableFuture<BufferedImage> 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);
}
}
}

View File

@ -0,0 +1,63 @@
/*
* This file is part of BlueMap, licensed under the MIT License (MIT).
*
* Copyright (c) Blue (Lukas Rieger) <https://bluecolored.de>
* 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<UUID, PlayerSkin> 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);
}
}

View File

@ -0,0 +1,39 @@
/*
* This file is part of BlueMap, licensed under the MIT License (MIT).
*
* Copyright (c) Blue (Lukas Rieger) <https://bluecolored.de>
* 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<String> getHiddenGameModes();
boolean isHideInvisible();
boolean isHideSneaking();
}

View File

@ -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<MapConfig> mapConfigs = new ArrayList<>();
private boolean liveUpdatesEnabled = false;
private Collection<String> 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<MapConfig> getMapConfigs(){
return mapConfigs;
}
@Override
public boolean isLiveUpdatesEnabled() {
return this.liveUpdatesEnabled;
}
@Override
public Collection<String> 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;

View File

@ -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

View File

@ -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<String, String> 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 {

View File

@ -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) {

View File

@ -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<String, Set<String>> header;
private Map<String, Set<String>> headerLC;
private byte[] data;
public HttpRequest(String method, String path, String version, Map<String, Set<String>> header) {
private String path = null;
private Map<String, String> getParams = null;
public HttpRequest(String method, String adress, String version, Map<String, Set<String>> 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<String, Set<String>> getHeader() {
}
public Map<String, Set<String>> getLowercaseHeader() {
return header;
return headerLC;
}
public Set<String> getHeader(String key){
@ -107,6 +110,40 @@ public Set<String> getLowercaseHeader(String key){
return headerValues;
}
public String getPath() {
if (path == null) parseAdress();
return path;
}
public Map<String, String> 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<String, String> 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);
}

View File

@ -24,6 +24,7 @@
*/
package de.bluecolored.bluemap.core.webserver;
@FunctionalInterface
public interface HttpRequestHandler {
HttpResponse handle(HttpRequest request);

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

View File

@ -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 = $('<div class="bluemap-container"></div>').appendTo(element)[0];
this.dataRoot = dataRoot;
this.liveApiRoot = liveApiRoot;
this.locationHash = '';
this.cacheSuffix = '';

View File

@ -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 => {

View File

@ -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";

View File

@ -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 = $(`<div class="marker-player"><img src="assets/playerheads/${this.player}.png" onerror="this.onerror=null;this.src='assets/playerheads/steve.png';"><div class="nameplate">${this.label}</div></div>`);
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 = () => {
}
}

View File

@ -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);
}
});
}
}

View File

@ -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;
}
}
}

View File

@ -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);
}
}

View File

@ -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<File, UUID> worldUuids;
private Map<File, UUID> worldUUIDs;
private FabricEventForwarder eventForwarder;
private LoadingCache<ServerWorld, UUID> worldUuidCache;
private int playerUpdateIndex = 0;
private Map<UUID, Player> onlinePlayerMap;
private List<FabricPlayer> 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<Player> getOnlinePlayers() {
return onlinePlayerMap.values();
}
@Override
public Optional<Player> 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();
}
}
}
}

View File

@ -0,0 +1,156 @@
/*
* This file is part of BlueMap, licensed under the MIT License (MIT).
*
* Copyright (c) Blue (Lukas Rieger) <https://bluecolored.de>
* 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, Gamemode> 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<ServerPlayerEntity> 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;
}
}
}

View File

@ -0,0 +1,42 @@
/*
* This file is part of BlueMap, licensed under the MIT License (MIT).
*
* Copyright (c) Blue (Lukas Rieger) <https://bluecolored.de>
* 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<PlayerJoinCallback> EVENT = EventFactory.createArrayBacked(PlayerJoinCallback.class,
(listeners) -> (server, player) -> {
for (PlayerJoinCallback event : listeners) {
event.onPlayerJoin(server, player);
}
}
);
void onPlayerJoin(MinecraftServer server, ServerPlayerEntity player);
}

View File

@ -0,0 +1,42 @@
/*
* This file is part of BlueMap, licensed under the MIT License (MIT).
*
* Copyright (c) Blue (Lukas Rieger) <https://bluecolored.de>
* 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<PlayerLeaveCallback> EVENT = EventFactory.createArrayBacked(PlayerLeaveCallback.class,
(listeners) -> (server, player) -> {
for (PlayerLeaveCallback event : listeners) {
event.onPlayerLeave(server, player);
}
}
);
void onPlayerLeave(MinecraftServer server, ServerPlayerEntity player);
}

View File

@ -0,0 +1,56 @@
/*
* This file is part of BlueMap, licensed under the MIT License (MIT).
*
* Copyright (c) Blue (Lukas Rieger) <https://bluecolored.de>
* 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);
}
}

View File

@ -9,3 +9,9 @@ webserver {
port: 8100
maxConnectionCount: 100
}
liveUpdates {
enabled: true
hiddenGameModes: []
hideInvisible: true
hideSneaking: false
}

View File

@ -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
}

View File

@ -6,8 +6,9 @@
"mixins": [],
"client": [],
"server": [
"MixinServerWorld",
"MixinChunkGenerator"
"MixinChunkGenerator",
"MixinPlayerManager",
"MixinServerWorld"
],
"injectors": {
"defaultRequire": 1

View File

@ -0,0 +1,116 @@
/*
* This file is part of BlueMap, licensed under the MIT License (MIT).
*
* Copyright (c) Blue (Lukas Rieger) <https://bluecolored.de>
* 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<ServerEventListener> 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);
}
}

View File

@ -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<CommandSource> commands;
private Map<String, UUID> worldUUIDs;
private Collection<ServerEventListener> eventListeners;
private Map<File, UUID> worldUUIDs;
private ForgeEventForwarder eventForwarder;
private LoadingCache<ServerWorld, UUID> worldUuidCache;
private int playerUpdateIndex = 0;
private Map<UUID, Player> onlinePlayerMap;
private List<ForgePlayer> 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<CommandSource> getCommands() {
return commands;
public MinecraftServer getServer() {
return this.serverInstance;
}
@Override
public Collection<Player> getOnlinePlayers() {
return onlinePlayerMap.values();
}
@Override
public Optional<Player> 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();
}
}
}
}

View File

@ -0,0 +1,155 @@
/*
* This file is part of BlueMap, licensed under the MIT License (MIT).
*
* Copyright (c) Blue (Lukas Rieger) <https://bluecolored.de>
* 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<GameType, Gamemode> 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<ServerPlayerEntity> 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;
}
}
}

View File

@ -9,3 +9,9 @@ webserver {
port: 8100
maxConnectionCount: 100
}
liveUpdates {
enabled: true
hiddenGameModes: []
hideInvisible: true
hideSneaking: false
}

View File

@ -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
}

View File

@ -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()));
}
}

View File

@ -0,0 +1,151 @@
/*
* This file is part of BlueMap, licensed under the MIT License (MIT).
*
* Copyright (c) Blue (Lukas Rieger) <https://bluecolored.de>
* 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, Gamemode> 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<org.spongepowered.api.entity.living.player.Player> 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<List<PotionEffect>> 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();
}
}

View File

@ -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<UUID, Player> onlinePlayerMap;
private List<SpongePlayer> 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<Player> getOnlinePlayers() {
return onlinePlayerMap.values();
}
@Override
public Optional<Player> 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();
}
}
}
}

View File

@ -9,3 +9,9 @@ webserver {
port: 8100
maxConnectionCount: 100
}
liveUpdates {
enabled: true
hiddenGameModes: []
hideInvisible: true
hideSneaking: false
}

View File

@ -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
}