mirror of
https://github.com/Grasscutters/Grasscutter.git
synced 2025-01-10 13:32:52 +08:00
Merge branch 'development' into dev-world-scripts
This commit is contained in:
commit
fe4e599014
10
.github/workflows/build.yml
vendored
10
.github/workflows/build.yml
vendored
@ -25,6 +25,16 @@ jobs:
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: '17'
|
||||
- name: Cache gradle files
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: |
|
||||
~/.gradle/caches
|
||||
~/.gradle/wrapper
|
||||
./.gradle/loom-cache
|
||||
key: ${{ runner.os }}-gradle-${{ hashFiles('*.gradle', 'gradle.properties', '**/*.accesswidener') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-gradle-
|
||||
- name: Run Gradle
|
||||
run: ./gradlew && ./gradlew jar
|
||||
- name: Upload build
|
||||
|
26
.gitignore
vendored
26
.gitignore
vendored
@ -17,7 +17,7 @@
|
||||
*.nar
|
||||
*.ear
|
||||
*.zip
|
||||
*.tar.gz
|
||||
*.gz
|
||||
*.rar
|
||||
|
||||
# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
|
||||
@ -52,21 +52,23 @@ tmp/
|
||||
.vscode
|
||||
|
||||
# Grasscutter
|
||||
resources/
|
||||
logs/
|
||||
plugins/
|
||||
data/AbilityEmbryos.json
|
||||
data/OpenConfig.json
|
||||
/resources
|
||||
/logs
|
||||
/plugins
|
||||
/data
|
||||
/keys
|
||||
/language
|
||||
/languages
|
||||
/src/generated
|
||||
|
||||
/*.jar
|
||||
/*.sh
|
||||
|
||||
GM Handbook.txt
|
||||
config.json
|
||||
mitmdump.exe
|
||||
*.jar
|
||||
!lib/*.jar
|
||||
mongod.exe
|
||||
/src/generated/
|
||||
/*.sh
|
||||
language/
|
||||
languages/
|
||||
|
||||
gacha-mapping.js
|
||||
mappings.js
|
||||
BuildConfig.java
|
||||
|
162
data/documentation/handbook.html
Normal file
162
data/documentation/handbook.html
Normal file
@ -0,0 +1,162 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,400&display=swap">
|
||||
<link rel="stylesheet"
|
||||
href="https://cdn.jsdelivr.net/npm/bootstrap@4.6.1/dist/css/bootstrap.min.css">
|
||||
<style>
|
||||
body {
|
||||
background-color: #f0f0f0;
|
||||
}
|
||||
|
||||
p {
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
a, a:hover {
|
||||
text-decoration: none !important;
|
||||
color: #626976;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 3rem 0;
|
||||
}
|
||||
|
||||
.container {
|
||||
color: #626976;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
width: 70%;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
table thead tr {
|
||||
height: 60px;
|
||||
background: #626976;
|
||||
}
|
||||
|
||||
table thead tr th {
|
||||
font-size: 18px;
|
||||
color: white;
|
||||
}
|
||||
|
||||
table tbody tr {
|
||||
height: 50px;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
tbody tr:nth-child(even) {
|
||||
background-color: #fdfdfd;
|
||||
}
|
||||
|
||||
table th, table td {
|
||||
text-align: left;
|
||||
padding: 0 8px;
|
||||
}
|
||||
</style>
|
||||
<title>GM Handbook</title>
|
||||
</head>
|
||||
<body>
|
||||
<div class="content">
|
||||
<div class="container">
|
||||
<h2 class="mb-5">{{TITLE}}</h2>
|
||||
|
||||
<h3>{{TITLE_COMMANDS}}</h3>
|
||||
<hr/>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{HEADER_COMMAND}}</th>
|
||||
<th>{{HEADER_DESCRIPTION}}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
{{COMMANDS_TABLE}}
|
||||
</table>
|
||||
|
||||
<h3>{{TITLE_AVATARS}}</h3>
|
||||
<hr/>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{HEADER_ID}}</th>
|
||||
<th>{{HEADER_AVATAR}}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
{{AVATARS_TABLE}}
|
||||
</table>
|
||||
|
||||
<h3>{{TITLE_ITEMS}}</h3>
|
||||
<hr/>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{HEADER_ID}}</th>
|
||||
<th>{{HEADER_ITEM}}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
{{ITEMS_TABLE}}
|
||||
</table>
|
||||
|
||||
|
||||
<h3>{{TITLE_SCENES}}</h3>
|
||||
<hr/>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{HEADER_ID}}</th>
|
||||
<th>{{HEADER_SCENE}}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
{{SCENES_TABLE}}
|
||||
</table>
|
||||
|
||||
<h3>{{TITLE_MONSTERS}}</h3>
|
||||
<hr/>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{HEADER_ID}}</th>
|
||||
<th>{{HEADER_MONSTER}}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
{{MONSTERS_TABLE}}
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<footer>
|
||||
<div class="copyright">
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<span>Template by BecodReyes. All rights reserved.</span>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<ul style="float:right">
|
||||
<li class="list-inline-item">
|
||||
<a href="https://github.com/Grasscutters/Grasscutter">Github</a>
|
||||
</li>
|
||||
<li class="list-inline-item">·</li>
|
||||
<li class="list-inline-item">
|
||||
<a href="https://github.com/Grasscutters/Grasscutter/blob/stable/LICENSE">License</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
106
data/documentation/index.html
Normal file
106
data/documentation/index.html
Normal file
@ -0,0 +1,106 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,400&display=swap">
|
||||
<link rel="stylesheet"
|
||||
href="https://cdn.jsdelivr.net/npm/bootstrap@4.6.1/dist/css/bootstrap.min.css">
|
||||
<style>
|
||||
body {
|
||||
background-color: #f0f0f0;
|
||||
}
|
||||
|
||||
p {
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
a, a:hover {
|
||||
text-decoration: none !important;
|
||||
color: #626976;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 3rem 0;
|
||||
}
|
||||
|
||||
.container {
|
||||
color: #626976;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
width: 70%;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
table thead tr {
|
||||
height: 60px;
|
||||
background: #626976;
|
||||
}
|
||||
|
||||
table thead tr th {
|
||||
font-size: 18px;
|
||||
color: white;
|
||||
}
|
||||
|
||||
table tbody tr {
|
||||
height: 50px;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
tbody tr:nth-child(even) {
|
||||
background-color: #fdfdfd;
|
||||
}
|
||||
|
||||
table th, table td {
|
||||
text-align: left;
|
||||
padding: 0 8px;
|
||||
}
|
||||
</style>
|
||||
<title>Documentation</title>
|
||||
</head>
|
||||
<body>
|
||||
<div class="content">
|
||||
<div class="container">
|
||||
<h2 class="mb-5">{{TITLE}}</h2>
|
||||
|
||||
<ul>
|
||||
<li><a href="/documentation/handbook">{{ITEM_HANDBOOK}}</a></li>
|
||||
<li><a href="/documentation/gachamapping">{{ITEM_GACHA_MAPPING}}</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<footer>
|
||||
<div class="copyright">
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<span>Template by BecodReyes. All rights reserved.</span>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<ul style="float:right">
|
||||
<li class="list-inline-item">
|
||||
<a href="https://github.com/Grasscutters/Grasscutter">Github</a>
|
||||
</li>
|
||||
<li class="list-inline-item">·</li>
|
||||
<li class="list-inline-item">
|
||||
<a href="https://github.com/Grasscutters/Grasscutter/blob/stable/LICENSE">License</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
@ -28,7 +28,6 @@ public final class Configuration extends ConfigContainer {
|
||||
public static final Locale FALLBACK_LANGUAGE = config.language.fallback;
|
||||
private static final String DATA_FOLDER = config.folderStructure.data;
|
||||
private static final String RESOURCES_FOLDER = config.folderStructure.resources;
|
||||
private static final String KEYS_FOLDER = config.folderStructure.keys;
|
||||
private static final String PLUGINS_FOLDER = config.folderStructure.plugins;
|
||||
private static final String SCRIPTS_FOLDER = config.folderStructure.scripts;
|
||||
private static final String PACKETS_FOLDER = config.folderStructure.packets;
|
||||
@ -62,10 +61,6 @@ public final class Configuration extends ConfigContainer {
|
||||
public static String RESOURCE(String path) {
|
||||
return Paths.get(RESOURCES_FOLDER, path).toString();
|
||||
}
|
||||
|
||||
public static String KEY(String path) {
|
||||
return Paths.get(KEYS_FOLDER, path).toString();
|
||||
}
|
||||
|
||||
public static String PLUGIN() {
|
||||
return PLUGINS_FOLDER;
|
||||
|
@ -14,6 +14,7 @@ import emu.grasscutter.server.http.HttpServer;
|
||||
import emu.grasscutter.server.http.dispatch.DispatchHandler;
|
||||
import emu.grasscutter.server.http.handlers.*;
|
||||
import emu.grasscutter.server.http.dispatch.RegionHandler;
|
||||
import emu.grasscutter.server.http.documentation.DocumentationServerHandler;
|
||||
import emu.grasscutter.utils.ConfigContainer;
|
||||
import emu.grasscutter.utils.Utils;
|
||||
import org.jline.reader.EndOfFileException;
|
||||
@ -129,6 +130,7 @@ public final class Grasscutter {
|
||||
httpServer.addRouter(AnnouncementsHandler.class);
|
||||
httpServer.addRouter(DispatchHandler.class);
|
||||
httpServer.addRouter(GachaHandler.class);
|
||||
httpServer.addRouter(DocumentationServerHandler.class);
|
||||
|
||||
// TODO: find a better place?
|
||||
StaminaManager.initialize();
|
||||
|
@ -1,5 +1,6 @@
|
||||
package emu.grasscutter.auth;
|
||||
|
||||
import emu.grasscutter.game.Account;
|
||||
import emu.grasscutter.server.http.objects.*;
|
||||
import express.http.Request;
|
||||
import express.http.Response;
|
||||
@ -30,10 +31,10 @@ public interface AuthenticationSystem {
|
||||
|
||||
/**
|
||||
* Called by plugins to internally verify a user's identity.
|
||||
* @param details A unique, one-time token to verify the user.
|
||||
* @return True if the user is verified, False otherwise.
|
||||
* @param details A unique identifier to identify the user. (For example: a JWT token)
|
||||
* @return The user's account if the verification was successful, null if the user was unable to be verified.
|
||||
*/
|
||||
boolean verifyUser(String details);
|
||||
Account verifyUser(String details);
|
||||
|
||||
/**
|
||||
* This is the authenticator used for password authentication.
|
||||
@ -59,6 +60,12 @@ public interface AuthenticationSystem {
|
||||
*/
|
||||
ExternalAuthenticator getExternalAuthenticator();
|
||||
|
||||
/**
|
||||
* This is the authenticator used for handling OAuth authentication requests.
|
||||
* @return An authenticator.
|
||||
*/
|
||||
OAuthAuthenticator getOAuthAuthenticator();
|
||||
|
||||
/**
|
||||
* A data container that holds relevant data for authenticating a client.
|
||||
*/
|
||||
@ -124,4 +131,16 @@ public interface AuthenticationSystem {
|
||||
return AuthenticationRequest.builder().request(request)
|
||||
.response(response).build();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Generates an authentication request from a {@link Response} object.
|
||||
* @param request The Express request.
|
||||
* @param jsonData The JSON data.
|
||||
* @return An authentication request.
|
||||
*/
|
||||
static AuthenticationRequest fromOAuthRequest(Request request, Response response) {
|
||||
return AuthenticationRequest.builder().request(request)
|
||||
.response(response).build();
|
||||
}
|
||||
}
|
||||
|
@ -2,6 +2,7 @@ package emu.grasscutter.auth;
|
||||
|
||||
import emu.grasscutter.Grasscutter;
|
||||
import emu.grasscutter.auth.DefaultAuthenticators.*;
|
||||
import emu.grasscutter.game.Account;
|
||||
import emu.grasscutter.server.http.objects.ComboTokenResJson;
|
||||
import emu.grasscutter.server.http.objects.LoginResultJson;
|
||||
|
||||
@ -16,6 +17,7 @@ public final class DefaultAuthentication implements AuthenticationSystem {
|
||||
private final Authenticator<LoginResultJson> tokenAuthenticator = new TokenAuthenticator();
|
||||
private final Authenticator<ComboTokenResJson> sessionKeyAuthenticator = new SessionKeyAuthenticator();
|
||||
private final ExternalAuthenticator externalAuthenticator = new ExternalAuthentication();
|
||||
private final OAuthAuthenticator oAuthAuthenticator = new OAuthAuthentication();
|
||||
|
||||
@Override
|
||||
public void createAccount(String username, String password) {
|
||||
@ -26,11 +28,11 @@ public final class DefaultAuthentication implements AuthenticationSystem {
|
||||
public void resetPassword(String username) {
|
||||
// Unhandled. The default authenticator doesn't store passwords.
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public boolean verifyUser(String details) {
|
||||
public Account verifyUser(String details) {
|
||||
Grasscutter.getLogger().info(translate("dispatch.authentication.default_unable_to_verify"));
|
||||
return false;
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -52,4 +54,9 @@ public final class DefaultAuthentication implements AuthenticationSystem {
|
||||
public ExternalAuthenticator getExternalAuthenticator() {
|
||||
return this.externalAuthenticator;
|
||||
}
|
||||
|
||||
@Override
|
||||
public OAuthAuthenticator getOAuthAuthenticator() {
|
||||
return this.oAuthAuthenticator;
|
||||
}
|
||||
}
|
||||
|
@ -41,10 +41,6 @@ public final class DefaultAuthenticators {
|
||||
responseMessage = translate("messages.dispatch.account.username_create_error");
|
||||
Grasscutter.getLogger().info(translate("messages.dispatch.account.account_login_create_error", address));
|
||||
} else {
|
||||
// Add default permissions.
|
||||
for (var permission : ACCOUNT.defaultPermissions)
|
||||
account.addPermission(permission);
|
||||
|
||||
// Continue with login.
|
||||
successfulLogin = true;
|
||||
|
||||
@ -178,4 +174,29 @@ public final class DefaultAuthenticators {
|
||||
request.getResponse().send("Authentication is not available with the default authentication method.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles authentication requests from OAuth sources.
|
||||
*/
|
||||
public static class OAuthAuthentication implements OAuthAuthenticator {
|
||||
@Override public void handleLogin(AuthenticationRequest request) {
|
||||
assert request.getResponse() != null;
|
||||
request.getResponse().send("Authentication is not available with the default authentication method.");
|
||||
}
|
||||
|
||||
@Override public void handleDesktopRedirection(AuthenticationRequest request) {
|
||||
assert request.getResponse() != null;
|
||||
request.getResponse().send("Authentication is not available with the default authentication method.");
|
||||
}
|
||||
|
||||
@Override public void handleMobileRedirection(AuthenticationRequest request) {
|
||||
assert request.getResponse() != null;
|
||||
request.getResponse().send("Authentication is not available with the default authentication method.");
|
||||
}
|
||||
|
||||
@Override public void handleTokenProcess(AuthenticationRequest request) {
|
||||
assert request.getResponse() != null;
|
||||
request.getResponse().send("Authentication is not available with the default authentication method.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
28
src/main/java/emu/grasscutter/auth/OAuthAuthenticator.java
Normal file
28
src/main/java/emu/grasscutter/auth/OAuthAuthenticator.java
Normal file
@ -0,0 +1,28 @@
|
||||
package emu.grasscutter.auth;
|
||||
|
||||
import emu.grasscutter.auth.AuthenticationSystem.AuthenticationRequest;
|
||||
|
||||
/**
|
||||
* Handles authentication via OAuth routes.
|
||||
*/
|
||||
public interface OAuthAuthenticator {
|
||||
|
||||
/**
|
||||
* Called when an OAuth login request is made.
|
||||
* @param request The authentication request.
|
||||
*/
|
||||
void handleLogin(AuthenticationRequest request);
|
||||
|
||||
/**
|
||||
* Called when an client requests to redirect to login page.
|
||||
* @param request The authentication request.
|
||||
*/
|
||||
void handleDesktopRedirection(AuthenticationRequest request);
|
||||
void handleMobileRedirection(AuthenticationRequest request);
|
||||
|
||||
/**
|
||||
* Called when an OAuth login requests callback.
|
||||
* @param request The authentication request.
|
||||
*/
|
||||
void handleTokenProcess(AuthenticationRequest request);
|
||||
}
|
@ -2,6 +2,8 @@ package emu.grasscutter.command;
|
||||
|
||||
import emu.grasscutter.Grasscutter;
|
||||
import emu.grasscutter.game.player.Player;
|
||||
import emu.grasscutter.server.event.game.CommandResponseEvent;
|
||||
import emu.grasscutter.server.event.types.ServerEvent;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@ -19,6 +21,8 @@ public interface CommandHandler {
|
||||
} else {
|
||||
player.dropMessage(message);
|
||||
}
|
||||
CommandResponseEvent event = new CommandResponseEvent(ServerEvent.Type.GAME,player, message);
|
||||
event.call();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -9,7 +9,7 @@ import java.util.List;
|
||||
|
||||
import static emu.grasscutter.utils.Language.translate;
|
||||
|
||||
@Command(label = "resetshop", usage = "resetshop", permission = "server.resetshop", permissionTargeted = "server.resetshop.others", description = "commands.resetshop.description")
|
||||
@Command(label = "resetshop", usage = "resetshop <player id>", permission = "server.resetshop", permissionTargeted = "server.resetshop.others", description = "commands.resetShopLimit.description")
|
||||
public final class ResetShopLimitCommand implements CommandHandler {
|
||||
|
||||
@Override
|
||||
@ -19,6 +19,11 @@ public final class ResetShopLimitCommand implements CommandHandler {
|
||||
return;
|
||||
}
|
||||
|
||||
if (args.isEmpty()) {
|
||||
CommandHandler.sendMessage(sender, translate(sender, "commands.resetShopLimit.usage"));
|
||||
return;
|
||||
}
|
||||
|
||||
targetPlayer.getShopLimit().forEach(x -> x.setNextRefreshTime(0));
|
||||
targetPlayer.save();
|
||||
CommandHandler.sendMessage(sender, translate(sender, "commands.status.success"));
|
||||
|
101
src/main/java/emu/grasscutter/data/DataLoader.java
Normal file
101
src/main/java/emu/grasscutter/data/DataLoader.java
Normal file
@ -0,0 +1,101 @@
|
||||
package emu.grasscutter.data;
|
||||
|
||||
import emu.grasscutter.Grasscutter;
|
||||
import emu.grasscutter.server.http.handlers.GachaHandler;
|
||||
import emu.grasscutter.tools.Tools;
|
||||
import emu.grasscutter.utils.FileUtils;
|
||||
import emu.grasscutter.utils.Utils;
|
||||
|
||||
import java.io.*;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
|
||||
import static emu.grasscutter.Configuration.DATA;
|
||||
|
||||
public class DataLoader {
|
||||
|
||||
/**
|
||||
* Load a data file by its name. If the file isn't found within the /data directory then it will fallback to the default within the jar resources
|
||||
* @see #load(String, boolean)
|
||||
* @param resourcePath The path to the data file to be loaded.
|
||||
* @return InputStream of the data file.
|
||||
* @throws FileNotFoundException
|
||||
*/
|
||||
public static InputStream load(String resourcePath) throws FileNotFoundException {
|
||||
return load(resourcePath, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load a data file by its name.
|
||||
* @param resourcePath The path to the data file to be loaded.
|
||||
* @param useFallback If the file does not exist in the /data directory, should it use the default file in the jar?
|
||||
* @return InputStream of the data file.
|
||||
* @throws FileNotFoundException
|
||||
*/
|
||||
public static InputStream load(String resourcePath, boolean useFallback) throws FileNotFoundException {
|
||||
if(Utils.fileExists(DATA(resourcePath))) {
|
||||
// Data is in the resource directory
|
||||
return new FileInputStream(DATA(resourcePath));
|
||||
} else {
|
||||
if(useFallback) {
|
||||
return FileUtils.readResourceAsStream("/defaults/data/" + resourcePath);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public static void CheckAllFiles() {
|
||||
|
||||
try {
|
||||
List<Path> filenames = FileUtils.getPathsFromResource("/defaults/data/");
|
||||
|
||||
for (Path file : filenames) {
|
||||
String relativePath = String.valueOf(file).split("/defaults/data/")[1];
|
||||
|
||||
CheckAndCopyData(relativePath);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Grasscutter.getLogger().error("An error occurred while trying to check the data folder. \n" + e);
|
||||
}
|
||||
|
||||
GenerateGachaMappings();
|
||||
}
|
||||
|
||||
private static void CheckAndCopyData(String name) {
|
||||
String filePath = Utils.toFilePath(DATA(name));
|
||||
|
||||
if (!Utils.fileExists(filePath)) {
|
||||
// Check if file is in subdirectory
|
||||
if (name.indexOf("/") != -1) {
|
||||
String[] path = name.split("/");
|
||||
|
||||
String folder = "";
|
||||
for(int i = 0; i < (path.length - 1); i++) {
|
||||
folder += path[i] + "/";
|
||||
|
||||
// Make sure the current folder exists
|
||||
String folderToCreate = Utils.toFilePath(DATA(folder));
|
||||
if(!Utils.fileExists(folderToCreate)) {
|
||||
Grasscutter.getLogger().info("Creating data folder '" + folder + "'");
|
||||
Utils.createFolder(folderToCreate);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Grasscutter.getLogger().info("Creating default '" + name + "' data");
|
||||
FileUtils.copyResource("/defaults/data/" + name, filePath);
|
||||
}
|
||||
}
|
||||
|
||||
private static void GenerateGachaMappings() {
|
||||
if (!Utils.fileExists(GachaHandler.gachaMappings)) {
|
||||
try {
|
||||
Grasscutter.getLogger().info("Creating default '" + GachaHandler.gachaMappings + "' data");
|
||||
Tools.createGachaMapping(GachaHandler.gachaMappings);
|
||||
} catch (Exception exception) {
|
||||
Grasscutter.getLogger().warn("Failed to create gacha mappings. \n" + exception);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,7 +1,6 @@
|
||||
package emu.grasscutter.data;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileReader;
|
||||
import java.io.*;
|
||||
import java.util.*;
|
||||
import java.util.Map.Entry;
|
||||
import java.util.regex.Matcher;
|
||||
@ -33,6 +32,8 @@ import static emu.grasscutter.Configuration.*;
|
||||
|
||||
public class ResourceLoader {
|
||||
|
||||
private static List<String> loadedResources = new ArrayList<String>();
|
||||
|
||||
public static List<Class<?>> getResourceDefClasses() {
|
||||
Reflections reflections = new Reflections(ResourceLoader.class.getPackage().getName());
|
||||
Set<?> classes = reflections.getSubTypesOf(GameResource.class);
|
||||
@ -98,6 +99,10 @@ public class ResourceLoader {
|
||||
}
|
||||
|
||||
public static void loadResources() {
|
||||
loadResources(false);
|
||||
}
|
||||
|
||||
public static void loadResources(boolean doReload) {
|
||||
for (Class<?> resourceDefinition : getResourceDefClasses()) {
|
||||
ResourceType type = resourceDefinition.getAnnotation(ResourceType.class);
|
||||
|
||||
@ -113,7 +118,7 @@ public class ResourceLoader {
|
||||
}
|
||||
|
||||
try {
|
||||
loadFromResource(resourceDefinition, type, map);
|
||||
loadFromResource(resourceDefinition, type, map, doReload);
|
||||
} catch (Exception e) {
|
||||
Grasscutter.getLogger().error("Error loading resource file: " + Arrays.toString(type.name()), e);
|
||||
}
|
||||
@ -121,13 +126,16 @@ public class ResourceLoader {
|
||||
}
|
||||
|
||||
@SuppressWarnings("rawtypes")
|
||||
protected static void loadFromResource(Class<?> c, ResourceType type, Int2ObjectMap map) throws Exception {
|
||||
for (String name : type.name()) {
|
||||
loadFromResource(c, name, map);
|
||||
protected static void loadFromResource(Class<?> c, ResourceType type, Int2ObjectMap map, boolean doReload) throws Exception {
|
||||
if(!loadedResources.contains(c.getSimpleName()) || doReload) {
|
||||
for (String name : type.name()) {
|
||||
loadFromResource(c, name, map);
|
||||
}
|
||||
Grasscutter.getLogger().info("Loaded " + map.size() + " " + c.getSimpleName() + "s.");
|
||||
loadedResources.add(c.getSimpleName());
|
||||
}
|
||||
Grasscutter.getLogger().info("Loaded " + map.size() + " " + c.getSimpleName() + "s.");
|
||||
}
|
||||
|
||||
|
||||
@SuppressWarnings({"rawtypes", "unchecked"})
|
||||
protected static void loadFromResource(Class<?> c, String fileName, Int2ObjectMap map) throws Exception {
|
||||
FileReader fileReader = new FileReader(RESOURCE("ExcelBinOutput/" + fileName));
|
||||
@ -138,6 +146,9 @@ public class ResourceLoader {
|
||||
Map<String, Object> tempMap = Utils.switchPropertiesUpperLowerCase((Map<String, Object>) o, c);
|
||||
GameResource res = gson.fromJson(gson.toJson(tempMap), TypeToken.get(c).getType());
|
||||
res.onLoad();
|
||||
if(map.containsKey(res.getId())) {
|
||||
map.remove(res.getId());
|
||||
}
|
||||
map.put(res.getId(), res);
|
||||
}
|
||||
}
|
||||
@ -191,18 +202,14 @@ public class ResourceLoader {
|
||||
}
|
||||
|
||||
private static void loadAbilityEmbryos() {
|
||||
// Read from cached file if exists
|
||||
File embryoCache = new File(DATA("AbilityEmbryos.json"));
|
||||
List<AbilityEmbryoEntry> embryoList = null;
|
||||
|
||||
if (embryoCache.exists()) {
|
||||
// Load from cache
|
||||
try (FileReader fileReader = new FileReader(embryoCache)) {
|
||||
embryoList = Grasscutter.getGsonFactory().fromJson(fileReader, TypeToken.getParameterized(Collection.class, AbilityEmbryoEntry.class).getType());
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
} else {
|
||||
|
||||
// Read from cached file if exists
|
||||
try(InputStream embryoCache = DataLoader.load("AbilityEmbryos.json", false)) {
|
||||
embryoList = Grasscutter.getGsonFactory().fromJson(new InputStreamReader(embryoCache), TypeToken.getParameterized(Collection.class, AbilityEmbryoEntry.class).getType());
|
||||
} catch(Exception ignored) {}
|
||||
|
||||
if(embryoList == null) {
|
||||
// Load from BinOutput
|
||||
Pattern pattern = Pattern.compile("(?<=ConfigAvatar_)(.*?)(?=.json)");
|
||||
|
||||
@ -316,18 +323,12 @@ public class ResourceLoader {
|
||||
}
|
||||
|
||||
private static void loadSpawnData() {
|
||||
// Read from cached file if exists
|
||||
File spawnDataEntries = new File(DATA("Spawns.json"));
|
||||
List<SpawnGroupEntry> spawnEntryList = null;
|
||||
|
||||
if (spawnDataEntries.exists()) {
|
||||
// Load from cache
|
||||
try (FileReader fileReader = new FileReader(spawnDataEntries)) {
|
||||
spawnEntryList = Grasscutter.getGsonFactory().fromJson(fileReader, TypeToken.getParameterized(Collection.class, SpawnGroupEntry.class).getType());
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
// Read from cached file if exists
|
||||
try(InputStream spawnDataEntries = DataLoader.load("Spawns.json")) {
|
||||
spawnEntryList = Grasscutter.getGsonFactory().fromJson(new InputStreamReader(spawnDataEntries), TypeToken.getParameterized(Collection.class, SpawnGroupEntry.class).getType());
|
||||
} catch (Exception ignored) {}
|
||||
|
||||
if (spawnEntryList == null || spawnEntryList.isEmpty()) {
|
||||
Grasscutter.getLogger().error("No spawn data loaded!");
|
||||
@ -342,16 +343,13 @@ public class ResourceLoader {
|
||||
|
||||
private static void loadOpenConfig() {
|
||||
// Read from cached file if exists
|
||||
File openConfigCache = new File(DATA("OpenConfig.json"));
|
||||
List<OpenConfigEntry> list = null;
|
||||
|
||||
if (openConfigCache.exists()) {
|
||||
try (FileReader fileReader = new FileReader(openConfigCache)) {
|
||||
list = Grasscutter.getGsonFactory().fromJson(fileReader, TypeToken.getParameterized(Collection.class, OpenConfigEntry.class).getType());
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
} else {
|
||||
|
||||
try(InputStream openConfigCache = DataLoader.load("OpenConfig.json", false)) {
|
||||
list = Grasscutter.getGsonFactory().fromJson(new InputStreamReader(openConfigCache), TypeToken.getParameterized(Collection.class, SpawnGroupEntry.class).getType());
|
||||
} catch (Exception ignored) {}
|
||||
|
||||
if (list == null) {
|
||||
Map<String, OpenConfigEntry> map = new TreeMap<>();
|
||||
java.lang.reflect.Type type = new TypeToken<Map<String, OpenConfigData[]>>() {}.getType();
|
||||
String[] folderNames = {"BinOutput/Talent/EquipTalents/", "BinOutput/Talent/AvatarTalents/"};
|
||||
|
@ -1,14 +1,12 @@
|
||||
package emu.grasscutter.game;
|
||||
|
||||
import dev.morphia.annotations.*;
|
||||
import emu.grasscutter.Grasscutter;
|
||||
import emu.grasscutter.database.DatabaseHelper;
|
||||
import emu.grasscutter.utils.Crypto;
|
||||
import emu.grasscutter.utils.Utils;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.*;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.bson.Document;
|
||||
|
||||
@ -144,19 +142,24 @@ public class Account {
|
||||
}
|
||||
|
||||
public boolean hasPermission(String permission) {
|
||||
|
||||
if (this.permissions.contains(permission)) return true;
|
||||
if(this.permissions.contains("*") && this.permissions.size() == 1) return true;
|
||||
|
||||
// Add default permissions if it doesn't exist
|
||||
List<String> permissions = Stream.of(this.permissions, Arrays.asList(ACCOUNT.defaultPermissions))
|
||||
.flatMap(Collection::stream)
|
||||
.distinct().toList();
|
||||
|
||||
if (permissions.contains(permission)) return true;
|
||||
|
||||
String[] permissionParts = permission.split("\\.");
|
||||
for (String p : this.permissions) {
|
||||
for (String p : permissions) {
|
||||
if (p.startsWith("-") && permissionMatchesWildcard(p.substring(1), permissionParts)) return false;
|
||||
if (permissionMatchesWildcard(p, permissionParts)) return true;
|
||||
}
|
||||
|
||||
return this.permissions.contains("*");
|
||||
return permissions.contains("*");
|
||||
}
|
||||
|
||||
|
||||
public boolean removePermission(String permission) {
|
||||
return this.permissions.remove(permission);
|
||||
}
|
||||
|
@ -2,6 +2,7 @@ package emu.grasscutter.game.drop;
|
||||
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
import emu.grasscutter.Grasscutter;
|
||||
import emu.grasscutter.data.DataLoader;
|
||||
import emu.grasscutter.data.GameData;
|
||||
import emu.grasscutter.data.def.ItemData;
|
||||
import emu.grasscutter.game.entity.EntityItem;
|
||||
@ -17,12 +18,11 @@ import emu.grasscutter.utils.Utils;
|
||||
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
|
||||
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
|
||||
|
||||
import java.io.FileReader;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.Reader;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
import static emu.grasscutter.Configuration.*;
|
||||
|
||||
public class DropManager {
|
||||
public GameServer getGameServer() {
|
||||
return gameServer;
|
||||
@ -43,7 +43,7 @@ public class DropManager {
|
||||
}
|
||||
|
||||
public synchronized void load() {
|
||||
try (FileReader fileReader = new FileReader(DATA("Drop.json"))) {
|
||||
try (Reader fileReader = new InputStreamReader(DataLoader.load("Drop.json"))) {
|
||||
getDropData().clear();
|
||||
List<DropInfo> banners = Grasscutter.getGsonFactory().fromJson(fileReader, TypeToken.getParameterized(Collection.class, DropInfo.class).getType());
|
||||
if(banners.size() > 0) {
|
||||
|
@ -2,11 +2,14 @@ package emu.grasscutter.game.expedition;
|
||||
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
import emu.grasscutter.Grasscutter;
|
||||
import emu.grasscutter.data.DataLoader;
|
||||
import emu.grasscutter.server.game.GameServer;
|
||||
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
|
||||
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
|
||||
|
||||
import java.io.FileReader;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.Reader;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
@ -30,7 +33,7 @@ public class ExpeditionManager {
|
||||
}
|
||||
|
||||
public synchronized void load() {
|
||||
try (FileReader fileReader = new FileReader(DATA("ExpeditionReward.json"))) {
|
||||
try (Reader fileReader = new InputStreamReader(DataLoader.load("ExpeditionReward.json"))) {
|
||||
getExpeditionRewardDataList().clear();
|
||||
List<ExpeditionRewardInfo> banners = Grasscutter.getGsonFactory().fromJson(fileReader, TypeToken.getParameterized(Collection.class, ExpeditionRewardInfo.class).getType());
|
||||
if(banners.size() > 0) {
|
||||
|
@ -2,6 +2,8 @@ package emu.grasscutter.game.gacha;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileReader;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.Reader;
|
||||
import java.nio.file.*;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
@ -13,6 +15,7 @@ import com.google.gson.reflect.TypeToken;
|
||||
|
||||
import com.sun.nio.file.SensitivityWatchEventModifier;
|
||||
import emu.grasscutter.Grasscutter;
|
||||
import emu.grasscutter.data.DataLoader;
|
||||
import emu.grasscutter.data.GameData;
|
||||
import emu.grasscutter.data.common.ItemParamData;
|
||||
import emu.grasscutter.data.def.ItemData;
|
||||
@ -74,7 +77,7 @@ public class GachaManager {
|
||||
}
|
||||
|
||||
public synchronized void load() {
|
||||
try (FileReader fileReader = new FileReader(DATA("Banners.json"))) {
|
||||
try (Reader fileReader = new InputStreamReader(DataLoader.load("Banners.json"))) {
|
||||
getGachaBanners().clear();
|
||||
List<GachaBanner> banners = Grasscutter.getGsonFactory().fromJson(fileReader, TypeToken.getParameterized(Collection.class, GachaBanner.class).getType());
|
||||
if(banners.size() > 0) {
|
||||
|
@ -2,6 +2,7 @@ package emu.grasscutter.game.shop;
|
||||
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
import emu.grasscutter.Grasscutter;
|
||||
import emu.grasscutter.data.DataLoader;
|
||||
import emu.grasscutter.data.GameData;
|
||||
import emu.grasscutter.data.common.ItemParamData;
|
||||
import emu.grasscutter.data.def.ShopGoodsData;
|
||||
@ -11,6 +12,8 @@ import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
|
||||
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
|
||||
|
||||
import java.io.FileReader;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.Reader;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Iterator;
|
||||
@ -58,7 +61,7 @@ public class ShopManager {
|
||||
}
|
||||
|
||||
private void loadShop() {
|
||||
try (FileReader fileReader = new FileReader(DATA("Shop.json"))) {
|
||||
try (Reader fileReader = new InputStreamReader(DataLoader.load("Shop.json"))) {
|
||||
getShopData().clear();
|
||||
List<ShopTable> banners = Grasscutter.getGsonFactory().fromJson(fileReader, TypeToken.getParameterized(Collection.class, ShopTable.class).getType());
|
||||
if(banners.size() > 0) {
|
||||
@ -102,7 +105,7 @@ public class ShopManager {
|
||||
}
|
||||
|
||||
private void loadShopChest() {
|
||||
try (FileReader fileReader = new FileReader(DATA("ShopChest.json"))) {
|
||||
try (Reader fileReader = new InputStreamReader(DataLoader.load("ShopChest.json"))) {
|
||||
getShopChestData().clear();
|
||||
List<ShopChestTable> shopChestTableList = Grasscutter.getGsonFactory().fromJson(fileReader, TypeToken.getParameterized(Collection.class, ShopChestTable.class).getType());
|
||||
if (shopChestTableList.size() > 0) {
|
||||
@ -117,7 +120,7 @@ public class ShopManager {
|
||||
}
|
||||
|
||||
private void loadShopChestBatchUse() {
|
||||
try (FileReader fileReader = new FileReader(DATA("ShopChestBatchUse.json"))) {
|
||||
try (Reader fileReader = new InputStreamReader(DataLoader.load("ShopChestBatchUse.json"))) {
|
||||
getShopChestBatchUseData().clear();
|
||||
List<ShopChestBatchUseTable> shopChestBatchUseTableList = Grasscutter.getGsonFactory().fromJson(fileReader, TypeToken.getParameterized(Collection.class, ShopChestBatchUseTable.class).getType());
|
||||
if (shopChestBatchUseTableList.size() > 0) {
|
||||
|
@ -1,11 +1,14 @@
|
||||
package emu.grasscutter.game.tower;
|
||||
|
||||
import emu.grasscutter.Grasscutter;
|
||||
import emu.grasscutter.data.DataLoader;
|
||||
import emu.grasscutter.data.GameData;
|
||||
import emu.grasscutter.data.def.TowerScheduleData;
|
||||
import emu.grasscutter.server.game.GameServer;
|
||||
|
||||
import java.io.FileReader;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.Reader;
|
||||
import java.util.List;
|
||||
|
||||
import static emu.grasscutter.Configuration.*;
|
||||
@ -25,7 +28,7 @@ public class TowerScheduleManager {
|
||||
private TowerScheduleConfig towerScheduleConfig;
|
||||
|
||||
public synchronized void load(){
|
||||
try (FileReader fileReader = new FileReader(DATA("TowerSchedule.json"))) {
|
||||
try (Reader fileReader = new InputStreamReader(DataLoader.load("TowerSchedule.json"))) {
|
||||
towerScheduleConfig = Grasscutter.getGsonFactory().fromJson(fileReader, TowerScheduleConfig.class);
|
||||
} catch (Exception e) {
|
||||
Grasscutter.getLogger().error("Unable to load tower schedule config.", e);
|
||||
|
@ -1,6 +1,7 @@
|
||||
package emu.grasscutter.net.packet;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
|
||||
public class PacketOpcodes {
|
||||
@ -1556,5 +1557,8 @@ public class PacketOpcodes {
|
||||
public static final int UNKNOWN_44 = 8983;
|
||||
public static final int UNKNOWN_45 = 943;
|
||||
|
||||
public static final List<Integer> BANNED_PACKETS = Arrays.asList(PacketOpcodes.WindSeedClientNotify, PacketOpcodes.PlayerLuaShellNotify);
|
||||
public static final HashSet<Integer> BANNED_PACKETS = new HashSet<Integer>() {{
|
||||
add(PacketOpcodes.WindSeedClientNotify);
|
||||
add(PacketOpcodes.PlayerLuaShellNotify);
|
||||
}};
|
||||
}
|
||||
|
@ -17,10 +17,12 @@ public class PacketOpcodesUtil {
|
||||
Field[] fields = PacketOpcodes.class.getFields();
|
||||
|
||||
for (Field f : fields) {
|
||||
try {
|
||||
opcodeMap.put(f.getInt(null), f.getName());
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
if(f.getType().equals(int.class)) {
|
||||
try {
|
||||
opcodeMap.put(f.getInt(null), f.getName());
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,24 @@
|
||||
package emu.grasscutter.server.event.game;
|
||||
|
||||
import emu.grasscutter.game.player.Player;
|
||||
import emu.grasscutter.server.event.types.GameEvent;
|
||||
import emu.grasscutter.server.event.types.ServerEvent;
|
||||
|
||||
public class CommandResponseEvent extends ServerEvent {
|
||||
private String message;
|
||||
private Player player;
|
||||
|
||||
public CommandResponseEvent(Type type, Player player,String message) {
|
||||
super(type);
|
||||
this.message = message;
|
||||
this.player = player;
|
||||
}
|
||||
|
||||
public String getMessage() {
|
||||
return message;
|
||||
}
|
||||
|
||||
public Player getPlayer() {
|
||||
return player;
|
||||
}
|
||||
}
|
@ -33,6 +33,14 @@ public final class DispatchHandler implements Router {
|
||||
.handleAccountCreation(AuthenticationSystem.fromExternalRequest(request, response)));
|
||||
express.post("/authentication/change_password", (request, response) -> Grasscutter.getAuthenticationSystem().getExternalAuthenticator()
|
||||
.handlePasswordReset(AuthenticationSystem.fromExternalRequest(request, response)));
|
||||
|
||||
// OAuth login
|
||||
express.post("/hk4e_global/mdk/shield/api/loginByThirdparty", (request, response) -> Grasscutter.getAuthenticationSystem().getOAuthAuthenticator().handleLogin(AuthenticationSystem.fromOAuthRequest(request, response)));
|
||||
// OAuth querystring convert redirection
|
||||
express.get("/authentication/openid/redirect", (request, response) -> Grasscutter.getAuthenticationSystem().getOAuthAuthenticator().handleTokenProcess(AuthenticationSystem.fromOAuthRequest(request, response)));
|
||||
// OAuth redirection
|
||||
express.get("/Api/twitter_login", (request, response) -> Grasscutter.getAuthenticationSystem().getOAuthAuthenticator().handleDesktopRedirection(AuthenticationSystem.fromOAuthRequest(request, response)));
|
||||
express.get("/sdkTwitterLogin.html", (request, response) -> Grasscutter.getAuthenticationSystem().getOAuthAuthenticator().handleMobileRedirection(AuthenticationSystem.fromOAuthRequest(request, response)));
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -60,7 +60,8 @@ public final class RegionHandler implements Router {
|
||||
if(SERVER.runMode != ServerRunMode.HYBRID && configuredRegions.size() == 0) {
|
||||
Grasscutter.getLogger().error("[Dispatch] There are no game servers available. Exiting due to unplayable state.");
|
||||
System.exit(1);
|
||||
} else configuredRegions.add(new Region("os_usa", DISPATCH_INFO.defaultName,
|
||||
} else if (configuredRegions.size() == 0)
|
||||
configuredRegions.add(new Region("os_usa", DISPATCH_INFO.defaultName,
|
||||
lr(GAME_INFO.accessAddress, GAME_INFO.bindAddress),
|
||||
lr(GAME_INFO.accessPort, GAME_INFO.bindPort)));
|
||||
|
||||
|
@ -0,0 +1,9 @@
|
||||
package emu.grasscutter.server.http.documentation;
|
||||
|
||||
import express.http.Request;
|
||||
import express.http.Response;
|
||||
|
||||
interface DocumentationHandler {
|
||||
|
||||
void handle(Request request, Response response);
|
||||
}
|
@ -0,0 +1,19 @@
|
||||
package emu.grasscutter.server.http.documentation;
|
||||
|
||||
import emu.grasscutter.server.http.Router;
|
||||
import express.Express;
|
||||
import io.javalin.Javalin;
|
||||
|
||||
public final class DocumentationServerHandler implements Router {
|
||||
|
||||
@Override
|
||||
public void applyRoutes(Express express, Javalin handle) {
|
||||
final RootRequestHandler root = new RootRequestHandler();
|
||||
final HandbookRequestHandler handbook = new HandbookRequestHandler();
|
||||
final GachaMappingRequestHandler gachaMapping = new GachaMappingRequestHandler();
|
||||
|
||||
express.get("/documentation/handbook", handbook::handle);
|
||||
express.get("/documentation/gachamapping", gachaMapping::handle);
|
||||
express.get("/documentation", root::handle);
|
||||
}
|
||||
}
|
@ -0,0 +1,155 @@
|
||||
package emu.grasscutter.server.http.documentation;
|
||||
|
||||
import static emu.grasscutter.Configuration.RESOURCE;
|
||||
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
import emu.grasscutter.Grasscutter;
|
||||
import emu.grasscutter.data.GameData;
|
||||
import emu.grasscutter.data.ResourceLoader;
|
||||
import emu.grasscutter.data.def.AvatarData;
|
||||
import emu.grasscutter.data.def.ItemData;
|
||||
import emu.grasscutter.tools.Tools;
|
||||
import emu.grasscutter.utils.Utils;
|
||||
import express.http.Request;
|
||||
import express.http.Response;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
final class GachaMappingRequestHandler implements DocumentationHandler {
|
||||
|
||||
private Map<Long, String> map;
|
||||
|
||||
GachaMappingRequestHandler() {
|
||||
ResourceLoader.loadResources();
|
||||
final String textMapFile = "TextMap/TextMap" + Tools.getLanguageOption() + ".json";
|
||||
try (InputStreamReader fileReader = new InputStreamReader(new FileInputStream(
|
||||
Utils.toFilePath(RESOURCE(textMapFile))), StandardCharsets.UTF_8)) {
|
||||
map = Grasscutter.getGsonFactory().fromJson(fileReader,
|
||||
new TypeToken<Map<Long, String>>() {
|
||||
}.getType());
|
||||
} catch (IOException e) {
|
||||
Grasscutter.getLogger().warn("Resource does not exist: " + textMapFile);
|
||||
map = new HashMap<>();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handle(Request request, Response response) {
|
||||
if (map.isEmpty()) {
|
||||
response.status(500);
|
||||
} else {
|
||||
response.set("Content-Type", "application/json")
|
||||
.ctx()
|
||||
.result(createGachaMappingJson());
|
||||
}
|
||||
}
|
||||
|
||||
private String createGachaMappingJson() {
|
||||
List<Integer> list;
|
||||
|
||||
final StringBuilder sb = new StringBuilder();
|
||||
list = new ArrayList<>(GameData.getAvatarDataMap().keySet());
|
||||
Collections.sort(list);
|
||||
|
||||
final String newLine = System.lineSeparator();
|
||||
|
||||
// if the user made choices for language, I assume it's okay to assign his/her selected language to "en-us"
|
||||
// since it's the fallback language and there will be no difference in the gacha record page.
|
||||
// The enduser can still modify the `gacha_mappings.js` directly to enable multilingual for the gacha record system.
|
||||
sb.append("{").append(newLine);
|
||||
|
||||
// Avatars
|
||||
boolean first = true;
|
||||
for (Integer id : list) {
|
||||
AvatarData data = GameData.getAvatarDataMap().get(id);
|
||||
int avatarID = data.getId();
|
||||
if (avatarID >= 11000000) { // skip test avatar
|
||||
continue;
|
||||
}
|
||||
if (first) { // skip adding comma for the first element
|
||||
first = false;
|
||||
} else {
|
||||
sb.append(",");
|
||||
}
|
||||
String color;
|
||||
switch (data.getQualityType()) {
|
||||
case "QUALITY_PURPLE":
|
||||
color = "purple";
|
||||
break;
|
||||
case "QUALITY_ORANGE":
|
||||
color = "yellow";
|
||||
break;
|
||||
case "QUALITY_BLUE":
|
||||
default:
|
||||
color = "blue";
|
||||
}
|
||||
// Got the magic number 4233146695 from manually search in the json file
|
||||
sb.append("\"")
|
||||
.append(avatarID % 1000 + 1000)
|
||||
.append("\" : [\"")
|
||||
.append(map.get(data.getNameTextMapHash()))
|
||||
.append("(")
|
||||
.append(map.get(4233146695L))
|
||||
.append(")\", \"")
|
||||
.append(color)
|
||||
.append("\"]")
|
||||
.append(newLine);
|
||||
}
|
||||
|
||||
list = new ArrayList<>(GameData.getItemDataMap().keySet());
|
||||
Collections.sort(list);
|
||||
|
||||
// Weapons
|
||||
for (Integer id : list) {
|
||||
ItemData data = GameData.getItemDataMap().get(id);
|
||||
if (data.getId() <= 11101 || data.getId() >= 20000) {
|
||||
continue; //skip non weapon items
|
||||
}
|
||||
String color;
|
||||
|
||||
switch (data.getRankLevel()) {
|
||||
case 3:
|
||||
color = "blue";
|
||||
break;
|
||||
case 4:
|
||||
color = "purple";
|
||||
break;
|
||||
case 5:
|
||||
color = "yellow";
|
||||
break;
|
||||
default:
|
||||
continue; // skip unnecessary entries
|
||||
}
|
||||
|
||||
// Got the magic number 4231343903 from manually search in the json file
|
||||
|
||||
sb.append(",\"")
|
||||
.append(data.getId())
|
||||
.append("\" : [\"")
|
||||
.append(map.get(data.getNameTextMapHash()).replaceAll("\"", ""))
|
||||
.append("(")
|
||||
.append(map.get(4231343903L))
|
||||
.append(")\",\"")
|
||||
.append(color)
|
||||
.append("\"]")
|
||||
.append(newLine);
|
||||
}
|
||||
sb.append(",\"200\": \"")
|
||||
.append(map.get(332935371L))
|
||||
.append("\", \"301\": \"")
|
||||
.append(map.get(2272170627L))
|
||||
.append("\", \"302\": \"")
|
||||
.append(map.get(2864268523L))
|
||||
.append("\"")
|
||||
.append("}\n}")
|
||||
.append(newLine);
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
@ -0,0 +1,127 @@
|
||||
package emu.grasscutter.server.http.documentation;
|
||||
|
||||
import static emu.grasscutter.Configuration.DATA;
|
||||
import static emu.grasscutter.Configuration.RESOURCE;
|
||||
import static emu.grasscutter.utils.Language.translate;
|
||||
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
import emu.grasscutter.Grasscutter;
|
||||
import emu.grasscutter.command.CommandMap;
|
||||
import emu.grasscutter.data.GameData;
|
||||
import emu.grasscutter.data.ResourceLoader;
|
||||
import emu.grasscutter.data.def.AvatarData;
|
||||
import emu.grasscutter.data.def.ItemData;
|
||||
import emu.grasscutter.data.def.MonsterData;
|
||||
import emu.grasscutter.data.def.SceneData;
|
||||
import emu.grasscutter.tools.Tools;
|
||||
import emu.grasscutter.utils.FileUtils;
|
||||
import emu.grasscutter.utils.Utils;
|
||||
import express.http.Request;
|
||||
import express.http.Response;
|
||||
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
final class HandbookRequestHandler implements DocumentationHandler {
|
||||
|
||||
private final String template;
|
||||
private Map<Long, String> map;
|
||||
|
||||
|
||||
public HandbookRequestHandler() {
|
||||
ResourceLoader.loadResources();
|
||||
final File templateFile = new File(Utils.toFilePath(DATA("documentation/handbook.html")));
|
||||
if (templateFile.exists()) {
|
||||
template = new String(FileUtils.read(templateFile), StandardCharsets.UTF_8);
|
||||
} else {
|
||||
Grasscutter.getLogger().warn("File does not exist: " + templateFile);
|
||||
template = null;
|
||||
}
|
||||
|
||||
final String textMapFile = "TextMap/TextMap" + Tools.getLanguageOption() + ".json";
|
||||
try (InputStreamReader fileReader = new InputStreamReader(new FileInputStream(
|
||||
Utils.toFilePath(RESOURCE(textMapFile))), StandardCharsets.UTF_8)) {
|
||||
map = Grasscutter.getGsonFactory()
|
||||
.fromJson(fileReader, new TypeToken<Map<Long, String>>() {
|
||||
}.getType());
|
||||
} catch (IOException e) {
|
||||
Grasscutter.getLogger().warn("Resource does not exist: " + textMapFile);
|
||||
map = new HashMap<>();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handle(Request request, Response response) {
|
||||
if (template == null) {
|
||||
response.status(500);
|
||||
return;
|
||||
}
|
||||
|
||||
final CommandMap cmdMap = new CommandMap(true);
|
||||
final Int2ObjectMap<AvatarData> avatarMap = GameData.getAvatarDataMap();
|
||||
final Int2ObjectMap<ItemData> itemMap = GameData.getItemDataMap();
|
||||
final Int2ObjectMap<SceneData> sceneMap = GameData.getSceneDataMap();
|
||||
final Int2ObjectMap<MonsterData> monsterMap = GameData.getMonsterDataMap();
|
||||
|
||||
// Add translated title etc. to the page.
|
||||
String content = template.replace("{{TITLE}}", translate("documentation.handbook.title"))
|
||||
.replace("{{TITLE_COMMANDS}}", translate("documentation.handbook.title_commands"))
|
||||
.replace("{{TITLE_AVATARS}}", translate("documentation.handbook.title_avatars"))
|
||||
.replace("{{TITLE_ITEMS}}", translate("documentation.handbook.title_items"))
|
||||
.replace("{{TITLE_SCENES}}", translate("documentation.handbook.title_scenes"))
|
||||
.replace("{{TITLE_MONSTERS}}", translate("documentation.handbook.title_monsters"))
|
||||
.replace("{{HEADER_ID}}", translate("documentation.handbook.header_id"))
|
||||
.replace("{{HEADER_COMMAND}}", translate("documentation.handbook.header_command"))
|
||||
.replace("{{HEADER_DESCRIPTION}}",
|
||||
translate("documentation.handbook.header_description"))
|
||||
.replace("{{HEADER_AVATAR}}", translate("documentation.handbook.header_avatar"))
|
||||
.replace("{{HEADER_ITEM}}", translate("documentation.handbook.header_item"))
|
||||
.replace("{{HEADER_SCENE}}", translate("documentation.handbook.header_scene"))
|
||||
.replace("{{HEADER_MONSTER}}", translate("documentation.handbook.header_monster"))
|
||||
// Commands table
|
||||
.replace("{{COMMANDS_TABLE}}", cmdMap.getAnnotationsAsList()
|
||||
.stream()
|
||||
.map(cmd -> "<tr><td><code>" + cmd.label() + "</code></td><td>" +
|
||||
cmd.description() + "</td></tr>")
|
||||
.collect(Collectors.joining("\n")))
|
||||
// Avatars table
|
||||
.replace("{{AVATARS_TABLE}}", GameData.getAvatarDataMap().keySet()
|
||||
.intStream()
|
||||
.sorted()
|
||||
.mapToObj(avatarMap::get)
|
||||
.map(data -> "<tr><td><code>" + data.getId() + "</code></td><td>" +
|
||||
map.get(data.getNameTextMapHash()) + "</td></tr>")
|
||||
.collect(Collectors.joining("\n")))
|
||||
// Items table
|
||||
.replace("{{ITEMS_TABLE}}", GameData.getItemDataMap().keySet()
|
||||
.intStream()
|
||||
.sorted()
|
||||
.mapToObj(itemMap::get)
|
||||
.map(data -> "<tr><td><code>" + data.getId() + "</code></td><td>" +
|
||||
map.get(data.getNameTextMapHash()) + "</td></tr>")
|
||||
.collect(Collectors.joining("\n")))
|
||||
// Scenes table
|
||||
.replace("{{SCENES_TABLE}}", GameData.getSceneDataMap().keySet()
|
||||
.intStream()
|
||||
.sorted()
|
||||
.mapToObj(sceneMap::get)
|
||||
.map(data -> "<tr><td><code>" + data.getId() + "</code></td><td>" +
|
||||
data.getScriptData() + "</td></tr>")
|
||||
.collect(Collectors.joining("\n")))
|
||||
.replace("{{MONSTERS_TABLE}}", GameData.getMonsterDataMap().keySet()
|
||||
.intStream()
|
||||
.sorted()
|
||||
.mapToObj(monsterMap::get)
|
||||
.map(data -> "<tr><td><code>" + data.getId() + "</code></td><td>" +
|
||||
map.get(data.getNameTextMapHash()) + "</td></tr>")
|
||||
.collect(Collectors.joining("\n")));
|
||||
|
||||
response.send(content);
|
||||
}
|
||||
}
|
@ -0,0 +1,42 @@
|
||||
package emu.grasscutter.server.http.documentation;
|
||||
|
||||
import static emu.grasscutter.Configuration.DATA;
|
||||
import static emu.grasscutter.utils.Language.translate;
|
||||
|
||||
import emu.grasscutter.Grasscutter;
|
||||
import emu.grasscutter.data.ResourceLoader;
|
||||
import emu.grasscutter.utils.FileUtils;
|
||||
import emu.grasscutter.utils.Utils;
|
||||
import express.http.Request;
|
||||
import express.http.Response;
|
||||
import java.io.File;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
final class RootRequestHandler implements DocumentationHandler {
|
||||
|
||||
private final String template;
|
||||
|
||||
public RootRequestHandler() {
|
||||
ResourceLoader.loadResources();
|
||||
final File templateFile = new File(Utils.toFilePath(DATA("documentation/index.html")));
|
||||
if (templateFile.exists()) {
|
||||
template = new String(FileUtils.read(templateFile), StandardCharsets.UTF_8);
|
||||
} else {
|
||||
Grasscutter.getLogger().warn("File does not exist: " + templateFile);
|
||||
template = null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handle(Request request, Response response) {
|
||||
if (template == null) {
|
||||
response.status(500);
|
||||
return;
|
||||
}
|
||||
|
||||
String content = template.replace("{{TITLE}}", translate("documentation.index.title"))
|
||||
.replace("{{ITEM_HANDBOOK}}", translate("documentation.index.handbook"))
|
||||
.replace("{{ITEM_GACHA_MAPPING}}", translate("documentation.index.gacha_mapping"));
|
||||
response.send(content);
|
||||
}
|
||||
}
|
@ -1,6 +1,7 @@
|
||||
package emu.grasscutter.server.http.handlers;
|
||||
|
||||
import emu.grasscutter.Grasscutter;
|
||||
import emu.grasscutter.data.DataLoader;
|
||||
import emu.grasscutter.server.http.objects.HttpJsonResponse;
|
||||
import emu.grasscutter.server.http.Router;
|
||||
import emu.grasscutter.utils.FileUtils;
|
||||
@ -14,6 +15,7 @@ import io.javalin.Javalin;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Objects;
|
||||
|
||||
@ -41,9 +43,21 @@ public final class AnnouncementsHandler implements Router {
|
||||
private static void getAnnouncement(Request request, Response response) {
|
||||
String data = "";
|
||||
if (Objects.equals(request.baseUrl(), "/common/hk4e_global/announcement/api/getAnnContent")) {
|
||||
data = readToString(new File(Utils.toFilePath(DATA("GameAnnouncement.json"))));
|
||||
try {
|
||||
data = FileUtils.readToString(DataLoader.load("GameAnnouncement.json"));
|
||||
} catch (Exception e) {
|
||||
if(e.getClass() == IOException.class) {
|
||||
Grasscutter.getLogger().info("Unable to read file 'GameAnnouncementList.json'. \n" + e);
|
||||
}
|
||||
}
|
||||
} else if (Objects.equals(request.baseUrl(), "/common/hk4e_global/announcement/api/getAnnList")) {
|
||||
data = readToString(new File(Utils.toFilePath(DATA("GameAnnouncementList.json"))));
|
||||
try {
|
||||
data = FileUtils.readToString(DataLoader.load("GameAnnouncementList.json"));
|
||||
} catch (Exception e) {
|
||||
if(e.getClass() == IOException.class) {
|
||||
Grasscutter.getLogger().info("Unable to read file 'GameAnnouncementList.json'. \n" + e);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
response.send("{\"retcode\":404,\"message\":\"Unknown request path\"}");
|
||||
}
|
||||
@ -64,29 +78,15 @@ public final class AnnouncementsHandler implements Router {
|
||||
}
|
||||
|
||||
private static void getPageResources(Request request, Response response) {
|
||||
String filename = Utils.toFilePath(DATA(request.path()));
|
||||
File file = new File(filename);
|
||||
if (file.exists() && file.isFile()) {
|
||||
MediaType fromExtension = MediaType.getByExtension(filename.substring(filename.lastIndexOf(".") + 1));
|
||||
try(InputStream filestream = DataLoader.load(request.path())) {
|
||||
String possibleFilename = Utils.toFilePath(DATA(request.path()));
|
||||
|
||||
MediaType fromExtension = MediaType.getByExtension(possibleFilename.substring(possibleFilename.lastIndexOf(".") + 1));
|
||||
response.type((fromExtension != null) ? fromExtension.getMIME() : "application/octet-stream");
|
||||
response.send(FileUtils.read(file));
|
||||
} else {
|
||||
Grasscutter.getLogger().warn("File does not exist: " + file);
|
||||
response.send(filestream.readAllBytes());
|
||||
} catch (Exception e) {
|
||||
Grasscutter.getLogger().warn("File does not exist: " + request.path());
|
||||
response.status(404);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("ResultOfMethodCallIgnored")
|
||||
private static String readToString(File file) {
|
||||
byte[] content = new byte[(int) file.length()];
|
||||
|
||||
try {
|
||||
FileInputStream in = new FileInputStream(file);
|
||||
in.read(content); in.close();
|
||||
} catch (IOException ignored) {
|
||||
Grasscutter.getLogger().warn("File does not exist: " + file);
|
||||
}
|
||||
|
||||
return new String(content, StandardCharsets.UTF_8);
|
||||
}
|
||||
}
|
||||
|
@ -29,18 +29,7 @@ import static emu.grasscutter.utils.Language.translate;
|
||||
* Handles all gacha-related HTTP requests.
|
||||
*/
|
||||
public final class GachaHandler implements Router {
|
||||
private final String gachaMappings;
|
||||
|
||||
public GachaHandler() {
|
||||
this.gachaMappings = Utils.toFilePath(DATA("/gacha/mappings.js"));
|
||||
if(!(new File(this.gachaMappings).exists())) {
|
||||
try {
|
||||
Tools.createGachaMapping(this.gachaMappings);
|
||||
} catch (Exception exception) {
|
||||
Grasscutter.getLogger().warn("Failed to create gacha mappings.", exception);
|
||||
}
|
||||
}
|
||||
}
|
||||
public static final String gachaMappings = DATA(Utils.toFilePath("gacha/mappings.js"));
|
||||
|
||||
@Override public void applyRoutes(Express express, Javalin handle) {
|
||||
express.get("/gacha", GachaHandler::gachaRecords);
|
||||
|
@ -41,9 +41,7 @@ public final class GenericHandler implements Router {
|
||||
express.all("/perf/config/verify", new HttpJsonResponse("{\"code\":0}"));
|
||||
|
||||
// webstatic-sea.hoyoverse.com
|
||||
express.get("/admin/mi18n/plat_oversea/m202003048/m202003048-version.json", new HttpJsonResponse("{\"version\":51}"));
|
||||
express.get("/admin/mi18n/plat_oversea/m2020030410/m2020030410-version.json", new HttpJsonResponse("{\"version\":51}"));
|
||||
express.get("/admin/mi18n/plat_oversea/m2020030410/m2020030410-zh-cn.json", new HttpJsonResponse("{\"version\":51}"));
|
||||
express.get("/admin/mi18n/plat_oversea/*", new HttpJsonResponse("{\"version\":51}"));
|
||||
|
||||
express.get("/status/server", GenericHandler::serverStatus);
|
||||
}
|
||||
|
@ -84,7 +84,6 @@ public class ConfigContainer {
|
||||
public String resources = "./resources/";
|
||||
public String data = "./data/";
|
||||
public String packets = "./packets/";
|
||||
public String keys = "./keys/";
|
||||
public String scripts = "./resources/scripts/";
|
||||
public String plugins = "./plugins/";
|
||||
|
||||
|
@ -20,11 +20,11 @@ public final class Crypto {
|
||||
public static byte[] ENCRYPT_SEED_BUFFER = new byte[0];
|
||||
|
||||
public static void loadKeys() {
|
||||
DISPATCH_KEY = FileUtils.read(KEY("dispatchKey.bin"));
|
||||
DISPATCH_SEED = FileUtils.read(KEY("dispatchSeed.bin"));
|
||||
DISPATCH_KEY = FileUtils.readResource("/keys/dispatchKey.bin");
|
||||
DISPATCH_SEED = FileUtils.readResource("/keys/dispatchSeed.bin");
|
||||
|
||||
ENCRYPT_KEY = FileUtils.read(KEY("secretKey.bin"));
|
||||
ENCRYPT_SEED_BUFFER = FileUtils.read(KEY("secretKeyBuffer.bin"));
|
||||
ENCRYPT_KEY = FileUtils.readResource("/keys/secretKey.bin");
|
||||
ENCRYPT_SEED_BUFFER = FileUtils.readResource("/keys/secretKeyBuffer.bin");
|
||||
}
|
||||
|
||||
public static void xor(byte[] packet, byte[] key) {
|
||||
@ -37,25 +37,6 @@ public final class Crypto {
|
||||
}
|
||||
}
|
||||
|
||||
public static void extractSecretKeyBuffer(byte[] data) {
|
||||
try {
|
||||
GetPlayerTokenRsp p = GetPlayerTokenRsp.parseFrom(data);
|
||||
FileUtils.write(KEY("/secretKeyBuffer.bin"), p.getSecretKeyBytes().toByteArray());
|
||||
Grasscutter.getLogger().info("Secret Key: " + p.getSecretKey());
|
||||
} catch (Exception e) {
|
||||
Grasscutter.getLogger().error("Crypto error.", e);
|
||||
}
|
||||
}
|
||||
|
||||
public static void extractDispatchSeed(String data) {
|
||||
try {
|
||||
QueryCurrRegionHttpRsp p = QueryCurrRegionHttpRsp.parseFrom(Base64.getDecoder().decode(data));
|
||||
FileUtils.write(KEY("/dispatchSeed.bin"), p.getRegionInfo().getSecretKey().toByteArray());
|
||||
} catch (Exception e) {
|
||||
Grasscutter.getLogger().error("Crypto error.", e);
|
||||
}
|
||||
}
|
||||
|
||||
public static byte[] createSessionKey(int length) {
|
||||
byte[] bytes = new byte[length];
|
||||
secureRandom.nextBytes(bytes);
|
||||
|
@ -4,9 +4,14 @@ import emu.grasscutter.Grasscutter;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.io.InputStream;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.*;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public final class FileUtils {
|
||||
public static void write(String dest, byte[] bytes) {
|
||||
@ -32,10 +37,34 @@ public final class FileUtils {
|
||||
|
||||
return new byte[0];
|
||||
}
|
||||
|
||||
public static InputStream readResourceAsStream(String resourcePath) {
|
||||
return Grasscutter.class.getResourceAsStream(resourcePath);
|
||||
}
|
||||
|
||||
public static byte[] readResource(String resourcePath) {
|
||||
try (InputStream is = Grasscutter.class.getResourceAsStream(resourcePath)) {
|
||||
return is.readAllBytes();
|
||||
} catch (Exception exception) {
|
||||
Grasscutter.getLogger().warn("Failed to read resource: " + resourcePath);
|
||||
exception.printStackTrace();
|
||||
}
|
||||
|
||||
return new byte[0];
|
||||
}
|
||||
|
||||
public static byte[] read(File file) {
|
||||
return read(file.getPath());
|
||||
}
|
||||
|
||||
public static void copyResource(String resourcePath, String destination) {
|
||||
try {
|
||||
byte[] resource = FileUtils.readResource(resourcePath);
|
||||
FileUtils.write(destination, resource);
|
||||
} catch (Exception exception) {
|
||||
Grasscutter.getLogger().warn("Failed to copy resource: " + resourcePath + "\n" + exception);
|
||||
}
|
||||
}
|
||||
|
||||
public static String getFilenameWithoutPath(String fileName) {
|
||||
if (fileName.indexOf(".") > 0) {
|
||||
@ -44,4 +73,33 @@ public final class FileUtils {
|
||||
return fileName;
|
||||
}
|
||||
}
|
||||
|
||||
// From https://mkyong.com/java/java-read-a-file-from-resources-folder/
|
||||
public static List<Path> getPathsFromResource(String folder) throws URISyntaxException, IOException {
|
||||
List<Path> result;
|
||||
|
||||
// get path of the current running JAR
|
||||
String jarPath = Grasscutter.class.getProtectionDomain()
|
||||
.getCodeSource()
|
||||
.getLocation()
|
||||
.toURI()
|
||||
.getPath();
|
||||
|
||||
// file walks JAR
|
||||
URI uri = URI.create("jar:file:" + jarPath);
|
||||
try (FileSystem fs = FileSystems.newFileSystem(uri, Collections.emptyMap())) {
|
||||
result = Files.walk(fs.getPath(folder))
|
||||
.filter(Files::isRegularFile)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@SuppressWarnings("ResultOfMethodCallIgnored")
|
||||
public static String readToString(InputStream file) throws IOException {
|
||||
byte[] content = file.readAllBytes();
|
||||
|
||||
return new String(content, StandardCharsets.UTF_8);
|
||||
}
|
||||
}
|
||||
|
@ -9,6 +9,7 @@ import java.time.temporal.TemporalAdjusters;
|
||||
import java.util.*;
|
||||
|
||||
import emu.grasscutter.Grasscutter;
|
||||
import emu.grasscutter.data.DataLoader;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import io.netty.buffer.ByteBufUtil;
|
||||
import io.netty.buffer.Unpooled;
|
||||
@ -198,6 +199,9 @@ public final class Utils {
|
||||
if(!fileExists(dataFolder))
|
||||
createFolder(dataFolder);
|
||||
|
||||
// Make sure the data folder is populated, if there are any missing files copy them from resources
|
||||
DataLoader.CheckAllFiles();
|
||||
|
||||
if(exit) System.exit(1);
|
||||
}
|
||||
|
||||
|
@ -1,396 +1,412 @@
|
||||
{
|
||||
"messages": {
|
||||
"game": {
|
||||
"port_bind": "Game Server started on port %s",
|
||||
"connect": "Client connected from %s",
|
||||
"disconnect": "Client disconnected from %s",
|
||||
"game_update_error": "An error occurred during game update.",
|
||||
"command_error": "Command error:"
|
||||
},
|
||||
"dispatch": {
|
||||
"port_bind": "[Dispatch] Dispatch server started on port %s",
|
||||
"request": "[Dispatch] Client %s %s request: %s",
|
||||
"keystore": {
|
||||
"general_error": "[Dispatch] Error while loading keystore!",
|
||||
"password_error": "[Dispatch] Unable to load keystore. Trying default keystore password...",
|
||||
"no_keystore_error": "[Dispatch] No SSL cert found! Falling back to HTTP server.",
|
||||
"default_password": "[Dispatch] The default keystore password was loaded successfully. Please consider setting the password to 123456 in config.json."
|
||||
},
|
||||
"authentication": {
|
||||
"default_unable_to_verify": "[Authentication] Something called the verifyUser method which is unavailable in the default authentication handler"
|
||||
},
|
||||
"no_commands_error": "Commands are not supported in dispatch only mode.",
|
||||
"unhandled_request_error": "[Dispatch] Potential unhandled %s request: %s",
|
||||
"account": {
|
||||
"login_attempt": "[Dispatch] Client %s is trying to log in",
|
||||
"login_success": "[Dispatch] Client %s logged in as %s",
|
||||
"login_token_attempt": "[Dispatch] Client %s is trying to log in via token",
|
||||
"login_token_error": "[Dispatch] Client %s failed to log in via token",
|
||||
"login_token_success": "[Dispatch] Client %s logged in via token as %s",
|
||||
"combo_token_success": "[Dispatch] Client %s succeed to exchange combo token",
|
||||
"combo_token_error": "[Dispatch] Client %s failed to exchange combo token",
|
||||
"account_login_create_success": "[Dispatch] Client %s failed to log in: Account %s created",
|
||||
"account_login_create_error": "[Dispatch] Client %s failed to log in: Account create failed",
|
||||
"account_login_exist_error": "[Dispatch] Client %s failed to log in: Account no found",
|
||||
"account_cache_error": "Game account cache information error",
|
||||
"session_key_error": "Wrong session key.",
|
||||
"username_error": "Username not found.",
|
||||
"username_create_error": "Username not found, create failed."
|
||||
},
|
||||
"router_error": "[Dispatch] Unable to attach router."
|
||||
},
|
||||
"status": {
|
||||
"free_software": "Grasscutter is FREE software. If you have paid for this, you may have been scammed. Homepage: https://github.com/Grasscutters/Grasscutter",
|
||||
"starting": "Starting Grasscutter...",
|
||||
"shutdown": "Shutting down...",
|
||||
"done": "Done! For help, type \"help\"",
|
||||
"error": "An error occurred.",
|
||||
"welcome": "Welcome to Grasscutter",
|
||||
"run_mode_error": "Invalid server run mode: %s.",
|
||||
"run_mode_help": "Server run mode must be 'HYBRID', 'DISPATCH_ONLY', or 'GAME_ONLY'. Unable to start Grasscutter...",
|
||||
"create_resources": "Creating resources folder...",
|
||||
"resources_error": "Place a copy of 'BinOutput' and 'ExcelBinOutput' in the resources folder.",
|
||||
"version": "Grasscutter version: %s-%s"
|
||||
}
|
||||
},
|
||||
"commands": {
|
||||
"generic": {
|
||||
"not_specified": "No command specified.",
|
||||
"unknown_command": "Unknown command: %s",
|
||||
"permission_error": "You do not have permission to run this command.",
|
||||
"console_execute_error": "This command can only be run from the console.",
|
||||
"player_execute_error": "Run this command in-game.",
|
||||
"command_exist_error": "No command found.",
|
||||
"no_description_specified": "No description specified",
|
||||
"invalid": {
|
||||
"amount": "Invalid amount.",
|
||||
"artifactId": "Invalid artifactId.",
|
||||
"avatarId": "Invalid avatarId.",
|
||||
"avatarLevel": "Invalid avatarLevel.",
|
||||
"entityId": "Invalid entityId.",
|
||||
"itemId": "Invalid itemId.",
|
||||
"itemLevel": "Invalid itemLevel.",
|
||||
"itemRefinement": "Invalid itemRefinement.",
|
||||
"playerId": "Invalid playerId.",
|
||||
"uid": "Invalid UID."
|
||||
}
|
||||
},
|
||||
"execution": {
|
||||
"uid_error": "Invalid UID.",
|
||||
"player_exist_error": "Player not found.",
|
||||
"player_offline_error": "Player is not online.",
|
||||
"item_id_error": "Invalid item ID.",
|
||||
"item_player_exist_error": "Invalid item or UID.",
|
||||
"entity_id_error": "Invalid entity ID.",
|
||||
"player_exist_offline_error": "Player not found or is not online.",
|
||||
"argument_error": "Invalid arguments.",
|
||||
"clear_target": "Target cleared.",
|
||||
"set_target": "Subsequent commands will target @%s by default.",
|
||||
"need_target": "This command requires a target UID. Add a <@UID> argument or set a persistent target with /target @UID."
|
||||
},
|
||||
"status": {
|
||||
"enabled": "Enabled",
|
||||
"disabled": "Disabled",
|
||||
"help": "Help",
|
||||
"success": "Success"
|
||||
},
|
||||
"account": {
|
||||
"modify": "Modify user accounts",
|
||||
"invalid": "Invalid UID.",
|
||||
"exists": "Account already exists.",
|
||||
"create": "Account created with UID %s.",
|
||||
"delete": "Account deleted.",
|
||||
"no_account": "Account not found.",
|
||||
"command_usage": "Usage: account <create|delete> <username> [uid]",
|
||||
"description": "Modify user accounts"
|
||||
},
|
||||
"broadcast": {
|
||||
"command_usage": "Usage: broadcast <message>",
|
||||
"message_sent": "Message sent.",
|
||||
"description": "Sends a message to all the players"
|
||||
},
|
||||
"changescene": {
|
||||
"usage": "Usage: changescene <sceneId>",
|
||||
"already_in_scene": "You are already in that scene.",
|
||||
"success": "Changed to scene %s.",
|
||||
"exists_error": "The specified scene does not exist.",
|
||||
"description": "Changes your scene"
|
||||
},
|
||||
"clear": {
|
||||
"command_usage": "Usage: clear <all|wp|art|mat>",
|
||||
"weapons": "Cleared weapons for %s.",
|
||||
"artifacts": "Cleared artifacts for %s.",
|
||||
"materials": "Cleared materials for %s.",
|
||||
"furniture": "Cleared furniture for %s.",
|
||||
"displays": "Cleared displays for %s.",
|
||||
"virtuals": "Cleared virtuals for %s.",
|
||||
"everything": "Cleared everything for %s.",
|
||||
"description": "Deletes unequipped unlocked items, including yellow rarity ones from your inventory"
|
||||
},
|
||||
"coop": {
|
||||
"usage": "Usage: coop <playerId> <target playerId>",
|
||||
"success": "Summoned %s to %s's world.",
|
||||
"description": "Forces someone to join the world of others"
|
||||
},
|
||||
"enter_dungeon": {
|
||||
"usage": "Usage: enterdungeon <dungeon id>",
|
||||
"changed": "Changed to dungeon %s",
|
||||
"not_found_error": "Dungeon does not exist",
|
||||
"in_dungeon_error": "You are already in that dungeon",
|
||||
"description": "Enter a dungeon"
|
||||
},
|
||||
"giveAll": {
|
||||
"usage": "Usage: giveall [player] [amount]",
|
||||
"started": "Receiving all items...",
|
||||
"success": "Successfully gave all items to %s.",
|
||||
"invalid_amount_or_playerId": "Invalid amount or player ID.",
|
||||
"description": "Gives all items"
|
||||
},
|
||||
"giveArtifact": {
|
||||
"usage": "Usage: giveart|gart [player] <artifactId> <mainPropId> [<appendPropId>[,<times>]]... [level]",
|
||||
"id_error": "Invalid artifact ID.",
|
||||
"success": "Given %s to %s.",
|
||||
"description": "Gives the player a specified artifact"
|
||||
},
|
||||
"giveChar": {
|
||||
"usage": "Usage: givechar <player> <itemId|itemName> [amount]",
|
||||
"given": "Given %s with level %s to %s.",
|
||||
"invalid_avatar_id": "Invalid avatar id.",
|
||||
"invalid_avatar_level": "Invalid avatar level.",
|
||||
"invalid_avatar_or_player_id": "Invalid avatar or player ID.",
|
||||
"description": "Gives the player a specified character"
|
||||
},
|
||||
"give": {
|
||||
"usage": "Usage: give <player> <itemId|itemName> [amount] [level]",
|
||||
"refinement_only_applicable_weapons": "Refinement is only applicable to weapons.",
|
||||
"refinement_must_between_1_and_5": "Refinement must be between 1 and 5.",
|
||||
"given": "Given %s of %s to %s.",
|
||||
"given_with_level_and_refinement": "Given %s with level %s, refinement %s %s times to %s",
|
||||
"given_level": "Given %s with level %s %s times to %s",
|
||||
"description": "Gives an item to you or the specified player"
|
||||
},
|
||||
"godmode": {
|
||||
"success": "Godmode is now %s for %s.",
|
||||
"description": "Prevents you from taking damage. Defaults to toggle."
|
||||
},
|
||||
"nostamina": {
|
||||
"success": "NoStamina is now %s for %s.",
|
||||
"description": "Keep your endurance to the maximum."
|
||||
},
|
||||
"heal": {
|
||||
"success": "All characters have been healed.",
|
||||
"description": "Heal all characters in your current team."
|
||||
},
|
||||
"kick": {
|
||||
"player_kick_player": "Player [%s:%s] has kicked player [%s:%s]",
|
||||
"server_kick_player": "Kicking player [%s:%s]",
|
||||
"description": "Kicks the specified player from the server (WIP)"
|
||||
},
|
||||
"kill": {
|
||||
"usage": "Usage: killall [playerUid] [sceneId]",
|
||||
"scene_not_found_in_player_world": "Scene not found in player world",
|
||||
"kill_monsters_in_scene": "Killing %s monsters in scene %s",
|
||||
"description": "Kill all entities"
|
||||
},
|
||||
"killCharacter": {
|
||||
"usage": "Usage: /killcharacter [playerId]",
|
||||
"success": "Killed %s's current character.",
|
||||
"description": "Kills the players current character"
|
||||
},
|
||||
"language": {
|
||||
"current_language": "current language is %s",
|
||||
"language_changed": "language changed to %s",
|
||||
"language_not_found": "currently, server does not have that language: %s",
|
||||
"description": "display or change current language"
|
||||
},
|
||||
"list": {
|
||||
"success": "There are %s player(s) online:",
|
||||
"description": "List online players"
|
||||
},
|
||||
"permission": {
|
||||
"usage": "Usage: permission <add|remove> <username> <permission>",
|
||||
"add": "Permission added.",
|
||||
"has_error": "They already have this permission!",
|
||||
"remove": "Permission removed.",
|
||||
"not_have_error": "They don't have this permission!",
|
||||
"account_error": "The account cannot be found.",
|
||||
"description": "Grants or removes a permission for a user"
|
||||
},
|
||||
"position": {
|
||||
"success": "Coordinates: %s, %s, %s\nScene id: %s",
|
||||
"description": "Get coordinates."
|
||||
},
|
||||
"quest": {
|
||||
"description": "Add or finish quests",
|
||||
"usage": "quest <add|finish> [quest id]",
|
||||
"added": "Quest %s added",
|
||||
"finished": "Finished quest %s",
|
||||
"not_found": "Quest not found",
|
||||
"invalid_id": "Invalid quest id"
|
||||
},
|
||||
"reload": {
|
||||
"reload_start": "Reloading config.",
|
||||
"reload_done": "Reload complete.",
|
||||
"description": "Reload server config"
|
||||
},
|
||||
"resetConst": {
|
||||
"reset_all": "Reset all avatars' constellations.",
|
||||
"success": "Constellations for %s have been reset. Please relog to see changes.",
|
||||
"description": "Resets the constellation level on your current active character, will need to relog after using the command to see any changes."
|
||||
},
|
||||
"resetShopLimit": {
|
||||
"usage": "Usage: /resetshop <player id>",
|
||||
"description": "Reset target player's shop refresh time."
|
||||
},
|
||||
"sendMail": {
|
||||
"usage": "Usage: give [player] <itemId|itemName> [amount]",
|
||||
"user_not_exist": "The user with an id of '%s' does not exist",
|
||||
"start_composition": "Starting composition of message.\nPlease use `/sendmail <title>` to continue.\nYou can use `/sendmail stop` at any time",
|
||||
"templates": "Mail templates coming soon implemented...",
|
||||
"invalid_arguments": "Invalid arguments.\nUsage `/sendmail <userId|all|help> [templateId]`",
|
||||
"send_cancel": "Message sending cancelled",
|
||||
"send_done": "Message sent to user %s!",
|
||||
"send_all_done": "Message sent to all users!",
|
||||
"not_composition_end": "Message composition not at final stage.\nPlease use `/sendmail %s` or `/sendmail stop` to cancel",
|
||||
"please_use": "Please use `/sendmail %s`",
|
||||
"set_title": "Message title set as '%s'.\nUse '/sendmail <content>' to continue.",
|
||||
"set_contents": "Message contents set as '%s'.\nUse '/sendmail <sender>' to continue.",
|
||||
"set_message_sender": "Message sender set as '%s'.\nUse '/sendmail <itemId|itemName|finish> [amount] [level]' to continue.",
|
||||
"send": "Attached %s of %s (level %s) to the message.\nContinue adding more items or use `/sendmail finish` to send the message.",
|
||||
"invalid_arguments_please_use": "Invalid arguments \n Please use `/sendmail %s`",
|
||||
"title": "<title>",
|
||||
"message": "<message>",
|
||||
"sender": "<sender>",
|
||||
"arguments": "<itemId|itemName|finish> [amount] [level]",
|
||||
"error": "ERROR: invalid construction stage %s. Check console for stacktrace.",
|
||||
"description": "Sends mail to the specified user. The usage of this command changes based on it's composition state."
|
||||
},
|
||||
"sendMessage": {
|
||||
"usage": "Usage: sendmessage <player> <message>",
|
||||
"success": "Message sent.",
|
||||
"description": "Sends a message to a player as the server"
|
||||
},
|
||||
"setFetterLevel": {
|
||||
"usage": "Usage: setfetterlevel <level>",
|
||||
"range_error": "Fetter level must be between 0 and 10.",
|
||||
"success": "Fetter level set to %s",
|
||||
"level_error": "Invalid fetter level.",
|
||||
"description": "Sets your fetter level for your current active character"
|
||||
},
|
||||
"setStats": {
|
||||
"usage_console": "Usage: setstats|stats @<UID> <stat> <value>",
|
||||
"usage_ingame": "Usage: setstats|stats [@UID] <stat> <value>",
|
||||
"help_message": "\n\tValues for <stat>: hp | maxhp | def | atk | em | er | crate | cdmg | cdr | heal | heali | shield | defi\n\t(cont.) Elemental DMG Bonus: epyro | ecryo | ehydro | egeo | edendro | eelectro | ephys\n\t(cont.) Elemental RES: respyro | rescryo | reshydro | resgeo | resdendro | reselectro | resphys\n",
|
||||
"value_error": "Invalid stat value.",
|
||||
"uid_error": "Invalid UID.",
|
||||
"player_error": "Player not found or offline.",
|
||||
"set_self": "%s set to %s.",
|
||||
"set_for_uid": "%s for %s set to %s.",
|
||||
"set_max_hp": "MAX HP set to %s.",
|
||||
"description": "Set fight property for your current active character"
|
||||
},
|
||||
"setWorldLevel": {
|
||||
"usage": "Usage: setworldlevel <level>",
|
||||
"value_error": "World level must be between 0-8",
|
||||
"success": "World level set to %s.",
|
||||
"invalid_world_level": "Invalid world level.",
|
||||
"description": "Sets your world level (Relog to see proper effects)"
|
||||
},
|
||||
"spawn": {
|
||||
"usage": "Usage: spawn <entityId> [amount] [level(monster only)]",
|
||||
"success": "Spawned %s of %s.",
|
||||
"description": "Spawns an entity near you"
|
||||
},
|
||||
"stop": {
|
||||
"success": "Server shutting down...",
|
||||
"description": "Stops the server"
|
||||
},
|
||||
"talent": {
|
||||
"usage_1": "To set talent level: /talent set <talentID> <value>",
|
||||
"usage_2": "Another way to set talent level: /talent <n or e or q> <value>",
|
||||
"usage_3": "To get talent ID: /talent getid",
|
||||
"lower_16": "Invalid talent level. Level should be lower than 16",
|
||||
"set_id": "Set talent to %s.",
|
||||
"set_atk": "Set talent Normal ATK to %s.",
|
||||
"set_e": "Set talent E to %s.",
|
||||
"set_q": "Set talent Q to %s.",
|
||||
"invalid_skill_id": "Invalid skill ID.",
|
||||
"set_this": "Set this talent to %s.",
|
||||
"invalid_level": "Invalid talent level.",
|
||||
"normal_attack_id": "Normal Attack ID %s.",
|
||||
"e_skill_id": "E skill ID %s.",
|
||||
"q_skill_id": "Q skill ID %s.",
|
||||
"description": "Set talent level for your current active character"
|
||||
},
|
||||
"team": {
|
||||
"usage": "Usage: team <add|remove|set> [avatarId,...] [index|first|last|index-index,...]",
|
||||
"invalid_usage": "invalid usage",
|
||||
"add_usage": "usage(add): team add <avatarId,...> [index]",
|
||||
"invalid_index": "index is invalid",
|
||||
"add_too_much": "server is only allow you to have at most %d avatar(s) in your team",
|
||||
"failed_to_add_avatar": "failed to add avatar by id: %s",
|
||||
"remove_usage": "usage(remove): team remove <index|first|last|index-index,...>",
|
||||
"failed_parse_index": "failed to parse index: %s",
|
||||
"remove_too_much": "you can't remove so many avatars, your team list will be empty for this",
|
||||
"ignore_index": "ignored index(es): %s",
|
||||
"set_usage": "usage(set): team set <index> <avatarId>",
|
||||
"index_out_of_range": "index your specified is out of range",
|
||||
"failed_parse_avatar_id": "failed to parse avatar id: %s",
|
||||
"avatar_already_in_team": "avatar is already in team",
|
||||
"avatar_not_found": "avatar not found: %d",
|
||||
"description": "modify your team manually"
|
||||
},
|
||||
"teleportAll": {
|
||||
"success": "Summoned all players to your location.",
|
||||
"error": "You only can use this command in MP mode.",
|
||||
"description": "Teleports all players in your world to your position"
|
||||
},
|
||||
"teleport": {
|
||||
"usage_server": "Usage: /tp @<player id> <x> <y> <z> [scene id]",
|
||||
"usage": "Usage: /tp [@<player id>] <x> <y> <z> [scene id]",
|
||||
"specify_player_id": "You must specify a player id.",
|
||||
"invalid_position": "Invalid position.",
|
||||
"success": "Teleported %s to %s, %s, %s in scene %s",
|
||||
"description": "Change the player's position."
|
||||
},
|
||||
"tower": {
|
||||
"unlock_done": "Abyss Corridor's Floors are all unlocked now."
|
||||
},
|
||||
"weather": {
|
||||
"usage": "Usage: weather <weatherId> [climateId]",
|
||||
"success": "Changed weather to %s with climate %s",
|
||||
"invalid_id": "Invalid ID.",
|
||||
"description": "Changes the weather."
|
||||
},
|
||||
"drop": {
|
||||
"command_usage": "Usage: drop <itemId|itemName> [amount]",
|
||||
"success": "Dropped %s of %s.",
|
||||
"description": "Drops an item near you"
|
||||
},
|
||||
"help": {
|
||||
"usage": "Usage: ",
|
||||
"aliases": "Aliases: ",
|
||||
"available_commands": "Available commands: ",
|
||||
"description": "Sends the help message or shows information about a specified command"
|
||||
},
|
||||
"restart": {
|
||||
"description": "Restarts the current session"
|
||||
},
|
||||
"unlocktower": {
|
||||
"success": "unlock done",
|
||||
"description": "Unlock all levels of tower"
|
||||
},
|
||||
"resetshop": {
|
||||
"description": "reset shop"
|
||||
}
|
||||
},
|
||||
"gacha": {
|
||||
"details": {
|
||||
"title": "Banner Details",
|
||||
"available_five_stars": "Available 5-star Items",
|
||||
"available_four_stars": "Available 4-star Items",
|
||||
"available_three_stars": "Available 3-star Items",
|
||||
"template_missing": "data/gacha_details.html is missing."
|
||||
}
|
||||
}
|
||||
}
|
||||
{
|
||||
"messages": {
|
||||
"game": {
|
||||
"port_bind": "Game Server started on port %s",
|
||||
"connect": "Client connected from %s",
|
||||
"disconnect": "Client disconnected from %s",
|
||||
"game_update_error": "An error occurred during game update.",
|
||||
"command_error": "Command error:"
|
||||
},
|
||||
"dispatch": {
|
||||
"port_bind": "[Dispatch] Dispatch server started on port %s",
|
||||
"request": "[Dispatch] Client %s %s request: %s",
|
||||
"keystore": {
|
||||
"general_error": "[Dispatch] Error while loading keystore!",
|
||||
"password_error": "[Dispatch] Unable to load keystore. Trying default keystore password...",
|
||||
"no_keystore_error": "[Dispatch] No SSL cert found! Falling back to HTTP server.",
|
||||
"default_password": "[Dispatch] The default keystore password was loaded successfully. Please consider setting the password to 123456 in config.json."
|
||||
},
|
||||
"authentication": {
|
||||
"default_unable_to_verify": "[Authentication] Something called the verifyUser method which is unavailable in the default authentication handler."
|
||||
},
|
||||
"no_commands_error": "Commands are not supported in dispatch only mode.",
|
||||
"unhandled_request_error": "[Dispatch] Potential unhandled %s request: %s.",
|
||||
"account": {
|
||||
"login_attempt": "[Dispatch] Client %s is trying to log in.",
|
||||
"login_success": "[Dispatch] Client %s logged in as %s.",
|
||||
"login_token_attempt": "[Dispatch] Client %s is trying to log in via token.",
|
||||
"login_token_error": "[Dispatch] Client %s failed to log in via token.",
|
||||
"login_token_success": "[Dispatch] Client %s logged in via token as %s.",
|
||||
"combo_token_success": "[Dispatch] Client %s succeed to exchange combo token.",
|
||||
"combo_token_error": "[Dispatch] Client %s failed to exchange combo token.",
|
||||
"account_login_create_success": "[Dispatch] Client %s failed to log in: Account %s created.",
|
||||
"account_login_create_error": "[Dispatch] Client %s failed to log in: Account create failed.",
|
||||
"account_login_exist_error": "[Dispatch] Client %s failed to log in: Account not found.",
|
||||
"account_cache_error": "Game account cache information error.",
|
||||
"session_key_error": "Wrong session key.",
|
||||
"username_error": "Username not found.",
|
||||
"username_create_error": "Username not found, create failed."
|
||||
},
|
||||
"router_error": "[Dispatch] Unable to attach router."
|
||||
},
|
||||
"status": {
|
||||
"free_software": "Grasscutter is FREE software. If you have paid for this, you may have been scammed. Homepage: https://github.com/Grasscutters/Grasscutter",
|
||||
"starting": "Starting Grasscutter...",
|
||||
"shutdown": "Shutting down...",
|
||||
"done": "Done! For help, type \"help\"",
|
||||
"error": "An error occurred.",
|
||||
"welcome": "Welcome to Grasscutter!",
|
||||
"run_mode_error": "Invalid server run mode: %s.",
|
||||
"run_mode_help": "Server run mode must be 'HYBRID', 'DISPATCH_ONLY', or 'GAME_ONLY'. Unable to start Grasscutter...",
|
||||
"create_resources": "Creating resources folder...",
|
||||
"resources_error": "Place a copy of 'BinOutput' and 'ExcelBinOutput' in the resources folder.",
|
||||
"version": "Grasscutter version: %s-%s"
|
||||
}
|
||||
},
|
||||
"commands": {
|
||||
"generic": {
|
||||
"not_specified": "No command specified.",
|
||||
"unknown_command": "Unknown command: %s",
|
||||
"permission_error": "You do not have permission to run this command.",
|
||||
"console_execute_error": "This command can only be run from the console.",
|
||||
"player_execute_error": "Run this command in-game.",
|
||||
"command_exist_error": "No command found.",
|
||||
"no_description_specified": "No description specified.",
|
||||
"invalid": {
|
||||
"amount": "Invalid amount.",
|
||||
"artifactId": "Invalid artifact ID.",
|
||||
"avatarId": "Invalid avatar ID.",
|
||||
"avatarLevel": "Invalid avatarLevel.",
|
||||
"entityId": "Invalid entity ID.",
|
||||
"itemId": "Invalid item ID.",
|
||||
"itemLevel": "Invalid itemLevel.",
|
||||
"itemRefinement": "Invalid itemRefinement.",
|
||||
"playerId": "Invalid player ID.",
|
||||
"uid": "Invalid UID."
|
||||
}
|
||||
},
|
||||
"execution": {
|
||||
"uid_error": "Invalid UID.",
|
||||
"player_exist_error": "Player not found.",
|
||||
"player_offline_error": "Player is not online.",
|
||||
"item_id_error": "Invalid item ID.",
|
||||
"item_player_exist_error": "Invalid item or UID.",
|
||||
"entity_id_error": "Invalid entity ID.",
|
||||
"player_exist_offline_error": "Player not found or is not online.",
|
||||
"argument_error": "Invalid arguments.",
|
||||
"clear_target": "Target cleared.",
|
||||
"set_target": "Subsequent commands will target @%s by default.",
|
||||
"need_target": "This command requires a target UID. Add a <@UID> argument or set a persistent target with /target @UID."
|
||||
},
|
||||
"status": {
|
||||
"enabled": "Enabled",
|
||||
"disabled": "Disabled",
|
||||
"help": "Help",
|
||||
"success": "Success"
|
||||
},
|
||||
"account": {
|
||||
"modify": "Modify user accounts",
|
||||
"invalid": "Invalid UID.",
|
||||
"exists": "Account already exists.",
|
||||
"create": "Account created with UID %s.",
|
||||
"delete": "Account deleted.",
|
||||
"no_account": "Account not found.",
|
||||
"command_usage": "Usage: account <create|delete> <username> [UID]",
|
||||
"description": "Modify user accounts"
|
||||
},
|
||||
"broadcast": {
|
||||
"command_usage": "Usage: broadcast <message>",
|
||||
"message_sent": "Message sent.",
|
||||
"description": "Sends a message to all the players"
|
||||
},
|
||||
"changescene": {
|
||||
"usage": "Usage: changescene <sceneID>",
|
||||
"already_in_scene": "You are already in that scene.",
|
||||
"success": "Changed to scene %s.",
|
||||
"exists_error": "The specified scene does not exist.",
|
||||
"description": "Changes your scene"
|
||||
},
|
||||
"clear": {
|
||||
"command_usage": "Usage: clear <all|wp|art|mat>",
|
||||
"weapons": "Cleared weapons for %s.",
|
||||
"artifacts": "Cleared artifacts for %s.",
|
||||
"materials": "Cleared materials for %s.",
|
||||
"furniture": "Cleared furniture for %s.",
|
||||
"displays": "Cleared displays for %s.",
|
||||
"virtuals": "Cleared virtuals for %s.",
|
||||
"everything": "Cleared everything for %s.",
|
||||
"description": "Deletes unequipped unlocked items, including yellow rarity ones from your inventory"
|
||||
},
|
||||
"coop": {
|
||||
"usage": "Usage: coop <playerID> <target playerID>",
|
||||
"success": "Summoned %s to %s's world.",
|
||||
"description": "Forces someone to join the world of others"
|
||||
},
|
||||
"enter_dungeon": {
|
||||
"usage": "Usage: enterdungeon <dungeon ID>",
|
||||
"changed": "Changed to dungeon %s.",
|
||||
"not_found_error": "Dungeon does not exist.",
|
||||
"in_dungeon_error": "You are already in that dungeon.",
|
||||
"description": "Enter a dungeon"
|
||||
},
|
||||
"giveAll": {
|
||||
"usage": "Usage: giveall [player] [amount]",
|
||||
"started": "Receiving all items...",
|
||||
"success": "Successfully gave all items to %s.",
|
||||
"invalid_amount_or_playerId": "Invalid amount or player ID.",
|
||||
"description": "Gives all items"
|
||||
},
|
||||
"giveArtifact": {
|
||||
"usage": "Usage: giveart|gart [player] <artifactID> <mainPropID> [<appendPropID>[,<times>]]... [level]",
|
||||
"id_error": "Invalid artifact ID.",
|
||||
"success": "Given %s to %s.",
|
||||
"description": "Gives the player a specified artifact"
|
||||
},
|
||||
"giveChar": {
|
||||
"usage": "Usage: givechar <player> <itemID|itemName> [amount]",
|
||||
"given": "Given %s with level %s to %s.",
|
||||
"invalid_avatar_id": "Invalid avatar ID.",
|
||||
"invalid_avatar_level": "Invalid avatar level.",
|
||||
"invalid_avatar_or_player_id": "Invalid avatar or player ID.",
|
||||
"description": "Gives the player a specified character"
|
||||
},
|
||||
"give": {
|
||||
"usage": "Usage: give <player> <itemID|itemName> [amount] [level]",
|
||||
"refinement_only_applicable_weapons": "Refinement is only applicable to weapons.",
|
||||
"refinement_must_between_1_and_5": "Refinement must be between 1 and 5.",
|
||||
"given": "Given %s of %s to %s.",
|
||||
"given_with_level_and_refinement": "Given %s with level %s, refinement %s %s times to %s.",
|
||||
"given_level": "Given %s with level %s %s times to %s.",
|
||||
"description": "Gives an item to you or the specified player"
|
||||
},
|
||||
"godmode": {
|
||||
"success": "Godmode is now %s for %s.",
|
||||
"description": "Prevents you from taking damage. Defaults to toggle."
|
||||
},
|
||||
"nostamina": {
|
||||
"success": "NoStamina is now %s for %s.",
|
||||
"description": "Keep your endurance to the maximum."
|
||||
},
|
||||
"heal": {
|
||||
"success": "All characters have been healed.",
|
||||
"description": "Heal all characters in your current team."
|
||||
},
|
||||
"kick": {
|
||||
"player_kick_player": "Player [%s:%s] has kicked player [%s:%s]",
|
||||
"server_kick_player": "Kicking player [%s:%s]...",
|
||||
"description": "Kicks the specified player from the server (WIP)"
|
||||
},
|
||||
"kill": {
|
||||
"usage": "Usage: killall [playerUID] [sceneID]",
|
||||
"scene_not_found_in_player_world": "Scene not found in player world.",
|
||||
"kill_monsters_in_scene": "Killing %s monsters in scene %s.",
|
||||
"description": "Kill all entities"
|
||||
},
|
||||
"killCharacter": {
|
||||
"usage": "Usage: /killcharacter [playerID]",
|
||||
"success": "Killed %s's current character.",
|
||||
"description": "Kills the players current character"
|
||||
},
|
||||
"language": {
|
||||
"current_language": "Current language is %s.",
|
||||
"language_changed": "Language changed to %s.",
|
||||
"language_not_found": "Currently, the server does not have that language.",
|
||||
"description": "Display or change current language"
|
||||
},
|
||||
"list": {
|
||||
"success": "There are %s player(s) online:",
|
||||
"description": "List online players"
|
||||
},
|
||||
"permission": {
|
||||
"usage": "Usage: permission <add|remove> <username> <permission>",
|
||||
"add": "Permission added.",
|
||||
"has_error": "They already have this permission!",
|
||||
"remove": "Permission removed.",
|
||||
"not_have_error": "They don't have this permission!",
|
||||
"account_error": "The account cannot be found.",
|
||||
"description": "Grants or removes a permission for a user"
|
||||
},
|
||||
"position": {
|
||||
"success": "Coordinates: %s, %s, %s\nScene ID: %s",
|
||||
"description": "Get coordinates"
|
||||
},
|
||||
"quest": {
|
||||
"usage": "quest <add|finish> [quest ID]",
|
||||
"added": "Quest %s added.",
|
||||
"finished": "Finished quest %s.",
|
||||
"not_found": "Quest not found.",
|
||||
"invalid_id": "Invalid quest ID.",
|
||||
"description": "Add or finish quests"
|
||||
},
|
||||
"reload": {
|
||||
"reload_start": "Reloading config.",
|
||||
"reload_done": "Reload complete.",
|
||||
"description": "Reload server config"
|
||||
},
|
||||
"resetConst": {
|
||||
"reset_all": "Reset all avatars' constellations.",
|
||||
"success": "Constellations for %s have been reset. Please relog to see changes.",
|
||||
"description": "Resets the constellation level on your current active character, you will need to relog after using the command to see any changes"
|
||||
},
|
||||
"resetShopLimit": {
|
||||
"usage": "Usage: /resetshop <player ID>",
|
||||
"description": "Reset target player's shop refresh time"
|
||||
},
|
||||
"sendMail": {
|
||||
"usage": "Usage: give [player] <itemID|itemName> [amount]",
|
||||
"user_not_exist": "The user with an ID of '%s' does not exist.",
|
||||
"start_composition": "Starting composition of message.\nPlease use `/sendmail <title>` to continue.\nYou can use `/sendmail stop` at any time.",
|
||||
"templates": "Mail templates coming soon implemented...",
|
||||
"invalid_arguments": "Invalid arguments.\nUsage `/sendmail <userID|all|help> [templateID]`",
|
||||
"send_cancel": "Message sending cancelled.",
|
||||
"send_done": "Message sent to user %s!",
|
||||
"send_all_done": "Message sent to all users!",
|
||||
"not_composition_end": "Message composition not at final stage.\nPlease use `/sendmail %s` or `/sendmail stop` to cancel",
|
||||
"please_use": "Please use `/sendmail %s`",
|
||||
"set_title": "Message title set as '%s'.\nUse '/sendmail <content>' to continue.",
|
||||
"set_contents": "Message contents set as '%s'.\nUse '/sendmail <sender>' to continue.",
|
||||
"set_message_sender": "Message sender set as '%s'.\nUse '/sendmail <itemID|itemName|finish> [amount] [level]' to continue.",
|
||||
"send": "Attached %s of %s (level %s) to the message.\nContinue adding more items or use `/sendmail finish` to send the message.",
|
||||
"invalid_arguments_please_use": "Invalid arguments \n Please use `/sendmail %s`",
|
||||
"title": "<title>",
|
||||
"message": "<message>",
|
||||
"sender": "<sender>",
|
||||
"arguments": "<itemID|itemName|finish> [amount] [level]",
|
||||
"error": "ERROR: Invalid construction stage %s. Check console for stacktrace.",
|
||||
"description": "Sends mail to the specified user. The usage of this command changes based on its composition state"
|
||||
},
|
||||
"sendMessage": {
|
||||
"usage": "Usage: sendmessage <player> <message>",
|
||||
"success": "Message sent.",
|
||||
"description": "Sends a message to a player as the server"
|
||||
},
|
||||
"setFetterLevel": {
|
||||
"usage": "Usage: setfetterlevel <level>",
|
||||
"range_error": "Fetter level must be between 0 and 10.",
|
||||
"success": "Fetter level set to %s.",
|
||||
"level_error": "Invalid fetter level.",
|
||||
"description": "Sets your fetter level for your current active character"
|
||||
},
|
||||
"setStats": {
|
||||
"usage_console": "Usage: setstats|stats @<UID> <stat> <value>",
|
||||
"usage_ingame": "Usage: setstats|stats [@UID] <stat> <value>",
|
||||
"help_message": "\n\tValues for <stat>: hp | maxhp | def | atk | em | er | crate | cdmg | cdr | heal | heali | shield | defi\n\t(cont.) Elemental DMG Bonus: epyro | ecryo | ehydro | egeo | edendro | eelectro | ephys\n\t(cont.) Elemental RES: respyro | rescryo | reshydro | resgeo | resdendro | reselectro | resphys\n",
|
||||
"value_error": "Invalid stat value.",
|
||||
"uid_error": "Invalid UID.",
|
||||
"player_error": "Player not found or offline.",
|
||||
"set_self": "%s set to %s.",
|
||||
"set_for_uid": "%s for %s set to %s.",
|
||||
"set_max_hp": "MAX HP set to %s.",
|
||||
"description": "Sets fight property for your current active character"
|
||||
},
|
||||
"setWorldLevel": {
|
||||
"usage": "Usage: setworldlevel <level>",
|
||||
"value_error": "World level must be between 0-8.",
|
||||
"success": "World level set to %s.",
|
||||
"invalid_world_level": "Invalid world level.",
|
||||
"description": "Sets your world level (Relog to see proper effects)"
|
||||
},
|
||||
"spawn": {
|
||||
"usage": "Usage: spawn <entityID> [amount] [level(monster only)]",
|
||||
"success": "Spawned %s of %s.",
|
||||
"description": "Spawns an entity near you"
|
||||
},
|
||||
"stop": {
|
||||
"success": "Server shutting down...",
|
||||
"description": "Stops the server"
|
||||
},
|
||||
"talent": {
|
||||
"usage_1": "To set talent level: /talent set <talentID> <value>",
|
||||
"usage_2": "Another way to set talent level: /talent <n or e or q> <value>",
|
||||
"usage_3": "To get talent ID: /talent getid",
|
||||
"lower_16": "Invalid talent level. Level should be lower than 16.",
|
||||
"set_id": "Set talent to %s.",
|
||||
"set_atk": "Set talent Normal ATK to %s.",
|
||||
"set_e": "Set talent E to %s.",
|
||||
"set_q": "Set talent Q to %s.",
|
||||
"invalid_skill_id": "Invalid skill ID.",
|
||||
"set_this": "Set this talent to %s.",
|
||||
"invalid_level": "Invalid talent level.",
|
||||
"normal_attack_id": "Normal Attack ID %s.",
|
||||
"e_skill_id": "E skill ID %s.",
|
||||
"q_skill_id": "Q skill ID %s.",
|
||||
"description": "Set talent level for your current active character"
|
||||
},
|
||||
"team": {
|
||||
"usage": "Usage: team <add|remove|set> [avatarID,...] [index|first|last|index-index,...]",
|
||||
"invalid_usage": "Invalid usage.",
|
||||
"add_usage": "Usage (add): team add <avatarID,...> [index]",
|
||||
"invalid_index": "Index is invalid.",
|
||||
"add_too_much": "The server only allows you to have at most %d avatar(s) in your team.",
|
||||
"failed_to_add_avatar": "Failed to add avatar ID %s.",
|
||||
"remove_usage": "Usage (remove): team remove <index|first|last|index-index,...>",
|
||||
"failed_to_parse_index": "Failed to parse index: %s",
|
||||
"remove_too_much": "You can't remove all your avatars.",
|
||||
"ignore_index": "Ignored index(es): %s",
|
||||
"set_usage": "Usage (set): team set <index> <avatarID>",
|
||||
"index_out_of_range": "The index you specified is out of range.",
|
||||
"failed_parse_avatar_id": "Failed to parse avatar ID: %s",
|
||||
"avatar_already_in_team": "Avatar is already in team.",
|
||||
"avatar_not_found": "Avatar %d not found.",
|
||||
"description": "Modify your team manually."
|
||||
},
|
||||
"teleportAll": {
|
||||
"success": "Summoned all players to your location.",
|
||||
"error": "You can only use this command in MP mode.",
|
||||
"description": "Teleports all players in your world to your position"
|
||||
},
|
||||
"teleport": {
|
||||
"usage_server": "Usage: /tp @<player ID> <x> <y> <z> [scene ID]",
|
||||
"usage": "Usage: /tp [@<player ID>] <x> <y> <z> [scene ID]",
|
||||
"specify_player_id": "You must specify a player ID.",
|
||||
"invalid_position": "Invalid position.",
|
||||
"success": "Teleported %s to %s, %s, %s in scene %s.",
|
||||
"description": "Change the player's position"
|
||||
},
|
||||
"weather": {
|
||||
"usage": "Usage: weather <weatherID> [climateID]",
|
||||
"success": "Changed weather to %s with climate %s.",
|
||||
"invalid_id": "Invalid ID.",
|
||||
"description": "Changes the weather"
|
||||
},
|
||||
"drop": {
|
||||
"command_usage": "Usage: drop <itemID|itemName> [amount]",
|
||||
"success": "Dropped %s of %s.",
|
||||
"description": "Drops an item near you"
|
||||
},
|
||||
"help": {
|
||||
"usage": "Usage: ",
|
||||
"aliases": "Aliases: ",
|
||||
"available_commands": "Available commands: ",
|
||||
"description": "Sends the help message or shows information about a specified command"
|
||||
},
|
||||
"restart": {
|
||||
"description": "Restarts the current session"
|
||||
},
|
||||
"unlocktower": {
|
||||
"success": "Unlock done.",
|
||||
"description": "Unlock all levels of tower"
|
||||
}
|
||||
},
|
||||
"gacha": {
|
||||
"details": {
|
||||
"title": "Banner Details",
|
||||
"available_five_stars": "Available 5-star Items",
|
||||
"available_four_stars": "Available 4-star Items",
|
||||
"available_three_stars": "Available 3-star Items",
|
||||
"template_missing": "data/gacha_details.html is missing."
|
||||
}
|
||||
},
|
||||
"documentation": {
|
||||
"handbook": {
|
||||
"title": "GM Handbook",
|
||||
"title_commands": "Commands",
|
||||
"title_avatars": "Avatars",
|
||||
"title_items": "Items",
|
||||
"title_scenes": "Scenes",
|
||||
"title_monsters": "Monsters",
|
||||
"header_id": "Id",
|
||||
"header_command": "Command",
|
||||
"header_description": "Description",
|
||||
"header_avatar": "Avatar",
|
||||
"header_item": "Item",
|
||||
"header_scene": "Scene",
|
||||
"header_monster": "Monster"
|
||||
},
|
||||
"index": {
|
||||
"title": "Documentation",
|
||||
"handbook": "GM Handbook",
|
||||
"gacha_mapping": "Gacha mapping JSON"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -298,9 +298,6 @@
|
||||
"unlocktower": {
|
||||
"success": "odblokować gotowe",
|
||||
"description": "Odblokuj głęboką spiralę"
|
||||
},
|
||||
"resetshop": {
|
||||
"description": "zresetuj sklep"
|
||||
}
|
||||
},
|
||||
"gacha": {
|
||||
|
@ -1,394 +1,389 @@
|
||||
{
|
||||
"messages": {
|
||||
"game": {
|
||||
"port_bind": "游戏服务器已在端口 %s 上启动",
|
||||
"connect": "客户端 %s 已连接",
|
||||
"disconnect": "客户端 %s 已断开连接",
|
||||
"game_update_error": "游戏更新时发生错误",
|
||||
"command_error": "命令发生错误:"
|
||||
},
|
||||
"dispatch": {
|
||||
"port_bind": "[Dispatch] 服务器已在端口 %s 上启动",
|
||||
"request": "[Dispatch] 客户端 %s 请求:%s %s",
|
||||
"keystore": {
|
||||
"general_error": "[Dispatch] 加载 keystore 文件时发生错误!",
|
||||
"password_error": "[Dispatch] 加载 keystore 失败。正在尝试使用 keystore 默认密码...",
|
||||
"no_keystore_error": "[Dispatch] 未找到 SSL 证书!已降级到 HTTP 模式",
|
||||
"default_password": "[Dispatch] 成功加载 keystore 默认密码。请考虑将 config.json 的默认密码设置为 123456"
|
||||
},
|
||||
"authentication": {
|
||||
"default_unable_to_verify": "[Authentication] 称为 verifyUser 的方法在默认验证程序中不可用"
|
||||
},
|
||||
"no_commands_error": "此命令不适用于 Dispatch-only 模式",
|
||||
"unhandled_request_error": "[Dispatch] 潜在的未处理请求:%s %s",
|
||||
"account": {
|
||||
"login_attempt": "[Dispatch] 客户端 %s 正在尝试登录",
|
||||
"login_success": "[Dispatch] 客户端 %s 已登录,UID 为 %s",
|
||||
"login_token_attempt": "[Dispatch] 客户端 %s 正在尝试使用 token 登录",
|
||||
"login_token_error": "[Dispatch] 客户端 %s 使用 token 登录失败",
|
||||
"login_token_success": "[Dispatch] 客户端 %s 已通过 token 登录,UID 为 %s",
|
||||
"combo_token_success": "[Dispatch] 客户端 %s 交换 token 成功",
|
||||
"combo_token_error": "[Dispatch] 客户端 %s 交换 token 失败",
|
||||
"account_login_create_success": "[Dispatch] 客户端 %s 登录失败: 已注册 UID 为 %s 的账号",
|
||||
"account_login_create_error": "[Dispatch] 客户端 %s 登录失败:账号创建失败。",
|
||||
"account_login_exist_error": "[Dispatch] 客户端 %s 登录失败:账号不存在",
|
||||
"account_cache_error": "游戏账号缓存信息错误",
|
||||
"session_key_error": "会话密钥错误。",
|
||||
"username_error": "未找到此用户名。",
|
||||
"username_create_error": "未找到用户名,建立连接失败。"
|
||||
}
|
||||
},
|
||||
"status": {
|
||||
"free_software": "Grasscutter 是免费开源软件,遵循 AGPL-3.0 license。如果你是付费购买的,那你已经被骗了。项目地址:https://github.com/Grasscutters/Grasscutter",
|
||||
"starting": "正在启动 Grasscutter...",
|
||||
"shutdown": "正在关闭...",
|
||||
"done": "加载完成!输入 \"help\" 查看命令列表",
|
||||
"error": "发生了一个错误。",
|
||||
"welcome": "欢迎使用 Grasscutter!珍惜这段美妙的旅途吧!",
|
||||
"run_mode_error": "无效的服务器运行模式:%s。",
|
||||
"run_mode_help": "服务器运行模式必须为 HYBRID、DISPATCH_ONLY 或 GAME_ONLY。Grasscutter 启动失败...",
|
||||
"create_resources": "正在创建 resources 目录...",
|
||||
"resources_error": "请将 BinOutput 和 ExcelBinOutput 复制到 resources 目录。",
|
||||
"version": "Grasscutter 版本: %s-%s"
|
||||
}
|
||||
},
|
||||
"commands": {
|
||||
"generic": {
|
||||
"not_specified": "没有指定命令。",
|
||||
"unknown_command": "未知的命令:%s",
|
||||
"permission_error": "哼哼哼!你没有执行此命令的权限!请联系服务器管理员解决!",
|
||||
"console_execute_error": "此命令只能在服务器控制台执行呐~",
|
||||
"player_execute_error": "此命令只能在游戏内执行哦~",
|
||||
"command_exist_error": "这条命令...好像找不到呢?",
|
||||
"no_description_specified": "没有指定说明",
|
||||
"invalid": {
|
||||
"amount": "无效的数量。",
|
||||
"artifactId": "无效的圣遗物ID。",
|
||||
"avatarId": "无效的角色ID。",
|
||||
"avatarLevel": "无效的角色等级。",
|
||||
"entityId": "无效的实体ID。",
|
||||
"itemId": "无效的物品ID。",
|
||||
"itemLevel": "无效的物品等级。",
|
||||
"itemRefinement": "无效的物品精炼等级。",
|
||||
"playerId": "无效的玩家ID。",
|
||||
"uid": "无效的UID。"
|
||||
}
|
||||
},
|
||||
"execution": {
|
||||
"uid_error": "无效的UID。",
|
||||
"player_exist_error": "玩家不存在。",
|
||||
"player_offline_error": "玩家已离线。",
|
||||
"item_id_error": "无效的物品ID。",
|
||||
"item_player_exist_error": "无效的物品/玩家UID。",
|
||||
"entity_id_error": "无效的实体ID。",
|
||||
"player_exist_offline_error": "玩家不存在或已离线。",
|
||||
"argument_error": "无效的参数。",
|
||||
"clear_target": "目标已清除。",
|
||||
"set_target": "随后的的命令都会以 @%s 为预设。",
|
||||
"need_target": "此命令需要一个目标 UID。添加 <@UID> 参数或使用 /target @UID 来指定默认目标。"
|
||||
},
|
||||
"status": {
|
||||
"enabled": "已启用",
|
||||
"disabled": "未启用",
|
||||
"help": "帮助",
|
||||
"success": "成功"
|
||||
},
|
||||
"account": {
|
||||
"modify": "修改用户账号",
|
||||
"invalid": "无效的UID。",
|
||||
"exists": "账号已存在。",
|
||||
"create": "已创建账号,UID 为 %s。",
|
||||
"delete": "账号已删除。",
|
||||
"no_account": "账号不存在。",
|
||||
"command_usage": "用法:account <create|delete> <用户名> [UID]",
|
||||
"description": "创建或删除账号"
|
||||
},
|
||||
"broadcast": {
|
||||
"command_usage": "用法:broadcast <消息>",
|
||||
"message_sent": "公告已发送。",
|
||||
"description": "向所有玩家发送公告"
|
||||
},
|
||||
"changescene": {
|
||||
"usage": "用法:changescene <场景ID>",
|
||||
"already_in_scene": "你已经在这个场景中了。",
|
||||
"success": "已切换至场景 %s。",
|
||||
"exists_error": "此场景不存在。",
|
||||
"description": "切换指定场景"
|
||||
},
|
||||
"clear": {
|
||||
"command_usage": "用法:clear <all|wp|art|mat>\nall: 所有, wp: 武器, art: 圣遗物, mat: 材料",
|
||||
"weapons": "已清除 %s 的武器。",
|
||||
"artifacts": "已清除 %s 的圣遗物。",
|
||||
"materials": "已清除 %s 的材料。",
|
||||
"furniture": "已清除 %s 的尘歌壶家具。",
|
||||
"displays": "已清空 %s 的屏幕。",
|
||||
"virtuals": "已清除 %s 的所有货币和经验值。",
|
||||
"everything": "已清除 %s 的所有物品。",
|
||||
"description": "从你的背包中删除所有未装备且已解锁的物品,包括稀有物品"
|
||||
},
|
||||
"coop": {
|
||||
"usage": "用法:coop <玩家ID> <目标玩家ID>",
|
||||
"success": "已强制传送 %s 到 %s 的世界。",
|
||||
"description": "强制传送指定用户到他人的世界"
|
||||
},
|
||||
"enter_dungeon": {
|
||||
"usage": "用法:enterdungeon <秘境ID>",
|
||||
"changed": "已进入秘境 %s。",
|
||||
"not_found_error": "此秘境不存在。",
|
||||
"in_dungeon_error": "你已经在秘境中了。",
|
||||
"description": "进入指定秘境"
|
||||
},
|
||||
"giveAll": {
|
||||
"usage": "用法:giveall [玩家] [数量]",
|
||||
"started": "正在给予全部物品...",
|
||||
"success": "已给予 %s 全部物品。",
|
||||
"invalid_amount_or_playerId": "无效的数量/玩家ID。",
|
||||
"description": "给予所有物品"
|
||||
},
|
||||
"nostamina": {
|
||||
"success": "NoStamina %s 对于 %s.",
|
||||
"description": "保持你的体力处于最高状态。"
|
||||
},
|
||||
"giveArtifact": {
|
||||
"usage": "用法:giveart|gart [玩家] <圣遗物ID> <主词条ID> [<副词条ID>[,<强化次数>]]... [等级]",
|
||||
"id_error": "无效的圣遗物ID。",
|
||||
"success": "已将 %s 给予 %s。",
|
||||
"description": "给予指定圣遗物"
|
||||
},
|
||||
"giveChar": {
|
||||
"usage": "用法:givechar <玩家> <角色ID|角色名> [数量]",
|
||||
"given": "已将角色 %s [等级 %s] 给与 %s。",
|
||||
"invalid_avatar_id": "无效的角色ID。",
|
||||
"invalid_avatar_level": "无效的角色等级。",
|
||||
"invalid_avatar_or_player_id": "无效的角色ID/玩家ID。",
|
||||
"description": "给予指定角色"
|
||||
},
|
||||
"give": {
|
||||
"usage": "用法:give <玩家> <物品ID|物品名> [数量] [等级] [精炼等级]",
|
||||
"refinement_only_applicable_weapons": "只有武器可以设置精炼等级。",
|
||||
"refinement_must_between_1_and_5": "精炼等级必须在 1 到 5 之间。",
|
||||
"given": "已将 %s 个 %s 给予 %s。",
|
||||
"given_with_level_and_refinement": "已将 %s [等级 %s, 精炼 %s] %s 个给予 %s。",
|
||||
"given_level": "已将 %s [等级 %s] %s 个给予 %s。",
|
||||
"description": "给予指定物品"
|
||||
},
|
||||
"godmode": {
|
||||
"success": "上帝模式已设为 %s。[用户:%s]",
|
||||
"description": "防止你受到伤害"
|
||||
},
|
||||
"heal": {
|
||||
"success": "已治疗所有角色。",
|
||||
"description": "治疗当前队伍的角色"
|
||||
},
|
||||
"kick": {
|
||||
"player_kick_player": "玩家 [%s:%s] 已将 [%s:%s] 踢出。",
|
||||
"server_kick_player": "正在踢出玩家 [%s:%s]...",
|
||||
"description": "从服务器内踢出指定玩家"
|
||||
},
|
||||
"kill": {
|
||||
"usage": "用法:killall [玩家UID] [场景ID]",
|
||||
"scene_not_found_in_player_world": "未在玩家世界中找到此场景。",
|
||||
"kill_monsters_in_scene": "已杀死场景 %s 中的 %s 个怪物。",
|
||||
"description": "杀死所有怪物"
|
||||
},
|
||||
"killCharacter": {
|
||||
"usage": "用法:/killcharacter [玩家ID]",
|
||||
"success": "已杀死 %s 当前角色。",
|
||||
"description": "杀死当前角色"
|
||||
},
|
||||
"language": {
|
||||
"current_language": "当前语言是: %s",
|
||||
"language_changed": "语言切换至: %s",
|
||||
"language_not_found": "目前服务端没有这种语言: %s",
|
||||
"description": "显示或切换当前语言"
|
||||
},
|
||||
"list": {
|
||||
"success": "目前在线人数:%s",
|
||||
"description": "查看所有玩家"
|
||||
},
|
||||
"permission": {
|
||||
"usage": "用法:permission <add|remove> <用户名> <权限>",
|
||||
"add": "权限已添加。",
|
||||
"has_error": "此玩家已拥有此权限!",
|
||||
"remove": "权限已移除。",
|
||||
"not_have_error": "此玩家未拥有权限!",
|
||||
"account_error": "账号不存在。",
|
||||
"description": "添加或移除指定玩家的权限"
|
||||
},
|
||||
"position": {
|
||||
"success": "坐标:%s, %s, %s\n场景ID:%s",
|
||||
"description": "获取所在位置"
|
||||
},
|
||||
"quest": {
|
||||
"description": "添加或完成任务",
|
||||
"usage": "quest <add|finish> [任务ID]",
|
||||
"added": "已添加任务 %s",
|
||||
"finished": "已完成任务 %s",
|
||||
"not_found": "未找到任务",
|
||||
"invalid_id": "无效的任务ID"
|
||||
},
|
||||
"reload": {
|
||||
"reload_start": "正在重载配置文件和数据。",
|
||||
"reload_done": "重载完成。",
|
||||
"description": "重载配置文件和数据"
|
||||
},
|
||||
"resetConst": {
|
||||
"reset_all": "重置所有角色的命座。",
|
||||
"success": "已重置 %s 的命座,重新登录后生效。",
|
||||
"description": "重置当前角色的命之座,执行命令后需重新登录以生效"
|
||||
},
|
||||
"resetShopLimit": {
|
||||
"usage": "用法:/resetshop <玩家ID>",
|
||||
"description": "重置所选玩家的商店刷新时间"
|
||||
},
|
||||
"sendMail": {
|
||||
"usage": "用法:give [玩家] <物品ID|物品名称> [数量]",
|
||||
"user_not_exist": "ID '%s' 的用户不存在。",
|
||||
"start_composition": "发送邮件流程。\n请使用`/sendmail <标题>`前进到下一步。\n你可以在任何时间使用`/sendmail stop`来停止发送。",
|
||||
"templates": "邮件模板尚未实装...",
|
||||
"invalid_arguments": "无效的参数。\n指令使用方法 `/sendmail <用户ID|all|help> [模板ID]`",
|
||||
"send_cancel": "取消发送邮件",
|
||||
"send_done": "已将邮件发送给 %s!",
|
||||
"send_all_done": "邮件已发送给所有人!",
|
||||
"not_composition_end": "现在邮件发送未到最后阶段。\n请使用 `/sendmail %s` 继续发送邮件,或使用 `/sendmail stop` 来停止发送邮件。",
|
||||
"please_use": "请使用 `/sendmail %s`",
|
||||
"set_title": "成功将邮件标题设置为 '%s'。\n使用 '/sendmail <正文>' 来设置邮件内容。",
|
||||
"set_contents": "成功将邮件内容设置为 '%s'。\n使用 '/sendmail <发件人>' 来设置发件人。",
|
||||
"set_message_sender": "发件人已设置为 '%s'。\n使用 '/sendmail <物品ID|物品名称|finish> [数量] [等级]' 来添加附件。",
|
||||
"send": "已添加 %s 个 %s (等级 %s) 邮件附件。\n如果没有要继续添加的附件请使用 `/sendmail finish` 来发送邮件。",
|
||||
"invalid_arguments_please_use": "错误的参数 \n请使用 `/sendmail %s`",
|
||||
"title": "<标题>",
|
||||
"message": "<正文>",
|
||||
"sender": "<发件人>",
|
||||
"arguments": "<物品ID|物品名称|finish> [数量] [等级]",
|
||||
"error": "错误:无效的编写阶段 %s。需要 StackTrace 请查看服务器控制台。",
|
||||
"description": "向指定用户发送邮件。此命令的用法可根据附加的参数而变化"
|
||||
},
|
||||
"sendMessage": {
|
||||
"usage": "用法:sendmessage <玩家> <消息>",
|
||||
"success": "消息已发送。",
|
||||
"description": "向指定玩家发送消息"
|
||||
},
|
||||
"setFetterLevel": {
|
||||
"usage": "用法:setfetterlevel <好感度等级>",
|
||||
"range_error": "好感度等级必须在 0 到 10 之间。",
|
||||
"success": "好感度已设为 %s 级。",
|
||||
"level_error": "无效的好感度等级。",
|
||||
"description": "设置当前角色的好感度等级"
|
||||
},
|
||||
"setStats": {
|
||||
"usage_console": "用法:setstats|stats @<UID> <属性> <数值>",
|
||||
"usage_ingame": "用法:setstats|stats [@UID] <属性> <数值>",
|
||||
"help_message": "\n可更改的属性列表:hp (生命值)| maxhp (最大生命值) | def(防御力) | atk (攻击力)| em (元素精通) | er (元素充能效率) | crate(暴击率) | cdmg (暴击伤害)| cdr (冷却缩减) | heal(治疗加成)| heali (受治疗加成)| shield (护盾强效)| defi (无视防御)\n(续) 元素增伤:epyro (火) | ecryo (冰) | ehydro (水) | egeo (岩) | edendro (草) | eelectro (雷) | ephys (物理)\n(续) 元素抗性:respyro (火) | rescryo (冰) | reshydro (水) | resgeo (岩) | resdendro (草) | reselectro (雷) | resphys (物理)\n",
|
||||
"value_error": "无效的属性值。",
|
||||
"uid_error": "无效的UID。",
|
||||
"player_error": "玩家不存在或已离线。",
|
||||
"set_self": "%s 已设为 %s。",
|
||||
"set_for_uid": "%s [来自 %s] 已设为 %s。",
|
||||
"set_max_hp": "最大生命值已设为 %s。",
|
||||
"description": "设置当前角色的属性"
|
||||
},
|
||||
"setWorldLevel": {
|
||||
"usage": "用法:setworldlevel <等级>",
|
||||
"value_error": "世界等级必须设置在0-8之间。",
|
||||
"success": "世界等级已设为 %s。",
|
||||
"invalid_world_level": "无效的世界等级。",
|
||||
"description": "设置世界等级,执行命令后需重新登录以生效"
|
||||
},
|
||||
"spawn": {
|
||||
"usage": "用法:spawn <实体ID> [数量] [等级(仅怪物)]",
|
||||
"success": "已生成 %s 个 %s。",
|
||||
"description": "在你附近生成一个生物"
|
||||
},
|
||||
"stop": {
|
||||
"success": "正在关闭服务器...",
|
||||
"description": "停止服务器"
|
||||
},
|
||||
"talent": {
|
||||
"usage_1": "设置天赋等级:/talent set <天赋ID> <数值>",
|
||||
"usage_2": "另一种设置天赋等级的方法:/talent <n (普通攻击) | e (元素战技) | q (元素爆发)> <数值>",
|
||||
"usage_3": "获取天赋ID:/talent getid",
|
||||
"lower_16": "无效的天赋等级,天赋等级应小于等于15。",
|
||||
"set_id": "将天赋等级设为 %s。",
|
||||
"set_atk": "将普通攻击等级设为 %s。",
|
||||
"set_e": "将元素战技等级设为 %s。",
|
||||
"set_q": "将元素爆发等级设为 %s。",
|
||||
"invalid_skill_id": "无效的技能ID。",
|
||||
"set_this": "将天赋等级设为 %s。",
|
||||
"invalid_level": "无效的天赋等级。",
|
||||
"normal_attack_id": "普通攻击的 ID 为 %s。",
|
||||
"e_skill_id": "元素战技ID %s。",
|
||||
"q_skill_id": "元素爆发ID %s。",
|
||||
"description": "设置当前角色的天赋等级"
|
||||
},
|
||||
"team": {
|
||||
"usage": "用法: team <add|remove|set> [avatarId,...] [index|first|last|index-index,...]",
|
||||
"invalid_usage": "无效用法",
|
||||
"add_usage": "用法(add): team add <avatarId,...> [index]",
|
||||
"invalid_index": "无效索引",
|
||||
"add_too_much": "服务端仅允许你队伍里至多有%d名角色",
|
||||
"failed_to_add_avatar": "无法根据id %s 添加角色",
|
||||
"remove_usage": "用法(remove): team remove <index|first|last|index-index,...>",
|
||||
"failed_parse_index": "无法解析索引: %s",
|
||||
"remove_too_much": "你不能删除那么多角色,你的队伍列表将会变空",
|
||||
"ignore_index": "忽略的索引列表: %s",
|
||||
"set_usage": "用法(set): team set <index> <avatarId>",
|
||||
"index_out_of_range": "你指定的索引超出了范围",
|
||||
"failed_parse_avatar_id": "无法解析的角色id: %s",
|
||||
"avatar_already_in_team": "角色已经在你的队伍中了",
|
||||
"avatar_not_found": "无法找到该角色: %d",
|
||||
"description": "手动修改你的队伍"
|
||||
},
|
||||
"teleportAll": {
|
||||
"success": "已将所有玩家传送到你的位置。",
|
||||
"error": "你只能在多人游戏状态下执行此命令。",
|
||||
"description": "将你世界中的所有玩家传送到你所在的位置"
|
||||
},
|
||||
"teleport": {
|
||||
"usage_server": "用法:/tp @<玩家ID> <x> <y> <z> [场景ID]",
|
||||
"usage": "用法:/tp [@<玩家ID>] <x> <y> <z> [场景ID]",
|
||||
"specify_player_id": "你必须指定一个玩家ID。",
|
||||
"invalid_position": "无效的位置。",
|
||||
"success": "传送 %s 到坐标 %s,%s,%s,场景为 %s。",
|
||||
"description": "改变指定玩家的位置"
|
||||
},
|
||||
"tower": {
|
||||
"unlock_done": "深境回廊的所有层已全部解锁。"
|
||||
},
|
||||
"weather": {
|
||||
"usage": "用法:weather <天气ID> [气候ID]",
|
||||
"success": "已更改天气为 %s,气候为 %s。",
|
||||
"invalid_id": "无效的天气ID。",
|
||||
"description": "更改天气"
|
||||
},
|
||||
"drop": {
|
||||
"command_usage": "用法:drop <物品ID|物品名称> [数量]",
|
||||
"success": "已丢下 %s 个 %s。",
|
||||
"description": "在你附近丢下一个物品"
|
||||
},
|
||||
"help": {
|
||||
"usage": "用法:",
|
||||
"aliases": "别名:",
|
||||
"available_commands": "可用命令:",
|
||||
"description": "发送帮助信息或显示指定命令的信息"
|
||||
},
|
||||
"restart": {
|
||||
"description": "重新启动服务器"
|
||||
},
|
||||
"unlocktower": {
|
||||
"success": "解锁完成。",
|
||||
"description": "解锁深境螺旋的所有层"
|
||||
},
|
||||
"resetshop": {
|
||||
"description": "重置商店刷新时间"
|
||||
}
|
||||
},
|
||||
"gacha": {
|
||||
"details": {
|
||||
"title": "祈愿详情",
|
||||
"available_five_stars": "可获得的5星物品",
|
||||
"available_four_stars": "可获得的4星物品",
|
||||
"available_three_stars": "可获得的3星物品"
|
||||
}
|
||||
}
|
||||
}
|
||||
{
|
||||
"messages": {
|
||||
"game": {
|
||||
"port_bind": "游戏服务器已在端口 %s 上启动",
|
||||
"connect": "客户端 %s 已连接",
|
||||
"disconnect": "客户端 %s 已断开连接",
|
||||
"game_update_error": "游戏更新时发生错误",
|
||||
"command_error": "命令发生错误:"
|
||||
},
|
||||
"dispatch": {
|
||||
"port_bind": "[Dispatch] 服务器已在端口 %s 上启动",
|
||||
"request": "[Dispatch] 客户端 %s 请求:%s %s",
|
||||
"keystore": {
|
||||
"general_error": "[Dispatch] 加载 keystore 文件时发生错误!",
|
||||
"password_error": "[Dispatch] 加载 keystore 失败。正在尝试使用 keystore 默认密码...",
|
||||
"no_keystore_error": "[Dispatch] 未找到 SSL 证书!已降级到 HTTP 模式",
|
||||
"default_password": "[Dispatch] 成功加载 keystore 默认密码。请考虑将 config.json 的默认密码设置为 123456"
|
||||
},
|
||||
"authentication": {
|
||||
"default_unable_to_verify": "[Authentication] 称为 verifyUser 的方法在默认验证程序中不可用"
|
||||
},
|
||||
"no_commands_error": "此命令不适用于 Dispatch-only 模式",
|
||||
"unhandled_request_error": "[Dispatch] 潜在的未处理请求:%s %s",
|
||||
"account": {
|
||||
"login_attempt": "[Dispatch] 客户端 %s 正在尝试登录",
|
||||
"login_success": "[Dispatch] 客户端 %s 已登录,UID 为 %s",
|
||||
"login_token_attempt": "[Dispatch] 客户端 %s 正在尝试使用 token 登录",
|
||||
"login_token_error": "[Dispatch] 客户端 %s 使用 token 登录失败",
|
||||
"login_token_success": "[Dispatch] 客户端 %s 已通过 token 登录,UID 为 %s",
|
||||
"combo_token_success": "[Dispatch] 客户端 %s 交换 token 成功",
|
||||
"combo_token_error": "[Dispatch] 客户端 %s 交换 token 失败",
|
||||
"account_login_create_success": "[Dispatch] 客户端 %s 登录失败:已注册 UID 为 %s 的账号",
|
||||
"account_login_create_error": "[Dispatch] 客户端 %s 登录失败:账号创建失败",
|
||||
"account_login_exist_error": "[Dispatch] 客户端 %s 登录失败:账号不存在",
|
||||
"account_cache_error": "游戏账号缓存信息错误",
|
||||
"session_key_error": "会话密钥错误",
|
||||
"username_error": "未找到此用户名",
|
||||
"username_create_error": "未找到用户名,建立连接失败"
|
||||
},
|
||||
"router_error": "[Dispatch] 无法连接路由"
|
||||
},
|
||||
"status": {
|
||||
"free_software": "Grasscutter 是免费开源软件,遵循 AGPL-3.0 license。如果你是付费购买的,那你已经被骗了。项目地址:https://github.com/Grasscutters/Grasscutter",
|
||||
"starting": "正在启动 Grasscutter...",
|
||||
"shutdown": "正在关闭...",
|
||||
"done": "加载完成!输入 \"help\" 查看命令列表",
|
||||
"error": "发生了一个错误。",
|
||||
"welcome": "欢迎使用 Grasscutter!珍惜这段美妙的旅途吧!",
|
||||
"run_mode_error": "无效的服务器运行模式:%s。",
|
||||
"run_mode_help": "服务器运行模式必须为 HYBRID、DISPATCH_ONLY 或 GAME_ONLY。Grasscutter 启动失败...",
|
||||
"create_resources": "正在创建 resources 目录...",
|
||||
"resources_error": "请将 BinOutput 和 ExcelBinOutput 复制到 resources 目录。",
|
||||
"version": "Grasscutter 版本:%s-%s"
|
||||
}
|
||||
},
|
||||
"commands": {
|
||||
"generic": {
|
||||
"not_specified": "没有指定命令。",
|
||||
"unknown_command": "未知的命令:%s",
|
||||
"permission_error": "哼哼哼!你没有执行此命令的权限!请联系服务器管理员解决!",
|
||||
"console_execute_error": "此命令只能在服务器控制台执行呐~",
|
||||
"player_execute_error": "此命令只能在游戏内执行哦~",
|
||||
"command_exist_error": "这条命令...好像找不到呢?",
|
||||
"no_description_specified": "没有指定说明。",
|
||||
"invalid": {
|
||||
"amount": "无效的数量。",
|
||||
"artifactId": "无效的圣遗物ID。",
|
||||
"avatarId": "无效的角色ID。",
|
||||
"avatarLevel": "无效的角色等级。",
|
||||
"entityId": "无效的实体ID。",
|
||||
"itemId": "无效的物品ID。",
|
||||
"itemLevel": "无效的物品等级。",
|
||||
"itemRefinement": "无效的物品精炼等级。",
|
||||
"playerId": "无效的玩家ID。",
|
||||
"uid": "无效的UID。"
|
||||
}
|
||||
},
|
||||
"execution": {
|
||||
"uid_error": "无效的UID。",
|
||||
"player_exist_error": "玩家不存在。",
|
||||
"player_offline_error": "玩家已离线。",
|
||||
"item_id_error": "无效的物品ID。",
|
||||
"item_player_exist_error": "无效的物品/玩家UID。",
|
||||
"entity_id_error": "无效的实体ID。",
|
||||
"player_exist_offline_error": "玩家不存在或已离线。",
|
||||
"argument_error": "无效的参数。",
|
||||
"clear_target": "目标已清除。",
|
||||
"set_target": "随后的的命令都会以 @%s 为预设。",
|
||||
"need_target": "此命令需要一个目标 UID。添加 <@UID> 参数或使用 /target @UID 来指定默认目标。"
|
||||
},
|
||||
"status": {
|
||||
"enabled": "已启用",
|
||||
"disabled": "未启用",
|
||||
"help": "帮助",
|
||||
"success": "成功"
|
||||
},
|
||||
"account": {
|
||||
"modify": "修改用户账号",
|
||||
"invalid": "无效的UID。",
|
||||
"exists": "账号已存在。",
|
||||
"create": "已创建账号,UID 为 %s。",
|
||||
"delete": "账号已删除。",
|
||||
"no_account": "账号不存在。",
|
||||
"command_usage": "用法:account <create|delete> <用户名> [UID]",
|
||||
"description": "创建或删除账号"
|
||||
},
|
||||
"broadcast": {
|
||||
"command_usage": "用法:broadcast <消息>",
|
||||
"message_sent": "公告已发送。",
|
||||
"description": "向所有玩家发送公告"
|
||||
},
|
||||
"changescene": {
|
||||
"usage": "用法:changescene <场景ID>",
|
||||
"already_in_scene": "你已经在这个场景中了。",
|
||||
"success": "已切换至场景 %s。",
|
||||
"exists_error": "此场景不存在。",
|
||||
"description": "切换指定场景"
|
||||
},
|
||||
"clear": {
|
||||
"command_usage": "用法:clear <all|wp|art|mat>\nall: 所有, wp: 武器, art: 圣遗物, mat: 材料",
|
||||
"weapons": "已清除 %s 的武器。",
|
||||
"artifacts": "已清除 %s 的圣遗物。",
|
||||
"materials": "已清除 %s 的材料。",
|
||||
"furniture": "已清除 %s 的尘歌壶家具。",
|
||||
"displays": "已清空 %s 的屏幕。",
|
||||
"virtuals": "已清除 %s 的所有货币和经验值。",
|
||||
"everything": "已清除 %s 的所有物品。",
|
||||
"description": "从你的背包中删除所有未装备且已解锁的物品,包括稀有物品"
|
||||
},
|
||||
"coop": {
|
||||
"usage": "用法:coop <玩家ID> <目标玩家ID>",
|
||||
"success": "已强制传送 %s 到 %s 的世界。",
|
||||
"description": "强制传送指定用户到他人的世界"
|
||||
},
|
||||
"enter_dungeon": {
|
||||
"usage": "用法:enterdungeon <秘境ID>",
|
||||
"changed": "已进入秘境 %s。",
|
||||
"not_found_error": "此秘境不存在。",
|
||||
"in_dungeon_error": "你已经在秘境中了。",
|
||||
"description": "进入指定秘境"
|
||||
},
|
||||
"giveAll": {
|
||||
"usage": "用法:giveall [玩家] [数量]",
|
||||
"started": "正在给予全部物品...",
|
||||
"success": "已给予 %s 全部物品。",
|
||||
"invalid_amount_or_playerId": "无效的数量/玩家ID。",
|
||||
"description": "给予所有物品"
|
||||
},
|
||||
"nostamina": {
|
||||
"success": "NoStamina 已设为 %s。[用户:%s]",
|
||||
"description": "保持你的体力处于最高状态"
|
||||
},
|
||||
"giveArtifact": {
|
||||
"usage": "用法:giveart|gart [玩家] <圣遗物ID> <主词条ID> [<副词条ID>[,<强化次数>]]... [等级]",
|
||||
"id_error": "无效的圣遗物ID。",
|
||||
"success": "已将 %s 给予 %s。",
|
||||
"description": "给予指定圣遗物"
|
||||
},
|
||||
"giveChar": {
|
||||
"usage": "用法:givechar <玩家> <角色ID|角色名> [数量]",
|
||||
"given": "已将角色 %s [等级 %s] 给与 %s。",
|
||||
"invalid_avatar_id": "无效的角色ID。",
|
||||
"invalid_avatar_level": "无效的角色等级。",
|
||||
"invalid_avatar_or_player_id": "无效的角色ID/玩家ID。",
|
||||
"description": "给予指定角色"
|
||||
},
|
||||
"give": {
|
||||
"usage": "用法:give <玩家> <物品ID|物品名> [数量] [等级] [精炼等级]",
|
||||
"refinement_only_applicable_weapons": "只有武器可以设置精炼等级。",
|
||||
"refinement_must_between_1_and_5": "精炼等级必须在 1-5 之间。",
|
||||
"given": "已将 %s 个 %s 给予 %s。",
|
||||
"given_with_level_and_refinement": "已将 %s [等级 %s, 精炼 %s] %s 个给予 %s。",
|
||||
"given_level": "已将 %s [等级 %s] %s 个给予 %s。",
|
||||
"description": "给予指定物品"
|
||||
},
|
||||
"godmode": {
|
||||
"success": "上帝模式已设为 %s。[用户:%s]",
|
||||
"description": "防止你受到伤害"
|
||||
},
|
||||
"heal": {
|
||||
"success": "已治疗所有角色。",
|
||||
"description": "治疗当前队伍的角色"
|
||||
},
|
||||
"kick": {
|
||||
"player_kick_player": "玩家 [%s:%s] 已将 [%s:%s] 踢出。",
|
||||
"server_kick_player": "正在踢出玩家 [%s:%s]...",
|
||||
"description": "从服务器内踢出指定玩家"
|
||||
},
|
||||
"kill": {
|
||||
"usage": "用法:killall [玩家UID] [场景ID]",
|
||||
"scene_not_found_in_player_world": "未在玩家世界中找到此场景。",
|
||||
"kill_monsters_in_scene": "已杀死场景 %s 中的 %s 个怪物。",
|
||||
"description": "杀死所有怪物"
|
||||
},
|
||||
"killCharacter": {
|
||||
"usage": "用法:killcharacter [玩家ID]",
|
||||
"success": "已杀死 %s 当前角色。",
|
||||
"description": "杀死当前角色"
|
||||
},
|
||||
"language": {
|
||||
"current_language": "当前语言是:%s",
|
||||
"language_changed": "语言切换至:%s",
|
||||
"language_not_found": "目前服务端没有这种语言:%s",
|
||||
"description": "显示或切换当前语言"
|
||||
},
|
||||
"list": {
|
||||
"success": "目前在线人数:%s",
|
||||
"description": "查看所有玩家"
|
||||
},
|
||||
"permission": {
|
||||
"usage": "用法:permission <add|remove> <用户名> <权限>",
|
||||
"add": "权限已添加。",
|
||||
"has_error": "此玩家已拥有此权限!",
|
||||
"remove": "权限已移除。",
|
||||
"not_have_error": "此玩家未拥有权限!",
|
||||
"account_error": "账号不存在。",
|
||||
"description": "添加或移除指定玩家的权限"
|
||||
},
|
||||
"position": {
|
||||
"success": "坐标:%s, %s, %s\n场景ID:%s",
|
||||
"description": "获取所在位置"
|
||||
},
|
||||
"quest": {
|
||||
"usage": "quest <add|finish> [任务ID]",
|
||||
"added": "已添加任务 %s。",
|
||||
"finished": "已完成任务 %s。",
|
||||
"not_found": "此任务不存在。",
|
||||
"invalid_id": "无效的任务ID。",
|
||||
"description": "添加或完成任务"
|
||||
},
|
||||
"reload": {
|
||||
"reload_start": "正在重载配置文件和数据。",
|
||||
"reload_done": "重载完成。",
|
||||
"description": "重载配置文件和数据"
|
||||
},
|
||||
"resetConst": {
|
||||
"reset_all": "重置所有角色的命座。",
|
||||
"success": "已重置 %s 的命座,重新登录后生效。",
|
||||
"description": "重置当前角色的命之座,执行命令后需重新登录以生效"
|
||||
},
|
||||
"resetShopLimit": {
|
||||
"usage": "用法:resetshop <玩家ID>",
|
||||
"description": "重置所选玩家的商店刷新时间"
|
||||
},
|
||||
"sendMail": {
|
||||
"usage": "用法:give [玩家] <物品ID|物品名称> [数量]",
|
||||
"user_not_exist": "ID '%s' 的用户不存在。",
|
||||
"start_composition": "发送邮件流程。\n请使用`/sendmail <标题>`前进到下一步。\n你可以在任何时间使用`/sendmail stop`来停止发送。",
|
||||
"templates": "邮件模板尚未实装...",
|
||||
"invalid_arguments": "无效的参数。\n指令使用方法 `/sendmail <用户ID|all|help> [模板ID]`",
|
||||
"send_cancel": "取消发送邮件",
|
||||
"send_done": "已将邮件发送给 %s!",
|
||||
"send_all_done": "邮件已发送给所有人!",
|
||||
"not_composition_end": "现在邮件发送未到最后阶段。\n请使用 `/sendmail %s` 继续发送邮件,或使用 `/sendmail stop` 来停止发送邮件。",
|
||||
"please_use": "请使用 `/sendmail %s`",
|
||||
"set_title": "成功将邮件标题设置为 '%s'。\n使用 '/sendmail <正文>' 来设置邮件内容。",
|
||||
"set_contents": "成功将邮件内容设置为 '%s'。\n使用 '/sendmail <发件人>' 来设置发件人。",
|
||||
"set_message_sender": "发件人已设置为 '%s'。\n使用 '/sendmail <物品ID|物品名称|finish> [数量] [等级]' 来添加附件。",
|
||||
"send": "已添加 %s 个 %s [等级 %s] 邮件附件。\n如果没有要继续添加的附件请使用 `/sendmail finish` 来发送邮件。",
|
||||
"invalid_arguments_please_use": "错误的参数 \n请使用 `/sendmail %s`",
|
||||
"title": "<标题>",
|
||||
"message": "<正文>",
|
||||
"sender": "<发件人>",
|
||||
"arguments": "<物品ID|物品名称|finish> [数量] [等级]",
|
||||
"error": "错误:无效的编写阶段 %s。需要 StackTrace 请查看服务器控制台。",
|
||||
"description": "向指定用户发送邮件。此命令的用法可根据附加的参数而变化"
|
||||
},
|
||||
"sendMessage": {
|
||||
"usage": "用法:sendmessage <玩家> <消息>",
|
||||
"success": "消息已发送。",
|
||||
"description": "向指定玩家发送消息"
|
||||
},
|
||||
"setFetterLevel": {
|
||||
"usage": "用法:setfetterlevel <好感度等级>",
|
||||
"range_error": "好感度等级必须在 0-10 之间。",
|
||||
"success": "好感度已设为 %s 级。",
|
||||
"level_error": "无效的好感度等级。",
|
||||
"description": "设置当前角色的好感度等级"
|
||||
},
|
||||
"setStats": {
|
||||
"usage_console": "用法:setstats|stats @<UID> <属性> <数值>",
|
||||
"usage_ingame": "用法:setstats|stats [@UID] <属性> <数值>",
|
||||
"help_message": "\n可更改的属性列表:hp (生命值)| maxhp (最大生命值) | def (防御力) | atk (攻击力)| em (元素精通) | er (元素充能效率) | crate (暴击率) | cdmg (暴击伤害)| cdr (冷却缩减) | heal (治疗加成)| heali (受治疗加成)| shield (护盾强效)| defi (无视防御)\n(续) 元素增伤:epyro (火) | ecryo (冰) | ehydro (水) | egeo (岩) | edendro (草) | eelectro (雷) | ephys (物理)\n(续) 元素抗性:respyro (火) | rescryo (冰) | reshydro (水) | resgeo (岩) | resdendro (草) | reselectro (雷) | resphys (物理)\n",
|
||||
"value_error": "无效的属性值。",
|
||||
"uid_error": "无效的UID。",
|
||||
"player_error": "玩家不存在或已离线。",
|
||||
"set_self": "%s 已设为 %s。",
|
||||
"set_for_uid": "%s [来自 %s] 已设为 %s。",
|
||||
"set_max_hp": "最大生命值已设为 %s。",
|
||||
"description": "设置当前角色的属性"
|
||||
},
|
||||
"setWorldLevel": {
|
||||
"usage": "用法:setworldlevel <等级>",
|
||||
"value_error": "世界等级必须在 0-8 之间。",
|
||||
"success": "世界等级已设为 %s。",
|
||||
"invalid_world_level": "无效的世界等级。",
|
||||
"description": "设置世界等级,执行命令后需重新登录以生效"
|
||||
},
|
||||
"spawn": {
|
||||
"usage": "用法:spawn <实体ID> [数量] [等级(仅怪物)]",
|
||||
"success": "已生成 %s 个 %s。",
|
||||
"description": "在你附近生成一个生物"
|
||||
},
|
||||
"stop": {
|
||||
"success": "正在关闭服务器...",
|
||||
"description": "停止服务器"
|
||||
},
|
||||
"talent": {
|
||||
"usage_1": "设置天赋等级:/talent set <天赋ID> <数值>",
|
||||
"usage_2": "另一种设置天赋等级的方法:/talent <n|e|q> <数值>\nn: 普通攻击, e: 元素战技, q: 元素爆发",
|
||||
"usage_3": "获取天赋ID:/talent getid",
|
||||
"lower_16": "无效的天赋等级,天赋等级应小于等于15。",
|
||||
"set_id": "将天赋等级设为 %s。",
|
||||
"set_atk": "将普通攻击等级设为 %s。",
|
||||
"set_e": "将元素战技等级设为 %s。",
|
||||
"set_q": "将元素爆发等级设为 %s。",
|
||||
"invalid_skill_id": "无效的技能ID。",
|
||||
"set_this": "将天赋等级设为 %s。",
|
||||
"invalid_level": "无效的天赋等级。",
|
||||
"normal_attack_id": "普通攻击的 ID 为 %s。",
|
||||
"e_skill_id": "元素战技ID %s。",
|
||||
"q_skill_id": "元素爆发ID %s。",
|
||||
"description": "设置当前角色的天赋等级"
|
||||
},
|
||||
"team": {
|
||||
"usage": "用法:team <add|remove|set> [角色ID,...] [索引|first|last|索引-索引,...]",
|
||||
"invalid_usage": "无效用法。",
|
||||
"add_usage": "用法 (add):team add <角色ID,...> [索引]",
|
||||
"invalid_index": "无效索引。",
|
||||
"add_too_much": "服务端仅允许你队伍里至多有 %d 名角色。",
|
||||
"failed_to_add_avatar": "无法根据ID %s 添加角色。",
|
||||
"remove_usage": "用法 (remove):team remove <索引|first|last|索引-索引,...>",
|
||||
"failed_to_parse_index": "无法解析索引:%s",
|
||||
"remove_too_much": "你不能删除那么多角色,你的队伍列表将会变空。",
|
||||
"ignore_index": "忽略的索引列表:%s",
|
||||
"set_usage": "用法 (set):team set <索引> <角色ID>",
|
||||
"index_out_of_range": "你指定的索引超出了范围。",
|
||||
"failed_parse_avatar_id": "无法解析的角色ID:%s",
|
||||
"avatar_already_in_team": "角色已经在你的队伍中了。",
|
||||
"avatar_not_found": "无法找到该角色:%d",
|
||||
"description": "手动修改你的队伍"
|
||||
},
|
||||
"teleportAll": {
|
||||
"success": "已将所有玩家传送到你的位置。",
|
||||
"error": "你只能在多人游戏状态下执行此命令。",
|
||||
"description": "将你世界中的所有玩家传送到你所在的位置"
|
||||
},
|
||||
"teleport": {
|
||||
"usage_server": "用法:tp @<玩家ID> <x> <y> <z> [场景ID]",
|
||||
"usage": "用法:tp [@<玩家ID>] <x> <y> <z> [场景ID]",
|
||||
"specify_player_id": "你必须指定一个玩家ID。",
|
||||
"invalid_position": "无效的位置。",
|
||||
"success": "传送 %s 到坐标 %s, %s, %s,场景为 %s。",
|
||||
"description": "改变指定玩家的位置"
|
||||
},
|
||||
"weather": {
|
||||
"usage": "用法:weather <天气ID> [气候ID]",
|
||||
"success": "已更改天气为 %s,气候为 %s。",
|
||||
"invalid_id": "无效的天气ID。",
|
||||
"description": "更改天气"
|
||||
},
|
||||
"drop": {
|
||||
"command_usage": "用法:drop <物品ID|物品名称> [数量]",
|
||||
"success": "已丢下 %s 个 %s。",
|
||||
"description": "在你附近丢下一个物品"
|
||||
},
|
||||
"help": {
|
||||
"usage": "用法:",
|
||||
"aliases": "别名:",
|
||||
"available_commands": "可用命令:",
|
||||
"description": "发送帮助信息或显示指定命令的信息"
|
||||
},
|
||||
"restart": {
|
||||
"description": "重新启动服务器"
|
||||
},
|
||||
"unlocktower": {
|
||||
"success": "解锁完成。",
|
||||
"description": "解锁深境螺旋的所有层"
|
||||
}
|
||||
},
|
||||
"gacha": {
|
||||
"details": {
|
||||
"title": "祈愿详情",
|
||||
"available_five_stars": "可获得的5星物品",
|
||||
"available_four_stars": "可获得的4星物品",
|
||||
"available_three_stars": "可获得的3星物品"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -335,9 +335,6 @@
|
||||
"success": "傳送 %s 到座標 %s,%s,%s ,場景為 %s 。",
|
||||
"description": "將玩家的位置傳送到你所指定的座標。"
|
||||
},
|
||||
"tower": {
|
||||
"unlock_done": "解鎖所有級別的深境螺旋已全部解鎖。"
|
||||
},
|
||||
"weather": {
|
||||
"usage": "用法:weather <weatherId> [climateId]",
|
||||
"success": "已將當前天氣設定為 %s ,氣候則為 %s 。",
|
||||
@ -361,9 +358,6 @@
|
||||
"unlocktower": {
|
||||
"success": "解鎖完成。",
|
||||
"description": "解鎖所有級別的深境螺旋。"
|
||||
},
|
||||
"resetshop": {
|
||||
"description": "重置商店刷新時間。"
|
||||
}
|
||||
},
|
||||
"gacha": {
|
||||
|
@ -9,7 +9,7 @@
|
||||
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
|
||||
<file>logs/latest.log</file>
|
||||
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
|
||||
<fileNamePattern>logs/log.%d{yyyy-MM-dd}_%d{HH}.log.tar.gz</fileNamePattern>
|
||||
<fileNamePattern>logs/log.%d{yyyy-MM-dd}_%d{HH}.log.gz</fileNamePattern>
|
||||
<maxHistory>24</maxHistory>
|
||||
</rollingPolicy>
|
||||
<encoder>
|
||||
|
Loading…
Reference in New Issue
Block a user