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
20 changed files with 434 additions and 279 deletions

View File

@ -18,12 +18,6 @@
* Spawning monsters via console * Spawning monsters via console
* Inventory features (receiving items/characters, upgrading items/characters, etc) * Inventory features (receiving items/characters, upgrading items/characters, etc)
## Foreward
### **Grasscutter beyond the latest release will have no handholding in terms of instructions.**
Grasscutter has not been actively maintained and currently (as of January 12th, 2025) only works up to version REL4.0.1 (introduction to Fontaine). If you have a beta version/unofficial version of Grasscutter, this guide should theoretically still work, however, we will not provide official support these versions. You can still try your luck in the Discord if you are stuck, but please don't act entitled.
## Quick setup guide ## Quick setup guide
**Note**: For support please join our [Discord](https://discord.gg/T5vZU6UyeG). **Note**: For support please join our [Discord](https://discord.gg/T5vZU6UyeG).
@ -35,7 +29,6 @@ Grasscutter has not been actively maintained and currently (as of January 12th,
- Get game version REL4.0.x (If you don't have a 4.0.x client, you can find it here and open any of the links to download it): - Get game version REL4.0.x (If you don't have a 4.0.x client, you can find it here and open any of the links to download it):
[4.0.x Client-github](https://github.com/JRSKelvin/GenshinRepository/blob/main/Version%204.0.0.md) [4.0.x Client-github](https://github.com/JRSKelvin/GenshinRepository/blob/main/Version%204.0.0.md)
[4.0.x Client-cloud drive](https://www.123pan.com/s/HoqUVv-U7SBA.html) [4.0.x Client-cloud drive](https://www.123pan.com/s/HoqUVv-U7SBA.html)
- ***UPDATE JAN 12, 2025: YOU CANNOT MIX AND MATCH GAME VERSIONS AND SERVER VERSIONS, PLEASE DOWNLOAD THE CORRECT VERSION OF GRASSCUTTER FOR YOUR VERSION OF THE GAME.***
- Download the [latest Cultivation version](https://github.com/Grasscutters/Cultivation/releases/latest). Use the `.msi` installer. - Download the [latest Cultivation version](https://github.com/Grasscutters/Cultivation/releases/latest). Use the `.msi` installer.
- After opening Cultivation (as admin), press the download button in the upper right corner. - After opening Cultivation (as admin), press the download button in the upper right corner.

View File

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

View File

@ -5,7 +5,7 @@
[EN](../README.md) | [简中](README_zh-CN.md) | [繁中](README_zh-TW.md) | [FR](README_fr-FR.md) | [ES](README_es-ES.md) | [HE](README_HE.md) | [RU](README_ru-RU.md) | [PL](README_pl-PL.md) | [ID](README_id-ID.md) | [KR](README_ko-KR.md) | [FIL/PH](README_fil-PH.md) | [NL](README_NL.md) | [JP](README_ja-JP.md) | [IT](README_it-IT.md) | [VI](README_vi-VN.md) | [हिंदी](README_hn-IN.md) [EN](../README.md) | [简中](README_zh-CN.md) | [繁中](README_zh-TW.md) | [FR](README_fr-FR.md) | [ES](README_es-ES.md) | [HE](README_HE.md) | [RU](README_ru-RU.md) | [PL](README_pl-PL.md) | [ID](README_id-ID.md) | [KR](README_ko-KR.md) | [FIL/PH](README_fil-PH.md) | [NL](README_NL.md) | [JP](README_ja-JP.md) | [IT](README_it-IT.md) | [VI](README_vi-VN.md) | [हिंदी](README_hn-IN.md)
**Aantekening:** We verwelkomen altijd bijdragers aan het project. Lees onze [Gedragscode](https://github.com/Grasscutters/Grasscutter/blob/stable/CONTRIBUTING.md) zorgvuldig door voordat u uw bijdrage toevoegt. **Aantekening:** We verwelkomen altijd bijdragers aan het project. Lees onze [Gedragscode](https://github.com/Grasscutters/Grasscutter/blob/development/README_NL.md#bijdragen-aan-het-project) zorgvuldig door voordat u uw bijdrage toevoegt.
## Huidige functies ## Huidige functies

View File

@ -22,25 +22,52 @@
**각주 :** 도움이 필요할 경우 [Discord](https://discord.gg/T5vZU6UyeG)에 가입하세요. **각주 :** 도움이 필요할 경우 [Discord](https://discord.gg/T5vZU6UyeG)에 가입하세요.
### 빠른 설치 (자동) ### 설치에 필요한 것들
- [Java 17](https://www.oracle.com/java/technologies/javase/jdk17-archive-downloads.html) 설치 * Java SE - 17 ([링크](https://www.oracle.com/java/technologies/javase/jdk17-archive-downloads.html))
- [MongoDB Community Server](https://www.mongodb.com/try/download/community) 설치
- 게임 버전 REL4.0.x 다운로드 (만약 4.0.x 클라이언트를 가지고 있지 않다면, 여기서 찾을 수 있습니다.):
[4.0.x 클라이언트 - GitHub](https://github.com/JRSKelvin/GenshinRepository/blob/main/Version%204.0.0.md)
[4.0.x 클라이언트 - 구글 드라이브브](https://www.123pan.com/s/HoqUVv-U7SBA.html)
- [최신 Cultivation](https://github.com/Grasscutters/Cultivation/releases/latest) 다운로드하세요. `.msi` 설치파일을 사용하면 됩니다. **각주 :** **실행**만을 원한다면, **jre**만 있어도 괜찮습니다.
- (관리자 권한으로) Cultivation을 실행한 후, 우측 상단에 위치한 다운로드 버튼을 클릭하세요.
- `올인원 다운로드`를 클릭하세요.
- 우측 상단에 위치한 톱니바퀴 버튼을 누르세요.
- 게임 설치 경로를 게임이 위치한 경로로 설정하세요.
- 사용자 지정 Java 경로 설정을 `C:\Program Files\Java\jdk-17\bin\java.exe`로 설정하세요.
- 다른 모든 설정은 기본값으로 두세요.
- 게임 시작 버튼 옆에 위치한 작은 버튼을 누르세요. * [MongoDB](https://www.mongodb.com/try/download/community) (4.0 이상의 버전 추천)
- 게임 시작 버튼을 누르세요.
- 원하는 사용자 이름으로 로그인하세요. 비밀번호는 무엇이든 가능합니다. * 프록시 데몬 : mitmproxy (mitmdump 추천), Fiddler Classic 등.
### 실행
**각주 :** 구버전에서 업데이트 했을 경우, `config.json` 파일을 재생성하기 위해 파일을 삭제하세요.
1. `grasscutter.jar` 얻기
- [Actions](https://github.com/Grasscutters/Grasscutter/suites/6895963598/artifacts/267483297) 탭에서 다운로드
- [직접 빌드하기](#빌드하기)
2. grasscutter.jar 파일이 위치한 폴더에 `resources` 폴더를 생성하고, `BinOutput``ExcelBinOutput` 폴더를 생성한 폴더 내로 옮기세요. *(이 파일들을 얻는 더 자세한 방법에 대해서는 [위키](https://github.com/Grasscutters/Grasscutter/wiki)를 참조하세요.)*
3. Grasscutter를 `java -jar grasscutter.jar` 명령어로 실행합니다. **MongoDB 서비스가 정상적으로 실행되고 있는지 확인하세요.**
### 클라이언트와의 연결
½. [서버 콘솔 명령어](https://github.com/Grasscutters/Grasscutter/wiki/Commands#targeting)를 이용해서 계정을 생성합니다.
1. 리다이렉트 트래픽 : (1가지 선택)
- mitmdump: `mitmdump -s proxy.py -k`
신뢰하는 인증 기관 인증서 (CA Cert) :
**각주 :** CA 인증서는 보통 `%USERPROFILE%\ .mitmproxy` 경로에 저장되며, `http://mitm.it`에서 다운로드 받을 수도 있습니다.
더블 클릭하여 [설치](https://docs.microsoft.com/en-us/skype-sdk/sdn/articles/installing-the-trusted-root-certificate#installing-a-trusted-root-certificate) 또는 ...
- 명령어를 통해서
```shell
certutil -addstore root %USERPROFILE%\.mitmproxy\mitmproxy-ca-cert.cer
```
- Fiddler Classic : Fiddler Classic을 실행한 후, Setting에서 `Decrypt https traffic` 옵션을 켜고, Tools -> Options -> Connections에 있는 기본 포트를 `8888`을 제외한 다른 포트로 지정합니다. 그리고 [이 스크립트](https://github.lunatic.moe/fiddlerscript)를 불러옵니다.
- [호스트 파일](https://github.com/Grasscutters/Grasscutter/wiki/Running#traffic-route-map)
2. 네트워크 프록시를 `127.0.0.1:8080` 로 설정하거나 지정한 프록시 포트로 설정합니다.
**또한 `start.cmd`를 실행함으로써, 서버와 프록시 데몬을 자동으로 실행되게 할 수 있습니다. 이를 이용하기 위해서는 JAVA_HOME 환경 변수를 등록해야 합니다.**
### 빌드하기 ### 빌드하기
@ -50,50 +77,39 @@ Grasscutter는 종속성 및 컴파일 처리를 위해 Gradle을 이용합니
- [Java SE 개발 키트 - 17](https://www.oracle.com/java/technologies/javase/jdk17-archive-downloads.html) - [Java SE 개발 키트 - 17](https://www.oracle.com/java/technologies/javase/jdk17-archive-downloads.html)
- [Git](https://git-scm.com/downloads) - [Git](https://git-scm.com/downloads)
- [NodeJS](https://nodejs.org/en/download) (선택, 핸드북을 빌드하기 위해 필요함.)
##### 클론 ##### 윈도우 (온라인)
```shell ```shell
git clone --recurse-submodules https://github.com/Grasscutters/Grasscutter.git git clone https://github.com/Grasscutters/Grasscutter.git
cd Grasscutter cd Grasscutter
.\gradlew.bat # 개발 환경 설정
.\gradlew jar # 컴파일
``` ```
##### 컴파일 ##### 윈도우 (로컬)
**각주**: 핸드북 생성은 일부 시스템에서 실패할 수도 있습니다. 핸드북 생성을 비활성화하려면, `gradlew jar`명령에 `-PskipHandbook=1`명령줄 스위치를 추가하세요.
윈도우:
```shell ```shell
.\gradlew.bat # 환경 준비 cd <로컬 주소>/Grasscutter
.\gradlew jar .\gradlew.bat # 개발 환경 설정
.\gradlew jar # 컴파일
``` ```
리눅스 (GNU): ##### 리눅스
```bash ```bash
git clone https://github.com/Grasscutters/Grasscutter.git
cd Grasscutter
chmod +x gradlew chmod +x gradlew
./gradlew jar ./gradlew jar # 컴파일
```
##### 핸드북 컴파일 (수동동)
Gradle 사용:
```shell
./gradlew generateHandbook
```
NPM 사용:
```shell
cd src/handbook
npm install
npm run build
``` ```
프로젝트 폴더의 최상단에서 jar 파일을 찾을 수 있습니다. 프로젝트 폴더의 최상단에서 jar 파일을 찾을 수 있습니다.
### 문제 해결 ### 명령어들은 [위키](https://github.com/Grasscutters/Grasscutter/wiki/Commands)에서 확인할 수 있습니다.
흔한 문제들의 해결방법과 도움을 요청하려면, [우리의 디스코드 서버](https://discord.gg/T5vZU6UyeG)에 참가하고 support 채널에 가보세요.
# 빠른 문제 해결
* 만약 컴파일링이 정상적으로 완료되지 않을 경우, JDK 설치를 확인하세요. (JDK 버전 17 및 JDK의 bin 경로 변수 등록을 확인)
* 클라이언트가 연결되지 않거나, 로그인이 안 되거나, 4206 오류가 뜨는 등의 경우 - 대부분 프록시 데몬의 설치에 문제가 있을 것입니다. Fiddler를 사용하고 있다면, 8888을 제외한 다른 포트에서 구동되고 있는지 확인하세요.
* 구동 순서 : MongoDB > Grasscutter > 프록시 데몬 (mitmdump, fiddler 등) > 게임

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 :) # spikehd was here :)

View File

@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists 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 zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists

View File

@ -29,13 +29,15 @@ import lombok.*;
import org.jline.reader.*; import org.jline.reader.*;
import org.jline.terminal.*; import org.jline.terminal.*;
import org.reflections.Reflections; import org.reflections.Reflections;
import org.reflections.util.ConfigurationBuilder;
import org.reflections.util.FilterBuilder;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
public final class Grasscutter { public final class Grasscutter {
public static final File configFile = new File("./config.json"); 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); @Getter private static final Logger logger = (Logger) LoggerFactory.getLogger(Grasscutter.class);
public static final Reflections reflector;
@Getter public static ConfigContainer config; @Getter public static ConfigContainer config;
@Getter @Setter private static Language language; @Getter @Setter private static Language language;
@ -75,6 +77,16 @@ public final class Grasscutter {
var mongoLogger = (Logger) LoggerFactory.getLogger("org.mongodb.driver"); var mongoLogger = (Logger) LoggerFactory.getLogger("org.mongodb.driver");
mongoLogger.setLevel(Level.OFF); 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. // Load server configuration.
Grasscutter.loadConfig(); Grasscutter.loadConfig();
// Attempt to update configuration. // Attempt to update configuration.

View File

@ -112,7 +112,13 @@ public final class DefaultAuthenticators {
cipher.doFinal(Utils.base64Decode(request.getPasswordRequest().password)), cipher.doFinal(Utils.base64Decode(request.getPasswordRequest().password)),
StandardCharsets.UTF_8); StandardCharsets.UTF_8);
} catch (Exception ignored) { } 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) { if (decryptedPassword == null) {

View File

@ -17,10 +17,8 @@ public final class StopCommand implements CommandHandler {
@Override @Override
public void execute(Player sender, Player targetPlayer, List<String> args) { public void execute(Player sender, Player targetPlayer, List<String> args) {
CommandHandler.sendMessage(null, translate("commands.stop.success")); CommandHandler.sendMessage(null, translate("commands.stop.success"));
if (Grasscutter.getGameServer() != null) { for (Player p : Grasscutter.getGameServer().getPlayers().values()) {
for (Player p : Grasscutter.getGameServer().getPlayers().values()) { CommandHandler.sendMessage(p, translate(p, "commands.stop.success"));
CommandHandler.sendMessage(p, translate(p, "commands.stop.success"));
}
} }
System.exit(1000); System.exit(1000);

View File

@ -35,9 +35,10 @@ public class ConfigContainer {
* HTTP server should start immediately. * HTTP server should start immediately.
* Version 13 - 'game.useUniquePacketKey' was added to control whether the * Version 13 - 'game.useUniquePacketKey' was added to control whether the
* encryption key used for packets is a constant or randomly generated. * 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() { private static int version() {
return 13; return 14;
} }
/** /**
@ -183,6 +184,9 @@ public class ConfigContainer {
/* Kcp internal work interval (milliseconds) */ /* Kcp internal work interval (milliseconds) */
public int kcpInterval = 20; 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 */ /* Controls whether packets should be logged in console or not */
public ServerDebugMode logPackets = ServerDebugMode.NONE; public ServerDebugMode logPackets = ServerDebugMode.NONE;
/* Show packet payload in console or no (in any case the payload is shown in encrypted view) */ /* 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( .forEach(
block -> { block -> {
block.load(sceneId, meta.context); block.load(sceneId, meta.context);
if (block.groups == null) {
Grasscutter.getLogger().error("block.groups null for block {}", block.id);
return;
}
block.groups.values().stream() block.groups.values().stream()
.filter(g -> !g.dynamic_load) .filter(g -> !g.dynamic_load)
.forEach( .forEach(

View File

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

View File

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