mirror of
https://github.com/Grasscutters/Grasscutter.git
synced 2025-12-22 03:45:10 +01:00
Separate the dispatch and game servers (pt. 1)
gacha is still broken, handbook still needs to be done
This commit is contained in:
@@ -27,6 +27,12 @@ public final class HttpServer {
|
||||
* Configures the Javalin application.
|
||||
*/
|
||||
public HttpServer() {
|
||||
// Check if we are in game only mode.
|
||||
if (Grasscutter.getRunMode() == Grasscutter.ServerRunMode.GAME_ONLY) {
|
||||
this.javalin = null;
|
||||
return;
|
||||
}
|
||||
|
||||
this.javalin = Javalin.create(config -> {
|
||||
// Set the Javalin HTTP server.
|
||||
config.jetty.server(HttpServer::createServer);
|
||||
@@ -51,6 +57,13 @@ public final class HttpServer {
|
||||
|
||||
// Static files should be added like this https://javalin.io/documentation#static-files
|
||||
});
|
||||
|
||||
this.javalin.exception(Exception.class, (exception, ctx) -> {
|
||||
ctx.status(500).result("Internal server error. %s"
|
||||
.formatted(exception.getMessage()));
|
||||
Grasscutter.getLogger().debug("Exception thrown: " +
|
||||
exception.getMessage(), exception);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
package emu.grasscutter.server.http.dispatch;
|
||||
|
||||
import static emu.grasscutter.utils.Language.translate;
|
||||
|
||||
import emu.grasscutter.Grasscutter;
|
||||
import emu.grasscutter.auth.AuthenticationSystem;
|
||||
import emu.grasscutter.auth.OAuthAuthenticator.ClientType;
|
||||
@@ -14,8 +12,10 @@ import emu.grasscutter.utils.JsonUtils;
|
||||
import io.javalin.Javalin;
|
||||
import io.javalin.http.Context;
|
||||
|
||||
/** Handles requests related to authentication. (aka dispatch) */
|
||||
public final class DispatchHandler implements Router {
|
||||
import static emu.grasscutter.utils.Language.translate;
|
||||
|
||||
/** Handles requests related to authentication. */
|
||||
public final class AuthenticationHandler implements Router {
|
||||
/**
|
||||
* @route /hk4e_global/mdk/shield/api/login
|
||||
*/
|
||||
@@ -92,19 +92,19 @@ public final class DispatchHandler implements Router {
|
||||
public void applyRoutes(Javalin javalin) {
|
||||
// OS
|
||||
// Username & Password login (from client).
|
||||
javalin.post("/hk4e_global/mdk/shield/api/login", DispatchHandler::clientLogin);
|
||||
javalin.post("/hk4e_global/mdk/shield/api/login", AuthenticationHandler::clientLogin);
|
||||
// Cached token login (from registry).
|
||||
javalin.post("/hk4e_global/mdk/shield/api/verify", DispatchHandler::tokenLogin);
|
||||
javalin.post("/hk4e_global/mdk/shield/api/verify", AuthenticationHandler::tokenLogin);
|
||||
// Combo token login (from session key).
|
||||
javalin.post("/hk4e_global/combo/granter/login/v2/login", DispatchHandler::sessionKeyLogin);
|
||||
javalin.post("/hk4e_global/combo/granter/login/v2/login", AuthenticationHandler::sessionKeyLogin);
|
||||
|
||||
// CN
|
||||
// Username & Password login (from client).
|
||||
javalin.post("/hk4e_cn/mdk/shield/api/login", DispatchHandler::clientLogin);
|
||||
javalin.post("/hk4e_cn/mdk/shield/api/login", AuthenticationHandler::clientLogin);
|
||||
// Cached token login (from registry).
|
||||
javalin.post("/hk4e_cn/mdk/shield/api/verify", DispatchHandler::tokenLogin);
|
||||
javalin.post("/hk4e_cn/mdk/shield/api/verify", AuthenticationHandler::tokenLogin);
|
||||
// Combo token login (from session key).
|
||||
javalin.post("/hk4e_cn/combo/granter/login/v2/login", DispatchHandler::sessionKeyLogin);
|
||||
javalin.post("/hk4e_cn/combo/granter/login/v2/login", AuthenticationHandler::sessionKeyLogin);
|
||||
|
||||
// External login (from other clients).
|
||||
javalin.get(
|
||||
@@ -1,7 +1,5 @@
|
||||
package emu.grasscutter.server.http.dispatch;
|
||||
|
||||
import static emu.grasscutter.config.Configuration.*;
|
||||
|
||||
import com.google.gson.JsonArray;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.protobuf.ByteString;
|
||||
@@ -23,11 +21,15 @@ import emu.grasscutter.utils.JsonUtils;
|
||||
import emu.grasscutter.utils.Utils;
|
||||
import io.javalin.Javalin;
|
||||
import io.javalin.http.Context;
|
||||
import org.slf4j.Logger;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.*;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.regex.Pattern;
|
||||
import org.slf4j.Logger;
|
||||
|
||||
import static emu.grasscutter.config.Configuration.*;
|
||||
|
||||
/** Handles requests related to region queries. */
|
||||
public final class RegionHandler implements Router {
|
||||
@@ -57,8 +59,8 @@ public final class RegionHandler implements Router {
|
||||
var servers = new ArrayList<RegionSimpleInfo>();
|
||||
var usedNames = new ArrayList<String>(); // List to check for potential naming conflicts.
|
||||
|
||||
var configuredRegions = new ArrayList<>(List.of(DISPATCH_INFO.regions));
|
||||
if (SERVER.runMode != ServerRunMode.HYBRID && configuredRegions.size() == 0) {
|
||||
var configuredRegions = new ArrayList<>(DISPATCH_INFO.regions);
|
||||
if (Grasscutter.getRunMode() != ServerRunMode.HYBRID && configuredRegions.size() == 0) {
|
||||
Grasscutter.getLogger()
|
||||
.error(
|
||||
"[Dispatch] There are no game servers available. Exiting due to unplayable state.");
|
||||
@@ -340,6 +342,7 @@ public final class RegionHandler implements Router {
|
||||
* @return A {@link QueryCurrRegionHttpRsp} object.
|
||||
*/
|
||||
public static QueryCurrRegionHttpRsp getCurrentRegion() {
|
||||
return SERVER.runMode == ServerRunMode.HYBRID ? regions.get("os_usa").getRegionQuery() : null;
|
||||
return Grasscutter.getRunMode() == ServerRunMode.HYBRID ?
|
||||
regions.get("os_usa").getRegionQuery() : null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package emu.grasscutter.server.http.documentation;
|
||||
|
||||
import emu.grasscutter.Grasscutter;
|
||||
import emu.grasscutter.server.http.Router;
|
||||
import io.javalin.Javalin;
|
||||
|
||||
@@ -7,13 +8,16 @@ public final class DocumentationServerHandler implements Router {
|
||||
|
||||
@Override
|
||||
public void applyRoutes(Javalin javalin) {
|
||||
final RootRequestHandler root = new RootRequestHandler();
|
||||
final HandbookRequestHandler handbook = new HandbookRequestHandler();
|
||||
final GachaMappingRequestHandler gachaMapping = new GachaMappingRequestHandler();
|
||||
final var root = new RootRequestHandler();
|
||||
final var gachaMapping = new GachaMappingRequestHandler();
|
||||
|
||||
// TODO: Removal
|
||||
// TODO: Forward /documentation requests to https://grasscutter.io/wiki
|
||||
javalin.get("/documentation/handbook", handbook::handle);
|
||||
if (Grasscutter.getRunMode() != Grasscutter.ServerRunMode.DISPATCH_ONLY) {
|
||||
final var handbook = new HandbookRequestHandler();
|
||||
javalin.get("/documentation/handbook", handbook::handle);
|
||||
}
|
||||
|
||||
javalin.get("/documentation/gachamapping", gachaMapping::handle);
|
||||
javalin.get("/documentation", root::handle);
|
||||
}
|
||||
|
||||
@@ -1,28 +1,27 @@
|
||||
package emu.grasscutter.server.http.handlers;
|
||||
|
||||
import static emu.grasscutter.utils.Language.translate;
|
||||
|
||||
import com.google.gson.JsonObject;
|
||||
import emu.grasscutter.Grasscutter;
|
||||
import emu.grasscutter.database.DatabaseHelper;
|
||||
import emu.grasscutter.game.Account;
|
||||
import emu.grasscutter.game.gacha.GachaBanner;
|
||||
import emu.grasscutter.game.gacha.GachaSystem;
|
||||
import emu.grasscutter.game.player.Player;
|
||||
import emu.grasscutter.server.http.Router;
|
||||
import emu.grasscutter.utils.DispatchUtils;
|
||||
import emu.grasscutter.utils.FileUtils;
|
||||
import emu.grasscutter.utils.Utils;
|
||||
import io.javalin.Javalin;
|
||||
import io.javalin.http.ContentType;
|
||||
import io.javalin.http.Context;
|
||||
import io.javalin.http.staticfiles.Location;
|
||||
import lombok.Getter;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Arrays;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.Set;
|
||||
import lombok.Getter;
|
||||
|
||||
import static emu.grasscutter.utils.Language.translate;
|
||||
|
||||
/** Handles all gacha-related HTTP requests. */
|
||||
public final class GachaHandler implements Router {
|
||||
@@ -33,55 +32,52 @@ public final class GachaHandler implements Router {
|
||||
public static final String gachaMappings = gachaMappingsPath.toString();
|
||||
|
||||
private static void gachaRecords(Context ctx) {
|
||||
String sessionKey = ctx.queryParam("s");
|
||||
Account account = DatabaseHelper.getAccountBySessionKey(sessionKey);
|
||||
var sessionKey = ctx.queryParam("s");
|
||||
var account = DatabaseHelper.getAccountBySessionKey(sessionKey);
|
||||
if (account == null) {
|
||||
ctx.status(403).result("Requested account was not found");
|
||||
return;
|
||||
}
|
||||
Player player = Grasscutter.getGameServer().getPlayerByAccountId(account.getId());
|
||||
if (player == null) {
|
||||
ctx.status(403).result("No player associated with requested account");
|
||||
ctx.status(403).result("Requested account was not found.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Get page and gacha type.
|
||||
int page = 0, gachaType = 0;
|
||||
if (ctx.queryParam("p") != null) page = Integer.parseInt(ctx.queryParam("p"));
|
||||
if (ctx.queryParam("gachaType") != null)
|
||||
gachaType = Integer.parseInt(ctx.queryParam("gachaType"));
|
||||
|
||||
String records = DatabaseHelper.getGachaRecords(player.getUid(), page, gachaType).toString();
|
||||
long maxPage = DatabaseHelper.getGachaRecordsMaxPage(player.getUid(), page, gachaType);
|
||||
var pageStr = ctx.queryParam("p");
|
||||
if (pageStr != null) page = Integer.parseInt(pageStr);
|
||||
|
||||
String template =
|
||||
var gachaTypeStr = ctx.queryParam("gachaType");
|
||||
if (gachaTypeStr != null) gachaType = Integer.parseInt(gachaTypeStr);
|
||||
|
||||
// Make request to dispatch server.
|
||||
var data = DispatchUtils.fetchGachaRecords(
|
||||
account.getId(), page, gachaType);
|
||||
var records = data.get("records").getAsString();
|
||||
var maxPage = data.get("maxPage").getAsLong();
|
||||
|
||||
var locale = account.getLocale();
|
||||
var template =
|
||||
new String(
|
||||
FileUtils.read(FileUtils.getDataPath("gacha/records.html")), StandardCharsets.UTF_8)
|
||||
.replace("{{REPLACE_RECORDS}}", records)
|
||||
.replace("{{REPLACE_MAXPAGE}}", String.valueOf(maxPage))
|
||||
.replace("{{TITLE}}", translate(player, "gacha.records.title"))
|
||||
.replace("{{DATE}}", translate(player, "gacha.records.date"))
|
||||
.replace("{{ITEM}}", translate(player, "gacha.records.item"))
|
||||
.replace("'{{REPLACE_RECORDS}}'", records)
|
||||
.replace("'{{REPLACE_MAXPAGE}}'", String.valueOf(maxPage))
|
||||
.replace("{{TITLE}}", translate(locale, "gacha.records.title"))
|
||||
.replace("{{DATE}}", translate(locale, "gacha.records.date"))
|
||||
.replace("{{ITEM}}", translate(locale, "gacha.records.item"))
|
||||
.replace("{{LANGUAGE}}", Utils.getLanguageCode(account.getLocale()));
|
||||
ctx.contentType(ContentType.TEXT_HTML);
|
||||
ctx.result(template);
|
||||
}
|
||||
|
||||
private static void gachaDetails(Context ctx) {
|
||||
Path detailsTemplate = FileUtils.getDataPath("gacha/details.html");
|
||||
String sessionKey = ctx.queryParam("s");
|
||||
Account account = DatabaseHelper.getAccountBySessionKey(sessionKey);
|
||||
var detailsTemplate = FileUtils.getDataPath("gacha/details.html");
|
||||
var sessionKey = ctx.queryParam("s");
|
||||
var account = DatabaseHelper.getAccountBySessionKey(sessionKey);
|
||||
if (account == null) {
|
||||
ctx.status(403).result("Requested account was not found");
|
||||
return;
|
||||
}
|
||||
Player player = Grasscutter.getGameServer().getPlayerByAccountId(account.getId());
|
||||
if (player == null) {
|
||||
ctx.status(403).result("No player associated with requested account");
|
||||
return;
|
||||
}
|
||||
|
||||
String template;
|
||||
try {
|
||||
String template;try {
|
||||
template = Files.readString(detailsTemplate);
|
||||
} catch (IOException e) {
|
||||
Grasscutter.getLogger().warn("Failed to read data/gacha/details.html");
|
||||
@@ -90,27 +86,35 @@ public final class GachaHandler implements Router {
|
||||
}
|
||||
|
||||
// Add translated title etc. to the page.
|
||||
var locale = account.getLocale();
|
||||
template =
|
||||
template
|
||||
.replace("{{TITLE}}", translate(player, "gacha.details.title"))
|
||||
.replace("{{TITLE}}", translate(locale, "gacha.details.title"))
|
||||
.replace(
|
||||
"{{AVAILABLE_FIVE_STARS}}", translate(player, "gacha.details.available_five_stars"))
|
||||
"{{AVAILABLE_FIVE_STARS}}", translate(locale, "gacha.details.available_five_stars"))
|
||||
.replace(
|
||||
"{{AVAILABLE_FOUR_STARS}}", translate(player, "gacha.details.available_four_stars"))
|
||||
"{{AVAILABLE_FOUR_STARS}}", translate(locale, "gacha.details.available_four_stars"))
|
||||
.replace(
|
||||
"{{AVAILABLE_THREE_STARS}}",
|
||||
translate(player, "gacha.details.available_three_stars"))
|
||||
translate(locale, "gacha.details.available_three_stars"))
|
||||
.replace("{{LANGUAGE}}", Utils.getLanguageCode(account.getLocale()));
|
||||
|
||||
// Get the banner info for the banner we want.
|
||||
int scheduleId = Integer.parseInt(ctx.queryParam("scheduleId"));
|
||||
GachaSystem manager = Grasscutter.getGameServer().getGachaSystem();
|
||||
GachaBanner banner = manager.getGachaBanners().get(scheduleId);
|
||||
var scheduleIdStr = ctx.queryParam("scheduleId");
|
||||
if (scheduleIdStr == null) {
|
||||
ctx.status(400).result("Missing scheduleId parameter");
|
||||
return;
|
||||
}
|
||||
|
||||
var scheduleId = Integer.parseInt(scheduleIdStr);
|
||||
var manager = Grasscutter.getGameServer().getGachaSystem();
|
||||
var banner = manager.getGachaBanners().get(scheduleId);
|
||||
|
||||
// Add 5-star items.
|
||||
Set<String> fiveStarItems = new LinkedHashSet<>();
|
||||
var fiveStarItems = new LinkedHashSet<String>();
|
||||
|
||||
Arrays.stream(banner.getRateUpItems5()).forEach(i -> fiveStarItems.add(Integer.toString(i)));
|
||||
Arrays.stream(banner.getRateUpItems5())
|
||||
.forEach(i -> fiveStarItems.add(Integer.toString(i)));
|
||||
Arrays.stream(banner.getFallbackItems5Pool1())
|
||||
.forEach(i -> fiveStarItems.add(Integer.toString(i)));
|
||||
Arrays.stream(banner.getFallbackItems5Pool2())
|
||||
@@ -119,9 +123,10 @@ public final class GachaHandler implements Router {
|
||||
template = template.replace("{{FIVE_STARS}}", "[" + String.join(",", fiveStarItems) + "]");
|
||||
|
||||
// Add 4-star items.
|
||||
Set<String> fourStarItems = new LinkedHashSet<>();
|
||||
var fourStarItems = new LinkedHashSet<String>();
|
||||
|
||||
Arrays.stream(banner.getRateUpItems4()).forEach(i -> fourStarItems.add(Integer.toString(i)));
|
||||
Arrays.stream(banner.getRateUpItems4())
|
||||
.forEach(i -> fourStarItems.add(Integer.toString(i)));
|
||||
Arrays.stream(banner.getFallbackItems4Pool1())
|
||||
.forEach(i -> fourStarItems.add(Integer.toString(i)));
|
||||
Arrays.stream(banner.getFallbackItems4Pool2())
|
||||
@@ -130,8 +135,9 @@ public final class GachaHandler implements Router {
|
||||
template = template.replace("{{FOUR_STARS}}", "[" + String.join(",", fourStarItems) + "]");
|
||||
|
||||
// Add 3-star items.
|
||||
Set<String> threeStarItems = new LinkedHashSet<>();
|
||||
Arrays.stream(banner.getFallbackItems3()).forEach(i -> threeStarItems.add(Integer.toString(i)));
|
||||
var threeStarItems = new LinkedHashSet<String>();
|
||||
Arrays.stream(banner.getFallbackItems3())
|
||||
.forEach(i -> threeStarItems.add(Integer.toString(i)));
|
||||
template = template.replace("{{THREE_STARS}}", "[" + String.join(",", threeStarItems) + "]");
|
||||
|
||||
// Done.
|
||||
@@ -139,6 +145,30 @@ public final class GachaHandler implements Router {
|
||||
ctx.result(template);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches the gacha records for the specified player.
|
||||
*
|
||||
* @param player The player to fetch the records for.
|
||||
* @param response The response to write to.
|
||||
* @param page The page to fetch.
|
||||
* @param type The gacha type to fetch.
|
||||
*/
|
||||
public static void fetchGachaRecords(
|
||||
Player player, JsonObject response,
|
||||
int page, int type
|
||||
) {
|
||||
var playerId = player.getUid();
|
||||
var records = DatabaseHelper.getGachaRecords(
|
||||
playerId, page, type).toString();
|
||||
var maxPage = DatabaseHelper.getGachaRecordsMaxPage(
|
||||
playerId, page, type);
|
||||
|
||||
// Finish the response.
|
||||
response.addProperty("retcode", 0);
|
||||
response.addProperty("records", records);
|
||||
response.addProperty("maxPage", maxPage);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void applyRoutes(Javalin javalin) {
|
||||
javalin.get("/gacha", GachaHandler::gachaRecords);
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
package emu.grasscutter.server.http.objects;
|
||||
|
||||
import lombok.Builder;
|
||||
|
||||
/**
|
||||
* This request object is used in both token-related authenticators.
|
||||
*/
|
||||
@Builder
|
||||
public class LoginTokenRequestJson {
|
||||
public String uid;
|
||||
public String token;
|
||||
|
||||
Reference in New Issue
Block a user