From 6a091b254c9843ce36a38af24d04998989592e31 Mon Sep 17 00:00:00 2001 From: Benjamin Elsdon Date: Thu, 21 Apr 2022 18:04:00 +0800 Subject: [PATCH] Multi-server dispatch support and server run modes --- src/main/java/emu/grasscutter/Config.java | 19 +++ .../java/emu/grasscutter/Grasscutter.java | 32 +++- .../grasscutter/database/DatabaseHelper.java | 12 +- .../grasscutter/database/DatabaseManager.java | 43 ++++- .../server/dispatch/DispatchServer.java | 161 ++++++++++++------ .../packet/send/PacketPlayerLoginRsp.java | 2 +- 6 files changed, 198 insertions(+), 71 deletions(-) diff --git a/src/main/java/emu/grasscutter/Config.java b/src/main/java/emu/grasscutter/Config.java index c6748fb41..938cbec77 100644 --- a/src/main/java/emu/grasscutter/Config.java +++ b/src/main/java/emu/grasscutter/Config.java @@ -1,5 +1,7 @@ package emu.grasscutter; +import java.util.ArrayList; + public final class Config { public String DatabaseUrl = "mongodb://localhost:27017"; @@ -11,6 +13,7 @@ public final class Config { public String DUMPS_FOLDER = "./dumps/"; public String KEY_FOLDER = "./keys/"; + public String RunMode = "HYBRID"; // HYBRID, DISPATCH_ONLY, GAME_ONLY public GameServerOptions GameServer = new GameServerOptions(); public DispatchServerOptions DispatchServer = new DispatchServerOptions(); @@ -30,6 +33,19 @@ public final class Config { public Boolean UseSSL = true; public boolean AutomaticallyCreateAccounts = false; + + public RegionInfo[] GameServers = {}; + + public RegionInfo[] getGameServers() { + return GameServers; + } + + public static class RegionInfo { + public String Name = "os_usa"; + public String Title = "Test"; + public String Ip = "127.0.0.1"; + public int Port = 22102; + } } public static class GameServerOptions { @@ -38,6 +54,9 @@ public final class Config { public String PublicIp = ""; public int Port = 22102; + public String DispatchServerDatabaseUrl = "mongodb://localhost:27017"; + public String DispatchServerDatabaseCollection = "grasscutter"; + public boolean LOG_PACKETS = false; public int InventoryLimitWeapon = 2000; diff --git a/src/main/java/emu/grasscutter/Grasscutter.java b/src/main/java/emu/grasscutter/Grasscutter.java index 0b8f1efd8..614b67ca1 100644 --- a/src/main/java/emu/grasscutter/Grasscutter.java +++ b/src/main/java/emu/grasscutter/Grasscutter.java @@ -73,11 +73,26 @@ public final class Grasscutter { DatabaseManager.initialize(); // Start servers. - dispatchServer = new DispatchServer(); - dispatchServer.start(); - - gameServer = new GameServer(new InetSocketAddress(getConfig().getGameServerOptions().Ip, getConfig().getGameServerOptions().Port)); - gameServer.start(); + if(getConfig().RunMode.equalsIgnoreCase("HYBRID")) { + dispatchServer = new DispatchServer(); + dispatchServer.start(); + + gameServer = new GameServer(new InetSocketAddress(getConfig().getGameServerOptions().Ip, getConfig().getGameServerOptions().Port)); + gameServer.start(); + } else if(getConfig().RunMode.equalsIgnoreCase("DISPATCH_ONLY")) { + dispatchServer = new DispatchServer(); + dispatchServer.start(); + } else if(getConfig().RunMode.equalsIgnoreCase("GAME_ONLY")) { + gameServer = new GameServer(new InetSocketAddress(getConfig().getGameServerOptions().Ip, getConfig().getGameServerOptions().Port)); + gameServer.start(); + } else { + getLogger().error("Invalid server run mode. " + getConfig().RunMode); + getLogger().error("Server run mode must be 'HYBRID', 'DISPATCH_ONLY', or 'GAME_ONLY'. Unable to start Grasscutter..."); + getLogger().error("Shutting down..."); + System.exit(1); + } + + // Open console. startConsole(); @@ -104,9 +119,14 @@ public final class Grasscutter { try (BufferedReader br = new BufferedReader(new InputStreamReader(System.in))) { while ((input = br.readLine()) != null) { try { + if(getConfig().RunMode.equalsIgnoreCase("DISPATCH_ONLY")) { + getLogger().error("Commands are not supported in dispatch only mode"); + return; + } CommandMap.getInstance().invoke(null, input); } catch (Exception e) { - Grasscutter.getLogger().error("Command error: " + e.getMessage()); + Grasscutter.getLogger().error("Command error: "); + e.printStackTrace(); } } } catch (Exception e) { diff --git a/src/main/java/emu/grasscutter/database/DatabaseHelper.java b/src/main/java/emu/grasscutter/database/DatabaseHelper.java index 0289c3e71..ed092e976 100644 --- a/src/main/java/emu/grasscutter/database/DatabaseHelper.java +++ b/src/main/java/emu/grasscutter/database/DatabaseHelper.java @@ -74,36 +74,36 @@ public class DatabaseHelper { } public static void saveAccount(Account account) { - DatabaseManager.getDatastore().save(account); + DatabaseManager.getAccountDatastore().save(account); } public static Account getAccountByName(String username) { - MorphiaCursor cursor = DatabaseManager.getDatastore().createQuery(Account.class).field("username").equalIgnoreCase(username).find(FIND_ONE); + MorphiaCursor cursor = DatabaseManager.getAccountDatastore().createQuery(Account.class).field("username").equalIgnoreCase(username).find(FIND_ONE); if (!cursor.hasNext()) return null; return cursor.next(); } public static Account getAccountByToken(String token) { if (token == null) return null; - MorphiaCursor cursor = DatabaseManager.getDatastore().createQuery(Account.class).field("token").equal(token).find(FIND_ONE); + MorphiaCursor cursor = DatabaseManager.getAccountDatastore().createQuery(Account.class).field("token").equal(token).find(FIND_ONE); if (!cursor.hasNext()) return null; return cursor.next(); } public static Account getAccountById(String uid) { - MorphiaCursor cursor = DatabaseManager.getDatastore().createQuery(Account.class).field("_id").equal(uid).find(FIND_ONE); + MorphiaCursor cursor = DatabaseManager.getAccountDatastore().createQuery(Account.class).field("_id").equal(uid).find(FIND_ONE); if (!cursor.hasNext()) return null; return cursor.next(); } public static Account getAccountByPlayerId(int playerId) { - MorphiaCursor cursor = DatabaseManager.getDatastore().createQuery(Account.class).field("playerId").equal(playerId).find(FIND_ONE); + MorphiaCursor cursor = DatabaseManager.getAccountDatastore().createQuery(Account.class).field("playerId").equal(playerId).find(FIND_ONE); if (!cursor.hasNext()) return null; return cursor.next(); } public static boolean deleteAccount(String username) { - Query q = DatabaseManager.getDatastore().createQuery(Account.class).field("username").equalIgnoreCase(username); + Query q = DatabaseManager.getAccountDatastore().createQuery(Account.class).field("username").equalIgnoreCase(username); return DatabaseManager.getDatastore().findAndDelete(q) != null; } diff --git a/src/main/java/emu/grasscutter/database/DatabaseManager.java b/src/main/java/emu/grasscutter/database/DatabaseManager.java index 97e27a81a..530581517 100644 --- a/src/main/java/emu/grasscutter/database/DatabaseManager.java +++ b/src/main/java/emu/grasscutter/database/DatabaseManager.java @@ -17,7 +17,10 @@ import emu.grasscutter.game.inventory.GenshinItem; public final class DatabaseManager { private static MongoClient mongoClient; + private static MongoClient dispatchMongoClient; + private static Datastore datastore; + private static Datastore dispatchDatastore; private static final Class[] mappedClasses = new Class[] { DatabaseCounter.class, Account.class, GenshinPlayer.class, GenshinAvatar.class, GenshinItem.class, Friendship.class @@ -26,18 +29,28 @@ public final class DatabaseManager { public static MongoClient getMongoClient() { return mongoClient; } - - public static Datastore getDatastore() { - return datastore; - } - - public static MongoDatabase getDatabase() { + + public static Datastore getDatastore() { + return datastore; + } + + public static MongoDatabase getDatabase() { return getDatastore().getDatabase(); } + + // Yes. I very dislike this method also but I'm lazy. Probably replace it by making the game server connect to the dispatch server instead. + public static Datastore getAccountDatastore() { + if(Grasscutter.getConfig().RunMode.equalsIgnoreCase("GAME_ONLY")) { + return dispatchDatastore; + } else { + return datastore; + } + } public static void initialize() { // Initialize mongoClient = new MongoClient(new MongoClientURI(Grasscutter.getConfig().DatabaseUrl)); + dispatchMongoClient = new MongoClient(new MongoClientURI(Grasscutter.getConfig().getGameServerOptions().DispatchServerDatabaseUrl)); Morphia morphia = new Morphia(); // TODO Update when migrating to Morphia 2.0 @@ -50,6 +63,7 @@ public final class DatabaseManager { // Build datastore datastore = morphia.createDatastore(mongoClient, Grasscutter.getConfig().DatabaseCollection); + dispatchDatastore = morphia.createDatastore(dispatchMongoClient, Grasscutter.getConfig().getGameServerOptions().DispatchServerDatabaseCollection); // Ensure indexes try { @@ -67,6 +81,23 @@ public final class DatabaseManager { datastore.ensureIndexes(); } } + + // Ensure indexes for dispatch server + try { + dispatchDatastore.ensureIndexes(); + } catch (MongoCommandException e) { + Grasscutter.getLogger().info("Mongo index error: ", e); + // Duplicate index error + if (e.getCode() == 85) { + // Drop all indexes and re add them + MongoIterable collections = dispatchDatastore.getDatabase().listCollectionNames(); + for (String name : collections) { + dispatchDatastore.getDatabase().getCollection(name).dropIndexes(); + } + // Add back indexes + dispatchDatastore.ensureIndexes(); + } + } } public static synchronized int getNextId(Class c) { diff --git a/src/main/java/emu/grasscutter/server/dispatch/DispatchServer.java b/src/main/java/emu/grasscutter/server/dispatch/DispatchServer.java index cda908480..53699b05b 100644 --- a/src/main/java/emu/grasscutter/server/dispatch/DispatchServer.java +++ b/src/main/java/emu/grasscutter/server/dispatch/DispatchServer.java @@ -9,10 +9,7 @@ import java.net.InetSocketAddress; import java.net.URI; import java.net.URLDecoder; import java.security.KeyStore; -import java.util.Base64; -import java.util.Collections; -import java.util.HashMap; -import java.util.Map; +import java.util.*; import javax.net.ssl.KeyManagerFactory; import javax.net.ssl.SSLContext; @@ -25,6 +22,7 @@ import com.sun.net.httpserver.HttpHandler; import com.sun.net.httpserver.HttpsConfigurator; import com.sun.net.httpserver.HttpsServer; +import emu.grasscutter.Config; import emu.grasscutter.Grasscutter; import emu.grasscutter.database.DatabaseHelper; import emu.grasscutter.game.Account; @@ -46,15 +44,18 @@ import com.sun.net.httpserver.HttpServer; public final class DispatchServer { private final InetSocketAddress address; private final Gson gson; - private QueryCurrRegionHttpRsp currRegion; + //private QueryCurrRegionHttpRsp currRegion; public String regionListBase64; - public String regionCurrentBase64; - + public HashMap regions; + public HashMap usersIngame; + public static String query_region_list = ""; public static String query_cur_region = ""; - + public DispatchServer() { + this.regions = new HashMap(); + this.usersIngame = new HashMap(); this.address = new InetSocketAddress(Grasscutter.getConfig().getDispatchOptions().Ip, Grasscutter.getConfig().getDispatchOptions().Port); this.gson = new GsonBuilder().create(); @@ -70,8 +71,13 @@ public final class DispatchServer { return gson; } - public QueryCurrRegionHttpRsp getCurrRegion() { - return currRegion; + public QueryCurrRegionHttpRsp getCurrRegion(InetSocketAddress address) { + if(usersIngame.containsKey(address)) { + return regions.get(usersIngame.get(address)).parsedRegionQuery; + } + + Grasscutter.getLogger().error("User is not logged in to dispatch server. " + address.getAddress() + ":" + address.getPort()); + return null; } public void loadQueries() { @@ -100,39 +106,67 @@ public final class DispatchServer { byte[] decoded2 = Base64.getDecoder().decode(query_cur_region); QueryCurrRegionHttpRsp regionQuery = QueryCurrRegionHttpRsp.parseFrom(decoded2); - RegionSimpleInfo server = RegionSimpleInfo.newBuilder() - .setName("os_usa") - .setTitle(Grasscutter.getConfig().getGameServerOptions().Name) - .setType("DEV_PUBLIC") - .setDispatchUrl("https://" + (Grasscutter.getConfig().getDispatchOptions().PublicIp.isEmpty() ? Grasscutter.getConfig().getDispatchOptions().Ip : Grasscutter.getConfig().getDispatchOptions().PublicIp) + ":" + getAddress().getPort() + "/query_cur_region") - .build(); - - RegionSimpleInfo serverTest2 = RegionSimpleInfo.newBuilder() - .setName("os_euro") - .setTitle("Grasscutter") - .setType("DEV_PUBLIC") - .setDispatchUrl("https://" + (Grasscutter.getConfig().getDispatchOptions().PublicIp.isEmpty() ? Grasscutter.getConfig().getDispatchOptions().Ip : Grasscutter.getConfig().getDispatchOptions().PublicIp) + ":" + getAddress().getPort() + "/query_cur_region") - .build(); - + List servers = new ArrayList(); + List usedNames = new ArrayList(); // List to check for potential naming conflicts + if(Grasscutter.getConfig().RunMode.equalsIgnoreCase("HYBRID")) { // Automatically add the game server if in hybrid mode + String defaultServerName = "os_usa"; + RegionSimpleInfo server = RegionSimpleInfo.newBuilder() + .setName("os_usa") + .setTitle(Grasscutter.getConfig().getGameServerOptions().Name) + .setType("DEV_PUBLIC") + .setDispatchUrl("https://" + (Grasscutter.getConfig().getDispatchOptions().PublicIp.isEmpty() ? Grasscutter.getConfig().getDispatchOptions().Ip : Grasscutter.getConfig().getDispatchOptions().PublicIp) + ":" + getAddress().getPort() + "/query_cur_region_" + defaultServerName) + .build(); + usedNames.add(defaultServerName); + servers.add(server); + + RegionInfo serverRegion = regionQuery.getRegionInfo().toBuilder() + .setIp((Grasscutter.getConfig().getGameServerOptions().PublicIp.isEmpty() ? Grasscutter.getConfig().getGameServerOptions().Ip : Grasscutter.getConfig().getGameServerOptions().PublicIp)) + .setPort(Grasscutter.getConfig().getGameServerOptions().Port) + .setSecretKey(ByteString.copyFrom(FileUtils.read(Grasscutter.getConfig().KEY_FOLDER + "dispatchSeed.bin"))) + .build(); + + QueryCurrRegionHttpRsp parsedRegionQuery = regionQuery.toBuilder().setRegionInfo(serverRegion).build(); + regions.put(defaultServerName, new RegionData(parsedRegionQuery, Base64.getEncoder().encodeToString(parsedRegionQuery.toByteString().toByteArray()))); + + } else { + if(Grasscutter.getConfig().getDispatchOptions().getGameServers().length == 0) { + Grasscutter.getLogger().error("Dispatch server has no game servers available. Exiting due to unplayable state."); + System.exit(1); + } + } + + for (Config.DispatchServerOptions.RegionInfo regionInfo : Grasscutter.getConfig().getDispatchOptions().getGameServers()) { + if(usedNames.contains(regionInfo.Name)) { + Grasscutter.getLogger().error("Region name already in use."); + continue; + } + RegionSimpleInfo server = RegionSimpleInfo.newBuilder() + .setName(regionInfo.Name) + .setTitle(regionInfo.Title) + .setType("DEV_PUBLIC") + .setDispatchUrl("https://" + (Grasscutter.getConfig().getDispatchOptions().PublicIp.isEmpty() ? Grasscutter.getConfig().getDispatchOptions().Ip : Grasscutter.getConfig().getDispatchOptions().PublicIp) + ":" + getAddress().getPort() + "/query_cur_region_" + regionInfo.Name) + .build(); + usedNames.add(regionInfo.Name); + servers.add(server); + + RegionInfo serverRegion = regionQuery.getRegionInfo().toBuilder() + .setIp(regionInfo.Ip) + .setPort(regionInfo.Port) + .setSecretKey(ByteString.copyFrom(FileUtils.read(Grasscutter.getConfig().KEY_FOLDER + "dispatchSeed.bin"))) + .build(); + + QueryCurrRegionHttpRsp parsedRegionQuery = regionQuery.toBuilder().setRegionInfo(serverRegion).build(); + regions.put(regionInfo.Name, new RegionData(parsedRegionQuery, Base64.getEncoder().encodeToString(parsedRegionQuery.toByteString().toByteArray()))); + } + QueryRegionListHttpRsp regionList = QueryRegionListHttpRsp.newBuilder() - .addServers(server) - .addServers(serverTest2) + .addAllServers(servers) .setClientSecretKey(rl.getClientSecretKey()) .setClientCustomConfigEncrypted(rl.getClientCustomConfigEncrypted()) .setEnableLoginPc(true) .build(); - - RegionInfo currentRegion = regionQuery.getRegionInfo().toBuilder() - .setIp((Grasscutter.getConfig().getGameServerOptions().PublicIp.isEmpty() ? Grasscutter.getConfig().getGameServerOptions().Ip : Grasscutter.getConfig().getGameServerOptions().PublicIp)) - .setPort(Grasscutter.getConfig().getGameServerOptions().Port) - .setSecretKey(ByteString.copyFrom(FileUtils.read(Grasscutter.getConfig().KEY_FOLDER + "dispatchSeed.bin"))) - .build(); - - QueryCurrRegionHttpRsp parsedRegionQuery = regionQuery.toBuilder().setRegionInfo(currentRegion).build(); this.regionListBase64 = Base64.getEncoder().encodeToString(regionList.toByteString().toByteArray()); - this.regionCurrentBase64 = Base64.getEncoder().encodeToString(parsedRegionQuery.toByteString().toByteArray()); - this.currRegion = parsedRegionQuery; } catch (Exception e) { Grasscutter.getLogger().error("Error while initializing region info!", e); } @@ -188,24 +222,36 @@ public final class DispatchServer { OutputStream os = t.getResponseBody(); os.write(response.getBytes()); os.close(); - }); - server.createContext("/query_cur_region", t -> { - // Log - Grasscutter.getLogger().info("Client request: query_cur_region"); - // Create a response form the request query parameters - URI uri = t.getRequestURI(); - String response = "CAESGE5vdCBGb3VuZCB2ZXJzaW9uIGNvbmZpZw=="; - if (uri.getQuery() != null && uri.getQuery().length() > 0) { - response = regionCurrentBase64; + + if(usersIngame.containsKey(t.getRemoteAddress())) { + usersIngame.remove(t.getRemoteAddress()); } - // Set the response header status and length - t.getResponseHeaders().put("Content-Type", Collections.singletonList("text/html; charset=UTF-8")); - t.sendResponseHeaders(200, response.getBytes().length); - // Write the response string - OutputStream os = t.getResponseBody(); - os.write(response.getBytes()); - os.close(); }); + + for (String regionName : regions.keySet()) { + server.createContext("/query_cur_region_" + regionName, t -> { + String regionCurrentBase64 = regions.get(regionName).Base64; + + // Log + Grasscutter.getLogger().info("Client request: query_cur_region_" + regionName); + // Create a response form the request query parameters + URI uri = t.getRequestURI(); + String response = "CAESGE5vdCBGb3VuZCB2ZXJzaW9uIGNvbmZpZw=="; + if (uri.getQuery() != null && uri.getQuery().length() > 0) { + response = regionCurrentBase64; + } + // Set the response header status and length + t.getResponseHeaders().put("Content-Type", Collections.singletonList("text/html; charset=UTF-8")); + t.sendResponseHeaders(200, response.getBytes().length); + // Write the response string + OutputStream os = t.getResponseBody(); + os.write(response.getBytes()); + os.close(); + //Save region info to hashmap for user, this for getCurrRegion(); + usersIngame.put(t.getRemoteAddress(), regionName); + }); + } + // Login via account server.createContext("/hk4e_global/mdk/shield/api/login", t -> { // Get post data @@ -464,4 +510,15 @@ public final class DispatchServer { } return result; } + + public static class RegionData { + + QueryCurrRegionHttpRsp parsedRegionQuery; + String Base64; + + public RegionData(QueryCurrRegionHttpRsp prq, String b64) { + this.parsedRegionQuery = prq; + this.Base64 = b64; + } + } } diff --git a/src/main/java/emu/grasscutter/server/packet/send/PacketPlayerLoginRsp.java b/src/main/java/emu/grasscutter/server/packet/send/PacketPlayerLoginRsp.java index 0d0871ca9..eef1a03e6 100644 --- a/src/main/java/emu/grasscutter/server/packet/send/PacketPlayerLoginRsp.java +++ b/src/main/java/emu/grasscutter/server/packet/send/PacketPlayerLoginRsp.java @@ -14,7 +14,7 @@ public class PacketPlayerLoginRsp extends GenshinPacket { this.setUseDispatchKey(true); - RegionInfo info = Grasscutter.getDispatchServer().getCurrRegion().getRegionInfo(); + RegionInfo info = Grasscutter.getDispatchServer().getCurrRegion(session.getAddress()).getRegionInfo(); PlayerLoginRsp p = PlayerLoginRsp.newBuilder() .setIsUseAbilityHash(true) // true