6 Commits

Author SHA1 Message Date
d59e14e383 Attempt to fix untranslated login window 2022-05-20 05:52:15 -07:00
c683effead Added banner start and end time checks 2022-05-20 05:51:44 -07:00
b9eef26d8b Changing how banners work 2022-05-20 05:51:44 -07:00
b60596f41f Update FileUtils.java
fix: Error when checking files & always checking "/default/data" instead of folder
2022-05-20 05:49:28 -07:00
e2cb56ee28 Server Log Events (#996)
* Server Log Event

* LogEventAppender Encoder
2022-05-20 05:48:20 -07:00
b2a07044e2 Set the maximum number of player on the server through the config.json (#1001)
* Show server status to three-party game launcher

* Set the maximum number of player on the server through the config.json

* modify the logical order and show the number of maxplayer to API /status/server

* Now even players who have token already cannot bypass the maxPlayer check
2022-05-20 05:47:47 -07:00
19 changed files with 539 additions and 88 deletions

View File

@ -23,14 +23,16 @@ public final class DefaultAuthenticators {
var requestData = request.getPasswordRequest();
assert requestData != null; // This should never be null.
int playerCount = Grasscutter.getGameServer().getPlayers().size();
boolean successfulLogin = false;
String address = request.getRequest().ip();
String responseMessage = translate("messages.dispatch.account.username_error");
String loggerMessage = "";
// Get account from database.
Account account = DatabaseHelper.getAccountByName(requestData.account);
if (ACCOUNT.maxPlayer <= -1 || playerCount < ACCOUNT.maxPlayer) {
// Check if account exists.
if(account == null && ACCOUNT.autoCreate) {
// This account has been created AUTOMATICALLY. There will be no permissions added.
@ -49,6 +51,14 @@ public final class DefaultAuthenticators {
}
} else if(account != null)
successfulLogin = true;
else
loggerMessage = translate("messages.dispatch.account.account_login_exist_error", address);
} else {
responseMessage = translate("messages.dispatch.account.server_max_player_limit");
loggerMessage = translate("messages.dispatch.account.login_max_player_limit", address);
}
// Set response data.
if(successfulLogin) {
@ -57,15 +67,13 @@ public final class DefaultAuthenticators {
response.data.account.token = account.generateSessionKey();
response.data.account.email = account.getEmail();
// Log the login.
Grasscutter.getLogger().info(translate("messages.dispatch.account.login_success", address, account.getId()));
loggerMessage = translate("messages.dispatch.account.login_success", address, account.getId());
} else {
response.retcode = -201;
response.message = responseMessage;
// Log the failure.
Grasscutter.getLogger().info(translate("messages.dispatch.account.account_login_exist_error", address));
}
Grasscutter.getLogger().info(loggerMessage);
return response;
}
@ -83,10 +91,14 @@ public final class DefaultAuthenticators {
boolean successfulLogin;
String address = request.getRequest().ip();
String loggerMessage;
int playerCount = Grasscutter.getGameServer().getPlayers().size();
// Log the attempt.
Grasscutter.getLogger().info(translate("messages.dispatch.account.login_token_attempt", address));
if (ACCOUNT.maxPlayer <= -1 || playerCount < ACCOUNT.maxPlayer) {
// Get account from database.
Account account = DatabaseHelper.getAccountById(requestData.uid);
@ -101,15 +113,23 @@ public final class DefaultAuthenticators {
response.data.account.email = account.getEmail();
// Log the login.
Grasscutter.getLogger().info(translate("messages.dispatch.account.login_token_success", address, requestData.uid));
loggerMessage = translate("messages.dispatch.account.login_token_success", address, requestData.uid);
} else {
response.retcode = -201;
response.message = translate("messages.dispatch.account.account_cache_error");
// Log the failure.
Grasscutter.getLogger().info(translate("messages.dispatch.account.login_token_error", address));
loggerMessage = translate("messages.dispatch.account.login_token_error", address);
}
} else {
response.retcode = -201;
response.message = translate("messages.dispatch.account.server_max_player_limit");
loggerMessage = translate("messages.dispatch.account.login_max_player_limit", address);
}
Grasscutter.getLogger().info(loggerMessage);
return response;
}
}
@ -127,7 +147,10 @@ public final class DefaultAuthenticators {
boolean successfulLogin;
String address = request.getRequest().ip();
String loggerMessage;
int playerCount = Grasscutter.getGameServer().getPlayers().size();
if (ACCOUNT.maxPlayer <= -1 || playerCount < ACCOUNT.maxPlayer) {
// Get account from database.
Account account = DatabaseHelper.getAccountById(loginData.uid);
@ -142,15 +165,23 @@ public final class DefaultAuthenticators {
response.data.combo_token = account.generateLoginToken();
// Log the login.
Grasscutter.getLogger().info(translate("messages.dispatch.account.combo_token_success", address));
loggerMessage = translate("messages.dispatch.account.combo_token_success", address);
} else {
response.retcode = -201;
response.message = translate("messages.dispatch.account.session_key_error");
// Log the failure.
Grasscutter.getLogger().info(translate("messages.dispatch.account.combo_token_error", address));
loggerMessage = translate("messages.dispatch.account.combo_token_error", address);
}
} else {
response.retcode = -201;
response.message = translate("messages.dispatch.account.server_max_player_limit");
loggerMessage = translate("messages.dispatch.account.login_max_player_limit", address);
}
Grasscutter.getLogger().info(loggerMessage);
return response;
}
}

View File

@ -143,7 +143,7 @@ public class GachaBanner {
String details = "http" + (HTTP_ENCRYPTION.useInRouting ? "s" : "") + "://"
+ lr(HTTP_INFO.accessAddress, HTTP_INFO.bindAddress) + ":"
+ lr(HTTP_INFO.accessPort, HTTP_INFO.bindPort)
+ "/gacha/details?s=" + sessionKey + "&gachaType=" + gachaType;
+ "/gacha/details?s=" + sessionKey + "&scheduleId=" + scheduleId;
// Grasscutter.getLogger().info("record = " + record);
ItemParamData costItem1 = this.getCost(1);

View File

@ -21,6 +21,7 @@ import emu.grasscutter.data.common.ItemParamData;
import emu.grasscutter.data.def.ItemData;
import emu.grasscutter.database.DatabaseHelper;
import emu.grasscutter.game.avatar.Avatar;
import emu.grasscutter.game.gacha.GachaBanner.BannerType;
import emu.grasscutter.game.inventory.GameItem;
import emu.grasscutter.game.inventory.Inventory;
import emu.grasscutter.game.inventory.ItemType;
@ -82,7 +83,7 @@ public class GachaManager {
List<GachaBanner> banners = Grasscutter.getGsonFactory().fromJson(fileReader, TypeToken.getParameterized(Collection.class, GachaBanner.class).getType());
if(banners.size() > 0) {
for (GachaBanner banner : banners) {
getGachaBanners().put(banner.getGachaType(), banner);
getGachaBanners().put(banner.getScheduleId(), banner);
}
Grasscutter.getLogger().info("Banners successfully loaded.");
@ -236,7 +237,7 @@ public class GachaManager {
};
}
public synchronized void doPulls(Player player, int gachaType, int times) {
public synchronized void doPulls(Player player, int scheduleId, int times) {
// Sanity check
if (times != 10 && times != 1) {
return;
@ -248,7 +249,7 @@ public class GachaManager {
}
// Get banner
GachaBanner banner = this.getGachaBanners().get(gachaType);
GachaBanner banner = this.getGachaBanners().get(scheduleId);
if (banner == null) {
player.sendPacket(new PacketDoGachaRsp());
return;
@ -285,7 +286,7 @@ public class GachaManager {
}
// Write gacha record
GachaRecord gachaRecord = new GachaRecord(itemId, player.getUid(), gachaType);
GachaRecord gachaRecord = new GachaRecord(itemId, player.getUid(), banner.getGachaType());
DatabaseHelper.saveGachaRecord(gachaRecord);
// Create gacha item
@ -411,9 +412,14 @@ public class GachaManager {
private synchronized GetGachaInfoRsp createProto(String sessionKey) {
GetGachaInfoRsp.Builder proto = GetGachaInfoRsp.newBuilder().setGachaRandom(12345);
long currentTime = System.currentTimeMillis() / 1000L;
for (GachaBanner banner : getGachaBanners().values()) {
if ((banner.getEndTime() >= currentTime && banner.getBeginTime() <= currentTime) || (banner.getBannerType() == BannerType.STANDARD))
{
proto.addGachaInfoList(banner.toProto(sessionKey));
}
}
return proto.build();
}

View File

@ -0,0 +1,22 @@
package emu.grasscutter.server.event.internal;
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.core.AppenderBase;
import emu.grasscutter.server.event.types.ServerEvent;
public class ServerLogEvent extends ServerEvent {
ILoggingEvent loggingEvent;
String consoleMessage;
public ServerLogEvent(Type type, ILoggingEvent loggingEvent, String consoleMessage) {
super(type);
this.loggingEvent = loggingEvent;
this.consoleMessage = consoleMessage;
}
public ILoggingEvent getLoggingEvent() { return loggingEvent; }
public String getConsoleMessage() {
return consoleMessage;
}
}

View File

@ -104,9 +104,9 @@ public final class GachaHandler implements Router {
.replace("{{LANGUAGE}}", Utils.getLanguageCode(account.getLocale()));
// Get the banner info for the banner we want.
int gachaType = Integer.parseInt(request.query("gachaType"));
int scheduleId = Integer.parseInt(request.query("scheduleId"));
GachaManager manager = Grasscutter.getGameServer().getGachaManager();
GachaBanner banner = manager.getGachaBanners().get(gachaType);
GachaBanner banner = manager.getGachaBanners().get(scheduleId);
// Add 5-star items.
Set<String> fiveStarItems = new LinkedHashSet<>();

View File

@ -4,11 +4,14 @@ import emu.grasscutter.GameConstants;
import emu.grasscutter.Grasscutter;
import emu.grasscutter.server.http.objects.HttpJsonResponse;
import emu.grasscutter.server.http.Router;
import emu.grasscutter.server.http.objects.WebStaticVersionResponse;
import express.Express;
import express.http.Request;
import express.http.Response;
import io.javalin.Javalin;
import static emu.grasscutter.Configuration.ACCOUNT;
/**
* Handles all generic, hard-coded responses.
*/
@ -41,15 +44,16 @@ 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/*", new HttpJsonResponse("{\"version\":51}"));
express.get("/admin/mi18n/plat_oversea/*", new WebStaticVersionResponse());
express.get("/status/server", GenericHandler::serverStatus);
}
private static void serverStatus(Request request, Response response) {
int playerCount = Grasscutter.getGameServer().getPlayers().size();
int maxPlayer = ACCOUNT.maxPlayer;
String version = GameConstants.VERSION;
response.send("{\"retcode\":0,\"status\":{\"playerCount\":" + playerCount + ",\"version\":\"" + version + "\"}}");
response.send("{\"retcode\":0,\"status\":{\"playerCount\":" + playerCount + ",\"maxPlayer\":" + maxPlayer + ",\"version\":\"" + version + "\"}}");
}
}

View File

@ -0,0 +1,41 @@
package emu.grasscutter.server.http.objects;
import emu.grasscutter.Grasscutter;
import emu.grasscutter.data.DataLoader;
import emu.grasscutter.utils.FileUtils;
import emu.grasscutter.utils.Utils;
import express.http.HttpContextHandler;
import express.http.MediaType;
import express.http.Request;
import express.http.Response;
import io.javalin.core.util.FileUtil;
import java.io.IOException;
import java.io.InputStream;
import static emu.grasscutter.Configuration.DATA;
public class WebStaticVersionResponse implements HttpContextHandler {
@Override
public void handle(Request request, Response response) throws IOException {
if(request.path().contains("version")) {
getPageResources("/webstatic/version.json", response);
return;
} else { // TODO other versions
getPageResources("/webstatic/en.json", response);
}
}
private static void getPageResources(String path, Response response) {
try(InputStream filestream = FileUtils.readResourceAsStream(path)) {
MediaType fromExtension = MediaType.getByExtension(path.substring(path.lastIndexOf(".") + 1));
response.type((fromExtension != null) ? fromExtension.getMIME() : "application/octet-stream");
response.send(filestream.readAllBytes());
} catch (Exception e) {
Grasscutter.getLogger().warn("File does not exist: " + path);
response.status(404);
}
}
}

View File

@ -12,6 +12,6 @@ public class HandlerDoGachaReq extends PacketHandler {
public void handle(GameSession session, byte[] header, byte[] payload) throws Exception {
DoGachaReq req = DoGachaReq.parseFrom(payload);
session.getServer().getGachaManager().doPulls(session.getPlayer(), req.getGachaType(), req.getGachaTimes());
session.getServer().getGachaManager().doPulls(session.getPlayer(), req.getGachaScheduleId(), req.getGachaTimes());
}
}

View File

@ -1,5 +1,6 @@
package emu.grasscutter.server.packet.recv;
import emu.grasscutter.Grasscutter;
import emu.grasscutter.database.DatabaseHelper;
import emu.grasscutter.game.player.Player;
import emu.grasscutter.net.packet.BasePacket;
@ -12,6 +13,8 @@ import emu.grasscutter.server.game.GameSession.SessionState;
import emu.grasscutter.server.packet.send.PacketPlayerLoginRsp;
import emu.grasscutter.server.packet.send.PacketTakeAchievementRewardReq;
import static emu.grasscutter.Configuration.ACCOUNT;
@Opcodes(PacketOpcodes.PlayerLoginReq) // Sends initial data packets
public class HandlerPlayerLoginReq extends PacketHandler {
@ -22,6 +25,11 @@ public class HandlerPlayerLoginReq extends PacketHandler {
return;
}
// Max players limit
if (ACCOUNT.maxPlayer > -1 && Grasscutter.getGameServer().getPlayers().size() >= ACCOUNT.maxPlayer) {
return;
}
// Parse request
PlayerLoginReq req = PlayerLoginReq.parseFrom(payload);

View File

@ -110,6 +110,7 @@ public class ConfigContainer {
public static class Account {
public boolean autoCreate = false;
public String[] defaultPermissions = {};
public int maxPlayer = -1;
}
/* Server options. */

View File

@ -97,7 +97,7 @@ public final class FileUtils {
}
} catch (Exception e) {
// Eclipse puts resources in its bin folder
File f = new File(jarPath + "defaults/data/");
File f = new File(System.getProperty("user.dir") + folder);
if (!f.exists() || f.listFiles().length == 0) {
return null;

View File

@ -0,0 +1,31 @@
package emu.grasscutter.utils;
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.core.AppenderBase;
import ch.qos.logback.core.encoder.Encoder;
import ch.qos.logback.core.spi.DeferredProcessingAware;
import ch.qos.logback.core.status.ErrorStatus;
import emu.grasscutter.server.event.internal.ServerLogEvent;
import emu.grasscutter.server.event.types.ServerEvent;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
public class ServerLogEventAppender<E> extends AppenderBase<E> {
protected Encoder<E> encoder;
@Override
protected void append(E event) {
byte[] byteArray = this.encoder.encode(event);
ServerLogEvent sle = new ServerLogEvent(ServerEvent.Type.GAME, (ILoggingEvent) event, new String(byteArray, StandardCharsets.UTF_8));
sle.call();
}
public Encoder<E> getEncoder() {
return encoder;
}
public void setEncoder(Encoder<E> encoder) {
this.encoder = encoder;
}
}

View File

@ -24,6 +24,7 @@
"account": {
"login_attempt": "[Dispatch] Client %s is trying to log in.",
"login_success": "[Dispatch] Client %s logged in as %s.",
"login_max_player_limit": "[Dispatch] Client %s failed to log in: The number of online players has reached the limit",
"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.",
@ -35,7 +36,8 @@
"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."
"username_create_error": "Username not found, create failed.",
"server_max_player_limit": "The number of online players has reached the limit"
},
"router_error": "[Dispatch] Unable to attach router."
},

View File

@ -21,6 +21,7 @@
"account": {
"login_attempt": "[Dispatch] Klient %s próbuje się zalogować",
"login_success": "[Dispatch] Klient %s zalogował się jako %s",
"login_max_player_limit": "[Dispatch] Klient %s nie powiodło się: Liczba graczy online osiągnęła limit",
"login_token_attempt": "[Dispatch] Klient %s próbuje się zalogować poprzez token",
"login_token_error": "[Dispatch] Klient %s nie mógł się zalogować poprzez token",
"login_token_success": "[Dispatch] Klient %s zalogował się poprzez token jako %s",
@ -32,7 +33,8 @@
"account_cache_error": "Błąd pamięci cache konta gry",
"session_key_error": "Błędny klucz sesji.",
"username_error": "Nazwa użytkownika nie znaleziona.",
"username_create_error": "Nazwa użytkownika nie znaleziona, tworzenie nie powiodło się."
"username_create_error": "Nazwa użytkownika nie znaleziona, tworzenie nie powiodło się.",
"server_max_player_limit": "Liczba graczy online osiągnęła limit"
}
},
"status": {

View File

@ -24,6 +24,7 @@
"account": {
"login_attempt": "[Dispatch] 客户端 %s 正在尝试登录",
"login_success": "[Dispatch] 客户端 %s 已登录UID 为 %s",
"login_max_player_limit": "[Dispatch] 客户端 %s 登录失败:在线人数已满",
"login_token_attempt": "[Dispatch] 客户端 %s 正在尝试使用 token 登录",
"login_token_error": "[Dispatch] 客户端 %s 使用 token 登录失败",
"login_token_success": "[Dispatch] 客户端 %s 已通过 token 登录UID 为 %s",
@ -35,7 +36,8 @@
"account_cache_error": "游戏账号缓存信息错误",
"session_key_error": "会话密钥错误",
"username_error": "未找到此用户名",
"username_create_error": "未找到用户名,建立连接失败"
"username_create_error": "未找到用户名,建立连接失败",
"server_max_player_limit": "服务器在线人数已满"
},
"router_error": "[Dispatch] 无法连接路由"
},

View File

@ -24,6 +24,7 @@
"account": {
"login_attempt": "[Dispatch] 客戶端 %s 正在嘗試登入",
"login_success": "[Dispatch] 客戶端 %s 已登入UID為 %s",
"login_max_player_limit": "[Dispatch] 客戶端 %s 登入失敗:在綫人數已滿",
"login_token_attempt": "[Dispatch] 客戶端 %s 正在嘗試用憑證登入",
"login_token_error": "[Dispatch] 客戶端 %s 使用憑證登入失敗",
"login_token_success": "[Dispatch] 客戶端 %s 已透過憑證登入UID為 %s",
@ -35,7 +36,8 @@
"account_cache_error": "遊戲帳號緩存資訊錯誤",
"session_key_error": "對話密鑰不符。",
"username_error": "未找到此用戶名。",
"username_create_error": "未找到用戶名,建立失敗。"
"username_create_error": "未找到用戶名,建立失敗。",
"server_max_player_limit": "服務器在綫人數已滿"
}
},
"status": {

View File

@ -16,6 +16,11 @@
<pattern>%d{yyyy-MM-dd'T'HH:mm:ss'Z'} &lt;%level:%class&gt; %m%n</pattern>
</encoder>
</appender>
<appender name="SERVEREVENT" class="emu.grasscutter.utils.ServerLogEventAppender">
<encoder>
<pattern>%d{HH:mm:ss} &lt;%highlight(%level):%gray(%class{0})&gt; %msg%n</pattern>
</encoder>
</appender>
<logger name="org.reflections" level="OFF" />
<logger name="emu.grasscutter" level="${LOG_LEVEL}" />
@ -23,5 +28,6 @@
<root level="INFO">
<appender-ref ref="STDOUT" />
<appender-ref ref="FILE" />
<appender-ref ref="SERVEREVENT" />
</root>
</Configuration>

View File

@ -0,0 +1,290 @@
{
"Apple": "Apple",
"Game Center": "Game Center",
"accept": "Accept",
"account_deactive": "Your current account is in the process of being deleted.",
"account_empty": "",
"account_login": "Log in using account & password",
"agree": "Agree",
"another_account": "Log in to another account",
"back": "",
"bind": "Link",
"bind_email": "Link email",
"bind_email_oversea": "Link Email",
"bind_email_success": "Email linked successfully",
"bind_request": "Linking...",
"cancel": "Cancel",
"cancel_pay": "Leave",
"captcha_empty": "Please enter SMS verification code",
"captcha_mail_empty": "Please enter the verification code from the email",
"captcha_success": "Verification code sent",
"change_password_success": "Password successfully changed",
"check_network": "Please check your Internet connection or refresh the page and try again",
"combo_download_downloading": "Downloading...",
"combo_download_failed_title": "Download Failed",
"combo_download_finish_content": "Download complete",
"combo_download_pause": "Pause",
"combo_download_speed": "Download speed: %s Time remaining: %s",
"combo_download_title": "Download Complete",
"combo_ensure_login": "Confirm login",
"combo_ensure_login_tips": "Logging in to desktop version of %s. Please confirm that this is you.",
"combo_expired_qrcode": "QR code expired",
"combo_go_setting": "Open Camera",
"combo_invalid_qrcode": "QR code invalid",
"combo_login_first": "Please log in first",
"combo_notice_auth_key_failed": "Failed to load Notices",
"combo_platform_cancel": "Cancel",
"combo_platform_ensure": "OK",
"combo_platform_ensure_exit": "Do you want to exit?",
"combo_platform_ensure_logout": "Select \"OK\" to log out",
"combo_qrcode_failed": "Login error, please try again",
"combo_qrcode_goto_setting": "Settings",
"combo_qrcode_goto_setting_tips": "Go to Settings > Apps and enable camera permissions in order to log in using the scan function.",
"combo_qrcode_notice_permission": "Please give this app permission to access your camera. You can do this in your phone's settings.",
"combo_qrcode_notice_scan": "Place the QR code within the frame",
"combo_qrcode_success": "Login successful",
"combo_qrcode_tips": "In order to log in via QR code scan, you must allow access to your camera.",
"combo_qrcode_title": "Scan",
"combo_re_login": "Please log in again",
"confirm_order": "Confirming payment...",
"continue_login": "",
"continue_pay": "Continue Payment",
"createOrder_failed": "Failed to create order",
"currency": "$",
"delete_account_notice": "Delete account login records? (Does not delete other account data)",
"delete_ensure": "Delete",
"determine_reactivate_account_or_not": "Would you like to reactivate it?",
"email_empty": "Please enter your email address",
"email_exist": "Email already registered. Log in?",
"email_register_tips": "Email not registered. Register now?",
"ensure": "OK",
"ensure_back": "Confirm and return",
"ensure_email": "Confirm registration email:\\n%s",
"enter_game": "Log in",
"existing_account": "Bind Account",
"exit": "Exit",
"facebook": "Facebook",
"fast_game": "Guest",
"fatigue_reminder_tip": "",
"file_upload_setting_camera_tip": "Go to \"Settings - Apps\" and allow camera permissions in order to use camera features.",
"file_upload_setting_microphone_tip": "Go to Settings > Apps and allow access to your microphone in order to use the video recording function in the feedback center.",
"file_upload_setting_photos_tip": "Go to Settings > Apps and enable photo permissions in order to upload images.",
"forget_pwd": "Forgot password?",
"gamecenter_tips": "Log in to Game Center in Settings",
"go_login": "Log in",
"go_pay": "Confirm and pay",
"go_register": "Register",
"guest_account": "Guest",
"guest_bind_email": "You are about to link the following email address: %s",
"guest_bind_failed": "Current account is not a guest account or user is not logged in",
"guest_bind_phone_notice": "To protect your data, please link at least one other account.",
"guest_login": "Guest login",
"guest_login_request": "Logging in as guest...",
"guest_login_tips": "Welcome, guest.",
"guest_pay_error": "Guest accounts cannot make purchases",
"http_time_out": "Connection failed. Try again later.",
"http_unknow_host": "Network error",
"if_cancel_pay": "Leave payment screen?",
"init_first": "Please complete initialization first.",
"input_account": "Enter mobile number or email address",
"input_code_number": "Enter SMS verification code",
"input_email": "Enter email address",
"input_get_code": "Get Code",
"input_mail_capture": "Verification Code",
"input_mail_code": "",
"input_mi_email": "Enter email address",
"input_password": "Enter password",
"input_password_ensure": "Enter password again",
"input_phone_number": "Enter mobile number to register/log in",
"input_phone_number_bind": "Enter mobile number",
"input_re_get_code": "Try again",
"invaild_captcha": "Invalid SMS verification code, please check",
"invaild_mail_captcha": "Verification code invalid, please check",
"invaild_name": "Please enter your name correctly",
"invaild_password": "Please enter the correct password",
"invaild_phone": "Please provide a valid mobile phone number",
"invaild_realname": "Please provide a valid ID Card number",
"last_login_day_number": "Last login %s days ago",
"last_login_hour_number": "Last login %s hours ago",
"last_login_just_now": "Last login just now",
"last_login_minute_number": "Last login %s minutes ago",
"last_login_month_number": "Last login six months ago",
"last_login_time": "Last login on %s",
"login": "Log In",
"login_again": "Please log in again",
"login_bind_mobile": "",
"login_bind_mobile_verify_mail": "",
"login_bind_safe_notice": "Before logging in to the game, please link your account with a security phone number",
"login_failed": "Login failed",
"login_first": "Please log in first",
"login_request": "Logging in...",
"login_verify_by_bind": "Verify using linked number",
"login_verify_by_bind_phone": "Verify using linked number: %s",
"login_verify_by_safety": "Verify using security phone number",
"login_verify_by_safety_phone": "Verify using security phone number: %s",
"login_verify_notice": "Verification is required when you log in using a new device.",
"meet_problem": "Having Problems?",
"more": "More",
"name_empty": "Please enter your full name",
"network_json_error": "System error. Please try again later.",
"network_time_out": "Connection failed. Try again later.",
"next": "Next",
"no_account": "Haven't signed up yet?",
"no_captcha": "Please obtain a verification code",
"no_more_interruptions_today": "Don't show again today",
"other_device_know": "OK",
"other_device_login_day_number": "Logged in %s day(s) ago",
"other_device_login_device_info": "Login Device Information:",
"other_device_login_hour_number": "Logged in %s hour(s) ago",
"other_device_login_minute_number": "Logged in %s minute(s) ago",
"other_device_login_month_number": "Logged in %s month(s) ago",
"other_device_suggest": "If this was not you, we recommend that you link an email to your account.",
"other_device_title": "Your account was logged into on another device.",
"other_way_verification": "Other Verification Methods",
"oversea_guardian": "접속하신 아이디는 만 14세 미만 법정대리인 동의가 필요한 아이디로써 관련 규정에 따라 법정대리인의 동의가 필요합니다. \\\\n관련 정보를 입력하고 인증해 주세요",
"oversea_input_account": "Enter email/username",
"oversea_pay": "Pay",
"oversea_pay_button": "Proceed to payment",
"oversea_pay_card_payment": "Credit card",
"oversea_pay_error": "Loading failed",
"oversea_pay_error_btn": "Reload",
"oversea_pay_error_tips": "Try again later.",
"oversea_pay_operator": "Select payment method",
"oversea_pay_product_name": "Product",
"oversea_pay_success": "Payment successful",
"oversea_pay_success_btn": "Return to game",
"oversea_pay_success_tips": "Please enter game to access product",
"oversea_pay_type": "Select payment type",
"oversea_realname": "관련 규정에 따라 실명인증을 완료해 주세요\\\\n궁금하신 점이 있을 경우 고객센터로 문의하세요",
"password_empty": "Enter password",
"pay": "Pay",
"pay_aid_uid_mismatch_tips": "Your account login status has expired, please log in again.",
"pay_back_game": "Return to game %s",
"pay_bind_notice": "Guest accounts must be linked to another account before making payment",
"pay_choose_new_way": "Payment incomplete. Please select another payment method to continue.",
"pay_failed": "Payment failed. Try again later.",
"pay_failed_notice": "Payment failed. Please try again.",
"pay_limit_amount_tips": "",
"pay_loading_notice": "If payment was successful, please check your account shortly.",
"pay_loading_time": "(%s)",
"pay_success": "Payment successful",
"pay_success_notice": "You have successfully purchased: %s",
"pay_time_out": "Payment timeout",
"pay_turn": "Proceed to payment",
"phone_empty": "Enter mobile number",
"phone_login": "Log in using phone",
"phone_message_request": "Verifying...",
"phone_message_request_fail": "Failed to send verification code. Please try again.",
"phone_register_tips": "Mobile number not yet registered. Register now?",
"phone_registered": "Number already registered. Enter verification code to log in.",
"privacy": "Privacy Policy",
"product_name": "Product",
"re_login": "Please log in again",
"re_read": "Read Again",
"re_register": "Enter again",
"reactivate_accoun_notice_cn": "Your account is currently in the \"deletion confirmation period.\"\\nDo you want to reactivate your account and log in?\\nPlease note that the deletion process will be canceled after reactivation.\\n※The deletion confirmation period is 3 days from the date on which you request the deletion.",
"reactivate_accoun_notice_os": "Your account is currently in the \"deletion confirmation period.\"\\nDo you want to reactivate your account and log in?\\nPlease note that the deletion process will be canceled after reactivation.\\n※The deletion confirmation period is 30 days from the date on which you request the deletion.",
"reactivate_account": "Reactivate Account",
"read_user_agreement_first": "Please read and agree to the Terms of Service",
"real_name_request": "Verifying real name...",
"real_people_agree": "",
"real_people_agreement": "",
"real_people_anti_addiction_pc_tip": "",
"real_people_anti_addiction_rule_click": "",
"real_people_anti_addiction_tip": "",
"real_people_back_tip": "",
"real_people_continue_verify": "",
"real_people_message": "",
"real_people_minor_privacy": "",
"real_people_modify": "",
"real_people_not_verify": "",
"real_people_pc_tip": "",
"real_people_real_name_full_name": "",
"real_people_real_name_identity_card": "",
"real_people_real_name_info_tip": "",
"real_people_request_camera_tip": "",
"real_people_search_result": "",
"real_people_service_agreement": "",
"real_people_setting_camera_tip": "",
"real_people_start_verify": "",
"real_people_title": "",
"real_people_verify_cancel": "",
"real_people_verify_fail": "",
"real_people_verify_success": "",
"real_people_wait_search_result": "",
"realname_account": "Name on ID Card",
"realname_account_notice": "Last name\\u3000First name",
"realname_button_finish": "Complete verification",
"realname_close_notice": "To finish account registration, you must complete real-name verification. Otherwise, you will be returned to the %s screen.",
"realname_continue": "Continue verification",
"realname_empty": "Please enter your ID Card number",
"realname_failed": "Real-name verification failed. Please try again.",
"realname_notice": "Users must provide an ID to play online. For a smooth, uninterrupted gaming experience, please enter your ID information below.",
"realname_number": "ID Card Number",
"realname_number_notice": "ID Card",
"realname_pay_close_notice": "Real-name verification must be completed before payment can be made. Are you sure you want to cancel?",
"realname_success": "Real-name verification successful",
"refresh": "Refresh",
"refuse": "Refuse",
"register_bind": "Register and link",
"register_email": "Register with email address",
"register_login": "Register and log in",
"register_new_account": "Register",
"register_now": "Register now",
"register_phone": "Register with phone number",
"register_request": "Registering...",
"second_real_name_hint": "",
"second_real_name_verify_hint": "",
"send_mail_success": "A verification code has been sent to your email address (%s). Please check your email.",
"share_image_request_photos_tip": "Access to your photos is required in order to use the photo sharing function.",
"share_image_setting_photos_tip": "Go to Settings > Apps and allow access to your photos in order to use the photo sharing function.",
"sign_in_with": "Log in with",
"status_code_429": "Network busy. Please try again later.",
"status_code_4xx": "Network busy. Please try again later.",
"status_code_5xx": "Server busy. Please try again later.",
"suggest_bind_email": "To improve your account security, it is strongly recommended that you link your email address",
"suggest_device_grant": "You must complete security verification when logging in on a new device",
"suggest_verify_phone": "To improve your account security, it is strongly recommended that you link your email address.\nPlease complete the security verification first.",
"tips_bind_account": "Please link at least one account as soon as possible to protect your data and to avoid losing it.",
"tips_bind_success": "Sucessfully linked with: %s",
"tips_enter_game": "Welcome, %s!",
"tips_exprience_full": "Dear player, guest accounts provide a limited experience of up to n miHoYo games.\\n\\nFor a better gaming experience, it is highly recommended to register a full account.",
"tips_mail_capture": "Verification code sent to: %s",
"tips_ok": "OK",
"tips_prefix_verify_email": "Verify linked email:",
"tips_prefix_verify_mobile": "Verify using linked number:",
"tips_request_camera_permission": "In order to upload images in the Feedback Center, you must allow access to your camera.",
"tips_request_microphone_permission": "In order to use the video recording function in the feedback center, you must allow recording permissions.",
"tips_request_photos_permission": "In order to take and upload photos in the feedback center, you must allow access to your photos and camera.",
"title_realname": "Real-name Verification",
"token_invalid": "Login expired, please log in again",
"twitter": "Twitter",
"twitter_login": "Twitter",
"update_notice": "The Terms of Service or Privacy Policy has been updated",
"user_agreement": "Terms of Service",
"user_agreement_agree_tips": "Please carefully read the Terms of Service and Privacy Policy, and tick to agree",
"user_agreement_all_agree": "Agree to all the following",
"user_agreement_content": "Please read carefully and understand the terms before agreeing.",
"user_agreement_link_click_desc": "Tap the following link to read the full terms:",
"user_agreement_marketing_content": "After carefully reading and agreeing to the policies, please select Accept",
"user_agreement_marketing_update_title": "Marketing Agreement Update Notice",
"user_agreement_notice": "I have read and agree to the %s and %s",
"user_agreement_notice_content": "",
"user_agreement_notice_content_with_third_privacy": "",
"user_agreement_notice_minors": "",
"user_agreement_notice_privacy": "Privacy Policy",
"user_agreement_notice_third_privacy": "",
"user_agreement_notice_ua": "Terms of Service",
"user_agreement_optional": "(Optional)",
"user_agreement_refuse_tips": "Please read and accept our Terms of Service and Privacy Policy before you continue playing.",
"user_agreement_required": "(Required)",
"user_agreement_title": "Terms of Service and Privacy Policy Notice",
"user_agreement_update_content": "Please read carefully and understand the terms before agreeing.",
"user_agreement_update_title": "Terms of Service and Privacy Policy Update Notice",
"user_center": "User Center",
"username_empty": "Please enter email address/username",
"verify_email": "",
"verify_finish": "",
"verify_other": "",
"verify_phone": ""
}

View File

@ -0,0 +1,3 @@
{
"version": 51
}