Separate the dispatch and game servers (pt. 1)

gacha is still broken, handbook still needs to be done
This commit is contained in:
KingRainbow44
2023-05-15 00:43:16 -04:00
Unverified
parent 97fbbdca84
commit bcc9ae10cd
28 changed files with 1225 additions and 379 deletions
@@ -0,0 +1,122 @@
package emu.grasscutter.utils;
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.server.dispatch.IDispatcher;
import emu.grasscutter.server.dispatch.PacketIds;
import emu.grasscutter.server.http.handlers.GachaHandler;
import emu.grasscutter.server.http.objects.LoginTokenRequestJson;
import javax.annotation.Nullable;
import java.net.http.HttpClient;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
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.
*/
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 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<JsonObject>();
// 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;
}
};
}
}
@@ -11,12 +11,13 @@ import emu.grasscutter.data.common.DynamicFloat;
import it.unimi.dsi.fastutil.floats.FloatArrayList;
import it.unimi.dsi.fastutil.ints.IntArrayList;
import it.unimi.dsi.fastutil.ints.IntList;
import lombok.val;
import java.io.IOException;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Objects;
import lombok.val;
public class JsonAdapters {
static class DynamicFloatAdapter extends TypeAdapter<DynamicFloat> {
@@ -77,6 +78,18 @@ public class JsonAdapters {
}
}
public static class ByteArrayAdapter extends TypeAdapter<byte[]> {
@Override
public void write(JsonWriter out, byte[] value) throws IOException {
out.value(Utils.base64Encode(value));
}
@Override
public byte[] read(JsonReader in) throws IOException {
return Utils.base64Decode(in.nextString());
}
}
static class GridPositionAdapter extends TypeAdapter<GridPosition> {
@Override
public void write(JsonWriter out, GridPosition value) throws IOException {
@@ -1,9 +1,6 @@
package emu.grasscutter.utils;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonElement;
import com.google.gson.JsonSyntaxException;
import com.google.gson.*;
import com.google.gson.reflect.TypeToken;
import emu.grasscutter.data.common.DynamicFloat;
import emu.grasscutter.utils.JsonAdapters.*;
@@ -27,9 +24,21 @@ public final class JsonUtils {
.registerTypeAdapter(IntList.class, new IntListAdapter())
.registerTypeAdapter(Position.class, new PositionAdapter())
.registerTypeAdapter(GridPosition.class, new GridPositionAdapter())
.registerTypeAdapter(byte[].class, new ByteArrayAdapter())
.registerTypeAdapterFactory(new EnumTypeAdapterFactory())
.disableHtmlEscaping()
.create();
/**
* Converts the given object to a JsonElement.
*
* @param object The object to convert.
* @return The JsonElement.
*/
public static JsonElement toJson(Object object) {
return gson.toJsonTree(object);
}
/*
* Encode an object to a JSON string
*/
@@ -1,9 +1,5 @@
package emu.grasscutter.utils;
import static emu.grasscutter.config.Configuration.FALLBACK_LANGUAGE;
import static emu.grasscutter.utils.FileUtils.getCachePath;
import static emu.grasscutter.utils.FileUtils.getResourcePath;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import emu.grasscutter.Grasscutter;
@@ -17,21 +13,23 @@ import it.unimi.dsi.fastutil.ints.IntOpenHashSet;
import it.unimi.dsi.fastutil.ints.IntSet;
import it.unimi.dsi.fastutil.objects.Object2IntMap;
import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap;
import lombok.EqualsAndHashCode;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import lombok.EqualsAndHashCode;
import static emu.grasscutter.config.Configuration.FALLBACK_LANGUAGE;
import static emu.grasscutter.utils.FileUtils.getCachePath;
import static emu.grasscutter.utils.FileUtils.getResourcePath;
public final class Language {
private static final Map<String, Language> cachedLanguages = new ConcurrentHashMap<>();
@@ -117,6 +115,41 @@ public final class Language {
}
}
/**
* Returns the translated value from the key while substituting arguments.
*
* @param locale The locale to use.
* @param key The key of the translated value to return.
* @param args The arguments to substitute.
* @return A translated value with arguments substituted.
*/
public static String translate(Locale locale, String key, Object... args) {
if (locale == null) {
return translate(key, args);
}
var langCode = Utils.getLanguageCode(locale);
var translated = getLanguage(langCode).get(key);
for (var i = 0; i < args.length; i++) {
args[i] =
switch (args[i].getClass().getSimpleName()) {
case "String" -> args[i];
case "TextStrings" -> ((TextStrings) args[i])
.getGC(langCode)
.replace("\\\\n", "\n"); // Note that we don't unescape \n for server console
default -> args[i].toString();
};
}
try {
return translated.formatted(args);
} catch (Exception exception) {
Grasscutter.getLogger().error("Failed to format string: " + key, exception);
return translated;
}
}
/**
* Returns the translated value from the key while substituting arguments.
*
@@ -130,26 +163,7 @@ public final class Language {
return translate(key, args);
}
var langCode = Utils.getLanguageCode(player.getAccount().getLocale());
String translated = getLanguage(langCode).get(key);
for (int i = 0; i < args.length; i++) {
args[i] =
switch (args[i].getClass().getSimpleName()) {
case "String" -> args[i];
case "TextStrings" -> ((TextStrings) args[i])
.getGC(langCode)
.replace("\\\\n", "\n"); // Note that we don't unescape \n for server console
default -> args[i].toString();
};
}
try {
return translated.formatted(args);
} catch (Exception exception) {
Grasscutter.getLogger().error("Failed to format string: " + key, exception);
return translated;
}
return translate(player.getAccount().getLocale(), key, args);
}
/**
@@ -1,62 +1,73 @@
package emu.grasscutter.utils;
import static emu.grasscutter.config.Configuration.*;
import ch.qos.logback.classic.Level;
import ch.qos.logback.classic.Logger;
import emu.grasscutter.BuildConfig;
import emu.grasscutter.Grasscutter;
import emu.grasscutter.Grasscutter.ServerRunMode;
import emu.grasscutter.net.packet.PacketOpcodesUtils;
import emu.grasscutter.tools.Dumpers;
import org.slf4j.LoggerFactory;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Function;
import org.slf4j.LoggerFactory;
import static emu.grasscutter.config.Configuration.*;
/** A parser for start-up arguments. */
public final class StartupArguments {
/* A map of parameter -> argument handler. */
private static final Map<String, Function<String, Boolean>> argumentHandlers =
Map.of(
"-dumppacketids",
parameter -> {
PacketOpcodesUtils.dumpPacketIds();
return true;
},
"-version", StartupArguments::printVersion,
"-debug", StartupArguments::enableDebug,
"-lang",
parameter -> {
Grasscutter.setPreferredLanguage(parameter);
return false;
},
"-game",
parameter -> {
Grasscutter.setRunModeOverride(ServerRunMode.GAME_ONLY);
return false;
},
"-dispatch",
parameter -> {
Grasscutter.setRunModeOverride(ServerRunMode.DISPATCH_ONLY);
return false;
},
"-test",
parameter -> {
// Disable the console.
SERVER.game.enableConsole = false;
// Disable HTTP encryption.
SERVER.http.encryption.useEncryption = false;
return false;
},
"-dump", StartupArguments::dump,
private static final Map<String, Function<String, Boolean>> argumentHandlers = new HashMap<>() {
{
putAll(Map.of("-dumppacketids",
parameter -> {
PacketOpcodesUtils.dumpPacketIds();
return true;
},
"-version", StartupArguments::printVersion,
"-debug", StartupArguments::enableDebug,
"-lang",
parameter -> {
Grasscutter.setPreferredLanguage(parameter);
return false;
},
"-game",
parameter -> {
Grasscutter.setRunModeOverride(Grasscutter.ServerRunMode.GAME_ONLY);
return false;
},
"-dispatch",
parameter -> {
Grasscutter.setRunModeOverride(Grasscutter.ServerRunMode.DISPATCH_ONLY);
return false;
},
"-noconsole",
parameter -> {
Grasscutter.setNoConsole(true);
return false;
},
"-test",
parameter -> {
// Disable the console.
SERVER.game.enableConsole = false;
// Disable HTTP encryption.
SERVER.http.encryption.useEncryption = false;
return false;
},
"-dump", StartupArguments::dump,
// Aliases.
"-v", StartupArguments::printVersion,
"-debugall",
parameter -> {
StartupArguments.enableDebug("all");
return false;
});
// Aliases.
"-v", StartupArguments::printVersion
));
putAll(Map.of(
"-debugall",
parameter -> {
StartupArguments.enableDebug("all");
return false;
}
));
}
};
private StartupArguments() {
// This class is not meant to be instantiated.