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
This commit is contained in:
parent
ac6f911f15
commit
448e9369de
|
@ -47,6 +47,8 @@ dependencies {
|
|||
testImplementation 'io.netty:netty-common:4.1.77.Final'
|
||||
testImplementation 'io.netty:netty-transport:4.1.77.Final'
|
||||
testImplementation 'org.spigotmc:spigot:1.19.4-R0.1-SNAPSHOT'
|
||||
testImplementation 'net.kyori:adventure-text-serializer-gson:4.13.0'
|
||||
testImplementation 'net.kyori:adventure-text-serializer-plain:4.13.1'
|
||||
}
|
||||
|
||||
java {
|
||||
|
|
5
pom.xml
5
pom.xml
|
@ -332,6 +332,11 @@
|
|||
<version>4.13.0</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>net.kyori</groupId>
|
||||
<artifactId>adventure-text-serializer-plain</artifactId>
|
||||
<version>4.13.0</version>
|
||||
</dependency>
|
||||
<!-- https://mvnrepository.com/artifact/net.bytebuddy/byte-buddy -->
|
||||
<dependency>
|
||||
<groupId>net.bytebuddy</groupId>
|
||||
|
|
|
@ -1685,6 +1685,22 @@ public final class MinecraftReflection {
|
|||
}
|
||||
}
|
||||
|
||||
public static Class<?> getDynamicOpsClass() {
|
||||
return getLibraryClass("com.mojang.serialization.DynamicOps");
|
||||
}
|
||||
|
||||
public static Class<?> getJsonOpsClass() {
|
||||
return getLibraryClass("com.mojang.serialization.JsonOps");
|
||||
}
|
||||
|
||||
public static Class<?> getNbtOpsClass() {
|
||||
return getMinecraftClass("nbt.DynamicOpsNBT" /* Spigot Mappings */, "nbt.NbtOps" /* Mojang Mappings */);
|
||||
}
|
||||
|
||||
public static Class<?> getCodecClass() {
|
||||
return getLibraryClass("com.mojang.serialization.Codec");
|
||||
}
|
||||
|
||||
public static Class<?> getHolderClass() {
|
||||
return getMinecraftClass("core.Holder");
|
||||
}
|
||||
|
|
|
@ -1,11 +1,8 @@
|
|||
package com.comphenix.protocol.wrappers;
|
||||
|
||||
import com.comphenix.protocol.PacketType;
|
||||
import com.comphenix.protocol.injector.BukkitUnwrapper;
|
||||
import com.comphenix.protocol.reflect.EquivalentConverter;
|
||||
import com.comphenix.protocol.reflect.accessors.Accessors;
|
||||
import com.comphenix.protocol.reflect.accessors.FieldAccessor;
|
||||
import com.comphenix.protocol.reflect.accessors.MethodAccessor;
|
||||
import com.comphenix.protocol.utility.MinecraftReflection;
|
||||
import com.comphenix.protocol.utility.MinecraftVersion;
|
||||
import com.comphenix.protocol.wrappers.ping.LegacyServerPing;
|
||||
|
@ -15,7 +12,6 @@ import com.google.common.base.Preconditions;
|
|||
import com.google.common.base.Splitter;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.io.ByteStreams;
|
||||
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import io.netty.buffer.Unpooled;
|
||||
import io.netty.handler.codec.base64.Base64;
|
||||
|
@ -103,8 +99,10 @@ public class WrappedServerPing implements ClonableWrapper {
|
|||
* @return The wrapped server ping.
|
||||
*/
|
||||
public static WrappedServerPing fromJson(String json) {
|
||||
// return fromHandle(GSON_FROM_JSON.invoke(PING_GSON.get(null), json, SERVER_PING));
|
||||
return null;
|
||||
if(MinecraftVersion.FEATURE_PREVIEW_2.atOrAbove()) {
|
||||
return new WrappedServerPing(ServerPingRecord.fromJson(json).getHandle());
|
||||
}
|
||||
return new WrappedServerPing(LegacyServerPing.fromJson(json));
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -350,8 +348,7 @@ public class WrappedServerPing implements ClonableWrapper {
|
|||
* @return The JSON representation.
|
||||
*/
|
||||
public String toJson() {
|
||||
return null;
|
||||
// return (String) GSON_TO_JSON.invoke(PING_GSON.get(null), getHandle());
|
||||
return impl.getJson();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -546,4 +543,17 @@ public class WrappedServerPing implements ClonableWrapper {
|
|||
return encoded;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if(!(obj instanceof WrappedServerPing)) {
|
||||
return false;
|
||||
}
|
||||
return getHandle().equals(((WrappedServerPing) obj).getHandle());
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return getHandle().hashCode();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,31 @@
|
|||
package com.comphenix.protocol.wrappers.codecs;
|
||||
|
||||
import com.comphenix.protocol.reflect.accessors.Accessors;
|
||||
import com.comphenix.protocol.reflect.accessors.MethodAccessor;
|
||||
import com.comphenix.protocol.utility.MinecraftReflection;
|
||||
import com.comphenix.protocol.wrappers.AbstractWrapper;
|
||||
|
||||
public class WrappedCodec extends AbstractWrapper {
|
||||
private static final Class<?> HANDLE_TYPE = MinecraftReflection.getCodecClass();
|
||||
private static final Class<?> ENCODER_CLASS = MinecraftReflection.getLibraryClass("com.mojang.serialization.Encoder");
|
||||
private static final Class<?> DECODER_CLASS = MinecraftReflection.getLibraryClass("com.mojang.serialization.Decoder");
|
||||
private static final MethodAccessor ENCODE_START_ACCESSOR = Accessors.getMethodAccessor(ENCODER_CLASS, "encodeStart", MinecraftReflection.getDynamicOpsClass(), Object.class);
|
||||
private static final MethodAccessor PARSE_ACCESSOR = Accessors.getMethodAccessor(DECODER_CLASS, "parse", MinecraftReflection.getDynamicOpsClass(), Object.class);
|
||||
|
||||
private WrappedCodec(Object handle) {
|
||||
super(HANDLE_TYPE);
|
||||
this.setHandle(handle);
|
||||
}
|
||||
|
||||
public static WrappedCodec fromHandle(Object handle) {
|
||||
return new WrappedCodec(handle);
|
||||
}
|
||||
|
||||
public WrappedDataResult encode(Object object, WrappedDynamicOps ops) {
|
||||
return WrappedDataResult.fromHandle(ENCODE_START_ACCESSOR.invoke(handle, ops.getHandle(), object));
|
||||
}
|
||||
|
||||
public WrappedDataResult parse(Object value, WrappedDynamicOps ops) {
|
||||
return WrappedDataResult.fromHandle(PARSE_ACCESSOR.invoke(handle, ops.getHandle(), value));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,50 @@
|
|||
package com.comphenix.protocol.wrappers.codecs;
|
||||
|
||||
import com.comphenix.protocol.reflect.accessors.Accessors;
|
||||
import com.comphenix.protocol.reflect.accessors.MethodAccessor;
|
||||
import com.comphenix.protocol.utility.MinecraftReflection;
|
||||
import com.comphenix.protocol.wrappers.AbstractWrapper;
|
||||
|
||||
import java.util.NoSuchElementException;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Function;
|
||||
|
||||
public class WrappedDataResult extends AbstractWrapper {
|
||||
private final static Class<?> HANDLE_TYPE = MinecraftReflection.getLibraryClass("com.mojang.serialization.DataResult");
|
||||
private final static Class<?> PARTIAL_DATA_RESULT_CLASS = MinecraftReflection.getLibraryClass("com.mojang.serialization.DataResult$PartialResult");
|
||||
private final static MethodAccessor ERROR_ACCESSOR = Accessors.getMethodAccessor(HANDLE_TYPE, "error");
|
||||
private final static MethodAccessor RESULT_ACCESSOR = Accessors.getMethodAccessor(HANDLE_TYPE, "result");
|
||||
private final static MethodAccessor PARTIAL_RESULT_MESSAGE_ACCESSOR = Accessors.getMethodAccessor(PARTIAL_DATA_RESULT_CLASS, "message");
|
||||
|
||||
/**
|
||||
* Construct a new NMS wrapper.
|
||||
**/
|
||||
public WrappedDataResult(Object handle) {
|
||||
super(HANDLE_TYPE);
|
||||
this.setHandle(handle);
|
||||
}
|
||||
|
||||
public static WrappedDataResult fromHandle(Object handle) {
|
||||
return new WrappedDataResult(handle);
|
||||
}
|
||||
|
||||
public Optional<Object> getResult() {
|
||||
return (Optional) RESULT_ACCESSOR.invoke(this.handle);
|
||||
}
|
||||
|
||||
public Optional<String> getErrorMessage() {
|
||||
return (Optional) ERROR_ACCESSOR.invoke(this.handle);
|
||||
}
|
||||
|
||||
public Object getOrThrow(Function<String, Throwable> errorHandler) {
|
||||
Optional<String> err = getErrorMessage();
|
||||
if(err.isPresent()) {
|
||||
return errorHandler.apply((String) PARTIAL_RESULT_MESSAGE_ACCESSOR.invoke(err.get()));
|
||||
}
|
||||
Optional<Object> result = getResult();
|
||||
if(result.isPresent()) {
|
||||
return result.get();
|
||||
}
|
||||
throw new NoSuchElementException();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
package com.comphenix.protocol.wrappers.codecs;
|
||||
|
||||
import com.comphenix.protocol.reflect.accessors.Accessors;
|
||||
import com.comphenix.protocol.reflect.accessors.FieldAccessor;
|
||||
import com.comphenix.protocol.utility.MinecraftReflection;
|
||||
import com.comphenix.protocol.wrappers.AbstractWrapper;
|
||||
|
||||
public class WrappedDynamicOps extends AbstractWrapper {
|
||||
private static final Class<?> HANDLE_TYPE = MinecraftReflection.getDynamicOpsClass();
|
||||
public static final FieldAccessor NBT_ACCESSOR = Accessors.getFieldAccessor(MinecraftReflection.getNbtOpsClass(), MinecraftReflection.getNbtOpsClass(), false);
|
||||
public static final FieldAccessor[] JSON_ACCESSORS = Accessors.getFieldAccessorArray(MinecraftReflection.getJsonOpsClass(), MinecraftReflection.getJsonOpsClass(), false);
|
||||
private WrappedDynamicOps(Object handle) {
|
||||
super(HANDLE_TYPE);
|
||||
this.setHandle(handle);
|
||||
}
|
||||
public static WrappedDynamicOps fromHandle(Object handle) {
|
||||
return new WrappedDynamicOps(handle);
|
||||
}
|
||||
public static WrappedDynamicOps json(boolean compressed) {
|
||||
return fromHandle(JSON_ACCESSORS[compressed ? 1 : 0].get(null));
|
||||
}
|
||||
|
||||
public static WrappedDynamicOps nbt() {
|
||||
return fromHandle(NBT_ACCESSOR);
|
||||
}
|
||||
}
|
|
@ -286,6 +286,11 @@ public final class LegacyServerPing extends AbstractWrapper implements ServerPin
|
|||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getJson() {
|
||||
return (String) GSON_TO_JSON.invoke(PING_GSON.get(null), getHandle());
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the player count and maximum is visible.
|
||||
* <p>
|
||||
|
|
|
@ -1,10 +1,7 @@
|
|||
package com.comphenix.protocol.wrappers.ping;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
import com.comphenix.protocol.wrappers.WrappedChatComponent;
|
||||
import com.comphenix.protocol.wrappers.WrappedGameProfile;
|
||||
|
||||
import com.google.common.collect.ImmutableList;
|
||||
|
||||
public interface ServerPingImpl extends Cloneable {
|
||||
|
@ -38,6 +35,7 @@ public interface ServerPingImpl extends Cloneable {
|
|||
|
||||
boolean arePlayersVisible();
|
||||
void setPlayersVisible(boolean visible);
|
||||
String getJson();
|
||||
|
||||
Object getHandle();
|
||||
}
|
|
@ -1,31 +1,39 @@
|
|||
package com.comphenix.protocol.wrappers.ping;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.Semaphore;
|
||||
|
||||
import com.comphenix.protocol.events.InternalStructure;
|
||||
import com.comphenix.protocol.reflect.EquivalentConverter;
|
||||
import com.comphenix.protocol.reflect.StructureModifier;
|
||||
import com.comphenix.protocol.reflect.accessors.Accessors;
|
||||
import com.comphenix.protocol.reflect.accessors.ConstructorAccessor;
|
||||
import com.comphenix.protocol.reflect.accessors.FieldAccessor;
|
||||
import com.comphenix.protocol.reflect.accessors.MethodAccessor;
|
||||
import com.comphenix.protocol.utility.MinecraftProtocolVersion;
|
||||
import com.comphenix.protocol.utility.MinecraftReflection;
|
||||
import com.comphenix.protocol.utility.MinecraftVersion;
|
||||
import com.comphenix.protocol.wrappers.*;
|
||||
|
||||
import com.comphenix.protocol.wrappers.codecs.WrappedCodec;
|
||||
import com.comphenix.protocol.wrappers.codecs.WrappedDynamicOps;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import org.bukkit.Bukkit;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.*;
|
||||
|
||||
public final class ServerPingRecord implements ServerPingImpl {
|
||||
private static Class<?> SERVER_PING;
|
||||
private static Class<?> PLAYER_SAMPLE_CLASS;
|
||||
private static Class<?> SERVER_DATA_CLASS;
|
||||
|
||||
private static Class<?> GSON_CLASS;
|
||||
private static MethodAccessor GSON_TO_JSON;
|
||||
private static MethodAccessor GSON_FROM_JSON;
|
||||
private static FieldAccessor DATA_SERIALIZER_GSON;
|
||||
private static Class<?> JSON_ELEMENT_CLASS;
|
||||
|
||||
private static WrappedChatComponent DEFAULT_DESCRIPTION;
|
||||
|
||||
private static ConstructorAccessor PING_CTOR;
|
||||
private static WrappedCodec CODEC;
|
||||
|
||||
private static EquivalentConverter<List<WrappedGameProfile>> PROFILE_LIST_CONVERTER;
|
||||
|
||||
|
@ -57,6 +65,13 @@ public final class ServerPingRecord implements ServerPingImpl {
|
|||
PROFILE_LIST_CONVERTER = BukkitConverters.getListConverter(BukkitConverters.getWrappedGameProfileConverter());
|
||||
|
||||
DEFAULT_DESCRIPTION = WrappedChatComponent.fromLegacyText("A Minecraft Server");
|
||||
|
||||
GSON_CLASS = MinecraftReflection.getMinecraftGsonClass();
|
||||
GSON_TO_JSON = Accessors.getMethodAccessor(GSON_CLASS, "toJson", Object.class);
|
||||
GSON_FROM_JSON = Accessors.getMethodAccessor(GSON_CLASS, "fromJson", String.class, Class.class);
|
||||
DATA_SERIALIZER_GSON = Accessors.getFieldAccessor(MinecraftReflection.getPacketDataSerializerClass(), GSON_CLASS, true);
|
||||
JSON_ELEMENT_CLASS = MinecraftReflection.getLibraryClass("com.google.gson.JsonElement");
|
||||
CODEC = WrappedCodec.fromHandle(Accessors.getFieldAccessor(SERVER_PING, MinecraftReflection.getCodecClass(), false).get(null));
|
||||
} catch (Exception ex) {
|
||||
throw new RuntimeException("Failed to initialize Server Ping", ex);
|
||||
} finally {
|
||||
|
@ -133,15 +148,26 @@ public final class ServerPingRecord implements ServerPingImpl {
|
|||
int max = Bukkit.getMaxPlayers();
|
||||
int online = Bukkit.getOnlinePlayers().size();
|
||||
|
||||
return new PlayerSample(max, online, null);
|
||||
return new PlayerSample(max, online, new ArrayList<>());
|
||||
}
|
||||
|
||||
private static Favicon defaultFavicon() {
|
||||
return new Favicon();
|
||||
}
|
||||
|
||||
public static ServerPingRecord fromJson(String json) {
|
||||
|
||||
Object jsonElement = GSON_FROM_JSON.invoke(DATA_SERIALIZER_GSON.get(null), json, JSON_ELEMENT_CLASS);
|
||||
|
||||
Object decoded = CODEC.parse(jsonElement, WrappedDynamicOps.json(false)).getOrThrow(e -> new IllegalStateException("Failed to decode: " + e));
|
||||
return new ServerPingRecord(decoded);
|
||||
}
|
||||
|
||||
public ServerPingRecord(Object handle) {
|
||||
initialize();
|
||||
if(handle.getClass() != SERVER_PING) {
|
||||
throw new IllegalArgumentException("Expected handle of type " + SERVER_PING.getName() + " but got " + handle.getClass().getName());
|
||||
}
|
||||
|
||||
StructureModifier<Object> modifier = new StructureModifier<>(handle.getClass()).withTarget(handle);
|
||||
InternalStructure structure = new InternalStructure(handle, modifier);
|
||||
|
@ -286,15 +312,35 @@ public final class ServerPingRecord implements ServerPingImpl {
|
|||
this.playersVisible = visible;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getJson() {
|
||||
Object encoded = CODEC.encode(getHandle(), WrappedDynamicOps.json(false)).getOrThrow(e -> new IllegalStateException("Failed to encode: " + e));
|
||||
return (String) GSON_TO_JSON.invoke(DATA_SERIALIZER_GSON.get(null), encoded);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getHandle() {
|
||||
WrappedChatComponent wrappedDescription = description != null ? description : DEFAULT_DESCRIPTION;
|
||||
Object descHandle = wrappedDescription.getHandle();
|
||||
|
||||
Optional<Object> playersHandle = Optional.ofNullable(playerSample != null ? SAMPLE_WRAPPER.unwrap(playerSample) : null);
|
||||
Optional<Object> playersHandle = Optional.ofNullable(SAMPLE_WRAPPER.unwrap(playerSample != null ? playerSample : new ArrayList<>())); // sample has to be non-null in handle
|
||||
Optional<Object> versionHandle = Optional.ofNullable(serverData != null ? DATA_WRAPPER.unwrap(serverData) : null);
|
||||
Optional<Object> favHandle = Optional.ofNullable(favicon != null ? FAVICON_WRAPPER.unwrap(favicon) : null);
|
||||
|
||||
return PING_CTOR.invoke(descHandle, playersHandle, versionHandle, favHandle, enforceSafeChat);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if(!(obj instanceof ServerPingRecord)) {
|
||||
return false;
|
||||
}
|
||||
ServerPingRecord other = (ServerPingRecord) obj;
|
||||
|
||||
return Objects.equals(description, other.description)
|
||||
&& Objects.equals(playerSample, other.playerSample)
|
||||
&& Objects.equals(serverData, other.serverData)
|
||||
&& ((favicon == null && other.favicon.iconBytes == null)
|
||||
|| ((favicon != null) == (other.favicon != null) && Arrays.equals(favicon.iconBytes, other.favicon.iconBytes)));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,6 +9,7 @@ import com.comphenix.protocol.events.PacketContainer;
|
|||
import com.comphenix.protocol.utility.MinecraftProtocolVersion;
|
||||
import com.comphenix.protocol.wrappers.WrappedServerPing.CompressedImage;
|
||||
import com.google.common.io.Resources;
|
||||
import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.yaml.snakeyaml.external.biz.base64Coder.Base64Coder;
|
||||
|
@ -49,6 +50,18 @@ public class WrappedServerPingTest {
|
|||
|
||||
WrappedServerPing roundTrip = packet.getServerPings().read(0);
|
||||
|
||||
String asJson = serverPing.toJson();
|
||||
WrappedServerPing deserialized = WrappedServerPing.fromJson(asJson);
|
||||
deserialized.setMotD(serverPing.getMotD());
|
||||
|
||||
// Deserializing to JSON and parsing the JSON again can lead to a different object as the Mojang Datafixer optimizes server icons or reorders components in the server description
|
||||
assertEquals(serverPing.getVersionName(), deserialized.getVersionName(), "Failed to serialize as JSON and deserialize afterwards (version name mismatch)");
|
||||
assertEquals(serverPing.getVersionProtocol(), deserialized.getVersionProtocol(), "Failed to serialize as JSON and deserialize afterwards (version protocol mismatch)");
|
||||
assertEquals(serverPing.getPlayersOnline(), deserialized.getPlayersOnline(), "Failed to serialize as JSON and deserialize afterwards (players online mismatch)");
|
||||
assertEquals(serverPing.getPlayersMaximum(), deserialized.getPlayersMaximum(), "Failed to serialize as JSON and deserialize afterwards (players maximum mismatch)");
|
||||
assertEquals(serverPing.getPlayers(), deserialized.getPlayers(), "Failed to serialize as JSON and deserialize afterwards (player sample mismatch)");
|
||||
assertEquals(PlainTextComponentSerializer.plainText().serialize(AdventureComponentConverter.fromWrapper(serverPing.getMotD())), PlainTextComponentSerializer.plainText().serialize(AdventureComponentConverter.fromWrapper(deserialized.getMotD()))); // Check if plain text is equivalent
|
||||
|
||||
assertEquals(5, roundTrip.getPlayersOnline());
|
||||
assertEquals(10, roundTrip.getPlayersMaximum());
|
||||
assertEquals("Minecraft 123", roundTrip.getVersionName());
|
||||
|
|
Loading…
Reference in New Issue