diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldInfoPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldInfoPage.java index d638f445ad..dd58359b82 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldInfoPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldInfoPage.java @@ -17,7 +17,6 @@ */ package org.jackhuang.hmcl.ui.versions; -import com.github.steveice10.opennbt.tag.builtin.*; import com.jfoenix.controls.JFXButton; import com.jfoenix.controls.JFXTextField; import javafx.beans.property.SimpleBooleanProperty; @@ -46,6 +45,8 @@ import org.jackhuang.hmcl.util.StringUtils; import org.jackhuang.hmcl.util.i18n.I18n; import org.jackhuang.hmcl.util.io.FileUtils; +import tech.minediamond.micanbt.path.NBTFinder; +import tech.minediamond.micanbt.tag.*; import java.io.IOException; import java.nio.file.Files; @@ -67,7 +68,8 @@ public final class WorldInfoPage extends SpinnerPane implements WorldManagePage. private final WorldManagePage worldManagePage; private boolean isReadOnly; private final World world; - private CompoundTag levelDat; + private CompoundTag levelData; + private CompoundTag playerData; ImageView iconImageView = new ImageView(); @@ -77,15 +79,9 @@ public WorldInfoPage(WorldManagePage worldManagePage) { refresh(); } - private CompoundTag loadWorldInfo() throws IOException { - if (!Files.isDirectory(world.getFile())) - throw new IOException("Not a valid world directory"); - - return world.getLevelData(); - } - private void updateControls() { - CompoundTag dataTag = levelDat.get("Data"); + CompoundTag dataTag = (CompoundTag) levelData.get("Data"); + CompoundTag playerTag = playerData; ScrollPane scrollPane = new ScrollPane(); scrollPane.setFitToHeight(true); @@ -108,7 +104,7 @@ private void updateControls() { setRightTextField(worldNamePane, worldNameField, 200); if (dataTag.get("LevelName") instanceof StringTag worldNameTag) { - var worldName = new SimpleStringProperty(worldNameTag.getValue()); + var worldName = new SimpleStringProperty(worldNameTag.getClonedValue()); FXUtils.bindString(worldNameField, worldName); worldNameField.getProperties().put(WorldInfoPage.class.getName() + ".worldNameProperty", worldName); worldName.addListener((observable, oldValue, newValue) -> { @@ -211,17 +207,19 @@ private void updateControls() { worldSpawnPoint.setTitle(i18n("world.info.spawn")); String value; - if (dataTag.get("spawn") instanceof CompoundTag spawnTag && spawnTag.get("pos") instanceof IntArrayTag posTag) { - value = Dimension.of(spawnTag.get("dimension") instanceof StringTag dimensionTag - ? dimensionTag - : new StringTag("SpawnDimension", "minecraft:overworld")) - .formatPosition(posTag); - } else if (dataTag.get("SpawnX") instanceof IntTag intX + // Valid after 1.21.9-pre1 + if (dataTag.at("spawn.pos") instanceof IntArrayTag posTag) { + value = dataTag.at("spawn.dimension") instanceof StringTag dimensionTag + ? Dimension.of(dimensionTag).formatPosition(posTag) + : Dimension.OVERWORLD.formatPosition(posTag); + } + // Valid before 1.21.9-pre1 + else if (dataTag.get("SpawnX") instanceof IntTag intX && dataTag.get("SpawnY") instanceof IntTag intY && dataTag.get("SpawnZ") instanceof IntTag intZ) { - value = Dimension.OVERWORLD.formatPosition(intX.getValue(), intY.getValue(), intZ.getValue()); + value = Dimension.OVERWORLD.formatPosition(intX.getClonedValue(), intY.getClonedValue(), intZ.getClonedValue()); } else { - value = null; + value = ""; } worldSpawnPoint.setText(value); @@ -237,7 +235,7 @@ private void updateControls() { { timePane.setTitle(i18n("world.info.time")); if (dataTag.get("Time") instanceof LongTag timeTag) { - Duration duration = Duration.ofSeconds(timeTag.getValue() / 20); + Duration duration = Duration.ofSeconds(timeTag.getClonedValue() / 20); timePane.setText(i18n("world.info.time.format", duration.toDays(), duration.toHoursPart(), duration.toMinutesPart())); } } @@ -255,11 +253,15 @@ private void updateControls() { generateFeaturesButton.setTitle(i18n("world.info.generate_features")); generateFeaturesButton.setDisable(isReadOnly); - // generate_features was valid after 20w20a and MapFeatures was before that - if (dataTag.get("WorldGenSettings") instanceof CompoundTag worldGenSettings) { - bindTagAndToggleButton(worldGenSettings.get("generate_features"), generateFeaturesButton); + // Valid before (1.16)20w20a + if (dataTag.at("MapFeatures") instanceof ByteTag generateFeaturesTag) { + bindTagAndToggleButton(generateFeaturesTag, generateFeaturesButton); + } + // Valid after (1.16)20w20a + else if (NBTFinder.findFirst(world.getUnifiedWorldGenSettingsData(), "generate_features", "generate_structures") instanceof ByteTag generateStructures) { + bindTagAndToggleButton(generateStructures, generateFeaturesButton); } else { - bindTagAndToggleButton(dataTag.get("MapFeatures"), generateFeaturesButton); + generateFeaturesButton.setDisable(true); } } @@ -269,19 +271,28 @@ private void updateControls() { difficultyButton.setDisable(worldManagePage.isReadOnly()); difficultyButton.setItems(Difficulty.items); - if (dataTag.get("Difficulty") instanceof ByteTag difficultyTag) { - Difficulty difficulty = Difficulty.of(difficultyTag.getValue()); - if (difficulty != null) { - difficultyButton.setValue(difficulty); - difficultyButton.valueProperty().addListener((o, oldValue, newValue) -> { - if (newValue != null) { - difficultyTag.setValue((byte) newValue.ordinal()); - saveLevelDat(); - } - }); - } else { - difficultyButton.setDisable(true); - } + Difficulty difficulty; + // Valid before 26.1-snapshot-6 + if (dataTag.get("Difficulty") instanceof ByteTag difficultyTag + && (difficulty = Difficulty.of(difficultyTag.getClonedValue())) != null) { + difficultyButton.setValue(difficulty); + difficultyButton.valueProperty().addListener((o, oldValue, newValue) -> { + if (newValue != null) { + difficultyTag.setValue((byte) newValue.ordinal()); + saveWorldData(); + } + }); + } + // Valid after 26.1-snapshot-6 + else if (dataTag.at("difficulty_settings.difficulty") instanceof StringTag difficultyTag + && (difficulty = Difficulty.of(difficultyTag.getClonedValue())) != null) { + difficultyButton.setValue(difficulty); + difficultyButton.valueProperty().addListener((o, oldValue, newValue) -> { + if (newValue != null) { + difficultyTag.setValue(newValue.getTagStringValue()); + saveWorldData(); + } + }); } else { difficultyButton.setDisable(true); } @@ -291,8 +302,12 @@ private void updateControls() { { difficultyLockPane.setTitle(i18n("world.info.difficulty_lock")); difficultyLockPane.setDisable(isReadOnly); - - bindTagAndToggleButton(dataTag.get("DifficultyLocked"), difficultyLockPane); + // Valid before 26.1-snapshot-6 / Valid after 26.1-snapshot-6 + if (dataTag.atAny("DifficultyLocked", "difficulty_settings.locked") instanceof ByteTag difficultyLockedTag) { + bindTagAndToggleButton(difficultyLockedTag, difficultyLockPane); + } else { + difficultyLockPane.setDisable(true); + } } worldInfo.getContent().setAll( @@ -302,14 +317,14 @@ private void updateControls() { rootPane.getChildren().addAll(ComponentList.createComponentListTitle(i18n("world.info")), worldInfo); } - if (dataTag.get("Player") instanceof CompoundTag playerTag) { + if (playerTag != null) { ComponentList playerInfo = new ComponentList(); var locationPane = new LineTextPane(); { locationPane.setTitle(i18n("world.info.player.location")); Dimension dimension = Dimension.of(playerTag.get("Dimension")); - if (dimension != null && playerTag.get("Pos") instanceof ListTag posTag) { + if (dimension != null && playerTag.get("Pos") instanceof ListTag posTag) { locationPane.setText(dimension.formatPosition(posTag)); } } @@ -318,9 +333,11 @@ private void updateControls() { { lastDeathLocationPane.setTitle(i18n("world.info.player.last_death_location")); // Valid after 22w14a; prior to this version, the game did not record the last death location data. - if (playerTag.get("LastDeathLocation") instanceof CompoundTag LastDeathLocationTag) { - Dimension dimension = Dimension.of(LastDeathLocationTag.get("dimension")); - if (dimension != null && LastDeathLocationTag.get("pos") instanceof IntArrayTag posTag) { + if (playerTag.at("LastDeathLocation.dimension") instanceof StringTag dimensionTag + && playerTag.at("LastDeathLocation.pos") instanceof IntArrayTag posTag + && posTag.size() == 3) { + Dimension dimension = Dimension.of(dimensionTag); + if (dimension != null) { lastDeathLocationPane.setText(dimension.formatPosition(posTag)); } } @@ -329,17 +346,22 @@ private void updateControls() { var spawnPane = new LineTextPane(); { spawnPane.setTitle(i18n("world.info.player.spawn")); - if (playerTag.get("respawn") instanceof CompoundTag respawnTag - && respawnTag.get("dimension") instanceof StringTag dimensionTag - && respawnTag.get("pos") instanceof IntArrayTag intArrayTag - && intArrayTag.length() >= 3) { // Valid after 25w07a - spawnPane.setText(Dimension.of(dimensionTag).formatPosition(intArrayTag)); - } else if (playerTag.get("SpawnX") instanceof IntTag intX + // Valid after 1.21.5(25w07a) + if (playerTag.at("respawn.dimension") instanceof StringTag dimensionTag + && playerTag.at("respawn.pos") instanceof IntArrayTag intArrayTag + && intArrayTag.size() == 3) { + Dimension dimension = Dimension.of(dimensionTag); + if (dimension != null) { + spawnPane.setText(dimension.formatPosition(intArrayTag)); + } + } + // Valid before 1.21.5(25w07a) + else if (playerTag.get("SpawnX") instanceof IntTag intX && playerTag.get("SpawnY") instanceof IntTag intY - && playerTag.get("SpawnZ") instanceof IntTag intZ) { // Valid before 25w07a - // SpawnDimension tag is valid after 20w12a. Prior to this version, the game did not record the respawn point dimension and respawned in the Overworld. + && playerTag.get("SpawnZ") instanceof IntTag intZ) { + // SpawnDimension tag is valid after 1.16(20w12a). Prior to this version, the game did not record the respawn point dimension and respawned in the Overworld. spawnPane.setText((playerTag.get("SpawnDimension") instanceof StringTag dimensionTag ? Dimension.of(dimensionTag) : Dimension.OVERWORLD) - .formatPosition(intX.getValue(), intY.getValue(), intZ.getValue())); + .formatPosition(intX.getClonedValue(), intY.getClonedValue(), intZ.getClonedValue())); } } @@ -349,10 +371,13 @@ private void updateControls() { playerGameTypePane.setDisable(worldManagePage.isReadOnly()); playerGameTypePane.setItems(GameType.items); + // Valid before 26.1-snapshot-6 / Valid after 26.1-snapshot-6 + Tag hardcore = dataTag.atAny("hardcore", "difficulty_settings.hardcore"); + if (playerTag.get("playerGameType") instanceof IntTag playerGameTypeTag - && dataTag.get("hardcore") instanceof ByteTag hardcoreTag) { - boolean isHardcore = hardcoreTag.getValue() == 1; - GameType gameType = GameType.of(playerGameTypeTag.getValue(), isHardcore); + && hardcore instanceof ByteTag hardcoreTag) { + boolean isHardcore = hardcoreTag.getClonedValue() == 1; + GameType gameType = GameType.of(playerGameTypeTag.getClonedValue(), isHardcore); if (gameType != null) { playerGameTypePane.setValue(gameType); playerGameTypePane.valueProperty().addListener((o, oldValue, newValue) -> { @@ -364,7 +389,7 @@ private void updateControls() { playerGameTypeTag.setValue(newValue.ordinal()); hardcoreTag.setValue((byte) 0); } - saveLevelDat(); + saveWorldData(); } }); } else { @@ -428,16 +453,16 @@ private void setRightTextField(LinePane linePane, JFXTextField textField, int pe private void bindTagAndToggleButton(Tag tag, LineToggleButton toggleButton) { if (tag instanceof ByteTag byteTag) { - byte value = byteTag.getValue(); + byte value = byteTag.getClonedValue(); if (value == 0 || value == 1) { toggleButton.setSelected(value == 1); toggleButton.selectedProperty().addListener((o, oldValue, newValue) -> { try { byteTag.setValue((byte) (newValue ? 1 : 0)); - saveLevelDat(); + saveWorldData(); } catch (Exception e) { toggleButton.setSelected(oldValue); - LOG.warning("Exception happened when saving level.dat", e); + LOG.warning("Exception happened when saving world info", e); } }); } else { @@ -449,7 +474,7 @@ private void bindTagAndToggleButton(Tag tag, LineToggleButton toggleButton) { } private void bindTagAndTextField(IntTag intTag, JFXTextField jfxTextField) { - jfxTextField.setText(intTag.getValue().toString()); + jfxTextField.setText(intTag.getClonedValue().toString()); jfxTextField.textProperty().addListener((o, oldValue, newValue) -> { if (newValue != null) { @@ -457,11 +482,11 @@ private void bindTagAndTextField(IntTag intTag, JFXTextField jfxTextField) { Integer integer = Lang.toIntOrNull(newValue); if (integer != null) { intTag.setValue(integer); - saveLevelDat(); + saveWorldData(); } } catch (Exception e) { jfxTextField.setText(oldValue); - LOG.warning("Exception happened when saving level.dat", e); + LOG.warning("Exception happened when saving world info", e); } } }); @@ -470,7 +495,7 @@ private void bindTagAndTextField(IntTag intTag, JFXTextField jfxTextField) { } private void bindTagAndTextField(FloatTag floatTag, JFXTextField jfxTextField) { - jfxTextField.setText(new DecimalFormat("0.#").format(floatTag.getValue())); + jfxTextField.setText(new DecimalFormat("0.#").format(floatTag.getClonedValue())); jfxTextField.textProperty().addListener((o, oldValue, newValue) -> { if (newValue != null) { @@ -478,11 +503,11 @@ private void bindTagAndTextField(FloatTag floatTag, JFXTextField jfxTextField) { Float floatValue = Lang.toFloatOrNull(newValue); if (floatValue != null) { floatTag.setValue(floatValue); - saveLevelDat(); + saveWorldData(); } } catch (Exception e) { jfxTextField.setText(oldValue); - LOG.warning("Exception happened when saving level.dat", e); + LOG.warning("Exception happened when saving world info", e); } } }); @@ -490,12 +515,12 @@ private void bindTagAndTextField(FloatTag floatTag, JFXTextField jfxTextField) { jfxTextField.setValidators(new DoubleValidator(i18n("input.number"), true)); } - private void saveLevelDat() { - LOG.info("Saving level.dat of world " + world.getWorldName()); + private void saveWorldData() { + LOG.info("Saving world info of world " + world.getWorldName()); try { - this.world.writeLevelDat(levelDat); + this.world.writeWorldData(); } catch (IOException e) { - LOG.warning("Failed to save level.dat of world " + world.getWorldName(), e); + LOG.warning("Failed to save world info of world " + world.getWorldName(), e); } } @@ -503,17 +528,13 @@ private void saveLevelDat() { public void refresh() { this.isReadOnly = worldManagePage.isReadOnly(); this.setLoading(true); - Task.supplyAsync(this::loadWorldInfo) - .whenComplete(Schedulers.javafx(), ((result, exception) -> { - if (exception == null) { - this.levelDat = result; - updateControls(); - setLoading(false); - } else { - LOG.warning("Failed to load level.dat", exception); - setFailedReason(i18n("world.info.failed")); - } - })).start(); + Task.runAsync(Schedulers.javafx(), () -> { + this.levelData = world.getLevelData(); + this.playerData = world.getPlayerData(); + + updateControls(); + setLoading(false); + }).start(); } private record Dimension(String name) { @@ -523,14 +544,14 @@ private record Dimension(String name) { static Dimension of(Tag tag) { if (tag instanceof IntTag intTag) { - return switch (intTag.getValue()) { + return switch (intTag.getClonedValue()) { case 0 -> OVERWORLD; case -1 -> THE_NETHER; case 1 -> THE_END; default -> null; }; } else if (tag instanceof StringTag stringTag) { - String id = stringTag.getValue(); + String id = stringTag.getClonedValue(); return switch (id) { case "overworld", "minecraft:overworld" -> OVERWORLD; case "the_nether", "minecraft:the_nether" -> THE_NETHER; @@ -543,7 +564,7 @@ static Dimension of(Tag tag) { } String formatPosition(Tag tag) { - if (tag instanceof ListTag listTag && listTag.size() == 3) { + if (tag instanceof ListTag listTag && listTag.size() == 3) { Tag x = listTag.get(0); Tag y = listTag.get(1); @@ -551,8 +572,8 @@ String formatPosition(Tag tag) { if (x instanceof DoubleTag && y instanceof DoubleTag && z instanceof DoubleTag) { return this == OVERWORLD - ? String.format("(%.2f, %.2f, %.2f)", x.getValue(), y.getValue(), z.getValue()) - : String.format("%s (%.2f, %.2f, %.2f)", name, x.getValue(), y.getValue(), z.getValue()); + ? String.format("(%.2f, %.2f, %.2f)", x.getClonedValue(), y.getClonedValue(), z.getClonedValue()) + : String.format("%s (%.2f, %.2f, %.2f)", name, x.getClonedValue(), y.getClonedValue(), z.getClonedValue()); } return null; @@ -594,6 +615,19 @@ static Difficulty of(int d) { return (d >= 0 && d < items.size()) ? items.get(d) : null; } + static Difficulty of(String name) { + for (Difficulty item : items) { + if (item.name().toLowerCase(Locale.ROOT).equals(name)) { + return item; + } + } + return null; + } + + String getTagStringValue() { + return this.name().toLowerCase(Locale.ROOT); + } + @Override public String toString() { return i18n("world.info.difficulty." + name().toLowerCase(Locale.ROOT)); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldManagePage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldManagePage.java index e46d7c6b86..ae650921fa 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldManagePage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldManagePage.java @@ -78,7 +78,7 @@ public WorldManagePage(World world, Profile profile, String instanceId) { updateSessionLockChannel(); try { - this.world.reloadLevelDat(); + this.world.reloadWorldData(); } catch (IOException e) { LOG.warning("Can not load world level.dat of world: " + this.world.getFile(), e); this.addEventHandler(Navigator.NavigationEvent.NAVIGATED, event -> closePageForLoadingFail()); @@ -106,7 +106,7 @@ public WorldManagePage(World world, Profile profile, String instanceId) { public void refresh() { updateSessionLockChannel(); try { - world.reloadLevelDat(); + world.reloadWorldData(); } catch (IOException e) { LOG.warning("Can not load world level.dat of world: " + world.getFile(), e); closePageForLoadingFail(); diff --git a/HMCL/src/main/resources/assets/about/deps.json b/HMCL/src/main/resources/assets/about/deps.json index c121c5360f..c9ee0cbcaf 100644 --- a/HMCL/src/main/resources/assets/about/deps.json +++ b/HMCL/src/main/resources/assets/about/deps.json @@ -49,6 +49,11 @@ "subtitle" : "Copyright © 2013-2021 Steveice10.\nLicensed under the MIT License.", "externalLink" : "https://github.com/GeyserMC/OpenNBT" }, + { + "title" : "MicaNBT", + "subtitle" : "Copyright © 2013-2021 Steveice10, 2026 Mine-diamond.\nLicensed under the MIT License.", + "externalLink" : "https://github.com/Mine-diamond/MicaNBT" + }, { "title" : "minecraft-jfx-skin", "subtitle" : "Copyright © 2016 InfinityStudio.\nLicensed under the GPL 3.", diff --git a/HMCLCore/build.gradle.kts b/HMCLCore/build.gradle.kts index 86ca2bde92..6961ba37fc 100644 --- a/HMCLCore/build.gradle.kts +++ b/HMCLCore/build.gradle.kts @@ -26,6 +26,7 @@ dependencies { api(libs.chardet) api(libs.jna) api(libs.pci.ids) + api(libs.micanbt) compileOnlyApi(libs.jetbrains.annotations) diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/World.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/World.java index 8e3f6fc38c..c1fc85f557 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/World.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/World.java @@ -17,16 +17,17 @@ */ package org.jackhuang.hmcl.game; -import com.github.steveice10.opennbt.NBTIO; -import com.github.steveice10.opennbt.tag.builtin.*; import javafx.scene.image.Image; import org.jackhuang.hmcl.util.io.*; import org.jackhuang.hmcl.util.versioning.GameVersionNumber; import org.jetbrains.annotations.Nullable; +import tech.minediamond.micanbt.NBT.NBT; +import tech.minediamond.micanbt.NBT.NBTCompressType; +import tech.minediamond.micanbt.path.NBTFinder; +import tech.minediamond.micanbt.tag.*; import java.io.IOException; import java.io.InputStream; -import java.io.OutputStream; import java.nio.ByteBuffer; import java.nio.channels.FileChannel; import java.nio.channels.FileLock; @@ -34,9 +35,8 @@ import java.nio.charset.StandardCharsets; import java.nio.file.*; import java.util.List; +import java.util.UUID; import java.util.stream.Stream; -import java.util.zip.GZIPInputStream; -import java.util.zip.GZIPOutputStream; import static org.jackhuang.hmcl.util.logging.Logger.LOG; @@ -44,9 +44,15 @@ public final class World { private final Path file; private String fileName; - private CompoundTag levelData; private Image icon; + + private CompoundTag levelData; private Path levelDataPath; + private CompoundTag worldGenSettingsData; + private CompoundTag unifiedWorldGenSettingsData; + private Path worldGenSettingsDataPath; + private CompoundTag playerData; + private Path playerDataPath; public World(Path file) throws IOException { this.file = file; @@ -68,22 +74,16 @@ public String getFileName() { } public String getWorldName() { - CompoundTag data = levelData.get("Data"); - StringTag levelNameTag = data.get("LevelName"); - return levelNameTag.getValue(); + return ((StringTag) levelData.at("Data.LevelName")).getClonedValue(); } public void setWorldName(String worldName) throws IOException { - if (levelData.get("Data") instanceof CompoundTag data && data.get("LevelName") instanceof StringTag levelNameTag) { + if (levelData.at("Data.LevelName") instanceof StringTag levelNameTag) { levelNameTag.setValue(worldName); - writeLevelDat(levelData); + writeLevelData(); } } - public Path getLevelDatFile() { - return file.resolve("level.dat"); - } - public Path getSessionLockFile() { return file.resolve("session.lock"); } @@ -92,49 +92,52 @@ public CompoundTag getLevelData() { return levelData; } + public @Nullable CompoundTag getUnifiedWorldGenSettingsData() { + return unifiedWorldGenSettingsData; + } + + public @Nullable CompoundTag getPlayerData() { + return playerData; + } + public long getLastPlayed() { - CompoundTag data = levelData.get("Data"); - LongTag lastPlayedTag = data.get("LastPlayed"); - return lastPlayedTag.getValue(); + return ((LongTag) levelData.at("Data.LastPlayed")).getClonedValue(); } public @Nullable GameVersionNumber getGameVersion() { - if (levelData.get("Data") instanceof CompoundTag data && - data.get("Version") instanceof CompoundTag versionTag && - versionTag.get("Name") instanceof StringTag nameTag) { - return GameVersionNumber.asGameVersion(nameTag.getValue()); + if (levelData.at("Data.Version.Name") instanceof StringTag nameTag) { + return GameVersionNumber.asGameVersion(nameTag.getClonedValue()); } return null; } public @Nullable Long getSeed() { - CompoundTag data = levelData.get("Data"); - if (data.get("WorldGenSettings") instanceof CompoundTag worldGenSettingsTag && worldGenSettingsTag.get("seed") instanceof LongTag seedTag) { //Valid after 1.16 - return seedTag.getValue(); - } else if (data.get("RandomSeed") instanceof LongTag seedTag) { //Valid before 1.16 - return seedTag.getValue(); + // Valid after 1.16(20w20a) + if (NBTFinder.get(unifiedWorldGenSettingsData, "seed") instanceof LongTag seedTag) { + return seedTag.getClonedValue(); + } + // Valid before 1.16(20w20a) + else if (levelData.at("Data.RandomSeed") instanceof LongTag seedTag) { + return seedTag.getClonedValue(); } return null; } public boolean isLargeBiomes() { - CompoundTag data = levelData.get("Data"); - if (data.get("generatorName") instanceof StringTag generatorNameTag) { //Valid before 1.16 - return "largeBiomes".equals(generatorNameTag.getValue()); - } else { - if (data.get("WorldGenSettings") instanceof CompoundTag worldGenSettingsTag - && worldGenSettingsTag.get("dimensions") instanceof CompoundTag dimensionsTag - && dimensionsTag.get("minecraft:overworld") instanceof CompoundTag overworldTag - && overworldTag.get("generator") instanceof CompoundTag generatorTag) { - if (generatorTag.get("biome_source") instanceof CompoundTag biomeSourceTag - && biomeSourceTag.get("large_biomes") instanceof ByteTag largeBiomesTag) { //Valid between 1.16 and 1.16.2 - return largeBiomesTag.getValue() == (byte) 1; - } else if (generatorTag.get("settings") instanceof StringTag settingsTag) { //Valid after 1.16.2 - return "minecraft:large_biomes".equals(settingsTag.getValue()); - } - } - return false; + // Valid before 1.16(20w20a) + if (levelData.at("Data.generatorName") instanceof StringTag generatorNameTag) { + return "largeBiomes".equals(generatorNameTag.getClonedValue()); + } + // Valid between 1.16(20w20a) and 1.18(21w37a) + else if (NBTFinder.get(unifiedWorldGenSettingsData, "dimensions.minecraft:overworld.generator.biome_source.large_biomes") instanceof ByteTag largeBiomesTag) { + return largeBiomesTag.getClonedValue() == (byte) 1; } + // Valid after 1.18(21w37a) + // Note: In versions 1.16(20w20a) and 1.18(21w37a), the settings tag exists but does not indicate large biomes information + else if (NBTFinder.get(unifiedWorldGenSettingsData, "dimensions.minecraft:overworld.generator.settings") instanceof StringTag settingsTag) { + return "minecraft:large_biomes".equals(settingsTag.getClonedValue()); + } + return false; } public Image getIcon() { @@ -166,8 +169,8 @@ private void loadFromDirectory() throws IOException { if (!Files.exists(levelDat)) { throw new IOException("Not a valid world directory since level.dat or special_level.dat cannot be found."); } - loadAndCheckLevelDat(levelDat); this.levelDataPath = levelDat; + loadAndCheckWorldData(); Path iconFile = file.resolve("icon.png"); if (Files.isRegularFile(iconFile)) { @@ -189,7 +192,7 @@ private void loadFromZipImpl(Path root) throws IOException { if (!Files.exists(levelDat)) { throw new IOException("Not a valid world zip file since level.dat or special_level.dat cannot be found."); } - loadAndCheckLevelDat(levelDat); + loadAndCheckLevelData(levelDat); Path iconFile = root.resolve("icon.png"); if (Files.isRegularFile(iconFile)) { @@ -221,9 +224,14 @@ private void loadFromZip() throws IOException { } } - private void loadAndCheckLevelDat(Path levelDat) throws IOException { - this.levelData = parseLevelDat(levelDat); - CompoundTag data = levelData.get("Data"); + private void loadAndCheckWorldData() throws IOException { + loadAndCheckLevelData(levelDataPath); + loadOtherData(); + } + + private void loadAndCheckLevelData(Path levelDat) throws IOException { + this.levelData = NBT.read(levelDat); + CompoundTag data = (CompoundTag) levelData.get("Data"); if (data == null) throw new IOException("level.dat missing Data"); @@ -234,12 +242,47 @@ private void loadAndCheckLevelDat(Path levelDat) throws IOException { throw new IOException("level.dat missing LastPlayed"); } - public void reloadLevelDat() throws IOException { - if (levelDataPath != null) { - loadAndCheckLevelDat(this.levelDataPath); + private void loadOtherData() throws IOException { + Path worldGenSettingsDatPath = file.resolve("data/minecraft/world_gen_settings.dat"); + if (levelData.at("Data.WorldGenSettings") instanceof CompoundTag worldGenSettingsTag) { + this.worldGenSettingsDataPath = null; + this.worldGenSettingsData = worldGenSettingsTag; + this.unifiedWorldGenSettingsData = worldGenSettingsData; + } else if (Files.exists(worldGenSettingsDatPath)) { + this.worldGenSettingsDataPath = worldGenSettingsDatPath; + this.worldGenSettingsData = NBT.read(worldGenSettingsDatPath); + this.unifiedWorldGenSettingsData = (CompoundTag) worldGenSettingsData.get("data"); + } else { + this.worldGenSettingsDataPath = null; + this.worldGenSettingsData = null; + this.unifiedWorldGenSettingsData = null; + } + + if (levelData.at("Data.Player") instanceof CompoundTag playerTag) { + this.playerData = playerTag; + this.playerDataPath = null; + } else if (levelData.at("Data.singleplayer_uuid") instanceof IntArrayTag uuidTag) { + int[] uuidValue = uuidTag.getClonedValue(); + if (uuidValue.length == 4) { + long mostSigBits = ((long) uuidValue[0] << 32) | (uuidValue[1] & 0xFFFFFFFFL); + long leastSigBits = ((long) uuidValue[2] << 32) | (uuidValue[3] & 0xFFFFFFFFL); + String playerUUID = new UUID(mostSigBits, leastSigBits).toString(); + Path playerDatPath = file.resolve("players/data/" + playerUUID + ".dat"); + if (Files.exists(playerDatPath)) { + this.playerData = NBT.read(playerDatPath); + this.playerDataPath = playerDatPath; + } + } + } else { + this.playerData = null; + this.playerDataPath = null; } } + public void reloadWorldData() throws IOException { + loadAndCheckWorldData(); + } + // The rename method is used to rename temporary world object during installation and copying, // so there is no need to modify the `file` field. public void rename(String newName) throws IOException { @@ -247,9 +290,9 @@ public void rename(String newName) throws IOException { throw new IOException("Not a valid world directory"); // Change the name recorded in level.dat - CompoundTag data = levelData.get("Data"); + CompoundTag data = (CompoundTag) levelData.get("Data"); data.put(new StringTag("LevelName", newName)); - writeLevelDat(levelData); + writeLevelData(); // then change the folder's name Files.move(file, file.resolveSibling(newName)); @@ -345,25 +388,30 @@ public FileChannel lock() throws WorldLockedException { } } - public void writeLevelDat(CompoundTag nbt) throws IOException { + public void writeWorldData() throws IOException { if (!Files.isDirectory(file)) throw new IOException("Not a valid world directory"); - FileUtils.saveSafely(getLevelDatFile(), os -> { - try (OutputStream gos = new GZIPOutputStream(os)) { - NBTIO.writeTag(gos, nbt); - } - }); - } + writeLevelData(); - private static CompoundTag parseLevelDat(Path path) throws IOException { - try (InputStream is = new GZIPInputStream(Files.newInputStream(path))) { - Tag nbt = NBTIO.readTag(is); - if (nbt instanceof CompoundTag compoundTag) - return compoundTag; - else - throw new IOException("level.dat malformed"); + if (worldGenSettingsDataPath != null) { + writeTag(worldGenSettingsData, worldGenSettingsDataPath); } + + if (playerDataPath != null) { + writeTag(playerData, playerDataPath); + } + } + + public void writeLevelData() throws IOException { + writeTag(levelData, levelDataPath); + } + + public void writeTag(CompoundTag nbt, Path path) throws IOException { + if (!Files.isDirectory(file)) + throw new IOException("Not a valid world directory"); + + FileUtils.saveSafely(path, os -> NBT.toStream(nbt, os).compressType(NBTCompressType.GZIP).write()); } private static boolean isLocked(Path sessionLockFile) { diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 2897211f61..d3909117c8 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -9,6 +9,7 @@ xz = "1.11" fx-gson = "5.0.0" constant-pool-scanner = "1.2" opennbt = "1.5" +micanbt = "0.5.0" nanohttpd = "2.3.1" jsoup = "1.21.2" chardet = "2.5.0" @@ -39,6 +40,7 @@ xz = { module = "org.tukaani:xz", version.ref = "xz" } fx-gson = { module = "org.hildan.fxgson:fx-gson", version.ref = "fx-gson" } constant-pool-scanner = { module = "org.jenkins-ci:constant-pool-scanner", version.ref = "constant-pool-scanner" } opennbt = { module = "com.github.steveice10:opennbt", version.ref = "opennbt" } +micanbt = {module = "tech.minediamond:micanbt", version.ref = "micanbt"} nanohttpd = { module = "org.nanohttpd:nanohttpd", version.ref = "nanohttpd" } jsoup = { module = "org.jsoup:jsoup", version.ref = "jsoup" } chardet = { module = "org.glavo:chardet", version.ref = "chardet" }