Add feature starterkit per dimension

This commit is contained in:
Darto KLoning
2026-09-08 12:49:21 +07:00
parent c1747e2c27
commit 90455467e1
6 changed files with 707 additions and 5 deletions
@@ -11,6 +11,8 @@ public final class ConfigPaths {
public static final Path PORTALS = DIRECTORY.resolve("portals.json");
public static final Path DIMENSION_GAMEMODES = DIRECTORY.resolve("dimension-gamemodes.json");
public static final Path DIMENSION_GAMERULES = DIRECTORY.resolve("dimension-gamerules.json");
public static final Path STARTER_KITS = DIRECTORY.resolve("starterkits.json");
public static final Path STARTER_KIT_RECIPIENTS = DIRECTORY.resolve("starterkit-recipients.json");
private ConfigPaths() {}
}
@@ -13,6 +13,7 @@ import net.minecraft.commands.CommandSourceStack;
import net.minecraft.commands.Commands;
import net.minecraft.commands.SharedSuggestionProvider;
import net.minecraft.commands.arguments.IdentifierArgument;
import net.minecraft.commands.arguments.EntityArgument;
import net.minecraft.core.BlockPos;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.core.registries.Registries;
@@ -45,6 +46,7 @@ public final class LobbySystemsMod implements ModInitializer {
private static final TeleportCoordinator TELEPORTS = new TeleportCoordinator();
private static final DimensionGameModeManager DIMENSION_GAMEMODES = new DimensionGameModeManager();
private static final DimensionGameRuleManager DIMENSION_GAMERULES = new DimensionGameRuleManager();
private static final StarterKitManager STARTER_KITS = new StarterKitManager();
private static int saveTicker;
@Override
@@ -92,6 +94,15 @@ public final class LobbySystemsMod implements ModInitializer {
"Mode: native per-dimension"
);
boolean starterKitsLoaded = STARTER_KITS.load();
LobbyLog.state(
starterKitsLoaded ? "STARTER KITS LOADED" : "STARTER KITS LOAD FAILED",
"Definitions: " + ConfigPaths.STARTER_KITS,
"Recipients: " + ConfigPaths.STARTER_KIT_RECIPIENTS,
"Kits: " + STARTER_KITS.size(),
"Active dimensions: " + STARTER_KITS.activeCount()
);
ServerLifecycleEvents.SERVER_STARTED.register(server -> {
DimensionGameRuleManager.ApplySummary summary = DIMENSION_GAMERULES.applyAll(server);
LobbyLog.state(
@@ -140,6 +151,7 @@ public final class LobbySystemsMod implements ModInitializer {
DIMENSION_GAMEMODES.applyForPlayer(player, true);
DIMENSION_GAMEMODES.beginJoinEnforcement(player.getUUID());
SESSIONS.onJoin(player.getUUID(), lobbyKey());
STARTER_KITS.onJoin(player);
LobbyLog.state(
"PLAYER JOIN",
@@ -157,6 +169,7 @@ public final class LobbySystemsMod implements ModInitializer {
TELEPORTS.onDisconnect(player.getUUID());
PORTALS.onDisconnect(player.getUUID());
DIMENSION_GAMEMODES.onDisconnect(player.getUUID());
STARTER_KITS.onDisconnect(player.getUUID());
LobbyLog.state(
"PLAYER DISCONNECT",
"Player: " + player.getName().getString(),
@@ -169,6 +182,7 @@ public final class LobbySystemsMod implements ModInitializer {
TELEPORTS.tick();
DIMENSION_GAMEMODES.tick(server);
STARTER_KITS.tick(server);
ResourceKey<Level> lobby = lobbyKey();
for (ServerPlayer player : server.getPlayerList().getPlayers()) {
@@ -271,6 +285,75 @@ public final class LobbySystemsMod implements ModInitializer {
.executes(ctx -> removeDimensionGameMode(
ctx.getSource(),
IdentifierArgument.getId(ctx, "dimension").toString())))))
.then(Commands.literal("starterkit")
.then(Commands.literal("set")
.then(Commands.argument("name", StringArgumentType.word())
.then(Commands.argument("dimension", IdentifierArgument.id())
.suggests((ctx, builder) -> SharedSuggestionProvider.suggestResource(
ctx.getSource().getServer().levelKeys().stream().map(ResourceKey::identifier), builder))
.executes(ctx -> starterKitSet(
ctx.getSource(),
ctx.getSource().getServer(),
ctx.getSource().getPlayerOrException(),
StringArgumentType.getString(ctx, "name"),
IdentifierArgument.getId(ctx, "dimension").toString())))))
.then(Commands.literal("load")
.then(Commands.argument("name", StringArgumentType.word())
.suggests((ctx, builder) -> SharedSuggestionProvider.suggest(STARTER_KITS.names(), builder))
.executes(ctx -> starterKitLoad(
ctx.getSource(),
ctx.getSource().getServer(),
ctx.getSource().getPlayerOrException(),
StringArgumentType.getString(ctx, "name")))))
.then(Commands.literal("activate")
.then(Commands.argument("name", StringArgumentType.word())
.suggests((ctx, builder) -> SharedSuggestionProvider.suggest(STARTER_KITS.names(), builder))
.executes(ctx -> starterKitOperation(
ctx.getSource(), STARTER_KITS.activate(StringArgumentType.getString(ctx, "name"))))))
.then(Commands.literal("deactivate")
.then(Commands.argument("name", StringArgumentType.word())
.suggests((ctx, builder) -> SharedSuggestionProvider.suggest(STARTER_KITS.names(), builder))
.executes(ctx -> starterKitOperation(
ctx.getSource(), STARTER_KITS.deactivate(StringArgumentType.getString(ctx, "name"))))))
.then(Commands.literal("give")
.then(Commands.argument("name", StringArgumentType.word())
.suggests((ctx, builder) -> SharedSuggestionProvider.suggest(STARTER_KITS.names(), builder))
.then(Commands.argument("player", EntityArgument.player())
.executes(ctx -> starterKitGive(
ctx.getSource(),
ctx.getSource().getServer(),
StringArgumentType.getString(ctx, "name"),
EntityArgument.getPlayer(ctx, "player"))))))
.then(Commands.literal("list")
.executes(ctx -> starterKitList(ctx.getSource())))
.then(Commands.literal("info")
.then(Commands.argument("name", StringArgumentType.word())
.suggests((ctx, builder) -> SharedSuggestionProvider.suggest(STARTER_KITS.names(), builder))
.executes(ctx -> starterKitInfo(ctx.getSource(), StringArgumentType.getString(ctx, "name")))))
.then(Commands.literal("status")
.then(Commands.argument("dimension", IdentifierArgument.id())
.suggests((ctx, builder) -> SharedSuggestionProvider.suggestResource(
ctx.getSource().getServer().levelKeys().stream().map(ResourceKey::identifier), builder))
.then(Commands.argument("player", EntityArgument.player())
.executes(ctx -> starterKitStatus(
ctx.getSource(),
IdentifierArgument.getId(ctx, "dimension").toString(),
EntityArgument.getPlayer(ctx, "player"))))))
.then(Commands.literal("reset")
.then(Commands.argument("dimension", IdentifierArgument.id())
.suggests((ctx, builder) -> SharedSuggestionProvider.suggestResource(
ctx.getSource().getServer().levelKeys().stream().map(ResourceKey::identifier), builder))
.then(Commands.argument("player", EntityArgument.player())
.executes(ctx -> starterKitReset(
ctx.getSource(),
IdentifierArgument.getId(ctx, "dimension").toString(),
EntityArgument.getPlayer(ctx, "player"))))))
.then(Commands.literal("delete")
.then(Commands.argument("name", StringArgumentType.word())
.suggests((ctx, builder) -> SharedSuggestionProvider.suggest(STARTER_KITS.names(), builder))
.executes(ctx -> starterKitOperation(
ctx.getSource(), STARTER_KITS.delete(StringArgumentType.getString(ctx, "name"))))))
)
.then(Commands.literal("portal")
.then(Commands.literal("create")
.then(Commands.argument("name", StringArgumentType.word())
@@ -313,6 +396,88 @@ public final class LobbySystemsMod implements ModInitializer {
);
}
private static int starterKitSet(CommandSourceStack source, MinecraftServer server, ServerPlayer player, String name, String dimension) {
return starterKitOperation(source, STARTER_KITS.setFromPlayer(server, player, name, dimension));
}
private static int starterKitLoad(CommandSourceStack source, MinecraftServer server, ServerPlayer player, String name) {
return starterKitOperation(source, STARTER_KITS.loadToPlayer(server, player, name));
}
private static int starterKitGive(CommandSourceStack source, MinecraftServer server, String name, ServerPlayer target) {
return starterKitOperation(source, STARTER_KITS.give(server, target, name));
}
private static int starterKitOperation(CommandSourceStack source, StarterKitManager.OperationResult result) {
if (result.success()) {
sendCommandMessage(source, Component.literal("§a" + result.message()));
return 1;
}
source.sendFailure(Component.literal("§c" + result.message()));
return 0;
}
private static int starterKitList(CommandSourceStack source) {
sendCommandMessage(source, Component.literal("§8§m----------------------------------------"));
sendCommandMessage(source, Component.literal("§6§lLobby Systems Starter Kits §r§7[§f" + STARTER_KITS.size() + "§7]"));
sendCommandMessage(source, Component.literal("§8§m----------------------------------------"));
if (STARTER_KITS.size() == 0) {
sendCommandMessage(source, Component.literal("§7Belum ada starter kit."));
} else {
for (Map.Entry<String, StarterKitManager.KitDefinition> entry : STARTER_KITS.entries()) {
boolean active = STARTER_KITS.isActive(entry.getKey());
sendCommandMessage(source, Component.literal(
"§8• §f" + entry.getKey()
+ " §8-> §7" + entry.getValue().dimension
+ " " + (active ? "§a[ACTIVE]" : "§8[INACTIVE]")
+ " §8| §7slots: §f" + entry.getValue().occupiedSlots()));
}
}
sendCommandMessage(source, Component.literal("§8§m----------------------------------------"));
return STARTER_KITS.size();
}
private static int starterKitInfo(CommandSourceStack source, String name) {
StarterKitManager.KitDefinition kit = STARTER_KITS.get(name);
if (kit == null) {
source.sendFailure(Component.literal("§cStarter kit '" + name + "' tidak ditemukan."));
return 0;
}
String normalized = name.trim().toLowerCase();
sendCommandMessage(source, Component.literal("§6§lStarter Kit: §f" + normalized));
sendCommandMessage(source, Component.literal("§7Dimension: §f" + kit.dimension));
sendCommandMessage(source, Component.literal("§7Status: " + (STARTER_KITS.isActive(normalized) ? "§aACTIVE" : "§8INACTIVE")));
sendCommandMessage(source, Component.literal("§7Occupied slots: §f" + kit.occupiedSlots()));
return 1;
}
private static int starterKitStatus(CommandSourceStack source, String dimension, ServerPlayer player) {
StarterKitManager.RecipientRecord record = STARTER_KITS.recipient(player.getUUID(), dimension);
String active = STARTER_KITS.activeForDimension(dimension);
sendCommandMessage(source, Component.literal("§6§lStarter Kit Status"));
sendCommandMessage(source, Component.literal("§7Player: §f" + player.getName().getString()));
sendCommandMessage(source, Component.literal("§7Dimension: §f" + dimension));
if (record == null) {
sendCommandMessage(source, Component.literal("§7Status: §eNOT RECEIVED"));
} else {
sendCommandMessage(source, Component.literal("§7Status: §aRECEIVED"));
sendCommandMessage(source, Component.literal("§7Kit received: §f" + record.kit));
sendCommandMessage(source, Component.literal("§7Received at: §f" + record.receivedAt));
}
sendCommandMessage(source, Component.literal("§7Current active kit: §f" + (active == null ? "none" : active)));
return 1;
}
private static int starterKitReset(CommandSourceStack source, String dimension, ServerPlayer player) {
StarterKitManager.OperationResult result = STARTER_KITS.resetRecipient(player.getUUID(), dimension);
if (result.success()) {
sendCommandMessage(source, Component.literal("§a" + player.getName().getString() + ": " + result.message()));
return 1;
}
source.sendFailure(Component.literal("§c" + result.message()));
return 0;
}
private static int handleLobbyCommand(ServerPlayer player, MinecraftServer server) {
ResourceKey<Level> lobby = lobbyKey();
if (player.level().dimension().equals(lobby)) {
@@ -390,6 +555,13 @@ public final class LobbySystemsMod implements ModInitializer {
}
DimensionGameRuleManager.ApplySummary gameRuleSummary = DIMENSION_GAMERULES.applyAll(server);
boolean starterKitsOk = STARTER_KITS.load();
if (!starterKitsOk) {
source.sendFailure(Component.literal("§estarterkits.json atau starterkit-recipients.json bermasalah. Cek console."));
LobbyLog.state("RELOAD FAILED", "Reason: starter kit data gagal dibaca");
return 0;
}
if (source.getEntity() instanceof ServerPlayer player) {
player.sendSystemMessage(Component.literal(
"§aLobby Systems direload. §7Lobby: §f" + CONFIG.get().lobbyDimension
@@ -404,6 +576,7 @@ public final class LobbySystemsMod implements ModInitializer {
"Portal visual blocks restored: " + repairedPortalVisuals,
"Dimension gamemodes: " + DIMENSION_GAMEMODES.all().size() + " (authoritative)",
"Dimension gamerules: " + DIMENSION_GAMERULES.all().size() + " dimensions / " + gameRuleSummary.applied() + " rules applied",
"Starter kits: " + STARTER_KITS.size() + " / active dimensions: " + STARTER_KITS.activeCount(),
"Player positions lobby exclusion: active"
);
return 1;
@@ -0,0 +1,502 @@
package id.dartokloning.lobbysystems;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import com.mojang.serialization.JsonOps;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.entity.player.Inventory;
import net.minecraft.world.item.ItemStack;
import java.io.Reader;
import java.io.Writer;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
/**
* Starter kit per-dimension.
*
* Eligibility automatic delivery sengaja dicatat per PLAYER + DIMENSION, bukan per kit.
* Dengan begitu mengganti active kit tidak memberikan paket baru kepada player lama.
*/
public final class StarterKitManager {
private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create();
private static final Path KITS_PATH = ConfigPaths.STARTER_KITS;
private static final Path RECIPIENTS_PATH = ConfigPaths.STARTER_KIT_RECIPIENTS;
private static final int DELIVERY_DELAY_TICKS = 10;
private StarterKitConfig config = new StarterKitConfig();
private final Map<String, Map<String, RecipientRecord>> recipients = new LinkedHashMap<>();
private final Map<UUID, String> lastDimension = new LinkedHashMap<>();
private final Map<UUID, PendingDelivery> pending = new LinkedHashMap<>();
private long tick;
public boolean load() {
boolean kitsOk = loadKits();
boolean recipientsOk = loadRecipients();
return kitsOk && recipientsOk;
}
public boolean loadKits() {
if (!Files.exists(KITS_PATH)) {
config = new StarterKitConfig();
return saveKits();
}
try (Reader reader = Files.newBufferedReader(KITS_PATH)) {
StarterKitConfig loaded = GSON.fromJson(reader, StarterKitConfig.class);
config = loaded == null ? new StarterKitConfig() : loaded;
config.normalize();
return true;
} catch (Exception e) {
LobbySystemsMod.LOGGER.error("Failed to load {}. Keeping previous starter-kit config.", KITS_PATH, e);
return false;
}
}
public boolean loadRecipients() {
if (!Files.exists(RECIPIENTS_PATH)) {
recipients.clear();
return saveRecipients();
}
try (Reader reader = Files.newBufferedReader(RECIPIENTS_PATH)) {
JsonElement root = JsonParser.parseReader(reader);
Map<String, Map<String, RecipientRecord>> loaded = new LinkedHashMap<>();
if (root.isJsonObject()) {
for (Map.Entry<String, JsonElement> playerEntry : root.getAsJsonObject().entrySet()) {
if (!playerEntry.getValue().isJsonObject()) continue;
Map<String, RecipientRecord> byDimension = new LinkedHashMap<>();
for (Map.Entry<String, JsonElement> dimensionEntry : playerEntry.getValue().getAsJsonObject().entrySet()) {
try {
RecipientRecord record = GSON.fromJson(dimensionEntry.getValue(), RecipientRecord.class);
if (record != null) byDimension.put(normalizeDimension(dimensionEntry.getKey()), record);
} catch (Exception ignored) {
LobbySystemsMod.LOGGER.warn("Skipping invalid starter-kit recipient entry for {} / {}",
playerEntry.getKey(), dimensionEntry.getKey());
}
}
loaded.put(playerEntry.getKey(), byDimension);
}
}
recipients.clear();
recipients.putAll(loaded);
return true;
} catch (Exception e) {
LobbySystemsMod.LOGGER.error("Failed to load {}. Keeping previous starter-kit recipients.", RECIPIENTS_PATH, e);
return false;
}
}
public boolean saveKits() {
try {
Files.createDirectories(KITS_PATH.getParent());
try (Writer writer = Files.newBufferedWriter(KITS_PATH)) {
GSON.toJson(config, writer);
}
return true;
} catch (Exception e) {
LobbySystemsMod.LOGGER.error("Failed to save {}", KITS_PATH, e);
return false;
}
}
public boolean saveRecipients() {
try {
Files.createDirectories(RECIPIENTS_PATH.getParent());
try (Writer writer = Files.newBufferedWriter(RECIPIENTS_PATH)) {
GSON.toJson(recipients, writer);
}
return true;
} catch (Exception e) {
LobbySystemsMod.LOGGER.error("Failed to save {}", RECIPIENTS_PATH, e);
return false;
}
}
public void onJoin(ServerPlayer player) {
String dimension = dimensionOf(player);
lastDimension.put(player.getUUID(), dimension);
schedule(player.getUUID(), dimension);
}
public void onDisconnect(UUID uuid) {
lastDimension.remove(uuid);
pending.remove(uuid);
}
public void tick(MinecraftServer server) {
tick++;
for (ServerPlayer player : server.getPlayerList().getPlayers()) {
UUID uuid = player.getUUID();
String dimension = dimensionOf(player);
String previous = lastDimension.put(uuid, dimension);
if (previous == null || !previous.equals(dimension)) {
schedule(uuid, dimension);
}
}
Iterator<Map.Entry<UUID, PendingDelivery>> iterator = pending.entrySet().iterator();
while (iterator.hasNext()) {
Map.Entry<UUID, PendingDelivery> entry = iterator.next();
PendingDelivery job = entry.getValue();
if (job.dueTick > tick) continue;
iterator.remove();
ServerPlayer player = server.getPlayerList().getPlayer(entry.getKey());
if (player == null) continue;
if (!dimensionOf(player).equals(job.dimension)) continue;
tryAutomaticDelivery(server, player, job.dimension);
}
}
private void schedule(UUID uuid, String dimension) {
pending.put(uuid, new PendingDelivery(dimension, tick + DELIVERY_DELAY_TICKS));
}
private void tryAutomaticDelivery(MinecraftServer server, ServerPlayer player, String dimension) {
String normalized = normalizeDimension(dimension);
String activeName = config.activeByDimension.get(normalized);
if (activeName == null || hasReceived(player.getUUID(), normalized)) return;
KitDefinition kit = config.kits.get(activeName);
if (kit == null || !normalizeDimension(kit.dimension).equals(normalized)) {
LobbySystemsMod.LOGGER.warn("Starter kit active mapping is invalid: {} -> {}", normalized, activeName);
return;
}
DeliveryResult result = deliver(server, player, kit);
if (!result.success()) return;
recipients.computeIfAbsent(player.getUUID().toString(), ignored -> new LinkedHashMap<>())
.put(normalized, new RecipientRecord(activeName, Instant.now().toString()));
saveRecipients();
LobbyLog.state(
"STARTER KIT DELIVERED",
"Player: " + player.getName().getString() + " (" + player.getUUID() + ")",
"Dimension: " + normalized,
"Kit: " + activeName,
"Items: " + result.itemStacks(),
"Fallback/drop stacks: " + result.fallbackStacks()
);
}
public OperationResult setFromPlayer(MinecraftServer server, ServerPlayer player, String rawName, String rawDimension) {
String name = normalizeName(rawName);
String dimension = normalizeDimension(rawDimension);
if (name.isBlank()) return OperationResult.fail("Nama starter kit tidak valid.");
if (dimension.isBlank()) return OperationResult.fail("Dimension tidak valid.");
if (server.getLevel(net.minecraft.resources.ResourceKey.create(
net.minecraft.core.registries.Registries.DIMENSION,
net.minecraft.resources.Identifier.parse(dimension))) == null) {
return OperationResult.fail("Dimension '" + dimension + "' tidak ditemukan atau tidak ter-load.");
}
List<SlotSnapshot> slots = new ArrayList<>();
try {
var ops = server.registryAccess().createSerializationContext(JsonOps.INSTANCE);
Inventory inventory = player.getInventory();
for (int slot = 0; slot < inventory.getContainerSize(); slot++) {
ItemStack stack = inventory.getItem(slot);
if (stack.isEmpty()) continue;
JsonElement encoded = ItemStack.MAP_CODEC.codec().encodeStart(ops, stack).getOrThrow();
slots.add(new SlotSnapshot(slot, encoded));
}
} catch (Exception e) {
LobbySystemsMod.LOGGER.error("Failed to serialize starter kit '{}'", name, e);
return OperationResult.fail("Gagal menyimpan ItemStack starter kit. Cek console.");
}
KitDefinition previous = config.kits.get(name);
String previousDimension = previous == null ? null : normalizeDimension(previous.dimension);
boolean wasActive = previousDimension != null && name.equals(config.activeByDimension.get(previousDimension));
config.kits.put(name, new KitDefinition(dimension, slots));
if (wasActive && !previousDimension.equals(dimension)) {
config.activeByDimension.remove(previousDimension);
}
if (!saveKits()) return OperationResult.fail("Gagal menyimpan starterkits.json.");
String status = name.equals(config.activeByDimension.get(dimension)) ? "ACTIVE" : "INACTIVE";
if (wasActive && !previousDimension.equals(dimension)) {
status = "INACTIVE (dipindahkan dari " + previousDimension + ")";
}
return OperationResult.ok("Starter kit '" + name + "' tersimpan untuk " + dimension
+ " dengan " + slots.size() + " slot berisi. Status: " + status + ".");
}
public OperationResult loadToPlayer(MinecraftServer server, ServerPlayer player, String rawName) {
String name = normalizeName(rawName);
KitDefinition kit = config.kits.get(name);
if (kit == null) return OperationResult.fail("Starter kit '" + name + "' tidak ditemukan.");
List<DecodedSlot> decoded;
try {
decoded = decode(server, kit);
} catch (Exception e) {
LobbySystemsMod.LOGGER.error("Failed to decode starter kit '{}'", name, e);
return OperationResult.fail("Gagal membaca ItemStack starter kit. Cek console.");
}
Inventory inventory = player.getInventory();
player.closeContainer();
inventory.clearContent();
int loaded = 0;
for (DecodedSlot slot : decoded) {
if (slot.slot < 0 || slot.slot >= inventory.getContainerSize()) continue;
inventory.setItem(slot.slot, slot.stack.copy());
loaded++;
}
inventory.setChanged();
player.inventoryMenu.broadcastChanges();
return OperationResult.ok("Starter kit '" + name + "' dimuat ke inventory Anda (" + loaded + " slot berisi). Dimension: " + kit.dimension + ".");
}
public OperationResult activate(String rawName) {
String name = normalizeName(rawName);
KitDefinition kit = config.kits.get(name);
if (kit == null) return OperationResult.fail("Starter kit '" + name + "' tidak ditemukan.");
String dimension = normalizeDimension(kit.dimension);
String previous = config.activeByDimension.put(dimension, name);
if (!saveKits()) return OperationResult.fail("Gagal menyimpan starterkits.json.");
if (name.equals(previous)) return OperationResult.ok("Starter kit '" + name + "' sudah ACTIVE untuk " + dimension + ".");
return OperationResult.ok("Starter kit '" + name + "' sekarang ACTIVE untuk " + dimension
+ (previous == null ? "." : ". Kit sebelumnya: '" + previous + "' -> INACTIVE."));
}
public OperationResult deactivate(String rawName) {
String name = normalizeName(rawName);
KitDefinition kit = config.kits.get(name);
if (kit == null) return OperationResult.fail("Starter kit '" + name + "' tidak ditemukan.");
String dimension = normalizeDimension(kit.dimension);
if (!name.equals(config.activeByDimension.get(dimension))) {
return OperationResult.ok("Starter kit '" + name + "' sudah INACTIVE.");
}
config.activeByDimension.remove(dimension);
if (!saveKits()) return OperationResult.fail("Gagal menyimpan starterkits.json.");
return OperationResult.ok("Starter kit '" + name + "' sekarang INACTIVE. Dimension " + dimension + " tidak memiliki active starter kit.");
}
public OperationResult delete(String rawName) {
String name = normalizeName(rawName);
KitDefinition removed = config.kits.remove(name);
if (removed == null) return OperationResult.fail("Starter kit '" + name + "' tidak ditemukan.");
String dimension = normalizeDimension(removed.dimension);
boolean wasActive = name.equals(config.activeByDimension.get(dimension));
if (wasActive) config.activeByDimension.remove(dimension);
if (!saveKits()) return OperationResult.fail("Gagal menyimpan starterkits.json.");
return OperationResult.ok("Starter kit '" + name + "' dihapus" + (wasActive ? " dan active assignment untuk " + dimension + " dilepas." : "."));
}
public OperationResult give(MinecraftServer server, ServerPlayer target, String rawName) {
String name = normalizeName(rawName);
KitDefinition kit = config.kits.get(name);
if (kit == null) return OperationResult.fail("Starter kit '" + name + "' tidak ditemukan.");
DeliveryResult result = deliver(server, target, kit);
if (!result.success()) return OperationResult.fail("Gagal memberikan starter kit. Cek console.");
return OperationResult.ok("Starter kit '" + name + "' diberikan ke " + target.getName().getString()
+ ". Recipient history tidak diubah.");
}
public DeliveryResult deliver(MinecraftServer server, ServerPlayer player, KitDefinition kit) {
final List<DecodedSlot> decoded;
try {
decoded = decode(server, kit);
} catch (Exception e) {
LobbySystemsMod.LOGGER.error("Failed to decode starter kit for delivery", e);
return new DeliveryResult(false, 0, 0);
}
Inventory inventory = player.getInventory();
int itemStacks = 0;
int fallbackStacks = 0;
for (DecodedSlot decodedSlot : decoded) {
ItemStack stack = decodedSlot.stack.copy();
if (stack.isEmpty()) continue;
itemStacks++;
if (decodedSlot.slot >= 0 && decodedSlot.slot < inventory.getContainerSize()
&& inventory.getItem(decodedSlot.slot).isEmpty()) {
inventory.setItem(decodedSlot.slot, stack);
continue;
}
fallbackStacks++;
if (!inventory.add(stack) && !stack.isEmpty()) {
player.drop(stack, false);
} else if (!stack.isEmpty()) {
player.drop(stack, false);
}
}
inventory.setChanged();
player.inventoryMenu.broadcastChanges();
return new DeliveryResult(true, itemStacks, fallbackStacks);
}
private List<DecodedSlot> decode(MinecraftServer server, KitDefinition kit) {
var ops = server.registryAccess().createSerializationContext(JsonOps.INSTANCE);
List<DecodedSlot> decoded = new ArrayList<>();
for (SlotSnapshot snapshot : kit.inventory) {
ItemStack stack = ItemStack.MAP_CODEC.codec().parse(ops, snapshot.stack).getOrThrow();
if (!stack.isEmpty()) decoded.add(new DecodedSlot(snapshot.slot, stack));
}
return decoded;
}
public boolean hasReceived(UUID uuid, String rawDimension) {
Map<String, RecipientRecord> byDimension = recipients.get(uuid.toString());
return byDimension != null && byDimension.containsKey(normalizeDimension(rawDimension));
}
public RecipientRecord recipient(UUID uuid, String rawDimension) {
Map<String, RecipientRecord> byDimension = recipients.get(uuid.toString());
return byDimension == null ? null : byDimension.get(normalizeDimension(rawDimension));
}
public OperationResult resetRecipient(UUID uuid, String rawDimension) {
String dimension = normalizeDimension(rawDimension);
Map<String, RecipientRecord> byDimension = recipients.get(uuid.toString());
if (byDimension == null || byDimension.remove(dimension) == null) {
return OperationResult.ok("Player belum memiliki recipient history untuk " + dimension + ".");
}
if (byDimension.isEmpty()) recipients.remove(uuid.toString());
if (!saveRecipients()) return OperationResult.fail("Gagal menyimpan starterkit-recipients.json.");
return OperationResult.ok("Recipient history untuk " + dimension + " sudah di-reset.");
}
public KitDefinition get(String rawName) {
return config.kits.get(normalizeName(rawName));
}
public String activeForDimension(String rawDimension) {
return config.activeByDimension.get(normalizeDimension(rawDimension));
}
public boolean isActive(String rawName) {
String name = normalizeName(rawName);
KitDefinition kit = config.kits.get(name);
return kit != null && name.equals(config.activeByDimension.get(normalizeDimension(kit.dimension)));
}
public List<String> names() {
return config.kits.keySet().stream().sorted().toList();
}
public List<Map.Entry<String, KitDefinition>> entries() {
return config.kits.entrySet().stream()
.sorted(Comparator.comparing(Map.Entry::getKey))
.toList();
}
public int size() {
return config.kits.size();
}
public int activeCount() {
return config.activeByDimension.size();
}
private static String dimensionOf(ServerPlayer player) {
return normalizeDimension(player.level().dimension().identifier().toString());
}
private static String normalizeName(String raw) {
return raw == null ? "" : raw.trim().toLowerCase();
}
private static String normalizeDimension(String raw) {
return raw == null ? "" : raw.trim().toLowerCase();
}
public static final class StarterKitConfig {
public Map<String, String> activeByDimension = new LinkedHashMap<>();
public Map<String, KitDefinition> kits = new LinkedHashMap<>();
void normalize() {
if (activeByDimension == null) activeByDimension = new LinkedHashMap<>();
if (kits == null) kits = new LinkedHashMap<>();
Map<String, KitDefinition> normalizedKits = new LinkedHashMap<>();
for (Map.Entry<String, KitDefinition> entry : kits.entrySet()) {
String name = normalizeName(entry.getKey());
KitDefinition kit = entry.getValue();
if (name.isBlank() || kit == null) continue;
kit.dimension = normalizeDimension(kit.dimension);
if (kit.inventory == null) kit.inventory = new ArrayList<>();
normalizedKits.put(name, kit);
}
kits = normalizedKits;
Map<String, String> normalizedActive = new LinkedHashMap<>();
for (Map.Entry<String, String> entry : activeByDimension.entrySet()) {
String dimension = normalizeDimension(entry.getKey());
String name = normalizeName(entry.getValue());
KitDefinition kit = kits.get(name);
if (kit != null && normalizeDimension(kit.dimension).equals(dimension)) {
normalizedActive.put(dimension, name);
}
}
activeByDimension = normalizedActive;
}
}
public static final class KitDefinition {
public String dimension;
public List<SlotSnapshot> inventory;
public KitDefinition() {
this("", new ArrayList<>());
}
public KitDefinition(String dimension, List<SlotSnapshot> inventory) {
this.dimension = dimension;
this.inventory = inventory;
}
public int occupiedSlots() {
return inventory == null ? 0 : inventory.size();
}
}
public static final class SlotSnapshot {
public int slot;
public JsonElement stack;
public SlotSnapshot() {}
public SlotSnapshot(int slot, JsonElement stack) {
this.slot = slot;
this.stack = stack;
}
}
public static final class RecipientRecord {
public String kit;
public String receivedAt;
public RecipientRecord() {}
public RecipientRecord(String kit, String receivedAt) {
this.kit = kit;
this.receivedAt = receivedAt;
}
}
private record PendingDelivery(String dimension, long dueTick) {}
private record DecodedSlot(int slot, ItemStack stack) {}
public record DeliveryResult(boolean success, int itemStacks, int fallbackStacks) {}
public record OperationResult(boolean success, String message) {
static OperationResult ok(String message) { return new OperationResult(true, message); }
static OperationResult fail(String message) { return new OperationResult(false, message); }
}
}