9 Commits

Author SHA1 Message Date
93df2d0b0e Format code [skip actions] 2024-07-07 02:59:06 +00:00
e7ed66477f fix(networking): Prevent hanging the network loop if an exception occurs 2024-07-06 22:54:10 -04:00
af70de316e fix(SceneScriptManager.java): Catch Lua groups NPE
this is a weird issue; found it while testing networking stack and it also crashed the network thread
2024-07-06 22:47:37 -04:00
f29189be8f misc: Update package versions
this also moves some packages to a general number set in `gradle.properties`
2024-07-06 22:34:10 -04:00
4ced11d567 fix(auth): Skip further decryption if encrypted password fails to decrypt
this should only occur if the wrong RSA key is used on the client, otherwise the patch probably forgot to set `is_crypto` to false
2024-07-06 22:33:46 -04:00
446e994ff0 fix(handbook): Skip reading handbook from resources if it is disabled 2024-07-06 22:25:18 -04:00
655016c92e fix(Grasscutter.java): Exclude compiled protos package from being scanned by reflections 2024-07-06 22:24:56 -04:00
d0e3720748 feat(networking): Abstract game session networking
includes:
- abstracted form of session handling
- existing implementation using new abstracted system
- general clean-up of GameSession.java
2024-07-06 22:14:26 -04:00
db4542653a misc(gradle): Allow support with Java 21 2024-07-06 19:30:13 -04:00
16 changed files with 368 additions and 220 deletions

View File

@ -54,7 +54,7 @@ spotless {
compileJava.options.encoding = 'UTF-8'
compileTestJava.options.encoding = 'UTF-8'
sourceCompatibility = JavaVersion.VERSION_17
sourceCompatibility = JavaVersion.VERSION_21
targetCompatibility = JavaVersion.VERSION_17
group = 'io.grasscutter'
@ -77,19 +77,19 @@ dependencies {
// Logging libraries.
implementation group: 'org.slf4j', name: 'slf4j-api', version: '2.0.7'
implementation group: 'ch.qos.logback', name: 'logback-core', version: '1.4.7'
implementation group: 'ch.qos.logback', name: 'logback-classic', version: '1.4.7'
implementation group: 'ch.qos.logback', name: 'logback-core', version: '1.4.14'
implementation group: 'ch.qos.logback', name: 'logback-classic', version: '1.4.12'
// Line reading libraries.
implementation group: 'org.jline', name: 'jline', version: '3.21.0'
implementation group: 'org.jline', name: 'jline', version: '3.25.0'
implementation group: 'org.jline', name: 'jline-terminal-jna', version: '3.21.0'
implementation group: 'net.java.dev.jna', name: 'jna', version: '5.10.0'
// Java Netty for networking.
implementation group: 'io.netty', name: 'netty-common', version: '4.1.86.Final'
implementation group: 'io.netty', name: 'netty-handler', version: '4.1.86.Final'
implementation group: 'io.netty', name: 'netty-transport-native-epoll', version: '4.1.86.Final'
implementation group: 'io.netty', name: 'netty-transport-native-kqueue', version: '4.1.86.Final'
implementation group: 'io.netty', name: 'netty-common', version: project.netty_version
implementation group: 'io.netty', name: 'netty-handler', version: project.netty_version
implementation group: 'io.netty', name: 'netty-transport-native-epoll', version: project.netty_version
implementation group: 'io.netty', name: 'netty-transport-native-kqueue', version: project.netty_version
// Serialization.
implementation group: 'com.google.code.gson', name: 'gson', version: '2.9.0'
@ -136,10 +136,10 @@ dependencies {
testImplementation group: 'com.squareup.okhttp3', name: 'okhttp', version: '4.10.0'
// Lombok.
compileOnly group: 'org.projectlombok', name: 'lombok', version: '1.18.26'
annotationProcessor group: 'org.projectlombok', name: 'lombok', version: '1.18.26'
testCompileOnly group: 'org.projectlombok', name: 'lombok', version: '1.18.26'
testAnnotationProcessor group: 'org.projectlombok', name: 'lombok', version: '1.18.26'
compileOnly group: 'org.projectlombok', name: 'lombok', version: project.lombok_version
annotationProcessor group: 'org.projectlombok', name: 'lombok', version: project.lombok_version
testCompileOnly group: 'org.projectlombok', name: 'lombok', version: project.lombok_version
testAnnotationProcessor group: 'org.projectlombok', name: 'lombok', version: project.lombok_version
}
configurations.configureEach {

View File

@ -1,2 +1,6 @@
org.gradle.jvmargs=-Xmx4096m
org.gradle.jvmargs=-Xmx8G
netty_version=4.1.111.Final
lombok_version=1.18.34
# spikehd was here :)

View File

@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.2-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

View File

@ -29,13 +29,15 @@ import lombok.*;
import org.jline.reader.*;
import org.jline.terminal.*;
import org.reflections.Reflections;
import org.reflections.util.ConfigurationBuilder;
import org.reflections.util.FilterBuilder;
import org.slf4j.LoggerFactory;
public final class Grasscutter {
public static final File configFile = new File("./config.json");
public static final Reflections reflector = new Reflections("emu.grasscutter");
@Getter private static final Logger logger = (Logger) LoggerFactory.getLogger(Grasscutter.class);
public static final Reflections reflector;
@Getter public static ConfigContainer config;
@Getter @Setter private static Language language;
@ -75,6 +77,16 @@ public final class Grasscutter {
var mongoLogger = (Logger) LoggerFactory.getLogger("org.mongodb.driver");
mongoLogger.setLevel(Level.OFF);
// Configure the reflector.
reflector =
new Reflections(
new ConfigurationBuilder()
.forPackage("emu.grasscutter")
.filterInputsBy(
new FilterBuilder()
.includePackage("emu.grasscutter")
.excludePackage("emu.grasscutter.net.proto")));
// Load server configuration.
Grasscutter.loadConfig();
// Attempt to update configuration.

View File

@ -112,7 +112,13 @@ public final class DefaultAuthenticators {
cipher.doFinal(Utils.base64Decode(request.getPasswordRequest().password)),
StandardCharsets.UTF_8);
} catch (Exception ignored) {
decryptedPassword = request.getPasswordRequest().password;
if (requestData.is_crypto) {
response.retcode = -201;
response.message = translate("messages.dispatch.account.password_crypto_error");
return response;
} else {
decryptedPassword = request.getPasswordRequest().password;
}
}
if (decryptedPassword == null) {

View File

@ -35,9 +35,10 @@ public class ConfigContainer {
* HTTP server should start immediately.
* Version 13 - 'game.useUniquePacketKey' was added to control whether the
* encryption key used for packets is a constant or randomly generated.
* Version 14 - 'game.timeout' was added to control the UDP client timeout.
*/
private static int version() {
return 13;
return 14;
}
/**
@ -183,6 +184,9 @@ public class ConfigContainer {
/* Kcp internal work interval (milliseconds) */
public int kcpInterval = 20;
/* Time to wait (in seconds) before terminating a connection. */
public long timeout = 30;
/* Controls whether packets should be logged in console or not */
public ServerDebugMode logPackets = ServerDebugMode.NONE;
/* Show packet payload in console or no (in any case the payload is shown in encrypted view) */

View File

@ -0,0 +1,27 @@
package emu.grasscutter.net;
import java.net.InetSocketAddress;
import org.slf4j.Logger;
/** This is most closely related to the previous `KcpTunnel` interface. */
public interface IKcpSession {
/**
* @return The session's unique logger.
*/
Logger getLogger();
/**
* @return The connecting client's address.
*/
InetSocketAddress getAddress();
/** Closes the server's connection to the client. */
void close();
/**
* Sends raw data to the client.
*
* @param data The data to send. This should not be KCP-encoded.
*/
void send(byte[] data);
}

View File

@ -0,0 +1,38 @@
package emu.grasscutter.net;
import emu.grasscutter.Grasscutter;
import emu.grasscutter.server.game.GameServer;
import java.net.InetSocketAddress;
public interface INetworkTransport {
/**
* Waits for the server to be active. This should be used to ensure that the server is ready to
* accept connections.
*/
default GameServer waitForServer() throws InterruptedException {
int depth = 0;
GameServer server;
while ((server = Grasscutter.getGameServer()) == null) {
Thread.sleep(1000);
if (depth++ > 5) {
throw new IllegalStateException("Game server is not available!");
}
}
return server;
}
/**
* This is invoked when the transport should start listening for incoming connections.
*
* @param listening The address/port to listen on.
*/
void start(InetSocketAddress listening);
/**
* This is invoked when the transport should stop listening for incoming connections. This should
* also close all active connections.
*/
void shutdown();
}

View File

@ -0,0 +1,46 @@
package emu.grasscutter.net.impl;
import emu.grasscutter.net.IKcpSession;
import io.netty.buffer.Unpooled;
import java.net.InetSocketAddress;
import kcp.highway.Ukcp;
import lombok.Getter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* This is the default implementation of a KCP session. It uses {@link Ukcp} as the underlying
* wrapper.
*/
@Getter
public class KcpSessionImpl implements IKcpSession {
private final Ukcp handle;
private final Logger logger;
public KcpSessionImpl(Ukcp handle) {
this.handle = handle;
this.logger = LoggerFactory.getLogger("KcpSession " + handle.getConv());
}
@Override
public InetSocketAddress getAddress() {
return this.getHandle().user().getRemoteAddress();
}
@Override
public void close() {
this.getHandle().close(true);
}
@Override
public void send(byte[] data) {
var buffer = Unpooled.wrappedBuffer(data);
try {
this.getHandle().write(buffer);
} catch (Exception ex) {
this.getLogger().warn("Unable to send packet.", ex);
} finally {
buffer.release();
}
}
}

View File

@ -0,0 +1,122 @@
package emu.grasscutter.net.impl;
import static emu.grasscutter.config.Configuration.GAME_INFO;
import emu.grasscutter.net.INetworkTransport;
import emu.grasscutter.server.game.GameSession;
import emu.grasscutter.utils.Utils;
import io.netty.buffer.ByteBuf;
import io.netty.channel.DefaultEventLoop;
import io.netty.channel.EventLoop;
import java.net.InetSocketAddress;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import kcp.highway.ChannelConfig;
import kcp.highway.KcpListener;
import kcp.highway.KcpServer;
import kcp.highway.Ukcp;
import lombok.extern.slf4j.Slf4j;
/**
* The default implementation of a {@link INetworkTransport}. Uses {@link KcpServer} as the
* underlying transport.
*/
@Slf4j
public class NetworkTransportImpl extends KcpServer implements INetworkTransport {
private final EventLoop networkLoop = new DefaultEventLoop();
private final ConcurrentHashMap<Ukcp, GameSession> sessions = new ConcurrentHashMap<>();
@Override
public void start(InetSocketAddress listening) {
var settings = new ChannelConfig();
settings.setTimeoutMillis(GAME_INFO.timeout * 1000);
settings.nodelay(true, GAME_INFO.kcpInterval, 2, true);
settings.setMtu(1400);
settings.setSndwnd(256);
settings.setRcvwnd(256);
settings.setUseConvChannel(true);
settings.setAckNoDelay(false);
this.init(new Listener(), settings, listening);
}
@Override
public void shutdown() {
this.stop();
try {
this.networkLoop.shutdownGracefully();
if (!this.networkLoop.awaitTermination(5, TimeUnit.SECONDS)) {
log.warn("Network loop did not terminate in time.");
}
} catch (Exception ex) {
log.warn("Failed to shutdown network loop.", ex);
}
}
class Listener implements KcpListener {
@Override
public void onConnected(Ukcp ukcp) {
var transport = NetworkTransportImpl.this;
try {
var server = transport.waitForServer();
var session = new KcpSessionImpl(ukcp);
var gameSession = new GameSession(server, session);
transport.sessions.put(ukcp, gameSession);
gameSession.onConnected();
} catch (InterruptedException | IllegalStateException ex) {
NetworkTransportImpl.log.warn("Unable to establish connection.", ex);
ukcp.close();
}
}
@Override
public void handleReceive(ByteBuf byteBuf, Ukcp ukcp) {
var transport = NetworkTransportImpl.this;
try {
var session = transport.sessions.get(ukcp);
if (session == null) {
NetworkTransportImpl.log.debug("Received data from unknown session.");
return;
}
// Copy the buffer to avoid reference issues.
var data = Utils.byteBufToArray(byteBuf);
transport.networkLoop.submit(
() -> {
// Fun fact: if we don't catch exceptions here,
// we run the risk of locking the entire network loop.
try {
session.onReceived(data);
} catch (Exception ex) {
session.getLogger().warn("Unable to handle received data.", ex);
}
});
} catch (Exception ex) {
NetworkTransportImpl.log.warn("Unable to handle received data.", ex);
}
}
@Override
public void handleException(Throwable throwable, Ukcp ukcp) {
NetworkTransportImpl.log.debug("Exception occurred in session.", throwable);
}
@Override
public void handleClose(Ukcp ukcp) {
var sessions = NetworkTransportImpl.this.sessions;
var session = sessions.get(ukcp);
if (session == null) {
NetworkTransportImpl.log.debug("Received close from unknown session.");
return;
}
session.onDisconnected();
sessions.remove(ukcp);
}
}
}

View File

@ -507,6 +507,11 @@ public class SceneScriptManager {
.forEach(
block -> {
block.load(sceneId, meta.context);
if (block.groups == null) {
Grasscutter.getLogger().error("block.groups null for block {}", block.id);
return;
}
block.groups.values().stream()
.filter(g -> !g.dynamic_load)
.forEach(

View File

@ -32,6 +32,8 @@ import emu.grasscutter.game.talk.TalkSystem;
import emu.grasscutter.game.tower.TowerSystem;
import emu.grasscutter.game.world.World;
import emu.grasscutter.game.world.WorldDataSystem;
import emu.grasscutter.net.INetworkTransport;
import emu.grasscutter.net.impl.NetworkTransportImpl;
import emu.grasscutter.net.packet.PacketHandler;
import emu.grasscutter.net.proto.SocialDetailOuterClass.SocialDetail;
import emu.grasscutter.server.dispatch.DispatchClient;
@ -47,14 +49,20 @@ import java.net.*;
import java.time.*;
import java.util.*;
import java.util.concurrent.*;
import kcp.highway.*;
import lombok.*;
import lombok.extern.slf4j.Slf4j;
import org.jetbrains.annotations.*;
@Getter
public final class GameServer extends KcpServer implements Iterable<Player> {
@Slf4j
public final class GameServer implements Iterable<Player> {
/** This can be set by plugins to change the network transport implementation. */
@Setter private static Class<? extends INetworkTransport> transport = NetworkTransportImpl.class;
// Game server base
private final InetSocketAddress address;
private final INetworkTransport netTransport;
private final GameServerPacketHandler packetHandler;
private final Map<Integer, Player> players;
private final Set<World> worlds;
@ -106,6 +114,7 @@ public final class GameServer extends KcpServer implements Iterable<Player> {
this.taskMap = null;
this.address = null;
this.netTransport = null;
this.packetHandler = null;
this.dispatchClient = null;
this.players = null;
@ -131,16 +140,18 @@ public final class GameServer extends KcpServer implements Iterable<Player> {
return;
}
var channelConfig = new ChannelConfig();
channelConfig.nodelay(true, GAME_INFO.kcpInterval, 2, true);
channelConfig.setMtu(1400);
channelConfig.setSndwnd(256);
channelConfig.setRcvwnd(256);
channelConfig.setTimeoutMillis(30 * 1000); // 30s
channelConfig.setUseConvChannel(true);
channelConfig.setAckNoDelay(false);
// Create the network transport.
INetworkTransport transport;
try {
transport = GameServer.transport.getDeclaredConstructor().newInstance();
} catch (Exception ex) {
log.error("Failed to create network transport.", ex);
transport = new NetworkTransportImpl();
}
this.init(GameSessionManager.getListener(), channelConfig, address);
// Initialize the transport.
this.netTransport = transport;
this.netTransport.start(this.address = address);
EnergyManager.initialize();
StaminaManager.initialize();
@ -149,7 +160,6 @@ public final class GameServer extends KcpServer implements Iterable<Player> {
CombineManger.initialize();
// Game Server base
this.address = address;
this.packetHandler = new GameServerPacketHandler(PacketHandler.class);
this.dispatchClient = new DispatchClient(GameServer.getDispatchUrl());
this.players = new ConcurrentHashMap<>();
@ -184,7 +194,7 @@ public final class GameServer extends KcpServer implements Iterable<Player> {
private static InetSocketAddress getAdapterInetSocketAddress() {
InetSocketAddress inetSocketAddress;
if (GAME_INFO.bindAddress.equals("")) {
if (GAME_INFO.bindAddress.isEmpty()) {
inetSocketAddress = new InetSocketAddress(GAME_INFO.bindPort);
} else {
inetSocketAddress = new InetSocketAddress(GAME_INFO.bindAddress, GAME_INFO.bindPort);
@ -353,19 +363,6 @@ public final class GameServer extends KcpServer implements Iterable<Player> {
this.getWorlds().forEach(World::save);
Utils.sleep(1000L); // Wait 1 second for operations to finish.
this.stop(); // Stop the server.
try {
var threadPool = GameSessionManager.getLogicThread();
// Shutdown network thread.
threadPool.shutdownGracefully();
// Wait for the network thread to finish.
if (!threadPool.awaitTermination(5, TimeUnit.SECONDS)) {
Grasscutter.getLogger().error("Logic thread did not terminate!");
}
} catch (InterruptedException ignored) {
}
}
@NotNull @Override

View File

@ -7,18 +7,18 @@ import emu.grasscutter.Grasscutter;
import emu.grasscutter.Grasscutter.ServerDebugMode;
import emu.grasscutter.game.Account;
import emu.grasscutter.game.player.Player;
import emu.grasscutter.net.IKcpSession;
import emu.grasscutter.net.packet.*;
import emu.grasscutter.server.event.game.SendPacketEvent;
import emu.grasscutter.utils.*;
import io.netty.buffer.*;
import java.io.File;
import java.net.InetSocketAddress;
import java.nio.file.Path;
import lombok.*;
import org.slf4j.Logger;
public class GameSession implements GameSessionManager.KcpChannel {
private final GameServer server;
private GameSessionManager.KcpTunnel tunnel;
public class GameSession implements IGameSession {
@Getter private final GameServer server;
private IKcpSession session;
@Getter @Setter private Account account;
@Getter private Player player;
@ -33,8 +33,10 @@ public class GameSession implements GameSessionManager.KcpChannel {
@Getter private long lastPingTime;
private int lastClientSeq = 10;
public GameSession(GameServer server) {
public GameSession(GameServer server, IKcpSession session) {
this.server = server;
this.session = session;
this.state = SessionState.WAITING_FOR_TOKEN;
this.lastPingTime = System.currentTimeMillis();
@ -44,24 +46,12 @@ public class GameSession implements GameSessionManager.KcpChannel {
}
}
public GameServer getServer() {
return server;
}
public InetSocketAddress getAddress() {
try {
return tunnel.getAddress();
} catch (Throwable ignore) {
return null;
}
return this.session.getAddress();
}
public boolean useSecretKey() {
return useSecretKey;
}
public String getAccountId() {
return this.getAccount().getId();
public Logger getLogger() {
return this.session.getLogger();
}
public synchronized void setPlayer(Player player) {
@ -83,30 +73,17 @@ public class GameSession implements GameSessionManager.KcpChannel {
return ++lastClientSeq;
}
public void replayPacket(int opcode, String name) {
Path filePath = FileUtils.getPluginPath(name);
File p = filePath.toFile();
if (!p.exists()) return;
byte[] packet = FileUtils.read(p);
BasePacket basePacket = new BasePacket(opcode);
basePacket.setData(packet);
send(basePacket);
}
public void logPacket(String sendOrRecv, int opcode, byte[] payload) {
Grasscutter.getLogger()
.info(sendOrRecv + ": " + PacketOpcodesUtils.getOpcodeName(opcode) + " (" + opcode + ")");
this.session
.getLogger()
.info("{}: {} ({})", sendOrRecv, PacketOpcodesUtils.getOpcodeName(opcode), opcode);
if (GAME_INFO.isShowPacketPayload) System.out.println(Utils.bytesToHex(payload));
}
public void send(BasePacket packet) {
// Test
if (packet.getOpcode() <= 0) {
Grasscutter.getLogger().warn("Tried to send packet with missing cmd id!");
this.session.getLogger().warn("Attempted to send packet with unknown ID!");
return;
}
@ -146,28 +123,24 @@ public class GameSession implements GameSessionManager.KcpChannel {
if (packet.shouldEncrypt) {
Crypto.xor(bytes, packet.useDispatchKey() ? Crypto.DISPATCH_KEY : this.encryptKey);
}
tunnel.writeData(bytes);
} catch (Exception ignored) {
Grasscutter.getLogger().debug("Unable to send packet to client.");
this.session.send(bytes);
} catch (Exception ex) {
this.session.getLogger().debug("Unable to send packet to client.", ex);
}
}
}
@Override
public void onConnected(GameSessionManager.KcpTunnel tunnel) {
this.tunnel = tunnel;
public void onConnected() {
Grasscutter.getLogger().info(translate("messages.game.connect", this.getAddress().toString()));
}
@Override
public void handleReceive(byte[] bytes) {
public void onReceived(byte[] bytes) {
// Decrypt and turn back into a packet
Crypto.xor(bytes, useSecretKey() ? this.encryptKey : Crypto.DISPATCH_KEY);
Crypto.xor(bytes, this.useSecretKey ? this.encryptKey : Crypto.DISPATCH_KEY);
ByteBuf packet = Unpooled.wrappedBuffer(bytes);
// Log
// logPacket(packet);
// Handle
try {
boolean allDebug = GAME_INFO.logPackets == ServerDebugMode.ALL;
while (packet.readableBytes() > 0) {
@ -179,11 +152,13 @@ public class GameSession implements GameSessionManager.KcpChannel {
int const1 = packet.readShort();
if (const1 != 17767) {
if (allDebug) {
Grasscutter.getLogger()
.error("Bad Data Package Received: got {} ,expect 17767", const1);
this.session
.getLogger()
.error("Invalid packet header received: got {}, expected 17767", const1);
}
return; // Bad packet
}
// Data
int opcode = packet.readShort();
int headerLength = packet.readShort();
@ -197,8 +172,9 @@ public class GameSession implements GameSessionManager.KcpChannel {
int const2 = packet.readShort();
if (const2 != -30293) {
if (allDebug) {
Grasscutter.getLogger()
.error("Bad Data Package Received: got {} ,expect -30293", const2);
this.session
.getLogger()
.error("Invalid packet footer received: got {}, expected -30293", const2);
}
return; // Bad packet
}
@ -226,16 +202,15 @@ public class GameSession implements GameSessionManager.KcpChannel {
// Handle
getServer().getPacketHandler().handle(this, opcode, header, payload);
}
} catch (Exception e) {
e.printStackTrace();
} catch (Exception ex) {
this.session.getLogger().warn("Unable to process packet.", ex);
} finally {
// byteBuf.release(); //Needn't
packet.release();
}
}
@Override
public void handleClose() {
public void onDisconnected() {
setState(SessionState.INACTIVE);
// send disconnection pack in case of reconnection
Grasscutter.getLogger()
@ -247,19 +222,20 @@ public class GameSession implements GameSessionManager.KcpChannel {
player.onLogout();
}
try {
send(new BasePacket(PacketOpcodes.ServerDisconnectClientNotify));
} catch (Throwable ignore) {
Grasscutter.getLogger().warn("closing {} error", getAddress().getAddress().getHostAddress());
this.send(new BasePacket(PacketOpcodes.ServerDisconnectClientNotify));
} catch (Throwable ex) {
this.session.getLogger().warn("Failed to disconnect client.", ex);
}
tunnel = null;
this.session = null;
}
public void close() {
tunnel.close();
this.session.close();
}
public boolean isActive() {
return getState() == SessionState.ACTIVE;
return this.getState() == SessionState.ACTIVE;
}
public enum SessionState {

View File

@ -1,114 +0,0 @@
package emu.grasscutter.server.game;
import emu.grasscutter.Grasscutter;
import emu.grasscutter.utils.Utils;
import io.netty.buffer.*;
import io.netty.channel.DefaultEventLoop;
import java.net.InetSocketAddress;
import java.util.concurrent.ConcurrentHashMap;
import kcp.highway.*;
import lombok.Getter;
public class GameSessionManager {
@Getter private static final DefaultEventLoop logicThread = new DefaultEventLoop();
private static final ConcurrentHashMap<Ukcp, GameSession> sessions = new ConcurrentHashMap<>();
private static final KcpListener listener =
new KcpListener() {
@Override
public void onConnected(Ukcp ukcp) {
int times = 0;
GameServer server = Grasscutter.getGameServer();
while (server == null) { // Waiting server to establish
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
ukcp.close();
return;
}
if (times++ > 5) {
Grasscutter.getLogger().error("Service is not available!");
ukcp.close();
return;
}
server = Grasscutter.getGameServer();
}
GameSession conversation = new GameSession(server);
conversation.onConnected(
new KcpTunnel() {
@Override
public InetSocketAddress getAddress() {
return ukcp.user().getRemoteAddress();
}
@Override
public void writeData(byte[] bytes) {
ByteBuf buf = Unpooled.wrappedBuffer(bytes);
ukcp.write(buf);
buf.release();
}
@Override
public void close() {
ukcp.close();
}
@Override
public int getSrtt() {
return ukcp.srtt();
}
});
sessions.put(ukcp, conversation);
}
@Override
public void handleReceive(ByteBuf buf, Ukcp kcp) {
var byteData = Utils.byteBufToArray(buf);
logicThread.execute(
() -> {
try {
var conversation = sessions.get(kcp);
if (conversation != null) {
conversation.handleReceive(byteData);
}
} catch (Exception e) {
e.printStackTrace();
}
});
}
@Override
public void handleException(Throwable ex, Ukcp ukcp) {}
@Override
public void handleClose(Ukcp ukcp) {
GameSession conversation = sessions.get(ukcp);
if (conversation != null) {
conversation.handleClose();
sessions.remove(ukcp);
}
}
};
public static KcpListener getListener() {
return listener;
}
public interface KcpTunnel {
InetSocketAddress getAddress();
void writeData(byte[] bytes);
void close();
int getSrtt();
}
interface KcpChannel {
void onConnected(KcpTunnel tunnel);
void handleClose();
void handleReceive(byte[] bytes);
}
}

View File

@ -0,0 +1,20 @@
package emu.grasscutter.server.game;
public interface IGameSession {
/**
* Invoked when the server establishes a connection to the client.
*
* <p>This is invoked after the KCP handshake is completed.
*/
void onConnected();
/** Invoked when the server loses connection to the client. */
void onDisconnected();
/**
* Invoked when the server receives data from the client.
*
* @param data The raw data (not KCP-encoded) received from the client.
*/
void onReceived(byte[] data);
}

View File

@ -25,8 +25,13 @@ public final class HandbookHandler implements Router {
* found.
*/
public HandbookHandler() {
if (!HANDBOOK.enable) {
this.serve = false;
return;
}
this.handbook = new String(FileUtils.readResource("/html/handbook.html"));
this.serve = HANDBOOK.enable && this.handbook.length() > 0;
this.serve = !this.handbook.isEmpty();
var server = HANDBOOK.server;
if (this.serve && server.enforced) {