diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/LocalSession.java b/worldedit-core/src/main/java/com/sk89q/worldedit/LocalSession.java index 0563cb0d91..8d3d532bc1 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/LocalSession.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/LocalSession.java @@ -49,6 +49,7 @@ import com.sk89q.worldedit.session.ClipboardHolder; import com.sk89q.worldedit.session.Placement; import com.sk89q.worldedit.session.PlacementType; +import com.sk89q.worldedit.session.SessionCUIState; import com.sk89q.worldedit.session.request.Request; import com.sk89q.worldedit.util.Countable; import com.sk89q.worldedit.util.SideEffectSet; @@ -84,17 +85,14 @@ */ public class LocalSession { - private static final int CUI_VERSION_UNINITIALIZED = -1; public static int MAX_HISTORY_SIZE = 15; // Non-session related fields private transient LocalConfiguration config; private final transient AtomicBoolean dirty = new AtomicBoolean(); - // Single-connection lifetime fields - private transient int failedCuiAttempts = 0; - private transient boolean hasCUISupport = false; - private transient int cuiVersion = CUI_VERSION_UNINITIALIZED; + // CUI state (extracted to SessionCUIState - move field) + private final transient SessionCUIState cuiState = new SessionCUIState(); // Session related private transient RegionSelector selector = new CuboidRegionSelector(); @@ -103,7 +101,7 @@ public class LocalSession { private transient int historyPointer = 0; private transient ClipboardHolder clipboard; private transient boolean superPickaxe = false; - private transient BlockTool pickaxeMode = new SinglePickaxe(); + private transient BlockTool superPickaxeTool = new SinglePickaxe(); private final transient Map tools = new HashMap<>(); private transient int maxBlocksChanged = -1; private transient int maxTimeoutTime; @@ -113,7 +111,6 @@ public class LocalSession { private transient SideEffectSet sideEffectSet = SideEffectSet.defaults(); private transient Mask mask; private transient ZoneId timezone = ZoneId.systemDefault(); - private transient BlockVector3 cuiTemporaryBlock; @SuppressWarnings("deprecation") private transient EditSession.ReorderMode reorderMode = EditSession.ReorderMode.FAST; private transient List> lastDistribution; @@ -428,13 +425,25 @@ public Region getSelection() throws IncompleteRegionException { * @throws IncompleteRegionException if no region is selected, or the provided world is null */ public Region getSelection(@Nullable World world) throws IncompleteRegionException { - if (world == null || selector.getIncompleteRegion().getWorld() == null - || !selector.getIncompleteRegion().getWorld().equals(world)) { + if (!isSelectionDefinedForWorld(world)) { throw new IncompleteRegionException(); } return selector.getRegion(); } + /** + * Returns whether the selection is fully defined for the given world. + * Decomposed conditional for clarity (code smell: complex conditional). + */ + private boolean isSelectionDefinedForWorld(@Nullable World world) { + if (world == null) { + return false; + } + World selectionWorld = selector.getIncompleteRegion().getWorld(); + boolean selectionMatchesWorld = selectionWorld != null && selectionWorld.equals(world); + return selectionMatchesWorld; + } + /** * Get the selection world. * @@ -699,7 +708,7 @@ public void setSnapshotExperimental(@Nullable SnapshotInfo snapshotExperimental) * @return the super pickaxe tool mode */ public BlockTool getSuperPickaxe() { - return pickaxeMode; + return superPickaxeTool; } /** @@ -709,7 +718,7 @@ public BlockTool getSuperPickaxe() { */ public void setSuperPickaxe(BlockTool tool) { checkNotNull(tool); - this.pickaxeMode = tool; + this.superPickaxeTool = tool; } /** @@ -899,10 +908,11 @@ public void updateServerCUI(Actor actor) { Player player = (Player) actor; - if (!useServerCUI || hasCUISupport) { - if (cuiTemporaryBlock != null) { - player.sendFakeBlock(cuiTemporaryBlock, null); - cuiTemporaryBlock = null; + if (!useServerCUI || cuiState.hasCUISupport()) { + BlockVector3 pos = cuiState.getServerCuiStructureBlockPosition(); + if (pos != null) { + player.sendFakeBlock(pos, null); + cuiState.setServerCuiStructureBlockPosition(null); } return; // If it's not enabled, ignore this. } @@ -912,22 +922,23 @@ public void updateServerCUI(Actor actor) { LinCompoundTag tags = Objects.requireNonNull( block.getNbt(), "createStructureBlock should return nbt" ); - BlockVector3 tempCuiTemporaryBlock = BlockVector3.at( + BlockVector3 newStructureBlockPosition = BlockVector3.at( tags.getTag("x", LinTagType.intTag()).valueAsInt(), tags.getTag("y", LinTagType.intTag()).valueAsInt(), tags.getTag("z", LinTagType.intTag()).valueAsInt() ); - // If it's null, we don't need to do anything. The old was already removed. - if (cuiTemporaryBlock != null && !tempCuiTemporaryBlock.equals(cuiTemporaryBlock)) { - // Update the existing block if it's the same location - player.sendFakeBlock(cuiTemporaryBlock, null); + BlockVector3 currentPos = cuiState.getServerCuiStructureBlockPosition(); + if (currentPos != null && !newStructureBlockPosition.equals(currentPos)) { + player.sendFakeBlock(currentPos, null); + } + cuiState.setServerCuiStructureBlockPosition(newStructureBlockPosition); + player.sendFakeBlock(newStructureBlockPosition, block); + } else { + BlockVector3 pos = cuiState.getServerCuiStructureBlockPosition(); + if (pos != null) { + player.sendFakeBlock(pos, null); + cuiState.setServerCuiStructureBlockPosition(null); } - cuiTemporaryBlock = tempCuiTemporaryBlock; - player.sendFakeBlock(cuiTemporaryBlock, block); - } else if (cuiTemporaryBlock != null) { - // Remove the old block - player.sendFakeBlock(cuiTemporaryBlock, null); - cuiTemporaryBlock = null; } } @@ -941,7 +952,7 @@ public void dispatchCUIEvent(Actor actor, CUIEvent event) { checkNotNull(actor); checkNotNull(event); - if (hasCUISupport) { + if (cuiState.hasCUISupport()) { actor.dispatchCUIEvent(event); } else if (useServerCUI) { updateServerCUI(actor); @@ -967,7 +978,7 @@ public void dispatchCUISetup(Actor actor) { public void dispatchCUISelection(Actor actor) { checkNotNull(actor); - if (!hasCUISupport) { + if (!cuiState.hasCUISupport()) { if (useServerCUI) { updateServerCUI(actor); } @@ -975,7 +986,7 @@ public void dispatchCUISelection(Actor actor) { } if (selector instanceof CUIRegion tempSel) { - if (tempSel.getProtocolVersion() > cuiVersion) { + if (tempSel.getProtocolVersion() > cuiState.getCUIVersion()) { actor.dispatchCUIEvent(new SelectionShapeEvent(tempSel.getLegacyTypeID())); tempSel.describeLegacyCUI(this, actor); } else { @@ -994,12 +1005,12 @@ public void dispatchCUISelection(Actor actor) { public void describeCUI(Actor actor) { checkNotNull(actor); - if (!hasCUISupport) { + if (!cuiState.hasCUISupport()) { return; } if (selector instanceof CUIRegion tempSel) { - if (tempSel.getProtocolVersion() > cuiVersion) { + if (tempSel.getProtocolVersion() > cuiState.getCUIVersion()) { tempSel.describeLegacyCUI(this, actor); } else { tempSel.describeCUI(this, actor); @@ -1019,18 +1030,18 @@ public void handleCUIInitializationMessage(String eventType, List args, checkNotNull(eventType); checkNotNull(args); - if (this.hasCUISupport) { + if (cuiState.hasCUISupport()) { // WECUI is a bit aggressive about re-initializing itself // the last attempt to touch handshakes didn't go well, so this will do... for now dispatchCUISelection(actor); return; - } else if (this.failedCuiAttempts > 3) { + } else if (cuiState.getFailedCuiAttempts() > 3) { return; } if (!args.isEmpty() && eventType.equalsIgnoreCase("v")) { // enough fields and right message if (args.size() > 1) { - this.failedCuiAttempts++; + cuiState.incrementFailedCuiAttempts(); return; } @@ -1039,11 +1050,11 @@ public void handleCUIInitializationMessage(String eventType, List args, version = Integer.parseInt(args.getFirst()); } catch (NumberFormatException e) { WorldEdit.logger.warn("Error while reading CUI init message"); - this.failedCuiAttempts++; + cuiState.incrementFailedCuiAttempts(); return; } - setCUISupport(true); - setCUIVersion(version); + cuiState.setCUISupport(true); + cuiState.setCUIVersion(version); dispatchCUISelection(actor); } } @@ -1070,7 +1081,7 @@ public void handleCUIInitializationMessage(String text, Actor actor) { * @return true if CUI is enabled */ public boolean hasCUISupport() { - return hasCUISupport; + return cuiState.hasCUISupport(); } /** @@ -1079,7 +1090,7 @@ public boolean hasCUISupport() { * @param support true if CUI is enabled */ public void setCUISupport(boolean support) { - hasCUISupport = support; + cuiState.setCUISupport(support); } /** @@ -1088,7 +1099,7 @@ public void setCUISupport(boolean support) { * @return the CUI version */ public int getCUIVersion() { - return cuiVersion; + return cuiState.getCUIVersion(); } /** @@ -1097,11 +1108,7 @@ public int getCUIVersion() { * @param cuiVersion the CUI version */ public void setCUIVersion(int cuiVersion) { - if (cuiVersion < 0) { - throw new IllegalArgumentException("CUI protocol version must be non-negative, but '" + cuiVersion + "' was received."); - } - - this.cuiVersion = cuiVersion; + cuiState.setCUIVersion(cuiVersion); } /** @@ -1314,8 +1321,6 @@ public void setLastDistribution(List> dist) { *

This is for internal use only.

*/ public void onIdle() { - this.cuiVersion = CUI_VERSION_UNINITIALIZED; - this.hasCUISupport = false; - this.failedCuiAttempts = 0; + cuiState.reset(); } } diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/session/SessionCUIState.java b/worldedit-core/src/main/java/com/sk89q/worldedit/session/SessionCUIState.java new file mode 100644 index 0000000000..f5e07f6e6e --- /dev/null +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/session/SessionCUIState.java @@ -0,0 +1,88 @@ +/* + * WorldEdit, a Minecraft world manipulation toolkit + * Copyright (C) sk89q + * Copyright (C) WorldEdit team and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.sk89q.worldedit.session; + +import com.sk89q.worldedit.math.BlockVector3; + +import javax.annotation.Nullable; + +/** + * Holds CUI (CUINetworking)-related state for a session. + * Extracted from LocalSession to separate concerns (code smell: large class with mixed responsibilities). + */ +public final class SessionCUIState { + + public static final int CUI_VERSION_UNINITIALIZED = -1; + + private int failedCuiAttempts = 0; + private boolean hasCUISupport = false; + private int cuiVersion = CUI_VERSION_UNINITIALIZED; + @Nullable + private BlockVector3 serverCuiStructureBlockPosition; + + public int getFailedCuiAttempts() { + return failedCuiAttempts; + } + + public void setFailedCuiAttempts(int failedCuiAttempts) { + this.failedCuiAttempts = failedCuiAttempts; + } + + public void incrementFailedCuiAttempts() { + this.failedCuiAttempts++; + } + + public boolean hasCUISupport() { + return hasCUISupport; + } + + public void setCUISupport(boolean support) { + this.hasCUISupport = support; + } + + public int getCUIVersion() { + return cuiVersion; + } + + public void setCUIVersion(int cuiVersion) { + if (cuiVersion < 0) { + throw new IllegalArgumentException("CUI protocol version must be non-negative, but '" + cuiVersion + "' was received."); + } + this.cuiVersion = cuiVersion; + } + + @Nullable + public BlockVector3 getServerCuiStructureBlockPosition() { + return serverCuiStructureBlockPosition; + } + + public void setServerCuiStructureBlockPosition(@Nullable BlockVector3 position) { + this.serverCuiStructureBlockPosition = position; + } + + /** + * Reset CUI state when the session becomes idle. + */ + public void reset() { + this.cuiVersion = CUI_VERSION_UNINITIALIZED; + this.hasCUISupport = false; + this.failedCuiAttempts = 0; + } +} diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/world/storage/ChunkFromTagLoaders.java b/worldedit-core/src/main/java/com/sk89q/worldedit/world/storage/ChunkFromTagLoaders.java new file mode 100644 index 0000000000..f1864a2390 --- /dev/null +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/world/storage/ChunkFromTagLoaders.java @@ -0,0 +1,169 @@ +/* + * WorldEdit, a Minecraft world manipulation toolkit + * Copyright (C) sk89q + * Copyright (C) WorldEdit team and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.sk89q.worldedit.world.storage; + +import com.sk89q.worldedit.internal.Constants; +import com.sk89q.worldedit.world.DataException; +import com.sk89q.worldedit.world.chunk.AnvilChunk; +import com.sk89q.worldedit.world.chunk.AnvilChunk13; +import com.sk89q.worldedit.world.chunk.AnvilChunk16; +import com.sk89q.worldedit.world.chunk.AnvilChunk18; +import com.sk89q.worldedit.world.chunk.Chunk; +import com.sk89q.worldedit.world.chunk.OldChunk; +import org.enginehub.linbus.tree.LinCompoundTag; +import org.enginehub.linbus.tree.LinTagType; + +import java.util.List; + +/** + * Registry of chunk format loaders. Order matters: first supporting loader is used. + */ +public final class ChunkFromTagLoaders { + + /** + * Strategy for creating a {@link Chunk} from NBT data. + * Used to replace version-based conditionals with polymorphism. + */ + public interface ChunkFromTagLoader { + + /** + * Whether this loader supports the given data version and tag structure. + * + * @param dataVersion the chunk data version + * @param rootTag the root NBT tag + * @return true if this loader can create a Chunk from the tag + */ + boolean supports(int dataVersion, LinCompoundTag rootTag); + + /** + * Create a Chunk from the given root tag. + * + * @param rootTag the root NBT tag (may be the full chunk or contain a Level tag) + * @return the chunk + * @throws DataException if the tag is not valid for this format + */ + Chunk load(LinCompoundTag rootTag) throws DataException; + } + + private static final List LOADERS = List.of( + new AnvilChunk18Loader(), + new AnvilChunk16Loader(), + new AnvilChunk13Loader(), + new AnvilChunkLoader(), + new OldChunkLoader() + ); + + /** + * Find the first loader that supports the given data version and tag, then load the chunk. + * + * @param dataVersion the chunk data version + * @param rootTag the root NBT tag + * @return the chunk + * @throws DataException if no loader supports the tag or loading fails + */ + public static Chunk loadChunk(int dataVersion, LinCompoundTag rootTag) throws DataException { + for (ChunkFromTagLoader loader : LOADERS) { + if (loader.supports(dataVersion, rootTag)) { + return loader.load(rootTag); + } + } + throw new ChunkStoreException("Unsupported chunk format: dataVersion=" + dataVersion); + } + + private ChunkFromTagLoaders() { + } + + private static final class AnvilChunk18Loader implements ChunkFromTagLoader { + @Override + public boolean supports(int dataVersion, LinCompoundTag rootTag) { + return dataVersion >= Constants.DATA_VERSION_MC_1_18; + } + + @Override + public Chunk load(LinCompoundTag rootTag) throws DataException { + return new AnvilChunk18(rootTag); + } + } + + private static final class AnvilChunk16Loader implements ChunkFromTagLoader { + @Override + public boolean supports(int dataVersion, LinCompoundTag rootTag) { + return dataVersion >= Constants.DATA_VERSION_MC_1_16; + } + + @Override + public Chunk load(LinCompoundTag rootTag) throws DataException { + LinCompoundTag levelTag = rootTag.findTag("Level", LinTagType.compoundTag()); + if (levelTag == null) { + throw new ChunkStoreException("Missing root 'Level' tag"); + } + return new AnvilChunk16(levelTag); + } + } + + private static final class AnvilChunk13Loader implements ChunkFromTagLoader { + @Override + public boolean supports(int dataVersion, LinCompoundTag rootTag) { + return dataVersion >= Constants.DATA_VERSION_MC_1_13; + } + + @Override + public Chunk load(LinCompoundTag rootTag) throws DataException { + LinCompoundTag levelTag = rootTag.findTag("Level", LinTagType.compoundTag()); + if (levelTag == null) { + throw new ChunkStoreException("Missing root 'Level' tag"); + } + return new AnvilChunk13(levelTag); + } + } + + private static final class AnvilChunkLoader implements ChunkFromTagLoader { + @Override + public boolean supports(int dataVersion, LinCompoundTag rootTag) { + LinCompoundTag levelTag = rootTag.findTag("Level", LinTagType.compoundTag()); + return levelTag != null && levelTag.value().containsKey("Sections"); + } + + @Override + public Chunk load(LinCompoundTag rootTag) throws DataException { + LinCompoundTag levelTag = rootTag.findTag("Level", LinTagType.compoundTag()); + if (levelTag == null) { + throw new ChunkStoreException("Missing root 'Level' tag"); + } + return new AnvilChunk(levelTag); + } + } + + private static final class OldChunkLoader implements ChunkFromTagLoader { + @Override + public boolean supports(int dataVersion, LinCompoundTag rootTag) { + return rootTag.findTag("Level", LinTagType.compoundTag()) != null; + } + + @Override + public Chunk load(LinCompoundTag rootTag) throws DataException { + LinCompoundTag levelTag = rootTag.findTag("Level", LinTagType.compoundTag()); + if (levelTag == null) { + throw new ChunkStoreException("Missing root 'Level' tag"); + } + return new OldChunk(levelTag); + } + } +} diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/world/storage/ChunkStoreHelper.java b/worldedit-core/src/main/java/com/sk89q/worldedit/world/storage/ChunkStoreHelper.java index da39f36533..bbe933c98c 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/world/storage/ChunkStoreHelper.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/world/storage/ChunkStoreHelper.java @@ -26,15 +26,9 @@ import com.sk89q.worldedit.WorldEdit; import com.sk89q.worldedit.extension.platform.Capability; import com.sk89q.worldedit.extension.platform.Platform; -import com.sk89q.worldedit.internal.Constants; import com.sk89q.worldedit.world.DataException; import com.sk89q.worldedit.world.DataFixer; -import com.sk89q.worldedit.world.chunk.AnvilChunk; -import com.sk89q.worldedit.world.chunk.AnvilChunk13; -import com.sk89q.worldedit.world.chunk.AnvilChunk16; -import com.sk89q.worldedit.world.chunk.AnvilChunk18; import com.sk89q.worldedit.world.chunk.Chunk; -import com.sk89q.worldedit.world.chunk.OldChunk; import org.enginehub.linbus.tree.LinCompoundTag; import org.enginehub.linbus.tree.LinNumberTag; import org.enginehub.linbus.tree.LinTagType; @@ -99,40 +93,40 @@ public static Chunk getChunk(CompoundTag rootTag) throws DataException { * @throws DataException if the rootTag is not valid chunk data */ public static Chunk getChunk(LinCompoundTag rootTag) throws DataException { - int dataVersion = rootTag.value().get("DataVersion") instanceof LinNumberTag t - ? t.value().intValue() : -1; - - final Platform platform = WorldEdit.getInstance().getPlatformManager().queryCapability(Capability.WORLD_EDITING); - final int currentDataVersion = platform.getDataVersion(); - if ((dataVersion > 0 || hasLevelSections(rootTag)) && dataVersion < currentDataVersion) { // only fix up MCA format, DFU doesn't support MCR chunks - final DataFixer dataFixer = platform.getDataFixer(); - if (dataFixer != null) { - rootTag = dataFixer.fixUp(DataFixer.FixTypes.CHUNK, rootTag, dataVersion); - dataVersion = currentDataVersion; - } - } + int dataVersion = extractDataVersion(rootTag); + DataFixResult fixResult = applyDataFixerIfNeeded(rootTag, dataVersion); + return ChunkFromTagLoaders.loadChunk(fixResult.effectiveDataVersion(), fixResult.rootTag()); + } - if (dataVersion >= Constants.DATA_VERSION_MC_1_18) { - return new AnvilChunk18(rootTag); - } + /** + * Extracts the DataVersion from the chunk root tag, or -1 if missing/invalid. + */ + private static int extractDataVersion(LinCompoundTag rootTag) { + return rootTag.value().get("DataVersion") instanceof LinNumberTag numberTag + ? numberTag.value().intValue() : -1; + } - LinCompoundTag tag = rootTag.findTag("Level", LinTagType.compoundTag()); - if (tag == null) { - throw new ChunkStoreException("Missing root 'Level' tag"); - } + private record DataFixResult(LinCompoundTag rootTag, int effectiveDataVersion) {} - if (dataVersion >= Constants.DATA_VERSION_MC_1_16) { - return new AnvilChunk16(tag); - } - if (dataVersion >= Constants.DATA_VERSION_MC_1_13) { - return new AnvilChunk13(tag); + /** + * Applies the platform data fixer when the chunk is in MCA format and behind current version. + * Only fixes MCA format; DFU doesn't support MCR chunks. + */ + private static DataFixResult applyDataFixerIfNeeded(LinCompoundTag rootTag, int dataVersion) { + final Platform platform = WorldEdit.getInstance().getPlatformManager().queryCapability(Capability.WORLD_EDITING); + final int currentDataVersion = platform.getDataVersion(); + boolean isMcAFormat = dataVersion > 0 || hasLevelSections(rootTag); + boolean isBehindCurrentVersion = dataVersion < currentDataVersion; + boolean shouldApplyFix = isMcAFormat && isBehindCurrentVersion; + if (!shouldApplyFix) { + return new DataFixResult(rootTag, dataVersion); } - - if (tag.value().containsKey("Sections")) { - return new AnvilChunk(tag); + final DataFixer dataFixer = platform.getDataFixer(); + if (dataFixer == null) { + return new DataFixResult(rootTag, dataVersion); } - - return new OldChunk(tag); + LinCompoundTag fixedTag = dataFixer.fixUp(DataFixer.FixTypes.CHUNK, rootTag, dataVersion); + return new DataFixResult(fixedTag, currentDataVersion); } private static boolean hasLevelSections(LinCompoundTag rootTag) { diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/world/storage/LegacyChunkStore.java b/worldedit-core/src/main/java/com/sk89q/worldedit/world/storage/LegacyChunkStore.java index e65b2108b1..37f903c0cc 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/world/storage/LegacyChunkStore.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/world/storage/LegacyChunkStore.java @@ -40,22 +40,38 @@ public abstract class LegacyChunkStore extends ChunkStore { /** - * Get the filename of a chunk. - * - * @param position chunk position - * @param separator folder separator character - * @return pathname + * Path components for a chunk file (folder1, folder2, filename). + * Pulled up so both getFilename and getChunkData use the same computation (code smell: duplicated logic). */ - public static String getFilename(BlockVector2 position, String separator) { + public static ChunkPathComponents getChunkPathComponents(BlockVector2 position) { int x = position.x(); int z = position.z(); - String folder1 = Integer.toString(divisorMod(x, 64), 36); String folder2 = Integer.toString(divisorMod(z, 64), 36); String filename = "c." + Integer.toString(x, 36) + "." + Integer.toString(z, 36) + ".dat"; + return new ChunkPathComponents(folder1, folder2, filename); + } - return folder1 + separator + folder2 + separator + filename; + /** + * Path components for a legacy chunk file. + * + * @param folder1 first folder segment + * @param folder2 second folder segment + * @param filename chunk filename + */ + public record ChunkPathComponents(String folder1, String folder2, String filename) {} + + /** + * Get the filename of a chunk. + * + * @param position chunk position + * @param separator folder separator character + * @return pathname + */ + public static String getFilename(BlockVector2 position, String separator) { + ChunkPathComponents path = getChunkPathComponents(position); + return path.folder1() + separator + path.folder2() + separator + path.filename(); } /** @@ -71,15 +87,8 @@ public static String getFilename(BlockVector2 position) { @Override public LinCompoundTag getChunkData(BlockVector2 position, World world) throws DataException, IOException { - int x = position.x(); - int z = position.z(); - - String folder1 = Integer.toString(divisorMod(x, 64), 36); - String folder2 = Integer.toString(divisorMod(z, 64), 36); - String filename = "c." + Integer.toString(x, 36) - + "." + Integer.toString(z, 36) + ".dat"; - - try (var chunkStream = new DataInputStream(new GZIPInputStream(getInputStream(folder1, folder2, filename)))) { + ChunkPathComponents path = getChunkPathComponents(position); + try (var chunkStream = new DataInputStream(new GZIPInputStream(getInputStream(path.folder1(), path.folder2(), path.filename())))) { return LinBinaryIO.readUsing(chunkStream, LinRootEntry::readFrom).toLinTag(); } }