diff --git a/integrations/bluemap/build.gradle b/integrations/bluemap/build.gradle deleted file mode 100644 index 1d09c23..0000000 --- a/integrations/bluemap/build.gradle +++ /dev/null @@ -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' -} diff --git a/integrations/bluemap/src/main/java/net/scopenet/bluemap/MapAddon.java b/integrations/bluemap/src/main/java/net/scopenet/bluemap/MapAddon.java deleted file mode 100644 index aa8b107..0000000 --- a/integrations/bluemap/src/main/java/net/scopenet/bluemap/MapAddon.java +++ /dev/null @@ -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: - * - * 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 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 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 points = new ArrayList<>(); - for (ChunkRegions.Point p : ring.points()) points.add(new Vector2d(p.x(), p.z())); - return new Shape(points); - } - - private static Map claimMarkers(MapData data, String dimension) { - Map 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 guildMarkers(MapData data, String dimension) { - Map 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 pinMarkers(MapData data, String dimension, boolean shops) { - Map 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; - } -} diff --git a/integrations/bluemap/src/main/resources/assets/scopenet/guild-home.svg b/integrations/bluemap/src/main/resources/assets/scopenet/guild-home.svg deleted file mode 100644 index bb2f090..0000000 --- a/integrations/bluemap/src/main/resources/assets/scopenet/guild-home.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/integrations/bluemap/src/main/resources/assets/scopenet/guild.svg b/integrations/bluemap/src/main/resources/assets/scopenet/guild.svg deleted file mode 100644 index 2028d40..0000000 --- a/integrations/bluemap/src/main/resources/assets/scopenet/guild.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/integrations/bluemap/src/main/resources/assets/scopenet/home.svg b/integrations/bluemap/src/main/resources/assets/scopenet/home.svg deleted file mode 100644 index cdb1214..0000000 --- a/integrations/bluemap/src/main/resources/assets/scopenet/home.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/integrations/bluemap/src/main/resources/assets/scopenet/market.svg b/integrations/bluemap/src/main/resources/assets/scopenet/market.svg deleted file mode 100644 index 5ad9750..0000000 --- a/integrations/bluemap/src/main/resources/assets/scopenet/market.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/integrations/bluemap/src/main/resources/assets/scopenet/scopenet.css b/integrations/bluemap/src/main/resources/assets/scopenet/scopenet.css deleted file mode 100644 index ee98a19..0000000 --- a/integrations/bluemap/src/main/resources/assets/scopenet/scopenet.css +++ /dev/null @@ -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; } diff --git a/integrations/bluemap/src/main/resources/assets/scopenet/scopenet.js b/integrations/bluemap/src/main/resources/assets/scopenet/scopenet.js deleted file mode 100644 index 3f93b23..0000000 --- a/integrations/bluemap/src/main/resources/assets/scopenet/scopenet.js +++ /dev/null @@ -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-". */ - 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 = - '' + - '
' + - '

' + esc(name) + '

' + (p.title ? '
' + esc(p.title) + '
' : '') + '
' + - '
' + - 'Level ' + esc(p.level || 1) + '' + - (p.guild ? '[' + esc(p.guildTag) + '] ' + esc(p.guild) + '' : '') + - (p.rank ? '' + esc(p.rank) + '' : '') + '
' + - '
' + - '
Playtime' + esc(hours(p.playtime)) + '
' + - '
Server level' + esc(p.serverLevel || 1) + '
' + - (p.guildRole ? '
Guild role' + esc(p.guildRole) + '
' : '') + '
' + - '

Open this map in the SCOPENET launcher to send a teleport request or message, or to invite ' + esc(name) + ' to your guild.

'; - 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 }; -})(); diff --git a/integrations/bluemap/src/main/resources/assets/scopenet/shop.svg b/integrations/bluemap/src/main/resources/assets/scopenet/shop.svg deleted file mode 100644 index 9e1f3f0..0000000 --- a/integrations/bluemap/src/main/resources/assets/scopenet/shop.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/integrations/bluemap/src/main/resources/assets/scopenet/spawn.svg b/integrations/bluemap/src/main/resources/assets/scopenet/spawn.svg deleted file mode 100644 index 63c3e83..0000000 --- a/integrations/bluemap/src/main/resources/assets/scopenet/spawn.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/integrations/bluemap/src/main/resources/assets/scopenet/warp.svg b/integrations/bluemap/src/main/resources/assets/scopenet/warp.svg deleted file mode 100644 index 3feb25d..0000000 --- a/integrations/bluemap/src/main/resources/assets/scopenet/warp.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/integrations/common/src/main/java/net/scopenet/core/map/MapModel.java b/integrations/common/src/main/java/net/scopenet/core/map/MapModel.java index dfdb3fe..935a00e 100644 --- a/integrations/common/src/main/java/net/scopenet/core/map/MapModel.java +++ b/integrations/common/src/main/java/net/scopenet/core/map/MapModel.java @@ -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); } - /** 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) { List words = Arrays.asList(((id == null ? "" : id) + " " + (name == null ? "" : name)).toLowerCase(Locale.ROOT).split("[^a-z]+")); if (words.contains("nether")) return "minecraft:the_nether"; diff --git a/integrations/common/src/main/java/net/scopenet/core/map/MapOverlay.java b/integrations/common/src/main/java/net/scopenet/core/map/MapOverlay.java new file mode 100644 index 0000000..4fb9416 --- /dev/null +++ b/integrations/common/src/main/java/net/scopenet/core/map/MapOverlay.java @@ -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)||||]*>", "\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; + } +} diff --git a/integrations/common/src/main/java/net/scopenet/core/map/MapService.java b/integrations/common/src/main/java/net/scopenet/core/map/MapService.java index 18fc3b8..46e6769 100644 --- a/integrations/common/src/main/java/net/scopenet/core/map/MapService.java +++ b/integrations/common/src/main/java/net/scopenet/core/map/MapService.java @@ -9,7 +9,7 @@ import net.scopenet.integration.ClaimIndex; 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. */ public final class MapService { diff --git a/integrations/common/src/main/java/net/scopenet/integration/Integration.java b/integrations/common/src/main/java/net/scopenet/integration/Integration.java index 6c144fc..3551d11 100644 --- a/integrations/common/src/main/java/net/scopenet/integration/Integration.java +++ b/integrations/common/src/main/java/net/scopenet/integration/Integration.java @@ -20,7 +20,8 @@ public final class Integration implements AutoCloseable { private final AtomicBoolean syncing = new AtomicBoolean(); private volatile boolean closed; private volatile Consumer notificationHandler; - private volatile LiveMapSync liveMap; + private volatile net.scopenet.worldmap.WorldMapSync map; + private volatile java.util.function.Supplier mapOverlay; private boolean registered; private JsonObject pending; private long nextSync; @@ -144,17 +145,34 @@ public final class Integration implements AutoCloseable { this.notificationHandler = handler; } - /** Start sending player positions for the in-app BlueMap territory view. */ - public void enableLiveMap() { - if (closed || liveMap != null) return; - liveMap = new LiveMapSync(client); - liveMap.start(); + /** + * Start the SCOPENET Map: render this world's region files to tiles and send them, player positions and the claims + * overlay to the panel. {@code worldRoot} is the level folder (e.g. {@code world}); {@code cacheDir} holds the rendered tiles. + */ + 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 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. */ - public void liveMapPlayers(java.util.List players) { - LiveMapSync map = liveMap; - if (map != null) map.offerPlayers(players); + public void mapPlayers(java.util.List players) { + net.scopenet.worldmap.WorldMapSync current = map; + 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) { @@ -173,8 +191,8 @@ public final class Integration implements AutoCloseable { @Override public void close() { closed = true; - LiveMapSync map = liveMap; - if (map != null) map.close(); + net.scopenet.worldmap.WorldMapSync current = map; + if (current != null) current.close(); client.close(); loginWorker.shutdownNow(); activity.leaveAll(); diff --git a/integrations/common/src/main/java/net/scopenet/integration/LiveMapSync.java b/integrations/common/src/main/java/net/scopenet/integration/LiveMapSync.java deleted file mode 100644 index 0657a8d..0000000 --- a/integrations/common/src/main/java/net/scopenet/integration/LiveMapSync.java +++ /dev/null @@ -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 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 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(); - } -} diff --git a/integrations/common/src/main/java/net/scopenet/integration/Settings.java b/integrations/common/src/main/java/net/scopenet/integration/Settings.java index dae69fd..3e88541 100644 --- a/integrations/common/src/main/java/net/scopenet/integration/Settings.java +++ b/integrations/common/src/main/java/net/scopenet/integration/Settings.java @@ -107,12 +107,12 @@ public record Settings( ); } - /** Local opt-out for the central live map (the panel also has a per-server switch). */ - public static boolean liveMapEnabled(Path path) { + /** Local opt-out for the SCOPENET Map (the panel also has a per-server switch). */ + public static boolean mapEnabled(Path path) { try (Reader reader = Files.newBufferedReader(path, StandardCharsets.UTF_8)) { Properties properties = new Properties(); 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) { return true; } @@ -139,9 +139,9 @@ public record Settings( essentials.enabled=true economy.enabled=true - # Send world chunks and player positions to the panel's central Live Map. - # It only runs when Live Map is also enabled for this server in the panel. - livemap.enabled=true + # Draw this world on the SCOPENET Map: tiles, player positions, claims and pins go to the panel. + # It only runs when the map is also enabled for this server in the panel. + map.enabled=true """; Files.writeString(path, defaultProps, StandardCharsets.UTF_8); } diff --git a/integrations/common/src/main/java/net/scopenet/worldmap/WorldMapSync.java b/integrations/common/src/main/java/net/scopenet/worldmap/WorldMapSync.java index 4fe29e8..825aa07 100644 --- a/integrations/common/src/main/java/net/scopenet/worldmap/WorldMapSync.java +++ b/integrations/common/src/main/java/net/scopenet/worldmap/WorldMapSync.java @@ -59,7 +59,7 @@ public final class WorldMapSync implements AutoCloseable { private volatile int maxZoom = 6; private volatile long playerIntervalMs = 2000; private volatile List players = List.of(); - private volatile String overlay; + private volatile java.util.function.Supplier overlay; private long configAt; private long coarseUploadAt; private boolean stateDirty; @@ -90,9 +90,9 @@ public final class WorldMapSync implements AutoCloseable { players = List.copyOf(next); } - /** The claims and pins as JSON ({@code {"claims":[...],"pins":[...]}}); sent when it changes. */ - public void offerOverlay(String json) { - overlay = json; + /** Where the claims and pins come from ({@code {"claims":[...],"pins":[...]}}); asked every few seconds, sent when it changes. */ + public void setOverlay(java.util.function.Supplier source) { + overlay = source; } /** The world was saved: look for changed regions soon. */ @@ -337,7 +337,8 @@ public final class WorldMapSync implements AutoCloseable { lastPlayersSent = text; playersSentAt = now; } - String ov = overlay; + java.util.function.Supplier 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) { client.post("map/overlay", JsonParser.parseString(ov).getAsJsonObject()); lastOverlaySent = ov; diff --git a/integrations/common/src/test/java/net/scopenet/core/MapOverlayTest.java b/integrations/common/src/test/java/net/scopenet/core/MapOverlayTest.java new file mode 100644 index 0000000..dc65f4e --- /dev/null +++ b/integrations/common/src/test/java/net/scopenet/core/MapOverlayTest.java @@ -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 ", "IRN", "", 0x3366CC, "minecraft:overworld", ring, List.of(), 1, 8, 8, + "
Iron <Wolves> [IRN]
1 claimed chunk in this area
"); + var pin = new MapData.Pin("warp-a", "warp", "Warp: a", "minecraft:overworld", 5, 64, 6, + "
Warp a
/warp a
"); + 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 [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(" y < 5 ? 1 : 0); + MapRenderTest.region(dir.resolve("world/region"), Map.of(RegionFile.index(1, 1), flat, RegionFile.index(20, 3), flat)); + + List tiles = new CopyOnWriteArrayList<>(); + List players = new CopyOnWriteArrayList<>(); + List 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(); + } +} diff --git a/integrations/fabric/build.gradle b/integrations/fabric/build.gradle index 613d817..2fe3891 100644 --- a/integrations/fabric/build.gradle +++ b/integrations/fabric/build.gradle @@ -16,7 +16,6 @@ dependencies { implementation project(':common') if (features) { 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. compileOnly 'net.luckperms:api:5.4' } @@ -32,6 +31,5 @@ processResources { jar { archiveBaseName = "scopenet-fabric-${mc}" from project(':common').sourceSets.main.output - if (features) from project(':bluemap').sourceSets.main.output } base.archivesName = "scopenet-fabric-${mc}" diff --git a/integrations/fabric/src/main/resources/fabric.mod.json b/integrations/fabric/src/main/resources/fabric.mod.json index 535e65d..547db89 100644 --- a/integrations/fabric/src/main/resources/fabric.mod.json +++ b/integrations/fabric/src/main/resources/fabric.mod.json @@ -7,5 +7,5 @@ "entrypoints": { "server": ["net.scopenet.fabric.ScopenetFabric"] }, "mixins": ["scopenet.mixins.json"], "depends": { "fabricloader": ">=0.16.10", "minecraft": "${mc}", "java": ">=${java}" }, - "suggests": { "fabric-api": "*", "luckperms": "*", "placeholder-api": "*", "bluemap": "*" } + "suggests": { "fabric-api": "*", "luckperms": "*", "placeholder-api": "*" } } diff --git a/integrations/fabric/src/mc1.20.1/java/net/scopenet/fabric/features/BlueMapHook.java b/integrations/fabric/src/mc1.20.1/java/net/scopenet/fabric/features/BlueMapHook.java deleted file mode 100644 index d1d97c5..0000000 --- a/integrations/fabric/src/mc1.20.1/java/net/scopenet/fabric/features/BlueMapHook.java +++ /dev/null @@ -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 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(); } -} diff --git a/integrations/fabric/src/mc1.20.1/java/net/scopenet/fabric/features/FabricFeatures.java b/integrations/fabric/src/mc1.20.1/java/net/scopenet/fabric/features/FabricFeatures.java index 156e571..a34a5a4 100644 --- a/integrations/fabric/src/mc1.20.1/java/net/scopenet/fabric/features/FabricFeatures.java +++ b/integrations/fabric/src/mc1.20.1/java/net/scopenet/fabric/features/FabricFeatures.java @@ -56,17 +56,12 @@ public final class FabricFeatures implements Runnable { private FabricConfig config; private ActionPoller poller; private MapService map; - private Object blueMap; private int ticks; private String placeholderStatus = "off"; CommandSet commandSet() { return commands; } @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_STOPPING.register(s -> stopping()); ServerTickEvents.END_SERVER_TICK.register(s -> tick()); @@ -116,10 +111,11 @@ public final class FabricFeatures implements Runnable { integration.onNotifications(events -> link.onPanelEvents(events)); 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()})); 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()); registerPlaceholders(); ScopenetEconomy.install(new ScopenetEconomy(env.panel, r -> platform.runAsync(r))); @@ -130,7 +126,6 @@ public final class FabricFeatures implements Runnable { private void stopping() { ScopenetEconomy.uninstall(); Bridge.claimBypass = p -> false; - if (blueMap != null) { try { BlueMapHook.stop(blueMap); } catch (Throwable ignored) { } blueMap = null; } if (platform != null) platform.shutdown(); commands = null; map = null; diff --git a/integrations/minecraft/src/main/java/net/scopenet/minecraft/Bridge.java b/integrations/minecraft/src/main/java/net/scopenet/minecraft/Bridge.java index ff6a8c3..ff40336 100644 --- a/integrations/minecraft/src/main/java/net/scopenet/minecraft/Bridge.java +++ b/integrations/minecraft/src/main/java/net/scopenet/minecraft/Bridge.java @@ -33,7 +33,7 @@ public final class Bridge { })); integration.startClaimSync(); 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) { // Throwing aborts startup; silently disabling an auth mod is unsafe. throw new IllegalStateException("SCOPENET cannot start: " + e.getMessage(), e); @@ -71,13 +71,13 @@ public final class Bridge { current.tick(); if (++mapTicks >= 20) { mapTicks = 0; - List positions = new ArrayList<>(); + List positions = new ArrayList<>(); 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.getYRot()), Math.round(player.getXRot()))); } - current.liveMapPlayers(positions); + current.mapPlayers(positions); } } diff --git a/integrations/paper/build.gradle b/integrations/paper/build.gradle index ca2d36d..c59a7e4 100644 --- a/integrations/paper/build.gradle +++ b/integrations/paper/build.gradle @@ -6,7 +6,6 @@ repositories { dependencies { implementation project(':common') implementation project(':api') - implementation project(':bluemap') implementation 'com.google.code.gson:gson:2.10.1' compileOnly 'org.spigotmc:spigot-api:1.20.1-R0.1-SNAPSHOT' // Optional integrations: the plugin detects these at runtime and works without them. @@ -23,7 +22,6 @@ jar { duplicatesStrategy = DuplicatesStrategy.EXCLUDE from project(':common').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-') && !it.name.startsWith('bluemap-') }.collect { zipTree(it) } } + from { configurations.runtimeClasspath.findAll { it.name.endsWith('.jar') && !it.name.startsWith('common-') && !it.name.startsWith('api-') }.collect { zipTree(it) } } exclude 'META-INF/*.SF', 'META-INF/*.RSA', 'META-INF/*.DSA' } diff --git a/integrations/paper/src/main/java/net/scopenet/paper/ClientLinkBridge.java b/integrations/paper/src/main/java/net/scopenet/paper/ClientLinkBridge.java index b42c203..e33f648 100644 --- a/integrations/paper/src/main/java/net/scopenet/paper/ClientLinkBridge.java +++ b/integrations/paper/src/main/java/net/scopenet/paper/ClientLinkBridge.java @@ -31,7 +31,6 @@ final class ClientLinkBridge implements PluginMessageListener { private final ShopPoints shops; private ActionPoller poller; private MapService mapService; - private Object blueMap; private EconomyHandler economy; 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. */ void startMap(EssentialsHandler essentials, GuildHandler guilds, EconomyHandler economy) { @@ -81,11 +80,9 @@ final class ClientLinkBridge implements PluginMessageListener { @Override public Collection shops() { return shops.all(); } }; 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); - 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). */ @@ -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() { - if (blueMap != null) { BlueMapHook.stop(blueMap); blueMap = null; } Bukkit.getMessenger().unregisterOutgoingPluginChannel(plugin, Wire.S2C); Bukkit.getMessenger().unregisterIncomingPluginChannel(plugin, Wire.C2S, this); } diff --git a/integrations/paper/src/main/java/net/scopenet/paper/ScopenetPlugin.java b/integrations/paper/src/main/java/net/scopenet/paper/ScopenetPlugin.java index e071d21..94b025c 100644 --- a/integrations/paper/src/main/java/net/scopenet/paper/ScopenetPlugin.java +++ b/integrations/paper/src/main/java/net/scopenet/paper/ScopenetPlugin.java @@ -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. integration.startClaimSync(); - // Live positions center the in-app BlueMap claim view on the player. - if (getConfig().getBoolean("livemap.enabled", true)) { - integration.enableLiveMap(); - Bukkit.getScheduler().runTaskTimer(this, this::snapshotPlayers, 40, 20); + // The SCOPENET Map: renders the world to tiles and sends them, positions, claims and pins to the panel. + // Started on the first tick so the worlds exist. + 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); + }); + 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, @@ -114,7 +123,7 @@ public final class ScopenetPlugin extends JavaPlugin implements Listener { 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); // Reloading an auth plugin cannot silently admit existing sessions. @@ -160,7 +169,7 @@ public final class ScopenetPlugin extends JavaPlugin implements Listener { private void snapshotPlayers() { if (integration == null) return; - java.util.List list = new java.util.ArrayList<>(); + java.util.List list = new java.util.ArrayList<>(); for (Player p : Bukkit.getOnlinePlayers()) { Location at = p.getLocation(); String dim = switch (at.getWorld().getEnvironment()) { @@ -169,11 +178,11 @@ public final class ScopenetPlugin extends JavaPlugin implements Listener { default -> "minecraft:overworld"; }; // 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.getYaw()), Math.round(at.getPitch()))); } - integration.liveMapPlayers(list); + integration.mapPlayers(list); } public static String dimension(World world) { diff --git a/integrations/paper/src/main/resources/config.yml b/integrations/paper/src/main/resources/config.yml index d6db894..4c90bbe 100644 --- a/integrations/paper/src/main/resources/config.yml +++ b/integrations/paper/src/main/resources/config.yml @@ -51,10 +51,13 @@ essentials: # RTP search radius rtp-radius: 2500 -livemap: - # Send live player positions to the panel for the launcher BlueMap claim view. - # Configure BlueMap's public address and enable live positions in the panel. +map: + # The SCOPENET Map: this server renders its world to map tiles in the background and sends only what changed, + # 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 + # Show every player's homes as pins. Off by default: homes are private. + show-homes: false economy: # Server Economy commands (/balance, /pay, /baltop, /shop, /sell, /market, /orders, /trade, /transactions) @@ -93,17 +96,6 @@ integrations: enabled: true 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 # Sends balance, level, guild and claim info to players who have the optional SCOPENET diff --git a/integrations/paper/src/main/resources/plugin.yml b/integrations/paper/src/main/resources/plugin.yml index 3d1c682..dda1abb 100644 --- a/integrations/paper/src/main/resources/plugin.yml +++ b/integrations/paper/src/main/resources/plugin.yml @@ -4,7 +4,7 @@ main: net.scopenet.paper.ScopenetPlugin api-version: '1.20' description: SCOPENET private authentication, access control and activity reporting. load: STARTUP -softdepend: [LuckPerms, PlaceholderAPI, Vault, CoreProtect, WorldGuard, WorldEdit, spark, BlueMap] +softdepend: [LuckPerms, PlaceholderAPI, Vault, CoreProtect, WorldGuard, WorldEdit, spark] commands: scopenet: diff --git a/integrations/settings.gradle b/integrations/settings.gradle index 558b8bb..91a2dcc 100644 --- a/integrations/settings.gradle +++ b/integrations/settings.gradle @@ -6,7 +6,7 @@ pluginManagement { } } rootProject.name = 'scopenet-integrations' -include 'common', 'api', 'paper', 'bluemap' +include 'common', 'api', 'paper' // Build loaders separately: Forge and Loom use different Gradle generations. if (providers.gradleProperty('loader').orNull == 'fabric') include 'fabric' if (providers.gradleProperty('loader').orNull == 'forge') include 'forge'