[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'