From 228727a395f3c64f20f8570f6438ae3705282803 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 30 Sep 2026 20:24:42 +0000 Subject: [PATCH] BlueMap docs, command guide entries, early BlueMap hook on Fabric --- docs/bluemap.md | 76 +++++++++++++++++++ .../scopenet/fabric/features/BlueMapHook.java | 13 +++- .../fabric/features/FabricFeatures.java | 9 ++- launcher/src/pages/Commands.svelte | 4 + 4 files changed, 95 insertions(+), 7 deletions(-) create mode 100644 docs/bluemap.md diff --git a/docs/bluemap.md b/docs/bluemap.md new file mode 100644 index 0000000..9b52d5d --- /dev/null +++ b/docs/bluemap.md @@ -0,0 +1,76 @@ +# BlueMap: the 3D map + +SCOPENET uses [BlueMap](https://bluemap.bluecolored.de/) for the 3D world map. BlueMap runs on each Minecraft server as a plugin or mod and serves the map on its own web address. SCOPENET adds to it, and the launcher shows it inside the app exactly as a browser would. + +## What you get + +| On the map | Where it comes from | +|---|---| +| **Guild claims** as coloured regions with the guild's icon and name (one colour per guild, outlines follow the real shape, holes included) | the claim index | +| **Spawn, warps and guild homes** as pins | the server | +| **Homes** as pins (off by default, see privacy) | the server | +| **Markets and shops** as pins that list what is for sale | shop points + the panel's market | +| **Players** with BlueMap's own live markers; **click one** for a side panel | BlueMap + SCOPENET | + +The player panel shows level, guild, rank, title, playtime and server level. Inside the launcher it also offers **Send teleport request**, **Send message** and **Invite to my guild** (you need to be signed in with a SCOPENET account, and the other player must be online on that server; inviting needs a leader or officer role). In a plain browser it shows the info and points you to the launcher for the actions. + +## Set it up + +1. **Install BlueMap** on the server: the Paper plugin, or the Fabric/Forge mod, from . Start the server once. BlueMap asks you to accept a download of the Minecraft client files in its config (`core.conf`: `accept-download: true`), then renders the world. +2. **Make its web address reachable.** BlueMap serves on port 8100 by default. Put it behind HTTPS with a reverse proxy (recommended; see below) or open the port. +3. **Install the SCOPENET plugin or Fabric mod** (Paper, or Fabric 1.20.1). It finds BlueMap by itself and adds its markers and script. Nothing else to configure. +4. **Tell the panel the address:** *Servers → your server → Settings → Map address (BlueMap)*, for example `https://map.example.com`. Players then see a **Live Map** button for that server in the launcher. + +`bluemap.enabled` (Paper `config.yml`, Fabric `scopenet.properties`) turns the addon off. `bluemap.show-homes` / `bluemap.show_homes` shows every player's homes. + +## Shop points (physical shops) + +A shop point is a block that opens the market or the server shop when right-clicked. It works with the normal windows (chest window on Paper; the client mod's shop and market windows where installed) and appears as a pin on the map. + +``` +/market point add [market|shop] (look at a block; needs scopenet.command.market.point, default op) +/market point remove +/market point list +``` + +`market` opens the player market and lists recent listings on the map pin; `shop` opens the server shop. + +## Inside the launcher + +The launcher loads your map address in a frame, so everything BlueMap does in a browser works there too. SCOPENET's script on the map page passes player clicks to the launcher, which shows the side panel. A few things make it work or not: + +* **Use HTTPS.** Loading an `http://` map inside an HTTPS page can be blocked as mixed content on some systems. +* **Don't forbid framing.** If your reverse proxy sends `X-Frame-Options: DENY` or a `Content-Security-Policy: frame-ancestors` that excludes the launcher, the map won't load in the app. Remove that header for the map address. The launcher's **Browser** button opens the same map in your browser either way. +* The map address is checked by the panel (web address only, no credentials). + +## Privacy + +* The map is public to anyone who knows its address, like any BlueMap. **Homes are off by default** for that reason. Claims and guild names are visible by design. +* `players.json`, which drives the browser panel, holds only public facts (name, level, title, guild, rank, playtime). Never balances. +* Actions from the map go through the panel, are rate-limited, and need a signed-in account. A message or teleport request reaches only players who are online on that server. + +## Reverse proxy example (nginx) + +```nginx +server { + server_name map.example.com; + listen 443 ssl; + location / { + proxy_pass http://127.0.0.1:8100; + proxy_set_header Host $host; + proxy_http_version 1.1; + proxy_set_header Connection ""; # BlueMap uses server-sent events for live updates + proxy_buffering off; + } +} +``` + +## How it works (for developers) + +* `net.scopenet.core.map` builds the map data without any map software: `ChunkRegions` turns chunks into outlines (tested with an area-conservation check), `MapModel` builds pins and regions and escapes every name, `MapService` gathers it from any thread. +* `integrations/bluemap` (`MapAddon`) is the only code that touches BlueMap's API. It copies `scopenet.js`, `scopenet.css` and the icons into BlueMap's web root, registers them, and refreshes the marker sets every 10 seconds. +* Map actions are queued in the panel (`server_actions`); the server collects them every 3 seconds (`ActionPoller`) and carries them out with its normal rules. Guild invitations are real invitations (`guild_invites`) that the other player accepts in the launcher or with `/guild accept`. + +## Not verified yet + +The BlueMap addon, the Paper and Fabric wiring, and the launcher's map view have not been compiled or run against a real BlueMap and Minecraft: the build environment cannot download them. The geometry, map model, shop points, action poller and guild-invite commands are unit-tested, and so are the panel endpoints. Build it, and report what BlueMap says in its log and what the map shows. 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 index 52b473a..d1d97c5 100644 --- 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 @@ -10,10 +10,17 @@ import java.util.logging.Logger; final class BlueMapHook { private BlueMapHook() {} - static Object start(MapService service, Logger log) { + /** + * 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() { return service.snapshot(); } - @Override public com.google.gson.JsonObject playersJson() { return service.playersJson(); } + @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); } 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 505e2ce..156e571 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 @@ -63,6 +63,10 @@ public final class FabricFeatures implements Runnable { 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 +120,6 @@ public final class FabricFeatures implements Runnable { 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()); - if (config.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); } - } - registerCommands(s.getCommands().getDispatcher()); registerPlaceholders(); ScopenetEconomy.install(new ScopenetEconomy(env.panel, r -> platform.runAsync(r))); @@ -133,6 +133,7 @@ public final class FabricFeatures implements Runnable { if (blueMap != null) { try { BlueMapHook.stop(blueMap); } catch (Throwable ignored) { } blueMap = null; } if (platform != null) platform.shutdown(); commands = null; + map = null; } private void registerPlaceholders() { diff --git a/launcher/src/pages/Commands.svelte b/launcher/src/pages/Commands.svelte index 5878c1b..edbd6e8 100644 --- a/launcher/src/pages/Commands.svelte +++ b/launcher/src/pages/Commands.svelte @@ -26,6 +26,7 @@ { usage: '/sell', does: 'Open the selling menu.', perm: 'scopenet.command.sell' }, { usage: '/market', does: 'Browse and buy from the player market.', perm: 'scopenet.command.market', aliases: ['ah', 'auction'] }, { usage: '/market sell ', does: 'List the item in your hand on the market.', perm: 'scopenet.command.market.sell' }, + { usage: '/market point add [market|shop]', does: 'Staff: turn the block you look at into a shop that opens the market or shop when right-clicked. Also shown on the map.', perm: 'scopenet.command.market.point' }, { usage: '/orders', does: 'View your active market orders.', perm: 'scopenet.command.orders' }, { usage: '/trade ', does: 'Start a secure trade with another player.', perm: 'scopenet.command.trade' }, { usage: '/transactions', does: 'See your recent money movements.', perm: 'scopenet.command.transactions' }, @@ -38,6 +39,8 @@ { usage: '/guild chat ', does: 'Talk to your guild only.', perm: 'scopenet.command.guild.chat', aliases: ['/guild c'] }, { usage: '/guild sethome · /guild home', does: 'Set or visit the guild home.', perm: 'scopenet.command.guild.sethome' }, { usage: '/guild map', does: 'Show nearby claims on a map.', perm: 'scopenet.command.guild.map' }, + { usage: '/guild invite ', does: 'Invite a player to your guild (leaders and officers).', perm: 'scopenet.command.guild.invite' }, + { usage: '/guild accept [tag] · /guild decline [tag]', does: 'Answer a guild invitation. You can also answer in the launcher.', perm: 'scopenet.command.guild.accept' }, { usage: '/claim', does: 'Claim the chunk you stand in for your guild.', perm: 'scopenet.command.claim' }, { usage: '/unclaim', does: 'Give up the chunk you stand in.', perm: 'scopenet.command.unclaim' }, ] }, @@ -59,6 +62,7 @@ const tips = [ 'Placeholders: use %scopenet_level%, %scopenet_guild%, %scopenet_balance% and more in scoreboards, tab and chat.', + 'Right-click a shop point (a marked block) to open the market or shop. On a server with the SCOPENET client mod it opens the shop window.', 'Some commands may be switched off or limited by the server. If one says you lack permission, ask an admin.', 'Everything here also works through your rank: servers using LuckPerms can grant each permission node to any group.', ];