Bugfix portal visual systems
This commit is contained in:
@@ -40,6 +40,7 @@ public final class LobbySystemsMod implements ModInitializer {
|
||||
private static final SessionTracker SESSIONS = new SessionTracker();
|
||||
private static final PositionManager POSITIONS = new PositionManager();
|
||||
private static final PortalManager PORTALS = new PortalManager();
|
||||
private static final PortalVisualManager PORTAL_VISUALS = new PortalVisualManager();
|
||||
private static final TeleportCoordinator TELEPORTS = new TeleportCoordinator();
|
||||
private static final DimensionGameModeManager DIMENSION_GAMEMODES = new DimensionGameModeManager();
|
||||
private static final DimensionGameRuleManager DIMENSION_GAMERULES = new DimensionGameRuleManager();
|
||||
@@ -98,6 +99,16 @@ public final class LobbySystemsMod implements ModInitializer {
|
||||
"Rules applied: " + summary.applied(),
|
||||
"Rules skipped: " + summary.skipped()
|
||||
);
|
||||
|
||||
int visualBlocks = PORTAL_VISUALS.repairAll(server, PORTALS, false);
|
||||
if (visualBlocks > 0) {
|
||||
LobbyLog.state(
|
||||
"PORTAL VISUALS RESTORED",
|
||||
"Visual blocks restored: " + visualBlocks,
|
||||
"Visual type: vanilla nether_portal",
|
||||
"Client mod required: no"
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
ServerLevelEvents.LOAD.register((server, world) -> {
|
||||
@@ -110,6 +121,12 @@ public final class LobbySystemsMod implements ModInitializer {
|
||||
"Rules skipped: " + summary.skipped()
|
||||
);
|
||||
}
|
||||
|
||||
int visualBlocks = PORTAL_VISUALS.applyToLevel(world, PORTALS);
|
||||
if (visualBlocks > 0) {
|
||||
LOGGER.info("Restored {} Lobby Systems portal visual block(s) in {}.",
|
||||
visualBlocks, world.dimension().identifier());
|
||||
}
|
||||
});
|
||||
|
||||
ServerPlayConnectionEvents.JOIN.register((listener, sender, server) -> {
|
||||
@@ -161,6 +178,7 @@ public final class LobbySystemsMod implements ModInitializer {
|
||||
LobbyConfig cfg = CONFIG.get();
|
||||
PORTALS.tick(server, cfg.portalCheckIntervalTicks, cfg.portalCooldownTicks,
|
||||
(player, portalName, portal) -> activatePortal(player, server, portalName, portal));
|
||||
PORTAL_VISUALS.tick(server, PORTALS);
|
||||
|
||||
if (++saveTicker >= 1200) {
|
||||
saveTicker = 0;
|
||||
@@ -351,6 +369,7 @@ public final class LobbySystemsMod implements ModInitializer {
|
||||
LobbyLog.state("RELOAD FAILED", "Reason: portals.json gagal dibaca");
|
||||
return 0;
|
||||
}
|
||||
int repairedPortalVisuals = PORTAL_VISUALS.repairAll(server, PORTALS, false);
|
||||
|
||||
boolean gameModesOk = DIMENSION_GAMEMODES.load();
|
||||
if (!gameModesOk) {
|
||||
@@ -385,6 +404,7 @@ public final class LobbySystemsMod implements ModInitializer {
|
||||
"Lobby: " + CONFIG.get().lobbyDimension,
|
||||
"Minimum play: " + CONFIG.get().minimumPlaySeconds + "s",
|
||||
"Portals: " + PORTALS.size(),
|
||||
"Portal visual blocks restored: " + repairedPortalVisuals,
|
||||
"Dimension gamemodes: " + DIMENSION_GAMEMODES.all().size()
|
||||
+ " (management " + (CONFIG.get().manageDimensionGamemode ? "enabled" : "disabled") + ")",
|
||||
"Dimension gamerules: " + DIMENSION_GAMERULES.all().size() + " dimensions / " + gameRuleSummary.applied() + " rules applied",
|
||||
@@ -697,42 +717,66 @@ public final class LobbySystemsMod implements ModInitializer {
|
||||
|
||||
try {
|
||||
WorldEditSelectionBridge.Selection s = WorldEditSelectionBridge.readSelection(player);
|
||||
String visualAxis = PORTAL_VISUALS.determineAxis(s, player);
|
||||
PortalDefinition portal = new PortalDefinition(
|
||||
player.level().dimension().identifier().toString(),
|
||||
new PortalPoint(s.minX(), s.minY(), s.minZ()),
|
||||
new PortalPoint(s.maxX(), s.maxY(), s.maxZ()),
|
||||
targetId
|
||||
targetId,
|
||||
"nether_portal",
|
||||
visualAxis
|
||||
);
|
||||
|
||||
// Apply the visual before persisting the portal so a bad selection can
|
||||
// fail safely without leaving a half-created portal definition.
|
||||
int visualBlocks = PORTAL_VISUALS.applyInitial((ServerLevel) player.level(), portal);
|
||||
if (!PORTALS.create(name, portal)) {
|
||||
PORTAL_VISUALS.clear((ServerLevel) player.level(), portal);
|
||||
player.sendSystemMessage(Component.literal("§cPortal '" + name + "' sudah ada."));
|
||||
return 0;
|
||||
}
|
||||
|
||||
PORTALS.suppress(player.getUUID(), Math.max(CONFIG.get().portalCooldownTicks, 100));
|
||||
player.sendSystemMessage(Component.literal(
|
||||
"§aPortal §f" + name + "§a dibuat. §7" + portal.sourceDimension + " §8→ §f" + targetId));
|
||||
"§aPortal §f" + name + "§a dibuat. §7" + portal.sourceDimension + " §8→ §f" + targetId
|
||||
+ "\n§dVisual portal aktif §7(" + visualBlocks + " blok, axis=" + visualAxis + ")."
|
||||
+ "\n§7Frame boleh block apa saja; selection harus hanya area dalam portal."));
|
||||
LobbyLog.state(
|
||||
"PORTAL CREATED",
|
||||
"Name: " + name,
|
||||
"Source: " + portal.sourceDimension,
|
||||
"Target: " + targetId
|
||||
"Target: " + targetId,
|
||||
"Visual: vanilla nether_portal",
|
||||
"Axis: " + visualAxis,
|
||||
"Visual blocks: " + visualBlocks
|
||||
);
|
||||
return 1;
|
||||
} catch (Exception e) {
|
||||
LOGGER.warn("Failed to read WorldEdit selection for {}", player.getUUID(), e);
|
||||
player.sendSystemMessage(Component.literal("§cGagal membaca selection WorldEdit: " + rootMessage(e)));
|
||||
LOGGER.warn("Failed to create portal for {}", player.getUUID(), e);
|
||||
player.sendSystemMessage(Component.literal("§cGagal membuat portal: " + rootMessage(e)));
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
private static int deletePortal(CommandSourceStack source, String name) {
|
||||
String key = PortalManager.normalizeName(name);
|
||||
if (!PORTALS.delete(key)) {
|
||||
PortalDefinition existing = PORTALS.get(key);
|
||||
if (existing == null || !PORTALS.delete(key)) {
|
||||
source.sendFailure(Component.literal("§cPortal '" + key + "' tidak ditemukan."));
|
||||
return 0;
|
||||
}
|
||||
source.sendSuccess(() -> Component.literal("§aPortal §f" + key + "§a dihapus."), false);
|
||||
LobbyLog.state("PORTAL DELETED", "Name: " + key);
|
||||
|
||||
int cleared = 0;
|
||||
ServerLevel sourceLevel = findLevel(source.getServer(), existing.sourceDimension);
|
||||
if (sourceLevel != null) {
|
||||
cleared = PORTAL_VISUALS.clear(sourceLevel, existing);
|
||||
}
|
||||
|
||||
int finalCleared = cleared;
|
||||
source.sendSuccess(() -> Component.literal(
|
||||
"§aPortal §f" + key + "§a dihapus."
|
||||
+ (finalCleared > 0 ? " §7Visual dibersihkan: §f" + finalCleared + " blok." : "")), false);
|
||||
LobbyLog.state("PORTAL DELETED", "Name: " + key, "Visual blocks cleared: " + cleared);
|
||||
return 1;
|
||||
}
|
||||
|
||||
@@ -744,7 +788,8 @@ public final class LobbySystemsMod implements ModInitializer {
|
||||
source.sendSuccess(() -> Component.literal("§6Lobby Portals §7(" + PORTALS.size() + ")"), false);
|
||||
for (String name : PORTALS.names()) {
|
||||
PortalDefinition p = PORTALS.get(name);
|
||||
source.sendSuccess(() -> Component.literal("§e- §f" + name + " §7: " + p.sourceDimension + " §8→ §f" + p.targetDimension), false);
|
||||
source.sendSuccess(() -> Component.literal("§e- §f" + name + " §7: " + p.sourceDimension + " §8→ §f" + p.targetDimension
|
||||
+ (p.hasNetherPortalVisual() ? " §d[visual]" : " §8[legacy/no visual]")), false);
|
||||
}
|
||||
return PORTALS.size();
|
||||
}
|
||||
@@ -760,7 +805,8 @@ public final class LobbySystemsMod implements ModInitializer {
|
||||
"§6Portal §f" + name
|
||||
+ "\n§7Source: §f" + p.sourceDimension
|
||||
+ "\n§7Region: §f(" + p.min.x + ", " + p.min.y + ", " + p.min.z + ") §7→ §f(" + p.max.x + ", " + p.max.y + ", " + p.max.z + ")"
|
||||
+ "\n§7Target: §f" + p.targetDimension), false);
|
||||
+ "\n§7Target: §f" + p.targetDimension
|
||||
+ "\n§7Visual: §f" + (p.hasNetherPortalVisual() ? "nether_portal (axis=" + p.visualAxis + ")" : "none / legacy")), false);
|
||||
return 1;
|
||||
}
|
||||
|
||||
|
||||
@@ -6,13 +6,28 @@ public final class PortalDefinition {
|
||||
public PortalPoint max;
|
||||
public String targetDimension;
|
||||
|
||||
// Added in v1.5.0. Legacy portals without these fields stay visual-free.
|
||||
public String visualMode = "none";
|
||||
public String visualAxis = null;
|
||||
|
||||
public PortalDefinition() {}
|
||||
|
||||
public PortalDefinition(String sourceDimension, PortalPoint min, PortalPoint max, String targetDimension) {
|
||||
this(sourceDimension, min, max, targetDimension, "none", null);
|
||||
}
|
||||
|
||||
public PortalDefinition(String sourceDimension, PortalPoint min, PortalPoint max, String targetDimension,
|
||||
String visualMode, String visualAxis) {
|
||||
this.sourceDimension = sourceDimension;
|
||||
this.min = min;
|
||||
this.max = max;
|
||||
this.targetDimension = targetDimension;
|
||||
this.visualMode = visualMode;
|
||||
this.visualAxis = visualAxis;
|
||||
}
|
||||
|
||||
public boolean hasNetherPortalVisual() {
|
||||
return "nether_portal".equalsIgnoreCase(visualMode);
|
||||
}
|
||||
|
||||
public boolean contains(double x, double y, double z) {
|
||||
|
||||
@@ -88,6 +88,24 @@ public final class PortalManager {
|
||||
public void tick(MinecraftServer server, int intervalTicks, int cooldownTicks, PortalTrigger trigger) {
|
||||
tick++;
|
||||
cooldownUntil.entrySet().removeIf(e -> e.getValue() <= tick);
|
||||
|
||||
// v1.5.0 uses vanilla nether_portal blocks only as a visual layer.
|
||||
// Keep Minecraft's own Nether portal processor on cooldown while a player
|
||||
// is inside a managed visual region, so Lobby Systems remains the sole
|
||||
// owner of the destination (important for Creative players as well).
|
||||
for (ServerPlayer player : server.getPlayerList().getPlayers()) {
|
||||
String dimension = player.level().dimension().identifier().toString();
|
||||
List<Map.Entry<String, PortalDefinition>> candidates = byDimension.get(dimension);
|
||||
if (candidates == null) continue;
|
||||
for (Map.Entry<String, PortalDefinition> entry : candidates) {
|
||||
PortalDefinition portal = entry.getValue();
|
||||
if (portal.hasNetherPortalVisual() && portal.contains(player.getX(), player.getY(), player.getZ())) {
|
||||
player.setPortalCooldown(20);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (tick % Math.max(1, intervalTicks) != 0) return;
|
||||
|
||||
for (ServerPlayer player : server.getPlayerList().getPlayers()) {
|
||||
|
||||
@@ -0,0 +1,188 @@
|
||||
package id.dartokloning.lobbysystems;
|
||||
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.Direction;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.world.level.block.Block;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.block.NetherPortalBlock;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
|
||||
/**
|
||||
* Server-only visual layer for Lobby Systems portals.
|
||||
*
|
||||
* It deliberately uses Minecraft's existing nether_portal block state, so clients
|
||||
* need no Lobby Systems mod/resource pack. Teleport routing remains owned by
|
||||
* PortalManager; the vanilla block is only used as the animated purple surface.
|
||||
*/
|
||||
public final class PortalVisualManager {
|
||||
private static final int REPAIR_INTERVAL_TICKS = 20;
|
||||
private int repairTicker;
|
||||
|
||||
public String determineAxis(WorldEditSelectionBridge.Selection selection, ServerPlayer player) {
|
||||
int sizeX = Math.abs(selection.maxX() - selection.minX()) + 1;
|
||||
int sizeY = Math.abs(selection.maxY() - selection.minY()) + 1;
|
||||
int sizeZ = Math.abs(selection.maxZ() - selection.minZ()) + 1;
|
||||
|
||||
if (sizeY < 1) {
|
||||
throw new IllegalArgumentException("Selection portal tidak valid.");
|
||||
}
|
||||
|
||||
// A vertical portal surface must be one block thick on exactly one
|
||||
// horizontal axis. This supports both X-facing and Z-facing portals.
|
||||
if (sizeX == 1 && sizeZ > 1) return "z";
|
||||
if (sizeZ == 1 && sizeX > 1) return "x";
|
||||
|
||||
// A one-block-wide column has no geometric width axis. Pick the axis
|
||||
// from the player's facing so the vanilla portal texture still renders.
|
||||
if (sizeX == 1 && sizeZ == 1) {
|
||||
return player.getDirection().getAxis() == Direction.Axis.Z ? "x" : "z";
|
||||
}
|
||||
|
||||
throw new IllegalArgumentException(
|
||||
"Selection visual portal harus berupa bidang vertikal setebal 1 blok. "
|
||||
+ "Select hanya area DALAM portal, contoh 1x5x3 atau 3x5x1; bukan frame/volume penuh.");
|
||||
}
|
||||
|
||||
public int applyInitial(ServerLevel level, PortalDefinition portal) {
|
||||
if (!portal.hasNetherPortalVisual()) return 0;
|
||||
ensureReplaceable(level, portal);
|
||||
return apply(level, portal);
|
||||
}
|
||||
|
||||
public int apply(ServerLevel level, PortalDefinition portal) {
|
||||
if (!portal.hasNetherPortalVisual()) return 0;
|
||||
|
||||
Direction.Axis axis = parseAxis(portal.visualAxis);
|
||||
BlockState visual = Blocks.NETHER_PORTAL.defaultBlockState()
|
||||
.setValue(NetherPortalBlock.AXIS, axis);
|
||||
|
||||
int changed = 0;
|
||||
int minX = Math.min(portal.min.x, portal.max.x);
|
||||
int minY = Math.min(portal.min.y, portal.max.y);
|
||||
int minZ = Math.min(portal.min.z, portal.max.z);
|
||||
int maxX = Math.max(portal.min.x, portal.max.x);
|
||||
int maxY = Math.max(portal.min.y, portal.max.y);
|
||||
int maxZ = Math.max(portal.min.z, portal.max.z);
|
||||
|
||||
// UPDATE_KNOWN_SHAPE is intentional: visual portals are allowed to use
|
||||
// arbitrary decorative frames and must not require vanilla obsidian shape
|
||||
// validation when the block state is placed.
|
||||
int flags = Block.UPDATE_CLIENTS | Block.UPDATE_KNOWN_SHAPE;
|
||||
|
||||
for (int x = minX; x <= maxX; x++) {
|
||||
for (int y = minY; y <= maxY; y++) {
|
||||
for (int z = minZ; z <= maxZ; z++) {
|
||||
BlockPos pos = new BlockPos(x, y, z);
|
||||
BlockState current = level.getBlockState(pos);
|
||||
if (!current.equals(visual)) {
|
||||
level.setBlock(pos, visual, flags);
|
||||
changed++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return changed;
|
||||
}
|
||||
|
||||
public int clear(ServerLevel level, PortalDefinition portal) {
|
||||
if (!portal.hasNetherPortalVisual()) return 0;
|
||||
int changed = 0;
|
||||
int flags = Block.UPDATE_CLIENTS | Block.UPDATE_KNOWN_SHAPE;
|
||||
|
||||
int minX = Math.min(portal.min.x, portal.max.x);
|
||||
int minY = Math.min(portal.min.y, portal.max.y);
|
||||
int minZ = Math.min(portal.min.z, portal.max.z);
|
||||
int maxX = Math.max(portal.min.x, portal.max.x);
|
||||
int maxY = Math.max(portal.min.y, portal.max.y);
|
||||
int maxZ = Math.max(portal.min.z, portal.max.z);
|
||||
|
||||
for (int x = minX; x <= maxX; x++) {
|
||||
for (int y = minY; y <= maxY; y++) {
|
||||
for (int z = minZ; z <= maxZ; z++) {
|
||||
BlockPos pos = new BlockPos(x, y, z);
|
||||
if (level.getBlockState(pos).is(Blocks.NETHER_PORTAL)) {
|
||||
level.setBlock(pos, Blocks.AIR.defaultBlockState(), flags);
|
||||
changed++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return changed;
|
||||
}
|
||||
|
||||
public void tick(MinecraftServer server, PortalManager portals) {
|
||||
if (++repairTicker < REPAIR_INTERVAL_TICKS) return;
|
||||
repairTicker = 0;
|
||||
repairAll(server, portals, false);
|
||||
}
|
||||
|
||||
public int repairAll(MinecraftServer server, PortalManager portals, boolean log) {
|
||||
int changed = 0;
|
||||
for (String name : portals.names()) {
|
||||
PortalDefinition portal = portals.get(name);
|
||||
if (portal == null || !portal.hasNetherPortalVisual()) continue;
|
||||
ServerLevel level = findLevel(server, portal.sourceDimension);
|
||||
if (level == null) continue;
|
||||
int repaired = apply(level, portal);
|
||||
changed += repaired;
|
||||
if (log && repaired > 0) {
|
||||
LobbySystemsMod.LOGGER.info("Re-applied {} portal visual block(s) for '{}'.", repaired, name);
|
||||
}
|
||||
}
|
||||
return changed;
|
||||
}
|
||||
|
||||
public int applyToLevel(ServerLevel level, PortalManager portals) {
|
||||
int changed = 0;
|
||||
String dimension = level.dimension().identifier().toString();
|
||||
for (String name : portals.names()) {
|
||||
PortalDefinition portal = portals.get(name);
|
||||
if (portal != null && portal.hasNetherPortalVisual() && dimension.equals(portal.sourceDimension)) {
|
||||
changed += apply(level, portal);
|
||||
}
|
||||
}
|
||||
return changed;
|
||||
}
|
||||
|
||||
private static void ensureReplaceable(ServerLevel level, PortalDefinition portal) {
|
||||
int blocked = 0;
|
||||
int minX = Math.min(portal.min.x, portal.max.x);
|
||||
int minY = Math.min(portal.min.y, portal.max.y);
|
||||
int minZ = Math.min(portal.min.z, portal.max.z);
|
||||
int maxX = Math.max(portal.min.x, portal.max.x);
|
||||
int maxY = Math.max(portal.min.y, portal.max.y);
|
||||
int maxZ = Math.max(portal.min.z, portal.max.z);
|
||||
|
||||
for (int x = minX; x <= maxX; x++) {
|
||||
for (int y = minY; y <= maxY; y++) {
|
||||
for (int z = minZ; z <= maxZ; z++) {
|
||||
BlockState state = level.getBlockState(new BlockPos(x, y, z));
|
||||
if (!(state.isAir() || state.is(Blocks.FIRE) || state.is(Blocks.SOUL_FIRE)
|
||||
|| state.is(Blocks.NETHER_PORTAL))) {
|
||||
blocked++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (blocked > 0) {
|
||||
throw new IllegalArgumentException(
|
||||
"Selection menyentuh " + blocked + " blok non-kosong. "
|
||||
+ "Select hanya area DALAM portal; frame/dekorasi jangan ikut terseleksi.");
|
||||
}
|
||||
}
|
||||
|
||||
private static Direction.Axis parseAxis(String axis) {
|
||||
return "z".equalsIgnoreCase(axis) ? Direction.Axis.Z : Direction.Axis.X;
|
||||
}
|
||||
|
||||
private static ServerLevel findLevel(MinecraftServer server, String dimensionId) {
|
||||
for (ServerLevel level : server.getAllLevels()) {
|
||||
if (level.dimension().identifier().toString().equals(dimensionId)) return level;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user