diff --git a/src/main/java/game/data/LevelData.java b/src/main/java/game/data/LevelData.java index a4b52bd4..d220c5c4 100644 --- a/src/main/java/game/data/LevelData.java +++ b/src/main/java/game/data/LevelData.java @@ -6,6 +6,9 @@ import game.data.coordinates.Coordinate3D; import game.data.coordinates.CoordinateDouble3D; import game.data.dimension.Dimension; +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; import se.llbit.nbt.*; import util.NbtUtil; import util.PathUtils; @@ -27,10 +30,18 @@ public class LevelData { private CompoundTag data; private boolean savingBroken; + + private List> levelDataModifiers; + public LevelData(WorldManager worldManager) { this.worldManager = worldManager; this.outputDir = PathUtils.toPath(Config.getWorldOutputDir()); this.file = Paths.get(outputDir.toString(), "level.dat").toFile(); + this.levelDataModifiers = new ArrayList<>(); + } + + public void registerModifier(Consumer fn) { + levelDataModifiers.add(fn); } public CoordinateDouble3D getPlayerPosition() { @@ -162,6 +173,9 @@ public void save() throws IOException { enableWorldGeneration(data); } + // check for modifiers + levelDataModifiers.forEach(fn -> fn.accept(root)); + // write the file NbtUtil.write(root, file.toPath()); } diff --git a/src/main/java/game/data/WorldManager.java b/src/main/java/game/data/WorldManager.java index 87aec3dd..2e606f8a 100644 --- a/src/main/java/game/data/WorldManager.java +++ b/src/main/java/game/data/WorldManager.java @@ -22,6 +22,7 @@ import java.util.concurrent.TimeUnit; import java.util.function.BiConsumer; +import java.util.function.Consumer; import org.apache.commons.io.FileUtils; import org.apache.commons.io.IOUtils; @@ -52,6 +53,8 @@ import packets.DataTypeProvider; import packets.builder.PacketBuilder; import proxy.PacketInjector; +import se.llbit.nbt.CompoundTag; +import se.llbit.nbt.Tag; import util.PathUtils; /** @@ -105,6 +108,10 @@ public WorldManager() { this.renderDistanceExtender = new RenderDistanceExtender(this); } + public void registerLevelDataModifier(Consumer fn) { + this.levelData.registerModifier(fn); + } + public static WorldManager getInstance() { if (instance == null) { instance = new WorldManager(); diff --git a/src/main/java/game/data/chunk/Chunk.java b/src/main/java/game/data/chunk/Chunk.java index 7a612c5b..9c3d7702 100644 --- a/src/main/java/game/data/chunk/Chunk.java +++ b/src/main/java/game/data/chunk/Chunk.java @@ -11,6 +11,8 @@ import game.data.coordinates.CoordinateDim2D; import game.data.dimension.Dimension; import game.protocol.Protocol; +import java.util.function.BiConsumer; +import java.util.function.Consumer; import packets.DataTypeProvider; import packets.builder.PacketBuilder; import se.llbit.nbt.*; @@ -22,6 +24,11 @@ * Basic chunk class. May be extended by version-specific ones as they can have implementation differences. */ public abstract class Chunk extends ChunkEntities { + private static final List> chunkNbtModifiers = new ArrayList<>(); + public static void registerNbtModifier(BiConsumer fn) { + chunkNbtModifiers.add(fn); + } + public static final int SECTION_HEIGHT = 16; public static final int SECTION_WIDTH = 16; protected static final int LIGHT_SIZE = 2048; @@ -207,6 +214,8 @@ public NamedTag toNbt() { root.add("Level", createNbtLevel()); root.add("DataVersion", new IntTag(getDataVersion())); + chunkNbtModifiers.forEach(fn -> fn.accept(this, root)); + return new NamedTag("", root); } diff --git a/src/main/java/game/data/chunk/palette/BlockState.java b/src/main/java/game/data/chunk/palette/BlockState.java index b14ec704..33001828 100644 --- a/src/main/java/game/data/chunk/palette/BlockState.java +++ b/src/main/java/game/data/chunk/palette/BlockState.java @@ -48,6 +48,10 @@ public String getProperty(String name) { return properties.get(name).stringValue(); } + CompoundTag getProperties() { + return properties; + } + public boolean isChest() { return name.equals("minecraft:chest") || name.equals("minecraft:trapped_chest"); } @@ -114,7 +118,7 @@ public String toString() { return "BlockState{" + "name='" + name + '\'' + ", id=" + id + - ", properties=" + properties + +// ", properties=" + properties + '}'; } } diff --git a/src/main/java/game/data/chunk/palette/GlobalPalette.java b/src/main/java/game/data/chunk/palette/GlobalPalette.java index 579b5035..2e9f5bfd 100644 --- a/src/main/java/game/data/chunk/palette/GlobalPalette.java +++ b/src/main/java/game/data/chunk/palette/GlobalPalette.java @@ -45,9 +45,7 @@ public GlobalPalette(InputStream input) { CompoundTag properties = state.getProperties(); - BlockState s = new BlockState(name, state.id, properties); - states.put(state.id, s); - nameStates.put(new BlockStateIdentifier(name, properties), s); + addBlockState(new BlockState(name, state.id, properties)); })); } @@ -84,6 +82,16 @@ public int getStateId(SpecificTag nbt) { } 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 { diff --git a/src/main/java/game/data/chunk/palette/GlobalPaletteProvider.java b/src/main/java/game/data/chunk/palette/GlobalPaletteProvider.java index 96901a71..35c08c18 100644 --- a/src/main/java/game/data/chunk/palette/GlobalPaletteProvider.java +++ b/src/main/java/game/data/chunk/palette/GlobalPaletteProvider.java @@ -1,12 +1,14 @@ package game.data.chunk.palette; import config.Config; -import game.protocol.ProtocolVersionHandler; import game.data.registries.RegistryLoader; import game.protocol.Protocol; - +import game.protocol.ProtocolVersionHandler; import java.io.IOException; import java.util.HashMap; +import java.util.Queue; +import java.util.concurrent.ConcurrentLinkedDeque; +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 @@ -16,16 +18,19 @@ public final class GlobalPaletteProvider { private GlobalPaletteProvider() { } 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 * created through requestPalette. */ public static GlobalPalette getGlobalPalette(int dataVersion) { - if (palettes.containsKey(dataVersion)) { - return palettes.get(dataVersion); + GlobalPalette palette = palettes.get(dataVersion); + + if (palette == null) { + return requestPalette(dataVersion); } - return requestPalette(dataVersion); + return palette; } /** @@ -45,10 +50,36 @@ private static GlobalPalette requestPalette(int dataVersion) { try { GlobalPalette p = RegistryLoader.forVersion(version.getVersion()).generateGlobalPalette(); palettes.put(dataVersion, p); + + if (uninitialised != null) { + while (!uninitialised.isEmpty()) { + p.addBlockState(uninitialised.remove()); + } + } return p; } catch (IOException e) { e.printStackTrace(); return null; } } + + public static void registerBlock(String name, int id) { + GlobalPalette palette = palettes.get(Config.getDataVersion()); + + BlockState state = new BlockState(name, id, new CompoundTag()); + + if (palette == null) { + enqueueBlock(state); + return; + } + + palette.addBlockState(state); + } + + private static void enqueueBlock(BlockState state) { + if (uninitialised == null) { + uninitialised = new ConcurrentLinkedDeque<>(); + } + uninitialised.add(state); + } } diff --git a/src/main/java/game/data/chunk/version/ChunkSection_1_12.java b/src/main/java/game/data/chunk/version/ChunkSection_1_12.java index 6fe9cc45..94b73552 100644 --- a/src/main/java/game/data/chunk/version/ChunkSection_1_12.java +++ b/src/main/java/game/data/chunk/version/ChunkSection_1_12.java @@ -9,6 +9,8 @@ import game.data.chunk.palette.PaletteBuilder; import game.data.coordinates.Coordinate3D; import game.data.dimension.Dimension; +import java.util.Optional; +import javafx.util.Pair; import packets.builder.PacketBuilder; import se.llbit.nbt.ByteArrayTag; import se.llbit.nbt.CompoundTag; @@ -59,21 +61,41 @@ public void setBlocks(long[] blocks) { @Override protected void addNbtTags(CompoundTag map) { - map.add("Blocks", new ByteArrayTag(getBlockIds())); + Pair> blocks = getBlockIds(); + + map.add("Blocks", new ByteArrayTag(blocks.getKey())); map.add("Data", new ByteArrayTag(getBlockStates())); + + if (blocks.getValue().isPresent()) { + map.add("Add", new ByteArrayTag(blocks.getValue().get())); + } } - private byte[] getBlockIds() { + private Pair> getBlockIds() { byte[] blockData = new byte[4096]; + byte[] additional = new byte[2048]; + boolean hasAdditionalData = false; for (int x = 0; x < 16; x++) { for (int y = 0; y < 16; y++) { for (int z = 0; z < 16; z++) { - blockData[getBlockIndex(x, y, z)] = (byte) (blockStates[x][y][z] >>> 4); + int blockState = blockStates[x][y][z] >>> 4; + int index = getBlockIndex(x, y, z); + blockData[index] = (byte) (blockState); + + // excess data is packed into half a byte per block, for Forge + int excess = blockState >>> 8; + if (excess > 0) { + hasAdditionalData = true; + + // if index is odd, shift by 4, otherwise leave unchanged + additional[index / 2] |= (byte) (excess << ((index % 2) * 4));; + } } } } - return blockData; + + return new Pair<>(blockData, hasAdditionalData ? Optional.of(additional) : Optional.empty()); } private byte[] getBlockStates() { diff --git a/src/main/java/game/data/registries/modded/ForgeRegistryHandler.java b/src/main/java/game/data/registries/modded/ForgeRegistryHandler.java new file mode 100644 index 00000000..5a9ad6cd --- /dev/null +++ b/src/main/java/game/data/registries/modded/ForgeRegistryHandler.java @@ -0,0 +1,156 @@ +package game.data.registries.modded; + +import game.data.WorldManager; +import game.data.chunk.Chunk; +import game.data.chunk.palette.GlobalPaletteProvider; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import javafx.util.Pair; +import packets.DataTypeProvider; +import packets.handler.PacketOperator; +import se.llbit.nbt.CompoundTag; +import se.llbit.nbt.IntTag; +import se.llbit.nbt.ListTag; +import se.llbit.nbt.NamedTag; +import se.llbit.nbt.StringTag; +import se.llbit.nbt.Tag; + +/** + * Handle plugin channel messages for Forge (at least on 1.12.2) + */ +public class ForgeRegistryHandler implements PacketOperator { + private static final byte TYPE_MODLIST = 0x02; + private static final byte TYPE_REGISTRY = 0x03; + + private final Map registries; + private final List> modList; + + public ForgeRegistryHandler() { + this.registries = new HashMap<>(); + this.modList = new ArrayList<>(); + + // add ForgeDataVersion to chunks, not sure if this does anything + Chunk.registerNbtModifier((chunk, tag) -> { + CompoundTag forgeData = new CompoundTag(); + forgeData.add("DataVersion", new IntTag(chunk.getDataVersion())); + + tag.asCompound().add("ForgeDataVersion", forgeData); + }); + + // include mod list and registries in the level.dat file + WorldManager.getInstance().registerLevelDataModifier(tag -> { + CompoundTag root = tag.asCompound(); + + CompoundTag fml = new CompoundTag(); + fml.add("Registries", new CompoundTag( + registries.entrySet().stream() + .map((e) -> new NamedTag(e.getKey(), e.getValue().toNbt().asCompound())) + .toList() + )); + fml.add("ModList", new ListTag(Tag.TAG_COMPOUND, modList.stream().map(pair -> new CompoundTag(List.of( + new NamedTag("ModId", new StringTag(pair.getKey())), + new NamedTag("ModVersion", new StringTag(pair.getValue()))))).toList())); + root.add("FML", fml); + }); + } + + @Override + public Boolean apply(DataTypeProvider provider) { + byte type = provider.readNext(); + + switch (type) { + case TYPE_MODLIST -> handleModList(provider); + case TYPE_REGISTRY -> handleRegistry(provider); + } + + return true; + } + + private void handleRegistry(DataTypeProvider provider) { + // true if another registry will follow, not sure what the point of this is + provider.readBoolean(); + + String name = provider.readString(); + + ForgeRegistry registry = registries.computeIfAbsent(name, (n) -> new ForgeRegistry()); + + int numIds = provider.readVarInt(); + for (int i = 0; i < numIds; i++) { + registry.addId(provider.readString(), provider.readVarInt()); + } + + if (name.equals("minecraft:blocks")) { + registry.registerBlocks(); + } + + int numAliases = provider.readVarInt(); + for (int i = 0; i < numAliases; i++) { + registry.addAlias(provider.readString()); + } + + if (!provider.hasNext()) { + return; + } + + int numDummied = provider.readVarInt(); + for (int i = 0; i < numDummied; i++) { + registry.addDummied(provider.readString()); + } + } + + private void handleModList(DataTypeProvider provider) { + int numMods = provider.readVarInt(); + for (int i = 0; i < numMods; i++) { + modList.add(new Pair<>(provider.readString(), provider.readString())); + } + } +} + +class ForgeRegistry { + List aliases; + List dummied; + List> ids; + + public ForgeRegistry() { + this.aliases = new ArrayList<>(); + this.dummied = new ArrayList<>(); + this.ids = new ArrayList<>(); + } + + public void addId(String name, int id) { + ids.add(new Pair<>(name, id)); + } + + public void addDummied(String name) { + dummied.add(name); + } + + public void addAlias(String name) { + aliases.add(name); + } + + public Tag toNbt() { + CompoundTag tag = new CompoundTag(); + tag.add("aliases", new ListTag(Tag.TAG_STRING, aliases.stream().map(StringTag::new).toList())); + tag.add("dummied", new ListTag(Tag.TAG_STRING, dummied.stream().map(StringTag::new).toList())); + tag.add("ids", new ListTag(Tag.TAG_COMPOUND, ids.stream().map(pair -> { + CompoundTag id = new CompoundTag(); + id.add("K", new StringTag(pair.getKey())); + id.add("V", new IntTag(pair.getValue())); + return id; + }).toList())); + + return tag; + } + + /** + * Add blocks to the global palette + */ + 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) + ids.forEach(pair -> GlobalPaletteProvider.registerBlock(pair.getKey(), pair.getValue() << 4)); + } +} diff --git a/src/main/java/packets/handler/ClientBoundGamePacketHandler.java b/src/main/java/packets/handler/ClientBoundGamePacketHandler.java index 002ff135..c55f08d5 100644 --- a/src/main/java/packets/handler/ClientBoundGamePacketHandler.java +++ b/src/main/java/packets/handler/ClientBoundGamePacketHandler.java @@ -19,6 +19,7 @@ import packets.handler.version.ClientBoundGamePacketHandler_1_17; import packets.handler.version.ClientBoundGamePacketHandler_1_18; import packets.handler.version.ClientBoundGamePacketHandler_1_19; +import packets.handler.plugins.PluginChannelHandler; import proxy.ConnectionManager; import se.llbit.nbt.SpecificTag; @@ -167,9 +168,9 @@ public ClientBoundGamePacketHandler(ConnectionManager connectionManager) { return true; }); - operations.put("update_view_distance", provider -> { - System.out.println("Server tried to change view distance to " + provider.readVarInt()); - return false; + operations.put("CustomPayload", provider -> { + PluginChannelHandler.getInstance().handleCustomPayload(provider); + return true; }); } diff --git a/src/main/java/packets/handler/plugins/PluginChannelHandler.java b/src/main/java/packets/handler/plugins/PluginChannelHandler.java new file mode 100644 index 00000000..23e46d1c --- /dev/null +++ b/src/main/java/packets/handler/plugins/PluginChannelHandler.java @@ -0,0 +1,28 @@ +package packets.handler.plugins; + +import config.Config; +import config.Option; +import config.Version; +import packets.DataTypeProvider; + +public abstract class PluginChannelHandler { + + private static PluginChannelHandler instance; + + public static PluginChannelHandler getInstance() { + if (instance == null) { + instance = Config.versionReporter().select(PluginChannelHandler.class, + Option.of(Version.V1_12, PluginChannelHandler1_12::new), + Option.of(Version.ANY, DefaultPluginChannelHandler::new) + ); + } + return instance; + } + + public abstract void handleCustomPayload(DataTypeProvider provider); +} + +class DefaultPluginChannelHandler extends PluginChannelHandler { + @Override + public void handleCustomPayload(DataTypeProvider provider) { } +} diff --git a/src/main/java/packets/handler/plugins/PluginChannelHandler1_12.java b/src/main/java/packets/handler/plugins/PluginChannelHandler1_12.java new file mode 100644 index 00000000..4084fdae --- /dev/null +++ b/src/main/java/packets/handler/plugins/PluginChannelHandler1_12.java @@ -0,0 +1,25 @@ +package packets.handler.plugins; + +import java.util.HashMap; +import java.util.Map; +import packets.DataTypeProvider; +import packets.handler.PacketOperator; +import game.data.registries.modded.ForgeRegistryHandler; + +public class PluginChannelHandler1_12 extends PluginChannelHandler { + private final static String FORGE_CHANNEL = "FML|HS"; + private final Map operators; + + public PluginChannelHandler1_12() { + this.operators = new HashMap<>(); + } + + @Override + public void handleCustomPayload(DataTypeProvider provider) { + String channel = provider.readString(); + + if (channel.equals(FORGE_CHANNEL)) { + this.operators.computeIfAbsent(channel, s -> new ForgeRegistryHandler()).apply(provider); + } + } +} diff --git a/src/main/resources/protocol-versions.json b/src/main/resources/protocol-versions.json index ad25cd5f..619ddae4 100644 --- a/src/main/resources/protocol-versions.json +++ b/src/main/resources/protocol-versions.json @@ -23,7 +23,8 @@ "0x3F": "SetEquipment", "0x26": "MoveEntityPos", "0x4C": "TeleportEntity", - "0x32": "RemoveEntities" + "0x32": "RemoveEntities", + "0x18": "CustomPayload" }, "serverBound": { "0x0D": "MovePlayerPos",