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; import emu.grasscutter.database.DatabaseHelper; import emu.grasscutter.game.Account; import emu.grasscutter.game.HandbookActions; import emu.grasscutter.server.dispatch.IDispatcher; import emu.grasscutter.server.dispatch.PacketIds; 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 java.util.concurrent.CompletableFuture; import java.util.concurrent.TimeUnit; import javax.annotation.Nullable; 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. */ static String getDispatchUrl() { return DISPATCH_INFO.dispatchUrl; } /** * Validates an authentication request. * * @param accountId The account ID. * @param token The token. * @return {@code true} if the authentication request is valid, otherwise {@code false}. */ @Nullable static Account authenticate(String accountId, String token) { return switch (Grasscutter.getRunMode()) { case GAME_ONLY -> // Use the authentication system to validate the token. Grasscutter.getAuthenticationSystem() .getSessionTokenValidator() .authenticate( AuthenticationRequest.builder() .tokenRequest(LoginTokenRequestJson.builder().uid(accountId).token(token).build()) .build()); case HYBRID, DISPATCH_ONLY -> { // Fetch the account from the database. var account = DatabaseHelper.getAccountById(accountId); if (account == null) yield null; // Check if the token is valid. yield account.getToken().equals(token) ? account : null; } }; } /** * Fetches the session key for the specified player ID. * * @param playerId The player ID. * @return The session key. */ @Nullable static String fetchSessionKey(int playerId) { return switch (Grasscutter.getRunMode()) { case GAME_ONLY -> { // Fetch the player from the game server. var player = DatabaseHelper.getPlayerByUid(playerId); if (player == null) yield null; // Fetch the account from the dispatch server. var accountId = player.getAccountId(); var account = DispatchUtils.getAccountById(accountId); // Return the session key. yield account == null ? null : account.getSessionKey(); } case DISPATCH_ONLY -> { // Fetch the player's account ID from the game server. var playerFields = DispatchUtils.getPlayerFields(playerId, "accountId"); if (playerFields == null) yield null; // Get the account ID. var accountId = playerFields.get("accountId").getAsString(); if (accountId == null) yield null; // Fetch the account from the dispatch server. var account = DatabaseHelper.getAccountById(accountId); // Return the session key. yield account == null ? null : account.getSessionKey(); } case HYBRID -> { // Fetch the player from the game server. var player = DatabaseHelper.getPlayerByUid(playerId); if (player == null) yield null; // Fetch the account from the database. var account = player.getAccount(); // Return the session key. yield account == null ? null : account.getSessionKey(); } }; } /** * Fetches an account by its ID. * * @param accountId The account ID. * @return The account. */ @Nullable static Account getAccountById(String accountId) { return switch (Grasscutter.getRunMode()) { case GAME_ONLY -> { // Create a request for account information. var request = new JsonObject(); request.addProperty("accountId", accountId); // Wait for the request to complete. yield Grasscutter.getGameServer() .getDispatchClient() .await( request, PacketIds.GetAccountReq, PacketIds.GetAccountRsp, packet -> IDispatcher.decode(packet, Account.class)); } case HYBRID, DISPATCH_ONLY -> DatabaseHelper.getAccountById(accountId); }; } /** * Fetches the values of fields for a player. * * @param playerId The player's ID. * @param fields The fields to fetch. * @return An object holding the field values. */ @Nullable static JsonObject getPlayerFields(int playerId, String... fields) { return switch (Grasscutter.getRunMode()) { case DISPATCH_ONLY -> { // Create a request for player fields. var request = new JsonObject(); request.addProperty("playerId", playerId); request.add("fields", IDispatcher.JSON.toJsonTree(fields)); // Wait for the request to complete. yield Grasscutter.getDispatchServer() .await( request, PacketIds.GetPlayerFieldsReq, PacketIds.GetPlayerFieldsRsp, IDispatcher.DEFAULT_PARSER); } case HYBRID, GAME_ONLY -> { // Get the player by ID. 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)); // 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(); } } // Return the values. yield fieldValues; } }; } /** * Fetches the gacha history for the specified account. * * @param accountId The account ID. * @param page The page. * @param gachaType The gacha type. * @return The gacha history. */ static JsonObject fetchGachaRecords(String accountId, int page, int gachaType) { return switch (Grasscutter.getRunMode()) { case DISPATCH_ONLY -> { // Create a request for gacha records. var request = new JsonObject(); request.addProperty("accountId", accountId); request.addProperty("page", page); request.addProperty("gachaType", gachaType); // Create a future for the response. var future = new CompletableFuture(); // Listen for the response. var server = Grasscutter.getDispatchServer(); server.registerCallback( PacketIds.GachaHistoryRsp, packet -> future.complete(IDispatcher.decode(packet, JsonObject.class))); // Broadcast the request. server.sendMessage(PacketIds.GachaHistoryReq, request); try { // Wait for the response. yield future.get(5L, TimeUnit.SECONDS); } catch (Exception ignored) { yield null; } } case HYBRID, GAME_ONLY -> { // Create a response object. var response = new JsonObject(); // Get the player's ID from the account. var player = Grasscutter.getGameServer().getPlayerByAccountId(accountId); if (player == null) { response.addProperty("retcode", 1); yield response; } // Fetch the gacha records. GachaHandler.fetchGachaRecords(player, response, page, gachaType); yield response; } }; } /** * Performs a handbook action. * * @param action The action. * @param data The data. * @return The response. */ static Response performHandbookAction(HandbookBody.Action action, Object data) { return switch (Grasscutter.getRunMode()) { case DISPATCH_ONLY -> { // Create a request for the 'GM Talk' action. var request = new JsonObject(); request.addProperty("action", action.name()); request.add("data", JsonUtils.toJson(data)); // Create a future for the response. var future = new CompletableFuture(); // Listen for the response. var server = Grasscutter.getDispatchServer(); server.registerCallback( PacketIds.GmTalkRsp, packet -> future.complete(IDispatcher.decode(packet, Response.class))); // Broadcast the request. server.sendMessage(PacketIds.GmTalkReq, request); try { // Wait for the response. yield future.get(5L, TimeUnit.SECONDS); } catch (Exception ignored) { yield Response.builder() .status(400) .message("No response received from any server.") .build(); } } case HYBRID, GAME_ONLY -> switch (action) { case GRANT_AVATAR -> HandbookActions.grantAvatar((GrantAvatar) data); case GIVE_ITEM -> HandbookActions.giveItem((GiveItem) data); case TELEPORT_TO -> HandbookActions.teleportTo((TeleportTo) data); case SPAWN_ENTITY -> HandbookActions.spawnEntity((SpawnEntity) data); }; }; } }