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();
+ }
+ }
+ }
+}