1
0
mirror of https://github.com/SquidDev-CC/CC-Tweaked synced 2024-12-12 19:20:29 +00:00

Merge branch 'mc-1.20.x' into mc-1.21.x

This commit is contained in:
Jonathan Coates 2024-12-05 12:19:16 +00:00
commit f04c699df6
No known key found for this signature in database
GPG Key ID: B9E431FF07C98D06
41 changed files with 331 additions and 417 deletions

View File

@ -30,6 +30,18 @@ jobs:
- name: ⚒️ Build - name: ⚒️ Build
run: ./gradlew assemble || ./gradlew assemble run: ./gradlew assemble || ./gradlew assemble
- name: 📦 Prepare Jars
run: |
# Find the main jar and append the git hash onto it.
mkdir -p jars
find projects/forge/build/libs projects/fabric/build/libs -type f -regex '.*[0-9.]+\(-SNAPSHOT\)?\.jar$' -exec bash -c 'cp {} "jars/$(basename {} .jar)-$(git rev-parse HEAD).jar"' \;
- name: 📤 Upload Jar
uses: actions/upload-artifact@v4
with:
name: CC-Tweaked
path: ./jars
- name: Cache pre-commit - name: Cache pre-commit
uses: actions/cache@v4 uses: actions/cache@v4
with: with:
@ -54,18 +66,6 @@ jobs:
run: ./tools/parse-reports.py run: ./tools/parse-reports.py
if: ${{ failure() }} if: ${{ failure() }}
- name: 📦 Prepare Jars
run: |
# Find the main jar and append the git hash onto it.
mkdir -p jars
find projects/forge/build/libs projects/fabric/build/libs -type f -regex '.*[0-9.]+\(-SNAPSHOT\)?\.jar$' -exec bash -c 'cp {} "jars/$(basename {} .jar)-$(git rev-parse HEAD).jar"' \;
- name: 📤 Upload Jar
uses: actions/upload-artifact@v4
with:
name: CC-Tweaked
path: ./jars
build-core: build-core:
strategy: strategy:
fail-fast: false fail-fast: false

View File

@ -40,8 +40,8 @@ path = [
"projects/core/src/main/resources/data/computercraft/lua/rom/modules/main/.ignoreme", "projects/core/src/main/resources/data/computercraft/lua/rom/modules/main/.ignoreme",
"projects/core/src/main/resources/data/computercraft/lua/rom/modules/turtle/.ignoreme", "projects/core/src/main/resources/data/computercraft/lua/rom/modules/turtle/.ignoreme",
"projects/core/src/main/resources/data/computercraft/lua/rom/motd.txt", "projects/core/src/main/resources/data/computercraft/lua/rom/motd.txt",
"projects/fabric-api/src/main/modJson/fabric.mod.json",
"projects/fabric/src/client/resources/computercraft-client.fabric.mixins.json", "projects/fabric/src/client/resources/computercraft-client.fabric.mixins.json",
"projects/fabric/src/datagen/resources/fabric.mod.json",
"projects/fabric/src/main/resources/computercraft.fabric.mixins.json", "projects/fabric/src/main/resources/computercraft.fabric.mixins.json",
"projects/fabric/src/main/resources/fabric.mod.json", "projects/fabric/src/main/resources/fabric.mod.json",
"projects/fabric/src/testMod/resources/computercraft-gametest.fabric.mixins.json", "projects/fabric/src/testMod/resources/computercraft-gametest.fabric.mixins.json",

View File

@ -19,26 +19,32 @@ plugins {
val main = sourceSets["main"] val main = sourceSets["main"]
val client = sourceSets["client"] val client = sourceSets["client"]
// Both testMod and testFixtures inherit from the main and client classpath, just so we have access to Minecraft classes. // datagen and testMod inherit from the main and client classpath, just so we have access to Minecraft classes.
val datagen by sourceSets.creating {
compileClasspath += main.compileClasspath + client.compileClasspath
runtimeClasspath += main.runtimeClasspath + client.runtimeClasspath
}
val testMod by sourceSets.creating { val testMod by sourceSets.creating {
compileClasspath += main.compileClasspath + client.compileClasspath compileClasspath += main.compileClasspath + client.compileClasspath
runtimeClasspath += main.runtimeClasspath + client.runtimeClasspath runtimeClasspath += main.runtimeClasspath + client.runtimeClasspath
} }
configurations { val extraConfigurations = listOf(datagen, testMod)
named(testMod.compileClasspathConfigurationName) {
shouldResolveConsistentlyWith(compileClasspath.get())
}
named(testMod.runtimeClasspathConfigurationName) { configurations {
shouldResolveConsistentlyWith(runtimeClasspath.get()) for (config in extraConfigurations) {
named(config.compileClasspathConfigurationName) { shouldResolveConsistentlyWith(compileClasspath.get()) }
named(config.runtimeClasspathConfigurationName) { shouldResolveConsistentlyWith(runtimeClasspath.get()) }
} }
} }
// Like the main test configurations, we're safe to depend on source set outputs. // Like the main test configurations, we're safe to depend on source set outputs.
dependencies { dependencies {
add(testMod.implementationConfigurationName, main.output) for (config in extraConfigurations) {
add(testMod.implementationConfigurationName, client.output) add(config.implementationConfigurationName, main.output)
add(config.implementationConfigurationName, client.output)
}
} }
// Similar to java-test-fixtures, but tries to avoid putting the obfuscated jar on the classpath. // Similar to java-test-fixtures, but tries to avoid putting the obfuscated jar on the classpath.

View File

@ -109,14 +109,13 @@ abstract class CCTweakedExtension(
val otherJava = otherProject.extensions.getByType(JavaPluginExtension::class.java) val otherJava = otherProject.extensions.getByType(JavaPluginExtension::class.java)
val main = otherJava.sourceSets.getByName("main") val main = otherJava.sourceSets.getByName("main")
val client = otherJava.sourceSets.getByName("client") val client = otherJava.sourceSets.getByName("client")
val testMod = otherJava.sourceSets.findByName("testMod")
val testFixtures = otherJava.sourceSets.findByName("testFixtures")
// Pull in sources from the other project. // Pull in sources from the other project.
extendSourceSet(otherProject, main) extendSourceSet(otherProject, main)
extendSourceSet(otherProject, client) extendSourceSet(otherProject, client)
if (testMod != null) extendSourceSet(otherProject, testMod) for (sourceSet in listOf("datagen", "testMod", "testFixtures")) {
if (testFixtures != null) extendSourceSet(otherProject, testFixtures) otherJava.sourceSets.findByName(sourceSet)?.let { extendSourceSet(otherProject, it) }
}
// The extra source-processing tasks should include these files too. // The extra source-processing tasks should include these files too.
project.tasks.named(main.javadocTaskName, Javadoc::class.java) { source(main.allJava, client.allJava) } project.tasks.named(main.javadocTaskName, Javadoc::class.java) { source(main.allJava, client.allJava) }

View File

@ -8,7 +8,7 @@ SPDX-FileCopyrightText: 2021 The CC: Tweaked Developers
SPDX-License-Identifier: MPL-2.0 SPDX-License-Identifier: MPL-2.0
--> -->
The [`monitor_resize`] event is fired when an adjacent or networked monitor's size is changed. The [`monitor_resize`] event is fired when an adjacent or networked [monitor's][`monitor`] size is changed.
## Return Values ## Return Values
1. [`string`]: The event name. 1. [`string`]: The event name.

View File

@ -8,7 +8,7 @@ SPDX-FileCopyrightText: 2021 The CC: Tweaked Developers
SPDX-License-Identifier: MPL-2.0 SPDX-License-Identifier: MPL-2.0
--> -->
The [`monitor_touch`] event is fired when an adjacent or networked Advanced Monitor is right-clicked. The [`monitor_touch`] event is fired when an adjacent or networked [Advanced Monitor][`monitor`] is right-clicked.
## Return Values ## Return Values
1. [`string`]: The event name. 1. [`string`]: The event name.

View File

@ -6,8 +6,8 @@ import cc.tweaked.gradle.*
plugins { plugins {
id("cc-tweaked.vanilla") id("cc-tweaked.vanilla")
id("cc-tweaked.gametest")
id("cc-tweaked.illuaminate") id("cc-tweaked.illuaminate")
id("cc-tweaked.mod")
id("cc-tweaked.publishing") id("cc-tweaked.publishing")
} }

View File

@ -6,7 +6,6 @@ package dan200.computercraft.client.gui;
import dan200.computercraft.api.ComputerCraftAPI; import dan200.computercraft.api.ComputerCraftAPI;
import dan200.computercraft.client.render.ComputerBorderRenderer; import dan200.computercraft.client.render.ComputerBorderRenderer;
import dan200.computercraft.data.client.ClientDataProviders;
import dan200.computercraft.shared.computer.core.ComputerFamily; import dan200.computercraft.shared.computer.core.ComputerFamily;
import net.minecraft.client.renderer.texture.TextureAtlasSprite; import net.minecraft.client.renderer.texture.TextureAtlasSprite;
import net.minecraft.client.renderer.texture.TextureManager; import net.minecraft.client.renderer.texture.TextureManager;
@ -113,7 +112,6 @@ public final class GuiSprites extends TextureAtlasHolder {
* @param pocketBottom The texture for the bottom of a pocket computer. * @param pocketBottom The texture for the bottom of a pocket computer.
* @param sidebar The texture for the computer sidebar. * @param sidebar The texture for the computer sidebar.
* @see ComputerBorderRenderer * @see ComputerBorderRenderer
* @see ClientDataProviders
*/ */
public record ComputerTextures( public record ComputerTextures(
ResourceLocation border, ResourceLocation border,

View File

@ -1,60 +0,0 @@
// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package dan200.computercraft.data.client;
import dan200.computercraft.client.gui.GuiSprites;
import dan200.computercraft.client.model.LecternPrintoutModel;
import dan200.computercraft.data.DataProviders;
import dan200.computercraft.shared.turtle.TurtleOverlay;
import dan200.computercraft.shared.turtle.inventory.UpgradeSlot;
import net.minecraft.client.renderer.texture.atlas.SpriteSource;
import net.minecraft.client.renderer.texture.atlas.SpriteSources;
import net.minecraft.client.renderer.texture.atlas.sources.SingleFile;
import net.minecraft.core.HolderLookup;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.server.packs.PackType;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Stream;
/**
* A version of {@link DataProviders} which relies on client-side classes.
* <p>
* This is called from {@link DataProviders#add(DataProviders.GeneratorSink)}.
*/
public final class ClientDataProviders {
private ClientDataProviders() {
}
public static void add(DataProviders.GeneratorSink generator, CompletableFuture<HolderLookup.Provider> registries) {
generator.addFromCodec("Block atlases", PackType.CLIENT_RESOURCES, "atlases", SpriteSources.FILE_CODEC, out -> {
out.accept(ResourceLocation.withDefaultNamespace("blocks"), List.of(
new SingleFile(UpgradeSlot.LEFT_UPGRADE, Optional.empty()),
new SingleFile(UpgradeSlot.RIGHT_UPGRADE, Optional.empty()),
new SingleFile(LecternPrintoutModel.TEXTURE, Optional.empty())
));
out.accept(GuiSprites.SPRITE_SHEET, Stream.of(
// Buttons
GuiSprites.TURNED_OFF.textures(),
GuiSprites.TURNED_ON.textures(),
GuiSprites.TERMINATE.textures(),
// Computers
GuiSprites.COMPUTER_NORMAL.textures(),
GuiSprites.COMPUTER_ADVANCED.textures(),
GuiSprites.COMPUTER_COMMAND.textures(),
GuiSprites.COMPUTER_COLOUR.textures()
).flatMap(x -> x).<SpriteSource>map(x -> new SingleFile(x, Optional.empty())).toList());
});
generator.add(pack -> new ExtraModelsProvider(pack, registries) {
@Override
public Stream<ResourceLocation> getModels(HolderLookup.Provider registries) {
return registries.lookupOrThrow(TurtleOverlay.REGISTRY).listElements().map(x -> x.value().model());
}
});
}
}

View File

@ -7,8 +7,15 @@ package dan200.computercraft.data;
import com.mojang.serialization.Codec; import com.mojang.serialization.Codec;
import dan200.computercraft.api.pocket.IPocketUpgrade; import dan200.computercraft.api.pocket.IPocketUpgrade;
import dan200.computercraft.api.turtle.ITurtleUpgrade; import dan200.computercraft.api.turtle.ITurtleUpgrade;
import dan200.computercraft.client.gui.GuiSprites;
import dan200.computercraft.client.model.LecternPrintoutModel;
import dan200.computercraft.data.client.ExtraModelsProvider;
import dan200.computercraft.shared.turtle.TurtleOverlay; import dan200.computercraft.shared.turtle.TurtleOverlay;
import dan200.computercraft.shared.turtle.inventory.UpgradeSlot;
import net.minecraft.Util; import net.minecraft.Util;
import net.minecraft.client.renderer.texture.atlas.SpriteSource;
import net.minecraft.client.renderer.texture.atlas.SpriteSources;
import net.minecraft.client.renderer.texture.atlas.sources.SingleFile;
import net.minecraft.core.HolderLookup; import net.minecraft.core.HolderLookup;
import net.minecraft.core.RegistrySetBuilder; import net.minecraft.core.RegistrySetBuilder;
import net.minecraft.data.DataProvider; import net.minecraft.data.DataProvider;
@ -20,10 +27,15 @@ import net.minecraft.server.packs.PackType;
import net.minecraft.world.item.Item; import net.minecraft.world.item.Item;
import net.minecraft.world.level.block.Block; import net.minecraft.world.level.block.Block;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import java.util.Set; import java.util.Set;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
import java.util.function.BiConsumer; import java.util.function.BiConsumer;
import java.util.function.Consumer; import java.util.function.Consumer;
import java.util.function.Function;
import java.util.stream.Stream;
/** /**
* All data providers for ComputerCraft. We require a mod-loader abstraction {@link GeneratorSink} (instead of * All data providers for ComputerCraft. We require a mod-loader abstraction {@link GeneratorSink} (instead of
@ -55,15 +67,37 @@ public final class DataProviders {
generator.add(out -> new LanguageProvider(out, fullRegistries)); generator.add(out -> new LanguageProvider(out, fullRegistries));
// Unfortunately we rely on some client-side classes in this code. We just load in the client side data provider generator.addFromCodec("Block atlases", PackType.CLIENT_RESOURCES, "atlases", SpriteSources.FILE_CODEC, out -> {
// and invoke that. out.accept(ResourceLocation.withDefaultNamespace("blocks"), makeSprites(Stream.of(
try { UpgradeSlot.LEFT_UPGRADE,
Class.forName("dan200.computercraft.data.client.ClientDataProviders") UpgradeSlot.RIGHT_UPGRADE,
.getMethod("add", GeneratorSink.class, CompletableFuture.class) LecternPrintoutModel.TEXTURE
.invoke(null, generator, fullRegistries); )));
} catch (ReflectiveOperationException e) { out.accept(GuiSprites.SPRITE_SHEET, makeSprites(
throw new RuntimeException(e); // Buttons
GuiSprites.TURNED_OFF.textures(),
GuiSprites.TURNED_ON.textures(),
GuiSprites.TERMINATE.textures(),
// Computers
GuiSprites.COMPUTER_NORMAL.textures(),
GuiSprites.COMPUTER_ADVANCED.textures(),
GuiSprites.COMPUTER_COMMAND.textures(),
GuiSprites.COMPUTER_COLOUR.textures()
));
});
generator.add(pack -> new ExtraModelsProvider(pack, fullRegistries) {
@Override
public Stream<ResourceLocation> getModels(HolderLookup.Provider registries) {
return registries.lookupOrThrow(TurtleOverlay.REGISTRY).listElements().map(x -> x.value().model());
} }
});
}
@SafeVarargs
@SuppressWarnings("varargs")
private static List<SpriteSource> makeSprites(final Stream<ResourceLocation>... files) {
return Arrays.stream(files).flatMap(Function.identity()).<SpriteSource>map(x -> new SingleFile(x, Optional.empty())).toList();
} }
public interface GeneratorSink { public interface GeneratorSink {

View File

@ -7,6 +7,7 @@ package dan200.computercraft.data;
import com.google.common.hash.HashCode; import com.google.common.hash.HashCode;
import com.google.common.hash.HashFunction; import com.google.common.hash.HashFunction;
import com.google.common.hash.Hashing; import com.google.common.hash.Hashing;
import dan200.computercraft.shared.util.PrettyJsonWriter;
import net.minecraft.data.CachedOutput; import net.minecraft.data.CachedOutput;
import net.minecraft.data.DataProvider; import net.minecraft.data.DataProvider;

View File

@ -148,7 +148,7 @@ class TagProvider {
/** /**
* A wrapper over {@link ItemTagsProvider}. * A wrapper over {@link ItemTagsProvider}.
*/ */
interface ItemTagConsumer extends TagConsumer<Item> { public interface ItemTagConsumer extends TagConsumer<Item> {
void copy(TagKey<Block> block, TagKey<Item> item); void copy(TagKey<Block> block, TagKey<Item> item);
} }
} }

View File

@ -19,11 +19,11 @@ import java.util.stream.Stream;
/** /**
* A data provider to generate {@link ExtraModels}. * A data provider to generate {@link ExtraModels}.
*/ */
abstract class ExtraModelsProvider implements DataProvider { public abstract class ExtraModelsProvider implements DataProvider {
private final Path path; private final Path path;
private final CompletableFuture<HolderLookup.Provider> registries; private final CompletableFuture<HolderLookup.Provider> registries;
ExtraModelsProvider(PackOutput output, CompletableFuture<HolderLookup.Provider> registries) { public ExtraModelsProvider(PackOutput output, CompletableFuture<HolderLookup.Provider> registries) {
path = output.getOutputFolder(PackOutput.Target.RESOURCE_PACK).resolve(ExtraModels.PATH.getNamespace()).resolve(ExtraModels.PATH.getPath()); path = output.getOutputFolder(PackOutput.Target.RESOURCE_PACK).resolve(ExtraModels.PATH.getNamespace()).resolve(ExtraModels.PATH.getPath());
this.registries = registries; this.registries = registries;
} }

View File

@ -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.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": "Execution",
"gui.computercraft.config.execution.computer_threads": "Computer threads", "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": "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": "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.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": "Floppy Disk space limit (bytes)",
"gui.computercraft.config.floppy_space_limit.tooltip": "The disk space limit for floppy disks, in 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": "HTTP",
"gui.computercraft.config.http.bandwidth": "Bandwidth", "gui.computercraft.config.http.bandwidth": "Bandwidth",
"gui.computercraft.config.http.bandwidth.global_download": "Global download limit", "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": "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.bandwidth.tooltip": "Limits bandwidth used by computers.",
"gui.computercraft.config.http.enabled": "Enable the HTTP API", "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.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": "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": "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": "Proxy",
"gui.computercraft.config.http.proxy.host": "Host name", "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.host.tooltip": "The hostname or IP address of the proxy server.",
"gui.computercraft.config.http.proxy.port": "Port", "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.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": "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": "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.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", "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": "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.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": "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": "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": "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": "Peripherals",
"gui.computercraft.config.peripheral.command_block_enabled": "Enable command block peripheral", "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.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": "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": "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": "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": "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": "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": "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.peripheral.tooltip": "Various options relating to peripherals.",
"gui.computercraft.config.term_sizes": "Terminal sizes", "gui.computercraft.config.term_sizes": "Terminal sizes",
"gui.computercraft.config.term_sizes.computer": "Computer", "gui.computercraft.config.term_sizes.computer": "Computer",
"gui.computercraft.config.term_sizes.computer.height": "Terminal height", "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.tooltip": "Terminal size of computers.",
"gui.computercraft.config.term_sizes.computer.width": "Terminal width", "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": "Monitor",
"gui.computercraft.config.term_sizes.monitor.height": "Max monitor height", "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.tooltip": "Maximum size of monitors (in blocks).",
"gui.computercraft.config.term_sizes.monitor.width": "Max monitor width", "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": "Pocket Computer",
"gui.computercraft.config.term_sizes.pocket_computer.height": "Terminal height", "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.tooltip": "Terminal size of pocket computers.",
"gui.computercraft.config.term_sizes.pocket_computer.width": "Terminal width", "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.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": "Turtles",
"gui.computercraft.config.turtle.advanced_fuel_limit": "Advanced Turtle fuel limit", "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": "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.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": "Enable fuel",
"gui.computercraft.config.turtle.need_fuel.tooltip": "Set whether Turtles require fuel to move.", "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": "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.turtle.tooltip": "Various options relating to turtles.",
"gui.computercraft.config.upload_max_size": "File upload size limit (bytes)", "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": "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.pocket_computer_overlay": "Pocket computer open. Press ESC to close.",
"gui.computercraft.terminal": "Computer terminal", "gui.computercraft.terminal": "Computer terminal",
"gui.computercraft.tooltip.computer_id": "Computer ID: %s", "gui.computercraft.tooltip.computer_id": "Computer ID: %s",

View File

@ -9,9 +9,7 @@ import com.google.common.base.Splitter;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import javax.annotation.OverridingMethodsMustInvokeSuper; import javax.annotation.OverridingMethodsMustInvokeSuper;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.ArrayDeque; import java.util.*;
import java.util.Deque;
import java.util.List;
import java.util.function.Predicate; import java.util.function.Predicate;
import java.util.function.Supplier; import java.util.function.Supplier;
import java.util.stream.Stream; import java.util.stream.Stream;
@ -19,27 +17,47 @@ import java.util.stream.Stream;
/** /**
* A config file which the user can modify. * A config file which the user can modify.
*/ */
public interface ConfigFile { public abstract class ConfigFile {
String TRANSLATION_PREFIX = "gui.computercraft.config."; public static final String TRANSLATION_PREFIX = "gui.computercraft.config.";
Splitter SPLITTER = Splitter.on('.'); public static final Splitter SPLITTER = Splitter.on('.');
/** /**
* An entry in the config file, either a {@link Value} or {@linkplain Group group of other entries}. * 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. * Get the translation key of this config entry.
* *
* @return This entry's translation key. * @return This entry's translation key.
*/ */
String translationKey(); public final String translationKey() {
return translationKey;
}
/** /**
* Get the comment about this config entry. * Get the comment about this config entry.
* *
* @return The comment for this config entry. * @return The comment for this config entry.
*/ */
String comment(); public final String comment() {
return comment;
}
abstract Stream<Entry> entries();
} }
/** /**
@ -47,13 +65,38 @@ public interface ConfigFile {
* *
* @param <T> The type of the stored value. * @param <T> The type of the stored value.
*/ */
non-sealed interface Value<T> extends Entry, Supplier<T> { public abstract static non-sealed class Value<T> extends Entry implements Supplier<T> {
protected Value(String translationKey, String comment) {
super(translationKey, comment);
}
@Override
Stream<Entry> entries() {
return Stream.of(this);
}
} }
/** /**
* A group of config entries. * A group of config entries.
*/ */
non-sealed interface Group extends Entry { public static final class Group extends Entry {
private final Map<String, Entry> children;
public Group(String translationKey, String comment, Map<String, Entry> children) {
super(translationKey, comment);
this.children = children;
}
@Override
Stream<Entry> entries() {
return Stream.concat(Stream.of(this), children.values().stream().flatMap(Entry::entries));
}
}
protected final Map<String, Entry> entries;
protected ConfigFile(Map<String, Entry> entries) {
this.entries = entries;
} }
/** /**
@ -61,16 +104,46 @@ public interface ConfigFile {
* *
* @return All config keys. * @return All config keys.
*/ */
Stream<Entry> entries(); public final Stream<Entry> entries() {
return entries.values().stream().flatMap(Entry::entries);
}
@Nullable public final @Nullable Entry getEntry(String path) {
Entry getEntry(String path); var iterator = SPLITTER.split(path).iterator();
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. * A builder which can be used to generate a config object.
*/ */
abstract class Builder { public abstract static class Builder {
protected final Deque<String> groupStack = new ArrayDeque<>(); protected record RootGroup(String path, Map<String, Entry> children) {
public RootGroup {
}
}
protected final Deque<RootGroup> 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) { protected String getTranslation(String name) {
var key = new StringBuilder(TRANSLATION_PREFIX); var key = new StringBuilder(TRANSLATION_PREFIX);
@ -86,7 +159,19 @@ public interface ConfigFile {
* @param comment The comment. * @param comment The comment.
* @return The current object, for chaining. * @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. * Push a new config group.
@ -95,7 +180,10 @@ public interface ConfigFile {
*/ */
@OverridingMethodsMustInvokeSuper @OverridingMethodsMustInvokeSuper
public void push(String name) { public void push(String name) {
groupStack.addLast(name); var path = getPath(name);
Map<String, Entry> children = new HashMap<>();
groupStack.getLast().children().put(name, new Group(path, takeComment(), children));
groupStack.addLast(new RootGroup(path, children));
} }
/** /**
@ -113,22 +201,22 @@ public interface ConfigFile {
*/ */
public abstract Builder worldRestart(); public abstract Builder worldRestart();
public abstract <T> ConfigFile.Value<T> define(String path, T defaultValue); public abstract <T> ConfigFile.Value<T> define(String name, T defaultValue);
/** /**
* A boolean-specific override of the above {@link #define(String, Object)} method. * 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. * @param defaultValue The default value.
* @return The accessor for this config option. * @return The accessor for this config option.
*/ */
public abstract ConfigFile.Value<Boolean> define(String path, boolean defaultValue); public abstract ConfigFile.Value<Boolean> define(String name, boolean defaultValue);
public abstract ConfigFile.Value<Integer> defineInRange(String path, int defaultValue, int min, int max); public abstract ConfigFile.Value<Integer> defineInRange(String name, int defaultValue, int min, int max);
public abstract <T> ConfigFile.Value<List<? extends T>> defineList(String path, List<? extends T> defaultValue, Supplier<T> newValue, Predicate<Object> elementValidator); public abstract <T> ConfigFile.Value<List<? extends T>> defineList(String name, List<? extends T> defaultValue, Supplier<T> newValue, Predicate<Object> elementValidator);
public abstract <V extends Enum<V>> ConfigFile.Value<V> defineEnum(String path, V defaultValue); public abstract <V extends Enum<V>> ConfigFile.Value<V> defineEnum(String name, V defaultValue);
/** /**
* Finalise this config file. * Finalise this config file.
@ -140,7 +228,7 @@ public interface ConfigFile {
} }
@FunctionalInterface @FunctionalInterface
interface ConfigListener { public interface ConfigListener {
/** /**
* The function called then a config file is changed. * The function called then a config file is changed.
* *

View File

@ -344,18 +344,18 @@ public final class ConfigSpec {
.push("term_sizes"); .push("term_sizes");
builder.comment("Terminal size of computers.").push("computer"); builder.comment("Terminal size of computers.").push("computer");
computerTermWidth = builder.defineInRange("width", Config.computerTermWidth, 1, 255); computerTermWidth = builder.comment("Width of computer terminal").defineInRange("width", Config.computerTermWidth, 1, 255);
computerTermHeight = builder.defineInRange("height", Config.computerTermHeight, 1, 255); computerTermHeight = builder.comment("Height of computer terminal").defineInRange("height", Config.computerTermHeight, 1, 255);
builder.pop(); builder.pop();
builder.comment("Terminal size of pocket computers.").push("pocket_computer"); builder.comment("Terminal size of pocket computers.").push("pocket_computer");
pocketTermWidth = builder.defineInRange("width", Config.pocketTermWidth, 1, 255); pocketTermWidth = builder.comment("Width of pocket computer terminal").defineInRange("width", Config.pocketTermWidth, 1, 255);
pocketTermHeight = builder.defineInRange("height", Config.pocketTermHeight, 1, 255); pocketTermHeight = builder.comment("Height of pocket computer terminal").defineInRange("height", Config.pocketTermHeight, 1, 255);
builder.pop(); builder.pop();
builder.comment("Maximum size of monitors (in blocks).").push("monitor"); builder.comment("Maximum size of monitors (in blocks).").push("monitor");
monitorWidth = builder.defineInRange("width", Config.monitorWidth, 1, 32); monitorWidth = builder.comment("Maximum width of monitors").defineInRange("width", Config.monitorWidth, 1, 32);
monitorHeight = builder.defineInRange("height", Config.monitorHeight, 1, 32); monitorHeight = builder.comment("Maximum height of monitors").defineInRange("height", Config.monitorHeight, 1, 32);
builder.pop(); builder.pop();
builder.pop(); builder.pop();

View File

@ -21,7 +21,11 @@ import javax.annotation.Nullable;
* Monitors act as [terminal redirects][`term.Redirect`] and so expose the same methods, as well as several additional * Monitors act as [terminal redirects][`term.Redirect`] and so expose the same methods, as well as several additional
* ones, which are documented below. * ones, which are documented below.
* <p> * <p>
* Like computers, monitors come in both normal (no colour) and advanced (colour) varieties. * If the monitor is resized (by adding new blocks to the monitor, or by calling {@link setTextScale}), then a
* [`monitor_resize`] event will be queued.
* <p>
* Like computers, monitors come in both normal (no colour) and advanced (colour) varieties. Advanced monitors be right
* clicked, which will trigger a [`monitor_touch`] event.
* <p> * <p>
* ## Recipes * ## Recipes
* <div class="recipe-container"> * <div class="recipe-container">
@ -37,6 +41,9 @@ import javax.annotation.Nullable;
* monitor.setCursorPos(1, 1) * monitor.setCursorPos(1, 1)
* monitor.write("Hello, world!") * monitor.write("Hello, world!")
* }</pre> * }</pre>
*
* @cc.see monitor_resize Queued when a monitor is resized.
* @cc.see monitor_touch Queued when an advanced monitor is clicked.
*/ */
public class MonitorPeripheral extends TermMethods implements IPeripheral { public class MonitorPeripheral extends TermMethods implements IPeripheral {
private final MonitorBlockEntity monitor; private final MonitorBlockEntity monitor;

View File

@ -2,7 +2,7 @@
// //
// SPDX-License-Identifier: MPL-2.0 // SPDX-License-Identifier: MPL-2.0
package dan200.computercraft.data; package dan200.computercraft.shared.util;
import com.google.gson.Gson; import com.google.gson.Gson;
import com.google.gson.GsonBuilder; import com.google.gson.GsonBuilder;
@ -23,7 +23,7 @@ import java.util.List;
/** /**
* Alternative version of {@link JsonWriter} which attempts to lay out the JSON in a more compact format. * Alternative version of {@link JsonWriter} which attempts to lay out the JSON in a more compact format.
* *
* @see PrettyDataProvider * @see dan200.computercraft.data.PrettyDataProvider
*/ */
public class PrettyJsonWriter extends JsonWriter { public class PrettyJsonWriter extends JsonWriter {
private static final Gson GSON = new GsonBuilder().setPrettyPrinting().disableHtmlEscaping().create(); private static final Gson GSON = new GsonBuilder().setPrettyPrinting().disableHtmlEscaping().create();

View File

@ -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 <K> The type of keys in this trie.
* @param <V> The values in this map.
*/
public class Trie<K, V> {
private @Nullable V current;
private @Nullable Map<K, Trie<K, V>> children;
public Trie<K, V> getChild(Iterable<K> 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<K> key) {
return getChild(key).current;
}
public void setValue(Iterable<K> key, V value) {
getChild(key).current = value;
}
public Stream<V> stream() {
return Stream.concat(
current == null ? Stream.empty() : Stream.of(current),
children == null ? Stream.empty() : children.values().stream().flatMap(Trie::stream)
);
}
}

View File

@ -14,9 +14,9 @@ import com.mojang.brigadier.arguments.StringArgumentType;
import com.mojang.brigadier.builder.LiteralArgumentBuilder; import com.mojang.brigadier.builder.LiteralArgumentBuilder;
import com.mojang.brigadier.builder.RequiredArgumentBuilder; import com.mojang.brigadier.builder.RequiredArgumentBuilder;
import dan200.computercraft.api.ComputerCraftAPI; import dan200.computercraft.api.ComputerCraftAPI;
import dan200.computercraft.data.PrettyJsonWriter;
import dan200.computercraft.gametest.core.TestHooks; import dan200.computercraft.gametest.core.TestHooks;
import dan200.computercraft.shared.util.RegistryHelper; import dan200.computercraft.shared.util.RegistryHelper;
import dan200.computercraft.shared.util.PrettyJsonWriter;
import net.minecraft.client.Minecraft; import net.minecraft.client.Minecraft;
import net.minecraft.core.registries.BuiltInRegistries; import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.client.gui.GuiGraphics; import net.minecraft.client.gui.GuiGraphics;

View File

@ -8,7 +8,7 @@ import java.util.*
plugins { plugins {
id("cc-tweaked.fabric") id("cc-tweaked.fabric")
id("cc-tweaked.gametest") id("cc-tweaked.mod")
id("cc-tweaked.mod-publishing") id("cc-tweaked.mod-publishing")
} }
@ -147,6 +147,8 @@ loom {
configName = "Datagen" configName = "Datagen"
client() client()
source(sourceSets.datagen.get())
runDir("run/dataGen") runDir("run/dataGen")
property("fabric-api.datagen") property("fabric-api.datagen")
property("fabric-api.datagen.output-dir", layout.buildDirectory.dir("generatedResources").getAbsolutePath()) property("fabric-api.datagen.output-dir", layout.buildDirectory.dir("generatedResources").getAbsolutePath())

View File

@ -29,7 +29,7 @@ import java.util.concurrent.CompletableFuture;
import java.util.function.BiConsumer; import java.util.function.BiConsumer;
import java.util.function.Consumer; import java.util.function.Consumer;
public class FabricDataGenerators implements DataGeneratorEntrypoint { public class FabricDataProviders implements DataGeneratorEntrypoint {
@Override @Override
public void onInitializeDataGenerator(FabricDataGenerator generator) { public void onInitializeDataGenerator(FabricDataGenerator generator) {
var pack = new PlatformGeneratorsImpl(generator.createPack(), generator.getRegistries()); var pack = new PlatformGeneratorsImpl(generator.createPack(), generator.getRegistries());

View File

@ -0,0 +1,13 @@
{
"schemaVersion": 1,
"id": "cc-datagen",
"version": "1.0.0",
"entrypoints": {
"fabric-datagen": [
"dan200.computercraft.data.FabricDataProviders"
]
},
"depends": {
"computercraft": "*"
}
}

View File

@ -11,7 +11,6 @@ import com.electronwill.nightconfig.core.file.FileWatcher;
import com.electronwill.nightconfig.core.io.WritingMode; import com.electronwill.nightconfig.core.io.WritingMode;
import com.google.errorprone.annotations.concurrent.GuardedBy; import com.google.errorprone.annotations.concurrent.GuardedBy;
import dan200.computercraft.shared.config.ConfigFile; import dan200.computercraft.shared.config.ConfigFile;
import dan200.computercraft.shared.util.Trie;
import org.apache.commons.lang3.function.TriFunction; import org.apache.commons.lang3.function.TriFunction;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@ -22,6 +21,7 @@ import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.function.Predicate; import java.util.function.Predicate;
import java.util.function.Supplier; import java.util.function.Supplier;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@ -30,18 +30,17 @@ import java.util.stream.Stream;
/** /**
* A {@link ConfigFile} which sits directly on top of NightConfig. * 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 static final Logger LOG = LoggerFactory.getLogger(FabricConfigFile.class);
private final ConfigSpec spec; private final ConfigSpec spec;
private final Trie<String, Entry> entries;
private final ConfigListener onChange; private final ConfigListener onChange;
private @Nullable CommentedFileConfig config; private @Nullable CommentedFileConfig config;
public FabricConfigFile(ConfigSpec spec, Trie<String, Entry> entries, ConfigListener onChange) { private FabricConfigFile(ConfigSpec spec, Map<String, Entry> entries, ConfigListener onChange) {
super(entries);
this.spec = spec; this.spec = spec;
this.entries = entries;
this.onChange = onChange; this.onChange = onChange;
} }
@ -76,7 +75,7 @@ public class FabricConfigFile implements ConfigFile {
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
private Stream<ValueImpl<?>> values() { private Stream<ValueImpl<?>> values() {
return (Stream<ValueImpl<?>>) (Stream<?>) entries.stream().filter(ValueImpl.class::isInstance); return (Stream<ValueImpl<?>>) (Stream<?>) entries().filter(ValueImpl.class::isInstance);
} }
public synchronized void unload() { public synchronized void unload() {
@ -104,7 +103,7 @@ public class FabricConfigFile implements ConfigFile {
// Ensure the config file matches the spec // Ensure the config file matches the spec
var isNewFile = config.isEmpty(); 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) -> { 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); LOG.warn("Incorrect key {} was corrected from {} to {}", String.join(".", entryPath), oldValue, newValue);
}); });
@ -116,148 +115,77 @@ public class FabricConfigFile implements ConfigFile {
return corrected > 0; return corrected > 0;
} }
@Override
public Stream<ConfigFile.Entry> 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 { static class Builder extends ConfigFile.Builder {
private final ConfigSpec spec = new ConfigSpec(); private final ConfigSpec spec = new ConfigSpec();
private final Trie<String, Entry> 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 @Override
public ConfigFile.Builder worldRestart() { public ConfigFile.Builder worldRestart() {
return this; return this;
} }
private <T> Value<T> defineValue(String fullPath, String comment, T defaultValue, TriFunction<Config, String, T, T> getter) { private <T> Value<T> defineValue(String name, String comment, @Nullable String suffix, T defaultValue, TriFunction<Config, String, T, T> getter) {
var value = new ValueImpl<T>(fullPath, comment, defaultValue, getter); var fullComment = suffix == null ? comment : comment + "\n" + suffix;
entries.setValue(SPLITTER.split(fullPath), value); var value = new ValueImpl<T>(getPath(name), comment, fullComment, defaultValue, getter);
groupStack.getLast().children().put(name, value);
return value; return value;
} }
@Override @Override
public <T> Value<T> define(String path, T defaultValue) { public <T> Value<T> define(String name, T defaultValue) {
var fullPath = getFullPath(path); var path = getPath(name);
spec.define(fullPath, defaultValue); spec.define(path, defaultValue);
return defineValue(fullPath, takeComment(), defaultValue, Config::getOrElse); return defineValue(name, takeComment(), null, defaultValue, Config::getOrElse);
} }
@Override @Override
public Value<Boolean> define(String path, boolean defaultValue) { public Value<Boolean> define(String name, boolean defaultValue) {
var fullPath = getFullPath(path); var path = getPath(name);
spec.define(fullPath, defaultValue, x -> x instanceof Boolean); spec.define(path, defaultValue, x -> x instanceof Boolean);
return defineValue(fullPath, takeComment(), defaultValue, UnmodifiableConfig::getOrElse); return defineValue(name, takeComment(), null, defaultValue, UnmodifiableConfig::getOrElse);
} }
@Override @Override
public Value<Integer> defineInRange(String path, int defaultValue, int min, int max) { public Value<Integer> defineInRange(String name, int defaultValue, int min, int max) {
var fullPath = getFullPath(path); var path = getPath(name);
spec.defineInRange(fullPath, defaultValue, min, max); spec.defineInRange(path, defaultValue, min, max);
var suffix = max == Integer.MAX_VALUE ? "Range: > " + min : "Range: " + 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 @Override
public <T> Value<List<? extends T>> defineList(String path, List<? extends T> defaultValue, Supplier<T> newValue, Predicate<Object> elementValidator) { public <T> Value<List<? extends T>> defineList(String name, List<? extends T> defaultValue, Supplier<T> newValue, Predicate<Object> elementValidator) {
var fullPath = getFullPath(path); var path = getPath(name);
spec.defineList(fullPath, defaultValue, elementValidator); spec.defineList(path, defaultValue, elementValidator);
return defineValue(fullPath, takeComment(), defaultValue, Config::getOrElse); return defineValue(name, takeComment(), null, defaultValue, Config::getOrElse);
} }
@Override @Override
public <V extends Enum<V>> Value<V> defineEnum(String path, V defaultValue) { public <V extends Enum<V>> Value<V> defineEnum(String name, V defaultValue) {
var fullPath = getFullPath(path); var path = getPath(name);
spec.define(fullPath, defaultValue, o -> o != null && o != NullObject.NULL_OBJECT && EnumGetMethod.NAME_IGNORECASE.validate(o, defaultValue.getDeclaringClass())); 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(", ")); 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 @Override
public ConfigFile build(ConfigListener onChange) { public ConfigFile build(ConfigListener onChange) {
return new FabricConfigFile(spec, entries, onChange); var children = groupStack.removeLast().children();
if (!groupStack.isEmpty()) throw new IllegalStateException("Mismatched config push/pop");
return new FabricConfigFile(spec, children, onChange);
} }
} }
private static class Entry { private static final class ValueImpl<T> extends Value<T> {
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);
}
}
private static final class ValueImpl<T> extends Entry implements Value<T> {
private @Nullable T value; private @Nullable T value;
private final T defaultValue; private final T defaultValue;
private final TriFunction<Config, String, T, T> get; private final TriFunction<Config, String, T, T> get;
private final String fullComment;
private ValueImpl(String path, String comment, T defaultValue, TriFunction<Config, String, T, T> get) { private ValueImpl(String path, String comment, String fullComment, T defaultValue, TriFunction<Config, String, T, T> get) {
super(path, comment); super(path, comment);
this.fullComment = fullComment;
this.defaultValue = defaultValue; this.defaultValue = defaultValue;
this.get = get; this.get = get;
} }

View File

@ -23,9 +23,6 @@
"client": [ "client": [
"dan200.computercraft.client.ComputerCraftClient::init" "dan200.computercraft.client.ComputerCraftClient::init"
], ],
"fabric-datagen": [
"dan200.computercraft.data.FabricDataGenerators"
],
"jei_mod_plugin": [ "jei_mod_plugin": [
"dan200.computercraft.client.integration.jei.JEIComputerCraft" "dan200.computercraft.client.integration.jei.JEIComputerCraft"
], ],

View File

@ -1,6 +1,6 @@
{ {
"schemaVersion": 1, "schemaVersion": 1,
"id": "cctest", "id": "cc-datagen",
"version": "1.0.0", "version": "1.0.0",
"entrypoints": { "entrypoints": {
"main": [ "main": [

View File

@ -7,7 +7,7 @@ import net.neoforged.gradle.dsl.common.runs.run.Run
plugins { plugins {
id("cc-tweaked.forge") id("cc-tweaked.forge")
id("cc-tweaked.gametest") id("cc-tweaked.mod")
id("cc-tweaked.mod-publishing") id("cc-tweaked.mod-publishing")
} }
@ -65,6 +65,8 @@ runs {
"--existing", project(":common").file("src/main/resources/").absolutePath, "--existing", project(":common").file("src/main/resources/").absolutePath,
"--existing", file("src/main/resources/").absolutePath, "--existing", file("src/main/resources/").absolutePath,
) )
modSources.add("computercraft", sourceSets.datagen.get())
} }
fun Run.configureForGameTest() { fun Run.configureForGameTest() {

View File

@ -32,7 +32,7 @@ import java.util.function.BiConsumer;
import java.util.function.Consumer; import java.util.function.Consumer;
@EventBusSubscriber(bus = EventBusSubscriber.Bus.MOD) @EventBusSubscriber(bus = EventBusSubscriber.Bus.MOD)
public class Generators { public class ForgeDataProviders {
@SubscribeEvent @SubscribeEvent
public static void gather(GatherDataEvent event) { public static void gather(GatherDataEvent event) {
var generator = event.getGenerator(); var generator = event.getGenerator();

View File

@ -5,50 +5,33 @@
package dan200.computercraft.shared.platform; package dan200.computercraft.shared.platform;
import dan200.computercraft.shared.config.ConfigFile; import dan200.computercraft.shared.config.ConfigFile;
import dan200.computercraft.shared.util.Trie;
import net.neoforged.neoforge.common.ModConfigSpec; import net.neoforged.neoforge.common.ModConfigSpec;
import javax.annotation.Nullable;
import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Objects; import java.util.Map;
import java.util.function.Predicate; import java.util.function.Predicate;
import java.util.function.Supplier; import java.util.function.Supplier;
import java.util.stream.Stream;
/** /**
* A {@link ConfigFile} which wraps Forge's config implementation. * A {@link ConfigFile} which wraps Forge's config implementation.
*/ */
public final class ForgeConfigFile implements ConfigFile { public final class ForgeConfigFile extends ConfigFile {
private final ModConfigSpec spec; private final ModConfigSpec spec;
private final Trie<String, ConfigFile.Entry> entries;
public ForgeConfigFile(ModConfigSpec spec, Trie<String, Entry> entries) { private ForgeConfigFile(ModConfigSpec spec, Map<String, Entry> entries) {
super(entries);
this.spec = spec; this.spec = spec;
this.entries = entries;
} }
public ModConfigSpec spec() { public ModConfigSpec spec() {
return spec; return spec;
} }
@Override
public Stream<Entry> entries() {
return entries.stream();
}
@Nullable
@Override
public Entry getEntry(String path) {
return entries.getValue(SPLITTER.split(path));
}
/** /**
* Wraps {@link ModConfigSpec.Builder} into our own config builder abstraction. * Wraps {@link ModConfigSpec.Builder} into our own config builder abstraction.
*/ */
static class Builder extends ConfigFile.Builder { static class Builder extends ConfigFile.Builder {
private final ModConfigSpec.Builder builder = new ModConfigSpec.Builder(); private final ModConfigSpec.Builder builder = new ModConfigSpec.Builder();
private final Trie<String, ConfigFile.Entry> entries = new Trie<>();
private void translation(String name) { private void translation(String name) {
builder.translation(getTranslation(name)); builder.translation(getTranslation(name));
@ -56,24 +39,23 @@ public final class ForgeConfigFile implements ConfigFile {
@Override @Override
public ConfigFile.Builder comment(String comment) { public ConfigFile.Builder comment(String comment) {
super.comment(comment);
builder.comment(comment); builder.comment(comment);
return this; return this;
} }
@Override @Override
public void push(String name) { public void push(String name) {
super.push(name);
translation(name); translation(name);
builder.push(name); builder.push(name);
super.push(name);
} }
@Override @Override
public void pop() { public void pop() {
var path = new ArrayList<>(groupStack);
entries.setValue(path, new GroupImpl(path));
builder.pop();
super.pop(); super.pop();
builder.pop();
} }
@Override @Override
@ -82,100 +64,63 @@ public final class ForgeConfigFile implements ConfigFile {
return this; return this;
} }
private <T> ConfigFile.Value<T> defineValue(ModConfigSpec.ConfigValue<T> value) { private <T> ConfigFile.Value<T> defineValue(String name, ModConfigSpec.ConfigValue<T> value) {
var wrapped = new ValueImpl<>(value); var wrapped = new ValueImpl<>(getPath(name), takeComment(), value);
entries.setValue(value.getPath(), wrapped); groupStack.getLast().children().put(name, wrapped);
return wrapped; return wrapped;
} }
@Override @Override
public <T> ConfigFile.Value<T> define(String path, T defaultValue) { public <T> ConfigFile.Value<T> define(String name, T defaultValue) {
translation(path); translation(name);
return defineValue(builder.define(path, defaultValue)); return defineValue(name, builder.define(name, defaultValue));
} }
@Override @Override
public ConfigFile.Value<Boolean> define(String path, boolean defaultValue) { public ConfigFile.Value<Boolean> define(String name, boolean defaultValue) {
translation(path); translation(name);
return defineValue(builder.define(path, defaultValue)); return defineValue(name, builder.define(name, defaultValue));
} }
@Override @Override
public ConfigFile.Value<Integer> defineInRange(String path, int defaultValue, int min, int max) { public ConfigFile.Value<Integer> defineInRange(String name, int defaultValue, int min, int max) {
translation(path); translation(name);
return defineValue(builder.defineInRange(path, defaultValue, min, max)); return defineValue(name, builder.defineInRange(name, defaultValue, min, max));
} }
@Override @Override
public <T> ConfigFile.Value<List<? extends T>> defineList(String path, List<? extends T> defaultValue, Supplier<T> newValue, Predicate<Object> elementValidator) { public <T> ConfigFile.Value<List<? extends T>> defineList(String name, List<? extends T> defaultValue, Supplier<T> newValue, Predicate<Object> elementValidator) {
translation(path); translation(name);
return defineValue(builder.defineList(path, defaultValue, newValue, elementValidator)); return defineValue(name, builder.defineList(name, defaultValue, newValue, elementValidator));
} }
@Override @Override
public <V extends Enum<V>> ConfigFile.Value<V> defineEnum(String path, V defaultValue) { public <V extends Enum<V>> ConfigFile.Value<V> defineEnum(String name, V defaultValue) {
translation(path); translation(name);
return defineValue(builder.defineEnum(path, defaultValue)); return defineValue(name, builder.defineEnum(name, defaultValue));
} }
@Override @Override
public ConfigFile build(ConfigListener onChange) { public ConfigFile build(ConfigListener onChange) {
var children = groupStack.removeLast().children();
if (!groupStack.isEmpty()) throw new IllegalStateException("Mismatched config push/pop");
var spec = builder.build(); var spec = builder.build();
entries.stream().forEach(x -> { return new ForgeConfigFile(spec, children);
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 static final class ValueImpl<T> extends Value<T> {
private final List<String> path;
private @Nullable ModConfigSpec owner;
private GroupImpl(List<String> 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);
}
}
private static final class ValueImpl<T> implements ConfigFile.Value<T> {
private final ModConfigSpec.ConfigValue<T> value; private final ModConfigSpec.ConfigValue<T> value;
private @Nullable ModConfigSpec owner;
private ValueImpl(ModConfigSpec.ConfigValue<T> value) { private ValueImpl(String path, String comment, ModConfigSpec.ConfigValue<T> value) {
super(path, comment);
this.value = value; this.value = value;
} }
private ModConfigSpec.ValueSpec spec() {
if (owner == null) throw new IllegalStateException("Config has not been built yet");
return owner.getSpec().get(value.getPath());
}
@Override @Override
public T get() { public T get() {
return value.get(); return value.get();
} }
@Override
public String translationKey() {
return Objects.requireNonNull(spec().getTranslationKey(), "No comment for value");
}
@Override
public String comment() {
return Objects.requireNonNull(spec().getComment(), "No comment for value");
}
} }
} }