23 Commits

Author SHA1 Message Date
f9d46ace7f misc(gradle): Update plugin versions 2024-07-06 23:09:12 -04:00
36346f87f9 Update languages [skip actions] 2024-07-07 03:04:15 +00:00
ea84789c47 Merge remote-tracking branch 'origin/unstable' into unstable 2024-07-06 23:04:06 -04:00
85719b9aeb fix(ci): Upgrade GitHub Actions to Java 21 2024-07-06 23:02:44 -04:00
e5b3d65916 fix(lang): Add language key for password crypto failure
this was copilot generated lmfao
2024-07-06 23:01:18 -04:00
93df2d0b0e Format code [skip actions] 2024-07-07 02:59:06 +00:00
e7ed66477f fix(networking): Prevent hanging the network loop if an exception occurs 2024-07-06 22:54:10 -04:00
af70de316e fix(SceneScriptManager.java): Catch Lua groups NPE
this is a weird issue; found it while testing networking stack and it also crashed the network thread
2024-07-06 22:47:37 -04:00
f29189be8f misc: Update package versions
this also moves some packages to a general number set in `gradle.properties`
2024-07-06 22:34:10 -04:00
4ced11d567 fix(auth): Skip further decryption if encrypted password fails to decrypt
this should only occur if the wrong RSA key is used on the client, otherwise the patch probably forgot to set `is_crypto` to false
2024-07-06 22:33:46 -04:00
446e994ff0 fix(handbook): Skip reading handbook from resources if it is disabled 2024-07-06 22:25:18 -04:00
655016c92e fix(Grasscutter.java): Exclude compiled protos package from being scanned by reflections 2024-07-06 22:24:56 -04:00
d0e3720748 feat(networking): Abstract game session networking
includes:
- abstracted form of session handling
- existing implementation using new abstracted system
- general clean-up of GameSession.java
2024-07-06 22:14:26 -04:00
db4542653a misc(gradle): Allow support with Java 21 2024-07-06 19:30:13 -04:00
76fd5b2e9c Update README_ja-JP.md (#2516) 2024-06-05 21:14:11 -04:00
4022267888 Configuration Update - Shown Email (#2509)
* This version will allow the private server owner to show a different email then "@grasscutter.io" if they want.

* Update src/main/java/emu/grasscutter/config/ConfigContainer.java

Co-authored-by: Magix <27646710+KingRainbow44@users.noreply.github.com>

* Update src/main/java/emu/grasscutter/game/Account.java

Co-authored-by: Magix <27646710+KingRainbow44@users.noreply.github.com>

* Update src/main/java/emu/grasscutter/game/Account.java

Co-authored-by: Magix <27646710+KingRainbow44@users.noreply.github.com>

---------

Co-authored-by: Magix <27646710+KingRainbow44@users.noreply.github.com>
2024-05-13 21:58:46 -04:00
f1f5b54939 (fix:docker) Fix uploading to container registry 2024-03-16 23:09:40 -07:00
f871f261e1 Add Docker Support (#2486)
* chore(docker): add build workflow

* chore(docker): update gradle image

* chore(docker): this really shouldnt be running on raspberry pi's right now.

* chore(docker): not sure why we need unzip here

* chore(docker): attempt to add nodejs to allow the handbook to build

* chore(docker): whoops, needs to be done during build

* chore(docker): i dont know if this is going to work

* chore(docker): replace my username with repo org as I am no longer testing this

* chore(docker): version will change in the future, so fix it now.
2024-03-17 01:57:39 -04:00
eeaccf32c4 add some client download link and fix readme (#2475)
* Update README.md

* Update README_zh-CN.md

* Update README_hn-IN.md
2024-03-17 01:14:10 -04:00
6e1913aacb Add documentation on 404 error page. (#2463)
* Update HttpServer.java

* Update HttpServer.java

---------

Co-authored-by: Magix <27646710+KingRainbow44@users.noreply.github.com>
2024-01-18 23:30:03 -05:00
9e17e4aacb Update client link (#2470) 2024-01-18 23:15:13 -05:00
770a793c69 Format code [skip actions] 2023-12-14 05:36:30 +00:00
c4402cc287 Fix some more dungeons (#2449)
* Monds weapon mats domain: Fix time between kill not refreshing
* Inaz husk domain: Fix broken domain challenge
    * `EVENT_ANY_MONSTER_LIVE` is likely sent on tick, not on create. See scene40801_group240801001.lua:
        1. `condition_EVENT_ANY_MONSTER_LIVE_1023` checks for mob 1008 to spawn AND for variable `challenge` to be 1
        2. Mob 1008 spawns during `action_EVENT_SELECT_OPTION_1003`, at `ScriptLib.AddExtraGroupSuite(context, 240801001, 2)`
        3. This spawn triggers `EVENT_ANY_MONSTER_LIVE` for mob 1008 but still fails the condition because `challenge` is still 0.
        4. `challenge` is set to 1 at the end of `action_EVENT_SELECT_OPTION_1003`. By now, `EVENT_ANY_MONSTER_LIVE` for mob 1008 no longer fires, causing the domain challenge to fail to start.
2023-12-14 00:34:50 -05:00
44 changed files with 559 additions and 262 deletions

View File

@ -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
View 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 }}

View File

@ -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
View File

@ -64,6 +64,7 @@ tmp/
/*.jar
/*.sh
!entrypoint.sh
GM Handbook*.txt
handbook.html

38
Dockerfile Normal file
View 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

View File

@ -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

View File

@ -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 {

View File

@ -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).

View File

@ -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ファイルはプロジェクトのルートフォルダにあります。
### トラブルシューティング

View File

@ -26,7 +26,9 @@
- 获取Java 17https://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
View File

@ -0,0 +1,3 @@
#/bin/sh
java -jar /app/grasscutter.jar

View File

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

View File

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

View File

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

View File

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

View File

@ -35,9 +35,10 @@ public class ConfigContainer {
* HTTP server should start immediately.
* Version 13 - 'game.useUniquePacketKey' was added to control whether the
* encryption key used for packets is a constant or randomly generated.
* Version 14 - 'game.timeout' was added to control the UDP client timeout.
*/
private static int version() {
return 13;
return 14;
}
/**
@ -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) */

View File

@ -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;
}

View File

@ -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;
/**

View File

@ -36,6 +36,6 @@ public class KillMonsterCountInTimeIncChallengeFactoryHandler implements Challen
List.of(
new KillMonsterCountTrigger(),
new InTimeTrigger(),
new KillMonsterTimeIncTrigger(timeInc)));
new KillMonsterTimeIncTrigger(timeLimit, timeInc)));
}
}

View File

@ -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);
}
}

View File

@ -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()));
}
}

View File

@ -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()

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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>
""");

View File

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

View File

@ -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."
},

View File

@ -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."
},

View File

@ -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."
},

View File

@ -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."
},

View File

@ -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] ルーターを接続できません。"
},

View File

@ -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] 라우터에 연결할 수 없습니다."
},

View File

@ -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."
},

View File

@ -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."
},

View File

@ -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] Не удалось присоединить маршрутизатор."
},

View File

@ -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] 无法连接路由"
},

View File

@ -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] 無法附加路由。"
},