diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b4f8e5da..d460ce6a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -9,7 +9,7 @@ jobs: - uses: actions/checkout@v2 - uses: actions/setup-java@v1 with: - java-version: '17' + java-version: '21' java-package: jdk+fx - run: | mvn package -DskipTests @@ -19,7 +19,7 @@ jobs: - uses: actions/checkout@v2 - uses: actions/setup-java@v1 with: - java-version: '17' + java-version: '21' java-package: jdk+fx - run: | mvn test diff --git a/README.md b/README.md index 94b2ffdc..16e2722e 100644 --- a/README.md +++ b/README.md @@ -27,8 +27,8 @@ If you run into any problems, check the [FAQ](https://github.com/mircokroon/mine ### Requirements -- Java 17 or higher -- Minecraft version 1.12.2+ // 1.13.2+ // 1.14.1+ // 1.15.2+ // 1.16.2+ // 1.17+ // 1.18+ // 1.19.3+ // 1.20+ +- Java 21 or higher +- Minecraft version 1.12.2+ // 1.13.2+ // 1.14.1+ // 1.15.2+ // 1.16.2+ // 1.17+ // 1.18+ // 1.19.3+ // 1.20+ // 1.21+ ### Command-line [Download](https://github.com/mircokroon/minecraft-world-downloader/releases/latest/download/world-downloader.jar) the cross-platform `world-downloader.jar` and run it using the command-line: diff --git a/pom.xml b/pom.xml index 7147d7ce..860dae32 100644 --- a/pom.xml +++ b/pom.xml @@ -9,8 +9,8 @@ 1.0.0.0 jar - 17 - 17 + 21 + 21 UTF-8 @@ -43,7 +43,7 @@ org.apache.maven.plugins maven-shade-plugin - 3.3.0 + 3.5.3 true diff --git a/src/main/java/config/Version.java b/src/main/java/config/Version.java index fcea1f89..17081c7d 100644 --- a/src/main/java/config/Version.java +++ b/src/main/java/config/Version.java @@ -13,7 +13,8 @@ public enum Version { V1_19_3(761, 3218), V1_20(763, 3463), V1_20_2(764, 3578), - V1_20_3(765, 3698), + V1_20_4(765, 3698), + V1_20_6(766, 3839), ANY(0, 0); public final int dataVersion; diff --git a/src/main/java/game/data/LevelData.java b/src/main/java/game/data/LevelData.java index 8d3c27f1..24480821 100644 --- a/src/main/java/game/data/LevelData.java +++ b/src/main/java/game/data/LevelData.java @@ -3,7 +3,6 @@ import static util.ExceptionHandling.attempt; import config.Config; -import config.Option; import config.Version; import game.data.coordinates.Coordinate3D; import game.data.coordinates.CoordinateDouble3D; @@ -224,7 +223,7 @@ private void disableWorldGeneration(CompoundTag data) { new NamedTag("biome", new StringTag("minecraft:the_void")) ))); - CompoundTag dimensions = new CompoundTag(worldManager.getDimensionCodec().getDimensions().stream().map(dimension -> { + CompoundTag dimensions = new CompoundTag(worldManager.getDimensionRegistry().getDimensions().stream().map(dimension -> { CompoundTag dim = new CompoundTag(); dim.add("type", new StringTag(dimension.getType())); dim.add("generator", generator); diff --git a/src/main/java/game/data/WorldManager.java b/src/main/java/game/data/WorldManager.java index 6fb0d9cf..da7e0ac4 100644 --- a/src/main/java/game/data/WorldManager.java +++ b/src/main/java/game/data/WorldManager.java @@ -2,6 +2,8 @@ import static util.ExceptionHandling.attempt; +import game.data.chunk.version.Chunk_1_17; +import game.data.dimension.DimensionType; import gui.ChunkImageState; import java.io.File; import java.io.IOException; @@ -41,7 +43,7 @@ import game.data.coordinates.CoordinateDim2D; import game.data.coordinates.CoordinateDouble3D; import game.data.dimension.Dimension; -import game.data.dimension.DimensionCodec; +import game.data.dimension.DimensionRegistry; import game.data.entity.EntityNames; import game.data.entity.EntityRegistry; import game.data.maps.MapRegistry; @@ -53,7 +55,6 @@ import packets.DataTypeProvider; import packets.builder.PacketBuilder; import proxy.PacketInjector; -import se.llbit.nbt.CompoundTag; import se.llbit.nbt.Tag; import util.PathUtils; @@ -81,7 +82,7 @@ public class WorldManager { private ContainerManager containerManager; private CommandBlockManager commandBlockManager; private VillagerManager villagerManager; - private DimensionCodec dimensionCodec; + private DimensionRegistry dimensionCodec; private final RenderDistanceExtender renderDistanceExtender; private BiConsumer playerPosListener; @@ -89,6 +90,7 @@ public class WorldManager { private boolean isBelowGround = false; private double playerRotation = 0; private Dimension dimension; + private DimensionType dimensionType; private final EntityRegistry entityRegistry; private final ChunkFactory chunkFactory; @@ -160,6 +162,11 @@ public void setDimension(Dimension dimension) { GuiManager.setDimension(this.dimension); } + public void setDimensionType(DimensionType dimensionType) { + this.dimensionType = dimensionType; + Chunk_1_17.setWorldHeight(dimensionType.getDimensionMinHeight(), dimensionType.getDimensionMaxHeight()); + } + private void saveAndUnloadChunks() { if (saveService == null) { return; @@ -381,20 +388,23 @@ public void touchChunk(ChunkEntities c) { regions.get(c.getLocation().chunkToDimRegion()).touch(); } - public DimensionCodec getDimensionCodec() { + public DimensionRegistry getDimensionRegistry() { + if (dimensionCodec == null) { + this.dimensionCodec = DimensionRegistry.empty(); + } return dimensionCodec; } /** - * Set the dimension codec, used to store information about the dimensions that this server supports. + * Set the dimension registry, used to store information about the dimensions that this server supports. */ - public void setDimensionCodec(DimensionCodec codec) { - dimensionCodec = codec; + public void setDimensionRegistry(DimensionRegistry registry) { + dimensionCodec = registry; // We can immediately try to write the dimension data to the proper directory. try { Path p = PathUtils.toPath(Config.getWorldOutputDir(), "datapacks", "downloaded", "data"); - if (codec.write(p)) { + if (registry.write(p)) { // we need to copy that pack.mcmeta file from so that Minecraft will recognise the datapack Path packMeta = PathUtils.toPath(p.getParent().toString(), "pack.mcmeta"); diff --git a/src/main/java/game/data/chunk/ChunkSection.java b/src/main/java/game/data/chunk/ChunkSection.java index 4ddbb1de..882cfb83 100644 --- a/src/main/java/game/data/chunk/ChunkSection.java +++ b/src/main/java/game/data/chunk/ChunkSection.java @@ -1,6 +1,6 @@ package game.data.chunk; -import game.data.chunk.palette.GlobalPalette; +import game.data.chunk.palette.BlockRegistry; import java.util.Arrays; import org.apache.commons.lang3.mutable.MutableBoolean; @@ -92,7 +92,7 @@ public byte getY() { } public int computeHeight(int x, int z, MutableBoolean foundAir) { - GlobalPalette globalPalette = GlobalPaletteProvider.getGlobalPalette(getDataVersion()); + BlockRegistry globalPalette = GlobalPaletteProvider.getGlobalPalette(getDataVersion()); for (int y = 15; y >= 0 ; y--) { int blockStateId = getNumericBlockStateAt(x, y, z); diff --git a/src/main/java/game/data/chunk/palette/GlobalPalette.java b/src/main/java/game/data/chunk/palette/BlockRegistry.java similarity index 87% rename from src/main/java/game/data/chunk/palette/GlobalPalette.java rename to src/main/java/game/data/chunk/palette/BlockRegistry.java index 2e9f5bfd..9bacc43b 100644 --- a/src/main/java/game/data/chunk/palette/GlobalPalette.java +++ b/src/main/java/game/data/chunk/palette/BlockRegistry.java @@ -1,162 +1,161 @@ -package game.data.chunk.palette; - -import com.google.gson.Gson; -import com.google.gson.JsonPrimitive; -import org.apache.commons.lang3.NotImplementedException; -import se.llbit.nbt.*; - -import java.io.InputStream; -import java.io.InputStreamReader; -import java.util.*; - -/** - * Holds a global palette as introduced in 1.13. These are read from a simple JSON file that is generated by the - * Minecraft server.jar. More details are in the readme file in the resource folder. - */ -public class GlobalPalette implements StateProvider { - private static final HashMap EMPTY_MAP = new HashMap<>(); - private final Map states; - private final Map nameStates; - private String version; - - /** - * Instantiate a global palette using the given Minecraft version. - * @param version the Minecraft version (e.g. 1.12.2), NOT protocol version - */ - public GlobalPalette(String version) { - this(GlobalPalette.class.getClassLoader().getResourceAsStream("blocks-" + version + ".json")); - } - - /** - * Instantiate a global palette using the input stream (to a JSON file). - */ - public GlobalPalette(InputStream input) { - this.states = new HashMap<>(); - this.nameStates = new HashMap<>(); - - // if the file doesn't exist, there is no palette for this version. - if (input == null) { return; } - - JsonResult map = new Gson().fromJson(new InputStreamReader(input), JsonResult.class); - map.forEach((name, type) -> type.states.forEach(state -> { - if (state.properties == null) { - state.properties = EMPTY_MAP; - } - - CompoundTag properties = state.getProperties(); - - addBlockState(new BlockState(name, state.id, properties)); - })); - } - - public int getRequiredBits() { - return (int) Math.ceil(Math.log(states.size()) / Math.log(2)); - } - - /** - * Get a block state from a given index. Used to convert packet palettes to the global palette. - */ - @Override - public BlockState getState(int key) { - return states.getOrDefault(key, null); - } - - /** - * Returns the first state in the palette, used to replace unknown states with air. - */ - @Override - public BlockState getDefaultState() { - return states.values().iterator().next(); - } - - @Override - public BlockState getState(SpecificTag nbt) { - return nameStates.get(new BlockStateIdentifier(nbt)); - } - - @Override - public int getStateId(SpecificTag nbt) { - BlockState state = getState(nbt); - if (state == null) { - return 0; - } - return state.getNumericId(); - } - - public void addBlockState(BlockState state) { - // skip existing states, these should be the same but might have different names - if (states.containsKey(state.getNumericId())) { - return; - } - - states.put(state.getNumericId(), state); - nameStates.put(new BlockStateIdentifier(state.getName(), state.getProperties()), state); - } -} - -class BlockStateIdentifier { - String name; - CompoundTag properties; - - public BlockStateIdentifier(SpecificTag t) { - this(t.get("Name").stringValue(), t.get("Properties").asCompound()); - } - - public BlockStateIdentifier(String name, CompoundTag properties) { - this.name = name; - this.properties = properties; - - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - - BlockStateIdentifier that = (BlockStateIdentifier) o; - - if (!Objects.equals(name, that.name)) return false; - return Objects.equals(properties, that.properties); - } - - @Override - public int hashCode() { - int result = name != null ? name.hashCode() : 0; - result = 31 * result + (properties != null ? properties.hashCode() : 0); - return result; - } -} - -// we need a class to represent this type because of type erasure, otherwise Gson will get angry over casting. -class JsonResult extends HashMap { } - -// additional classes for inside the JsonResult -class JsonBlockType { ArrayList states; } -class JsonBlockState { - int id; - HashMap properties; - - /** - * Turns properties hashmap into a compoundtag, which lets us correctly handle the block states that depend on - * properties. - */ - CompoundTag getProperties() { - CompoundTag res = new CompoundTag(); - for (Map.Entry entry : properties.entrySet()) { - JsonPrimitive wrappedVal = entry.getValue(); - SpecificTag value; - - if (wrappedVal.isBoolean()) { - value = new ByteTag(wrappedVal.getAsBoolean() ? 1 : 0); - } else if (wrappedVal.isNumber()) { - value = new IntTag(wrappedVal.getAsInt()); - } else { - value = new StringTag(wrappedVal.getAsString()); - } - - res.add(entry.getKey(), value); - - } - return res; - } -} +package game.data.chunk.palette; + +import com.google.gson.Gson; +import com.google.gson.JsonPrimitive; +import se.llbit.nbt.*; + +import java.io.InputStream; +import java.io.InputStreamReader; +import java.util.*; + +/** + * Holds a block registry as introduced in 1.13. These are read from a simple JSON file that is generated by the + * Minecraft server.jar. More details are in the readme file in the resource folder. + */ +public class BlockRegistry implements Registry { + private static final HashMap EMPTY_MAP = new HashMap<>(); + private final Map states; + private final Map nameStates; + private String version; + + /** + * Instantiate a block registry using the given Minecraft version. + * @param version the Minecraft version (e.g. 1.12.2), NOT protocol version + */ + public BlockRegistry(String version) { + this(BlockRegistry.class.getClassLoader().getResourceAsStream("blocks-" + version + ".json")); + } + + /** + * Instantiate a block registry using the input stream (to a JSON file). + */ + public BlockRegistry(InputStream input) { + this.states = new HashMap<>(); + this.nameStates = new HashMap<>(); + + // if the file doesn't exist, there is no palette for this version. + if (input == null) { return; } + + JsonResult map = new Gson().fromJson(new InputStreamReader(input), JsonResult.class); + map.forEach((name, type) -> type.states.forEach(state -> { + if (state.properties == null) { + state.properties = EMPTY_MAP; + } + + CompoundTag properties = state.getProperties(); + + addBlockState(new BlockState(name, state.id, properties)); + })); + } + + public int getRequiredBits() { + return (int) Math.ceil(Math.log(states.size()) / Math.log(2)); + } + + /** + * Get a block state from a given index. Used to convert packet palettes to the block registry. + */ + @Override + public BlockState getState(int key) { + return states.getOrDefault(key, null); + } + + /** + * Returns the first state in the palette, used to replace unknown states with air. + */ + @Override + public BlockState getDefaultState() { + return states.values().iterator().next(); + } + + @Override + public BlockState getState(SpecificTag nbt) { + return nameStates.get(new BlockStateIdentifier(nbt)); + } + + @Override + public int getStateId(SpecificTag nbt) { + BlockState state = getState(nbt); + if (state == null) { + return 0; + } + return state.getNumericId(); + } + + public void addBlockState(BlockState state) { + // skip existing states, these should be the same but might have different names + if (states.containsKey(state.getNumericId())) { + return; + } + + states.put(state.getNumericId(), state); + nameStates.put(new BlockStateIdentifier(state.getName(), state.getProperties()), state); + } +} + +class BlockStateIdentifier { + String name; + CompoundTag properties; + + public BlockStateIdentifier(SpecificTag t) { + this(t.get("Name").stringValue(), t.get("Properties").asCompound()); + } + + public BlockStateIdentifier(String name, CompoundTag properties) { + this.name = name; + this.properties = properties; + + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + BlockStateIdentifier that = (BlockStateIdentifier) o; + + if (!Objects.equals(name, that.name)) return false; + return Objects.equals(properties, that.properties); + } + + @Override + public int hashCode() { + int result = name != null ? name.hashCode() : 0; + result = 31 * result + (properties != null ? properties.hashCode() : 0); + return result; + } +} + +// we need a class to represent this type because of type erasure, otherwise Gson will get angry over casting. +class JsonResult extends HashMap { } + +// additional classes for inside the JsonResult +class JsonBlockType { ArrayList states; } +class JsonBlockState { + int id; + HashMap properties; + + /** + * Turns properties hashmap into a compoundtag, which lets us correctly handle the block states that depend on + * properties. + */ + CompoundTag getProperties() { + CompoundTag res = new CompoundTag(); + for (Map.Entry entry : properties.entrySet()) { + JsonPrimitive wrappedVal = entry.getValue(); + SpecificTag value; + + if (wrappedVal.isBoolean()) { + value = new ByteTag(wrappedVal.getAsBoolean() ? 1 : 0); + } else if (wrappedVal.isNumber()) { + value = new IntTag(wrappedVal.getAsInt()); + } else { + value = new StringTag(wrappedVal.getAsString()); + } + + res.add(entry.getKey(), value); + + } + return res; + } +} diff --git a/src/main/java/game/data/chunk/palette/BlockState.java b/src/main/java/game/data/chunk/palette/BlockState.java index 33001828..7d3b2ed0 100644 --- a/src/main/java/game/data/chunk/palette/BlockState.java +++ b/src/main/java/game/data/chunk/palette/BlockState.java @@ -11,7 +11,7 @@ import java.util.Map; /** - * A block state in the global palette (1.13+). + * A block state in the block registry (1.13+). */ public class BlockState implements State { private static final Map transparency; @@ -48,7 +48,7 @@ public String getProperty(String name) { return properties.get(name).stringValue(); } - CompoundTag getProperties() { + public CompoundTag getProperties() { return properties; } @@ -118,7 +118,7 @@ public String toString() { return "BlockState{" + "name='" + name + '\'' + ", id=" + id + -// ", properties=" + properties + + ", properties=" + properties.toString().replaceAll("[\\s\\n\\r\\t]", " ") + '}'; } } diff --git a/src/main/java/game/data/chunk/palette/GlobalPaletteProvider.java b/src/main/java/game/data/chunk/palette/GlobalPaletteProvider.java index 35c08c18..e043f02b 100644 --- a/src/main/java/game/data/chunk/palette/GlobalPaletteProvider.java +++ b/src/main/java/game/data/chunk/palette/GlobalPaletteProvider.java @@ -11,21 +11,21 @@ import se.llbit.nbt.CompoundTag; /** - * This class manages the global palettes. It can hold not only a palette for the current game version, but also for + * This class manages the block registries. It can hold not only a palette for the current game version, but also for * different versions when these are needed to load previously saved chunks. */ public final class GlobalPaletteProvider { private GlobalPaletteProvider() { } - private static HashMap palettes = new HashMap<>(); + private static HashMap palettes = new HashMap<>(); private static Queue uninitialised; /** - * Retrieves a global palette based on the data version number. If the palette is not already known, it will be + * Retrieves a block registry based on the data version number. If the palette is not already known, it will be * created through requestPalette. */ - public static GlobalPalette getGlobalPalette(int dataVersion) { - GlobalPalette palette = palettes.get(dataVersion); + public static BlockRegistry getGlobalPalette(int dataVersion) { + BlockRegistry palette = palettes.get(dataVersion); if (palette == null) { return requestPalette(dataVersion); @@ -36,7 +36,7 @@ public static GlobalPalette getGlobalPalette(int dataVersion) { /** * If no data version is specified, the current game version is used instead. */ - public static GlobalPalette getGlobalPalette() { + public static BlockRegistry getGlobalPalette() { return getGlobalPalette(Config.getDataVersion()); } @@ -45,10 +45,10 @@ public static GlobalPalette getGlobalPalette() { * version handler has this value for us. The registry loader will load it either from a previously generated * report, or it will download the relevant Minecraft version and generate it. */ - private static GlobalPalette requestPalette(int dataVersion) { + private static BlockRegistry requestPalette(int dataVersion) { Protocol version = ProtocolVersionHandler.getInstance().getProtocolByDataVersion(dataVersion); try { - GlobalPalette p = RegistryLoader.forVersion(version.getVersion()).generateGlobalPalette(); + BlockRegistry p = RegistryLoader.forVersion(version.getVersion()).generateGlobalPalette(); palettes.put(dataVersion, p); if (uninitialised != null) { @@ -64,7 +64,7 @@ private static GlobalPalette requestPalette(int dataVersion) { } public static void registerBlock(String name, int id) { - GlobalPalette palette = palettes.get(Config.getDataVersion()); + BlockRegistry palette = palettes.get(Config.getDataVersion()); BlockState state = new BlockState(name, id, new CompoundTag()); diff --git a/src/main/java/game/data/chunk/palette/Palette.java b/src/main/java/game/data/chunk/palette/Palette.java index a7654ebb..4ec9e54a 100644 --- a/src/main/java/game/data/chunk/palette/Palette.java +++ b/src/main/java/game/data/chunk/palette/Palette.java @@ -6,6 +6,7 @@ import java.util.List; import game.data.chunk.ChunkSection; +import java.util.stream.Stream; import packets.DataTypeProvider; import packets.builder.PacketBuilder; import se.llbit.nbt.ListTag; @@ -17,26 +18,29 @@ public class Palette { private int bitsPerBlock; private int[] palette; - StateProvider stateProvider; + Registry registry; PaletteType type = PaletteType.BLOCKS; protected Palette() { // palette needs initializing this.palette = new int[1]; - this.stateProvider = GlobalPaletteProvider.getGlobalPalette(); + this.registry = GlobalPaletteProvider.getGlobalPalette(); + if (this.registry == null) { + System.out.println("No state provider available: GlobalPaletteProvider.getGlobalPalette()"); + } } private Palette(int bitsPerBlock, int[] palette) { this.bitsPerBlock = bitsPerBlock; this.palette = palette; - this.stateProvider = GlobalPaletteProvider.getGlobalPalette(); + this.registry = GlobalPaletteProvider.getGlobalPalette(); synchronizeBitsPerBlock(); } Palette(int[] arr) { this.palette = arr; this.bitsPerBlock = computeBitsPerBlock(Math.max(0, arr.length - 1)); - this.stateProvider = GlobalPaletteProvider.getGlobalPalette(); + this.registry = GlobalPaletteProvider.getGlobalPalette(); } public static Palette biomes(int dataVersion, ListTag palette) { @@ -47,7 +51,7 @@ public static Palette biomes(int dataVersion, ListTag palette) { } public void biomePalette() { - this.stateProvider = WorldManager.getInstance().getDimensionCodec().getBiomeProvider(); + this.registry = WorldManager.getInstance().getDimensionRegistry().getBiomeRegistry(); this.type = PaletteType.BIOMES; } @@ -84,14 +88,14 @@ public Palette(int dataVersion, ListTag nbt, boolean isBiomePalette) { if (isBiomePalette) { this.biomePalette(); } else { - this.stateProvider = GlobalPaletteProvider.getGlobalPalette(dataVersion); + this.registry = GlobalPaletteProvider.getGlobalPalette(dataVersion); } this.palette = new int[nbt.size()]; this.bitsPerBlock = computeBitsPerBlock(nbt.size() - 1); for (int i = 0; i < nbt.size(); i++) { - int bs = this.stateProvider.getStateId((SpecificTag) nbt.get(i)); + int bs = this.registry.getStateId((SpecificTag) nbt.get(i)); this.palette[i] = bs; } @@ -165,18 +169,18 @@ public boolean isEmpty() { } /** - * Create an NBT version of this palette using the global palette. + * Create an NBT version of this palette using the block registry. */ public List toNbt() { List tags = new ArrayList<>(); - if (stateProvider == null) { - throw new UnsupportedOperationException("Cannot create palette NBT without a global palette."); + if (registry == null) { + throw new UnsupportedOperationException("Cannot create palette NBT without a block registry."); } for (int i : palette) { - State state = stateProvider.getState(i); - if (state == null) { state = stateProvider.getDefaultState(); } + State state = registry.getState(i); + if (state == null) { state = registry.getDefaultState(); } tags.add(state.toNbt()); @@ -256,5 +260,9 @@ private void resize(ChunkSection section, int oldBpp) { public int size() { return palette.length; } + + public Stream values() { + return Arrays.stream(palette).mapToObj(el -> registry.getState(el)); + } } diff --git a/src/main/java/game/data/chunk/palette/StateProvider.java b/src/main/java/game/data/chunk/palette/Registry.java similarity index 75% rename from src/main/java/game/data/chunk/palette/StateProvider.java rename to src/main/java/game/data/chunk/palette/Registry.java index d96c4cd8..d4978ea0 100644 --- a/src/main/java/game/data/chunk/palette/StateProvider.java +++ b/src/main/java/game/data/chunk/palette/Registry.java @@ -1,17 +1,16 @@ -package game.data.chunk.palette; - -import se.llbit.nbt.CompoundTag; -import se.llbit.nbt.SpecificTag; - -/** - * Interface for data source of palettes. - */ -public interface StateProvider { - State getState(int i); - - State getState(SpecificTag nbt); - - int getStateId(SpecificTag nbt); - - State getDefaultState(); -} +package game.data.chunk.palette; + +import se.llbit.nbt.SpecificTag; + +/** + * Interface for data source of palettes. + */ +public interface Registry { + State getState(int i); + + State getState(SpecificTag nbt); + + int getStateId(SpecificTag nbt); + + State getDefaultState(); +} diff --git a/src/main/java/game/data/chunk/palette/SingleValuePalette.java b/src/main/java/game/data/chunk/palette/SingleValuePalette.java index 42660df9..10b38b4a 100644 --- a/src/main/java/game/data/chunk/palette/SingleValuePalette.java +++ b/src/main/java/game/data/chunk/palette/SingleValuePalette.java @@ -16,7 +16,7 @@ public SingleValuePalette(PaletteType type, SpecificTag state) { if (type == PaletteType.BIOMES) { this.biomePalette(); } - this.val = this.stateProvider.getStateId(state); + this.val = this.registry.getStateId(state); } @Override @@ -62,7 +62,7 @@ public int getBitsPerBlock() { @Override public List toNbt() { - return List.of(stateProvider.getState(val).toNbt()); + return List.of(registry.getState(val).toNbt()); } public Palette asNormalPalette() { diff --git a/src/main/java/game/data/chunk/version/Chunk_1_18.java b/src/main/java/game/data/chunk/version/Chunk_1_18.java index fa55e2b8..04359af2 100644 --- a/src/main/java/game/data/chunk/version/Chunk_1_18.java +++ b/src/main/java/game/data/chunk/version/Chunk_1_18.java @@ -4,7 +4,7 @@ import game.data.chunk.BlockEntityRegistry; import game.data.chunk.ChunkSection; import game.data.chunk.palette.BlockState; -import game.data.chunk.palette.GlobalPalette; +import game.data.chunk.palette.BlockRegistry; import game.data.chunk.palette.GlobalPaletteProvider; import game.data.chunk.palette.Palette; import game.data.chunk.palette.PaletteType; @@ -135,7 +135,7 @@ public void readChunkColumn(DataTypeProvider dataProvider) { protected void findBlockEntities(ChunkSection section, int sectionY) { BlockEntityRegistry blockEntities = RegistryManager.getInstance().getBlockEntityRegistry(); - GlobalPalette globalPalette = GlobalPaletteProvider.getGlobalPalette(getDataVersion()); + BlockRegistry globalPalette = GlobalPaletteProvider.getGlobalPalette(getDataVersion()); for (int y = 0; y < 16; y++) { for (int z = 0; z < 16; z++) { diff --git a/src/main/java/game/data/container/ItemRegistry.java b/src/main/java/game/data/container/ItemRegistry.java index 0ff07ee6..2bb113bd 100644 --- a/src/main/java/game/data/container/ItemRegistry.java +++ b/src/main/java/game/data/container/ItemRegistry.java @@ -2,7 +2,7 @@ import com.google.gson.Gson; -import game.data.chunk.palette.GlobalPalette; +import game.data.chunk.palette.BlockRegistry; import game.data.registries.RegistriesJson; import java.io.FileInputStream; @@ -16,7 +16,7 @@ public class ItemRegistry { private Map nameToId; public static ItemRegistry fromJson(String version) { - InputStream x = GlobalPalette.class.getClassLoader().getResourceAsStream("items-" + version + ".json"); + InputStream x = BlockRegistry.class.getClassLoader().getResourceAsStream("items-" + version + ".json"); if (x == null) { return null; } diff --git a/src/main/java/game/data/dimension/Biome.java b/src/main/java/game/data/dimension/Biome.java index cd7d7eb3..f7e4301c 100644 --- a/src/main/java/game/data/dimension/Biome.java +++ b/src/main/java/game/data/dimension/Biome.java @@ -50,6 +50,17 @@ public void write(Path fromPath) throws IOException { Files.createDirectories(p.getParent()); Files.write(p, Collections.singleton(properties.json())); } + + @Override + public String toString() { + return "Biome{" + + "namespace='" + namespace + '\'' + + ", path='" + path + '\'' + + ", name='" + name + '\'' + + ", id=" + id + + ", properties=" + properties + + '}'; + } } /** @@ -82,6 +93,10 @@ class BiomeProperties { BiomeProperties(CompoundTag tag) { properties = new HashMap<>(defaultProperties); + if (tag == null) { + return; + } + for (NamedTag el : tag) { properties.put(el.name, el.tag); } @@ -91,6 +106,6 @@ class BiomeProperties { * Datapack stores this as a JSON file. */ public String json() { - return DimensionCodec.GSON.toJson(this.properties); + return DimensionRegistry.GSON.toJson(this.properties); } } diff --git a/src/main/java/game/data/dimension/BiomeProvider.java b/src/main/java/game/data/dimension/BiomeRegistry.java similarity index 70% rename from src/main/java/game/data/dimension/BiomeProvider.java rename to src/main/java/game/data/dimension/BiomeRegistry.java index 758dd0f1..d798e348 100644 --- a/src/main/java/game/data/dimension/BiomeProvider.java +++ b/src/main/java/game/data/dimension/BiomeRegistry.java @@ -1,25 +1,31 @@ package game.data.dimension; import game.data.chunk.palette.State; -import game.data.chunk.palette.StateProvider; +import game.data.chunk.palette.Registry; import java.util.HashMap; import java.util.Map; import java.util.Objects; import se.llbit.nbt.SpecificTag; import se.llbit.nbt.StringTag; -public class BiomeProvider implements StateProvider { +public class BiomeRegistry implements Registry { private final Map biomesFromId; private final Map biomeIdsFromIdentifier; - public BiomeProvider(Map biomes) { - biomesFromId = new HashMap<>(); - biomeIdsFromIdentifier = new HashMap<>(); + public BiomeRegistry(Map biomes) { + this(); - biomes.forEach((name, biome) -> { - biomesFromId.put(biome.id, new BiomeIdentifier(name)); - biomeIdsFromIdentifier.put(new BiomeIdentifier(name), biome.id); - }); + biomes.forEach(this::addBiome); + } + + public void addBiome(String name, Biome biome) { + biomesFromId.put(biome.id, new BiomeIdentifier(name)); + biomeIdsFromIdentifier.put(new BiomeIdentifier(name), biome.id); + } + + public BiomeRegistry() { + this.biomesFromId = new HashMap<>(); + this.biomeIdsFromIdentifier = new HashMap<>(); } @Override @@ -41,6 +47,11 @@ public State getState(SpecificTag nbt) { public State getDefaultState() { return biomesFromId.get(0); } + + @Override + public String toString() { + return "BiomeRegistry{" + biomesFromId + '}'; + } } class BiomeIdentifier implements State { diff --git a/src/main/java/game/data/dimension/Dimension.java b/src/main/java/game/data/dimension/Dimension.java index d2266697..af3701ac 100644 --- a/src/main/java/game/data/dimension/Dimension.java +++ b/src/main/java/game/data/dimension/Dimension.java @@ -66,10 +66,10 @@ public static Dimension fromString(String readString) { case "minecraft:the_nether": return NETHER; case "minecraft:overworld": return OVERWORLD; default: { - DimensionCodec codec = WorldManager.getInstance().getDimensionCodec(); - if (codec == null) { return OVERWORLD; } + DimensionRegistry registry = WorldManager.getInstance().getDimensionRegistry(); + if (registry == null) { return OVERWORLD; } - Dimension dim = codec.getDimension(readString); + Dimension dim = registry.getDimension(readString); if (dim == null) { return OVERWORLD; } return dim; @@ -127,7 +127,7 @@ public void registerType(SpecificTag dimensionNbt) { } int hash = dimensionNbt.hashCode(); - DimensionType type = WorldManager.getInstance().getDimensionCodec().getDimensionType(hash); + DimensionType type = WorldManager.getInstance().getDimensionRegistry().getDimensionType(hash); if (type != null) { this.type = type.getName(); @@ -168,8 +168,7 @@ public String getName() { * implementation). */ public void setType(String dimensionType) { - DimensionType type = - WorldManager.getInstance().getDimensionCodec().getDimensionType(dimensionType); + DimensionType type = WorldManager.getInstance().getDimensionRegistry().getDimensionType(dimensionType); if (type == null) { return; diff --git a/src/main/java/game/data/dimension/DimensionCodec.java b/src/main/java/game/data/dimension/DimensionRegistry.java similarity index 63% rename from src/main/java/game/data/dimension/DimensionCodec.java rename to src/main/java/game/data/dimension/DimensionRegistry.java index 5c7ea402..ac9a2c8e 100644 --- a/src/main/java/game/data/dimension/DimensionCodec.java +++ b/src/main/java/game/data/dimension/DimensionRegistry.java @@ -1,176 +1,218 @@ -package game.data.dimension; - -import com.google.gson.*; -import game.data.chunk.palette.StateProvider; -import se.llbit.nbt.*; - -import java.io.IOException; -import java.nio.file.Path; -import java.util.Collection; -import java.util.HashMap; -import java.util.Map; - -/** - * Storage for custom dimensions, dimension types and biomes. Default biomes/dimensions are also sent by the server but - * not stored. - */ -public class DimensionCodec { - public static final Gson GSON; - - static { - /* - * To convert the properties to JSON, we need to register adapters so that GSON knows how to turn our NBT object - * into desirable JSON. - */ - GsonBuilder g = new GsonBuilder(); - g.registerTypeAdapter(StringTag.class, (JsonSerializer) (str, x, y) -> new JsonPrimitive(str.value)); - g.registerTypeAdapter(DoubleTag.class, (JsonSerializer) (num, x, y) -> new JsonPrimitive(num.value)); - g.registerTypeAdapter(FloatTag.class, (JsonSerializer) (num, x, y) -> new JsonPrimitive(num.value)); - g.registerTypeAdapter(IntTag.class, (JsonSerializer) (num, x, y) -> new JsonPrimitive(num.value)); - g.registerTypeAdapter(NamedTag.class, (JsonSerializer) (tag, x, ctx) -> ctx.serialize(tag.tag)); - g.registerTypeAdapter(ByteTag.class, (JsonSerializer) (tag, x, ctx) -> ctx.serialize(tag.value == 1)); - g.registerTypeAdapter(CompoundTag.class, (JsonSerializer) (list, x, ctx) -> { - JsonObject obj = new JsonObject(); - - for (NamedTag t : list) { - obj.add(t.name, ctx.serialize(t)); - } - - return obj; - }); - g.registerTypeAdapter(ListTag.class, (JsonSerializer) (list, x, ctx) -> { - JsonArray arr = new JsonArray(); - - for (SpecificTag t : list) { - arr.add(ctx.serialize(t)); - } - - return arr; - }); - GSON = g.create(); - } - - - private BiomeProvider biomeProvider; - - private final Map dimensions; - private final Map dimensionTypesByHash; - private final Map dimensionTypesByName; - private final Map biomes; - - private DimensionCodec() { - this.dimensions = new HashMap<>(); - this.dimensionTypesByHash = new HashMap<>(); - this.dimensionTypesByName = new HashMap<>(); - this.biomes = new HashMap<>(); - } - - public static DimensionCodec fromNbt(SpecificTag tag) { - DimensionCodec codec = new DimensionCodec(); - - codec.readDimensionTypes(tag.get("minecraft:dimension_type").asCompound().get("value").asList()); - codec.readBiomes(tag.get("minecraft:worldgen/biome").asCompound().get("value").asList()); - - return codec; - } - - public DimensionCodec setDimensionNames(String[] dimensionNames) { - this.readDimensions(dimensionNames); - return this; - } - - public Collection getDimensions() { - return dimensions.values(); - } - - private void readDimensions(String[] dimensionNames) { - for (String dimensionName : dimensionNames) { - String[] parts = dimensionName.split(":"); - String namespace = parts[0]; - String name = parts[1]; - - this.dimensions.put(dimensionName, new Dimension(namespace, name)); - } - } - - /** - * Dimension types. These are not very useful currently as the server does not actually let the client know which - * dimensions have which types. - */ - private void readDimensionTypes(ListTag dimensionList) { - for (SpecificTag dim : dimensionList) { - CompoundTag d = dim.asCompound(); - - String identifier = ((StringTag) d.get("name")).value; - String[] parts = identifier.split(":"); - String namespace = parts[0]; - String name = parts[1]; - - DimensionType type = new DimensionType(namespace, name, d); - this.dimensionTypesByHash.put(type.getSignature(), type); - this.dimensionTypesByName.put(type.getName(), type); - } - } - - /** - * Biome registry. - */ - private void readBiomes(ListTag biomeList) { - for (SpecificTag biome : biomeList) { - CompoundTag b = biome.asCompound(); - - String identifier = ((StringTag) b.get("name")).value; - int id = ((IntTag) b.get("id")).value; - String[] parts = identifier.split(":"); - String namespace = parts[0]; - String name = parts[1]; - - this.biomes.put(identifier, new Biome(namespace, name, id, b.get("element").asCompound())); - } - this.biomeProvider = new BiomeProvider(this.biomes); - } - - public Dimension getDimension(String name) { - return dimensions.get(name); - } - - /** - * Get a dimension by it's signature. The signature is just the hash of the properties. - */ - public DimensionType getDimensionType(int signature) { - return dimensionTypesByHash.get(signature); - } - - public DimensionType getDimensionType(String name) { - return dimensionTypesByName.get(name); - } - - /** - * Write all the custom dimension data, if there is any. - */ - public boolean write(Path destination) throws IOException { - if (biomes.isEmpty() && dimensionTypesByHash.isEmpty()) { - // nothing to write - return false; - } - - for (Dimension d : dimensions.values()) { - d.write(destination); - } - - for (DimensionType d : dimensionTypesByHash.values()) { - d.write(destination); - } - - for (Biome b : biomes.values()) { - b.write(destination); - } - - - return true; - } - - public StateProvider getBiomeProvider() { - return biomeProvider; - } -} +package game.data.dimension; + +import com.google.gson.*; +import config.Config; +import game.data.chunk.palette.Registry; +import java.util.List; +import packets.DataTypeProvider; +import se.llbit.nbt.*; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; + +/** + * Storage for custom dimensions, dimension types and biomes. Default biomes/dimensions are also sent by the server but + * not stored. + */ +public class DimensionRegistry { + public static final Gson GSON; + + static { + /* + * To convert the properties to JSON, we need to register adapters so that GSON knows how to turn our NBT object + * into desirable JSON. + */ + GsonBuilder g = new GsonBuilder(); + g.registerTypeAdapter(StringTag.class, (JsonSerializer) (str, x, y) -> new JsonPrimitive(str.value)); + g.registerTypeAdapter(DoubleTag.class, (JsonSerializer) (num, x, y) -> new JsonPrimitive(num.value)); + g.registerTypeAdapter(FloatTag.class, (JsonSerializer) (num, x, y) -> new JsonPrimitive(num.value)); + g.registerTypeAdapter(IntTag.class, (JsonSerializer) (num, x, y) -> new JsonPrimitive(num.value)); + g.registerTypeAdapter(NamedTag.class, (JsonSerializer) (tag, x, ctx) -> ctx.serialize(tag.tag)); + g.registerTypeAdapter(ByteTag.class, (JsonSerializer) (tag, x, ctx) -> ctx.serialize(tag.value == 1)); + g.registerTypeAdapter(CompoundTag.class, (JsonSerializer) (list, x, ctx) -> { + JsonObject obj = new JsonObject(); + + for (NamedTag t : list) { + obj.add(t.name, ctx.serialize(t)); + } + + return obj; + }); + g.registerTypeAdapter(ListTag.class, (JsonSerializer) (list, x, ctx) -> { + JsonArray arr = new JsonArray(); + + for (SpecificTag t : list) { + arr.add(ctx.serialize(t)); + } + + return arr; + }); + GSON = g.create(); + } + + + private BiomeRegistry biomeRegistry; + + private final Map dimensions; + private final Map dimensionTypesByID; + private final Map dimensionTypesByName; + private final Map biomes; + + private DimensionRegistry() { + this.dimensions = new HashMap<>(); + this.dimensionTypesByName = new HashMap<>(); + this.dimensionTypesByID = new HashMap<>(); + this.biomes = new HashMap<>(); + + this.dimensions.put("minecraft:overworld", Dimension.OVERWORLD); + this.dimensions.put("minecraft:the_nether", Dimension.NETHER); + this.dimensions.put("minecraft:the_end", Dimension.END); + } + + public static DimensionRegistry empty() { + return new DimensionRegistry(); + } + + public static DimensionRegistry fromNbt(SpecificTag tag) { + DimensionRegistry codec = new DimensionRegistry(); + + codec.readDimensionTypes(tag.get("minecraft:dimension_type").asCompound().get("value").asList()); + codec.readBiomes(tag.get("minecraft:worldgen/biome").asCompound().get("value").asList()); + + return codec; + } + + public void setDimensionNames(String[] dimensionNames) { + this.readDimensions(dimensionNames); + } + + public Collection getDimensions() { + return dimensions.values(); + } + + private void readDimensions(String[] dimensionNames) { + for (String dimensionName : dimensionNames) { + String[] parts = dimensionName.split(":"); + String namespace = parts[0]; + String name = parts[1]; + + this.dimensions.put(dimensionName, new Dimension(namespace, name)); + } + } + + /** + * Dimension types. These are not very useful currently as the server does not actually let the client know which + * dimensions have which types. + */ + private void readDimensionTypes(ListTag dimensionList) { + for (SpecificTag dim : dimensionList) { + CompoundTag d = dim.asCompound(); + + String identifier = ((StringTag) d.get("name")).value; + String[] parts = identifier.split(":"); + String namespace = parts[0]; + String name = parts[1]; + + DimensionType type = new DimensionType(namespace, name, d); + this.dimensionTypesByID.put(type.getSignature(), type); + this.dimensionTypesByName.put(type.getName(), type); + } + } + + /** + * Biome registry. + */ + private void readBiomes(ListTag biomeList) { + for (SpecificTag biome : biomeList) { + CompoundTag b = biome.asCompound(); + + String identifier = ((StringTag) b.get("name")).value; + int id = ((IntTag) b.get("id")).value; + String[] parts = identifier.split(":"); + String namespace = parts[0]; + String name = parts[1]; + + this.biomes.put(identifier, new Biome(namespace, name, id, b.get("element").asCompound())); + } + this.biomeRegistry = new BiomeRegistry(this.biomes); + } + + public Dimension getDimension(String name) { + return dimensions.get(name); + } + + /** + * Get a dimension by it's ID. Pre-1.19 the ID is the hash of the properties. + */ + public DimensionType getDimensionType(int id) { + return dimensionTypesByID.get(id); + } + + public DimensionType getDimensionType(String id) { + return dimensionTypesByName.get(id); + } + + /** + * Write all the custom dimension data, if there is any. + */ + public boolean write(Path destination) throws IOException { + if (biomes.isEmpty() && dimensionTypesByID.isEmpty()) { + // nothing to write + return false; + } + + for (Dimension d : dimensions.values()) { + d.write(destination); + } + + for (DimensionType d : dimensionTypesByID.values()) { + d.write(destination); + } + + for (Biome b : biomes.values()) { + b.write(destination); + } + + + return true; + } + + public Registry getBiomeRegistry() { + return biomeRegistry; + } + + public void loadBiomes(DataTypeProvider.Registry registry) { + this.biomeRegistry = new BiomeRegistry(); + + List entries = registry.entries(); + for (int id = 0; id < entries.size(); id++) { + var biome = entries.get(id); + + String[] parts = biome.name().split(":"); + String namespace = parts[0]; + String name = parts[1]; + + var res = new Biome(namespace, name, id, biome.nbt().map(Tag::asCompound).orElse(null)); + this.biomes.put(biome.name(), res); + this.biomeRegistry.addBiome(biome.name(), res); + } + } + + public void loadDimensions(DataTypeProvider.Registry registry) { + List entries = registry.entries(); + for (int id = 0; id < entries.size(); id++) { + var dim = entries.get(id); + + String[] parts = dim.name().split(":"); + String namespace = parts[0]; + String name = parts[1]; + + DimensionType type = new DimensionType(namespace, name, id, dim.nbt().map(Tag::asCompound).orElse(null)); + this.dimensionTypesByID.put(id, type); + this.dimensionTypesByName.put(type.getName(), type); + } + } +} diff --git a/src/main/java/game/data/dimension/DimensionType.java b/src/main/java/game/data/dimension/DimensionType.java index 2c3bc456..5332f300 100644 --- a/src/main/java/game/data/dimension/DimensionType.java +++ b/src/main/java/game/data/dimension/DimensionType.java @@ -15,6 +15,18 @@ * lava flows. */ public class DimensionType { + private static final CompoundTag PROPERTIES_OVERWORLD_DEFAULT; + private static final CompoundTag PROPERTIES_DEFAULT; + + static { + PROPERTIES_OVERWORLD_DEFAULT = new CompoundTag(); + PROPERTIES_OVERWORLD_DEFAULT.add("min_y", new IntTag(-64)); + PROPERTIES_OVERWORLD_DEFAULT.add("height", new IntTag(384)); + + PROPERTIES_DEFAULT = new CompoundTag(); + PROPERTIES_DEFAULT.add("min_y", new IntTag(0)); + PROPERTIES_DEFAULT.add("height", new IntTag(256)); + } private final String namespace; private final String name; @@ -22,12 +34,60 @@ public class DimensionType { private final int signature; private CompoundTag properties; + // default values + private int dimensionMinHeight = 0; + private int dimensionMaxHeight = 256; + + // after 1.20.6 + DimensionType(String namespace, String name, int id, CompoundTag properties) { + this.properties = properties; + this.index = id; + this.namespace = namespace; + this.name = name; + this.signature = this.properties == null ? 0 : this.properties.hashCode(); + + if (properties == null) { + setPropertiesFromName(); + } + + parseHeights(); + } + + // before 1.20.6 DimensionType(String namespace, String name, CompoundTag tag) { this.properties = (CompoundTag) tag.get("element"); this.index = ((IntTag) tag.get("id")).value; this.namespace = namespace; this.name = name; this.signature = this.properties.hashCode(); + + parseHeights(); + } + + private void setPropertiesFromName() { + if (!this.namespace.equals("minecraft")) { return; } + + if (this.name.equals("overworld")) { + this.properties = PROPERTIES_OVERWORLD_DEFAULT; + } else { + this.properties = PROPERTIES_DEFAULT; + } + } + + private void parseHeights() { + if (this.properties == null) { + return; + } + + var minTag = this.properties.get("min_y"); + var heightTag = this.properties.get("height"); + + if (minTag.isError() || heightTag.isError()) { + return; + } + + this.dimensionMinHeight = minTag.intValue(); + this.dimensionMaxHeight = heightTag.intValue(); } public CompoundTag getProperties() { @@ -50,7 +110,7 @@ public void write(Path prefix) throws IOException { Path destination = PathUtils.toPath(prefix.toString(), namespace, "dimension_type", name + ".json"); Files.createDirectories(destination.getParent()); - Files.write(destination, Collections.singleton(DimensionCodec.GSON.toJson(properties))); + Files.write(destination, Collections.singleton(DimensionRegistry.GSON.toJson(properties))); } /** @@ -59,4 +119,12 @@ public void write(Path prefix) throws IOException { public String getName() { return namespace + ":" + name; } + + public int getDimensionMinHeight() { + return dimensionMinHeight; + } + + public int getDimensionMaxHeight() { + return dimensionMaxHeight; + } } diff --git a/src/main/java/game/data/entity/EntityNames.java b/src/main/java/game/data/entity/EntityNames.java index 4c2ab6f7..6834b933 100644 --- a/src/main/java/game/data/entity/EntityNames.java +++ b/src/main/java/game/data/entity/EntityNames.java @@ -49,7 +49,7 @@ public static EntityNames fromRegistry(InputStream input) { } /** - * Get a block state from a given index. Used to convert packet palettes to the global palette. + * Get a block state from a given index. Used to convert packet palettes to the block registry. */ public String getName(int key) { return entities.getOrDefault(key, null); diff --git a/src/main/java/game/data/registries/RegistryLoader.java b/src/main/java/game/data/registries/RegistryLoader.java index 8309d6ef..29b4db1f 100644 --- a/src/main/java/game/data/registries/RegistryLoader.java +++ b/src/main/java/game/data/registries/RegistryLoader.java @@ -10,6 +10,7 @@ import java.io.InputStreamReader; import java.nio.file.Files; import java.nio.file.Path; +import java.nio.file.Paths; import java.nio.file.StandardCopyOption; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; @@ -19,7 +20,7 @@ import config.Config; import game.UnsupportedMinecraftVersionException; import game.data.chunk.BlockEntityRegistry; -import game.data.chunk.palette.GlobalPalette; +import game.data.chunk.palette.BlockRegistry; import game.data.container.ItemRegistry; import game.data.container.MenuRegistry; import game.data.entity.EntityNames; @@ -130,13 +131,14 @@ private void generateReports() throws IOException, InterruptedException { System.out.println("Starting output of Minecraft server.jar:"); ProcessBuilder pb; + String javaRuntime = Paths.get(System.getProperty("java.home"), "bin", "java").toString(); if (Config.versionReporter().isAtLeast(Version.V1_18)) { pb = new ProcessBuilder( - "java", "-DbundlerMainClass=net.minecraft.data.Main", "-jar", "server.jar", "--reports" + javaRuntime, "-DbundlerMainClass=net.minecraft.data.Main", "-jar", "server.jar", "--reports" ); } else { pb = new ProcessBuilder( - "java", "-cp", "server.jar", "net.minecraft.data.Main", "--reports" + javaRuntime, "-cp", "server.jar", "net.minecraft.data.Main", "--reports" ); } @@ -200,11 +202,11 @@ public EntityNames generateEntityNames() throws IOException { } } - public GlobalPalette generateGlobalPalette() throws IOException { + public BlockRegistry generateGlobalPalette() throws IOException { if (versionSupportsBlockGenerator()) { - return new GlobalPalette(new FileInputStream(blocksPath.toFile())); + return new BlockRegistry(new FileInputStream(blocksPath.toFile())); } else { - return new GlobalPalette("1.12.2"); + return new BlockRegistry("1.12.2"); } } diff --git a/src/main/java/game/data/registries/modded/ForgeRegistryHandler.java b/src/main/java/game/data/registries/modded/ForgeRegistryHandler.java index 5a9ad6cd..ba6ffaee 100644 --- a/src/main/java/game/data/registries/modded/ForgeRegistryHandler.java +++ b/src/main/java/game/data/registries/modded/ForgeRegistryHandler.java @@ -146,11 +146,11 @@ public Tag toNbt() { } /** - * Add blocks to the global palette + * Add blocks to the block registry */ public void registerBlocks() { // since blockstates in 1.12.2 have half a byte of data at the end, we need to shift the - // blockstates we register to the global palette for the minimap to remain correct(ish) + // blockstates we register to the block registry for the minimap to remain correct(ish) ids.forEach(pair -> GlobalPaletteProvider.registerBlock(pair.getKey(), pair.getValue() << 4)); } } diff --git a/src/main/java/game/protocol/ConfigurationProtocol.java b/src/main/java/game/protocol/ConfigurationProtocol.java index 2bbfa1dd..e09529c8 100644 --- a/src/main/java/game/protocol/ConfigurationProtocol.java +++ b/src/main/java/game/protocol/ConfigurationProtocol.java @@ -14,9 +14,11 @@ public ConfigurationProtocol() { clientBound = new HashMap<>(); serverBound = new HashMap<>(); - if (Config.versionReporter().isAtLeast(Version.V1_20_2)) { + if (Config.versionReporter().isAtLeast(Version.V1_20_6)) { + serverBound.put(0x03, "FinishConfiguration"); + clientBound.put(0x07, "RegistryData"); + } else if (Config.versionReporter().isAtLeast(Version.V1_20_2)) { serverBound.put(0x02, "FinishConfiguration"); - clientBound.put(0x05, "RegistryData"); } else { serverBound.put(0x03, "FinishConfiguration"); diff --git a/src/main/java/game/protocol/LoginProtocol.java b/src/main/java/game/protocol/LoginProtocol.java index ccf0e144..1117457c 100644 --- a/src/main/java/game/protocol/LoginProtocol.java +++ b/src/main/java/game/protocol/LoginProtocol.java @@ -11,14 +11,15 @@ public LoginProtocol() { clientBound = new HashMap<>(); serverBound = new HashMap<>(); - clientBound.put(0x00, "disconnect"); - clientBound.put(0x01, "encryption_request"); - clientBound.put(0x02, "login_success"); - clientBound.put(0x03, "set_compression"); + clientBound.put(0x00, "LoginDisconnect"); + clientBound.put(0x01, "Hello"); + clientBound.put(0x02, "GameProfile"); + clientBound.put(0x03, "LoginCompression"); - serverBound.put(0x00, "login_start"); - serverBound.put(0x01, "encryption_response"); - serverBound.put(0x02, "login_plugin_response"); + serverBound.put(0x00, "Hello"); + serverBound.put(0x01, "Key"); + serverBound.put(0x02, "CustomQueryAnswer"); + serverBound.put(0x03, "LoginAcknowledged"); } @Override diff --git a/src/main/java/gui/AuthTabController.java b/src/main/java/gui/AuthTabController.java index 1d9acd46..e550ebbd 100644 --- a/src/main/java/gui/AuthTabController.java +++ b/src/main/java/gui/AuthTabController.java @@ -89,7 +89,7 @@ private void clearAuthenticationStatus() { } public void checkButtonPressed(ActionEvent actionEvent) { - AuthDetailsManager.validateAuthStatus(this::setStatusText, this::setStatusError); + AuthDetailsManager.validateAuthStatus(username -> setStatusText("Valid session found! \n\nUsername: " + username), this::setStatusError); clearAuthenticationStatus(); } diff --git a/src/main/java/gui/GuiMap.java b/src/main/java/gui/GuiMap.java index 9777ee3d..2ad646c9 100644 --- a/src/main/java/gui/GuiMap.java +++ b/src/main/java/gui/GuiMap.java @@ -358,6 +358,10 @@ private void drawEntities() { * If the name of the player is not known it's first requested from the Mojang API. */ private void drawOtherPlayer(GraphicsContext graphics, PlayerEntity player) { + if (player.getPosition() == null) { + return; + } + double playerX = ((player.getPosition().getX() - bounds.getMinX()) / blocksPerPixel); double playerZ = ((player.getPosition().getZ() - bounds.getMinZ()) / blocksPerPixel); if (mouseOver && isNear(playerX, playerZ)) { diff --git a/src/main/java/gui/GuiSettings.java b/src/main/java/gui/GuiSettings.java index fc93c341..75d3b14c 100644 --- a/src/main/java/gui/GuiSettings.java +++ b/src/main/java/gui/GuiSettings.java @@ -1,6 +1,7 @@ package gui; +import static util.ExceptionHandling.attempt; import static util.ExceptionHandling.attemptQuiet; import config.Config; @@ -14,6 +15,7 @@ import java.util.Arrays; import java.util.List; import java.util.Map; +import java.util.function.Consumer; import javafx.application.Platform; import javafx.collections.ListChangeListener; import javafx.collections.ObservableList; @@ -31,6 +33,14 @@ import javafx.scene.control.TextArea; import javafx.scene.control.TextField; import javafx.scene.control.Tooltip; +import javafx.scene.input.Clipboard; +import javafx.scene.input.ClipboardContent; +import javafx.scene.paint.Paint; +import org.apache.commons.lang3.SystemUtils; +import proxy.auth.AuthDetailsManager; +import proxy.auth.AuthenticationMethod; +import proxy.auth.MicrosoftAuthHandler; +import proxy.auth.MicrosoftAuthServer; import util.PathUtils; public class GuiSettings { @@ -54,7 +64,7 @@ public class GuiSettings { public Slider extendedDistance; public IntField extendedDistanceText; public Hyperlink openWorldDir; - public Hyperlink verifyAuthLink; + public Hyperlink projectSupportLink; public CheckBox renderOtherPlayers; public CheckBox enableInfoMessages; public Tab generalTab; @@ -64,6 +74,12 @@ public class GuiSettings { public AuthTabController authController; public CheckBox enableDrawExtendedChunks; public CheckBox enableCaveRenderMode; + public Label authResult; + + + + + private MicrosoftAuthServer authServer; Config config; private boolean portInUse; @@ -142,13 +158,14 @@ void initialize() { } })); - verifyAuthLink.setOnAction(e -> tabPane.getSelectionModel().select(authTab)); + projectSupportLink.setOnAction(actionEvent -> GuiManager.openWebLink("https://github.com/sponsors/mircokroon?frequency=one-time&amount=5")); handleDataValidation(); handleErrorTab(); handleResizing(); resetHeight(); + validateAuth(); } private void resetHeight() { @@ -331,4 +348,39 @@ public void setSelectedIp(String address) { tabPane.getSelectionModel().selectFirst(); this.saveButton.requestFocus(); } + + public void msAuthPressed(ActionEvent actionEvent) { + if (!SystemUtils.IS_OS_WINDOWS && !SystemUtils.IS_OS_MAC) { + tabPane.getSelectionModel().select(authTab); + return; + } + Config.setAuthMethod(AuthenticationMethod.MICROSOFT); + Consumer onStart = shortUrl -> GuiManager.openWebLink(shortUrl); + + + // server already running + if (authServer != null) { + onStart.accept(authServer.getShortUrl()); + return; + } + + attempt(() -> { + authServer = new MicrosoftAuthServer(onStart, (authCode, usedPort) -> Platform.runLater(() -> { + Config.setMicrosoftAuth(MicrosoftAuthHandler.fromCode(authCode, usedPort)); + authServer = null; + + validateAuth(); + })); + }); + } + + private void validateAuth() { + AuthDetailsManager.validateAuthStatus(username -> { + authResult.setText("Username: " + username); + authResult.getStyleClass().remove("label-err"); + }, str -> { + authResult.setText("Not logged in"); + authResult.getStyleClass().add("label-err"); + }); + } } diff --git a/src/main/java/packets/DataTypeProvider.java b/src/main/java/packets/DataTypeProvider.java index 2a5cd7d0..210333c6 100644 --- a/src/main/java/packets/DataTypeProvider.java +++ b/src/main/java/packets/DataTypeProvider.java @@ -7,9 +7,12 @@ import game.data.container.Slot; import game.data.container.Slot_1_12; import game.data.coordinates.CoordinateDouble3D; +import java.util.Optional; +import java.util.function.Supplier; import packets.version.DataTypeProvider_1_13; import packets.version.DataTypeProvider_1_14; import packets.version.DataTypeProvider_1_20_2; +import packets.version.DataTypeProvider_1_20_6; import se.llbit.nbt.NamedTag; import se.llbit.nbt.SpecificTag; @@ -21,6 +24,7 @@ import java.util.Arrays; import java.util.BitSet; import java.util.List; +import se.llbit.nbt.Tag; /** * Class to provide an interface between the raw byte data and the various data types. Most methods are @@ -31,6 +35,21 @@ public class DataTypeProvider { private byte[] finalFullPacket; private int pos; + public byte[] debug__getFullArray() { + return finalFullPacket; + } + public String debug__readableString() { + char[] out = new char[finalFullPacket.length]; + for (int i = 0; i < finalFullPacket.length; i++) { + byte b = finalFullPacket[i]; + if (b >= 32) { + out[i] = (char) b; + } else { + out[i] = '_'; + } + } + return new String(out); + } public DataTypeProvider(byte[] finalFullPacket) { this.finalFullPacket = finalFullPacket; this.pos = 0; @@ -38,6 +57,7 @@ public DataTypeProvider(byte[] finalFullPacket) { public static DataTypeProvider ofPacket(byte[] finalFullPacket) { return Config.versionReporter().select(DataTypeProvider.class, + Option.of(Version.V1_20_6, () -> new DataTypeProvider_1_20_6(finalFullPacket)), Option.of(Version.V1_20_2, () -> new DataTypeProvider_1_20_2(finalFullPacket)), Option.of(Version.V1_14, () -> new DataTypeProvider_1_14(finalFullPacket)), Option.of(Version.V1_13, () -> new DataTypeProvider_1_13(finalFullPacket)), @@ -295,6 +315,30 @@ public DataTypeProvider copy() { return new DataTypeProvider(Arrays.copyOf(this.finalFullPacket, this.finalFullPacket.length)); } + public record Registry(String name, List entries) {} + public record RegistryEntry(String name, Optional nbt) {} + public Registry readRegistry() { + String name = readString(); + int numEntries = readVarInt(); + + List entries = new ArrayList<>(numEntries); + for (int i = 0; i < numEntries; i++) { + String identifier = readString(); + + Optional b = readOptional(this::readNbtTag); + entries.add(new RegistryEntry(identifier, b)); + } + + return new Registry(name, entries); + } + + public Optional readOptional(Supplier provider) { + if (readBoolean()) { + return Optional.of(provider.get()); + } + return Optional.empty(); + } + public int remaining() { return this.finalFullPacket.length - pos; } diff --git a/src/main/java/packets/builder/Chat.java b/src/main/java/packets/builder/Chat.java index 7baa77e2..4ee932f9 100644 --- a/src/main/java/packets/builder/Chat.java +++ b/src/main/java/packets/builder/Chat.java @@ -4,6 +4,8 @@ import java.util.ArrayList; import java.util.List; +import se.llbit.nbt.SpecificTag; +import se.llbit.nbt.StringTag; /** * Chat object, can be sent to the client to display messages. @@ -34,4 +36,8 @@ public void addChild(Chat c) { public String toJson() { return new Gson().toJson(this); } + + public SpecificTag toNbt() { + return new StringTag(text); + } } diff --git a/src/main/java/packets/builder/MessageTarget.java b/src/main/java/packets/builder/MessageTarget.java index e0f4c930..32cfd62f 100644 --- a/src/main/java/packets/builder/MessageTarget.java +++ b/src/main/java/packets/builder/MessageTarget.java @@ -9,12 +9,11 @@ public enum MessageTarget { GAMEINFO; byte getIdentifier() { - switch (this) { - case CHAT: return 0; - case SYSTEM: return 1; - case GAMEINFO: return 2; - } - return 0; + return switch (this) { + case CHAT -> 0; + case SYSTEM -> 1; + case GAMEINFO -> 2; + }; } } diff --git a/src/main/java/packets/builder/PacketBuilder.java b/src/main/java/packets/builder/PacketBuilder.java index c7ba2c83..4fdf602d 100644 --- a/src/main/java/packets/builder/PacketBuilder.java +++ b/src/main/java/packets/builder/PacketBuilder.java @@ -1,10 +1,16 @@ package packets.builder; import config.Config; +import config.Option; import config.Version; import game.protocol.Protocol; +import java.util.Arrays; import packets.DataTypeProvider; import packets.UUID; +import packets.handler.ClientBoundConfigurationPacketHandler; +import packets.handler.PacketHandler; +import packets.handler.version.ClientBoundConfigurationPacketHandler_1_20_2; +import packets.handler.version.ClientBoundConfigurationPacketHandler_1_20_6; import packets.lib.ByteQueue; import proxy.CompressionManager; import se.llbit.nbt.NamedTag; @@ -48,16 +54,30 @@ public void copy(DataTypeProvider provider, NetworkType... types) { public static PacketBuilder constructClientMessage(String message, MessageTarget target) { return constructClientMessage(new Chat(message), target); } + /** * Construct a message packet for the client. */ public static PacketBuilder constructClientMessage(Chat message, MessageTarget target) { Protocol protocol = Config.versionReporter().getProtocol(); - String packetName = Config.versionReporter().isAtLeast(Version.V1_19) ? "SystemChat" : "Chat"; + String packetName = Config.versionReporter().select(String.class, + Option.of(Version.V1_20_6, () -> "SetActionBarText"), + Option.of(Version.V1_19, () -> "SystemChat"), + Option.of(Version.ANY, () -> "Chat") + ); + PacketBuilder builder = new PacketBuilder(protocol.clientBound(packetName)); - builder.writeString(message.toJson()); - builder.writeByte(target.getIdentifier()); + if (Config.versionReporter().isAtLeast(Version.V1_20_6)) { + builder.writeNbtDirect(message.toNbt()); + } else { + builder.writeString(message.toJson()); + } + + // 1.20.6 has separate packet for action bar text, so dont need to give identifier anymore + if (!Config.versionReporter().isAtLeast(Version.V1_20_6)) { + builder.writeByte(target.getIdentifier()); + } // uuid is only included from 1.16, from 1.19 player chat is a different packet if (Config.versionReporter().isAtLeast(Version.V1_16) && !Config.versionReporter().isAtLeast(Version.V1_19)) { @@ -199,7 +219,6 @@ public void writeBoolean(boolean val) { /** * Writes an NBT tag. We need to wrap this in a NamedTag, as the named tag is not written itself. - * TODO: update on 1.20.2 */ public void writeNbt(SpecificTag nbt) { try { @@ -214,6 +233,21 @@ public void write(int b) { } } + // write NBT without wrapping it in a NamedTag + public void writeNbtDirect(SpecificTag nbt) { + try { + writeByte((byte) nbt.tagType()); + nbt.write(new DataOutputStream(new OutputStream() { + @Override + public void write(int b) { + bytes.insert((byte) b); + } + })); + } catch (IOException e) { + e.printStackTrace(); + } + } + public void writeByte(byte b) { bytes.insert(b); diff --git a/src/main/java/packets/handler/ClientBoundConfigurationPacketHandler.java b/src/main/java/packets/handler/ClientBoundConfigurationPacketHandler.java index 2166ae5c..01a5d7f1 100644 --- a/src/main/java/packets/handler/ClientBoundConfigurationPacketHandler.java +++ b/src/main/java/packets/handler/ClientBoundConfigurationPacketHandler.java @@ -4,6 +4,7 @@ import config.Option; import config.Version; import packets.handler.version.ClientBoundConfigurationPacketHandler_1_20_2; +import packets.handler.version.ClientBoundConfigurationPacketHandler_1_20_6; import proxy.ConnectionManager; import java.util.HashMap; @@ -18,6 +19,7 @@ public ClientBoundConfigurationPacketHandler(ConnectionManager connectionManager public static PacketHandler of(ConnectionManager connectionManager) { return Config.versionReporter().select(PacketHandler.class, + Option.of(Version.V1_20_6, () -> new ClientBoundConfigurationPacketHandler_1_20_6(connectionManager)), Option.of(Version.V1_20_2, () -> new ClientBoundConfigurationPacketHandler_1_20_2(connectionManager)), Option.of(Version.ANY, () -> new ClientBoundConfigurationPacketHandler(connectionManager)) ); diff --git a/src/main/java/packets/handler/ClientBoundGamePacketHandler.java b/src/main/java/packets/handler/ClientBoundGamePacketHandler.java index 3474e6ae..4b2448ce 100644 --- a/src/main/java/packets/handler/ClientBoundGamePacketHandler.java +++ b/src/main/java/packets/handler/ClientBoundGamePacketHandler.java @@ -160,7 +160,7 @@ public ClientBoundGamePacketHandler(ConnectionManager connectionManager) { return true; }); - operations.put("TradeList", provider -> { + operations.put("MerchantOffers", provider -> { worldManager.getEntityRegistry().addVillagerTrades(provider); return true; }); @@ -179,6 +179,7 @@ public ClientBoundGamePacketHandler(ConnectionManager connectionManager) { public static PacketHandler of(ConnectionManager connectionManager) { return Config.versionReporter().select(PacketHandler.class, + Option.of(Version.V1_20_6, () -> new ClientBoundGamePacketHandler_1_20_6(connectionManager)), Option.of(Version.V1_20_2, () -> new ClientBoundGamePacketHandler_1_20_2(connectionManager)), Option.of(Version.V1_19, () -> new ClientBoundGamePacketHandler_1_19(connectionManager)), Option.of(Version.V1_18, () -> new ClientBoundGamePacketHandler_1_18(connectionManager)), diff --git a/src/main/java/packets/handler/ClientBoundLoginPacketHandler.java b/src/main/java/packets/handler/ClientBoundLoginPacketHandler.java index 6251c643..e91eb170 100644 --- a/src/main/java/packets/handler/ClientBoundLoginPacketHandler.java +++ b/src/main/java/packets/handler/ClientBoundLoginPacketHandler.java @@ -14,20 +14,27 @@ public class ClientBoundLoginPacketHandler extends PacketHandler { public ClientBoundLoginPacketHandler(ConnectionManager connectionManager) { super(connectionManager); - operations.put("disconnect", provider -> { + operations.put("LoginDisconnect", provider -> { String reason = provider.readString(); System.out.println("Disconnect: " + reason); return true; }); - operations.put("encryption_request", provider -> { + + operations.put("Hello", provider -> { String serverId = provider.readString(); byte[] pubKey = provider.readByteArray(provider.readVarInt()); byte[] nonce = provider.readByteArray(provider.readVarInt()); - getConnectionManager().getEncryptionManager().setServerEncryptionRequest(pubKey, nonce, serverId); + if (Config.versionReporter().isAtLeast(Version.V1_20_6)) { + boolean shouldAuthenticate = provider.readBoolean(); + getConnectionManager().getEncryptionManager().setServerEncryptionRequest(pubKey, nonce, serverId, shouldAuthenticate); + } else { + getConnectionManager().getEncryptionManager().setServerEncryptionRequest(pubKey, nonce, serverId); + } + return false; }); - operations.put("login_success", provider -> { + operations.put("GameProfile", provider -> { String uuid = Config.versionReporter().select(String.class, Option.of(Version.V1_16, () -> provider.readUUID().toString()), Option.of(Version.ANY, provider::readString) @@ -36,14 +43,12 @@ public ClientBoundLoginPacketHandler(ConnectionManager connectionManager) { String username = provider.readString(); System.out.println("Login success: " + username + " logged in with uuid " + uuid); - if (Config.versionReporter().isAtLeast(Version.V1_20_2)) { - getConnectionManager().setMode(NetworkMode.CONFIGURATION); - } else { + if (!Config.versionReporter().isAtLeast(Version.V1_20_2)) { getConnectionManager().setMode(NetworkMode.GAME); } return true; }); - operations.put("set_compression", provider -> { + operations.put("LoginCompression", provider -> { int limit = provider.readVarInt(); getConnectionManager().getCompressionManager().enableCompression(limit); return true; diff --git a/src/main/java/packets/handler/PacketHandler.java b/src/main/java/packets/handler/PacketHandler.java index faf1b645..44f5ee15 100644 --- a/src/main/java/packets/handler/PacketHandler.java +++ b/src/main/java/packets/handler/PacketHandler.java @@ -58,6 +58,20 @@ public final boolean handle(int size) { return operator.apply(typeProvider); } + public int indexOf(byte[] outerArray, byte[] smallerArray) { + for(int i = 0; i < outerArray.length - smallerArray.length+1; ++i) { + boolean found = true; + for(int j = 0; j < smallerArray.length; ++j) { + if (outerArray[i+j] != smallerArray[j]) { + found = false; + break; + } + } + if (found) return i; + } + return -1; + } + public abstract Map getOperators(); public abstract boolean isClientBound(); diff --git a/src/main/java/packets/handler/ServerBoundLoginPacketHandler.java b/src/main/java/packets/handler/ServerBoundLoginPacketHandler.java index dc922536..941d4cab 100644 --- a/src/main/java/packets/handler/ServerBoundLoginPacketHandler.java +++ b/src/main/java/packets/handler/ServerBoundLoginPacketHandler.java @@ -2,6 +2,9 @@ import static util.PrintUtils.devPrint; +import config.Config; +import config.Version; +import game.NetworkMode; import java.util.HashMap; import java.util.Map; import proxy.ConnectionManager; @@ -11,7 +14,7 @@ public class ServerBoundLoginPacketHandler extends PacketHandler { public ServerBoundLoginPacketHandler(ConnectionManager connectionManager) { super(connectionManager); - operations.put("login_start", provider -> { + operations.put("Hello", provider -> { String username = provider.readString(); devPrint("Login by: " + username); @@ -20,7 +23,7 @@ public ServerBoundLoginPacketHandler(ConnectionManager connectionManager) { return true; }); - operations.put("encryption_response", provider -> { + operations.put("Key", provider -> { int sharedSecretLength = provider.readVarInt(); byte[] sharedSecret = provider.readByteArray(sharedSecretLength); byte[] nonce = provider.readByteArray(provider.readVarInt()); @@ -28,6 +31,15 @@ public ServerBoundLoginPacketHandler(ConnectionManager connectionManager) { return false; }); + + operations.put("LoginAcknowledged", provider -> { + if (Config.versionReporter().isAtLeast(Version.V1_20_2)) { + getConnectionManager().setMode(NetworkMode.CONFIGURATION); + } else { + getConnectionManager().setMode(NetworkMode.GAME); + } + return true; + }); } @Override diff --git a/src/main/java/packets/handler/version/ClientBoundConfigurationPacketHandler_1_20_2.java b/src/main/java/packets/handler/version/ClientBoundConfigurationPacketHandler_1_20_2.java index 9bb313bc..afaa2f98 100644 --- a/src/main/java/packets/handler/version/ClientBoundConfigurationPacketHandler_1_20_2.java +++ b/src/main/java/packets/handler/version/ClientBoundConfigurationPacketHandler_1_20_2.java @@ -1,7 +1,7 @@ package packets.handler.version; import game.data.WorldManager; -import game.data.dimension.DimensionCodec; +import game.data.dimension.DimensionRegistry; import packets.handler.ClientBoundConfigurationPacketHandler; import packets.handler.PacketOperator; import proxy.ConnectionManager; @@ -18,7 +18,7 @@ public ClientBoundConfigurationPacketHandler_1_20_2(ConnectionManager connection operators.put("RegistryData", provider -> { SpecificTag registryData = provider.readNbtTag(); - worldManager.setDimensionCodec(DimensionCodec.fromNbt(registryData)); + worldManager.setDimensionRegistry(DimensionRegistry.fromNbt(registryData)); return true; }); diff --git a/src/main/java/packets/handler/version/ClientBoundConfigurationPacketHandler_1_20_6.java b/src/main/java/packets/handler/version/ClientBoundConfigurationPacketHandler_1_20_6.java new file mode 100644 index 00000000..cb46d93b --- /dev/null +++ b/src/main/java/packets/handler/version/ClientBoundConfigurationPacketHandler_1_20_6.java @@ -0,0 +1,23 @@ +package packets.handler.version; + +import game.data.WorldManager; +import java.util.Map; +import packets.handler.ClientBoundConfigurationPacketHandler; +import packets.handler.PacketOperator; +import proxy.ConnectionManager; + +public class ClientBoundConfigurationPacketHandler_1_20_6 extends ClientBoundConfigurationPacketHandler { + public ClientBoundConfigurationPacketHandler_1_20_6(ConnectionManager connectionManager) { + super(connectionManager); + + Map operators = getOperators(); + operators.put("RegistryData", provider -> { + var registry = provider.readRegistry(); + switch (registry.name()) { + case "minecraft:worldgen/biome" -> WorldManager.getInstance().getDimensionRegistry().loadBiomes(registry); + case "minecraft:dimension_type" -> WorldManager.getInstance().getDimensionRegistry().loadDimensions(registry); + } + return true; + }); + } +} diff --git a/src/main/java/packets/handler/version/ClientBoundGamePacketHandler_1_16.java b/src/main/java/packets/handler/version/ClientBoundGamePacketHandler_1_16.java index a5426dd3..de7aa53b 100644 --- a/src/main/java/packets/handler/version/ClientBoundGamePacketHandler_1_16.java +++ b/src/main/java/packets/handler/version/ClientBoundGamePacketHandler_1_16.java @@ -5,9 +5,8 @@ import game.data.WorldManager; import game.data.coordinates.Coordinate3D; import game.data.dimension.Dimension; -import game.data.dimension.DimensionCodec; +import game.data.dimension.DimensionRegistry; import game.protocol.Protocol; -import game.protocol.ProtocolVersionHandler; import packets.builder.PacketBuilder; import packets.handler.PacketOperator; import proxy.ConnectionManager; @@ -34,7 +33,10 @@ public ClientBoundGamePacketHandler_1_16(ConnectionManager connectionManager) { String[] dimensionNames = provider.readStringArray(numDimensions); SpecificTag dimensionCodec = provider.readNbtTag(); - WorldManager.getInstance().setDimensionCodec(DimensionCodec.fromNbt(dimensionCodec).setDimensionNames(dimensionNames)); + + DimensionRegistry registry = DimensionRegistry.fromNbt(dimensionCodec); + registry.setDimensionNames(dimensionNames); + WorldManager.getInstance().setDimensionRegistry(registry); SpecificTag dimensionNbt = provider.readNbtTag(); diff --git a/src/main/java/packets/handler/version/ClientBoundGamePacketHandler_1_19.java b/src/main/java/packets/handler/version/ClientBoundGamePacketHandler_1_19.java index f264cfae..d358f895 100644 --- a/src/main/java/packets/handler/version/ClientBoundGamePacketHandler_1_19.java +++ b/src/main/java/packets/handler/version/ClientBoundGamePacketHandler_1_19.java @@ -10,13 +10,14 @@ import config.Config; import game.data.WorldManager; import game.data.dimension.Dimension; -import game.data.dimension.DimensionCodec; +import game.data.dimension.DimensionRegistry; import game.protocol.Protocol; import java.util.Map; import packets.builder.PacketBuilder; import packets.handler.PacketOperator; import proxy.ConnectionManager; import se.llbit.nbt.SpecificTag; +import se.llbit.nbt.Tag; public class ClientBoundGamePacketHandler_1_19 extends ClientBoundGamePacketHandler_1_18 { public ClientBoundGamePacketHandler_1_19(ConnectionManager connectionManager) { @@ -34,8 +35,11 @@ public ClientBoundGamePacketHandler_1_19(ConnectionManager connectionManager) { int numDimensions = provider.readVarInt(); String[] dimensionNames = provider.readStringArray(numDimensions); - SpecificTag dimensionCodec = provider.readNbtTag(); - WorldManager.getInstance().setDimensionCodec(DimensionCodec.fromNbt(dimensionCodec).setDimensionNames(dimensionNames)); + SpecificTag nbt = provider.readNbtTag(); + DimensionRegistry registry = DimensionRegistry.fromNbt(nbt); + registry.setDimensionNames(dimensionNames); + + WorldManager.getInstance().setDimensionRegistry(registry); String dimensionType = provider.readString(); // current active dimension @@ -46,7 +50,7 @@ public ClientBoundGamePacketHandler_1_19(ConnectionManager connectionManager) { replacement.writeVarInt(numDimensions); replacement.writeStringArray(dimensionNames); - replacement.writeNbt(dimensionCodec); + replacement.writeNbt(nbt); replacement.writeString(dimensionType); replacement.writeString(worldName); diff --git a/src/main/java/packets/handler/version/ClientBoundGamePacketHandler_1_20_2.java b/src/main/java/packets/handler/version/ClientBoundGamePacketHandler_1_20_2.java index 4599deb3..cb3fbbe2 100644 --- a/src/main/java/packets/handler/version/ClientBoundGamePacketHandler_1_20_2.java +++ b/src/main/java/packets/handler/version/ClientBoundGamePacketHandler_1_20_2.java @@ -3,14 +3,11 @@ import config.Config; import game.data.WorldManager; import game.data.dimension.Dimension; -import game.data.dimension.DimensionCodec; import game.data.entity.EntityRegistry; import game.protocol.Protocol; import packets.builder.PacketBuilder; import packets.handler.PacketOperator; import proxy.ConnectionManager; -import se.llbit.nbt.CompoundTag; -import se.llbit.nbt.SpecificTag; import java.util.Map; @@ -32,7 +29,7 @@ public ClientBoundGamePacketHandler_1_20_2(ConnectionManager connectionManager) // handle dimension codec int numDimensions = provider.readVarInt(); String[] dimensionNames = provider.readStringArray(numDimensions); - WorldManager.getInstance().getDimensionCodec().setDimensionNames(dimensionNames); + WorldManager.getInstance().getDimensionRegistry().setDimensionNames(dimensionNames); replacement.writeVarInt(numDimensions); replacement.writeStringArray(dimensionNames); @@ -51,6 +48,7 @@ public ClientBoundGamePacketHandler_1_20_2(ConnectionManager connectionManager) Dimension dimension = Dimension.fromString(dimensionName); dimension.setType(dimensionType); WorldManager.getInstance().setDimension(dimension); + WorldManager.getInstance().setDimensionType(WorldManager.getInstance().getDimensionRegistry().getDimensionType(dimensionType)); replacement.writeString(dimensionType); replacement.writeString(dimensionName); @@ -63,7 +61,7 @@ public ClientBoundGamePacketHandler_1_20_2(ConnectionManager connectionManager) return false; }); - operators.put("UpdatePlayerInfo", provider -> { + operators.put("PlayerInfoUpdate", provider -> { entityRegistry.updatePlayerAction(provider); return true; }); diff --git a/src/main/java/packets/handler/version/ClientBoundGamePacketHandler_1_20_6.java b/src/main/java/packets/handler/version/ClientBoundGamePacketHandler_1_20_6.java new file mode 100644 index 00000000..08492032 --- /dev/null +++ b/src/main/java/packets/handler/version/ClientBoundGamePacketHandler_1_20_6.java @@ -0,0 +1,91 @@ +package packets.handler.version; + +import static packets.builder.NetworkType.BOOL; +import static packets.builder.NetworkType.INT; +import static packets.builder.NetworkType.VARINT; + +import config.Config; +import game.NetworkMode; +import game.data.WorldManager; +import game.protocol.Protocol; +import java.util.Arrays; +import java.util.Map; +import packets.DataTypeProvider; +import packets.builder.PacketBuilder; +import packets.handler.PacketOperator; +import proxy.ConnectionManager; + +public class ClientBoundGamePacketHandler_1_20_6 extends ClientBoundGamePacketHandler_1_20_2 { + + private WorldManager world; + + public ClientBoundGamePacketHandler_1_20_6(ConnectionManager connectionManager) { + super(connectionManager); + + Protocol protocol = Config.versionReporter().getProtocol(); + + Map operators = getOperators(); + + operators.put("StartConfiguration", dataTypeProvider -> { + getConnectionManager().setMode(NetworkMode.CONFIGURATION); + return true; + }); + + operators.put("ContainerSetContent", provider -> { + int windowId = provider.readNext(); + + int stateId = provider.readVarInt(); + int count = provider.readVarInt(); + WorldManager.getInstance().getContainerManager().items(windowId, count, provider); + + return true; + }); + + operators.put("Login", provider -> { + PacketBuilder replacement = new PacketBuilder(protocol.clientBound("Login")); + + replacement.copy(provider, INT, BOOL); /* playerId, hardcore */ + + int numLevels = provider.readVarInt(); + String[] levels = provider.readStringArray(numLevels); + + replacement.writeVarInt(numLevels); + replacement.writeStringArray(levels); + + replacement.copy(provider, VARINT); /* maxPlayers */ + + // extend view distance communicated to the client to the given value + int viewDist = provider.readVarInt(); + replacement.writeVarInt(Math.max(viewDist, Config.getExtendedRenderDistance())); + + replacement.copy(provider, VARINT, BOOL, BOOL, BOOL); /* simulationDist, reducedDebug, showDeathScreen, limitedCrafting */ + + commonInfo(provider, replacement); + + getConnectionManager().getEncryptionManager().sendImmediately(replacement); + return false; + }); + + operators.put("Respawn", provider -> { + commonInfo(provider, null); + + return true; + }); + } + + private void commonInfo(DataTypeProvider provider, PacketBuilder replacement) { + // handle dimension codec + int dimensionType = provider.readVarInt(); + String dimensionName = provider.readString(); + + world = WorldManager.getInstance(); + world.setDimension(world.getDimensionRegistry().getDimension(dimensionName)); + world.setDimensionType(world.getDimensionRegistry().getDimensionType(dimensionType)); + + if (replacement != null) { + replacement.writeVarInt(dimensionType); + replacement.writeString(dimensionName); + replacement.copyRemainder(provider); + } + } +} \ No newline at end of file diff --git a/src/main/java/packets/version/DataTypeProvider_1_20_6.java b/src/main/java/packets/version/DataTypeProvider_1_20_6.java new file mode 100644 index 00000000..2674446f --- /dev/null +++ b/src/main/java/packets/version/DataTypeProvider_1_20_6.java @@ -0,0 +1,22 @@ +package packets.version; + +import game.data.container.Slot; + +public class DataTypeProvider_1_20_6 extends DataTypeProvider_1_20_2 { + public DataTypeProvider_1_20_6(byte[] finalFullPacket) { + super(finalFullPacket); + } + + @Override + public Slot readSlot() { + int count = readVarInt(); + + // TODO: handle 1.20.6+ item components + if (count > 0) { + return new Slot(readVarInt(), (byte) count, null); + } + return null; + } + + +} diff --git a/src/main/java/proxy/EncryptionManager.java b/src/main/java/proxy/EncryptionManager.java index de5b32a6..9c46d5d8 100644 --- a/src/main/java/proxy/EncryptionManager.java +++ b/src/main/java/proxy/EncryptionManager.java @@ -3,6 +3,7 @@ import static util.PrintUtils.devPrintFormat; import config.Config; +import config.Version; import java.io.IOException; import java.io.OutputStream; import java.math.BigInteger; @@ -54,6 +55,7 @@ public class EncryptionManager { private final ClientAuthenticator clientAuthenticator; private RSAPublicKey clientProfilePublicKey; + private boolean shouldAuthenticate = true; /* >= 1.20.6 */ { // generate the keypair for the local server @@ -74,6 +76,10 @@ public boolean isEncryptionEnabled() { return encryptionEnabled; } + public void setServerEncryptionRequest(byte[] encoded, byte[] nonce, String serverId, boolean shouldAuthenticate) { + this.shouldAuthenticate = shouldAuthenticate; + setServerEncryptionRequest(encoded, nonce, serverId); + } /** * When the server sends the client an encryption request, this method will be called to get the server's given * public key and call the replacement request sender. @@ -136,6 +142,10 @@ private void sendReplacementEncryptionRequest() { builder.writeVarInt(nonce.length); // verify token len builder.writeByteArray(nonce); // verify token + if (Config.versionReporter().isAtLeast(Version.V1_20_6)) { + builder.writeBoolean(shouldAuthenticate); + } + attempt(() -> streamToClient(builder.build())); } @@ -295,13 +305,15 @@ public static byte[] generateSalt() { * future traffic. */ private void sendReplacementEncryptionConfirmation() { - // authenticate the client so that the remote server will accept us - boolean client = disconnectOnError(() -> clientAuthenticator.makeRequest(generateServerHash())); - if (!client) { return; } - - // verify the connecting client connection is who they claim to be - boolean server = disconnectOnError(() -> new ServerAuthenticator(username).makeRequest(generateServerHash())); - if (!server) { return; } + if (shouldAuthenticate) { + // authenticate the client so that the remote server will accept us + boolean client = disconnectOnError(() -> clientAuthenticator.makeRequest(generateServerHash())); + if (!client) { return; } + + // verify the connecting client connection is who they claim to be + boolean server = disconnectOnError(() -> new ServerAuthenticator(username).makeRequest(generateServerHash())); + if (!server) { return; } + } // encryption confirmation attempt(() -> { diff --git a/src/main/java/proxy/auth/AuthDetailsManager.java b/src/main/java/proxy/auth/AuthDetailsManager.java index e9c3d9ac..480bdea5 100644 --- a/src/main/java/proxy/auth/AuthDetailsManager.java +++ b/src/main/java/proxy/auth/AuthDetailsManager.java @@ -51,7 +51,7 @@ public static void validateAuthStatus(Consumer onSuccess, Consumer - - + + @@ -20,14 +20,17 @@ - + - +