diff --git a/src/main/java/emu/grasscutter/Grasscutter.java b/src/main/java/emu/grasscutter/Grasscutter.java index f4240c565..1c9c40506 100644 --- a/src/main/java/emu/grasscutter/Grasscutter.java +++ b/src/main/java/emu/grasscutter/Grasscutter.java @@ -1,322 +1,324 @@ -package emu.grasscutter; - -import static emu.grasscutter.config.Configuration.SERVER; -import static emu.grasscutter.utils.Language.translate; - -import ch.qos.logback.classic.Level; -import ch.qos.logback.classic.Logger; -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.plugin.PluginManager; -import emu.grasscutter.plugin.api.ServerHook; -import emu.grasscutter.scripts.ScriptLoader; -import emu.grasscutter.server.game.GameServer; -import emu.grasscutter.server.http.HttpServer; -import emu.grasscutter.server.http.dispatch.DispatchHandler; -import emu.grasscutter.server.http.dispatch.RegionHandler; -import emu.grasscutter.server.http.documentation.DocumentationServerHandler; -import emu.grasscutter.server.http.handlers.AnnouncementsHandler; -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.*; -import java.io.File; -import java.io.FileWriter; -import java.io.IOError; -import java.io.IOException; -import java.util.Calendar; -import javax.annotation.Nullable; -import lombok.Getter; -import lombok.Setter; -import org.jline.reader.EndOfFileException; -import org.jline.reader.LineReader; -import org.jline.reader.LineReaderBuilder; -import org.jline.reader.UserInterruptException; -import org.jline.terminal.Terminal; -import org.jline.terminal.TerminalBuilder; -import org.reflections.Reflections; -import org.slf4j.LoggerFactory; - -public final class Grasscutter { - public static final File configFile = new File("./config.json"); - public static final Reflections reflector = new Reflections("emu.grasscutter"); - @Getter private static final Logger logger = (Logger) LoggerFactory.getLogger(Grasscutter.class); - - @Getter public static ConfigContainer config; - - @Getter @Setter private static Language language; - @Getter @Setter private static String preferredLanguage; - - @Getter private static int currentDayOfWeek; - @Setter private static ServerRunMode runModeOverride = null; // Config override for run mode - - @Getter private static HttpServer httpServer; - @Getter private static GameServer gameServer; - @Getter private static PluginManager pluginManager; - @Getter private static CommandMap commandMap; - - @Getter @Setter private static AuthenticationSystem authenticationSystem; - @Getter @Setter private static PermissionHandler permissionHandler; - - private static LineReader consoleLineReader = null; - - static { - // Declare logback configuration. - System.setProperty("logback.configurationFile", "src/main/resources/logback.xml"); - - // Disable the MongoDB logger. - var mongoLogger = (Logger) LoggerFactory.getLogger("org.mongodb.driver"); - mongoLogger.setLevel(Level.OFF); - - // Load server configuration. - Grasscutter.loadConfig(); - // Attempt to update configuration. - ConfigContainer.updateConfig(); - - // Load translation files. - Grasscutter.loadLanguage(); - - // Check server structure. - Utils.startupCheck(); - } - - public static void main(String[] args) throws Exception { - Crypto.loadKeys(); // Load keys from buffers. - - // Parse start-up arguments. - if (StartupArguments.parse(args)) { - System.exit(0); // Exit early. - } - - // Create command map. - commandMap = new CommandMap(true); - - // Initialize server. - logger.info(translate("messages.status.starting")); - logger.info(translate("messages.status.game_version", GameConstants.VERSION)); - logger.info(translate("messages.status.version", BuildConfig.VERSION, BuildConfig.GIT_HASH)); - - // Load all resources. - Grasscutter.updateDayOfWeek(); - ResourceLoader.loadAll(); - ScriptLoader.init(); - - // Generate handbooks. - Tools.createGmHandbooks(false); - - // Initialize database. - DatabaseManager.initialize(); - - // Initialize the default systems. - authenticationSystem = new DefaultAuthentication(); - permissionHandler = new DefaultPermissionHandler(); - - // Create server instances. - httpServer = new HttpServer(); - gameServer = new GameServer(); - // Create a server hook instance with both servers. - new ServerHook(gameServer, httpServer); - - // Create plugin manager instance. - pluginManager = new PluginManager(); - // Add HTTP routes after loading plugins. - httpServer.addRouter(HttpServer.UnhandledRequestRouter.class); - httpServer.addRouter(HttpServer.DefaultRequestRouter.class); - httpServer.addRouter(RegionHandler.class); - httpServer.addRouter(LogHandler.class); - httpServer.addRouter(GenericHandler.class); - httpServer.addRouter(AnnouncementsHandler.class); - httpServer.addRouter(DispatchHandler.class); - httpServer.addRouter(GachaHandler.class); - httpServer.addRouter(DocumentationServerHandler.class); - - // Start servers. - var runMode = Grasscutter.getRunMode(); - if (runMode == ServerRunMode.HYBRID) { - httpServer.start(); - gameServer.start(); - } else if (runMode == ServerRunMode.DISPATCH_ONLY) { - httpServer.start(); - } else if (runMode == ServerRunMode.GAME_ONLY) { - gameServer.start(); - } else { - logger.error(translate("messages.status.run_mode_error", runMode)); - logger.error(translate("messages.status.run_mode_help")); - logger.error(translate("messages.status.shutdown")); - System.exit(1); - } - - // Enable all plugins. - pluginManager.enablePlugins(); - - // Hook into shutdown event. - Runtime.getRuntime().addShutdownHook(new Thread(Grasscutter::onShutdown)); - - // Open console. - Grasscutter.startConsole(); - } - - /** Server shutdown event. */ - private static void onShutdown() { - // Disable all plugins. - if (pluginManager != null) pluginManager.disablePlugins(); - } - - /* - * Methods for the language system component. - */ - - public static void loadLanguage() { - var locale = config.language.language; - language = Language.getLanguage(Utils.getLanguageCode(locale)); - } - - /* - * Methods for the configuration system component. - */ - - /** Attempts to load the configuration from a file. */ - public static void loadConfig() { - // Check if config.json exists. If not, we generate a new config. - if (!configFile.exists()) { - getLogger().info("config.json could not be found. Generating a default configuration ..."); - config = new ConfigContainer(); - Grasscutter.saveConfig(config); - return; - } - - // If the file already exists, we attempt to load it. - try { - config = JsonUtils.loadToClass(configFile.toPath(), ConfigContainer.class); - } catch (Exception exception) { - getLogger() - .error( - "There was an error while trying to load the configuration from config.json. Please make sure that there are no syntax errors. If you want to start with a default configuration, delete your existing config.json."); - System.exit(1); - } - } - - /** - * Saves the provided server configuration. - * - * @param config The configuration to save, or null for a new one. - */ - public static void saveConfig(@Nullable ConfigContainer config) { - if (config == null) config = new ConfigContainer(); - - try (FileWriter file = new FileWriter(configFile)) { - file.write(JsonUtils.encode(config)); - } catch (IOException ignored) { - logger.error("Unable to write to config file."); - } catch (Exception e) { - logger.error("Unable to save config file.", e); - } - } - - /* - * Getters for the various server components. - */ - - public static Language getLanguage(String langCode) { - return Language.getLanguage(langCode); - } - - public static ServerRunMode getRunMode() { - return Grasscutter.runModeOverride != null ? Grasscutter.runModeOverride : SERVER.runMode; - } - - public static LineReader getConsole() { - if (consoleLineReader == null) { - Terminal terminal = null; - try { - terminal = TerminalBuilder.builder().jna(true).build(); - } catch (Exception e) { - try { - // Fallback to a dumb jline terminal. - terminal = TerminalBuilder.builder().dumb(true).build(); - } catch (Exception ignored) { - // When dumb is true, build() never throws. - } - } - - consoleLineReader = LineReaderBuilder.builder().terminal(terminal).build(); - } - - return consoleLineReader; - } - - /* - * Utility methods. - */ - - public static void updateDayOfWeek() { - Calendar calendar = Calendar.getInstance(); - Grasscutter.currentDayOfWeek = calendar.get(Calendar.DAY_OF_WEEK); - logger.debug("Set day of week to " + currentDayOfWeek); - } - - public static void startConsole() { - // Console should not start in dispatch only mode. - if (SERVER.runMode == ServerRunMode.DISPATCH_ONLY) { - logger.info(translate("messages.dispatch.no_commands_error")); - return; - } else { - logger.info(translate("messages.status.done")); - } - - String input = null; - var isLastInterrupted = false; - while (config.server.game.enableConsole) { - try { - input = consoleLineReader.readLine("> "); - } catch (UserInterruptException e) { - if (!isLastInterrupted) { - isLastInterrupted = true; - logger.info("Press Ctrl-C again to shutdown."); - continue; - } else { - Runtime.getRuntime().exit(0); - } - } catch (EndOfFileException e) { - logger.info("EOF detected."); - continue; - } catch (IOError e) { - logger.error("An IO error occurred while trying to read from console.", e); - return; - } - - isLastInterrupted = false; - - try { - commandMap.invoke(null, null, input); - } catch (Exception e) { - logger.error(translate("messages.game.command_error"), e); - } - } - } - - /* - * Enums for the configuration. - */ - - public enum ServerRunMode { - HYBRID, - DISPATCH_ONLY, - GAME_ONLY - } - - public enum ServerDebugMode { - ALL, - MISSING, - WHITELIST, - BLACKLIST, - NONE - } -} +package emu.grasscutter; + +import static emu.grasscutter.config.Configuration.SERVER; +import static emu.grasscutter.utils.Language.translate; + +import ch.qos.logback.classic.Level; +import ch.qos.logback.classic.Logger; +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.plugin.PluginManager; +import emu.grasscutter.plugin.api.ServerHook; +import emu.grasscutter.scripts.ScriptLoader; +import emu.grasscutter.server.game.GameServer; +import emu.grasscutter.server.http.HttpServer; +import emu.grasscutter.server.http.dispatch.DispatchHandler; +import emu.grasscutter.server.http.dispatch.RegionHandler; +import emu.grasscutter.server.http.documentation.DocumentationServerHandler; +import emu.grasscutter.server.http.documentation.HandbookHandler; +import emu.grasscutter.server.http.handlers.AnnouncementsHandler; +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.*; +import java.io.File; +import java.io.FileWriter; +import java.io.IOError; +import java.io.IOException; +import java.util.Calendar; +import javax.annotation.Nullable; +import lombok.Getter; +import lombok.Setter; +import org.jline.reader.EndOfFileException; +import org.jline.reader.LineReader; +import org.jline.reader.LineReaderBuilder; +import org.jline.reader.UserInterruptException; +import org.jline.terminal.Terminal; +import org.jline.terminal.TerminalBuilder; +import org.reflections.Reflections; +import org.slf4j.LoggerFactory; + +public final class Grasscutter { + public static final File configFile = new File("./config.json"); + public static final Reflections reflector = new Reflections("emu.grasscutter"); + @Getter private static final Logger logger = (Logger) LoggerFactory.getLogger(Grasscutter.class); + + @Getter public static ConfigContainer config; + + @Getter @Setter private static Language language; + @Getter @Setter private static String preferredLanguage; + + @Getter private static int currentDayOfWeek; + @Setter private static ServerRunMode runModeOverride = null; // Config override for run mode + + @Getter private static HttpServer httpServer; + @Getter private static GameServer gameServer; + @Getter private static PluginManager pluginManager; + @Getter private static CommandMap commandMap; + + @Getter @Setter private static AuthenticationSystem authenticationSystem; + @Getter @Setter private static PermissionHandler permissionHandler; + + private static LineReader consoleLineReader = null; + + static { + // Declare logback configuration. + System.setProperty("logback.configurationFile", "src/main/resources/logback.xml"); + + // Disable the MongoDB logger. + var mongoLogger = (Logger) LoggerFactory.getLogger("org.mongodb.driver"); + mongoLogger.setLevel(Level.OFF); + + // Load server configuration. + Grasscutter.loadConfig(); + // Attempt to update configuration. + ConfigContainer.updateConfig(); + + // Load translation files. + Grasscutter.loadLanguage(); + + // Check server structure. + Utils.startupCheck(); + } + + public static void main(String[] args) throws Exception { + Crypto.loadKeys(); // Load keys from buffers. + + // Parse start-up arguments. + if (StartupArguments.parse(args)) { + System.exit(0); // Exit early. + } + + // Create command map. + commandMap = new CommandMap(true); + + // Initialize server. + logger.info(translate("messages.status.starting")); + logger.info(translate("messages.status.game_version", GameConstants.VERSION)); + logger.info(translate("messages.status.version", BuildConfig.VERSION, BuildConfig.GIT_HASH)); + + // Load all resources. + Grasscutter.updateDayOfWeek(); + ResourceLoader.loadAll(); + ScriptLoader.init(); + + // Generate handbooks. + Tools.createGmHandbooks(false); + + // Initialize database. + DatabaseManager.initialize(); + + // Initialize the default systems. + authenticationSystem = new DefaultAuthentication(); + permissionHandler = new DefaultPermissionHandler(); + + // Create server instances. + httpServer = new HttpServer(); + gameServer = new GameServer(); + // Create a server hook instance with both servers. + new ServerHook(gameServer, httpServer); + + // Create plugin manager instance. + pluginManager = new PluginManager(); + // Add HTTP routes after loading plugins. + httpServer.addRouter(HttpServer.UnhandledRequestRouter.class); + httpServer.addRouter(HttpServer.DefaultRequestRouter.class); + httpServer.addRouter(RegionHandler.class); + httpServer.addRouter(LogHandler.class); + httpServer.addRouter(GenericHandler.class); + httpServer.addRouter(AnnouncementsHandler.class); + httpServer.addRouter(DispatchHandler.class); + httpServer.addRouter(GachaHandler.class); + httpServer.addRouter(DocumentationServerHandler.class); + httpServer.addRouter(HandbookHandler.class); + + // Start servers. + var runMode = Grasscutter.getRunMode(); + if (runMode == ServerRunMode.HYBRID) { + httpServer.start(); + gameServer.start(); + } else if (runMode == ServerRunMode.DISPATCH_ONLY) { + httpServer.start(); + } else if (runMode == ServerRunMode.GAME_ONLY) { + gameServer.start(); + } else { + logger.error(translate("messages.status.run_mode_error", runMode)); + logger.error(translate("messages.status.run_mode_help")); + logger.error(translate("messages.status.shutdown")); + System.exit(1); + } + + // Enable all plugins. + pluginManager.enablePlugins(); + + // Hook into shutdown event. + Runtime.getRuntime().addShutdownHook(new Thread(Grasscutter::onShutdown)); + + // Open console. + Grasscutter.startConsole(); + } + + /** Server shutdown event. */ + private static void onShutdown() { + // Disable all plugins. + if (pluginManager != null) pluginManager.disablePlugins(); + } + + /* + * Methods for the language system component. + */ + + public static void loadLanguage() { + var locale = config.language.language; + language = Language.getLanguage(Utils.getLanguageCode(locale)); + } + + /* + * Methods for the configuration system component. + */ + + /** Attempts to load the configuration from a file. */ + public static void loadConfig() { + // Check if config.json exists. If not, we generate a new config. + if (!configFile.exists()) { + getLogger().info("config.json could not be found. Generating a default configuration ..."); + config = new ConfigContainer(); + Grasscutter.saveConfig(config); + return; + } + + // If the file already exists, we attempt to load it. + try { + config = JsonUtils.loadToClass(configFile.toPath(), ConfigContainer.class); + } catch (Exception exception) { + getLogger() + .error( + "There was an error while trying to load the configuration from config.json. Please make sure that there are no syntax errors. If you want to start with a default configuration, delete your existing config.json."); + System.exit(1); + } + } + + /** + * Saves the provided server configuration. + * + * @param config The configuration to save, or null for a new one. + */ + public static void saveConfig(@Nullable ConfigContainer config) { + if (config == null) config = new ConfigContainer(); + + try (FileWriter file = new FileWriter(configFile)) { + file.write(JsonUtils.encode(config)); + } catch (IOException ignored) { + logger.error("Unable to write to config file."); + } catch (Exception e) { + logger.error("Unable to save config file.", e); + } + } + + /* + * Getters for the various server components. + */ + + public static Language getLanguage(String langCode) { + return Language.getLanguage(langCode); + } + + public static ServerRunMode getRunMode() { + return Grasscutter.runModeOverride != null ? Grasscutter.runModeOverride : SERVER.runMode; + } + + public static LineReader getConsole() { + if (consoleLineReader == null) { + Terminal terminal = null; + try { + terminal = TerminalBuilder.builder().jna(true).build(); + } catch (Exception e) { + try { + // Fallback to a dumb jline terminal. + terminal = TerminalBuilder.builder().dumb(true).build(); + } catch (Exception ignored) { + // When dumb is true, build() never throws. + } + } + + consoleLineReader = LineReaderBuilder.builder().terminal(terminal).build(); + } + + return consoleLineReader; + } + + /* + * Utility methods. + */ + + public static void updateDayOfWeek() { + Calendar calendar = Calendar.getInstance(); + Grasscutter.currentDayOfWeek = calendar.get(Calendar.DAY_OF_WEEK); + logger.debug("Set day of week to " + currentDayOfWeek); + } + + public static void startConsole() { + // Console should not start in dispatch only mode. + if (SERVER.runMode == ServerRunMode.DISPATCH_ONLY) { + logger.info(translate("messages.dispatch.no_commands_error")); + return; + } else { + logger.info(translate("messages.status.done")); + } + + String input = null; + var isLastInterrupted = false; + while (config.server.game.enableConsole) { + try { + input = consoleLineReader.readLine("> "); + } catch (UserInterruptException e) { + if (!isLastInterrupted) { + isLastInterrupted = true; + logger.info("Press Ctrl-C again to shutdown."); + continue; + } else { + Runtime.getRuntime().exit(0); + } + } catch (EndOfFileException e) { + logger.info("EOF detected."); + continue; + } catch (IOError e) { + logger.error("An IO error occurred while trying to read from console.", e); + return; + } + + isLastInterrupted = false; + + try { + commandMap.invoke(null, null, input); + } catch (Exception e) { + logger.error(translate("messages.game.command_error"), e); + } + } + } + + /* + * Enums for the configuration. + */ + + public enum ServerRunMode { + HYBRID, + DISPATCH_ONLY, + GAME_ONLY + } + + public enum ServerDebugMode { + ALL, + MISSING, + WHITELIST, + BLACKLIST, + NONE + } +} diff --git a/src/main/java/emu/grasscutter/config/ConfigContainer.java b/src/main/java/emu/grasscutter/config/ConfigContainer.java index a989b3a5f..e9d19128f 100644 --- a/src/main/java/emu/grasscutter/config/ConfigContainer.java +++ b/src/main/java/emu/grasscutter/config/ConfigContainer.java @@ -227,6 +227,8 @@ public class ConfigContainer { public ResinOptions resinOptions = new ResinOptions(); public Rates rates = new Rates(); + public HandbookOptions handbook = new HandbookOptions(); + public static class InventoryLimits { public int weapons = 2000; public int relics = 2000; @@ -251,6 +253,13 @@ public class ConfigContainer { public int cap = 160; public int rechargeTime = 480; } + + public static class HandbookOptions { + public boolean enable = false; + public boolean allowCommands = true; + public int maxRequests = 10; + public int maxEntities = 100; + } } public static class JoinOptions { diff --git a/src/main/java/emu/grasscutter/server/http/documentation/HandbookHandler.java b/src/main/java/emu/grasscutter/server/http/documentation/HandbookHandler.java new file mode 100644 index 000000000..c9cb60364 --- /dev/null +++ b/src/main/java/emu/grasscutter/server/http/documentation/HandbookHandler.java @@ -0,0 +1,110 @@ +package emu.grasscutter.server.http.documentation; + +import emu.grasscutter.Grasscutter; +import emu.grasscutter.Grasscutter.ServerRunMode; +import emu.grasscutter.data.GameData; +import emu.grasscutter.game.avatar.Avatar; +import emu.grasscutter.server.http.Router; +import emu.grasscutter.utils.FileUtils; +import emu.grasscutter.utils.objects.HandbookBody; +import io.javalin.Javalin; +import io.javalin.http.Context; + +/** Handles requests for the new GM Handbook. */ +public final class HandbookHandler implements Router { + private final byte[] handbook; + private final boolean serve; + + /** + * Constructor for the handbook router. + * Enables serving the handbook if the handbook file is found. + */ + public HandbookHandler() { + this.handbook = FileUtils.readResource("/handbook.html"); + this.serve = this.handbook.length > 0; + } + + @Override + public void applyRoutes(Javalin javalin) { + // The handbook content. (built from src/handbook) + javalin.get("/handbook", this::serveHandbook); + + // Handbook control routes. + javalin.post("/handbook/avatar", this::grantAvatar); + } + + /** + * @return True if the server can execute handbook commands. + */ + private boolean controlSupported() { + return Grasscutter.getRunMode() == ServerRunMode.HYBRID; + } + + /** + * Serves the handbook if it is found. + * + * @route GET /handbook + * @param ctx The Javalin request context. + */ + private void serveHandbook(Context ctx) { + if (!this.serve) { + ctx.status(500).result("Handbook not found."); + } else { + ctx.contentType("text/html").result(this.handbook); + } + } + + /** + * Grants the avatar to the user. + * + * @route POST /handbook/avatar + * @param ctx The Javalin request context. + */ + private void grantAvatar(Context ctx) { + if (!this.controlSupported()) { + ctx.status(500).result("Handbook control not supported."); + return; + } + + // Parse the request body into a class. + var request = ctx.bodyAsClass(HandbookBody.GrantAvatar.class); + // Validate the request. + if (request.getPlayer() == null || request.getAvatar() == null) { + ctx.status(400).result("Invalid request."); + return; + } + + try { + // Parse the requested player. + var playerId = Integer.parseInt(request.getPlayer()); + var player = Grasscutter.getGameServer().getPlayerByUid(playerId); + + // Parse the requested avatar. + var avatarId = Integer.parseInt(request.getAvatar()); + var avatarData = GameData.getAvatarDataMap().get(avatarId); + + // Validate the request. + if (player == null || avatarData == null) { + ctx.status(400).result("Invalid player UID or avatar ID."); + return; + } + + // Create the new avatar. + var avatar = new Avatar(avatarData); + avatar.setLevel(request.getLevel()); + avatar.setPromoteLevel(Avatar.getMinPromoteLevel(avatar.getLevel())); + avatar.getSkillDepot().getSkillsAndEnergySkill().forEach(id -> + avatar.setSkillLevel(id, request.getTalentLevels())); + avatar.forceConstellationLevel(request.getConstellations()); + avatar.recalcStats(true); avatar.save(); + + player.addAvatar(avatar); // Add the avatar. + ctx.json(HandbookBody.Response.builder() + .status(200) + .message("Avatar granted.") + .build()); + } catch (NumberFormatException ignored) { + ctx.status(500).result("Invalid player UID or avatar ID."); + } + } +} diff --git a/src/main/java/emu/grasscutter/utils/FileUtils.java b/src/main/java/emu/grasscutter/utils/FileUtils.java index c6d00402d..292776098 100644 --- a/src/main/java/emu/grasscutter/utils/FileUtils.java +++ b/src/main/java/emu/grasscutter/utils/FileUtils.java @@ -1,265 +1,265 @@ -package emu.grasscutter.utils; - -import emu.grasscutter.Grasscutter; -import java.io.File; -import java.io.IOException; -import java.io.InputStream; -import java.net.URISyntaxException; -import java.nio.charset.StandardCharsets; -import java.nio.file.FileSystem; -import java.nio.file.FileSystems; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.List; -import java.util.Map; -import java.util.stream.Collectors; -import java.util.stream.Stream; -import lombok.val; - -public final class FileUtils { - private static final Path DATA_DEFAULT_PATH; - private static final Path DATA_USER_PATH = Path.of(Grasscutter.config.folderStructure.data); - private static final Path PACKETS_PATH = Path.of(Grasscutter.config.folderStructure.packets); - private static final Path PLUGINS_PATH = Path.of(Grasscutter.config.folderStructure.plugins); - private static final Path RESOURCES_PATH; - private static final Path SCRIPTS_PATH; - private static final String[] TSJ_JSON_TSV = {"tsj", "json", "tsv"}; - - static { - FileSystem fs = null; - Path path = null; - // Setup access to jar resources - try { - var uri = Grasscutter.class.getResource("/defaults/data").toURI(); - switch (uri.getScheme()) { - case "jar": // When running normally, as a jar - case "zip": // Honestly I have no idea what setup would result in this, but this should work - // regardless - fs = - FileSystems.newFileSystem( - uri, - Map.of()); // Have to mount zip filesystem. This leaks, but we want to keep it - // forever anyway. - // Fall-through - case "file": // When running in an IDE - path = Path.of(uri); // Can access directly - break; - default: - Grasscutter.getLogger() - .error("Invalid URI scheme for class resources: " + uri.getScheme()); - break; - } - } catch (URISyntaxException | IOException e) { - // Failed to load this jar. How? - Grasscutter.getLogger().error("Failed to load jar?!"); - } finally { - DATA_DEFAULT_PATH = path; - Grasscutter.getLogger().debug("Setting path for default data: " + path.toAbsolutePath()); - } - - // Setup Resources path - final String resources = Grasscutter.config.folderStructure.resources; - fs = null; - path = Path.of(resources); - if (resources.endsWith( - ".zip")) { // Would be nice to support .tar.gz too at some point, but it doesn't come for - // free in Java - try { - fs = FileSystems.newFileSystem(path); - } catch (IOException e) { - Grasscutter.getLogger().error("Failed to load resources zip \"" + resources + "\""); - } - } - - if (fs != null) { - var root = fs.getPath(""); - try (Stream pathStream = - Files.find( - root, - 3, - (p, a) -> { - var filename = p.getFileName(); - if (filename == null) return false; - return filename.toString().equals("ExcelBinOutput"); - })) { - var excelBinOutput = pathStream.findFirst(); - if (excelBinOutput.isPresent()) { - path = excelBinOutput.get().getParent(); - if (path == null) path = root; - Grasscutter.getLogger() - .debug("Resources will be loaded from \"" + resources + "/" + path + "\""); - } else { - Grasscutter.getLogger() - .error("Failed to find ExcelBinOutput in resources zip \"" + resources + "\""); - } - } catch (IOException e) { - Grasscutter.getLogger().error("Failed to scan resources zip \"" + resources + "\""); - } - } - RESOURCES_PATH = path; - - // Setup Scripts path - final String scripts = Grasscutter.config.folderStructure.scripts; - SCRIPTS_PATH = - (scripts.startsWith("resources:")) - ? RESOURCES_PATH.resolve(scripts.substring("resources:".length())) - : Path.of(scripts); - } - - /* Apply after initialization. */ - private static final Path[] DATA_PATHS = {DATA_USER_PATH, DATA_DEFAULT_PATH}; - - public static Path getDataPathTsjJsonTsv(String filename) { - return getDataPathTsjJsonTsv(filename, true); - } - - public static Path getDataPathTsjJsonTsv(String filename, boolean fallback) { - val name = getFilenameWithoutExtension(filename); - for (val data_path : DATA_PATHS) { - for (val ext : TSJ_JSON_TSV) { - val path = data_path.resolve(name + "." + ext); - if (Files.exists(path)) return path; - } - } - return fallback - ? DATA_USER_PATH.resolve(name + ".tsj") - : null; // Maybe they want to write to a new file - } - - public static Path getDataPath(String path) { - Path userPath = DATA_USER_PATH.resolve(path); - if (Files.exists(userPath)) return userPath; - Path defaultPath = DATA_DEFAULT_PATH.resolve(path); - if (Files.exists(defaultPath)) return defaultPath; - return userPath; // Maybe they want to write to a new file - } - - public static Path getDataUserPath(String path) { - return DATA_USER_PATH.resolve(path); - } - - public static Path getPacketPath(String path) { - return PACKETS_PATH.resolve(path); - } - - public static Path getPluginPath(String path) { - return PLUGINS_PATH.resolve(path); - } - - public static Path getResourcePath(String path) { - return RESOURCES_PATH.resolve(path); - } - - public static Path getExcelPath(String filename) { - return getTsjJsonTsv(RESOURCES_PATH.resolve("ExcelBinOutput"), filename); - } - - // Gets path of a resource. - // If multiple formats of it exist, priority is TSJ > JSON > TSV - // If none exist, return the TSJ path, in case it wants to create a file - public static Path getTsjJsonTsv(Path root, String filename) { - val name = getFilenameWithoutExtension(filename); - for (val ext : TSJ_JSON_TSV) { - val path = root.resolve(name + "." + ext); - if (Files.exists(path)) return path; - } - return root.resolve(name + ".tsj"); - } - - public static Path getScriptPath(String path) { - return SCRIPTS_PATH.resolve(path); - } - - public static void write(String dest, byte[] bytes) { - Path path = Path.of(dest); - - try { - Files.write(path, bytes); - } catch (IOException e) { - Grasscutter.getLogger().warn("Failed to write file: " + dest); - } - } - - public static byte[] read(String dest) { - return read(Path.of(dest)); - } - - public static byte[] read(Path path) { - try { - return Files.readAllBytes(path); - } catch (IOException e) { - Grasscutter.getLogger().warn("Failed to read file: " + path); - } - - return new byte[0]; - } - - public static InputStream readResourceAsStream(String resourcePath) { - return Grasscutter.class.getResourceAsStream(resourcePath); - } - - public static byte[] readResource(String resourcePath) { - try (InputStream is = Grasscutter.class.getResourceAsStream(resourcePath)) { - return is.readAllBytes(); - } catch (Exception exception) { - Grasscutter.getLogger().warn("Failed to read resource: " + resourcePath); - exception.printStackTrace(); - } - - return new byte[0]; - } - - public static byte[] read(File file) { - return read(file.getPath()); - } - - public static void copyResource(String resourcePath, String destination) { - try { - byte[] resource = FileUtils.readResource(resourcePath); - FileUtils.write(destination, resource); - } catch (Exception exception) { - Grasscutter.getLogger().warn("Failed to copy resource: " + resourcePath + "\n" + exception); - } - } - - @Deprecated // Misnamed legacy function - public static String getFilenameWithoutPath(String filename) { - return getFilenameWithoutExtension(filename); - } - - public static String getFilenameWithoutExtension(String filename) { - int i = filename.lastIndexOf("."); - return (i < 0) ? filename : filename.substring(0, i); - } - - public static String getFileExtension(Path path) { - val filename = path.toString(); - int i = filename.lastIndexOf("."); - return (i < 0) ? "" : filename.substring(i + 1); - } - - public static List getPathsFromResource(String folder) throws URISyntaxException { - try { - // file walks JAR - return Files.walk(Path.of(Grasscutter.class.getResource(folder).toURI())) - .filter(Files::isRegularFile) - .collect(Collectors.toList()); - } catch (IOException e) { - // Eclipse puts resources in its bin folder - try { - return Files.walk(Path.of(System.getProperty("user.dir"), folder)) - .filter(Files::isRegularFile) - .collect(Collectors.toList()); - } catch (IOException ignored) { - return null; - } - } - } - - @SuppressWarnings("ResultOfMethodCallIgnored") - public static String readToString(InputStream file) throws IOException { - byte[] content = file.readAllBytes(); - - return new String(content, StandardCharsets.UTF_8); - } -} +package emu.grasscutter.utils; + +import emu.grasscutter.Grasscutter; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.net.URISyntaxException; +import java.nio.charset.StandardCharsets; +import java.nio.file.FileSystem; +import java.nio.file.FileSystems; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import lombok.val; + +public final class FileUtils { + private static final Path DATA_DEFAULT_PATH; + private static final Path DATA_USER_PATH = Path.of(Grasscutter.config.folderStructure.data); + private static final Path PACKETS_PATH = Path.of(Grasscutter.config.folderStructure.packets); + private static final Path PLUGINS_PATH = Path.of(Grasscutter.config.folderStructure.plugins); + private static final Path RESOURCES_PATH; + private static final Path SCRIPTS_PATH; + private static final String[] TSJ_JSON_TSV = {"tsj", "json", "tsv"}; + + static { + FileSystem fs = null; + Path path = null; + // Setup access to jar resources + try { + var uri = Grasscutter.class.getResource("/defaults/data").toURI(); + switch (uri.getScheme()) { + case "jar": // When running normally, as a jar + case "zip": // Honestly I have no idea what setup would result in this, but this should work + // regardless + fs = + FileSystems.newFileSystem( + uri, + Map.of()); // Have to mount zip filesystem. This leaks, but we want to keep it + // forever anyway. + // Fall-through + case "file": // When running in an IDE + path = Path.of(uri); // Can access directly + break; + default: + Grasscutter.getLogger() + .error("Invalid URI scheme for class resources: " + uri.getScheme()); + break; + } + } catch (URISyntaxException | IOException e) { + // Failed to load this jar. How? + Grasscutter.getLogger().error("Failed to load jar?!"); + } finally { + DATA_DEFAULT_PATH = path; + Grasscutter.getLogger().debug("Setting path for default data: " + path.toAbsolutePath()); + } + + // Setup Resources path + final String resources = Grasscutter.config.folderStructure.resources; + fs = null; + path = Path.of(resources); + if (resources.endsWith( + ".zip")) { // Would be nice to support .tar.gz too at some point, but it doesn't come for + // free in Java + try { + fs = FileSystems.newFileSystem(path); + } catch (IOException e) { + Grasscutter.getLogger().error("Failed to load resources zip \"" + resources + "\""); + } + } + + if (fs != null) { + var root = fs.getPath(""); + try (Stream pathStream = + Files.find( + root, + 3, + (p, a) -> { + var filename = p.getFileName(); + if (filename == null) return false; + return filename.toString().equals("ExcelBinOutput"); + })) { + var excelBinOutput = pathStream.findFirst(); + if (excelBinOutput.isPresent()) { + path = excelBinOutput.get().getParent(); + if (path == null) path = root; + Grasscutter.getLogger() + .debug("Resources will be loaded from \"" + resources + "/" + path + "\""); + } else { + Grasscutter.getLogger() + .error("Failed to find ExcelBinOutput in resources zip \"" + resources + "\""); + } + } catch (IOException e) { + Grasscutter.getLogger().error("Failed to scan resources zip \"" + resources + "\""); + } + } + RESOURCES_PATH = path; + + // Setup Scripts path + final String scripts = Grasscutter.config.folderStructure.scripts; + SCRIPTS_PATH = + (scripts.startsWith("resources:")) + ? RESOURCES_PATH.resolve(scripts.substring("resources:".length())) + : Path.of(scripts); + } + + /* Apply after initialization. */ + private static final Path[] DATA_PATHS = {DATA_USER_PATH, DATA_DEFAULT_PATH}; + + public static Path getDataPathTsjJsonTsv(String filename) { + return getDataPathTsjJsonTsv(filename, true); + } + + public static Path getDataPathTsjJsonTsv(String filename, boolean fallback) { + val name = getFilenameWithoutExtension(filename); + for (val data_path : DATA_PATHS) { + for (val ext : TSJ_JSON_TSV) { + val path = data_path.resolve(name + "." + ext); + if (Files.exists(path)) return path; + } + } + return fallback + ? DATA_USER_PATH.resolve(name + ".tsj") + : null; // Maybe they want to write to a new file + } + + public static Path getDataPath(String path) { + Path userPath = DATA_USER_PATH.resolve(path); + if (Files.exists(userPath)) return userPath; + Path defaultPath = DATA_DEFAULT_PATH.resolve(path); + if (Files.exists(defaultPath)) return defaultPath; + return userPath; // Maybe they want to write to a new file + } + + public static Path getDataUserPath(String path) { + return DATA_USER_PATH.resolve(path); + } + + public static Path getPacketPath(String path) { + return PACKETS_PATH.resolve(path); + } + + public static Path getPluginPath(String path) { + return PLUGINS_PATH.resolve(path); + } + + public static Path getResourcePath(String path) { + return RESOURCES_PATH.resolve(path); + } + + public static Path getExcelPath(String filename) { + return getTsjJsonTsv(RESOURCES_PATH.resolve("ExcelBinOutput"), filename); + } + + // Gets path of a resource. + // If multiple formats of it exist, priority is TSJ > JSON > TSV + // If none exist, return the TSJ path, in case it wants to create a file + public static Path getTsjJsonTsv(Path root, String filename) { + val name = getFilenameWithoutExtension(filename); + for (val ext : TSJ_JSON_TSV) { + val path = root.resolve(name + "." + ext); + if (Files.exists(path)) return path; + } + return root.resolve(name + ".tsj"); + } + + public static Path getScriptPath(String path) { + return SCRIPTS_PATH.resolve(path); + } + + public static void write(String dest, byte[] bytes) { + Path path = Path.of(dest); + + try { + Files.write(path, bytes); + } catch (IOException e) { + Grasscutter.getLogger().warn("Failed to write file: " + dest); + } + } + + public static byte[] read(String dest) { + return read(Path.of(dest)); + } + + public static byte[] read(Path path) { + try { + return Files.readAllBytes(path); + } catch (IOException e) { + Grasscutter.getLogger().warn("Failed to read file: " + path); + } + + return new byte[0]; + } + + public static InputStream readResourceAsStream(String resourcePath) { + return Grasscutter.class.getResourceAsStream(resourcePath); + } + + public static byte[] readResource(String resourcePath) { + try (InputStream is = Grasscutter.class.getResourceAsStream(resourcePath)) { + return is.readAllBytes(); + } catch (Exception exception) { + Grasscutter.getLogger().warn("Failed to read resource: " + resourcePath); + Grasscutter.getLogger().debug("Failed to load resource: " + resourcePath, exception); + } + + return new byte[0]; + } + + public static byte[] read(File file) { + return read(file.getPath()); + } + + public static void copyResource(String resourcePath, String destination) { + try { + byte[] resource = FileUtils.readResource(resourcePath); + FileUtils.write(destination, resource); + } catch (Exception exception) { + Grasscutter.getLogger().warn("Failed to copy resource: " + resourcePath + "\n" + exception); + } + } + + @Deprecated // Misnamed legacy function + public static String getFilenameWithoutPath(String filename) { + return getFilenameWithoutExtension(filename); + } + + public static String getFilenameWithoutExtension(String filename) { + int i = filename.lastIndexOf("."); + return (i < 0) ? filename : filename.substring(0, i); + } + + public static String getFileExtension(Path path) { + val filename = path.toString(); + int i = filename.lastIndexOf("."); + return (i < 0) ? "" : filename.substring(i + 1); + } + + public static List getPathsFromResource(String folder) throws URISyntaxException { + try { + // file walks JAR + return Files.walk(Path.of(Grasscutter.class.getResource(folder).toURI())) + .filter(Files::isRegularFile) + .collect(Collectors.toList()); + } catch (IOException e) { + // Eclipse puts resources in its bin folder + try { + return Files.walk(Path.of(System.getProperty("user.dir"), folder)) + .filter(Files::isRegularFile) + .collect(Collectors.toList()); + } catch (IOException ignored) { + return null; + } + } + } + + @SuppressWarnings("ResultOfMethodCallIgnored") + public static String readToString(InputStream file) throws IOException { + byte[] content = file.readAllBytes(); + + return new String(content, StandardCharsets.UTF_8); + } +} diff --git a/src/main/java/emu/grasscutter/utils/objects/HandbookBody.java b/src/main/java/emu/grasscutter/utils/objects/HandbookBody.java new file mode 100644 index 000000000..1215864ab --- /dev/null +++ b/src/main/java/emu/grasscutter/utils/objects/HandbookBody.java @@ -0,0 +1,23 @@ +package emu.grasscutter.utils.objects; + +import lombok.Builder; +import lombok.Getter; + +/** HTTP request object for handbook controls. */ +public interface HandbookBody { + @Builder + class Response { + private int status; + private String message; + } + + @Getter + class GrantAvatar { + private String player; // Parse into online player ID. + private String avatar; // Parse into avatar ID. + + private int level = 90; // Range between 1 - 90. + private int constellations = 6; // Range between 0 - 6. + private int talentLevels = 10; // Range between 1 - 15. + } +}