Commit 01b190bc authored by Magix's avatar Magix Committed by GitHub
Browse files

UPGRADE TO 1.1.0 POG

Merge `development` into `stable`
parents 6b81b888 1beddf16
package emu.grasscutter.task;
import emu.grasscutter.Grasscutter;
import org.quartz.*;
import org.quartz.impl.StdSchedulerFactory;
import org.reflections.Reflections;
import java.util.*;
@SuppressWarnings({"UnusedReturnValue", "unused"})
public final class TaskMap {
private final Map<String, TaskHandler> tasks = new HashMap<>();
private final Map<String, Task> annotations = new HashMap<>();
private final Map<String, TaskHandler> afterReset = new HashMap<>();
private final SchedulerFactory schedulerFactory = new StdSchedulerFactory();
public TaskMap() {
this(false);
}
public TaskMap(boolean scan) {
if (scan) this.scan();
}
public static TaskMap getInstance() {
return Grasscutter.getGameServer().getTaskMap();
}
public void resetNow() {
// Unregister all tasks
for (TaskHandler task : this.tasks.values()) {
unregisterTask(task);
}
// Run all afterReset tasks
for (TaskHandler task : this.afterReset.values()) {
try {
task.restartExecute();
} catch (Exception e) {
e.printStackTrace();
}
}
// Remove all afterReset tasks
this.afterReset.clear();
// Register all tasks
for (TaskHandler task : this.tasks.values()) {
registerTask(task.getClass().getAnnotation(Task.class).taskName(), task);
}
}
public TaskMap unregisterTask(TaskHandler task) {
this.tasks.remove(task.getClass().getAnnotation(Task.class).taskName());
this.annotations.remove(task.getClass().getAnnotation(Task.class).taskName());
try {
Scheduler scheduler = schedulerFactory.getScheduler();
scheduler.deleteJob(new JobKey(task.getClass().getAnnotation(Task.class).taskName()));
} catch (SchedulerException e) {
e.printStackTrace();
}
task.onDisable();
return this;
}
public TaskMap registerTask(String taskName, TaskHandler task) {
Task annotation = task.getClass().getAnnotation(Task.class);
this.annotations.put(taskName, annotation);
this.tasks.put(taskName, task);
// register task
try {
Scheduler scheduler = schedulerFactory.getScheduler();
JobDetail job = JobBuilder
.newJob(task.getClass())
.withIdentity(taskName)
.build();
Trigger convTrigger = TriggerBuilder.newTrigger()
.withIdentity(annotation.triggerName())
.withSchedule(CronScheduleBuilder.cronSchedule(annotation.taskCronExpression()))
.build();
scheduler.scheduleJob(job, convTrigger);
if (annotation.executeImmediately()) {
task.execute(null);
}
task.onEnable();
} catch (SchedulerException e) {
e.printStackTrace();
}
return this;
}
public List<TaskHandler> getHandlersAsList() {
return new LinkedList<>(this.tasks.values());
}
public HashMap<String, TaskHandler> getHandlers() {
return new LinkedHashMap<>(this.tasks);
}
public TaskHandler getHandler(String taskName) {
return this.tasks.get(taskName);
}
private void scan() {
Reflections reflector = Grasscutter.reflector;
Set<Class<?>> classes = reflector.getTypesAnnotatedWith(Task.class);
classes.forEach(annotated -> {
try {
Task taskData = annotated.getAnnotation(Task.class);
Object object = annotated.newInstance();
if (object instanceof TaskHandler) {
this.registerTask(taskData.taskName(), (TaskHandler) object);
if (taskData.executeImmediatelyAfterReset()) {
this.afterReset.put(taskData.taskName(), (TaskHandler) object);
}
} else {
Grasscutter.getLogger().error("Class " + annotated.getName() + " is not a TaskHandler!");
}
} catch (Exception exception) {
Grasscutter.getLogger().error("Failed to register task handler for " + annotated.getSimpleName(), exception);
}
});
try {
Scheduler scheduler = schedulerFactory.getScheduler();
scheduler.start();
} catch (SchedulerException e) {
e.printStackTrace();
}
}
}
package emu.grasscutter.task.tasks;
import emu.grasscutter.Grasscutter;
import emu.grasscutter.task.Task;
import emu.grasscutter.task.TaskHandler;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
@Task(taskName = "MoonCard", taskCronExpression = "0 0 0 * * ?", triggerName = "MoonCardTrigger")
// taskCronExpression: Fixed time period: 0:0:0 every day (twenty-four hour system)
public final class MoonCard extends TaskHandler {
@Override
public void onEnable() {
Grasscutter.getLogger().debug("[Task] MoonCard task enabled.");
}
@Override
public void onDisable() {
Grasscutter.getLogger().debug("[Task] MoonCard task disabled.");
}
@Override
public synchronized void execute(JobExecutionContext context) throws JobExecutionException {
Grasscutter.getGameServer().getPlayers().forEach((uid, player) -> {
if (player.isOnline()) {
if (player.inMoonCard()) {
player.getTodayMoonCard();
}
}
});
}
}
package emu.grasscutter.tools;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.FilenameFilter;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.nio.charset.StandardCharsets;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.*;
import java.util.stream.Collectors;
import com.google.gson.reflect.TypeToken;
import emu.grasscutter.GenshinConstants;
import emu.grasscutter.GameConstants;
import emu.grasscutter.Grasscutter;
import emu.grasscutter.data.GenshinData;
import emu.grasscutter.command.Command;
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;
......@@ -29,13 +31,74 @@ import emu.grasscutter.data.def.SceneData;
import emu.grasscutter.utils.Utils;
public final class Tools {
public static void createGmHandbook() throws Exception {
ToolsWithLanguageOption.createGmHandbook(getLanguageOption());
}
public static void createGachaMapping(String location) throws Exception {
ToolsWithLanguageOption.createGachaMapping(location, getLanguageOption());
}
public static List<String> getAvailableLanguage() throws Exception {
File textMapFolder = new File(Grasscutter.getConfig().RESOURCE_FOLDER + "TextMap");
List<String> availableLangList = new ArrayList<String>();
for (String textMapFileName : textMapFolder.list(new FilenameFilter() {
@Override
public boolean accept(File dir, String name) {
if (name.startsWith("TextMap") && name.endsWith(".json")){
return true;
}
return false;
}
})) {
availableLangList.add(textMapFileName.replace("TextMap","").replace(".json","").toLowerCase());
}
return availableLangList;
}
public static String getLanguageOption() throws Exception {
List<String> availableLangList = getAvailableLanguage();
// Use system out for better format
if (availableLangList.size() == 1) {
return availableLangList.get(0).toUpperCase();
}
String stagedMessage = "";
stagedMessage += "The following languages mappings are available, please select one: [default: EN]\n";
String groupedLangList = ">\t";
int groupedLangCount = 0;
String input = "";
for (String availableLanguage: availableLangList){
groupedLangCount++;
groupedLangList = groupedLangList + "" + availableLanguage + "\t";
if (groupedLangCount == 6) {
stagedMessage += groupedLangList + "\n";
groupedLangCount = 0;
groupedLangList = ">\t";
}
}
if (groupedLangCount > 0) {
stagedMessage += groupedLangList + "\n";
}
stagedMessage += "\nYour choice:[EN] ";
input = Grasscutter.getConsole().readLine(stagedMessage);
if (availableLangList.contains(input.toLowerCase())) {
return input.toUpperCase();
}
Grasscutter.getLogger().info("Invalid option. Will use EN(English) as fallback");
return "EN";
}
}
final class ToolsWithLanguageOption {
@SuppressWarnings("deprecation")
public static void createGmHandbook() throws Exception {
public static void createGmHandbook(String language) throws Exception {
ResourceLoader.loadResources();
Map<Long, String> map;
try (InputStreamReader fileReader = new InputStreamReader(new FileInputStream(Utils.toFilePath(Grasscutter.getConfig().RESOURCE_FOLDER + "TextMap/TextMapEN.json")), StandardCharsets.UTF_8)) {
try (InputStreamReader fileReader = new InputStreamReader(new FileInputStream(Utils.toFilePath(Grasscutter.getConfig().RESOURCE_FOLDER + "TextMap/TextMap"+language+".json")), StandardCharsets.UTF_8)) {
map = Grasscutter.getGsonFactory().fromJson(fileReader, new TypeToken<Map<Long, String>>() {}.getType());
}
......@@ -45,52 +108,161 @@ public final class Tools {
DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm:ss");
LocalDateTime now = LocalDateTime.now();
writer.println("// Genshin Impact " + GenshinConstants.VERSION + " GM Handbook");
writer.println("// Grasscutter " + GameConstants.VERSION + " GM Handbook");
writer.println("// Created " + dtf.format(now) + System.lineSeparator() + System.lineSeparator());
list = new ArrayList<>(GenshinData.getAvatarDataMap().keySet());
CommandMap cmdMap = new CommandMap(true);
List<Command> cmdList = new ArrayList<>(cmdMap.getAnnotationsAsList());
writer.println("// Commands");
for (Command cmd : cmdList) {
String cmdName = cmd.label();
while (cmdName.length() <= 15) {
cmdName = " " + cmdName;
}
writer.println(cmdName + " : " + cmd.description());
}
writer.println();
list = new ArrayList<>(GameData.getAvatarDataMap().keySet());
Collections.sort(list);
writer.println("// Avatars");
for (Integer id : list) {
AvatarData data = GenshinData.getAvatarDataMap().get(id);
AvatarData data = GameData.getAvatarDataMap().get(id);
writer.println(data.getId() + " : " + map.get(data.getNameTextMapHash()));
}
writer.println();
list = new ArrayList<>(GenshinData.getItemDataMap().keySet());
list = new ArrayList<>(GameData.getItemDataMap().keySet());
Collections.sort(list);
writer.println("// Items");
for (Integer id : list) {
ItemData data = GenshinData.getItemDataMap().get(id);
ItemData data = GameData.getItemDataMap().get(id);
writer.println(data.getId() + " : " + map.get(data.getNameTextMapHash()));
}
writer.println();
writer.println("// Scenes");
list = new ArrayList<>(GenshinData.getSceneDataMap().keySet());
list = new ArrayList<>(GameData.getSceneDataMap().keySet());
Collections.sort(list);
for (Integer id : list) {
SceneData data = GenshinData.getSceneDataMap().get(id);
SceneData data = GameData.getSceneDataMap().get(id);
writer.println(data.getId() + " : " + data.getScriptData());
}
writer.println();
writer.println("// Monsters");
list = new ArrayList<>(GenshinData.getMonsterDataMap().keySet());
list = new ArrayList<>(GameData.getMonsterDataMap().keySet());
Collections.sort(list);
for (Integer id : list) {
MonsterData data = GenshinData.getMonsterDataMap().get(id);
MonsterData data = GameData.getMonsterDataMap().get(id);
writer.println(data.getId() + " : " + map.get(data.getNameTextMapHash()));
}
}
Grasscutter.getLogger().info("GM Handbook generated!");
}
@SuppressWarnings("deprecation")
public static void createGachaMapping(String location, String language) throws Exception {
ResourceLoader.loadResources();
Map<Long, String> map;
try (InputStreamReader fileReader = new InputStreamReader(new FileInputStream(Utils.toFilePath(Grasscutter.getConfig().RESOURCE_FOLDER + "TextMap/TextMap"+language+".json")), StandardCharsets.UTF_8)) {
map = Grasscutter.getGsonFactory().fromJson(fileReader, new TypeToken<Map<Long, String>>() {}.getType());
}
List<Integer> list;
String fileName = location;
try (PrintWriter writer = new PrintWriter(new OutputStreamWriter(new FileOutputStream(fileName), StandardCharsets.UTF_8), false)) {
list = new ArrayList<>(GameData.getAvatarDataMap().keySet());
Collections.sort(list);
// 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.
writer.println("mappings = {\"en-us\": {");
// 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 {
writer.print(",");
}
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
writer.println(
"\"" + (avatarID % 1000 + 1000) + "\" : [\""
+ map.get(data.getNameTextMapHash()) + "(" + map.get(4233146695L)+ ")\", \""
+ color + "\"]");
}
writer.println();
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
writer.println(",\"" + data.getId() +
"\" : [\"" + map.get(data.getNameTextMapHash()).replaceAll("\"", "")
+ "("+ map.get(4231343903L)+")\",\""+ color + "\"]");
}
writer.println(",\"200\": \""+map.get(332935371L)+"\", \"301\": \""+ map.get(2272170627L) + "\", \"302\": \""+map.get(2864268523L)+"\"");
writer.println("}\n}");
}
Grasscutter.getLogger().info("Mappings generated to " + location + " !");
}
}
......@@ -34,7 +34,7 @@ public final class Crypto {
public static void extractSecretKeyBuffer(byte[] data) {
try {
GetPlayerTokenRsp p = GetPlayerTokenRsp.parseFrom(data);
FileUtils.write(Grasscutter.getConfig().KEY_FOLDER + "secretKeyBuffer.bin", p.getSecretKeyBuffer().toByteArray());
FileUtils.write(Grasscutter.getConfig().KEY_FOLDER + "secretKeyBuffer.bin", p.getSecretKeyBytes().toByteArray());
Grasscutter.getLogger().info("Secret Key: " + p.getSecretKey());
} catch (Exception e) {
Grasscutter.getLogger().error("Crypto error.", e);
......
package emu.grasscutter.utils;
import java.util.Date;
import java.util.Calendar;
public final class DateHelper {
public static Date onlyYearMonthDay(Date now) {
Calendar calendar = Calendar.getInstance();
calendar.setTime(now);
calendar.set(Calendar.HOUR_OF_DAY, 0);
calendar.set(Calendar.MINUTE, 0);
calendar.set(Calendar.SECOND, 0);
calendar.set(Calendar.MILLISECOND, 0);
return calendar.getTime();
}
}
package emu.grasscutter.utils;
import emu.grasscutter.server.event.Event;
public interface EventConsumer<T extends Event> {
void consume(T event);
}
package emu.grasscutter.utils;
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.core.ConsoleAppender;
import emu.grasscutter.Grasscutter;
import org.jline.reader.LineReader;
import java.util.Arrays;
public class JlineLogbackAppender extends ConsoleAppender<ILoggingEvent> {
@Override
protected void append(ILoggingEvent eventObject) {
if (!started) {
return;
}
Arrays.stream(
new String(encoder.encode(eventObject)).split("\n\r")
).forEach(Grasscutter.getConsole()::printAbove);
}
}
package emu.grasscutter.utils;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import emu.grasscutter.Grasscutter;
import javax.annotation.Nullable;
import java.io.InputStream;
import java.util.HashMap;
import java.util.Map;
public final class Language {
private final JsonObject languageData;
private final Map<String, String> cachedTranslations = new HashMap<>();
/**
* Creates a language instance from a code.
* @param langCode The language code.
* @return A language instance.
*/
public static Language getLanguage(String langCode) {
return new Language(langCode + ".json", Grasscutter.getConfig().DefaultLanguage.toLanguageTag());
}
/**
* Returns the translated value from the key while substituting arguments.
* @param key The key of the translated value to return.
* @param args The arguments to substitute.
* @return A translated value with arguments substituted.
*/
public static String translate(String key, Object... args) {
String translated = Grasscutter.getLanguage().get(key);
try {
return translated.formatted(args);
} catch (Exception exception) {
Grasscutter.getLogger().error("Failed to format string: " + key, exception);
return translated;
}
}
/**
* Reads a file and creates a language instance.
* @param fileName The name of the language file.
*/
private Language(String fileName, String fallback) {
@Nullable JsonObject languageData = null;
try {
InputStream file = Grasscutter.class.getResourceAsStream("/languages/" + fileName);
if(file == null) {
file = Grasscutter.class.getResourceAsStream("/languages/" + fallback);
}
languageData = Grasscutter.getGsonFactory().fromJson(Utils.readFromInputStream(file), JsonObject.class);
} catch (Exception exception) {
Grasscutter.getLogger().warn("Failed to load language file: " + fileName, exception);
}
this.languageData = languageData;
}
/**
* Returns the value (as a string) from a nested key.
* @param key The key to look for.
* @return The value (as a string) from a nested key.
*/
public String get(String key) {
if(this.cachedTranslations.containsKey(key)) {
return this.cachedTranslations.get(key);
}
String[] keys = key.split("\\.");
JsonObject object = this.languageData;
int index = 0;
String result = "This value does not exist. Please report this to the Discord: " + key;
while (true) {
if(index == keys.length) break;
String currentKey = keys[index++];
if(object.has(currentKey)) {
JsonElement element = object.get(currentKey);
if(element.isJsonObject())
object = element.getAsJsonObject();
else {
result = element.getAsString(); break;
}
} else break;
}
this.cachedTranslations.put(key, result); return result;
}
}
package emu.grasscutter.utils;
public class MessageHandler {
private String message;
public MessageHandler(){
this.message = "";
}
public void append(String message){
this.message += message + "\r\n\r\n";
}
public String getMessage(){
return this.message;
}
public void setMessage(String message){
this.message = message;
}
}
......@@ -2,8 +2,10 @@ package emu.grasscutter.utils;
import java.io.Serializable;
import dev.morphia.annotations.Entity;
import emu.grasscutter.net.proto.VectorOuterClass.Vector;
@Entity
public class Position implements Serializable {
private static final long serialVersionUID = -2001232313615923575L;
......
package emu.grasscutter.utils;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.StandardCopyOption;
import java.time.*;
import java.time.temporal.TemporalAdjusters;
import java.util.HashMap;
import java.util.Map;
import java.util.Random;
import emu.grasscutter.Config;
......@@ -10,8 +15,11 @@ import emu.grasscutter.Grasscutter;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufUtil;
import io.netty.buffer.Unpooled;
import org.slf4j.Logger;
import static emu.grasscutter.utils.Language.translate;
@SuppressWarnings({"UnusedReturnValue", "BooleanMethodIsAlwaysInverted"})
public final class Utils {
public static final Random random = new Random();
......@@ -24,6 +32,19 @@ public final class Utils {
return random.nextFloat() * (max - min) + min;
}
public static double getDist(Position pos1, Position pos2) {
double xs = pos1.getX() - pos2.getX();
xs = xs * xs;
double ys = pos1.getY() - pos2.getY();
ys = ys * ys;
double zs = pos1.getZ() - pos2.getZ();
zs = zs * zs;
return Math.sqrt(xs + zs + ys);
}
public static int getCurrentSeconds() {
return (int) (System.currentTimeMillis() / 1000.0);
}
......@@ -51,6 +72,7 @@ public final class Utils {
private static final char[] HEX_ARRAY = "0123456789abcdef".toCharArray();
public static String bytesToHex(byte[] bytes) {
if (bytes == null) return "";
char[] hexChars = new char[bytes.length * 2];
for (int j = 0; j < bytes.length; j++) {
int v = bytes[j] & 0xFF;
......@@ -137,6 +159,15 @@ public final class Utils {
return nonNull != null ? nonNull : fallback;
}
/**
* Logs an object to the console.
* @param object The object to log.
*/
public static void logObject(Object object) {
String asJson = Grasscutter.getGsonFactory().toJson(object);
Grasscutter.getLogger().info(asJson);
}
/**
* Checks for required files and folders before startup.
*/
......@@ -150,15 +181,15 @@ public final class Utils {
// Check for resources folder.
if(!fileExists(resourcesFolder)) {
logger.info("Creating resources folder...");
logger.info("Place a copy of 'GenshinData' in the resources folder.");
logger.info(translate("messages.status.create_resources"));
logger.info(translate("messages.status.resources_error"));
createFolder(resourcesFolder); exit = true;
}
// Check for GenshinData.
// Check for BinOutput + ExcelBinOutput.
if(!fileExists(resourcesFolder + "BinOutput") ||
!fileExists(resourcesFolder + "ExcelBinOutput")) {
logger.info("Place a copy of 'GenshinData' in the resources folder.");
logger.info(translate("messages.status.resources_error"));
exit = true;
}
......@@ -168,4 +199,105 @@ public final class Utils {
if(exit) System.exit(1);
}
/**
* Gets the timestamp of the next hour.
* @return The timestamp in UNIX seconds.
*/
public static int getNextTimestampOfThisHour(int hour, String timeZone, int param) {
ZonedDateTime zonedDateTime = ZonedDateTime.now(ZoneId.of(timeZone));
for (int i = 0; i < param; i ++){
if (zonedDateTime.getHour() < hour) {
zonedDateTime = zonedDateTime.withHour(hour).withMinute(0).withSecond(0);
} else {
zonedDateTime = zonedDateTime.plusDays(1).withHour(hour).withMinute(0).withSecond(0);
}
}
return (int) zonedDateTime.toInstant().atZone(ZoneOffset.UTC).toEpochSecond();
}
/**
* Gets the timestamp of the next hour in a week.
* @return The timestamp in UNIX seconds.
*/
public static int getNextTimestampOfThisHourInNextWeek(int hour, String timeZone, int param) {
ZonedDateTime zonedDateTime = ZonedDateTime.now(ZoneId.of(timeZone));
for (int i = 0; i < param; i++) {
if (zonedDateTime.getDayOfWeek() == DayOfWeek.MONDAY && zonedDateTime.getHour() < hour) {
zonedDateTime = ZonedDateTime.now(ZoneId.of(timeZone)).withHour(hour).withMinute(0).withSecond(0);
} else {
zonedDateTime = zonedDateTime.with(TemporalAdjusters.next(DayOfWeek.MONDAY)).withHour(hour).withMinute(0).withSecond(0);
}
}
return (int) zonedDateTime.toInstant().atZone(ZoneOffset.UTC).toEpochSecond();
}
/**
* Gets the timestamp of the next hour in a month.
* @return The timestamp in UNIX seconds.
*/
public static int getNextTimestampOfThisHourInNextMonth(int hour, String timeZone, int param) {
ZonedDateTime zonedDateTime = ZonedDateTime.now(ZoneId.of(timeZone));
for (int i = 0; i < param; i++) {
if (zonedDateTime.getDayOfMonth() == 1 && zonedDateTime.getHour() < hour) {
zonedDateTime = ZonedDateTime.now(ZoneId.of(timeZone)).withHour(hour).withMinute(0).withSecond(0);
} else {
zonedDateTime = zonedDateTime.with(TemporalAdjusters.firstDayOfNextMonth()).withHour(hour).withMinute(0).withSecond(0);
}
}
return (int) zonedDateTime.toInstant().atZone(ZoneOffset.UTC).toEpochSecond();
}
/**
* Retrieves a string from an input stream.
* @param stream The input stream.
* @return The string.
*/
public static String readFromInputStream(InputStream stream) {
StringBuilder stringBuilder = new StringBuilder();
try (BufferedReader reader = new BufferedReader(new InputStreamReader(stream, StandardCharsets.UTF_8))) {
String line; while ((line = reader.readLine()) != null) {
stringBuilder.append(line);
} stream.close();
} catch (IOException e) {
Grasscutter.getLogger().warn("Failed to read from input stream.");
} return stringBuilder.toString();
}
/**
* Switch properties from upper case to lower case?
*/
public static Map<String, Object> switchPropertiesUpperLowerCase(Map<String, Object> objMap, Class<?> cls) {
Map<String, Object> map = new HashMap<>(objMap.size());
for (String key : objMap.keySet()) {
try {
char c = key.charAt(0);
if (c >= 'a' && c <= 'z') {
try {
cls.getDeclaredField(key);
map.put(key, objMap.get(key));
} catch (NoSuchFieldException e) {
String s1 = String.valueOf(c).toUpperCase();
String after = key.length() > 1 ? s1 + key.substring(1) : s1;
cls.getDeclaredField(after);
map.put(after, objMap.get(key));
}
} else if (c >= 'A' && c <= 'Z') {
try {
cls.getDeclaredField(key);
map.put(key, objMap.get(key));
} catch (NoSuchFieldException e) {
String s1 = String.valueOf(c).toLowerCase();
String after = key.length() > 1 ? s1 + key.substring(1) : s1;
cls.getDeclaredField(after);
map.put(after, objMap.get(key));
}
}
} catch (NoSuchFieldException e) {
map.put(key, objMap.get(key));
}
}
return map;
}
}
{
"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."
},
"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."
}
},
"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."
}
},
"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.",
"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]"
},
"broadcast": {
"command_usage": "Usage: broadcast <message>",
"message_sent": "Message sent."
},
"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."
},
"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."
},
"coop": {
"usage": "Usage: coop <playerId> <target playerId>",
"success": "Summoned %s to %s's world."
},
"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"
},
"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."
},
"giveArtifact": {
"usage": "Usage: giveart|gart [player] <artifactId> <mainPropId> [<appendPropId>[,<times>]]... [level]",
"id_error": "Invalid artifact ID.",
"success": "Given %s to %s."
},
"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."
},
"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"
},
"godmode": {
"success": "Godmode is now %s for %s."
},
"heal": {
"success": "All characters have been healed."
},
"kick": {
"player_kick_player": "Player [%s:%s] has kicked player [%s:%s]",
"server_kick_player": "Kicking player [%s:%s]"
},
"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"
},
"killCharacter": {
"usage": "Usage: /killcharacter [playerId]",
"success": "Killed %s's current character."
},
"list": {
"success": "There are %s player(s) online:"
},
"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."
},
"position": {
"success": "Coordinates: %s, %s, %s\nScene id: %s"
},
"reload": {
"reload_start": "Reloading config.",
"reload_done": "Reload complete."
},
"resetConst": {
"reset_all": "Reset all avatars' constellations.",
"success": "Constellations for %s have been reset. Please relog to see changes."
},
"resetShopLimit": {
"usage": "Usage: /resetshop <player id>"
},
"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."
},
"sendMessage": {
"usage": "Usage: sendmessage <player> <message>",
"success": "Message sent."
},
"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."
},
"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."
},
"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."
},
"spawn": {
"usage": "Usage: spawn <entityId> [amount] [level(monster only)]",
"success": "Spawned %s of %s."
},
"stop": {
"success": "Server shutting down..."
},
"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."
},
"teleportAll": {
"success": "Summoned all players to your location.",
"error": "You only can use this command in MP mode."
},
"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"
},
"weather": {
"usage": "Usage: weather <weatherId> [climateId]",
"success": "Changed weather to %s with climate %s",
"invalid_id": "Invalid ID."
},
"drop": {
"command_usage": "Usage: drop <itemId|itemName> [amount]",
"success": "Dropped %s of %s."
},
"help": {
"usage": "Usage: ",
"aliases": "Aliases: ",
"available_commands": "Available commands: "
}
}
}
{
"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"
},
"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 正在尝试使用令牌登录",
"login_token_error": "[Dispatch] 客户端 %s 使用令牌登录失败",
"login_token_success": "[Dispatch] 客户端 %s 已通过令牌登录,UID为 %s",
"combo_token_success": "[Dispatch] 客户端 %s 交换令牌成功",
"combo_token_error": "[Dispatch] 客户端 %s 交换令牌失败",
"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 是免费开源软件。如果你是付费购买的,那已经被骗了。Github: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 目录。"
}
},
"commands": {
"generic": {
"not_specified": "没有指定命令。",
"unknown_command": "未知的命令:%s",
"permission_error": "您没有执行此命令的权限。",
"console_execute_error": "此命令只能在服务器控制台执行。",
"player_execute_error": "此命令只能在游戏内执行。",
"command_exist_error": "找不到命令。",
"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> <username> [uid]"
},
"broadcast": {
"command_usage": "用法:broadcast <消息>",
"message_sent": "公告已发送。"
},
"changescene": {
"usage": "用法:changescene <scene id>",
"already_in_scene": "你已经在这个秘境中了。",
"success": "已切换至秘境 %s.",
"exists_error": "此秘境不存在。"
},
"clear": {
"command_usage": "用法: clear <all|wp|art|mat>",
"weapons": "已将 %s 的武器清空。",
"artifacts": "已将 %s 的圣遗物清空。",
"materials": "已将 %s 的材料清空。",
"furniture": "已将 %s 的尘歌壶家具清空。",
"displays": "已清除 %s 的显示。",
"virtuals": "已将 %s 的所有货币和经验值清空。",
"everything": "已将 %s 的所有物品清空。"
},
"coop": {
"usage": "用法:coop <playerId> <target playerId>",
"success": "已召唤 %s 到 %s的世界"
},
"enter_dungeon": {
"usage": "用法:enterdungeon <dungeon id>",
"changed": "已进入秘境 %s",
"not_found_error": "此秘境不存在。",
"in_dungeon_error": "你已经在秘境中了。"
},
"giveAll": {
"usage": "用法:giveall [player] [amount]",
"started": "正在给予全部物品...",
"success": "已给予全部物品。",
"invalid_amount_or_playerId": "无效的数量/玩家ID。"
},
"giveArtifact": {
"usage": "用法:giveart|gart [player] <artifactId> <mainPropId> [<appendPropId>[,<times>]]... [level]",
"id_error": "无效的圣遗物ID。",
"success": "已将 %s 给予 %s。"
},
"giveChar": {
"usage": "用法:givechar <player> <itemId|itemName> [amount]",
"given": "Given %s with level %s to %s.",
"invalid_avatar_id": "无效的角色ID。",
"invalid_avatar_level": "无效的角色等級。.",
"invalid_avatar_or_player_id": "无效的角色ID/玩家ID。"
},
"give": {
"usage": "用法:give <player> <itemId|itemName> [amount] [level] [refinement]",
"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"
},
"godmode": {
"success": "上帝模式被设置为 %s 。 [用户:%s]"
},
"heal": {
"success": "所有角色已被治疗。"
},
"kick": {
"player_kick_player": "玩家 [%s:%s] 已将 [%s:%s] 踢出",
"server_kick_player": "正在踢出玩家 [%s:%s]"
},
"kill": {
"usage": "用法:killall [playerUid] [sceneId]",
"scene_not_found_in_player_world": "未在玩家世界中找到此场景",
"kill_monsters_in_scene": "已杀死 %s 个怪物。 [场景ID: %s]"
},
"killCharacter": {
"usage": "用法:/killcharacter [playerId]",
"success": "已杀死 %s 目前使用的角色。"
},
"list": {
"message": "目前在线人数:%s"
},
"permission": {
"usage": "用法:permission <add|remove> <username> <permission>",
"add": "已设置权限。",
"has_error": "此玩家已拥有此权限!",
"remove": "权限已移除。",
"not_have_error": "此玩家未拥有权限!",
"account_error": "账号不存在!"
},
"position": {
"success": "坐标:%.3f, %.3f, %.3f\n场景ID:%d"
},
"reload": {
"reload_start": "正在重载配置文件和数据。",
"reload_done": "重装完毕。"
},
"resetConst": {
"reset_all": "重置所有角色的命座。",
"success": "已重置 %s 的命座,重新登录后将会生效。"
},
"resetShopLimit": {
"usage": "用法:/resetshop <player id>"
},
"sendMail": {
"usage": "用法:give [player] <itemId|itemName> [amount]",
"user_not_exist": "ID '%s' 的使用者不存在。",
"start_composition": "发送邮件流程。\n请使用`/send <标题>`前进到下一步。\n你可以在任何时间使用`/sendmail stop`来停止发送。",
"templates": "邮件模板尚未实装...",
"invalid_arguments": "无效的参数。\n指令使用方法 `/sendmail <userId|all|help> [templateId]`",
"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 <content>' 来设置邮件内容。",
"set_contents": "成功将'%s'设置为邮件内容。\n使用 '/sendmail <发件人>' 来设置发件人。",
"set_message_sender": "发件人已设置为 '%s'。\n使用 '/sendmail <itemId|itemName|finish> [amount] [level]' 来添加附件。",
"send": "已添加 %s 个 %s (等級为 %s) 邮件附件。\n如果没有要继续添加的道具请使用 `/sendmail finish` 来完成邮件发送。",
"invalid_arguments_please_use": "错误的参数 \n请使用 `/sendmail %s`",
"title": "<标题>",
"message": "<正文>",
"sender": "<发件人>",
"arguments": "<itemId|itemName|finish> [数量] [等级]",
"error": "错误:无效的编写阶段 %s。需要 StackTrace 请查看服务器控制台。"
},
"sendMessage": {
"usage": "用法:sendmessage <player> <message>",
"success": "消息已发送。"
},
"setFetterLevel": {
"usage": "用法:setfetterlevel <level>",
"range_error": "好感度等级必须在 0 到 10 之间。",
"fetter_set_level": "好感度已设置为 %s 级",
"level_error": "无效的好感度等级。"
},
"setStats": {
"usage_console": "用法:setstats|stats @<UID> <stat> <value>",
"usage_ingame": "用法:setstats|stats [@UID] <stat> <value>",
"help_message": "\n\t可使用的数据类型:hp (生命值)| maxhp (最大生命值) | def(防御力) | atk (攻击力)| em (元素精通) | er (元素充能效率) | crate(暴击率) | cdmg (暴击伤害)| cdr (冷却缩减) | heal(治疗加成)| heali (受治疗加成)| shield (护盾强效)| defi (无视防御)\n\t(cont.) 元素伤害:epyro (火) | ecryo (冰) | ehydro (水) | egeo (岩) | edendro (草) | eelectro (雷) | ephys (物理)(cont.) 元素抗性: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。"
},
"setWorldLevel": {
"usage": "用法:setworldlevel <level>",
"value_error": "世界等级必须设置在0-8之间。",
"success": "已将世界等级设为%s。",
"invalid_world_level": "无效的世界等级。"
},
"spawn": {
"usage": "用法:spawn <entityId> [amount] [level(仅限怪物]",
"success": "已生成 %s 个 %s。"
},
"stop": {
"success": "正在关闭服务器..."
},
"talent": {
"usage_1": "设置天赋等级:/talent set <talentID> <value>",
"usage_2": "另一种设置天赋等级的命令使用方法:/talent <n or e or q> <value>",
"usage_3": "获取天赋ID指令用法:/talent getid",
"lower_16": "无效的天赋等级,天赋等级应低于16。",
"set_id": "将天赋等级设为 %s。",
"set_atk": "将普通攻击等级设为 %s。",
"set_e": "设定天赋E等级为 %s。",
"set_q": "设定天赋Q等级为 %s。",
"invalid_skill_id": "无效的技能ID。",
"set_this": "将天赋等级设为 %s。",
"invalid_level": "无效的天赋等级。",
"normal_attack_id": "普通攻击的 ID 为 %s。",
"e_skill_id": "E技能ID %s。",
"q_skill_id": "Q技能ID %s。"
},
"teleportAll": {
"success": "已将全部玩家传送到你的位置",
"error": "命令仅限多人游戏使用。"
},
"teleport": {
"usage_server": "用法:/tp @<player id> <x> <y> <z> [scene id]",
"usage": "用法:/tp [@<player id>] <x> <y> <z> [scene id]",
"specify_player_id": "你必须指定一个玩家ID。",
"invalid_position": "无效的位置。",
"success": "传送 %s 到坐标 %s,%s,%s,场景为 %s"
},
"weather": {
"usage": "用法:weather <weatherId> [climateId]",
"success": "已将当前天气设定为 %s,气候则为 %s。",
"invalid_id": "无效的ID。"
},
"drop": {
"command_usage": "用法:drop <itemId|itemName> [amount]",
"success": "已將 %s x %s 丟在附近。"
},
"help": {
"usage": "用法:",
"aliases": "別名:",
"available_commands": "可用指令:"
}
}
}
{
"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。"
},
"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 正在嘗試用憑證登入",
"login_token_error": "[Dispatch] 客戶端 %s 使用憑證登入失敗",
"login_token_success": "[Dispatch] 客戶端 %s 已透過憑證登入,UID為 %s",
"combo_token_success": "[Dispatch] 客戶端 %s 交換憑證成功",
"combo_token_error": "[Dispatch] 客戶端 %s 交換憑證失敗",
"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 是免費開源軟體。如果你已經付錢了,那你可能被騙了。主頁: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 資料夾。"
}
},
"commands": {
"generic": {
"not_specified": "沒有指定指令。",
"unknown_command": "未知的指令:%s",
"permission_error": "您沒有執行此指令的權限。",
"console_execute_error": "此指令只能在伺服器的命令提示字元執行。",
"player_execute_error": "請在遊戲裡使用這條指令。",
"command_exist_error": "找不到指令。",
"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> <username> [uid]"
},
"broadcast": {
"command_usage": "用法:broadcast <message>",
"message_sent": "公告已發送。"
},
"changescene": {
"usage": "用法:changescene <scene id>",
"already_in_scene": "你已經在這個場景中了。",
"success": "已切換至場景 %s.",
"exists_error": "此場景不存在。"
},
"clear": {
"command_usage": "用法: clear <all|wp|art|mat>",
"weapons": "已將 %s 的武器清空。",
"artifacts": "已將 %s 的聖遺物清空。",
"materials": "已將 %s 的材料清空。",
"furniture": "已將 %s 的塵歌壺家具清空。",
"displays": "已清除 %s 的顯示。",
"virtuals": "已將 %s 的所有貨幣和經驗值清空。",
"everything": "已將 %s 的所有物品清空。"
},
"coop": {
"usage": "用法:coop <playerId> <target playerId>",
"success": "召喚了 %s 到 %s 的世界。"
},
"enter_dungeon": {
"usage": "用法:enterdungeon <dungeon id>",
"changed": "已進入副本 %s",
"not_found_error": "此副本不存在。",
"in_dungeon_error": "你已經在祕境中了。"
},
"giveAll": {
"usage": "用法:giveall [player] [amount]",
"started": "正在賦予全部物品...",
"success": "已賦予全部物品。",
"invalid_amount_or_playerId": "無效的數量/玩家ID。"
},
"giveArtifact": {
"usage": "用法:giveart|gart [player] <artifactId> <mainPropId> [<appendPropId>[,<times>]]... [level]",
"id_error": "無效的聖遺物ID。",
"success": "已把 %s 給予 %s。"
},
"giveChar": {
"usage": "用法:givechar <player> <itemId|itemName> [amount]",
"given": "Given %s with level %s to %s.",
"invalid_avatar_id": "無效的角色ID。",
"invalid_avatar_level": "無效的角色等級。.",
"invalid_avatar_or_player_id": "無效的角色ID/玩家ID。"
},
"give": {
"usage": "用法:give <player> <itemId|itemName> [amount] [level]",
"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"
},
"godmode": {
"success": "上帝模式設定為 %s 。 [用戶:%s]"
},
"heal": {
"success": "所有角色已被治療。"
},
"kick": {
"player_kick_player": "玩家 [%s:%s] 已把 [%s:%s] 踢出",
"server_kick_player": "正在踢出玩家 [%s:%s]"
},
"kill": {
"usage": "用法:killall [playerUid] [sceneId]",
"scene_not_found_in_player_world": "未在玩家世界中找到此場景",
"kill_monsters_in_scene": "已殺死 %s 個怪物。 [場景ID: %s]"
},
"killCharacter": {
"usage": "用法:/killcharacter [playerId]",
"success": "已殺死 %s 目前的場上角色。"
},
"list": {
"message": "目前總線上人數:%s"
},
"permission": {
"usage": "用法:permission <add|remove> <username> <permission>",
"add": "已指派權限。",
"has_error": "此玩家已擁有權限!",
"remove": "權限已移除。",
"not_have_error": "此玩家未擁有權限!",
"account_error": "The account cannot be found."
},
"position": {
"success": "坐標:%.3f, %.3f, %.3f\n場景ID:%d"
},
"reload": {
"reload_start": "正在重新加載設定檔。",
"reload_done": "重新加載已完成。"
},
"resetConst": {
"reset_all": "重設所有角色的命座。",
"success": "已重設 %s 的命座,重新登入後將會生效。"
},
"resetShopLimit": {
"usage": "用法:/resetshop <player id>"
},
"sendMail": {
"usage": "用法:give [player] <itemId|itemName> [amount]",
"user_not_exist": "ID '%s' 的使用者不存在。",
"start_composition": "發送郵件流程。\n請使用`/send <郵件標題>`來進到下一步。\n你可以在任何時間使用`/sendmail stop`來停止發送。",
"templates": "郵件模板尚未實裝...",
"invalid_arguments": "無效的參數。\n指令使用方法 `/sendmail <userId|all|help> [templateId]`",
"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 <content>' 來設定郵件內容。",
"set_contents": "成功將'%s'為郵件內容。\n接下來請打出 '/sendmail <寄件者名稱>' 來設定郵件寄件者名稱。",
"set_message_sender": "郵件寄件者已設為 '%s'。\n使用 '/sendmail <itemId|itemName|finish> [amount] [level]' 以繼續操作。",
"send": "已添加 %s 個 %s (等級為 %s) 到郵件附件。\n如果沒有要繼續添加道具請使用 `/sendmail finish` 來完成郵件發送。",
"invalid_arguments_please_use": "Invalid arguments \n Please use `/sendmail %s`",
"title": "<標題>",
"message": "<正文>",
"sender": "<寄件者>",
"arguments": "<itemId|itemName|finish> [數量] [等級]",
"error": "錯誤:無效的編寫階段 %s。需要 stacktrace 請查看伺服器命令提示字元。"
},
"sendMessage": {
"usage": "用法:sendmessage <player> <message>",
"success": "訊息已發送。"
},
"setFetterLevel": {
"usage": "用法:setfetterlevel <level>",
"range_error": "好感度必須在 0 到 10 之間。",
"fetter_set_level": "好感等級已設定為 %s",
"level_error": "無效的好感度。"
},
"setStats": {
"usage_console": "用法:setstats|stats @<UID> <stat> <value>",
"usage_ingame": "用法:setstats|stats [@UID] <stat> <value>",
"help_message": "\n\t可使用的數據類型:hp (生命值)| maxhp (最大生命值) | def(防禦力) | atk (攻擊力)| em (元素精通) | er (元素充能效率) | crate(暴擊率) | cdmg (暴擊傷害)| cdr (冷卻縮減) | heal(治療加成)| heali (受治療加成)| shield (護盾強效)| defi (無視防禦)\n\t(cont.) 元素增傷類:epyro (火傷) | ecryo (冰傷) | ehydro (水傷) | egeo (岩傷) | edendro (草傷) | eelectro (雷傷) | ephys (物傷)(cont.) 元素減傷類: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。"
},
"setWorldLevel": {
"usage": "用法:setworldlevel <level>",
"value_error": "世界等級必須設定在0-8之間。",
"success": "已將世界等級設為%s。",
"invalid_world_level": "無效的世界等級。"
},
"spawn": {
"usage": "用法:spawn <entityId> [amount] [level(僅限怪物)]",
"success": "已生成 %s 個 %s。"
},
"stop": {
"success": "正在關閉伺服器..."
},
"talent": {
"usage_1": "設定天賦等級:/talent set <talentID> <value>",
"usage_2": "另一種設定天賦等級的指令使用方法:/talent <n or e or q> <value>",
"usage_3": "獲取天賦ID指令用法:/talent getid",
"lower_16": "無效的技能等級,技能等級應低於 16。",
"set_id": "將天賦等級設為%s。",
"set_atk": "將普通攻擊等級設為 %s。",
"set_e": "設定天賦E等級至 %s。",
"set_q": "設定天賦Q等級至 %s。",
"invalid_skill_id": "無效的技能ID。",
"set_this": "將天賦等級設為 %s。",
"invalid_level": "無效的天賦等級。",
"normal_attack_id": "普通攻擊的 ID 為 %s。",
"e_skill_id": "E技能ID %s。",
"q_skill_id": "Q技能ID %s。"
},
"teleportAll": {
"success": "召喚了所有玩家到你的位置上。",
"error": "此指令僅可在多人遊戲下可用。"
},
"teleport": {
"usage_server": "用法:/tp @<player id> <x> <y> <z> [scene id]",
"usage": "用法:/tp [@<player id>] <x> <y> <z> [scene id]",
"specify_player_id": "你必須指定一個玩家ID。",
"invalid_position": "無效的位置。",
"success": "傳送 %s 到坐標 %s,%s,%s ,場景為 %s"
},
"weather": {
"usage": "用法:weather <weatherId> [climateId]",
"success": "已將當前天氣設定為 %s ,氣候則為 %s 。",
"invalid_id": "無效的ID。"
},
"drop": {
"command_usage": "用法:drop <itemId|itemName> [amount]",
"success": "已將 %s x %s 丟在附近。"
},
"help": {
"usage": "用法:",
"aliases": "別名:",
"available_commands": "可用指令:"
}
}
}
<Configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<appender name="STDOUT" class="emu.grasscutter.utils.JlineLogbackAppender">
<encoder>
<pattern>[%d{HH:mm:ss}] [%highlight(%level)] %msg%n</pattern>
</encoder>
</appender>
<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>
<maxHistory>24</maxHistory>
</rollingPolicy>
<encoder>
<pattern>%d{yyyy-MM-dd'T'HH:mm:ss'Z'} - %m%n</pattern>
</encoder>
</appender>
<logger name="org.reflections" level="OFF"/>
<root level="INFO">
<appender-ref ref="STDOUT" />
<appender-ref ref="FILE" />
</root>
</Configuration>
\ No newline at end of file
......@@ -33,7 +33,7 @@ if not "%JAVA_PATH%" == "DO_NOT_CHECK_PATH" (
goto :EXIT
)
) else set JAVA_PATH=
if not exist "%SERVER_PATH%grasscutter.jar" (
if not exist "%SERVER_PATH%%SERVER_JAR_NAME%" (
call :LOG [ERROR] Server jar not found.
goto :EXIT
)
......@@ -74,8 +74,11 @@ for /f "tokens=2*" %%a in ('reg query "HKCU\Software\Microsoft\Windows\CurrentVe
@rem TODO: External proxy when ORIG_PROXY_ENABLE == 0x1
echo set ws = createobject("wscript.shell") > "%temp%\proxy.vbs"
echo ws.currentdirectory = "%MITMDUMP_PATH%" >> "%temp%\proxy.vbs"
echo ws.run "cmd /c mitmdump.exe -s "^&chr(34)^&"%PROXY_SCRIPT_NAME%"^&chr(34)^&" -k",0 >> "%temp%\proxy.vbs"
if not "%MITMDUMP_PATH%" == "" (
echo ws.currentdirectory = "%MITMDUMP_PATH%" >> "%temp%\proxy.vbs"
)
echo ws.run "cmd /c mitmdump.exe -s "^&chr(34)^&"%CUR_PATH%%PROXY_SCRIPT_NAME%"^&chr(34)^&" -k --allow-hosts "^&chr(34)^&".*\.yuanshen\.com|.*\.mihoyo\.com|.*\.hoyoverse\.com"^&chr(34),0 >> "%temp%\proxy.vbs"
"%temp%\proxy.vbs"
del /f /q "%temp%\proxy.vbs" >nul 2>nul
......@@ -117,14 +120,16 @@ set DATABASE=true
mkdir "%DATABASE_STORAGE_PATH%" >nul 2>nul
echo set ws = createobject("wscript.shell") > "%temp%\db.vbs"
if not "%MONGODB_PATH%" == "" (
echo ws.currentdirectory = "%MONGODB_PATH%" >> "%temp%\db.vbs"
)
echo ws.run "cmd /c mongod.exe --dbpath "^&chr(34)^&"%DATABASE_STORAGE_PATH%"^&chr(34)^&"",0 >> "%temp%\db.vbs"
"%temp%\db.vbs"
del /f /q "%temp%\db.vbs" >nul 2>nul
:GAME
call :LOG [INFO] Starting server...
"%JAVA_PATH%java.exe" -jar "%SERVER_PATH%grasscutter.jar"
"%JAVA_PATH%java.exe" -jar "%SERVER_PATH%%SERVER_JAR_NAME%"
call :LOG [INFO] Server stopped
:EXIT
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment