Implement fetching a player across servers & Add a chainable JsonObject

useful for plugins! might be used in grasscutter eventually
This commit is contained in:
KingRainbow44 2023-05-26 14:51:58 -04:00
parent 273dadd4ba
commit d35777d969
No known key found for this signature in database
GPG Key ID: FC2CB64B00D257BE
8 changed files with 263 additions and 49 deletions

View File

@ -76,6 +76,7 @@ import emu.grasscutter.server.game.GameSession.SessionState;
import emu.grasscutter.server.packet.send.*; import emu.grasscutter.server.packet.send.*;
import emu.grasscutter.utils.*; import emu.grasscutter.utils.*;
import emu.grasscutter.utils.helpers.DateHelper; 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.Int2ObjectMap;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
import lombok.Getter; import lombok.Getter;
@ -92,7 +93,7 @@ import java.util.concurrent.LinkedBlockingQueue;
import static emu.grasscutter.config.Configuration.GAME_OPTIONS; import static emu.grasscutter.config.Configuration.GAME_OPTIONS;
@Entity(value = "players", useDiscriminator = false) @Entity(value = "players", useDiscriminator = false)
public class Player implements PlayerHook { public class Player implements PlayerHook, FieldFetch {
@Id private int id; @Id private int id;
@Indexed(options = @IndexOptions(unique = true)) @Indexed(options = @IndexOptions(unique = true))
@Getter private String accountId; @Getter private String accountId;

View File

@ -1,7 +1,5 @@
package emu.grasscutter.server.dispatch; package emu.grasscutter.server.dispatch;
import static emu.grasscutter.config.Configuration.DISPATCH_INFO;
import com.google.gson.JsonElement; import com.google.gson.JsonElement;
import com.google.gson.JsonObject; import com.google.gson.JsonObject;
import emu.grasscutter.Grasscutter; import emu.grasscutter.Grasscutter;
@ -12,6 +10,12 @@ import emu.grasscutter.utils.Crypto;
import emu.grasscutter.utils.DispatchUtils; import emu.grasscutter.utils.DispatchUtils;
import emu.grasscutter.utils.JsonUtils; import emu.grasscutter.utils.JsonUtils;
import emu.grasscutter.utils.objects.HandbookBody; 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.ConnectException;
import java.net.URI; import java.net.URI;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
@ -22,11 +26,8 @@ import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.function.BiConsumer; import java.util.function.BiConsumer;
import java.util.function.Consumer; import java.util.function.Consumer;
import lombok.Getter;
import org.java_websocket.WebSocket; import static emu.grasscutter.config.Configuration.DISPATCH_INFO;
import org.java_websocket.client.WebSocketClient;
import org.java_websocket.handshake.ServerHandshake;
import org.slf4j.Logger;
public final class DispatchClient extends WebSocketClient implements IDispatcher { public final class DispatchClient extends WebSocketClient implements IDispatcher {
@Getter private final Logger logger = Grasscutter.getLogger(); @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.GachaHistoryReq, this::fetchGachaHistory);
this.registerHandler(PacketIds.GmTalkReq, this::handleHandbookAction); this.registerHandler(PacketIds.GmTalkReq, this::handleHandbookAction);
this.registerHandler(PacketIds.GetPlayerFieldsReq, this::fetchPlayerFields); 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 socket The socket the packet was received from.
* @param object The packet data. * @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)); 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<String>();
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. * Sends a serialized encrypted message to the server.
* *

View File

@ -1,13 +1,15 @@
package emu.grasscutter.server.dispatch; package emu.grasscutter.server.dispatch;
import static emu.grasscutter.config.Configuration.DISPATCH_INFO;
import com.google.gson.Gson; import com.google.gson.Gson;
import com.google.gson.GsonBuilder; import com.google.gson.GsonBuilder;
import com.google.gson.JsonElement; import com.google.gson.JsonElement;
import com.google.gson.JsonObject; import com.google.gson.JsonObject;
import emu.grasscutter.utils.Crypto; import emu.grasscutter.utils.Crypto;
import emu.grasscutter.utils.JsonAdapters.ByteArrayAdapter; 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.nio.charset.StandardCharsets;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;
@ -17,14 +19,15 @@ import java.util.concurrent.TimeUnit;
import java.util.function.BiConsumer; import java.util.function.BiConsumer;
import java.util.function.Consumer; import java.util.function.Consumer;
import java.util.function.Function; 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 { public interface IDispatcher {
Gson JSON = Gson JSON =
new GsonBuilder() new GsonBuilder()
.disableHtmlEscaping() .disableHtmlEscaping()
.registerTypeAdapter(byte[].class, new ByteArrayAdapter()) .registerTypeAdapter(byte[].class, new ByteArrayAdapter())
.registerTypeAdapter(JObject.class, new JObject.Adapter())
.create(); .create();
Function<JsonElement, JsonObject> DEFAULT_PARSER = Function<JsonElement, JsonObject> DEFAULT_PARSER =

View File

@ -15,4 +15,6 @@ public interface PacketIds {
int GetAccountRsp = 7; int GetAccountRsp = 7;
int GetPlayerFieldsReq = 8; int GetPlayerFieldsReq = 8;
int GetPlayerFieldsRsp = 9; int GetPlayerFieldsRsp = 9;
int GetPlayerByAccountReq = 10;
int GetPlayerByAccountRsp = 11;
} }

View File

@ -1,8 +1,5 @@
package emu.grasscutter.utils; package emu.grasscutter.utils;
import static emu.grasscutter.config.Configuration.DISPATCH_INFO;
import com.google.gson.JsonNull;
import com.google.gson.JsonObject; import com.google.gson.JsonObject;
import emu.grasscutter.Grasscutter; import emu.grasscutter.Grasscutter;
import emu.grasscutter.auth.AuthenticationSystem.AuthenticationRequest; 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.server.http.objects.LoginTokenRequestJson;
import emu.grasscutter.utils.objects.HandbookBody; import emu.grasscutter.utils.objects.HandbookBody;
import emu.grasscutter.utils.objects.HandbookBody.*; import emu.grasscutter.utils.objects.HandbookBody.*;
import java.lang.reflect.Field; import emu.grasscutter.utils.objects.JObject;
import java.net.http.HttpClient;
import java.util.Arrays; import javax.annotation.Nullable;
import java.util.HashMap;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import javax.annotation.Nullable;
import static emu.grasscutter.config.Configuration.DISPATCH_INFO;
public interface DispatchUtils { 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. * @return The dispatch URL.
*/ */
@ -167,30 +157,44 @@ public interface DispatchUtils {
var player = Grasscutter.getGameServer().getPlayerByUid(playerId, true); var player = Grasscutter.getGameServer().getPlayerByUid(playerId, true);
if (player == null) yield null; if (player == null) yield null;
// Prepare field properties. // Return the values.
var fieldValues = new JsonObject(); yield player.fetchFields(fields);
var fieldMap = new HashMap<String, Field>(); }
Arrays.stream(player.getClass().getDeclaredFields()) };
.forEach(field -> fieldMap.put(field.getName(), field)); }
// Find the values of all requested fields. /**
for (var fieldName : fields) { * Fetches the values of fields for a player.
try { * Uses an account to find the player.
var field = fieldMap.get(fieldName); * Similar to {@link DispatchUtils#getPlayerFields(int, String...)}
if (field == null) fieldValues.add(fieldName, JsonNull.INSTANCE); *
else { * @param accountId The account ID.
var wasAccessible = field.canAccess(player); * @param fields The fields to fetch.
field.setAccessible(true); * @return An object holding the field values.
fieldValues.add(fieldName, IDispatcher.JSON.toJsonTree(field.get(player))); */
field.setAccessible(wasAccessible); @Nullable static JsonObject getPlayerByAccount(String accountId, String... fields) {
} return switch (Grasscutter.getRunMode()) {
} catch (Exception exception) { case DISPATCH_ONLY -> {
exception.printStackTrace(); // 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. // Return the values.
yield fieldValues; yield player.fetchFields(fields);
} }
}; };
} }

View File

@ -6,6 +6,7 @@ import emu.grasscutter.data.common.DynamicFloat;
import emu.grasscutter.game.world.GridPosition; import emu.grasscutter.game.world.GridPosition;
import emu.grasscutter.game.world.Position; import emu.grasscutter.game.world.Position;
import emu.grasscutter.utils.JsonAdapters.*; import emu.grasscutter.utils.JsonAdapters.*;
import emu.grasscutter.utils.objects.JObject;
import it.unimi.dsi.fastutil.ints.IntList; import it.unimi.dsi.fastutil.ints.IntList;
import java.io.FileInputStream; import java.io.FileInputStream;
import java.io.IOException; import java.io.IOException;
@ -27,6 +28,7 @@ public final class JsonUtils {
.registerTypeAdapter(Position.class, new PositionAdapter()) .registerTypeAdapter(Position.class, new PositionAdapter())
.registerTypeAdapter(GridPosition.class, new GridPositionAdapter()) .registerTypeAdapter(GridPosition.class, new GridPositionAdapter())
.registerTypeAdapter(byte[].class, new ByteArrayAdapter()) .registerTypeAdapter(byte[].class, new ByteArrayAdapter())
.registerTypeAdapter(JObject.class, new JObject.Adapter())
.registerTypeAdapterFactory(new EnumTypeAdapterFactory()) .registerTypeAdapterFactory(new EnumTypeAdapterFactory())
.disableHtmlEscaping() .disableHtmlEscaping()
.create(); .create();

View File

@ -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<String, Field>();
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;
}
}

View File

@ -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<JObject> {
@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<String, JsonElement> 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;
}
}