Skip to content
GitLab
Menu
Projects
Groups
Snippets
Loading...
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Sign in / Register
Toggle navigation
Menu
Open sidebar
ziqian zhang
Grasscutter
Commits
9ce43602
Commit
9ce43602
authored
Apr 21, 2022
by
Benjamin Elsdon
Browse files
Multi-server dispatch support and server run modes
parent
83fe5818
Changes
6
Show whitespace changes
Inline
Side-by-side
src/main/java/emu/grasscutter/Config.java
View file @
9ce43602
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
;
...
...
src/main/java/emu/grasscutter/Grasscutter.java
View file @
9ce43602
...
...
@@ -73,11 +73,26 @@ public final class Grasscutter {
DatabaseManager
.
initialize
();
// Start servers.
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
)
{
...
...
src/main/java/emu/grasscutter/database/DatabaseHelper.java
View file @
9ce43602
...
...
@@ -74,36 +74,36 @@ public class DatabaseHelper {
}
public
static
void
saveAccount
(
Account
account
)
{
DatabaseManager
.
getDatastore
().
save
(
account
);
DatabaseManager
.
get
Account
Datastore
().
save
(
account
);
}
public
static
Account
getAccountByName
(
String
username
)
{
MorphiaCursor
<
Account
>
cursor
=
DatabaseManager
.
getDatastore
().
createQuery
(
Account
.
class
).
field
(
"username"
).
equalIgnoreCase
(
username
).
find
(
FIND_ONE
);
MorphiaCursor
<
Account
>
cursor
=
DatabaseManager
.
get
Account
Datastore
().
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
<
Account
>
cursor
=
DatabaseManager
.
getDatastore
().
createQuery
(
Account
.
class
).
field
(
"token"
).
equal
(
token
).
find
(
FIND_ONE
);
MorphiaCursor
<
Account
>
cursor
=
DatabaseManager
.
get
Account
Datastore
().
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
<
Account
>
cursor
=
DatabaseManager
.
getDatastore
().
createQuery
(
Account
.
class
).
field
(
"_id"
).
equal
(
uid
).
find
(
FIND_ONE
);
MorphiaCursor
<
Account
>
cursor
=
DatabaseManager
.
get
Account
Datastore
().
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
<
Account
>
cursor
=
DatabaseManager
.
getDatastore
().
createQuery
(
Account
.
class
).
field
(
"playerId"
).
equal
(
playerId
).
find
(
FIND_ONE
);
MorphiaCursor
<
Account
>
cursor
=
DatabaseManager
.
get
Account
Datastore
().
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
<
Account
>
q
=
DatabaseManager
.
getDatastore
().
createQuery
(
Account
.
class
).
field
(
"username"
).
equalIgnoreCase
(
username
);
Query
<
Account
>
q
=
DatabaseManager
.
get
Account
Datastore
().
createQuery
(
Account
.
class
).
field
(
"username"
).
equalIgnoreCase
(
username
);
return
DatabaseManager
.
getDatastore
().
findAndDelete
(
q
)
!=
null
;
}
...
...
src/main/java/emu/grasscutter/database/DatabaseManager.java
View file @
9ce43602
...
...
@@ -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
...
...
@@ -35,9 +38,19 @@ public final class DatabaseManager {
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
<
String
>
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
)
{
...
...
src/main/java/emu/grasscutter/server/dispatch/DispatchServer.java
View file @
9ce43602
...
...
@@ -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
<
String
,
RegionData
>
regions
;
public
HashMap
<
InetSocketAddress
,
String
>
usersIngame
;
public
static
String
query_region_list
=
""
;
public
static
String
query_cur_region
=
""
;
public
DispatchServer
()
{
this
.
regions
=
new
HashMap
<
String
,
RegionData
>();
this
.
usersIngame
=
new
HashMap
<
InetSocketAddress
,
String
>();
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
);
List
<
RegionSimpleInfo
>
servers
=
new
ArrayList
<
RegionSimpleInfo
>();
List
<
String
>
usedNames
=
new
ArrayList
<
String
>();
// 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
"
)
.
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
);
}
}
RegionSimpleInfo
serverTest2
=
RegionSimpleInfo
.
newBuilder
()
.
setName
(
"os_euro"
)
.
setTitle
(
"Grasscutter"
)
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
"
)
.
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,10 +222,18 @@ public final class DispatchServer {
OutputStream
os
=
t
.
getResponseBody
();
os
.
write
(
response
.
getBytes
());
os
.
close
();
if
(
usersIngame
.
containsKey
(
t
.
getRemoteAddress
()))
{
usersIngame
.
remove
(
t
.
getRemoteAddress
());
}
});
server
.
createContext
(
"/query_cur_region"
,
t
->
{
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
"
);
Grasscutter
.
getLogger
().
info
(
"Client request: query_cur_region
_"
+
regionName
);
// Create a response form the request query parameters
URI
uri
=
t
.
getRequestURI
();
String
response
=
"CAESGE5vdCBGb3VuZCB2ZXJzaW9uIGNvbmZpZw=="
;
...
...
@@ -205,7 +247,11 @@ public final class DispatchServer {
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
;
}
}
}
src/main/java/emu/grasscutter/server/packet/send/PacketPlayerLoginRsp.java
View file @
9ce43602
...
...
@@ -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
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
.
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment