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
@@ -1,11 +1,22 @@
|
||||
package net.scopenet.fabric;
|
||||
|
||||
import net.fabricmc.api.DedicatedServerModInitializer;
|
||||
import net.fabricmc.loader.api.FabricLoader;
|
||||
|
||||
/** Server-only; lifecycle and admission hooks are shared mixins. */
|
||||
/** Server-only; lifecycle and admission hooks are shared mixins. Commands and extras need Fabric API (1.20.1 build). */
|
||||
public final class ScopenetFabric implements DedicatedServerModInitializer {
|
||||
@Override public void onInitializeServer() {
|
||||
if (Boolean.getBoolean("scopenet.verifyMixins"))
|
||||
org.spongepowered.asm.mixin.MixinEnvironment.getCurrentEnvironment().audit();
|
||||
if (!FabricLoader.getInstance().isModLoaded("fabric-api")) return;
|
||||
try {
|
||||
Class<?> features = Class.forName("net.scopenet.fabric.features.FabricFeatures");
|
||||
((Runnable) features.getDeclaredConstructor().newInstance()).run();
|
||||
} catch (ClassNotFoundException e) {
|
||||
// This build (another Minecraft version) only has the core integration.
|
||||
} catch (Throwable t) {
|
||||
System.err.println("[SCOPENET] Commands and extras could not start: " + t);
|
||||
t.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,5 +6,6 @@
|
||||
"environment": "server",
|
||||
"entrypoints": { "server": ["net.scopenet.fabric.ScopenetFabric"] },
|
||||
"mixins": ["scopenet.mixins.json"],
|
||||
"depends": { "fabricloader": ">=0.16.10", "minecraft": "${mc}", "java": ">=${java}" }
|
||||
"depends": { "fabricloader": ">=0.16.10", "minecraft": "${mc}", "java": ">=${java}" },
|
||||
"suggests": { "fabric-api": "*", "luckperms": "*", "placeholder-api": "*" }
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package net.scopenet.fabric.api;
|
||||
|
||||
import com.google.gson.JsonObject;
|
||||
import net.scopenet.core.Panel;
|
||||
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
/**
|
||||
* SCOPENET's economy for other mods (the Fabric stand-in for Vault). Balances live in the panel, so every call is
|
||||
* asynchronous. Every change carries an operation id, so a retried call never pays twice.
|
||||
*
|
||||
* <pre>{@code
|
||||
* ScopenetEconomy.get().ifPresent(eco -> eco.balance(uuid).thenAccept(b -> ...));
|
||||
* }</pre>
|
||||
*/
|
||||
public final class ScopenetEconomy {
|
||||
private static final AtomicReference<ScopenetEconomy> INSTANCE = new AtomicReference<>();
|
||||
|
||||
private final Panel panel;
|
||||
private final java.util.concurrent.Executor io;
|
||||
|
||||
public ScopenetEconomy(Panel panel, java.util.concurrent.Executor io) { this.panel = panel; this.io = io; }
|
||||
|
||||
/** Empty until the server has started and SCOPENET economy is enabled. */
|
||||
public static java.util.Optional<ScopenetEconomy> get() { return java.util.Optional.ofNullable(INSTANCE.get()); }
|
||||
|
||||
public static void install(ScopenetEconomy economy) { INSTANCE.set(economy); }
|
||||
|
||||
public static void uninstall() { INSTANCE.set(null); }
|
||||
|
||||
public CompletableFuture<Double> balance(UUID player, String name) {
|
||||
JsonObject body = new JsonObject();
|
||||
body.addProperty("uuid", player.toString());
|
||||
body.addProperty("username", name);
|
||||
return CompletableFuture.supplyAsync(() -> {
|
||||
try { return panel.call("economy/balance", body).getAsJsonObject().get("balance").getAsDouble(); }
|
||||
catch (Exception e) { throw new java.util.concurrent.CompletionException(e); }
|
||||
}, io);
|
||||
}
|
||||
|
||||
/** Add (positive) or remove (negative) money. Fails, without changing anything, if the player can't afford it. Returns the new balance. */
|
||||
public CompletableFuture<Double> change(UUID player, String name, double delta, String reason) {
|
||||
JsonObject body = new JsonObject();
|
||||
body.addProperty("operation_id", UUID.randomUUID().toString());
|
||||
body.addProperty("uuid", player.toString());
|
||||
body.addProperty("username", name);
|
||||
body.addProperty("delta", delta);
|
||||
body.addProperty("description", reason == null || reason.isBlank() ? "Other mod" : reason);
|
||||
return CompletableFuture.supplyAsync(() -> {
|
||||
try { return panel.call("economy/adjust", body).getAsJsonObject().get("balance").getAsDouble(); }
|
||||
catch (Exception e) { throw new java.util.concurrent.CompletionException(e); }
|
||||
}, io);
|
||||
}
|
||||
|
||||
public CompletableFuture<Double> deposit(UUID player, String name, double amount, String reason) { return change(player, name, Math.abs(amount), reason); }
|
||||
|
||||
public CompletableFuture<Double> withdraw(UUID player, String name, double amount, String reason) { return change(player, name, -Math.abs(amount), reason); }
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package net.scopenet.fabric.features;
|
||||
|
||||
import net.scopenet.core.EssentialsConfig;
|
||||
import net.scopenet.core.Features;
|
||||
import net.scopenet.integration.Settings;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.Reader;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Properties;
|
||||
|
||||
/** The Fabric-only keys in config/scopenet.properties (the shared keys are read by {@link Settings}). */
|
||||
final class FabricConfig {
|
||||
static final Path FILE = Path.of("config", "scopenet.properties");
|
||||
private final Properties p = new Properties();
|
||||
|
||||
static FabricConfig load() {
|
||||
FabricConfig c = new FabricConfig();
|
||||
if (Files.exists(FILE)) {
|
||||
try (Reader in = Files.newBufferedReader(FILE, StandardCharsets.UTF_8)) { c.p.load(in); }
|
||||
catch (IOException e) { System.err.println("[SCOPENET] Could not read " + FILE + ": " + e.getMessage()); }
|
||||
}
|
||||
return c;
|
||||
}
|
||||
|
||||
private int integer(String key, int fallback) {
|
||||
try { return Integer.parseInt(p.getProperty(key, String.valueOf(fallback)).trim()); } catch (NumberFormatException e) { return fallback; }
|
||||
}
|
||||
|
||||
boolean bool(String key, boolean fallback) { return Boolean.parseBoolean(p.getProperty(key, String.valueOf(fallback)).trim()); }
|
||||
String text(String key, String fallback) { return p.getProperty(key, fallback).trim(); }
|
||||
|
||||
EssentialsConfig essentials() {
|
||||
return new EssentialsConfig(Math.max(0, integer("essentials.max_homes", 5)), Math.max(10, integer("essentials.rtp_radius", 2500)),
|
||||
Math.max(5, integer("essentials.tpa_timeout_seconds", 60)) * 1000L, 15);
|
||||
}
|
||||
|
||||
Features features(Settings s) {
|
||||
return new Features(s.essentialsEnabled(), s.economyEnabled(), s.guildsEnabled(), s.socialEnabled(), s.landClaimingEnabled(),
|
||||
bool("clientlink.enabled", true), text("economy.currency_symbol", "$"));
|
||||
}
|
||||
|
||||
/** {@code permissions.default_level}: "all" lets everyone use commands unless LuckPerms says otherwise, "op" limits them to operators. */
|
||||
boolean everyoneByDefault() { return !text("permissions.default_level", "all").equalsIgnoreCase("op"); }
|
||||
}
|
||||
+214
@@ -0,0 +1,214 @@
|
||||
package net.scopenet.fabric.features;
|
||||
|
||||
import com.google.gson.JsonArray;
|
||||
import com.mojang.brigadier.CommandDispatcher;
|
||||
import com.mojang.brigadier.arguments.StringArgumentType;
|
||||
import com.mojang.brigadier.builder.LiteralArgumentBuilder;
|
||||
import com.mojang.brigadier.context.CommandContext;
|
||||
import com.mojang.brigadier.suggestion.Suggestions;
|
||||
import com.mojang.brigadier.suggestion.SuggestionsBuilder;
|
||||
import net.fabricmc.fabric.api.entity.event.v1.ServerLivingEntityEvents;
|
||||
import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents;
|
||||
import net.fabricmc.fabric.api.event.lifecycle.v1.ServerTickEvents;
|
||||
import net.fabricmc.fabric.api.networking.v1.PacketByteBufs;
|
||||
import net.fabricmc.fabric.api.networking.v1.ServerPlayConnectionEvents;
|
||||
import net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking;
|
||||
import net.minecraft.commands.CommandSourceStack;
|
||||
import net.minecraft.commands.Commands;
|
||||
import net.minecraft.network.FriendlyByteBuf;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.scopenet.core.*;
|
||||
import net.scopenet.fabric.api.ScopenetEconomy;
|
||||
import net.scopenet.integration.Integration;
|
||||
import net.scopenet.integration.Settings;
|
||||
import net.scopenet.minecraft.Bridge;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
/**
|
||||
* Everything beyond sign-in on Fabric 1.20.1: the shared commands (homes, warps, TPA, RTP, economy, guilds…), LuckPerms
|
||||
* permissions, Placeholder API, the economy API for other mods, and the link to the optional client mod.
|
||||
* Runs when Fabric API is installed; all behaviour comes from {@code net.scopenet.core}.
|
||||
*/
|
||||
public final class FabricFeatures implements Runnable {
|
||||
private static final Logger LOG = Logger.getLogger("SCOPENET");
|
||||
private static final ResourceLocation TO_CLIENT = new ResourceLocation(Wire.S2C);
|
||||
private static final ResourceLocation FROM_CLIENT = new ResourceLocation(Wire.C2S);
|
||||
|
||||
private MinecraftServer server;
|
||||
private FabricPlatform platform;
|
||||
private Env env;
|
||||
private CommandSet commands;
|
||||
private PlayerCache cache;
|
||||
private ClientLink link;
|
||||
private Perms perms;
|
||||
private FabricConfig config;
|
||||
private int ticks;
|
||||
private String placeholderStatus = "off";
|
||||
|
||||
CommandSet commandSet() { return commands; }
|
||||
|
||||
@Override public void run() {
|
||||
ServerLifecycleEvents.SERVER_STARTED.register(this::started);
|
||||
ServerLifecycleEvents.SERVER_STOPPING.register(s -> stopping());
|
||||
ServerTickEvents.END_SERVER_TICK.register(s -> tick());
|
||||
ServerPlayConnectionEvents.DISCONNECT.register((handler, s) -> { if (link != null) link.disconnected(handler.getPlayer().getUUID()); });
|
||||
// /back also returns you to where you died.
|
||||
ServerLivingEntityEvents.ALLOW_DEATH.register((entity, source, amount) -> {
|
||||
if (entity instanceof ServerPlayer p && commands != null) commands.essentials.remember(p.getUUID(), platform.wrap(p).pos());
|
||||
return true;
|
||||
});
|
||||
ServerPlayNetworking.registerGlobalReceiver(FROM_CLIENT, (s, player, handler, buf, sender) -> {
|
||||
byte[] data = new byte[buf.readableBytes()];
|
||||
buf.readBytes(data);
|
||||
s.execute(() -> { if (link != null) link.receive(platform.wrap(player), data); });
|
||||
});
|
||||
}
|
||||
|
||||
// ---- lifecycle -------------------------------------------------------------------------
|
||||
|
||||
private void started(MinecraftServer s) {
|
||||
Integration integration = Bridge.integration();
|
||||
if (integration == null) { LOG.warning("SCOPENET is not connected to a panel; commands are off."); return; }
|
||||
server = s;
|
||||
config = FabricConfig.load();
|
||||
perms = new Perms(config.everyoneByDefault());
|
||||
platform = new FabricPlatform(s, perms);
|
||||
Path dir = Path.of("config", "scopenet");
|
||||
env = new Env(platform, new PanelAdapter(integration), config.features(integration.settings()), dir, System::currentTimeMillis, new Random(), LOG);
|
||||
commands = new CommandSet(env, config.essentials());
|
||||
cache = new PlayerCache(env);
|
||||
link = new ClientLink(env, cache, s.getMotd(), (player, bytes) -> {
|
||||
FriendlyByteBuf buf = PacketByteBufs.create();
|
||||
buf.writeBytes(bytes);
|
||||
ServerPlayer target = s.getPlayerList().getPlayer(player.uuid());
|
||||
if (target != null && ServerPlayNetworking.canSend(target, TO_CLIENT)) ServerPlayNetworking.send(target, TO_CLIENT, buf);
|
||||
});
|
||||
integration.onNotifications(events -> link.onPanelEvents(events));
|
||||
Bridge.claimBypass = p -> perms.has(p, "scopenet.claims.bypass");
|
||||
|
||||
registerCommands(s.getCommands().getDispatcher());
|
||||
registerPlaceholders();
|
||||
ScopenetEconomy.install(new ScopenetEconomy(env.panel, r -> platform.runAsync(r)));
|
||||
LOG.info("SCOPENET commands ready (" + commands.all().size() + " commands, "
|
||||
+ (Perms.luckPermsPresent() ? "LuckPerms" : "default permissions") + ").");
|
||||
}
|
||||
|
||||
private void stopping() {
|
||||
ScopenetEconomy.uninstall();
|
||||
Bridge.claimBypass = p -> false;
|
||||
if (platform != null) platform.shutdown();
|
||||
commands = null;
|
||||
}
|
||||
|
||||
private void registerPlaceholders() {
|
||||
if (!config.bool("placeholders.enabled", true)) { placeholderStatus = "off (config)"; return; }
|
||||
if (!net.fabricmc.loader.api.FabricLoader.getInstance().isModLoaded("placeholder-api")) { placeholderStatus = "Placeholder API not installed"; return; }
|
||||
try {
|
||||
int n = PlaceholderApiModule.register(new Placeholders(env, cache));
|
||||
placeholderStatus = "active (" + n + ")";
|
||||
} catch (Throwable t) {
|
||||
placeholderStatus = "failed: " + t;
|
||||
LOG.warning("Placeholder API could not be used: " + t);
|
||||
}
|
||||
}
|
||||
|
||||
private void tick() {
|
||||
if (commands == null) return;
|
||||
ticks++;
|
||||
if (ticks % 20 == 0) {
|
||||
cache.tick(p -> link.pushState(p));
|
||||
link.tick();
|
||||
}
|
||||
if (ticks % 40 == 0) commands.tick();
|
||||
}
|
||||
|
||||
// ---- commands --------------------------------------------------------------------------
|
||||
|
||||
private void registerCommands(CommandDispatcher<CommandSourceStack> dispatcher) {
|
||||
List<CoreCommand> all = new ArrayList<>(commands.all());
|
||||
all.add(new ScopenetCommand(this));
|
||||
for (CoreCommand command : all) {
|
||||
List<String> names = new ArrayList<>();
|
||||
names.add(command.name());
|
||||
names.addAll(command.aliases());
|
||||
for (String name : names) {
|
||||
LiteralArgumentBuilder<CommandSourceStack> root = Commands.literal(name)
|
||||
.executes(ctx -> execute(ctx, command, ""))
|
||||
.then(Commands.argument("args", StringArgumentType.greedyString())
|
||||
.suggests((ctx, builder) -> suggest(ctx, command, builder))
|
||||
.executes(ctx -> execute(ctx, command, StringArgumentType.getString(ctx, "args"))));
|
||||
dispatcher.register(root);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private int execute(CommandContext<CommandSourceStack> ctx, CoreCommand command, String argText) {
|
||||
ServerPlayer sp = ctx.getSource().getPlayer();
|
||||
if (sp == null) { ctx.getSource().sendFailure(Component.literal("Only players can use this command.")); return 0; }
|
||||
String[] args = argText.isBlank() ? new String[0] : argText.trim().split("\\s+");
|
||||
CorePlayer player = platform.wrap(sp);
|
||||
try {
|
||||
command.run(player, args);
|
||||
} catch (RuntimeException e) {
|
||||
LOG.warning("/" + command.name() + " failed: " + e);
|
||||
player.send(Format.RED + "Something went wrong running that command.");
|
||||
return 0;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
private CompletableFuture<Suggestions> suggest(CommandContext<CommandSourceStack> ctx, CoreCommand command, SuggestionsBuilder builder) {
|
||||
ServerPlayer sp = ctx.getSource().getPlayer();
|
||||
if (sp != null) {
|
||||
String before = builder.getInput().substring(0, builder.getStart()).stripLeading();
|
||||
if (before.startsWith("/")) before = before.substring(1);
|
||||
String[] tokens = before.isBlank() ? new String[0] : before.split(" ");
|
||||
List<String> typed = new ArrayList<>(Arrays.asList(tokens).subList(Math.min(1, tokens.length), tokens.length));
|
||||
typed.add(builder.getRemaining());
|
||||
String partial = builder.getRemaining().toLowerCase(Locale.ROOT);
|
||||
for (String candidate : command.complete(platform.wrap(sp), typed.toArray(new String[0]))) {
|
||||
if (candidate.toLowerCase(Locale.ROOT).startsWith(partial)) builder.suggest(candidate);
|
||||
}
|
||||
}
|
||||
return builder.buildFuture();
|
||||
}
|
||||
|
||||
// ---- /scopenet -------------------------------------------------------------------------
|
||||
|
||||
void sendStatus(CorePlayer p) {
|
||||
Integration integration = Bridge.integration();
|
||||
Settings s = integration.settings();
|
||||
p.send(Format.GOLD + "=== SCOPENET Status ===");
|
||||
p.send(Format.GRAY + "Panel: " + Format.WHITE + s.panel() + Format.GRAY + " Token: " + (s.token().isEmpty() ? Format.RED + "missing" : Format.GREEN + "set"));
|
||||
Features f = env.features;
|
||||
p.send(Format.GRAY + "Essentials: " + flag(f.essentials()) + Format.GRAY + " Economy: " + flag(f.economy()) + Format.GRAY + " Guilds: " + flag(f.guilds())
|
||||
+ Format.GRAY + " Claims: " + flag(f.landClaiming()) + Format.GRAY + " Client link: " + flag(f.clientLink()));
|
||||
p.send(Format.GRAY + "Claim index: " + (integration.client().claims().loaded() ? Format.GREEN + integration.client().claims().claimCount() + " claims" : Format.YELLOW + "loading"));
|
||||
p.send(Format.GRAY + "Permissions: " + Format.WHITE + (Perms.luckPermsPresent() ? "LuckPerms" : "defaults (" + (config.everyoneByDefault() ? "everyone" : "operators") + ")"));
|
||||
p.send(Format.GRAY + "Placeholder API: " + Format.WHITE + placeholderStatus);
|
||||
p.send(Format.GRAY + "Pending transactions: " + Format.WHITE + commands.jobs.pending());
|
||||
}
|
||||
|
||||
private static String flag(boolean on) { return on ? Format.GREEN + "on" : Format.RED + "off"; }
|
||||
|
||||
void reload(CorePlayer p) {
|
||||
try {
|
||||
Settings next = Settings.load(FabricConfig.FILE);
|
||||
Bridge.integration().reload(next);
|
||||
config = FabricConfig.load();
|
||||
perms = new Perms(config.everyoneByDefault());
|
||||
env.features = config.features(next);
|
||||
commands.essentials.configure(config.essentials());
|
||||
p.send(Format.GREEN + "SCOPENET config reloaded.");
|
||||
} catch (Exception e) {
|
||||
p.send(Format.RED + "Reload failed: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package net.scopenet.fabric.features;
|
||||
|
||||
import net.minecraft.core.registries.BuiltInRegistries;
|
||||
import net.minecraft.nbt.CompoundTag;
|
||||
import net.minecraft.nbt.TagParser;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.scopenet.core.Item;
|
||||
|
||||
/** ItemStack <-> the shared {@link Item}. The exact stack (enchantments, names, NBT) travels as SNBT in {@code data}. */
|
||||
final class FabricItems {
|
||||
private FabricItems() {}
|
||||
|
||||
static Item toCore(ItemStack stack) {
|
||||
String registryId = BuiltInRegistries.ITEM.getKey(stack.getItem()).toString();
|
||||
return new Item(Item.panelId(registryId), stack.getHoverName().getString(), stack.getCount(), stack.save(new CompoundTag()).toString());
|
||||
}
|
||||
|
||||
/** The stack for an item, restoring the exact NBT when it came from this platform; otherwise rebuilt from id and count. */
|
||||
static ItemStack fromCore(Item item) {
|
||||
if (!item.data().isEmpty()) {
|
||||
try {
|
||||
ItemStack stack = ItemStack.of(TagParser.parseTag(item.data()));
|
||||
if (!stack.isEmpty()) return stack;
|
||||
} catch (Exception ignored) {
|
||||
// Another platform's serialisation: fall through to id and count.
|
||||
}
|
||||
}
|
||||
ResourceLocation id = ResourceLocation.tryParse(Item.registryId(item.id()));
|
||||
net.minecraft.world.item.Item type = id == null ? null : BuiltInRegistries.ITEM.get(id);
|
||||
if (type == null || type == net.minecraft.world.item.Items.AIR) return ItemStack.EMPTY;
|
||||
return new ItemStack(type, Math.max(1, item.count()));
|
||||
}
|
||||
}
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
package net.scopenet.fabric.features;
|
||||
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.levelgen.Heightmap;
|
||||
import net.scopenet.core.CorePlayer;
|
||||
import net.scopenet.core.Platform;
|
||||
import net.scopenet.core.Pos;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
/** The shared logic's view of a running Fabric server. */
|
||||
final class FabricPlatform implements Platform {
|
||||
private final MinecraftServer server;
|
||||
private final Perms perms;
|
||||
private final ExecutorService io = Executors.newFixedThreadPool(4, r -> {
|
||||
Thread t = new Thread(r, "scopenet-io");
|
||||
t.setDaemon(true);
|
||||
return t;
|
||||
});
|
||||
|
||||
FabricPlatform(MinecraftServer server, Perms perms) { this.server = server; this.perms = perms; }
|
||||
|
||||
void shutdown() { io.shutdownNow(); }
|
||||
|
||||
CorePlayer wrap(ServerPlayer p) { return new FabricPlayer(p, this, perms); }
|
||||
|
||||
ServerLevel level(String id) {
|
||||
ResourceLocation location = ResourceLocation.tryParse(id);
|
||||
return location == null ? null : server.getLevel(ResourceKey.create(Registries.DIMENSION, location));
|
||||
}
|
||||
|
||||
@Override public Optional<CorePlayer> player(UUID uuid) { return Optional.ofNullable(server.getPlayerList().getPlayer(uuid)).map(this::wrap); }
|
||||
|
||||
@Override public Optional<CorePlayer> playerByName(String name) { return Optional.ofNullable(server.getPlayerList().getPlayerByName(name)).map(this::wrap); }
|
||||
|
||||
@Override public Collection<? extends CorePlayer> online() {
|
||||
List<CorePlayer> out = new ArrayList<>();
|
||||
for (ServerPlayer p : server.getPlayerList().getPlayers()) out.add(wrap(p));
|
||||
return out;
|
||||
}
|
||||
|
||||
@Override public Optional<Pos> safeSurface(String world, int x, int z) {
|
||||
ServerLevel level = level(world);
|
||||
if (level == null || level.dimensionType().hasCeiling()) return Optional.empty(); // the Nether roof is bedrock
|
||||
int y = level.getHeight(Heightmap.Types.MOTION_BLOCKING_NO_LEAVES, x, z);
|
||||
if (y <= level.getMinBuildHeight() + 1) return Optional.empty();
|
||||
BlockState ground = level.getBlockState(new BlockPos(x, y - 1, z));
|
||||
if (!ground.blocksMotion() || !ground.getFluidState().isEmpty()) return Optional.empty();
|
||||
return Optional.of(new Pos(world, x + 0.5, y, z + 0.5, 0, 0));
|
||||
}
|
||||
|
||||
@Override public Pos worldSpawn(String world) {
|
||||
ServerLevel level = level(world);
|
||||
if (level == null) level = server.overworld();
|
||||
BlockPos spawn = level.getSharedSpawnPos();
|
||||
return new Pos(level.dimension().location().toString(), spawn.getX(), spawn.getY(), spawn.getZ(), 0, 0);
|
||||
}
|
||||
|
||||
@Override public void runMain(Runnable task) { server.execute(task); }
|
||||
|
||||
@Override public void runAsync(Runnable task) { io.execute(task); }
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
package net.scopenet.fabric.features;
|
||||
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.stats.Stats;
|
||||
import net.minecraft.world.InteractionHand;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.scopenet.core.CorePlayer;
|
||||
import net.scopenet.core.Item;
|
||||
import net.scopenet.core.Pos;
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
/** A {@link CorePlayer} over a Fabric ServerPlayer. */
|
||||
final class FabricPlayer implements CorePlayer {
|
||||
private final ServerPlayer player;
|
||||
private final FabricPlatform platform;
|
||||
private final Perms perms;
|
||||
|
||||
FabricPlayer(ServerPlayer player, FabricPlatform platform, Perms perms) {
|
||||
this.player = player; this.platform = platform; this.perms = perms;
|
||||
}
|
||||
|
||||
ServerPlayer handle() { return player; }
|
||||
|
||||
@Override public UUID uuid() { return player.getUUID(); }
|
||||
@Override public String name() { return player.getGameProfile().getName(); }
|
||||
|
||||
@Override public Pos pos() {
|
||||
ServerLevel level = player.serverLevel();
|
||||
return new Pos(level.dimension().location().toString(), player.getX(), player.getY(), player.getZ(), player.getYRot(), player.getXRot());
|
||||
}
|
||||
|
||||
@Override public void send(String message) { player.sendSystemMessage(Component.literal(message)); }
|
||||
|
||||
@Override public void teleport(Pos destination) {
|
||||
ServerLevel level = platform.level(destination.world());
|
||||
if (level == null) { send("§cThat dimension is not loaded."); return; }
|
||||
player.teleportTo(level, destination.x(), destination.y(), destination.z(), destination.yaw(), destination.pitch());
|
||||
}
|
||||
|
||||
@Override public boolean hasPermission(String node) { return perms.has(player, node); }
|
||||
|
||||
@Override public long playtimeSeconds() {
|
||||
return player.getStats().getValue(Stats.CUSTOM.get(Stats.PLAY_TIME)) / 20L;
|
||||
}
|
||||
|
||||
@Override public Optional<Item> heldItem() {
|
||||
ItemStack stack = player.getMainHandItem();
|
||||
return stack.isEmpty() ? Optional.empty() : Optional.of(FabricItems.toCore(stack));
|
||||
}
|
||||
|
||||
@Override public void clearHeld() { player.setItemInHand(InteractionHand.MAIN_HAND, ItemStack.EMPTY); }
|
||||
|
||||
@Override public void give(Item item) {
|
||||
ItemStack stack = FabricItems.fromCore(item);
|
||||
if (stack.isEmpty()) { send("§cCould not deliver " + item.id() + " x" + item.count() + ". Ask an admin."); return; }
|
||||
player.getInventory().add(stack);
|
||||
if (!stack.isEmpty()) player.drop(stack, false);
|
||||
}
|
||||
}
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
package net.scopenet.fabric.features;
|
||||
|
||||
import net.luckperms.api.LuckPerms;
|
||||
import net.luckperms.api.LuckPermsProvider;
|
||||
import net.luckperms.api.model.user.User;
|
||||
import net.luckperms.api.util.Tristate;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
|
||||
/** Only loaded when the LuckPerms mod is present (see {@link Perms}). */
|
||||
final class LuckPermsLookup {
|
||||
private LuckPermsLookup() {}
|
||||
|
||||
/** True or false when LuckPerms has the node set for this player, null when it has no opinion. */
|
||||
static Boolean check(ServerPlayer player, String node) {
|
||||
LuckPerms api = LuckPermsProvider.get();
|
||||
User user = api.getUserManager().getUser(player.getUUID());
|
||||
if (user == null) return null;
|
||||
Tristate result = user.getCachedData().getPermissionData().checkPermission(node);
|
||||
return result == Tristate.UNDEFINED ? null : result.asBoolean();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package net.scopenet.fabric.features;
|
||||
|
||||
import net.fabricmc.loader.api.FabricLoader;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
|
||||
/**
|
||||
* Permission checks. With the LuckPerms mod installed its answer wins whenever the node is set; otherwise (or when
|
||||
* LuckPerms has no opinion) the defaults apply: ordinary commands follow {@code permissions.default_level}, and
|
||||
* admin nodes need operator level 2. Same node names as the Paper plugin, so one LuckPerms setup serves both.
|
||||
*/
|
||||
final class Perms {
|
||||
private static final boolean LUCKPERMS = FabricLoader.getInstance().isModLoaded("luckperms");
|
||||
private final boolean everyoneByDefault;
|
||||
|
||||
Perms(boolean everyoneByDefault) { this.everyoneByDefault = everyoneByDefault; }
|
||||
|
||||
static boolean luckPermsPresent() { return LUCKPERMS; }
|
||||
|
||||
static boolean isAdminNode(String node) {
|
||||
return node.equals("scopenet.claims.bypass") || node.startsWith("scopenet.admin")
|
||||
|| node.equals("scopenet.command.scopenet.status") || node.equals("scopenet.command.scopenet.reload");
|
||||
}
|
||||
|
||||
boolean has(ServerPlayer player, String node) {
|
||||
if (LUCKPERMS) {
|
||||
try {
|
||||
Boolean answer = LuckPermsLookup.check(player, node);
|
||||
if (answer != null) return answer;
|
||||
} catch (Throwable t) {
|
||||
// LuckPerms not ready yet or an API change: fall back to the defaults below.
|
||||
}
|
||||
}
|
||||
if (isAdminNode(node)) return player.hasPermissions(2);
|
||||
return everyoneByDefault || player.hasPermissions(2);
|
||||
}
|
||||
}
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
package net.scopenet.fabric.features;
|
||||
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.scopenet.core.Placeholders;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.Proxy;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* Registers %scopenet:level%, %scopenet:balance%… with Placeholder API (Fabric) when it is installed. Done through
|
||||
* reflection so SCOPENET builds and runs without it and tolerates small API differences; any problem just leaves
|
||||
* the placeholders off and is reported in /scopenet status.
|
||||
*/
|
||||
final class PlaceholderApiModule {
|
||||
private PlaceholderApiModule() {}
|
||||
|
||||
/** Returns how many placeholders were registered; throws if the API isn't usable. */
|
||||
static int register(Placeholders values) throws ReflectiveOperationException {
|
||||
Class<?> placeholders = Class.forName("eu.pb4.placeholders.api.Placeholders");
|
||||
Class<?> handlerType = Class.forName("eu.pb4.placeholders.api.PlaceholderHandler");
|
||||
Class<?> resultType = Class.forName("eu.pb4.placeholders.api.PlaceholderResult");
|
||||
Method register = placeholders.getMethod("register", ResourceLocation.class, handlerType);
|
||||
Method value = resultType.getMethod("value", String.class);
|
||||
Method invalid = resultType.getMethod("invalid", String.class);
|
||||
|
||||
int count = 0;
|
||||
for (String key : Placeholders.KEYS) {
|
||||
Object handler = Proxy.newProxyInstance(handlerType.getClassLoader(), new Class<?>[]{handlerType}, (proxy, method, args) -> {
|
||||
if (method.getDeclaringClass() == Object.class) return method.invoke(new Object(), args);
|
||||
Object context = args[0];
|
||||
ServerPlayer player = (ServerPlayer) context.getClass().getMethod("player").invoke(context);
|
||||
if (player == null) return invalid.invoke(null, "No player");
|
||||
Optional<String> text = values.resolve(player.getUUID(), key);
|
||||
return text.isPresent() ? value.invoke(null, text.get()) : invalid.invoke(null, "Unknown placeholder");
|
||||
});
|
||||
register.invoke(null, new ResourceLocation("scopenet", key), handler);
|
||||
count++;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
}
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
package net.scopenet.fabric.features;
|
||||
|
||||
import net.scopenet.core.CommandSet;
|
||||
import net.scopenet.core.CoreCommand;
|
||||
import net.scopenet.core.CorePlayer;
|
||||
import net.scopenet.core.Format;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
/** /scopenet help | status | reload (alias /sn), with the same nodes as the Paper plugin. */
|
||||
final class ScopenetCommand implements CoreCommand {
|
||||
private final FabricFeatures features;
|
||||
|
||||
ScopenetCommand(FabricFeatures features) { this.features = features; }
|
||||
|
||||
@Override public String name() { return "scopenet"; }
|
||||
@Override public List<String> aliases() { return List.of("sn"); }
|
||||
@Override public String permission() { return "scopenet.command.scopenet"; }
|
||||
|
||||
@Override public List<String> complete(CorePlayer player, String[] args) {
|
||||
return args.length <= 1 ? List.of("help", "status", "reload") : List.of();
|
||||
}
|
||||
|
||||
@Override public void run(CorePlayer p, String[] args) {
|
||||
String sub = args.length == 0 ? "help" : args[0].toLowerCase(Locale.ROOT);
|
||||
switch (sub) {
|
||||
case "help" -> {
|
||||
if (!p.hasPermission("scopenet.command.scopenet.help")) { p.send(Format.RED + "You do not have permission to view SCOPENET help."); return; }
|
||||
help(p);
|
||||
}
|
||||
case "status" -> {
|
||||
if (!p.hasPermission("scopenet.command.scopenet.status")) { p.send(Format.RED + "You do not have permission to use /scopenet status."); return; }
|
||||
features.sendStatus(p);
|
||||
}
|
||||
case "reload" -> {
|
||||
if (!p.hasPermission("scopenet.command.scopenet.reload")) { p.send(Format.RED + "You do not have permission to reload SCOPENET."); return; }
|
||||
features.reload(p);
|
||||
}
|
||||
default -> p.send(Format.RED + "Unknown command. Use " + Format.YELLOW + "/scopenet help");
|
||||
}
|
||||
}
|
||||
|
||||
private void help(CorePlayer p) {
|
||||
p.send(Format.GOLD + "=================== " + Format.YELLOW + "SCOPENET Commands" + Format.GOLD + " ===================");
|
||||
CommandSet set = features.commandSet();
|
||||
if (set == null) return;
|
||||
for (CoreCommand c : set.all()) {
|
||||
if (!p.hasPermission(c.permission())) continue;
|
||||
String aliases = c.aliases().isEmpty() ? "" : Format.DARK_GRAY + " (" + String.join(", ", c.aliases().stream().map(a -> "/" + a).toList()) + ")";
|
||||
p.send(Format.YELLOW + " /" + c.name() + aliases);
|
||||
}
|
||||
p.send(Format.GRAY + "Open the launcher's Command guide for what each one does.");
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user