diff --git a/.github/workflows/lint_commit.yml b/.github/workflows/lint_commit.yml index 684cbe4bf..2b0f2a979 100644 --- a/.github/workflows/lint_commit.yml +++ b/.github/workflows/lint_commit.yml @@ -32,10 +32,10 @@ jobs: # - run: git merge development - run: git reset --hard development - run: git stash pop - - run: git add -u - - run: git commit -m 'Fix whitespace [skip actions]' + - name: Commit any whitespace changes + run: git add -u && git commit -m 'Fix whitespace [skip actions]' || true - name: Update Languages run: python manage_languages.py -u - - run: git add -u - - run: git commit -m 'Update languages [skip actions]' + - name: Commit any language changes + run: git add -u && git commit -m 'Update languages [skip actions]' || true - run: git push --set-upstream --force origin LintRatchet diff --git a/build.gradle b/build.gradle index 6872c1e49..38d354500 100644 --- a/build.gradle +++ b/build.gradle @@ -43,8 +43,7 @@ sourceCompatibility = JavaVersion.VERSION_17 targetCompatibility = JavaVersion.VERSION_17 group = 'xyz.grasscutters' -version = '1.2.2-dev' - +version = '1.2.3-dev' sourceCompatibility = 17 targetCompatibility = 17 @@ -61,15 +60,18 @@ repositories { dependencies { implementation fileTree(dir: 'lib', include: ['*.jar']) - implementation group: 'org.slf4j', name: 'slf4j-api', version: '1.7.32' - implementation group: 'ch.qos.logback', name: 'logback-core', version: '1.2.9' - implementation group: 'ch.qos.logback', name: 'logback-classic', version: '1.2.9' + implementation group: 'org.slf4j', name: 'slf4j-api', version: '1.7.36' + implementation group: 'ch.qos.logback', name: 'logback-core', version: '1.2.11' + implementation group: 'ch.qos.logback', name: 'logback-classic', version: '1.2.11' implementation group: 'org.jline', name: 'jline', version: '3.21.0' implementation group: 'org.jline', name: 'jline-terminal-jna', version: '3.21.0' implementation group: 'net.java.dev.jna', name: 'jna', version: '5.10.0' - implementation group: 'io.netty', name: 'netty-all', version: '4.1.71.Final' + implementation group: 'io.netty', name: 'netty-common', version: '4.1.79.Final' + implementation group: 'io.netty', name: 'netty-handler', version: '4.1.79.Final' + implementation group: 'io.netty', name: 'netty-transport-native-epoll', version: '4.1.79.Final' + implementation group: 'io.netty', name: 'netty-transport-native-kqueue', version: '4.1.79.Final' implementation group: 'com.google.code.gson', name: 'gson', version: '2.9.0' implementation group: 'com.google.protobuf', name: 'protobuf-java', version: '3.18.2' @@ -79,7 +81,7 @@ dependencies { implementation group: 'dev.morphia.morphia', name: 'morphia-core', version: '2.2.7' implementation group: 'org.greenrobot', name: 'eventbus-java', version: '3.3.1' - implementation group: 'org.danilopianini', name: 'java-quadtree', version: '0.1.9' + //implementation group: 'org.danilopianini', name: 'java-quadtree', version: '0.1.9' implementation group: 'org.quartz-scheduler', name: 'quartz', version: '2.3.2' implementation group: 'org.quartz-scheduler', name: 'quartz-jobs', version: '2.3.2' diff --git a/lib/bcrypt-0.8.0.jar b/lib/bcrypt-0.8.0.jar new file mode 100644 index 000000000..f4835ddbc Binary files /dev/null and b/lib/bcrypt-0.8.0.jar differ diff --git a/lib/bytes-1.3.0.jar b/lib/bytes-1.3.0.jar new file mode 100644 index 000000000..f6fbfeb9a Binary files /dev/null and b/lib/bytes-1.3.0.jar differ diff --git a/manage_languages.py b/manage_languages.py index ee7d1a5c1..ddaf73377 100644 --- a/manage_languages.py +++ b/manage_languages.py @@ -90,6 +90,7 @@ class JsonHelpers: class LanguageManager: TRANSLATION_KEY = re.compile(r'[Tt]ranslate.*"(\w+\.[\w\.]+)"') POTENTIAL_KEY = re.compile(r'"(\w+\.[\w\.]+)"') + COMMAND_LABEL = re.compile(r'@Command\s*\([\W\w]*?label\s*=\s*"(\w+)"', re.MULTILINE) # [\W\w] is a cheeky way to match everything including \n def __init__(self): self.load_jsons() @@ -122,6 +123,8 @@ class LanguageManager: used.add(k) for k in self.POTENTIAL_KEY.findall(data): potential.add(k) + for label in self.COMMAND_LABEL.findall(data): + used.add(f'commands.{label}.description') return used | (potential & expected_keys) def _lint_report_language(self, lang: str, keys: set, flattened: dict, primary_language_flattened: dict) -> None: diff --git a/src/main/java/emu/grasscutter/GameConstants.java b/src/main/java/emu/grasscutter/GameConstants.java index 970240ed5..c1f0ac9e0 100644 --- a/src/main/java/emu/grasscutter/GameConstants.java +++ b/src/main/java/emu/grasscutter/GameConstants.java @@ -6,29 +6,29 @@ import emu.grasscutter.utils.Position; import emu.grasscutter.utils.Utils; public final class GameConstants { - public static String VERSION = "2.7.0"; - - public static final int MAX_TEAMS = 4; - public static final int MAIN_CHARACTER_MALE = 10000005; - public static final int MAIN_CHARACTER_FEMALE = 10000007; - public static final Position START_POSITION = new Position(2747, 194, -1719); - - public static final int MAX_FRIENDS = 45; - public static final int MAX_FRIEND_REQUESTS = 50; - - public static final int SERVER_CONSOLE_UID = 99; // The UID of the server console's "player". - - public static final int BATTLE_PASS_MAX_LEVEL = 50; - public static final int BATTLE_PASS_POINT_PER_LEVEL = 1000; - public static final int BATTLE_PASS_POINT_PER_WEEK = 10000; - public static final int BATTLE_PASS_LEVEL_PRICE = 150; - public static final int BATTLE_PASS_CURRENT_INDEX = 2; - - // Default entity ability hashes. - public static final String[] DEFAULT_ABILITY_STRINGS = { - "Avatar_DefaultAbility_VisionReplaceDieInvincible", "Avatar_DefaultAbility_AvartarInShaderChange", "Avatar_SprintBS_Invincible", - "Avatar_Freeze_Duration_Reducer", "Avatar_Attack_ReviveEnergy", "Avatar_Component_Initializer", "Avatar_FallAnthem_Achievement_Listener" - }; - public static final int[] DEFAULT_ABILITY_HASHES = Arrays.stream(DEFAULT_ABILITY_STRINGS).mapToInt(Utils::abilityHash).toArray(); - public static final int DEFAULT_ABILITY_NAME = Utils.abilityHash("Default"); + public static String VERSION = "2.8.0"; + + public static final int MAX_TEAMS = 4; + public static final int MAIN_CHARACTER_MALE = 10000005; + public static final int MAIN_CHARACTER_FEMALE = 10000007; + public static final Position START_POSITION = new Position(2747, 194, -1719); + + public static final int MAX_FRIENDS = 45; + public static final int MAX_FRIEND_REQUESTS = 50; + + public static final int SERVER_CONSOLE_UID = 99; // The UID of the server console's "player". + + public static final int BATTLE_PASS_MAX_LEVEL = 50; + public static final int BATTLE_PASS_POINT_PER_LEVEL = 1000; + public static final int BATTLE_PASS_POINT_PER_WEEK = 10000; + public static final int BATTLE_PASS_LEVEL_PRICE = 150; + public static final int BATTLE_PASS_CURRENT_INDEX = 2; + + // Default entity ability hashes. + public static final String[] DEFAULT_ABILITY_STRINGS = { + "Avatar_DefaultAbility_VisionReplaceDieInvincible", "Avatar_DefaultAbility_AvartarInShaderChange", "Avatar_SprintBS_Invincible", + "Avatar_Freeze_Duration_Reducer", "Avatar_Attack_ReviveEnergy", "Avatar_Component_Initializer", "Avatar_FallAnthem_Achievement_Listener" + }; + public static final int[] DEFAULT_ABILITY_HASHES = Arrays.stream(DEFAULT_ABILITY_STRINGS).mapToInt(Utils::abilityHash).toArray(); + public static final int DEFAULT_ABILITY_NAME = Utils.abilityHash("Default"); } diff --git a/src/main/java/emu/grasscutter/Grasscutter.java b/src/main/java/emu/grasscutter/Grasscutter.java index 881a1d2ac..8a8c36e18 100644 --- a/src/main/java/emu/grasscutter/Grasscutter.java +++ b/src/main/java/emu/grasscutter/Grasscutter.java @@ -5,14 +5,15 @@ import ch.qos.logback.classic.Logger; import com.google.gson.Gson; import com.google.gson.GsonBuilder; -import emu.grasscutter.Grasscutter.ServerDebugMode; import emu.grasscutter.auth.AuthenticationSystem; import emu.grasscutter.auth.DefaultAuthentication; import emu.grasscutter.command.CommandMap; import emu.grasscutter.command.DefaultPermissionHandler; import emu.grasscutter.command.PermissionHandler; +import emu.grasscutter.config.ConfigContainer; import emu.grasscutter.data.ResourceLoader; import emu.grasscutter.database.DatabaseManager; +import emu.grasscutter.net.packet.PacketOpcodesUtils; import emu.grasscutter.plugin.PluginManager; import emu.grasscutter.plugin.api.ServerHook; import emu.grasscutter.scripts.ScriptLoader; @@ -26,7 +27,6 @@ import emu.grasscutter.server.http.handlers.GachaHandler; import emu.grasscutter.server.http.handlers.GenericHandler; import emu.grasscutter.server.http.handlers.LogHandler; import emu.grasscutter.tools.Tools; -import emu.grasscutter.utils.ConfigContainer; import emu.grasscutter.utils.Crypto; import emu.grasscutter.utils.Language; import emu.grasscutter.utils.Utils; @@ -43,8 +43,8 @@ import javax.annotation.Nullable; import java.io.*; import java.util.Calendar; -import static emu.grasscutter.Configuration.DATA; -import static emu.grasscutter.Configuration.SERVER; +import static emu.grasscutter.config.Configuration.DATA; +import static emu.grasscutter.config.Configuration.SERVER; import static emu.grasscutter.utils.Language.translate; public final class Grasscutter { @@ -98,6 +98,10 @@ public final class Grasscutter { Tools.createGmHandbook(); exitEarly = true; } + case "-dumppacketids" -> { + PacketOpcodesUtils.dumpPacketIds(); + exitEarly = true; + } case "-gachamap" -> { Tools.createGachaMapping(DATA("gacha_mappings.js")); exitEarly = true; @@ -213,7 +217,7 @@ public final class Grasscutter { */ private static void onShutdown() { // Disable all plugins. - if(pluginManager != null) + if (pluginManager != null) pluginManager.disablePlugins(); } diff --git a/src/main/java/emu/grasscutter/auth/DefaultAuthentication.java b/src/main/java/emu/grasscutter/auth/DefaultAuthentication.java index efe637530..9fa1ec4aa 100644 --- a/src/main/java/emu/grasscutter/auth/DefaultAuthentication.java +++ b/src/main/java/emu/grasscutter/auth/DefaultAuthentication.java @@ -6,6 +6,7 @@ import emu.grasscutter.game.Account; import emu.grasscutter.server.http.objects.ComboTokenResJson; import emu.grasscutter.server.http.objects.LoginResultJson; +import static emu.grasscutter.config.Configuration.ACCOUNT; import static emu.grasscutter.utils.Language.translate; /** @@ -13,12 +14,20 @@ import static emu.grasscutter.utils.Language.translate; * Allows all users to access any account. */ public final class DefaultAuthentication implements AuthenticationSystem { - private final Authenticator passwordAuthenticator = new PasswordAuthenticator(); - private final Authenticator tokenAuthenticator = new TokenAuthenticator(); - private final Authenticator sessionKeyAuthenticator = new SessionKeyAuthenticator(); - private final ExternalAuthenticator externalAuthenticator = new ExternalAuthentication(); - private final OAuthAuthenticator oAuthAuthenticator = new OAuthAuthentication(); - + private Authenticator passwordAuthenticator; + private Authenticator tokenAuthenticator = new TokenAuthenticator(); + private Authenticator sessionKeyAuthenticator = new SessionKeyAuthenticator(); + private ExternalAuthenticator externalAuthenticator = new ExternalAuthentication(); + private OAuthAuthenticator oAuthAuthenticator = new OAuthAuthentication(); + + public DefaultAuthentication() { + if (ACCOUNT.EXPERIMENTAL_RealPassword) { + passwordAuthenticator = new ExperimentalPasswordAuthenticator(); + } else { + passwordAuthenticator = new PasswordAuthenticator(); + } + } + @Override public void createAccount(String username, String password) { // Unhandled. The default authenticator doesn't store passwords. diff --git a/src/main/java/emu/grasscutter/auth/DefaultAuthenticators.java b/src/main/java/emu/grasscutter/auth/DefaultAuthenticators.java index 905535380..cc9080044 100644 --- a/src/main/java/emu/grasscutter/auth/DefaultAuthenticators.java +++ b/src/main/java/emu/grasscutter/auth/DefaultAuthenticators.java @@ -1,31 +1,41 @@ package emu.grasscutter.auth; +import at.favre.lib.crypto.bcrypt.BCrypt; import emu.grasscutter.Grasscutter; import emu.grasscutter.auth.AuthenticationSystem.AuthenticationRequest; import emu.grasscutter.database.DatabaseHelper; import emu.grasscutter.game.Account; import emu.grasscutter.server.http.objects.*; +import emu.grasscutter.utils.FileUtils; +import emu.grasscutter.utils.Utils; -import static emu.grasscutter.Configuration.*; +import javax.crypto.Cipher; +import java.nio.charset.StandardCharsets; +import java.security.KeyFactory; +import java.security.interfaces.RSAPrivateKey; +import java.security.spec.PKCS8EncodedKeySpec; + +import static emu.grasscutter.config.Configuration.*; import static emu.grasscutter.utils.Language.translate; /** * A class containing default authenticators. */ public final class DefaultAuthenticators { - + /** * Handles the authentication request from the username and password form. */ public static class PasswordAuthenticator implements Authenticator { - @Override public LoginResultJson authenticate(AuthenticationRequest request) { + @Override + public LoginResultJson authenticate(AuthenticationRequest request) { var response = new LoginResultJson(); - + var requestData = request.getPasswordRequest(); assert requestData != null; // This should never be null. int playerCount = Grasscutter.getGameServer().getPlayers().size(); - boolean successfulLogin = false; + boolean successfulLogin = false; String address = request.getRequest().ip(); String responseMessage = translate("messages.dispatch.account.username_error"); String loggerMessage = ""; @@ -34,12 +44,12 @@ public final class DefaultAuthenticators { Account account = DatabaseHelper.getAccountByName(requestData.account); if (ACCOUNT.maxPlayer <= -1 || playerCount < ACCOUNT.maxPlayer) { // Check if account exists. - if(account == null && ACCOUNT.autoCreate) { + if (account == null && ACCOUNT.autoCreate) { // This account has been created AUTOMATICALLY. There will be no permissions added. account = DatabaseHelper.createAccountWithUid(requestData.account, 0); // Check if the account was created successfully. - if(account == null) { + if (account == null) { responseMessage = translate("messages.dispatch.account.username_create_error"); Grasscutter.getLogger().info(translate("messages.dispatch.account.account_login_create_error", address)); } else { @@ -49,9 +59,9 @@ public final class DefaultAuthenticators { // Log the creation. Grasscutter.getLogger().info(translate("messages.dispatch.account.account_login_create_success", address, response.data.account.uid)); } - } else if(account != null) + } else if (account != null) successfulLogin = true; - else + else loggerMessage = translate("messages.dispatch.account.account_login_exist_error", address); } else { @@ -59,9 +69,113 @@ public final class DefaultAuthenticators { loggerMessage = translate("messages.dispatch.account.login_max_player_limit", address); } - + // Set response data. - if(successfulLogin) { + if (successfulLogin) { + response.message = "OK"; + response.data.account.uid = account.getId(); + response.data.account.token = account.generateSessionKey(); + response.data.account.email = account.getEmail(); + + loggerMessage = translate("messages.dispatch.account.login_success", address, account.getId()); + } else { + response.retcode = -201; + response.message = responseMessage; + + } + Grasscutter.getLogger().info(loggerMessage); + + return response; + } + } + + public static class ExperimentalPasswordAuthenticator implements Authenticator { + @Override + public LoginResultJson authenticate(AuthenticationRequest request) { + var response = new LoginResultJson(); + + 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 = ""; + String decryptedPassword = ""; + try { + byte[] key = FileUtils.readResource("/keys/auth_private-key.der"); + PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(key); + KeyFactory keyFactory = KeyFactory.getInstance("RSA"); + RSAPrivateKey private_key = (RSAPrivateKey) keyFactory.generatePrivate(keySpec); + + Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding"); + + cipher.init(Cipher.DECRYPT_MODE, private_key); + + decryptedPassword = new String(cipher.doFinal(Utils.base64Decode(request.getPasswordRequest().password)), StandardCharsets.UTF_8); + } catch (Exception ignored) { + decryptedPassword = request.getPasswordRequest().password; + } + + if (decryptedPassword == null) { + successfulLogin = false; + loggerMessage = translate("messages.dispatch.account.login_password_error", address); + responseMessage = translate("messages.dispatch.account.password_error"); + } + + // 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. + if (decryptedPassword.length() >= 8) { + account = DatabaseHelper.createAccountWithUid(requestData.account, 0); + account.setPassword(BCrypt.withDefaults().hashToString(12, decryptedPassword.toCharArray())); + account.save(); + + // Check if the account was created successfully. + if (account == null) { + responseMessage = translate("messages.dispatch.account.username_create_error"); + loggerMessage = translate("messages.dispatch.account.account_login_create_error", address); + } else { + // Continue with login. + successfulLogin = true; + + // Log the creation. + Grasscutter.getLogger().info(translate("messages.dispatch.account.account_login_create_success", address, response.data.account.uid)); + } + } else { + successfulLogin = false; + loggerMessage = translate("messages.dispatch.account.login_password_error", address); + responseMessage = translate("messages.dispatch.account.password_length_error"); + } + } else if (account != null) { + if (account.getPassword() != null && !account.getPassword().isEmpty()) { + if (BCrypt.verifyer().verify(decryptedPassword.toCharArray(), account.getPassword()).verified) { + successfulLogin = true; + } else { + successfulLogin = false; + loggerMessage = translate("messages.dispatch.account.login_password_error", address); + responseMessage = translate("messages.dispatch.account.password_error"); + } + } else { + successfulLogin = false; + loggerMessage = translate("messages.dispatch.account.login_password_storage_error", address); + responseMessage = translate("messages.dispatch.account.password_storage_error"); + } + } 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) { response.message = "OK"; response.data.account.uid = account.getId(); response.data.account.token = account.generateSessionKey(); @@ -83,9 +197,10 @@ public final class DefaultAuthenticators { * Handles the authentication request from the game when using a registry token. */ public static class TokenAuthenticator implements Authenticator { - @Override public LoginResultJson authenticate(AuthenticationRequest request) { + @Override + public LoginResultJson authenticate(AuthenticationRequest request) { var response = new LoginResultJson(); - + var requestData = request.getTokenRequest(); assert requestData != null; @@ -106,7 +221,7 @@ public final class DefaultAuthenticators { successfulLogin = account != null && account.getSessionKey().equals(requestData.token); // Set response data. - if(successfulLogin) { + if (successfulLogin) { response.message = "OK"; response.data.account.uid = account.getId(); response.data.account.token = account.getSessionKey(); @@ -138,13 +253,15 @@ public final class DefaultAuthenticators { * Handles the authentication request from the game when using a combo token/session key. */ public static class SessionKeyAuthenticator implements Authenticator { - @Override public ComboTokenResJson authenticate(AuthenticationRequest request) { - var response = new ComboTokenResJson(); - + @Override + public ComboTokenResJson authenticate(AuthenticationRequest request) { + var response = new ComboTokenResJson(); + var requestData = request.getSessionKeyRequest(); var loginData = request.getSessionKeyData(); - assert requestData != null; assert loginData != null; - + assert requestData != null; + assert loginData != null; + boolean successfulLogin; String address = request.getRequest().ip(); String loggerMessage; @@ -158,7 +275,7 @@ public final class DefaultAuthenticators { successfulLogin = account != null && account.getSessionKey().equals(loginData.token); // Set response data. - if(successfulLogin) { + if (successfulLogin) { response.message = "OK"; response.data.open_id = account.getId(); response.data.combo_id = "157795300"; @@ -190,17 +307,20 @@ public final class DefaultAuthenticators { * Handles authentication requests from external sources. */ public static class ExternalAuthentication implements ExternalAuthenticator { - @Override public void handleLogin(AuthenticationRequest request) { + @Override + public void handleLogin(AuthenticationRequest request) { assert request.getResponse() != null; request.getResponse().send("Authentication is not available with the default authentication method."); } - @Override public void handleAccountCreation(AuthenticationRequest request) { + @Override + public void handleAccountCreation(AuthenticationRequest request) { assert request.getResponse() != null; request.getResponse().send("Authentication is not available with the default authentication method."); } - @Override public void handlePasswordReset(AuthenticationRequest request) { + @Override + public void handlePasswordReset(AuthenticationRequest request) { assert request.getResponse() != null; request.getResponse().send("Authentication is not available with the default authentication method."); } @@ -210,17 +330,20 @@ public final class DefaultAuthenticators { * Handles authentication requests from OAuth sources. */ public static class OAuthAuthentication implements OAuthAuthenticator { - @Override public void handleLogin(AuthenticationRequest request) { + @Override + public void handleLogin(AuthenticationRequest request) { assert request.getResponse() != null; request.getResponse().send("Authentication is not available with the default authentication method."); } - @Override public void handleRedirection(AuthenticationRequest request, ClientType type) { + @Override + public void handleRedirection(AuthenticationRequest request, ClientType type) { assert request.getResponse() != null; request.getResponse().send("Authentication is not available with the default authentication method."); } - @Override public void handleTokenProcess(AuthenticationRequest request) { + @Override + public void handleTokenProcess(AuthenticationRequest request) { assert request.getResponse() != null; request.getResponse().send("Authentication is not available with the default authentication method."); } diff --git a/src/main/java/emu/grasscutter/command/Command.java b/src/main/java/emu/grasscutter/command/Command.java index 4af98e788..095e64cdb 100644 --- a/src/main/java/emu/grasscutter/command/Command.java +++ b/src/main/java/emu/grasscutter/command/Command.java @@ -7,14 +7,12 @@ import java.lang.annotation.RetentionPolicy; public @interface Command { String label() default ""; - String usage() default "commands.generic.no_usage_specified"; - - String description() default "commands.generic.no_description_specified"; - String[] aliases() default {}; + String[] usage() default {""}; + String permission() default ""; - + String permissionTargeted() default ""; public enum TargetRequirement { diff --git a/src/main/java/emu/grasscutter/command/CommandHandler.java b/src/main/java/emu/grasscutter/command/CommandHandler.java index 4803b154f..aef968558 100644 --- a/src/main/java/emu/grasscutter/command/CommandHandler.java +++ b/src/main/java/emu/grasscutter/command/CommandHandler.java @@ -2,12 +2,11 @@ package emu.grasscutter.command; import emu.grasscutter.Grasscutter; import emu.grasscutter.game.player.Player; -import emu.grasscutter.server.event.game.CommandResponseEvent; import emu.grasscutter.server.event.game.ReceiveCommandFeedbackEvent; -import emu.grasscutter.server.event.types.ServerEvent; import static emu.grasscutter.utils.Language.translate; import java.util.List; +import java.util.StringJoiner; public interface CommandHandler { @@ -37,6 +36,41 @@ public interface CommandHandler { sendMessage(player, translate(player, messageKey, args)); } + default String getUsageString(Player player, String... args) { + Command annotation = this.getClass().getAnnotation(Command.class); + String usage_prefix = translate(player, "commands.execution.usage_prefix"); + String command = annotation.label(); + for (String alias : annotation.aliases()) { + if (alias.length() < command.length()) + command = alias; + } + String target = switch (annotation.targetRequirement()) { + case NONE -> ""; + case OFFLINE -> "@ "; // TODO: make translation keys for offline and online players + case ONLINE -> (player == null) ? "@ " : "[@] "; // TODO: make translation keys for offline and online players + case PLAYER -> (player == null) ? "@ " : "[@] "; + }; + String[] usages = annotation.usage(); + StringJoiner joiner = new StringJoiner("\n\t"); + for (String usage : usages) + joiner.add(usage_prefix + command + " " + target + usage); + return joiner.toString(); + } + + default void sendUsageMessage(Player player, String... args) { + sendMessage(player, getUsageString(player, args)); + } + + default String getLabel() { + return this.getClass().getAnnotation(Command.class).label(); + } + + default String getDescriptionString(Player player) { + Command annotation = this.getClass().getAnnotation(Command.class); + String key = "commands.%s.description".formatted(annotation.label()); + return translate(player, key); + } + /** * Called when a player/console invokes a command. * @param sender The player/console that invoked the command. diff --git a/src/main/java/emu/grasscutter/command/CommandMap.java b/src/main/java/emu/grasscutter/command/CommandMap.java index fe19075bf..c976c5ccb 100644 --- a/src/main/java/emu/grasscutter/command/CommandMap.java +++ b/src/main/java/emu/grasscutter/command/CommandMap.java @@ -8,9 +8,9 @@ import java.util.*; @SuppressWarnings({"UnusedReturnValue", "unused"}) public final class CommandMap { - private final Map commands = new HashMap<>(); - private final Map aliases = new HashMap<>(); - private final Map annotations = new HashMap<>(); + private final Map commands = new TreeMap<>(); + private final Map aliases = new TreeMap<>(); + private final Map annotations = new TreeMap<>(); private final Map targetPlayerIds = new HashMap<>(); private static final String consoleId = "console"; @@ -35,6 +35,7 @@ public final class CommandMap { */ public CommandMap registerCommand(String label, CommandHandler command) { Grasscutter.getLogger().debug("Registered command: " + label); + label = label.toLowerCase(); // Get command data. Command annotation = command.getClass().getAnnotation(Command.class); @@ -167,7 +168,7 @@ public final class CommandMap { CommandHandler.sendTranslatedMessage(player, "commands.execution.clear_target"); return true; } - + // Sets default targetPlayer to the UID provided. try { int uid = Integer.parseInt(targetUid); @@ -203,7 +204,7 @@ public final class CommandMap { // Parse message. String[] split = rawMessage.split(" "); List args = new LinkedList<>(Arrays.asList(split)); - String label = args.remove(0); + String label = args.remove(0).toLowerCase(); String playerId = (player == null) ? consoleId : player.getAccount().getId(); // Check for special cases - currently only target command. @@ -237,7 +238,7 @@ public final class CommandMap { Command annotation = this.annotations.get(label); // Resolve targetPlayer - try{ + try { targetPlayer = getTargetPlayer(playerId, player, targetPlayer, args); } catch (IllegalArgumentException e) { return; diff --git a/src/main/java/emu/grasscutter/command/commands/AccountCommand.java b/src/main/java/emu/grasscutter/command/commands/AccountCommand.java index 0d50b3787..4127a3a2c 100644 --- a/src/main/java/emu/grasscutter/command/commands/AccountCommand.java +++ b/src/main/java/emu/grasscutter/command/commands/AccountCommand.java @@ -1,8 +1,10 @@ package emu.grasscutter.command.commands; +import at.favre.lib.crypto.bcrypt.BCrypt; import emu.grasscutter.Grasscutter; import emu.grasscutter.command.Command; import emu.grasscutter.command.CommandHandler; +import emu.grasscutter.config.Configuration; import emu.grasscutter.database.DatabaseHelper; import emu.grasscutter.game.Account; import emu.grasscutter.game.player.Player; @@ -11,18 +13,24 @@ import java.util.List; import static emu.grasscutter.utils.Language.translate; -@Command(label = "account", usage = "account [uid]", description = "commands.account.description", targetRequirement = Command.TargetRequirement.NONE) +@Command( + label = "account", + usage = { + "create []", // Only with EXPERIMENTAL_RealPassword == false + "delete ", + "create []", // Only with EXPERIMENTAL_RealPassword == true + "resetpass "}, // Only with EXPERIMENTAL_RealPassword == true + targetRequirement = Command.TargetRequirement.NONE) public final class AccountCommand implements CommandHandler { - @Override public void execute(Player sender, Player targetPlayer, List args) { if (sender != null) { - CommandHandler.sendMessage(sender, translate(sender, "commands.generic.console_execute_error")); + CommandHandler.sendTranslatedMessage(sender, "commands.generic.console_execute_error"); return; } if (args.size() < 2) { - CommandHandler.sendMessage(null, translate(sender, "commands.account.command_usage")); + sendUsageMessage(sender); return; } @@ -31,28 +39,55 @@ public final class AccountCommand implements CommandHandler { switch (action) { default: - CommandHandler.sendMessage(null, translate(sender, "commands.account.command_usage")); + sendUsageMessage(sender); return; case "create": int uid = 0; - if (args.size() > 2) { - try { - uid = Integer.parseInt(args.get(2)); - } catch (NumberFormatException ignored) { - CommandHandler.sendMessage(null, translate(sender, "commands.account.invalid")); + String password = ""; + + if(Configuration.ACCOUNT.EXPERIMENTAL_RealPassword == true) { + if(args.size() < 3) { + CommandHandler.sendMessage(sender, "EXPERIMENTAL_RealPassword requires a password argument"); + CommandHandler.sendMessage(sender, "Usage: account create [uid]"); return; } + password = args.get(2); + + if (args.size() == 4) { + try { + uid = Integer.parseInt(args.get(3)); + } catch (NumberFormatException ignored) { + CommandHandler.sendMessage(sender, translate(sender, "commands.account.invalid")); + if(Configuration.ACCOUNT.EXPERIMENTAL_RealPassword == true) { + CommandHandler.sendMessage(sender, "EXPERIMENTAL_RealPassword requires argument 2 to be a password, not a uid"); + CommandHandler.sendMessage(sender, "Usage: account create [uid]"); + } + return; + } + } + } else { + if (args.size() > 2) { + try { + uid = Integer.parseInt(args.get(2)); + } catch (NumberFormatException ignored) { + CommandHandler.sendMessage(sender, translate(sender, "commands.account.invalid")); + return; + } + } } emu.grasscutter.game.Account account = DatabaseHelper.createAccountWithUid(username, uid); if (account == null) { - CommandHandler.sendMessage(null, translate(sender, "commands.account.exists")); + CommandHandler.sendMessage(sender, translate(sender, "commands.account.exists")); return; } else { + if (Configuration.ACCOUNT.EXPERIMENTAL_RealPassword == true) { + account.setPassword(BCrypt.withDefaults().hashToString(12, password.toCharArray())); + } account.addPermission("*"); account.save(); // Save account to database. - CommandHandler.sendMessage(null, translate(sender, "commands.account.create", Integer.toString(account.getReservedPlayerUid()))); + CommandHandler.sendMessage(sender, translate(sender, "commands.account.create", Integer.toString(account.getReservedPlayerUid()))); } return; case "delete": @@ -60,20 +95,50 @@ public final class AccountCommand implements CommandHandler { Account toDelete = DatabaseHelper.getAccountByName(username); if (toDelete == null) { - CommandHandler.sendMessage(null, translate(sender, "commands.account.no_account")); + CommandHandler.sendMessage(sender, translate(sender, "commands.account.no_account")); return; } - - // Get the player for the account. - // If that player is currently online, we kick them before proceeding with the deletion. - Player player = Grasscutter.getGameServer().getPlayerByAccountId(toDelete.getId()); - if (player != null) { - player.getSession().close(); - } + + // Make sure player isn't online as we delete their account. + kickAccount(toDelete); // Finally, we do the actual deletion. DatabaseHelper.deleteAccount(toDelete); - CommandHandler.sendMessage(null, translate(sender, "commands.account.delete")); + CommandHandler.sendMessage(sender, translate(sender, "commands.account.delete")); + return; + case "resetpass": + if(Configuration.ACCOUNT.EXPERIMENTAL_RealPassword != true) { + CommandHandler.sendMessage(sender, "resetpass requires EXPERIMENTAL_RealPassword to be true."); + return; + } + + if(args.size() != 3) { + CommandHandler.sendMessage(sender, "Invalid Args"); + CommandHandler.sendMessage(sender, "Usage: account resetpass "); + return; + } + + Account toUpdate = DatabaseHelper.getAccountByName(username); + + if (toUpdate == null) { + CommandHandler.sendMessage(sender, translate(sender, "commands.account.no_account")); + return; + } + + // Make sure player can't stay logged in with old password. + kickAccount(toUpdate); + + toUpdate.setPassword(BCrypt.withDefaults().hashToString(12, args.get(2).toCharArray())); + toUpdate.save(); + CommandHandler.sendMessage(sender, "Password Updated."); + return; + } + } + + private void kickAccount(Account account) { + Player player = Grasscutter.getGameServer().getPlayerByAccountId(account.getId()); + if (player != null) { + player.getSession().close(); } } } diff --git a/src/main/java/emu/grasscutter/command/commands/AnnounceCommand.java b/src/main/java/emu/grasscutter/command/commands/AnnounceCommand.java index 7f8ed3680..dbccd43de 100644 --- a/src/main/java/emu/grasscutter/command/commands/AnnounceCommand.java +++ b/src/main/java/emu/grasscutter/command/commands/AnnounceCommand.java @@ -13,31 +13,30 @@ import java.util.Random; import static emu.grasscutter.utils.Language.translate; @Command(label = "announce", - usage = "announce|a <\"tpl\" templateId|\"refresh\"|\"revoke\" templateId|content>", + usage = {"", "refresh", "(tpl|revoke) "}, permission = "server.announce", aliases = {"a"}, - description = "commands.announce.description", targetRequirement = Command.TargetRequirement.NONE) public final class AnnounceCommand implements CommandHandler { @Override public void execute(Player sender, Player targetPlayer, List args) { - var manager = Grasscutter.getGameServer().getAnnouncementManager(); + var manager = Grasscutter.getGameServer().getAnnouncementSystem(); if (args.size() < 1) { - CommandHandler.sendTranslatedMessage(sender, "commands.announce.command_usage"); + sendUsageMessage(sender); return; } - switch (args.get(0)){ + switch (args.get(0)) { case "tpl": if (args.size() < 2) { - CommandHandler.sendTranslatedMessage(sender, "commands.announce.command_usage"); + sendUsageMessage(sender); return; } var templateId = Integer.parseInt(args.get(1)); var tpl = manager.getAnnounceConfigItemMap().get(templateId); - if(tpl == null){ + if (tpl == null) { CommandHandler.sendMessage(sender, translate(sender, "commands.announce.not_found", templateId)); return; } @@ -53,7 +52,7 @@ public final class AnnounceCommand implements CommandHandler { case "revoke": if (args.size() < 2) { - CommandHandler.sendTranslatedMessage(sender, "commands.announce.command_usage"); + sendUsageMessage(sender); return; } diff --git a/src/main/java/emu/grasscutter/command/commands/BanCommand.java b/src/main/java/emu/grasscutter/command/commands/BanCommand.java index 6b4dd9766..6f0b0abec 100644 --- a/src/main/java/emu/grasscutter/command/commands/BanCommand.java +++ b/src/main/java/emu/grasscutter/command/commands/BanCommand.java @@ -10,8 +10,7 @@ import emu.grasscutter.server.game.GameSession; @Command( label = "ban", - usage = "ban <@player> [time] [reason]", - description = "commands.ban.description", + usage = {"[