Add feature starterkit per dimension
This commit is contained in:
@@ -1,11 +1,13 @@
|
||||
# Lobby Systems v1.5.7 — Fabric 26.2
|
||||
# Lobby Systems v1.6.0 — Fabric 26.2
|
||||
|
||||
Server-side lobby routing untuk Minecraft 26.2/Fabric. v1.5.7 mempertahankan initial Lobby placement sebelum world awal dikirim ke client dan menjadikan Lobby Systems sebagai pengelola gamemode per-dimension yang selalu aktif.
|
||||
Server-side lobby routing dan dimension policy untuk Minecraft 26.2/Fabric. v1.6.0 menambahkan Starter Kit per-dimension dengan active assignment dan recipient history, sambil mempertahankan initial Lobby placement pre-login, gamemode authoritative, gamerule per-dimension, portal, dan position persistence.
|
||||
|
||||
**v1.5.6 login placement fix (dipertahankan):** player data tetap dibaca untuk menyimpan posisi gameplay terakhir, tetapi sebelum `PlayerList` mengirim initial world state, Lobby Systems mengganti initial level, posisi, dan yaw/pitch ke konfigurasi Lobby. Gamemode disinkronkan segera setelah JOIN dan dikoreksi singkat pada +1, +5, dan +20 tick.
|
||||
|
||||
**v1.5.7 behavior update:** Lobby Systems sekarang selalu menjadi source of truth untuk gamemode per-dimension. Opsi `manageDimensionGamemode` dihapus; config lama akan dimigrasikan otomatis dengan membuang field tersebut. Command `list`, `set`, dan `remove` selalu tersedia. Compatibility warning Dimensional Inventories tetap aktif.
|
||||
|
||||
**v1.6.0 Starter Kit:** operator dapat menyimpan snapshot inventory/equipment sebagai kit, mengaktifkan satu kit per dimension, memuat kit untuk diedit, memberi kit secara manual, dan mengelola recipient history. Automatic starter kit diberikan satu kali per player per dimension setelah transisi inventory destination stabil. Mengganti active kit tidak memberi ulang kepada player yang sudah pernah menerima starter kit di dimension tersebut.
|
||||
|
||||
### Reload feedback
|
||||
|
||||
`/ls reload` menulis banner `RELOAD START` dan `RELOAD COMPLETE` hanya ke console server. Ringkasan reload satu baris dikirim langsung hanya ke player admin/operator yang menjalankan command; ringkasan tersebut tidak dibroadcast ke player lain dan tidak dikirim melalui command feedback ke console.
|
||||
@@ -99,7 +101,9 @@ config/
|
||||
├── player-positions.json
|
||||
├── portals.json
|
||||
├── dimension-gamemodes.json
|
||||
└── dimension-gamerules.json
|
||||
├── dimension-gamerules.json
|
||||
├── starterkits.json
|
||||
└── starterkit-recipients.json
|
||||
```
|
||||
|
||||
### `config/lobby/config.json`
|
||||
@@ -235,6 +239,17 @@ Jika file tujuan baru sudah ada, migrator tidak menimpanya.
|
||||
/ls gamemode set <dimension> <survival|creative|adventure|spectator>
|
||||
/ls gamemode remove <dimension>
|
||||
|
||||
/ls starterkit set <name> <dimension>
|
||||
/ls starterkit load <name>
|
||||
/ls starterkit activate <name>
|
||||
/ls starterkit deactivate <name>
|
||||
/ls starterkit give <name> <player>
|
||||
/ls starterkit list
|
||||
/ls starterkit info <name>
|
||||
/ls starterkit status <dimension> <player>
|
||||
/ls starterkit reset <dimension> <player>
|
||||
/ls starterkit delete <name>
|
||||
|
||||
/ls portal create <name> <target_dimension>
|
||||
/ls portal delete <name>
|
||||
/ls portal list
|
||||
@@ -288,6 +303,14 @@ Gunakan mapping gamemode agar player otomatis mendapat gamemode yang sesuai saat
|
||||
|
||||
Command gamemode selalu tersedia: `/ls gamemode list`, `/ls gamemode set <dimension> <mode>`, dan `/ls gamemode remove <dimension>`. Mapping disimpan di `config/lobby/dimension-gamemodes.json`, diterapkan langsung ke player pada dimension terkait, dan ikut direload oleh `/ls reload`.
|
||||
|
||||
### Starter Kit per-dimension
|
||||
|
||||
Starter kit dibuat dari snapshot inventory operator dengan `/ls starterkit set <name> <dimension>`. Kit baru selalu INACTIVE sampai `/ls starterkit activate <name>` dijalankan. Satu dimension hanya dapat memiliki satu active starter kit; mengaktifkan kit lain otomatis mengganti pointer active tanpa menghapus definisi kit lama.
|
||||
|
||||
Automatic delivery hanya terjadi satu kali per **player + dimension**. Recipient history disimpan di `config/lobby/starterkit-recipients.json`, sedangkan definisi kit dan `activeByDimension` disimpan di `config/lobby/starterkits.json`. Mengganti active kit tidak membuat player lama menerima kit baru. `/ls starterkit give` adalah forced administrative delivery dan tidak mengubah recipient history. `/ls starterkit reset <dimension> <player>` membuat player eligible lagi pada entry berikutnya.
|
||||
|
||||
Snapshot menyimpan slot inventory player termasuk hotbar, main inventory, armor/equipment, dan offhand menggunakan codec ItemStack vanilla agar components/data item modded ikut tersimpan. Automatic delivery ditunda singkat setelah dimension entry untuk memberi kesempatan mod inventory per-dimension seperti Dimensional Inventories menyelesaikan pergantian inventory destination. Jika slot snapshot sudah terisi, Lobby Systems mencoba inventory kosong lain dan menjatuhkan sisa stack dengan aman bila inventory penuh; item existing player tidak dihapus.
|
||||
|
||||
### `/ls reload`
|
||||
|
||||
Reload live:
|
||||
@@ -295,6 +318,8 @@ Reload live:
|
||||
- `config/lobby/config.json`
|
||||
- `config/lobby/portals.json`
|
||||
- `config/lobby/dimension-gamemodes.json`
|
||||
- `config/lobby/starterkits.json`
|
||||
- `config/lobby/starterkit-recipients.json`
|
||||
- `config/lobby/dimension-gamerules.json`
|
||||
- lobby exclusion pada player-position store
|
||||
|
||||
|
||||
+1
-1
@@ -5,6 +5,6 @@ minecraft_version=26.2
|
||||
loader_version=0.19.3
|
||||
loom_version=1.17-SNAPSHOT
|
||||
fabric_api_version=0.156.0+26.2
|
||||
mod_version=1.5.7
|
||||
mod_version=1.6.0
|
||||
maven_group=id.dartokloning
|
||||
archives_base_name=lobby-systems
|
||||
|
||||
@@ -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); }
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,7 @@
|
||||
"id": "dartokloning",
|
||||
"version": "${version}",
|
||||
"name": "Lobby Systems",
|
||||
"description": "Server-side Fabric lobby routing, persistent world positions, portals, per-dimension gamemodes, and native persistent per-dimension gamerules.",
|
||||
"description": "Server-side Fabric lobby routing, persistent world positions, portals, per-dimension gamemodes/gamerules, and starter kits with recipient history.",
|
||||
"authors": [
|
||||
"DartoKloning"
|
||||
],
|
||||
|
||||
Reference in New Issue
Block a user