BlueMap addon, shop points, map actions wired into Paper and Fabric; client opens shop windows
This commit is contained in:
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
|
||||
|
||||
Reference in new issue
Block a user