Skip to content

Commit 448e936

Browse files
authored
JSON Parsing for WrappedServerPing and fixed modifying favicon (#2265)
* Fix WrappedServerPing access and ensure legacy compatability for JSON parsing * added wrappers for mojang codecs and allow serializing server pings
1 parent ac6f911 commit 448e936

File tree

11 files changed

+221
-19
lines changed

11 files changed

+221
-19
lines changed

build.gradle

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,8 @@ dependencies {
4747
testImplementation 'io.netty:netty-common:4.1.77.Final'
4848
testImplementation 'io.netty:netty-transport:4.1.77.Final'
4949
testImplementation 'org.spigotmc:spigot:1.19.4-R0.1-SNAPSHOT'
50+
testImplementation 'net.kyori:adventure-text-serializer-gson:4.13.0'
51+
testImplementation 'net.kyori:adventure-text-serializer-plain:4.13.1'
5052
}
5153

5254
java {

pom.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -332,6 +332,11 @@
332332
<version>4.13.0</version>
333333
<scope>provided</scope>
334334
</dependency>
335+
<dependency>
336+
<groupId>net.kyori</groupId>
337+
<artifactId>adventure-text-serializer-plain</artifactId>
338+
<version>4.13.0</version>
339+
</dependency>
335340
<!-- https://mvnrepository.com/artifact/net.bytebuddy/byte-buddy -->
336341
<dependency>
337342
<groupId>net.bytebuddy</groupId>

src/main/java/com/comphenix/protocol/utility/MinecraftReflection.java

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1685,6 +1685,22 @@ public static Class<?> getBlockEntityInfoClass() {
16851685
}
16861686
}
16871687

1688+
public static Class<?> getDynamicOpsClass() {
1689+
return getLibraryClass("com.mojang.serialization.DynamicOps");
1690+
}
1691+
1692+
public static Class<?> getJsonOpsClass() {
1693+
return getLibraryClass("com.mojang.serialization.JsonOps");
1694+
}
1695+
1696+
public static Class<?> getNbtOpsClass() {
1697+
return getMinecraftClass("nbt.DynamicOpsNBT" /* Spigot Mappings */, "nbt.NbtOps" /* Mojang Mappings */);
1698+
}
1699+
1700+
public static Class<?> getCodecClass() {
1701+
return getLibraryClass("com.mojang.serialization.Codec");
1702+
}
1703+
16881704
public static Class<?> getHolderClass() {
16891705
return getMinecraftClass("core.Holder");
16901706
}

src/main/java/com/comphenix/protocol/wrappers/WrappedServerPing.java

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,8 @@
11
package com.comphenix.protocol.wrappers;
22

3-
import com.comphenix.protocol.PacketType;
43
import com.comphenix.protocol.injector.BukkitUnwrapper;
5-
import com.comphenix.protocol.reflect.EquivalentConverter;
64
import com.comphenix.protocol.reflect.accessors.Accessors;
75
import com.comphenix.protocol.reflect.accessors.FieldAccessor;
8-
import com.comphenix.protocol.reflect.accessors.MethodAccessor;
96
import com.comphenix.protocol.utility.MinecraftReflection;
107
import com.comphenix.protocol.utility.MinecraftVersion;
118
import com.comphenix.protocol.wrappers.ping.LegacyServerPing;
@@ -15,7 +12,6 @@
1512
import com.google.common.base.Splitter;
1613
import com.google.common.collect.ImmutableList;
1714
import com.google.common.io.ByteStreams;
18-
1915
import io.netty.buffer.ByteBuf;
2016
import io.netty.buffer.Unpooled;
2117
import io.netty.handler.codec.base64.Base64;
@@ -103,8 +99,10 @@ public static WrappedServerPing fromHandle(Object handle) {
10399
* @return The wrapped server ping.
104100
*/
105101
public static WrappedServerPing fromJson(String json) {
106-
// return fromHandle(GSON_FROM_JSON.invoke(PING_GSON.get(null), json, SERVER_PING));
107-
return null;
102+
if(MinecraftVersion.FEATURE_PREVIEW_2.atOrAbove()) {
103+
return new WrappedServerPing(ServerPingRecord.fromJson(json).getHandle());
104+
}
105+
return new WrappedServerPing(LegacyServerPing.fromJson(json));
108106
}
109107

110108
/**
@@ -350,8 +348,7 @@ public WrappedServerPing deepClone() {
350348
* @return The JSON representation.
351349
*/
352350
public String toJson() {
353-
return null;
354-
// return (String) GSON_TO_JSON.invoke(PING_GSON.get(null), getHandle());
351+
return impl.getJson();
355352
}
356353

357354
@Override
@@ -546,4 +543,17 @@ public String toEncodedText() {
546543
return encoded;
547544
}
548545
}
546+
547+
@Override
548+
public boolean equals(Object obj) {
549+
if(!(obj instanceof WrappedServerPing)) {
550+
return false;
551+
}
552+
return getHandle().equals(((WrappedServerPing) obj).getHandle());
553+
}
554+
555+
@Override
556+
public int hashCode() {
557+
return getHandle().hashCode();
558+
}
549559
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package com.comphenix.protocol.wrappers.codecs;
2+
3+
import com.comphenix.protocol.reflect.accessors.Accessors;
4+
import com.comphenix.protocol.reflect.accessors.MethodAccessor;
5+
import com.comphenix.protocol.utility.MinecraftReflection;
6+
import com.comphenix.protocol.wrappers.AbstractWrapper;
7+
8+
public class WrappedCodec extends AbstractWrapper {
9+
private static final Class<?> HANDLE_TYPE = MinecraftReflection.getCodecClass();
10+
private static final Class<?> ENCODER_CLASS = MinecraftReflection.getLibraryClass("com.mojang.serialization.Encoder");
11+
private static final Class<?> DECODER_CLASS = MinecraftReflection.getLibraryClass("com.mojang.serialization.Decoder");
12+
private static final MethodAccessor ENCODE_START_ACCESSOR = Accessors.getMethodAccessor(ENCODER_CLASS, "encodeStart", MinecraftReflection.getDynamicOpsClass(), Object.class);
13+
private static final MethodAccessor PARSE_ACCESSOR = Accessors.getMethodAccessor(DECODER_CLASS, "parse", MinecraftReflection.getDynamicOpsClass(), Object.class);
14+
15+
private WrappedCodec(Object handle) {
16+
super(HANDLE_TYPE);
17+
this.setHandle(handle);
18+
}
19+
20+
public static WrappedCodec fromHandle(Object handle) {
21+
return new WrappedCodec(handle);
22+
}
23+
24+
public WrappedDataResult encode(Object object, WrappedDynamicOps ops) {
25+
return WrappedDataResult.fromHandle(ENCODE_START_ACCESSOR.invoke(handle, ops.getHandle(), object));
26+
}
27+
28+
public WrappedDataResult parse(Object value, WrappedDynamicOps ops) {
29+
return WrappedDataResult.fromHandle(PARSE_ACCESSOR.invoke(handle, ops.getHandle(), value));
30+
}
31+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
package com.comphenix.protocol.wrappers.codecs;
2+
3+
import com.comphenix.protocol.reflect.accessors.Accessors;
4+
import com.comphenix.protocol.reflect.accessors.MethodAccessor;
5+
import com.comphenix.protocol.utility.MinecraftReflection;
6+
import com.comphenix.protocol.wrappers.AbstractWrapper;
7+
8+
import java.util.NoSuchElementException;
9+
import java.util.Optional;
10+
import java.util.function.Function;
11+
12+
public class WrappedDataResult extends AbstractWrapper {
13+
private final static Class<?> HANDLE_TYPE = MinecraftReflection.getLibraryClass("com.mojang.serialization.DataResult");
14+
private final static Class<?> PARTIAL_DATA_RESULT_CLASS = MinecraftReflection.getLibraryClass("com.mojang.serialization.DataResult$PartialResult");
15+
private final static MethodAccessor ERROR_ACCESSOR = Accessors.getMethodAccessor(HANDLE_TYPE, "error");
16+
private final static MethodAccessor RESULT_ACCESSOR = Accessors.getMethodAccessor(HANDLE_TYPE, "result");
17+
private final static MethodAccessor PARTIAL_RESULT_MESSAGE_ACCESSOR = Accessors.getMethodAccessor(PARTIAL_DATA_RESULT_CLASS, "message");
18+
19+
/**
20+
* Construct a new NMS wrapper.
21+
**/
22+
public WrappedDataResult(Object handle) {
23+
super(HANDLE_TYPE);
24+
this.setHandle(handle);
25+
}
26+
27+
public static WrappedDataResult fromHandle(Object handle) {
28+
return new WrappedDataResult(handle);
29+
}
30+
31+
public Optional<Object> getResult() {
32+
return (Optional) RESULT_ACCESSOR.invoke(this.handle);
33+
}
34+
35+
public Optional<String> getErrorMessage() {
36+
return (Optional) ERROR_ACCESSOR.invoke(this.handle);
37+
}
38+
39+
public Object getOrThrow(Function<String, Throwable> errorHandler) {
40+
Optional<String> err = getErrorMessage();
41+
if(err.isPresent()) {
42+
return errorHandler.apply((String) PARTIAL_RESULT_MESSAGE_ACCESSOR.invoke(err.get()));
43+
}
44+
Optional<Object> result = getResult();
45+
if(result.isPresent()) {
46+
return result.get();
47+
}
48+
throw new NoSuchElementException();
49+
}
50+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package com.comphenix.protocol.wrappers.codecs;
2+
3+
import com.comphenix.protocol.reflect.accessors.Accessors;
4+
import com.comphenix.protocol.reflect.accessors.FieldAccessor;
5+
import com.comphenix.protocol.utility.MinecraftReflection;
6+
import com.comphenix.protocol.wrappers.AbstractWrapper;
7+
8+
public class WrappedDynamicOps extends AbstractWrapper {
9+
private static final Class<?> HANDLE_TYPE = MinecraftReflection.getDynamicOpsClass();
10+
public static final FieldAccessor NBT_ACCESSOR = Accessors.getFieldAccessor(MinecraftReflection.getNbtOpsClass(), MinecraftReflection.getNbtOpsClass(), false);
11+
public static final FieldAccessor[] JSON_ACCESSORS = Accessors.getFieldAccessorArray(MinecraftReflection.getJsonOpsClass(), MinecraftReflection.getJsonOpsClass(), false);
12+
private WrappedDynamicOps(Object handle) {
13+
super(HANDLE_TYPE);
14+
this.setHandle(handle);
15+
}
16+
public static WrappedDynamicOps fromHandle(Object handle) {
17+
return new WrappedDynamicOps(handle);
18+
}
19+
public static WrappedDynamicOps json(boolean compressed) {
20+
return fromHandle(JSON_ACCESSORS[compressed ? 1 : 0].get(null));
21+
}
22+
23+
public static WrappedDynamicOps nbt() {
24+
return fromHandle(NBT_ACCESSOR);
25+
}
26+
}

src/main/java/com/comphenix/protocol/wrappers/ping/LegacyServerPing.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -286,6 +286,11 @@ public void setPlayersVisible(boolean visible) {
286286
}
287287
}
288288

289+
@Override
290+
public String getJson() {
291+
return (String) GSON_TO_JSON.invoke(PING_GSON.get(null), getHandle());
292+
}
293+
289294
/**
290295
* Determine if the player count and maximum is visible.
291296
* <p>

src/main/java/com/comphenix/protocol/wrappers/ping/ServerPingImpl.java

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,7 @@
11
package com.comphenix.protocol.wrappers.ping;
22

3-
import java.util.Optional;
4-
53
import com.comphenix.protocol.wrappers.WrappedChatComponent;
64
import com.comphenix.protocol.wrappers.WrappedGameProfile;
7-
85
import com.google.common.collect.ImmutableList;
96

107
public interface ServerPingImpl extends Cloneable {
@@ -38,6 +35,7 @@ default void setChatPreviewEnabled(boolean enabled) {
3835

3936
boolean arePlayersVisible();
4037
void setPlayersVisible(boolean visible);
38+
String getJson();
4139

4240
Object getHandle();
4341
}

src/main/java/com/comphenix/protocol/wrappers/ping/ServerPingRecord.java

Lines changed: 54 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,39 @@
11
package com.comphenix.protocol.wrappers.ping;
22

3-
import java.nio.charset.StandardCharsets;
4-
import java.util.List;
5-
import java.util.Optional;
6-
import java.util.concurrent.Semaphore;
7-
83
import com.comphenix.protocol.events.InternalStructure;
94
import com.comphenix.protocol.reflect.EquivalentConverter;
105
import com.comphenix.protocol.reflect.StructureModifier;
116
import com.comphenix.protocol.reflect.accessors.Accessors;
127
import com.comphenix.protocol.reflect.accessors.ConstructorAccessor;
8+
import com.comphenix.protocol.reflect.accessors.FieldAccessor;
9+
import com.comphenix.protocol.reflect.accessors.MethodAccessor;
1310
import com.comphenix.protocol.utility.MinecraftProtocolVersion;
1411
import com.comphenix.protocol.utility.MinecraftReflection;
1512
import com.comphenix.protocol.utility.MinecraftVersion;
1613
import com.comphenix.protocol.wrappers.*;
17-
14+
import com.comphenix.protocol.wrappers.codecs.WrappedCodec;
15+
import com.comphenix.protocol.wrappers.codecs.WrappedDynamicOps;
1816
import com.google.common.collect.ImmutableList;
1917
import org.bukkit.Bukkit;
2018

19+
import java.nio.charset.StandardCharsets;
20+
import java.util.*;
21+
2122
public final class ServerPingRecord implements ServerPingImpl {
2223
private static Class<?> SERVER_PING;
2324
private static Class<?> PLAYER_SAMPLE_CLASS;
2425
private static Class<?> SERVER_DATA_CLASS;
2526

27+
private static Class<?> GSON_CLASS;
28+
private static MethodAccessor GSON_TO_JSON;
29+
private static MethodAccessor GSON_FROM_JSON;
30+
private static FieldAccessor DATA_SERIALIZER_GSON;
31+
private static Class<?> JSON_ELEMENT_CLASS;
32+
2633
private static WrappedChatComponent DEFAULT_DESCRIPTION;
2734

2835
private static ConstructorAccessor PING_CTOR;
36+
private static WrappedCodec CODEC;
2937

3038
private static EquivalentConverter<List<WrappedGameProfile>> PROFILE_LIST_CONVERTER;
3139

@@ -57,6 +65,13 @@ private static void initialize() {
5765
PROFILE_LIST_CONVERTER = BukkitConverters.getListConverter(BukkitConverters.getWrappedGameProfileConverter());
5866

5967
DEFAULT_DESCRIPTION = WrappedChatComponent.fromLegacyText("A Minecraft Server");
68+
69+
GSON_CLASS = MinecraftReflection.getMinecraftGsonClass();
70+
GSON_TO_JSON = Accessors.getMethodAccessor(GSON_CLASS, "toJson", Object.class);
71+
GSON_FROM_JSON = Accessors.getMethodAccessor(GSON_CLASS, "fromJson", String.class, Class.class);
72+
DATA_SERIALIZER_GSON = Accessors.getFieldAccessor(MinecraftReflection.getPacketDataSerializerClass(), GSON_CLASS, true);
73+
JSON_ELEMENT_CLASS = MinecraftReflection.getLibraryClass("com.google.gson.JsonElement");
74+
CODEC = WrappedCodec.fromHandle(Accessors.getFieldAccessor(SERVER_PING, MinecraftReflection.getCodecClass(), false).get(null));
6075
} catch (Exception ex) {
6176
throw new RuntimeException("Failed to initialize Server Ping", ex);
6277
} finally {
@@ -133,15 +148,26 @@ private static PlayerSample defaultSample() {
133148
int max = Bukkit.getMaxPlayers();
134149
int online = Bukkit.getOnlinePlayers().size();
135150

136-
return new PlayerSample(max, online, null);
151+
return new PlayerSample(max, online, new ArrayList<>());
137152
}
138153

139154
private static Favicon defaultFavicon() {
140155
return new Favicon();
141156
}
142157

158+
public static ServerPingRecord fromJson(String json) {
159+
160+
Object jsonElement = GSON_FROM_JSON.invoke(DATA_SERIALIZER_GSON.get(null), json, JSON_ELEMENT_CLASS);
161+
162+
Object decoded = CODEC.parse(jsonElement, WrappedDynamicOps.json(false)).getOrThrow(e -> new IllegalStateException("Failed to decode: " + e));
163+
return new ServerPingRecord(decoded);
164+
}
165+
143166
public ServerPingRecord(Object handle) {
144167
initialize();
168+
if(handle.getClass() != SERVER_PING) {
169+
throw new IllegalArgumentException("Expected handle of type " + SERVER_PING.getName() + " but got " + handle.getClass().getName());
170+
}
145171

146172
StructureModifier<Object> modifier = new StructureModifier<>(handle.getClass()).withTarget(handle);
147173
InternalStructure structure = new InternalStructure(handle, modifier);
@@ -286,15 +312,35 @@ public void setPlayersVisible(boolean visible) {
286312
this.playersVisible = visible;
287313
}
288314

315+
@Override
316+
public String getJson() {
317+
Object encoded = CODEC.encode(getHandle(), WrappedDynamicOps.json(false)).getOrThrow(e -> new IllegalStateException("Failed to encode: " + e));
318+
return (String) GSON_TO_JSON.invoke(DATA_SERIALIZER_GSON.get(null), encoded);
319+
}
320+
289321
@Override
290322
public Object getHandle() {
291323
WrappedChatComponent wrappedDescription = description != null ? description : DEFAULT_DESCRIPTION;
292324
Object descHandle = wrappedDescription.getHandle();
293325

294-
Optional<Object> playersHandle = Optional.ofNullable(playerSample != null ? SAMPLE_WRAPPER.unwrap(playerSample) : null);
326+
Optional<Object> playersHandle = Optional.ofNullable(SAMPLE_WRAPPER.unwrap(playerSample != null ? playerSample : new ArrayList<>())); // sample has to be non-null in handle
295327
Optional<Object> versionHandle = Optional.ofNullable(serverData != null ? DATA_WRAPPER.unwrap(serverData) : null);
296328
Optional<Object> favHandle = Optional.ofNullable(favicon != null ? FAVICON_WRAPPER.unwrap(favicon) : null);
297329

298330
return PING_CTOR.invoke(descHandle, playersHandle, versionHandle, favHandle, enforceSafeChat);
299331
}
332+
333+
@Override
334+
public boolean equals(Object obj) {
335+
if(!(obj instanceof ServerPingRecord)) {
336+
return false;
337+
}
338+
ServerPingRecord other = (ServerPingRecord) obj;
339+
340+
return Objects.equals(description, other.description)
341+
&& Objects.equals(playerSample, other.playerSample)
342+
&& Objects.equals(serverData, other.serverData)
343+
&& ((favicon == null && other.favicon.iconBytes == null)
344+
|| ((favicon != null) == (other.favicon != null) && Arrays.equals(favicon.iconBytes, other.favicon.iconBytes)));
345+
}
300346
}

0 commit comments

Comments
 (0)