Commit a8293102 authored by Melledy's avatar Melledy Committed by GitHub
Browse files

Merge branch 'development' into stable

parents 304b9cb8 ecf7a81a
package emu.grasscutter.server.dispatch.http;
import java.io.File;
import java.io.IOException;
import emu.grasscutter.Grasscutter;
import emu.grasscutter.database.DatabaseHelper;
import emu.grasscutter.game.Account;
import emu.grasscutter.utils.FileUtils;
import emu.grasscutter.utils.Utils;
import express.http.HttpContextHandler;
import express.http.Request;
import express.http.Response;
public final class GachaRecordHandler implements HttpContextHandler {
String render_template;
public GachaRecordHandler() {
File template = new File(Utils.toFilePath(Grasscutter.getConfig().DATA_FOLDER + "/gacha_records.html"));
if (template.exists()) {
// Load from cache
render_template = new String(FileUtils.read(template));
} else {
render_template = "{{REPLACE_RECORD}}";
}
}
@Override
public void handle(Request req, Response res) throws IOException {
// Grasscutter.getLogger().info( req.query().toString() );
String sessionKey = req.query("s");
int page = 0;
int gachaType = 0;
if (req.query("p") != null) {
page = Integer.valueOf(req.query("p"));
}
if (req.query("gachaType") != null) {
gachaType = Integer.valueOf(req.query("gachaType"));
}
Account account = DatabaseHelper.getAccountBySessionKey(sessionKey);
if (account != null) {
String records = DatabaseHelper.getGachaRecords(account.getPlayerUid(), page, gachaType).toString();
// Grasscutter.getLogger().info(records);
String response = render_template.replace("{{REPLACE_RECORD}}", records)
.replace("{{REPLACE_MAXPAGE}}", String.valueOf(DatabaseHelper.getGachaRecordsMaxPage(account.getPlayerUid(), page, gachaType)));
res.send(response);
} else {
res.send("No account found.");
}
}
}
package emu.grasscutter.server.event.game;
import emu.grasscutter.game.player.Player;
import emu.grasscutter.server.event.types.GameEvent;
import emu.grasscutter.server.event.types.ServerEvent;
public class CommandResponseEvent extends ServerEvent {
private String message;
private Player player;
public CommandResponseEvent(Type type, Player player,String message) {
super(type);
this.message = message;
this.player = player;
}
public String getMessage() {
return message;
}
public Player getPlayer() {
return player;
}
}
package emu.grasscutter.server.event.internal;
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.core.AppenderBase;
import emu.grasscutter.server.event.types.ServerEvent;
public class ServerLogEvent extends ServerEvent {
ILoggingEvent loggingEvent;
String consoleMessage;
public ServerLogEvent(Type type, ILoggingEvent loggingEvent, String consoleMessage) {
super(type);
this.loggingEvent = loggingEvent;
this.consoleMessage = consoleMessage;
}
public ILoggingEvent getLoggingEvent() { return loggingEvent; }
public String getConsoleMessage() {
return consoleMessage;
}
}
...@@ -10,11 +10,14 @@ import emu.grasscutter.game.drop.DropManager; ...@@ -10,11 +10,14 @@ import emu.grasscutter.game.drop.DropManager;
import emu.grasscutter.game.dungeons.DungeonManager; import emu.grasscutter.game.dungeons.DungeonManager;
import emu.grasscutter.game.expedition.ExpeditionManager; import emu.grasscutter.game.expedition.ExpeditionManager;
import emu.grasscutter.game.gacha.GachaManager; import emu.grasscutter.game.gacha.GachaManager;
import emu.grasscutter.game.managers.ChatManager; import emu.grasscutter.game.managers.ChatManager.ChatManager;
import emu.grasscutter.game.managers.ChatManager.ChatManagerHandler;
import emu.grasscutter.game.managers.InventoryManager; import emu.grasscutter.game.managers.InventoryManager;
import emu.grasscutter.game.managers.MultiplayerManager; import emu.grasscutter.game.managers.MultiplayerManager;
import emu.grasscutter.game.player.Player; import emu.grasscutter.game.player.Player;
import emu.grasscutter.game.quest.ServerQuestHandler;
import emu.grasscutter.game.shop.ShopManager; import emu.grasscutter.game.shop.ShopManager;
import emu.grasscutter.game.tower.TowerScheduleManager;
import emu.grasscutter.game.world.World; import emu.grasscutter.game.world.World;
import emu.grasscutter.net.packet.PacketHandler; import emu.grasscutter.net.packet.PacketHandler;
import emu.grasscutter.net.proto.SocialDetailOuterClass.SocialDetail; import emu.grasscutter.net.proto.SocialDetailOuterClass.SocialDetail;
...@@ -24,25 +27,25 @@ import emu.grasscutter.server.event.game.ServerTickEvent; ...@@ -24,25 +27,25 @@ import emu.grasscutter.server.event.game.ServerTickEvent;
import emu.grasscutter.server.event.internal.ServerStartEvent; import emu.grasscutter.server.event.internal.ServerStartEvent;
import emu.grasscutter.server.event.internal.ServerStopEvent; import emu.grasscutter.server.event.internal.ServerStopEvent;
import emu.grasscutter.task.TaskMap; import emu.grasscutter.task.TaskMap;
import emu.grasscutter.BuildConfig;
import java.net.InetSocketAddress; import java.net.InetSocketAddress;
import java.time.OffsetDateTime; import java.time.OffsetDateTime;
import java.util.*; import java.util.*;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import static emu.grasscutter.utils.Language.translate; import static emu.grasscutter.utils.Language.translate;
import static emu.grasscutter.Configuration.*;
public final class GameServer extends KcpServer { public final class GameServer extends KcpServer {
private final InetSocketAddress address; private final InetSocketAddress address;
private final GameServerPacketHandler packetHandler; private final GameServerPacketHandler packetHandler;
private final ServerQuestHandler questHandler;
private final Map<Integer, Player> players; private final Map<Integer, Player> players;
private final Set<World> worlds; private final Set<World> worlds;
private final ChatManager chatManager; private ChatManagerHandler chatManager;
private final InventoryManager inventoryManager; private final InventoryManager inventoryManager;
private final GachaManager gachaManager; private final GachaManager gachaManager;
private final ShopManager shopManager; private final ShopManager shopManager;
...@@ -54,20 +57,30 @@ public final class GameServer extends KcpServer { ...@@ -54,20 +57,30 @@ public final class GameServer extends KcpServer {
private final DropManager dropManager; private final DropManager dropManager;
private final CombineManger combineManger; private final CombineManger combineManger;
private final TowerScheduleManager towerScheduleManager;
private static InetSocketAddress getAdapterInetSocketAddress(){
InetSocketAddress inetSocketAddress = null;
if(GAME_INFO.bindAddress.equals("")){
inetSocketAddress=new InetSocketAddress(GAME_INFO.bindPort);
}else{
inetSocketAddress=new InetSocketAddress(
GAME_INFO.bindAddress,
GAME_INFO.bindPort
);
}
return inetSocketAddress;
}
public GameServer() { public GameServer() {
this(new InetSocketAddress( this(getAdapterInetSocketAddress());
Grasscutter.getConfig().getGameServerOptions().Ip,
Grasscutter.getConfig().getGameServerOptions().Port
));
} }
public GameServer(InetSocketAddress address) { public GameServer(InetSocketAddress address) {
super(address); super(address);
this.setServerInitializer(new GameServerInitializer(this)); this.setServerInitializer(new GameServerInitializer(this));
this.address = address; this.address = address;
this.packetHandler = new GameServerPacketHandler(PacketHandler.class); this.packetHandler = new GameServerPacketHandler(PacketHandler.class);
this.questHandler = new ServerQuestHandler();
this.players = new ConcurrentHashMap<>(); this.players = new ConcurrentHashMap<>();
this.worlds = Collections.synchronizedSet(new HashSet<>()); this.worlds = Collections.synchronizedSet(new HashSet<>());
...@@ -82,7 +95,7 @@ public final class GameServer extends KcpServer { ...@@ -82,7 +95,7 @@ public final class GameServer extends KcpServer {
this.dropManager = new DropManager(this); this.dropManager = new DropManager(this);
this.expeditionManager = new ExpeditionManager(this); this.expeditionManager = new ExpeditionManager(this);
this.combineManger = new CombineManger(this); this.combineManger = new CombineManger(this);
this.towerScheduleManager = new TowerScheduleManager(this);
// Hook into shutdown event. // Hook into shutdown event.
Runtime.getRuntime().addShutdownHook(new Thread(this::onServerShutdown)); Runtime.getRuntime().addShutdownHook(new Thread(this::onServerShutdown));
} }
...@@ -91,6 +104,10 @@ public final class GameServer extends KcpServer { ...@@ -91,6 +104,10 @@ public final class GameServer extends KcpServer {
return packetHandler; return packetHandler;
} }
public ServerQuestHandler getQuestHandler() {
return questHandler;
}
public Map<Integer, Player> getPlayers() { public Map<Integer, Player> getPlayers() {
return players; return players;
} }
...@@ -99,9 +116,13 @@ public final class GameServer extends KcpServer { ...@@ -99,9 +116,13 @@ public final class GameServer extends KcpServer {
return worlds; return worlds;
} }
public ChatManager getChatManager() { public ChatManagerHandler getChatManager() {
return chatManager; return chatManager;
} }
public void setChatManager(ChatManagerHandler chatManager) {
this.chatManager = chatManager;
}
public InventoryManager getInventoryManager() { public InventoryManager getInventoryManager() {
return inventoryManager; return inventoryManager;
...@@ -139,6 +160,10 @@ public final class GameServer extends KcpServer { ...@@ -139,6 +160,10 @@ public final class GameServer extends KcpServer {
return this.combineManger; return this.combineManger;
} }
public TowerScheduleManager getTowerScheduleManager() {
return towerScheduleManager;
}
public TaskMap getTaskMap() { public TaskMap getTaskMap() {
return this.taskMap; return this.taskMap;
} }
...@@ -166,12 +191,17 @@ public final class GameServer extends KcpServer { ...@@ -166,12 +191,17 @@ public final class GameServer extends KcpServer {
// Check database if character isnt here // Check database if character isnt here
if (player == null) { if (player == null) {
player = DatabaseHelper.getPlayerById(id); player = DatabaseHelper.getPlayerByUid(id);
} }
return player; return player;
} }
public Player getPlayerByAccountId(String accountId) {
Optional<Player> playerOpt = getPlayers().values().stream().filter(player -> player.getAccount().getId().equals(accountId)).findFirst();
return playerOpt.orElse(null);
}
public SocialDetail.Builder getSocialDetailByUid(int id) { public SocialDetail.Builder getSocialDetailByUid(int id) {
// Get from online players // Get from online players
Player player = this.getPlayerByUid(id, true); Player player = this.getPlayerByUid(id, true);
......
...@@ -13,8 +13,10 @@ public class GameServerInitializer extends KcpServerInitializer { ...@@ -13,8 +13,10 @@ public class GameServerInitializer extends KcpServerInitializer {
@Override @Override
protected void initChannel(UkcpChannel ch) throws Exception { protected void initChannel(UkcpChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline(); ChannelPipeline pipeline=null;
GameSession session = new GameSession(server); if(ch!=null){
pipeline.addLast(session); pipeline = ch.pipeline();
}
new GameSession(server,pipeline);
} }
} }
...@@ -14,6 +14,8 @@ import emu.grasscutter.server.game.GameSession.SessionState; ...@@ -14,6 +14,8 @@ import emu.grasscutter.server.game.GameSession.SessionState;
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 static emu.grasscutter.Configuration.*;
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
public class GameServerPacketHandler { public class GameServerPacketHandler {
private final Int2ObjectMap<PacketHandler> handlers; private final Int2ObjectMap<PacketHandler> handlers;
...@@ -92,7 +94,7 @@ public class GameServerPacketHandler { ...@@ -92,7 +94,7 @@ public class GameServerPacketHandler {
} }
// Log unhandled packets // Log unhandled packets
if (Grasscutter.getConfig().DebugMode == ServerDebugMode.MISSING) { if (SERVER.debugLevel == ServerDebugMode.MISSING) {
Grasscutter.getLogger().info("Unhandled packet (" + opcode + "): " + emu.grasscutter.net.packet.PacketOpcodesUtil.getOpcodeName(opcode)); Grasscutter.getLogger().info("Unhandled packet (" + opcode + "): " + emu.grasscutter.net.packet.PacketOpcodesUtil.getOpcodeName(opcode));
} }
} }
......
...@@ -3,7 +3,8 @@ package emu.grasscutter.server.game; ...@@ -3,7 +3,8 @@ package emu.grasscutter.server.game;
import java.io.File; import java.io.File;
import java.net.InetSocketAddress; import java.net.InetSocketAddress;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.util.HashSet; import java.util.Iterator;
import java.util.Map;
import java.util.Set; import java.util.Set;
import emu.grasscutter.Grasscutter; import emu.grasscutter.Grasscutter;
...@@ -18,14 +19,17 @@ import emu.grasscutter.server.event.game.SendPacketEvent; ...@@ -18,14 +19,17 @@ import emu.grasscutter.server.event.game.SendPacketEvent;
import emu.grasscutter.utils.Crypto; import emu.grasscutter.utils.Crypto;
import emu.grasscutter.utils.FileUtils; import emu.grasscutter.utils.FileUtils;
import emu.grasscutter.utils.Utils; import emu.grasscutter.utils.Utils;
import io.jpower.kcp.netty.UkcpChannel;
import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled; import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelPipeline;
import static emu.grasscutter.utils.Language.translate; import static emu.grasscutter.utils.Language.translate;
import static emu.grasscutter.Configuration.*;
public class GameSession extends KcpChannel { public class GameSession extends KcpChannel {
private GameServer server; private final GameServer server;
private Account account; private Account account;
private Player player; private Player player;
...@@ -36,11 +40,30 @@ public class GameSession extends KcpChannel { ...@@ -36,11 +40,30 @@ public class GameSession extends KcpChannel {
private int clientTime; private int clientTime;
private long lastPingTime; private long lastPingTime;
private int lastClientSeq = 10; private int lastClientSeq = 10;
private final ChannelPipeline pipeline;
@Override
public void close() {
setState(SessionState.INACTIVE);
//send disconnection pack in case of reconnection
try {
send(new BasePacket(PacketOpcodes.ServerDisconnectClientNotify));
}catch (Throwable ignore){
}
super.close();
}
public GameSession(GameServer server) { public GameSession(GameServer server) {
this(server,null);
}
public GameSession(GameServer server, ChannelPipeline pipeline) {
this.server = server; this.server = server;
this.state = SessionState.WAITING_FOR_TOKEN; this.state = SessionState.WAITING_FOR_TOKEN;
this.lastPingTime = System.currentTimeMillis(); this.lastPingTime = System.currentTimeMillis();
this.pipeline = pipeline;
if(pipeline!=null) {
pipeline.addLast(this);
}
} }
public GameServer getServer() { public GameServer getServer() {
...@@ -124,13 +147,17 @@ public class GameSession extends KcpChannel { ...@@ -124,13 +147,17 @@ public class GameSession extends KcpChannel {
// Set state so no more packets can be handled // Set state so no more packets can be handled
this.setState(SessionState.INACTIVE); this.setState(SessionState.INACTIVE);
// Save after disconnecting // Save after disconnecting
if (this.isLoggedIn()) { if (this.isLoggedIn()) {
Player player = getPlayer();
// Call logout event. // Call logout event.
getPlayer().onLogout(); player.onLogout();
// Remove from server. }
getServer().getPlayers().remove(getPlayer().getUid()); try {
pipeline.remove(this);
} catch (Throwable ignore) {
} }
} }
...@@ -140,7 +167,7 @@ public class GameSession extends KcpChannel { ...@@ -140,7 +167,7 @@ public class GameSession extends KcpChannel {
} }
public void replayPacket(int opcode, String name) { public void replayPacket(int opcode, String name) {
String filePath = Grasscutter.getConfig().PACKETS_FOLDER + name; String filePath = PACKET(name);
File p = new File(filePath); File p = new File(filePath);
if (!p.exists()) return; if (!p.exists()) return;
...@@ -172,7 +199,7 @@ public class GameSession extends KcpChannel { ...@@ -172,7 +199,7 @@ public class GameSession extends KcpChannel {
} }
// Log // Log
if (Grasscutter.getConfig().DebugMode == ServerDebugMode.ALL) { if (SERVER.debugLevel == ServerDebugMode.ALL) {
logPacket(packet); logPacket(packet);
} }
...@@ -239,7 +266,7 @@ public class GameSession extends KcpChannel { ...@@ -239,7 +266,7 @@ public class GameSession extends KcpChannel {
} }
// Log packet // Log packet
if (Grasscutter.getConfig().DebugMode == ServerDebugMode.ALL) { if (SERVER.debugLevel == ServerDebugMode.ALL) {
if (!loopPacket.contains(opcode)) { if (!loopPacket.contains(opcode)) {
Grasscutter.getLogger().info("RECV: " + PacketOpcodesUtil.getOpcodeName(opcode) + " (" + opcode + ")"); Grasscutter.getLogger().info("RECV: " + PacketOpcodesUtil.getOpcodeName(opcode) + " (" + opcode + ")");
System.out.println(Utils.bytesToHex(payload)); System.out.println(Utils.bytesToHex(payload));
...@@ -252,6 +279,7 @@ public class GameSession extends KcpChannel { ...@@ -252,6 +279,7 @@ public class GameSession extends KcpChannel {
} catch (Exception e) { } catch (Exception e) {
e.printStackTrace(); e.printStackTrace();
} finally { } finally {
data.release();
packet.release(); packet.release();
} }
} }
......
package emu.grasscutter.server.http;
import emu.grasscutter.Grasscutter;
import emu.grasscutter.Grasscutter.ServerDebugMode;
import emu.grasscutter.utils.FileUtils;
import express.Express;
import express.http.MediaType;
import io.javalin.Javalin;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector;
import org.eclipse.jetty.util.ssl.SslContextFactory;
import java.io.File;
import java.io.IOException;
import static emu.grasscutter.Configuration.*;
import static emu.grasscutter.utils.Language.translate;
/**
* Manages all HTTP-related classes.
* (including dispatch, announcements, gacha, etc.)
*/
public final class HttpServer {
private final Express express;
/**
* Configures the Express application.
*/
public HttpServer() {
this.express = new Express(config -> {
// Set the Express HTTP server.
config.server(HttpServer::createServer);
// Configure encryption/HTTPS/SSL.
config.enforceSsl = HTTP_ENCRYPTION.useEncryption;
// Configure HTTP policies.
if(HTTP_POLICIES.cors.enabled) {
var allowedOrigins = HTTP_POLICIES.cors.allowedOrigins;
if (allowedOrigins.length > 0)
config.enableCorsForOrigin(allowedOrigins);
else config.enableCorsForAllOrigins();
}
// Configure debug logging.
if(SERVER.debugLevel == ServerDebugMode.ALL)
config.enableDevLogging();
// Disable compression on static files.
config.precompressStaticFiles = false;
});
}
/**
* Creates an HTTP(S) server.
* @return A server instance.
*/
@SuppressWarnings("resource")
private static Server createServer() {
Server server = new Server();
ServerConnector serverConnector
= new ServerConnector(server);
if(HTTP_ENCRYPTION.useEncryption) {
var sslContextFactory = new SslContextFactory.Server();
var keystoreFile = new File(HTTP_ENCRYPTION.keystore);
if(!keystoreFile.exists()) {
HTTP_ENCRYPTION.useEncryption = false;
HTTP_ENCRYPTION.useInRouting = false;
Grasscutter.getLogger().warn(translate("messages.dispatch.keystore.no_keystore_error"));
} else try {
sslContextFactory.setKeyStorePath(keystoreFile.getPath());
sslContextFactory.setKeyStorePassword(HTTP_ENCRYPTION.keystorePassword);
} catch (Exception ignored) {
Grasscutter.getLogger().warn(translate("messages.dispatch.keystore.password_error"));
try {
sslContextFactory.setKeyStorePath(keystoreFile.getPath());
sslContextFactory.setKeyStorePassword("123456");
Grasscutter.getLogger().warn(translate("messages.dispatch.keystore.default_password"));
} catch (Exception exception) {
Grasscutter.getLogger().warn(translate("messages.dispatch.keystore.general_error"), exception);
}
} finally {
serverConnector = new ServerConnector(server, sslContextFactory);
}
}
serverConnector.setPort(HTTP_INFO.bindPort);
server.setConnectors(new ServerConnector[]{serverConnector});
return server;
}
/**
* Returns the handle for the Express application.
* @return A Javalin instance.
*/
public Javalin getHandle() {
return this.express.raw();
}
/**
* Initializes the provided class.
* @param router The router class.
* @return Method chaining.
*/
@SuppressWarnings("UnusedReturnValue")
public HttpServer addRouter(Class<? extends Router> router, Object... args) {
// Get all constructor parameters.
Class<?>[] types = new Class<?>[args.length];
for(var argument : args)
types[args.length - 1] = argument.getClass();
try { // Create a router instance & apply routes.
var constructor = router.getDeclaredConstructor(types); // Get the constructor.
var routerInstance = constructor.newInstance(args); // Create instance.
routerInstance.applyRoutes(this.express, this.getHandle()); // Apply routes.
} catch (Exception exception) {
Grasscutter.getLogger().warn(translate("messages.dispatch.router_error"), exception);
} return this;
}
/**
* Starts listening on the HTTP server.
*/
public void start() {
// Attempt to start the HTTP server.
if(HTTP_INFO.bindAddress.equals("")){
this.express.listen(HTTP_INFO.bindPort);
}else{
this.express.listen(HTTP_INFO.bindAddress, HTTP_INFO.bindPort);
}
// Log bind information.
Grasscutter.getLogger().info(translate("messages.dispatch.port_bind", Integer.toString(this.express.raw().port())));
}
/**
* Handles the '/' (index) endpoint on the Express application.
*/
public static class DefaultRequestRouter implements Router {
@Override public void applyRoutes(Express express, Javalin handle) {
express.get("/", (request, response) -> {
File file = new File(HTTP_STATIC_FILES.indexFile);
if(!file.exists())
response.send("""
<!DOCTYPE html>
<html>
<head>
<meta charset="utf8">
</head>
<body>%s</body>
</html>
""".formatted(translate("messages.status.welcome")));
else {
final var filePath = file.getPath();
final MediaType fromExtension = MediaType.getByExtension(filePath.substring(filePath.lastIndexOf(".") + 1));
response.type((fromExtension != null) ? fromExtension.getMIME() : "text/plain")
.send(FileUtils.read(filePath));
}
});
}
}
/**
* Handles unhandled endpoints on the Express application.
*/
public static class UnhandledRequestRouter implements Router {
@Override public void applyRoutes(Express express, Javalin handle) {
handle.error(404, context -> {
if(SERVER.debugLevel == ServerDebugMode.MISSING)
Grasscutter.getLogger().info(translate("messages.dispatch.unhandled_request_error", context.method(), context.url()));
context.contentType("text/html");
File file = new File(HTTP_STATIC_FILES.errorFile);
if(!file.exists())
context.result("""
<!DOCTYPE html>
<html>
<head>
<meta charset="utf8">
</head>
<body>
<img src="https://http.cat/404" />
</body>
</html>
""");
else {
final var filePath = file.getPath();
final MediaType fromExtension = MediaType.getByExtension(filePath.substring(filePath.lastIndexOf(".") + 1));
context.contentType((fromExtension != null) ? fromExtension.getMIME() : "text/plain")
.result(FileUtils.read(filePath));
}
});
}
}
}
package emu.grasscutter.server.http;
import express.Express;
import io.javalin.Javalin;
/**
* Defines routes for an {@link Express} instance.
*/
public interface Router {
/**
* Called when the router is initialized by Express.
* @param express An Express instance.
*/
void applyRoutes(Express express, Javalin handle);
}
package emu.grasscutter.server.http.dispatch;
import emu.grasscutter.Grasscutter;
import emu.grasscutter.auth.AuthenticationSystem;
import emu.grasscutter.auth.OAuthAuthenticator;
import emu.grasscutter.auth.OAuthAuthenticator.ClientType;
import emu.grasscutter.server.http.Router;
import emu.grasscutter.server.http.objects.*;
import emu.grasscutter.server.http.objects.ComboTokenReqJson.LoginTokenData;
import emu.grasscutter.utils.Utils;
import express.Express;
import express.http.Request;
import express.http.Response;
import io.javalin.Javalin;
import static emu.grasscutter.utils.Language.translate;
/**
* Handles requests related to authentication. (aka dispatch)
*/
public final class DispatchHandler implements Router {
@Override public void applyRoutes(Express express, Javalin handle) {
// Username & Password login (from client).
express.post("/hk4e_global/mdk/shield/api/login", DispatchHandler::clientLogin);
// Cached token login (from registry).
express.post("/hk4e_global/mdk/shield/api/verify", DispatchHandler::tokenLogin);
// Combo token login (from session key).
express.post("/hk4e_global/combo/granter/login/v2/login", DispatchHandler::sessionKeyLogin);
// External login (from other clients).
express.get("/authentication/type", (request, response) -> response.send(Grasscutter.getAuthenticationSystem().getClass().getSimpleName()));
express.post("/authentication/login", (request, response) -> Grasscutter.getAuthenticationSystem().getExternalAuthenticator()
.handleLogin(AuthenticationSystem.fromExternalRequest(request, response)));
express.post("/authentication/register", (request, response) -> Grasscutter.getAuthenticationSystem().getExternalAuthenticator()
.handleAccountCreation(AuthenticationSystem.fromExternalRequest(request, response)));
express.post("/authentication/change_password", (request, response) -> Grasscutter.getAuthenticationSystem().getExternalAuthenticator()
.handlePasswordReset(AuthenticationSystem.fromExternalRequest(request, response)));
// External login (from OAuth2).
express.post("/hk4e_global/mdk/shield/api/loginByThirdparty", (request, response) -> Grasscutter.getAuthenticationSystem().getOAuthAuthenticator()
.handleLogin(AuthenticationSystem.fromExternalRequest(request, response)));
express.get("/authentication/openid/redirect", (request, response) -> Grasscutter.getAuthenticationSystem().getOAuthAuthenticator()
.handleTokenProcess(AuthenticationSystem.fromExternalRequest(request, response)));
express.get("/Api/twitter_login", (request, response) -> Grasscutter.getAuthenticationSystem().getOAuthAuthenticator()
.handleRedirection(AuthenticationSystem.fromExternalRequest(request, response), ClientType.DESKTOP));
express.get("/sdkTwitterLogin.html", (request, response) -> Grasscutter.getAuthenticationSystem().getOAuthAuthenticator()
.handleRedirection(AuthenticationSystem.fromExternalRequest(request, response), ClientType.MOBILE));
}
/**
* @route /hk4e_global/mdk/shield/api/login
*/
private static void clientLogin(Request request, Response response) {
// Parse body data.
String rawBodyData = request.ctx().body();
var bodyData = Utils.jsonDecode(rawBodyData, LoginAccountRequestJson.class);
// Validate body data.
if(bodyData == null)
return;
// Pass data to authentication handler.
var responseData = Grasscutter.getAuthenticationSystem()
.getPasswordAuthenticator()
.authenticate(AuthenticationSystem.fromPasswordRequest(request, bodyData));
// Send response.
response.send(responseData);
// Log to console.
Grasscutter.getLogger().info(translate("messages.dispatch.account.login_attempt", request.ip()));
}
/**
* @route /hk4e_global/mdk/shield/api/verify
*/
private static void tokenLogin(Request request, Response response) {
// Parse body data.
String rawBodyData = request.ctx().body();
var bodyData = Utils.jsonDecode(rawBodyData, LoginTokenRequestJson.class);
// Validate body data.
if(bodyData == null)
return;
// Pass data to authentication handler.
var responseData = Grasscutter.getAuthenticationSystem()
.getTokenAuthenticator()
.authenticate(AuthenticationSystem.fromTokenRequest(request, bodyData));
// Send response.
response.send(responseData);
// Log to console.
Grasscutter.getLogger().info(translate("messages.dispatch.account.login_attempt", request.ip()));
}
/**
* @route /hk4e_global/combo/granter/login/v2/login
*/
private static void sessionKeyLogin(Request request, Response response) {
// Parse body data.
String rawBodyData = request.ctx().body();
var bodyData = Utils.jsonDecode(rawBodyData, ComboTokenReqJson.class);
// Validate body data.
if(bodyData == null || bodyData.data == null)
return;
// Decode additional body data.
var tokenData = Utils.jsonDecode(bodyData.data, LoginTokenData.class);
// Pass data to authentication handler.
var responseData = Grasscutter.getAuthenticationSystem()
.getSessionKeyAuthenticator()
.authenticate(AuthenticationSystem.fromComboTokenRequest(request, bodyData, tokenData));
// Send response.
response.send(responseData);
// Log to console.
Grasscutter.getLogger().info(translate("messages.dispatch.account.login_attempt", request.ip()));
}
}
package emu.grasscutter.server.http.dispatch;
import com.google.protobuf.ByteString;
import com.google.protobuf.InvalidProtocolBufferException;
import emu.grasscutter.Grasscutter;
import emu.grasscutter.Grasscutter.ServerRunMode;
import emu.grasscutter.net.proto.QueryCurrRegionHttpRspOuterClass.*;
import emu.grasscutter.net.proto.RegionInfoOuterClass;
import emu.grasscutter.net.proto.RegionInfoOuterClass.RegionInfo;
import emu.grasscutter.net.proto.RegionSimpleInfoOuterClass.RegionSimpleInfo;
import emu.grasscutter.server.event.dispatch.QueryAllRegionsEvent;
import emu.grasscutter.server.event.dispatch.QueryCurrentRegionEvent;
import emu.grasscutter.server.http.Router;
import emu.grasscutter.utils.Crypto;
import emu.grasscutter.utils.FileUtils;
import emu.grasscutter.utils.Utils;
import express.Express;
import express.http.Request;
import express.http.Response;
import io.javalin.Javalin;
import java.io.File;
import java.util.ArrayList;
import java.util.Base64;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import static emu.grasscutter.Configuration.*;
import static emu.grasscutter.net.proto.QueryRegionListHttpRspOuterClass.*;
/**
* Handles requests related to region queries.
*/
public final class RegionHandler implements Router {
private static final Map<String, RegionData> regions = new ConcurrentHashMap<>();
private static String regionListResponse;
public RegionHandler() {
try { // Read & initialize region data.
this.initialize();
} catch (Exception exception) {
Grasscutter.getLogger().error("Failed to initialize region data.", exception);
}
}
/**
* Configures region data according to configuration.
*/
private void initialize() {
String dispatchDomain = "http" + (HTTP_ENCRYPTION.useInRouting ? "s" : "") + "://"
+ lr(HTTP_INFO.accessAddress, HTTP_INFO.bindAddress) + ":"
+ lr(HTTP_INFO.accessPort, HTTP_INFO.bindPort);
// Create regions.
List<RegionSimpleInfo> servers = new ArrayList<>();
List<String> usedNames = new ArrayList<>(); // List to check for potential naming conflicts.
var configuredRegions = new ArrayList<>(List.of(DISPATCH_INFO.regions));
if(SERVER.runMode != ServerRunMode.HYBRID && configuredRegions.size() == 0) {
Grasscutter.getLogger().error("[Dispatch] There are no game servers available. Exiting due to unplayable state.");
System.exit(1);
} else if (configuredRegions.size() == 0)
configuredRegions.add(new Region("os_usa", DISPATCH_INFO.defaultName,
lr(GAME_INFO.accessAddress, GAME_INFO.bindAddress),
lr(GAME_INFO.accessPort, GAME_INFO.bindPort)));
configuredRegions.forEach(region -> {
if (usedNames.contains(region.Name)) {
Grasscutter.getLogger().error("Region name already in use.");
return;
}
// Create a region identifier.
var identifier = RegionSimpleInfo.newBuilder()
.setName(region.Name).setTitle(region.Title).setType("DEV_PUBLIC")
.setDispatchUrl(dispatchDomain + "/query_cur_region/" + region.Name)
.build();
usedNames.add(region.Name); servers.add(identifier);
// Create a region info object.
var regionInfo = RegionInfo.newBuilder()
.setGateserverIp(region.Ip).setGateserverPort(region.Port)
.setSecretKey(ByteString.copyFrom(Crypto.DISPATCH_SEED))
.build();
// Create an updated region query.
var updatedQuery = QueryCurrRegionHttpRsp.newBuilder().setRegionInfo(regionInfo).build();
regions.put(region.Name, new RegionData(updatedQuery, Utils.base64Encode(updatedQuery.toByteString().toByteArray())));
});
// Create a config object.
byte[] customConfig = "{\"sdkenv\":\"2\",\"checkdevice\":\"false\",\"loadPatch\":\"false\",\"showexception\":\"false\",\"regionConfig\":\"pm|fk|add\",\"downloadMode\":\"0\"}".getBytes();
Crypto.xor(customConfig, Crypto.DISPATCH_KEY); // XOR the config with the key.
// Create an updated region list.
QueryRegionListHttpRsp updatedRegionList = QueryRegionListHttpRsp.newBuilder()
.addAllRegionList(servers)
.setClientSecretKey(ByteString.copyFrom(Crypto.DISPATCH_SEED))
.setClientCustomConfigEncrypted(ByteString.copyFrom(customConfig))
.setEnableLoginPc(true).build();
// Set the region list response.
regionListResponse = Utils.base64Encode(updatedRegionList.toByteString().toByteArray());
}
@Override public void applyRoutes(Express express, Javalin handle) {
express.get("/query_region_list", RegionHandler::queryRegionList);
express.get("/query_cur_region/:region", RegionHandler::queryCurrentRegion );
}
/**
* @route /query_region_list
*/
private static void queryRegionList(Request request, Response response) {
// Invoke event.
QueryAllRegionsEvent event = new QueryAllRegionsEvent(regionListResponse); event.call();
// Respond with event result.
response.send(event.getRegionList());
// Log to console.
Grasscutter.getLogger().info(String.format("[Dispatch] Client %s request: query_region_list", request.ip()));
}
/**
* @route /query_cur_region/:region
*/
private static void queryCurrentRegion(Request request, Response response) {
// Get region to query.
String regionName = request.params("region");
// Get region data.
String regionData = "CAESGE5vdCBGb3VuZCB2ZXJzaW9uIGNvbmZpZw==";
if (request.query().values().size() > 0) {
var region = regions.get(regionName);
if(region != null) regionData = region.getBase64();
}
// Invoke event.
QueryCurrentRegionEvent event = new QueryCurrentRegionEvent(regionData); event.call();
// Respond with event result.
response.send(event.getRegionInfo());
// Log to console.
Grasscutter.getLogger().info(String.format("Client %s request: query_cur_region/%s", request.ip(), regionName));
}
/**
* Region data container.
*/
public static class RegionData {
private final QueryCurrRegionHttpRsp regionQuery;
private final String base64;
public RegionData(QueryCurrRegionHttpRsp prq, String b64) {
this.regionQuery = prq;
this.base64 = b64;
}
public QueryCurrRegionHttpRsp getRegionQuery() {
return this.regionQuery;
}
public String getBase64() {
return this.base64;
}
}
/**
* Gets the current region query.
* @return A {@link QueryCurrRegionHttpRsp} object.
*/
public static QueryCurrRegionHttpRsp getCurrentRegion() {
return SERVER.runMode == ServerRunMode.HYBRID ? regions.get("os_usa").getRegionQuery() : null;
}
}
package emu.grasscutter.server.http.documentation;
import express.http.Request;
import express.http.Response;
interface DocumentationHandler {
void handle(Request request, Response response);
}
package emu.grasscutter.server.http.documentation;
import emu.grasscutter.server.http.Router;
import express.Express;
import io.javalin.Javalin;
public final class DocumentationServerHandler implements Router {
@Override
public void applyRoutes(Express express, Javalin handle) {
final RootRequestHandler root = new RootRequestHandler();
final HandbookRequestHandler handbook = new HandbookRequestHandler();
final GachaMappingRequestHandler gachaMapping = new GachaMappingRequestHandler();
express.get("/documentation/handbook", handbook::handle);
express.get("/documentation/gachamapping", gachaMapping::handle);
express.get("/documentation", root::handle);
}
}
package emu.grasscutter.server.http.documentation;
import static emu.grasscutter.Configuration.RESOURCE;
import com.google.gson.reflect.TypeToken;
import emu.grasscutter.Grasscutter;
import emu.grasscutter.data.GameData;
import emu.grasscutter.data.excels.AvatarData;
import emu.grasscutter.data.excels.ItemData;
import emu.grasscutter.utils.Utils;
import static emu.grasscutter.Configuration.DOCUMENT_LANGUAGE;
import express.http.Request;
import express.http.Response;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
final class GachaMappingRequestHandler implements DocumentationHandler {
private Map<Long, String> map;
GachaMappingRequestHandler() {
final String textMapFile = "TextMap/TextMap" + DOCUMENT_LANGUAGE + ".json";
try (InputStreamReader fileReader = new InputStreamReader(new FileInputStream(
Utils.toFilePath(RESOURCE(textMapFile))), StandardCharsets.UTF_8)) {
map = Grasscutter.getGsonFactory().fromJson(fileReader,
new TypeToken<Map<Long, String>>() {
}.getType());
} catch (IOException e) {
Grasscutter.getLogger().warn("Resource does not exist: " + textMapFile);
map = new HashMap<>();
}
}
@Override
public void handle(Request request, Response response) {
if (map.isEmpty()) {
response.status(500);
} else {
response.set("Content-Type", "application/json")
.ctx()
.result(createGachaMappingJson());
}
}
private String createGachaMappingJson() {
List<Integer> list;
final StringBuilder sb = new StringBuilder();
list = new ArrayList<>(GameData.getAvatarDataMap().keySet());
Collections.sort(list);
final String newLine = System.lineSeparator();
// if the user made choices for language, I assume it's okay to assign his/her selected language to "en-us"
// since it's the fallback language and there will be no difference in the gacha record page.
// The enduser can still modify the `gacha_mappings.js` directly to enable multilingual for the gacha record system.
sb.append("{").append(newLine);
// Avatars
boolean first = true;
for (Integer id : list) {
AvatarData data = GameData.getAvatarDataMap().get(id);
int avatarID = data.getId();
if (avatarID >= 11000000) { // skip test avatar
continue;
}
if (first) { // skip adding comma for the first element
first = false;
} else {
sb.append(",");
}
String color;
switch (data.getQualityType()) {
case "QUALITY_PURPLE":
color = "purple";
break;
case "QUALITY_ORANGE":
color = "yellow";
break;
case "QUALITY_BLUE":
default:
color = "blue";
}
// Got the magic number 4233146695 from manually search in the json file
sb.append("\"")
.append(avatarID % 1000 + 1000)
.append("\" : [\"")
.append(map.get(data.getNameTextMapHash()))
.append("(")
.append(map.get(4233146695L))
.append(")\", \"")
.append(color)
.append("\"]")
.append(newLine);
}
list = new ArrayList<>(GameData.getItemDataMap().keySet());
Collections.sort(list);
// Weapons
for (Integer id : list) {
ItemData data = GameData.getItemDataMap().get(id);
if (data.getId() <= 11101 || data.getId() >= 20000) {
continue; //skip non weapon items
}
String color;
switch (data.getRankLevel()) {
case 3:
color = "blue";
break;
case 4:
color = "purple";
break;
case 5:
color = "yellow";
break;
default:
continue; // skip unnecessary entries
}
// Got the magic number 4231343903 from manually search in the json file
sb.append(",\"")
.append(data.getId())
.append("\" : [\"")
.append(map.get(data.getNameTextMapHash()).replaceAll("\"", ""))
.append("(")
.append(map.get(4231343903L))
.append(")\",\"")
.append(color)
.append("\"]")
.append(newLine);
}
sb.append(",\"200\": \"")
.append(map.get(332935371L))
.append("\", \"301\": \"")
.append(map.get(2272170627L))
.append("\", \"302\": \"")
.append(map.get(2864268523L))
.append("\"")
.append("}\n}")
.append(newLine);
return sb.toString();
}
}
package emu.grasscutter.server.http.documentation;
import static emu.grasscutter.Configuration.*;
import static emu.grasscutter.utils.Language.translate;
import com.google.gson.reflect.TypeToken;
import emu.grasscutter.Grasscutter;
import emu.grasscutter.command.CommandMap;
import emu.grasscutter.data.GameData;
import emu.grasscutter.data.excels.AvatarData;
import emu.grasscutter.data.excels.ItemData;
import emu.grasscutter.data.excels.MonsterData;
import emu.grasscutter.data.excels.SceneData;
import emu.grasscutter.utils.FileUtils;
import emu.grasscutter.utils.Utils;
import express.http.Request;
import express.http.Response;
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
import java.util.stream.Collectors;
final class HandbookRequestHandler implements DocumentationHandler {
private final String template;
private Map<Long, String> map;
public HandbookRequestHandler() {
final File templateFile = new File(Utils.toFilePath(DATA("documentation/handbook.html")));
if (templateFile.exists()) {
template = new String(FileUtils.read(templateFile), StandardCharsets.UTF_8);
} else {
Grasscutter.getLogger().warn("File does not exist: " + templateFile);
template = null;
}
final String textMapFile = "TextMap/TextMap" + DOCUMENT_LANGUAGE + ".json";
try (InputStreamReader fileReader = new InputStreamReader(new FileInputStream(
Utils.toFilePath(RESOURCE(textMapFile))), StandardCharsets.UTF_8)) {
map = Grasscutter.getGsonFactory()
.fromJson(fileReader, new TypeToken<Map<Long, String>>() {
}.getType());
} catch (IOException e) {
Grasscutter.getLogger().warn("Resource does not exist: " + textMapFile);
map = new HashMap<>();
}
}
@Override
public void handle(Request request, Response response) {
if (template == null) {
response.status(500);
return;
}
final CommandMap cmdMap = new CommandMap(true);
final Int2ObjectMap<AvatarData> avatarMap = GameData.getAvatarDataMap();
final Int2ObjectMap<ItemData> itemMap = GameData.getItemDataMap();
final Int2ObjectMap<SceneData> sceneMap = GameData.getSceneDataMap();
final Int2ObjectMap<MonsterData> monsterMap = GameData.getMonsterDataMap();
// Add translated title etc. to the page.
String content = template.replace("{{TITLE}}", translate("documentation.handbook.title"))
.replace("{{TITLE_COMMANDS}}", translate("documentation.handbook.title_commands"))
.replace("{{TITLE_AVATARS}}", translate("documentation.handbook.title_avatars"))
.replace("{{TITLE_ITEMS}}", translate("documentation.handbook.title_items"))
.replace("{{TITLE_SCENES}}", translate("documentation.handbook.title_scenes"))
.replace("{{TITLE_MONSTERS}}", translate("documentation.handbook.title_monsters"))
.replace("{{HEADER_ID}}", translate("documentation.handbook.header_id"))
.replace("{{HEADER_COMMAND}}", translate("documentation.handbook.header_command"))
.replace("{{HEADER_DESCRIPTION}}",
translate("documentation.handbook.header_description"))
.replace("{{HEADER_AVATAR}}", translate("documentation.handbook.header_avatar"))
.replace("{{HEADER_ITEM}}", translate("documentation.handbook.header_item"))
.replace("{{HEADER_SCENE}}", translate("documentation.handbook.header_scene"))
.replace("{{HEADER_MONSTER}}", translate("documentation.handbook.header_monster"))
// Commands table
.replace("{{COMMANDS_TABLE}}", cmdMap.getAnnotationsAsList()
.stream()
.map(cmd -> "<tr><td><code>" + cmd.label() + "</code></td><td>" +
cmd.description() + "</td></tr>")
.collect(Collectors.joining("\n")))
// Avatars table
.replace("{{AVATARS_TABLE}}", GameData.getAvatarDataMap().keySet()
.intStream()
.sorted()
.mapToObj(avatarMap::get)
.map(data -> "<tr><td><code>" + data.getId() + "</code></td><td>" +
map.get(data.getNameTextMapHash()) + "</td></tr>")
.collect(Collectors.joining("\n")))
// Items table
.replace("{{ITEMS_TABLE}}", GameData.getItemDataMap().keySet()
.intStream()
.sorted()
.mapToObj(itemMap::get)
.map(data -> "<tr><td><code>" + data.getId() + "</code></td><td>" +
map.get(data.getNameTextMapHash()) + "</td></tr>")
.collect(Collectors.joining("\n")))
// Scenes table
.replace("{{SCENES_TABLE}}", GameData.getSceneDataMap().keySet()
.intStream()
.sorted()
.mapToObj(sceneMap::get)
.map(data -> "<tr><td><code>" + data.getId() + "</code></td><td>" +
data.getScriptData() + "</td></tr>")
.collect(Collectors.joining("\n")))
.replace("{{MONSTERS_TABLE}}", GameData.getMonsterDataMap().keySet()
.intStream()
.sorted()
.mapToObj(monsterMap::get)
.map(data -> "<tr><td><code>" + data.getId() + "</code></td><td>" +
map.get(data.getNameTextMapHash()) + "</td></tr>")
.collect(Collectors.joining("\n")));
response.send(content);
}
}
package emu.grasscutter.server.http.documentation;
import static emu.grasscutter.Configuration.DATA;
import static emu.grasscutter.utils.Language.translate;
import emu.grasscutter.Grasscutter;
import emu.grasscutter.data.ResourceLoader;
import emu.grasscutter.utils.FileUtils;
import emu.grasscutter.utils.Utils;
import express.http.Request;
import express.http.Response;
import java.io.File;
import java.nio.charset.StandardCharsets;
final class RootRequestHandler implements DocumentationHandler {
private final String template;
public RootRequestHandler() {
final File templateFile = new File(Utils.toFilePath(DATA("documentation/index.html")));
if (templateFile.exists()) {
template = new String(FileUtils.read(templateFile), StandardCharsets.UTF_8);
} else {
Grasscutter.getLogger().warn("File does not exist: " + templateFile);
template = null;
}
}
@Override
public void handle(Request request, Response response) {
if (template == null) {
response.status(500);
return;
}
String content = template.replace("{{TITLE}}", translate("documentation.index.title"))
.replace("{{ITEM_HANDBOOK}}", translate("documentation.index.handbook"))
.replace("{{ITEM_GACHA_MAPPING}}", translate("documentation.index.gacha_mapping"));
response.send(content);
}
}
package emu.grasscutter.server.http.handlers;
import emu.grasscutter.Grasscutter;
import emu.grasscutter.data.DataLoader;
import emu.grasscutter.server.http.objects.HttpJsonResponse;
import emu.grasscutter.server.http.Router;
import emu.grasscutter.utils.FileUtils;
import emu.grasscutter.utils.Utils;
import express.Express;
import express.http.MediaType;
import express.http.Request;
import express.http.Response;
import io.javalin.Javalin;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.Objects;
import static emu.grasscutter.Configuration.*;
/**
* Handles requests related to the announcements page.
*/
public final class AnnouncementsHandler implements Router {
@Override public void applyRoutes(Express express, Javalin handle) {
// hk4e-api-os.hoyoverse.com
express.all("/common/hk4e_global/announcement/api/getAlertPic", new HttpJsonResponse("{\"retcode\":0,\"message\":\"OK\",\"data\":{\"total\":0,\"list\":[]}}"));
// hk4e-api-os.hoyoverse.com
express.all("/common/hk4e_global/announcement/api/getAlertAnn", new HttpJsonResponse("{\"retcode\":0,\"message\":\"OK\",\"data\":{\"alert\":false,\"alert_id\":0,\"remind\":true}}"));
// hk4e-api-os.hoyoverse.com
express.all("/common/hk4e_global/announcement/api/getAnnList", AnnouncementsHandler::getAnnouncement);
// hk4e-api-os-static.hoyoverse.com
express.all("/common/hk4e_global/announcement/api/getAnnContent", AnnouncementsHandler::getAnnouncement);
// hk4e-sdk-os.hoyoverse.com
express.all("/hk4e_global/mdk/shopwindow/shopwindow/listPriceTier", new HttpJsonResponse("{\"retcode\":0,\"message\":\"OK\",\"data\":{\"suggest_currency\":\"USD\",\"tiers\":[]}}"));
express.get("/hk4e/announcement/*", AnnouncementsHandler::getPageResources);
}
private static void getAnnouncement(Request request, Response response) {
String data = "";
if (Objects.equals(request.baseUrl(), "/common/hk4e_global/announcement/api/getAnnContent")) {
try {
data = FileUtils.readToString(DataLoader.load("GameAnnouncement.json"));
} catch (Exception e) {
if(e.getClass() == IOException.class) {
Grasscutter.getLogger().info("Unable to read file 'GameAnnouncementList.json'. \n" + e);
}
}
} else if (Objects.equals(request.baseUrl(), "/common/hk4e_global/announcement/api/getAnnList")) {
try {
data = FileUtils.readToString(DataLoader.load("GameAnnouncementList.json"));
} catch (Exception e) {
if(e.getClass() == IOException.class) {
Grasscutter.getLogger().info("Unable to read file 'GameAnnouncementList.json'. \n" + e);
}
}
} else {
response.send("{\"retcode\":404,\"message\":\"Unknown request path\"}");
}
if (data.isEmpty()) {
response.send("{\"retcode\":500,\"message\":\"Unable to fetch requsted content\"}");
return;
}
String dispatchDomain = "http" + (HTTP_ENCRYPTION.useInRouting ? "s" : "") + "://"
+ lr(HTTP_INFO.accessAddress, HTTP_INFO.bindAddress) + ":"
+ lr(HTTP_INFO.accessPort, HTTP_INFO.bindPort);
data = data
.replace("{{DISPATCH_PUBLIC}}", dispatchDomain)
.replace("{{SYSTEM_TIME}}", String.valueOf(System.currentTimeMillis()));
response.send("{\"retcode\":0,\"message\":\"OK\",\"data\": " + data + "}");
}
private static void getPageResources(Request request, Response response) {
try(InputStream filestream = DataLoader.load(request.path())) {
String possibleFilename = Utils.toFilePath(DATA(request.path()));
MediaType fromExtension = MediaType.getByExtension(possibleFilename.substring(possibleFilename.lastIndexOf(".") + 1));
response.type((fromExtension != null) ? fromExtension.getMIME() : "application/octet-stream");
response.send(filestream.readAllBytes());
} catch (Exception e) {
Grasscutter.getLogger().warn("File does not exist: " + request.path());
response.status(404);
}
}
}
package emu.grasscutter.server.http.handlers;
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.GachaManager;
import emu.grasscutter.game.player.Player;
import emu.grasscutter.server.http.Router;
import emu.grasscutter.tools.Tools;
import emu.grasscutter.utils.FileUtils;
import emu.grasscutter.utils.Utils;
import express.Express;
import express.http.Request;
import express.http.Response;
import io.javalin.Javalin;
import io.javalin.http.staticfiles.Location;
import java.io.File;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.LinkedHashSet;
import java.util.Set;
import static emu.grasscutter.Configuration.DATA;
import static emu.grasscutter.utils.Language.translate;
/**
* Handles all gacha-related HTTP requests.
*/
public final class GachaHandler implements Router {
public static final String gachaMappings = DATA(Utils.toFilePath("gacha/mappings.js"));
@Override public void applyRoutes(Express express, Javalin handle) {
express.get("/gacha", GachaHandler::gachaRecords);
express.get("/gacha/details", GachaHandler::gachaDetails);
express.useStaticFallback("/gacha/mappings", gachaMappings, Location.EXTERNAL);
}
private static void gachaRecords(Request request, Response response) {
File recordsTemplate = new File(Utils.toFilePath(DATA("gacha/records.html")));
if (!recordsTemplate.exists()) {
Grasscutter.getLogger().warn("File does not exist: " + recordsTemplate);
response.status(500);
return;
}
String sessionKey = request.query("s");
Account account = DatabaseHelper.getAccountBySessionKey(sessionKey);
if(account == null) {
response.status(403).send("Requested account was not found");
return;
}
Player player = Grasscutter.getGameServer().getPlayerByAccountId(account.getId());
if (player == null) {
response.status(403).send("No player associated with requested account");
return;
}
int page = 0, gachaType = 0;
if(request.query("p") != null)
page = Integer.parseInt(request.query("p"));
if(request.query("gachaType") != null)
gachaType = Integer.parseInt(request.query("gachaType"));
String records = DatabaseHelper.getGachaRecords(player.getUid(), page, gachaType).toString();
long maxPage = DatabaseHelper.getGachaRecordsMaxPage(player.getUid(), page, gachaType);
String template = new String(FileUtils.read(recordsTemplate), StandardCharsets.UTF_8)
.replace("{{REPLACE_RECORDS}}", records)
.replace("{{REPLACE_MAXPAGE}}", String.valueOf(maxPage))
.replace("{{LANGUAGE}}", Utils.getLanguageCode(account.getLocale()));
response.send(template);
}
private static void gachaDetails(Request request, Response response) {
File detailsTemplate = new File(Utils.toFilePath(DATA("gacha/details.html")));
if (!detailsTemplate.exists()) {
Grasscutter.getLogger().warn("File does not exist: " + detailsTemplate);
response.status(500);
return;
}
String sessionKey = request.query("s");
Account account = DatabaseHelper.getAccountBySessionKey(sessionKey);
if(account == null) {
response.status(403).send("Requested account was not found");
return;
}
Player player = Grasscutter.getGameServer().getPlayerByAccountId(account.getId());
if (player == null) {
response.status(403).send("No player associated with requested account");
return;
}
String template = new String(FileUtils.read(detailsTemplate), StandardCharsets.UTF_8);
// Add translated title etc. to the page.
template = template.replace("{{TITLE}}", translate(player, "gacha.details.title"))
.replace("{{AVAILABLE_FIVE_STARS}}", translate(player, "gacha.details.available_five_stars"))
.replace("{{AVAILABLE_FOUR_STARS}}", translate(player, "gacha.details.available_four_stars"))
.replace("{{AVAILABLE_THREE_STARS}}", translate(player, "gacha.details.available_three_stars"))
.replace("{{LANGUAGE}}", Utils.getLanguageCode(account.getLocale()));
// Get the banner info for the banner we want.
int scheduleId = Integer.parseInt(request.query("scheduleId"));
GachaManager manager = Grasscutter.getGameServer().getGachaManager();
GachaBanner banner = manager.getGachaBanners().get(scheduleId);
// Add 5-star items.
Set<String> fiveStarItems = new LinkedHashSet<>();
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()).forEach(i -> fiveStarItems.add(Integer.toString(i)));
template = template.replace("{{FIVE_STARS}}", "[" + String.join(",", fiveStarItems) + "]");
// Add 4-star items.
Set<String> fourStarItems = new LinkedHashSet<>();
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()).forEach(i -> fourStarItems.add(Integer.toString(i)));
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)));
template = template.replace("{{THREE_STARS}}", "[" + String.join(",", threeStarItems) + "]");
// Done.
response.send(template);
}
}
package emu.grasscutter.server.http.handlers;
import emu.grasscutter.GameConstants;
import emu.grasscutter.Grasscutter;
import emu.grasscutter.server.http.objects.HttpJsonResponse;
import emu.grasscutter.server.http.Router;
import emu.grasscutter.server.http.objects.WebStaticVersionResponse;
import express.Express;
import express.http.Request;
import express.http.Response;
import io.javalin.Javalin;
import static emu.grasscutter.Configuration.ACCOUNT;
/**
* Handles all generic, hard-coded responses.
*/
public final class GenericHandler implements Router {
@Override public void applyRoutes(Express express, Javalin handle) {
// hk4e-sdk-os.hoyoverse.com
express.get("/hk4e_global/mdk/agreement/api/getAgreementInfos", new HttpJsonResponse("{\"retcode\":0,\"message\":\"OK\",\"data\":{\"marketing_agreements\":[]}}"));
// hk4e-sdk-os.hoyoverse.com
// this could be either GET or POST based on the observation of different clients
express.all("/hk4e_global/combo/granter/api/compareProtocolVersion", new HttpJsonResponse("{\"retcode\":0,\"message\":\"OK\",\"data\":{\"modified\":true,\"protocol\":{\"id\":0,\"app_id\":4,\"language\":\"en\",\"user_proto\":\"\",\"priv_proto\":\"\",\"major\":7,\"minimum\":0,\"create_time\":\"0\",\"teenager_proto\":\"\",\"third_proto\":\"\"}}}"));
// api-account-os.hoyoverse.com
express.post("/account/risky/api/check", new HttpJsonResponse("{\"retcode\":0,\"message\":\"OK\",\"data\":{\"id\":\"none\",\"action\":\"ACTION_NONE\",\"geetest\":null}}"));
// sdk-os-static.hoyoverse.com
express.get("/combo/box/api/config/sdk/combo", new HttpJsonResponse("{\"retcode\":0,\"message\":\"OK\",\"data\":{\"vals\":{\"disable_email_bind_skip\":\"false\",\"email_bind_remind_interval\":\"7\",\"email_bind_remind\":\"true\"}}}"));
// hk4e-sdk-os-static.hoyoverse.com
express.get("/hk4e_global/combo/granter/api/getConfig", new HttpJsonResponse("{\"retcode\":0,\"message\":\"OK\",\"data\":{\"protocol\":true,\"qr_enabled\":false,\"log_level\":\"INFO\",\"announce_url\":\"https://webstatic-sea.hoyoverse.com/hk4e/announcement/index.html?sdk_presentation_style=fullscreen\\u0026sdk_screen_transparent=true\\u0026game_biz=hk4e_global\\u0026auth_appid=announcement\\u0026game=hk4e#/\",\"push_alias_type\":2,\"disable_ysdk_guard\":false,\"enable_announce_pic_popup\":true}}"));
// hk4e-sdk-os-static.hoyoverse.com
express.get("/hk4e_global/mdk/shield/api/loadConfig", new HttpJsonResponse("{\"retcode\":0,\"message\":\"OK\",\"data\":{\"id\":6,\"game_key\":\"hk4e_global\",\"client\":\"PC\",\"identity\":\"I_IDENTITY\",\"guest\":false,\"ignore_versions\":\"\",\"scene\":\"S_NORMAL\",\"name\":\"原神海外\",\"disable_regist\":false,\"enable_email_captcha\":false,\"thirdparty\":[\"fb\",\"tw\"],\"disable_mmt\":false,\"server_guest\":false,\"thirdparty_ignore\":{\"tw\":\"\",\"fb\":\"\"},\"enable_ps_bind_account\":false,\"thirdparty_login_configs\":{\"tw\":{\"token_type\":\"TK_GAME_TOKEN\",\"game_token_expires_in\":604800},\"fb\":{\"token_type\":\"TK_GAME_TOKEN\",\"game_token_expires_in\":604800}}}}"));
// Test api?
// abtest-api-data-sg.hoyoverse.com
express.post("/data_abtest_api/config/experiment/list", new HttpJsonResponse("{\"retcode\":0,\"success\":true,\"message\":\"\",\"data\":[{\"code\":1000,\"type\":2,\"config_id\":\"14\",\"period_id\":\"6036_99\",\"version\":\"1\",\"configs\":{\"cardType\":\"old\"}}]}"));
// log-upload-os.mihoyo.com
express.all("/log/sdk/upload", new HttpJsonResponse("{\"code\":0}"));
express.all("/sdk/upload", new HttpJsonResponse("{\"code\":0}"));
express.post("/sdk/dataUpload", new HttpJsonResponse("{\"code\":0}"));
// /perf/config/verify?device_id=xxx&platform=x&name=xxx
express.all("/perf/config/verify", new HttpJsonResponse("{\"code\":0}"));
// webstatic-sea.hoyoverse.com
express.get("/admin/mi18n/plat_oversea/*", new WebStaticVersionResponse());
express.get("/status/server", GenericHandler::serverStatus);
}
private static void serverStatus(Request request, Response response) {
int playerCount = Grasscutter.getGameServer().getPlayers().size();
int maxPlayer = ACCOUNT.maxPlayer;
String version = GameConstants.VERSION;
response.send("{\"retcode\":0,\"status\":{\"playerCount\":" + playerCount + ",\"maxPlayer\":" + maxPlayer + ",\"version\":\"" + version + "\"}}");
}
}
package emu.grasscutter.server.http.handlers;
import emu.grasscutter.server.http.Router;
import express.Express;
import express.http.Request;
import express.http.Response;
import io.javalin.Javalin;
/**
* Handles logging requests made to the server.
*/
public final class LogHandler implements Router {
@Override public void applyRoutes(Express express, Javalin handle) {
// overseauspider.yuanshen.com
express.post("/log", LogHandler::log);
// log-upload-os.mihoyo.com
express.post("/crash/dataUpload", LogHandler::log);
}
private static void log(Request request, Response response) {
// TODO: Figure out how to dump request body and log to file.
response.send("{\"code\":0}");
}
}
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment