Commit 38fd7bb7 authored by Magix's avatar Magix Committed by GitHub
Browse files

Merge pull request #567 from Grasscutters/localization

Implement a proper language system
parents 6d678557 c11e83c4
package emu.grasscutter.languages;
public final class CNLanguage {
public String An_error_occurred_during_game_update = "游戏更新时发生了错误.";
public String Starting_Grasscutter = "正在开启Grasscutter...";
public String Invalid_server_run_mode = "无效的服务器运行模式. ";
public String Server_run_mode = "服务器运行模式必须为以下几种之一: 'HYBRID'(混合模式), 'DISPATCH_ONLY'(仅dispatch模式), 或 'GAME_ONLY'(仅游戏模式). 无法启动Grasscutter...";
public String Shutting_down = "正在停止....";
public String Start_done = "加载完成!需要指令帮助请输入 \"help\"";
public String Dispatch_mode_not_support_command = "仅dispatch模式无法使用指令。";
public String Command_error = "命令错误:";
public String Error = "出现错误.";
public String Grasscutter_is_free = "Grasscutter是免费软件,如果你是花钱买到的,你大概被骗了。主页: https://github.com/Grasscutters/Grasscutter";
public String Game_start_port = "游戏服务器已在端口 {port} 上开启。";
public String Client_connect = "来自 {address} 的客户端已连接。";
public String Client_disconnect = "来自 {address} 的客户端已断开。";
public String Client_request = "[Dispatch] 客户端 {ip} 请求: {method} {url}";
public String Not_load_keystore = "[Dispatch] 无法加载证书,正在尝试默认密码...";
public String Use_default_keystore = "[Dispatch] 成功使用默认密码加载证书. 请考虑将config.json中的KeystorePassword项改为123456.";
public String Load_keystore_error = "[Dispatch] 加载证书时出现错误!";
public String Not_find_ssl_cert = "[Dispatch] 未找到SSL证书,正在回滚至HTTP模式。";
public String Welcome = "欢迎使用Grasscutter";
public String Potential_unhandled_request = "[Dispatch] 潜在的未处理请求: {method} {url}";
public String Client_login_token = "[Dispatch] 客户端 {ip} 正在尝试使用token登录...";
public String Client_token_login_failed = "[Dispatch] 客户端 {ip} 使用token登录失败。";
public String Client_login_in_token = "[Dispatch] 客户端 {ip} 使用token以 {uid} 的身份登录。";
public String Game_account_cache_error = "游戏账户缓存出现错误。";
public String Wrong_session_key = "会话密钥错误。";
public String Client_exchange_combo_token = "[Dispatch] 客户端 {ip} 成功交换token。";
public String Client_failed_exchange_combo_token = "[Dispatch] 客户端 {ip} 交换token失败。";
public String Dispatch_start_server_port = "[Dispatch] Dispatch服务器已在端口 {port} 上开启。";
public String Client_failed_login_account_create = "[Dispatch] 客户端 {ip} 登录失败: 已创建UID为 {uid} 的账户。";
public String Client_failed_login_account_create_failed = "[Dispatch] 客户端 {ip} 登录失败: 创建账户失败。";
public String Client_failed_login_account_no_found = "[Dispatch] 客户端 {ip} 登录失败: 未找到帐户。";
public String Client_login = "[Dispatch] 客户端 {ip} 以 {uid} 的身份登录。";
public String Username_not_found = "未找到此用户名.";
public String Username_not_found_create_failed = "未找到此用户名, 创建失败。.";
// Command
public String No_command_specified = "未指定命令.";
public String Unknown_command = "未知命令: ";
public String You_not_permission_run_command = "你没有权限运行此命令.";
public String This_command_can_only_run_from_console = "此命令只能在控制台运行.";
public String Run_this_command_in_game = "请在游戏内运行此命令.";
public String Invalid_playerId = "无效的玩家ID.";
public String Player_not_found = "未找到此玩家.";
public String Player_is_offline = "此玩家已离线.";
public String Invalid_amount = "无效的数量.";
public String Invalid_arguments = "无效的命令参数.";
public String Invalid_artifact_id = "无效的圣遗物ID.";
public String Invalid_avatar_id = "无效的角色ID.";
public String Invalid_avatar_level = "无效的角色等级.";
public String Invalid_entity_id = "无效的物品ID.";
public String Invalid_item_id = "无效的物品ID.";
public String Invalid_item_level = "无效的物品等级.";
public String Invalid_item_refinement = "无效的精炼等级.";
public String Invalid_UID = "无效的UID.";
public String Enabled = "启用";
public String Disabled = "禁用";
public String No_command_found = "未找到命令.";
public String Help = "帮助";
public String Player_not_found_or_offline = "此玩家不存在或已离线.";
public String Success = "成功";
public String Target_cleared = "已清除选择目标";
public String Target_set = "接下来的命令将默认以 @{uid} 为目标。输入命令时不必继续携带UID参数。";
public String Target_needed = "此命令需要指定一个目标用户. 输入命令时携带 <@UID> 参数或使用 /target @UID 来指定一个默认目标用户.";
// Help
public String Help_usage = " 用法: ";
public String Help_aliases = " 别名: ";
public String Help_available_command = " 可用命令:";
// Account
public String Modify_user_account = "修改用户帐户";
public String Account_exists = "账户已存在.";
public String Account_create_UID = "UID为 {uid} 的账户已创建.";
public String Account_delete = "已删除账户.";
public String Account_not_find = "账户不存在.";
public String Account_command_usage = "用法: account <create(创建)|delete(删除)> <用户名> [uid]";
// Broadcast
public String Broadcast_command_usage = "用法: broadcast <消息>";
public String Broadcast_message_sent = "消息已发送.";
// ChangeScene
public String Change_screen_usage = "用法: changescene <场景id>";
public String Change_screen_you_in_that_screen = "你已经在此场景中了";
public String Change_screen = "切换到场景 ";
public String Change_screen_not_exist = "此场景不存在。";
// Cleart_or_playerId
public String Clear_weapons = "已清除 {name} 的武器.";
public String Clear_artifacts = "已清除 {name} 的圣遗物 .";
public String Clear_materials = "已清除 {name} 的材料.";
public String Clear_furniture = "已清除 {name} 的摆设.";
public String Clear_displays = "已清除 {name} 的displays.";
public String Clear_virtuals = "已清除 {name} 的virtuals.";
public String Clear_everything = "已清除 {name} 的所有物品.";
// Coop
public String Coop_usage = "用法: coop <房主的UID>";
public String Coop_success = "已将{target}拉进{host}的世界.";
// Drop
public String Drop_usage = "用法: drop <物品ID|物品名> [数量]";
public String Drop_dropped_of = "已在地上丢弃 {amount} 个 {item}.";
// EnterDungeon
public String EnterDungeon_usage = "用法: enterdungeon <副本 id>";
public String EnterDungeon_changed_to_dungeon = "已进入副本 ";
public String EnterDungeon_dungeon_not_found = "副本不存在";
public String EnterDungeon_you_in_that_dungeon = "你已经在此副本中了。";
// GiveAll
public String GiveAll_usage = "用法: giveall [数量]";
public String GiveAll_item = "正在给予所有物品...";
public String GiveAll_done = "完成。";
// GiveArtifact
public String GiveArtifact_usage = "用法: giveart|gart [玩家] <圣遗物Id> <主词条Id> [<副词条Id>[,<被强化次数>]]... [等级]";
public String GiveArtifact_given = "已将 {itemId} 给予 {target}.";
// GiveChar
public String GiveChar_usage = "用法: givechar <p玩家> <角色Id|角色名> [等级]";
public String GiveChar_given = "将等级为 {level} 的 {avatarId} 给予 {target}.";
// Give
public String Give_usage = "用法: give [玩家名] <物品ID|物品名> [数量] [等级] ";
public String Give_refinement_only_applicable_weapons = "精炼只对武器有效。";
public String Give_refinement_must_between_1_and_5 = "精炼等级必须在1和5之间。";
public String Give_given = "已将 {amount} 个 {item} 给与 {target}.";
public String Give_given_with_level_and_refinement = "已将 {amount} 个等级为 {lvl}, 精炼 {refinement} 的 {item} 给予 {target}.";
public String Give_given_level = "已将 {amount} 个等级为 {lvl} 的 {item} 给与 {target}.";
// GodMode
public String Godmode_usage = "用法: godmode [on|off|toggle]";
public String Godmode_status = "设置 {name} 的无敌模式为: {status} ";
// Heal
public String Heal_message = "所有角色已被治疗。";
// Kick
public String Kick_player_kick_player = "玩家 [{sendUid}:{sendName}] 已踢出 [{kickUid}:{kickName}]";
public String Kick_server_player = "正在踢出玩家 [{kickUid}:{kickName}]";
// Kill
public String Kill_usage = "用法: killall [玩家UID] [场景ID]";
public String Kill_scene_not_found_in_player_world = "未在玩家世界中找到此场景";
public String Kill_kill_monsters_in_scene = "已杀死场景 {id} 中的 {size} 只怪物。 ";
// KillCharacter
public String KillCharacter_usage = "用法: /killcharacter [玩家Id]";
public String KillCharacter_kill_current_character = "已干掉 {name} 当前的场上角色.";
// List
public String List_message = "现有 {size} 名玩家在线:";
// Permission
public String Permission_usage = "用法: permission <add|remove> <权限名>";
public String Permission_add = "权限已添加。";
public String Permission_have_permission = "此玩家已拥有此权限!";
public String Permission_remove = "权限已移除。";
public String Permission_not_have_permission = "此玩家未拥有此权限!";
// Position
public String Position_message = "坐标: {x},{y},{z}\n场景: {id}";
// Reload
public String Reload_reload_start = "正在重新加载配置.";
public String Reload_reload_done = "完成.";
// ResetConst
public String ResetConst_reset_all = "重置你所有角色的命座。";
public String ResetConst_reset_all_done = "{name} 的命座已被重置。请重新登录。";
// ResetShopLimit
public String ResetShopLimit_usage = "用法: /resetshop <玩家id>";
// SendMail
public String SendMail_usage = "用法: give [player] <itemId|itemName> [amount]";
public String SendMail_user_not_exist = "不存在id为 '{id}' 的用户。";
public String SendMail_start_composition = "开始编辑邮件的组成部分.\n请使用 `/sendmail <标题>` 以继续.\n你可以在任何时候使用`/sendmail stop` 来停止编辑.";
public String SendMail_templates = "很快就会有邮件模板了.......";
public String SendMail_invalid_arguments = "无效的参数.\n用法: `/sendmail <用户Id|all|help> [模板Id]``";
public String SendMail_send_cancel = "已取消发送邮件。";
public String SendMail_send_done = "已向 {name} 发送邮件!";
public String SendMail_send_all_done = "已向所有玩家发送邮件!";
public String SendMail_not_composition_end = "邮件组成部分编辑尚未结束.\n请使用 `/sendmail {args}` 或 `/sendmail stop` 来停止编辑";
public String SendMail_Please_use = "请使用 `/sendmail {args}`";
public String SendMail_set_title = "邮件标题已设为 '{title}'.\n使用 '/sendmail <邮件正文>' 以继续.";
public String SendMail_set_contents = "邮件的正文如下:\n '{contents}'\n使用 '/sendmail <发送者署名>' 以继续.";
public String SendMail_set_message_sender = "邮件的发送者已设为 '{send}'.\n使用 '/sendmail <物品Id|物品名|finish(结束编辑并发送)> [数量] [等级]";
public String SendMail_send = "已将 {amount} 个 {item} (等级 {lvl}) 作为邮件附件.\n你可以继续添加附件,也可以使用 `/sendmail finish` 来停止编辑并发送邮件.";
public String SendMail_invalid_arguments_please_use = "无效的参数 \n 请使用 `/sendmail {args}`";
public String SendMail_title = "<标题>";
public String SendMail_message = "<正文>";
public String SendMail_sender = "<发送者>";
public String SendMail_arguments = "<物品Id|物品名|finish(结束编辑并发送)> [数量] [等级]";
public String SendMail_error = "错误:无效的编写阶段 {stage}. 需要stacktrace请看服务器命令行.";
// SendMessage
public String SendMessage_usage = "用法: sendmessage <玩家名> <消息>";
public String SenaMessage_message_sent = "已发送.";
// SetFetterLevel
public String SetFetterLevel_usage = "用法: setfetterlevel <等级>";
public String SetFetterLevel_fetter_level_must_between_0_and_10 = "设置的好感等级必须位于 0 和 10 之间。";
public String SetFetterLevel_fetter_set_level = "好感等级已设置为 {level}";
public String SetFetterLevel_invalid_fetter_level = "无效的好感等级。";
// SetStats
public String SetStats_usage = "用法: setstats|stats <stat> <value>";
public String SetStats_setstats_help_message = "用法: /setstats|stats <hp(生命值) | mhp(最大生命值) | def(防御力) | atk(攻击) | em(元素精通) | crate(暴击率) | cdmg(暴击伤害)> <数值> 基本属性(整数)";
public String SetStats_stats_help_message = "用法: /stats <er(元素充能) | epyro(火伤) | ecryo(冰伤) | ehydro(水伤) | eanemo(风伤) | egeo(岩伤) | edend(草伤) | eelec(雷伤) | ephys(物伤)> <数值> 元素属性(百分比)";
public String SetStats_set_max_hp = "最大生命值已设为 {int}.";
public String SetStats_set_max_hp_error = "无效的生命数值.";
public String SetStats_set_hp = "生命设置为 {int}.";
public String SetStats_set_hp_error = "无效的生命数值.";
public String SetStats_set_def = "防御力设置为 {int}.";
public String SetStats_set_def_error = "无效的防御力数值.";
public String SetStats_set_atk = "攻击力设置为 {int}.";
public String SetStats_set_atk_error = "无效的攻击力数值.";
public String SetStats_set_em = "元素精通设置为 {int}.";
public String SetStats_set_em_error = "无效的元素精通数值.";
public String SetStats_set_er = "元素充能设置为 {int}%.";
public String SetStats_set_er_error = "无效的元素充能数值.";
public String SetStats_set_cr = "暴击率设置为 {int}%.";
public String SetStats_set_cr_error = "无效的暴击率数值.";
public String SetStats_set_cd = "暴击伤害设置为 {int}%.";
public String SetStats_set_cd_error = "无效的暴击伤害数值.";
public String SetStats_set_pdb = "火伤设置为 {int}%.";
public String SetStats_set_pdb_error = "无效的火伤数值.";
public String SetStats_set_cdb = "冰伤设置为 {int}%.";
public String SetStats_set_cdb_error = "无效的冰伤数值.";
public String SetStats_set_hdb = "水伤设置为 {int}%.";
public String SetStats_set_hdb_error = "无效的水伤数值.";
public String SetStats_set_adb = "风伤设置为 {int}%.";
public String SetStats_set_adb_error = "无效的风伤数值.";
public String SetStats_set_gdb = "岩伤设置为 {int}%.";
public String SetStats_set_gdb_error = "无效的岩伤数值.";
public String SetStats_set_edb = "雷伤设置为 {int}%.";
public String SetStats_set_edb_error = "无效的雷伤数值.";
public String SetStats_set_physdb = "物伤设置为 {int}%.";
public String SetStats_set_physdb_error = "无效的物伤数值.";
public String SetStats_set_ddb = "草伤设置为 {int}%.";
public String SetStats_set_ddb_error = "无效的草伤数值.";
// SetWorldLevel
public String SetWorldLevel_usage = "用法: setworldlevel <level>";
public String SetWorldLevel_world_level_must_between_0_and_8 = "世界等级必须在0-8之间。";
public String SetWorldLevel_set_world_level = "世界等级已设置为 {level}.";
public String SetWorldLevel_invalid_world_level = "无效的世界等级.";
// Spawn
public String Spawn_usage = "用法: spawn <实体ID|实体名> [数量] [等级(仅限怪物)]";
public String Spawn_message = "已生成 {amount} 个 {id}.";
// Stop
public String Stop_message = "正在关闭服务器...";
// Talent
public String Talent_usage_1 = "设置技能等级: /talent set <技能ID> <数值>";
public String Talent_usage_2 = "另一种方式: /talent <n 或 e 或 q> <数值>";
public String Talent_usage_3 = "获取技能ID: /talent getid";
public String Talent_lower_16 = "技能等级应低于16。";
public String Talent_set_atk = "设置普通攻击等级为 {level}.";
public String Talent_set_e = "设置元素战技(e技能)等级为 {level}.";
public String Talent_set_q = "设置元素爆发(q技能)等级为 {level}.";
public String Talent_invalid_skill_id = "无效的技能ID。";
public String Talent_set_this = "技能等级已设为 {level}.";
public String Talent_set_id = "将技能 {id} 的等级设为 {level}.";
public String Talent_invalid_talent_level = "无效的技能等级。";
public String Talent_normal_attack_id = "普通攻击技能ID {id}.";
public String Talent_e_skill_id = "元素战技(e技能)ID {id}.";
public String Talent_q_skill_id = "元素爆发(q技能)ID {id}.";
// TeleportAll
public String TeleportAll_message = "此命令仅在多人游戏下可用。";
// Teleport
public String Teleport_usage_server = "用法: /tp <x> <y> <z> [场景ID]";
public String Teleport_invalid_position = "无效的位置。";
public String Teleport_message = "已将 {name} 传送到场景 {id} ,坐标 {x},{y},{z}";
// Weather
public String Weather_usage = "用法: weather <天气ID> [气候ID]";
public String Weather_message = "已修改天气为 {weatherId} 气候为 {climateId}";
public String Weather_invalid_id = "无效的ID。";
}
package emu.grasscutter.languages;
public final class Language {
public String An_error_occurred_during_game_update = "An error occurred during game update.";
public String Starting_Grasscutter = "Starting Grasscutter...";
public String Invalid_server_run_mode = "Invalid server run mode.";
public String Server_run_mode = "Server run mode must be 'HYBRID', 'DISPATCH_ONLY', or 'GAME_ONLY'. Unable to start Grasscutter...";
public String Shutting_down = "Shutting down...";
public String Start_done = "Done! For help, type \"help\"";
public String Dispatch_mode_not_support_command = "Commands are not supported in dispatch only mode.";
public String Command_error = "Command error:";
public String Error = "An error occurred.";
public String Grasscutter_is_free = "Grasscutter is FREE software. If you have paid for this, you may have been scammed. Homepage: https://github.com/Grasscutters/Grasscutter";
public String Game_start_port = "Game Server started on port {port}";
public String Client_connect = "Client connected from {address}";
public String Client_disconnect = "Client disconnected from {address}";
public String Client_request = "[Dispatch] Client {ip} {method} request: {url}";
public String Not_load_keystore = "[Dispatch] Unable to load keystore. Trying default keystore password...";
public String Use_default_keystore = "[Dispatch] The default keystore password was loaded successfully. Please consider setting the password to 123456 in config.json.";
public String Load_keystore_error = "[Dispatch] Error while loading keystore!";
public String Not_find_ssl_cert = "[Dispatch] No SSL cert found! Falling back to HTTP server.";
public String Welcome = "Welcome to Grasscutter";
public String Potential_unhandled_request = "[Dispatch] Potential unhandled {method} request: {url}";
public String Client_try_login = "[Dispatch] Client {ip} is trying to log in";
public String Client_login_token = "[Dispatch] Client {ip} is trying to log in via token";
public String Client_token_login_failed = "[Dispatch] Client {ip} failed to log in via token";
public String Client_login_in_token = "[Dispatch] Client {ip} logged in via token as {uid}";
public String Game_account_cache_error = "Game account cache information error";
public String Wrong_session_key = "Wrong session key.";
public String Client_exchange_combo_token = "[Dispatch] Client {ip} succeed to exchange combo token";
public String Client_failed_exchange_combo_token = "[Dispatch] Client {ip} failed to exchange combo token";
public String Dispatch_start_server_port = "[Dispatch] Dispatch server started on port {port}";
public String Client_failed_login_account_create = "[Dispatch] Client {ip} failed to log in: Account {uid} created";
public String Client_failed_login_account_create_failed = "[Dispatch] Client {ip} failed to log in: Account create failed";
public String Client_failed_login_account_no_found = "[Dispatch] Client {ip} failed to log in: Account no found";
public String Client_login = "[Dispatch] Client {ip} logged in as {uid}";
public String Username_not_found = "Username not found.";
public String Username_not_found_create_failed = "Username not found, create failed.";
public String Create_resources_folder = "Creating resources folder...";
public String Place_copy = "Place a copy of 'BinOutput' and 'ExcelBinOutput' in the resources folder.";
// Command
public String No_command_specified = "No command specified.";
public String Unknown_command = "Unknown command: ";
public String You_not_permission_run_command = "You do not have permission to run this command.";
public String This_command_can_only_run_from_console = "This command can only be run from the console.";
public String Run_this_command_in_game = "Run this command in-game.";
public String Invalid_amount = "Invalid amount.";
public String Invalid_arguments = "Invalid arguments.";
public String Invalid_artifact_id = "Invalid artifact ID.";
public String Invalid_avatar_id = "Invalid avatar id.";
public String Invalid_avatar_level = "Invalid avatar level.";
public String Invalid_entity_id = "Invalid entity id.";
public String Invalid_item_id = "Invalid item id.";
public String Invalid_item_level = "Invalid item level.";
public String Invalid_item_refinement = "Invalid item refinement level.";
public String Invalid_playerId = "Invalid playerId.";
public String Invalid_UID = "Invalid UID.";
public String Player_not_found = "Player not found.";
public String Player_is_offline = "Player is offline.";
public String Enabled = "enabled";
public String Disabled = "disabled";
public String No_command_found = "No command found.";
public String Help = "Help";
public String Player_not_found_or_offline = "Player not found or offline.";
public String Success = "Success";
public String Target_cleared = "Target cleared.";
public String Target_set = "Subsequent commands will target @{uid} by default.";
public String Target_needed = "This command requires a target UID. Add a <@UID> argument or set a persistent target with /target @UID.";
// Help
public String Help_usage = " Usage: ";
public String Help_aliases = " Aliases: ";
public String Help_available_command = "Available commands:";
// Account
public String Modify_user_account = "Modify user accounts";
public String Account_exists = "Account already exists.";
public String Account_create_UID = "Account created with UID {uid}.";
public String Account_delete = "Account deleted.";
public String Account_not_find = "Account not found.";
public String Account_command_usage = "Usage: account <create|delete> <username> [uid]";
// Broadcast
public String Broadcast_command_usage = "Usage: broadcast <message>";
public String Broadcast_message_sent = "Message sent.";
// ChangeScene
public String Change_screen_usage = "Usage: changescene <scene id>";
public String Change_screen_you_in_that_screen = "You are already in that scene";
public String Change_screen = "Changed to scene ";
public String Change_screen_not_exist = "Scene does not exist";
// Clear
public String Clear_usage = "Usage: clear <all|wp|art|mat>";
public String Clear_weapons = "Cleared weapons for {name} .";
public String Clear_artifacts = "Cleared artifacts for {name} .";
public String Clear_materials = "Cleared materials for {name} .";
public String Clear_furniture = "Cleared furniture for {name} .";
public String Clear_displays = "Cleared displays for {name} .";
public String Clear_virtuals = "Cleared virtuals for {name} .";
public String Clear_everything = "Cleared everything for {name} .";
// Coop
public String Coop_usage = "Usage: coop <host UID>";
public String Coop_success = "Summoned {target} to {host}'s world.";
// Drop
public String Drop_usage = "Usage: drop <itemId|itemName> [amount]";
public String Drop_dropped_of = "Dropped {amount} of {item}.";
// EnterDungeon
public String EnterDungeon_usage = "Usage: enterdungeon <dungeon id>";
public String EnterDungeon_changed_to_dungeon = "Changed to dungeon ";
public String EnterDungeon_dungeon_not_found = "Dungeon does not exist";
public String EnterDungeon_you_in_that_dungeon = "You are already in that dungeon";
// GiveAll
public String GiveAll_usage = "Usage: giveall [amount]";
public String GiveAll_item = "Giving all items...";
public String GiveAll_done = "Giving all items done";
// GiveArtifact
public String GiveArtifact_usage = "Usage: giveart|gart [player] <artifactId> <mainPropId> [<appendPropId>[,<times>]]... [level]";
public String GiveArtifact_given = "Given {itemId} to {target}.";
// GiveChar
public String GiveChar_usage = "Usage: givechar <player> <itemId|itemName> [amount]";
public String GiveChar_given = "Given {avatarId} with level {level} to {target}.";
// Give
public String Give_usage = "Usage: give <player> <itemId|itemName> [amount] [level]";
public String Give_refinement_only_applicable_weapons = "Refinement is only applicable to weapons.";
public String Give_refinement_must_between_1_and_5 = "Refinement must be between 1 and 5.";
public String Give_given = "Given {amount} of {item} to {target}.";
public String Give_given_with_level_and_refinement = "Given {item} with level {lvl}, refinement {refinement} {amount} times to {target}";
public String Give_given_level = "Given {item} with level {lvl} {amount} times to {target}";
// GodMode
public String Godmode_usage = "Usage: godmode [on|off|toggle]";
public String Godmode_status = "Godmode is now {status} for {name}.";
// Heal
public String Heal_message = "All characters have been healed.";
// Kick
public String Kick_player_kick_player = "Player [{sendUid}:{sendName}] has kicked player [{kickUid}:{kickName}]";
public String Kick_server_player = "Kicking player [{kickUid}:{kickName}]";
// Kill
public String Kill_usage = "Usage: killall [playerUid] [sceneId]";
public String Kill_scene_not_found_in_player_world = "Scene not found in player world";
public String Kill_kill_monsters_in_scene = "Killing {size} monsters in scene {id}";
// KillCharacter
public String KillCharacter_usage = "Usage: /killcharacter [playerId]";
public String KillCharacter_kill_current_character = "Killed {name} current character.";
// List
public String List_message = "There are {size} player(s) online:";
// Permission
public String Permission_usage = "Usage: permission <add|remove> <permission>";
public String Permission_add = "Permission added.";
public String Permission_have_permission = "They already have this permission!";
public String Permission_remove = "Permission removed.";
public String Permission_not_have_permission = "They don't have this permission!";
// Position
public String Position_message = "Coord: {x}, {y}, {z}\nScene id: {id}";
// Reload
public String Reload_reload_start = "Reloading config.";
public String Reload_reload_done = "Reload complete.";
// ResetConst
public String ResetConst_reset_all = "Reset all avatars' constellations.";
public String ResetConst_reset_all_done = "Constellations for {name} have been reset. Please relog to see changes.";
// ResetShopLimit
public String ResetShopLimit_usage = "Usage: /resetshop <player id>";
// SendMail
public String SendMail_usage = "Usage: give [player] <itemId|itemName> [amount]";
public String SendMail_user_not_exist = "The user with an id of '{id}' does not exist";
public String SendMail_start_composition = "Starting composition of message.\nPlease use `/sendmail <title>` to continue.\nYou can use `/sendmail stop` at any time";
public String SendMail_templates = "Mail templates coming soon implemented...";
public String SendMail_invalid_arguments = "Invalid arguments.\nUsage `/sendmail <userId|all|help> [templateId]`";
public String SendMail_send_cancel = "Message sending cancelled";
public String SendMail_send_done = "Message sent to user {name}!";
public String SendMail_send_all_done = "Message sent to all users!";
public String SendMail_not_composition_end = "Message composition not at final stage.\nPlease use `/sendmail {args}` or `/sendmail stop` to cancel";
public String SendMail_please_use = "Please use `/sendmail {args}`";
public String SendMail_set_title = "Message title set as '{title}'.\nUse '/sendmail <content>' to continue.";
public String SendMail_set_contents = "Message contents set as '{contents}'.\nUse '/sendmail <sender>' to continue.";
public String SendMail_set_message_sender = "Message sender set as '{send}'.\nUse '/sendmail <itemId|itemName|finish> [amount] [level]' to continue.";
public String SendMail_send = "Attached {amount} of {item} (level {lvl}) to the message.\nContinue adding more items or use `/sendmail finish` to send the message.";
public String SendMail_invalid_arguments_please_use = "Invalid arguments \n Please use `/sendmail {args}`";
public String SendMail_title = "<title>";
public String SendMail_message = "<message>";
public String SendMail_sender = "<sender>";
public String SendMail_arguments = "<itemId|itemName|finish> [amount] [level]";
public String SendMail_error = "ERROR: invalid construction stage {stage}. Check console for stacktrace.";
// SendMessage
public String SendMessage_usage = "Usage: sendmessage <player> <message>";
public String SenaMessage_message_sent = "Message sent.";
// SetFetterLevel
public String SetFetterLevel_usage = "Usage: setfetterlevel <level>";
public String SetFetterLevel_fetter_level_must_between_0_and_10 = "Fetter level must be between 0 and 10.";
public String SetFetterLevel_fetter_set_level = "Fetter level set to {level}";
public String SetFetterLevel_invalid_fetter_level = "Invalid fetter level.";
// SetStats
public String SetStats_usage_console = "Usage: setstats|stats @<UID> <stat> <value>";
public String SetStats_usage_ingame = "Usage: setstats|stats [@UID] <stat> <value>";
public String SetStats_help_message = """
\n\tValues for <stat>: hp | maxhp | def | atk | em | er | crate | cdmg | cdr | heal | heali | shield | defi
\t(cont.) Elemental DMG Bonus: epyro | ecryo | ehydro | egeo | edendro | eelectro | ephys
\t(cont.) Elemental RES: respyro | rescryo | reshydro | resgeo | resdendro | reselectro | resphys
""";
public String SetStats_value_error = "Invalid stat value.";
public String SetStats_set_self = "{name} set to {value}.";
public String SetStats_set_for_uid = "{name} for {uid} set to {value}.";
public String Stats_FIGHT_PROP_MAX_HP = "Max HP";
public String Stats_FIGHT_PROP_CUR_HP = "Current HP";
public String Stats_FIGHT_PROP_CUR_ATTACK = "ATK";
public String Stats_FIGHT_PROP_BASE_ATTACK = "Base ATK";
public String Stats_FIGHT_PROP_DEFENSE = "DEF";
public String Stats_FIGHT_PROP_ELEMENT_MASTERY = "Elemental Mastery";
public String Stats_FIGHT_PROP_CHARGE_EFFICIENCY = "Energy Recharge";
public String Stats_FIGHT_PROP_CRITICAL = "Crit Rate";
public String Stats_FIGHT_PROP_CRITICAL_HURT = "Crit DMG";
public String Stats_FIGHT_PROP_ADD_HURT = "DMG Bonus";
public String Stats_FIGHT_PROP_WIND_ADD_HURT = "Anemo DMG Bonus";
public String Stats_FIGHT_PROP_ICE_ADD_HURT = "Cryo DMG Bonus";
public String Stats_FIGHT_PROP_GRASS_ADD_HURT = "Dendro DMG Bonus";
public String Stats_FIGHT_PROP_ELEC_ADD_HURT = "Electro DMG Bonus";
public String Stats_FIGHT_PROP_ROCK_ADD_HURT = "Geo DMG Bonus";
public String Stats_FIGHT_PROP_WATER_ADD_HURT = "Hydro DMG Bonus";
public String Stats_FIGHT_PROP_FIRE_ADD_HURT = "Pyro DMG Bonus";
public String Stats_FIGHT_PROP_PHYSICAL_ADD_HURT = "Physical DMG Bonus";
public String Stats_FIGHT_PROP_SUB_HURT = "DMG Reduction";
public String Stats_FIGHT_PROP_WIND_SUB_HURT = "Anemo RES";
public String Stats_FIGHT_PROP_ICE_SUB_HURT = "Cryo RES";
public String Stats_FIGHT_PROP_GRASS_SUB_HURT = "Dendro RES";
public String Stats_FIGHT_PROP_ELEC_SUB_HURT = "Electro RES";
public String Stats_FIGHT_PROP_ROCK_SUB_HURT = "Geo RES";
public String Stats_FIGHT_PROP_WATER_SUB_HURT = "Hydro RES";
public String Stats_FIGHT_PROP_FIRE_SUB_HURT = "Pyro RES";
public String Stats_FIGHT_PROP_PHYSICAL_SUB_HURT = "Physical RES";
public String Stats_FIGHT_PROP_SKILL_CD_MINUS_RATIO = "Cooldown Reduction";
public String Stats_FIGHT_PROP_HEAL_ADD = "Healing Bonus";
public String Stats_FIGHT_PROP_HEALED_ADD = "Incoming Healing Bonus";
public String Stats_FIGHT_PROP_SHIELD_COST_MINUS_RATIO = "Shield Strength";
public String Stats_FIGHT_PROP_DEFENCE_IGNORE_RATIO = "DEF Ignore";
// SetWorldLevel
public String SetWorldLevel_usage = "Usage: setworldlevel <level>";
public String SetWorldLevel_world_level_must_between_0_and_8 = "World level must be between 0-8";
public String SetWorldLevel_set_world_level = "World level set to {level}.";
public String SetWorldLevel_invalid_world_level = "Invalid world level.";
// Spawn
public String Spawn_usage = "Usage: spawn <entityId> [amount] [level(monster only)]";
public String Spawn_message = "Spawned {amount} of {id}.";
// Stop
public String Stop_message = "Server shutting down...";
// Talent
public String Talent_usage_1 = "To set talent level: /talent set <talentID> <value>";
public String Talent_usage_2 = "Another way to set talent level: /talent <n or e or q> <value>";
public String Talent_usage_3 = "To get talent ID: /talent getid";
public String Talent_lower_16 = "Invalid talent level. Level should be lower than 16";
public String Talent_set_id = "Set talent {id} to {level}.";
public String Talent_set_atk = "Set talent Normal ATK to {level}.";
public String Talent_set_e = "Set talent E to {level}.";
public String Talent_set_q = "Set talent Q to {level}.";
public String Talent_invalid_skill_id = "Invalid skill ID.";
public String Talent_set_this = "Set this talent to {level}.";
public String Talent_invalid_talent_level = "Invalid talent level.";
public String Talent_normal_attack_id = "Normal Attack ID {id}.";
public String Talent_e_skill_id = "E skill ID {id}.";
public String Talent_q_skill_id = "Q skill ID {id}.";
// TeleportAll
public String TeleportAll_message = "You only can use this command in MP mode.";
// Teleport
public String Teleport_usage = "Usage: /tp <x> <y> <z> [scene id]";
public String Teleport_invalid_position = "Invalid position.";
public String Teleport_message = "Teleported {name} to {x},{y},{z} in scene {id}";
// Weather
public String Weather_usage = "Usage: weather <weatherId> [climateId]";
public String Weather_message = "Changed weather to {weatherId} with climate {climateId}";
public String Weather_invalid_id = "Invalid ID.";
}
...@@ -28,6 +28,7 @@ public final class PlayerHook { ...@@ -28,6 +28,7 @@ public final class PlayerHook {
/** /**
* Kicks a player from the server. * Kicks a player from the server.
* TODO: Refactor to kick using a packet.
*/ */
public void kick() { public void kick() {
this.player.getSession().close(); this.player.getSession().close();
......
# Grasscutter Plugin API
**Warning!** As of now, this is a work in progress and isn't completely documented.
\ No newline at end of file
...@@ -2,6 +2,7 @@ package emu.grasscutter.server.dispatch; ...@@ -2,6 +2,7 @@ package emu.grasscutter.server.dispatch;
import java.io.IOException; import java.io.IOException;
import java.util.Arrays; import java.util.Arrays;
import java.util.Objects;
import emu.grasscutter.Grasscutter; import emu.grasscutter.Grasscutter;
import emu.grasscutter.Grasscutter.ServerDebugMode; import emu.grasscutter.Grasscutter.ServerDebugMode;
...@@ -9,6 +10,8 @@ import express.http.HttpContextHandler; ...@@ -9,6 +10,8 @@ import express.http.HttpContextHandler;
import express.http.Request; import express.http.Request;
import express.http.Response; import express.http.Response;
import static emu.grasscutter.utils.Language.translate;
public final class DispatchHttpJsonHandler implements HttpContextHandler { public final class DispatchHttpJsonHandler implements HttpContextHandler {
private final String response; private final String response;
private final String[] missingRoutes = { // TODO: When http requests for theses routes are found please remove it from this list and update the route request type in the DispatchServer private final String[] missingRoutes = { // TODO: When http requests for theses routes are found please remove it from this list and update the route request type in the DispatchServer
...@@ -31,8 +34,8 @@ public final class DispatchHttpJsonHandler implements HttpContextHandler { ...@@ -31,8 +34,8 @@ public final class DispatchHttpJsonHandler implements HttpContextHandler {
@Override @Override
public void handle(Request req, Response res) throws IOException { public void handle(Request req, Response res) throws IOException {
// Checking for ALL here isn't required as when ALL is enabled enableDevLogging() gets enabled // Checking for ALL here isn't required as when ALL is enabled enableDevLogging() gets enabled
if(Grasscutter.getConfig().DebugMode == ServerDebugMode.MISSING && Arrays.stream(missingRoutes).anyMatch(x -> x == req.baseUrl())) { if(Grasscutter.getConfig().DebugMode == ServerDebugMode.MISSING && Arrays.stream(missingRoutes).anyMatch(x -> Objects.equals(x, req.baseUrl()))) {
Grasscutter.getLogger().info(Grasscutter.getLanguage().Client_request.replace("{ip}", req.ip()).replace("{method}", req.method()).replace("{url}", req.baseUrl()) + (Grasscutter.getConfig().DebugMode == ServerDebugMode.MISSING ? "(MISSING)" : "")); Grasscutter.getLogger().info(translate("messages.dispatch.request", req.ip(), req.method(), req.baseUrl()) + (Grasscutter.getConfig().DebugMode == ServerDebugMode.MISSING ? "(MISSING)" : ""));
} }
res.send(response); res.send(response);
} }
......
...@@ -35,6 +35,8 @@ import java.io.*; ...@@ -35,6 +35,8 @@ import java.io.*;
import java.net.URLDecoder; import java.net.URLDecoder;
import java.util.*; import java.util.*;
import static emu.grasscutter.utils.Language.translate;
public final class DispatchServer { public final class DispatchServer {
public static String query_region_list = ""; public static String query_region_list = "";
public static String query_cur_region = ""; public static String query_cur_region = "";
...@@ -213,21 +215,21 @@ public final class DispatchServer { ...@@ -213,21 +215,21 @@ public final class DispatchServer {
sslContextFactory.setKeyStorePassword(Grasscutter.getConfig().getDispatchOptions().KeystorePassword); sslContextFactory.setKeyStorePassword(Grasscutter.getConfig().getDispatchOptions().KeystorePassword);
} catch (Exception e) { } catch (Exception e) {
e.printStackTrace(); e.printStackTrace();
Grasscutter.getLogger().warn(Grasscutter.getLanguage().Not_load_keystore); Grasscutter.getLogger().warn(translate("messages.dispatch.keystore.password_error"));
try { try {
sslContextFactory.setKeyStorePath(keystoreFile.getPath()); sslContextFactory.setKeyStorePath(keystoreFile.getPath());
sslContextFactory.setKeyStorePassword("123456"); sslContextFactory.setKeyStorePassword("123456");
Grasscutter.getLogger().warn(Grasscutter.getLanguage().Use_default_keystore); Grasscutter.getLogger().warn(translate("messages.dispatch.keystore.default_password"));
} catch (Exception e2) { } catch (Exception e2) {
Grasscutter.getLogger().warn(Grasscutter.getLanguage().Load_keystore_error); Grasscutter.getLogger().warn(translate("messages.dispatch.keystore.general_error"));
e2.printStackTrace(); e2.printStackTrace();
} }
} }
serverConnector = new ServerConnector(server, sslContextFactory); serverConnector = new ServerConnector(server, sslContextFactory);
} else { } else {
Grasscutter.getLogger().warn(Grasscutter.getLanguage().Not_find_ssl_cert); Grasscutter.getLogger().warn(translate("messages.dispatch.keystore.no_keystore_error"));
Grasscutter.getConfig().getDispatchOptions().UseSSL = false; Grasscutter.getConfig().getDispatchOptions().UseSSL = false;
serverConnector = new ServerConnector(server); serverConnector = new ServerConnector(server);
...@@ -250,11 +252,11 @@ public final class DispatchServer { ...@@ -250,11 +252,11 @@ public final class DispatchServer {
else config.enableCorsForAllOrigins(); else config.enableCorsForAllOrigins();
} }
}); });
httpServer.get("/", (req, res) -> res.send(Grasscutter.getLanguage().Welcome)); httpServer.get("/", (req, res) -> res.send(translate("messages.status.welcome")));
httpServer.raw().error(404, ctx -> { httpServer.raw().error(404, ctx -> {
if(Grasscutter.getConfig().DebugMode == ServerDebugMode.MISSING) { if(Grasscutter.getConfig().DebugMode == ServerDebugMode.MISSING) {
Grasscutter.getLogger().info(Grasscutter.getLanguage().Potential_unhandled_request.replace("{method}", ctx.method()).replace("{url}", ctx.url())); Grasscutter.getLogger().info(translate("messages.dispatch.unhandled_request_error", ctx.method(), ctx.url()));
} }
ctx.contentType("text/html"); ctx.contentType("text/html");
ctx.result("<!doctype html><html lang=\"en\"><body><img src=\"https://http.cat/404\" /></body></html>"); // I'm like 70% sure this won't break anything. ctx.result("<!doctype html><html lang=\"en\"><body><img src=\"https://http.cat/404\" /></body></html>"); // I'm like 70% sure this won't break anything.
...@@ -312,7 +314,7 @@ public final class DispatchServer { ...@@ -312,7 +314,7 @@ public final class DispatchServer {
if (requestData == null) { if (requestData == null) {
return; return;
} }
Grasscutter.getLogger().info(Grasscutter.getLanguage().Client_try_login.replace("{ip}", req.ip())); Grasscutter.getLogger().info(translate("messages.dispatch.account.login_attempt", req.ip()));
res.send(this.getAuthHandler().handleGameLogin(req, requestData)); res.send(this.getAuthHandler().handleGameLogin(req, requestData));
}); });
...@@ -332,7 +334,7 @@ public final class DispatchServer { ...@@ -332,7 +334,7 @@ public final class DispatchServer {
return; return;
} }
LoginResultJson responseData = new LoginResultJson(); LoginResultJson responseData = new LoginResultJson();
Grasscutter.getLogger().info(Grasscutter.getLanguage().Client_login_token.replace("{ip}", req.ip())); Grasscutter.getLogger().info(translate("messages.dispatch.account.login_token_attempt"));
// Login // Login
Account account = DatabaseHelper.getAccountById(requestData.uid); Account account = DatabaseHelper.getAccountById(requestData.uid);
...@@ -340,16 +342,16 @@ public final class DispatchServer { ...@@ -340,16 +342,16 @@ public final class DispatchServer {
// Test // Test
if (account == null || !account.getSessionKey().equals(requestData.token)) { if (account == null || !account.getSessionKey().equals(requestData.token)) {
responseData.retcode = -111; responseData.retcode = -111;
responseData.message = Grasscutter.getLanguage().Game_account_cache_error; responseData.message = translate("messages.dispatch.account.account_cache_error");
Grasscutter.getLogger().info(Grasscutter.getLanguage().Client_token_login_failed.replace("{ip}", req.ip())); Grasscutter.getLogger().info(translate("messages.dispatch.account.login_token_error", req.ip()));
} else { } else {
responseData.message = "OK"; responseData.message = "OK";
responseData.data.account.uid = requestData.uid; responseData.data.account.uid = requestData.uid;
responseData.data.account.token = requestData.token; responseData.data.account.token = requestData.token;
responseData.data.account.email = account.getEmail(); responseData.data.account.email = account.getEmail();
Grasscutter.getLogger().info(Grasscutter.getLanguage().Client_login_in_token.replace("{ip}", req.ip()).replace("{uid}", responseData.data.account.uid)); Grasscutter.getLogger().info(translate("messages.dispatch.account.login_token_success", req.ip(), requestData.uid));
} }
res.send(responseData); res.send(responseData);
...@@ -379,16 +381,16 @@ public final class DispatchServer { ...@@ -379,16 +381,16 @@ public final class DispatchServer {
// Test // Test
if (account == null || !account.getSessionKey().equals(loginData.token)) { if (account == null || !account.getSessionKey().equals(loginData.token)) {
responseData.retcode = -201; responseData.retcode = -201;
responseData.message = Grasscutter.getLanguage().Wrong_session_key; responseData.message = translate("messages.dispatch.account.session_key_error");
Grasscutter.getLogger().info(Grasscutter.getLanguage().Client_failed_exchange_combo_token.replace("{ip}", req.ip())); Grasscutter.getLogger().info(translate("messages.dispatch.account.combo_token_error", req.ip()));
} else { } else {
responseData.message = "OK"; responseData.message = "OK";
responseData.data.open_id = loginData.uid; responseData.data.open_id = loginData.uid;
responseData.data.combo_id = "157795300"; responseData.data.combo_id = "157795300";
responseData.data.combo_token = account.generateLoginToken(); responseData.data.combo_token = account.generateLoginToken();
Grasscutter.getLogger().info(Grasscutter.getLanguage().Client_exchange_combo_token.replace("{ip}", req.ip())); Grasscutter.getLogger().info(translate("messages.dispatch.account.combo_token_success", req.ip()));
} }
res.send(responseData); res.send(responseData);
...@@ -461,7 +463,7 @@ public final class DispatchServer { ...@@ -461,7 +463,7 @@ public final class DispatchServer {
httpServer.raw().config.precompressStaticFiles = false; // If this isn't set to false, files such as images may appear corrupted when serving static files httpServer.raw().config.precompressStaticFiles = false; // If this isn't set to false, files such as images may appear corrupted when serving static files
httpServer.listen(Grasscutter.getConfig().getDispatchOptions().Port); httpServer.listen(Grasscutter.getConfig().getDispatchOptions().Port);
Grasscutter.getLogger().info(Grasscutter.getLanguage().Dispatch_start_server_port.replace("{port}", Integer.toString(httpServer.raw().port()))); Grasscutter.getLogger().info(translate("messages.dispatch.port_bind", Integer.toString(httpServer.raw().port())));
} }
private Map<String, String> parseQueryString(String qs) { private Map<String, String> parseQueryString(String qs) {
......
...@@ -8,6 +8,8 @@ import emu.grasscutter.server.dispatch.json.LoginResultJson; ...@@ -8,6 +8,8 @@ import emu.grasscutter.server.dispatch.json.LoginResultJson;
import express.http.Request; import express.http.Request;
import express.http.Response; import express.http.Response;
import static emu.grasscutter.utils.Language.translate;
public class DefaultAuthenticationHandler implements AuthenticationHandler { public class DefaultAuthenticationHandler implements AuthenticationHandler {
@Override @Override
...@@ -34,11 +36,9 @@ public class DefaultAuthenticationHandler implements AuthenticationHandler { ...@@ -34,11 +36,9 @@ public class DefaultAuthenticationHandler implements AuthenticationHandler {
// Check if account exists, else create a new one. // Check if account exists, else create a new one.
if (account == null) { if (account == null) {
// Account doesnt exist, so we can either auto create it if the config value is // Account doesn't exist, so we can either auto create it if the config value is set.
// set
if (Grasscutter.getConfig().getDispatchOptions().AutomaticallyCreateAccounts) { if (Grasscutter.getConfig().getDispatchOptions().AutomaticallyCreateAccounts) {
// This account has been created AUTOMATICALLY. There will be no permissions // This account has been created AUTOMATICALLY. There will be no permissions added.
// added.
account = DatabaseHelper.createAccountWithId(requestData.account, 0); account = DatabaseHelper.createAccountWithId(requestData.account, 0);
for (String permission : Grasscutter.getConfig().getDispatchOptions().defaultPermissions) { for (String permission : Grasscutter.getConfig().getDispatchOptions().defaultPermissions) {
...@@ -51,19 +51,18 @@ public class DefaultAuthenticationHandler implements AuthenticationHandler { ...@@ -51,19 +51,18 @@ public class DefaultAuthenticationHandler implements AuthenticationHandler {
responseData.data.account.token = account.generateSessionKey(); responseData.data.account.token = account.generateSessionKey();
responseData.data.account.email = account.getEmail(); responseData.data.account.email = account.getEmail();
Grasscutter.getLogger().info(Grasscutter.getLanguage().Client_failed_login_account_create.replace("{ip}", req.ip()).replace("{uid}", responseData.data.account.uid)); Grasscutter.getLogger().info(translate("messages.dispatch.account.account_login_create_success", req.ip(), responseData.data.account.uid));
} else { } else {
responseData.retcode = -201; responseData.retcode = -201;
responseData.message = Grasscutter.getLanguage().Username_not_found_create_failed; responseData.message = translate("messages.dispatch.account.username_create_error");
Grasscutter.getLogger().info(Grasscutter.getLanguage().Client_failed_login_account_no_found.replace("{ip}", req.ip())); Grasscutter.getLogger().info(translate("messages.dispatch.account.account_login_create_error", req.ip()));
} }
} else { } else {
responseData.retcode = -201; responseData.retcode = -201;
responseData.message = Grasscutter.getLanguage().Username_not_found; responseData.message = translate("messages.dispatch.account.username_error");
Grasscutter.getLogger().info(String Grasscutter.getLogger().info(translate("messages.dispatch.account.account_login_exist_error", req.ip()));
.format(Grasscutter.getLanguage().Client_failed_login_account_no_found, req.ip()));
} }
} else { } else {
// Account was found, log the player in // Account was found, log the player in
...@@ -72,7 +71,7 @@ public class DefaultAuthenticationHandler implements AuthenticationHandler { ...@@ -72,7 +71,7 @@ public class DefaultAuthenticationHandler implements AuthenticationHandler {
responseData.data.account.token = account.generateSessionKey(); responseData.data.account.token = account.generateSessionKey();
responseData.data.account.email = account.getEmail(); responseData.data.account.email = account.getEmail();
Grasscutter.getLogger().info(Grasscutter.getLanguage().Client_login.replace("{ip}", req.ip()).replace("{uid}", responseData.data.account.uid)); Grasscutter.getLogger().info(translate("messages.dispatch.account.login_success", req.ip(), responseData.data.account.uid));
} }
return responseData; return responseData;
......
...@@ -30,6 +30,8 @@ import java.time.OffsetDateTime; ...@@ -30,6 +30,8 @@ import java.time.OffsetDateTime;
import java.util.*; import java.util.*;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import static emu.grasscutter.utils.Language.translate;
public final class GameServer extends KcpServer { public final class GameServer extends KcpServer {
private final InetSocketAddress address; private final InetSocketAddress address;
private final GameServerPacketHandler packetHandler; private final GameServerPacketHandler packetHandler;
...@@ -50,6 +52,13 @@ public final class GameServer extends KcpServer { ...@@ -50,6 +52,13 @@ public final class GameServer extends KcpServer {
private final CombineManger combineManger; private final CombineManger combineManger;
public GameServer() {
this(new InetSocketAddress(
Grasscutter.getConfig().getGameServerOptions().Ip,
Grasscutter.getConfig().getGameServerOptions().Port
));
}
public GameServer(InetSocketAddress address) { public GameServer(InetSocketAddress address) {
super(address); super(address);
...@@ -79,7 +88,7 @@ public final class GameServer extends KcpServer { ...@@ -79,7 +88,7 @@ public final class GameServer extends KcpServer {
try { try {
onTick(); onTick();
} catch (Exception e) { } catch (Exception e) {
Grasscutter.getLogger().error(Grasscutter.getLanguage().An_error_occurred_during_game_update, e); Grasscutter.getLogger().error(translate("messages.game.game_update_error"), e);
} }
} }
}, new Date(), 1000L); }, new Date(), 1000L);
...@@ -222,8 +231,8 @@ public final class GameServer extends KcpServer { ...@@ -222,8 +231,8 @@ public final class GameServer extends KcpServer {
@Override @Override
public void onStartFinish() { public void onStartFinish() {
Grasscutter.getLogger().info(Grasscutter.getLanguage().Grasscutter_is_free); Grasscutter.getLogger().info(translate("messages.status.free_software"));
Grasscutter.getLogger().info(Grasscutter.getLanguage().Game_start_port.replace("{port}", Integer.toString(address.getPort()))); Grasscutter.getLogger().info(translate("messages.game.port_bind", Integer.toString(address.getPort())));
ServerStartEvent event = new ServerStartEvent(ServerEvent.Type.GAME, OffsetDateTime.now()); event.call(); ServerStartEvent event = new ServerStartEvent(ServerEvent.Type.GAME, OffsetDateTime.now()); event.call();
} }
......
...@@ -14,6 +14,7 @@ import emu.grasscutter.server.game.GameSession.SessionState; ...@@ -14,6 +14,7 @@ import emu.grasscutter.server.game.GameSession.SessionState;
import it.unimi.dsi.fastutil.ints.Int2ObjectMap; import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
@SuppressWarnings("unchecked")
public class GameServerPacketHandler { public class GameServerPacketHandler {
private final Int2ObjectMap<PacketHandler> handlers; private final Int2ObjectMap<PacketHandler> handlers;
......
...@@ -22,6 +22,8 @@ import io.netty.buffer.ByteBuf; ...@@ -22,6 +22,8 @@ import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled; import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelHandlerContext;
import static emu.grasscutter.utils.Language.translate;
public class GameSession extends KcpChannel { public class GameSession extends KcpChannel {
private GameServer server; private GameServer server;
...@@ -113,21 +115,21 @@ public class GameSession extends KcpChannel { ...@@ -113,21 +115,21 @@ public class GameSession extends KcpChannel {
@Override @Override
protected void onConnect() { protected void onConnect() {
Grasscutter.getLogger().info(Grasscutter.getLanguage().Client_connect.replace("{address}", getAddress().getHostString().toLowerCase())); Grasscutter.getLogger().info(translate("messages.game.connect", this.getAddress().getHostString().toLowerCase()));
} }
@Override @Override
protected synchronized void onDisconnect() { // Synchronize so we dont add character at the same time protected synchronized void onDisconnect() { // Synchronize so we don't add character at the same time.
Grasscutter.getLogger().info(Grasscutter.getLanguage().Client_disconnect.replace("{address}", getAddress().getHostString().toLowerCase())); Grasscutter.getLogger().info(translate("messages.game.disconnect", this.getAddress().getHostString().toLowerCase()));
// Set state so no more packets can be handled // Set state so no more packets can be handled
this.setState(SessionState.INACTIVE); this.setState(SessionState.INACTIVE);
// Save after disconnecting // Save after disconnecting
if (this.isLoggedIn()) { if (this.isLoggedIn()) {
// Save // Call logout event.
getPlayer().onLogout(); getPlayer().onLogout();
// Remove from gameserver // Remove from server.
getServer().getPlayers().remove(getPlayer().getUid()); getServer().getPlayers().remove(getPlayer().getUid());
} }
} }
......
...@@ -180,6 +180,7 @@ public final class Tools { ...@@ -180,6 +180,7 @@ public final class Tools {
writer.println(",\"200\": \"Standard\", \"301\": \"Avatar Event\", \"302\": \"Weapon event\""); writer.println(",\"200\": \"Standard\", \"301\": \"Avatar Event\", \"302\": \"Weapon event\"");
writer.println("}\n}"); writer.println("}\n}");
} }
Grasscutter.getLogger().info("Mappings generated!"); Grasscutter.getLogger().info("Mappings generated!");
} }
} }
package emu.grasscutter.utils;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import emu.grasscutter.Grasscutter;
import javax.annotation.Nullable;
import java.io.InputStream;
import java.util.HashMap;
import java.util.Map;
public final class Language {
private final JsonObject languageData;
private final Map<String, String> cachedTranslations = new HashMap<>();
/**
* Creates a language instance from a code.
* @param langCode The language code.
* @return A language instance.
*/
public static Language getLanguage(String langCode) {
return new Language(langCode + ".json");
}
/**
* Returns the translated value from the key while substituting arguments.
* @param key The key of the translated value to return.
* @param args The arguments to substitute.
* @return A translated value with arguments substituted.
*/
public static String translate(String key, Object... args) {
return Grasscutter.getLanguage().get(key).formatted(args);
}
/**
* Reads a file and creates a language instance.
* @param fileName The name of the language file.
*/
private Language(String fileName) {
@Nullable JsonObject languageData = null;
try {
InputStream file = Grasscutter.class.getResourceAsStream("/languages/" + fileName);
languageData = Grasscutter.getGsonFactory().fromJson(Utils.readFromInputStream(file), JsonObject.class);
} catch (Exception exception) {
Grasscutter.getLogger().error("Failed to load language file: " + fileName, exception);
} this.languageData = languageData;
}
/**
* Returns the value (as a string) from a nested key.
* @param key The key to look for.
* @return The value (as a string) from a nested key.
*/
public String get(String key) {
if(this.cachedTranslations.containsKey(key)) {
return this.cachedTranslations.get(key);
}
String[] keys = key.split("\\.");
JsonObject object = this.languageData;
int index = 0;
String result = "This value does not exist. Please report this to the Discord: " + key;
while (true) {
if(index == keys.length) break;
String currentKey = keys[index++];
if(object.has(currentKey)) {
JsonElement element = object.get(currentKey);
if(element.isJsonObject())
object = element.getAsJsonObject();
else {
result = element.getAsString(); break;
}
} else break;
}
this.cachedTranslations.put(key, result); return result;
}
}
package emu.grasscutter.utils; package emu.grasscutter.utils;
import emu.grasscutter.Config;
import emu.grasscutter.Grasscutter;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufUtil;
import io.netty.buffer.Unpooled;
import org.slf4j.Logger;
import java.io.*; import java.io.*;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.StandardCopyOption; import java.nio.file.StandardCopyOption;
import java.time.DayOfWeek; import java.time.*;
import java.time.ZoneId;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
import java.time.temporal.TemporalAdjusters; import java.time.temporal.TemporalAdjusters;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
import java.util.Random; import java.util.Random;
import emu.grasscutter.Config;
import emu.grasscutter.Grasscutter;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufUtil;
import io.netty.buffer.Unpooled;
import org.slf4j.Logger;
import static emu.grasscutter.utils.Language.translate;
@SuppressWarnings({"UnusedReturnValue", "BooleanMethodIsAlwaysInverted"}) @SuppressWarnings({"UnusedReturnValue", "BooleanMethodIsAlwaysInverted"})
public final class Utils { public final class Utils {
public static final Random random = new Random(); public static final Random random = new Random();
...@@ -180,15 +180,15 @@ public final class Utils { ...@@ -180,15 +180,15 @@ public final class Utils {
// Check for resources folder. // Check for resources folder.
if(!fileExists(resourcesFolder)) { if(!fileExists(resourcesFolder)) {
logger.info(Grasscutter.getLanguage().Create_resources_folder); logger.info(translate("messages.status.create_resources"));
logger.info(Grasscutter.getLanguage().Place_copy); logger.info(translate("messages.status.resources_error"));
createFolder(resourcesFolder); exit = true; createFolder(resourcesFolder); exit = true;
} }
// Check for BinOutput + ExcelBinOuput. // Check for BinOutput + ExcelBinOutput.
if(!fileExists(resourcesFolder + "BinOutput") || if(!fileExists(resourcesFolder + "BinOutput") ||
!fileExists(resourcesFolder + "ExcelBinOutput")) { !fileExists(resourcesFolder + "ExcelBinOutput")) {
logger.info(Grasscutter.getLanguage().Place_copy); logger.info(translate("messages.status.resources_error"));
exit = true; exit = true;
} }
...@@ -199,7 +199,11 @@ public final class Utils { ...@@ -199,7 +199,11 @@ public final class Utils {
if(exit) System.exit(1); if(exit) System.exit(1);
} }
public static int GetNextTimestampOfThisHour(int hour, String timeZone, int param) { /**
* Gets the timestamp of the next hour.
* @return The timestamp in UNIX seconds.
*/
public static int getNextTimestampOfThisHour(int hour, String timeZone, int param) {
ZonedDateTime zonedDateTime = ZonedDateTime.now(ZoneId.of(timeZone)); ZonedDateTime zonedDateTime = ZonedDateTime.now(ZoneId.of(timeZone));
for (int i = 0; i < param; i ++){ for (int i = 0; i < param; i ++){
if (zonedDateTime.getHour() < hour) { if (zonedDateTime.getHour() < hour) {
...@@ -208,10 +212,14 @@ public final class Utils { ...@@ -208,10 +212,14 @@ public final class Utils {
zonedDateTime = zonedDateTime.plusDays(1).withHour(hour).withMinute(0).withSecond(0); zonedDateTime = zonedDateTime.plusDays(1).withHour(hour).withMinute(0).withSecond(0);
} }
} }
return (int)zonedDateTime.toInstant().atZone(ZoneOffset.UTC).toEpochSecond(); return (int) zonedDateTime.toInstant().atZone(ZoneOffset.UTC).toEpochSecond();
} }
public static int GetNextTimestampOfThisHourInNextWeek(int hour, String timeZone, int param) { /**
* Gets the timestamp of the next hour in a week.
* @return The timestamp in UNIX seconds.
*/
public static int getNextTimestampOfThisHourInNextWeek(int hour, String timeZone, int param) {
ZonedDateTime zonedDateTime = ZonedDateTime.now(ZoneId.of(timeZone)); ZonedDateTime zonedDateTime = ZonedDateTime.now(ZoneId.of(timeZone));
for (int i = 0; i < param; i++) { for (int i = 0; i < param; i++) {
if (zonedDateTime.getDayOfWeek() == DayOfWeek.MONDAY && zonedDateTime.getHour() < hour) { if (zonedDateTime.getDayOfWeek() == DayOfWeek.MONDAY && zonedDateTime.getHour() < hour) {
...@@ -220,10 +228,14 @@ public final class Utils { ...@@ -220,10 +228,14 @@ public final class Utils {
zonedDateTime = zonedDateTime.with(TemporalAdjusters.next(DayOfWeek.MONDAY)).withHour(hour).withMinute(0).withSecond(0); zonedDateTime = zonedDateTime.with(TemporalAdjusters.next(DayOfWeek.MONDAY)).withHour(hour).withMinute(0).withSecond(0);
} }
} }
return (int)zonedDateTime.toInstant().atZone(ZoneOffset.UTC).toEpochSecond(); return (int) zonedDateTime.toInstant().atZone(ZoneOffset.UTC).toEpochSecond();
} }
public static int GetNextTimestampOfThisHourInNextMonth(int hour, String timeZone, int param) { /**
* Gets the timestamp of the next hour in a month.
* @return The timestamp in UNIX seconds.
*/
public static int getNextTimestampOfThisHourInNextMonth(int hour, String timeZone, int param) {
ZonedDateTime zonedDateTime = ZonedDateTime.now(ZoneId.of(timeZone)); ZonedDateTime zonedDateTime = ZonedDateTime.now(ZoneId.of(timeZone));
for (int i = 0; i < param; i++) { for (int i = 0; i < param; i++) {
if (zonedDateTime.getDayOfMonth() == 1 && zonedDateTime.getHour() < hour) { if (zonedDateTime.getDayOfMonth() == 1 && zonedDateTime.getHour() < hour) {
...@@ -232,9 +244,28 @@ public final class Utils { ...@@ -232,9 +244,28 @@ public final class Utils {
zonedDateTime = zonedDateTime.with(TemporalAdjusters.firstDayOfNextMonth()).withHour(hour).withMinute(0).withSecond(0); zonedDateTime = zonedDateTime.with(TemporalAdjusters.firstDayOfNextMonth()).withHour(hour).withMinute(0).withSecond(0);
} }
} }
return (int)zonedDateTime.toInstant().atZone(ZoneOffset.UTC).toEpochSecond(); return (int) zonedDateTime.toInstant().atZone(ZoneOffset.UTC).toEpochSecond();
} }
/**
* Retrieves a string from an input stream.
* @param stream The input stream.
* @return The string.
*/
public static String readFromInputStream(InputStream stream) {
StringBuilder stringBuilder = new StringBuilder();
try (BufferedReader reader = new BufferedReader(new InputStreamReader(stream))) {
String line; while ((line = reader.readLine()) != null) {
stringBuilder.append(line);
} stream.close();
} catch (IOException e) {
Grasscutter.getLogger().warn("Failed to read from input stream.");
} return stringBuilder.toString();
}
/**
* Switch properties from upper case to lower case?
*/
public static Map<String, Object> switchPropertiesUpperLowerCase(Map<String, Object> objMap, Class<?> cls) { public static Map<String, Object> switchPropertiesUpperLowerCase(Map<String, Object> objMap, Class<?> cls) {
Map<String, Object> map = new HashMap<>(objMap.size()); Map<String, Object> map = new HashMap<>(objMap.size());
for (String key : objMap.keySet()) { for (String key : objMap.keySet()) {
...@@ -268,5 +299,4 @@ public final class Utils { ...@@ -268,5 +299,4 @@ public final class Utils {
return map; return map;
} }
} }
{
"messages": {
"game": {
"port_bind": "Game Server started on port %s",
"connect": "Client connected from %s",
"disconnect": "Client disconnected from %s",
"game_update_error": "An error occurred during game update.",
"command_error": "Command error:"
},
"dispatch": {
"port_bind": "[Dispatch] Dispatch server started on port %s",
"request": "[Dispatch] Client %s %s request: %s",
"keystore": {
"general_error": "[Dispatch] Error while loading keystore!",
"password_error": "[Dispatch] Unable to load keystore. Trying default keystore password...",
"no_keystore_error": "[Dispatch] No SSL cert found! Falling back to HTTP server.",
"default_password": "[Dispatch] The default keystore password was loaded successfully. Please consider setting the password to 123456 in config.json."
},
"no_commands_error": "Commands are not supported in dispatch only mode.",
"unhandled_request_error": "[Dispatch] Potential unhandled %s request: %s",
"account": {
"login_attempt": "[Dispatch] Client %s is trying to log in",
"login_success": "[Dispatch] Client %s logged in as %s",
"login_token_attempt": "[Dispatch] Client %s is trying to log in via token",
"login_token_error": "[Dispatch] Client %s failed to log in via token",
"login_token_success": "[Dispatch] Client %s logged in via token as %s",
"combo_token_success": "[Dispatch] Client %s succeed to exchange combo token",
"combo_token_error": "[Dispatch] Client %s failed to exchange combo token",
"account_login_create_success": "[Dispatch] Client %s failed to log in: Account %s created",
"account_login_create_error": "[Dispatch] Client %s failed to log in: Account create failed",
"account_login_exist_error": "[Dispatch] Client %s failed to log in: Account no found",
"account_cache_error": "Game account cache information error",
"session_key_error": "Wrong session key.",
"username_error": "Username not found.",
"username_create_error": "Username not found, create failed."
}
},
"status": {
"free_software": "Grasscutter is FREE software. If you have paid for this, you may have been scammed. Homepage: https://github.com/Grasscutters/Grasscutter",
"starting": "Starting Grasscutter...",
"shutdown": "Shutting down...",
"done": "Done! For help, type \"help\"",
"error": "An error occurred.",
"welcome": "Welcome to Grasscutter",
"run_mode_error": "Invalid server run mode: %s.",
"run_mode_help": "Server run mode must be 'HYBRID', 'DISPATCH_ONLY', or 'GAME_ONLY'. Unable to start Grasscutter...",
"create_resources": "Creating resources folder...",
"resources_error": "Place a copy of 'BinOutput' and 'ExcelBinOutput' in the resources folder."
}
},
"commands": {
"generic": {
"not_specified": "No command specified.",
"unknown_command": "Unknown command: %s",
"permission_error": "You do not have permission to run this command.",
"console_execute_error": "This command can only be run from the console.",
"player_execute_error": "Run this command in-game.",
"command_exist_error": "No command found.",
"invalid": {
"amount": "Invalid amount.",
"artifactId": "Invalid artifactId.",
"avatarId": "Invalid avatarId.",
"avatarLevel": "Invalid avatarLevel.",
"entityId": "Invalid entityId.",
"itemId": "Invalid itemId.",
"itemLevel": "Invalid itemLevel.",
"itemRefinement": "Invalid itemRefinement.",
"playerId": "Invalid playerId.",
"uid": "Invalid UID."
}
},
"execution": {
"uid_error": "Invalid UID.",
"player_exist_error": "Player not found.",
"player_offline_error": "Player is not online.",
"item_id_error": "Invalid item ID.",
"item_player_exist_error": "Invalid item or UID.",
"entity_id_error": "Invalid entity ID.",
"player_exist_offline_error": "Player not found or is not online.",
"argument_error": "Invalid arguments.",
"clear_target": "Target cleared.",
"set_target": "Subsequent commands will target @%s by default.",
"need_target": "This command requires a target UID. Add a <@UID> argument or set a persistent target with /target @UID."
},
"status": {
"enabled": "Enabled",
"disabled": "Disabled",
"help": "Help",
"success": "Success"
},
"account": {
"modify": "Modify user accounts",
"invalid": "Invalid UID.",
"exists": "Account already exists.",
"create": "Account created with UID %s.",
"delete": "Account deleted.",
"no_account": "Account not found.",
"command_usage": "Usage: account <create|delete> <username> [uid]"
},
"broadcast": {
"command_usage": "Usage: broadcast <message>",
"message_sent": "Message sent."
},
"changescene": {
"usage": "Usage: changescene <sceneId>",
"already_in_scene": "You are already in that scene.",
"success": "Changed to scene %s.",
"exists_error": "The specified scene does not exist."
},
"clear": {
"command_usage": "Usage: clear <all|wp|art|mat>",
"weapons": "Cleared weapons for %s.",
"artifacts": "Cleared artifacts for %s.",
"materials": "Cleared materials for %s.",
"furniture": "Cleared furniture for %s.",
"displays": "Cleared displays for %s.",
"virtuals": "Cleared virtuals for %s.",
"everything": "Cleared everything for %s."
},
"coop": {
"usage": "Usage: coop <playerId> <target playerId>",
"success": "Summoned %s to %s's world."
},
"enter_dungeon": {
"usage": "Usage: enterdungeon <dungeon id>",
"changed": "Changed to dungeon %s",
"not_found_error": "Dungeon does not exist",
"in_dungeon_error": "You are already in that dungeon"
},
"giveAll": {
"usage": "Usage: giveall [player] [amount]",
"started": "Receiving all items...",
"success": "Successfully gave all items to %s.",
"invalid_amount_or_playerId": "Invalid amount or player ID."
},
"giveArtifact": {
"usage": "Usage: giveart|gart [player] <artifactId> <mainPropId> [<appendPropId>[,<times>]]... [level]",
"id_error": "Invalid artifact ID.",
"success": "Given %s to %s."
},
"giveChar": {
"usage": "Usage: givechar <player> <itemId|itemName> [amount]",
"given": "Given %s with level %s to %s.",
"invalid_avatar_id": "Invalid avatar id.",
"invalid_avatar_level": "Invalid avatar level.",
"invalid_avatar_or_player_id": "Invalid avatar or player ID."
},
"give": {
"usage": "Usage: give <player> <itemId|itemName> [amount] [level]",
"refinement_only_applicable_weapons": "Refinement is only applicable to weapons.",
"refinement_must_between_1_and_5": "Refinement must be between 1 and 5.",
"given": "Given %s of %s to %s.",
"given_with_level_and_refinement": "Given %s with level %s, refinement %s %s times to %s",
"given_level": "Given %s with level %s %s times to %s"
},
"godmode": {
"success": "Godmode is now %s for %s."
},
"heal": {
"success": "All characters have been healed."
},
"kick": {
"player_kick_player": "Player [%s:%s] has kicked player [%s:%s]",
"server_kick_player": "Kicking player [%s:%s]"
},
"kill": {
"usage": "Usage: killall [playerUid] [sceneId]",
"scene_not_found_in_player_world": "Scene not found in player world",
"kill_monsters_in_scene": "Killing %s monsters in scene %s"
},
"killCharacter": {
"usage": "Usage: /killcharacter [playerId]",
"success": "Killed %s's current character."
},
"list": {
"success": "There are %s player(s) online:"
},
"permission": {
"usage": "Usage: permission <add|remove> <username> <permission>",
"add": "Permission added.",
"has_error": "They already have this permission!",
"remove": "Permission removed.",
"not_have_error": "They don't have this permission!",
"account_error": "The account cannot be found."
},
"position": {
"success": "Coordinates: %.3f, %.3f, %.3f\nScene id: %d"
},
"reload": {
"reload_start": "Reloading config.",
"reload_done": "Reload complete."
},
"resetConst": {
"reset_all": "Reset all avatars' constellations.",
"success": "Constellations for %s have been reset. Please relog to see changes."
},
"resetShopLimit": {
"usage": "Usage: /resetshop <player id>"
},
"sendMail": {
"usage": "Usage: give [player] <itemId|itemName> [amount]",
"user_not_exist": "The user with an id of '%s' does not exist",
"start_composition": "Starting composition of message.\nPlease use `/sendmail <title>` to continue.\nYou can use `/sendmail stop` at any time",
"templates": "Mail templates coming soon implemented...",
"invalid_arguments": "Invalid arguments.\nUsage `/sendmail <userId|all|help> [templateId]`",
"send_cancel": "Message sending cancelled",
"send_done": "Message sent to user %s!",
"send_all_done": "Message sent to all users!",
"not_composition_end": "Message composition not at final stage.\nPlease use `/sendmail %s` or `/sendmail stop` to cancel",
"please_use": "Please use `/sendmail %s`",
"set_title": "Message title set as '%s'.\nUse '/sendmail <content>' to continue.",
"set_contents": "Message contents set as '%s'.\nUse '/sendmail <sender>' to continue.",
"set_message_sender": "Message sender set as '%s'.\nUse '/sendmail <itemId|itemName|finish> [amount] [level]' to continue.",
"send": "Attached %s of %s (level %s) to the message.\nContinue adding more items or use `/sendmail finish` to send the message.",
"invalid_arguments_please_use": "Invalid arguments \n Please use `/sendmail %s`",
"title": "<title>",
"message": "<message>",
"sender": "<sender>",
"arguments": "<itemId|itemName|finish> [amount] [level]",
"error": "ERROR: invalid construction stage %s. Check console for stacktrace."
},
"sendMessage": {
"usage": "Usage: sendmessage <player> <message>",
"success": "Message sent."
},
"setFetterLevel": {
"usage": "Usage: setfetterlevel <level>",
"range_error": "Fetter level must be between 0 and 10.",
"success": "Fetter level set to %s",
"level_error": "Invalid fetter level."
},
"setStats": {
"usage_console": "Usage: setstats|stats @<UID> <stat> <value>",
"usage_ingame": "Usage: setstats|stats [@UID] <stat> <value>",
"help_message": "\n\tValues for <stat>: hp | maxhp | def | atk | em | er | crate | cdmg | cdr | heal | heali | shield | defi\n\t(cont.) Elemental DMG Bonus: epyro | ecryo | ehydro | egeo | edendro | eelectro | ephys\n\t(cont.) Elemental RES: respyro | rescryo | reshydro | resgeo | resdendro | reselectro | resphys\n",
"value_error": "Invalid stat value.",
"uid_error": "Invalid UID.",
"player_error": "Player not found or offline.",
"set_self": "%s set to %s.",
"set_for_uid": "%s for %s set to %s.",
"set_max_hp": "MAX HP set to %s."
},
"setWorldLevel": {
"usage": "Usage: setworldlevel <level>",
"value_error": "World level must be between 0-8",
"success": "World level set to %s.",
"invalid_world_level": "Invalid world level."
},
"spawn": {
"usage": "Usage: spawn <entityId> [amount] [level(monster only)]",
"success": "Spawned %s of %s."
},
"stop": {
"success": "Server shutting down..."
},
"talent": {
"usage_1": "To set talent level: /talent set <talentID> <value>",
"usage_2": "Another way to set talent level: /talent <n or e or q> <value>",
"usage_3": "To get talent ID: /talent getid",
"lower_16": "Invalid talent level. Level should be lower than 16",
"set_id": "Set talent to %s.",
"set_atk": "Set talent Normal ATK to %s.",
"set_e": "Set talent E to %s.",
"set_q": "Set talent Q to %s.",
"invalid_skill_id": "Invalid skill ID.",
"set_this": "Set this talent to %s.",
"invalid_level": "Invalid talent level.",
"normal_attack_id": "Normal Attack ID %s.",
"e_skill_id": "E skill ID %s.",
"q_skill_id": "Q skill ID %s."
},
"teleportAll": {
"success": "Summoned all players to your location.",
"error": "You only can use this command in MP mode."
},
"teleport": {
"usage_server": "Usage: /tp @<player id> <x> <y> <z> [scene id]",
"usage": "Usage: /tp [@<player id>] <x> <y> <z> [scene id]",
"specify_player_id": "You must specify a player id.",
"invalid_position": "Invalid position.",
"success": "Teleported %s to %s, %s, %s in scene %s"
},
"weather": {
"usage": "Usage: weather <weatherId> [climateId]",
"success": "Changed weather to %s with climate %s",
"invalid_id": "Invalid ID."
},
"drop": {
"command_usage": "Usage: drop <itemId|itemName> [amount]",
"success": "Dropped %s of %s."
},
"help": {
"usage": "Usage: ",
"aliases": "Aliases: ",
"available_commands": "Available commands: "
}
}
}
\ No newline at end of file
{
"messages": {
"game": {
"port_bind": "遊戲伺服器已成功啟動。端口號:%s",
"connect": "客戶端已連接至 %s",
"disconnect": "客戶端 %s 已斷開連接。",
"game_update_error": "遊戲更新時發生了錯誤。",
"command_error": "指令發生錯誤:"
},
"dispatch": {
"port_bind": "[Dispatch] 伺服器已在端口 %s 上開啟。",
"request": "[Dispatch] 客戶端 %s 請求: %s %s",
"keystore": {
"general_error": "[Dispatch] 加載keystore文件時發生錯誤!",
"password_error": "[Dispatch] 加載 keystore 失敗。正在嘗試使用預設 keystore 密碼...",
"no_keystore_error": "[Dispatch] 未找到 SSL 憑證!已後降到 HTTP 伺服器。",
"default_password": "[Dispatch] 默認的 keystore 密碼加載成功。請考慮將 config.json 的憑證密碼設定成 123456。"
},
"no_commands_error": "此指令不適用於Dispatch-only模式。",
"unhandled_request_error": "[Dispatch] 潛在的未處理請求 %s 請求:%s",
"account": {
"login_attempt": "[Dispatch] 客戶端 %s 正在嘗試登入",
"login_success": "[Dispatch] 客戶端 %s 已登入,UID為 %s",
"login_token_attempt": "[Dispatch] 客戶端 %s 正在嘗試用憑證登入",
"login_token_error": "[Dispatch] 客戶端 %s 使用憑證登入失敗",
"login_token_success": "[Dispatch] 客戶端 %s 已透過憑證登入,UID為 %s",
"combo_token_success": "[Dispatch] 客戶端 %s 交換憑證成功",
"combo_token_error": "[Dispatch] 客戶端 %s 交換憑證失敗",
"account_login_create_success": "[Dispatch] 客戶端 %s 登入失敗: 已註冊UID為 %s 的帳號",
"account_login_create_error": "[Dispatch] 客戶端 %s 登入失敗:帳號建立失敗。",
"account_login_exist_error": "[Dispatch] 客戶端 %s 登入失敗: 帳號不存在",
"account_cache_error": "遊戲帳號緩存資訊錯誤",
"session_key_error": "對話密鑰不符。",
"username_error": "未找到此用戶名。",
"username_create_error": "未找到用戶名,建立失敗。"
}
},
"status": {
"free_software": "Grasscutter 是免費開源軟體。如果你已經付錢了,那你可能被騙了。主頁:https://github.com/Grasscutters/Grasscutter",
"starting": "正在啟動 Grasscutter...",
"shutdown": "正在關閉...",
"done": "加載完成!需要指令幫助請輸入 \"help\"",
"error": "發生了一個錯誤。",
"welcome": "歡迎使用 Grasscutter",
"run_mode_error": "無效的伺服器運行模式: %s。",
"run_mode_help": "伺服器運行模式必須為 HYBRID 或者 DISPATCH_ONLY 或者 GAME_ONLY。Grasscutter 啟動失敗...",
"create_resources": "正在建立 resources 資料夾...",
"resources_error": "請將 BinOutput 和 ExcelBinOutput 複製到 resources 資料夾。"
}
},
"commands": {
"generic": {
"not_specified": "沒有指定指令。",
"unknown_command": "未知的指令:%s",
"permission_error": "您沒有執行此指令的權限。",
"console_execute_error": "此指令只能在伺服器的命令提示字元執行。",
"player_execute_error": "請在遊戲裡使用這條指令。",
"command_exist_error": "找不到指令。",
"invalid": {
"amount": "無效的 數量.",
"artifactId": "無效的聖遺物ID。",
"avatarId": "無效的角色ID。",
"avatarLevel": "無效的角色等級。",
"entityId": "無效的實體ID。",
"itemId": "無效的物品ID。",
"itemLevel": "無效的物品等級。",
"itemRefinement": "無效的物品精煉度。",
"playerId": "無效的玩家ID。",
"uid": "無效的UID。"
}
},
"execution": {
"uid_error": "無效的UID。",
"player_exist_error": "用戶不存在。",
"player_offline_error": "玩家已離線。",
"item_id_error": "無效的物品ID。.",
"item_player_exist_error": "無效的物品/玩家UID。",
"entity_id_error": "無效的實體ID。",
"player_exist_offline_error": "玩家不存在或已離線。",
"argument_error": "無效的參數。",
"clear_target": "目標已清除.",
"set_target": "隨後的指令都會以@%s為預設。",
"need_target": "此指令需要一個目標 UID。添加 <@UID> 引數或者使用 /target @UID 來設定持久目標。"
},
"status": {
"enabled": "已啟用",
"disabled": "未啟用",
"help": "幫助",
"success": "成功"
},
"account": {
"modify": "修改使用者帳號",
"invalid": "無效的UID。",
"exists": "帳號已存在。",
"create": "已建立帳號,UID 為 %s 。",
"delete": "帳號已刪除。",
"no_account": "帳號不存在。",
"command_usage": "用法:account <create|delete> <username> [uid]"
},
"broadcast": {
"command_usage": "用法:broadcast <message>",
"message_sent": "公告已發送。"
},
"changescene": {
"usage": "用法:changescene <scene id>",
"already_in_scene": "你已經在這個場景中了。",
"success": "已切換至場景 %s.",
"exists_error": "此場景不存在。"
},
"clear": {
"command_usage": "用法: clear <all|wp|art|mat>",
"weapons": "已將 %s 的武器清空。",
"artifacts": "已將 %s 的聖遺物清空。",
"materials": "已將 %s 的材料清空。",
"furniture": "已將 %s 的塵歌壺家具清空。",
"displays": "已清除 %s 的顯示。",
"virtuals": "已將 %s 的所有貨幣和經驗值清空。",
"everything": "已將 %s 的所有物品清空。"
},
"coop": {
"usage": "用法:coop <playerId> <target playerId>",
"success": "Summoned %s to %s's world."
},
"enter_dungeon": {
"usage": "用法:enterdungeon <dungeon id>",
"changed": "已進入副本 %s",
"not_found_error": "此副本不存在。",
"in_dungeon_error": "你已經在祕境中了。"
},
"giveAll": {
"usage": "用法:giveall [player] [amount]",
"started": "正在賦予全部物品...",
"success": "已賦予全部物品。",
"invalid_amount_or_playerId": "無效的數量/玩家ID。"
},
"giveArtifact": {
"usage": "用法:giveart|gart [player] <artifactId> <mainPropId> [<appendPropId>[,<times>]]... [level]",
"id_error": "無效的聖遺物ID。",
"success": "已把 %s 給予 %s。"
},
"giveChar": {
"usage": "用法:givechar <player> <itemId|itemName> [amount]",
"given": "Given %s with level %s to %s.",
"invalid_avatar_id": "無效的角色ID。",
"invalid_avatar_level": "無效的角色等級。.",
"invalid_avatar_or_player_id": "無效的角色ID/玩家ID。"
},
"give": {
"usage": "用法:give <player> <itemId|itemName> [amount] [level]",
"refinement_only_applicable_weapons": "精煉度只能施加在武器上面。",
"refinement_must_between_1_and_5": "精煉度必需在 1 到 5 之間。",
"given": "已經將 %s 個 %s 給予 %s。",
"given_with_level_and_refinement": "已將 %s [等級%s, 精煉%s] %s個給予 %s",
"given_level": "已將 %s 等級 %s %s 個給予 %s"
},
"godmode": {
"success": "上帝模式設定為 %s 。 [用戶:%s]"
},
"heal": {
"success": "所有角色已被治療。"
},
"kick": {
"player_kick_player": "玩家 [%s:%s] 已把 [%s:%s] 踢出",
"server_kick_player": "正在踢出玩家 [%s:%s]"
},
"kill": {
"usage": "用法:killall [playerUid] [sceneId]",
"scene_not_found_in_player_world": "未在玩家世界中找到此場景",
"kill_monsters_in_scene": "已殺死 %s 個怪物。 [場景ID: %s]"
},
"killCharacter": {
"usage": "用法:/killcharacter [playerId]",
"success": "已殺死 %s 目前的場上角色。"
},
"list": {
"message": "目前總線上人數:%s"
},
"permission": {
"usage": "用法:permission <add|remove> <username> <permission>",
"add": "已指派權限。",
"has_error": "此玩家已擁有權限!",
"remove": "權限已移除。",
"not_have_error": "此玩家未擁有權限!",
"account_error": "The account cannot be found."
},
"position": {
"success": "坐標:%.3f, %.3f, %.3f\n場景ID:%d"
},
"reload": {
"reload_start": "正在重新加載設定檔。",
"reload_done": "重新加載已完成。"
},
"resetConst": {
"reset_all": "重設所有角色的命座。",
"success": "已重設 %s 的命座,重新登入後將會生效。"
},
"resetShopLimit": {
"usage": "用法:/resetshop <player id>"
},
"sendMail": {
"usage": "用法:give [player] <itemId|itemName> [amount]",
"user_not_exist": "ID '%s' 的使用者不存在。",
"start_composition": "發送郵件流程。\n請使用`/send <郵件標題>`來進到下一步。\n你可以在任何時間使用`/sendmail stop`來停止發送。",
"templates": "郵件模板尚未實裝...",
"invalid_arguments": "無效的參數。\n指令使用方法 `/sendmail <userId|all|help> [templateId]`",
"send_cancel": "取消傳送信息",
"send_done": "已將消息發送給 %s!",
"send_all_done": "消息已發送給全體用戶!",
"not_composition_end": "現在郵件發送未到最後階段。\n請使用 `/sendmail %s` 繼續發送郵件,或者 `/sendmail stop` 來停止發送郵件。",
"please_use": "請使用 `/sendmail %s`",
"set_title": "成功將郵件標題設定成 '%s'。\n接下來請繼續使用 '/sendmail <content>' 來設定郵件內容。",
"set_contents": "成功將'%s'為郵件內容。\n接下來請打出 '/sendmail <寄件者名稱>' 來設定郵件寄件者名稱。",
"set_message_sender": "郵件寄件者已設為 '%s'。\n使用 '/sendmail <itemId|itemName|finish> [amount] [level]' 以繼續操作。",
"send": "已添加 %s 個 %s (等級為 %s) 到郵件附件。\n如果沒有要繼續添加道具請使用 `/sendmail finish` 來完成郵件發送。",
"invalid_arguments_please_use": "Invalid arguments \n Please use `/sendmail %s`",
"title": "<標題>",
"message": "<正文>",
"sender": "<寄件者>",
"arguments": "<itemId|itemName|finish> [數量] [等級]",
"error": "錯誤:無效的編寫階段 %s。需要 stacktrace 請查看伺服器命令提示字元。"
},
"sendMessage": {
"usage": "用法:sendmessage <player> <message>",
"success": "訊息已發送。"
},
"setFetterLevel": {
"usage": "用法:setfetterlevel <level>",
"range_error": "好感度必須在 0 到 10 之間。",
"fetter_set_level": "好感等級已設定為 %s",
"level_error": "無效的好感度。"
},
"setStats": {
"usage_console": "用法:setstats|stats @<UID> <stat> <value>",
"usage_ingame": "用法:setstats|stats [@UID] <stat> <value>",
"help_message": "\n\t可使用的數據類型:hp (生命值)| maxhp (最大生命值) | def(防禦力) | atk (攻擊力)| em (元素精通) | er (元素充能效率) | crate(暴擊率) | cdmg (暴擊傷害)| cdr (冷卻縮減) | heal(治療加成)| heali (受治療加成)| shield (護盾強效)| defi (無視防禦)\n\t(cont.) 元素增傷類:epyro (火傷) | ecryo (冰傷) | ehydro (水傷) | egeo (岩傷) | edendro (草傷) | eelectro (雷傷) | ephys (物傷)(cont.) 元素減傷類:respyro (火抗) | rescryo (冰抗) | reshydro (水抗) | resgeo (岩抗) | resdendro (草抗) | reselectro (雷抗) | resphys (物抗)\n",
"value_error": "無效的數據值。",
"uid_error": "無效的UID。",
"player_error": "玩家不存在或已離線。",
"set_self": "%s 已經設為 %s。",
"set_for_uid": "%s 的使用者 %s 更改為 %s。",
"set_max_hp": "最大生命值更改為 %s。"
},
"setWorldLevel": {
"usage": "用法:setworldlevel <level>",
"value_error": "世界等級必須設定在0-8之間。",
"success": "已將世界等級設為%s。",
"invalid_world_level": "無效的世界等級。"
},
"spawn": {
"usage": "用法:spawn <entityId> [amount] [level(僅限怪物)]",
"success": "已生成 %s 個 %s。"
},
"stop": {
"success": "正在關閉伺服器..."
},
"talent": {
"usage_1": "設定天賦等級:/talent set <talentID> <value>",
"usage_2": "另一種設定天賦等級的指令使用方法:/talent <n or e or q> <value>",
"usage_3": "獲取天賦ID指令用法:/talent getid",
"lower_16": "無效的技能等級,技能等級應低於 16。",
"set_id": "將天賦等級設為%s。",
"set_atk": "將普通攻擊等級設為 %s。",
"set_e": "設定天賦E等級至 %s。",
"set_q": "設定天賦Q等級至 %s。",
"invalid_skill_id": "無效的技能ID。",
"set_this": "將天賦等級設為 %s。",
"invalid_level": "無效的天賦等級。",
"normal_attack_id": "普通攻擊的 ID 為 %s。",
"e_skill_id": "E技能ID %s。",
"q_skill_id": "Q技能ID %s。"
},
"teleportAll": {
"success": "Summoned all players to your location.",
"error": "此指令僅可在多人遊戲下可用。"
},
"teleport": {
"usage_server": "用法:/tp @<player id> <x> <y> <z> [scene id]",
"usage": "用法:/tp [@<player id>] <x> <y> <z> [scene id]",
"specify_player_id": "你必須指定一個玩家ID。",
"invalid_position": "無效的位置。",
"success": "傳送 %s 到坐標 %s,%s,%s ,場景為 %s"
},
"weather": {
"usage": "用法:weather <weatherId> [climateId]",
"success": "已將當前天氣設定為 %s ,氣候則為 %s 。",
"invalid_id": "無效的ID。"
},
"drop": {
"command_usage": "用法:drop <itemId|itemName> [amount]",
"success": "已將 %s x %s 丟在附近。"
},
"help": {
"usage": "用法:",
"aliases": "別名:",
"available_commands": "可用指令:"
}
}
}
\ No newline at end of file
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment