diff --git a/common/src/main/java/com/viaversion/viarewind/protocol/v1_9to1_8/Protocol1_9To1_8.java b/common/src/main/java/com/viaversion/viarewind/protocol/v1_9to1_8/Protocol1_9To1_8.java index b4140ccbf..34b6342d5 100644 --- a/common/src/main/java/com/viaversion/viarewind/protocol/v1_9to1_8/Protocol1_9To1_8.java +++ b/common/src/main/java/com/viaversion/viarewind/protocol/v1_9to1_8/Protocol1_9To1_8.java @@ -26,6 +26,7 @@ import com.viaversion.viarewind.protocol.v1_9to1_8.rewriter.WorldPacketRewriter1_9; import com.viaversion.viarewind.protocol.v1_9to1_8.storage.BlockPlaceDestroyTracker; import com.viaversion.viarewind.protocol.v1_9to1_8.storage.BossBarStorage; +import com.viaversion.viarewind.protocol.v1_9to1_8.storage.CommandBlockStateStorage; import com.viaversion.viarewind.protocol.v1_9to1_8.storage.CooldownStorage; import com.viaversion.viarewind.protocol.v1_9to1_8.storage.EntityTracker1_9; import com.viaversion.viarewind.protocol.v1_9to1_8.storage.LevitationStorage; @@ -89,6 +90,7 @@ public void init(UserConnection connection) { connection.put(new CooldownStorage()); connection.put(new BlockPlaceDestroyTracker()); connection.put(new BossBarStorage(connection)); + connection.put(new CommandBlockStateStorage()); } @Override diff --git a/common/src/main/java/com/viaversion/viarewind/protocol/v1_9to1_8/data/CommandBlockState.java b/common/src/main/java/com/viaversion/viarewind/protocol/v1_9to1_8/data/CommandBlockState.java new file mode 100644 index 000000000..7b7aadca0 --- /dev/null +++ b/common/src/main/java/com/viaversion/viarewind/protocol/v1_9to1_8/data/CommandBlockState.java @@ -0,0 +1,165 @@ +/* + * This file is part of ViaRewind - https://github.com/ViaVersion/ViaRewind + * Copyright (C) 2018-2026 ViaVersion and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.viaversion.viarewind.protocol.v1_9to1_8.data; + +import com.viaversion.nbt.tag.CompoundTag; +import com.viaversion.nbt.tag.NumberTag; +import com.viaversion.nbt.tag.StringTag; + +public final class CommandBlockState { + + public static final int COMMAND_BLOCK_COMMAND_LIMIT = 32500; + + private static final String IMPULSE = "IMPULSE"; + private static final String REPEAT = "REPEAT"; + private static final String CHAIN = "CHAIN"; + private static final String CONDITIONAL = "CONDITIONAL"; + private static final String UNCONDITIONAL = "UNCONDITIONAL"; + private static final String ALWAYS_ACTIVE = "ALWAYSACTIVE"; + private static final String REDSTONE = "REDSTONE"; + + private CommandBlockState() { + } + + public static boolean isCommandBlock(final int blockState) { + return isImpulse(blockState) || isRepeat(blockState) || isChain(blockState); + } + + public static void decorateCommand(final CompoundTag tag, final int blockState) { + if (tag == null || !isCommandBlock(blockState)) { + return; + } + + final StringTag commandTag = tag.getStringTag("Command"); + if (commandTag == null) { + return; + } + + final Mode mode = mode(blockState); + final boolean conditional = (blockState & 8) != 0; + final boolean automatic = booleanTag(tag, "auto"); + if (mode == Mode.REDSTONE && !conditional && !automatic) { + return; + } + + final String prefix = mode.prefix + ' ' + (conditional ? CONDITIONAL : UNCONDITIONAL) + ' ' + (automatic ? ALWAYS_ACTIVE : REDSTONE) + ' '; + final String command = commandTag.getValue(); + if (!parseDecoratedCommand(command).prefixed() && prefix.length() + command.length() <= COMMAND_BLOCK_COMMAND_LIMIT) { + commandTag.setValue(prefix + command); + } + } + + public static ParsedCommand parseDecoratedCommand(final String command) { + final int first = command.indexOf(' '); + if (first == -1) { + return ParsedCommand.defaultState(command); + } + final int second = command.indexOf(' ', first + 1); + if (second == -1) { + return ParsedCommand.defaultState(command); + } + final int third = command.indexOf(' ', second + 1); + if (third == -1) { + return ParsedCommand.defaultState(command); + } + + final Mode mode = Mode.fromPrefix(command.substring(0, first)); + if (mode == null) { + return ParsedCommand.defaultState(command); + } + + final String conditionalToken = command.substring(first + 1, second); + final boolean conditional; + if (CONDITIONAL.equalsIgnoreCase(conditionalToken)) { + conditional = true; + } else if (UNCONDITIONAL.equalsIgnoreCase(conditionalToken)) { + conditional = false; + } else { + return ParsedCommand.defaultState(command); + } + + final String automaticToken = command.substring(second + 1, third); + final boolean automatic; + if (ALWAYS_ACTIVE.equalsIgnoreCase(automaticToken)) { + automatic = true; + } else if (REDSTONE.equalsIgnoreCase(automaticToken)) { + automatic = false; + } else { + return ParsedCommand.defaultState(command); + } + + return new ParsedCommand(command.substring(third + 1), mode.serverName, conditional, automatic, true); + } + + private static boolean booleanTag(final CompoundTag tag, final String key) { + final NumberTag numberTag = tag.getNumberTag(key); + return numberTag != null && numberTag.asByte() != 0; + } + + private static Mode mode(final int blockState) { + if (isRepeat(blockState)) { + return Mode.AUTO; + } + if (isChain(blockState)) { + return Mode.SEQUENCE; + } + return Mode.REDSTONE; + } + + private static boolean isImpulse(final int blockState) { + return blockState >= 2192 && blockState <= 2207; + } + + private static boolean isRepeat(final int blockState) { + return blockState >= 3360 && blockState <= 3375; + } + + private static boolean isChain(final int blockState) { + return blockState >= 3376 && blockState <= 3391; + } + + public record ParsedCommand(String command, String mode, boolean conditional, boolean automatic, boolean prefixed) { + + private static ParsedCommand defaultState(final String command) { + return new ParsedCommand(command, Mode.REDSTONE.serverName, false, false, false); + } + } + + private enum Mode { + REDSTONE(IMPULSE, "REDSTONE"), + AUTO(REPEAT, "AUTO"), + SEQUENCE(CHAIN, "SEQUENCE"); + + private final String prefix; + private final String serverName; + + Mode(final String prefix, final String serverName) { + this.prefix = prefix; + this.serverName = serverName; + } + + private static Mode fromPrefix(final String prefix) { + for (final Mode mode : values()) { + if (mode.prefix.equalsIgnoreCase(prefix)) { + return mode; + } + } + return null; + } + } +} diff --git a/common/src/main/java/com/viaversion/viarewind/protocol/v1_9to1_8/rewriter/BlockItemPacketRewriter1_9.java b/common/src/main/java/com/viaversion/viarewind/protocol/v1_9to1_8/rewriter/BlockItemPacketRewriter1_9.java index af91a19a3..9d486ec2a 100644 --- a/common/src/main/java/com/viaversion/viarewind/protocol/v1_9to1_8/rewriter/BlockItemPacketRewriter1_9.java +++ b/common/src/main/java/com/viaversion/viarewind/protocol/v1_9to1_8/rewriter/BlockItemPacketRewriter1_9.java @@ -26,8 +26,11 @@ import com.viaversion.viarewind.api.rewriter.VRBlockItemRewriter; import com.viaversion.viarewind.protocol.v1_9to1_8.Protocol1_9To1_8; import com.viaversion.viarewind.protocol.v1_9to1_8.data.PotionIdMappings1_8; +import com.viaversion.viarewind.protocol.v1_9to1_8.storage.CommandBlockStateStorage; import com.viaversion.viarewind.protocol.v1_9to1_8.storage.WindowTracker; import com.viaversion.viaversion.api.connection.UserConnection; +import com.viaversion.viaversion.api.minecraft.BlockChangeRecord; +import com.viaversion.viaversion.api.minecraft.BlockPosition; import com.viaversion.viaversion.api.minecraft.item.Item; import com.viaversion.viaversion.api.protocol.remapper.PacketHandlers; import com.viaversion.viaversion.api.type.Types; @@ -59,8 +62,8 @@ public BlockItemPacketRewriter1_9(Protocol1_9To1_8 protocol) { @Override protected void registerPackets() { - registerBlockChange(ClientboundPackets1_9.BLOCK_UPDATE); - registerMultiBlockChange(ClientboundPackets1_9.CHUNK_BLOCKS_UPDATE); + registerBlockChangeWithCommandBlockStorage(ClientboundPackets1_9.BLOCK_UPDATE); + registerMultiBlockChangeWithCommandBlockStorage(ClientboundPackets1_9.CHUNK_BLOCKS_UPDATE); registerSetCreativeModeSlot(ServerboundPackets1_8.SET_CREATIVE_MODE_SLOT); protocol.registerClientbound(ClientboundPackets1_9.CONTAINER_CLOSE, wrapper -> { @@ -184,6 +187,46 @@ public void register() { }); } + private void registerBlockChangeWithCommandBlockStorage(final ClientboundPackets1_9 packetType) { + protocol.registerClientbound(packetType, new PacketHandlers() { + @Override + public void register() { + map(Types.BLOCK_POSITION1_8); // Block Position + map(Types.VAR_INT); // Block + + handler(wrapper -> { + final int blockState = wrapper.get(Types.VAR_INT, 0); + wrapper.user().get(CommandBlockStateStorage.class).storeOrRemove(wrapper.get(Types.BLOCK_POSITION1_8, 0), blockState); + wrapper.set(Types.VAR_INT, 0, handleBlockId(blockState)); + }); + } + }); + } + + private void registerMultiBlockChangeWithCommandBlockStorage(final ClientboundPackets1_9 packetType) { + protocol.registerClientbound(packetType, new PacketHandlers() { + @Override + public void register() { + map(Types.INT); // Chunk X + map(Types.INT); // Chunk Z + map(Types.BLOCK_CHANGE_ARRAY); + + handler(wrapper -> { + final int chunkX = wrapper.get(Types.INT, 0); + final int chunkZ = wrapper.get(Types.INT, 1); + final CommandBlockStateStorage storage = wrapper.user().get(CommandBlockStateStorage.class); + + for (BlockChangeRecord record : wrapper.get(Types.BLOCK_CHANGE_ARRAY, 0)) { + final int blockState = record.getBlockId(); + final BlockPosition position = new BlockPosition((chunkX << 4) + record.getSectionX(), record.getY(), (chunkZ << 4) + record.getSectionZ()); + storage.storeOrRemove(position, blockState); + record.setBlockId(handleBlockId(blockState)); + } + }); + } + }); + } + @Override protected void registerRewrites() { enchantmentRewriter = new LegacyEnchantmentRewriter(nbtTagName()); diff --git a/common/src/main/java/com/viaversion/viarewind/protocol/v1_9to1_8/rewriter/PlayerPacketRewriter1_9.java b/common/src/main/java/com/viaversion/viarewind/protocol/v1_9to1_8/rewriter/PlayerPacketRewriter1_9.java index 2bc10570c..8413394df 100644 --- a/common/src/main/java/com/viaversion/viarewind/protocol/v1_9to1_8/rewriter/PlayerPacketRewriter1_9.java +++ b/common/src/main/java/com/viaversion/viarewind/protocol/v1_9to1_8/rewriter/PlayerPacketRewriter1_9.java @@ -23,6 +23,7 @@ import com.viaversion.viarewind.ViaRewind; import com.viaversion.viarewind.api.type.RewindTypes; import com.viaversion.viarewind.protocol.v1_9to1_8.Protocol1_9To1_8; +import com.viaversion.viarewind.protocol.v1_9to1_8.data.CommandBlockState; import com.viaversion.viarewind.protocol.v1_9to1_8.provider.InventoryProvider; import com.viaversion.viarewind.protocol.v1_9to1_8.storage.BlockPlaceDestroyTracker; import com.viaversion.viarewind.protocol.v1_9to1_8.storage.BossBarStorage; @@ -524,7 +525,39 @@ public void register() { pageTag.setValue(ChatUtil.jsonToLegacy(value)); } } else if (channel.equals("MC|AdvCdm")) { - wrapper.set(Types.STRING, 0, "MC|AdvCmd"); + final byte type = wrapper.read(Types.BYTE); + if (type == 0) { + final int x = wrapper.read(Types.INT); + final int y = wrapper.read(Types.INT); + final int z = wrapper.read(Types.INT); + final CommandBlockState.ParsedCommand command = CommandBlockState.parseDecoratedCommand(wrapper.read(Types.STRING)); + final boolean trackOutput = wrapper.read(Types.BOOLEAN); + if (command.prefixed()) { + wrapper.set(Types.STRING, 0, "MC|AutoCmd"); + wrapper.write(Types.INT, x); + wrapper.write(Types.INT, y); + wrapper.write(Types.INT, z); + wrapper.write(Types.STRING, command.command()); + wrapper.write(Types.BOOLEAN, trackOutput); + wrapper.write(Types.STRING, command.mode()); + wrapper.write(Types.BOOLEAN, command.conditional()); + wrapper.write(Types.BOOLEAN, command.automatic()); + } else { + wrapper.set(Types.STRING, 0, "MC|AdvCmd"); + wrapper.write(Types.BYTE, type); + wrapper.write(Types.INT, x); + wrapper.write(Types.INT, y); + wrapper.write(Types.INT, z); + wrapper.write(Types.STRING, command.command()); + wrapper.write(Types.BOOLEAN, trackOutput); + } + } else { + wrapper.set(Types.STRING, 0, "MC|AdvCmd"); + wrapper.write(Types.BYTE, type); + wrapper.passthrough(Types.INT); // Entity id + wrapper.passthrough(Types.STRING); // Command + wrapper.passthrough(Types.BOOLEAN); // Track output + } } }); } diff --git a/common/src/main/java/com/viaversion/viarewind/protocol/v1_9to1_8/rewriter/WorldPacketRewriter1_9.java b/common/src/main/java/com/viaversion/viarewind/protocol/v1_9to1_8/rewriter/WorldPacketRewriter1_9.java index e476d99f5..14960ccaa 100644 --- a/common/src/main/java/com/viaversion/viarewind/protocol/v1_9to1_8/rewriter/WorldPacketRewriter1_9.java +++ b/common/src/main/java/com/viaversion/viarewind/protocol/v1_9to1_8/rewriter/WorldPacketRewriter1_9.java @@ -22,7 +22,9 @@ import com.viaversion.nbt.tag.Tag; import com.viaversion.viarewind.ViaRewind; import com.viaversion.viarewind.protocol.v1_9to1_8.Protocol1_9To1_8; +import com.viaversion.viarewind.protocol.v1_9to1_8.data.CommandBlockState; import com.viaversion.viarewind.protocol.v1_9to1_8.data.EffectIdMappings1_8; +import com.viaversion.viarewind.protocol.v1_9to1_8.storage.CommandBlockStateStorage; import com.viaversion.viaversion.api.minecraft.BlockPosition; import com.viaversion.viaversion.api.minecraft.Environment; import com.viaversion.viaversion.api.minecraft.chunks.BaseChunk; @@ -57,6 +59,12 @@ public void register() { map(Types.NAMED_COMPOUND_TAG); // Tag handler(wrapper -> { final CompoundTag tag = wrapper.get(Types.NAMED_COMPOUND_TAG, 0); + final short action = wrapper.get(Types.UNSIGNED_BYTE, 0); + + if (action == 2) { + final BlockPosition position = wrapper.get(Types.BLOCK_POSITION1_8, 0); + CommandBlockState.decorateCommand(tag, wrapper.user().get(CommandBlockStateStorage.class).state(position)); + } if (tag.remove("SpawnData") instanceof CompoundTag spawnData) { final Tag id = spawnData.remove("id"); @@ -111,9 +119,11 @@ public void register() { public void register() { handler(wrapper -> { final Environment environment = wrapper.user().getClientWorld(Protocol1_9To1_8.class).getEnvironment(); + final CommandBlockStateStorage commandBlockStates = wrapper.user().get(CommandBlockStateStorage.class); final int chunkX = wrapper.read(Types.INT); final int chunkZ = wrapper.read(Types.INT); + commandBlockStates.unloadChunk(chunkX, chunkZ); wrapper.write(ChunkType1_8.forEnvironment(environment), new BaseChunk(chunkX, chunkZ, true, false, 0, new ChunkSection[16], null, new ArrayList<>())); }); @@ -125,8 +135,23 @@ public void register() { public void register() { handler(wrapper -> { final Environment environment = wrapper.user().getClientWorld(Protocol1_9To1_8.class).getEnvironment(); + final CommandBlockStateStorage commandBlockStates = wrapper.user().get(CommandBlockStateStorage.class); Chunk chunk = wrapper.read(ChunkType1_9_1.forEnvironment(environment)); + final Chunk originalChunk = chunk; + + chunk.getBlockEntities().forEach(nbt -> { + if (!nbt.contains("x") || !nbt.contains("y") || !nbt.contains("z")) { + return; + } + + final BlockPosition position = new BlockPosition((int) nbt.get("x").getValue(), (int) nbt.get("y").getValue(), (int) nbt.get("z").getValue()); + final int blockState = blockStateAt(originalChunk, position); + if (CommandBlockState.isCommandBlock(blockState)) { + commandBlockStates.storeOrRemove(position, blockState); + CommandBlockState.decorateCommand(nbt, blockState); + } + }); for (ChunkSection section : chunk.getSections()) { if (section == null) continue; @@ -267,4 +292,18 @@ public void register() { } }); } + + private static int blockStateAt(final Chunk chunk, final BlockPosition position) { + final int sectionY = position.y() >> 4; + if (sectionY < 0 || sectionY >= chunk.getSections().length) { + return -1; + } + + final ChunkSection section = chunk.getSections()[sectionY]; + if (section == null) { + return -1; + } + + return section.palette(PaletteType.BLOCKS).idAt(position.x() & 0xF, position.y() & 0xF, position.z() & 0xF); + } } diff --git a/common/src/main/java/com/viaversion/viarewind/protocol/v1_9to1_8/storage/CommandBlockStateStorage.java b/common/src/main/java/com/viaversion/viarewind/protocol/v1_9to1_8/storage/CommandBlockStateStorage.java new file mode 100644 index 000000000..53a7a1d50 --- /dev/null +++ b/common/src/main/java/com/viaversion/viarewind/protocol/v1_9to1_8/storage/CommandBlockStateStorage.java @@ -0,0 +1,52 @@ +/* + * This file is part of ViaRewind - https://github.com/ViaVersion/ViaRewind + * Copyright (C) 2018-2026 ViaVersion and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.viaversion.viarewind.protocol.v1_9to1_8.storage; + +import com.viaversion.viarewind.protocol.v1_9to1_8.data.CommandBlockState; +import com.viaversion.viaversion.api.connection.StorableObject; +import com.viaversion.viaversion.api.minecraft.BlockPosition; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; + +public final class CommandBlockStateStorage implements StorableObject { + + private final Map commandBlockStates = new HashMap<>(); + + public void storeOrRemove(final BlockPosition position, final int blockState) { + if (CommandBlockState.isCommandBlock(blockState)) { + commandBlockStates.put(position, blockState); + } else { + commandBlockStates.remove(position); + } + } + + public int state(final BlockPosition position) { + return commandBlockStates.getOrDefault(position, -1); + } + + public void unloadChunk(final int chunkX, final int chunkZ) { + final Iterator iterator = commandBlockStates.keySet().iterator(); + while (iterator.hasNext()) { + final BlockPosition position = iterator.next(); + if (position.x() >> 4 == chunkX && position.z() >> 4 == chunkZ) { + iterator.remove(); + } + } + } +}