Plot system migrated from static Java + KubeJS to a runtime configurable, FTB-Chunks-integrated system. * Plot definitions now live in JSON at <world>/serverconfig/bnstoolkit/plots.json. Default file written on first server start with the three v0.33 plots. Editable in-place + reloadable via /bns admin plot reload. * New static Plot.REGISTRY replaces Plot.ALL. Loaded on ServerStartedEvent. Plot.all() returns the current snapshot. * New FtbBridge.java — compileOnly deps on ftb-teams + ftb-chunks (always in the pack). Wraps team lookup, chunk claim/unclaim, and the BEFORE_UNCLAIM event. * /bns admin spawn-init creates the Spawn (cyan) + Plots (gold) server teams using configured UUIDs/colors and claims every defined plot for the Plots team. Idempotent. * /bns admin plot define <id> <name> <price> — adds the chunk you're standing in to the plot. Re-run with the same id (different chunks) to make multi-chunk plots. Auto-claims for the Plots team and persists to JSON. * /bns admin plot list / remove / reload commands. * /bns plot buy <id> — pays via NumismaticsBridge.spend (cash+bank combined), unclaims chunks from Plots team, claims for the buyer's personal FTB Teams team. Records ownership in server NBT and the player's bnsOwnedPlots list. * /bns plot release <id> — reverse flow, refunds 50% to bank. * Plot chunk guard: ClaimedChunkEvent.BEFORE_UNCLAIM subscriber rejects any unclaim of a registered plot chunk unless a thread-local SYSTEM_UNCLAIMING flag is set (only set during the release/transfer flows). Players who try to unclaim via the FTB Chunks UI get a chat message redirecting them to /bns plot release. * KubeJS plots.js retired (.retired suffix kept). All logic now in Java; KubeJS no longer participates in plot ownership state. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
+7
-7
@@ -81,15 +81,15 @@ configurations {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
// FTB Library: provides the Panel/Widget/Button framework used by every
|
// FTB Library: Panel/Widget/Button framework for screens.
|
||||||
// bnstoolkit screen. compileOnly because it's shipped in the modpack
|
|
||||||
// already — we don't want to ship a second copy in our jar. Version
|
|
||||||
// pinned to the live pack's installed version (2101.1.31).
|
|
||||||
compileOnly "dev.ftb.mods:ftb-library-neoforge:2101.1.31"
|
compileOnly "dev.ftb.mods:ftb-library-neoforge:2101.1.31"
|
||||||
|
// FTB Teams: server-team creation + lookup (Spawn + Plots admin teams).
|
||||||
|
compileOnly "dev.ftb.mods:ftb-teams-neoforge:2101.1.9"
|
||||||
|
// FTB Chunks: programmatic claim/unclaim + BEFORE_UNCLAIM event (plot guard).
|
||||||
|
compileOnly "dev.ftb.mods:ftb-chunks-neoforge:2101.1.14"
|
||||||
|
|
||||||
// No Numismatics compileOnly dep — we read coin balances by inventory-
|
// Numismatics + KubeJS are accessed reflectively (NumismaticsBridge,
|
||||||
// scanning items via their string IDs ("numismatics:spur", etc) which
|
// ServerEconomyBridge) so they don't need compileOnly deps.
|
||||||
// doesn't need any of Numismatics' Java API on the classpath.
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var generateModMetadata = tasks.register("generateModMetadata", ProcessResources) {
|
var generateModMetadata = tasks.register("generateModMetadata", ProcessResources) {
|
||||||
|
|||||||
+1
-1
@@ -19,5 +19,5 @@ loader_version_range=[1,)
|
|||||||
mod_id=bnstoolkit
|
mod_id=bnstoolkit
|
||||||
mod_name=Brass and Sigil Toolkit
|
mod_name=Brass and Sigil Toolkit
|
||||||
mod_license=All Rights Reserved
|
mod_license=All Rights Reserved
|
||||||
mod_version=0.7.0
|
mod_version=0.8.0
|
||||||
mod_group_id=uk.sijbers.bnstoolkit
|
mod_group_id=uk.sijbers.bnstoolkit
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ public final class PlotPurchaseScreen extends BaseBnsScreen {
|
|||||||
@Override public void addWidgets() {
|
@Override public void addWidgets() {
|
||||||
int owned = ClientBnsState.ownedPlotIds.size();
|
int owned = ClientBnsState.ownedPlotIds.size();
|
||||||
add(new RowLabel(this, "§7Slots: " + owned + " / " + ClientBnsState.plotSlotCap));
|
add(new RowLabel(this, "§7Slots: " + owned + " / " + ClientBnsState.plotSlotCap));
|
||||||
for (Plot p : Plot.ALL) add(new PlotRow(this, p));
|
for (Plot p : Plot.all()) add(new PlotRow(this, p));
|
||||||
}
|
}
|
||||||
@Override public void alignWidgets() {
|
@Override public void alignWidgets() {
|
||||||
int y = 0;
|
int y = 0;
|
||||||
|
|||||||
@@ -1,25 +1,32 @@
|
|||||||
package uk.sijbers.bnstoolkit.data;
|
package uk.sijbers.bnstoolkit.data;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Spawn-region plot catalogue. Mirrors PLOTS in
|
* One sellable plot in the spawn region.
|
||||||
* pack/overrides/kubejs/server_scripts/plots.js.
|
*
|
||||||
|
* <p>Loaded from {@code config/bnstoolkit/plots.json} at server start
|
||||||
|
* via {@link PlotConfig#load}. Static helpers {@link #ALL} and
|
||||||
|
* {@link #byId} delegate to whatever {@link #REGISTRY} is currently
|
||||||
|
* set (initially {@link PlotRegistry#empty()} — populated on server
|
||||||
|
* start).
|
||||||
|
*
|
||||||
|
* <p>{@code chunks} is a list of {@code [chunkX, chunkZ]} pairs in the
|
||||||
|
* given {@code dimension}.
|
||||||
*/
|
*/
|
||||||
public record Plot(String id, String name, String size, long price, String dimension, List<int[]> chunks) {
|
public record Plot(String id, String name, String size, long price, String dimension, List<int[]> chunks) {
|
||||||
|
|
||||||
public static final List<Plot> ALL = List.of(
|
/** Mutable global reference; replaced wholesale on reload. */
|
||||||
new Plot("p001", "Market Stall 1", "small", 100L, "minecraft:overworld",
|
public static volatile PlotRegistry REGISTRY = PlotRegistry.empty();
|
||||||
List.of(new int[]{10, 10})),
|
|
||||||
new Plot("p002", "Corner Lot", "medium", 250L, "minecraft:overworld",
|
/** Snapshot of currently loaded plots. */
|
||||||
List.of(new int[]{12, 10}, new int[]{12, 11})),
|
public static List<Plot> all() {
|
||||||
new Plot("p003", "Anchor Plot", "large", 600L, "minecraft:overworld",
|
return REGISTRY.plots();
|
||||||
List.of(new int[]{14, 10}, new int[]{14, 11}, new int[]{15, 10}, new int[]{15, 11}))
|
}
|
||||||
);
|
|
||||||
|
|
||||||
public static Plot byId(String id) {
|
public static Plot byId(String id) {
|
||||||
for (Plot p : ALL) if (p.id.equals(id)) return p;
|
return REGISTRY.byId(id);
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public long refundOnRelease() {
|
public long refundOnRelease() {
|
||||||
|
|||||||
@@ -0,0 +1,196 @@
|
|||||||
|
package uk.sijbers.bnstoolkit.data;
|
||||||
|
|
||||||
|
import com.google.gson.JsonArray;
|
||||||
|
import com.google.gson.JsonElement;
|
||||||
|
import com.google.gson.JsonObject;
|
||||||
|
import com.google.gson.JsonParser;
|
||||||
|
import net.minecraft.server.MinecraftServer;
|
||||||
|
import uk.sijbers.bnstoolkit.BnsToolkit;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* On-disk plot configuration loaded from
|
||||||
|
* {@code config/bnstoolkit/plots.json}.
|
||||||
|
*
|
||||||
|
* <h2>Format</h2>
|
||||||
|
* <pre>
|
||||||
|
* {
|
||||||
|
* "spawnTeam": { "name": "Spawn", "color": "#00BFFF", "id": "..." },
|
||||||
|
* "plotsTeam": { "name": "Plots", "color": "#FFD700", "id": "..." },
|
||||||
|
* "plots": [
|
||||||
|
* {
|
||||||
|
* "id": "p001",
|
||||||
|
* "name": "Market Stall 1",
|
||||||
|
* "size": "small",
|
||||||
|
* "price": 100,
|
||||||
|
* "dimension": "minecraft:overworld",
|
||||||
|
* "chunks": [[10, 10]]
|
||||||
|
* }
|
||||||
|
* ]
|
||||||
|
* }
|
||||||
|
* </pre>
|
||||||
|
*
|
||||||
|
* <p>If the file is missing on first load, a default is written with the
|
||||||
|
* three pre-existing spawn plots so the world isn't broken at boot.
|
||||||
|
* Edits land via {@code /bns admin plot reload} or by re-running the
|
||||||
|
* relevant {@code /bns admin plot} subcommand.
|
||||||
|
*/
|
||||||
|
public final class PlotConfig {
|
||||||
|
private PlotConfig() {}
|
||||||
|
|
||||||
|
public static final int SCHEMA_VERSION = 1;
|
||||||
|
public static final String DEFAULT_SPAWN_COLOR = "#00BFFF"; // cyan
|
||||||
|
public static final String DEFAULT_PLOTS_COLOR = "#FFD700"; // gold
|
||||||
|
|
||||||
|
public record TeamConfig(String name, String displayName, String colorHex, String fixedUuid) {}
|
||||||
|
|
||||||
|
public static Path configPath(MinecraftServer server) {
|
||||||
|
// <world>/serverconfig/bnstoolkit/plots.json — per-world, so a wipe
|
||||||
|
// resets it. Falls back to <server>/config/bnstoolkit/plots.json
|
||||||
|
// if the per-world dir isn't writable.
|
||||||
|
return server.getWorldPath(net.minecraft.world.level.storage.LevelResource.ROOT)
|
||||||
|
.resolve("serverconfig").resolve("bnstoolkit").resolve("plots.json");
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Load + parse the config. Returns a populated registry. Writes defaults if file missing. */
|
||||||
|
public static PlotRegistry load(MinecraftServer server) {
|
||||||
|
Path path = configPath(server);
|
||||||
|
try {
|
||||||
|
if (!Files.exists(path)) {
|
||||||
|
Files.createDirectories(path.getParent());
|
||||||
|
Files.writeString(path, defaultJson());
|
||||||
|
BnsToolkit.LOG.info("[bns/plots] wrote default plot config to {}", path);
|
||||||
|
}
|
||||||
|
String content = Files.readString(path);
|
||||||
|
return parse(content);
|
||||||
|
} catch (Exception e) {
|
||||||
|
BnsToolkit.LOG.error("[bns/plots] failed to load plot config from {}: {}", path, e.toString());
|
||||||
|
return PlotRegistry.empty();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Persist the registry back to disk. */
|
||||||
|
public static void save(MinecraftServer server, PlotRegistry registry) {
|
||||||
|
Path path = configPath(server);
|
||||||
|
try {
|
||||||
|
Files.createDirectories(path.getParent());
|
||||||
|
Files.writeString(path, serialize(registry));
|
||||||
|
BnsToolkit.LOG.info("[bns/plots] saved {} plots to {}", registry.plots().size(), path);
|
||||||
|
} catch (IOException e) {
|
||||||
|
BnsToolkit.LOG.error("[bns/plots] save failed: {}", e.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static PlotRegistry parse(String json) {
|
||||||
|
JsonObject root = JsonParser.parseString(json).getAsJsonObject();
|
||||||
|
|
||||||
|
TeamConfig spawnTeam = parseTeam(root, "spawnTeam", "Spawn",
|
||||||
|
DEFAULT_SPAWN_COLOR, "00000001-0000-0000-0000-bnstoolspawn");
|
||||||
|
TeamConfig plotsTeam = parseTeam(root, "plotsTeam", "Plots",
|
||||||
|
DEFAULT_PLOTS_COLOR, "00000002-0000-0000-0000-bnstoolplots");
|
||||||
|
|
||||||
|
List<Plot> plots = new ArrayList<>();
|
||||||
|
if (root.has("plots")) {
|
||||||
|
for (JsonElement e : root.getAsJsonArray("plots")) {
|
||||||
|
try {
|
||||||
|
JsonObject p = e.getAsJsonObject();
|
||||||
|
String id = p.get("id").getAsString();
|
||||||
|
String name = p.get("name").getAsString();
|
||||||
|
String size = p.has("size") ? p.get("size").getAsString() : "medium";
|
||||||
|
long price = p.get("price").getAsLong();
|
||||||
|
String dim = p.has("dimension") ? p.get("dimension").getAsString() : "minecraft:overworld";
|
||||||
|
List<int[]> chunks = new ArrayList<>();
|
||||||
|
for (JsonElement c : p.getAsJsonArray("chunks")) {
|
||||||
|
JsonArray arr = c.getAsJsonArray();
|
||||||
|
chunks.add(new int[]{ arr.get(0).getAsInt(), arr.get(1).getAsInt() });
|
||||||
|
}
|
||||||
|
plots.add(new Plot(id, name, size, price, dim, chunks));
|
||||||
|
} catch (Exception ex) {
|
||||||
|
BnsToolkit.LOG.warn("[bns/plots] skipping malformed plot entry: {}", ex.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new PlotRegistry(spawnTeam, plotsTeam, Collections.unmodifiableList(plots));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static TeamConfig parseTeam(JsonObject root, String key, String defaultName,
|
||||||
|
String defaultColor, String defaultUuid) {
|
||||||
|
if (!root.has(key)) return new TeamConfig(defaultName, defaultName, defaultColor, defaultUuid);
|
||||||
|
JsonObject t = root.getAsJsonObject(key);
|
||||||
|
String name = t.has("name") ? t.get("name").getAsString() : defaultName;
|
||||||
|
String display = t.has("displayName") ? t.get("displayName").getAsString() : name;
|
||||||
|
String color = t.has("color") ? t.get("color").getAsString() : defaultColor;
|
||||||
|
String uuid = t.has("id") ? t.get("id").getAsString() : defaultUuid;
|
||||||
|
return new TeamConfig(name, display, color, uuid);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String defaultJson() {
|
||||||
|
// Ships the three pre-existing v0.33 plots so a fresh world has
|
||||||
|
// something to test against. Matt edits + /bns admin plot reload
|
||||||
|
// after spawn build is done.
|
||||||
|
return """
|
||||||
|
{
|
||||||
|
"$schema": "bnstoolkit/plots-v1",
|
||||||
|
"spawnTeam": {
|
||||||
|
"name": "Spawn",
|
||||||
|
"displayName": "Spawn",
|
||||||
|
"color": "#00BFFF",
|
||||||
|
"id": "00000001-0000-0000-0000-bnstoolspawn"
|
||||||
|
},
|
||||||
|
"plotsTeam": {
|
||||||
|
"name": "Plots",
|
||||||
|
"displayName": "Plots",
|
||||||
|
"color": "#FFD700",
|
||||||
|
"id": "00000002-0000-0000-0000-bnstoolplots"
|
||||||
|
},
|
||||||
|
"plots": [
|
||||||
|
{ "id": "p001", "name": "Market Stall 1", "size": "small", "price": 100, "dimension": "minecraft:overworld", "chunks": [[10, 10]] },
|
||||||
|
{ "id": "p002", "name": "Corner Lot", "size": "medium", "price": 250, "dimension": "minecraft:overworld", "chunks": [[12, 10], [12, 11]] },
|
||||||
|
{ "id": "p003", "name": "Anchor Plot", "size": "large", "price": 600, "dimension": "minecraft:overworld", "chunks": [[14, 10], [14, 11], [15, 10], [15, 11]] }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String serialize(PlotRegistry r) {
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
sb.append("{\n \"$schema\": \"bnstoolkit/plots-v1\",\n");
|
||||||
|
appendTeam(sb, "spawnTeam", r.spawnTeam(), false);
|
||||||
|
appendTeam(sb, "plotsTeam", r.plotsTeam(), false);
|
||||||
|
sb.append(" \"plots\": [\n");
|
||||||
|
for (int i = 0; i < r.plots().size(); i++) {
|
||||||
|
Plot p = r.plots().get(i);
|
||||||
|
sb.append(" { \"id\": \"").append(p.id())
|
||||||
|
.append("\", \"name\": \"").append(p.name())
|
||||||
|
.append("\", \"size\": \"").append(p.size())
|
||||||
|
.append("\", \"price\": ").append(p.price())
|
||||||
|
.append(", \"dimension\": \"").append(p.dimension())
|
||||||
|
.append("\", \"chunks\": [");
|
||||||
|
for (int j = 0; j < p.chunks().size(); j++) {
|
||||||
|
int[] c = p.chunks().get(j);
|
||||||
|
if (j > 0) sb.append(", ");
|
||||||
|
sb.append('[').append(c[0]).append(", ").append(c[1]).append(']');
|
||||||
|
}
|
||||||
|
sb.append("] }");
|
||||||
|
if (i + 1 < r.plots().size()) sb.append(',');
|
||||||
|
sb.append('\n');
|
||||||
|
}
|
||||||
|
sb.append(" ]\n}\n");
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void appendTeam(StringBuilder sb, String key, TeamConfig t, boolean trailingComma) {
|
||||||
|
sb.append(" \"").append(key).append("\": { ")
|
||||||
|
.append("\"name\": \"").append(t.name()).append("\", ")
|
||||||
|
.append("\"displayName\": \"").append(t.displayName()).append("\", ")
|
||||||
|
.append("\"color\": \"").append(t.colorHex()).append("\", ")
|
||||||
|
.append("\"id\": \"").append(t.fixedUuid()).append("\" },\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
package uk.sijbers.bnstoolkit.data;
|
||||||
|
|
||||||
|
import net.minecraft.core.BlockPos;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Runtime store of loaded plots + the spawn/plots team configs.
|
||||||
|
*
|
||||||
|
* <p>Populated from JSON at server start (see {@link PlotConfig#load}) and
|
||||||
|
* stored on the global {@link Plot#REGISTRY} reference. Replaces the
|
||||||
|
* earlier static {@code Plot.ALL} list — that's now a delegating view
|
||||||
|
* over whatever's currently loaded.
|
||||||
|
*/
|
||||||
|
public final class PlotRegistry {
|
||||||
|
private final PlotConfig.TeamConfig spawnTeam;
|
||||||
|
private final PlotConfig.TeamConfig plotsTeam;
|
||||||
|
private final List<Plot> plots;
|
||||||
|
|
||||||
|
public PlotRegistry(PlotConfig.TeamConfig spawnTeam,
|
||||||
|
PlotConfig.TeamConfig plotsTeam,
|
||||||
|
List<Plot> plots) {
|
||||||
|
this.spawnTeam = spawnTeam;
|
||||||
|
this.plotsTeam = plotsTeam;
|
||||||
|
this.plots = plots;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static PlotRegistry empty() {
|
||||||
|
return new PlotRegistry(
|
||||||
|
new PlotConfig.TeamConfig("Spawn", "Spawn", PlotConfig.DEFAULT_SPAWN_COLOR,
|
||||||
|
"00000001-0000-0000-0000-bnstoolspawn"),
|
||||||
|
new PlotConfig.TeamConfig("Plots", "Plots", PlotConfig.DEFAULT_PLOTS_COLOR,
|
||||||
|
"00000002-0000-0000-0000-bnstoolplots"),
|
||||||
|
Collections.emptyList());
|
||||||
|
}
|
||||||
|
|
||||||
|
public PlotConfig.TeamConfig spawnTeam() { return spawnTeam; }
|
||||||
|
public PlotConfig.TeamConfig plotsTeam() { return plotsTeam; }
|
||||||
|
public List<Plot> plots() { return plots; }
|
||||||
|
|
||||||
|
public Plot byId(String id) {
|
||||||
|
for (Plot p : plots) if (p.id().equals(id)) return p;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Find the plot containing a given chunk position, or null. */
|
||||||
|
public Plot byChunk(int chunkX, int chunkZ, String dimensionId) {
|
||||||
|
for (Plot p : plots) {
|
||||||
|
if (!p.dimension().equals(dimensionId)) continue;
|
||||||
|
for (int[] c : p.chunks()) {
|
||||||
|
if (c[0] == chunkX && c[1] == chunkZ) return p;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Plot byBlockPos(BlockPos pos, String dimensionId) {
|
||||||
|
return byChunk(pos.getX() >> 4, pos.getZ() >> 4, dimensionId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Create a new registry with one extra plot. Immutable replace. */
|
||||||
|
public PlotRegistry withPlot(Plot p) {
|
||||||
|
List<Plot> next = new ArrayList<>(plots);
|
||||||
|
next.removeIf(existing -> existing.id().equals(p.id()));
|
||||||
|
next.add(p);
|
||||||
|
return new PlotRegistry(spawnTeam, plotsTeam, Collections.unmodifiableList(next));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Create a new registry without the plot with the given id. */
|
||||||
|
public PlotRegistry withoutPlot(String id) {
|
||||||
|
List<Plot> next = new ArrayList<>(plots);
|
||||||
|
next.removeIf(p -> p.id().equals(id));
|
||||||
|
return new PlotRegistry(spawnTeam, plotsTeam, Collections.unmodifiableList(next));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,202 @@
|
|||||||
|
package uk.sijbers.bnstoolkit.economy;
|
||||||
|
|
||||||
|
import dev.ftb.mods.ftbchunks.api.ChunkTeamData;
|
||||||
|
import dev.ftb.mods.ftbchunks.api.ClaimResult;
|
||||||
|
import dev.ftb.mods.ftbchunks.api.ClaimedChunk;
|
||||||
|
import dev.ftb.mods.ftbchunks.api.ClaimedChunkManager;
|
||||||
|
import dev.ftb.mods.ftbchunks.api.FTBChunksAPI;
|
||||||
|
import dev.ftb.mods.ftbchunks.api.event.ClaimedChunkEvent;
|
||||||
|
import dev.ftb.mods.ftblibrary.icon.Color4I;
|
||||||
|
import dev.ftb.mods.ftblibrary.math.ChunkDimPos;
|
||||||
|
import dev.ftb.mods.ftbteams.api.FTBTeamsAPI;
|
||||||
|
import dev.ftb.mods.ftbteams.api.Team;
|
||||||
|
import dev.ftb.mods.ftbteams.api.TeamManager;
|
||||||
|
import dev.architectury.event.CompoundEventResult;
|
||||||
|
import net.minecraft.commands.CommandSourceStack;
|
||||||
|
import net.minecraft.network.chat.Component;
|
||||||
|
import net.minecraft.resources.ResourceKey;
|
||||||
|
import net.minecraft.server.MinecraftServer;
|
||||||
|
import net.minecraft.server.level.ServerPlayer;
|
||||||
|
import net.minecraft.world.level.ChunkPos;
|
||||||
|
import net.minecraft.world.level.Level;
|
||||||
|
import uk.sijbers.bnstoolkit.BnsToolkit;
|
||||||
|
import uk.sijbers.bnstoolkit.data.Plot;
|
||||||
|
import uk.sijbers.bnstoolkit.data.PlotConfig;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type-safe wrapper around FTB Teams + FTB Chunks.
|
||||||
|
*
|
||||||
|
* <p>The mods are always in the BNS pack, so we depend on them at
|
||||||
|
* compile time (via build.gradle compileOnly) rather than the
|
||||||
|
* reflection pattern we use for KubeJS / Numismatics. The compileOnly
|
||||||
|
* choice keeps bnstoolkit's jar small and lets IntelliJ resolve types.
|
||||||
|
*
|
||||||
|
* <p>If FTB Teams/Chunks ever became optional, every public method
|
||||||
|
* here would need a try/catch around {@code FTBTeamsAPI.api()} etc.
|
||||||
|
* For now: it's a hard requirement — if either mod is missing,
|
||||||
|
* bnstoolkit's plot system breaks loudly, which is the correct
|
||||||
|
* failure mode.
|
||||||
|
*
|
||||||
|
* <h2>Plot chunk guard</h2>
|
||||||
|
* {@link #installUnclaimGuard()} subscribes to {@link
|
||||||
|
* ClaimedChunkEvent#BEFORE_UNCLAIM} and rejects any unclaim of a
|
||||||
|
* registered plot chunk unless the bnstoolkit system flag is set
|
||||||
|
* (which only happens during a {@code /bns plot release} flow).
|
||||||
|
*/
|
||||||
|
public final class FtbBridge {
|
||||||
|
private FtbBridge() {}
|
||||||
|
|
||||||
|
/** Set by Plot release/transfer paths so the guard lets the unclaim through. */
|
||||||
|
private static final ThreadLocal<Boolean> SYSTEM_UNCLAIMING = ThreadLocal.withInitial(() -> false);
|
||||||
|
|
||||||
|
/** Run {@code body} with system-unclaim privilege. Use sparingly. */
|
||||||
|
public static <T> T withSystemUnclaim(java.util.function.Supplier<T> body) {
|
||||||
|
SYSTEM_UNCLAIMING.set(true);
|
||||||
|
try { return body.get(); }
|
||||||
|
finally { SYSTEM_UNCLAIMING.set(false); }
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean installUnclaimGuard() {
|
||||||
|
try {
|
||||||
|
ClaimedChunkEvent.BEFORE_UNCLAIM.register((src, chunk) -> {
|
||||||
|
if (Boolean.TRUE.equals(SYSTEM_UNCLAIMING.get())) {
|
||||||
|
return CompoundEventResult.pass();
|
||||||
|
}
|
||||||
|
ChunkDimPos pos = chunk.getPos();
|
||||||
|
String dim = pos.dimension().location().toString();
|
||||||
|
Plot plot = Plot.REGISTRY.byChunk(pos.x(), pos.z(), dim);
|
||||||
|
if (plot == null) return CompoundEventResult.pass();
|
||||||
|
// Plot chunks can't be manually unclaimed
|
||||||
|
try {
|
||||||
|
src.sendFailure(Component.literal(
|
||||||
|
"§cChunk is part of plot '" + plot.name() + "'. " +
|
||||||
|
"Use /bns plot release " + plot.id() + " at the Plot Office to sell it back."));
|
||||||
|
} catch (Exception ignored) {}
|
||||||
|
return CompoundEventResult.interruptFalse(ClaimResult.StandardProblem.NOT_OWNER);
|
||||||
|
});
|
||||||
|
BnsToolkit.LOG.info("[bns/plots] installed FTB Chunks BEFORE_UNCLAIM guard");
|
||||||
|
return true;
|
||||||
|
} catch (Throwable t) {
|
||||||
|
BnsToolkit.LOG.warn("[bns/plots] couldn't install unclaim guard: {}", t.toString());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Teams ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
public static TeamManager teamManager() {
|
||||||
|
return FTBTeamsAPI.api().getManager();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Optional<Team> getTeam(UUID id) {
|
||||||
|
return teamManager().getTeamByID(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Optional<Team> getTeamByName(String name) {
|
||||||
|
return teamManager().getTeamByName(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Optional<Team> getPersonalTeam(ServerPlayer player) {
|
||||||
|
return teamManager().getPlayerTeamForPlayerID(player.getUUID());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Optional<Team> getActiveTeam(ServerPlayer player) {
|
||||||
|
return teamManager().getTeamForPlayer(player);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get-or-create a server team (admin-owned, no human owner). Used
|
||||||
|
* for the Spawn + Plots admin teams. Idempotent — if a team with
|
||||||
|
* the given {@code teamId} UUID already exists, returns it.
|
||||||
|
*/
|
||||||
|
public static Optional<Team> ensureServerTeam(CommandSourceStack src,
|
||||||
|
String name,
|
||||||
|
String displayName,
|
||||||
|
Color4I color,
|
||||||
|
UUID teamId) {
|
||||||
|
TeamManager mgr = teamManager();
|
||||||
|
Optional<Team> existing = mgr.getTeamByID(teamId);
|
||||||
|
if (existing.isPresent()) return existing;
|
||||||
|
|
||||||
|
// Some old packs store these as named server teams — try name lookup too
|
||||||
|
Optional<Team> byName = mgr.getTeamByName(name);
|
||||||
|
if (byName.isPresent()) return byName;
|
||||||
|
|
||||||
|
try {
|
||||||
|
Team created = mgr.createServerTeam(src, name, displayName, color, teamId);
|
||||||
|
BnsToolkit.LOG.info("[bns/plots] created server team {} (id {})", name, teamId);
|
||||||
|
return Optional.of(created);
|
||||||
|
} catch (Throwable t) {
|
||||||
|
BnsToolkit.LOG.warn("[bns/plots] couldn't create server team {}: {}", name, t.toString());
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Chunks ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
public static ClaimedChunkManager chunkManager() {
|
||||||
|
return FTBChunksAPI.api().getManager();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ChunkTeamData chunkData(Team team) {
|
||||||
|
return chunkManager().getOrCreateData(team);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean isClaimed(ResourceKey<Level> dim, int chunkX, int chunkZ) {
|
||||||
|
ChunkDimPos pos = new ChunkDimPos(dim, chunkX, chunkZ);
|
||||||
|
ClaimedChunk c = chunkManager().getChunk(pos);
|
||||||
|
return c != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Team ownerOf(ResourceKey<Level> dim, int chunkX, int chunkZ) {
|
||||||
|
ChunkDimPos pos = new ChunkDimPos(dim, chunkX, chunkZ);
|
||||||
|
ClaimedChunk c = chunkManager().getChunk(pos);
|
||||||
|
return c == null ? null : c.getTeamData().getTeam();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Claim a chunk for the given team. Returns success. */
|
||||||
|
public static boolean claimFor(Team team, CommandSourceStack src,
|
||||||
|
ResourceKey<Level> dim, int chunkX, int chunkZ) {
|
||||||
|
try {
|
||||||
|
ChunkDimPos pos = new ChunkDimPos(dim, chunkX, chunkZ);
|
||||||
|
ClaimResult r = chunkData(team).claim(src, pos, true);
|
||||||
|
return r.isSuccess();
|
||||||
|
} catch (Throwable t) {
|
||||||
|
BnsToolkit.LOG.warn("[bns/plots] claimFor failed: {}", t.toString());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Unclaim a chunk currently held by the given team. Uses system privilege. */
|
||||||
|
public static boolean unclaimFor(Team team, CommandSourceStack src,
|
||||||
|
ResourceKey<Level> dim, int chunkX, int chunkZ) {
|
||||||
|
return withSystemUnclaim(() -> {
|
||||||
|
try {
|
||||||
|
ChunkDimPos pos = new ChunkDimPos(dim, chunkX, chunkZ);
|
||||||
|
ClaimResult r = chunkData(team).unclaim(src, pos, true);
|
||||||
|
return r.isSuccess();
|
||||||
|
} catch (Throwable t) {
|
||||||
|
BnsToolkit.LOG.warn("[bns/plots] unclaimFor failed: {}", t.toString());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Color parser ───────────────────────────────────────────────
|
||||||
|
|
||||||
|
/** Parse "#RRGGBB" or "#AARRGGBB" hex to Color4I. */
|
||||||
|
public static Color4I parseColor(String hex) {
|
||||||
|
try {
|
||||||
|
String s = hex.startsWith("#") ? hex.substring(1) : hex;
|
||||||
|
long v = Long.parseLong(s, 16);
|
||||||
|
if (s.length() == 6) return Color4I.rgb((int) v);
|
||||||
|
return Color4I.rgba((int) v);
|
||||||
|
} catch (Exception e) {
|
||||||
|
BnsToolkit.LOG.warn("[bns/plots] bad color '{}', defaulting to white", hex);
|
||||||
|
return Color4I.WHITE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -277,7 +277,7 @@ public final class ServerEconomyBridge {
|
|||||||
public static void pushPlotSnapshot(ServerPlayer sp) {
|
public static void pushPlotSnapshot(ServerPlayer sp) {
|
||||||
Map<String, String> owners = new HashMap<>();
|
Map<String, String> owners = new HashMap<>();
|
||||||
List<String> ownedIds = new ArrayList<>();
|
List<String> ownedIds = new ArrayList<>();
|
||||||
for (Plot p : Plot.ALL) owners.put(p.id(), "");
|
for (Plot p : Plot.all()) owners.put(p.id(), "");
|
||||||
|
|
||||||
// KubeJS attaches its server-scope persistentData to MinecraftServer via
|
// KubeJS attaches its server-scope persistentData to MinecraftServer via
|
||||||
// a mixin (kjs$getPersistentData). Reflective access avoids a hard
|
// a mixin (kjs$getPersistentData). Reflective access avoids a hard
|
||||||
|
|||||||
@@ -0,0 +1,389 @@
|
|||||||
|
package uk.sijbers.bnstoolkit.server;
|
||||||
|
|
||||||
|
import com.mojang.brigadier.arguments.IntegerArgumentType;
|
||||||
|
import com.mojang.brigadier.arguments.StringArgumentType;
|
||||||
|
import com.mojang.brigadier.builder.LiteralArgumentBuilder;
|
||||||
|
import dev.ftb.mods.ftbteams.api.Team;
|
||||||
|
import net.minecraft.commands.CommandSourceStack;
|
||||||
|
import net.minecraft.commands.Commands;
|
||||||
|
import net.minecraft.nbt.CompoundTag;
|
||||||
|
import net.minecraft.nbt.ListTag;
|
||||||
|
import net.minecraft.nbt.StringTag;
|
||||||
|
import net.minecraft.nbt.Tag;
|
||||||
|
import net.minecraft.network.chat.Component;
|
||||||
|
import net.minecraft.resources.ResourceKey;
|
||||||
|
import net.minecraft.server.MinecraftServer;
|
||||||
|
import net.minecraft.server.level.ServerLevel;
|
||||||
|
import net.minecraft.server.level.ServerPlayer;
|
||||||
|
import net.minecraft.world.level.Level;
|
||||||
|
import net.neoforged.bus.api.SubscribeEvent;
|
||||||
|
import net.neoforged.fml.common.EventBusSubscriber;
|
||||||
|
import net.neoforged.neoforge.event.RegisterCommandsEvent;
|
||||||
|
import net.neoforged.neoforge.event.server.ServerStartedEvent;
|
||||||
|
import uk.sijbers.bnstoolkit.BnsToolkit;
|
||||||
|
import uk.sijbers.bnstoolkit.data.CivicTier;
|
||||||
|
import uk.sijbers.bnstoolkit.data.Plot;
|
||||||
|
import uk.sijbers.bnstoolkit.data.PlotConfig;
|
||||||
|
import uk.sijbers.bnstoolkit.data.PlotRegistry;
|
||||||
|
import uk.sijbers.bnstoolkit.economy.FtbBridge;
|
||||||
|
import uk.sijbers.bnstoolkit.economy.NumismaticsBridge;
|
||||||
|
import uk.sijbers.bnstoolkit.network.ServerEconomyBridge;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Plot-system commands — both admin (define/remove/reload/spawn-init)
|
||||||
|
* and player (buy/release). Owns plot ownership transitions and the
|
||||||
|
* FTB Chunks integration.
|
||||||
|
*
|
||||||
|
* <p>Plot definitions live in {@code config/bnstoolkit/plots.json}
|
||||||
|
* (per-world). The {@link Plot#REGISTRY} static is updated on server
|
||||||
|
* start, on reload, and on admin define/remove.
|
||||||
|
*
|
||||||
|
* <p>Ownership is tracked in {@code server.persistentData.bnsPlots}
|
||||||
|
* (the same compound the KubeJS plots.js used) and mirrored to each
|
||||||
|
* player's NBT as {@code bnsOwnedPlots} for HUD/screen consumption.
|
||||||
|
*/
|
||||||
|
@EventBusSubscriber(modid = BnsToolkit.MOD_ID)
|
||||||
|
public final class PlotCommands {
|
||||||
|
private PlotCommands() {}
|
||||||
|
|
||||||
|
private static final String OWNERSHIP_KEY = "bnsPlots";
|
||||||
|
private static final String PER_PLAYER_KEY = "bnsOwnedPlots";
|
||||||
|
|
||||||
|
// ─── Lifecycle ───────────────────────────────────────────────────
|
||||||
|
|
||||||
|
@SubscribeEvent
|
||||||
|
public static void onServerStarted(ServerStartedEvent event) {
|
||||||
|
MinecraftServer server = event.getServer();
|
||||||
|
PlotRegistry registry = PlotConfig.load(server);
|
||||||
|
Plot.REGISTRY = registry;
|
||||||
|
BnsToolkit.LOG.info("[bns/plots] loaded {} plots from config", registry.plots().size());
|
||||||
|
// Install the BEFORE_UNCLAIM guard so plot chunks can't be unclaimed
|
||||||
|
// by players manually.
|
||||||
|
FtbBridge.installUnclaimGuard();
|
||||||
|
}
|
||||||
|
|
||||||
|
@SubscribeEvent
|
||||||
|
public static void onRegisterCommands(RegisterCommandsEvent event) {
|
||||||
|
LiteralArgumentBuilder<CommandSourceStack> bns = Commands.literal("bns")
|
||||||
|
// ─── Player verbs ────────────────────────────────────
|
||||||
|
.then(Commands.literal("plot")
|
||||||
|
.then(Commands.literal("buy")
|
||||||
|
.then(Commands.argument("id", StringArgumentType.word())
|
||||||
|
.executes(ctx -> doBuy(ctx.getSource(), StringArgumentType.getString(ctx, "id")))))
|
||||||
|
.then(Commands.literal("release")
|
||||||
|
.then(Commands.argument("id", StringArgumentType.word())
|
||||||
|
.executes(ctx -> doRelease(ctx.getSource(), StringArgumentType.getString(ctx, "id"))))))
|
||||||
|
// ─── Admin verbs ─────────────────────────────────────
|
||||||
|
.then(Commands.literal("admin")
|
||||||
|
.requires(src -> src.hasPermission(2))
|
||||||
|
.then(Commands.literal("plot")
|
||||||
|
.then(Commands.literal("define")
|
||||||
|
.then(Commands.argument("id", StringArgumentType.word())
|
||||||
|
.then(Commands.argument("name", StringArgumentType.string())
|
||||||
|
.then(Commands.argument("price", IntegerArgumentType.integer(0))
|
||||||
|
.executes(ctx -> doDefine(ctx.getSource(),
|
||||||
|
StringArgumentType.getString(ctx, "id"),
|
||||||
|
StringArgumentType.getString(ctx, "name"),
|
||||||
|
IntegerArgumentType.getInteger(ctx, "price"))))))
|
||||||
|
)
|
||||||
|
.then(Commands.literal("list").executes(ctx -> doList(ctx.getSource())))
|
||||||
|
.then(Commands.literal("remove")
|
||||||
|
.then(Commands.argument("id", StringArgumentType.word())
|
||||||
|
.executes(ctx -> doRemove(ctx.getSource(), StringArgumentType.getString(ctx, "id")))))
|
||||||
|
.then(Commands.literal("reload").executes(ctx -> doReload(ctx.getSource()))))
|
||||||
|
.then(Commands.literal("spawn-init").executes(ctx -> doSpawnInit(ctx.getSource()))));
|
||||||
|
event.getDispatcher().register(bns);
|
||||||
|
BnsToolkit.LOG.info("[bns/plots] registered /bns plot + /bns admin plot/spawn-init");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── /bns plot buy <id> ─────────────────────────────────────────
|
||||||
|
|
||||||
|
private static int doBuy(CommandSourceStack src, String plotId) {
|
||||||
|
ServerPlayer sp = src.getPlayer();
|
||||||
|
if (sp == null) { src.sendFailure(Component.literal("/bns plot buy must be run as a player")); return 0; }
|
||||||
|
Plot plot = Plot.byId(plotId);
|
||||||
|
if (plot == null) { sp.sendSystemMessage(Component.literal("§cUnknown plot: " + plotId)); return 0; }
|
||||||
|
|
||||||
|
// Tier slot cap check
|
||||||
|
CompoundTag pd = sp.getPersistentData();
|
||||||
|
int tier = pd.contains("bnsTier") ? pd.getInt("bnsTier") : 1;
|
||||||
|
if (tier < 1 || tier > CivicTier.MAX) tier = 1;
|
||||||
|
int cap = CivicTier.byId(tier).plotSlots();
|
||||||
|
int owned = ownedPlotsOf(sp).size();
|
||||||
|
if (cap <= 0) { sp.sendSystemMessage(Component.literal("§cYou need tier 3 (Citizen) before owning a plot.")); return 0; }
|
||||||
|
if (owned >= cap) {
|
||||||
|
sp.sendSystemMessage(Component.literal("§cAt plot-slot cap (" + owned + " / " + cap + "). Upgrade your tier first."));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ownership check (server-scope)
|
||||||
|
String currentOwner = getOwnerUuid(sp.server, plotId);
|
||||||
|
if (currentOwner != null) {
|
||||||
|
sp.sendSystemMessage(Component.literal("§cThat plot is already owned."));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pay
|
||||||
|
if (!NumismaticsBridge.spend(sp, plot.price())) {
|
||||||
|
sp.sendSystemMessage(Component.literal("§cNot enough spurs. Plot costs " + plot.price()));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Move chunks from Plots team to buyer's personal team
|
||||||
|
ResourceKey<Level> dim = ResourceKey.create(net.minecraft.core.registries.Registries.DIMENSION,
|
||||||
|
net.minecraft.resources.ResourceLocation.parse(plot.dimension()));
|
||||||
|
Optional<Team> plotsTeam = FtbBridge.getTeamByName(Plot.REGISTRY.plotsTeam().name());
|
||||||
|
Optional<Team> buyerTeam = FtbBridge.getPersonalTeam(sp);
|
||||||
|
if (plotsTeam.isPresent() && buyerTeam.isPresent()) {
|
||||||
|
for (int[] c : plot.chunks()) {
|
||||||
|
FtbBridge.unclaimFor(plotsTeam.get(), src, dim, c[0], c[1]);
|
||||||
|
FtbBridge.claimFor(buyerTeam.get(), src, dim, c[0], c[1]);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
BnsToolkit.LOG.warn("[bns/plots] FTB Teams unavailable — purchase recorded but chunks NOT transferred");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Record ownership
|
||||||
|
setOwner(sp.server, plotId, sp.getUUID().toString());
|
||||||
|
addOwnedPlot(sp, plotId);
|
||||||
|
|
||||||
|
sp.sendSystemMessage(Component.literal("§aPurchased '" + plot.name() + "' for " + plot.price() + " spurs."));
|
||||||
|
BnsToolkit.LOG.info("[bns/plots] {} bought {} for {}", sp.getName().getString(), plotId, plot.price());
|
||||||
|
|
||||||
|
ServerEconomyBridge.pushTierUpdate(sp);
|
||||||
|
ServerEconomyBridge.pushPlotSnapshot(sp);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── /bns plot release <id> ─────────────────────────────────────
|
||||||
|
|
||||||
|
private static int doRelease(CommandSourceStack src, String plotId) {
|
||||||
|
ServerPlayer sp = src.getPlayer();
|
||||||
|
if (sp == null) { src.sendFailure(Component.literal("/bns plot release must be run as a player")); return 0; }
|
||||||
|
Plot plot = Plot.byId(plotId);
|
||||||
|
if (plot == null) { sp.sendSystemMessage(Component.literal("§cUnknown plot: " + plotId)); return 0; }
|
||||||
|
|
||||||
|
String currentOwner = getOwnerUuid(sp.server, plotId);
|
||||||
|
if (currentOwner == null || !currentOwner.equals(sp.getUUID().toString())) {
|
||||||
|
sp.sendSystemMessage(Component.literal("§cYou don't own that plot."));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Move chunks back to Plots team
|
||||||
|
ResourceKey<Level> dim = ResourceKey.create(net.minecraft.core.registries.Registries.DIMENSION,
|
||||||
|
net.minecraft.resources.ResourceLocation.parse(plot.dimension()));
|
||||||
|
Optional<Team> plotsTeam = FtbBridge.getTeamByName(Plot.REGISTRY.plotsTeam().name());
|
||||||
|
Optional<Team> buyerTeam = FtbBridge.getPersonalTeam(sp);
|
||||||
|
if (plotsTeam.isPresent() && buyerTeam.isPresent()) {
|
||||||
|
for (int[] c : plot.chunks()) {
|
||||||
|
FtbBridge.unclaimFor(buyerTeam.get(), src, dim, c[0], c[1]);
|
||||||
|
FtbBridge.claimFor(plotsTeam.get(), src, dim, c[0], c[1]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Refund + clear ownership
|
||||||
|
long refund = plot.refundOnRelease();
|
||||||
|
NumismaticsBridge.deposit(sp, refund);
|
||||||
|
clearOwner(sp.server, plotId);
|
||||||
|
removeOwnedPlot(sp, plotId);
|
||||||
|
|
||||||
|
sp.sendSystemMessage(Component.literal("§eReleased '" + plot.name() + "'. Refunded " + refund + " spurs to bank."));
|
||||||
|
BnsToolkit.LOG.info("[bns/plots] {} released {} (refund {})", sp.getName().getString(), plotId, refund);
|
||||||
|
|
||||||
|
ServerEconomyBridge.pushTierUpdate(sp);
|
||||||
|
ServerEconomyBridge.pushPlotSnapshot(sp);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── /bns admin spawn-init ──────────────────────────────────────
|
||||||
|
|
||||||
|
private static int doSpawnInit(CommandSourceStack src) {
|
||||||
|
PlotRegistry registry = Plot.REGISTRY;
|
||||||
|
var spawn = registry.spawnTeam();
|
||||||
|
var plotsCfg = registry.plotsTeam();
|
||||||
|
|
||||||
|
UUID spawnId = UUID.fromString(spawn.fixedUuid());
|
||||||
|
UUID plotsId = UUID.fromString(plotsCfg.fixedUuid());
|
||||||
|
|
||||||
|
Optional<Team> spawnTeam = FtbBridge.ensureServerTeam(src,
|
||||||
|
spawn.name(), spawn.displayName(), FtbBridge.parseColor(spawn.colorHex()), spawnId);
|
||||||
|
Optional<Team> plotsTeam = FtbBridge.ensureServerTeam(src,
|
||||||
|
plotsCfg.name(), plotsCfg.displayName(), FtbBridge.parseColor(plotsCfg.colorHex()), plotsId);
|
||||||
|
|
||||||
|
if (spawnTeam.isEmpty() || plotsTeam.isEmpty()) {
|
||||||
|
src.sendFailure(Component.literal("§cFailed to create FTB teams — check log"));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Claim all unsold plots for the Plots team
|
||||||
|
int claimed = 0, skipped = 0;
|
||||||
|
for (Plot plot : registry.plots()) {
|
||||||
|
ResourceKey<Level> dim = ResourceKey.create(net.minecraft.core.registries.Registries.DIMENSION,
|
||||||
|
net.minecraft.resources.ResourceLocation.parse(plot.dimension()));
|
||||||
|
String owner = getOwnerUuid(src.getServer(), plot.id());
|
||||||
|
if (owner != null) { skipped++; continue; } // already owned, leave it
|
||||||
|
for (int[] c : plot.chunks()) {
|
||||||
|
if (FtbBridge.isClaimed(dim, c[0], c[1])) { skipped++; continue; }
|
||||||
|
if (FtbBridge.claimFor(plotsTeam.get(), src, dim, c[0], c[1])) claimed++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
final int claimedFinal = claimed;
|
||||||
|
final int skippedFinal = skipped;
|
||||||
|
src.sendSuccess(() -> Component.literal(
|
||||||
|
"§aSpawn init complete. Spawn team: " + spawn.name() +
|
||||||
|
", Plots team: " + plotsCfg.name() +
|
||||||
|
". Claimed " + claimedFinal + " plot chunks, skipped " + skippedFinal + "."), true);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── /bns admin plot define <id> <name> <price> ─────────────────
|
||||||
|
|
||||||
|
private static int doDefine(CommandSourceStack src, String id, String name, int price) {
|
||||||
|
ServerPlayer sp = src.getPlayer();
|
||||||
|
if (sp == null) { src.sendFailure(Component.literal("/bns admin plot define must be run as a player")); return 0; }
|
||||||
|
int chunkX = sp.blockPosition().getX() >> 4;
|
||||||
|
int chunkZ = sp.blockPosition().getZ() >> 4;
|
||||||
|
String dim = sp.level().dimension().location().toString();
|
||||||
|
|
||||||
|
PlotRegistry registry = Plot.REGISTRY;
|
||||||
|
Plot existing = registry.byId(id);
|
||||||
|
List<int[]> chunks;
|
||||||
|
String displayName;
|
||||||
|
String size;
|
||||||
|
long savePrice = price;
|
||||||
|
String saveDim = dim;
|
||||||
|
|
||||||
|
if (existing != null) {
|
||||||
|
// Add this chunk to the existing plot (multi-chunk plots)
|
||||||
|
chunks = new ArrayList<>(existing.chunks());
|
||||||
|
for (int[] c : chunks) {
|
||||||
|
if (c[0] == chunkX && c[1] == chunkZ) {
|
||||||
|
sp.sendSystemMessage(Component.literal("§eChunk already in plot " + id));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
chunks.add(new int[]{chunkX, chunkZ});
|
||||||
|
displayName = existing.name();
|
||||||
|
size = existing.size();
|
||||||
|
savePrice = existing.price(); // keep existing price
|
||||||
|
saveDim = existing.dimension();
|
||||||
|
} else {
|
||||||
|
chunks = new ArrayList<>();
|
||||||
|
chunks.add(new int[]{chunkX, chunkZ});
|
||||||
|
displayName = name;
|
||||||
|
size = "medium"; // default; adjust later or via JSON
|
||||||
|
}
|
||||||
|
|
||||||
|
Plot updated = new Plot(id, displayName, size, savePrice, saveDim, Collections.unmodifiableList(chunks));
|
||||||
|
PlotRegistry next = registry.withPlot(updated);
|
||||||
|
Plot.REGISTRY = next;
|
||||||
|
PlotConfig.save(src.getServer(), next);
|
||||||
|
|
||||||
|
// Claim chunk for the Plots team
|
||||||
|
Optional<Team> plotsTeam = FtbBridge.getTeamByName(next.plotsTeam().name());
|
||||||
|
if (plotsTeam.isPresent()) {
|
||||||
|
ResourceKey<Level> dimKey = sp.level().dimension();
|
||||||
|
FtbBridge.claimFor(plotsTeam.get(), src, dimKey, chunkX, chunkZ);
|
||||||
|
}
|
||||||
|
|
||||||
|
sp.sendSystemMessage(Component.literal("§aPlot '" + id + "' now has " + chunks.size() + " chunks."));
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int doList(CommandSourceStack src) {
|
||||||
|
PlotRegistry r = Plot.REGISTRY;
|
||||||
|
src.sendSuccess(() -> Component.literal("§e=== Plots (" + r.plots().size() + ") ==="), false);
|
||||||
|
for (Plot p : r.plots()) {
|
||||||
|
String owner = getOwnerUuid(src.getServer(), p.id());
|
||||||
|
String state = owner == null ? "§7for sale" : "§a owned";
|
||||||
|
String line = " " + p.id() + " §f" + p.name() + "§r " +
|
||||||
|
"§6" + p.price() + " spurs§r " + state +
|
||||||
|
" §8(" + p.chunks().size() + " chunks)§r";
|
||||||
|
src.sendSuccess(() -> Component.literal(line), false);
|
||||||
|
}
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int doRemove(CommandSourceStack src, String id) {
|
||||||
|
Plot p = Plot.byId(id);
|
||||||
|
if (p == null) { src.sendFailure(Component.literal("Unknown plot: " + id)); return 0; }
|
||||||
|
// Unclaim its chunks first
|
||||||
|
Optional<Team> plotsTeam = FtbBridge.getTeamByName(Plot.REGISTRY.plotsTeam().name());
|
||||||
|
if (plotsTeam.isPresent()) {
|
||||||
|
ResourceKey<Level> dim = ResourceKey.create(net.minecraft.core.registries.Registries.DIMENSION,
|
||||||
|
net.minecraft.resources.ResourceLocation.parse(p.dimension()));
|
||||||
|
for (int[] c : p.chunks()) {
|
||||||
|
FtbBridge.unclaimFor(plotsTeam.get(), src, dim, c[0], c[1]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
PlotRegistry next = Plot.REGISTRY.withoutPlot(id);
|
||||||
|
Plot.REGISTRY = next;
|
||||||
|
PlotConfig.save(src.getServer(), next);
|
||||||
|
clearOwner(src.getServer(), id); // also clear any stale ownership
|
||||||
|
src.sendSuccess(() -> Component.literal("§eRemoved plot " + id), true);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int doReload(CommandSourceStack src) {
|
||||||
|
PlotRegistry r = PlotConfig.load(src.getServer());
|
||||||
|
Plot.REGISTRY = r;
|
||||||
|
src.sendSuccess(() -> Component.literal("§aReloaded — " + r.plots().size() + " plots loaded"), true);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── NBT helpers ─────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private static List<String> ownedPlotsOf(ServerPlayer sp) {
|
||||||
|
List<String> out = new ArrayList<>();
|
||||||
|
CompoundTag pd = sp.getPersistentData();
|
||||||
|
if (!pd.contains(PER_PLAYER_KEY)) return out;
|
||||||
|
ListTag list = pd.getList(PER_PLAYER_KEY, Tag.TAG_STRING);
|
||||||
|
for (int i = 0; i < list.size(); i++) out.add(list.getString(i));
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void addOwnedPlot(ServerPlayer sp, String plotId) {
|
||||||
|
CompoundTag pd = sp.getPersistentData();
|
||||||
|
ListTag list = pd.contains(PER_PLAYER_KEY) ? pd.getList(PER_PLAYER_KEY, Tag.TAG_STRING) : new ListTag();
|
||||||
|
list.add(StringTag.valueOf(plotId));
|
||||||
|
pd.put(PER_PLAYER_KEY, list);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void removeOwnedPlot(ServerPlayer sp, String plotId) {
|
||||||
|
CompoundTag pd = sp.getPersistentData();
|
||||||
|
if (!pd.contains(PER_PLAYER_KEY)) return;
|
||||||
|
ListTag list = pd.getList(PER_PLAYER_KEY, Tag.TAG_STRING);
|
||||||
|
for (int i = list.size() - 1; i >= 0; i--) {
|
||||||
|
if (list.getString(i).equals(plotId)) list.remove(i);
|
||||||
|
}
|
||||||
|
pd.put(PER_PLAYER_KEY, list);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String getOwnerUuid(MinecraftServer server, String plotId) {
|
||||||
|
CompoundTag serverData = ServerEconomyBridge.getKubeJSServerData(server);
|
||||||
|
if (!serverData.contains(OWNERSHIP_KEY)) return null;
|
||||||
|
CompoundTag map = serverData.getCompound(OWNERSHIP_KEY);
|
||||||
|
return map.contains(plotId) ? map.getString(plotId) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void setOwner(MinecraftServer server, String plotId, String uuid) {
|
||||||
|
CompoundTag serverData = ServerEconomyBridge.getKubeJSServerData(server);
|
||||||
|
CompoundTag map = serverData.contains(OWNERSHIP_KEY) ? serverData.getCompound(OWNERSHIP_KEY) : new CompoundTag();
|
||||||
|
map.putString(plotId, uuid);
|
||||||
|
serverData.put(OWNERSHIP_KEY, map);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void clearOwner(MinecraftServer server, String plotId) {
|
||||||
|
CompoundTag serverData = ServerEconomyBridge.getKubeJSServerData(server);
|
||||||
|
if (!serverData.contains(OWNERSHIP_KEY)) return;
|
||||||
|
CompoundTag map = serverData.getCompound(OWNERSHIP_KEY);
|
||||||
|
map.remove(plotId);
|
||||||
|
serverData.put(OWNERSHIP_KEY, map);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user