From ea670cc358ba3c96ac66a8133e8b48c5f7540428 Mon Sep 17 00:00:00 2001 From: Jonathan Coates Date: Sun, 24 Nov 2024 21:53:04 +0000 Subject: [PATCH] Try to unify our config files a bit I've tried so many rewrites of the config system over the last few months, in an attempt to get started on #1727. All of them stink, so this is an attempt to apply some of the cleanup. - Move some of the common logic into ConfigFile. This means we now store more information ourselves for Forge, rather than reading it out of the ForgeConfigSpec. - Don't include the Range/Allowed keys in the translation key. This was mostly there because of how we read comments from Forge, but it never made much sense. - Remove our separate Trie structure, and just encode the tree as part of the children of a Group. --- .../assets/computercraft/lang/en_us.json | 56 +++---- .../shared/config/ConfigFile.java | 138 ++++++++++++++--- .../shared/config/ConfigSpec.java | 12 +- .../computercraft/shared/util/Trie.java | 46 ------ .../shared/platform/FabricConfigFile.java | 146 +++++------------- .../shared/platform/ForgeConfigFile.java | 120 ++++---------- 6 files changed, 217 insertions(+), 301 deletions(-) delete mode 100644 projects/common/src/main/java/dan200/computercraft/shared/util/Trie.java diff --git a/projects/common/src/generated/resources/assets/computercraft/lang/en_us.json b/projects/common/src/generated/resources/assets/computercraft/lang/en_us.json index 0802004c44..3b9807e286 100644 --- a/projects/common/src/generated/resources/assets/computercraft/lang/en_us.json +++ b/projects/common/src/generated/resources/assets/computercraft/lang/en_us.json @@ -83,35 +83,35 @@ "gui.computercraft.config.disabled_generic_methods.tooltip": "A list of generic methods or method sources to disable. Generic methods are\nmethods added to a block/block entity when there is no explicit peripheral\nprovider. This includes inventory methods (i.e. inventory.getItemDetail,\ninventory.pushItems), and (if on Forge), the fluid_storage and energy_storage\nmethods.\nMethods in this list can either be a whole group of methods (computercraft:inventory)\nor a single method (computercraft:inventory#pushItems).\n", "gui.computercraft.config.execution": "Execution", "gui.computercraft.config.execution.computer_threads": "Computer threads", - "gui.computercraft.config.execution.computer_threads.tooltip": "Set the number of threads computers can run on. A higher number means more\ncomputers can run at once, but may induce lag. Please note that some mods may\nnot work with a thread count higher than 1. Use with caution.\nRange: > 1", + "gui.computercraft.config.execution.computer_threads.tooltip": "Set the number of threads computers can run on. A higher number means more\ncomputers can run at once, but may induce lag. Please note that some mods may\nnot work with a thread count higher than 1. Use with caution.", "gui.computercraft.config.execution.max_main_computer_time": "Server tick computer time limit", - "gui.computercraft.config.execution.max_main_computer_time.tooltip": "The ideal maximum time a computer can execute for in a tick, in milliseconds.\nNote, we will quite possibly go over this limit, as there's no way to tell how\nlong a will take - this aims to be the upper bound of the average time.\nRange: > 1", + "gui.computercraft.config.execution.max_main_computer_time.tooltip": "The ideal maximum time a computer can execute for in a tick, in milliseconds.\nNote, we will quite possibly go over this limit, as there's no way to tell how\nlong a will take - this aims to be the upper bound of the average time.", "gui.computercraft.config.execution.max_main_global_time": "Server tick global time limit", - "gui.computercraft.config.execution.max_main_global_time.tooltip": "The maximum time that can be spent executing tasks in a single tick, in\nmilliseconds.\nNote, we will quite possibly go over this limit, as there's no way to tell how\nlong a will take - this aims to be the upper bound of the average time.\nRange: > 1", + "gui.computercraft.config.execution.max_main_global_time.tooltip": "The maximum time that can be spent executing tasks in a single tick, in\nmilliseconds.\nNote, we will quite possibly go over this limit, as there's no way to tell how\nlong a will take - this aims to be the upper bound of the average time.", "gui.computercraft.config.execution.tooltip": "Controls execution behaviour of computers. This is largely intended for\nfine-tuning servers, and generally shouldn't need to be touched.", "gui.computercraft.config.floppy_space_limit": "Floppy Disk space limit (bytes)", "gui.computercraft.config.floppy_space_limit.tooltip": "The disk space limit for floppy disks, in bytes.", "gui.computercraft.config.http": "HTTP", "gui.computercraft.config.http.bandwidth": "Bandwidth", "gui.computercraft.config.http.bandwidth.global_download": "Global download limit", - "gui.computercraft.config.http.bandwidth.global_download.tooltip": "The number of bytes which can be downloaded in a second. This is shared across all computers. (bytes/s).\nRange: > 1", + "gui.computercraft.config.http.bandwidth.global_download.tooltip": "The number of bytes which can be downloaded in a second. This is shared across all computers. (bytes/s).", "gui.computercraft.config.http.bandwidth.global_upload": "Global upload limit", - "gui.computercraft.config.http.bandwidth.global_upload.tooltip": "The number of bytes which can be uploaded in a second. This is shared across all computers. (bytes/s).\nRange: > 1", + "gui.computercraft.config.http.bandwidth.global_upload.tooltip": "The number of bytes which can be uploaded in a second. This is shared across all computers. (bytes/s).", "gui.computercraft.config.http.bandwidth.tooltip": "Limits bandwidth used by computers.", "gui.computercraft.config.http.enabled": "Enable the HTTP API", "gui.computercraft.config.http.enabled.tooltip": "Enable the \"http\" API on Computers. Disabling this also disables the \"pastebin\" and\n\"wget\" programs, that many users rely on. It's recommended to leave this on and use\nthe \"rules\" config option to impose more fine-grained control.", "gui.computercraft.config.http.max_requests": "Maximum concurrent requests", - "gui.computercraft.config.http.max_requests.tooltip": "The number of http requests a computer can make at one time. Additional requests\nwill be queued, and sent when the running requests have finished. Set to 0 for\nunlimited.\nRange: > 0", + "gui.computercraft.config.http.max_requests.tooltip": "The number of http requests a computer can make at one time. Additional requests\nwill be queued, and sent when the running requests have finished. Set to 0 for\nunlimited.", "gui.computercraft.config.http.max_websockets": "Maximum concurrent websockets", - "gui.computercraft.config.http.max_websockets.tooltip": "The number of websockets a computer can have open at one time.\nRange: > 1", + "gui.computercraft.config.http.max_websockets.tooltip": "The number of websockets a computer can have open at one time.", "gui.computercraft.config.http.proxy": "Proxy", "gui.computercraft.config.http.proxy.host": "Host name", "gui.computercraft.config.http.proxy.host.tooltip": "The hostname or IP address of the proxy server.", "gui.computercraft.config.http.proxy.port": "Port", - "gui.computercraft.config.http.proxy.port.tooltip": "The port of the proxy server.\nRange: 1 ~ 65536", + "gui.computercraft.config.http.proxy.port.tooltip": "The port of the proxy server.", "gui.computercraft.config.http.proxy.tooltip": "Tunnels HTTP and websocket requests through a proxy server. Only affects HTTP\nrules with \"use_proxy\" set to true (off by default).\nIf authentication is required for the proxy, create a \"computercraft-proxy.pw\"\nfile in the same directory as \"computercraft-server.toml\", containing the\nusername and password separated by a colon, e.g. \"myuser:mypassword\". For\nSOCKS4 proxies only the username is required.", "gui.computercraft.config.http.proxy.type": "Proxy type", - "gui.computercraft.config.http.proxy.type.tooltip": "The type of proxy to use.\nAllowed Values: HTTP, HTTPS, SOCKS4, SOCKS5", + "gui.computercraft.config.http.proxy.type.tooltip": "The type of proxy to use.", "gui.computercraft.config.http.rules": "Allow/deny rules", "gui.computercraft.config.http.rules.tooltip": "A list of rules which control behaviour of the \"http\" API for specific domains or\nIPs. Each rule matches against a hostname and an optional port, and then sets several\nproperties for the request. Rules are evaluated in order, meaning earlier rules override\nlater ones.\n\nValid properties:\n - \"host\" (required): The domain or IP address this rule matches. This may be a domain name\n (\"pastebin.com\"), wildcard (\"*.pastebin.com\") or CIDR notation (\"127.0.0.0/8\").\n - \"port\" (optional): Only match requests for a specific port, such as 80 or 443.\n\n - \"action\" (optional): Whether to allow or deny this request.\n - \"max_download\" (optional): The maximum size (in bytes) that a computer can download in this\n request.\n - \"max_upload\" (optional): The maximum size (in bytes) that a computer can upload in a this request.\n - \"max_websocket_message\" (optional): The maximum size (in bytes) that a computer can send or\n receive in one websocket packet.\n - \"use_proxy\" (optional): Enable use of the HTTP/SOCKS proxy if it is configured.", "gui.computercraft.config.http.tooltip": "Controls the HTTP API", @@ -120,61 +120,61 @@ "gui.computercraft.config.log_computer_errors": "Log computer errors", "gui.computercraft.config.log_computer_errors.tooltip": "Log exceptions thrown by peripherals and other Lua objects. This makes it easier\nfor mod authors to debug problems, but may result in log spam should people use\nbuggy methods.", "gui.computercraft.config.maximum_open_files": "Maximum files open per computer", - "gui.computercraft.config.maximum_open_files.tooltip": "Set how many files a computer can have open at the same time. Set to 0 for unlimited.\nRange: > 0", + "gui.computercraft.config.maximum_open_files.tooltip": "Set how many files a computer can have open at the same time. Set to 0 for unlimited.", "gui.computercraft.config.monitor_distance": "Monitor distance", - "gui.computercraft.config.monitor_distance.tooltip": "The maximum distance monitors will render at. This defaults to the standard tile\nentity limit, but may be extended if you wish to build larger monitors.\nRange: 16 ~ 1024", + "gui.computercraft.config.monitor_distance.tooltip": "The maximum distance monitors will render at. This defaults to the standard tile\nentity limit, but may be extended if you wish to build larger monitors.", "gui.computercraft.config.monitor_renderer": "Monitor renderer", - "gui.computercraft.config.monitor_renderer.tooltip": "The renderer to use for monitors. Generally this should be kept at \"best\" - if\nmonitors have performance issues, you may wish to experiment with alternative\nrenderers.\nAllowed Values: BEST, TBO, VBO", + "gui.computercraft.config.monitor_renderer.tooltip": "The renderer to use for monitors. Generally this should be kept at \"best\" - if\nmonitors have performance issues, you may wish to experiment with alternative\nrenderers.", "gui.computercraft.config.peripheral": "Peripherals", "gui.computercraft.config.peripheral.command_block_enabled": "Enable command block peripheral", "gui.computercraft.config.peripheral.command_block_enabled.tooltip": "Enable Command Block peripheral support", "gui.computercraft.config.peripheral.max_notes_per_tick": "Maximum notes that a computer can play at once", - "gui.computercraft.config.peripheral.max_notes_per_tick.tooltip": "Maximum amount of notes a speaker can play at once.\nRange: > 1", + "gui.computercraft.config.peripheral.max_notes_per_tick.tooltip": "Maximum amount of notes a speaker can play at once.", "gui.computercraft.config.peripheral.modem_high_altitude_range": "Modem range (high-altitude)", - "gui.computercraft.config.peripheral.modem_high_altitude_range.tooltip": "The range of Wireless Modems at maximum altitude in clear weather, in meters.\nRange: 0 ~ 100000", + "gui.computercraft.config.peripheral.modem_high_altitude_range.tooltip": "The range of Wireless Modems at maximum altitude in clear weather, in meters.", "gui.computercraft.config.peripheral.modem_high_altitude_range_during_storm": "Modem range (high-altitude, bad weather)", - "gui.computercraft.config.peripheral.modem_high_altitude_range_during_storm.tooltip": "The range of Wireless Modems at maximum altitude in stormy weather, in meters.\nRange: 0 ~ 100000", + "gui.computercraft.config.peripheral.modem_high_altitude_range_during_storm.tooltip": "The range of Wireless Modems at maximum altitude in stormy weather, in meters.", "gui.computercraft.config.peripheral.modem_range": "Modem range (default)", - "gui.computercraft.config.peripheral.modem_range.tooltip": "The range of Wireless Modems at low altitude in clear weather, in meters.\nRange: 0 ~ 100000", + "gui.computercraft.config.peripheral.modem_range.tooltip": "The range of Wireless Modems at low altitude in clear weather, in meters.", "gui.computercraft.config.peripheral.modem_range_during_storm": "Modem range (bad weather)", - "gui.computercraft.config.peripheral.modem_range_during_storm.tooltip": "The range of Wireless Modems at low altitude in stormy weather, in meters.\nRange: 0 ~ 100000", + "gui.computercraft.config.peripheral.modem_range_during_storm.tooltip": "The range of Wireless Modems at low altitude in stormy weather, in meters.", "gui.computercraft.config.peripheral.monitor_bandwidth": "Monitor bandwidth", - "gui.computercraft.config.peripheral.monitor_bandwidth.tooltip": "The limit to how much monitor data can be sent *per tick*. Note:\n - Bandwidth is measured before compression, so the data sent to the client is\n smaller.\n - This ignores the number of players a packet is sent to. Updating a monitor for\n one player consumes the same bandwidth limit as sending to 20.\n - A full sized monitor sends ~25kb of data. So the default (1MB) allows for ~40\n monitors to be updated in a single tick.\nSet to 0 to disable.\nRange: > 0", + "gui.computercraft.config.peripheral.monitor_bandwidth.tooltip": "The limit to how much monitor data can be sent *per tick*. Note:\n - Bandwidth is measured before compression, so the data sent to the client is\n smaller.\n - This ignores the number of players a packet is sent to. Updating a monitor for\n one player consumes the same bandwidth limit as sending to 20.\n - A full sized monitor sends ~25kb of data. So the default (1MB) allows for ~40\n monitors to be updated in a single tick.\nSet to 0 to disable.", "gui.computercraft.config.peripheral.tooltip": "Various options relating to peripherals.", "gui.computercraft.config.term_sizes": "Terminal sizes", "gui.computercraft.config.term_sizes.computer": "Computer", "gui.computercraft.config.term_sizes.computer.height": "Terminal height", - "gui.computercraft.config.term_sizes.computer.height.tooltip": "Range: 1 ~ 255", + "gui.computercraft.config.term_sizes.computer.height.tooltip": "Height of computer terminal", "gui.computercraft.config.term_sizes.computer.tooltip": "Terminal size of computers.", "gui.computercraft.config.term_sizes.computer.width": "Terminal width", - "gui.computercraft.config.term_sizes.computer.width.tooltip": "Range: 1 ~ 255", + "gui.computercraft.config.term_sizes.computer.width.tooltip": "Width of computer terminal", "gui.computercraft.config.term_sizes.monitor": "Monitor", "gui.computercraft.config.term_sizes.monitor.height": "Max monitor height", - "gui.computercraft.config.term_sizes.monitor.height.tooltip": "Range: 1 ~ 32", + "gui.computercraft.config.term_sizes.monitor.height.tooltip": "Maximum height of monitors", "gui.computercraft.config.term_sizes.monitor.tooltip": "Maximum size of monitors (in blocks).", "gui.computercraft.config.term_sizes.monitor.width": "Max monitor width", - "gui.computercraft.config.term_sizes.monitor.width.tooltip": "Range: 1 ~ 32", + "gui.computercraft.config.term_sizes.monitor.width.tooltip": "Maximum width of monitors", "gui.computercraft.config.term_sizes.pocket_computer": "Pocket Computer", "gui.computercraft.config.term_sizes.pocket_computer.height": "Terminal height", - "gui.computercraft.config.term_sizes.pocket_computer.height.tooltip": "Range: 1 ~ 255", + "gui.computercraft.config.term_sizes.pocket_computer.height.tooltip": "Height of pocket computer terminal", "gui.computercraft.config.term_sizes.pocket_computer.tooltip": "Terminal size of pocket computers.", "gui.computercraft.config.term_sizes.pocket_computer.width": "Terminal width", - "gui.computercraft.config.term_sizes.pocket_computer.width.tooltip": "Range: 1 ~ 255", + "gui.computercraft.config.term_sizes.pocket_computer.width.tooltip": "Width of pocket computer terminal", "gui.computercraft.config.term_sizes.tooltip": "Configure the size of various computer's terminals.\nLarger terminals require more bandwidth, so use with care.", "gui.computercraft.config.turtle": "Turtles", "gui.computercraft.config.turtle.advanced_fuel_limit": "Advanced Turtle fuel limit", - "gui.computercraft.config.turtle.advanced_fuel_limit.tooltip": "The fuel limit for Advanced Turtles.\nRange: > 0", + "gui.computercraft.config.turtle.advanced_fuel_limit.tooltip": "The fuel limit for Advanced Turtles.", "gui.computercraft.config.turtle.can_push": "Turtles can push entities", "gui.computercraft.config.turtle.can_push.tooltip": "If set to true, Turtles will push entities out of the way instead of stopping if\nthere is space to do so.", "gui.computercraft.config.turtle.need_fuel": "Enable fuel", "gui.computercraft.config.turtle.need_fuel.tooltip": "Set whether Turtles require fuel to move.", "gui.computercraft.config.turtle.normal_fuel_limit": "Turtle fuel limit", - "gui.computercraft.config.turtle.normal_fuel_limit.tooltip": "The fuel limit for Turtles.\nRange: > 0", + "gui.computercraft.config.turtle.normal_fuel_limit.tooltip": "The fuel limit for Turtles.", "gui.computercraft.config.turtle.tooltip": "Various options relating to turtles.", "gui.computercraft.config.upload_max_size": "File upload size limit (bytes)", - "gui.computercraft.config.upload_max_size.tooltip": "The file upload size limit, in bytes. Must be in range of 1 KiB and 16 MiB.\nKeep in mind that uploads are processed in a single tick - large files or\npoor network performance can stall the networking thread. And mind the disk space!\nRange: 1024 ~ 16777216", + "gui.computercraft.config.upload_max_size.tooltip": "The file upload size limit, in bytes. Must be in range of 1 KiB and 16 MiB.\nKeep in mind that uploads are processed in a single tick - large files or\npoor network performance can stall the networking thread. And mind the disk space!", "gui.computercraft.config.upload_nag_delay": "Upload nag delay", - "gui.computercraft.config.upload_nag_delay.tooltip": "The delay in seconds after which we'll notify about unhandled imports. Set to 0 to disable.\nRange: 0 ~ 60", + "gui.computercraft.config.upload_nag_delay.tooltip": "The delay in seconds after which we'll notify about unhandled imports. Set to 0 to disable.", "gui.computercraft.pocket_computer_overlay": "Pocket computer open. Press ESC to close.", "gui.computercraft.terminal": "Computer terminal", "gui.computercraft.tooltip.computer_id": "Computer ID: %s", diff --git a/projects/common/src/main/java/dan200/computercraft/shared/config/ConfigFile.java b/projects/common/src/main/java/dan200/computercraft/shared/config/ConfigFile.java index 7a55c8284d..7dc998520b 100644 --- a/projects/common/src/main/java/dan200/computercraft/shared/config/ConfigFile.java +++ b/projects/common/src/main/java/dan200/computercraft/shared/config/ConfigFile.java @@ -9,9 +9,7 @@ import javax.annotation.Nullable; import javax.annotation.OverridingMethodsMustInvokeSuper; import java.nio.file.Path; -import java.util.ArrayDeque; -import java.util.Deque; -import java.util.List; +import java.util.*; import java.util.function.Predicate; import java.util.function.Supplier; import java.util.stream.Stream; @@ -19,27 +17,47 @@ /** * A config file which the user can modify. */ -public interface ConfigFile { - String TRANSLATION_PREFIX = "gui.computercraft.config."; - Splitter SPLITTER = Splitter.on('.'); +public abstract class ConfigFile { + public static final String TRANSLATION_PREFIX = "gui.computercraft.config."; + public static final Splitter SPLITTER = Splitter.on('.'); /** * An entry in the config file, either a {@link Value} or {@linkplain Group group of other entries}. */ - sealed interface Entry permits Group, Value { + public abstract static sealed class Entry permits Group, Value { + protected final String path; + private final String translationKey; + private final String comment; + + protected Entry(String path, String comment) { + this.path = path; + this.translationKey = TRANSLATION_PREFIX + path; + this.comment = comment; + } + + public final String path() { + return path; + } + /** * Get the translation key of this config entry. * * @return This entry's translation key. */ - String translationKey(); + public final String translationKey() { + return translationKey; + } /** * Get the comment about this config entry. * * @return The comment for this config entry. */ - String comment(); + public final String comment() { + return comment; + } + + abstract Stream entries(); } /** @@ -47,13 +65,38 @@ sealed interface Entry permits Group, Value { * * @param The type of the stored value. */ - non-sealed interface Value extends Entry, Supplier { + public abstract static non-sealed class Value extends Entry implements Supplier { + protected Value(String translationKey, String comment) { + super(translationKey, comment); + } + + @Override + Stream entries() { + return Stream.of(this); + } } /** * A group of config entries. */ - non-sealed interface Group extends Entry { + public static final class Group extends Entry { + private final Map children; + + public Group(String translationKey, String comment, Map children) { + super(translationKey, comment); + this.children = children; + } + + @Override + Stream entries() { + return Stream.concat(Stream.of(this), children.values().stream().flatMap(Entry::entries)); + } + } + + protected final Map entries; + + protected ConfigFile(Map entries) { + this.entries = entries; } /** @@ -61,16 +104,46 @@ non-sealed interface Group extends Entry { * * @return All config keys. */ - Stream entries(); + public final Stream entries() { + return entries.values().stream().flatMap(Entry::entries); + } + + public final @Nullable Entry getEntry(String path) { + var iterator = SPLITTER.split(path).iterator(); - @Nullable - Entry getEntry(String path); + var entry = entries.get(iterator.next()); + while (iterator.hasNext()) { + if (!(entry instanceof Group group)) return null; + entry = group.children.get(iterator.next()); + } + + return entry; + } /** * A builder which can be used to generate a config object. */ - abstract class Builder { - protected final Deque groupStack = new ArrayDeque<>(); + public abstract static class Builder { + protected record RootGroup(String path, Map children) { + public RootGroup { + } + } + + protected final Deque groupStack = new ArrayDeque<>(); + private @Nullable String pendingComment; + + protected Builder() { + groupStack.addLast(new RootGroup("", new HashMap<>())); + } + + protected final String getPath() { + return groupStack.getLast().path(); + } + + protected final String getPath(String name) { + var path = groupStack.getLast().path(); + return path.isEmpty() ? name : path + "." + name; + } protected String getTranslation(String name) { var key = new StringBuilder(TRANSLATION_PREFIX); @@ -86,7 +159,19 @@ protected String getTranslation(String name) { * @param comment The comment. * @return The current object, for chaining. */ - public abstract Builder comment(String comment); + @OverridingMethodsMustInvokeSuper + public ConfigFile.Builder comment(String comment) { + if (pendingComment != null) throw new IllegalStateException("Already have a comment"); + pendingComment = comment; + return this; + } + + protected String takeComment() { + var comment = pendingComment; + if (comment == null) throw new IllegalStateException("No comment specified"); + pendingComment = null; + return comment; + } /** * Push a new config group. @@ -95,7 +180,10 @@ protected String getTranslation(String name) { */ @OverridingMethodsMustInvokeSuper public void push(String name) { - groupStack.addLast(name); + var path = getPath(name); + Map children = new HashMap<>(); + groupStack.getLast().children().put(name, new Group(path, takeComment(), children)); + groupStack.addLast(new RootGroup(path, children)); } /** @@ -113,22 +201,22 @@ public void pop() { */ public abstract Builder worldRestart(); - public abstract ConfigFile.Value define(String path, T defaultValue); + public abstract ConfigFile.Value define(String name, T defaultValue); /** * A boolean-specific override of the above {@link #define(String, Object)} method. * - * @param path The path to the value we're defining. + * @param name The name of the value we're defining. * @param defaultValue The default value. * @return The accessor for this config option. */ - public abstract ConfigFile.Value define(String path, boolean defaultValue); + public abstract ConfigFile.Value define(String name, boolean defaultValue); - public abstract ConfigFile.Value defineInRange(String path, int defaultValue, int min, int max); + public abstract ConfigFile.Value defineInRange(String name, int defaultValue, int min, int max); - public abstract ConfigFile.Value> defineList(String path, List defaultValue, Predicate elementValidator); + public abstract ConfigFile.Value> defineList(String name, List defaultValue, Predicate elementValidator); - public abstract > ConfigFile.Value defineEnum(String path, V defaultValue); + public abstract > ConfigFile.Value defineEnum(String name, V defaultValue); /** * Finalise this config file. @@ -140,7 +228,7 @@ public void pop() { } @FunctionalInterface - interface ConfigListener { + public interface ConfigListener { /** * The function called then a config file is changed. * diff --git a/projects/common/src/main/java/dan200/computercraft/shared/config/ConfigSpec.java b/projects/common/src/main/java/dan200/computercraft/shared/config/ConfigSpec.java index 0be520ba5c..eaa17523aa 100644 --- a/projects/common/src/main/java/dan200/computercraft/shared/config/ConfigSpec.java +++ b/projects/common/src/main/java/dan200/computercraft/shared/config/ConfigSpec.java @@ -344,18 +344,18 @@ or a single method (computercraft:inventory#pushItems). .push("term_sizes"); builder.comment("Terminal size of computers.").push("computer"); - computerTermWidth = builder.defineInRange("width", Config.computerTermWidth, 1, 255); - computerTermHeight = builder.defineInRange("height", Config.computerTermHeight, 1, 255); + computerTermWidth = builder.comment("Width of computer terminal").defineInRange("width", Config.computerTermWidth, 1, 255); + computerTermHeight = builder.comment("Height of computer terminal").defineInRange("height", Config.computerTermHeight, 1, 255); builder.pop(); builder.comment("Terminal size of pocket computers.").push("pocket_computer"); - pocketTermWidth = builder.defineInRange("width", Config.pocketTermWidth, 1, 255); - pocketTermHeight = builder.defineInRange("height", Config.pocketTermHeight, 1, 255); + pocketTermWidth = builder.comment("Width of pocket computer terminal").defineInRange("width", Config.pocketTermWidth, 1, 255); + pocketTermHeight = builder.comment("Height of pocket computer terminal").defineInRange("height", Config.pocketTermHeight, 1, 255); builder.pop(); builder.comment("Maximum size of monitors (in blocks).").push("monitor"); - monitorWidth = builder.defineInRange("width", Config.monitorWidth, 1, 32); - monitorHeight = builder.defineInRange("height", Config.monitorHeight, 1, 32); + monitorWidth = builder.comment("Maximum width of monitors").defineInRange("width", Config.monitorWidth, 1, 32); + monitorHeight = builder.comment("Maximum height of monitors").defineInRange("height", Config.monitorHeight, 1, 32); builder.pop(); builder.pop(); diff --git a/projects/common/src/main/java/dan200/computercraft/shared/util/Trie.java b/projects/common/src/main/java/dan200/computercraft/shared/util/Trie.java deleted file mode 100644 index 70037585b7..0000000000 --- a/projects/common/src/main/java/dan200/computercraft/shared/util/Trie.java +++ /dev/null @@ -1,46 +0,0 @@ -// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers -// -// SPDX-License-Identifier: MPL-2.0 - -package dan200.computercraft.shared.util; - -import javax.annotation.Nullable; -import java.util.HashMap; -import java.util.Map; -import java.util.stream.Stream; - -/** - * A key-value map, where the key is a list of values. - * - * @param The type of keys in this trie. - * @param The values in this map. - */ -public class Trie { - private @Nullable V current; - private @Nullable Map> children; - - public Trie getChild(Iterable key) { - var self = this; - for (var keyElement : key) { - if (self.children == null) self.children = new HashMap<>(1); - self = self.children.computeIfAbsent(keyElement, x -> new Trie<>()); - } - - return self; - } - - public @Nullable V getValue(Iterable key) { - return getChild(key).current; - } - - public void setValue(Iterable key, V value) { - getChild(key).current = value; - } - - public Stream stream() { - return Stream.concat( - current == null ? Stream.empty() : Stream.of(current), - children == null ? Stream.empty() : children.values().stream().flatMap(Trie::stream) - ); - } -} diff --git a/projects/fabric/src/main/java/dan200/computercraft/shared/platform/FabricConfigFile.java b/projects/fabric/src/main/java/dan200/computercraft/shared/platform/FabricConfigFile.java index 0536d8e279..b31f6ba2ba 100644 --- a/projects/fabric/src/main/java/dan200/computercraft/shared/platform/FabricConfigFile.java +++ b/projects/fabric/src/main/java/dan200/computercraft/shared/platform/FabricConfigFile.java @@ -11,7 +11,6 @@ import com.electronwill.nightconfig.core.io.WritingMode; import com.google.errorprone.annotations.concurrent.GuardedBy; import dan200.computercraft.shared.config.ConfigFile; -import dan200.computercraft.shared.util.Trie; import org.apache.commons.lang3.function.TriFunction; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -22,6 +21,7 @@ import java.nio.file.Path; import java.util.Arrays; import java.util.List; +import java.util.Map; import java.util.function.Predicate; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -29,18 +29,17 @@ /** * A {@link ConfigFile} which sits directly on top of NightConfig. */ -public class FabricConfigFile implements ConfigFile { +public final class FabricConfigFile extends ConfigFile { private static final Logger LOG = LoggerFactory.getLogger(FabricConfigFile.class); private final ConfigSpec spec; - private final Trie entries; private final ConfigListener onChange; private @Nullable CommentedFileConfig config; - public FabricConfigFile(ConfigSpec spec, Trie entries, ConfigListener onChange) { + private FabricConfigFile(ConfigSpec spec, Map entries, ConfigListener onChange) { + super(entries); this.spec = spec; - this.entries = entries; this.onChange = onChange; } @@ -64,7 +63,7 @@ public synchronized void load(Path path) { @SuppressWarnings("unchecked") private Stream> values() { - return (Stream>) (Stream) entries.stream().filter(ValueImpl.class::isInstance); + return (Stream>) (Stream) entries().filter(ValueImpl.class::isInstance); } public synchronized void unload() { @@ -92,7 +91,7 @@ private synchronized boolean loadConfig() { // Ensure the config file matches the spec var isNewFile = config.isEmpty(); - entries.stream().forEach(x -> config.setComment(x.path, x.comment)); + entries().forEach(x -> config.setComment(x.path(), x instanceof ValueImpl v ? v.fullComment : x.comment())); var corrected = isNewFile ? spec.correct(config) : spec.correct(config, (action, entryPath, oldValue, newValue) -> { LOG.warn("Incorrect key {} was corrected from {} to {}", String.join(".", entryPath), oldValue, newValue); }); @@ -104,148 +103,77 @@ private synchronized boolean loadConfig() { return corrected > 0; } - @Override - public Stream entries() { - return entries.stream().map(x -> (ConfigFile.Entry) x); - } - - @Nullable - @Override - public ConfigFile.Entry getEntry(String path) { - return (ConfigFile.Entry) entries.getValue(SPLITTER.split(path)); - } - static class Builder extends ConfigFile.Builder { private final ConfigSpec spec = new ConfigSpec(); - private final Trie entries = new Trie<>(); - - private @Nullable String pendingComment; - - private String getFullPath(String path) { - var key = new StringBuilder(); - for (var group : groupStack) key.append(group).append('.'); - key.append(path); - return key.toString(); - } - - @Override - public ConfigFile.Builder comment(String comment) { - if (pendingComment != null) throw new IllegalStateException("Already have a comment"); - pendingComment = comment; - return this; - } - - private String takeComment() { - var comment = pendingComment; - if (comment == null) throw new IllegalStateException("No comment specified"); - pendingComment = null; - return comment; - } - - private String takeComment(String suffix) { - var comment = pendingComment == null ? "" : pendingComment + "\n"; - pendingComment = null; - return comment + suffix; - } - - @Override - public void push(String name) { - var path = getFullPath(name); - var splitPath = SPLITTER.split(path); - entries.setValue(splitPath, new GroupImpl(path, takeComment())); - - super.push(name); - } @Override public ConfigFile.Builder worldRestart() { return this; } - private Value defineValue(String fullPath, String comment, T defaultValue, TriFunction getter) { - var value = new ValueImpl(fullPath, comment, defaultValue, getter); - entries.setValue(SPLITTER.split(fullPath), value); + private Value defineValue(String name, String comment, @Nullable String suffix, T defaultValue, TriFunction getter) { + var fullComment = suffix == null ? comment : comment + "\n" + suffix; + var value = new ValueImpl(getPath(name), comment, fullComment, defaultValue, getter); + groupStack.getLast().children().put(name, value); return value; } @Override - public Value define(String path, T defaultValue) { - var fullPath = getFullPath(path); - spec.define(fullPath, defaultValue); - return defineValue(fullPath, takeComment(), defaultValue, Config::getOrElse); + public Value define(String name, T defaultValue) { + var path = getPath(name); + spec.define(path, defaultValue); + return defineValue(name, takeComment(), null, defaultValue, Config::getOrElse); } @Override - public Value define(String path, boolean defaultValue) { - var fullPath = getFullPath(path); - spec.define(fullPath, defaultValue, x -> x instanceof Boolean); - return defineValue(fullPath, takeComment(), defaultValue, UnmodifiableConfig::getOrElse); + public Value define(String name, boolean defaultValue) { + var path = getPath(name); + spec.define(path, defaultValue, x -> x instanceof Boolean); + return defineValue(name, takeComment(), null, defaultValue, UnmodifiableConfig::getOrElse); } @Override - public Value defineInRange(String path, int defaultValue, int min, int max) { - var fullPath = getFullPath(path); - spec.defineInRange(fullPath, defaultValue, min, max); + public Value defineInRange(String name, int defaultValue, int min, int max) { + var path = getPath(name); + spec.defineInRange(path, defaultValue, min, max); var suffix = max == Integer.MAX_VALUE ? "Range: > " + min : "Range: " + min + " ~ " + max; - return defineValue(fullPath, takeComment(suffix), defaultValue, UnmodifiableConfig::getIntOrElse); + return defineValue(name, takeComment(), suffix, defaultValue, UnmodifiableConfig::getIntOrElse); } @Override - public Value> defineList(String path, List defaultValue, Predicate elementValidator) { - var fullPath = getFullPath(path); - spec.defineList(fullPath, defaultValue, elementValidator); - return defineValue(fullPath, takeComment(), defaultValue, Config::getOrElse); + public Value> defineList(String name, List defaultValue, Predicate elementValidator) { + var path = getPath(name); + spec.defineList(path, defaultValue, elementValidator); + return defineValue(name, takeComment(), null, defaultValue, Config::getOrElse); } @Override - public > Value defineEnum(String path, V defaultValue) { - var fullPath = getFullPath(path); - spec.define(fullPath, defaultValue, o -> o != null && o != NullObject.NULL_OBJECT && EnumGetMethod.NAME_IGNORECASE.validate(o, defaultValue.getDeclaringClass())); + public > Value defineEnum(String name, V defaultValue) { + var path = getPath(name); + spec.define(path, defaultValue, o -> o != null && o != NullObject.NULL_OBJECT && EnumGetMethod.NAME_IGNORECASE.validate(o, defaultValue.getDeclaringClass())); var suffix = "Allowed Values: " + Arrays.stream(defaultValue.getDeclaringClass().getEnumConstants()).map(Enum::name).collect(Collectors.joining(", ")); - return defineValue(fullPath, takeComment(suffix), defaultValue, (c, p, d) -> c.getEnumOrElse(p, d, EnumGetMethod.NAME_IGNORECASE)); + return defineValue(name, takeComment(), suffix, defaultValue, (c, p, d) -> c.getEnumOrElse(p, d, EnumGetMethod.NAME_IGNORECASE)); } @Override public ConfigFile build(ConfigListener onChange) { - return new FabricConfigFile(spec, entries, onChange); - } - } - - private static class Entry { - final String path; - final String comment; - - Entry(String path, String comment) { - this.path = path; - this.comment = comment; - } - - @SuppressWarnings("UnusedMethod") - public final String translationKey() { - return TRANSLATION_PREFIX + path; - } - - @SuppressWarnings("UnusedMethod") - public final String comment() { - return comment; - } - } - - private static final class GroupImpl extends Entry implements Group { - private GroupImpl(String path, String comment) { - super(path, comment); + var children = groupStack.removeLast().children(); + if (!groupStack.isEmpty()) throw new IllegalStateException("Mismatched config push/pop"); + return new FabricConfigFile(spec, children, onChange); } } - private static final class ValueImpl extends Entry implements Value { + private static final class ValueImpl extends Value { private @Nullable T value; private final T defaultValue; private final TriFunction get; + private final String fullComment; - private ValueImpl(String path, String comment, T defaultValue, TriFunction get) { + private ValueImpl(String path, String comment, String fullComment, T defaultValue, TriFunction get) { super(path, comment); + this.fullComment = fullComment; this.defaultValue = defaultValue; this.get = get; } diff --git a/projects/forge/src/main/java/dan200/computercraft/shared/platform/ForgeConfigFile.java b/projects/forge/src/main/java/dan200/computercraft/shared/platform/ForgeConfigFile.java index ca74c4a9b8..07bfab3c18 100644 --- a/projects/forge/src/main/java/dan200/computercraft/shared/platform/ForgeConfigFile.java +++ b/projects/forge/src/main/java/dan200/computercraft/shared/platform/ForgeConfigFile.java @@ -5,48 +5,32 @@ package dan200.computercraft.shared.platform; import dan200.computercraft.shared.config.ConfigFile; -import dan200.computercraft.shared.util.Trie; import net.minecraftforge.common.ForgeConfigSpec; -import javax.annotation.Nullable; -import java.util.ArrayList; import java.util.List; +import java.util.Map; import java.util.function.Predicate; -import java.util.stream.Stream; /** * A {@link ConfigFile} which wraps Forge's config implementation. */ -public final class ForgeConfigFile implements ConfigFile { +public final class ForgeConfigFile extends ConfigFile { private final ForgeConfigSpec spec; - private final Trie entries; - public ForgeConfigFile(ForgeConfigSpec spec, Trie entries) { + private ForgeConfigFile(ForgeConfigSpec spec, Map entries) { + super(entries); this.spec = spec; - this.entries = entries; } public ForgeConfigSpec spec() { return spec; } - @Override - public Stream entries() { - return entries.stream(); - } - - @Nullable - @Override - public Entry getEntry(String path) { - return entries.getValue(SPLITTER.split(path)); - } - /** * Wraps {@link ForgeConfigSpec.Builder} into our own config builder abstraction. */ static class Builder extends ConfigFile.Builder { private final ForgeConfigSpec.Builder builder = new ForgeConfigSpec.Builder(); - private final Trie entries = new Trie<>(); private void translation(String name) { builder.translation(getTranslation(name)); @@ -54,24 +38,23 @@ private void translation(String name) { @Override public ConfigFile.Builder comment(String comment) { + super.comment(comment); builder.comment(comment); return this; } @Override public void push(String name) { + super.push(name); + translation(name); builder.push(name); - super.push(name); } @Override public void pop() { - var path = new ArrayList<>(groupStack); - entries.setValue(path, new GroupImpl(path)); - - builder.pop(); super.pop(); + builder.pop(); } @Override @@ -80,100 +63,63 @@ public ConfigFile.Builder worldRestart() { return this; } - private ConfigFile.Value defineValue(ForgeConfigSpec.ConfigValue value) { - var wrapped = new ValueImpl<>(value); - entries.setValue(value.getPath(), wrapped); + private ConfigFile.Value defineValue(String name, ForgeConfigSpec.ConfigValue value) { + var wrapped = new ValueImpl<>(getPath(name), takeComment(), value); + groupStack.getLast().children().put(name, wrapped); return wrapped; } @Override - public ConfigFile.Value define(String path, T defaultValue) { - translation(path); - return defineValue(builder.define(path, defaultValue)); + public ConfigFile.Value define(String name, T defaultValue) { + translation(name); + return defineValue(name, builder.define(name, defaultValue)); } @Override - public ConfigFile.Value define(String path, boolean defaultValue) { - translation(path); - return defineValue(builder.define(path, defaultValue)); + public ConfigFile.Value define(String name, boolean defaultValue) { + translation(name); + return defineValue(name, builder.define(name, defaultValue)); } @Override - public ConfigFile.Value defineInRange(String path, int defaultValue, int min, int max) { - translation(path); - return defineValue(builder.defineInRange(path, defaultValue, min, max)); + public ConfigFile.Value defineInRange(String name, int defaultValue, int min, int max) { + translation(name); + return defineValue(name, builder.defineInRange(name, defaultValue, min, max)); } @Override - public ConfigFile.Value> defineList(String path, List defaultValue, Predicate elementValidator) { - translation(path); - return defineValue(builder.defineList(path, defaultValue, elementValidator)); + public ConfigFile.Value> defineList(String name, List defaultValue, Predicate elementValidator) { + translation(name); + return defineValue(name, builder.defineList(name, defaultValue, elementValidator)); } @Override - public > ConfigFile.Value defineEnum(String path, V defaultValue) { - translation(path); - return defineValue(builder.defineEnum(path, defaultValue)); + public > ConfigFile.Value defineEnum(String name, V defaultValue) { + translation(name); + return defineValue(name, builder.defineEnum(name, defaultValue)); } @Override public ConfigFile build(ConfigListener onChange) { - var spec = builder.build(); - entries.stream().forEach(x -> { - if (x instanceof ValueImpl value) value.owner = spec; - if (x instanceof GroupImpl value) value.owner = spec; - }); - return new ForgeConfigFile(spec, entries); - } - } - - private static final class GroupImpl implements ConfigFile.Group { - private final List path; - private @Nullable ForgeConfigSpec owner; + var children = groupStack.removeLast().children(); + if (!groupStack.isEmpty()) throw new IllegalStateException("Mismatched config push/pop"); - private GroupImpl(List path) { - this.path = path; - } - - @Override - public String translationKey() { - if (owner == null) throw new IllegalStateException("Config has not been built yet"); - return owner.getLevelTranslationKey(path); - } - - @Override - public String comment() { - if (owner == null) throw new IllegalStateException("Config has not been built yet"); - return owner.getLevelComment(path); + var spec = builder.build(); + return new ForgeConfigFile(spec, children); } } - private static final class ValueImpl implements ConfigFile.Value { + private static final class ValueImpl extends Value { private final ForgeConfigSpec.ConfigValue value; - private @Nullable ForgeConfigSpec owner; - private ValueImpl(ForgeConfigSpec.ConfigValue value) { + private ValueImpl(String path, String comment, ForgeConfigSpec.ConfigValue value) { + super(path, comment); this.value = value; } - private ForgeConfigSpec.ValueSpec spec() { - if (owner == null) throw new IllegalStateException("Config has not been built yet"); - return owner.getSpec().get(value.getPath()); - } - @Override public T get() { return value.get(); } - - @Override - public String translationKey() { - return spec().getTranslationKey(); - } - - @Override - public String comment() { - return spec().getComment(); - } } }