Skip to content

Commit 7b86cc0

Browse files
authored
Support double and list dynamic values in ClientboundDataStorePacket (#349)
1 parent fbbeee7 commit 7b86cc0

2 files changed

Lines changed: 182 additions & 1 deletion

File tree

bedrock-codec/src/main/java/org/cloudburstmc/protocol/bedrock/codec/v898/serializer/ClientboundDataStoreSerializer_v898.java

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@
1111
import org.cloudburstmc.protocol.bedrock.packet.ClientboundDataStorePacket;
1212
import org.cloudburstmc.protocol.common.util.VarInts;
1313

14+
import java.util.ArrayList;
1415
import java.util.LinkedHashMap;
16+
import java.util.List;
1517
import java.util.Map;
1618

1719
@RequiredArgsConstructor(access = AccessLevel.PROTECTED)
@@ -112,8 +114,27 @@ public void deserialize(ByteBuf buffer, BedrockCodecHelper helper, ClientboundDa
112114
}));
113115
}
114116

117+
/**
118+
* The dynamic value is a {@code cereal::DynamicValue} variant; the type tag is the
119+
* alternative index: 0 null, 1 bool, 2 int64, 3 double, 4 string, 5 list, 6 map.
120+
*/
115121
protected void writeDataStoreChange(ByteBuf buffer, BedrockCodecHelper helper, Object value) {
116-
int type = value instanceof Boolean ? 1 : value instanceof Number ? 2 : value instanceof String ? 4 : value instanceof Map ? 6 : 0;
122+
int type;
123+
if (value instanceof Boolean) {
124+
type = 1;
125+
} else if (value instanceof Double || value instanceof Float) {
126+
type = 3;
127+
} else if (value instanceof Number) {
128+
type = 2;
129+
} else if (value instanceof String) {
130+
type = 4;
131+
} else if (value instanceof List) {
132+
type = 5;
133+
} else if (value instanceof Map) {
134+
type = 6;
135+
} else {
136+
type = 0;
137+
}
117138

118139
buffer.writeIntLE(type);
119140

@@ -126,9 +147,18 @@ protected void writeDataStoreChange(ByteBuf buffer, BedrockCodecHelper helper, O
126147
case 2:
127148
buffer.writeLongLE(((Number) value).longValue());
128149
break;
150+
case 3:
151+
buffer.writeDoubleLE(((Number) value).doubleValue());
152+
break;
129153
case 4:
130154
helper.writeString(buffer, (String) value);
131155
break;
156+
case 5:
157+
VarInts.writeUnsignedInt(buffer, ((List<?>) value).size());
158+
for (Object item : (List<?>) value) {
159+
writeDataStoreChange(buffer, helper, item);
160+
}
161+
break;
132162
case 6:
133163
VarInts.writeUnsignedInt(buffer, ((Map<?, ?>) value).size());
134164
((Map<?, ?>) value).forEach((k, v) -> {
@@ -151,8 +181,19 @@ protected Object readDataStoreChange(ByteBuf buffer, BedrockCodecHelper helper)
151181
return buffer.readBoolean();
152182
case 2:
153183
return buffer.readLongLE();
184+
case 3:
185+
return buffer.readDoubleLE();
154186
case 4:
155187
return helper.readString(buffer);
188+
case 5:
189+
int length = VarInts.readUnsignedInt(buffer);
190+
// Each entry carries at least a 4-byte type tag, so cap the pre-sizing by what the
191+
// buffer can actually hold instead of trusting the length prefix with an allocation.
192+
List<Object> items = new ArrayList<>(Math.min(length, buffer.readableBytes() / 4));
193+
for (int i = 0; i < length; i++) {
194+
items.add(readDataStoreChange(buffer, helper));
195+
}
196+
return items;
156197
case 6:
157198
int size = VarInts.readUnsignedInt(buffer);
158199
Map<String, Object> values = new LinkedHashMap<>();
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
package org.cloudburstmc.protocol.bedrock.test;
2+
3+
import io.netty.buffer.ByteBuf;
4+
import io.netty.buffer.Unpooled;
5+
import org.cloudburstmc.protocol.bedrock.codec.BedrockCodecHelper;
6+
import org.cloudburstmc.protocol.bedrock.codec.v924.Bedrock_v924;
7+
import org.cloudburstmc.protocol.bedrock.codec.v924.serializer.ClientboundDataStoreSerializer_v924;
8+
import org.cloudburstmc.protocol.bedrock.data.datastore.DataStoreChange;
9+
import org.cloudburstmc.protocol.bedrock.packet.ClientboundDataStorePacket;
10+
import org.cloudburstmc.protocol.common.util.VarInts;
11+
import org.junit.jupiter.api.Test;
12+
13+
import java.util.Arrays;
14+
import java.util.Collections;
15+
import java.util.LinkedHashMap;
16+
import java.util.Map;
17+
18+
import static org.junit.jupiter.api.Assertions.assertEquals;
19+
import static org.junit.jupiter.api.Assertions.assertNull;
20+
import static org.junit.jupiter.api.Assertions.assertThrows;
21+
22+
public class DataStoreSerializationTest {
23+
24+
private static final BedrockCodecHelper HELPER = Bedrock_v924.CODEC.createHelper();
25+
26+
@Test
27+
public void testDynamicValueRoundTrip() {
28+
// A custom_form-shaped seed exercising every tag type at once.
29+
Map<String, Object> slot = new LinkedHashMap<>();
30+
slot.put("slider_visible", true);
31+
slot.put("label", "Slider");
32+
slot.put("value", 5.0d);
33+
slot.put("minValue", 0.0d);
34+
slot.put("maxValue", 10.0d);
35+
36+
Map<String, Object> label = new LinkedHashMap<>();
37+
label.put("label_visible", true);
38+
label.put("text", Collections.singletonMap("rawtext",
39+
Arrays.asList(Collections.singletonMap("translate", "gui.ok"))));
40+
41+
Map<String, Object> layout = new LinkedHashMap<>();
42+
layout.put("0", slot);
43+
layout.put("1", label);
44+
layout.put("length", 2L);
45+
46+
Map<String, Object> tree = new LinkedHashMap<>();
47+
tree.put("title", "Round trip");
48+
tree.put("layout", layout);
49+
tree.put("empty", null);
50+
51+
assertEquals(tree, roundTrip(tree));
52+
}
53+
54+
@Test
55+
public void testNullDynamicValueRoundTrip() {
56+
assertNull(roundTrip(null));
57+
}
58+
59+
@Test
60+
public void testScalarDynamicValueRoundTrip() {
61+
assertEquals(true, roundTrip(true));
62+
assertEquals(42L, roundTrip(42L));
63+
assertEquals(1.5d, roundTrip(1.5d));
64+
assertEquals("text", roundTrip("text"));
65+
assertEquals(Arrays.asList(1L, 2.5d, "three"), roundTrip(Arrays.asList(1L, 2.5d, "three")));
66+
}
67+
68+
@Test
69+
public void testHostileListLengthDoesNotPreAllocate() {
70+
// A forged length prefix must not translate into an up-front allocation; the loop then
71+
// fails on buffer underflow instead of exhausting memory.
72+
ByteBuf buffer = Unpooled.buffer();
73+
VarInts.writeUnsignedInt(buffer, 1); // update count
74+
VarInts.writeUnsignedInt(buffer, 1); // action type: change
75+
HELPER.writeString(buffer, "minecraft");
76+
HELPER.writeString(buffer, "custom_form_data_1");
77+
buffer.writeIntLE(1); // property update count
78+
buffer.writeIntLE(5); // list tag
79+
VarInts.writeUnsignedInt(buffer, Integer.MAX_VALUE);
80+
try {
81+
ClientboundDataStorePacket decoded = new ClientboundDataStorePacket();
82+
assertThrows(Exception.class,
83+
() -> ClientboundDataStoreSerializer_v924.INSTANCE.deserialize(buffer, HELPER, decoded));
84+
} finally {
85+
buffer.release();
86+
}
87+
}
88+
89+
@Test
90+
public void testDynamicValueTypeTags() {
91+
// A symmetric round trip would not catch swapped tags, so pin the values on the wire.
92+
assertEquals(0, writtenTypeTag(null));
93+
assertEquals(1, writtenTypeTag(true));
94+
assertEquals(2, writtenTypeTag(42L));
95+
assertEquals(3, writtenTypeTag(1.5d));
96+
assertEquals(4, writtenTypeTag("text"));
97+
assertEquals(5, writtenTypeTag(Arrays.asList("a", "b")));
98+
assertEquals(6, writtenTypeTag(Collections.singletonMap("key", "value")));
99+
}
100+
101+
private Object roundTrip(Object value) {
102+
ByteBuf buffer = serialize(value);
103+
try {
104+
ClientboundDataStorePacket decoded = new ClientboundDataStorePacket();
105+
ClientboundDataStoreSerializer_v924.INSTANCE.deserialize(buffer, HELPER, decoded);
106+
return ((DataStoreChange) decoded.getUpdates().get(0)).getNewValue();
107+
} finally {
108+
buffer.release();
109+
}
110+
}
111+
112+
private int writtenTypeTag(Object value) {
113+
ByteBuf buffer = serialize(value);
114+
try {
115+
VarInts.readUnsignedInt(buffer); // update count
116+
VarInts.readUnsignedInt(buffer); // action type
117+
HELPER.readString(buffer); // data store name
118+
HELPER.readString(buffer); // property
119+
buffer.readIntLE(); // property update count
120+
return buffer.readIntLE();
121+
} finally {
122+
buffer.release();
123+
}
124+
}
125+
126+
private ByteBuf serialize(Object value) {
127+
DataStoreChange change = new DataStoreChange();
128+
change.setDataStoreName("minecraft");
129+
change.setProperty("custom_form_data_1");
130+
change.setUpdateCount(1);
131+
change.setNewValue(value);
132+
133+
ClientboundDataStorePacket packet = new ClientboundDataStorePacket();
134+
packet.getUpdates().add(change);
135+
136+
ByteBuf buffer = Unpooled.buffer();
137+
ClientboundDataStoreSerializer_v924.INSTANCE.serialize(buffer, HELPER, packet);
138+
return buffer;
139+
}
140+
}

0 commit comments

Comments
 (0)