Map: game servers send tiles, players and overlay; BlueMap and LiveMapSync removed from the Java side
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DjMbLQujBHunCCu5GpsHaT
No files matched your search
@@ -1,16 +0,0 @@
|
|||||||
// SCOPENET's BlueMap addon: guild claims, spawn, warps, shops and a player side panel on the 3D map.
|
|
||||||
// BlueMap's API is the same on Paper, Fabric and Forge, so this one module serves them all. It is only ever called
|
|
||||||
// when BlueMap is installed, and BlueMap provides these classes at runtime (compileOnly).
|
|
||||||
repositories {
|
|
||||||
maven { url = 'https://repo.bluecolored.de/releases' }
|
|
||||||
}
|
|
||||||
configurations.compileClasspath {
|
|
||||||
attributes {
|
|
||||||
attribute(org.gradle.api.attributes.java.TargetJvmVersion.TARGET_JVM_VERSION_ATTRIBUTE, 21)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
dependencies {
|
|
||||||
implementation project(':common')
|
|
||||||
compileOnly 'de.bluecolored:bluemap-api:2.7.4'
|
|
||||||
implementation 'com.google.code.gson:gson:2.10.1'
|
|
||||||
}
|
|
||||||
@@ -1,211 +0,0 @@
|
|||||||
package net.scopenet.bluemap;
|
|
||||||
|
|
||||||
import com.flowpowered.math.vector.Vector2d;
|
|
||||||
import com.flowpowered.math.vector.Vector3d;
|
|
||||||
import com.google.gson.JsonObject;
|
|
||||||
import de.bluecolored.bluemap.api.BlueMapAPI;
|
|
||||||
import de.bluecolored.bluemap.api.BlueMapMap;
|
|
||||||
import de.bluecolored.bluemap.api.markers.Marker;
|
|
||||||
import de.bluecolored.bluemap.api.markers.MarkerSet;
|
|
||||||
import de.bluecolored.bluemap.api.markers.POIMarker;
|
|
||||||
import de.bluecolored.bluemap.api.markers.ShapeMarker;
|
|
||||||
import de.bluecolored.bluemap.api.math.Color;
|
|
||||||
import de.bluecolored.bluemap.api.math.Shape;
|
|
||||||
import net.scopenet.core.map.ChunkRegions;
|
|
||||||
import net.scopenet.core.map.MapData;
|
|
||||||
import net.scopenet.core.map.MapModel;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.nio.charset.StandardCharsets;
|
|
||||||
import java.nio.file.Files;
|
|
||||||
import java.nio.file.Path;
|
|
||||||
import java.nio.file.StandardCopyOption;
|
|
||||||
import java.util.*;
|
|
||||||
import java.util.concurrent.Executors;
|
|
||||||
import java.util.concurrent.ScheduledExecutorService;
|
|
||||||
import java.util.concurrent.TimeUnit;
|
|
||||||
import java.util.function.Supplier;
|
|
||||||
import java.util.logging.Logger;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Puts SCOPENET on the BlueMap 3D map:
|
|
||||||
* <ul>
|
|
||||||
* <li>guild land as coloured regions, with the guild's icon and name,</li>
|
|
||||||
* <li>spawn, warps, guild homes (and homes, if the admin allows) as pins,</li>
|
|
||||||
* <li>markets and shops as pins that list what is for sale,</li>
|
|
||||||
* <li>a script that opens a side panel when you click a player.</li>
|
|
||||||
* </ul>
|
|
||||||
* Start it once from the plugin or mod. It waits for BlueMap to come up, so the load order doesn't matter, and does
|
|
||||||
* nothing if BlueMap isn't installed.
|
|
||||||
*/
|
|
||||||
public final class MapAddon {
|
|
||||||
/** Where the addon gets its data. Both calls must be safe from any thread. */
|
|
||||||
public interface Feed {
|
|
||||||
MapData snapshot();
|
|
||||||
JsonObject playersJson();
|
|
||||||
}
|
|
||||||
|
|
||||||
static final String WEB_DIR = "assets/scopenet";
|
|
||||||
static final List<String> ASSETS = List.of("scopenet.js", "scopenet.css", "spawn.svg", "warp.svg", "home.svg", "guild-home.svg", "market.svg", "shop.svg", "guild.svg");
|
|
||||||
static final String CLAIMS = "scopenet-claims", GUILDS = "scopenet-guilds", PLACES = "scopenet-places", SHOPS = "scopenet-shops";
|
|
||||||
static final float CLAIM_Y = 66f;
|
|
||||||
|
|
||||||
private final Feed feed;
|
|
||||||
private final Logger log;
|
|
||||||
private ScheduledExecutorService worker;
|
|
||||||
private volatile boolean enabled;
|
|
||||||
|
|
||||||
private MapAddon(Feed feed, Logger log) { this.feed = feed; this.log = log; }
|
|
||||||
|
|
||||||
/** Hook into BlueMap. Returns a handle to stop it. */
|
|
||||||
public static MapAddon start(Feed feed, Logger log) {
|
|
||||||
MapAddon addon = new MapAddon(feed, log);
|
|
||||||
BlueMapAPI.onEnable(addon::enable);
|
|
||||||
BlueMapAPI.onDisable(api -> addon.disable());
|
|
||||||
return addon;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void stop() { disable(); }
|
|
||||||
|
|
||||||
// ---- lifecycle -------------------------------------------------------------------------
|
|
||||||
|
|
||||||
private synchronized void enable(BlueMapAPI api) {
|
|
||||||
try {
|
|
||||||
Path root = api.getWebApp().getWebRoot().resolve(WEB_DIR);
|
|
||||||
Files.createDirectories(root);
|
|
||||||
for (String name : ASSETS) copy(name, root.resolve(name));
|
|
||||||
api.getWebApp().registerScript(WEB_DIR + "/scopenet.js");
|
|
||||||
api.getWebApp().registerStyle(WEB_DIR + "/scopenet.css");
|
|
||||||
} catch (IOException | RuntimeException e) {
|
|
||||||
log.warning("SCOPENET could not add its files to BlueMap: " + e);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
enabled = true;
|
|
||||||
worker = Executors.newSingleThreadScheduledExecutor(r -> {
|
|
||||||
Thread t = new Thread(r, "scopenet-bluemap");
|
|
||||||
t.setDaemon(true);
|
|
||||||
return t;
|
|
||||||
});
|
|
||||||
worker.scheduleWithFixedDelay(() -> refresh(api), 2, 10, TimeUnit.SECONDS);
|
|
||||||
log.info("SCOPENET is adding guild claims, pins and the player panel to BlueMap.");
|
|
||||||
}
|
|
||||||
|
|
||||||
private synchronized void disable() {
|
|
||||||
enabled = false;
|
|
||||||
if (worker != null) worker.shutdownNow();
|
|
||||||
worker = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void copy(String name, Path target) throws IOException {
|
|
||||||
try (InputStream in = MapAddon.class.getResourceAsStream("/" + WEB_DIR + "/" + name)) {
|
|
||||||
if (in == null) throw new IOException("missing resource " + name);
|
|
||||||
Files.copy(in, target, StandardCopyOption.REPLACE_EXISTING);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- refresh ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
private void refresh(BlueMapAPI api) {
|
|
||||||
if (!enabled) return;
|
|
||||||
try {
|
|
||||||
MapData data = feed.snapshot();
|
|
||||||
writePlayers(api);
|
|
||||||
for (BlueMapMap map : api.getMaps()) update(map, data);
|
|
||||||
} catch (RuntimeException | IOException e) {
|
|
||||||
log.fine("SCOPENET map refresh failed: " + e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void writePlayers(BlueMapAPI api) throws IOException {
|
|
||||||
Path file = api.getWebApp().getWebRoot().resolve(WEB_DIR).resolve("players.json");
|
|
||||||
Path tmp = file.resolveSibling("players.json.tmp");
|
|
||||||
Files.writeString(tmp, feed.playersJson().toString(), StandardCharsets.UTF_8);
|
|
||||||
Files.move(tmp, file, StandardCopyOption.REPLACE_EXISTING);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void update(BlueMapMap map, MapData data) {
|
|
||||||
String dimension = MapModel.dimensionOf(map.getId(), map.getName());
|
|
||||||
set(map, CLAIMS, "Guild claims", claimMarkers(data, dimension));
|
|
||||||
set(map, GUILDS, "Guild names", guildMarkers(data, dimension));
|
|
||||||
set(map, PLACES, "Spawn, warps & homes", pinMarkers(data, dimension, false));
|
|
||||||
set(map, SHOPS, "Markets & shops", pinMarkers(data, dimension, true));
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Replace a marker set's contents in place, so the map doesn't flicker. */
|
|
||||||
private static void set(BlueMapMap map, String id, String label, Map<String, Marker> wanted) {
|
|
||||||
MarkerSet set = map.getMarkerSets().computeIfAbsent(id, k -> MarkerSet.builder().label(label).toggleable(true).defaultHidden(false).build());
|
|
||||||
set.getMarkers().keySet().removeIf(key -> !wanted.containsKey(key));
|
|
||||||
wanted.forEach(set::put);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- markers ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
private static Shape shape(ChunkRegions.Ring ring) {
|
|
||||||
List<Vector2d> points = new ArrayList<>();
|
|
||||||
for (ChunkRegions.Point p : ring.points()) points.add(new Vector2d(p.x(), p.z()));
|
|
||||||
return new Shape(points);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Map<String, Marker> claimMarkers(MapData data, String dimension) {
|
|
||||||
Map<String, Marker> out = new LinkedHashMap<>();
|
|
||||||
for (MapData.ClaimRegion c : data.claims()) {
|
|
||||||
if (!c.dimension().equals(dimension)) continue;
|
|
||||||
Shape[] holes = c.holes().stream().map(MapAddon::shape).toArray(Shape[]::new);
|
|
||||||
ShapeMarker marker = ShapeMarker.builder()
|
|
||||||
.label(c.name())
|
|
||||||
.shape(shape(c.outer()), CLAIM_Y)
|
|
||||||
.holes(holes)
|
|
||||||
.lineColor(new Color(c.color(), 0.95f))
|
|
||||||
.fillColor(new Color(c.color(), 0.28f))
|
|
||||||
.lineWidth(3)
|
|
||||||
.depthTestEnabled(false)
|
|
||||||
.detail(c.detail())
|
|
||||||
.build();
|
|
||||||
out.put("claim-" + c.id(), marker);
|
|
||||||
}
|
|
||||||
return out;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** One icon and name per area, in the middle of it, fading out when you are far away. */
|
|
||||||
private static Map<String, Marker> guildMarkers(MapData data, String dimension) {
|
|
||||||
Map<String, Marker> out = new LinkedHashMap<>();
|
|
||||||
for (MapData.ClaimRegion c : data.claims()) {
|
|
||||||
if (!c.dimension().equals(dimension)) continue;
|
|
||||||
POIMarker.Builder builder = POIMarker.builder()
|
|
||||||
.label(c.name() + " [" + c.tag() + "]")
|
|
||||||
.position(c.labelX(), CLAIM_Y + 1, c.labelZ())
|
|
||||||
.styleClasses("scopenet-guild-icon")
|
|
||||||
.maxDistance(1600)
|
|
||||||
.detail(c.detail());
|
|
||||||
builder = c.icon().isEmpty() ? builder.icon(WEB_DIR + "/guild.svg", 16, 16) : builder.icon(c.icon(), 16, 16);
|
|
||||||
out.put("guild-" + c.id(), builder.build());
|
|
||||||
}
|
|
||||||
return out;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Map<String, Marker> pinMarkers(MapData data, String dimension, boolean shops) {
|
|
||||||
Map<String, Marker> out = new LinkedHashMap<>();
|
|
||||||
for (MapData.Pin p : data.pins()) {
|
|
||||||
boolean isShop = p.kind().equals("market") || p.kind().equals("shop");
|
|
||||||
if (!p.dimension().equals(dimension) || isShop != shops) continue;
|
|
||||||
String icon = switch (p.kind()) {
|
|
||||||
case "spawn" -> "spawn.svg";
|
|
||||||
case "warp" -> "warp.svg";
|
|
||||||
case "home" -> "home.svg";
|
|
||||||
case "guild_home" -> "guild-home.svg";
|
|
||||||
case "market" -> "market.svg";
|
|
||||||
default -> "shop.svg";
|
|
||||||
};
|
|
||||||
out.put(p.id(), POIMarker.builder()
|
|
||||||
.label(p.label())
|
|
||||||
.position(new Vector3d(p.x(), p.y() + 1, p.z()))
|
|
||||||
.icon(WEB_DIR + "/" + icon, 16, 16)
|
|
||||||
.styleClasses("scopenet-pin", "scopenet-" + p.kind().replace('_', '-'))
|
|
||||||
.maxDistance(2400)
|
|
||||||
.detail(p.detail())
|
|
||||||
.build());
|
|
||||||
}
|
|
||||||
return out;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32"><circle cx="16" cy="16" r="14" fill="#22d3ee" stroke="#ffffff" stroke-width="2"/><path d="M16 6l8 3v6c0 5-3.4 8.6-8 10-4.6-1.4-8-5-8-10V9z" fill="#fff"/></svg>
|
|
||||||
|
Before Width: | Height: | Size: 243 B |
@@ -1 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32"><path d="M16 3l11 4v8c0 7-4.6 12-11 14C9.6 27 5 22 5 15V7z" fill="#8b6cff" stroke="#fff" stroke-width="2"/><path d="M16 9l2.2 4.6 5 .7-3.6 3.5.9 5L16 20.4 11.5 22.8l.9-5-3.6-3.5 5-.7z" fill="#fff"/></svg>
|
|
||||||
|
Before Width: | Height: | Size: 288 B |
@@ -1 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32"><circle cx="16" cy="16" r="14" fill="#fbbf24" stroke="#ffffff" stroke-width="2"/><path d="M16 7l9 8h-3v8h-4v-5h-4v5h-4v-8H7z" fill="#fff"/></svg>
|
|
||||||
|
Before Width: | Height: | Size: 229 B |
@@ -1 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32"><circle cx="16" cy="16" r="14" fill="#fb7185" stroke="#ffffff" stroke-width="2"/><path d="M7 12l2-5h14l2 5v2c0 1.7-1.3 3-3 3s-3-1.3-3-3c0 1.7-1.3 3-3 3s-3-1.3-3-3c0 1.7-1.3 3-3 3s-3-1.3-3-3zM9 19h14v6H9z" fill="#fff"/></svg>
|
|
||||||
|
Before Width: | Height: | Size: 308 B |
@@ -1,64 +0,0 @@
|
|||||||
/* SCOPENET on BlueMap: marker styling and the player side panel. Loaded by BlueMap through the addon. */
|
|
||||||
|
|
||||||
/* Guild icons sit on the map as small rounded badges, whatever size the uploaded image is. */
|
|
||||||
.scopenet-guild-icon img,
|
|
||||||
.bm-marker-poi.scopenet-guild-icon img {
|
|
||||||
width: 34px !important;
|
|
||||||
height: 34px !important;
|
|
||||||
object-fit: cover;
|
|
||||||
border-radius: 9px;
|
|
||||||
border: 2px solid rgba(255, 255, 255, 0.9);
|
|
||||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.55);
|
|
||||||
background: #1a1d25;
|
|
||||||
}
|
|
||||||
.scopenet-pin img,
|
|
||||||
.bm-marker-poi.scopenet-pin img {
|
|
||||||
width: 26px !important;
|
|
||||||
height: 26px !important;
|
|
||||||
filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.55));
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Popup text inside marker details. */
|
|
||||||
.scopenet-detail { font-size: 13px; line-height: 1.45; min-width: 160px; }
|
|
||||||
.scopenet-detail span { opacity: 0.7; }
|
|
||||||
.scopenet-detail code { background: rgba(255, 255, 255, 0.08); padding: 1px 5px; border-radius: 4px; }
|
|
||||||
.scopenet-list { margin: 4px 0 0; padding-left: 16px; }
|
|
||||||
|
|
||||||
/* Player panel (shown in a plain browser; inside the SCOPENET launcher the launcher shows its own). */
|
|
||||||
#scopenet-panel {
|
|
||||||
position: fixed;
|
|
||||||
top: 64px;
|
|
||||||
right: 14px;
|
|
||||||
width: 300px;
|
|
||||||
max-height: calc(100vh - 90px);
|
|
||||||
overflow-y: auto;
|
|
||||||
z-index: 50;
|
|
||||||
padding: 16px;
|
|
||||||
border-radius: 14px;
|
|
||||||
background: rgba(20, 22, 30, 0.96);
|
|
||||||
color: #e6e8ee;
|
|
||||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
|
||||||
box-shadow: 0 16px 48px -12px rgba(0, 0, 0, 0.75);
|
|
||||||
font-family: system-ui, -apple-system, "Segoe UI", sans-serif;
|
|
||||||
font-size: 13px;
|
|
||||||
animation: scopenet-in 0.18s ease;
|
|
||||||
}
|
|
||||||
@keyframes scopenet-in { from { opacity: 0; transform: translateX(18px); } }
|
|
||||||
#scopenet-panel button.close {
|
|
||||||
position: absolute; top: 8px; right: 8px; width: 26px; height: 26px; border: 0; border-radius: 8px;
|
|
||||||
background: transparent; color: #9aa0ae; font-size: 18px; cursor: pointer;
|
|
||||||
}
|
|
||||||
#scopenet-panel button.close:hover { background: rgba(255, 255, 255, 0.08); color: #fff; }
|
|
||||||
#scopenet-panel .head { display: flex; gap: 12px; align-items: center; padding-right: 26px; }
|
|
||||||
#scopenet-panel .head img { width: 56px; height: 56px; border-radius: 16px; border: 3px solid #8b6cff; background: #1a1d25; image-rendering: pixelated; }
|
|
||||||
#scopenet-panel h3 { margin: 0 0 2px; font-size: 17px; }
|
|
||||||
#scopenet-panel .title { color: #fbbf24; font-size: 12px; }
|
|
||||||
#scopenet-panel .chips { display: flex; flex-wrap: wrap; gap: 6px; margin: 12px 0; }
|
|
||||||
#scopenet-panel .chip { padding: 2px 9px; border-radius: 99px; background: rgba(255, 255, 255, 0.08); font-size: 11px; font-weight: 650; }
|
|
||||||
#scopenet-panel .chip.lv { color: #8b6cff; }
|
|
||||||
#scopenet-panel .chip.guild { color: #22d3ee; }
|
|
||||||
#scopenet-panel .chip.rank { color: #fbbf24; }
|
|
||||||
#scopenet-panel .facts { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
|
|
||||||
#scopenet-panel .facts div { padding: 8px 10px; border-radius: 10px; background: rgba(255, 255, 255, 0.05); color: #9aa0ae; font-size: 11px; }
|
|
||||||
#scopenet-panel .facts strong { display: block; color: #e6e8ee; font-size: 14px; margin-top: 2px; }
|
|
||||||
#scopenet-panel .note { margin: 14px 0 0; padding: 10px 12px; border-radius: 10px; background: rgba(139, 108, 255, 0.12); color: #c9bfff; font-size: 12px; line-height: 1.4; }
|
|
||||||
@@ -1,115 +0,0 @@
|
|||||||
/*
|
|
||||||
* SCOPENET on BlueMap. Registered by the SCOPENET addon, so it runs inside BlueMap's own page: the same in a browser and
|
|
||||||
* inside the SCOPENET launcher.
|
|
||||||
*
|
|
||||||
* - Click a player on the map to see who they are (level, guild, rank, title...), read from players.json that the
|
|
||||||
* server keeps up to date next to this file.
|
|
||||||
* - Inside the launcher (this page is in an iframe) the click is handed to the launcher, which shows the full side panel
|
|
||||||
* with the actions that need a signed-in account: teleport request, message, guild invite.
|
|
||||||
*/
|
|
||||||
(function () {
|
|
||||||
'use strict';
|
|
||||||
var BASE = 'assets/scopenet/';
|
|
||||||
var PLAYER_PREFIX = 'bm-marker-bm-player-';
|
|
||||||
var embedded = window.parent && window.parent !== window;
|
|
||||||
var players = {};
|
|
||||||
var selected = null;
|
|
||||||
|
|
||||||
function esc(text) {
|
|
||||||
return String(text == null ? '' : text).replace(/[&<>"']/g, function (c) {
|
|
||||||
return { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c];
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/** The uuid behind a player marker element, or null. BlueMap ids them "bm-marker-bm-player-<uuid>". */
|
|
||||||
function uuidFromId(id) {
|
|
||||||
return typeof id === 'string' && id.indexOf(PLAYER_PREFIX) === 0 ? id.substring(PLAYER_PREFIX.length) : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Which player marker is under this screen point? Works even where the marker ignores pointer events. */
|
|
||||||
function playerAt(x, y) {
|
|
||||||
var nodes = document.querySelectorAll('[id^="' + PLAYER_PREFIX + '"]');
|
|
||||||
for (var i = 0; i < nodes.length; i++) {
|
|
||||||
var r = nodes[i].getBoundingClientRect();
|
|
||||||
if (r.width === 0 && r.height === 0) continue;
|
|
||||||
if (x >= r.left - 6 && x <= r.right + 6 && y >= r.top - 6 && y <= r.bottom + 6) return uuidFromId(nodes[i].id);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function hours(seconds) {
|
|
||||||
seconds = Number(seconds) || 0;
|
|
||||||
return seconds >= 3600 ? Math.floor(seconds / 3600) + 'h ' + Math.floor((seconds % 3600) / 60) + 'm' : Math.floor(seconds / 60) + 'm';
|
|
||||||
}
|
|
||||||
|
|
||||||
function panel() {
|
|
||||||
var el = document.getElementById('scopenet-panel');
|
|
||||||
if (!el) { el = document.createElement('div'); el.id = 'scopenet-panel'; document.body.appendChild(el); }
|
|
||||||
return el;
|
|
||||||
}
|
|
||||||
|
|
||||||
function render() {
|
|
||||||
var el = document.getElementById('scopenet-panel');
|
|
||||||
if (!selected) { if (el) el.remove(); return; }
|
|
||||||
var p = players[selected] || players[selected.toLowerCase()] || {};
|
|
||||||
el = panel();
|
|
||||||
var name = p.name || 'Player';
|
|
||||||
el.innerHTML =
|
|
||||||
'<button class="close" aria-label="Close">×</button>' +
|
|
||||||
'<div class="head"><img alt="" src="assets/playerheads/' + esc(selected) + '.png" onerror="this.src=\'assets/steve.png\'">' +
|
|
||||||
'<div><h3>' + esc(name) + '</h3>' + (p.title ? '<div class="title">' + esc(p.title) + '</div>' : '') + '</div></div>' +
|
|
||||||
'<div class="chips">' +
|
|
||||||
'<span class="chip lv">Level ' + esc(p.level || 1) + '</span>' +
|
|
||||||
(p.guild ? '<span class="chip guild">[' + esc(p.guildTag) + '] ' + esc(p.guild) + '</span>' : '') +
|
|
||||||
(p.rank ? '<span class="chip rank">' + esc(p.rank) + '</span>' : '') + '</div>' +
|
|
||||||
'<div class="facts">' +
|
|
||||||
'<div>Playtime<strong>' + esc(hours(p.playtime)) + '</strong></div>' +
|
|
||||||
'<div>Server level<strong>' + esc(p.serverLevel || 1) + '</strong></div>' +
|
|
||||||
(p.guildRole ? '<div>Guild role<strong>' + esc(p.guildRole) + '</strong></div>' : '') + '</div>' +
|
|
||||||
'<p class="note">Open this map in the SCOPENET launcher to send a teleport request or message, or to invite ' + esc(name) + ' to your guild.</p>';
|
|
||||||
el.querySelector('.close').addEventListener('click', function () { select(null); });
|
|
||||||
}
|
|
||||||
|
|
||||||
function select(uuid) {
|
|
||||||
selected = uuid;
|
|
||||||
if (embedded) {
|
|
||||||
window.parent.postMessage(uuid
|
|
||||||
? { source: 'scopenet-map', type: 'player', uuid: uuid, name: (players[uuid] || {}).name || '' }
|
|
||||||
: { source: 'scopenet-map', type: 'deselect' }, '*');
|
|
||||||
} else {
|
|
||||||
render();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function refresh() {
|
|
||||||
fetch(BASE + 'players.json?t=' + Date.now(), { cache: 'no-store' })
|
|
||||||
.then(function (r) { return r.ok ? r.json() : null; })
|
|
||||||
.then(function (j) { if (j && j.players) { players = j.players; if (!embedded && selected) render(); } })
|
|
||||||
.catch(function () { /* the file appears once the server has written it */ });
|
|
||||||
}
|
|
||||||
|
|
||||||
// A click is a press and release in nearly the same place; a drag moves the map.
|
|
||||||
var down = null;
|
|
||||||
document.addEventListener('pointerdown', function (e) { down = { x: e.clientX, y: e.clientY, t: Date.now() }; }, true);
|
|
||||||
document.addEventListener('pointerup', function (e) {
|
|
||||||
var start = down;
|
|
||||||
down = null;
|
|
||||||
if (!start || e.button > 0) return;
|
|
||||||
if (Math.hypot(e.clientX - start.x, e.clientY - start.y) > 6 || Date.now() - start.t > 700) return;
|
|
||||||
if (e.target && e.target.closest && e.target.closest('#scopenet-panel')) return;
|
|
||||||
var uuid = playerAt(e.clientX, e.clientY);
|
|
||||||
if (uuid) select(uuid);
|
|
||||||
else if (selected) select(null);
|
|
||||||
}, true);
|
|
||||||
|
|
||||||
window.addEventListener('message', function (e) {
|
|
||||||
if (e.source === window.parent && e.data && e.data.source === 'scopenet-launcher' && e.data.type === 'hello') { /* acknowledged */ }
|
|
||||||
});
|
|
||||||
|
|
||||||
refresh();
|
|
||||||
setInterval(refresh, 10000);
|
|
||||||
if (embedded) window.parent.postMessage({ source: 'scopenet-map', type: 'ready' }, '*');
|
|
||||||
|
|
||||||
// Exposed for tests.
|
|
||||||
window.__scopenet = { uuidFromId: uuidFromId, esc: esc, hours: hours };
|
|
||||||
})();
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32"><circle cx="16" cy="16" r="14" fill="#f97316" stroke="#ffffff" stroke-width="2"/><path d="M8 10h3l2 10h10l2-7H12" fill="none" stroke="#fff" stroke-width="2.4" stroke-linejoin="round" stroke-linecap="round"/><circle cx="14" cy="24" r="1.8" fill="#fff"/><circle cx="22" cy="24" r="1.8" fill="#fff"/></svg>
|
|
||||||
|
Before Width: | Height: | Size: 387 B |
@@ -1 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32"><circle cx="16" cy="16" r="14" fill="#34d399" stroke="#ffffff" stroke-width="2"/><path d="M16 7l2.6 6.4 6.9.5-5.3 4.5 1.7 6.7L16 21.3 10.1 25l1.7-6.7-5.3-4.5 6.9-.5z" fill="#fff"/></svg>
|
|
||||||
|
Before Width: | Height: | Size: 270 B |
@@ -1 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32"><circle cx="16" cy="16" r="14" fill="#8b6cff" stroke="#ffffff" stroke-width="2"/><circle cx="16" cy="16" r="6" fill="none" stroke="#fff" stroke-width="2.5"/><circle cx="16" cy="16" r="2" fill="#fff"/></svg>
|
|
||||||
|
Before Width: | Height: | Size: 290 B |
@@ -52,7 +52,7 @@ public final class MapModel {
|
|||||||
return ((int) Math.round((r + m) * 255) << 16) | ((int) Math.round((g + m) * 255) << 8) | (int) Math.round((b + m) * 255);
|
return ((int) Math.round((r + m) * 255) << 16) | ((int) Math.round((g + m) * 255) << 8) | (int) Math.round((b + m) * 255);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Which Minecraft dimension a BlueMap map shows, judged from its id and name ("world_nether" -> the nether). */
|
/** Which Minecraft dimension a world id or name means ("world_nether" -> the nether). */
|
||||||
public static String dimensionOf(String id, String name) {
|
public static String dimensionOf(String id, String name) {
|
||||||
List<String> words = Arrays.asList(((id == null ? "" : id) + " " + (name == null ? "" : name)).toLowerCase(Locale.ROOT).split("[^a-z]+"));
|
List<String> words = Arrays.asList(((id == null ? "" : id) + " " + (name == null ? "" : name)).toLowerCase(Locale.ROOT).split("[^a-z]+"));
|
||||||
if (words.contains("nether")) return "minecraft:the_nether";
|
if (words.contains("nether")) return "minecraft:the_nether";
|
||||||
|
|||||||
@@ -0,0 +1,81 @@
|
|||||||
|
package net.scopenet.core.map;
|
||||||
|
|
||||||
|
import com.google.gson.JsonArray;
|
||||||
|
import com.google.gson.JsonObject;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The claims and pins as the JSON the panel's map viewer draws ({@code {"claims":[...],"pins":[...]}}). Details are plain text
|
||||||
|
* lines, never HTML: the viewer sets them as text, so nothing a player types can inject markup.
|
||||||
|
*/
|
||||||
|
public final class MapOverlay {
|
||||||
|
private MapOverlay() {}
|
||||||
|
|
||||||
|
public static JsonObject toJson(MapData data) {
|
||||||
|
JsonArray claims = new JsonArray();
|
||||||
|
for (MapData.ClaimRegion c : data.claims()) {
|
||||||
|
JsonObject o = new JsonObject();
|
||||||
|
o.addProperty("id", c.id());
|
||||||
|
o.addProperty("guild_id", c.guildId());
|
||||||
|
o.addProperty("name", c.name());
|
||||||
|
o.addProperty("tag", c.tag());
|
||||||
|
o.addProperty("icon", c.icon());
|
||||||
|
o.addProperty("color", String.format(Locale.ROOT, "#%06x", c.color() & 0xFFFFFF));
|
||||||
|
o.addProperty("dimension", c.dimension());
|
||||||
|
o.addProperty("chunks", c.chunks());
|
||||||
|
o.addProperty("label_x", c.labelX());
|
||||||
|
o.addProperty("label_z", c.labelZ());
|
||||||
|
o.add("outer", ring(c.outer()));
|
||||||
|
JsonArray holes = new JsonArray();
|
||||||
|
for (ChunkRegions.Ring hole : c.holes()) holes.add(ring(hole));
|
||||||
|
o.add("holes", holes);
|
||||||
|
o.add("lines", lines(c.detail()));
|
||||||
|
claims.add(o);
|
||||||
|
}
|
||||||
|
JsonArray pins = new JsonArray();
|
||||||
|
for (MapData.Pin p : data.pins()) {
|
||||||
|
JsonObject o = new JsonObject();
|
||||||
|
o.addProperty("id", p.id());
|
||||||
|
o.addProperty("kind", p.kind());
|
||||||
|
o.addProperty("label", p.label());
|
||||||
|
o.addProperty("dimension", p.dimension());
|
||||||
|
o.addProperty("x", p.x());
|
||||||
|
o.addProperty("y", p.y());
|
||||||
|
o.addProperty("z", p.z());
|
||||||
|
o.add("lines", lines(p.detail()));
|
||||||
|
pins.add(o);
|
||||||
|
}
|
||||||
|
JsonObject root = new JsonObject();
|
||||||
|
root.add("claims", claims);
|
||||||
|
root.add("pins", pins);
|
||||||
|
return root;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static JsonArray ring(ChunkRegions.Ring ring) {
|
||||||
|
JsonArray out = new JsonArray();
|
||||||
|
for (ChunkRegions.Point p : ring.points()) {
|
||||||
|
JsonArray pair = new JsonArray();
|
||||||
|
pair.add(p.x());
|
||||||
|
pair.add(p.z());
|
||||||
|
out.add(pair);
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Turns the model's escaped HTML into plain lines (the markup there is ours; the text inside is already escaped). */
|
||||||
|
static JsonArray lines(String html) {
|
||||||
|
String text = html == null ? "" : html
|
||||||
|
.replaceAll("(?i)<br\\s*/?>|</div>|</li>|</ul>|<ul[^>]*>", "\n")
|
||||||
|
.replaceAll("<[^>]*>", "");
|
||||||
|
text = text.replace("<", "<").replace(">", ">").replace(""", "\"").replace("'", "'")
|
||||||
|
.replace("·", "·").replace("&", "&");
|
||||||
|
JsonArray out = new JsonArray();
|
||||||
|
for (String line : text.split("\n")) {
|
||||||
|
String t = line.trim();
|
||||||
|
if (!t.isEmpty()) out.add(t.length() > 200 ? t.substring(0, 200) : t);
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,7 +9,7 @@ import net.scopenet.integration.ClaimIndex;
|
|||||||
import java.util.*;
|
import java.util.*;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gathers everything the map needs, from any thread. The map software (BlueMap) asks for a {@link MapData} every
|
* Gathers everything the map needs, from any thread. The map overlay asks for a {@link MapData} every
|
||||||
* few seconds; the market listings are refreshed here on a slower clock so the map never waits on the panel.
|
* few seconds; the market listings are refreshed here on a slower clock so the map never waits on the panel.
|
||||||
*/
|
*/
|
||||||
public final class MapService {
|
public final class MapService {
|
||||||
|
|||||||
@@ -20,7 +20,8 @@ public final class Integration implements AutoCloseable {
|
|||||||
private final AtomicBoolean syncing = new AtomicBoolean();
|
private final AtomicBoolean syncing = new AtomicBoolean();
|
||||||
private volatile boolean closed;
|
private volatile boolean closed;
|
||||||
private volatile Consumer<JsonArray> notificationHandler;
|
private volatile Consumer<JsonArray> notificationHandler;
|
||||||
private volatile LiveMapSync liveMap;
|
private volatile net.scopenet.worldmap.WorldMapSync map;
|
||||||
|
private volatile java.util.function.Supplier<String> mapOverlay;
|
||||||
private boolean registered;
|
private boolean registered;
|
||||||
private JsonObject pending;
|
private JsonObject pending;
|
||||||
private long nextSync;
|
private long nextSync;
|
||||||
@@ -144,17 +145,34 @@ public final class Integration implements AutoCloseable {
|
|||||||
this.notificationHandler = handler;
|
this.notificationHandler = handler;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Start sending player positions for the in-app BlueMap territory view. */
|
/**
|
||||||
public void enableLiveMap() {
|
* Start the SCOPENET Map: render this world's region files to tiles and send them, player positions and the claims
|
||||||
if (closed || liveMap != null) return;
|
* overlay to the panel. {@code worldRoot} is the level folder (e.g. {@code world}); {@code cacheDir} holds the rendered tiles.
|
||||||
liveMap = new LiveMapSync(client);
|
*/
|
||||||
liveMap.start();
|
public void enableMap(java.nio.file.Path worldRoot, java.nio.file.Path cacheDir) {
|
||||||
|
if (closed || map != null) return;
|
||||||
|
map = new net.scopenet.worldmap.WorldMapSync(client, worldRoot, cacheDir, log);
|
||||||
|
if (mapOverlay != null) map.setOverlay(mapOverlay);
|
||||||
|
map.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Where the map gets its claims and pins (JSON text); may be called before or after {@link #enableMap}. */
|
||||||
|
public void setMapOverlay(java.util.function.Supplier<String> overlay) {
|
||||||
|
mapOverlay = overlay;
|
||||||
|
net.scopenet.worldmap.WorldMapSync current = map;
|
||||||
|
if (current != null) current.setOverlay(overlay);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Hand over a snapshot of player positions; call about once a second on the server thread. */
|
/** Hand over a snapshot of player positions; call about once a second on the server thread. */
|
||||||
public void liveMapPlayers(java.util.List<LiveMapSync.Player> players) {
|
public void mapPlayers(java.util.List<net.scopenet.worldmap.WorldMapSync.Player> players) {
|
||||||
LiveMapSync map = liveMap;
|
net.scopenet.worldmap.WorldMapSync current = map;
|
||||||
if (map != null) map.offerPlayers(players);
|
if (current != null) current.offerPlayers(players);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The world was just saved: pick up its changed regions soon. */
|
||||||
|
public void mapSaved() {
|
||||||
|
net.scopenet.worldmap.WorldMapSync current = map;
|
||||||
|
if (current != null) current.requestScan();
|
||||||
}
|
}
|
||||||
|
|
||||||
public ChunkCheckResult checkChunk(String dimension, int chunkX, int chunkZ, UUID uuid) {
|
public ChunkCheckResult checkChunk(String dimension, int chunkX, int chunkZ, UUID uuid) {
|
||||||
@@ -173,8 +191,8 @@ public final class Integration implements AutoCloseable {
|
|||||||
|
|
||||||
@Override public void close() {
|
@Override public void close() {
|
||||||
closed = true;
|
closed = true;
|
||||||
LiveMapSync map = liveMap;
|
net.scopenet.worldmap.WorldMapSync current = map;
|
||||||
if (map != null) map.close();
|
if (current != null) current.close();
|
||||||
client.close();
|
client.close();
|
||||||
loginWorker.shutdownNow();
|
loginWorker.shutdownNow();
|
||||||
activity.leaveAll();
|
activity.leaveAll();
|
||||||
|
|||||||
@@ -1,70 +0,0 @@
|
|||||||
package net.scopenet.integration;
|
|
||||||
|
|
||||||
import com.google.gson.JsonArray;
|
|
||||||
import com.google.gson.JsonObject;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.concurrent.Executors;
|
|
||||||
import java.util.concurrent.ScheduledExecutorService;
|
|
||||||
import java.util.concurrent.TimeUnit;
|
|
||||||
|
|
||||||
/** Sends live player positions to the panel for the in-app BlueMap territory view. */
|
|
||||||
public final class LiveMapSync implements AutoCloseable {
|
|
||||||
public record Player(String uuid, String name, String dimension, double x, double y, double z, float yaw, float pitch) {}
|
|
||||||
|
|
||||||
private final PanelClient client;
|
|
||||||
private final ScheduledExecutorService worker = Executors.newSingleThreadScheduledExecutor(task -> {
|
|
||||||
Thread thread = new Thread(task, "scopenet-positions");
|
|
||||||
thread.setDaemon(true);
|
|
||||||
return thread;
|
|
||||||
});
|
|
||||||
private volatile List<Player> players = List.of();
|
|
||||||
private volatile boolean changed;
|
|
||||||
private volatile boolean closed;
|
|
||||||
private long lastSent;
|
|
||||||
|
|
||||||
public LiveMapSync(PanelClient client) { this.client = client; }
|
|
||||||
|
|
||||||
public void start() { worker.scheduleWithFixedDelay(this::sendSafely, 1, 1, TimeUnit.SECONDS); }
|
|
||||||
|
|
||||||
public void offerPlayers(List<Player> next) {
|
|
||||||
if (!next.equals(players)) {
|
|
||||||
players = List.copyOf(next);
|
|
||||||
changed = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void sendSafely() {
|
|
||||||
if (closed) return;
|
|
||||||
long now = System.nanoTime();
|
|
||||||
if (!changed && now - lastSent < TimeUnit.SECONDS.toNanos(10)) return;
|
|
||||||
JsonArray list = new JsonArray();
|
|
||||||
for (Player p : players) {
|
|
||||||
JsonObject entry = new JsonObject();
|
|
||||||
entry.addProperty("uuid", p.uuid());
|
|
||||||
entry.addProperty("name", p.name());
|
|
||||||
entry.addProperty("dimension", p.dimension());
|
|
||||||
entry.addProperty("x", p.x());
|
|
||||||
entry.addProperty("y", p.y());
|
|
||||||
entry.addProperty("z", p.z());
|
|
||||||
entry.addProperty("yaw", p.yaw());
|
|
||||||
entry.addProperty("pitch", p.pitch());
|
|
||||||
list.add(entry);
|
|
||||||
}
|
|
||||||
JsonObject body = new JsonObject();
|
|
||||||
body.add("players", list);
|
|
||||||
try {
|
|
||||||
client.post("livemap/players", body);
|
|
||||||
changed = false;
|
|
||||||
lastSent = now;
|
|
||||||
} catch (InterruptedException e) {
|
|
||||||
Thread.currentThread().interrupt();
|
|
||||||
} catch (Exception ignored) {
|
|
||||||
// Keep the latest snapshot and retry on the next interval.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override public void close() {
|
|
||||||
closed = true;
|
|
||||||
worker.shutdownNow();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -107,12 +107,12 @@ public record Settings(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Local opt-out for the central live map (the panel also has a per-server switch). */
|
/** Local opt-out for the SCOPENET Map (the panel also has a per-server switch). */
|
||||||
public static boolean liveMapEnabled(Path path) {
|
public static boolean mapEnabled(Path path) {
|
||||||
try (Reader reader = Files.newBufferedReader(path, StandardCharsets.UTF_8)) {
|
try (Reader reader = Files.newBufferedReader(path, StandardCharsets.UTF_8)) {
|
||||||
Properties properties = new Properties();
|
Properties properties = new Properties();
|
||||||
properties.load(reader);
|
properties.load(reader);
|
||||||
return Boolean.parseBoolean(properties.getProperty("livemap.enabled", "true"));
|
return Boolean.parseBoolean(properties.getProperty("map.enabled", properties.getProperty("livemap.enabled", "true")));
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -139,9 +139,9 @@ public record Settings(
|
|||||||
essentials.enabled=true
|
essentials.enabled=true
|
||||||
economy.enabled=true
|
economy.enabled=true
|
||||||
|
|
||||||
# Send world chunks and player positions to the panel's central Live Map.
|
# Draw this world on the SCOPENET Map: tiles, player positions, claims and pins go to the panel.
|
||||||
# It only runs when Live Map is also enabled for this server in the panel.
|
# It only runs when the map is also enabled for this server in the panel.
|
||||||
livemap.enabled=true
|
map.enabled=true
|
||||||
""";
|
""";
|
||||||
Files.writeString(path, defaultProps, StandardCharsets.UTF_8);
|
Files.writeString(path, defaultProps, StandardCharsets.UTF_8);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ public final class WorldMapSync implements AutoCloseable {
|
|||||||
private volatile int maxZoom = 6;
|
private volatile int maxZoom = 6;
|
||||||
private volatile long playerIntervalMs = 2000;
|
private volatile long playerIntervalMs = 2000;
|
||||||
private volatile List<Player> players = List.of();
|
private volatile List<Player> players = List.of();
|
||||||
private volatile String overlay;
|
private volatile java.util.function.Supplier<String> overlay;
|
||||||
private long configAt;
|
private long configAt;
|
||||||
private long coarseUploadAt;
|
private long coarseUploadAt;
|
||||||
private boolean stateDirty;
|
private boolean stateDirty;
|
||||||
@@ -90,9 +90,9 @@ public final class WorldMapSync implements AutoCloseable {
|
|||||||
players = List.copyOf(next);
|
players = List.copyOf(next);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** The claims and pins as JSON ({@code {"claims":[...],"pins":[...]}}); sent when it changes. */
|
/** Where the claims and pins come from ({@code {"claims":[...],"pins":[...]}}); asked every few seconds, sent when it changes. */
|
||||||
public void offerOverlay(String json) {
|
public void setOverlay(java.util.function.Supplier<String> source) {
|
||||||
overlay = json;
|
overlay = source;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** The world was saved: look for changed regions soon. */
|
/** The world was saved: look for changed regions soon. */
|
||||||
@@ -337,7 +337,8 @@ public final class WorldMapSync implements AutoCloseable {
|
|||||||
lastPlayersSent = text;
|
lastPlayersSent = text;
|
||||||
playersSentAt = now;
|
playersSentAt = now;
|
||||||
}
|
}
|
||||||
String ov = overlay;
|
java.util.function.Supplier<String> source = overlay;
|
||||||
|
String ov = source == null || now - overlaySentAt < 10_000 ? null : source.get();
|
||||||
if (ov != null && (!ov.equals(lastOverlaySent) || now - overlaySentAt >= 5 * 60_000) && now - overlaySentAt >= 10_000) {
|
if (ov != null && (!ov.equals(lastOverlaySent) || now - overlaySentAt >= 5 * 60_000) && now - overlaySentAt >= 10_000) {
|
||||||
client.post("map/overlay", JsonParser.parseString(ov).getAsJsonObject());
|
client.post("map/overlay", JsonParser.parseString(ov).getAsJsonObject());
|
||||||
lastOverlaySent = ov;
|
lastOverlaySent = ov;
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
package net.scopenet.core;
|
||||||
|
|
||||||
|
import com.google.gson.JsonObject;
|
||||||
|
import net.scopenet.core.map.*;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
|
||||||
|
class MapOverlayTest {
|
||||||
|
@Test void overlayCarriesShapesAndPlainText() {
|
||||||
|
var ring = new ChunkRegions.Ring(List.of(new ChunkRegions.Point(0, 0), new ChunkRegions.Point(16, 0), new ChunkRegions.Point(16, 16), new ChunkRegions.Point(0, 16)));
|
||||||
|
var claim = new MapData.ClaimRegion("g:o:0", "g", "Iron <Wolves>", "IRN", "", 0x3366CC, "minecraft:overworld", ring, List.of(), 1, 8, 8,
|
||||||
|
"<div class=\"scopenet-detail\"><strong>Iron <Wolves></strong> <span>[IRN]</span><br>1 claimed chunk in this area</div>");
|
||||||
|
var pin = new MapData.Pin("warp-a", "warp", "Warp: a", "minecraft:overworld", 5, 64, 6,
|
||||||
|
"<div class=\"scopenet-detail\"><strong>Warp</strong> a<br><code>/warp a</code></div>");
|
||||||
|
JsonObject json = MapOverlay.toJson(new MapData(List.of(claim), List.of(pin), List.of()));
|
||||||
|
JsonObject c = json.getAsJsonArray("claims").get(0).getAsJsonObject();
|
||||||
|
assertEquals("#3366cc", c.get("color").getAsString());
|
||||||
|
assertEquals(4, c.getAsJsonArray("outer").size());
|
||||||
|
assertEquals("Iron <Wolves> [IRN]", c.getAsJsonArray("lines").get(0).getAsString());
|
||||||
|
assertEquals("1 claimed chunk in this area", c.getAsJsonArray("lines").get(1).getAsString());
|
||||||
|
JsonObject p = json.getAsJsonArray("pins").get(0).getAsJsonObject();
|
||||||
|
assertEquals("warp", p.get("kind").getAsString());
|
||||||
|
assertEquals("/warp a", p.getAsJsonArray("lines").get(1).getAsString());
|
||||||
|
assertFalse(json.toString().contains("<div"), "no markup reaches the viewer");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
package net.scopenet.worldmap;
|
||||||
|
|
||||||
|
import com.google.gson.JsonObject;
|
||||||
|
import com.google.gson.JsonParser;
|
||||||
|
import com.sun.net.httpserver.HttpServer;
|
||||||
|
import net.scopenet.integration.PanelClient;
|
||||||
|
import net.scopenet.integration.Settings;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.io.TempDir;
|
||||||
|
|
||||||
|
import java.net.InetSocketAddress;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.concurrent.*;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
|
||||||
|
/** A fake panel and a generated world: tiles, players and the overlay all arrive, and only once. */
|
||||||
|
class WorldMapSyncTest {
|
||||||
|
@Test void worldsAreRenderedAndUploaded(@TempDir Path dir) throws Exception {
|
||||||
|
byte[] flat = MapRenderTest.chunk(0, List.of("minecraft:air", "minecraft:grass_block"), (x, y, z) -> y < 5 ? 1 : 0);
|
||||||
|
MapRenderTest.region(dir.resolve("world/region"), Map.of(RegionFile.index(1, 1), flat, RegionFile.index(20, 3), flat));
|
||||||
|
|
||||||
|
List<String> tiles = new CopyOnWriteArrayList<>();
|
||||||
|
List<JsonObject> players = new CopyOnWriteArrayList<>();
|
||||||
|
List<JsonObject> overlays = new CopyOnWriteArrayList<>();
|
||||||
|
HttpServer server = HttpServer.create(new InetSocketAddress("127.0.0.1", 0), 0);
|
||||||
|
server.createContext("/api/server/v1/map/config", ex -> reply(ex, "{\"enabled\":true,\"epoch\":3,\"max_zoom\":2,\"player_interval_ms\":500}"));
|
||||||
|
server.createContext("/api/server/v1/map/tiles", ex -> {
|
||||||
|
assertEquals("dim=minecraft:overworld", ex.getRequestURI().getRawQuery());
|
||||||
|
ByteBuffer body = ByteBuffer.wrap(ex.getRequestBody().readAllBytes());
|
||||||
|
while (body.hasRemaining()) {
|
||||||
|
int zoom = body.get() & 0xFF, x = body.getInt(), y = body.getInt(), len = body.getInt();
|
||||||
|
byte[] png = new byte[len];
|
||||||
|
body.get(png);
|
||||||
|
assertEquals((byte) 0x89, png[0]);
|
||||||
|
tiles.add(zoom + "/" + x + "/" + y);
|
||||||
|
}
|
||||||
|
reply(ex, "{\"stored\":1}");
|
||||||
|
});
|
||||||
|
server.createContext("/api/server/v1/map/players", ex -> { players.add(JsonParser.parseString(new String(ex.getRequestBody().readAllBytes(), StandardCharsets.UTF_8)).getAsJsonObject()); reply(ex, "{}"); });
|
||||||
|
server.createContext("/api/server/v1/map/overlay", ex -> { overlays.add(JsonParser.parseString(new String(ex.getRequestBody().readAllBytes(), StandardCharsets.UTF_8)).getAsJsonObject()); reply(ex, "{}"); });
|
||||||
|
server.start();
|
||||||
|
PanelClient client = new PanelClient(Settings.of("http://127.0.0.1:" + server.getAddress().getPort(), "sn_" + "a".repeat(40)));
|
||||||
|
WorldMapSync sync = new WorldMapSync(client, dir.resolve("world"), dir.resolve("cache"), System.err::println);
|
||||||
|
sync.setOverlay(() -> "{\"claims\":[],\"pins\":[{\"id\":\"spawn\"}]}");
|
||||||
|
sync.offerPlayers(List.of(new WorldMapSync.Player("11111111-1111-1111-1111-111111111111", "Steve", "minecraft:overworld", 10.5, 64, 20, 90, 0)));
|
||||||
|
try {
|
||||||
|
sync.start();
|
||||||
|
long deadline = System.currentTimeMillis() + 20_000;
|
||||||
|
while ((tiles.size() < 5 || players.isEmpty() || overlays.isEmpty()) && System.currentTimeMillis() < deadline) Thread.sleep(100);
|
||||||
|
// chunk (1,1) is in tile 0/0/0, chunk (20,3) in 0/1/0; zoom 1 and 2 are built above them
|
||||||
|
assertTrue(tiles.contains("0/0/0"), tiles.toString());
|
||||||
|
assertTrue(tiles.contains("0/1/0"), tiles.toString());
|
||||||
|
assertTrue(tiles.contains("1/0/0"), tiles.toString());
|
||||||
|
assertTrue(tiles.contains("2/0/0"), tiles.toString());
|
||||||
|
assertEquals("Steve", players.get(0).getAsJsonArray("players").get(0).getAsJsonObject().get("name").getAsString());
|
||||||
|
assertEquals("spawn", overlays.get(0).getAsJsonArray("pins").get(0).getAsJsonObject().get("id").getAsString());
|
||||||
|
int sent = tiles.size();
|
||||||
|
Thread.sleep(1500);
|
||||||
|
assertEquals(sent, tiles.size(), "an unchanged world isn't uploaded again");
|
||||||
|
} finally {
|
||||||
|
sync.close();
|
||||||
|
server.stop(0);
|
||||||
|
}
|
||||||
|
// the state survives a restart: a new sync with the same cache sends nothing
|
||||||
|
assertTrue(java.nio.file.Files.isRegularFile(dir.resolve("cache/state.json")));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void reply(com.sun.net.httpserver.HttpExchange ex, String json) throws java.io.IOException {
|
||||||
|
byte[] b = json.getBytes(StandardCharsets.UTF_8);
|
||||||
|
ex.sendResponseHeaders(200, b.length);
|
||||||
|
ex.getResponseBody().write(b);
|
||||||
|
ex.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,7 +16,6 @@ dependencies {
|
|||||||
implementation project(':common')
|
implementation project(':common')
|
||||||
if (features) {
|
if (features) {
|
||||||
modImplementation 'net.fabricmc.fabric-api:fabric-api:0.92.2+1.20.1'
|
modImplementation 'net.fabricmc.fabric-api:fabric-api:0.92.2+1.20.1'
|
||||||
implementation project(':bluemap')
|
|
||||||
// Optional at runtime: present only when the LuckPerms mod is installed.
|
// Optional at runtime: present only when the LuckPerms mod is installed.
|
||||||
compileOnly 'net.luckperms:api:5.4'
|
compileOnly 'net.luckperms:api:5.4'
|
||||||
}
|
}
|
||||||
@@ -32,6 +31,5 @@ processResources {
|
|||||||
jar {
|
jar {
|
||||||
archiveBaseName = "scopenet-fabric-${mc}"
|
archiveBaseName = "scopenet-fabric-${mc}"
|
||||||
from project(':common').sourceSets.main.output
|
from project(':common').sourceSets.main.output
|
||||||
if (features) from project(':bluemap').sourceSets.main.output
|
|
||||||
}
|
}
|
||||||
base.archivesName = "scopenet-fabric-${mc}"
|
base.archivesName = "scopenet-fabric-${mc}"
|
||||||
@@ -7,5 +7,5 @@
|
|||||||
"entrypoints": { "server": ["net.scopenet.fabric.ScopenetFabric"] },
|
"entrypoints": { "server": ["net.scopenet.fabric.ScopenetFabric"] },
|
||||||
"mixins": ["scopenet.mixins.json"],
|
"mixins": ["scopenet.mixins.json"],
|
||||||
"depends": { "fabricloader": ">=0.16.10", "minecraft": "${mc}", "java": ">=${java}" },
|
"depends": { "fabricloader": ">=0.16.10", "minecraft": "${mc}", "java": ">=${java}" },
|
||||||
"suggests": { "fabric-api": "*", "luckperms": "*", "placeholder-api": "*", "bluemap": "*" }
|
"suggests": { "fabric-api": "*", "luckperms": "*", "placeholder-api": "*" }
|
||||||
}
|
}
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
package net.scopenet.fabric.features;
|
|
||||||
|
|
||||||
import net.scopenet.bluemap.MapAddon;
|
|
||||||
import net.scopenet.core.map.MapData;
|
|
||||||
import net.scopenet.core.map.MapService;
|
|
||||||
|
|
||||||
import java.util.logging.Logger;
|
|
||||||
|
|
||||||
/** Kept apart so BlueMap's classes are only loaded when BlueMap is installed. */
|
|
||||||
final class BlueMapHook {
|
|
||||||
private BlueMapHook() {}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Registered while the mod initialises (BlueMap reads its script list when it starts), so the data comes from
|
|
||||||
* {@code service}, which only exists once the server is running: until then the map is simply empty.
|
|
||||||
*/
|
|
||||||
static Object start(java.util.function.Supplier<MapService> service, Logger log) {
|
|
||||||
return MapAddon.start(new MapAddon.Feed() {
|
|
||||||
@Override public MapData snapshot() { MapService s = service.get(); return s == null ? MapData.empty() : s.snapshot(); }
|
|
||||||
@Override public com.google.gson.JsonObject playersJson() {
|
|
||||||
MapService s = service.get();
|
|
||||||
return s == null ? net.scopenet.core.map.MapModel.playersJson("", java.util.List.of(), System.currentTimeMillis()) : s.playersJson();
|
|
||||||
}
|
|
||||||
}, log);
|
|
||||||
}
|
|
||||||
|
|
||||||
static void stop(Object handle) { ((MapAddon) handle).stop(); }
|
|
||||||
}
|
|
||||||
@@ -56,17 +56,12 @@ public final class FabricFeatures implements Runnable {
|
|||||||
private FabricConfig config;
|
private FabricConfig config;
|
||||||
private ActionPoller poller;
|
private ActionPoller poller;
|
||||||
private MapService map;
|
private MapService map;
|
||||||
private Object blueMap;
|
|
||||||
private int ticks;
|
private int ticks;
|
||||||
private String placeholderStatus = "off";
|
private String placeholderStatus = "off";
|
||||||
|
|
||||||
CommandSet commandSet() { return commands; }
|
CommandSet commandSet() { return commands; }
|
||||||
|
|
||||||
@Override public void run() {
|
@Override public void run() {
|
||||||
// Hook into BlueMap early: it reads the list of web scripts when it starts.
|
|
||||||
if (FabricConfig.load().bool("bluemap.enabled", true) && net.fabricmc.loader.api.FabricLoader.getInstance().isModLoaded("bluemap")) {
|
|
||||||
try { blueMap = BlueMapHook.start(() -> map, LOG); } catch (Throwable t) { LOG.warning("BlueMap addon could not start: " + t); }
|
|
||||||
}
|
|
||||||
ServerLifecycleEvents.SERVER_STARTED.register(this::started);
|
ServerLifecycleEvents.SERVER_STARTED.register(this::started);
|
||||||
ServerLifecycleEvents.SERVER_STOPPING.register(s -> stopping());
|
ServerLifecycleEvents.SERVER_STOPPING.register(s -> stopping());
|
||||||
ServerTickEvents.END_SERVER_TICK.register(s -> tick());
|
ServerTickEvents.END_SERVER_TICK.register(s -> tick());
|
||||||
@@ -116,10 +111,11 @@ public final class FabricFeatures implements Runnable {
|
|||||||
integration.onNotifications(events -> link.onPanelEvents(events));
|
integration.onNotifications(events -> link.onPanelEvents(events));
|
||||||
Bridge.claimBypass = p -> perms.has(p, "scopenet.claims.bypass");
|
Bridge.claimBypass = p -> perms.has(p, "scopenet.claims.bypass");
|
||||||
|
|
||||||
// Actions from the map, and the BlueMap addon (claims, pins, player panel) when BlueMap is installed.
|
// Actions from the map, and the claims and pins the SCOPENET Map draws.
|
||||||
poller = new ActionPoller(env, (from, to) -> commands.essentials.tpa(from, new String[]{to.name()}));
|
poller = new ActionPoller(env, (from, to) -> commands.essentials.tpa(from, new String[]{to.name()}));
|
||||||
map = new MapService(env, integration.client().claims(), commands.mapSource(), cache, integration.settings().panel().toString(),
|
map = new MapService(env, integration.client().claims(), commands.mapSource(), cache, integration.settings().panel().toString(),
|
||||||
config.bool("bluemap.show_homes", false), s.getMotd());
|
config.bool("map.show_homes", false), s.getMotd());
|
||||||
|
integration.setMapOverlay(() -> { MapService current = map; return current == null ? "{\"claims\":[],\"pins\":[]}" : net.scopenet.core.map.MapOverlay.toJson(current.snapshot()).toString(); });
|
||||||
registerCommands(s.getCommands().getDispatcher());
|
registerCommands(s.getCommands().getDispatcher());
|
||||||
registerPlaceholders();
|
registerPlaceholders();
|
||||||
ScopenetEconomy.install(new ScopenetEconomy(env.panel, r -> platform.runAsync(r)));
|
ScopenetEconomy.install(new ScopenetEconomy(env.panel, r -> platform.runAsync(r)));
|
||||||
@@ -130,7 +126,6 @@ public final class FabricFeatures implements Runnable {
|
|||||||
private void stopping() {
|
private void stopping() {
|
||||||
ScopenetEconomy.uninstall();
|
ScopenetEconomy.uninstall();
|
||||||
Bridge.claimBypass = p -> false;
|
Bridge.claimBypass = p -> false;
|
||||||
if (blueMap != null) { try { BlueMapHook.stop(blueMap); } catch (Throwable ignored) { } blueMap = null; }
|
|
||||||
if (platform != null) platform.shutdown();
|
if (platform != null) platform.shutdown();
|
||||||
commands = null;
|
commands = null;
|
||||||
map = null;
|
map = null;
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ public final class Bridge {
|
|||||||
}));
|
}));
|
||||||
integration.startClaimSync();
|
integration.startClaimSync();
|
||||||
Path config = Path.of("config/scopenet.properties");
|
Path config = Path.of("config/scopenet.properties");
|
||||||
if (Settings.liveMapEnabled(config)) integration.enableLiveMap();
|
if (Settings.mapEnabled(config)) integration.enableMap(server.getWorldPath(net.minecraft.world.level.storage.LevelResource.ROOT), Path.of("config/scopenet/map"));
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
// Throwing aborts startup; silently disabling an auth mod is unsafe.
|
// Throwing aborts startup; silently disabling an auth mod is unsafe.
|
||||||
throw new IllegalStateException("SCOPENET cannot start: " + e.getMessage(), e);
|
throw new IllegalStateException("SCOPENET cannot start: " + e.getMessage(), e);
|
||||||
@@ -71,13 +71,13 @@ public final class Bridge {
|
|||||||
current.tick();
|
current.tick();
|
||||||
if (++mapTicks >= 20) {
|
if (++mapTicks >= 20) {
|
||||||
mapTicks = 0;
|
mapTicks = 0;
|
||||||
List<LiveMapSync.Player> positions = new ArrayList<>();
|
List<net.scopenet.worldmap.WorldMapSync.Player> positions = new ArrayList<>();
|
||||||
for (ServerPlayer player : server.getPlayerList().getPlayers()) {
|
for (ServerPlayer player : server.getPlayerList().getPlayers()) {
|
||||||
positions.add(new LiveMapSync.Player(player.getUUID().toString(), player.getName().getString(), Compat.dimension(player),
|
positions.add(new net.scopenet.worldmap.WorldMapSync.Player(player.getUUID().toString(), player.getName().getString(), Compat.dimension(player),
|
||||||
Math.round(player.getX() * 10) / 10.0, Math.round(player.getY() * 10) / 10.0, Math.round(player.getZ() * 10) / 10.0,
|
Math.round(player.getX() * 10) / 10.0, Math.round(player.getY() * 10) / 10.0, Math.round(player.getZ() * 10) / 10.0,
|
||||||
Math.round(player.getYRot()), Math.round(player.getXRot())));
|
Math.round(player.getYRot()), Math.round(player.getXRot())));
|
||||||
}
|
}
|
||||||
current.liveMapPlayers(positions);
|
current.mapPlayers(positions);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ repositories {
|
|||||||
dependencies {
|
dependencies {
|
||||||
implementation project(':common')
|
implementation project(':common')
|
||||||
implementation project(':api')
|
implementation project(':api')
|
||||||
implementation project(':bluemap')
|
|
||||||
implementation 'com.google.code.gson:gson:2.10.1'
|
implementation 'com.google.code.gson:gson:2.10.1'
|
||||||
compileOnly 'org.spigotmc:spigot-api:1.20.1-R0.1-SNAPSHOT'
|
compileOnly 'org.spigotmc:spigot-api:1.20.1-R0.1-SNAPSHOT'
|
||||||
// Optional integrations: the plugin detects these at runtime and works without them.
|
// Optional integrations: the plugin detects these at runtime and works without them.
|
||||||
@@ -23,7 +22,6 @@ jar {
|
|||||||
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
|
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
|
||||||
from project(':common').sourceSets.main.output
|
from project(':common').sourceSets.main.output
|
||||||
from project(':api').sourceSets.main.output
|
from project(':api').sourceSets.main.output
|
||||||
from project(':bluemap').sourceSets.main.output
|
from { configurations.runtimeClasspath.findAll { it.name.endsWith('.jar') && !it.name.startsWith('common-') && !it.name.startsWith('api-') }.collect { zipTree(it) } }
|
||||||
from { configurations.runtimeClasspath.findAll { it.name.endsWith('.jar') && !it.name.startsWith('common-') && !it.name.startsWith('api-') && !it.name.startsWith('bluemap-') }.collect { zipTree(it) } }
|
|
||||||
exclude 'META-INF/*.SF', 'META-INF/*.RSA', 'META-INF/*.DSA'
|
exclude 'META-INF/*.SF', 'META-INF/*.RSA', 'META-INF/*.DSA'
|
||||||
}
|
}
|
||||||
@@ -31,7 +31,6 @@ final class ClientLinkBridge implements PluginMessageListener {
|
|||||||
private final ShopPoints shops;
|
private final ShopPoints shops;
|
||||||
private ActionPoller poller;
|
private ActionPoller poller;
|
||||||
private MapService mapService;
|
private MapService mapService;
|
||||||
private Object blueMap;
|
|
||||||
private EconomyHandler economy;
|
private EconomyHandler economy;
|
||||||
|
|
||||||
ClientLinkBridge(ScopenetPlugin plugin, Integration integration) {
|
ClientLinkBridge(ScopenetPlugin plugin, Integration integration) {
|
||||||
@@ -64,7 +63,7 @@ final class ClientLinkBridge implements PluginMessageListener {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Shop points, actions from the map, and the BlueMap addon. Called once the command handlers exist; any of them may be
|
* Shop points, actions from the map, and the claims and pins for the SCOPENET Map. Called once the command handlers exist; any of them may be
|
||||||
* null when that system is switched off.
|
* null when that system is switched off.
|
||||||
*/
|
*/
|
||||||
void startMap(EssentialsHandler essentials, GuildHandler guilds, EconomyHandler economy) {
|
void startMap(EssentialsHandler essentials, GuildHandler guilds, EconomyHandler economy) {
|
||||||
@@ -81,11 +80,9 @@ final class ClientLinkBridge implements PluginMessageListener {
|
|||||||
@Override public Collection<ShopPoints.Shop> shops() { return shops.all(); }
|
@Override public Collection<ShopPoints.Shop> shops() { return shops.all(); }
|
||||||
};
|
};
|
||||||
mapService = new MapService(env, integration.client().claims(), source, cache, integration.settings().panel().toString(),
|
mapService = new MapService(env, integration.client().claims(), source, cache, integration.settings().panel().toString(),
|
||||||
plugin.getConfig().getBoolean("bluemap.show-homes", false), Bukkit.getServer().getName());
|
plugin.getConfig().getBoolean("map.show-homes", false), Bukkit.getServer().getName());
|
||||||
|
integration.setMapOverlay(() -> net.scopenet.core.map.MapOverlay.toJson(mapService.snapshot()).toString());
|
||||||
Bukkit.getScheduler().runTaskTimer(plugin, () -> { poller.tick(); mapService.tick(); }, 60, 20);
|
Bukkit.getScheduler().runTaskTimer(plugin, () -> { poller.tick(); mapService.tick(); }, 60, 20);
|
||||||
if (plugin.getConfig().getBoolean("bluemap.enabled", true) && Bukkit.getPluginManager().getPlugin("BlueMap") != null) {
|
|
||||||
blueMap = BlueMapHook.start(mapService, plugin.getLogger());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** /market point add|remove|list, on the block the player is looking at (or standing on). */
|
/** /market point add|remove|list, on the block the player is looking at (or standing on). */
|
||||||
@@ -113,20 +110,7 @@ final class ClientLinkBridge implements PluginMessageListener {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Kept apart so BlueMap's classes are only loaded when BlueMap is installed. */
|
|
||||||
private static final class BlueMapHook {
|
|
||||||
static Object start(MapService service, Logger log) {
|
|
||||||
return net.scopenet.bluemap.MapAddon.start(new net.scopenet.bluemap.MapAddon.Feed() {
|
|
||||||
@Override public net.scopenet.core.map.MapData snapshot() { return service.snapshot(); }
|
|
||||||
@Override public com.google.gson.JsonObject playersJson() { return service.playersJson(); }
|
|
||||||
}, log);
|
|
||||||
}
|
|
||||||
|
|
||||||
static void stop(Object handle) { ((net.scopenet.bluemap.MapAddon) handle).stop(); }
|
|
||||||
}
|
|
||||||
|
|
||||||
void stop() {
|
void stop() {
|
||||||
if (blueMap != null) { BlueMapHook.stop(blueMap); blueMap = null; }
|
|
||||||
Bukkit.getMessenger().unregisterOutgoingPluginChannel(plugin, Wire.S2C);
|
Bukkit.getMessenger().unregisterOutgoingPluginChannel(plugin, Wire.S2C);
|
||||||
Bukkit.getMessenger().unregisterIncomingPluginChannel(plugin, Wire.C2S, this);
|
Bukkit.getMessenger().unregisterIncomingPluginChannel(plugin, Wire.C2S, this);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,10 +51,19 @@ public final class ScopenetPlugin extends JavaPlugin implements Listener {
|
|||||||
// Claim protection runs from a local copy of the panel's claims, refreshed in the background.
|
// Claim protection runs from a local copy of the panel's claims, refreshed in the background.
|
||||||
integration.startClaimSync();
|
integration.startClaimSync();
|
||||||
|
|
||||||
// Live positions center the in-app BlueMap claim view on the player.
|
// The SCOPENET Map: renders the world to tiles and sends them, positions, claims and pins to the panel.
|
||||||
if (getConfig().getBoolean("livemap.enabled", true)) {
|
// Started on the first tick so the worlds exist.
|
||||||
integration.enableLiveMap();
|
if (getConfig().getBoolean("map.enabled", getConfig().getBoolean("livemap.enabled", true))) {
|
||||||
|
Bukkit.getScheduler().runTask(this, () -> {
|
||||||
|
if (integration == null || Bukkit.getWorlds().isEmpty()) return;
|
||||||
|
integration.enableMap(Bukkit.getWorlds().get(0).getWorldFolder().toPath(), getDataFolder().toPath().resolve("map"));
|
||||||
Bukkit.getScheduler().runTaskTimer(this, this::snapshotPlayers, 40, 20);
|
Bukkit.getScheduler().runTaskTimer(this, this::snapshotPlayers, 40, 20);
|
||||||
|
});
|
||||||
|
getServer().getPluginManager().registerEvents(new org.bukkit.event.Listener() {
|
||||||
|
@org.bukkit.event.EventHandler public void onSave(org.bukkit.event.world.WorldSaveEvent event) {
|
||||||
|
if (integration != null) integration.mapSaved();
|
||||||
|
}
|
||||||
|
}, this);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Developer API: other plugins use SCOPENET through ScopenetApiProvider / the services manager,
|
// Developer API: other plugins use SCOPENET through ScopenetApiProvider / the services manager,
|
||||||
@@ -114,7 +123,7 @@ public final class ScopenetPlugin extends JavaPlugin implements Listener {
|
|||||||
bindCommand("autoclaim", guilds);
|
bindCommand("autoclaim", guilds);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Physical shops, actions from the map, and (when BlueMap is installed) claims, pins and the player panel on it.
|
// Physical shops, actions from the map, and the claims and pins the SCOPENET Map draws.
|
||||||
clientLink.startMap(essentialsHandler, guildHandler, economy);
|
clientLink.startMap(essentialsHandler, guildHandler, economy);
|
||||||
|
|
||||||
// Reloading an auth plugin cannot silently admit existing sessions.
|
// Reloading an auth plugin cannot silently admit existing sessions.
|
||||||
@@ -160,7 +169,7 @@ public final class ScopenetPlugin extends JavaPlugin implements Listener {
|
|||||||
|
|
||||||
private void snapshotPlayers() {
|
private void snapshotPlayers() {
|
||||||
if (integration == null) return;
|
if (integration == null) return;
|
||||||
java.util.List<LiveMapSync.Player> list = new java.util.ArrayList<>();
|
java.util.List<net.scopenet.worldmap.WorldMapSync.Player> list = new java.util.ArrayList<>();
|
||||||
for (Player p : Bukkit.getOnlinePlayers()) {
|
for (Player p : Bukkit.getOnlinePlayers()) {
|
||||||
Location at = p.getLocation();
|
Location at = p.getLocation();
|
||||||
String dim = switch (at.getWorld().getEnvironment()) {
|
String dim = switch (at.getWorld().getEnvironment()) {
|
||||||
@@ -169,11 +178,11 @@ public final class ScopenetPlugin extends JavaPlugin implements Listener {
|
|||||||
default -> "minecraft:overworld";
|
default -> "minecraft:overworld";
|
||||||
};
|
};
|
||||||
// Rounded so an idle roster doesn't look "changed" on every tick.
|
// Rounded so an idle roster doesn't look "changed" on every tick.
|
||||||
list.add(new LiveMapSync.Player(p.getUniqueId().toString(), p.getName(), dim,
|
list.add(new net.scopenet.worldmap.WorldMapSync.Player(p.getUniqueId().toString(), p.getName(), dim,
|
||||||
Math.round(at.getX() * 10) / 10.0, Math.round(at.getY() * 10) / 10.0, Math.round(at.getZ() * 10) / 10.0,
|
Math.round(at.getX() * 10) / 10.0, Math.round(at.getY() * 10) / 10.0, Math.round(at.getZ() * 10) / 10.0,
|
||||||
Math.round(at.getYaw()), Math.round(at.getPitch())));
|
Math.round(at.getYaw()), Math.round(at.getPitch())));
|
||||||
}
|
}
|
||||||
integration.liveMapPlayers(list);
|
integration.mapPlayers(list);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static String dimension(World world) {
|
public static String dimension(World world) {
|
||||||
|
|||||||
@@ -51,10 +51,13 @@ essentials:
|
|||||||
# RTP search radius
|
# RTP search radius
|
||||||
rtp-radius: 2500
|
rtp-radius: 2500
|
||||||
|
|
||||||
livemap:
|
map:
|
||||||
# Send live player positions to the panel for the launcher BlueMap claim view.
|
# The SCOPENET Map: this server renders its world to map tiles in the background and sends only what changed,
|
||||||
# Configure BlueMap's public address and enable live positions in the panel.
|
# with player positions, guild claims and pins, to the panel. Players see it in the launcher; admins in the panel.
|
||||||
|
# It also needs the map switched on for this server in the panel (it is by default).
|
||||||
enabled: true
|
enabled: true
|
||||||
|
# Show every player's homes as pins. Off by default: homes are private.
|
||||||
|
show-homes: false
|
||||||
|
|
||||||
economy:
|
economy:
|
||||||
# Server Economy commands (/balance, /pay, /baltop, /shop, /sell, /market, /orders, /trade, /transactions)
|
# Server Economy commands (/balance, /pay, /baltop, /shop, /sell, /market, /orders, /trade, /transactions)
|
||||||
@@ -93,17 +96,6 @@ integrations:
|
|||||||
enabled: true
|
enabled: true
|
||||||
report-interval-seconds: 30
|
report-interval-seconds: 30
|
||||||
|
|
||||||
# ==============================================================================
|
|
||||||
# BlueMap
|
|
||||||
# With BlueMap installed, SCOPENET adds guild claims (coloured, with the guild's icon and name), spawn, warps, guild
|
|
||||||
# homes, markets and shops to the 3D map, and a player panel when you click someone. Put the map's web address in the
|
|
||||||
# panel (Servers -> this server -> Map address) and players get a Live Map button in the launcher.
|
|
||||||
# ==============================================================================
|
|
||||||
bluemap:
|
|
||||||
enabled: true
|
|
||||||
# Show every player's homes as pins. Off by default: homes are private, and the map is public to anyone with the link.
|
|
||||||
show-homes: false
|
|
||||||
|
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
# Client mod link
|
# Client mod link
|
||||||
# Sends balance, level, guild and claim info to players who have the optional SCOPENET
|
# Sends balance, level, guild and claim info to players who have the optional SCOPENET
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ main: net.scopenet.paper.ScopenetPlugin
|
|||||||
api-version: '1.20'
|
api-version: '1.20'
|
||||||
description: SCOPENET private authentication, access control and activity reporting.
|
description: SCOPENET private authentication, access control and activity reporting.
|
||||||
load: STARTUP
|
load: STARTUP
|
||||||
softdepend: [LuckPerms, PlaceholderAPI, Vault, CoreProtect, WorldGuard, WorldEdit, spark, BlueMap]
|
softdepend: [LuckPerms, PlaceholderAPI, Vault, CoreProtect, WorldGuard, WorldEdit, spark]
|
||||||
|
|
||||||
commands:
|
commands:
|
||||||
scopenet:
|
scopenet:
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ pluginManagement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
rootProject.name = 'scopenet-integrations'
|
rootProject.name = 'scopenet-integrations'
|
||||||
include 'common', 'api', 'paper', 'bluemap'
|
include 'common', 'api', 'paper'
|
||||||
// Build loaders separately: Forge and Loom use different Gradle generations.
|
// Build loaders separately: Forge and Loom use different Gradle generations.
|
||||||
if (providers.gradleProperty('loader').orNull == 'fabric') include 'fabric'
|
if (providers.gradleProperty('loader').orNull == 'fabric') include 'fabric'
|
||||||
if (providers.gradleProperty('loader').orNull == 'forge') include 'forge'
|
if (providers.gradleProperty('loader').orNull == 'forge') include 'forge'
|
||||||
|
|||||||