From 1adffc21c0a2892f8feeffc97af5a5afec76a8ef Mon Sep 17 00:00:00 2001 From: xtaodada Date: Mon, 16 May 2022 00:40:10 +0800 Subject: [PATCH 01/18] Enhance the default permissions logic --- .../auth/DefaultAuthenticators.java | 4 ---- .../java/emu/grasscutter/game/Account.java | 21 +++++++++++-------- .../server/http/handlers/GachaHandler.java | 2 +- 3 files changed, 13 insertions(+), 14 deletions(-) diff --git a/src/main/java/emu/grasscutter/auth/DefaultAuthenticators.java b/src/main/java/emu/grasscutter/auth/DefaultAuthenticators.java index e1d5fddf0..57d0541f2 100644 --- a/src/main/java/emu/grasscutter/auth/DefaultAuthenticators.java +++ b/src/main/java/emu/grasscutter/auth/DefaultAuthenticators.java @@ -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; diff --git a/src/main/java/emu/grasscutter/game/Account.java b/src/main/java/emu/grasscutter/game/Account.java index 84873ec61..00a18c4b7 100644 --- a/src/main/java/emu/grasscutter/game/Account.java +++ b/src/main/java/emu/grasscutter/game/Account.java @@ -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 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); } diff --git a/src/main/java/emu/grasscutter/server/http/handlers/GachaHandler.java b/src/main/java/emu/grasscutter/server/http/handlers/GachaHandler.java index 05111de3c..b22da2635 100644 --- a/src/main/java/emu/grasscutter/server/http/handlers/GachaHandler.java +++ b/src/main/java/emu/grasscutter/server/http/handlers/GachaHandler.java @@ -32,7 +32,7 @@ public final class GachaHandler implements Router { private final String gachaMappings; public GachaHandler() { - this.gachaMappings = Utils.toFilePath(DATA("/gacha/mappings.js")); + this.gachaMappings = Utils.toFilePath(DATA("gacha/mappings.js")); if(!(new File(this.gachaMappings).exists())) { try { Tools.createGachaMapping(this.gachaMappings); From f473e44611620c3793633fa525359a8e8a66db64 Mon Sep 17 00:00:00 2001 From: 4Benj_ <5093878+4Benj@users.noreply.github.com> Date: Tue, 17 May 2022 18:00:52 +0800 Subject: [PATCH 02/18] "Autogenerate" data files with data fallbacks and moved keys folder into jar resources (#927) * Autogenerate keys and data files * Update gacha html files Accidentally pushed with old html files * Keys no longer copied. No more manually retrieving listing files. Recursive directory creation Removed unused code from old GC as well. * Moved somethings and better errors * Fixed resources from loading twice * Data files fallback --- .gitignore | 24 +++-- .../java/emu/grasscutter/Configuration.java | 5 - .../java/emu/grasscutter/data/DataLoader.java | 101 ++++++++++++++++++ .../emu/grasscutter/data/ResourceLoader.java | 76 +++++++------ .../grasscutter/game/drop/DropManager.java | 8 +- .../game/expedition/ExpeditionManager.java | 5 +- .../grasscutter/game/gacha/GachaManager.java | 5 +- .../grasscutter/game/shop/ShopManager.java | 9 +- .../game/tower/TowerScheduleManager.java | 5 +- .../http/handlers/AnnouncementsHandler.java | 46 ++++---- .../server/http/handlers/GachaHandler.java | 13 +-- .../grasscutter/utils/ConfigContainer.java | 1 - .../java/emu/grasscutter/utils/Crypto.java | 27 +---- .../java/emu/grasscutter/utils/FileUtils.java | 64 ++++++++++- .../java/emu/grasscutter/utils/Utils.java | 4 + .../resources/defaults/data}/Banners.json | 0 .../main/resources/defaults/data}/Drop.json | 0 .../defaults/data}/ExpeditionReward.json | 0 .../defaults/data}/GameAnnouncement.json | 0 .../defaults/data}/GameAnnouncementList.json | 0 .../main/resources/defaults/data}/Shop.json | 0 .../resources/defaults/data}/ShopChest.json | 0 .../defaults/data}/ShopChestBatchUse.json | 0 .../main/resources/defaults/data}/Spawns.json | 0 .../defaults/data}/TowerSchedule.json | 0 .../defaults/data}/gacha/details.html | 0 .../defaults/data}/gacha/records.html | 0 .../main/resources/keys}/dispatchKey.bin | Bin .../main/resources/keys}/dispatchSeed.bin | Bin .../main/resources/keys}/secretKey.bin | Bin .../main/resources/keys}/secretKeyBuffer.bin | 0 31 files changed, 266 insertions(+), 127 deletions(-) create mode 100644 src/main/java/emu/grasscutter/data/DataLoader.java rename {data => src/main/resources/defaults/data}/Banners.json (100%) rename {data => src/main/resources/defaults/data}/Drop.json (100%) rename {data => src/main/resources/defaults/data}/ExpeditionReward.json (100%) rename {data => src/main/resources/defaults/data}/GameAnnouncement.json (100%) rename {data => src/main/resources/defaults/data}/GameAnnouncementList.json (100%) rename {data => src/main/resources/defaults/data}/Shop.json (100%) rename {data => src/main/resources/defaults/data}/ShopChest.json (100%) rename {data => src/main/resources/defaults/data}/ShopChestBatchUse.json (100%) rename {data => src/main/resources/defaults/data}/Spawns.json (100%) rename {data => src/main/resources/defaults/data}/TowerSchedule.json (100%) rename {data => src/main/resources/defaults/data}/gacha/details.html (100%) rename {data => src/main/resources/defaults/data}/gacha/records.html (100%) rename {keys => src/main/resources/keys}/dispatchKey.bin (100%) rename {keys => src/main/resources/keys}/dispatchSeed.bin (100%) rename {keys => src/main/resources/keys}/secretKey.bin (100%) rename {keys => src/main/resources/keys}/secretKeyBuffer.bin (100%) diff --git a/.gitignore b/.gitignore index 30ac9f0d1..f792ae6b8 100644 --- a/.gitignore +++ b/.gitignore @@ -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 diff --git a/src/main/java/emu/grasscutter/Configuration.java b/src/main/java/emu/grasscutter/Configuration.java index 486ca8739..66fc2fe37 100644 --- a/src/main/java/emu/grasscutter/Configuration.java +++ b/src/main/java/emu/grasscutter/Configuration.java @@ -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; diff --git a/src/main/java/emu/grasscutter/data/DataLoader.java b/src/main/java/emu/grasscutter/data/DataLoader.java new file mode 100644 index 000000000..dc7e67281 --- /dev/null +++ b/src/main/java/emu/grasscutter/data/DataLoader.java @@ -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 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); + } + } + } +} diff --git a/src/main/java/emu/grasscutter/data/ResourceLoader.java b/src/main/java/emu/grasscutter/data/ResourceLoader.java index 4b940c44d..6fe9b19fb 100644 --- a/src/main/java/emu/grasscutter/data/ResourceLoader.java +++ b/src/main/java/emu/grasscutter/data/ResourceLoader.java @@ -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 loadedResources = new ArrayList(); + public static List> 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 tempMap = Utils.switchPropertiesUpperLowerCase((Map) 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 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 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 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 map = new TreeMap<>(); java.lang.reflect.Type type = new TypeToken>() {}.getType(); String[] folderNames = {"BinOutput/Talent/EquipTalents/", "BinOutput/Talent/AvatarTalents/"}; diff --git a/src/main/java/emu/grasscutter/game/drop/DropManager.java b/src/main/java/emu/grasscutter/game/drop/DropManager.java index 218624d1a..38dd2bed8 100644 --- a/src/main/java/emu/grasscutter/game/drop/DropManager.java +++ b/src/main/java/emu/grasscutter/game/drop/DropManager.java @@ -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 banners = Grasscutter.getGsonFactory().fromJson(fileReader, TypeToken.getParameterized(Collection.class, DropInfo.class).getType()); if(banners.size() > 0) { diff --git a/src/main/java/emu/grasscutter/game/expedition/ExpeditionManager.java b/src/main/java/emu/grasscutter/game/expedition/ExpeditionManager.java index 1b75d7306..9aab70992 100644 --- a/src/main/java/emu/grasscutter/game/expedition/ExpeditionManager.java +++ b/src/main/java/emu/grasscutter/game/expedition/ExpeditionManager.java @@ -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 banners = Grasscutter.getGsonFactory().fromJson(fileReader, TypeToken.getParameterized(Collection.class, ExpeditionRewardInfo.class).getType()); if(banners.size() > 0) { diff --git a/src/main/java/emu/grasscutter/game/gacha/GachaManager.java b/src/main/java/emu/grasscutter/game/gacha/GachaManager.java index e4fafd814..6ecc2b6b4 100644 --- a/src/main/java/emu/grasscutter/game/gacha/GachaManager.java +++ b/src/main/java/emu/grasscutter/game/gacha/GachaManager.java @@ -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 banners = Grasscutter.getGsonFactory().fromJson(fileReader, TypeToken.getParameterized(Collection.class, GachaBanner.class).getType()); if(banners.size() > 0) { diff --git a/src/main/java/emu/grasscutter/game/shop/ShopManager.java b/src/main/java/emu/grasscutter/game/shop/ShopManager.java index a27011012..03c868f6b 100644 --- a/src/main/java/emu/grasscutter/game/shop/ShopManager.java +++ b/src/main/java/emu/grasscutter/game/shop/ShopManager.java @@ -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 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 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 shopChestBatchUseTableList = Grasscutter.getGsonFactory().fromJson(fileReader, TypeToken.getParameterized(Collection.class, ShopChestBatchUseTable.class).getType()); if (shopChestBatchUseTableList.size() > 0) { diff --git a/src/main/java/emu/grasscutter/game/tower/TowerScheduleManager.java b/src/main/java/emu/grasscutter/game/tower/TowerScheduleManager.java index 1d9a12b89..6d4f94db9 100644 --- a/src/main/java/emu/grasscutter/game/tower/TowerScheduleManager.java +++ b/src/main/java/emu/grasscutter/game/tower/TowerScheduleManager.java @@ -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); diff --git a/src/main/java/emu/grasscutter/server/http/handlers/AnnouncementsHandler.java b/src/main/java/emu/grasscutter/server/http/handlers/AnnouncementsHandler.java index c4776a4b4..07790a641 100644 --- a/src/main/java/emu/grasscutter/server/http/handlers/AnnouncementsHandler.java +++ b/src/main/java/emu/grasscutter/server/http/handlers/AnnouncementsHandler.java @@ -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); - } } diff --git a/src/main/java/emu/grasscutter/server/http/handlers/GachaHandler.java b/src/main/java/emu/grasscutter/server/http/handlers/GachaHandler.java index b22da2635..8edbea554 100644 --- a/src/main/java/emu/grasscutter/server/http/handlers/GachaHandler.java +++ b/src/main/java/emu/grasscutter/server/http/handlers/GachaHandler.java @@ -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); diff --git a/src/main/java/emu/grasscutter/utils/ConfigContainer.java b/src/main/java/emu/grasscutter/utils/ConfigContainer.java index 33f844e34..7171caf92 100644 --- a/src/main/java/emu/grasscutter/utils/ConfigContainer.java +++ b/src/main/java/emu/grasscutter/utils/ConfigContainer.java @@ -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/"; diff --git a/src/main/java/emu/grasscutter/utils/Crypto.java b/src/main/java/emu/grasscutter/utils/Crypto.java index 1772e26de..3bde63aa7 100644 --- a/src/main/java/emu/grasscutter/utils/Crypto.java +++ b/src/main/java/emu/grasscutter/utils/Crypto.java @@ -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); diff --git a/src/main/java/emu/grasscutter/utils/FileUtils.java b/src/main/java/emu/grasscutter/utils/FileUtils.java index 06e6087b3..17e680576 100644 --- a/src/main/java/emu/grasscutter/utils/FileUtils.java +++ b/src/main/java/emu/grasscutter/utils/FileUtils.java @@ -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 getPathsFromResource(String folder) throws URISyntaxException, IOException { + List 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); + } } diff --git a/src/main/java/emu/grasscutter/utils/Utils.java b/src/main/java/emu/grasscutter/utils/Utils.java index 33472518e..e2ca98ca1 100644 --- a/src/main/java/emu/grasscutter/utils/Utils.java +++ b/src/main/java/emu/grasscutter/utils/Utils.java @@ -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); } diff --git a/data/Banners.json b/src/main/resources/defaults/data/Banners.json similarity index 100% rename from data/Banners.json rename to src/main/resources/defaults/data/Banners.json diff --git a/data/Drop.json b/src/main/resources/defaults/data/Drop.json similarity index 100% rename from data/Drop.json rename to src/main/resources/defaults/data/Drop.json diff --git a/data/ExpeditionReward.json b/src/main/resources/defaults/data/ExpeditionReward.json similarity index 100% rename from data/ExpeditionReward.json rename to src/main/resources/defaults/data/ExpeditionReward.json diff --git a/data/GameAnnouncement.json b/src/main/resources/defaults/data/GameAnnouncement.json similarity index 100% rename from data/GameAnnouncement.json rename to src/main/resources/defaults/data/GameAnnouncement.json diff --git a/data/GameAnnouncementList.json b/src/main/resources/defaults/data/GameAnnouncementList.json similarity index 100% rename from data/GameAnnouncementList.json rename to src/main/resources/defaults/data/GameAnnouncementList.json diff --git a/data/Shop.json b/src/main/resources/defaults/data/Shop.json similarity index 100% rename from data/Shop.json rename to src/main/resources/defaults/data/Shop.json diff --git a/data/ShopChest.json b/src/main/resources/defaults/data/ShopChest.json similarity index 100% rename from data/ShopChest.json rename to src/main/resources/defaults/data/ShopChest.json diff --git a/data/ShopChestBatchUse.json b/src/main/resources/defaults/data/ShopChestBatchUse.json similarity index 100% rename from data/ShopChestBatchUse.json rename to src/main/resources/defaults/data/ShopChestBatchUse.json diff --git a/data/Spawns.json b/src/main/resources/defaults/data/Spawns.json similarity index 100% rename from data/Spawns.json rename to src/main/resources/defaults/data/Spawns.json diff --git a/data/TowerSchedule.json b/src/main/resources/defaults/data/TowerSchedule.json similarity index 100% rename from data/TowerSchedule.json rename to src/main/resources/defaults/data/TowerSchedule.json diff --git a/data/gacha/details.html b/src/main/resources/defaults/data/gacha/details.html similarity index 100% rename from data/gacha/details.html rename to src/main/resources/defaults/data/gacha/details.html diff --git a/data/gacha/records.html b/src/main/resources/defaults/data/gacha/records.html similarity index 100% rename from data/gacha/records.html rename to src/main/resources/defaults/data/gacha/records.html diff --git a/keys/dispatchKey.bin b/src/main/resources/keys/dispatchKey.bin similarity index 100% rename from keys/dispatchKey.bin rename to src/main/resources/keys/dispatchKey.bin diff --git a/keys/dispatchSeed.bin b/src/main/resources/keys/dispatchSeed.bin similarity index 100% rename from keys/dispatchSeed.bin rename to src/main/resources/keys/dispatchSeed.bin diff --git a/keys/secretKey.bin b/src/main/resources/keys/secretKey.bin similarity index 100% rename from keys/secretKey.bin rename to src/main/resources/keys/secretKey.bin diff --git a/keys/secretKeyBuffer.bin b/src/main/resources/keys/secretKeyBuffer.bin similarity index 100% rename from keys/secretKeyBuffer.bin rename to src/main/resources/keys/secretKeyBuffer.bin From 20feb75c8658177b72e1b2514ae9598c8eb14741 Mon Sep 17 00:00:00 2001 From: Benjamin Elsdon Date: Tue, 17 May 2022 19:07:45 +0800 Subject: [PATCH 03/18] Permanent fix for version.json stuff (causes 4206) --- .../emu/grasscutter/server/http/handlers/GenericHandler.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/emu/grasscutter/server/http/handlers/GenericHandler.java b/src/main/java/emu/grasscutter/server/http/handlers/GenericHandler.java index 2de8969d7..c108ba776 100644 --- a/src/main/java/emu/grasscutter/server/http/handlers/GenericHandler.java +++ b/src/main/java/emu/grasscutter/server/http/handlers/GenericHandler.java @@ -41,7 +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/*", new HttpJsonResponse("{\"version\":51}")); express.get("/status/server", GenericHandler::serverStatus); } From c147ebda8f3bac7bf06d8fbff168d834ee4946c3 Mon Sep 17 00:00:00 2001 From: DancingSnow <60736156+DancingSnow0517@users.noreply.github.com> Date: Sat, 14 May 2022 20:52:48 +0800 Subject: [PATCH 04/18] Cache gradle files --- .github/workflows/build.yml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index d14ebf3e0..89d029f19 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -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 From fffa5a9c470b1dc145c094e925a062d7f8287def Mon Sep 17 00:00:00 2001 From: Benjamin Elsdon Date: Tue, 17 May 2022 18:34:01 +0800 Subject: [PATCH 05/18] Fix "Attempt to get java.util.List field 'PacketOpcodes.BANNED_PACKETS' with illegal data type conversion to int" --- .../emu/grasscutter/net/packet/PacketOpcodesUtil.java | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/main/java/emu/grasscutter/net/packet/PacketOpcodesUtil.java b/src/main/java/emu/grasscutter/net/packet/PacketOpcodesUtil.java index 6395a7d4d..ff7be1e45 100644 --- a/src/main/java/emu/grasscutter/net/packet/PacketOpcodesUtil.java +++ b/src/main/java/emu/grasscutter/net/packet/PacketOpcodesUtil.java @@ -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(); + } } } } From ef637086756002966be21ac12b0cafac55d6d3d0 Mon Sep 17 00:00:00 2001 From: Benjamin Elsdon Date: Tue, 17 May 2022 19:16:44 +0800 Subject: [PATCH 06/18] Change BANNED_PACKETS to HashSet instaed of List --- src/main/java/emu/grasscutter/net/packet/PacketOpcodes.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/main/java/emu/grasscutter/net/packet/PacketOpcodes.java b/src/main/java/emu/grasscutter/net/packet/PacketOpcodes.java index 685f054f6..a4c5905b1 100644 --- a/src/main/java/emu/grasscutter/net/packet/PacketOpcodes.java +++ b/src/main/java/emu/grasscutter/net/packet/PacketOpcodes.java @@ -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 BANNED_PACKETS = Arrays.asList(PacketOpcodes.WindSeedClientNotify, PacketOpcodes.PlayerLuaShellNotify); + public static final HashSet BANNED_PACKETS = new HashSet() {{ + add(PacketOpcodes.WindSeedClientNotify); + add(PacketOpcodes.PlayerLuaShellNotify); + }}; } From b5632aa62d082aa5e80e707d4abfe66bfccf78e0 Mon Sep 17 00:00:00 2001 From: tester233 <1804830236@qq.com> Date: Tue, 17 May 2022 18:09:24 +0800 Subject: [PATCH 07/18] Fix usage & remove outdated text --- .../commands/ResetShopLimitCommand.java | 2 +- src/main/resources/languages/en-US.json | 786 +++++++++--------- src/main/resources/languages/pl-PL.json | 3 - src/main/resources/languages/zh-CN.json | 782 +++++++++-------- src/main/resources/languages/zh-TW.json | 6 - 5 files changed, 779 insertions(+), 800 deletions(-) diff --git a/src/main/java/emu/grasscutter/command/commands/ResetShopLimitCommand.java b/src/main/java/emu/grasscutter/command/commands/ResetShopLimitCommand.java index d2b910811..cfff28668 100644 --- a/src/main/java/emu/grasscutter/command/commands/ResetShopLimitCommand.java +++ b/src/main/java/emu/grasscutter/command/commands/ResetShopLimitCommand.java @@ -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 = "commands.resetShopLimit.usage", permission = "server.resetshop", permissionTargeted = "server.resetshop.others", description = "commands.resetShopLimit.description") public final class ResetShopLimitCommand implements CommandHandler { @Override diff --git a/src/main/resources/languages/en-US.json b/src/main/resources/languages/en-US.json index 65111c363..c8b894d9a 100644 --- a/src/main/resources/languages/en-US.json +++ b/src/main/resources/languages/en-US.json @@ -1,396 +1,390 @@ -{ - "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 [uid]", - "description": "Modify user accounts" - }, - "broadcast": { - "command_usage": "Usage: broadcast ", - "message_sent": "Message sent.", - "description": "Sends a message to all the players" - }, - "changescene": { - "usage": "Usage: changescene ", - "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 ", - "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 ", - "success": "Summoned %s to %s's world.", - "description": "Forces someone to join the world of others" - }, - "enter_dungeon": { - "usage": "Usage: enterdungeon ", - "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] [[,]]... [level]", - "id_error": "Invalid artifact ID.", - "success": "Given %s to %s.", - "description": "Gives the player a specified artifact" - }, - "giveChar": { - "usage": "Usage: givechar [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 [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": "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 [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 ", - "description": "Reset target player's shop refresh time." - }, - "sendMail": { - "usage": "Usage: give [player] [amount]", - "user_not_exist": "The user with an id of '%s' does not exist", - "start_composition": "Starting composition of message.\nPlease use `/sendmail ` 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 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." + }, + "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." + } + } +} diff --git a/src/main/resources/languages/pl-PL.json b/src/main/resources/languages/pl-PL.json index 8906c99c6..968d91f01 100644 --- a/src/main/resources/languages/pl-PL.json +++ b/src/main/resources/languages/pl-PL.json @@ -298,9 +298,6 @@ "unlocktower": { "success": "odblokować gotowe", "description": "Odblokuj głęboką spiralę" - }, - "resetshop": { - "description": "zresetuj sklep" } }, "gacha": { diff --git a/src/main/resources/languages/zh-CN.json b/src/main/resources/languages/zh-CN.json index cd572c095..ac37ee986 100644 --- a/src/main/resources/languages/zh-CN.json +++ b/src/main/resources/languages/zh-CN.json @@ -1,394 +1,388 @@ -{ - "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": "未找到用户名,建立连接失败。" + } + }, + "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": "改变指定玩家的位置" + }, + "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星物品" + } + } +} diff --git a/src/main/resources/languages/zh-TW.json b/src/main/resources/languages/zh-TW.json index 00b77aac3..859841d65 100644 --- a/src/main/resources/languages/zh-TW.json +++ b/src/main/resources/languages/zh-TW.json @@ -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": { From 263369af9a360ff7d22d9055c0823fa12b5007a2 Mon Sep 17 00:00:00 2001 From: tester233 <105267106+tester233@users.noreply.github.com> Date: Tue, 17 May 2022 18:43:16 +0800 Subject: [PATCH 08/18] Modify usage --- .../emu/grasscutter/command/commands/ResetShopLimitCommand.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/emu/grasscutter/command/commands/ResetShopLimitCommand.java b/src/main/java/emu/grasscutter/command/commands/ResetShopLimitCommand.java index cfff28668..19992efa9 100644 --- a/src/main/java/emu/grasscutter/command/commands/ResetShopLimitCommand.java +++ b/src/main/java/emu/grasscutter/command/commands/ResetShopLimitCommand.java @@ -9,7 +9,7 @@ import java.util.List; import static emu.grasscutter.utils.Language.translate; -@Command(label = "resetshop", usage = "commands.resetShopLimit.usage", permission = "server.resetshop", permissionTargeted = "server.resetshop.others", description = "commands.resetShopLimit.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 From d0ab5fa5dfba4d59bc39cc6d17f55f0318881dc3 Mon Sep 17 00:00:00 2001 From: tester233 <105267106+tester233@users.noreply.github.com> Date: Tue, 17 May 2022 19:19:05 +0800 Subject: [PATCH 09/18] Add translation key --- .../grasscutter/command/commands/ResetShopLimitCommand.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/main/java/emu/grasscutter/command/commands/ResetShopLimitCommand.java b/src/main/java/emu/grasscutter/command/commands/ResetShopLimitCommand.java index 19992efa9..9c1564e78 100644 --- a/src/main/java/emu/grasscutter/command/commands/ResetShopLimitCommand.java +++ b/src/main/java/emu/grasscutter/command/commands/ResetShopLimitCommand.java @@ -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")); From cc98c569168d48d7896f166c074bb8be8ccecf23 Mon Sep 17 00:00:00 2001 From: YukariChiba <i@0x7f.cc> Date: Tue, 17 May 2022 16:38:07 +0800 Subject: [PATCH 10/18] Do not add default region if there are already regions --- .../emu/grasscutter/server/http/dispatch/RegionHandler.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/emu/grasscutter/server/http/dispatch/RegionHandler.java b/src/main/java/emu/grasscutter/server/http/dispatch/RegionHandler.java index 40edafb21..f2d1cc4ca 100644 --- a/src/main/java/emu/grasscutter/server/http/dispatch/RegionHandler.java +++ b/src/main/java/emu/grasscutter/server/http/dispatch/RegionHandler.java @@ -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))); From 10fe5d11caf31fe19f3dd6432b9c4482d467631e Mon Sep 17 00:00:00 2001 From: Benjamin Elsdon <benjamin7006@gmail.com> Date: Tue, 17 May 2022 20:18:12 +0800 Subject: [PATCH 11/18] AuthenticationSystem::verifyUser now returns the user's Account instead of a boolean --- .../java/emu/grasscutter/auth/AuthenticationSystem.java | 7 ++++--- .../java/emu/grasscutter/auth/DefaultAuthentication.java | 7 ++++--- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/main/java/emu/grasscutter/auth/AuthenticationSystem.java b/src/main/java/emu/grasscutter/auth/AuthenticationSystem.java index 41aba1c8e..c9ae98d50 100644 --- a/src/main/java/emu/grasscutter/auth/AuthenticationSystem.java +++ b/src/main/java/emu/grasscutter/auth/AuthenticationSystem.java @@ -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. diff --git a/src/main/java/emu/grasscutter/auth/DefaultAuthentication.java b/src/main/java/emu/grasscutter/auth/DefaultAuthentication.java index 08958d8e9..b47985cf6 100644 --- a/src/main/java/emu/grasscutter/auth/DefaultAuthentication.java +++ b/src/main/java/emu/grasscutter/auth/DefaultAuthentication.java @@ -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; @@ -26,11 +27,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 From 0c6521806cff271eea0ebaa5acbc0a976c80bdca Mon Sep 17 00:00:00 2001 From: muhammadeko <muhammadekoprasetyo29@gmail.com> Date: Tue, 17 May 2022 23:24:01 +0700 Subject: [PATCH 12/18] add CommandResponseEvent --- .../grasscutter/command/CommandHandler.java | 4 ++++ .../event/game/CommandResponseEvent.java | 24 +++++++++++++++++++ 2 files changed, 28 insertions(+) create mode 100644 src/main/java/emu/grasscutter/server/event/game/CommandResponseEvent.java diff --git a/src/main/java/emu/grasscutter/command/CommandHandler.java b/src/main/java/emu/grasscutter/command/CommandHandler.java index f4fe12b3f..6e19e7bc9 100644 --- a/src/main/java/emu/grasscutter/command/CommandHandler.java +++ b/src/main/java/emu/grasscutter/command/CommandHandler.java @@ -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(); } /** diff --git a/src/main/java/emu/grasscutter/server/event/game/CommandResponseEvent.java b/src/main/java/emu/grasscutter/server/event/game/CommandResponseEvent.java new file mode 100644 index 000000000..c3c8fd89a --- /dev/null +++ b/src/main/java/emu/grasscutter/server/event/game/CommandResponseEvent.java @@ -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; + } +} From d15c32df23ef2d8f12bc9c7723b68cf8ea30be30 Mon Sep 17 00:00:00 2001 From: YukariChiba <i@0x7f.cc> Date: Tue, 17 May 2022 14:34:13 +0800 Subject: [PATCH 13/18] Add OAuth in AuthenticationSystem --- .../auth/AuthenticationSystem.java | 18 ++++++++++++ .../auth/DefaultAuthentication.java | 6 ++++ .../auth/DefaultAuthenticators.java | 25 +++++++++++++++++ .../grasscutter/auth/OAuthAuthenticator.java | 28 +++++++++++++++++++ .../server/http/dispatch/DispatchHandler.java | 8 ++++++ 5 files changed, 85 insertions(+) create mode 100644 src/main/java/emu/grasscutter/auth/OAuthAuthenticator.java diff --git a/src/main/java/emu/grasscutter/auth/AuthenticationSystem.java b/src/main/java/emu/grasscutter/auth/AuthenticationSystem.java index c9ae98d50..ee27dbca0 100644 --- a/src/main/java/emu/grasscutter/auth/AuthenticationSystem.java +++ b/src/main/java/emu/grasscutter/auth/AuthenticationSystem.java @@ -60,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. */ @@ -125,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(); + } } diff --git a/src/main/java/emu/grasscutter/auth/DefaultAuthentication.java b/src/main/java/emu/grasscutter/auth/DefaultAuthentication.java index b47985cf6..ba77e7d6e 100644 --- a/src/main/java/emu/grasscutter/auth/DefaultAuthentication.java +++ b/src/main/java/emu/grasscutter/auth/DefaultAuthentication.java @@ -17,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) { @@ -53,4 +54,9 @@ public final class DefaultAuthentication implements AuthenticationSystem { public ExternalAuthenticator getExternalAuthenticator() { return this.externalAuthenticator; } + + @Override + public OAuthAuthenticator getOAuthAuthenticator() { + return this.oAuthAuthenticator; + } } diff --git a/src/main/java/emu/grasscutter/auth/DefaultAuthenticators.java b/src/main/java/emu/grasscutter/auth/DefaultAuthenticators.java index 57d0541f2..0a9916a59 100644 --- a/src/main/java/emu/grasscutter/auth/DefaultAuthenticators.java +++ b/src/main/java/emu/grasscutter/auth/DefaultAuthenticators.java @@ -174,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."); + } + } } diff --git a/src/main/java/emu/grasscutter/auth/OAuthAuthenticator.java b/src/main/java/emu/grasscutter/auth/OAuthAuthenticator.java new file mode 100644 index 000000000..394d14371 --- /dev/null +++ b/src/main/java/emu/grasscutter/auth/OAuthAuthenticator.java @@ -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); +} diff --git a/src/main/java/emu/grasscutter/server/http/dispatch/DispatchHandler.java b/src/main/java/emu/grasscutter/server/http/dispatch/DispatchHandler.java index 5f9edcf0a..96e1fda84 100644 --- a/src/main/java/emu/grasscutter/server/http/dispatch/DispatchHandler.java +++ b/src/main/java/emu/grasscutter/server/http/dispatch/DispatchHandler.java @@ -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))); } /** From e45ef7de518e6a5e3249917831388373759b4312 Mon Sep 17 00:00:00 2001 From: AZthemute <thereisoneazzy@protonmail.com> Date: Tue, 17 May 2022 18:14:21 +0100 Subject: [PATCH 14/18] General housekeeping and clarification of en-us.json file --- src/main/resources/languages/en-US.json | 188 ++++++++++++------------ 1 file changed, 94 insertions(+), 94 deletions(-) diff --git a/src/main/resources/languages/en-US.json b/src/main/resources/languages/en-US.json index c8b894d9a..b45862f94 100644 --- a/src/main/resources/languages/en-US.json +++ b/src/main/resources/languages/en-US.json @@ -17,22 +17,22 @@ "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" + "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", + "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", + "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." @@ -45,7 +45,7 @@ "shutdown": "Shutting down...", "done": "Done! For help, type \"help\"", "error": "An error occurred.", - "welcome": "Welcome to Grasscutter", + "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...", @@ -61,17 +61,17 @@ "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", + "no_description_specified": "No description specified.", "invalid": { "amount": "Invalid amount.", - "artifactId": "Invalid artifactId.", - "avatarId": "Invalid avatarId.", + "artifactId": "Invalid artifact ID.", + "avatarId": "Invalid avatar ID.", "avatarLevel": "Invalid avatarLevel.", - "entityId": "Invalid entityId.", - "itemId": "Invalid itemId.", + "entityId": "Invalid entity ID.", + "itemId": "Invalid item ID.", "itemLevel": "Invalid itemLevel.", "itemRefinement": "Invalid itemRefinement.", - "playerId": "Invalid playerId.", + "playerId": "Invalid player ID.", "uid": "Invalid UID." } }, @@ -101,7 +101,7 @@ "create": "Account created with UID %s.", "delete": "Account deleted.", "no_account": "Account not found.", - "command_usage": "Usage: account <create|delete> <username> [uid]", + "command_usage": "Usage: account <create|delete> <username> [UID]", "description": "Modify user accounts" }, "broadcast": { @@ -110,7 +110,7 @@ "description": "Sends a message to all the players" }, "changescene": { - "usage": "Usage: changescene <sceneId>", + "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.", @@ -128,15 +128,15 @@ "description": "Deletes unequipped unlocked items, including yellow rarity ones from your inventory" }, "coop": { - "usage": "Usage: coop <playerId> <target playerId>", + "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", + "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": { @@ -147,26 +147,26 @@ "description": "Gives all items" }, "giveArtifact": { - "usage": "Usage: giveart|gart [player] <artifactId> <mainPropId> [<appendPropId>[,<times>]]... [level]", + "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]", + "usage": "Usage: givechar <player> <itemID|itemName> [amount]", "given": "Given %s with level %s to %s.", - "invalid_avatar_id": "Invalid avatar id.", + "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]", + "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", + "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": { @@ -183,25 +183,25 @@ }, "kick": { "player_kick_player": "Player [%s:%s] has kicked player [%s:%s]", - "server_kick_player": "Kicking 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", + "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]", + "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" + "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:", @@ -217,16 +217,16 @@ "description": "Grants or removes a permission for a user" }, "position": { - "success": "Coordinates: %s, %s, %s\nScene id: %s", - "description": "Get coordinates." + "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" + "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.", @@ -236,34 +236,34 @@ "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." + "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." + "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", + "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", + "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.", + "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." + "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>", @@ -273,7 +273,7 @@ "setFetterLevel": { "usage": "Usage: setfetterlevel <level>", "range_error": "Fetter level must be between 0 and 10.", - "success": "Fetter level set to %s", + "success": "Fetter level set to %s.", "level_error": "Invalid fetter level.", "description": "Sets your fetter level for your current active character" }, @@ -287,17 +287,17 @@ "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" + "description": "Sets fight property for your current active character" }, "setWorldLevel": { "usage": "Usage: setworldlevel <level>", - "value_error": "World level must be between 0-8", + "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)]", + "usage": "Usage: spawn <entityID> [amount] [level(monster only)]", "success": "Spawned %s of %s.", "description": "Spawns an entity near you" }, @@ -309,7 +309,7 @@ "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", + "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.", @@ -323,44 +323,44 @@ "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" + "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 only can use this command in MP mode.", + "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.", + "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." + "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", + "usage": "Usage: weather <weatherID> [climateID]", + "success": "Changed weather to %s with climate %s.", "invalid_id": "Invalid ID.", - "description": "Changes the weather." + "description": "Changes the weather" }, "drop": { - "command_usage": "Usage: drop <itemId|itemName> [amount]", + "command_usage": "Usage: drop <itemID|itemName> [amount]", "success": "Dropped %s of %s.", "description": "Drops an item near you" }, @@ -374,7 +374,7 @@ "description": "Restarts the current session" }, "unlocktower": { - "success": "unlock done", + "success": "Unlock done.", "description": "Unlock all levels of tower" } }, From b8cb6e108bc030ef87e1e5cfd4e6c0802ab651bc Mon Sep 17 00:00:00 2001 From: tester233 <1804830236@qq.com> Date: Wed, 18 May 2022 20:29:37 +0800 Subject: [PATCH 15/18] Improve text & update zh-CN.json --- src/main/resources/languages/zh-CN.json | 87 +++++++++++++------------ 1 file changed, 44 insertions(+), 43 deletions(-) diff --git a/src/main/resources/languages/zh-CN.json b/src/main/resources/languages/zh-CN.json index ac37ee986..3c9585523 100644 --- a/src/main/resources/languages/zh-CN.json +++ b/src/main/resources/languages/zh-CN.json @@ -29,14 +29,15 @@ "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_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": "未找到用户名,建立连接失败。" - } + "session_key_error": "会话密钥错误", + "username_error": "未找到此用户名", + "username_create_error": "未找到用户名,建立连接失败" + }, + "router_error": "[Dispatch] 无法连接路由" }, "status": { "free_software": "Grasscutter 是免费开源软件,遵循 AGPL-3.0 license。如果你是付费购买的,那你已经被骗了。项目地址:https://github.com/Grasscutters/Grasscutter", @@ -49,7 +50,7 @@ "run_mode_help": "服务器运行模式必须为 HYBRID、DISPATCH_ONLY 或 GAME_ONLY。Grasscutter 启动失败...", "create_resources": "正在创建 resources 目录...", "resources_error": "请将 BinOutput 和 ExcelBinOutput 复制到 resources 目录。", - "version": "Grasscutter 版本: %s-%s" + "version": "Grasscutter 版本:%s-%s" } }, "commands": { @@ -60,7 +61,7 @@ "console_execute_error": "此命令只能在服务器控制台执行呐~", "player_execute_error": "此命令只能在游戏内执行哦~", "command_exist_error": "这条命令...好像找不到呢?", - "no_description_specified": "没有指定说明", + "no_description_specified": "没有指定说明。", "invalid": { "amount": "无效的数量。", "artifactId": "无效的圣遗物ID。", @@ -146,8 +147,8 @@ "description": "给予所有物品" }, "nostamina": { - "success": "NoStamina %s 对于 %s.", - "description": "保持你的体力处于最高状态。" + "success": "NoStamina 已设为 %s。[用户:%s]", + "description": "保持你的体力处于最高状态" }, "giveArtifact": { "usage": "用法:giveart|gart [玩家] <圣遗物ID> <主词条ID> [<副词条ID>[,<强化次数>]]... [等级]", @@ -166,7 +167,7 @@ "give": { "usage": "用法:give <玩家> <物品ID|物品名> [数量] [等级] [精炼等级]", "refinement_only_applicable_weapons": "只有武器可以设置精炼等级。", - "refinement_must_between_1_and_5": "精炼等级必须在 1 到 5 之间。", + "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。", @@ -192,14 +193,14 @@ "description": "杀死所有怪物" }, "killCharacter": { - "usage": "用法:/killcharacter [玩家ID]", + "usage": "用法:killcharacter [玩家ID]", "success": "已杀死 %s 当前角色。", "description": "杀死当前角色" }, "language": { - "current_language": "当前语言是: %s", - "language_changed": "语言切换至: %s", - "language_not_found": "目前服务端没有这种语言: %s", + "current_language": "当前语言是:%s", + "language_changed": "语言切换至:%s", + "language_not_found": "目前服务端没有这种语言:%s", "description": "显示或切换当前语言" }, "list": { @@ -222,10 +223,10 @@ "quest": { "description": "添加或完成任务", "usage": "quest <add|finish> [任务ID]", - "added": "已添加任务 %s", - "finished": "已完成任务 %s", - "not_found": "未找到任务", - "invalid_id": "无效的任务ID" + "added": "已添加任务 %s。", + "finished": "已完成任务 %s。", + "not_found": "此任务不存在。", + "invalid_id": "无效的任务ID。" }, "reload": { "reload_start": "正在重载配置文件和数据。", @@ -238,7 +239,7 @@ "description": "重置当前角色的命之座,执行命令后需重新登录以生效" }, "resetShopLimit": { - "usage": "用法:/resetshop <玩家ID>", + "usage": "用法:resetshop <玩家ID>", "description": "重置所选玩家的商店刷新时间" }, "sendMail": { @@ -255,7 +256,7 @@ "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` 来发送邮件。", + "send": "已添加 %s 个 %s [等级 %s] 邮件附件。\n如果没有要继续添加的附件请使用 `/sendmail finish` 来发送邮件。", "invalid_arguments_please_use": "错误的参数 \n请使用 `/sendmail %s`", "title": "<标题>", "message": "<正文>", @@ -271,7 +272,7 @@ }, "setFetterLevel": { "usage": "用法:setfetterlevel <好感度等级>", - "range_error": "好感度等级必须在 0 到 10 之间。", + "range_error": "好感度等级必须在 0-10 之间。", "success": "好感度已设为 %s 级。", "level_error": "无效的好感度等级。", "description": "设置当前角色的好感度等级" @@ -279,7 +280,7 @@ "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", + "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": "玩家不存在或已离线。", @@ -290,7 +291,7 @@ }, "setWorldLevel": { "usage": "用法:setworldlevel <等级>", - "value_error": "世界等级必须设置在0-8之间。", + "value_error": "世界等级必须在 0-8 之间。", "success": "世界等级已设为 %s。", "invalid_world_level": "无效的世界等级。", "description": "设置世界等级,执行命令后需重新登录以生效" @@ -306,7 +307,7 @@ }, "talent": { "usage_1": "设置天赋等级:/talent set <天赋ID> <数值>", - "usage_2": "另一种设置天赋等级的方法:/talent <n (普通攻击) | e (元素战技) | q (元素爆发)> <数值>", + "usage_2": "另一种设置天赋等级的方法:/talent <n|e|q> <数值>\nn: 普通攻击, e: 元素战技, q: 元素爆发", "usage_3": "获取天赋ID:/talent getid", "lower_16": "无效的天赋等级,天赋等级应小于等于15。", "set_id": "将天赋等级设为 %s。", @@ -322,21 +323,21 @@ "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", + "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_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": { @@ -345,11 +346,11 @@ "description": "将你世界中的所有玩家传送到你所在的位置" }, "teleport": { - "usage_server": "用法:/tp @<玩家ID> <x> <y> <z> [场景ID]", - "usage": "用法:/tp [@<玩家ID>] <x> <y> <z> [场景ID]", + "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。", + "success": "传送 %s 到坐标 %s, %s, %s,场景为 %s。", "description": "改变指定玩家的位置" }, "weather": { From efe459e17f6587ea8c87639d9b9ced2d05126848 Mon Sep 17 00:00:00 2001 From: tester233 <105267106+tester233@users.noreply.github.com> Date: Thu, 19 May 2022 04:46:47 +0800 Subject: [PATCH 16/18] Fix punctuation & update zh-CN.json (#976) * Fix punctuation & update zh-CN.json * Fix error --- src/main/resources/languages/zh-CN.json | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/main/resources/languages/zh-CN.json b/src/main/resources/languages/zh-CN.json index 3c9585523..2e2af6ee5 100644 --- a/src/main/resources/languages/zh-CN.json +++ b/src/main/resources/languages/zh-CN.json @@ -221,12 +221,12 @@ "description": "获取所在位置" }, "quest": { - "description": "添加或完成任务", "usage": "quest <add|finish> [任务ID]", "added": "已添加任务 %s。", "finished": "已完成任务 %s。", "not_found": "此任务不存在。", - "invalid_id": "无效的任务ID。" + "invalid_id": "无效的任务ID。", + "description": "添加或完成任务" }, "reload": { "reload_start": "正在重载配置文件和数据。", @@ -323,17 +323,17 @@ "description": "设置当前角色的天赋等级" }, "team": { - "usage": "用法: team <add|remove|set> [角色ID,...] [索引|first|last|索引-索引,...]", + "usage": "用法:team <add|remove|set> [角色ID,...] [索引|first|last|索引-索引,...]", "invalid_usage": "无效用法。", - "add_usage": "用法 (add): team add <角色ID,...> [索引]", + "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_parse_index": "无法解析索引:%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>", + "set_usage": "用法 (set):team set <索引> <角色ID>", "index_out_of_range": "你指定的索引超出了范围。", "failed_parse_avatar_id": "无法解析的角色ID:%s", "avatar_already_in_team": "角色已经在你的队伍中了。", From 827044b3da0d1395e0daf046dd00bdea8b61415d Mon Sep 17 00:00:00 2001 From: linebear <linebear@mail.com> Date: Thu, 19 May 2022 05:21:25 +0800 Subject: [PATCH 17/18] Fix this does not look like a tar archive error when extract log file --- .gitignore | 2 +- src/main/resources/logback.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index f792ae6b8..ede33151b 100644 --- a/.gitignore +++ b/.gitignore @@ -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 diff --git a/src/main/resources/logback.xml b/src/main/resources/logback.xml index 656c1b443..82ec85ccd 100644 --- a/src/main/resources/logback.xml +++ b/src/main/resources/logback.xml @@ -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> From e3ed396889673e4d1c1f557d4cceed712c90c290 Mon Sep 17 00:00:00 2001 From: 2bllw8 <2bllw8@gmail.com> Date: Sun, 15 May 2022 17:04:00 +0200 Subject: [PATCH 18/18] Add web documentation - '/documentation': home page with all links - '/documentation/handbook': html version of the gm handbook - '/documentation/gachamapping': json document with the gacha mappings --- data/documentation/handbook.html | 162 ++++++++++++++++++ data/documentation/index.html | 106 ++++++++++++ .../java/emu/grasscutter/Grasscutter.java | 2 + .../documentation/DocumentationHandler.java | 9 + .../DocumentationServerHandler.java | 19 ++ .../GachaMappingRequestHandler.java | 155 +++++++++++++++++ .../documentation/HandbookRequestHandler.java | 127 ++++++++++++++ .../documentation/RootRequestHandler.java | 42 +++++ src/main/resources/languages/en-US.json | 22 +++ 9 files changed, 644 insertions(+) create mode 100644 data/documentation/handbook.html create mode 100644 data/documentation/index.html create mode 100644 src/main/java/emu/grasscutter/server/http/documentation/DocumentationHandler.java create mode 100644 src/main/java/emu/grasscutter/server/http/documentation/DocumentationServerHandler.java create mode 100644 src/main/java/emu/grasscutter/server/http/documentation/GachaMappingRequestHandler.java create mode 100644 src/main/java/emu/grasscutter/server/http/documentation/HandbookRequestHandler.java create mode 100644 src/main/java/emu/grasscutter/server/http/documentation/RootRequestHandler.java diff --git a/data/documentation/handbook.html b/data/documentation/handbook.html new file mode 100644 index 000000000..6a77d3806 --- /dev/null +++ b/data/documentation/handbook.html @@ -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}}

+ +

{{TITLE_COMMANDS}}

+
+ + + + + + + + {{COMMANDS_TABLE}} +
{{HEADER_COMMAND}}{{HEADER_DESCRIPTION}}
+ +

{{TITLE_AVATARS}}

+
+ + + + + + + + {{AVATARS_TABLE}} +
{{HEADER_ID}}{{HEADER_AVATAR}}
+ +

{{TITLE_ITEMS}}

+
+ + + + + + + + {{ITEMS_TABLE}} +
{{HEADER_ID}}{{HEADER_ITEM}}
+ + +

{{TITLE_SCENES}}

+
+ + + + + + + + {{SCENES_TABLE}} +
{{HEADER_ID}}{{HEADER_SCENE}}
+ +

{{TITLE_MONSTERS}}

+
+ + + + + + + + {{MONSTERS_TABLE}} +
{{HEADER_ID}}{{HEADER_MONSTER}}
+
+
+
+ +
+ + diff --git a/data/documentation/index.html b/data/documentation/index.html new file mode 100644 index 000000000..f89dbe258 --- /dev/null +++ b/data/documentation/index.html @@ -0,0 +1,106 @@ + + + + + + + + + Documentation + + +
+
+

{{TITLE}}

+ + +
+
+
+ +
+ + diff --git a/src/main/java/emu/grasscutter/Grasscutter.java b/src/main/java/emu/grasscutter/Grasscutter.java index 316c2030e..3cf8363e7 100644 --- a/src/main/java/emu/grasscutter/Grasscutter.java +++ b/src/main/java/emu/grasscutter/Grasscutter.java @@ -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(); diff --git a/src/main/java/emu/grasscutter/server/http/documentation/DocumentationHandler.java b/src/main/java/emu/grasscutter/server/http/documentation/DocumentationHandler.java new file mode 100644 index 000000000..de7f543a3 --- /dev/null +++ b/src/main/java/emu/grasscutter/server/http/documentation/DocumentationHandler.java @@ -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); +} diff --git a/src/main/java/emu/grasscutter/server/http/documentation/DocumentationServerHandler.java b/src/main/java/emu/grasscutter/server/http/documentation/DocumentationServerHandler.java new file mode 100644 index 000000000..24c8236de --- /dev/null +++ b/src/main/java/emu/grasscutter/server/http/documentation/DocumentationServerHandler.java @@ -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); + } +} diff --git a/src/main/java/emu/grasscutter/server/http/documentation/GachaMappingRequestHandler.java b/src/main/java/emu/grasscutter/server/http/documentation/GachaMappingRequestHandler.java new file mode 100644 index 000000000..e405d9b54 --- /dev/null +++ b/src/main/java/emu/grasscutter/server/http/documentation/GachaMappingRequestHandler.java @@ -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 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>() { + }.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 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(); + } +} diff --git a/src/main/java/emu/grasscutter/server/http/documentation/HandbookRequestHandler.java b/src/main/java/emu/grasscutter/server/http/documentation/HandbookRequestHandler.java new file mode 100644 index 000000000..65250d12c --- /dev/null +++ b/src/main/java/emu/grasscutter/server/http/documentation/HandbookRequestHandler.java @@ -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 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>() { + }.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 avatarMap = GameData.getAvatarDataMap(); + final Int2ObjectMap itemMap = GameData.getItemDataMap(); + final Int2ObjectMap sceneMap = GameData.getSceneDataMap(); + final Int2ObjectMap 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 -> "" + cmd.label() + "" + + cmd.description() + "") + .collect(Collectors.joining("\n"))) + // Avatars table + .replace("{{AVATARS_TABLE}}", GameData.getAvatarDataMap().keySet() + .intStream() + .sorted() + .mapToObj(avatarMap::get) + .map(data -> "" + data.getId() + "" + + map.get(data.getNameTextMapHash()) + "") + .collect(Collectors.joining("\n"))) + // Items table + .replace("{{ITEMS_TABLE}}", GameData.getItemDataMap().keySet() + .intStream() + .sorted() + .mapToObj(itemMap::get) + .map(data -> "" + data.getId() + "" + + map.get(data.getNameTextMapHash()) + "") + .collect(Collectors.joining("\n"))) + // Scenes table + .replace("{{SCENES_TABLE}}", GameData.getSceneDataMap().keySet() + .intStream() + .sorted() + .mapToObj(sceneMap::get) + .map(data -> "" + data.getId() + "" + + data.getScriptData() + "") + .collect(Collectors.joining("\n"))) + .replace("{{MONSTERS_TABLE}}", GameData.getMonsterDataMap().keySet() + .intStream() + .sorted() + .mapToObj(monsterMap::get) + .map(data -> "" + data.getId() + "" + + map.get(data.getNameTextMapHash()) + "") + .collect(Collectors.joining("\n"))); + + response.send(content); + } +} diff --git a/src/main/java/emu/grasscutter/server/http/documentation/RootRequestHandler.java b/src/main/java/emu/grasscutter/server/http/documentation/RootRequestHandler.java new file mode 100644 index 000000000..56a68cac8 --- /dev/null +++ b/src/main/java/emu/grasscutter/server/http/documentation/RootRequestHandler.java @@ -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); + } +} diff --git a/src/main/resources/languages/en-US.json b/src/main/resources/languages/en-US.json index b45862f94..f48eafeee 100644 --- a/src/main/resources/languages/en-US.json +++ b/src/main/resources/languages/en-US.json @@ -386,5 +386,27 @@ "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" + } } }