From d35777d96937ff7e748beb675a3ab5e30a9ced6e Mon Sep 17 00:00:00 2001 From: KingRainbow44 Date: Fri, 26 May 2023 14:51:58 -0400 Subject: [PATCH] Implement fetching a player across servers & Add a chainable JsonObject useful for plugins! might be used in grasscutter eventually --- .../emu/grasscutter/game/player/Player.java | 3 +- .../server/dispatch/DispatchClient.java | 42 ++++-- .../server/dispatch/IDispatcher.java | 11 +- .../server/dispatch/PacketIds.java | 2 + .../emu/grasscutter/utils/DispatchUtils.java | 76 +++++----- .../java/emu/grasscutter/utils/JsonUtils.java | 2 + .../grasscutter/utils/objects/FieldFetch.java | 44 ++++++ .../grasscutter/utils/objects/JObject.java | 132 ++++++++++++++++++ 8 files changed, 263 insertions(+), 49 deletions(-) create mode 100644 src/main/java/emu/grasscutter/utils/objects/FieldFetch.java create mode 100644 src/main/java/emu/grasscutter/utils/objects/JObject.java diff --git a/src/main/java/emu/grasscutter/game/player/Player.java b/src/main/java/emu/grasscutter/game/player/Player.java index d567650a0..a0a6eddd1 100644 --- a/src/main/java/emu/grasscutter/game/player/Player.java +++ b/src/main/java/emu/grasscutter/game/player/Player.java @@ -76,6 +76,7 @@ import emu.grasscutter.server.game.GameSession.SessionState; import emu.grasscutter.server.packet.send.*; import emu.grasscutter.utils.*; import emu.grasscutter.utils.helpers.DateHelper; +import emu.grasscutter.utils.objects.FieldFetch; import it.unimi.dsi.fastutil.ints.Int2ObjectMap; import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; import lombok.Getter; @@ -92,7 +93,7 @@ import java.util.concurrent.LinkedBlockingQueue; import static emu.grasscutter.config.Configuration.GAME_OPTIONS; @Entity(value = "players", useDiscriminator = false) -public class Player implements PlayerHook { +public class Player implements PlayerHook, FieldFetch { @Id private int id; @Indexed(options = @IndexOptions(unique = true)) @Getter private String accountId; diff --git a/src/main/java/emu/grasscutter/server/dispatch/DispatchClient.java b/src/main/java/emu/grasscutter/server/dispatch/DispatchClient.java index dbd3832f7..77b12692f 100644 --- a/src/main/java/emu/grasscutter/server/dispatch/DispatchClient.java +++ b/src/main/java/emu/grasscutter/server/dispatch/DispatchClient.java @@ -1,7 +1,5 @@ package emu.grasscutter.server.dispatch; -import static emu.grasscutter.config.Configuration.DISPATCH_INFO; - import com.google.gson.JsonElement; import com.google.gson.JsonObject; import emu.grasscutter.Grasscutter; @@ -12,6 +10,12 @@ import emu.grasscutter.utils.Crypto; import emu.grasscutter.utils.DispatchUtils; import emu.grasscutter.utils.JsonUtils; import emu.grasscutter.utils.objects.HandbookBody; +import lombok.Getter; +import org.java_websocket.WebSocket; +import org.java_websocket.client.WebSocketClient; +import org.java_websocket.handshake.ServerHandshake; +import org.slf4j.Logger; + import java.net.ConnectException; import java.net.URI; import java.nio.ByteBuffer; @@ -22,11 +26,8 @@ import java.util.List; import java.util.Map; import java.util.function.BiConsumer; import java.util.function.Consumer; -import lombok.Getter; -import org.java_websocket.WebSocket; -import org.java_websocket.client.WebSocketClient; -import org.java_websocket.handshake.ServerHandshake; -import org.slf4j.Logger; + +import static emu.grasscutter.config.Configuration.DISPATCH_INFO; public final class DispatchClient extends WebSocketClient implements IDispatcher { @Getter private final Logger logger = Grasscutter.getLogger(); @@ -43,6 +44,7 @@ public final class DispatchClient extends WebSocketClient implements IDispatcher this.registerHandler(PacketIds.GachaHistoryReq, this::fetchGachaHistory); this.registerHandler(PacketIds.GmTalkReq, this::handleHandbookAction); this.registerHandler(PacketIds.GetPlayerFieldsReq, this::fetchPlayerFields); + this.registerHandler(PacketIds.GetPlayerByAccountReq, this::fetchPlayerByAccount); } /** @@ -108,7 +110,7 @@ public final class DispatchClient extends WebSocketClient implements IDispatcher } /** - * Fetches the fields of an online player. + * Fetches the fields of a player. * * @param socket The socket the packet was received from. * @param object The packet data. @@ -131,6 +133,30 @@ public final class DispatchClient extends WebSocketClient implements IDispatcher this.sendMessage(PacketIds.GetPlayerFieldsRsp, DispatchUtils.getPlayerFields(playerId, fields)); } + /** + * Fetches the fields of a player by the account. + * + * @param socket The socket the packet was received from. + * @param object The packet data. + */ + private void fetchPlayerByAccount(WebSocket socket, JsonElement object) { + var message = IDispatcher.decode(object); + var accountId = message.get("accountId").getAsString(); + var fieldsRaw = message.get("fields").getAsJsonArray(); + + // Get the player with the specified ID. + var player = Grasscutter.getGameServer().getPlayerByAccountId(accountId); + if (player == null) return; + + // Convert the fields array. + var fieldsList = new ArrayList(); + for (var field : fieldsRaw) fieldsList.add(field.getAsString()); + var fields = fieldsList.toArray(new String[0]); + + // Return the response object. + this.sendMessage(PacketIds.GetPlayerByAccountRsp, DispatchUtils.getPlayerByAccount(accountId, fields)); + } + /** * Sends a serialized encrypted message to the server. * diff --git a/src/main/java/emu/grasscutter/server/dispatch/IDispatcher.java b/src/main/java/emu/grasscutter/server/dispatch/IDispatcher.java index 702f1a0a5..0d459f8a7 100644 --- a/src/main/java/emu/grasscutter/server/dispatch/IDispatcher.java +++ b/src/main/java/emu/grasscutter/server/dispatch/IDispatcher.java @@ -1,13 +1,15 @@ package emu.grasscutter.server.dispatch; -import static emu.grasscutter.config.Configuration.DISPATCH_INFO; - import com.google.gson.Gson; import com.google.gson.GsonBuilder; import com.google.gson.JsonElement; import com.google.gson.JsonObject; import emu.grasscutter.utils.Crypto; import emu.grasscutter.utils.JsonAdapters.ByteArrayAdapter; +import emu.grasscutter.utils.objects.JObject; +import org.java_websocket.WebSocket; +import org.slf4j.Logger; + import java.nio.charset.StandardCharsets; import java.util.LinkedList; import java.util.List; @@ -17,14 +19,15 @@ import java.util.concurrent.TimeUnit; import java.util.function.BiConsumer; import java.util.function.Consumer; import java.util.function.Function; -import org.java_websocket.WebSocket; -import org.slf4j.Logger; + +import static emu.grasscutter.config.Configuration.DISPATCH_INFO; public interface IDispatcher { Gson JSON = new GsonBuilder() .disableHtmlEscaping() .registerTypeAdapter(byte[].class, new ByteArrayAdapter()) + .registerTypeAdapter(JObject.class, new JObject.Adapter()) .create(); Function DEFAULT_PARSER = diff --git a/src/main/java/emu/grasscutter/server/dispatch/PacketIds.java b/src/main/java/emu/grasscutter/server/dispatch/PacketIds.java index 314cd4e05..d54f61c3b 100644 --- a/src/main/java/emu/grasscutter/server/dispatch/PacketIds.java +++ b/src/main/java/emu/grasscutter/server/dispatch/PacketIds.java @@ -15,4 +15,6 @@ public interface PacketIds { int GetAccountRsp = 7; int GetPlayerFieldsReq = 8; int GetPlayerFieldsRsp = 9; + int GetPlayerByAccountReq = 10; + int GetPlayerByAccountRsp = 11; } diff --git a/src/main/java/emu/grasscutter/utils/DispatchUtils.java b/src/main/java/emu/grasscutter/utils/DispatchUtils.java index b46d91c51..841def471 100644 --- a/src/main/java/emu/grasscutter/utils/DispatchUtils.java +++ b/src/main/java/emu/grasscutter/utils/DispatchUtils.java @@ -1,8 +1,5 @@ package emu.grasscutter.utils; -import static emu.grasscutter.config.Configuration.DISPATCH_INFO; - -import com.google.gson.JsonNull; import com.google.gson.JsonObject; import emu.grasscutter.Grasscutter; import emu.grasscutter.auth.AuthenticationSystem.AuthenticationRequest; @@ -15,22 +12,15 @@ import emu.grasscutter.server.http.handlers.GachaHandler; import emu.grasscutter.server.http.objects.LoginTokenRequestJson; import emu.grasscutter.utils.objects.HandbookBody; import emu.grasscutter.utils.objects.HandbookBody.*; -import java.lang.reflect.Field; -import java.net.http.HttpClient; -import java.util.Arrays; -import java.util.HashMap; +import emu.grasscutter.utils.objects.JObject; + +import javax.annotation.Nullable; import java.util.concurrent.CompletableFuture; import java.util.concurrent.TimeUnit; -import javax.annotation.Nullable; + +import static emu.grasscutter.config.Configuration.DISPATCH_INFO; public interface DispatchUtils { - /** HTTP client used for dispatch queries. */ - HttpClient HTTP_CLIENT = - HttpClient.newBuilder() - .version(HttpClient.Version.HTTP_2) - .followRedirects(HttpClient.Redirect.ALWAYS) - .build(); - /** * @return The dispatch URL. */ @@ -167,30 +157,44 @@ public interface DispatchUtils { var player = Grasscutter.getGameServer().getPlayerByUid(playerId, true); if (player == null) yield null; - // Prepare field properties. - var fieldValues = new JsonObject(); - var fieldMap = new HashMap(); - Arrays.stream(player.getClass().getDeclaredFields()) - .forEach(field -> fieldMap.put(field.getName(), field)); + // Return the values. + yield player.fetchFields(fields); + } + }; + } - // Find the values of all requested fields. - for (var fieldName : fields) { - try { - var field = fieldMap.get(fieldName); - if (field == null) fieldValues.add(fieldName, JsonNull.INSTANCE); - else { - var wasAccessible = field.canAccess(player); - field.setAccessible(true); - fieldValues.add(fieldName, IDispatcher.JSON.toJsonTree(field.get(player))); - field.setAccessible(wasAccessible); - } - } catch (Exception exception) { - exception.printStackTrace(); - } - } + /** + * Fetches the values of fields for a player. + * Uses an account to find the player. + * Similar to {@link DispatchUtils#getPlayerFields(int, String...)} + * + * @param accountId The account ID. + * @param fields The fields to fetch. + * @return An object holding the field values. + */ + @Nullable static JsonObject getPlayerByAccount(String accountId, String... fields) { + return switch (Grasscutter.getRunMode()) { + case DISPATCH_ONLY -> { + // Create a request for player fields. + var request = JObject.c() + .add("accountId", accountId) + .add("fields", fields); + + // Wait for the request to complete. + yield Grasscutter.getDispatchServer() + .await( + request.gson(), + PacketIds.GetPlayerByAccountReq, + PacketIds.GetPlayerByAccountRsp, + IDispatcher.DEFAULT_PARSER); + } + case HYBRID, GAME_ONLY -> { + // Get the player by the account. + var player = Grasscutter.getGameServer().getPlayerByAccountId(accountId); + if (player == null) yield null; // Return the values. - yield fieldValues; + yield player.fetchFields(fields); } }; } diff --git a/src/main/java/emu/grasscutter/utils/JsonUtils.java b/src/main/java/emu/grasscutter/utils/JsonUtils.java index 851558268..5ecc30b07 100644 --- a/src/main/java/emu/grasscutter/utils/JsonUtils.java +++ b/src/main/java/emu/grasscutter/utils/JsonUtils.java @@ -6,6 +6,7 @@ import emu.grasscutter.data.common.DynamicFloat; import emu.grasscutter.game.world.GridPosition; import emu.grasscutter.game.world.Position; import emu.grasscutter.utils.JsonAdapters.*; +import emu.grasscutter.utils.objects.JObject; import it.unimi.dsi.fastutil.ints.IntList; import java.io.FileInputStream; import java.io.IOException; @@ -27,6 +28,7 @@ public final class JsonUtils { .registerTypeAdapter(Position.class, new PositionAdapter()) .registerTypeAdapter(GridPosition.class, new GridPositionAdapter()) .registerTypeAdapter(byte[].class, new ByteArrayAdapter()) + .registerTypeAdapter(JObject.class, new JObject.Adapter()) .registerTypeAdapterFactory(new EnumTypeAdapterFactory()) .disableHtmlEscaping() .create(); diff --git a/src/main/java/emu/grasscutter/utils/objects/FieldFetch.java b/src/main/java/emu/grasscutter/utils/objects/FieldFetch.java new file mode 100644 index 000000000..329167982 --- /dev/null +++ b/src/main/java/emu/grasscutter/utils/objects/FieldFetch.java @@ -0,0 +1,44 @@ +package emu.grasscutter.utils.objects; + +import com.google.gson.JsonNull; +import com.google.gson.JsonObject; +import emu.grasscutter.server.dispatch.IDispatcher; + +import java.lang.reflect.Field; +import java.util.Arrays; +import java.util.HashMap; + +public interface FieldFetch { + /** + * Fetches the specified fields. + * Serializes them into a JSON object. + * + * @param fields The fields to fetch. + * @return The JSON object containing the fields. + */ + default JsonObject fetchFields(String... fields) { + // Prepare field properties. + var fieldValues = new JsonObject(); + var fieldMap = new HashMap(); + Arrays.stream(this.getClass().getDeclaredFields()) + .forEach(field -> fieldMap.put(field.getName(), field)); + + // Find the values of all requested fields. + for (var fieldName : fields) { + try { + var field = fieldMap.get(fieldName); + if (field == null) fieldValues.add(fieldName, JsonNull.INSTANCE); + else { + var wasAccessible = field.canAccess(this); + field.setAccessible(true); + fieldValues.add(fieldName, IDispatcher.JSON.toJsonTree(field.get(this))); + field.setAccessible(wasAccessible); + } + } catch (Exception exception) { + exception.printStackTrace(); + } + } + + return fieldValues; + } +} diff --git a/src/main/java/emu/grasscutter/utils/objects/JObject.java b/src/main/java/emu/grasscutter/utils/objects/JObject.java new file mode 100644 index 000000000..a27f8bf9e --- /dev/null +++ b/src/main/java/emu/grasscutter/utils/objects/JObject.java @@ -0,0 +1,132 @@ +package emu.grasscutter.utils.objects; + +import com.google.gson.*; +import com.google.gson.internal.LinkedTreeMap; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonWriter; +import emu.grasscutter.utils.JsonUtils; + +import java.io.IOException; + +/* Replica of JsonObject. Includes chaining. */ +public final class JObject { + /* Type adapter for gson. */ + public static class Adapter extends TypeAdapter { + @Override + public void write(JsonWriter out, JObject value) throws IOException { + out.beginObject(); + for (var entry : value.members.entrySet()) { + out.name(entry.getKey()); + out.jsonValue(entry.getValue().toString()); + } + out.endObject(); + } + + @Override + public JObject read(JsonReader in) throws IOException { + var object = JObject.c(); + in.beginObject(); + while (in.hasNext()) { + object.add(in.nextName(), JsonParser.parseReader(in)); + } + in.endObject(); + return object; + } + } + + /** + * Creates a new empty object. + * + * @return The new object. + */ + public static JObject c() { + return new JObject(); + } + + private final LinkedTreeMap members = new LinkedTreeMap<>(); + + /** + * Adds a member to this object. + * + * @param name The name of the member. + * @param value The value of the member. + * @return This object. + */ + public JObject add(String name, JsonElement value) { + this.members.put(name, value); + return this; + } + + /** + * Adds a member to this object. + * + * @param name The name of the member. + * @param value The value of the member. + * @return This object. + */ + public JObject add(String name, String value) { + return this.add(name, value == null ? JsonNull.INSTANCE : new JsonPrimitive(value)); + } + + /** + * Adds a member to this object. + * + * @param name The name of the member. + * @param value The value of the member. + * @return This object. + */ + public JObject add(String name, Number value) { + return this.add(name, value == null ? JsonNull.INSTANCE : new JsonPrimitive(value)); + } + + /** + * Adds a member to this object. + * + * @param name The name of the member. + * @param value The value of the member. + * @return This object. + */ + public JObject add(String name, Boolean value) { + return this.add(name, value == null ? JsonNull.INSTANCE : new JsonPrimitive(value)); + } + + /** + * Adds a member to this object. + * + * @param name The name of the member. + * @param value The value of the member. + * @return This object. + */ + public JObject add(String name, Character value) { + return this.add(name, value == null ? JsonNull.INSTANCE : new JsonPrimitive(value)); + } + + /** + * Adds a member to this object. + * + * @param name The name of the member. + * @param value The value of the member. + * @return This object. + */ + public JObject add(String name, Object[] value) { + return this.add(name, value == null ? JsonNull.INSTANCE : JsonUtils.toJson(value)); + } + + /** + * @return A {@link JsonObject} representation of this object. + */ + public JsonObject gson() { + var object = new JsonObject(); + for (var entry : this.members.entrySet()) { + object.add(entry.getKey(), entry.getValue()); + } + return object; + } + + /** + * @return The property holder. + */ + public Object json() { + return this.members; + } +}