Fabric 1.20.1: server features (commands, LuckPerms, Placeholder API, economy API, client link) and client mod (HUD, editor, shop and market windows); Paper client link; docs and examples
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DjMbLQujBHunCCu5GpsHaT
This commit is contained in:
43 files changed
+2112
-42
No files matched your search
@@ -0,0 +1,108 @@
|
||||
package net.scopenet.paper;
|
||||
|
||||
import net.scopenet.core.*;
|
||||
import net.scopenet.integration.Integration;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.bukkit.plugin.messaging.PluginMessageListener;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.util.*;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
/**
|
||||
* Talks to the optional SCOPENET client mod (HUD, claim overlay, shop and market windows) through plugin messaging,
|
||||
* using the same shared logic the Fabric server mod uses. Display only: the client's actions are ordinary commands.
|
||||
*/
|
||||
final class ClientLinkBridge implements PluginMessageListener {
|
||||
private final ScopenetPlugin plugin;
|
||||
private final Env env;
|
||||
private final PlayerCache cache;
|
||||
private final ClientLink link;
|
||||
private final Placeholders placeholders;
|
||||
|
||||
ClientLinkBridge(ScopenetPlugin plugin, Integration integration) {
|
||||
this.plugin = plugin;
|
||||
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(),
|
||||
plugin.getConfig().getBoolean("clientlink.enabled", true), plugin.getConfig().getString("economy.currency-symbol", "$"));
|
||||
Logger log = plugin.getLogger();
|
||||
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);
|
||||
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);
|
||||
});
|
||||
}
|
||||
|
||||
ClientLink link() { return link; }
|
||||
|
||||
void start() {
|
||||
Bukkit.getMessenger().registerOutgoingPluginChannel(plugin, Wire.S2C);
|
||||
Bukkit.getMessenger().registerIncomingPluginChannel(plugin, Wire.C2S, this);
|
||||
Bukkit.getScheduler().runTaskTimer(plugin, () -> {
|
||||
cache.tick(p -> link.pushState(p));
|
||||
link.tick();
|
||||
}, 40, 20);
|
||||
}
|
||||
|
||||
void stop() {
|
||||
Bukkit.getMessenger().unregisterOutgoingPluginChannel(plugin, Wire.S2C);
|
||||
Bukkit.getMessenger().unregisterIncomingPluginChannel(plugin, Wire.C2S, this);
|
||||
}
|
||||
|
||||
void quit(Player player) { link.disconnected(player.getUniqueId()); }
|
||||
|
||||
@Override public void onPluginMessageReceived(String channel, Player player, byte[] message) {
|
||||
if (!Wire.C2S.equals(channel)) return;
|
||||
link.receive(new PaperPlatform.PaperPlayer(player), message);
|
||||
}
|
||||
|
||||
/** Panel events (level-ups, achievements, guild changes) become toasts on clients. Any thread. */
|
||||
void panelEvents(com.google.gson.JsonArray events) { link.onPanelEvents(events); }
|
||||
|
||||
/** Bukkit adapters for the shared logic. Only what the client link needs is real. */
|
||||
static final class PaperPlatform implements Platform {
|
||||
private final ScopenetPlugin plugin;
|
||||
PaperPlatform(ScopenetPlugin plugin) { this.plugin = plugin; }
|
||||
|
||||
@Override public Optional<CorePlayer> player(UUID uuid) { return Optional.ofNullable(Bukkit.getPlayer(uuid)).map(PaperPlayer::new); }
|
||||
@Override public Optional<CorePlayer> playerByName(String name) { return Optional.ofNullable(Bukkit.getPlayerExact(name)).map(PaperPlayer::new); }
|
||||
@Override public Collection<? extends CorePlayer> online() {
|
||||
List<CorePlayer> out = new ArrayList<>();
|
||||
for (Player p : Bukkit.getOnlinePlayers()) out.add(new PaperPlayer(p));
|
||||
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 void runMain(Runnable task) {
|
||||
if (Bukkit.isPrimaryThread()) task.run(); else Bukkit.getScheduler().runTask(plugin, task);
|
||||
}
|
||||
@Override public void runAsync(Runnable task) { Bukkit.getScheduler().runTaskAsynchronously(plugin, task); }
|
||||
|
||||
record PaperPlayer(Player p) implements CorePlayer {
|
||||
@Override public UUID uuid() { return p.getUniqueId(); }
|
||||
@Override public String name() { return p.getName(); }
|
||||
@Override public Pos pos() {
|
||||
org.bukkit.Location l = p.getLocation();
|
||||
return new Pos(ScopenetPlugin.dimension(l.getWorld()), l.getX(), l.getY(), l.getZ(), l.getYaw(), l.getPitch());
|
||||
}
|
||||
@Override public void send(String message) { p.sendMessage(message); }
|
||||
@Override public void teleport(Pos destination) { }
|
||||
@Override public boolean hasPermission(String node) { return p.hasPermission(node); }
|
||||
@Override public long playtimeSeconds() { return p.getStatistic(org.bukkit.Statistic.PLAY_ONE_MINUTE) / 20L; }
|
||||
@Override public Optional<Item> heldItem() {
|
||||
ItemStack hand = p.getInventory().getItemInMainHand();
|
||||
if (hand.getType() == Material.AIR) return Optional.empty();
|
||||
String name = hand.hasItemMeta() && hand.getItemMeta().hasDisplayName() ? hand.getItemMeta().getDisplayName() : hand.getType().name().replace('_', ' ');
|
||||
return Optional.of(new Item(hand.getType().name(), name, hand.getAmount(), ""));
|
||||
}
|
||||
@Override public void clearHeld() { }
|
||||
@Override public void give(Item item) { }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -24,6 +24,7 @@ public final class ScopenetPlugin extends JavaPlugin implements Listener {
|
||||
private EconomyHandler economy;
|
||||
private ScopenetApiImpl api;
|
||||
private CompatManager compat;
|
||||
private ClientLinkBridge clientLink;
|
||||
|
||||
@Override public void onEnable() {
|
||||
// Install the deny-by-default listener even if configuration is invalid.
|
||||
@@ -58,7 +59,11 @@ public final class ScopenetPlugin extends JavaPlugin implements Listener {
|
||||
api = new ScopenetApiImpl(integration.client());
|
||||
ScopenetApiProvider.register(api);
|
||||
getServer().getServicesManager().register(ScopenetApi.class, api, this, org.bukkit.plugin.ServicePriority.Normal);
|
||||
integration.onNotifications(new EventBridge(this, api, getLogger())::accept);
|
||||
// Optional client mod (HUD, claim overlay, shop and market windows).
|
||||
clientLink = new ClientLinkBridge(this, integration);
|
||||
clientLink.start();
|
||||
EventBridge events = new EventBridge(this, api, getLogger());
|
||||
integration.onNotifications(list -> { events.accept(list); clientLink.panelEvents(list); });
|
||||
Bukkit.getScheduler().runTaskTimer(this, this::warmProfiles, 100, 20 * 20);
|
||||
|
||||
// Optional integrations (LuckPerms, PlaceholderAPI, Vault, CoreProtect, WorldGuard, Spark) are found and
|
||||
@@ -180,6 +185,7 @@ public final class ScopenetPlugin extends JavaPlugin implements Listener {
|
||||
|
||||
@Override public void onDisable() {
|
||||
if (compat != null) compat.stop();
|
||||
if (clientLink != null) clientLink.stop();
|
||||
if (api != null) {
|
||||
getServer().getServicesManager().unregister(ScopenetApi.class, api);
|
||||
ScopenetApiProvider.unregister(api);
|
||||
@@ -213,6 +219,7 @@ public final class ScopenetPlugin extends JavaPlugin implements Listener {
|
||||
public void leave(PlayerQuitEvent event) {
|
||||
deniedAt.remove(event.getPlayer().getUniqueId());
|
||||
if (api != null) api.forget(event.getPlayer().getUniqueId());
|
||||
if (clientLink != null) clientLink.quit(event.getPlayer());
|
||||
if (integration != null) integration.activity.leave(event.getPlayer().getUniqueId(), event.getPlayer().getName());
|
||||
}
|
||||
|
||||
|
||||
@@ -269,17 +269,21 @@ 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 >= 2 && args[0].equalsIgnoreCase("buy")) {
|
||||
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.");
|
||||
return;
|
||||
}
|
||||
long listing;
|
||||
try { listing = Long.parseLong(args[1].replace("#", "")); } catch (NumberFormatException e) {
|
||||
player.sendMessage(ChatColor.RED + "Usage: /guild market buy <listing number> (shown as Listing #n in /market)");
|
||||
player.sendMessage(ChatColor.RED + "Usage: /" + (asGuild ? "guild market" : "market") + " buy <listing number> (shown as Listing #n in /market)");
|
||||
return;
|
||||
}
|
||||
JsonObject payload = new JsonObject();
|
||||
payload.addProperty("listing_id", listing);
|
||||
payload.addProperty("buyer_uuid", player.getUniqueId().toString());
|
||||
payload.addProperty("buyer_name", player.getName());
|
||||
payload.addProperty("as_guild", true);
|
||||
if (asGuild) payload.addProperty("as_guild", true);
|
||||
operations.enqueue(player, "economy/market/buy", payload, Collections.emptyList(), null);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -93,3 +93,12 @@ integrations:
|
||||
spark:
|
||||
enabled: true
|
||||
report-interval-seconds: 30
|
||||
|
||||
# ==============================================================================
|
||||
# Client mod link
|
||||
# Sends balance, level, guild and claim info to players who have the optional SCOPENET
|
||||
# client mod (HUD widgets, claim overlay, shop and market windows). Players without it
|
||||
# are unaffected. Display only: everything the client does is an ordinary command.
|
||||
# ==============================================================================
|
||||
clientlink:
|
||||
enabled: true
|
||||
Reference in new issue
Block a user