mirror of
https://github.com/Grasscutters/Grasscutter.git
synced 2026-05-26 20:50:47 +08:00
Separate the dispatch and game servers (pt. 1)
gacha is still broken, handbook still needs to be done
This commit is contained in:
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user