mirror of
https://github.com/Grasscutters/Grasscutter.git
synced 2025-07-12 17:53:53 +00:00
Compare commits
23 Commits
v1.7.4
...
f9d46ace7f
Author | SHA1 | Date | |
---|---|---|---|
f9d46ace7f | |||
36346f87f9 | |||
ea84789c47 | |||
85719b9aeb | |||
e5b3d65916 | |||
93df2d0b0e | |||
e7ed66477f | |||
af70de316e | |||
f29189be8f | |||
4ced11d567 | |||
446e994ff0 | |||
655016c92e | |||
d0e3720748 | |||
db4542653a | |||
76fd5b2e9c | |||
4022267888 | |||
f1f5b54939 | |||
f871f261e1 | |||
eeaccf32c4 | |||
6e1913aacb | |||
9e17e4aacb | |||
770a793c69 | |||
c4402cc287 |
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:
|
||||
|
51
.github/workflows/build_container.yml
vendored
Normal file
51
.github/workflows/build_container.yml
vendored
Normal file
@ -0,0 +1,51 @@
|
||||
name: Build Docker Container
|
||||
on:
|
||||
push:
|
||||
release:
|
||||
types: [published]
|
||||
workflow_dispatch: ~
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
steps:
|
||||
- name: Checkout Project
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Generate Docker Meta
|
||||
uses: docker/metadata-action@v5
|
||||
id: meta
|
||||
with:
|
||||
images: ghcr.io/${{ github.repository }}
|
||||
tags: |
|
||||
type=ref,event=branch
|
||||
type=semver,pattern={{version}}
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
type=semver,pattern={{major}}
|
||||
type=sha
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Setup Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3.1.0
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v3.0.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build and Push Docker image
|
||||
uses: docker/build-push-action@v5.2.0
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
platforms: linux/amd64
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
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:
|
||||
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -64,6 +64,7 @@ tmp/
|
||||
|
||||
/*.jar
|
||||
/*.sh
|
||||
!entrypoint.sh
|
||||
|
||||
GM Handbook*.txt
|
||||
handbook.html
|
||||
|
38
Dockerfile
Normal file
38
Dockerfile
Normal file
@ -0,0 +1,38 @@
|
||||
# Builder
|
||||
FROM gradle:jdk17-alpine as builder
|
||||
|
||||
RUN apk add --update nodejs npm
|
||||
|
||||
WORKDIR /app
|
||||
COPY ./ /app/
|
||||
|
||||
RUN gradle jar --no-daemon
|
||||
|
||||
# Fetch Data
|
||||
FROM bitnami/git:2.43.0-debian-11-r1 as data
|
||||
|
||||
ARG DATA_REPOSITORY=https://gitlab.com/YuukiPS/GC-Resources.git
|
||||
ARG DATA_BRANCH=4.0
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
RUN git clone --branch ${DATA_BRANCH} --depth 1 ${DATA_REPOSITORY}
|
||||
|
||||
# Result Container
|
||||
FROM amazoncorretto:17-alpine
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Copy built assets
|
||||
COPY --from=builder /app/grasscutter-*.jar /app/grasscutter.jar
|
||||
COPY --from=builder /app/keystore.p12 /app/keystore.p12
|
||||
|
||||
# Copy the resources
|
||||
COPY --from=data /app/GC-Resources/Resources /app/resources/
|
||||
|
||||
# Copy startup files
|
||||
COPY ./entrypoint.sh /app/
|
||||
|
||||
CMD [ "sh", "/app/entrypoint.sh" ]
|
||||
|
||||
EXPOSE 80 443 8888 22102
|
@ -24,9 +24,11 @@
|
||||
|
||||
### Quick Start (automatic)
|
||||
|
||||
- Get Java 17: https://www.oracle.com/java/technologies/javase/jdk17-archive-downloads.html
|
||||
- Get [Java 17](https://www.oracle.com/java/technologies/javase/jdk17-archive-downloads.html)
|
||||
- Get [MongoDB Community Server](https://www.mongodb.com/try/download/community)
|
||||
- Get game version REL4.0.x (4.0.x client can be found here if you don't have it): https://github.com/MAnggiarMustofa/GI-Download-Library/blob/main/GenshinImpact/Client/4.0.0.md
|
||||
- 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)
|
||||
|
||||
- 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.
|
||||
@ -38,7 +40,7 @@
|
||||
|
||||
- Click the small button next to launch.
|
||||
- Click the launch button.
|
||||
- Log in with whatever username you want. Password doesn't matter.
|
||||
- Log in with whatever username you want. Password can be anything.
|
||||
|
||||
### Building
|
||||
|
||||
|
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 {
|
||||
|
@ -3,7 +3,7 @@
|
||||
|
||||
<div align="center"><a href="https://discord.gg/T5vZU6UyeG"><img alt="Discord - Grasscutter" src="https://img.shields.io/discord/965284035985305680?label=Discord&logo=discord&style=for-the-badge"></a></div>
|
||||
|
||||
[EN](README.md) | [简中](docs/README_zh-CN.md) | [繁中](docs/README_zh-TW.md) | [FR](docs/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)
|
||||
|
||||
**ध्यान:** हम हमेशा परियोजना में योगदानकर्ताओं का स्वागत करते हैं।. अपना योगदान जोड़ने से पहले कृपया हमारा ध्यानपूर्वक पढ़ें [आचार संहिता](https://github.com/Grasscutters/Grasscutter/blob/stable/CONTRIBUTING.md).
|
||||
|
||||
|
@ -3,7 +3,7 @@
|
||||
|
||||
<div align="center"><a href="https://discord.gg/T5vZU6UyeG"><img alt="Discord - Grasscutter" src="https://img.shields.io/discord/965284035985305680?label=Discord&logo=discord&style=for-the-badge"></a></div>
|
||||
|
||||
[EN](README.md) | [简中](docs/README_zh-CN.md) | [繁中](docs/README_zh-TW.md) | [FR](docs/README_fr-FR.md) | [ES](docs/README_es-ES.md) | [HE](docs/README_HE.md) | [RU](docs/README_ru-RU.md) | [PL](docs/README_pl-PL.md) | [ID](docs/README_id-ID.md) | [KR](docs/README_ko-KR.md) | [FIL/PH](docs/README_fil-PH.md) | [NL](docs/README_NL.md) | [JP](docs/README_ja-JP.md) | [IT](docs/README_it-IT.md) | [VI](docs/README_vi-VN.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) | [HI](README_hn-IN.md)
|
||||
|
||||
|
||||
**Attention:** 私たちはプロジェクトへのコントリビュータをいつでも歓迎します。コントリビュートする前に、私たちの [行動規範](https://github.com/Grasscutters/Grasscutter/blob/stable/CONTRIBUTING.md)をよくお読みください。
|
||||
@ -27,7 +27,7 @@
|
||||
|
||||
- [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のクライアントを持っていない場合は右のリンクからダウンロード): https://github.com/MAnggiarMustofa/GI-Download-Library/blob/main/GenshinImpact/Client/4.0.0.md
|
||||
- ゲームバージョンがREL4.0.Xのクライアントを用意する (4.0.Xのクライアントを持っていない場合は右のリンクからダウンロード): [Github](https://github.com/JRSKelvin/GenshinRepository/blob/main/Version%204.0.0.md), [クラウド(123云盘)](https://www.123pan.com/s/HoqUVv-U7SBA.html)
|
||||
- [最新の Cultivation](https://github.com/Grasscutters/Cultivation/releases/latest)をダウンロードする。`.msi`インストーラを使ってください。
|
||||
- 管理者権限を付与して Cultivation を実行した後、右上端にあるダウンロードアイコンのボタンを押す。
|
||||
- `Download All-in-One` をクリックする
|
||||
@ -35,10 +35,9 @@
|
||||
- `Game Install Path` にゲームファイルのパスを指定する。
|
||||
- `Custom Java Path` に、自分が用意したJavaのパスを指定する。 (例: `C:\Program Files\Java\jdk-17\bin\java.exe`)
|
||||
- その他の設定には手を付けず次の段階に進む。
|
||||
|
||||
- Launch の隣にある小さいボタンを押す。
|
||||
- Launchボタンを押す
|
||||
- 好きなユーザ名でログインする。パスワードは特段気にすることはない。
|
||||
- 好きなユーザ名でログインする。ログインに関する設定がデフォルトの場合、パスワードは何を入れてもいい。
|
||||
|
||||
|
||||
### ビルド
|
||||
@ -79,7 +78,22 @@ chmod +x gradlew
|
||||
./gradlew jar # コンパイル
|
||||
```
|
||||
|
||||
生成されたjarファイルはプロジェクトフォルダのルートにあります。
|
||||
##### 手動によるハンドブックの生成
|
||||
|
||||
Gradleを使用する場合:
|
||||
```shell
|
||||
./gradlew generateHandbook
|
||||
```
|
||||
|
||||
NPMを使用する場合:
|
||||
```shell
|
||||
cd src/handbook
|
||||
npm install
|
||||
npm run build
|
||||
```
|
||||
|
||||
|
||||
生成されたjarファイルはプロジェクトのルートフォルダにあります。
|
||||
|
||||
### トラブルシューティング
|
||||
|
||||
|
@ -26,7 +26,9 @@
|
||||
|
||||
- 获取Java 17:https://www.oracle.com/java/technologies/javase/jdk17-archive-downloads.html
|
||||
- 获取[MongoDB社区版](https://www.mongodb.com/try/download/community)
|
||||
- 获取游戏4.0正式版 (如果你没有4.0的客户端,可以在这里找到):https://github.com/MAnggiarMustofa/GI-Download-Library/blob/main/GenshinImpact/Client/4.0.0.md)
|
||||
- 获取游戏4.0正式版 (如果你没有4.0的客户端,可以在这里找到):
|
||||
[123pan share](https://www.123pan.com/s/HoqUVv-U7SBA.html)
|
||||
[github](https://github.com/JRSKelvin/GenshinRepository/blob/main/Version%204.0.0.md)
|
||||
|
||||
- 下载[最新的Cultivation版本](https://github.com/Grasscutters/Cultivation/releases/latest)(使用以“.msi”为后缀的安装包)。
|
||||
- 以管理员身份打开Cultivation,按右上角的下载按钮。
|
||||
|
3
entrypoint.sh
Normal file
3
entrypoint.sh
Normal file
@ -0,0 +1,3 @@
|
||||
#/bin/sh
|
||||
|
||||
java -jar /app/grasscutter.jar
|
@ -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) {
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -140,6 +141,7 @@ public class ConfigContainer {
|
||||
public boolean autoCreate = false;
|
||||
public boolean EXPERIMENTAL_RealPassword = false;
|
||||
public String[] defaultPermissions = {};
|
||||
public String playerEmail = "grasscutter.io";
|
||||
public int maxPlayer = -1;
|
||||
}
|
||||
|
||||
@ -182,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) */
|
||||
|
@ -109,7 +109,7 @@ public class Account {
|
||||
return email;
|
||||
} else {
|
||||
// As of game version 3.5+, only the email is displayed to a user.
|
||||
return this.getUsername() + "@grasscutter.io";
|
||||
return this.getUsername() + "@" + ACCOUNT.playerEmail;
|
||||
}
|
||||
}
|
||||
|
||||
@ -235,7 +235,7 @@ public class Account {
|
||||
this.addPermission("*");
|
||||
}
|
||||
|
||||
// Set account default language as server default language
|
||||
// Set account default language to server default language
|
||||
if (!document.containsKey("locale")) {
|
||||
this.locale = LANGUAGE;
|
||||
}
|
||||
|
@ -29,7 +29,7 @@ public class WorldChallenge {
|
||||
private final AtomicInteger score;
|
||||
private boolean progress;
|
||||
private boolean success;
|
||||
private long startedAt;
|
||||
private int startedAt;
|
||||
private int finishedTime;
|
||||
|
||||
/**
|
||||
|
@ -36,6 +36,6 @@ public class KillMonsterCountInTimeIncChallengeFactoryHandler implements Challen
|
||||
List.of(
|
||||
new KillMonsterCountTrigger(),
|
||||
new InTimeTrigger(),
|
||||
new KillMonsterTimeIncTrigger(timeInc)));
|
||||
new KillMonsterTimeIncTrigger(timeLimit, timeInc)));
|
||||
}
|
||||
}
|
||||
|
@ -1,11 +1,12 @@
|
||||
package emu.grasscutter.game.dungeons.challenge.factory;
|
||||
|
||||
import emu.grasscutter.data.GameData;
|
||||
import emu.grasscutter.game.dungeons.challenge.WorldChallenge;
|
||||
import emu.grasscutter.game.dungeons.challenge.enums.ChallengeType;
|
||||
import emu.grasscutter.game.dungeons.challenge.trigger.*;
|
||||
import emu.grasscutter.game.world.Scene;
|
||||
import emu.grasscutter.scripts.data.SceneGroup;
|
||||
import java.util.List;
|
||||
import java.util.*;
|
||||
import lombok.val;
|
||||
|
||||
public class KillMonsterTimeChallengeFactoryHandler implements ChallengeFactoryHandler {
|
||||
@ -28,6 +29,16 @@ public class KillMonsterTimeChallengeFactoryHandler implements ChallengeFactoryH
|
||||
Scene scene,
|
||||
SceneGroup group) {
|
||||
val realGroup = scene.getScriptManager().getGroupById(groupId);
|
||||
val challengeTriggers = new ArrayList<ChallengeTrigger>();
|
||||
challengeTriggers.addAll(List.of(new KillMonsterCountTrigger(), new InTimeTrigger()));
|
||||
|
||||
val challengeData = GameData.getDungeonChallengeConfigDataMap().get(challengeId);
|
||||
val challengeType = challengeData.getChallengeType();
|
||||
if (challengeType == ChallengeType.CHALLENGE_KILL_COUNT_FAST) {
|
||||
challengeTriggers.add(
|
||||
new KillMonsterTimeIncTrigger(timeLimit, 0 /* refresh to original limit on kill */));
|
||||
}
|
||||
|
||||
return new WorldChallenge(
|
||||
scene,
|
||||
realGroup,
|
||||
@ -36,6 +47,6 @@ public class KillMonsterTimeChallengeFactoryHandler implements ChallengeFactoryH
|
||||
List.of(targetCount, timeLimit),
|
||||
timeLimit, // Limit
|
||||
targetCount, // Goal
|
||||
List.of(new KillMonsterCountTrigger(), new InTimeTrigger()));
|
||||
challengeTriggers);
|
||||
}
|
||||
}
|
||||
|
@ -6,22 +6,33 @@ import emu.grasscutter.server.packet.send.PacketChallengeDataNotify;
|
||||
|
||||
public class KillMonsterTimeIncTrigger extends ChallengeTrigger {
|
||||
|
||||
private int increment;
|
||||
private final int maxTime;
|
||||
private final int increment;
|
||||
|
||||
public KillMonsterTimeIncTrigger(int increment) {
|
||||
public KillMonsterTimeIncTrigger(int maxTime, int increment) {
|
||||
this.maxTime = maxTime;
|
||||
this.increment = increment;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBegin(WorldChallenge challenge) {
|
||||
// challenge.getScene().broadcastPacket(new PacketChallengeDataNotify(challenge, 0,
|
||||
// challenge.getScore().get()));
|
||||
}
|
||||
public void onBegin(WorldChallenge challenge) {}
|
||||
|
||||
@Override
|
||||
public void onMonsterDeath(WorldChallenge challenge, EntityMonster monster) {
|
||||
challenge.getScene().broadcastPacket(new PacketChallengeDataNotify(challenge, 0, increment));
|
||||
|
||||
var scene = challenge.getScene();
|
||||
var elapsed = scene.getSceneTimeSeconds() - challenge.getStartedAt();
|
||||
var timeLeft = challenge.getTimeLimit() - elapsed;
|
||||
var increment = this.increment;
|
||||
if (increment == 0) {
|
||||
// Refresh time limit back to max
|
||||
increment = maxTime - timeLeft;
|
||||
} else if (maxTime < timeLeft + increment) {
|
||||
// Don't add back more time than original limit
|
||||
increment -= timeLeft + increment - maxTime;
|
||||
}
|
||||
challenge.setTimeLimit(challenge.getTimeLimit() + increment);
|
||||
scene.broadcastPacket(
|
||||
new PacketChallengeDataNotify(
|
||||
challenge, 2, timeLeft + increment + scene.getSceneTimeSeconds()));
|
||||
}
|
||||
}
|
||||
|
@ -222,7 +222,9 @@ public class EntityMonster extends GameEntity {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
public void onTick(int sceneTime) {
|
||||
super.onTick(sceneTime);
|
||||
|
||||
// Lua event
|
||||
getScene()
|
||||
.getScriptManager()
|
||||
|
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);
|
||||
}
|
@ -218,6 +218,8 @@ public final class HttpServer {
|
||||
|
||||
<body>
|
||||
<img src="https://http.cat/404" />
|
||||
<h1>Grasscutter cannot find the route you're trying to access.</h1>
|
||||
<p>Your proxy is active, so if you're trying to download something close the game/stop the proxy.</p>
|
||||
</body>
|
||||
</html>
|
||||
""");
|
||||
|
@ -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