mirror of
https://github.com/Grasscutters/Grasscutter.git
synced 2025-07-12 17:53:53 +00:00
Compare commits
14 Commits
developmen
...
f9d46ace7f
Author | SHA1 | Date | |
---|---|---|---|
f9d46ace7f | |||
36346f87f9 | |||
ea84789c47 | |||
85719b9aeb | |||
e5b3d65916 | |||
93df2d0b0e | |||
e7ed66477f | |||
af70de316e | |||
f29189be8f | |||
4ced11d567 | |||
446e994ff0 | |||
655016c92e | |||
d0e3720748 | |||
db4542653a |
2
.github/workflows/build.yml
vendored
2
.github/workflows/build.yml
vendored
@ -25,7 +25,7 @@ jobs:
|
||||
uses: actions/setup-java@v3
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: '17'
|
||||
java-version: '21'
|
||||
- name: Cache gradle files
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
|
2
.github/workflows/check_code.yml
vendored
2
.github/workflows/check_code.yml
vendored
@ -26,7 +26,7 @@ jobs:
|
||||
uses: actions/setup-java@v3
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: '17'
|
||||
java-version: '21'
|
||||
- name: Cache gradle files
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
|
@ -18,12 +18,6 @@
|
||||
* Spawning monsters via console
|
||||
* 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
|
||||
|
||||
**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):
|
||||
[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)
|
||||
- ***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.
|
||||
- After opening Cultivation (as admin), press the download button in the upper right corner.
|
||||
|
30
build.gradle
30
build.gradle
@ -23,7 +23,7 @@ plugins {
|
||||
id 'java-library' // Apply the java-library plugin for API and implementation separation.
|
||||
id 'application' // Apply the application plugin to add support for building a CLI application
|
||||
id 'com.google.protobuf' version '0.8.18' // Apply the protobuf auto generator
|
||||
id 'com.diffplug.spotless' version '6.11.0' // Apply the Spotless linter plugin.
|
||||
id 'com.diffplug.spotless' version '6.25.0' // Apply the Spotless linter plugin.
|
||||
|
||||
id 'eclipse' // Eclipse Support
|
||||
id 'idea' // IntelliJ Support
|
||||
@ -31,7 +31,7 @@ plugins {
|
||||
id 'maven-publish' // Support for publishing to Maven repositories.
|
||||
id 'signing' // Support for signing build artifacts.
|
||||
|
||||
id 'io.freefair.lombok' version '6.6.1' // Lombok for delombok'ification
|
||||
id 'io.freefair.lombok' version '8.6' // Lombok for delombok'ification
|
||||
}
|
||||
|
||||
spotless {
|
||||
@ -43,7 +43,7 @@ spotless {
|
||||
}
|
||||
|
||||
importOrder('io.grasscutter', '', 'java', 'javax', '\\#java', '\\#') // Configure import order.
|
||||
googleJavaFormat('1.15.0') // Use Google's Java formatter.
|
||||
googleJavaFormat('1.17.0') // Use Google's Java formatter.
|
||||
formatAnnotations() // Reformat annotations.
|
||||
endWithNewline() // Ensure files end with a newline.
|
||||
indentWithTabs(2); indentWithSpaces(4) // Use 4 spaces for indentation.
|
||||
@ -54,7 +54,7 @@ spotless {
|
||||
compileJava.options.encoding = 'UTF-8'
|
||||
compileTestJava.options.encoding = 'UTF-8'
|
||||
|
||||
sourceCompatibility = JavaVersion.VERSION_17
|
||||
sourceCompatibility = JavaVersion.VERSION_21
|
||||
targetCompatibility = JavaVersion.VERSION_17
|
||||
|
||||
group = 'io.grasscutter'
|
||||
@ -77,19 +77,19 @@ dependencies {
|
||||
|
||||
// Logging libraries.
|
||||
implementation group: 'org.slf4j', name: 'slf4j-api', version: '2.0.7'
|
||||
implementation group: 'ch.qos.logback', name: 'logback-core', version: '1.4.7'
|
||||
implementation group: 'ch.qos.logback', name: 'logback-classic', version: '1.4.7'
|
||||
implementation group: 'ch.qos.logback', name: 'logback-core', version: '1.4.14'
|
||||
implementation group: 'ch.qos.logback', name: 'logback-classic', version: '1.4.12'
|
||||
|
||||
// Line reading libraries.
|
||||
implementation group: 'org.jline', name: 'jline', version: '3.21.0'
|
||||
implementation group: 'org.jline', name: 'jline', version: '3.25.0'
|
||||
implementation group: 'org.jline', name: 'jline-terminal-jna', version: '3.21.0'
|
||||
implementation group: 'net.java.dev.jna', name: 'jna', version: '5.10.0'
|
||||
|
||||
// Java Netty for networking.
|
||||
implementation group: 'io.netty', name: 'netty-common', version: '4.1.86.Final'
|
||||
implementation group: 'io.netty', name: 'netty-handler', version: '4.1.86.Final'
|
||||
implementation group: 'io.netty', name: 'netty-transport-native-epoll', version: '4.1.86.Final'
|
||||
implementation group: 'io.netty', name: 'netty-transport-native-kqueue', version: '4.1.86.Final'
|
||||
implementation group: 'io.netty', name: 'netty-common', version: project.netty_version
|
||||
implementation group: 'io.netty', name: 'netty-handler', version: project.netty_version
|
||||
implementation group: 'io.netty', name: 'netty-transport-native-epoll', version: project.netty_version
|
||||
implementation group: 'io.netty', name: 'netty-transport-native-kqueue', version: project.netty_version
|
||||
|
||||
// Serialization.
|
||||
implementation group: 'com.google.code.gson', name: 'gson', version: '2.9.0'
|
||||
@ -136,10 +136,10 @@ dependencies {
|
||||
testImplementation group: 'com.squareup.okhttp3', name: 'okhttp', version: '4.10.0'
|
||||
|
||||
// Lombok.
|
||||
compileOnly group: 'org.projectlombok', name: 'lombok', version: '1.18.26'
|
||||
annotationProcessor group: 'org.projectlombok', name: 'lombok', version: '1.18.26'
|
||||
testCompileOnly group: 'org.projectlombok', name: 'lombok', version: '1.18.26'
|
||||
testAnnotationProcessor group: 'org.projectlombok', name: 'lombok', version: '1.18.26'
|
||||
compileOnly group: 'org.projectlombok', name: 'lombok', version: project.lombok_version
|
||||
annotationProcessor group: 'org.projectlombok', name: 'lombok', version: project.lombok_version
|
||||
testCompileOnly group: 'org.projectlombok', name: 'lombok', version: project.lombok_version
|
||||
testAnnotationProcessor group: 'org.projectlombok', name: 'lombok', version: project.lombok_version
|
||||
}
|
||||
|
||||
configurations.configureEach {
|
||||
|
@ -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)
|
||||
|
||||
**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
|
||||
|
||||
|
@ -22,25 +22,52 @@
|
||||
|
||||
**각주 :** 도움이 필요할 경우 [Discord](https://discord.gg/T5vZU6UyeG)에 가입하세요.
|
||||
|
||||
### 빠른 설치 (자동)
|
||||
### 설치에 필요한 것들
|
||||
|
||||
- [Java 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)
|
||||
* Java SE - 17 ([링크](https://www.oracle.com/java/technologies/javase/jdk17-archive-downloads.html))
|
||||
|
||||
- [최신 Cultivation](https://github.com/Grasscutters/Cultivation/releases/latest) 다운로드하세요. `.msi` 설치파일을 사용하면 됩니다.
|
||||
- (관리자 권한으로) Cultivation을 실행한 후, 우측 상단에 위치한 다운로드 버튼을 클릭하세요.
|
||||
- `올인원 다운로드`를 클릭하세요.
|
||||
- 우측 상단에 위치한 톱니바퀴 버튼을 누르세요.
|
||||
- 게임 설치 경로를 게임이 위치한 경로로 설정하세요.
|
||||
- 사용자 지정 Java 경로 설정을 `C:\Program Files\Java\jdk-17\bin\java.exe`로 설정하세요.
|
||||
- 다른 모든 설정은 기본값으로 두세요.
|
||||
**각주 :** **실행**만을 원한다면, **jre**만 있어도 괜찮습니다.
|
||||
|
||||
- 게임 시작 버튼 옆에 위치한 작은 버튼을 누르세요.
|
||||
- 게임 시작 버튼을 누르세요.
|
||||
- 원하는 사용자 이름으로 로그인하세요. 비밀번호는 무엇이든 가능합니다.
|
||||
* [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)
|
||||
- [Git](https://git-scm.com/downloads)
|
||||
- [NodeJS](https://nodejs.org/en/download) (선택, 핸드북을 빌드하기 위해 필요함.)
|
||||
|
||||
##### 클론
|
||||
##### 윈도우 (온라인)
|
||||
|
||||
```shell
|
||||
git clone --recurse-submodules https://github.com/Grasscutters/Grasscutter.git
|
||||
git clone https://github.com/Grasscutters/Grasscutter.git
|
||||
cd Grasscutter
|
||||
.\gradlew.bat # 개발 환경 설정
|
||||
.\gradlew jar # 컴파일
|
||||
```
|
||||
|
||||
##### 컴파일
|
||||
|
||||
**각주**: 핸드북 생성은 일부 시스템에서 실패할 수도 있습니다. 핸드북 생성을 비활성화하려면, `gradlew jar`명령에 `-PskipHandbook=1`명령줄 스위치를 추가하세요.
|
||||
|
||||
|
||||
윈도우:
|
||||
##### 윈도우 (로컬)
|
||||
|
||||
```shell
|
||||
.\gradlew.bat # 환경 준비
|
||||
.\gradlew jar
|
||||
cd <로컬 주소>/Grasscutter
|
||||
.\gradlew.bat # 개발 환경 설정
|
||||
.\gradlew jar # 컴파일
|
||||
```
|
||||
|
||||
리눅스 (GNU):
|
||||
##### 리눅스
|
||||
|
||||
```bash
|
||||
git clone https://github.com/Grasscutters/Grasscutter.git
|
||||
cd Grasscutter
|
||||
chmod +x gradlew
|
||||
./gradlew jar
|
||||
```
|
||||
|
||||
##### 핸드북 컴파일 (수동동)
|
||||
|
||||
Gradle 사용:
|
||||
|
||||
```shell
|
||||
./gradlew generateHandbook
|
||||
```
|
||||
|
||||
NPM 사용:
|
||||
|
||||
```shell
|
||||
cd src/handbook
|
||||
npm install
|
||||
npm run build
|
||||
./gradlew jar # 컴파일
|
||||
```
|
||||
|
||||
프로젝트 폴더의 최상단에서 jar 파일을 찾을 수 있습니다.
|
||||
|
||||
### 문제 해결
|
||||
흔한 문제들의 해결방법과 도움을 요청하려면, [우리의 디스코드 서버](https://discord.gg/T5vZU6UyeG)에 참가하고 support 채널에 가보세요.
|
||||
### 명령어들은 [위키](https://github.com/Grasscutters/Grasscutter/wiki/Commands)에서 확인할 수 있습니다.
|
||||
|
||||
# 빠른 문제 해결
|
||||
|
||||
* 만약 컴파일링이 정상적으로 완료되지 않을 경우, 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 :)
|
||||
|
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
@ -1,5 +1,5 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.2-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-bin.zip
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
|
@ -29,13 +29,15 @@ import lombok.*;
|
||||
import org.jline.reader.*;
|
||||
import org.jline.terminal.*;
|
||||
import org.reflections.Reflections;
|
||||
import org.reflections.util.ConfigurationBuilder;
|
||||
import org.reflections.util.FilterBuilder;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
public final class Grasscutter {
|
||||
public static final File configFile = new File("./config.json");
|
||||
public static final Reflections reflector = new Reflections("emu.grasscutter");
|
||||
@Getter private static final Logger logger = (Logger) LoggerFactory.getLogger(Grasscutter.class);
|
||||
|
||||
public static final Reflections reflector;
|
||||
@Getter public static ConfigContainer config;
|
||||
|
||||
@Getter @Setter private static Language language;
|
||||
@ -75,6 +77,16 @@ public final class Grasscutter {
|
||||
var mongoLogger = (Logger) LoggerFactory.getLogger("org.mongodb.driver");
|
||||
mongoLogger.setLevel(Level.OFF);
|
||||
|
||||
// Configure the reflector.
|
||||
reflector =
|
||||
new Reflections(
|
||||
new ConfigurationBuilder()
|
||||
.forPackage("emu.grasscutter")
|
||||
.filterInputsBy(
|
||||
new FilterBuilder()
|
||||
.includePackage("emu.grasscutter")
|
||||
.excludePackage("emu.grasscutter.net.proto")));
|
||||
|
||||
// Load server configuration.
|
||||
Grasscutter.loadConfig();
|
||||
// Attempt to update configuration.
|
||||
|
@ -112,7 +112,13 @@ public final class DefaultAuthenticators {
|
||||
cipher.doFinal(Utils.base64Decode(request.getPasswordRequest().password)),
|
||||
StandardCharsets.UTF_8);
|
||||
} catch (Exception ignored) {
|
||||
decryptedPassword = request.getPasswordRequest().password;
|
||||
if (requestData.is_crypto) {
|
||||
response.retcode = -201;
|
||||
response.message = translate("messages.dispatch.account.password_crypto_error");
|
||||
return response;
|
||||
} else {
|
||||
decryptedPassword = request.getPasswordRequest().password;
|
||||
}
|
||||
}
|
||||
|
||||
if (decryptedPassword == null) {
|
||||
|
@ -17,10 +17,8 @@ public final class StopCommand implements CommandHandler {
|
||||
@Override
|
||||
public void execute(Player sender, Player targetPlayer, List<String> args) {
|
||||
CommandHandler.sendMessage(null, translate("commands.stop.success"));
|
||||
if (Grasscutter.getGameServer() != null) {
|
||||
for (Player p : Grasscutter.getGameServer().getPlayers().values()) {
|
||||
CommandHandler.sendMessage(p, translate(p, "commands.stop.success"));
|
||||
}
|
||||
for (Player p : Grasscutter.getGameServer().getPlayers().values()) {
|
||||
CommandHandler.sendMessage(p, translate(p, "commands.stop.success"));
|
||||
}
|
||||
|
||||
System.exit(1000);
|
||||
|
@ -35,9 +35,10 @@ public class ConfigContainer {
|
||||
* HTTP server should start immediately.
|
||||
* Version 13 - 'game.useUniquePacketKey' was added to control whether the
|
||||
* encryption key used for packets is a constant or randomly generated.
|
||||
* Version 14 - 'game.timeout' was added to control the UDP client timeout.
|
||||
*/
|
||||
private static int version() {
|
||||
return 13;
|
||||
return 14;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -183,6 +184,9 @@ public class ConfigContainer {
|
||||
|
||||
/* Kcp internal work interval (milliseconds) */
|
||||
public int kcpInterval = 20;
|
||||
/* Time to wait (in seconds) before terminating a connection. */
|
||||
public long timeout = 30;
|
||||
|
||||
/* Controls whether packets should be logged in console or not */
|
||||
public ServerDebugMode logPackets = ServerDebugMode.NONE;
|
||||
/* Show packet payload in console or no (in any case the payload is shown in encrypted view) */
|
||||
|
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(
|
||||
block -> {
|
||||
block.load(sceneId, meta.context);
|
||||
if (block.groups == null) {
|
||||
Grasscutter.getLogger().error("block.groups null for block {}", block.id);
|
||||
return;
|
||||
}
|
||||
|
||||
block.groups.values().stream()
|
||||
.filter(g -> !g.dynamic_load)
|
||||
.forEach(
|
||||
|
@ -32,6 +32,8 @@ import emu.grasscutter.game.talk.TalkSystem;
|
||||
import emu.grasscutter.game.tower.TowerSystem;
|
||||
import emu.grasscutter.game.world.World;
|
||||
import emu.grasscutter.game.world.WorldDataSystem;
|
||||
import emu.grasscutter.net.INetworkTransport;
|
||||
import emu.grasscutter.net.impl.NetworkTransportImpl;
|
||||
import emu.grasscutter.net.packet.PacketHandler;
|
||||
import emu.grasscutter.net.proto.SocialDetailOuterClass.SocialDetail;
|
||||
import emu.grasscutter.server.dispatch.DispatchClient;
|
||||
@ -47,14 +49,20 @@ import java.net.*;
|
||||
import java.time.*;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.*;
|
||||
import kcp.highway.*;
|
||||
import lombok.*;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.jetbrains.annotations.*;
|
||||
|
||||
@Getter
|
||||
public final class GameServer extends KcpServer implements Iterable<Player> {
|
||||
@Slf4j
|
||||
public final class GameServer implements Iterable<Player> {
|
||||
/** This can be set by plugins to change the network transport implementation. */
|
||||
@Setter private static Class<? extends INetworkTransport> transport = NetworkTransportImpl.class;
|
||||
|
||||
// Game server base
|
||||
private final InetSocketAddress address;
|
||||
private final INetworkTransport netTransport;
|
||||
|
||||
private final GameServerPacketHandler packetHandler;
|
||||
private final Map<Integer, Player> players;
|
||||
private final Set<World> worlds;
|
||||
@ -106,6 +114,7 @@ public final class GameServer extends KcpServer implements Iterable<Player> {
|
||||
this.taskMap = null;
|
||||
|
||||
this.address = null;
|
||||
this.netTransport = null;
|
||||
this.packetHandler = null;
|
||||
this.dispatchClient = null;
|
||||
this.players = null;
|
||||
@ -131,16 +140,18 @@ public final class GameServer extends KcpServer implements Iterable<Player> {
|
||||
return;
|
||||
}
|
||||
|
||||
var channelConfig = new ChannelConfig();
|
||||
channelConfig.nodelay(true, GAME_INFO.kcpInterval, 2, true);
|
||||
channelConfig.setMtu(1400);
|
||||
channelConfig.setSndwnd(256);
|
||||
channelConfig.setRcvwnd(256);
|
||||
channelConfig.setTimeoutMillis(30 * 1000); // 30s
|
||||
channelConfig.setUseConvChannel(true);
|
||||
channelConfig.setAckNoDelay(false);
|
||||
// Create the network transport.
|
||||
INetworkTransport transport;
|
||||
try {
|
||||
transport = GameServer.transport.getDeclaredConstructor().newInstance();
|
||||
} catch (Exception ex) {
|
||||
log.error("Failed to create network transport.", ex);
|
||||
transport = new NetworkTransportImpl();
|
||||
}
|
||||
|
||||
this.init(GameSessionManager.getListener(), channelConfig, address);
|
||||
// Initialize the transport.
|
||||
this.netTransport = transport;
|
||||
this.netTransport.start(this.address = address);
|
||||
|
||||
EnergyManager.initialize();
|
||||
StaminaManager.initialize();
|
||||
@ -149,7 +160,6 @@ public final class GameServer extends KcpServer implements Iterable<Player> {
|
||||
CombineManger.initialize();
|
||||
|
||||
// Game Server base
|
||||
this.address = address;
|
||||
this.packetHandler = new GameServerPacketHandler(PacketHandler.class);
|
||||
this.dispatchClient = new DispatchClient(GameServer.getDispatchUrl());
|
||||
this.players = new ConcurrentHashMap<>();
|
||||
@ -184,7 +194,7 @@ public final class GameServer extends KcpServer implements Iterable<Player> {
|
||||
|
||||
private static InetSocketAddress getAdapterInetSocketAddress() {
|
||||
InetSocketAddress inetSocketAddress;
|
||||
if (GAME_INFO.bindAddress.equals("")) {
|
||||
if (GAME_INFO.bindAddress.isEmpty()) {
|
||||
inetSocketAddress = new InetSocketAddress(GAME_INFO.bindPort);
|
||||
} else {
|
||||
inetSocketAddress = new InetSocketAddress(GAME_INFO.bindAddress, GAME_INFO.bindPort);
|
||||
@ -353,19 +363,6 @@ public final class GameServer extends KcpServer implements Iterable<Player> {
|
||||
this.getWorlds().forEach(World::save);
|
||||
|
||||
Utils.sleep(1000L); // Wait 1 second for operations to finish.
|
||||
this.stop(); // Stop the server.
|
||||
|
||||
try {
|
||||
var threadPool = GameSessionManager.getLogicThread();
|
||||
|
||||
// Shutdown network thread.
|
||||
threadPool.shutdownGracefully();
|
||||
// Wait for the network thread to finish.
|
||||
if (!threadPool.awaitTermination(5, TimeUnit.SECONDS)) {
|
||||
Grasscutter.getLogger().error("Logic thread did not terminate!");
|
||||
}
|
||||
} catch (InterruptedException ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
@NotNull @Override
|
||||
|
@ -7,18 +7,18 @@ import emu.grasscutter.Grasscutter;
|
||||
import emu.grasscutter.Grasscutter.ServerDebugMode;
|
||||
import emu.grasscutter.game.Account;
|
||||
import emu.grasscutter.game.player.Player;
|
||||
import emu.grasscutter.net.IKcpSession;
|
||||
import emu.grasscutter.net.packet.*;
|
||||
import emu.grasscutter.server.event.game.SendPacketEvent;
|
||||
import emu.grasscutter.utils.*;
|
||||
import io.netty.buffer.*;
|
||||
import java.io.File;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.nio.file.Path;
|
||||
import lombok.*;
|
||||
import org.slf4j.Logger;
|
||||
|
||||
public class GameSession implements GameSessionManager.KcpChannel {
|
||||
private final GameServer server;
|
||||
private GameSessionManager.KcpTunnel tunnel;
|
||||
public class GameSession implements IGameSession {
|
||||
@Getter private final GameServer server;
|
||||
private IKcpSession session;
|
||||
|
||||
@Getter @Setter private Account account;
|
||||
@Getter private Player player;
|
||||
@ -33,8 +33,10 @@ public class GameSession implements GameSessionManager.KcpChannel {
|
||||
@Getter private long lastPingTime;
|
||||
private int lastClientSeq = 10;
|
||||
|
||||
public GameSession(GameServer server) {
|
||||
public GameSession(GameServer server, IKcpSession session) {
|
||||
this.server = server;
|
||||
this.session = session;
|
||||
|
||||
this.state = SessionState.WAITING_FOR_TOKEN;
|
||||
this.lastPingTime = System.currentTimeMillis();
|
||||
|
||||
@ -44,24 +46,12 @@ public class GameSession implements GameSessionManager.KcpChannel {
|
||||
}
|
||||
}
|
||||
|
||||
public GameServer getServer() {
|
||||
return server;
|
||||
}
|
||||
|
||||
public InetSocketAddress getAddress() {
|
||||
try {
|
||||
return tunnel.getAddress();
|
||||
} catch (Throwable ignore) {
|
||||
return null;
|
||||
}
|
||||
return this.session.getAddress();
|
||||
}
|
||||
|
||||
public boolean useSecretKey() {
|
||||
return useSecretKey;
|
||||
}
|
||||
|
||||
public String getAccountId() {
|
||||
return this.getAccount().getId();
|
||||
public Logger getLogger() {
|
||||
return this.session.getLogger();
|
||||
}
|
||||
|
||||
public synchronized void setPlayer(Player player) {
|
||||
@ -83,30 +73,17 @@ public class GameSession implements GameSessionManager.KcpChannel {
|
||||
return ++lastClientSeq;
|
||||
}
|
||||
|
||||
public void replayPacket(int opcode, String name) {
|
||||
Path filePath = FileUtils.getPluginPath(name);
|
||||
File p = filePath.toFile();
|
||||
|
||||
if (!p.exists()) return;
|
||||
|
||||
byte[] packet = FileUtils.read(p);
|
||||
|
||||
BasePacket basePacket = new BasePacket(opcode);
|
||||
basePacket.setData(packet);
|
||||
|
||||
send(basePacket);
|
||||
}
|
||||
|
||||
public void logPacket(String sendOrRecv, int opcode, byte[] payload) {
|
||||
Grasscutter.getLogger()
|
||||
.info(sendOrRecv + ": " + PacketOpcodesUtils.getOpcodeName(opcode) + " (" + opcode + ")");
|
||||
this.session
|
||||
.getLogger()
|
||||
.info("{}: {} ({})", sendOrRecv, PacketOpcodesUtils.getOpcodeName(opcode), opcode);
|
||||
if (GAME_INFO.isShowPacketPayload) System.out.println(Utils.bytesToHex(payload));
|
||||
}
|
||||
|
||||
public void send(BasePacket packet) {
|
||||
// Test
|
||||
if (packet.getOpcode() <= 0) {
|
||||
Grasscutter.getLogger().warn("Tried to send packet with missing cmd id!");
|
||||
this.session.getLogger().warn("Attempted to send packet with unknown ID!");
|
||||
return;
|
||||
}
|
||||
|
||||
@ -146,28 +123,24 @@ public class GameSession implements GameSessionManager.KcpChannel {
|
||||
if (packet.shouldEncrypt) {
|
||||
Crypto.xor(bytes, packet.useDispatchKey() ? Crypto.DISPATCH_KEY : this.encryptKey);
|
||||
}
|
||||
tunnel.writeData(bytes);
|
||||
} catch (Exception ignored) {
|
||||
Grasscutter.getLogger().debug("Unable to send packet to client.");
|
||||
this.session.send(bytes);
|
||||
} catch (Exception ex) {
|
||||
this.session.getLogger().debug("Unable to send packet to client.", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onConnected(GameSessionManager.KcpTunnel tunnel) {
|
||||
this.tunnel = tunnel;
|
||||
public void onConnected() {
|
||||
Grasscutter.getLogger().info(translate("messages.game.connect", this.getAddress().toString()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleReceive(byte[] bytes) {
|
||||
public void onReceived(byte[] bytes) {
|
||||
// Decrypt and turn back into a packet
|
||||
Crypto.xor(bytes, useSecretKey() ? this.encryptKey : Crypto.DISPATCH_KEY);
|
||||
Crypto.xor(bytes, this.useSecretKey ? this.encryptKey : Crypto.DISPATCH_KEY);
|
||||
ByteBuf packet = Unpooled.wrappedBuffer(bytes);
|
||||
|
||||
// Log
|
||||
// logPacket(packet);
|
||||
// Handle
|
||||
try {
|
||||
boolean allDebug = GAME_INFO.logPackets == ServerDebugMode.ALL;
|
||||
while (packet.readableBytes() > 0) {
|
||||
@ -179,11 +152,13 @@ public class GameSession implements GameSessionManager.KcpChannel {
|
||||
int const1 = packet.readShort();
|
||||
if (const1 != 17767) {
|
||||
if (allDebug) {
|
||||
Grasscutter.getLogger()
|
||||
.error("Bad Data Package Received: got {} ,expect 17767", const1);
|
||||
this.session
|
||||
.getLogger()
|
||||
.error("Invalid packet header received: got {}, expected 17767", const1);
|
||||
}
|
||||
return; // Bad packet
|
||||
}
|
||||
|
||||
// Data
|
||||
int opcode = packet.readShort();
|
||||
int headerLength = packet.readShort();
|
||||
@ -197,8 +172,9 @@ public class GameSession implements GameSessionManager.KcpChannel {
|
||||
int const2 = packet.readShort();
|
||||
if (const2 != -30293) {
|
||||
if (allDebug) {
|
||||
Grasscutter.getLogger()
|
||||
.error("Bad Data Package Received: got {} ,expect -30293", const2);
|
||||
this.session
|
||||
.getLogger()
|
||||
.error("Invalid packet footer received: got {}, expected -30293", const2);
|
||||
}
|
||||
return; // Bad packet
|
||||
}
|
||||
@ -226,16 +202,15 @@ public class GameSession implements GameSessionManager.KcpChannel {
|
||||
// Handle
|
||||
getServer().getPacketHandler().handle(this, opcode, header, payload);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
} catch (Exception ex) {
|
||||
this.session.getLogger().warn("Unable to process packet.", ex);
|
||||
} finally {
|
||||
// byteBuf.release(); //Needn't
|
||||
packet.release();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleClose() {
|
||||
public void onDisconnected() {
|
||||
setState(SessionState.INACTIVE);
|
||||
// send disconnection pack in case of reconnection
|
||||
Grasscutter.getLogger()
|
||||
@ -247,19 +222,20 @@ public class GameSession implements GameSessionManager.KcpChannel {
|
||||
player.onLogout();
|
||||
}
|
||||
try {
|
||||
send(new BasePacket(PacketOpcodes.ServerDisconnectClientNotify));
|
||||
} catch (Throwable ignore) {
|
||||
Grasscutter.getLogger().warn("closing {} error", getAddress().getAddress().getHostAddress());
|
||||
this.send(new BasePacket(PacketOpcodes.ServerDisconnectClientNotify));
|
||||
} catch (Throwable ex) {
|
||||
this.session.getLogger().warn("Failed to disconnect client.", ex);
|
||||
}
|
||||
tunnel = null;
|
||||
|
||||
this.session = null;
|
||||
}
|
||||
|
||||
public void close() {
|
||||
tunnel.close();
|
||||
this.session.close();
|
||||
}
|
||||
|
||||
public boolean isActive() {
|
||||
return getState() == SessionState.ACTIVE;
|
||||
return this.getState() == SessionState.ACTIVE;
|
||||
}
|
||||
|
||||
public enum SessionState {
|
||||
|
@ -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.
|
||||
*/
|
||||
public HandbookHandler() {
|
||||
if (!HANDBOOK.enable) {
|
||||
this.serve = false;
|
||||
return;
|
||||
}
|
||||
|
||||
this.handbook = new String(FileUtils.readResource("/html/handbook.html"));
|
||||
this.serve = HANDBOOK.enable && this.handbook.length() > 0;
|
||||
this.serve = !this.handbook.isEmpty();
|
||||
|
||||
var server = HANDBOOK.server;
|
||||
if (this.serve && server.enforced) {
|
||||
|
@ -44,7 +44,8 @@
|
||||
"password_error": "Invalid Password",
|
||||
"password_length_error": "Password length must be greater then or equal to 8",
|
||||
"password_storage_error": "You don't have a password for your account. Please contact an administrator.",
|
||||
"server_max_player_limit": "The number of online players has reached the limit"
|
||||
"server_max_player_limit": "The number of online players has reached the limit",
|
||||
"password_crypto_error": "Unable to decrypt the client's given password. Are you using the right version of the game?"
|
||||
},
|
||||
"router_error": "[Dispatch] Unable to attach router."
|
||||
},
|
||||
|
@ -44,7 +44,8 @@
|
||||
"password_error": "Contraseña no válida",
|
||||
"password_length_error": "La longitud de la contraseña debe ser mayor o igual a 8",
|
||||
"password_storage_error": "No tienes contraseña para tu cuenta. Por favor contacta a un administrador.",
|
||||
"server_max_player_limit": "Se ha alcanzado el límite de jugadores activos"
|
||||
"server_max_player_limit": "Se ha alcanzado el límite de jugadores activos",
|
||||
"password_crypto_error": "🇺🇸Unable to decrypt the client's given password. Are you using the right version of the game?"
|
||||
},
|
||||
"router_error": "[Dispatch] No se ha podido vincular el router."
|
||||
},
|
||||
|
@ -44,7 +44,8 @@
|
||||
"password_error": "Mot de passe invalide",
|
||||
"password_length_error": "La longueur du mot de passe doit être supérieure a 8",
|
||||
"password_storage_error": "Vous n'avez pas de mot de passe pour votre compte. Veuillez contacter un administrateur.",
|
||||
"server_max_player_limit": "Le nombre de joueurs maximum est atteint."
|
||||
"server_max_player_limit": "Le nombre de joueurs maximum est atteint.",
|
||||
"password_crypto_error": "🇺🇸Unable to decrypt the client's given password. Are you using the right version of the game?"
|
||||
},
|
||||
"router_error": "[Dispatch] Impossible d'attacher le routeur."
|
||||
},
|
||||
|
@ -44,7 +44,8 @@
|
||||
"password_error": "Password non valida",
|
||||
"password_length_error": "La lunghezza della password deve essere maggiore o uguale a 8",
|
||||
"password_storage_error": "Non hai una password per il tuo account. Contatta un amministratore.",
|
||||
"server_max_player_limit": "Il numero di giocatori online ha raggiunto il limite"
|
||||
"server_max_player_limit": "Il numero di giocatori online ha raggiunto il limite",
|
||||
"password_crypto_error": "🇺🇸Unable to decrypt the client's given password. Are you using the right version of the game?"
|
||||
},
|
||||
"router_error": "[Dispatch] Impossibile collegare il router."
|
||||
},
|
||||
|
@ -44,7 +44,8 @@
|
||||
"password_error": "無効なパスワード",
|
||||
"password_length_error": "パスワードの長さは8文字以上でなければなりません",
|
||||
"password_storage_error": "アカウントのパスワードがありません。 管理者に連絡してください。",
|
||||
"server_max_player_limit": "オンライン プレイヤーの数が上限に達しました"
|
||||
"server_max_player_limit": "オンライン プレイヤーの数が上限に達しました",
|
||||
"password_crypto_error": "🇺🇸Unable to decrypt the client's given password. Are you using the right version of the game?"
|
||||
},
|
||||
"router_error": "[Dispatch] ルーターを接続できません。"
|
||||
},
|
||||
|
@ -44,7 +44,8 @@
|
||||
"password_error": "암호가 올바르지 않습니다",
|
||||
"password_length_error": "암호의 길이는 8자리보다 크거나 같아야합니다.",
|
||||
"password_storage_error": "계정에 암호가 없습니다. 관리자에게 문의하십시오.",
|
||||
"server_max_player_limit": "온라인 플레이어 수가 최대에 도달했습니다."
|
||||
"server_max_player_limit": "온라인 플레이어 수가 최대에 도달했습니다.",
|
||||
"password_crypto_error": "🇺🇸Unable to decrypt the client's given password. Are you using the right version of the game?"
|
||||
},
|
||||
"router_error": "[Dispatch] 라우터에 연결할 수 없습니다."
|
||||
},
|
||||
|
@ -44,7 +44,8 @@
|
||||
"password_error": "Nieprawidłowe hasło",
|
||||
"password_length_error": "Długość hasła musi być większa niż równa 8 znaków",
|
||||
"password_storage_error": "Nie posiadasz hasła do tego konta. Proszę skontaktować się z Administratorem.",
|
||||
"server_max_player_limit": "Liczba graczy online osiągnęła swój limit."
|
||||
"server_max_player_limit": "Liczba graczy online osiągnęła swój limit.",
|
||||
"password_crypto_error": "🇺🇸Unable to decrypt the client's given password. Are you using the right version of the game?"
|
||||
},
|
||||
"router_error": "[Dispatch] Wystąpił błąd podczas tworzenia routera."
|
||||
},
|
||||
|
@ -44,7 +44,8 @@
|
||||
"password_error": "🇺🇸Invalid Password",
|
||||
"password_length_error": "🇺🇸Password length must be greater then or equal to 8",
|
||||
"password_storage_error": "🇺🇸You don't have a password for your account. Please contact an administrator.",
|
||||
"server_max_player_limit": "Numărul de jucători online a ajuns la limită."
|
||||
"server_max_player_limit": "Numărul de jucători online a ajuns la limită.",
|
||||
"password_crypto_error": "🇺🇸Unable to decrypt the client's given password. Are you using the right version of the game?"
|
||||
},
|
||||
"router_error": "[Dispatch] Nu se poate atașa routerul."
|
||||
},
|
||||
|
@ -44,7 +44,8 @@
|
||||
"password_error": "Некорректный пароль",
|
||||
"password_length_error": "Длина пароля должна быть не менее 8 символов",
|
||||
"password_storage_error": "У вашего аккаунта отсутствует пароль. Свяжитесь с администратором.",
|
||||
"server_max_player_limit": "Число игроков в сети достигло предела"
|
||||
"server_max_player_limit": "Число игроков в сети достигло предела",
|
||||
"password_crypto_error": "🇺🇸Unable to decrypt the client's given password. Are you using the right version of the game?"
|
||||
},
|
||||
"router_error": "[Dispatch] Не удалось присоединить маршрутизатор."
|
||||
},
|
||||
|
@ -44,7 +44,8 @@
|
||||
"password_error": "登录失败,请确认账号/密码是否正确",
|
||||
"password_length_error": "密码必须大于或等于 8 位",
|
||||
"password_storage_error": "你没有密码,请联系管理员",
|
||||
"server_max_player_limit": "服务器在线人数已满"
|
||||
"server_max_player_limit": "服务器在线人数已满",
|
||||
"password_crypto_error": "🇺🇸Unable to decrypt the client's given password. Are you using the right version of the game?"
|
||||
},
|
||||
"router_error": "[Dispatch] 无法连接路由"
|
||||
},
|
||||
|
@ -44,7 +44,8 @@
|
||||
"password_error": "密碼無效",
|
||||
"password_length_error": "密碼長度必須大於或等於 8",
|
||||
"password_storage_error": "您的帳號沒有設定密碼,請聯繫管理員。",
|
||||
"server_max_player_limit": "伺服器線上人數已滿"
|
||||
"server_max_player_limit": "伺服器線上人數已滿",
|
||||
"password_crypto_error": "🇺🇸Unable to decrypt the client's given password. Are you using the right version of the game?"
|
||||
},
|
||||
"router_error": "[Dispatch] 無法附加路由。"
|
||||
},
|
||||
|
Reference in New Issue
Block a user