BlueMap addon, shop points, map actions wired into Paper and Fabric; client opens shop windows

This commit is contained in:
Claude committed 2026-09-30 20:24:02 +00:00
1 parent ad7285efac
commit 2817ad2ac5
33 files changed
+754 -13

No files matched your search

@@ -1,6 +1,11 @@
package net.scopenet.paper;
import net.scopenet.core.*;
import net.scopenet.core.map.MapService;
import net.scopenet.core.map.MapSource;
import net.scopenet.paper.commands.EconomyHandler;
import net.scopenet.paper.commands.EssentialsHandler;
import net.scopenet.paper.commands.GuildHandler;
import net.scopenet.integration.Integration;
import org.bukkit.Bukkit;
import org.bukkit.Material;
@@ -22,9 +27,16 @@ final class ClientLinkBridge implements PluginMessageListener {
private final PlayerCache cache;
private final ClientLink link;
private final Placeholders placeholders;
private final Integration integration;
private final ShopPoints shops;
private ActionPoller poller;
private MapService mapService;
private Object blueMap;
private EconomyHandler economy;
ClientLinkBridge(ScopenetPlugin plugin, Integration integration) {
this.plugin = plugin;
this.integration = integration;
PaperPlatform platform = new PaperPlatform(plugin);
Features features = new Features(true, plugin.getConfig().getBoolean("economy.enabled", true), integration.settings().guildsEnabled(),
integration.settings().socialEnabled(), integration.settings().landClaimingEnabled(),
@@ -33,6 +45,7 @@ final class ClientLinkBridge implements PluginMessageListener {
env = new Env(platform, new PanelAdapter(integration), features, Path.of(plugin.getDataFolder().getPath()), System::currentTimeMillis, new Random(), log);
cache = new PlayerCache(env);
placeholders = new Placeholders(env, cache);
shops = new ShopPoints(Path.of(plugin.getDataFolder().getPath()).resolve("shops.json"));
link = new ClientLink(env, cache, Bukkit.getServer().getName(), (p, bytes) -> {
Player target = Bukkit.getPlayer(p.uuid());
if (target != null) target.sendPluginMessage(plugin, Wire.S2C, bytes);
@@ -50,7 +63,70 @@ final class ClientLinkBridge implements PluginMessageListener {
}, 40, 20);
}
/**
* Shop points, actions from the map, and the BlueMap addon. 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) {
this.economy = economy;
Bukkit.getPluginManager().registerEvents(new ShopListener(), plugin);
poller = new ActionPoller(env, (from, to) -> {
Player a = Bukkit.getPlayer(from.uuid()), b = Bukkit.getPlayer(to.uuid());
if (a != null && b != null && essentials != null) essentials.requestTpa(a, b);
});
MapSource source = new MapSource() {
@Override public Map<String, Pos> warps() { return essentials == null ? Map.of() : essentials.warpsForMap(); }
@Override public Map<UUID, Map<String, Pos>> homes() { return essentials == null ? Map.of() : essentials.homesForMap(); }
@Override public Map<String, Pos> guildHomes() { return guilds == null ? Map.of() : guilds.guildHomesForMap(); }
@Override public Collection<ShopPoints.Shop> 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());
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). */
void shopCommand(Player player, String[] args) {
org.bukkit.block.Block looked = player.getTargetBlockExact(6);
org.bukkit.Location l = looked != null ? looked.getLocation() : player.getLocation().subtract(0, 1, 0);
shops.command(new PaperPlatform.PaperPlayer(player), args, new Pos(ScopenetPlugin.dimension(l.getWorld()), l.getBlockX(), l.getBlockY(), l.getBlockZ(), 0, 0));
}
/** Right-clicking a shop point opens the market or shop: the client mod's window if they have it, otherwise the chest GUI. */
private final class ShopListener implements org.bukkit.event.Listener {
@org.bukkit.event.EventHandler(ignoreCancelled = true)
public void onInteract(org.bukkit.event.player.PlayerInteractEvent event) {
if (event.getAction() != org.bukkit.event.block.Action.RIGHT_CLICK_BLOCK || event.getHand() != org.bukkit.inventory.EquipmentSlot.HAND) return;
org.bukkit.block.Block block = event.getClickedBlock();
if (block == null) return;
Optional<ShopPoints.Shop> shop = shops.at(ScopenetPlugin.dimension(block.getWorld()), block.getX(), block.getY(), block.getZ());
if (shop.isEmpty() || economy == null) return;
event.setCancelled(true);
Player player = event.getPlayer();
String kind = shop.get().kind().equals("market") ? "market" : "shop";
if (!link.open(new PaperPlatform.PaperPlayer(player), kind)) {
if (kind.equals("market")) economy.openMarket(player); else economy.openShop(player);
}
}
}
/** 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);
}
@@ -78,7 +154,13 @@ final class ClientLinkBridge implements PluginMessageListener {
return out;
}
@Override public Optional<Pos> safeSurface(String world, int x, int z) { return Optional.empty(); }
@Override public Pos worldSpawn(String world) { return new Pos(world, 0, 64, 0, 0, 0); }
@Override public Pos worldSpawn(String world) {
for (org.bukkit.World w : Bukkit.getWorlds()) {
if (ScopenetPlugin.dimension(w).equals(world)) { org.bukkit.Location l = w.getSpawnLocation(); return new Pos(world, l.getX(), l.getY(), l.getZ(), 0, 0); }
}
org.bukkit.Location l = Bukkit.getWorlds().get(0).getSpawnLocation();
return new Pos(ScopenetPlugin.dimension(Bukkit.getWorlds().get(0)), l.getX(), l.getY(), l.getZ(), 0, 0);
}
@Override public void runMain(Runnable task) {
if (Bukkit.isPrimaryThread()) task.run(); else Bukkit.getScheduler().runTask(plugin, task);
}
@@ -25,6 +25,8 @@ public final class ScopenetPlugin extends JavaPlugin implements Listener {
private ScopenetApiImpl api;
private CompatManager compat;
private ClientLinkBridge clientLink;
private EssentialsHandler essentialsHandler;
private GuildHandler guildHandler;
@Override public void onEnable() {
// Install the deny-by-default listener even if configuration is invalid.
@@ -78,6 +80,7 @@ public final class ScopenetPlugin extends JavaPlugin implements Listener {
// Register Essentials commands & GUI
{
EssentialsHandler essentials = new EssentialsHandler(this);
essentialsHandler = essentials;
getServer().getPluginManager().registerEvents(essentials, this);
String[] essCmds = {"spawn", "home", "sethome", "delhome", "back", "tpa", "tpaccept", "tpdeny", "rtp", "warp", "playtime"};
for (String c : essCmds) bindCommand(c, essentials);
@@ -94,12 +97,16 @@ public final class ScopenetPlugin extends JavaPlugin implements Listener {
// Register Guilds & Land Claims commands & GUI
{
GuildHandler guilds = new GuildHandler(this, integration, economy);
guildHandler = guilds;
getServer().getPluginManager().registerEvents(guilds, this);
bindCommand("guild", guilds);
bindCommand("claim", guilds);
bindCommand("unclaim", guilds);
}
// Physical shops, actions from the map, and (when BlueMap is installed) claims, pins and the player panel on it.
clientLink.startMap(essentialsHandler, guildHandler, economy);
// Reloading an auth plugin cannot silently admit existing sessions.
for (Player player : Bukkit.getOnlinePlayers()) player.kickPlayer("SCOPENET restarted. Please reconnect.");
getLogger().info("SCOPENET integration initialized. Configured modules:");
@@ -168,6 +175,11 @@ public final class ScopenetPlugin extends JavaPlugin implements Listener {
return world.getKey().toString();
}
/** {@code /market point ...}: manage shop points (blocks that open the market or shop when right-clicked). */
public void shopCommand(Player player, String[] args) {
if (clientLink != null) clientLink.shopCommand(player, args);
}
private void bindCommand(String name, org.bukkit.command.CommandExecutor executor) {
org.bukkit.command.PluginCommand cmd = getCommand(name);
if (cmd != null) {
@@ -109,6 +109,12 @@ public final class EconomyHandler implements CommandExecutor, Listener {
return true;
}
/** Open the player market window (used by physical shop points). */
public void openMarket(Player player) { handleMarket(player, new String[0], false); }
/** Open the server shop window (used by physical shop points). */
public void openShop(Player player) { handleShop(player); }
private void handleBalance(Player player) {
player.sendMessage(ChatColor.GRAY + "Fetching your balance...");
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
@@ -269,6 +275,10 @@ public final class EconomyHandler implements CommandExecutor, Listener {
/** {@code asGuild}: list on behalf of the player's guild, or buy with its bank (see /guild market). */
void handleMarket(Player player, String[] args, boolean asGuild) {
if (!asGuild && args.length >= 1 && args[0].equalsIgnoreCase("point")) {
plugin.shopCommand(player, java.util.Arrays.copyOfRange(args, 1, args.length));
return;
}
if (args.length >= 2 && args[0].equalsIgnoreCase("buy")) {
if (!asGuild && !player.hasPermission("scopenet.command.market.buy")) {
player.sendMessage(ChatColor.RED + "You do not have permission to buy from the market.");
@@ -172,6 +172,28 @@ public final class EssentialsHandler implements CommandExecutor, Listener {
player.sendMessage(ChatColor.GREEN + "Teleported to spawn.");
}
private static net.scopenet.core.Pos toPos(Location l) {
return new net.scopenet.core.Pos(l.getWorld() == null ? "minecraft:overworld" : ScopenetPlugin.dimension(l.getWorld()), l.getX(), l.getY(), l.getZ(), l.getYaw(), l.getPitch());
}
/** Warps as shown on the map. A copy. */
public Map<String, net.scopenet.core.Pos> warpsForMap() {
Map<String, net.scopenet.core.Pos> out = new TreeMap<>();
warps.forEach((name, loc) -> { if (loc.getWorld() != null) out.put(name, toPos(loc)); });
return out;
}
/** Every player's homes, for the map (only shown if the admin allows it). A copy. */
public Map<UUID, Map<String, net.scopenet.core.Pos>> homesForMap() {
Map<UUID, Map<String, net.scopenet.core.Pos>> out = new HashMap<>();
homes.forEach((id, byName) -> {
Map<String, net.scopenet.core.Pos> mine = new TreeMap<>();
byName.forEach((name, loc) -> { if (loc.getWorld() != null) mine.put(name, toPos(loc)); });
out.put(id, mine);
});
return out;
}
private void handleHome(Player player, String[] args) {
Map<String, Location> playerHomes = homes.computeIfAbsent(player.getUniqueId(), k -> new HashMap<>());
if (args.length == 0) {
@@ -277,6 +299,11 @@ public final class EssentialsHandler implements CommandExecutor, Listener {
return;
}
requestTpa(player, target);
}
/** Ask {@code target} to accept a teleport of {@code player} to them. Also used for requests made from the map. */
public void requestTpa(Player player, Player target) {
pendingTpa.put(target.getUniqueId(), player.getUniqueId());
tpaTimestamp.put(target.getUniqueId(), System.currentTimeMillis());
@@ -99,7 +99,7 @@ public final class GuildHandler implements CommandExecutor, Listener {
String sub = args[0].toLowerCase();
String permission = sub.equals("c") ? "chat" : sub;
if (java.util.Set.of("create", "leave", "claim", "unclaim", "map", "chat", "sethome", "home", "members", "bank", "sell", "market", "pay").contains(permission)
if (java.util.Set.of("create", "leave", "claim", "unclaim", "map", "chat", "sethome", "home", "members", "bank", "sell", "market", "pay", "invite", "accept", "decline").contains(permission)
&& !player.hasPermission("scopenet.command.guild." + permission)) {
player.sendMessage(ChatColor.RED + "You do not have permission to use /guild " + permission + ".");
return true;
@@ -124,6 +124,9 @@ public final class GuildHandler implements CommandExecutor, Listener {
case "members" -> handleMembers(player);
case "bank" -> bank.bank(player, Arrays.copyOfRange(args, 1, args.length));
case "pay" -> bank.pay(player, Arrays.copyOfRange(args, 1, args.length));
case "invite" -> handleInvite(player, args);
case "accept" -> handleInviteAnswer(player, args, true);
case "decline", "deny" -> handleInviteAnswer(player, args, false);
case "sell" -> economy.handleSell(player, Arrays.copyOfRange(args, 1, args.length), true);
case "market" -> economy.handleMarket(player, Arrays.copyOfRange(args, 1, args.length), true);
default -> sendGuildHelp(player);
@@ -134,6 +137,46 @@ public final class GuildHandler implements CommandExecutor, Listener {
return false;
}
private void handleInvite(Player player, String[] args) {
if (args.length < 2) { player.sendMessage(ChatColor.RED + "Usage: /guild invite <player>"); return; }
com.google.gson.JsonObject body = new com.google.gson.JsonObject();
body.addProperty("uuid", player.getUniqueId().toString());
body.addProperty("target", args[1]);
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
try {
com.google.gson.JsonObject r = integration.client().post("guilds/invite/send", body);
player.sendMessage(ChatColor.GREEN + "Invitation sent to " + ChatColor.YELLOW + (r.has("player") ? r.get("player").getAsString() : args[1]) + ChatColor.GREEN + ".");
} catch (Exception e) {
player.sendMessage(ChatColor.RED + "Could not invite: " + e.getMessage());
}
});
}
private void handleInviteAnswer(Player player, String[] args, boolean accept) {
com.google.gson.JsonObject body = new com.google.gson.JsonObject();
body.addProperty("uuid", player.getUniqueId().toString());
body.addProperty("tag", args.length > 1 ? args[1] : "");
body.addProperty("accept", accept);
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
try {
com.google.gson.JsonObject r = integration.client().post("guilds/invite/respond", body);
if (accept) integration.client().requestClaimRefresh();
player.sendMessage(accept
? ChatColor.GREEN + "Welcome to " + ChatColor.YELLOW + "[" + r.get("tag").getAsString() + "] " + r.get("guild").getAsString() + ChatColor.GREEN + "!"
: ChatColor.YELLOW + "Invitation declined.");
} catch (Exception e) {
player.sendMessage(ChatColor.RED + e.getMessage());
}
});
}
/** Guild homes by guild id, for the map. */
public Map<String, net.scopenet.core.Pos> guildHomesForMap() {
Map<String, net.scopenet.core.Pos> out = new java.util.TreeMap<>();
guildHomes.forEach((id, l) -> { if (l.getWorld() != null) out.put(id, new net.scopenet.core.Pos(ScopenetPlugin.dimension(l.getWorld()), l.getX(), l.getY(), l.getZ(), l.getYaw(), l.getPitch())); });
return out;
}
private void sendGuildHelp(Player player) {
player.sendMessage(ChatColor.GOLD + "=== SCOPENET Guild Commands ===");
player.sendMessage(ChatColor.YELLOW + "/guild" + ChatColor.GRAY + " - Open interactive Guild Dashboard GUI");
@@ -148,6 +191,8 @@ public final class GuildHandler implements CommandExecutor, Listener {
player.sendMessage(ChatColor.YELLOW + "/guild sell [hand]" + ChatColor.GRAY + " - Sell items to the shop for the guild bank");
player.sendMessage(ChatColor.YELLOW + "/guild market sell <price> | buy <#id>" + ChatColor.GRAY + " - Trade on the market as a guild");
player.sendMessage(ChatColor.YELLOW + "/guild pay <tag> <amount>" + ChatColor.GRAY + " - Pay another guild");
player.sendMessage(ChatColor.YELLOW + "/guild invite <player>" + ChatColor.GRAY + " - Invite a player (leaders and officers)");
player.sendMessage(ChatColor.YELLOW + "/guild accept [tag] | decline [tag]" + ChatColor.GRAY + " - Answer a guild invitation");
}
private void handleClaim(Player player) {
@@ -94,6 +94,17 @@ 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
@@ -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]
softdepend: [LuckPerms, PlaceholderAPI, Vault, CoreProtect, WorldGuard, WorldEdit, spark, BlueMap]
commands:
scopenet:
@@ -242,6 +242,18 @@ permissions:
scopenet.command.guild.pay:
description: Pay another guild (/guild pay)
default: true
scopenet.command.guild.invite:
description: Invite a player to your guild (/guild invite); the panel also requires leader or officer
default: true
scopenet.command.guild.accept:
description: Accept a guild invitation (/guild accept)
default: true
scopenet.command.guild.decline:
description: Decline a guild invitation (/guild decline)
default: true
scopenet.command.market.point:
description: Create and remove shop points, blocks that open the market or shop (/market point)
default: op
scopenet.command.sell.hand:
description: Use /sell hand
default: true
@@ -278,6 +290,9 @@ permissions:
scopenet.command.guild.market.sell: true
scopenet.command.guild.market.buy: true
scopenet.command.guild.pay: true
scopenet.command.guild.invite: true
scopenet.command.guild.accept: true
scopenet.command.guild.decline: true
scopenet.command.*:
description: Every SCOPENET command and subcommand, including admin ones
default: false
@@ -328,8 +343,12 @@ permissions:
scopenet.command.guild.market.sell: true
scopenet.command.guild.market.buy: true
scopenet.command.guild.pay: true
scopenet.command.guild.invite: true
scopenet.command.guild.accept: true
scopenet.command.guild.decline: true
scopenet.command.sell.hand: true
scopenet.command.market.sell: true
scopenet.command.market.point: true
scopenet.command.market.buy: true
scopenet.command.economy.*:
description: All economy commands