When using Replay Mod v2.6.27 for Minecraft 26.2, the replays were failing to save in my world that contained teams with non-empty team prefixes.
Here's a video of me replicating the bug in single player: link
I fed the jar and the Minecraft crash report to Claude Opus 4.8 and it identified and fixed the bug! Here's my Claude chat if you're curious.
Summary
PacketTeam.skipTeamInfo walks the fields of the clientbound set_player_team packet in the pre-26.2 order. In 26.2 the fields after the display name were reordered: prefix and suffix moved up ahead of nametag visibility and collision rule. The existing 26.2 branch already accounts for the color becoming optional and the flags byte moving to the end, but not for the two components moving.
The result is that the reader desyncs partway through the packet and then tries to parse arbitrary bytes as NBT. In my case it threw an exception while reading the suffix, aborting the replay save entirely.
Environment
|
|
| Minecraft |
26.2 (protocol 776) |
| ReplayMod |
26.2-2.6.27 |
| Fabric Loader / API |
0.19.3 / 0.155.2+26.2 |
| Java |
25.0.1 (Microsoft) |
| OS |
Windows 11 |
Symptom
Recording saves fine right up until post-processing, then:
---- Minecraft Crash Report ----
Description: Saving replay file
java.io.IOException: Failed to create tag.
at com.replaymod.replaystudio.protocol.Packet$Reader.readText(Packet.java:277)
at com.replaymod.replaystudio.protocol.packets.PacketTeam.skipTeamInfo(PacketTeam.java:104)
at com.replaymod.replaystudio.protocol.packets.PacketTeam.getPlayers(PacketTeam.java:63)
at com.replaymod.replaystudio.filter.SquashFilter.onPacket(SquashFilter.java:508)
at com.replaymod.editor.gui.MarkerProcessor.apply(MarkerProcessor.java:218)
at com.replaymod.recording.packet.PacketListener.lambda$channelInactive$1(PacketListener.java:314)
Caused by: com.github.steveice10.opennbt.tag.TagCreateException: Could not find tag with ID "114".
at com.github.steveice10.opennbt.tag.TagRegistry.createInstance(TagRegistry.java:112)
PacketTeam.java:104 is the second readText() in skipTeamInfo, i.e. the suffix. Tag ID 114 is the ASCII byte r, so the reader is sitting inside a string payload rather than at a tag boundary.
Root cause
Current parse order in skipTeamInfo versus the actual 26.2 wire order:
| # |
skipTeamInfo expects |
26.2 actually sends |
| 1 |
display name (component) |
display name (component) |
| 2 |
nametag visibility (VarInt) |
prefix (component) |
| 3 |
collision rule (VarInt) |
suffix (component) |
| 4 |
color (bool + VarInt) |
nametag visibility (VarInt) |
| 5 |
prefix (component) |
collision rule (VarInt) |
| 6 |
suffix (component) |
color (bool + VarInt) |
| 7 |
flags (byte) |
flags (byte) |
This is verifiable without leaving the shipped jar. Protocol26_1To26_2 from the bundled ViaVersion registers a replaceClientbound handler for ClientboundPackets26_1.SET_PLAYER_TEAM that does, for actions 0 and 2:
passthrough the team name and action byte
passthroughAndProcess the display name
read the flags byte and three VarInts (visibility, collision, color) without writing them
passthroughAndProcess the prefix, then the suffix
write visibility, collision, BOOL_OPTIONAL_VAR_INT color (null when the value is >= 16), and finally the flags byte
Since passthrough emits in call order and the intervening fields are consumed but written later, the emitted 26.2 layout is exactly the right-hand column above.
Reproduction
You can replicate this bug by joining a world that contains at least one team with a non-empty team prefix or suffix, manually starting and stopping the recording, then disconnecting from the world.
- Create a 26.2 Superflat world in Single Player, and ensure allow commands is on
- Create a team by running the command
/team add TestTeam
- Modify the team prefix by running the command
/team modify TestTeam prefix TEST
- Save and quit
- Ensure automatic recording is off
- Join the world again (the server sends a CREATE for every existing team on join)
- Start the recording manually rather than letting it auto-record on join. This places an
_RM_START_CUT marker at timestamp 0, so the join-time team packets fall inside the region that gets squashed.
- Press Stop, then Save and Quit.
- The save fails during
MarkerProcessor.apply.
Worth noting for anyone trying to reproduce: markers are required. SquashFilter is the only caller of PacketTeam, and MarkerProcessor is the only caller of SquashFilter, so with no _RM_ markers in the replay nothing ever parses the packet and the save succeeds. Only Action.CREATE reaches skipTeamInfo — getPlayers returns early for UPDATE, and add/remove-player packets go straight to the name list.
Depending on the byte content the desync does not always throw; an empty prefix and suffix can slip through the misread and yield a silently wrong player list instead of a crash. I have not confirmed that case, but it seems worth checking, since it would mean squashed replays are being corrupted quietly. createTeam shares skipTeamInfo to compute the template length, so it is affected the same way.
Patch
Full disclosure: this section with the bug fix was generated by Claude. Although the .class file it generated using the following code worked when I dragged it into the .jar with 7-Zip, please test the code first to ensure it compiles and fixes the bug
Still present on master as of this writing. Isolating 26.2 into its own branch keeps the legacy path untouched:
--- a/src/main/java/com/replaymod/replaystudio/protocol/packets/PacketTeam.java
+++ b/src/main/java/com/replaymod/replaystudio/protocol/packets/PacketTeam.java
@@ -79,13 +79,24 @@
private static void skipTeamInfo(Packet packet, Packet.Reader in) throws IOException {
in.readText(); // display name
+ if (packet.atLeast(ProtocolVersion.v26_2)) {
+ // 26.2 reordered everything after the display name: prefix and suffix moved up
+ // directly behind it, the color became optional and the flags byte moved to the end.
+ in.readText(); // prefix
+ in.readText(); // suffix
+ in.readVarInt(); // name tag visibility
+ in.readVarInt(); // collision rule
+ if (in.readBoolean()) {
+ in.readVarInt(); // color
+ }
+ in.readByte(); // flags
+ return;
+ }
if (!packet.atLeast(ProtocolVersion.v1_13)) {
in.readString(); // prefix
in.readString(); // suffix
}
- if (packet.olderThan(ProtocolVersion.v26_2)) {
- in.readByte(); // flags
- }
+ in.readByte(); // flags
if (packet.atLeast(ProtocolVersion.v1_8)) {
if (packet.atLeast(ProtocolVersion.v1_21_5)) {
in.readVarInt(); // name tag visibility
@@ -97,18 +108,13 @@
}
}
if (packet.atLeast(ProtocolVersion.v1_13)) {
- if (packet.olderThan(ProtocolVersion.v26_2) || in.readBoolean()) {
- in.readVarInt(); // color
- }
+ in.readVarInt(); // color
in.readText(); // prefix
in.readText(); // suffix
} else {
in.readByte(); // color
}
}
- if (packet.atLeast(ProtocolVersion.v26_2)) {
- in.readByte(); // flags
- }
}
public static Packet createTeam(Packet template, Collection<String> players) throws IOException {
I have been running this patched into my local jar and it saves correctly on a world with several teams with non-empty prefixes.
When using Replay Mod v2.6.27 for Minecraft 26.2, the replays were failing to save in my world that contained teams with non-empty team prefixes.
Here's a video of me replicating the bug in single player: link
I fed the jar and the Minecraft crash report to Claude Opus 4.8 and it identified and fixed the bug! Here's my Claude chat if you're curious.
Summary
PacketTeam.skipTeamInfowalks the fields of the clientboundset_player_teampacket in the pre-26.2 order. In 26.2 the fields after the display name were reordered: prefix and suffix moved up ahead of nametag visibility and collision rule. The existing 26.2 branch already accounts for the color becoming optional and the flags byte moving to the end, but not for the two components moving.The result is that the reader desyncs partway through the packet and then tries to parse arbitrary bytes as NBT. In my case it threw an exception while reading the suffix, aborting the replay save entirely.
Environment
Symptom
Recording saves fine right up until post-processing, then:
PacketTeam.java:104is the secondreadText()inskipTeamInfo, i.e. the suffix. Tag ID 114 is the ASCII byter, so the reader is sitting inside a string payload rather than at a tag boundary.Root cause
Current parse order in
skipTeamInfoversus the actual 26.2 wire order:skipTeamInfoexpectsThis is verifiable without leaving the shipped jar.
Protocol26_1To26_2from the bundled ViaVersion registers areplaceClientboundhandler forClientboundPackets26_1.SET_PLAYER_TEAMthat does, for actions 0 and 2:passthroughthe team name and action bytepassthroughAndProcessthe display namereadthe flags byte and three VarInts (visibility, collision, color) without writing thempassthroughAndProcessthe prefix, then the suffixwritevisibility, collision,BOOL_OPTIONAL_VAR_INTcolor (null when the value is >= 16), and finally the flags byteSince
passthroughemits in call order and the intervening fields are consumed but written later, the emitted 26.2 layout is exactly the right-hand column above.Reproduction
You can replicate this bug by joining a world that contains at least one team with a non-empty team prefix or suffix, manually starting and stopping the recording, then disconnecting from the world.
/team add TestTeam/team modify TestTeam prefix TEST_RM_START_CUTmarker at timestamp 0, so the join-time team packets fall inside the region that gets squashed.MarkerProcessor.apply.Worth noting for anyone trying to reproduce: markers are required.
SquashFilteris the only caller ofPacketTeam, andMarkerProcessoris the only caller ofSquashFilter, so with no_RM_markers in the replay nothing ever parses the packet and the save succeeds. OnlyAction.CREATEreachesskipTeamInfo—getPlayersreturns early for UPDATE, and add/remove-player packets go straight to the name list.Depending on the byte content the desync does not always throw; an empty prefix and suffix can slip through the misread and yield a silently wrong player list instead of a crash. I have not confirmed that case, but it seems worth checking, since it would mean squashed replays are being corrupted quietly.
createTeamsharesskipTeamInfoto compute the template length, so it is affected the same way.Patch
Full disclosure: this section with the bug fix was generated by Claude. Although the .class file it generated using the following code worked when I dragged it into the .jar with 7-Zip, please test the code first to ensure it compiles and fixes the bug
Still present on master as of this writing. Isolating 26.2 into its own branch keeps the legacy path untouched:
I have been running this patched into my local jar and it saves correctly on a world with several teams with non-empty prefixes.