diff --git a/Dockerfile b/Dockerfile
index 38f8b7c..acdf99b 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -9,6 +9,8 @@ FROM --platform=$BUILDPLATFORM node:22-alpine AS web
WORKDIR /src/panel/web
COPY panel/web/package.json panel/web/package-lock.json ./
RUN npm ci --no-audit --no-fund
+# The map viewer is shared with the launcher (imported as @scopenet/map).
+COPY shared/ /src/shared/
COPY panel/web/ ./
RUN npm run build
diff --git a/README.md b/README.md
index 1375e9c..8db3306 100644
--- a/README.md
+++ b/README.md
@@ -99,6 +99,9 @@ docker-compose.yml Panel deployment
- [Admin guide](docs/admin-guide.md) — deploying, HTTPS, modpacks, players, backups
- [Official server setup](docs/server-integration.md) — private authentication, supported versions, activity and UUID protection
+- [The SCOPENET Map](docs/map.md) — the built-in world map: terrain, guild land, shops and players
+- [Discord, live embeds and email](docs/discord-and-email.md) — customisable announcements, self-updating status boards and emails to players
+- [Rewards, limits and permission nodes](docs/rewards-and-limits.md) — what quests can grant, and every setting you can change
- [Architecture](docs/architecture.md) — how the pieces fit, API reference
- [Development](docs/development.md) — running everything locally, tests
- [Deployment](docs/deployment.md) — local Compose build, health check, upgrades and backups
diff --git a/docs/bluemap.md b/docs/bluemap.md
deleted file mode 100644
index 9b52d5d..0000000
--- a/docs/bluemap.md
+++ /dev/null
@@ -1,76 +0,0 @@
-# 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/docs/deployment.md b/docs/deployment.md
index 81d2ab4..799e0d8 100644
--- a/docs/deployment.md
+++ b/docs/deployment.md
@@ -23,9 +23,8 @@ The panel listens on `PANEL_PORT` (default `8080`). Docker checks
`docker compose ps` before directing players to it. If `ADMIN_PASSWORD` is
blank on first start, the generated password appears in the panel logs.
-To show a 2D world map and claim land in the launcher, install BlueMap on the
-game server and configure its public web address in the panel. See
-[Live map setup](live-map.md).
+Servers draw their own world map and send it to the panel; nothing extra to
+install or host. See [The SCOPENET Map](map.md).
The named `panel-data` volume holds the database, uploads, hosted launcher
downloads, and the generated JWT signing key. Preserve that volume when
diff --git a/docs/discord-and-email.md b/docs/discord-and-email.md
new file mode 100644
index 0000000..847a6ed
--- /dev/null
+++ b/docs/discord-and-email.md
@@ -0,0 +1,39 @@
+# Discord messages, live embeds and emails
+
+## Announcements (Admin panel → Discord → Announcements)
+
+Three messages are posted by the panel's webhook (Settings → Discord → webhook): **Achievement unlocked**, **New player** and **New guild**. Each one is fully editable, with a live preview built the way Discord will show it:
+
+- Message text above the box (mentions such as `<@&roleid>` work there), title and link, description, colour, a small picture on the right, a big picture at the bottom, author and footer (with icons), the time, and any number of side-by-side or full-width fields.
+- The sender's name and picture.
+- **Placeholders** such as `{player}`, `{avatar}`, `{achievement}`, `{achievement_description}`, `{xp}`, `{level}`, `{rank_title}`, `{guild_line}` (achievements), `{members_total}` (new players), `{guild}`, `{leader}`, `{members}`, `{claims}` (guilds) and `{server_name}`, `{time}` everywhere. Click a placeholder to insert it where you are typing. Unknown ones stay as typed so mistakes are easy to spot.
+- Player names are cleaned so they can't ping anyone or break the formatting.
+
+Switch each message on or off, and press **Save & send a sample** to see it in your channel.
+
+## Live embeds (Discord → Live embeds)
+
+Four boards that the panel posts once and then **edits in place**, so the channel never fills up:
+
+| Embed | Shows | Rows are set by |
+|---|---|---|
+| **Server status** | Every server: online, players, TPS, version | the line template |
+| **Active guilds** | Top guilds with level, members, land and leader | rows shown |
+| **Player leaderboard** | Top players by playtime, level, kills or blocks | ranked by, rows shown |
+| **Richest players** | Top balances of one server economy | economy of, rows shown |
+
+Every embed has the same editor as the announcements, plus a **line for each row** (for example `{rank}. **{player}** — {value}`) that fills `{rows}` in the description, an update interval (1 minute to 1 hour), and an optional **own webhook** so each board can live in its own channel. If someone deletes the message, the next update posts a fresh one; **Post as new message** forces it.
+
+## Emails (Admin panel → Emails)
+
+Set up the sender and Resend key under **Settings → Connections** first. Then:
+
+1. Write a **template**: subject and message with placeholders (`{player}`, `{level}`, `{rank_title}`, `{guild}`, `{brand}`, `{panel_url}`…). Simple formatting turns into a branded HTML email in your launcher colours: `# Heading`, `**bold**`, `- list`, `[link](https://…)`, `[button: Label](https://…)`.
+2. Check the live **preview**, and **send a test** to yourself.
+3. **Send** to everyone with an email, a group, or chosen players. The panel shows how many will receive it first, sends them one by one in the background, and keeps a log with failures and their reasons.
+
+Every email has an unsubscribe link; players who use it are skipped by later emails. Mark a message as an **account notice** to reach everyone regardless, without the unsubscribe link: use that only for things about their account.
+
+## Live map on the landing page
+
+In **Landing page**, add a **Live map** section. Choose the server (or leave it at the first server with a map), the height, and what visitors may see: guild land and places are on by default, **live player positions are off** (they let strangers find players). Visitors can pan and zoom the same map your players see in the launcher; removing the section closes the public map again.
diff --git a/docs/live-map.md b/docs/live-map.md
deleted file mode 100644
index 38970a1..0000000
--- a/docs/live-map.md
+++ /dev/null
@@ -1,21 +0,0 @@
-# Live map and land claims
-
-SCOPENET uses [BlueMap](https://bluemap.bluecolored.de/) for terrain. Install BlueMap on the Minecraft server and make its web app reachable from players' computers. In the panel's **Servers → server settings**, enter the BlueMap **Map address**. The launcher opens the same map in its Live Map window and reads BlueMap's low resolution 2D tile images for the Guilds territory view.
-
-The terrain address must serve BlueMap's `settings.json`, per-map `settings.json`, and low resolution PNG tiles from the same URL path. A URL such as `https://maps.example.com/` works. If BlueMap is hosted below a path, include the trailing slash, for example `https://example.com/bluemap/`.
-
-## Center on the player
-
-Turn on **Live player positions** in the server settings and leave `livemap.enabled: true` in the SCOPENET plugin or mod configuration. The game server sends current positions to the panel. The panel returns each signed-in player only their own position to the launcher. The **Find me** button centers the territory view on it; the blue dot marks their position. Positions expire when the server stops sending updates.
-
-The BlueMap map itself remains available when position relay is off. In that case, players can pan manually, but **Find me** is unavailable.
-
-## Claim controls
-
-In **Guilds & Territories**, select the game server and dimension. Left click a chunk to claim it for your guild, or drag across chunks to claim an area. Right click or right drag unclaims your guild's chunks. Alt drag pans; the wheel zooms. The overlay shows your guild's claims in green and other guilds' claims in red. The panel checks membership and claim limits for every request; only successful claims appear in the final map state.
-
-BlueMap must have rendered the area to show terrain. Claims can still be managed by the in-game `/claim` and `/unclaim` commands when the web map is unavailable.
-
-## Existing world mirror
-
-Older SCOPENET game integrations uploaded region chunks to the panel under `/api/server/v1/livemap`. The panel retains those ingest endpoints for compatibility with existing servers, but the launcher terrain comes from BlueMap. Current integrations send player positions only.
diff --git a/docs/map.md b/docs/map.md
new file mode 100644
index 0000000..224d446
--- /dev/null
+++ b/docs/map.md
@@ -0,0 +1,48 @@
+# The SCOPENET Map
+
+Every server draws its own world map. There is nothing to install, host or configure besides the plugin or mod you already run: the game server renders its world to small tiles in the background, sends only what changed to the panel, and the launcher and admin panel show them in a fast native map.
+
+What's on it:
+
+- **Terrain** in the look of a Minecraft map, coloured from the real block textures (modded blocks get a matching colour). The Nether is drawn below its ceiling, so you see caves and lava lakes, not bedrock.
+- **Guild land** as coloured outlines with the guild's icon and name, holes and all.
+- **Spawn, warps, guild homes, markets and shop points** as pins you can click.
+- **Players online**, with their head and facing direction. Click one to open the player panel (profile, teleport request, message in game, invite to your guild).
+- **Dimensions**: Overworld, Nether and End tabs appear as soon as the server has drawn them.
+
+## Turn it on
+
+It is on by default for every server. To change it:
+
+1. In the admin panel open **Servers → your server → Settings** and use the **SCOPENET Map** switch.
+2. In the plugin/mod config: `map.enabled` (Paper `config.yml`) or `map.enabled=true` (Fabric `scopenet.properties`). The old `livemap.enabled` key is still read.
+
+The first tiles appear a minute or two after the server starts. The server draws the regions closest to players first, then works through the rest quietly in the background at low priority.
+
+## In the launcher
+
+Players with a linked server get a **Live Map** button on the Home screen. Pan by dragging, zoom with the wheel or the + and − buttons, double click to zoom in. Use the toggles to hide claims, places or players, **Online** to jump to a player, the crosshair to find yourself and the star to go to spawn. The **Guilds → Territory** claim painter uses the same tiles.
+
+## In the admin panel
+
+**Servers → your server** shows a map card with the number of tiles, how much space they use, when the server last sent something and the live map itself. **Redraw world** deletes the stored tiles; the server notices and draws everything again (useful after a world reset or a renderer update).
+
+## On your public landing page
+
+Add a **Live map** section in the landing page builder to show the map to visitors, with only the layers you allow. See [discord-and-email.md](discord-and-email.md#live-map-on-the-landing-page).
+
+## How it works
+
+- The game server reads its region files (`region/*.mca`), works out the top block of every column, and writes 256 × 256 pixel tiles at one pixel per block. Zoomed-out levels are built from them, keeping real map colours so coastlines and roads stay sharp.
+- Tiles are palette PNGs, so a typical tile is a few kilobytes. Only tiles that changed are uploaded, and a region is redrawn at most every 90 seconds while players are building.
+- The panel stores tiles under its data folder (`map//…`) and serves them to signed-in players only, with a short-lived key on every address and ETag caching.
+- Player positions go up about every two seconds; claims and pins whenever they change.
+- A local cache lives next to the plugin (`plugins/SCOPENET/map`) or mod (`config/scopenet/map`) so a restart doesn't redraw anything.
+
+Disk use is small: a world of 500 explored regions is roughly 50–150 MB on the panel. Servers using region files compressed with LZ4 (an opt-in setting in recent Minecraft versions) are not drawn; leave `region-file-compression` on its default.
+
+## Troubleshooting
+
+- **"Drawing the world…" never ends.** The server has to be connected to the panel with a valid token, have `map.enabled` on, and have its world folder readable by the Minecraft process. The server log says `SCOPENET Map is on` when it starts drawing.
+- **A flat grey or missing area.** The chunk was never generated, or is saved in an older world format (before Minecraft 1.18).
+- **Colours look off for a modded block.** Unknown blocks get a colour from their name; vanilla blocks use their texture colour.
diff --git a/docs/ranks-guilds-and-warps.md b/docs/ranks-guilds-and-warps.md
index 97a9df9..77ca413 100644
--- a/docs/ranks-guilds-and-warps.md
+++ b/docs/ranks-guilds-and-warps.md
@@ -12,7 +12,7 @@ Paper chat shows `[Guild tag] [Level title] [LuckPerms group display] nickname:
Players can request to join a guild from the launcher directory. Leaders and officers review those requests from the guild roster. Leaders can edit the guild description, message of the day, icon and banner from **Guild settings**. They can create and assign custom roles with permissions for invites, kicks, claims, posts and guild details. Deleting a role returns its members to `member`.
-The in-app territory view uses the configured BlueMap 2D tile address for each server. It centers on the player's last position when position relay is enabled. Left click or drag to claim; right click or drag to unclaim. On Paper, `/autoclaim on` claims each new unclaimed chunk entered while walking; `/autoclaim off` stops it. Auto-claim stops on a failed claim so it does not repeatedly send requests at the claim limit or in another guild's territory.
+The in-app territory view draws on the server's SCOPENET Map tiles (see [map.md](map.md)). It centers on the player's last position while they are in game. Left click or drag to claim; right click or drag to unclaim. On Paper, `/autoclaim on` claims each new unclaimed chunk entered while walking; `/autoclaim off` stops it. Auto-claim stops on a failed claim so it does not repeatedly send requests at the claim limit or in another guild's territory.
## Warps and item sharing on Paper
diff --git a/docs/rewards-and-limits.md b/docs/rewards-and-limits.md
new file mode 100644
index 0000000..ff25ccf
--- /dev/null
+++ b/docs/rewards-and-limits.md
@@ -0,0 +1,70 @@
+# Rewards, limits and permission nodes
+
+## Rewards beyond XP
+
+Quests, achievements and rank milestones (level rewards) can hand out more than XP and a title. In the admin panel, open any quest, achievement or milestone and use **Extra rewards**:
+
+| Reward | What happens | Where it happens |
+|---|---|---|
+| **Money** | Credits the balance, once per economy group (or one chosen server) | Panel, instantly |
+| **Claim chunks** | Raises the land limit of the player's guild while they are a member | Panel, instantly |
+| **Badge** | Shows a badge on their profile | Panel, instantly |
+| **Item** | Puts the item in their inventory (anything that doesn't fit drops at their feet). Vanilla ids such as `diamond` or `modid:item` | Game server, when they are online |
+| **Permission** | Grants a permission node, permanently or for a number of days | Game server (LuckPerms by default) |
+| **Group** | Adds them to a permission group | Game server (LuckPerms by default) |
+| **Message** | Sends a chat message | Game server |
+| **Command** | Runs a console command with `{player}` and `{uuid}` filled in | Game server, only if the server allows it |
+
+Players see short summaries ("$250.00", "Diamond x3", "+4 claim chunks") next to each quest and achievement in the launcher. Commands are shown only as "A special reward".
+
+Items, permissions, groups and messages are queued in the panel and handed to the game server once the player is online there. Anything that fails (an unknown item, LuckPerms missing) is retried a few times and then kept in the delivery log with the reason. A reward earned on one server can be delivered by whichever server the player is on, unless you pin it to a server.
+
+### Server settings
+
+Paper `config.yml` / Fabric `scopenet.properties`:
+
+```yaml
+rewards:
+ enabled: true
+ allow-commands: false # let the panel run its own console commands. Leave off unless every panel admin is trusted.
+ permission-command: "lp user {player} permission set {node} true"
+ permission-temp-command: "lp user {player} permission settemp {node} {value} {duration}"
+ group-command: "lp user {player} parent add {group}"
+```
+
+Using another permissions plugin? Change the command templates. Placeholders: `{player}`, `{uuid}`, `{node}`, `{value}`, `{duration}`, `{group}`.
+
+## What you can configure
+
+### Per server (plugin/mod config)
+
+| Setting | Meaning |
+|---|---|
+| `essentials.max-homes` | Homes every player can set |
+| `essentials.home-name-max-length` | Longest home name |
+| `essentials.max-warps` | Most warps the server may have (0 = no limit) |
+| `essentials.tpa-timeout-seconds` | How long a `/tpa` request stays open |
+| `essentials.rtp-radius`, `rtp-min-radius`, `rtp-attempts` | Where `/rtp` sends players, and how hard it tries |
+| `essentials.cooldowns.` | Seconds between uses of `spawn`, `home`, `back`, `tpa`, `rtp`, `warp` |
+| `map.enabled`, `map.show-homes` | The SCOPENET Map and whether homes appear on it |
+| `economy.currency-symbol` | The symbol shown next to money |
+| `rewards.*` | See above |
+| Feature switches | `leveling`, `quests`, `achievements`, `guilds`, `social`, `essentials`, `economy` |
+
+### Network-wide (admin panel → Progression → Economy & guild rules)
+
+Starting balance, chunks a new guild may claim, extra claims per member and per guild level, guild member limit, and market listings per player. These apply on every server and take effect immediately.
+
+## Permission nodes that change limits
+
+Give these with LuckPerms (or any permissions plugin) to ranks:
+
+| Node | Effect |
+|---|---|
+| `scopenet.homes.` | Homes limit for the player, e.g. `scopenet.homes.10`. The highest one a player has wins and never lowers the base `max-homes` |
+| `scopenet.homes.unlimited` | No home limit |
+| `scopenet.cooldown.bypass` | Skip every command cooldown |
+
+Operators and anyone with `*` get the top tier on Paper. On Fabric these nodes are off for everyone until a permissions system (such as the LuckPerms mod) grants them; operators still skip cooldowns.
+
+Claim chunks are a guild limit, so they are changed with the network rules above or with **Claim chunks** rewards, not with a permission node.
diff --git a/examples/fabric-server/scopenet.properties b/examples/fabric-server/scopenet.properties
index 5ba77dd..7890a1e 100644
--- a/examples/fabric-server/scopenet.properties
+++ b/examples/fabric-server/scopenet.properties
@@ -21,15 +21,31 @@ social.chat_prefixes=true
essentials.enabled=true
economy.enabled=true
-# Send world chunks and player positions to the panel's Live Map
-# (also needs Live Map switched on for this server in the panel).
-livemap.enabled=true
+# Draw this world on the SCOPENET Map: tiles, player positions, guild land and pins go to the panel
+# (also needs the map switched on for this server in the panel, which it is by default).
+map.enabled=true
# --- Essentials (1.20.1) -----------------------------------------------------
# /spawn /home /sethome /delhome /back /tpa /tpaccept /tpdeny /rtp /warp /playtime
+# Homes every player can set. Raise it for a rank with a permission node: scopenet.homes.10 gives 10 homes,
+# scopenet.homes.25 gives 25 (highest wins), scopenet.homes.unlimited removes the limit.
essentials.max_homes=5
-essentials.rtp_radius=2500
+essentials.home_name_max_length=32
+# Most warps the server may have (0 = no limit).
+essentials.max_warps=0
essentials.tpa_timeout_seconds=60
+# /rtp picks a random safe spot between rtp_min_radius and rtp_radius blocks from 0,0.
+essentials.rtp_radius=2500
+essentials.rtp_min_radius=0
+essentials.rtp_attempts=15
+# Seconds between uses (0 = none). Starts when the command actually teleports.
+# Players with scopenet.cooldown.bypass skip all of these.
+essentials.cooldown.spawn=0
+essentials.cooldown.home=0
+essentials.cooldown.back=0
+essentials.cooldown.tpa=0
+essentials.cooldown.rtp=0
+essentials.cooldown.warp=0
# --- Economy (1.20.1) --------------------------------------------------------
# Shown next to amounts.
@@ -53,9 +69,15 @@ placeholders.enabled=true
# Send balance, level, guild and claim info to players who have the SCOPENET client mod.
clientlink.enabled=true
-# --- BlueMap (1.20.1) --------------------------------------------------------
-# With the BlueMap mod installed, adds guild claims, spawn, warps, guild homes, markets and shops to the 3D map, and a
-# player panel when you click someone. Set the map's web address in the panel (Servers -> this server -> Map address).
-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.
-bluemap.show_homes=false
+# --- Map pins ----------------------------------------------------------------
+# Show every player's homes as pins on the map. Off by default: homes are private.
+map.show_homes=false
+
+# --- Rewards -----------------------------------------------------------------
+# Quests, achievements and rank milestones can give items, permissions, groups, messages... (set in the admin panel).
+rewards.enabled=true
+# Let the panel run its own console commands as rewards. Leave off unless you trust every panel admin.
+rewards.allow_commands=false
+rewards.permission_command=lp user {player} permission set {node} true
+rewards.permission_temp_command=lp user {player} permission settemp {node} {value} {duration}
+rewards.group_command=lp user {player} parent add {group}
diff --git a/examples/paper/config.yml b/examples/paper/config.yml
index 600a84d..dc106e4 100644
--- a/examples/paper/config.yml
+++ b/examples/paper/config.yml
@@ -58,15 +58,38 @@ social:
essentials:
# /spawn /home /sethome /delhome /back /tpa /tpaccept /tpdeny /rtp /warp /playtime
enabled: true
- # Homes per player.
+ # Homes every player can set. Raise it for a rank with a permission node:
+ # scopenet.homes.10 gives 10 homes, scopenet.homes.25 gives 25 (highest wins),
+ # scopenet.homes.unlimited removes the limit. Operators and "*" get the top tier.
max-homes: 5
- # /rtp picks a random safe spot within this many blocks of 0,0.
+ # Longest allowed home name.
+ home-name-max-length: 32
+ # Most warps the server may have (0 = no limit).
+ max-warps: 0
+ # How long a /tpa request stays open.
+ tpa-timeout-seconds: 60
+ # /rtp picks a random safe spot between rtp-min-radius and rtp-radius blocks from 0,0.
rtp-radius: 2500
+ rtp-min-radius: 0
+ # Random spots tried before /rtp gives up.
+ rtp-attempts: 15
+ # Seconds between uses (0 = none). Starts when the command actually teleports.
+ # Players with scopenet.cooldown.bypass skip all of these.
+ cooldowns:
+ spawn: 0
+ home: 0
+ back: 0
+ tpa: 0
+ rtp: 0
+ warp: 0
-livemap:
- # Send live player positions to the panel so the launcher can center its
- # BlueMap territory view. Configure BlueMap's public address in the panel.
+map:
+ # The SCOPENET Map: this server draws its world to map tiles in the background and
+ # sends only what changed, with players, guild land and pins, to the panel.
+ # It also needs the map switched on for this server in the panel (on by default).
enabled: true
+ # Show every player's homes as pins. Off by default: homes are private.
+ show-homes: false
economy:
# /balance /pay /baltop /shop /sell /market /orders /trade /transactions
@@ -113,15 +136,21 @@ integrations:
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.
+# Rewards
+# Quests, achievements and rank milestones can give more than XP: money, items, permissions, groups, claim
+# chunks, badges and messages (set them in the admin panel). The panel hands items, permissions and commands
+# to this server when the player is online. Permissions and groups use console commands, so any permissions
+# plugin works; the defaults are LuckPerms'. Placeholders: {player} {uuid} {node} {value} {duration} {group}.
# ==============================================================================
-bluemap:
+rewards:
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
+ # Let the panel run its own console commands as rewards. Off by default: anyone who can edit rewards in the
+ # panel could otherwise run commands on this server. Only switch it on if you trust every panel admin.
+ allow-commands: false
+ permission-command: "lp user {player} permission set {node} true"
+ permission-deny-command: "lp user {player} permission set {node} false"
+ permission-temp-command: "lp user {player} permission settemp {node} {value} {duration}"
+ group-command: "lp user {player} parent add {group}"
# ==============================================================================
# Client mod link
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:
- *
- *
guild land as coloured regions, with the guild's icon and name,
- *
spawn, warps, guild homes (and homes, if the admin allows) as pins,
- *
markets and shops as pins that list what is for sale,
- *
a script that opens a side panel when you click a player.
- *
- * 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 =
- '' +
- '