mirror of
https://github.com/Grasscutters/Grasscutter.git
synced 2025-07-13 10:13:51 +00:00
Compare commits
9 Commits
developmen
...
93df2d0b0e
Author | SHA1 | Date | |
---|---|---|---|
93df2d0b0e | |||
e7ed66477f | |||
af70de316e | |||
f29189be8f | |||
4ced11d567 | |||
446e994ff0 | |||
655016c92e | |||
d0e3720748 | |||
db4542653a |
@ -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.
|
||||||
|
24
build.gradle
24
build.gradle
@ -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 {
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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 등) > 게임
|
||||||
|
@ -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 :)
|
||||||
|
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
@ -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
|
||||||
|
@ -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.
|
||||||
|
@ -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) {
|
||||||
|
@ -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);
|
||||||
|
@ -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) */
|
||||||
|
27
src/main/java/emu/grasscutter/net/IKcpSession.java
Normal file
27
src/main/java/emu/grasscutter/net/IKcpSession.java
Normal 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);
|
||||||
|
}
|
38
src/main/java/emu/grasscutter/net/INetworkTransport.java
Normal file
38
src/main/java/emu/grasscutter/net/INetworkTransport.java
Normal 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();
|
||||||
|
}
|
46
src/main/java/emu/grasscutter/net/impl/KcpSessionImpl.java
Normal file
46
src/main/java/emu/grasscutter/net/impl/KcpSessionImpl.java
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
122
src/main/java/emu/grasscutter/net/impl/NetworkTransportImpl.java
Normal file
122
src/main/java/emu/grasscutter/net/impl/NetworkTransportImpl.java
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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(
|
||||||
|
@ -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
|
||||||
|
@ -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 {
|
||||||
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
20
src/main/java/emu/grasscutter/server/game/IGameSession.java
Normal file
20
src/main/java/emu/grasscutter/server/game/IGameSession.java
Normal 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);
|
||||||
|
}
|
@ -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) {
|
||||||
|
Reference in New Issue
Block a user