Implement playermarkers on the web-app

This commit is contained in:
Blue (Lukas Rieger) 2020-08-07 14:17:48 +02:00
parent b6d358a097
commit c4f0256ca0
14 changed files with 490 additions and 33 deletions

View File

@ -35,7 +35,6 @@
import de.bluecolored.bluemap.common.MapType; import de.bluecolored.bluemap.common.MapType;
import de.bluecolored.bluemap.common.RenderManager; import de.bluecolored.bluemap.common.RenderManager;
import de.bluecolored.bluemap.common.plugin.serverinterface.ServerEventListener; import de.bluecolored.bluemap.common.plugin.serverinterface.ServerEventListener;
import de.bluecolored.bluemap.common.plugin.text.Text;
public class MapUpdateHandler implements ServerEventListener { public class MapUpdateHandler implements ServerEventListener {
@ -136,19 +135,4 @@ public void flushTileBuffer() {
} }
} }
@Override
public void onPlayerJoin(UUID playerUuid) {
}
@Override
public void onPlayerLeave(UUID playerUuid) {
}
@Override
public void onChatMessage(Text message) {
}
} }

View File

@ -51,6 +51,7 @@
import de.bluecolored.bluemap.common.RenderManager; import de.bluecolored.bluemap.common.RenderManager;
import de.bluecolored.bluemap.common.api.BlueMapAPIImpl; import de.bluecolored.bluemap.common.api.BlueMapAPIImpl;
import de.bluecolored.bluemap.common.plugin.serverinterface.ServerInterface; 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.ConfigManager;
import de.bluecolored.bluemap.core.config.MainConfig; import de.bluecolored.bluemap.core.config.MainConfig;
import de.bluecolored.bluemap.core.config.MainConfig.MapConfig; import de.bluecolored.bluemap.core.config.MainConfig.MapConfig;
@ -86,6 +87,7 @@ public class Plugin {
private Map<String, MapType> maps; private Map<String, MapType> maps;
private MapUpdateHandler updateHandler; private MapUpdateHandler updateHandler;
private PlayerSkinUpdater skinUpdater;
private RenderManager renderManager; private RenderManager renderManager;
private BlueMapWebServer webServer; private BlueMapWebServer webServer;
@ -269,6 +271,10 @@ public synchronized void load() throws IOException, ParseResourceException {
this.updateHandler = new MapUpdateHandler(this); this.updateHandler = new MapUpdateHandler(this);
serverInterface.registerListener(updateHandler); serverInterface.registerListener(updateHandler);
//start skin updater
this.skinUpdater = new PlayerSkinUpdater(config.getWebRoot().resolve("assets").resolve("playerheads").toFile());
serverInterface.registerListener(skinUpdater);
//create/update webfiles //create/update webfiles
WebFilesManager webFilesManager = new WebFilesManager(config.getWebRoot()); WebFilesManager webFilesManager = new WebFilesManager(config.getWebRoot());
if (webFilesManager.needsUpdate()) { if (webFilesManager.needsUpdate()) {

View File

@ -33,16 +33,16 @@
public interface ServerEventListener { 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) {};
void onPlayerJoin(UUID playerUuid); default void onPlayerJoin(UUID playerUuid) {};
void onPlayerLeave(UUID playerUuid); default void onPlayerLeave(UUID playerUuid) {};
void onChatMessage(Text message); default void onChatMessage(Text message) {};
} }

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

@ -198,9 +198,7 @@ public synchronized Texture loadTexture(FileAccess fileAccess, String path) thro
//crop off animation frames //crop off animation frames
if (image.getHeight() > image.getWidth()){ if (image.getHeight() > image.getWidth()){
BufferedImage cropped = new BufferedImage(image.getWidth(), image.getWidth(), image.getType()); image = image.getSubimage(0, 0, image.getWidth(), image.getWidth());
image.copyData(cropped.getRaster());
image = cropped;
} }
//check halfTransparency //check halfTransparency

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"; import {cachePreventionNr, getCookie, setCookie} from "./utils";
export default class BlueMap { export default class BlueMap {
constructor(element, dataRoot) { constructor(element, dataRoot = "data/", liveApiRoot = "live/") {
this.element = $('<div class="bluemap-container"></div>').appendTo(element)[0]; this.element = $('<div class="bluemap-container"></div>').appendTo(element)[0];
this.dataRoot = dataRoot; this.dataRoot = dataRoot;
this.liveApiRoot = liveApiRoot;
this.locationHash = ''; this.locationHash = '';
this.cacheSuffix = ''; this.cacheSuffix = '';

View File

@ -3,6 +3,7 @@ import $ from "jquery";
import ToggleButton from "../ui/ToggleButton"; import ToggleButton from "../ui/ToggleButton";
import Label from "../ui/Label"; import Label from "../ui/Label";
import {cachePreventionNr} from "../utils"; import {cachePreventionNr} from "../utils";
import PlayerMarkerSet from "./PlayerMarkerSet";
export default class MarkerManager { export default class MarkerManager {
@ -10,14 +11,23 @@ export default class MarkerManager {
this.blueMap = blueMap; this.blueMap = blueMap;
this.ui = ui; this.ui = ui;
this.markerData = null;
this.liveData = null;
this.markerSets = []; this.markerSets = [];
this.playerMarkerSet = null;
this.readyPromise = this.readyPromise =
Promise.all([
this.loadMarkerData() this.loadMarkerData()
.catch(ignore => { .catch(ignore => {
if (this.blueMap.debugInfo) console.debug("Failed load markers:", ignore); if (this.blueMap.debugInfo) console.debug("Failed load markers:", ignore);
}) }),
.then(this.loadMarkers); this.checkLiveAPI()
.then(this.initializePlayerMarkers)
])
.then(this.loadMarkers)
.then(this.updatePlayerMarkerLoop);
$(document).on('bluemap-map-change', this.onBlueMapMapChange); $(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 = () => { loadMarkers = () => {
if (this.markerData && this.markerData.markerSets) { if (this.markerData && this.markerData.markerSets) {
this.markerData.markerSets.forEach(setData => { 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(){ update(){
this.markerSets.forEach(markerSet => { this.markerSets.forEach(markerSet => {
markerSet.update(); markerSet.update();
}); });
} }
updatePlayerMarkerLoop = () => {
if (this.playerMarkerSet){
this.playerMarkerSet.updateLive();
}
setTimeout(this.updatePlayerMarkerLoop, 2000);
};
addMenuElements(menu){ addMenuElements(menu){
let addedLabel = false; let addedLabel = false;
this.markerSets.forEach(markerSet => { this.markerSets.forEach(markerSet => {

View File

@ -1,7 +1,6 @@
import $ from 'jquery'; import $ from 'jquery';
import Marker from "./Marker"; import Marker from "./Marker";
import {CSS2DObject} from "./CSS2DRenderer"; import {CSS2DObject} from "./CSS2DRenderer";
import {Vector3} from "three";
import POI from "../../../assets/poi.svg"; 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; pointer-events: none;
> * { > * {
@ -87,3 +87,38 @@
filter: drop-shadow(1px 1px 3px #0008); 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;
}
}
}