diff --git a/build.gradle b/build.gradle index 49826ad0..eaac73ed 100644 --- a/build.gradle +++ b/build.gradle @@ -9,6 +9,7 @@ plugins { // Import-block step for Spotless and its fixture tests (docs/code-format.md). apply from: 'gradle/import-blocks.gradle' +apply from: 'gradle/line-limit.gradle' def env = System.getenv() def tag = providers.environmentVariable('TAG') @@ -216,12 +217,18 @@ tasks.named('javadoc', Javadoc) { } // Formatting (docs/code-format.md): Eclipse JDT with the shared profile in -// gradle/java-formatter.xml (VS Code uses the same one), then the import-block step. +// gradle/java-formatter.xml (VS Code uses the same one), then the import-block step. Lines the +// formatter can't wrap below 80 may run to 120; `lineLimit` (gradle/line-limit.gradle) warns past +// that, without failing. // Ratcheted: only files changed since the base branch are checked and formatted, so code is // brought over to this style as it is next edited. Never reformat unrelated files in a change. // spotlessCheck is part of check/build; `./gradlew spotlessApply` (or `format`) fixes files. +// `-PformatAll` drops the ratchet, so the whole tree is checked or formatted: use it once after +// changing the style, in its own commit (listed in .git-blame-ignore-revs). spotless { - ratchetFrom 'origin/master-2.0-1.21.1-neo' + if (!providers.gradleProperty('formatAll').isPresent()) { + ratchetFrom 'origin/master-2.0-1.21.1-neo' + } java { target 'src/**/*.java' eclipse('4.35') diff --git a/docs/code-format.md b/docs/code-format.md index c91daa3c..a5b4b5c6 100644 --- a/docs/code-format.md +++ b/docs/code-format.md @@ -4,9 +4,16 @@ Java follows the [Google Java Style Guide](https://google.github.io/styleguide/j with these overrides: - **Indentation: 4 spaces for both blocks and continuations.** Wrapped expressions and array - initializers add one four-space level, not eight spaces. Existing explicit line breaks are kept - where the formatter supports it. The 100-column limit and the other Google conventions (brace - style, one statement per line, naming, `@Override` always, no wildcard imports, ...) still apply. + initializers add one four-space level, not eight spaces. +- **Lines aim for 80 columns.** Parameter, argument and record component lists are packed up to + the limit and wrapped only where needed, never one per line; breaks that fit are rejoined. +- **Up to 120 columns for what can't be wrapped**, such as a long string constant. Past 120, + `./gradlew lineLimit` (part of `check`) warns with the file and line. It never fails the build: + some lines read worse split than long. +- **Wildcard imports are allowed**, static or not; in a mod they usually mean less code. +- **Keep methods short**: aim for 25 to 30 lines. This is a review guideline; no tool enforces it. +- The other Google conventions (brace style, one statement per line, naming, `@Override` always, + ...) still apply. - **Imports are split into blocks, one per second-level package, separated by a single blank line.** Order: 1. All static imports, as one block. @@ -15,7 +22,8 @@ with these overrides: `mezz.jei`, `net.minecraft`, `net.neoforged`, `org.slf4j`, ...). 4. MMDLib's own imports (`zone.moddev.*`, the library and the companion) last, as one block. - Within a block, imports are ASCII-sorted. Example: + Within a block, imports are ASCII-sorted, so a wildcard comes before that package's single-type + imports. Example: ```java import java.util.List; @@ -35,11 +43,11 @@ with these overrides: import zone.moddev.mc.lib.MMDLib; ``` -## Code written before this style +## Changing the style -Most of the code predates this style: it uses google-java-format's 2-space indentation. It is -**not** reformatted in bulk. A file is brought over to the new style when it is next edited, as -part of that change, and only then. Don't reformat files a change doesn't otherwise touch. +Day to day, formatting only touches the files a change edits (see the ratchet below); don't +reformat files a change doesn't otherwise touch. When the style itself changes, reformat the whole +tree once, as described under Tools. ## Tools @@ -53,11 +61,15 @@ Both are **ratcheted** from `origin/master-2.0-1.21.1-neo`: they only look at fi from that branch, which is how older files are left alone until edited. Keep that branch fetched; CI checks out with full history for this reason. +After changing the style, `./gradlew spotlessApply -PformatAll` drops the ratchet and formats the +whole tree. Commit that on its own and add the commit to `.git-blame-ignore-revs`, so `git blame` +skips it. + The Java formatter is Eclipse JDT with the `MMDLib` profile in `gradle/java-formatter.xml`. Imports are regrouped afterwards by a custom step, `gradle/import-blocks.gradle`, since Spotless's own `importOrder` can't express rule 3. The step refuses, with the line number, input it can't rewrite -safely: comments between imports, more than one import or a trailing comment on a line, and -wildcard imports. Its fixtures in `gradle/import-blocks/` run as `testImportBlocks`, part of +safely: comments between imports, and more than one import or a trailing comment on a line. Its +fixtures in `gradle/import-blocks/` run as `testImportBlocks`, part of `check`. ### VS Code @@ -69,6 +81,7 @@ The same profile formats in VS Code. `.vscode/` is not committed, so add this to "java.format.settings.url": "gradle/java-formatter.xml", "java.format.settings.profile": "MMDLib", "[java]": { + "editor.rulers": [80, 120], "editor.tabSize": 4, "editor.insertSpaces": true } diff --git a/gradle/import-blocks.gradle b/gradle/import-blocks.gradle index c198ae0d..615926f5 100644 --- a/gradle/import-blocks.gradle +++ b/gradle/import-blocks.gradle @@ -15,9 +15,11 @@ * this is a custom step. * * Input the step can't rewrite safely is rejected with an error naming the line, never guessed at: - * comments between imports (they would lose their position), more than one import or a trailing - * comment on an import line, and wildcard imports (forbidden by Google style, which - * docs/code-format.md follows). + * comments between imports (they would lose their position), and more than one import or a + * trailing comment on an import line. + * + * Wildcard imports, static or not, are allowed (unlike Google style): they go in their package's + * block like any other import, and sort ahead of that package's single-type imports. * * Applied from build.gradle, this script exposes: * - `importBlocksStep`: a closure for `spotless { java { custom 'importBlocks', ... } } }`; @@ -89,10 +91,6 @@ class ImportBlocks { + "import statement with nothing else on the line, found: ${line.trim()}") } String name = match.group(2) - if (match.group(3) != null) { - throw new IllegalArgumentException("line ${lineNumber}: wildcard imports are not " - + "allowed (Google style, docs/code-format.md): ${line.trim()}") - } if (match.group(1) != null) { statics.add("import static ${name};".toString()) } else { diff --git a/gradle/import-blocks/static-wildcard.error.txt b/gradle/import-blocks/static-wildcard.error.txt deleted file mode 100644 index 0de2a0c7..00000000 --- a/gradle/import-blocks/static-wildcard.error.txt +++ /dev/null @@ -1 +0,0 @@ -line 3: wildcard imports are not allowed diff --git a/gradle/import-blocks/wildcard-with-single-types.expected.txt b/gradle/import-blocks/wildcard-with-single-types.expected.txt new file mode 100644 index 00000000..8c62a2b2 --- /dev/null +++ b/gradle/import-blocks/wildcard-with-single-types.expected.txt @@ -0,0 +1,13 @@ +package zone.moddev.mc.lib; + +import static net.minecraft.core.Direction.*; + +import java.util.*; +import java.util.List; + +import net.minecraft.world.item.*; +import net.minecraft.world.item.ItemStack; + +import zone.moddev.mc.lib.api.*; + +public final class Example {} diff --git a/gradle/import-blocks/wildcard-with-single-types.input.txt b/gradle/import-blocks/wildcard-with-single-types.input.txt new file mode 100644 index 00000000..84d7efad --- /dev/null +++ b/gradle/import-blocks/wildcard-with-single-types.input.txt @@ -0,0 +1,10 @@ +package zone.moddev.mc.lib; + +import net.minecraft.world.item.ItemStack; +import static net.minecraft.core.Direction.*; +import zone.moddev.mc.lib.api.*; +import java.util.List; +import net.minecraft.world.item.*; +import java.util.*; + +public final class Example {} diff --git a/gradle/import-blocks/wildcard.error.txt b/gradle/import-blocks/wildcard.error.txt deleted file mode 100644 index 0de2a0c7..00000000 --- a/gradle/import-blocks/wildcard.error.txt +++ /dev/null @@ -1 +0,0 @@ -line 3: wildcard imports are not allowed diff --git a/gradle/java-formatter.xml b/gradle/java-formatter.xml index 49fe2bdc..71e00fc2 100644 --- a/gradle/java-formatter.xml +++ b/gradle/java-formatter.xml @@ -10,9 +10,22 @@ - - - + + + + + + + + + + + + + + + + diff --git a/gradle/line-limit.gradle b/gradle/line-limit.gradle new file mode 100644 index 00000000..2d4b7b8d --- /dev/null +++ b/gradle/line-limit.gradle @@ -0,0 +1,34 @@ +/* + * Soft line-length limit (docs/code-format.md). The Eclipse profile wraps at 80 columns, but it + * never splits what can't be split, such as a long string constant, and leaves such a line long. + * Up to 120 columns is fine for those. Past that, `lineLimit` warns, naming the file and line; it + * never fails the build, since some lines can't be shortened without hurting readability. + * + * Applied from build.gradle; `check` runs it. + */ + +def lineLimitSources = fileTree('src') { include '**/*.java' } + +tasks.register('lineLimit') { + group = 'verification' + description = 'Warns about Java lines longer than 120 columns (never fails).' + // No outputs: always runs, so the warnings are shown on every check, not only the first. + doLast { + int count = 0 + lineLimitSources.files.sort().each { File file -> + file.readLines('UTF-8').eachWithIndex { String line, int index -> + if (line.length() > 120) { + def path = project.relativePath(file) + logger.warn("${path}:${index + 1}: warning: line is ${line.length()} columns" + + ' (limit 120)') + count++ + } + } + } + if (count > 0) logger.warn("${count} line(s) over 120 columns") + } +} + +tasks.named('check') { + dependsOn 'lineLimit' +} diff --git a/src/clientSmoke/java/zone/moddev/mc/lib/smoke/CraftTweakerClientSmoke.java b/src/clientSmoke/java/zone/moddev/mc/lib/smoke/CraftTweakerClientSmoke.java index 5cb6424a..4b9ca2c0 100644 --- a/src/clientSmoke/java/zone/moddev/mc/lib/smoke/CraftTweakerClientSmoke.java +++ b/src/clientSmoke/java/zone/moddev/mc/lib/smoke/CraftTweakerClientSmoke.java @@ -17,150 +17,261 @@ import net.minecraft.world.level.*; import net.minecraft.world.level.levelgen.WorldOptions; import net.minecraft.world.level.levelgen.presets.WorldPresets; + import net.neoforged.api.distmarker.Dist; import net.neoforged.bus.api.SubscribeEvent; import net.neoforged.fml.ModList; import net.neoforged.fml.common.EventBusSubscriber; import net.neoforged.neoforge.client.event.ClientTickEvent; import net.neoforged.neoforge.client.event.RenderFrameEvent; + import zone.moddev.mc.lib.MMDLib; import zone.moddev.mc.lib.common.runtime.RedstoneBlock; -/** Opt-in real-client validation; injected only by crafttweaker-smoke.init.gradle. */ +/** + * Opt-in real-client validation; injected only by + * crafttweaker-smoke.init.gradle. + */ @EventBusSubscriber(modid = "mmdlib", value = Dist.CLIENT) public final class CraftTweakerClientSmoke { - private static boolean started, requested, finished; - private static volatile boolean ready; - private static int ticks, frames, stage; - private static final long DEADLINE = System.currentTimeMillis() + 240_000; - private static final BlockPos POS = new BlockPos(0, 100, 0); - private static final String[] ITEMS = {"client_alloy_ingot", "client_alloy_pickaxe", "client_alloy_block", "client_gate", "client_widget"}; + private static boolean started, requested, finished; + private static volatile boolean ready; + private static int ticks, frames, stage; + private static final long DEADLINE = System.currentTimeMillis() + 240_000; + private static final BlockPos POS = new BlockPos(0, 100, 0); + private static final String[] ITEMS = {"client_alloy_ingot", + "client_alloy_pickaxe", "client_alloy_block", "client_gate", + "client_widget"}; - private static ResourceLocation id(String path) { return ResourceLocation.parse("mmdlib:" + path); } - private static void require(boolean condition, String message) { if (!condition) throw new AssertionError(message); } - private static boolean present() { return ModList.get().isLoaded("crafttweaker"); } + private static ResourceLocation id(String path) { + return ResourceLocation.parse("mmdlib:" + path); + } + private static void require(boolean condition, String message) { + if (!condition) + throw new AssertionError(message); + } + private static boolean present() { + return ModList.get().isLoaded("crafttweaker"); + } - @SubscribeEvent - public static void tick(ClientTickEvent.Post event) { - if (!Boolean.getBoolean("mmdlib.craftTweakerSmoke") || finished) return; - var mc = Minecraft.getInstance(); - try { - require(System.currentTimeMillis() < DEADLINE, "client smoke timed out: " + mc.screen); - if (!started && mc.screen instanceof TitleScreen) { - started = true; - require(BuiltInRegistries.ITEM.containsKey(id("client_widget")) == present(), "optional integration registry boundary"); - var settings = new LevelSettings("CT smoke", GameType.CREATIVE, false, Difficulty.PEACEFUL, true, new GameRules(), WorldDataConfiguration.DEFAULT); - mc.createWorldOpenFlows().createFreshLevel("ct-smoke-" + System.currentTimeMillis(), settings, - new WorldOptions(1, false, false), r -> r.registryOrThrow(Registries.WORLD_PRESET) - .getHolderOrThrow(WorldPresets.FLAT).value().createWorldDimensions(), mc.screen); - } - if (mc.player == null || mc.level == null) return; - if (!requested) { - requested = true; - mc.getSingleplayerServer().execute(() -> { - try { - var player = mc.getSingleplayerServer().getPlayerList().getPlayers().getFirst(); - var level = player.serverLevel(); - level.setDayTime(6000); - for (int x = -6; x <= 6; x++) for (int z = -2; z <= 8; z++) - level.setBlock(POS.offset(x, -1, z), net.minecraft.world.level.block.Blocks.STONE.defaultBlockState(), 3); - if (present()) { - int x = -3; - for (var direction : Direction.Plane.HORIZONTAL) { - level.setBlock(POS.offset(x, 0, 0), BuiltInRegistries.BLOCK.get(id("client_gate")) - .defaultBlockState().setValue(RedstoneBlock.FACING, direction), 3); - x += 2; - } - level.setBlock(POS.offset(0, 0, -2), BuiltInRegistries.BLOCK.get(id("client_alloy_block")).defaultBlockState(), 3); + @SubscribeEvent + public static void tick(ClientTickEvent.Post event) { + if (!Boolean.getBoolean("mmdlib.craftTweakerSmoke") || finished) + return; + var mc = Minecraft.getInstance(); + try { + require(System.currentTimeMillis() < DEADLINE, + "client smoke timed out: " + mc.screen); + if (!started && mc.screen instanceof TitleScreen) { + started = true; + require( + BuiltInRegistries.ITEM + .containsKey(id("client_widget")) == present(), + "optional integration registry boundary"); + var settings = new LevelSettings("CT smoke", GameType.CREATIVE, + false, Difficulty.PEACEFUL, true, new GameRules(), + WorldDataConfiguration.DEFAULT); + mc.createWorldOpenFlows().createFreshLevel( + "ct-smoke-" + System.currentTimeMillis(), settings, + new WorldOptions(1, false, false), + r -> r.registryOrThrow(Registries.WORLD_PRESET) + .getHolderOrThrow(WorldPresets.FLAT).value() + .createWorldDimensions(), + mc.screen); + } + if (mc.player == null || mc.level == null) + return; + if (!requested) { + requested = true; + mc.getSingleplayerServer().execute(() -> { + try { + var player = mc.getSingleplayerServer().getPlayerList() + .getPlayers().getFirst(); + var level = player.serverLevel(); + level.setDayTime(6000); + for (int x = -6; x <= 6; x++) + for (int z = -2; z <= 8; z++) + level.setBlock(POS.offset(x, -1, z), + net.minecraft.world.level.block.Blocks.STONE + .defaultBlockState(), + 3); + if (present()) { + int x = -3; + for (var direction : Direction.Plane.HORIZONTAL) { + level.setBlock(POS.offset(x, 0, 0), + BuiltInRegistries.BLOCK + .get(id("client_gate")) + .defaultBlockState().setValue( + RedstoneBlock.FACING, direction), + 3); + x += 2; + } + level.setBlock(POS.offset(0, 0, -2), + BuiltInRegistries.BLOCK + .get(id("client_alloy_block")) + .defaultBlockState(), + 3); + } + player.teleportTo(.5, 103, 7.5); + player.setYRot(180); + player.setXRot(22); + if (present()) { + checkRecipes(mc); + var server = mc.getSingleplayerServer(); + server + .reloadResources( + server.getPackRepository().getSelectedIds()) + .thenRunAsync(() -> { + try { + checkRecipes(mc); + MMDLib.LOGGER.info( + "MMDLIB_CT_RECIPES_RELOAD_PASS"); + ready = true; + } catch (Throwable error) { + fail(error); + } + }, server).exceptionally(error -> { + fail(error); + return null; + }); + } else + ready = true; + } catch (Throwable error) { + fail(error); + } + }); } - player.teleportTo(.5, 103, 7.5); - player.setYRot(180); player.setXRot(22); + if (!ready || ++ticks < 100 || stage != 0) + return; if (present()) { - checkRecipes(mc); - var server = mc.getSingleplayerServer(); - server.reloadResources(server.getPackRepository().getSelectedIds()).thenRunAsync(() -> { - try { - checkRecipes(mc); - MMDLib.LOGGER.info("MMDLIB_CT_RECIPES_RELOAD_PASS"); - ready = true; - } catch (Throwable error) { fail(error); } - }, server).exceptionally(error -> { fail(error); return null; }); - } else ready = true; - } catch (Throwable error) { fail(error); } - }); - } - if (!ready || ++ticks < 100 || stage != 0) return; - if (present()) { - require(new ItemStack(BuiltInRegistries.ITEM.get(id("client_widget"))).getHoverName().getString().equals("Named Test Widget"), "English name override"); - require(new ItemStack(BuiltInRegistries.ITEM.get(id("client_alloy_ingot"))).getHoverName().getString().equals("Amber Alloy Ingot"), "material English name"); - for (String name : ITEMS) { - var model = mc.getItemRenderer().getModel(new ItemStack(BuiltInRegistries.ITEM.get(id(name))), mc.level, mc.player, 0); - require(model != mc.getModelManager().getMissingModel(), "missing item model: " + name); - require(!model.getParticleIcon().contents().name().getPath().equals("missingno"), "missing texture: " + name); - } - var block = BuiltInRegistries.BLOCK.get(id("client_gate")); - for (var direction : Direction.Plane.HORIZONTAL) { - var state = block.defaultBlockState().setValue(RedstoneBlock.FACING, direction); - var model = mc.getBlockRenderer().getBlockModel(state); - require(model.getQuads(state, direction, RandomSource.create(1)).stream() - .anyMatch(q -> q.getSprite().contents().name().equals(id("block/client_gate_front"))), "front model rotation: " + direction); + require( + new ItemStack( + BuiltInRegistries.ITEM.get(id("client_widget"))) + .getHoverName().getString().equals("Named Test Widget"), + "English name override"); + require( + new ItemStack( + BuiltInRegistries.ITEM.get(id("client_alloy_ingot"))) + .getHoverName().getString().equals("Amber Alloy Ingot"), + "material English name"); + for (String name : ITEMS) { + var model = mc.getItemRenderer().getModel( + new ItemStack(BuiltInRegistries.ITEM.get(id(name))), + mc.level, mc.player, 0); + require(model != mc.getModelManager().getMissingModel(), + "missing item model: " + name); + require(!model.getParticleIcon().contents().name().getPath() + .equals("missingno"), "missing texture: " + name); + } + var block = BuiltInRegistries.BLOCK.get(id("client_gate")); + for (var direction : Direction.Plane.HORIZONTAL) { + var state = block.defaultBlockState() + .setValue(RedstoneBlock.FACING, direction); + var model = mc.getBlockRenderer().getBlockModel(state); + require( + model.getQuads(state, direction, RandomSource.create(1)) + .stream() + .anyMatch(q -> q.getSprite().contents().name() + .equals(id("block/client_gate_front"))), + "front model rotation: " + direction); + } + } + mc.options.hideGui = true; + mc.player.setYRot(180); + mc.player.setXRot(22); + mc.setScreen(null); + stage = 1; + frames = 0; + } catch (Throwable error) { + fail(error); } - } - mc.options.hideGui = true; - mc.player.setYRot(180); mc.player.setXRot(22); - mc.setScreen(null); - stage = 1; frames = 0; - } catch (Throwable error) { fail(error); } - } + } - private static void checkRecipes(Minecraft mc) { - var server = mc.getSingleplayerServer(); - var manager = server.getRecipeManager(); - var recipe = manager.byKey(ResourceLocation.parse("crafttweaker:ct_crusher_added")).orElseThrow().value(); - var output = recipe.getResultItem(server.registryAccess()); - require(output.is(net.minecraft.world.item.Items.DIAMOND) && output.getCount() == 2, "recipe survives reload"); - require(manager.byKey(ResourceLocation.parse("crafttweaker:ct_crusher_removed")).isEmpty(), "removal survives reload"); - long count = manager.getAllRecipesFor(zone.moddev.mc.lib.common.recipes.CrushingRecipe.TYPE.get()) - .stream().filter(holder -> holder.id().toString().equals("crafttweaker:ct_crusher_added")).count(); - require(count == 1, "recipe reload does not duplicate IDs"); - } + private static void checkRecipes(Minecraft mc) { + var server = mc.getSingleplayerServer(); + var manager = server.getRecipeManager(); + var recipe = manager + .byKey(ResourceLocation.parse("crafttweaker:ct_crusher_added")) + .orElseThrow().value(); + var output = recipe.getResultItem(server.registryAccess()); + require(output.is(net.minecraft.world.item.Items.DIAMOND) + && output.getCount() == 2, "recipe survives reload"); + require(manager + .byKey(ResourceLocation.parse("crafttweaker:ct_crusher_removed")) + .isEmpty(), "removal survives reload"); + long count = manager + .getAllRecipesFor( + zone.moddev.mc.lib.common.recipes.CrushingRecipe.TYPE.get()) + .stream().filter(holder -> holder.id().toString() + .equals("crafttweaker:ct_crusher_added")) + .count(); + require(count == 1, "recipe reload does not duplicate IDs"); + } - @SubscribeEvent - public static void frame(RenderFrameEvent.Post event) { - if (!Boolean.getBoolean("mmdlib.craftTweakerSmoke") || finished || stage == 0) return; - var mc = Minecraft.getInstance(); - try { - if (++frames < 30) return; - if (stage == 1) { - Screenshot.grab(mc.gameDirectory, "ct-directional-world.png", mc.getMainRenderTarget(), message -> {}); - if (present()) { mc.setScreen(new Gallery()); stage = 2; frames = 0; return; } - } else Screenshot.grab(mc.gameDirectory, "ct-items-and-names.png", mc.getMainRenderTarget(), message -> {}); - MMDLib.LOGGER.info("MMDLIB_CT_CLIENT_PASS: CraftTweaker installed={}, integrated world, resource checks and captures complete", present()); - finished = true; - mc.stop(); - } catch (Throwable error) { fail(error); } - } + @SubscribeEvent + public static void frame(RenderFrameEvent.Post event) { + if (!Boolean.getBoolean("mmdlib.craftTweakerSmoke") || finished + || stage == 0) + return; + var mc = Minecraft.getInstance(); + try { + if (++frames < 30) + return; + if (stage == 1) { + Screenshot.grab(mc.gameDirectory, "ct-directional-world.png", + mc.getMainRenderTarget(), message -> { + }); + if (present()) { + mc.setScreen(new Gallery()); + stage = 2; + frames = 0; + return; + } + } else + Screenshot.grab(mc.gameDirectory, "ct-items-and-names.png", + mc.getMainRenderTarget(), message -> { + }); + MMDLib.LOGGER.info( + "MMDLIB_CT_CLIENT_PASS: CraftTweaker installed={}, integrated world, resource checks and captures complete", + present()); + finished = true; + mc.stop(); + } catch (Throwable error) { + fail(error); + } + } - private static void fail(Throwable error) { - finished = true; - MMDLib.LOGGER.error("MMDLIB_CT_CLIENT_FAIL", error); - Minecraft.getInstance().execute(() -> Minecraft.getInstance().stop()); - } + private static void fail(Throwable error) { + finished = true; + MMDLib.LOGGER.error("MMDLIB_CT_CLIENT_FAIL", error); + Minecraft.getInstance().execute(() -> Minecraft.getInstance().stop()); + } - private static final class Gallery extends Screen { - Gallery() { super(Component.literal("CraftTweaker content smoke")); } - @Override public boolean isPauseScreen() { return false; } - @Override public void render(GuiGraphics g, int mouseX, int mouseY, float partialTick) { - g.fill(0, 0, width, height, 0xFF26313C); - g.drawString(font, title, 30, 25, 0xFFFFFFFF); - int y = 65; - for (String name : ITEMS) { - var stack = new ItemStack(BuiltInRegistries.ITEM.get(id(name))); - g.pose().pushPose(); g.pose().translate(35, y, 0); g.pose().scale(2, 2, 2); - g.renderItem(stack, 0, 0); g.pose().popPose(); - g.drawString(font, stack.getHoverName(), 90, y + 10, 0xFFFFFFFF); - y += 62; - } + private static final class Gallery extends Screen { + Gallery() { + super(Component.literal("CraftTweaker content smoke")); + } + @Override + public boolean isPauseScreen() { + return false; + } + @Override + public void render(GuiGraphics g, int mouseX, int mouseY, + float partialTick) { + g.fill(0, 0, width, height, 0xFF26313C); + g.drawString(font, title, 30, 25, 0xFFFFFFFF); + int y = 65; + for (String name : ITEMS) { + var stack = new ItemStack(BuiltInRegistries.ITEM.get(id(name))); + g.pose().pushPose(); + g.pose().translate(35, y, 0); + g.pose().scale(2, 2, 2); + g.renderItem(stack, 0, 0); + g.pose().popPose(); + g.drawString(font, stack.getHoverName(), 90, y + 10, + 0xFFFFFFFF); + y += 62; + } + } } - } } diff --git a/src/companion/java/zone/moddev/mc/companion/CompanionAddons.java b/src/companion/java/zone/moddev/mc/companion/CompanionAddons.java index 9f11af46..962804e3 100644 --- a/src/companion/java/zone/moddev/mc/companion/CompanionAddons.java +++ b/src/companion/java/zone/moddev/mc/companion/CompanionAddons.java @@ -17,151 +17,140 @@ import zone.moddev.mc.lib.api.automation.*; import zone.moddev.mc.lib.api.capabilities.IoMode; -/** Companion content demonstrates capabilities: the library does not register concrete addons. */ +/** + * Companion content demonstrates capabilities: the library does not register + * concrete addons. + */ public final class CompanionAddons { - private static final DeferredRegister ITEMS = - DeferredRegister.create(Registries.ITEM, CompanionMod.ID); - public static final DeferredHolder RECOVERY_CAPSULE = - ITEMS.register("recovery_capsule", RecoveryCapsuleItem::new); - private static final Map, InventoryAddon> ADDONS = - new LinkedHashMap<>(); - private static final Map, InventoryFilter> FILTERS = - new LinkedHashMap<>(); - public static final FilterWidgetType TAG_MODE = - FilterWidgetType.register( - CompanionMod.id("tag_mode"), - com.mojang.serialization.Codec.BOOL, - (config, deny) -> { - config.putBoolean("deny", deny); - return true; - }); - public static final DeferredHolder PULLER = - addon("mechanical_puller", ResourceType.ITEMS, IoMode.INPUT, 8, 20, 4, true); - public static final DeferredHolder PUSHER = - addon("mechanical_pusher", ResourceType.ITEMS, IoMode.OUTPUT, 8, 20, 4, true); - public static final DeferredHolder INPUT_PUMP = - addon("input_pump", ResourceType.FLUIDS, IoMode.INPUT, 250, 10, 1, true); - public static final DeferredHolder OUTPUT_PUMP = - addon("output_pump", ResourceType.FLUIDS, IoMode.OUTPUT, 250, 10, 1, true); - public static final DeferredHolder IMPORT_RELAY = - addon("import_relay", ResourceType.ENERGY, IoMode.INPUT, 1000, 5, 0, false); - public static final DeferredHolder EXPORT_RELAY = - addon("export_relay", ResourceType.ENERGY, IoMode.OUTPUT, 1000, 5, 0, false); - public static final DeferredHolder ITEM_FILTER = - filter("item_filter", ResourceType.ITEMS, 8, false); - public static final DeferredHolder LARGE_FILTER = - filter("large_item_filter", ResourceType.ITEMS, 36, false); - public static final DeferredHolder TAG_FILTER = - filter("tag_filter", ResourceType.ITEMS, 0, true); - public static final DeferredHolder FLUID_FILTER = - filter("fluid_filter", ResourceType.FLUIDS, 6, false); + private static final DeferredRegister ITEMS = DeferredRegister + .create(Registries.ITEM, CompanionMod.ID); + public static final DeferredHolder RECOVERY_CAPSULE = ITEMS + .register("recovery_capsule", RecoveryCapsuleItem::new); + private static final Map, InventoryAddon> ADDONS = new LinkedHashMap<>(); + private static final Map, InventoryFilter> FILTERS = new LinkedHashMap<>(); + public static final FilterWidgetType TAG_MODE = FilterWidgetType + .register(CompanionMod.id("tag_mode"), + com.mojang.serialization.Codec.BOOL, (config, deny) -> { + config.putBoolean("deny", deny); + return true; + }); + public static final DeferredHolder PULLER = addon( + "mechanical_puller", ResourceType.ITEMS, IoMode.INPUT, 8, 20, 4, true); + public static final DeferredHolder PUSHER = addon( + "mechanical_pusher", ResourceType.ITEMS, IoMode.OUTPUT, 8, 20, 4, true); + public static final DeferredHolder INPUT_PUMP = addon( + "input_pump", ResourceType.FLUIDS, IoMode.INPUT, 250, 10, 1, true); + public static final DeferredHolder OUTPUT_PUMP = addon( + "output_pump", ResourceType.FLUIDS, IoMode.OUTPUT, 250, 10, 1, true); + public static final DeferredHolder IMPORT_RELAY = addon( + "import_relay", ResourceType.ENERGY, IoMode.INPUT, 1000, 5, 0, false); + public static final DeferredHolder EXPORT_RELAY = addon( + "export_relay", ResourceType.ENERGY, IoMode.OUTPUT, 1000, 5, 0, false); + public static final DeferredHolder ITEM_FILTER = filter( + "item_filter", ResourceType.ITEMS, 8, false); + public static final DeferredHolder LARGE_FILTER = filter( + "large_item_filter", ResourceType.ITEMS, 36, false); + public static final DeferredHolder TAG_FILTER = filter( + "tag_filter", ResourceType.ITEMS, 0, true); + public static final DeferredHolder FLUID_FILTER = filter( + "fluid_filter", ResourceType.FLUIDS, 6, false); - private static DeferredHolder addon( - String name, - ResourceType type, - IoMode direction, - int batch, - int interval, - long cost, - boolean filters) { - var item = ITEMS.register(name, () -> new Item(new Item.Properties().stacksTo(1))); - var spec = - new AddonSpec(CompanionMod.id(name), type, direction, batch, interval, cost, filters); - ADDONS.put(item, stack -> spec); - return item; - } + private static DeferredHolder addon(String name, + ResourceType type, IoMode direction, int batch, int interval, + long cost, boolean filters) { + var item = ITEMS.register(name, + () -> new Item(new Item.Properties().stacksTo(1))); + var spec = new AddonSpec(CompanionMod.id(name), type, direction, batch, + interval, cost, filters); + ADDONS.put(item, stack -> spec); + return item; + } - private static DeferredHolder filter( - String name, ResourceType type, int size, boolean tag) { - var item = ITEMS.register(name, () -> new Item(new Item.Properties().stacksTo(1))); - FILTERS.put( - item, - new InventoryFilter() { - public ResourceType resource(ItemStack stack) { - return type; - } + private static DeferredHolder filter(String name, + ResourceType type, int size, boolean tag) { + var item = ITEMS.register(name, + () -> new Item(new Item.Properties().stacksTo(1))); + FILTERS.put(item, new InventoryFilter() { + public ResourceType resource(ItemStack stack) { + return type; + } - public ResourceLocation editorType(ItemStack stack) { - return CompanionMod.id(name); - } + public ResourceLocation editorType(ItemStack stack) { + return CompanionMod.id(name); + } - public FilterControl controls(ItemStack stack) { - return new FilterControl.Box( - "root", - false, - List.of( - tag - ? new FilterControl.Custom("deny", TAG_MODE, 14) - : new FilterControl.Toggle( - "deny", - ResourceLocation.parse("mmdlib:icon_filter_whitelist"), - ResourceLocation.parse("mmdlib:icon_filter_blacklist"), - Component.translatable("screen.mmdlib_companion.filter.whitelist"), - Component.translatable("screen.mmdlib_companion.filter.blacklist")), + public FilterControl controls(ItemStack stack) { + return new FilterControl.Box("root", false, List.of(tag + ? new FilterControl.Custom("deny", TAG_MODE, 14) + : new FilterControl.Toggle("deny", + ResourceLocation.parse("mmdlib:icon_filter_whitelist"), + ResourceLocation.parse("mmdlib:icon_filter_blacklist"), + Component.translatable( + "screen.mmdlib_companion.filter.whitelist"), + Component.translatable( + "screen.mmdlib_companion.filter.blacklist")), tag - ? new FilterControl.Text( - "tag", - 128, - Component.translatable("screen.mmdlib_companion.filter.tag")) + ? new FilterControl.Text("tag", 128, + Component.translatable( + "screen.mmdlib_companion.filter.tag")) : new FilterControl.Samples("samples", size, type))); - } + } - public boolean validate( - ItemStack stack, - net.minecraft.nbt.CompoundTag config, - HolderLookup.Provider registries) { - if (!tag || config.getString("tag").isEmpty()) return true; - var id = ResourceLocation.tryParse(config.getString("tag")); - return id != null - && registries - .lookupOrThrow(Registries.ITEM) - .get(TagKey.create(Registries.ITEM, id)) - .isPresent(); - } + public boolean validate(ItemStack stack, + net.minecraft.nbt.CompoundTag config, + HolderLookup.Provider registries) { + if (!tag || config.getString("tag").isEmpty()) + return true; + var id = ResourceLocation.tryParse(config.getString("tag")); + return id != null && registries.lookupOrThrow(Registries.ITEM) + .get(TagKey.create(Registries.ITEM, id)).isPresent(); + } - public boolean matches( - ItemStack stack, Object candidate, HolderLookup.Provider registries) { - var config = configuration(stack); - boolean match = false; - if (tag && candidate instanceof ItemStack item) { - var id = ResourceLocation.tryParse(config.getString("tag")); - match = id != null && item.is(TagKey.create(Registries.ITEM, id)); - } else { - String id = - candidate instanceof ItemStack item - ? BuiltInRegistries.ITEM.getKey(item.getItem()).toString() - : candidate instanceof FluidStack fluid - ? BuiltInRegistries.FLUID.getKey(fluid.getFluid()).toString() - : ""; - var samples = config.getList("samples", net.minecraft.nbt.Tag.TAG_STRING); - for (int i = 0; i < Math.min(size, samples.size()); i++) - if (samples.getString(i).equals(id)) { - match = true; - break; + public boolean matches(ItemStack stack, Object candidate, + HolderLookup.Provider registries) { + var config = configuration(stack); + boolean match = false; + if (tag && candidate instanceof ItemStack item) { + var id = ResourceLocation.tryParse(config.getString("tag")); + match = id != null + && item.is(TagKey.create(Registries.ITEM, id)); + } else { + String id = candidate instanceof ItemStack item + ? BuiltInRegistries.ITEM.getKey(item.getItem()) + .toString() + : candidate instanceof FluidStack fluid + ? BuiltInRegistries.FLUID.getKey(fluid.getFluid()) + .toString() + : ""; + var samples = config.getList("samples", + net.minecraft.nbt.Tag.TAG_STRING); + for (int i = 0; i < Math.min(size, samples.size()); i++) + if (samples.getString(i).equals(id)) { + match = true; + break; + } } + return config.getBoolean("deny") ? !match : match; } - return config.getBoolean("deny") ? !match : match; - } }); - return item; - } + return item; + } - public static List> items() { - var result = new ArrayList<>(ADDONS.keySet()); - result.addAll(FILTERS.keySet()); - return List.copyOf(result); - } + public static List> items() { + var result = new ArrayList<>(ADDONS.keySet()); + result.addAll(FILTERS.keySet()); + return List.copyOf(result); + } - public static void register(IEventBus bus) { - ITEMS.register(bus); - } + public static void register(IEventBus bus) { + ITEMS.register(bus); + } - public static void capabilities(RegisterCapabilitiesEvent event) { - ADDONS.forEach( - (item, provider) -> - event.registerItem(AddonCapabilities.ADDON, (stack, context) -> provider, item.get())); - FILTERS.forEach( - (item, provider) -> - event.registerItem(AddonCapabilities.FILTER, (stack, context) -> provider, item.get())); - } + public static void capabilities(RegisterCapabilitiesEvent event) { + ADDONS.forEach((item, provider) -> event.registerItem( + AddonCapabilities.ADDON, (stack, context) -> provider, item.get())); + FILTERS.forEach( + (item, provider) -> event.registerItem(AddonCapabilities.FILTER, + (stack, context) -> provider, item.get())); + } } diff --git a/src/companion/java/zone/moddev/mc/companion/CompanionMod.java b/src/companion/java/zone/moddev/mc/companion/CompanionMod.java index 01e9d15c..3ce00712 100644 --- a/src/companion/java/zone/moddev/mc/companion/CompanionMod.java +++ b/src/companion/java/zone/moddev/mc/companion/CompanionMod.java @@ -35,10 +35,10 @@ @Mod(CompanionMod.ID) public final class CompanionMod { public static final String ID = "mmdlib_companion"; - private static final DeferredRegister BLOCKS = DeferredRegister.create(Registries.BLOCK, - ID); - private static final DeferredRegister ITEMS = DeferredRegister.create(Registries.ITEM, - ID); + private static final DeferredRegister BLOCKS = DeferredRegister + .create(Registries.BLOCK, ID); + private static final DeferredRegister ITEMS = DeferredRegister + .create(Registries.ITEM, ID); private static final DeferredRegister> ENTITIES = DeferredRegister .create(Registries.BLOCK_ENTITY_TYPE, ID); private static final DeferredRegister> RECIPE_TYPES = DeferredRegister @@ -48,33 +48,34 @@ public final class CompanionMod { private static final DeferredRegister TABS = DeferredRegister .create(Registries.CREATIVE_MODE_TAB, ID); - public static final MachineRecipeType CRUSHING = MachineRecipeTypes.register(RECIPE_TYPES, - SERIALIZERS, "crushing"); - public static final MachineRecipeType TRANSMUTING = MachineRecipeTypes.register(RECIPE_TYPES, - SERIALIZERS, "transmuting"); - public static final Machine CRUSHER = new Machine("crusher", ProcessorEntity.Kind.CRUSHER); + public static final MachineRecipeType CRUSHING = MachineRecipeTypes + .register(RECIPE_TYPES, SERIALIZERS, "crushing"); + public static final MachineRecipeType TRANSMUTING = MachineRecipeTypes + .register(RECIPE_TYPES, SERIALIZERS, "transmuting"); + public static final Machine CRUSHER = new Machine("crusher", + ProcessorEntity.Kind.CRUSHER); public static final Machine TRANSMUTER = new Machine("transmuter", ProcessorEntity.Kind.TRANSMUTER); public static final Machine DUAL_CRUSHER = new Machine("dual_crusher", ProcessorEntity.Kind.DUAL); - public static final Machine CREATIVE_POWER = new Machine("creative_power", null); + public static final Machine CREATIVE_POWER = new Machine("creative_power", + null); public static final Machine LOGISTICS = new Machine("logistics", ProcessorEntity.Kind.LOGISTICS); - public static final List MACHINES = List.of(CRUSHER, TRANSMUTER, DUAL_CRUSHER, - CREATIVE_POWER, LOGISTICS); + public static final List MACHINES = List.of(CRUSHER, TRANSMUTER, + DUAL_CRUSHER, CREATIVE_POWER, LOGISTICS); static { - TABS.register( - "machines", + TABS.register("machines", () -> CreativeModeTab.builder() .title(Component.translatable("itemGroup.mmdlib_companion")) .icon(() -> new ItemStack(CRUSHER.block.get())) - .displayItems( - (parameters, output) -> { - MACHINES.forEach(machine -> output.accept(machine.block.get())); - CompanionAddons.items().forEach(item -> output.accept(item.get())); - }) - .build()); + .displayItems((parameters, output) -> { + MACHINES + .forEach(machine -> output.accept(machine.block.get())); + CompanionAddons.items() + .forEach(item -> output.accept(item.get())); + }).build()); } public CompanionMod(IEventBus bus) { @@ -87,64 +88,54 @@ public CompanionMod(IEventBus bus) { SERIALIZERS.register(bus); TABS.register(bus); bus.addListener(CompanionMod::capabilities); - // The custom progress dial remains clickable; processor buttons have a library-owned + // The custom progress dial remains clickable; processor buttons have a + // library-owned // action. - MaterialExtensions.GLOBAL - .events() - .registerCallback( - id("toggle_0"), - MaterialEventTypes.GUI_CLICK, - click -> { - if (click.menu().blockEntity instanceof ProcessorEntity machine) - machine.toggle(0); - }); + MaterialExtensions.GLOBAL.events().registerCallback(id("toggle_0"), + MaterialEventTypes.GUI_CLICK, click -> { + if (click.menu().blockEntity instanceof ProcessorEntity machine) + machine.toggle(0); + }); } private static void capabilities(RegisterCapabilitiesEvent event) { CompanionAddons.capabilities(event); for (var machine : MACHINES) { if (machine == CREATIVE_POWER) { - MachineCapabilities.energy( - event, - machine.type.get(), + MachineCapabilities.energy(event, machine.type.get(), entity -> entity.energy, - (entity, side) -> ((CreativePowerEntity) entity).automation.mode( - zone.moddev.mc.lib.api.automation.ResourceType.ENERGY, 0, side)); + (entity, side) -> ((CreativePowerEntity) entity).automation + .mode( + zone.moddev.mc.lib.api.automation.ResourceType.ENERGY, + 0, side)); continue; } - event.registerBlockEntity( - Capabilities.ItemHandler.BLOCK, - machine.type.get(), - (entity, side) -> { + event.registerBlockEntity(Capabilities.ItemHandler.BLOCK, + machine.type.get(), (entity, side) -> { var p = (ProcessorEntity) entity; - return p.items.view( - slot -> p.automation.mode( - zone.moddev.mc.lib.api.automation.ResourceType.ITEMS, - p.items.address(slot).inventory(), - p.items.address(slot).slot(), - side)); + return p.items.view(slot -> p.automation.mode( + zone.moddev.mc.lib.api.automation.ResourceType.ITEMS, + p.items.address(slot).inventory(), + p.items.address(slot).slot(), side)); }); - event.registerBlockEntity( - Capabilities.FluidHandler.BLOCK, - machine.type.get(), - (entity, side) -> { + event.registerBlockEntity(Capabilities.FluidHandler.BLOCK, + machine.type.get(), (entity, side) -> { var p = (ProcessorEntity) entity; return p.hasTank() - ? p.fluids.view( - tank -> p.automation.mode( - zone.moddev.mc.lib.api.automation.ResourceType.FLUIDS, tank, side)) + ? p.fluids.view(tank -> p.automation.mode( + zone.moddev.mc.lib.api.automation.ResourceType.FLUIDS, + tank, side)) : null; }); - event.registerBlockEntity( - Capabilities.EnergyStorage.BLOCK, - machine.type.get(), - (entity, side) -> { + event.registerBlockEntity(Capabilities.EnergyStorage.BLOCK, + machine.type.get(), (entity, side) -> { var p = (ProcessorEntity) entity; return side == null - ? p.energy.view(zone.moddev.mc.lib.api.capabilities.IoMode.BOTH) - : p.energy.view( - () -> p.automation.mode( - zone.moddev.mc.lib.api.automation.ResourceType.ENERGY, 0, side)); + ? p.energy.view( + zone.moddev.mc.lib.api.capabilities.IoMode.BOTH) + : p.energy.view(() -> p.automation.mode( + zone.moddev.mc.lib.api.automation.ResourceType.ENERGY, + 0, side)); }); } } @@ -153,7 +144,10 @@ public static ResourceLocation id(String path) { return ResourceLocation.fromNamespaceAndPath(ID, path); } - /** Each block has its own type and XML screen, while processors share one implementation. */ + /** + * Each block has its own type and XML screen, while processors share one + * implementation. + */ public static final class Machine { public final DeferredHolder block; public final DeferredHolder, BlockEntityType> type; @@ -161,14 +155,14 @@ public static final class Machine { private Machine(String name, ProcessorEntity.Kind kind) { this.kind = kind; - block = BLOCKS.register( - name, - () -> new DemoBlock( - BlockBehaviour.Properties.of().strength(3.5F).requiresCorrectToolForDrops(), + block = BLOCKS.register(name, + () -> new DemoBlock(BlockBehaviour.Properties.of() + .strength(3.5F).requiresCorrectToolForDrops(), this::entityType)); - type = ENTITIES.register( - name, () -> BlockEntityType.Builder.of(this::create, block.get()).build(null)); - ITEMS.register(name, () -> new BlockItem(block.get(), new Item.Properties())); + type = ENTITIES.register(name, () -> BlockEntityType.Builder + .of(this::create, block.get()).build(null)); + ITEMS.register(name, + () -> new BlockItem(block.get(), new Item.Properties())); } private BlockEntityType entityType() { diff --git a/src/companion/java/zone/moddev/mc/companion/CompanionWidgets.java b/src/companion/java/zone/moddev/mc/companion/CompanionWidgets.java index 574a2615..d41f5c10 100644 --- a/src/companion/java/zone/moddev/mc/companion/CompanionWidgets.java +++ b/src/companion/java/zone/moddev/mc/companion/CompanionWidgets.java @@ -9,46 +9,53 @@ import zone.moddev.mc.lib.gui.WidgetTypes; import zone.moddev.mc.lib.gui.value.ValueTypes; -/** A consuming mod owns the widget contract; no client class is referenced here. */ +/** + * A consuming mod owns the widget contract; no client class is referenced here. + */ public final class CompanionWidgets { - public static final ResourceLocation PROGRESS_DIAL = CompanionMod.id("progress_dial"); - public static final ResourceLocation NAME_EDITOR = CompanionMod.id("name_editor"); - public static final ResourceLocation RESERVE_SLIDER = CompanionMod.id("reserve_slider"); - public static final ResourceLocation LANE_SELECTOR = CompanionMod.id("lane_selector"); + public static final ResourceLocation PROGRESS_DIAL = CompanionMod + .id("progress_dial"); + public static final ResourceLocation NAME_EDITOR = CompanionMod + .id("name_editor"); + public static final ResourceLocation RESERVE_SLIDER = CompanionMod + .id("reserve_slider"); + public static final ResourceLocation LANE_SELECTOR = CompanionMod + .id("lane_selector"); public static final WidgetAction RENAME = new WidgetAction<>( CompanionMod.id("rename"), - net.minecraft.network.codec.StreamCodec.of( - (b, value) -> b.writeUtf(value, 32), b -> b.readUtf(32)), + net.minecraft.network.codec.StreamCodec + .of((b, value) -> b.writeUtf(value, 32), b -> b.readUtf(32)), 132, - (context, value) -> context.menu().blockEntity instanceof ProcessorEntity machine - && machine.rename(value)); + (context, + value) -> context + .menu().blockEntity instanceof ProcessorEntity machine + && machine.rename(value)); public static final WidgetAction RESERVE = new WidgetAction<>( CompanionMod.id("reserve"), - net.minecraft.network.codec.StreamCodec.of( - (b, value) -> b.writeVarInt(value), b -> b.readVarInt()), + net.minecraft.network.codec.StreamCodec + .of((b, value) -> b.writeVarInt(value), b -> b.readVarInt()), 5, - (context, value) -> context.menu().blockEntity instanceof ProcessorEntity machine - && machine.setReservePercent(value)); + (context, + value) -> context + .menu().blockEntity instanceof ProcessorEntity machine + && machine.setReservePercent(value)); public record LaneSetting(int lane, boolean paused) { } public static final WidgetAction SET_LANE = new WidgetAction<>( CompanionMod.id("set_lane"), - net.minecraft.network.codec.StreamCodec.of( - (b, value) -> { - b.writeVarInt(value.lane()); - b.writeBoolean(value.paused()); - }, - b -> new LaneSetting(b.readVarInt(), b.readBoolean())), - 6, + net.minecraft.network.codec.StreamCodec.of((b, value) -> { + b.writeVarInt(value.lane()); + b.writeBoolean(value.paused()); + }, b -> new LaneSetting(b.readVarInt(), b.readBoolean())), 6, (context, value) -> { if (!(context.menu().blockEntity instanceof ProcessorEntity machine) - || value.lane() != Integer.parseInt(context.widget().properties().get("lane")) - || value.lane() < 0 - || value.lane() >= machine.lanes.size()) + || value.lane() != Integer + .parseInt(context.widget().properties().get("lane")) + || value.lane() < 0 || value.lane() >= machine.lanes.size()) return false; if (machine.lanes.get(value.lane()).paused() != value.paused()) machine.toggle(value.lane()); @@ -59,35 +66,27 @@ private CompanionWidgets() { } public static void register() { - WidgetTypes.GLOBAL.register( - NAME_EDITOR, - new WidgetType( - Set.of(ValueTypes.TEXT), + WidgetTypes.GLOBAL.register(NAME_EDITOR, + new WidgetType(Set.of(ValueTypes.TEXT), widget -> validateEditor(widget, 140, 42, Set.of()), java.util.List.of(RENAME))); - WidgetTypes.GLOBAL.register( - RESERVE_SLIDER, - new WidgetType( - Set.of(ValueTypes.WHOLE), + WidgetTypes.GLOBAL.register(RESERVE_SLIDER, + new WidgetType(Set.of(ValueTypes.WHOLE), widget -> validateEditor(widget, 100, 30, Set.of()), java.util.List.of(RESERVE))); - WidgetTypes.GLOBAL.register( - LANE_SELECTOR, - new WidgetType( - Set.of(ValueTypes.BOOLEAN), - widget -> { - validateEditor(widget, 100, 30, Set.of("lane")); - String lane = widget.properties().get("lane"); - if (!"0".equals(lane) && !"1".equals(lane)) - throw new IllegalArgumentException("lane must be 0 or 1"); - }, - java.util.List.of(SET_LANE))); - WidgetTypes.GLOBAL.register( - PROGRESS_DIAL, + WidgetTypes.GLOBAL.register(LANE_SELECTOR, + new WidgetType(Set.of(ValueTypes.BOOLEAN), widget -> { + validateEditor(widget, 100, 30, Set.of("lane")); + String lane = widget.properties().get("lane"); + if (!"0".equals(lane) && !"1".equals(lane)) + throw new IllegalArgumentException("lane must be 0 or 1"); + }, java.util.List.of(SET_LANE))); + WidgetTypes.GLOBAL.register(PROGRESS_DIAL, new WidgetType( Set.of(ValueTypes.WHOLE, ValueTypes.DECIMAL, ValueTypes.NUMBER), widget -> { - if (widget.common().value().isEmpty() || widget.common().max().isEmpty()) + if (widget.common().value().isEmpty() + || widget.common().max().isEmpty()) throw new IllegalArgumentException( "A progress dial needs value and max getters"); if (widget.width() < 16 || widget.height() < 16) @@ -95,18 +94,19 @@ public static void register() { "A progress dial needs at least 16 x 16 px"); for (var name : widget.properties().keySet()) if (!name.equals("color")) - throw new IllegalArgumentException("Unknown property: " + name); - String color = widget.properties().getOrDefault("color", "#55CC88"); + throw new IllegalArgumentException( + "Unknown property: " + name); + String color = widget.properties().getOrDefault("color", + "#55CC88"); if (!color.matches("#[0-9a-fA-F]{6}")) - throw new IllegalArgumentException("color must be #RRGGBB"); + throw new IllegalArgumentException( + "color must be #RRGGBB"); })); } private static void validateEditor( - zone.moddev.mc.lib.gui.model.elements.CustomElement widget, - int width, - int height, - Set properties) { + zone.moddev.mc.lib.gui.model.elements.CustomElement widget, int width, + int height, Set properties) { if (widget.common().value().isEmpty()) throw new IllegalArgumentException("Editor needs a value getter"); if (widget.width() < width || widget.height() < height) diff --git a/src/companion/java/zone/moddev/mc/companion/CreativePowerEntity.java b/src/companion/java/zone/moddev/mc/companion/CreativePowerEntity.java index 3167c694..21078f36 100644 --- a/src/companion/java/zone/moddev/mc/companion/CreativePowerEntity.java +++ b/src/companion/java/zone/moddev/mc/companion/CreativePowerEntity.java @@ -21,72 +21,64 @@ /** Refills each tick and supplies up to 1,000 FE per adjacent face per tick. */ public final class CreativePowerEntity extends BasePoweredMachine { - private final ItemStorage empty = new ItemStorage(0); - public final AutomationInventory automation; - private final MachineInventoryLifecycle inventoryLifecycle; + private final ItemStorage empty = new ItemStorage(0); + public final AutomationInventory automation; + private final MachineInventoryLifecycle inventoryLifecycle; - public CreativePowerEntity(BlockEntityType type, BlockPos pos, BlockState state) { - super(type, pos, state, 100_000, 0, 1_000); - energy.setStored(energy.capacity()); - var group = - new ResourceGroup<>( - "energy", + public CreativePowerEntity(BlockEntityType type, BlockPos pos, + BlockState state) { + super(type, pos, state, 100_000, 0, 1_000); + energy.setStored(energy.capacity()); + var group = new ResourceGroup<>("energy", Component.translatable("screen.mmdlib_companion.group.energy"), - 0xd85b5b, - ResourceType.ENERGY, - IoMode.OUTPUT, - java.util.List.of(0), - java.util.List.of("energy"), - new SideConfig(IoMode.OUTPUT)); - automation = - new zone.moddev.mc.lib.machine.neoforge.MachineResources(this, this::facing) - .energy(group, energy) - .build() - .automation(); - inventoryLifecycle = poweredInventory(automation); - zone.moddev.mc.lib.gui.runtime.ScreenInventoryValidation.validate(this); - } + 0xd85b5b, ResourceType.ENERGY, IoMode.OUTPUT, java.util.List.of(0), + java.util.List.of("energy"), new SideConfig(IoMode.OUTPUT)); + automation = new zone.moddev.mc.lib.machine.neoforge.MachineResources( + this, this::facing).energy(group, energy).build().automation(); + inventoryLifecycle = poweredInventory(automation); + zone.moddev.mc.lib.gui.runtime.ScreenInventoryValidation.validate(this); + } - @Override - protected void beforeMachineTick() { - energy.insert(energy.space(), false); - } + @Override + protected void beforeMachineTick() { + energy.insert(energy.space(), false); + } - @Override - protected void afterMachineTick() { - for (var side : Direction.values()) { - if (!automation.mode(ResourceType.ENERGY, 0, side).canExtract()) continue; - var target = worldPosition.relative(side); - if (!level.hasChunkAt(target)) continue; - var receiver = - level.getCapability(Capabilities.EnergyStorage.BLOCK, target, side.getOpposite()); - if (receiver != null && receiver.canReceive()) - energy.extract(receiver.receiveEnergy(1_000, false), false); + @Override + protected void afterMachineTick() { + for (var side : Direction.values()) { + if (!automation.mode(ResourceType.ENERGY, 0, side).canExtract()) + continue; + var target = worldPosition.relative(side); + if (!level.hasChunkAt(target)) + continue; + var receiver = level.getCapability(Capabilities.EnergyStorage.BLOCK, + target, side.getOpposite()); + if (receiver != null && receiver.canReceive()) + energy.extract(receiver.receiveEnergy(1_000, false), false); + } } - } - @Override - public ItemStorage screenItems() { - return empty; - } + @Override + public ItemStorage screenItems() { + return empty; + } - @Override - public boolean screenAutomationAddons() { - return false; - } + @Override + public boolean screenAutomationAddons() { + return false; + } - @Override - protected MachineInventoryLifecycle machineInventory() { - return inventoryLifecycle; - } + @Override + protected MachineInventoryLifecycle machineInventory() { + return inventoryLifecycle; + } - @Override - protected void recoverResources(Level level, BlockPos pos, CompoundTag payload) { - net.minecraft.world.Containers.dropItemStack( - level, - pos.getX() + .5, - pos.getY() + .5, - pos.getZ() + .5, - RecoveryCapsuleItem.create(payload)); - } + @Override + protected void recoverResources(Level level, BlockPos pos, + CompoundTag payload) { + net.minecraft.world.Containers.dropItemStack(level, pos.getX() + .5, + pos.getY() + .5, pos.getZ() + .5, + RecoveryCapsuleItem.create(payload)); + } } diff --git a/src/companion/java/zone/moddev/mc/companion/DemoBlock.java b/src/companion/java/zone/moddev/mc/companion/DemoBlock.java index 1575391b..93f02ec4 100644 --- a/src/companion/java/zone/moddev/mc/companion/DemoBlock.java +++ b/src/companion/java/zone/moddev/mc/companion/DemoBlock.java @@ -38,20 +38,24 @@ public final class DemoBlock extends Block implements EntityBlock { public static final DirectionProperty FACING = BlockStateProperties.HORIZONTAL_FACING; private final Supplier> type; - public DemoBlock(Properties properties, Supplier> type) { + public DemoBlock(Properties properties, + Supplier> type) { super(properties); this.type = type; - registerDefaultState(stateDefinition.any().setValue(FACING, Direction.NORTH)); + registerDefaultState( + stateDefinition.any().setValue(FACING, Direction.NORTH)); } @Override - protected void createBlockStateDefinition(StateDefinition.Builder builder) { + protected void createBlockStateDefinition( + StateDefinition.Builder builder) { builder.add(FACING); } @Override public BlockState getStateForPlacement(BlockPlaceContext context) { - return defaultBlockState().setValue(FACING, context.getHorizontalDirection().getOpposite()); + return defaultBlockState().setValue(FACING, + context.getHorizontalDirection().getOpposite()); } @Override @@ -70,17 +74,15 @@ protected boolean isSignalSource(BlockState state) { } @Override - public boolean canConnectRedstone( - BlockState state, BlockGetter level, BlockPos pos, @Nullable Direction side) { + public boolean canConnectRedstone(BlockState state, BlockGetter level, + BlockPos pos, @Nullable Direction side) { return level.getBlockEntity(pos) instanceof ProcessorEntity entity && entity.redstone.connects(side); } @Override - protected int getSignal( - BlockState state, - net.minecraft.world.level.BlockGetter level, - BlockPos pos, + protected int getSignal(BlockState state, + net.minecraft.world.level.BlockGetter level, BlockPos pos, net.minecraft.core.Direction side) { return level.getBlockEntity(pos) instanceof ProcessorEntity entity ? entity.redstone.signal(side) @@ -93,21 +95,17 @@ public BlockEntity newBlockEntity(BlockPos pos, BlockState state) { } @Override - public BlockEntityTicker getTicker( - Level level, BlockState state, BlockEntityType requested) { + public BlockEntityTicker getTicker(Level level, + BlockState state, BlockEntityType requested) { if (level.isClientSide() || requested != type.get()) return null; - return (world, pos, blockState, entity) -> ((BasePoweredMachine) entity).serverTick(); + return (world, pos, blockState, entity) -> ((BasePoweredMachine) entity) + .serverTick(); } @Override - protected ItemInteractionResult useItemOn( - ItemStack stack, - BlockState state, - Level level, - BlockPos pos, - Player player, - InteractionHand hand, + protected ItemInteractionResult useItemOn(ItemStack stack, BlockState state, + Level level, BlockPos pos, Player player, InteractionHand hand, BlockHitResult hit) { if (stack.is(CompanionAddons.RECOVERY_CAPSULE.get()) && level.getBlockEntity(pos) instanceof ProcessorEntity machine) { @@ -122,19 +120,22 @@ protected ItemInteractionResult useItemOn( && FluidUtil.getFluidHandler(stack).isPresent()) { if (level.isClientSide()) return ItemInteractionResult.SUCCESS; - if (FluidUtil.interactWithFluidHandler(player, hand, machine.fluids)) + if (FluidUtil.interactWithFluidHandler(player, hand, + machine.fluids)) return ItemInteractionResult.CONSUME; player.displayClientMessage( - Component.translatable("screen.mmdlib_companion.tank.refused"), true); - // Do not let a rejected bucket spill its contents beside the machine. + Component.translatable("screen.mmdlib_companion.tank.refused"), + true); + // Do not let a rejected bucket spill its contents beside the + // machine. return ItemInteractionResult.FAIL; } return ItemInteractionResult.PASS_TO_DEFAULT_BLOCK_INTERACTION; } @Override - protected InteractionResult useWithoutItem( - BlockState state, Level level, BlockPos pos, Player player, BlockHitResult hit) { + protected InteractionResult useWithoutItem(BlockState state, Level level, + BlockPos pos, Player player, BlockHitResult hit) { return level.getBlockEntity(pos) instanceof BasePoweredMachine machine && machine.openScreen(player) ? InteractionResult.sidedSuccess(level.isClientSide()) @@ -142,8 +143,8 @@ protected InteractionResult useWithoutItem( } @Override - protected void onRemove( - BlockState state, Level level, BlockPos pos, BlockState replacement, boolean moved) { + protected void onRemove(BlockState state, Level level, BlockPos pos, + BlockState replacement, boolean moved) { DropsContents.onRemove(state, level, pos, replacement); super.onRemove(state, level, pos, replacement, moved); } diff --git a/src/companion/java/zone/moddev/mc/companion/ProcessorEntity.java b/src/companion/java/zone/moddev/mc/companion/ProcessorEntity.java index 2cbb208c..02d5b7b1 100644 --- a/src/companion/java/zone/moddev/mc/companion/ProcessorEntity.java +++ b/src/companion/java/zone/moddev/mc/companion/ProcessorEntity.java @@ -28,7 +28,10 @@ import zone.moddev.mc.lib.machine.neoforge.MachineInventoryLifecycle; import zone.moddev.mc.lib.machine.neoforge.MachineResources; -/** The dual crusher deliberately runs two lanes over the same input and output groups. */ +/** + * The dual crusher deliberately runs two lanes over the same input and output + * groups. + */ public final class ProcessorEntity extends BasePoweredMachine { public enum Kind { CRUSHER, TRANSMUTER, DUAL, LOGISTICS @@ -38,7 +41,8 @@ public enum Kind { public final AutomationInventory automation; public final MachineResources resources; public final MachineInventoryLifecycle inventoryLifecycle; - public final FluidStorage fluids = new FluidStorage().onChange(this::setChanged); + public final FluidStorage fluids = new FluidStorage() + .onChange(this::setChanged); public final List lanes; private final Kind kind; public final RedstoneControl redstone; @@ -70,124 +74,122 @@ public boolean setReservePercent(int value) { return true; } - public ProcessorEntity(BlockEntityType type, BlockPos pos, BlockState state, Kind kind) { + public ProcessorEntity(BlockEntityType type, BlockPos pos, + BlockState state, Kind kind) { super(type, pos, state); this.kind = kind; if (kind == Kind.CRUSHER) - fluids.addTank( - 4_000, - Role.STORAGE, - fluid -> fluid.getFluid() == Fluids.WATER || fluid.getFluid() == Fluids.LAVA); + fluids.addTank(4_000, Role.STORAGE, + fluid -> fluid.getFluid() == Fluids.WATER + || fluid.getFluid() == Fluids.LAVA); if (kind == Kind.LOGISTICS) for (int i = 0; i < 4; i++) fluids.addTank(4000, i < 2 ? Role.INPUT : Role.OUTPUT); - int[] outputSlots = kind == Kind.CRUSHER ? new int[]{1, 2} : new int[]{1}; + int[] outputSlots = kind == Kind.CRUSHER + ? new int[]{1, 2} + : new int[]{1}; resources = new MachineResources(this, this::facing); if (kind == Kind.TRANSMUTER) { - // One key, and one item: the recipe consumes the whole slot before inserting gold. + // One key, and one item: the recipe consumes the whole slot before + // inserting gold. items = ItemInventories.partition(List.of("input")).limit(1, 0) .onChange(this::setChanged); } else { - items = ItemInventories.partition( - java.util.stream.IntStream.range(0, 1 + outputSlots.length) - .mapToObj(i -> i == 0 ? "input" : "output") - .toList()) + items = ItemInventories + .partition( + java.util.stream.IntStream.range(0, 1 + outputSlots.length) + .mapToObj(i -> i == 0 ? "input" : "output").toList()) .role(Role.INPUT, 0) - .role(kind == Kind.CRUSHER ? Role.STORAGE : Role.OUTPUT, outputSlots) + .role(kind == Kind.CRUSHER ? Role.STORAGE : Role.OUTPUT, + outputSlots) .onChange(this::setChanged); } - var recipeType = kind == Kind.TRANSMUTER ? CompanionMod.TRANSMUTING : CompanionMod.CRUSHING; + var recipeType = kind == Kind.TRANSMUTER + ? CompanionMod.TRANSMUTING + : CompanionMod.CRUSHING; resources.items( - itemGroup("input", 0x568bd8, kind == Kind.TRANSMUTER ? IoMode.BOTH : IoMode.INPUT, 0), + itemGroup("input", 0x568bd8, + kind == Kind.TRANSMUTER ? IoMode.BOTH : IoMode.INPUT, 0), items.inventory("input")); if (kind != Kind.TRANSMUTER) - resources.items( - itemGroup( - "output", - 0xe48f45, - kind == Kind.CRUSHER ? IoMode.BOTH : IoMode.OUTPUT, - java.util.stream.IntStream.range(0, outputSlots.length).toArray()), + resources.items(itemGroup("output", 0xe48f45, + kind == Kind.CRUSHER ? IoMode.BOTH : IoMode.OUTPUT, + java.util.stream.IntStream.range(0, outputSlots.length) + .toArray()), items.inventory("output")); items.slotId(0, "input"); for (int i = 1; i < items.getSlots(); i++) items.slotId(i, "output_" + i); int[] colours = {0x64bfc1, 0xa678cb, 0xe0c55d, 0xce668e}; for (int i = 0; i < fluids.getTanks(); i++) { - var group = new ResourceGroup<>( - "tank_" + i, - Component.translatable("screen.mmdlib_companion.group.tank", i + 1), - colours[i], - ResourceType.FLUIDS, - kind == Kind.CRUSHER ? IoMode.BOTH : (i < 2 ? IoMode.INPUT : IoMode.OUTPUT), - List.of(i), - List.of(i == 0 ? "tank" : "tank_" + i), - new SideConfig( - kind == Kind.CRUSHER ? IoMode.BOTH : (i < 2 ? IoMode.INPUT : IoMode.OUTPUT))); + var group = new ResourceGroup<>("tank_" + i, + Component + .translatable("screen.mmdlib_companion.group.tank", i + 1), + colours[i], ResourceType.FLUIDS, + kind == Kind.CRUSHER + ? IoMode.BOTH + : (i < 2 ? IoMode.INPUT : IoMode.OUTPUT), + List.of(i), List.of(i == 0 ? "tank" : "tank_" + i), + new SideConfig(kind == Kind.CRUSHER + ? IoMode.BOTH + : (i < 2 ? IoMode.INPUT : IoMode.OUTPUT))); resources.fluids(group, fluids); fluids.tankId(i, group.id()); } - var energyGroup = new ResourceGroup<>( - "energy", + var energyGroup = new ResourceGroup<>("energy", Component.translatable("screen.mmdlib_companion.group.energy"), - 0xd85b5b, - ResourceType.ENERGY, - kind == Kind.LOGISTICS ? IoMode.BOTH : IoMode.INPUT, - List.of(0), - List.of("energy"), - new SideConfig(kind == Kind.LOGISTICS ? IoMode.BOTH : IoMode.INPUT)); + 0xd85b5b, ResourceType.ENERGY, + kind == Kind.LOGISTICS ? IoMode.BOTH : IoMode.INPUT, List.of(0), + List.of("energy"), new SideConfig( + kind == Kind.LOGISTICS ? IoMode.BOTH : IoMode.INPUT)); if (kind == Kind.LOGISTICS) energy.setRates(1000, 1000); resources.energy(energyGroup, energy); if (kind == Kind.TRANSMUTER) resources.recipeGroup("slot", "input"); if (kind != Kind.LOGISTICS) - resources.recipeInputs(recipeType, kind == Kind.TRANSMUTER ? "slot" : "input"); + resources.recipeInputs(recipeType, + kind == Kind.TRANSMUTER ? "slot" : "input"); resources.build(); automation = resources.automation(); var processors = new ArrayList(); for (int i = 0; i < (kind == Kind.DUAL ? 2 : 1); i++) { - var processor = new RecipeProcessor( - "lane_" + i, - recipeType, - resources.recipes(), - Component.translatable("screen.mmdlib_companion.processor", i + 1)); + var processor = new RecipeProcessor("lane_" + i, recipeType, + resources.recipes(), Component + .translatable("screen.mmdlib_companion.processor", i + 1)); processors.add(processor); features.add(processor); } lanes = List.copyOf(processors); - inventoryLifecycle = poweredInventory(automation).items("items", items).fluids("fluids", - fluids); + inventoryLifecycle = poweredInventory(automation).items("items", items) + .fluids("fluids", fluids); var outputParts = new ArrayList(); for (var group : automation.groups()) - outputParts.add( - RedstoneOutput.resource( - group, - () -> { - if (group.type() == ResourceType.ENERGY) - return energy.capacity() == 0 - ? 0 - : (double) energy.stored() / energy.capacity(); - double amount = 0, capacity = 0; - for (int index : group.members()) { - if (group.type() == ResourceType.ITEMS - && index < items.inventory(group.id()).getSlots()) { - var store = items.inventory(group.id()); - var stack = store.getStackInSlot(index); - int limit = Math.min( - store.getSlotLimit(index), - stack.isEmpty() ? 64 : stack.getMaxStackSize()); - if (limit > 0) { - amount += (double) stack.getCount() / limit; - capacity++; - } - } else if (group.type() == ResourceType.FLUIDS - && index < fluids.getTanks()) { - amount += fluids.getFluidInTank(index).getAmount(); - capacity += fluids.getTankCapacity(index); - } + outputParts.add(RedstoneOutput.resource(group, () -> { + if (group.type() == ResourceType.ENERGY) + return energy.capacity() == 0 + ? 0 + : (double) energy.stored() / energy.capacity(); + double amount = 0, capacity = 0; + for (int index : group.members()) { + if (group.type() == ResourceType.ITEMS + && index < items.inventory(group.id()).getSlots()) { + var store = items.inventory(group.id()); + var stack = store.getStackInSlot(index); + int limit = Math.min(store.getSlotLimit(index), + stack.isEmpty() ? 64 : stack.getMaxStackSize()); + if (limit > 0) { + amount += (double) stack.getCount() / limit; + capacity++; } - return capacity == 0 ? 0 : amount / capacity; - })); + } else if (group.type() == ResourceType.FLUIDS + && index < fluids.getTanks()) { + amount += fluids.getFluidInTank(index).getAmount(); + capacity += fluids.getTankCapacity(index); + } + } + return capacity == 0 ? 0 : amount / capacity; + })); if (kind != Kind.LOGISTICS) outputParts.add(features); redstone = new RedstoneControl(this, outputParts); @@ -196,16 +198,12 @@ public ProcessorEntity(BlockEntityType type, BlockPos pos, BlockState state, private ResourceGroup itemGroup( String id, int colour, IoMode mode, int... slots) { - var group = new ResourceGroup<>( - id, + var group = new ResourceGroup<>(id, Component.translatable("screen.mmdlib_companion.group." + id), - colour, - ResourceType.ITEMS, - mode, + colour, ResourceType.ITEMS, mode, java.util.Arrays.stream(slots).boxed().toList(), java.util.stream.IntStream.range(0, slots.length) - .mapToObj(i -> i == 0 ? id : id + "_" + (i + 1)) - .toList(), + .mapToObj(i -> i == 0 ? id : id + "_" + (i + 1)).toList(), new SideConfig(mode)); return group; } @@ -243,8 +241,8 @@ public int getSecondTime() { public List getWarnings() { var warnings = new ArrayList(super.getWarnings()); if (energy.stored() < energy.capacity() * reservePercent / 100) - warnings.add( - Component.translatable("screen.mmdlib_companion.reserve.waiting", reservePercent)); + warnings.add(Component.translatable( + "screen.mmdlib_companion.reserve.waiting", reservePercent)); return warnings; } @@ -263,21 +261,24 @@ public java.util.Map screenSides() { @Override public int[] screenRecipeSlots(ResourceLocation category) { - var recipeType = kind == Kind.TRANSMUTER ? CompanionMod.TRANSMUTING : CompanionMod.CRUSHING; + var recipeType = kind == Kind.TRANSMUTER + ? CompanionMod.TRANSMUTING + : CompanionMod.CRUSHING; return recipeType.id().equals(category) ? new int[]{0} : new int[0]; } @Override - protected void saveMachineData(CompoundTag tag, HolderLookup.Provider registries) { + protected void saveMachineData(CompoundTag tag, + HolderLookup.Provider registries) { tag.putString("machine_name", machineName); tag.putInt("reserve_percent", reservePercent); } @Override - protected void loadMachineData(CompoundTag tag, HolderLookup.Provider registries) { + protected void loadMachineData(CompoundTag tag, + HolderLookup.Provider registries) { String savedName = tag.getString("machine_name"); - machineName = !savedName.isBlank() - && savedName.length() <= 32 + machineName = !savedName.isBlank() && savedName.length() <= 32 && savedName.codePoints().noneMatch(Character::isISOControl) ? savedName.strip() : "Crusher"; @@ -296,12 +297,10 @@ protected boolean processingAllowed() { } @Override - protected void recoverResources(Level level, BlockPos pos, CompoundTag payload) { - net.minecraft.world.Containers.dropItemStack( - level, - pos.getX() + .5, - pos.getY() + .5, - pos.getZ() + .5, + protected void recoverResources(Level level, BlockPos pos, + CompoundTag payload) { + net.minecraft.world.Containers.dropItemStack(level, pos.getX() + .5, + pos.getY() + .5, pos.getZ() + .5, RecoveryCapsuleItem.create(payload)); } } diff --git a/src/companion/java/zone/moddev/mc/companion/RecoveryCapsuleItem.java b/src/companion/java/zone/moddev/mc/companion/RecoveryCapsuleItem.java index 5397a585..9661483b 100644 --- a/src/companion/java/zone/moddev/mc/companion/RecoveryCapsuleItem.java +++ b/src/companion/java/zone/moddev/mc/companion/RecoveryCapsuleItem.java @@ -14,96 +14,119 @@ import zone.moddev.mc.lib.api.automation.ResourceType; -/** Concrete companion content; the library hands its host only the pending resource data. */ +/** + * Concrete companion content; the library hands its host only the pending + * resource data. + */ public final class RecoveryCapsuleItem extends Item { - private static final String KEY = "mmdlib_recovery"; + private static final String KEY = "mmdlib_recovery"; - public RecoveryCapsuleItem() { - super(new Item.Properties().stacksTo(1)); - } + public RecoveryCapsuleItem() { + super(new Item.Properties().stacksTo(1)); + } - public static ItemStack create(CompoundTag payload) { - var stack = new ItemStack(CompanionAddons.RECOVERY_CAPSULE.get()); - var data = new CompoundTag(); - data.put(KEY, payload.copy()); - stack.set(DataComponents.CUSTOM_DATA, CustomData.of(data)); - return stack; - } + public static ItemStack create(CompoundTag payload) { + var stack = new ItemStack(CompanionAddons.RECOVERY_CAPSULE.get()); + var data = new CompoundTag(); + data.put(KEY, payload.copy()); + stack.set(DataComponents.CUSTOM_DATA, CustomData.of(data)); + return stack; + } - public static CompoundTag contents(ItemStack stack) { - return stack - .getOrDefault(DataComponents.CUSTOM_DATA, CustomData.EMPTY) - .copyTag() - .getCompound(KEY); - } + public static CompoundTag contents(ItemStack stack) { + return stack.getOrDefault(DataComponents.CUSTOM_DATA, CustomData.EMPTY) + .copyTag().getCompound(KEY); + } - /** - * Partial restoration leaves every unaccepted resource on the same item, including in creative. - */ - public static boolean restore(ItemStack stack, ProcessorEntity target) { - if (!stack.is(CompanionAddons.RECOVERY_CAPSULE.get()) - || target.getLevel() == null - || target.getLevel().isClientSide) return false; - var outer = stack.getOrDefault(DataComponents.CUSTOM_DATA, CustomData.EMPTY).copyTag(); - var payload = outer.getCompound(KEY); - var resources = payload.getList("resources", Tag.TAG_COMPOUND); - boolean changed = false; - for (int i = resources.size() - 1; i >= 0; i--) { - var entry = resources.getCompound(i); - var data = entry.getCompound("data"); - String type = entry.getString("type"); - if (type.equals(ResourceType.ITEMS.id().toString())) { - var item = - ItemStack.parseOptional(target.getLevel().registryAccess(), data.getCompound("item")); - if (!item.isEmpty()) { - var remaining = - zone.moddev.mc.lib.api.capabilities.Transfer.insert(target.items, item, false); - if (remaining.getCount() < item.getCount()) { - changed = true; - if (remaining.isEmpty()) data.remove("item"); - else data.put("item", remaining.save(target.getLevel().registryAccess())); - } + /** + * Partial restoration leaves every unaccepted resource on the same item, + * including in creative. + */ + public static boolean restore(ItemStack stack, ProcessorEntity target) { + if (!stack.is(CompanionAddons.RECOVERY_CAPSULE.get()) + || target.getLevel() == null || target.getLevel().isClientSide) + return false; + var outer = stack + .getOrDefault(DataComponents.CUSTOM_DATA, CustomData.EMPTY) + .copyTag(); + var payload = outer.getCompound(KEY); + var resources = payload.getList("resources", Tag.TAG_COMPOUND); + boolean changed = false; + for (int i = resources.size() - 1; i >= 0; i--) { + var entry = resources.getCompound(i); + var data = entry.getCompound("data"); + String type = entry.getString("type"); + if (type.equals(ResourceType.ITEMS.id().toString())) { + var item = ItemStack.parseOptional( + target.getLevel().registryAccess(), + data.getCompound("item")); + if (!item.isEmpty()) { + var remaining = zone.moddev.mc.lib.api.capabilities.Transfer + .insert(target.items, item, false); + if (remaining.getCount() < item.getCount()) { + changed = true; + if (remaining.isEmpty()) + data.remove("item"); + else + data.put("item", remaining + .save(target.getLevel().registryAccess())); + } + } + } else if (type.equals(ResourceType.FLUIDS.id().toString())) { + var fluid = FluidStack.parseOptional( + target.getLevel().registryAccess(), + data.getCompound("fluid")); + int moved = target.fluids.fill(fluid, + IFluidHandler.FluidAction.EXECUTE); + if (moved > 0) { + changed = true; + fluid.shrink(moved); + if (fluid.isEmpty()) + data.remove("fluid"); + else + data.put("fluid", + fluid.save(target.getLevel().registryAccess())); + } + } else if (type.equals(ResourceType.ENERGY.id().toString())) { + int amount = data.getInt("energy"); + int moved = (int) target.energy.insert(amount, false); + if (moved > 0) { + changed = true; + if (moved == amount) + data.remove("energy"); + else + data.putInt("energy", amount - moved); + } + } + if (data.isEmpty()) + resources.remove(i); } - } else if (type.equals(ResourceType.FLUIDS.id().toString())) { - var fluid = - FluidStack.parseOptional(target.getLevel().registryAccess(), data.getCompound("fluid")); - int moved = target.fluids.fill(fluid, IFluidHandler.FluidAction.EXECUTE); - if (moved > 0) { - changed = true; - fluid.shrink(moved); - if (fluid.isEmpty()) data.remove("fluid"); - else data.put("fluid", fluid.save(target.getLevel().registryAccess())); - } - } else if (type.equals(ResourceType.ENERGY.id().toString())) { - int amount = data.getInt("energy"); - int moved = (int) target.energy.insert(amount, false); - if (moved > 0) { - changed = true; - if (moved == amount) data.remove("energy"); - else data.putInt("energy", amount - moved); + long refund = Math.max(0, payload.getLong("refund")); + long moved = target.energy.insert(refund, false); + if (moved > 0) + changed = true; + if (!changed) + return false; + if (refund == moved) + payload.remove("refund"); + else + payload.putLong("refund", refund - moved); + if (resources.isEmpty()) + payload.remove("resources"); + if (payload.isEmpty()) + stack.shrink(1); + else { + outer.put(KEY, payload); + stack.set(DataComponents.CUSTOM_DATA, CustomData.of(outer)); } - } - if (data.isEmpty()) resources.remove(i); + target.setChanged(); + return true; } - long refund = Math.max(0, payload.getLong("refund")); - long moved = target.energy.insert(refund, false); - if (moved > 0) changed = true; - if (!changed) return false; - if (refund == moved) payload.remove("refund"); - else payload.putLong("refund", refund - moved); - if (resources.isEmpty()) payload.remove("resources"); - if (payload.isEmpty()) stack.shrink(1); - else { - outer.put(KEY, payload); - stack.set(DataComponents.CUSTOM_DATA, CustomData.of(outer)); - } - target.setChanged(); - return true; - } - @Override - public void appendHoverText( - ItemStack stack, TooltipContext context, List lines, TooltipFlag flag) { - lines.add(Component.translatable("item.mmdlib_companion.recovery_capsule.help")); - } + @Override + public void appendHoverText(ItemStack stack, TooltipContext context, + List lines, TooltipFlag flag) { + lines.add(Component + .translatable("item.mmdlib_companion.recovery_capsule.help")); + } } diff --git a/src/companion/java/zone/moddev/mc/companion/client/CompanionWidgetsClient.java b/src/companion/java/zone/moddev/mc/companion/client/CompanionWidgetsClient.java index d347bfab..9bc87781 100644 --- a/src/companion/java/zone/moddev/mc/companion/client/CompanionWidgetsClient.java +++ b/src/companion/java/zone/moddev/mc/companion/client/CompanionWidgetsClient.java @@ -10,85 +10,84 @@ import zone.moddev.mc.lib.gui.client.WidgetRenderer; import zone.moddev.mc.lib.gui.client.WidgetRenderers; -/** Client-only registration and drawing, using only MMDLib's public widget API. */ +/** + * Client-only registration and drawing, using only MMDLib's public widget API. + */ @EventBusSubscriber(modid = CompanionMod.ID, bus = EventBusSubscriber.Bus.MOD, value = Dist.CLIENT) public final class CompanionWidgetsClient { - private CompanionWidgetsClient() {} + private CompanionWidgetsClient() { + } - @SubscribeEvent - public static void setup(FMLClientSetupEvent event) { - zone.moddev.mc.lib.gui.client.FilterWidgets.register( - zone.moddev.mc.companion.CompanionAddons.TAG_MODE, - access -> - new zone.moddev.mc.lib.gui.client.FilterWidgets.Instance() { - public void render( - net.minecraft.client.gui.GuiGraphics g, - zone.moddev.mc.lib.gui.layout.Rect r, - net.minecraft.nbt.CompoundTag config, - int mx, - int my) { - var button = new zone.moddev.mc.lib.gui.layout.Rect(r.x(), r.y(), 14, 14); - g.blitSprite( - net.minecraft.resources.ResourceLocation.parse( - button.contains(mx, my) - ? "mmdlib:button_flat_highlighted" - : "mmdlib:button_flat"), - button.x(), - button.y(), - button.width(), - button.height()); - g.blitSprite( - net.minecraft.resources.ResourceLocation.parse( - config.getBoolean("deny") - ? "mmdlib:icon_filter_blacklist" - : "mmdlib:icon_filter_whitelist"), - r.x() + 2, - r.y() + 2, - 10, - 10); - } + @SubscribeEvent + public static void setup(FMLClientSetupEvent event) { + zone.moddev.mc.lib.gui.client.FilterWidgets.register( + zone.moddev.mc.companion.CompanionAddons.TAG_MODE, + access -> new zone.moddev.mc.lib.gui.client.FilterWidgets.Instance() { + public void render(net.minecraft.client.gui.GuiGraphics g, + zone.moddev.mc.lib.gui.layout.Rect r, + net.minecraft.nbt.CompoundTag config, int mx, int my) { + var button = new zone.moddev.mc.lib.gui.layout.Rect(r.x(), + r.y(), 14, 14); + g.blitSprite( + net.minecraft.resources.ResourceLocation + .parse(button.contains(mx, my) + ? "mmdlib:button_flat_highlighted" + : "mmdlib:button_flat"), + button.x(), button.y(), button.width(), + button.height()); + g.blitSprite( + net.minecraft.resources.ResourceLocation + .parse(config.getBoolean("deny") + ? "mmdlib:icon_filter_blacklist" + : "mmdlib:icon_filter_whitelist"), + r.x() + 2, r.y() + 2, 10, 10); + } - public net.minecraft.network.chat.Component tooltip() { - return net.minecraft.network.chat.Component.translatable( - access.configuration().get().getBoolean("deny") - ? "screen.mmdlib_companion.filter.blacklist" - : "screen.mmdlib_companion.filter.whitelist"); - } + public net.minecraft.network.chat.Component tooltip() { + return net.minecraft.network.chat.Component.translatable( + access.configuration().get().getBoolean("deny") + ? "screen.mmdlib_companion.filter.blacklist" + : "screen.mmdlib_companion.filter.whitelist"); + } - public boolean click(double x, double y, int button) { - if (!access.pending().getAsBoolean()) - access.submit().accept(!access.configuration().get().getBoolean("deny")); - return true; - } + public boolean click(double x, double y, int button) { + if (!access.pending().getAsBoolean()) + access.submit().accept( + !access.configuration().get().getBoolean("deny")); + return true; + } - public boolean key(int key, int scan, int modifiers) { - if (key == 257 || key == 32) return click(0, 0, 0); - return false; - } + public boolean key(int key, int scan, int modifiers) { + if (key == 257 || key == 32) + return click(0, 0, 0); + return false; + } }); - WidgetRenderers.GLOBAL.registerInteractive(CompanionWidgets.NAME_EDITOR, NameEditor::new); - WidgetRenderers.GLOBAL.registerInteractive(CompanionWidgets.RESERVE_SLIDER, ReserveSlider::new); - WidgetRenderers.GLOBAL.registerInteractive(CompanionWidgets.LANE_SELECTOR, LaneSelector::new); - WidgetRenderers.GLOBAL.register( - CompanionWidgets.PROGRESS_DIAL, CompanionWidgetsClient::drawDial); - } + WidgetRenderers.GLOBAL.registerInteractive(CompanionWidgets.NAME_EDITOR, + NameEditor::new); + WidgetRenderers.GLOBAL.registerInteractive( + CompanionWidgets.RESERVE_SLIDER, ReserveSlider::new); + WidgetRenderers.GLOBAL.registerInteractive( + CompanionWidgets.LANE_SELECTOR, LaneSelector::new); + WidgetRenderers.GLOBAL.register(CompanionWidgets.PROGRESS_DIAL, + CompanionWidgetsClient::drawDial); + } - private static void drawDial(WidgetRenderer.Context context) { - var g = context.graphics(); - var r = context.bounds(); - int color = - 0xff000000 - | Integer.parseInt( - context.widget().properties().getOrDefault("color", "#55CC88").substring(1), 16); - int cx = r.x() + r.width() / 2, cy = r.y() + r.height() / 2; - double radius = Math.min(r.width(), r.height()) / 2.0 - 2; - int lit = (int) Math.round(context.ratio() * 24); - for (int i = 0; i < 24; i++) { - double angle = i * Math.PI * 2 / 24 - Math.PI / 2; - int x = cx + (int) Math.round(Math.cos(angle) * radius); - int y = cy + (int) Math.round(Math.sin(angle) * radius); - g.fill(x - 1, y - 1, x + 1, y + 1, i < lit ? color : 0xff454545); + private static void drawDial(WidgetRenderer.Context context) { + var g = context.graphics(); + var r = context.bounds(); + int color = 0xff000000 | Integer.parseInt(context.widget().properties() + .getOrDefault("color", "#55CC88").substring(1), 16); + int cx = r.x() + r.width() / 2, cy = r.y() + r.height() / 2; + double radius = Math.min(r.width(), r.height()) / 2.0 - 2; + int lit = (int) Math.round(context.ratio() * 24); + for (int i = 0; i < 24; i++) { + double angle = i * Math.PI * 2 / 24 - Math.PI / 2; + int x = cx + (int) Math.round(Math.cos(angle) * radius); + int y = cy + (int) Math.round(Math.sin(angle) * radius); + g.fill(x - 1, y - 1, x + 1, y + 1, i < lit ? color : 0xff454545); + } + g.fill(cx - 1, cy - 1, cx + 1, cy + 1, + context.hovered() ? 0xffffffff : color); } - g.fill(cx - 1, cy - 1, cx + 1, cy + 1, context.hovered() ? 0xffffffff : color); - } } diff --git a/src/companion/java/zone/moddev/mc/companion/client/LaneSelector.java b/src/companion/java/zone/moddev/mc/companion/client/LaneSelector.java index 292dbca4..f4a59bff 100644 --- a/src/companion/java/zone/moddev/mc/companion/client/LaneSelector.java +++ b/src/companion/java/zone/moddev/mc/companion/client/LaneSelector.java @@ -7,81 +7,84 @@ import zone.moddev.mc.lib.gui.client.WidgetInstance; import zone.moddev.mc.lib.gui.layout.Rect; -/** Sends a structured lane-and-state action, validated against this element's configured lane. */ +/** + * Sends a structured lane-and-state action, validated against this element's + * configured lane. + */ final class LaneSelector implements WidgetInstance { - private final WidgetAccess access; - private boolean focused, unconfirmed; + private final WidgetAccess access; + private boolean focused, unconfirmed; - LaneSelector(WidgetAccess access) { - this.access = access; - } + LaneSelector(WidgetAccess access) { + this.access = access; + } - private Rect option(boolean paused) { - var r = access.bounds(); - int w = (r.width() - 4) / 2; - return new Rect(r.x() + (paused ? w + 4 : 0), r.y() + 12, w, 18); - } + private Rect option(boolean paused) { + var r = access.bounds(); + int w = (r.width() - 4) / 2; + return new Rect(r.x() + (paused ? w + 4 : 0), r.y() + 12, w, 18); + } - private void choose(boolean paused) { - if (!access.pending()) - access.submit( - CompanionWidgets.SET_LANE, - new CompanionWidgets.LaneSetting( - Integer.parseInt(access.element().properties().get("lane")), paused)); - } + private void choose(boolean paused) { + if (!access.pending()) + access.submit(CompanionWidgets.SET_LANE, + new CompanionWidgets.LaneSetting( + Integer.parseInt(access.element().properties().get("lane")), + paused)); + } - @Override - public void render(Context context) { - var r = context.bounds(); - context - .graphics() - .drawString( - Minecraft.getInstance().font, + @Override + public void render(Context context) { + var r = context.bounds(); + context.graphics().drawString(Minecraft.getInstance().font, WidgetDrawing.text(unconfirmed ? "unconfirmed" : "lane_state"), - r.x(), - r.y(), - 0xff404040, - false); - boolean paused = Boolean.TRUE.equals(access.value()); - WidgetDrawing.button(context.graphics(), option(false), "run", !paused, !access.pending()); - WidgetDrawing.button(context.graphics(), option(true), "pause", paused, !access.pending()); - if (focused) context.graphics().fill(r.x(), r.bottom() - 1, r.right(), r.bottom(), 0xfff5d477); - } - - @Override - public boolean focusable() { - return true; - } + r.x(), r.y(), 0xff404040, false); + boolean paused = Boolean.TRUE.equals(access.value()); + WidgetDrawing.button(context.graphics(), option(false), "run", !paused, + !access.pending()); + WidgetDrawing.button(context.graphics(), option(true), "pause", paused, + !access.pending()); + if (focused) + context.graphics().fill(r.x(), r.bottom() - 1, r.right(), + r.bottom(), 0xfff5d477); + } - @Override - public void setFocused(boolean value) { - focused = value; - } + @Override + public boolean focusable() { + return true; + } - @Override - public Click mouseClicked(double x, double y, int button) { - if (button == 0) { - if (option(false).contains(x, y)) choose(false); - else if (option(true).contains(x, y)) choose(true); + @Override + public void setFocused(boolean value) { + focused = value; } - return Click.CONSUME; - } - @Override - public boolean keyPressed(int key, int scan, int modifiers) { - if (key == 263 || key == 262) { - choose(key == 262); - return true; + @Override + public Click mouseClicked(double x, double y, int button) { + if (button == 0) { + if (option(false).contains(x, y)) + choose(false); + else if (option(true).contains(x, y)) + choose(true); + } + return Click.CONSUME; } - if (key == 32 || key == 257) { - choose(!Boolean.TRUE.equals(access.value())); - return true; + + @Override + public boolean keyPressed(int key, int scan, int modifiers) { + if (key == 263 || key == 262) { + choose(key == 262); + return true; + } + if (key == 32 || key == 257) { + choose(!Boolean.TRUE.equals(access.value())); + return true; + } + return false; } - return false; - } - @Override - public void actionResult(boolean accepted) { - unconfirmed = !accepted; - } + @Override + public void actionResult(boolean accepted) { + unconfirmed = !accepted; + } } diff --git a/src/companion/java/zone/moddev/mc/companion/client/NameEditor.java b/src/companion/java/zone/moddev/mc/companion/client/NameEditor.java index 8fcd25dd..286bbe28 100644 --- a/src/companion/java/zone/moddev/mc/companion/client/NameEditor.java +++ b/src/companion/java/zone/moddev/mc/companion/client/NameEditor.java @@ -9,168 +9,176 @@ import zone.moddev.mc.lib.gui.client.WidgetInstance; import zone.moddev.mc.lib.gui.layout.Rect; -/** Vanilla text editing supplies selection, cursor movement and clipboard shortcuts. */ +/** + * Vanilla text editing supplies selection, cursor movement and clipboard + * shortcuts. + */ final class NameEditor implements WidgetInstance { - private final WidgetAccess access; - private final EditBox field; - private boolean dirty, syncing; - private String status = ""; - private int selectionAnchor; - private boolean selecting; - - NameEditor(WidgetAccess access) { - this.access = access; - field = - new EditBox( - Minecraft.getInstance().font, access.bounds().width(), 18, WidgetDrawing.text("name")); - field.setMaxLength(32); - field.setResponder( - value -> { - if (!syncing) { - dirty = true; - status = ""; - } + private final WidgetAccess access; + private final EditBox field; + private boolean dirty, syncing; + private String status = ""; + private int selectionAnchor; + private boolean selecting; + + NameEditor(WidgetAccess access) { + this.access = access; + field = new EditBox(Minecraft.getInstance().font, + access.bounds().width(), 18, WidgetDrawing.text("name")); + field.setMaxLength(32); + field.setResponder(value -> { + if (!syncing) { + dirty = true; + status = ""; + } }); - } - - private void position() { - var r = access.bounds(); - field.setX(r.x()); - field.setY(r.y()); - field.setWidth(r.width()); - } - - private Rect applyBounds() { - var r = access.bounds(); - return new Rect(r.x(), r.y() + 22, 44, 18); - } - - private Rect cancelBounds() { - var r = access.bounds(); - return new Rect(r.x() + 48, r.y() + 22, 44, 18); - } - - private void reset() { - if (access.value() instanceof Component value) { - syncing = true; - if (!field.getValue().equals(value.getString())) field.setValue(value.getString()); - syncing = false; - dirty = false; - } - status = ""; - } - - @Override - public void render(Context context) { - position(); - if (!dirty && !access.pending()) reset(); - field.setEditable(!access.pending()); - field.render(context.graphics(), context.mouseX(), context.mouseY(), context.partialTick()); - WidgetDrawing.button(context.graphics(), applyBounds(), "apply", false, !access.pending()); - WidgetDrawing.button(context.graphics(), cancelBounds(), "cancel", false, !access.pending()); - String message = access.pending() ? "waiting" : status; - if (!message.isEmpty()) - context - .graphics() - .drawString( - Minecraft.getInstance().font, - WidgetDrawing.text(message), - access.bounds().x() + 96, - access.bounds().y() + 27, - 0xff883333, - false); - } - - private void apply() { - if (access.pending()) return; - if (field.getValue().isBlank()) { - status = "name_required"; - return; - } - access.submit(CompanionWidgets.RENAME, field.getValue()); - } - - @Override - public boolean focusable() { - return true; - } - - @Override - public void setFocused(boolean focused) { - field.setFocused(focused); - } - - @Override - public Click mouseClicked(double x, double y, int button) { - position(); - if (button != 0) return Click.CONSUME; - if (applyBounds().contains(x, y)) apply(); - else if (cancelBounds().contains(x, y)) { - if (!access.pending()) reset(); - } else if (!access.pending() && field.mouseClicked(x, y, button)) { - selectionAnchor = field.getCursorPosition(); - selecting = true; - return Click.CAPTURE; - } - return Click.CONSUME; - } - - @Override - public boolean keyPressed(int key, int scan, int modifiers) { - if (key == 256) { - if (!dirty || access.pending()) return false; - reset(); - return true; - } - if (key == 257 || key == 335) { - apply(); - return true; - } - return !access.pending() && field.keyPressed(key, scan, modifiers); - } - - @Override - public boolean charTyped(char character, int modifiers) { - return !access.pending() && field.charTyped(character, modifiers); - } - - @Override - public void resized() { - position(); - } - - @Override - public boolean mouseDragged(double x, double y, int button, double dx, double dy) { - if (selecting) { - field.onClick(x, y); - field.setHighlightPos(selectionAnchor); - } - return true; - } - - @Override - public boolean mouseReleased(double x, double y, int button) { - boolean handled = selecting; - selecting = false; - return handled; - } - - @Override - public void cancelInteraction() { - selecting = false; - } - - @Override - public void visibilityChanged(boolean visible) { - if (!visible) reset(); - } - - @Override - public void actionResult(boolean accepted) { - if (accepted) reset(); - else { - dirty = true; - status = "unconfirmed"; - } - } + } + + private void position() { + var r = access.bounds(); + field.setX(r.x()); + field.setY(r.y()); + field.setWidth(r.width()); + } + + private Rect applyBounds() { + var r = access.bounds(); + return new Rect(r.x(), r.y() + 22, 44, 18); + } + + private Rect cancelBounds() { + var r = access.bounds(); + return new Rect(r.x() + 48, r.y() + 22, 44, 18); + } + + private void reset() { + if (access.value() instanceof Component value) { + syncing = true; + if (!field.getValue().equals(value.getString())) + field.setValue(value.getString()); + syncing = false; + dirty = false; + } + status = ""; + } + + @Override + public void render(Context context) { + position(); + if (!dirty && !access.pending()) + reset(); + field.setEditable(!access.pending()); + field.render(context.graphics(), context.mouseX(), context.mouseY(), + context.partialTick()); + WidgetDrawing.button(context.graphics(), applyBounds(), "apply", false, + !access.pending()); + WidgetDrawing.button(context.graphics(), cancelBounds(), "cancel", + false, !access.pending()); + String message = access.pending() ? "waiting" : status; + if (!message.isEmpty()) + context.graphics().drawString(Minecraft.getInstance().font, + WidgetDrawing.text(message), access.bounds().x() + 96, + access.bounds().y() + 27, 0xff883333, false); + } + + private void apply() { + if (access.pending()) + return; + if (field.getValue().isBlank()) { + status = "name_required"; + return; + } + access.submit(CompanionWidgets.RENAME, field.getValue()); + } + + @Override + public boolean focusable() { + return true; + } + + @Override + public void setFocused(boolean focused) { + field.setFocused(focused); + } + + @Override + public Click mouseClicked(double x, double y, int button) { + position(); + if (button != 0) + return Click.CONSUME; + if (applyBounds().contains(x, y)) + apply(); + else if (cancelBounds().contains(x, y)) { + if (!access.pending()) + reset(); + } else if (!access.pending() && field.mouseClicked(x, y, button)) { + selectionAnchor = field.getCursorPosition(); + selecting = true; + return Click.CAPTURE; + } + return Click.CONSUME; + } + + @Override + public boolean keyPressed(int key, int scan, int modifiers) { + if (key == 256) { + if (!dirty || access.pending()) + return false; + reset(); + return true; + } + if (key == 257 || key == 335) { + apply(); + return true; + } + return !access.pending() && field.keyPressed(key, scan, modifiers); + } + + @Override + public boolean charTyped(char character, int modifiers) { + return !access.pending() && field.charTyped(character, modifiers); + } + + @Override + public void resized() { + position(); + } + + @Override + public boolean mouseDragged(double x, double y, int button, double dx, + double dy) { + if (selecting) { + field.onClick(x, y); + field.setHighlightPos(selectionAnchor); + } + return true; + } + + @Override + public boolean mouseReleased(double x, double y, int button) { + boolean handled = selecting; + selecting = false; + return handled; + } + + @Override + public void cancelInteraction() { + selecting = false; + } + + @Override + public void visibilityChanged(boolean visible) { + if (!visible) + reset(); + } + + @Override + public void actionResult(boolean accepted) { + if (accepted) + reset(); + else { + dirty = true; + status = "unconfirmed"; + } + } } diff --git a/src/companion/java/zone/moddev/mc/companion/client/ReserveSlider.java b/src/companion/java/zone/moddev/mc/companion/client/ReserveSlider.java index 8d00374c..ab58a779 100644 --- a/src/companion/java/zone/moddev/mc/companion/client/ReserveSlider.java +++ b/src/companion/java/zone/moddev/mc/companion/client/ReserveSlider.java @@ -6,114 +6,123 @@ import zone.moddev.mc.lib.gui.client.WidgetAccess; import zone.moddev.mc.lib.gui.client.WidgetInstance; -/** Drag previews stay local; only release commits. Arrow keys and scroll commit one step. */ +/** + * Drag previews stay local; only release commits. Arrow keys and scroll commit + * one step. + */ final class ReserveSlider implements WidgetInstance { - private final WidgetAccess access; - private int preview; - private boolean dragging, focused, unconfirmed; - - ReserveSlider(WidgetAccess access) { - this.access = access; - } - - private int confirmed() { - return access.value() instanceof Number value ? value.intValue() : 0; - } - - private void at(double x) { - var r = access.bounds(); - preview = (int) Math.clamp(Math.round((x - r.x() - 3) * 100 / (r.width() - 6)), 0, 100); - } - - private void commit() { - access.submit(CompanionWidgets.RESERVE, preview); - } - - @Override - public void render(Context context) { - if (!dragging && !access.pending()) preview = confirmed(); - var r = context.bounds(); - var g = context.graphics(); - g.drawString( - Minecraft.getInstance().font, - WidgetDrawing.text(unconfirmed ? "reserve.unconfirmed" : "reserve", preview), - r.x(), - r.y(), - 0xff404040, - false); - g.fill(r.x(), r.y() + 15, r.right(), r.y() + 28, focused ? 0xfff5d477 : 0xff444444); - g.fill(r.x() + 1, r.y() + 16, r.right() - 1, r.y() + 27, 0xff727272); - int knob = r.x() + 3 + (r.width() - 6) * preview / 100; - g.fill(knob - 2, r.y() + 16, knob + 2, r.y() + 27, access.pending() ? 0xffaaaaaa : 0xff66cc88); - } - - @Override - public boolean focusable() { - return true; - } - - @Override - public void setFocused(boolean value) { - focused = value; - } - - @Override - public Click mouseClicked(double x, double y, int button) { - if (button != 0 || access.pending()) return Click.CONSUME; - unconfirmed = false; - dragging = true; - at(x); - return Click.CAPTURE; - } - - @Override - public boolean mouseDragged(double x, double y, int button, double dx, double dy) { - at(x); - return true; - } - - @Override - public boolean mouseReleased(double x, double y, int button) { - if (!dragging || button != 0) return false; - at(x); - dragging = false; - commit(); - return true; - } - - @Override - public void cancelInteraction() { - dragging = false; - preview = confirmed(); - } - - @Override - public boolean keyPressed(int key, int scan, int modifiers) { - if (key != 263 && key != 262 && key != 268 && key != 269) return false; - if (access.pending()) return true; - preview = - key == 268 + private final WidgetAccess access; + private int preview; + private boolean dragging, focused, unconfirmed; + + ReserveSlider(WidgetAccess access) { + this.access = access; + } + + private int confirmed() { + return access.value() instanceof Number value ? value.intValue() : 0; + } + + private void at(double x) { + var r = access.bounds(); + preview = (int) Math + .clamp(Math.round((x - r.x() - 3) * 100 / (r.width() - 6)), 0, 100); + } + + private void commit() { + access.submit(CompanionWidgets.RESERVE, preview); + } + + @Override + public void render(Context context) { + if (!dragging && !access.pending()) + preview = confirmed(); + var r = context.bounds(); + var g = context.graphics(); + g.drawString( + Minecraft.getInstance().font, WidgetDrawing + .text(unconfirmed ? "reserve.unconfirmed" : "reserve", preview), + r.x(), r.y(), 0xff404040, false); + g.fill(r.x(), r.y() + 15, r.right(), r.y() + 28, + focused ? 0xfff5d477 : 0xff444444); + g.fill(r.x() + 1, r.y() + 16, r.right() - 1, r.y() + 27, 0xff727272); + int knob = r.x() + 3 + (r.width() - 6) * preview / 100; + g.fill(knob - 2, r.y() + 16, knob + 2, r.y() + 27, + access.pending() ? 0xffaaaaaa : 0xff66cc88); + } + + @Override + public boolean focusable() { + return true; + } + + @Override + public void setFocused(boolean value) { + focused = value; + } + + @Override + public Click mouseClicked(double x, double y, int button) { + if (button != 0 || access.pending()) + return Click.CONSUME; + unconfirmed = false; + dragging = true; + at(x); + return Click.CAPTURE; + } + + @Override + public boolean mouseDragged(double x, double y, int button, double dx, + double dy) { + at(x); + return true; + } + + @Override + public boolean mouseReleased(double x, double y, int button) { + if (!dragging || button != 0) + return false; + at(x); + dragging = false; + commit(); + return true; + } + + @Override + public void cancelInteraction() { + dragging = false; + preview = confirmed(); + } + + @Override + public boolean keyPressed(int key, int scan, int modifiers) { + if (key != 263 && key != 262 && key != 268 && key != 269) + return false; + if (access.pending()) + return true; + preview = key == 268 ? 0 : key == 269 ? 100 - : Math.clamp( - confirmed() + (key == 262 ? 1 : -1) * ((modifiers & 1) != 0 ? 10 : 1), 0, 100); - commit(); - return true; - } - - @Override - public boolean mouseScrolled(double x, double y, double dx, double dy) { - if (!access.pending() && dy != 0) { - preview = Math.clamp(confirmed() + (dy > 0 ? 1 : -1), 0, 100); - commit(); + : Math.clamp(confirmed() + + (key == 262 ? 1 : -1) * ((modifiers & 1) != 0 ? 10 : 1), + 0, 100); + commit(); + return true; + } + + @Override + public boolean mouseScrolled(double x, double y, double dx, double dy) { + if (!access.pending() && dy != 0) { + preview = Math.clamp(confirmed() + (dy > 0 ? 1 : -1), 0, 100); + commit(); + } + return true; + } + + @Override + public void actionResult(boolean accepted) { + unconfirmed = !accepted; + preview = confirmed(); } - return true; - } - - @Override - public void actionResult(boolean accepted) { - unconfirmed = !accepted; - preview = confirmed(); - } } diff --git a/src/companion/java/zone/moddev/mc/companion/client/WidgetDrawing.java b/src/companion/java/zone/moddev/mc/companion/client/WidgetDrawing.java index a2b55ae2..2ae379a9 100644 --- a/src/companion/java/zone/moddev/mc/companion/client/WidgetDrawing.java +++ b/src/companion/java/zone/moddev/mc/companion/client/WidgetDrawing.java @@ -7,23 +7,23 @@ import zone.moddev.mc.lib.gui.layout.Rect; final class WidgetDrawing { - private WidgetDrawing() {} + private WidgetDrawing() { + } - static Component text(String name, Object... arguments) { - return Component.translatable("screen.mmdlib_companion." + name, arguments); - } + static Component text(String name, Object... arguments) { + return Component.translatable("screen.mmdlib_companion." + name, + arguments); + } - static void button(GuiGraphics g, Rect r, String label, boolean selected, boolean enabled) { - g.fill(r.x(), r.y(), r.right(), r.bottom(), selected ? 0xff66bb88 : 0xff444444); - g.fill(r.x() + 1, r.y() + 1, r.right() - 1, r.bottom() - 1, enabled ? 0xff727272 : 0xff505050); - var font = Minecraft.getInstance().font; - var text = text(label); - g.drawString( - font, - text, - r.x() + (r.width() - font.width(text)) / 2, - r.y() + 5, - enabled ? 0xffffffff : 0xffaaaaaa, - false); - } + static void button(GuiGraphics g, Rect r, String label, boolean selected, + boolean enabled) { + g.fill(r.x(), r.y(), r.right(), r.bottom(), + selected ? 0xff66bb88 : 0xff444444); + g.fill(r.x() + 1, r.y() + 1, r.right() - 1, r.bottom() - 1, + enabled ? 0xff727272 : 0xff505050); + var font = Minecraft.getInstance().font; + var text = text(label); + g.drawString(font, text, r.x() + (r.width() - font.width(text)) / 2, + r.y() + 5, enabled ? 0xffffffff : 0xffaaaaaa, false); + } } diff --git a/src/companion/java/zone/moddev/mc/companion/recipe/LavaSmeltingRecipes.java b/src/companion/java/zone/moddev/mc/companion/recipe/LavaSmeltingRecipes.java index 2d02e0a8..902c4168 100644 --- a/src/companion/java/zone/moddev/mc/companion/recipe/LavaSmeltingRecipes.java +++ b/src/companion/java/zone/moddev/mc/companion/recipe/LavaSmeltingRecipes.java @@ -6,21 +6,22 @@ import zone.moddev.mc.lib.machine.recipe.generation.*; -/** Mod-owned adapter: the library emits energy recipes; this mod chooses lava as the fuel. */ +/** + * Mod-owned adapter: the library emits energy recipes; this mod chooses lava as + * the fuel. + */ public final class LavaSmeltingRecipes extends SmeltingRecipeGenerator { - @Override - public void generate(RecipeGenerationContext ctx, MachineRecipeOutput output) { - var p = ctx.properties(); - String group = p.string("fluid-group", "fuel"); - long rate = p.longValue("energy-per-mb", 40, 1, Long.MAX_VALUE); - var lava = - ctx.registries() - .lookupOrThrow(Registries.FLUID) + @Override + public void generate(RecipeGenerationContext ctx, + MachineRecipeOutput output) { + var p = ctx.properties(); + String group = p.string("fluid-group", "fuel"); + long rate = p.longValue("energy-per-mb", 40, 1, Long.MAX_VALUE); + var lava = ctx.registries().lookupOrThrow(Registries.FLUID) .getOrThrow(Fluids.LAVA.builtInRegistryHolder().key()); - super.generate( - ctx.withProperties(p.without("fluid-group", "energy-per-mb")), - (id, recipe) -> - output.accept( - id, RecipeTransforms.energyToFluid(recipe, group, HolderSet.direct(lava), rate))); - } + super.generate( + ctx.withProperties(p.without("fluid-group", "energy-per-mb")), + (id, recipe) -> output.accept(id, RecipeTransforms + .energyToFluid(recipe, group, HolderSet.direct(lava), rate))); + } } diff --git a/src/companionTest/java/zone/moddev/mc/companion/test/AutomationClientSmoke.java b/src/companionTest/java/zone/moddev/mc/companion/test/AutomationClientSmoke.java index 7e920689..56ffc17e 100644 --- a/src/companionTest/java/zone/moddev/mc/companion/test/AutomationClientSmoke.java +++ b/src/companionTest/java/zone/moddev/mc/companion/test/AutomationClientSmoke.java @@ -15,48 +15,49 @@ import zone.moddev.mc.lib.gui.model.containers.PlayerInventoryElement; import zone.moddev.mc.lib.gui.runtime.component.BuiltinMenuComponents; -/** Real client/server round trips, screenshots and item-owned editor transitions. */ +/** + * Real client/server round trips, screenshots and item-owned editor + * transitions. + */ final class AutomationClientSmoke { private static int stage; private static int compactHeight; - private static zone.moddev.mc.lib.gui.layout.Rect automationBounds(MachineScreen screen) { + private static zone.moddev.mc.lib.gui.layout.Rect automationBounds( + MachineScreen screen) { return screen.exclusions().stream() - .filter(rect -> rect.x() >= screen.panelBounds().right() && rect.width() > 22) - .findFirst() - .orElseThrow(); + .filter(rect -> rect.x() >= screen.panelBounds().right() + && rect.width() > 22) + .findFirst().orElseThrow(); } private static void toggleFilter(MachineScreen screen) { var drawer = automationBounds(screen); - var toggle = AutomationLayout.filterToggle(0).offset(drawer.x(), drawer.y()); - if (!screen.mouseClicked(toggle.x() + toggle.width() / 2, toggle.y() + toggle.height() / 2, - 0)) + var toggle = AutomationLayout.filterToggle(0).offset(drawer.x(), + drawer.y()); + if (!screen.mouseClicked(toggle.x() + toggle.width() / 2, + toggle.y() + toggle.height() / 2, 0)) throw new AssertionError("Filter toggle bar accepts clicks"); } - static boolean renderTooltipCapture(MachineScreen screen, GuiGraphics graphics, float partial) { - if (stage == 2 - && !screen - .getMenu() - .components() - .get(BuiltinMenuComponents.AUTOMATION) - .filter(0, 0) - .isEmpty()) { - var slot = screen - .getMenu().slots - .get( - screen.getMenu().components().get(BuiltinMenuComponents.AUTOMATION).slotStart() - + zone.moddev.mc.lib.api.automation.AutomationInventory.filterSlot(0, 0)); - screen.render(graphics, screen.guiLeft() + slot.x + 6, screen.guiTop() + slot.y + 6, - partial); + static boolean renderTooltipCapture(MachineScreen screen, + GuiGraphics graphics, float partial) { + if (stage == 2 && !screen.getMenu().components() + .get(BuiltinMenuComponents.AUTOMATION).filter(0, 0).isEmpty()) { + var slot = screen.getMenu().slots.get(screen.getMenu().components() + .get(BuiltinMenuComponents.AUTOMATION).slotStart() + + zone.moddev.mc.lib.api.automation.AutomationInventory + .filterSlot(0, 0)); + screen.render(graphics, screen.guiLeft() + slot.x + 6, + screen.guiTop() + slot.y + 6, partial); return true; } - if (stage != 8 - || !screen.getMenu().components().get(BuiltinMenuComponents.AUTOMATION).open()) + if (stage != 8 || !screen.getMenu().components() + .get(BuiltinMenuComponents.AUTOMATION).open()) return false; var drawer = automationBounds(screen); - screen.renderWithTooltip(graphics, drawer.x() + 81, drawer.y() + 52, partial); + screen.renderWithTooltip(graphics, drawer.x() + 81, drawer.y() + 52, + partial); return true; } @@ -64,30 +65,37 @@ static boolean run(MachineScreen screen) { var mc = Minecraft.getInstance(); var menu = screen.getMenu(); var a = menu.components().get(BuiltinMenuComponents.AUTOMATION); - if (screen.guiLeft() != (screen.width - screen.layout.panel().width()) / 2 - || screen.guiTop() != (screen.height - screen.layout.panel().height()) / 2) - throw new AssertionError("Drawers must never move the centred main panel"); + if (screen.guiLeft() != (screen.width - screen.layout.panel().width()) + / 2 + || screen + .guiTop() != (screen.height - screen.layout.panel().height()) + / 2) + throw new AssertionError( + "Drawers must never move the centred main panel"); if (a.pending()) return false; if (stage == 0) { if (a.inventory.groups().size() != 7) throw new AssertionError("Seven colour selectors"); - var info = (zone.moddev.mc.lib.gui.model.elements.PopoutElement) menu.bound.screen() - .find("info").orElseThrow(); + var info = (zone.moddev.mc.lib.gui.model.elements.PopoutElement) menu.bound + .screen().find("info").orElseThrow(); if (info.edge() != zone.moddev.mc.lib.gui.model.Edge.LEFT) throw new AssertionError("Information stays on the left"); - var colours = screen.layout.region(PlayerInventoryElement.TOOLBAR).orElseThrow(); - if (!screen.mouseClicked( - screen.guiLeft() + colours.x() + 3, screen.guiTop() + colours.y() + 3, 0)) + var colours = screen.layout.region(PlayerInventoryElement.TOOLBAR) + .orElseThrow(); + if (!screen.mouseClicked(screen.guiLeft() + colours.x() + 3, + screen.guiTop() + colours.y() + 3, 0)) throw new AssertionError( "Inventory colour bar opens the first displayed inventory"); for (int tick = 0; tick < 99; tick++) a.tick(); if (!a.pending()) - throw new AssertionError("Automation remains pending before timeout"); + throw new AssertionError( + "Automation remains pending before timeout"); a.tick(); if (a.pending()) - throw new AssertionError("Automation timeout unlocks without resync"); + throw new AssertionError( + "Automation timeout unlocks without resync"); stage++; return false; } @@ -95,7 +103,8 @@ static boolean run(MachineScreen screen) { if (!a.open()) throw new AssertionError("Colour opens drawer"); if (a.selected() != 2) - throw new AssertionError("Leftmost tank appears before item inventories"); + throw new AssertionError( + "Leftmost tank appears before item inventories"); var drawer = automationBounds(screen); var info = menu.bound.screen().find("info").orElseThrow(); if (drawer.y() != screen.bounds(info).y()) @@ -105,34 +114,29 @@ static boolean run(MachineScreen screen) { if (compactHeight >= screen.panelBounds().height()) throw new AssertionError( "The configuration drawer should fit its controls, not the main panel height"); - Screenshot.grab( - mc.gameDirectory, "automation-seven-colours.png", mc.getMainRenderTarget(), m -> { + Screenshot.grab(mc.gameDirectory, "automation-seven-colours.png", + mc.getMainRenderTarget(), m -> { }); a.send("select", 0, -1, "", 0, ""); - mc.getSingleplayerServer() - .execute( - () -> { - var player = mc.getSingleplayerServer().getPlayerList().getPlayers() - .getFirst(); - var machine = (ProcessorEntity) player.serverLevel() - .getBlockEntity(menu.blockEntity.getBlockPos()); - machine.automation - .slots() - .setStackInSlot(0, new ItemStack(CompanionAddons.PULLER.get())); - machine.automation - .slots() - .setStackInSlot(4, new ItemStack(CompanionAddons.LARGE_FILTER.get())); - player.containerMenu.broadcastChanges(); - }); + mc.getSingleplayerServer().execute(() -> { + var player = mc.getSingleplayerServer().getPlayerList() + .getPlayers().getFirst(); + var machine = (ProcessorEntity) player.serverLevel() + .getBlockEntity(menu.blockEntity.getBlockPos()); + machine.automation.slots().setStackInSlot(0, + new ItemStack(CompanionAddons.PULLER.get())); + machine.automation.slots().setStackInSlot(4, + new ItemStack(CompanionAddons.LARGE_FILTER.get())); + player.containerMenu.broadcastChanges(); + }); stage++; return false; } if (stage == 2) { if (a.filter(0, 0).isEmpty()) return false; - Screenshot.grab( - mc.gameDirectory, "automation-filter-collapsed.png", mc.getMainRenderTarget(), - m -> { + Screenshot.grab(mc.gameDirectory, "automation-filter-collapsed.png", + mc.getMainRenderTarget(), m -> { }); toggleFilter(screen); stage++; @@ -142,13 +146,13 @@ static boolean run(MachineScreen screen) { if (a.editor() != 0) throw new AssertionError("Filter editor opens"); var exclusions = screen.exclusions(); - var ownExclusions = screen.drawers().entries().stream() - .filter(zone.moddev.mc.lib.gui.client.SideConfigDrawer.class::isInstance) - .findFirst() - .orElseThrow() - .exclusions(); - if (ownExclusions.size() != 2 || !exclusions.containsAll(ownExclusions)) - throw new AssertionError("Filter must remain inside the existing drawer exclusion"); + var ownExclusions = screen.drawers().entries().stream().filter( + zone.moddev.mc.lib.gui.client.SideConfigDrawer.class::isInstance) + .findFirst().orElseThrow().exclusions(); + if (ownExclusions.size() != 2 + || !exclusions.containsAll(ownExclusions)) + throw new AssertionError( + "Filter must remain inside the existing drawer exclusion"); var drawer = automationBounds(screen); if (drawer.y() != screen.guiTop() + SideDrawerLayout.MARGIN || drawer.height() <= compactHeight @@ -156,76 +160,76 @@ static boolean run(MachineScreen screen) { || drawer.bottom() > screen.drawerBottom()) throw new AssertionError( "A large filter should grow beyond the panel when the screen permits it"); - Screenshot.grab( - mc.gameDirectory, "automation-filter-grid.png", mc.getMainRenderTarget(), m -> { + Screenshot.grab(mc.gameDirectory, "automation-filter-grid.png", + mc.getMainRenderTarget(), m -> { }); screen.resize(mc, screen.width, 360); var limited = automationBounds(screen); if (limited.bottom() != screen.drawerBottom()) - throw new AssertionError("The growing drawer must stop at the screen margin"); - if (!screen.mouseScrolled(limited.x() + 40, limited.bottom() - 20, 0, -2)) - throw new AssertionError("The embedded filter grid must scroll inside the drawer"); + throw new AssertionError( + "The growing drawer must stop at the screen margin"); + if (!screen.mouseScrolled(limited.x() + 40, limited.bottom() - 20, + 0, -2)) + throw new AssertionError( + "The embedded filter grid must scroll inside the drawer"); a.send("edit", 0, 0, "deny", 0, ""); stage++; return false; } if (stage == 4) { - Screenshot.grab( - mc.gameDirectory, "automation-filter-constrained.png", mc.getMainRenderTarget(), + Screenshot.grab(mc.gameDirectory, + "automation-filter-constrained.png", mc.getMainRenderTarget(), m -> { }); screen.resize(mc, mc.getWindow().getGuiScaledWidth(), mc.getWindow().getGuiScaledHeight()); - var provider = a.filter(0, 0) - .getCapability(zone.moddev.mc.lib.api.automation.AddonCapabilities.FILTER); + var provider = a.filter(0, 0).getCapability( + zone.moddev.mc.lib.api.automation.AddonCapabilities.FILTER); if (!provider.configuration(a.filter(0, 0)).getBoolean("deny")) throw new AssertionError("Filter toggle synced"); - mc.getSingleplayerServer() - .execute( - () -> { - var player = mc.getSingleplayerServer().getPlayerList().getPlayers() - .getFirst(); - var machine = (ProcessorEntity) player.serverLevel() - .getBlockEntity(menu.blockEntity.getBlockPos()); - machine.automation - .slots() - .setStackInSlot(4, new ItemStack(CompanionAddons.TAG_FILTER.get())); - player.containerMenu.broadcastChanges(); - }); + mc.getSingleplayerServer().execute(() -> { + var player = mc.getSingleplayerServer().getPlayerList() + .getPlayers().getFirst(); + var machine = (ProcessorEntity) player.serverLevel() + .getBlockEntity(menu.blockEntity.getBlockPos()); + machine.automation.slots().setStackInSlot(4, + new ItemStack(CompanionAddons.TAG_FILTER.get())); + player.containerMenu.broadcastChanges(); + }); stage++; return false; } if (stage == 5) { - if (!a.filter(0, 0).is(CompanionAddons.TAG_FILTER.get()) || a.editor() != -1) + if (!a.filter(0, 0).is(CompanionAddons.TAG_FILTER.get()) + || a.editor() != -1) return false; toggleFilter(screen); stage++; return false; } if (stage == 6) { - if (automationBounds(screen).height() >= screen.panelBounds().height()) + if (automationBounds(screen).height() >= screen.panelBounds() + .height()) throw new AssertionError( "A smaller filter should shrink the drawer to its content"); - Screenshot.grab( - mc.gameDirectory, "automation-filter-tag.png", mc.getMainRenderTarget(), m -> { + Screenshot.grab(mc.gameDirectory, "automation-filter-tag.png", + mc.getMainRenderTarget(), m -> { }); a.send("select", 6, -1, "", 0, ""); - mc.getSingleplayerServer() - .execute( - () -> { - var player = mc.getSingleplayerServer().getPlayerList().getPlayers() - .getFirst(); - var machine = (ProcessorEntity) player.serverLevel() - .getBlockEntity(menu.blockEntity.getBlockPos()); - var sides = machine.automation.group(6).sides(); - sides.set(RelativeFace.FRONT, IoMode.INPUT); - sides.set(RelativeFace.LEFT, IoMode.OUTPUT); - sides.set(RelativeFace.RIGHT, IoMode.BOTH); - sides.set(RelativeFace.BOTTOM, IoMode.DISABLED); - sides.set(RelativeFace.TOP, IoMode.DISABLED); - sides.set(RelativeFace.BACK, IoMode.OUTPUT); - player.containerMenu.broadcastChanges(); - }); + mc.getSingleplayerServer().execute(() -> { + var player = mc.getSingleplayerServer().getPlayerList() + .getPlayers().getFirst(); + var machine = (ProcessorEntity) player.serverLevel() + .getBlockEntity(menu.blockEntity.getBlockPos()); + var sides = machine.automation.group(6).sides(); + sides.set(RelativeFace.FRONT, IoMode.INPUT); + sides.set(RelativeFace.LEFT, IoMode.OUTPUT); + sides.set(RelativeFace.RIGHT, IoMode.BOTH); + sides.set(RelativeFace.BOTTOM, IoMode.DISABLED); + sides.set(RelativeFace.TOP, IoMode.DISABLED); + sides.set(RelativeFace.BACK, IoMode.OUTPUT); + player.containerMenu.broadcastChanges(); + }); stage++; return false; } @@ -238,48 +242,43 @@ static boolean run(MachineScreen screen) { if (automationBounds(screen).height() != compactHeight) throw new AssertionError( "Changing inventory should restore the compact drawer height"); - Screenshot.grab(mc.gameDirectory, "automation-energy.png", mc.getMainRenderTarget(), - m -> { + Screenshot.grab(mc.gameDirectory, "automation-energy.png", + mc.getMainRenderTarget(), m -> { }); - Screenshot.grab( - mc.gameDirectory, "automation-side-colours.png", mc.getMainRenderTarget(), m -> { + Screenshot.grab(mc.gameDirectory, "automation-side-colours.png", + mc.getMainRenderTarget(), m -> { }); stage++; return false; } if (stage == 8) { - Screenshot.grab( - mc.gameDirectory, "automation-side-tooltip.png", mc.getMainRenderTarget(), m -> { + Screenshot.grab(mc.gameDirectory, "automation-side-tooltip.png", + mc.getMainRenderTarget(), m -> { }); a.send("close", 6, -1, "", 0, ""); stage++; return false; } if (stage == 9) { - mc.getSingleplayerServer() - .execute( - () -> { - var player = mc.getSingleplayerServer().getPlayerList().getPlayers() - .getFirst(); - var machine = (ProcessorEntity) player.serverLevel() - .getBlockEntity(menu.blockEntity.getBlockPos()); - var filter = new ItemStack(CompanionAddons.ITEM_FILTER.get()); - var provider = filter.getCapability( - zone.moddev.mc.lib.api.automation.AddonCapabilities.FILTER); - var config = provider.configuration(filter); - var entries = new net.minecraft.nbt.ListTag(); - for (String item : new String[]{ - "minecraft:diamond", - "minecraft:dirt", - "minecraft:water_bucket", - "minecraft:glass" - }) - entries.add(net.minecraft.nbt.StringTag.valueOf(item)); - config.put("samples", entries); - provider.configure(filter, config); - machine.automation.slots().setStackInSlot(4, filter); - player.containerMenu.broadcastChanges(); - }); + mc.getSingleplayerServer().execute(() -> { + var player = mc.getSingleplayerServer().getPlayerList() + .getPlayers().getFirst(); + var machine = (ProcessorEntity) player.serverLevel() + .getBlockEntity(menu.blockEntity.getBlockPos()); + var filter = new ItemStack(CompanionAddons.ITEM_FILTER.get()); + var provider = filter.getCapability( + zone.moddev.mc.lib.api.automation.AddonCapabilities.FILTER); + var config = provider.configuration(filter); + var entries = new net.minecraft.nbt.ListTag(); + for (String item : new String[]{"minecraft:diamond", + "minecraft:dirt", "minecraft:water_bucket", + "minecraft:glass"}) + entries.add(net.minecraft.nbt.StringTag.valueOf(item)); + config.put("samples", entries); + provider.configure(filter, config); + machine.automation.slots().setStackInSlot(4, filter); + player.containerMenu.broadcastChanges(); + }); a.send("select", 0, -1, "", 0, ""); stage++; return false; @@ -292,39 +291,40 @@ static boolean run(MachineScreen screen) { return false; } if (stage == 11) { - var provider = a.filter(0, 0) - .getCapability(zone.moddev.mc.lib.api.automation.AddonCapabilities.FILTER); + var provider = a.filter(0, 0).getCapability( + zone.moddev.mc.lib.api.automation.AddonCapabilities.FILTER); var samples = zone.moddev.mc.lib.api.automation.FilterControl - .flatten(provider.controls(a.filter(0, 0))) - .stream() - .filter(c -> c instanceof zone.moddev.mc.lib.api.automation.FilterControl.Samples) - .map(c -> (zone.moddev.mc.lib.api.automation.FilterControl.Samples) c) - .findFirst() - .orElseThrow(); + .flatten(provider.controls(a.filter(0, 0))).stream() + .filter( + c -> c instanceof zone.moddev.mc.lib.api.automation.FilterControl.Samples) + .map( + c -> (zone.moddev.mc.lib.api.automation.FilterControl.Samples) c) + .findFirst().orElseThrow(); if (samples.count() != 8) - throw new AssertionError("Basic filter has two rows of four ghost cells"); - Screenshot.grab( - mc.gameDirectory, "automation-filter-whitelist.png", mc.getMainRenderTarget(), - m -> { + throw new AssertionError( + "Basic filter has two rows of four ghost cells"); + Screenshot.grab(mc.gameDirectory, "automation-filter-whitelist.png", + mc.getMainRenderTarget(), m -> { }); var drawer = automationBounds(screen); if (!screen.mouseClicked( drawer.x() + AutomationLayout.EDITOR_X + 10, - drawer.y() + AutomationLayout.EDITOR_Y + AutomationLayout.EDITOR_PADDING + 5, + drawer.y() + AutomationLayout.EDITOR_Y + + AutomationLayout.EDITOR_PADDING + 5, 0)) - throw new AssertionError("Header-free mode toggle accepts clicks"); + throw new AssertionError( + "Header-free mode toggle accepts clicks"); stage++; return false; } if (stage == 12) { - var provider = a.filter(0, 0) - .getCapability(zone.moddev.mc.lib.api.automation.AddonCapabilities.FILTER); + var provider = a.filter(0, 0).getCapability( + zone.moddev.mc.lib.api.automation.AddonCapabilities.FILTER); if (!provider.configuration(a.filter(0, 0)).getBoolean("deny")) throw new AssertionError( "Mode toggle changes whitelist to blacklist without collapsing editor"); - Screenshot.grab( - mc.gameDirectory, "automation-filter-blacklist.png", mc.getMainRenderTarget(), - m -> { + Screenshot.grab(mc.gameDirectory, "automation-filter-blacklist.png", + mc.getMainRenderTarget(), m -> { }); a.send("close", 0, -1, "", 0, ""); stage++; diff --git a/src/companionTest/java/zone/moddev/mc/companion/test/AutomationGameTests.java b/src/companionTest/java/zone/moddev/mc/companion/test/AutomationGameTests.java index 7792aff5..ca545a46 100644 --- a/src/companionTest/java/zone/moddev/mc/companion/test/AutomationGameTests.java +++ b/src/companionTest/java/zone/moddev/mc/companion/test/AutomationGameTests.java @@ -31,7 +31,8 @@ public final class AutomationGameTests { private static ProcessorEntity place(GameTestHelper h) { var pos = h.absolutePos(new BlockPos(2, 2, 2)); - h.getLevel().setBlock(pos, CompanionMod.LOGISTICS.block.get().defaultBlockState(), 3); + h.getLevel().setBlock(pos, + CompanionMod.LOGISTICS.block.get().defaultBlockState(), 3); return (ProcessorEntity) h.getLevel().getBlockEntity(pos); } @@ -41,196 +42,153 @@ private static void require(boolean b, String s) { } @GameTest(template = "empty") - public static void menuRecoveryReturnsOnlyTheCurrentPlayersDetachedFilter(GameTestHelper h) { + public static void menuRecoveryReturnsOnlyTheCurrentPlayersDetachedFilter( + GameTestHelper h) { var machine = place(h); var a = machine.automation; - a.slots().setStackInSlot(0, new ItemStack(CompanionAddons.PULLER.get())); - a.slots().setStackInSlot(4, new ItemStack(CompanionAddons.ITEM_FILTER.get())); + a.slots().setStackInSlot(0, + new ItemStack(CompanionAddons.PULLER.get())); + a.slots().setStackInSlot(4, + new ItemStack(CompanionAddons.ITEM_FILTER.get())); a.slots().extractItem(0, 1, false); - a.load(a.save(h.getLevel().registryAccess()), h.getLevel().registryAccess()); - a.slots().setStackInSlot(0, new ItemStack(CompanionAddons.PULLER.get())); - a.slots().setStackInSlot(4, new ItemStack(CompanionAddons.ITEM_FILTER.get())); + a.load(a.save(h.getLevel().registryAccess()), + h.getLevel().registryAccess()); + a.slots().setStackInSlot(0, + new ItemStack(CompanionAddons.PULLER.get())); + a.slots().setStackInSlot(4, + new ItemStack(CompanionAddons.ITEM_FILTER.get())); var player = h.makeMockServerPlayerInLevel(); - player.setPos( - machine.getBlockPos().getX(), machine.getBlockPos().getY(), - machine.getBlockPos().getZ()); + player.setPos(machine.getBlockPos().getX(), + machine.getBlockPos().getY(), machine.getBlockPos().getZ()); var menu = new MachineMenu(72, player.getInventory(), machine); player.containerMenu = menu; - menu.components() - .get(BuiltinMenuComponents.AUTOMATION) - .action( - player, - new AutomationNetwork.Action( - 72, menu.widgetSession(), 1, "select", 0, -1, 0, 0, "", 0, "")); + menu.components().get(BuiltinMenuComponents.AUTOMATION).action(player, + new AutomationNetwork.Action(72, menu.widgetSession(), 1, "select", + 0, -1, 0, 0, "", 0, "")); menu.clicked( menu.components().get(BuiltinMenuComponents.AUTOMATION).slotStart(), - 0, - net.minecraft.world.inventory.ClickType.PICKUP, - player); + 0, net.minecraft.world.inventory.ClickType.PICKUP, player); require( - player.getInventory().countItem(CompanionAddons.ITEM_FILTER.get()) == 1, + player.getInventory() + .countItem(CompanionAddons.ITEM_FILTER.get()) == 1, "Only the filter detached by this player's click goes to that player"); - var drops = h.getLevel() - .getEntitiesOfClass( - net.minecraft.world.entity.item.ItemEntity.class, - new net.minecraft.world.phys.AABB(machine.getBlockPos()).inflate(2), - e -> e.getItem().is(CompanionAddons.ITEM_FILTER.get())); - require( - drops.size() == 1 && drops.getFirst().getItem().getCount() == 1, + var drops = h.getLevel().getEntitiesOfClass( + net.minecraft.world.entity.item.ItemEntity.class, + new net.minecraft.world.phys.AABB(machine.getBlockPos()).inflate(2), + e -> e.getItem().is(CompanionAddons.ITEM_FILTER.get())); + require(drops.size() == 1 && drops.getFirst().getItem().getCount() == 1, "Previously queued recovery drops at the block"); var other = h.makeMockServerPlayerInLevel(); var otherMenu = new MachineMenu(73, other.getInventory(), machine); - otherMenu.clicked(-999, 0, net.minecraft.world.inventory.ClickType.PICKUP, other); + otherMenu.clicked(-999, 0, + net.minecraft.world.inventory.ClickType.PICKUP, other); require( - other.getInventory().countItem(CompanionAddons.ITEM_FILTER.get()) == 0, + other.getInventory() + .countItem(CompanionAddons.ITEM_FILTER.get()) == 0, "A later viewer receives no leftovers"); player.containerMenu = player.inventoryMenu; h.succeed(); } @GameTest(template = "empty") - public static void fullInventoryDropsConfiguredFilterOnce(GameTestHelper h) { + public static void fullInventoryDropsConfiguredFilterOnce( + GameTestHelper h) { var machine = place(h); var a = machine.automation; var player = h.makeMockServerPlayerInLevel(); for (int i = 0; i < player.getInventory().items.size(); i++) player.getInventory().items.set(i, new ItemStack(Items.STONE, 64)); - a.slots().setStackInSlot(0, new ItemStack(CompanionAddons.PULLER.get())); - a.slots().setStackInSlot(4, new ItemStack(CompanionAddons.ITEM_FILTER.get())); + a.slots().setStackInSlot(0, + new ItemStack(CompanionAddons.PULLER.get())); + a.slots().setStackInSlot(4, + new ItemStack(CompanionAddons.ITEM_FILTER.get())); long generation = a.generation(0, 0), revision = a.revision(0, 0); - a.edit( - 0, - 0, - generation, - revision, - "samples", - 0, - "sample", - new ItemStack(Items.DIAMOND), - h.getLevel().registryAccess()); - // A same-type replacement still invalidates the editor and detaches its old child. - a.slots().setStackInSlot(0, new ItemStack(CompanionAddons.PULLER.get())); + a.edit(0, 0, generation, revision, "samples", 0, "sample", + new ItemStack(Items.DIAMOND), h.getLevel().registryAccess()); + // A same-type replacement still invalidates the editor and detaches its + // old child. + a.slots().setStackInSlot(0, + new ItemStack(CompanionAddons.PULLER.get())); require( - !a.edit( - 0, - 0, - generation, - revision, - "deny", - 0, - "", - ItemStack.EMPTY, + !a.edit(0, 0, generation, revision, "deny", 0, "", ItemStack.EMPTY, h.getLevel().registryAccess()), "Old attachment cannot edit replacement"); a.recover(player); a.recover(player); - var drops = h.getLevel() - .getEntitiesOfClass( - net.minecraft.world.entity.item.ItemEntity.class, - new net.minecraft.world.phys.AABB(machine.getBlockPos()).inflate(1), - e -> e.getItem().is(CompanionAddons.ITEM_FILTER.get())); - require( - drops.size() == 1 && drops.getFirst().getItem().getCount() == 1, + var drops = h.getLevel().getEntitiesOfClass( + net.minecraft.world.entity.item.ItemEntity.class, + new net.minecraft.world.phys.AABB(machine.getBlockPos()).inflate(1), + e -> e.getItem().is(CompanionAddons.ITEM_FILTER.get())); + require(drops.size() == 1 && drops.getFirst().getItem().getCount() == 1, "Exactly one dropped filter"); var filter = drops.getFirst().getItem(); require( - filter - .getCapability(AddonCapabilities.FILTER) - .matches(filter, new ItemStack(Items.DIAMOND), h.getLevel().registryAccess()), + filter.getCapability(AddonCapabilities.FILTER).matches(filter, + new ItemStack(Items.DIAMOND), h.getLevel().registryAccess()), "Dropped filter keeps configuration"); h.succeed(); } @GameTest(template = "empty") - public static void typedCustomFilterActionValidatesPayload(GameTestHelper h) { + public static void typedCustomFilterActionValidatesPayload( + GameTestHelper h) { var machine = place(h); var a = machine.automation; - a.slots().setStackInSlot(0, new ItemStack(CompanionAddons.PULLER.get())); - a.slots().setStackInSlot(4, new ItemStack(CompanionAddons.TAG_FILTER.get())); + a.slots().setStackInSlot(0, + new ItemStack(CompanionAddons.PULLER.get())); + a.slots().setStackInSlot(4, + new ItemStack(CompanionAddons.TAG_FILTER.get())); long generation = a.generation(0, 0), revision = a.revision(0, 0); require( - !a.edit( - 0, - 0, - generation, - revision, - "deny", - 0, - "42", - ItemStack.EMPTY, - h.getLevel().registryAccess()), + !a.edit(0, 0, generation, revision, "deny", 0, "42", + ItemStack.EMPTY, h.getLevel().registryAccess()), "Typed boolean refuses numeric action"); require( - !a.edit( - 0, - 0, - generation, - revision, - "missing", - 0, - "true", - ItemStack.EMPTY, - h.getLevel().registryAccess()), + !a.edit(0, 0, generation, revision, "missing", 0, "true", + ItemStack.EMPTY, h.getLevel().registryAccess()), "Undeclared control refused"); require( - a.edit( - 0, - 0, - generation, - revision, - "deny", - 0, - "true", - ItemStack.EMPTY, - h.getLevel().registryAccess()), + a.edit(0, 0, generation, revision, "deny", 0, "true", + ItemStack.EMPTY, h.getLevel().registryAccess()), "Declared typed action accepted"); require( - !a.edit( - 0, - 0, - generation, - a.revision(0, 0), - "tag", - 0, - "bad tag!", - ItemStack.EMPTY, - h.getLevel().registryAccess()), + !a.edit(0, 0, generation, a.revision(0, 0), "tag", 0, "bad tag!", + ItemStack.EMPTY, h.getLevel().registryAccess()), "Invalid tag refused atomically"); h.succeed(); } @GameTest(template = "empty") - public static void refusedExecutionRetainsEscrowAcrossSave(GameTestHelper h) { + public static void refusedExecutionRetainsEscrowAcrossSave( + GameTestHelper h) { var from = new net.neoforged.neoforge.items.ItemStackHandler(1); from.setStackInSlot(0, new ItemStack(Items.DIAMOND, 8)); var to = new net.neoforged.neoforge.items.ItemStackHandler(1) { @Override - public ItemStack insertItem(int slot, ItemStack stack, boolean simulate) { + public ItemStack insertItem(int slot, ItemStack stack, + boolean simulate) { return simulate ? ItemStack.EMPTY : stack; } }; var machine = place(h); var escrow = machine.automation.transferState("input", 0); - var transport = AutomationTransfers.transport(ResourceType.ITEMS).orElseThrow(); - require( - transport.move(from, to, 8, value -> true, escrow, h.getLevel().registryAccess()) == 0, - "No actual delivery"); + var transport = AutomationTransfers.transport(ResourceType.ITEMS) + .orElseThrow(); + require(transport.move(from, to, 8, value -> true, escrow, + h.getLevel().registryAccess()) == 0, "No actual delivery"); require(from.getStackInSlot(0).isEmpty(), "Extraction staged"); var saved = machine.automation.save(h.getLevel().registryAccess()); machine.automation.load(saved, h.getLevel().registryAccess()); var accepting = new net.neoforged.neoforge.items.ItemStackHandler(1); escrow = machine.automation.transferState("input", 0); - require( - transport.move(from, accepting, 3, value -> true, escrow, - h.getLevel().registryAccess()) == 3, - "Bounded retry"); + require(transport.move(from, accepting, 3, value -> true, escrow, + h.getLevel().registryAccess()) == 3, "Bounded retry"); require( transport.move(from, accepting, 8, value -> true, escrow, h.getLevel().registryAccess()) == 5, "Remaining escrow delivered"); - require( - accepting.getStackInSlot(0).getCount() == 8 && !escrow.contains("item"), - "No loss or duplication"); + require(accepting.getStackInSlot(0).getCount() == 8 + && !escrow.contains("item"), "No loss or duplication"); h.succeed(); } @@ -238,42 +196,35 @@ public ItemStack insertItem(int slot, ItemStack stack, boolean simulate) { public static void pumpsAndRelaysRespectIndependentTanks(GameTestHelper h) { var machine = place(h); var neighbour = machine.getBlockPos().relative(Direction.NORTH); - h.getLevel().setBlock(neighbour, CompanionMod.LOGISTICS.block.get().defaultBlockState(), 3); + h.getLevel().setBlock(neighbour, + CompanionMod.LOGISTICS.block.get().defaultBlockState(), 3); var source = (ProcessorEntity) h.getLevel().getBlockEntity(neighbour); source.energy.setStored(10_000); machine.energy.setStored(1000); - source.fluids - .tank(2) - .setFluid( - new net.neoforged.neoforge.fluids.FluidStack( - net.minecraft.world.level.material.Fluids.WATER, 500)); + source.fluids.tank(2) + .setFluid(new net.neoforged.neoforge.fluids.FluidStack( + net.minecraft.world.level.material.Fluids.WATER, 500)); var a = machine.automation; - a.slots() - .setStackInSlot( - AutomationInventory.addonSlot(2, 0), - new ItemStack(CompanionAddons.INPUT_PUMP.get())); - a.slots() - .setStackInSlot( - AutomationInventory.addonSlot(6, 0), - new ItemStack(CompanionAddons.IMPORT_RELAY.get())); + a.slots().setStackInSlot(AutomationInventory.addonSlot(2, 0), + new ItemStack(CompanionAddons.INPUT_PUMP.get())); + a.slots().setStackInSlot(AutomationInventory.addonSlot(6, 0), + new ItemStack(CompanionAddons.IMPORT_RELAY.get())); for (var f : RelativeFace.values()) a.group(3).sides().set(f, IoMode.DISABLED); - var port = machine.fluids.view(i -> a.mode(ResourceType.FLUIDS, i, Direction.NORTH)); + var port = machine.fluids + .view(i -> a.mode(ResourceType.FLUIDS, i, Direction.NORTH)); require(port.getTanks() == 4, "All four tank numbers stay visible"); - h.runAfterDelay( - 35, - () -> { - require( - machine.fluids.getFluidInTank(0).getAmount() == 500, - "Input pump uses its own group"); - require(machine.fluids.getFluidInTank(1).isEmpty(), - "Same-role tank is independent"); - require(source.fluids.getFluidInTank(2).isEmpty(), "Output tank drained"); - require( - machine.energy.stored() + source.energy.stored() == 10_500, - "Energy relay conserves energy; pump costs 500 RF"); - h.succeed(); - }); + h.runAfterDelay(35, () -> { + require(machine.fluids.getFluidInTank(0).getAmount() == 500, + "Input pump uses its own group"); + require(machine.fluids.getFluidInTank(1).isEmpty(), + "Same-role tank is independent"); + require(source.fluids.getFluidInTank(2).isEmpty(), + "Output tank drained"); + require(machine.energy.stored() + source.energy.stored() == 10_500, + "Energy relay conserves energy; pump costs 500 RF"); + h.succeed(); + }); } @GameTest(template = "empty") @@ -281,58 +232,53 @@ public static void compatibilityAndFilterLifetime(GameTestHelper h) { var machine = place(h); var a = machine.automation; require(a.groups().size() == 7, "Seven independent inventories"); - require(a.accepts(0, new ItemStack(CompanionAddons.PULLER.get())), "Input accepts puller"); - require(!a.accepts(0, new ItemStack(CompanionAddons.PUSHER.get())), "Input refuses pusher"); + require(a.accepts(0, new ItemStack(CompanionAddons.PULLER.get())), + "Input accepts puller"); + require(!a.accepts(0, new ItemStack(CompanionAddons.PUSHER.get())), + "Input refuses pusher"); require(!a.accepts(0, new ItemStack(CompanionAddons.INPUT_PUMP.get())), "Items refuse pump"); require(!a.accepts(8, new ItemStack(CompanionAddons.PULLER.get())), "Output refuses puller"); - a.slots().setStackInSlot(0, new ItemStack(CompanionAddons.PULLER.get())); - require( - a.accepts(4, new ItemStack(CompanionAddons.ITEM_FILTER.get())), + a.slots().setStackInSlot(0, + new ItemStack(CompanionAddons.PULLER.get())); + require(a.accepts(4, new ItemStack(CompanionAddons.ITEM_FILTER.get())), "Filter capability accepted"); require( !a.accepts(4, new ItemStack(CompanionAddons.FLUID_FILTER.get())), "Wrong filter resource refused"); - a.slots().setStackInSlot(4, new ItemStack(CompanionAddons.ITEM_FILTER.get())); + a.slots().setStackInSlot(4, + new ItemStack(CompanionAddons.ITEM_FILTER.get())); long gen = a.generation(0, 0), rev = a.revision(0, 0); require( - a.edit( - 0, - 0, - gen, - rev, - "samples", - 0, - "sample", - new ItemStack(Items.DIAMOND), - h.getLevel().registryAccess()), + a.edit(0, 0, gen, rev, "samples", 0, "sample", + new ItemStack(Items.DIAMOND), h.getLevel().registryAccess()), "Sample edit"); - require( - !a.edit(0, 0, gen, rev, "deny", 0, "", ItemStack.EMPTY, h.getLevel().registryAccess()), - "Stale revision refused"); + require(!a.edit(0, 0, gen, rev, "deny", 0, "", ItemStack.EMPTY, + h.getLevel().registryAccess()), "Stale revision refused"); var filter = a.filter(0, 0); var provider = filter.getCapability(AddonCapabilities.FILTER); - require( - provider.matches(filter, new ItemStack(Items.DIAMOND), h.getLevel().registryAccess()), - "Configured predicate"); - require( - !provider.matches(filter, new ItemStack(Items.DIRT), h.getLevel().registryAccess()), - "Other item blocked"); + require(provider.matches(filter, new ItemStack(Items.DIAMOND), + h.getLevel().registryAccess()), "Configured predicate"); + require(!provider.matches(filter, new ItemStack(Items.DIRT), + h.getLevel().registryAccess()), "Other item blocked"); var saved = a.save(h.getLevel().registryAccess()); a.load(saved, h.getLevel().registryAccess()); a.load(saved, h.getLevel().registryAccess()); - require(a.filter(0, 0).getCount() == 1, "Reload does not duplicate filter"); + require(a.filter(0, 0).getCount() == 1, + "Reload does not duplicate filter"); var player = h.makeMockServerPlayerInLevel(); a.slots().extractItem(0, 1, false); a.recover(player); require(a.filter(0, 0).isEmpty(), "Parent extraction detaches child"); require( - player.getInventory().countItem(CompanionAddons.ITEM_FILTER.get()) == 1, + player.getInventory() + .countItem(CompanionAddons.ITEM_FILTER.get()) == 1, "Filter returned exactly once"); a.recover(player); require( - player.getInventory().countItem(CompanionAddons.ITEM_FILTER.get()) == 1, + player.getInventory() + .countItem(CompanionAddons.ITEM_FILTER.get()) == 1, "Recovery is idempotent"); h.succeed(); } @@ -342,44 +288,35 @@ public static void menuActionsAreSessionAndGenerationBound(GameTestHelper h) throws ReflectiveOperationException { var machine = place(h); var player = h.makeMockServerPlayerInLevel(); - player.setPos( - machine.getBlockPos().getX(), machine.getBlockPos().getY(), - machine.getBlockPos().getZ()); + player.setPos(machine.getBlockPos().getX(), + machine.getBlockPos().getY(), machine.getBlockPos().getZ()); var menu = new MachineMenu(71, player.getInventory(), machine); player.containerMenu = menu; var a = menu.components().get(BuiltinMenuComponents.AUTOMATION); - a.action( - player, - new AutomationNetwork.Action( - 71, menu.widgetSession() + 1, 1, "select", 0, -1, 0, 0, "", 0, "")); + a.action(player, new AutomationNetwork.Action(71, + menu.widgetSession() + 1, 1, "select", 0, -1, 0, 0, "", 0, "")); require(!a.open(), "Wrong session rejected"); - a.action( - player, - new AutomationNetwork.Action( - 71, menu.widgetSession(), 1, "select", 0, -1, 0, 0, "", 0, "")); + a.action(player, new AutomationNetwork.Action(71, menu.widgetSession(), + 1, "select", 0, -1, 0, 0, "", 0, "")); require( - a.open() && menu.slots.get(2).isActive() && !menu.slots.get(10).isActive(), + a.open() && menu.slots.get(2).isActive() + && !menu.slots.get(10).isActive(), "Only selected inventory active"); - a.action( - player, - new AutomationNetwork.Action( - 71, menu.widgetSession(), 2, "face", 0, -1, 0, 0, "OUTPUT", 0, "")); + a.action(player, new AutomationNetwork.Action(71, menu.widgetSession(), + 2, "face", 0, -1, 0, 0, "OUTPUT", 0, "")); + require(machine.automation.group(0).sides() + .get(RelativeFace.FRONT) == IoMode.INPUT, "Stale face refused"); + a.action(player, new AutomationNetwork.Action(71, menu.widgetSession(), + 3, "face", 0, -1, 0, 0, "INPUT", 0, "")); require( - machine.automation.group(0).sides().get(RelativeFace.FRONT) == IoMode.INPUT, - "Stale face refused"); - a.action( - player, - new AutomationNetwork.Action( - 71, menu.widgetSession(), 3, "face", 0, -1, 0, 0, "INPUT", 0, "")); - require( - machine.automation.group(0).sides().get(RelativeFace.FRONT) == IoMode.DISABLED, + machine.automation.group(0).sides() + .get(RelativeFace.FRONT) == IoMode.DISABLED, "Input cycles straight to disabled"); - a.action( - player, - new AutomationNetwork.Action( - 71, menu.widgetSession(), 3, "select", 1, -1, 0, 0, "", 0, "")); + a.action(player, new AutomationNetwork.Action(71, menu.widgetSession(), + 3, "select", 1, -1, 0, 0, "", 0, "")); require(a.selected() == 0, "Replay refused"); - var revision = AutomationMenu.class.getDeclaredField("lastInventoryRevision"); + var revision = AutomationMenu.class + .getDeclaredField("lastInventoryRevision"); revision.setAccessible(true); var selection = AutomationMenu.class.getDeclaredField("lastSelected"); selection.setAccessible(true); @@ -395,88 +332,68 @@ public static void menuActionsAreSessionAndGenerationBound(GameTestHelper h) } @GameTest(template = "empty", timeoutTicks = 100) - public static void outputAddonsTransferItemsFluidsAndEnergyThroughFilters(GameTestHelper h) { + public static void outputAddonsTransferItemsFluidsAndEnergyThroughFilters( + GameTestHelper h) { var machine = place(h); var pos = machine.getBlockPos().relative(Direction.NORTH); - h.getLevel().setBlock(pos, CompanionMod.LOGISTICS.block.get().defaultBlockState(), 3); + h.getLevel().setBlock(pos, + CompanionMod.LOGISTICS.block.get().defaultBlockState(), 3); var target = (ProcessorEntity) h.getLevel().getBlockEntity(pos); machine.energy.setStored(10_000); - // Leave room for one export batch without draining the source's powered item/fluid addons. + // Leave room for one export batch without draining the source's powered + // item/fluid addons. target.energy.setStored(target.energy.capacity() - 1000); long totalEnergy = machine.energy.stored() + target.energy.stored(); machine.items.setStackInSlot(1, new ItemStack(Items.DIAMOND, 16)); - machine.fluids - .tank(2) - .setFluid( - new net.neoforged.neoforge.fluids.FluidStack( - net.minecraft.world.level.material.Fluids.WATER, 500)); + machine.fluids.tank(2) + .setFluid(new net.neoforged.neoforge.fluids.FluidStack( + net.minecraft.world.level.material.Fluids.WATER, 500)); var a = machine.automation; - a.slots() - .setStackInSlot( - AutomationInventory.addonSlot(1, 0), new ItemStack(CompanionAddons.PUSHER.get())); - a.slots() - .setStackInSlot( - AutomationInventory.filterSlot(1, 0), - new ItemStack(CompanionAddons.LARGE_FILTER.get())); - a.slots() - .setStackInSlot( - AutomationInventory.addonSlot(4, 0), - new ItemStack(CompanionAddons.OUTPUT_PUMP.get())); - a.slots() - .setStackInSlot( - AutomationInventory.filterSlot(4, 0), - new ItemStack(CompanionAddons.FLUID_FILTER.get())); - a.slots() - .setStackInSlot( - AutomationInventory.addonSlot(6, 0), - new ItemStack(CompanionAddons.EXPORT_RELAY.get())); + a.slots().setStackInSlot(AutomationInventory.addonSlot(1, 0), + new ItemStack(CompanionAddons.PUSHER.get())); + a.slots().setStackInSlot(AutomationInventory.filterSlot(1, 0), + new ItemStack(CompanionAddons.LARGE_FILTER.get())); + a.slots().setStackInSlot(AutomationInventory.addonSlot(4, 0), + new ItemStack(CompanionAddons.OUTPUT_PUMP.get())); + a.slots().setStackInSlot(AutomationInventory.filterSlot(4, 0), + new ItemStack(CompanionAddons.FLUID_FILTER.get())); + a.slots().setStackInSlot(AutomationInventory.addonSlot(6, 0), + new ItemStack(CompanionAddons.EXPORT_RELAY.get())); sample(h, a, 1, 35, new ItemStack(Items.DIRT)); sample(h, a, 4, 0, new ItemStack(Items.LAVA_BUCKET)); - h.runAfterDelay( - 25, - () -> { - require( - target.items.getStackInSlot(0).isEmpty() - && target.fluids.getFluidInTank(0).isEmpty(), - "Item and fluid filters block disallowed exports"); - require( - machine.energy.stored() + target.energy.stored() == totalEnergy - && target.energy.stored() == target.energy.capacity(), - "Export relay moves energy; refused item/fluid transfers spend no RF"); - sample(h, a, 1, 35, new ItemStack(Items.DIAMOND)); - sample(h, a, 4, 0, new ItemStack(Items.WATER_BUCKET)); - }); - h.runAfterDelay( - 70, - () -> { - require( - machine.items.getStackInSlot(1).isEmpty() - && target.items.getStackInSlot(0).getCount() == 16, - "Pusher exports every allowed item"); - require( - machine.fluids.getFluidInTank(2).isEmpty() - && target.fluids.getFluidInTank(0).getAmount() == 500, - "Output pump exports the allowed fluid"); - require( - machine.energy.stored() + target.energy.stored() == totalEnergy - 564, - "Only actual exports cost RF: 16 items at 4 RF and 500 mB at 1 RF"); - h.succeed(); - }); + h.runAfterDelay(25, () -> { + require( + target.items.getStackInSlot(0).isEmpty() + && target.fluids.getFluidInTank(0).isEmpty(), + "Item and fluid filters block disallowed exports"); + require( + machine.energy.stored() + target.energy.stored() == totalEnergy + && target.energy.stored() == target.energy.capacity(), + "Export relay moves energy; refused item/fluid transfers spend no RF"); + sample(h, a, 1, 35, new ItemStack(Items.DIAMOND)); + sample(h, a, 4, 0, new ItemStack(Items.WATER_BUCKET)); + }); + h.runAfterDelay(70, () -> { + require( + machine.items.getStackInSlot(1).isEmpty() + && target.items.getStackInSlot(0).getCount() == 16, + "Pusher exports every allowed item"); + require( + machine.fluids.getFluidInTank(2).isEmpty() + && target.fluids.getFluidInTank(0).getAmount() == 500, + "Output pump exports the allowed fluid"); + require( + machine.energy.stored() + target.energy.stored() == totalEnergy + - 564, + "Only actual exports cost RF: 16 items at 4 RF and 500 mB at 1 RF"); + h.succeed(); + }); } - private static void sample( - GameTestHelper h, AutomationInventory a, int group, int entry, ItemStack sample) { - require( - a.edit( - group, - 0, - a.generation(group, 0), - a.revision(group, 0), - "samples", - entry, - "sample", - sample, - h.getLevel().registryAccess()), + private static void sample(GameTestHelper h, AutomationInventory a, + int group, int entry, ItemStack sample) { + require(a.edit(group, 0, a.generation(group, 0), a.revision(group, 0), + "samples", entry, "sample", sample, h.getLevel().registryAccess()), "Filter sample accepted"); } @@ -489,46 +406,35 @@ public static void tagFilterControlsRealPulling(GameTestHelper h) { chest.setItem(0, new ItemStack(Items.DIRT, 8)); chest.setItem(1, new ItemStack(Items.OAK_PLANKS, 8)); var a = machine.automation; - a.slots().setStackInSlot(0, new ItemStack(CompanionAddons.PULLER.get())); - a.slots().setStackInSlot(4, new ItemStack(CompanionAddons.TAG_FILTER.get())); - require( - a.edit( - 0, - 0, - a.generation(0, 0), - a.revision(0, 0), - "tag", - 0, - "minecraft:planks", - ItemStack.EMPTY, - h.getLevel().registryAccess()), + a.slots().setStackInSlot(0, + new ItemStack(CompanionAddons.PULLER.get())); + a.slots().setStackInSlot(4, + new ItemStack(CompanionAddons.TAG_FILTER.get())); + require(a.edit(0, 0, a.generation(0, 0), a.revision(0, 0), "tag", 0, + "minecraft:planks", ItemStack.EMPTY, h.getLevel().registryAccess()), "Tag filter configured"); - h.runAfterDelay( - 22, - () -> { - require(machine.items.getStackInSlot(0).isEmpty(), - "Unpowered puller cannot transfer"); - machine.energy.setStored(1000); - }); - h.runAfterDelay( - 65, - () -> { - require( - machine.items.getStackInSlot(0).is(Items.OAK_PLANKS) - && machine.items.getStackInSlot(0).getCount() == 8, - "Tag filter allows matching planks"); - require( - chest.getItem(0).getCount() == 8 && chest.getItem(1).isEmpty(), - "Tag filter leaves dirt behind"); - require( - machine.energy.stored() == 968, - "Powered tag-filtered transfer pays actual RF cost"); - h.succeed(); - }); + h.runAfterDelay(22, () -> { + require(machine.items.getStackInSlot(0).isEmpty(), + "Unpowered puller cannot transfer"); + machine.energy.setStored(1000); + }); + h.runAfterDelay(65, () -> { + require( + machine.items.getStackInSlot(0).is(Items.OAK_PLANKS) + && machine.items.getStackInSlot(0).getCount() == 8, + "Tag filter allows matching planks"); + require( + chest.getItem(0).getCount() == 8 && chest.getItem(1).isEmpty(), + "Tag filter leaves dirt behind"); + require(machine.energy.stored() == 968, + "Powered tag-filtered transfer pays actual RF cost"); + h.succeed(); + }); } @GameTest(template = "empty", timeoutTicks = 100) - public static void poweredPullerHonoursFilterAndDisabledSides(GameTestHelper h) { + public static void poweredPullerHonoursFilterAndDisabledSides( + GameTestHelper h) { var machine = place(h); var pos = machine.getBlockPos().relative(Direction.NORTH); h.getLevel().setBlock(pos, Blocks.CHEST.defaultBlockState(), 3); @@ -537,40 +443,33 @@ public static void poweredPullerHonoursFilterAndDisabledSides(GameTestHelper h) chest.setItem(1, new ItemStack(Items.DIAMOND, 16)); machine.energy.setStored(1000); var a = machine.automation; - a.slots().setStackInSlot(0, new ItemStack(CompanionAddons.PULLER.get())); - a.slots().setStackInSlot(4, new ItemStack(CompanionAddons.ITEM_FILTER.get())); - a.edit( - 0, - 0, - a.generation(0, 0), - a.revision(0, 0), - "samples", - 0, - "sample", - new ItemStack(Items.DIAMOND), + a.slots().setStackInSlot(0, + new ItemStack(CompanionAddons.PULLER.get())); + a.slots().setStackInSlot(4, + new ItemStack(CompanionAddons.ITEM_FILTER.get())); + a.edit(0, 0, a.generation(0, 0), a.revision(0, 0), "samples", 0, + "sample", new ItemStack(Items.DIAMOND), h.getLevel().registryAccess()); for (var face : RelativeFace.values()) a.group(0).sides().set(face, IoMode.DISABLED); - h.runAfterDelay( - 22, - () -> { - require( - machine.items.getStackInSlot(0).isEmpty() && machine.energy.stored() == 1000, - "Disabled sides idle without spending RF"); - a.group(0).sides().set(RelativeFace.FRONT, IoMode.INPUT); - }); - h.runAfterDelay( - 65, - () -> { - require( - machine.items.getStackInSlot(0).is(Items.DIAMOND) - && machine.items.getStackInSlot(0).getCount() == 16, - "Allowed items pulled"); - require( - chest.getItem(0).getCount() == 16 && chest.getItem(1).isEmpty(), - "Rejected items remain"); - require(machine.energy.stored() == 936, "RF charged only for delivered items"); - h.succeed(); - }); + h.runAfterDelay(22, () -> { + require( + machine.items.getStackInSlot(0).isEmpty() + && machine.energy.stored() == 1000, + "Disabled sides idle without spending RF"); + a.group(0).sides().set(RelativeFace.FRONT, IoMode.INPUT); + }); + h.runAfterDelay(65, () -> { + require( + machine.items.getStackInSlot(0).is(Items.DIAMOND) + && machine.items.getStackInSlot(0).getCount() == 16, + "Allowed items pulled"); + require( + chest.getItem(0).getCount() == 16 && chest.getItem(1).isEmpty(), + "Rejected items remain"); + require(machine.energy.stored() == 936, + "RF charged only for delivered items"); + h.succeed(); + }); } } diff --git a/src/companionTest/java/zone/moddev/mc/companion/test/AutomationRecoveryTests.java b/src/companionTest/java/zone/moddev/mc/companion/test/AutomationRecoveryTests.java index f4ca3139..84083f54 100644 --- a/src/companionTest/java/zone/moddev/mc/companion/test/AutomationRecoveryTests.java +++ b/src/companionTest/java/zone/moddev/mc/companion/test/AutomationRecoveryTests.java @@ -48,7 +48,8 @@ private static void require(boolean ok, String message) { } private static ProcessorEntity place(GameTestHelper h, BlockPos pos) { - h.getLevel().setBlock(pos, CompanionMod.LOGISTICS.block.get().defaultBlockState(), 3); + h.getLevel().setBlock(pos, + CompanionMod.LOGISTICS.block.get().defaultBlockState(), 3); return (ProcessorEntity) h.getLevel().getBlockEntity(pos); } @@ -56,7 +57,8 @@ private static ProcessorEntity place(GameTestHelper h) { return place(h, h.absolutePos(new BlockPos(2, 2, 2))); } - private static CompoundTag pending(AutomationInventory a, int group, boolean input) { + private static CompoundTag pending(AutomationInventory a, int group, + boolean input) { var state = a.transferState(a.group(group).id(), 0); state.putBoolean("input", input); state.putInt("face", RelativeFace.FRONT.ordinal()); @@ -65,28 +67,29 @@ private static CompoundTag pending(AutomationInventory a, int group, boolean inp return state; } - /** Stage real extractions into handlers that accept simulation but refuse execution. */ - private static void stage(GameTestHelper h, AutomationInventory a, boolean input) { + /** + * Stage real extractions into handlers that accept simulation but refuse + * execution. + */ + private static void stage(GameTestHelper h, AutomationInventory a, + boolean input) { var items = new ItemStackHandler(1); items.setStackInSlot(0, new ItemStack(Items.DIAMOND, 8)); var refusedItems = new ItemStackHandler(1) { @Override - public ItemStack insertItem(int slot, ItemStack stack, boolean simulate) { + public ItemStack insertItem(int slot, ItemStack stack, + boolean simulate) { return simulate ? ItemStack.EMPTY : stack; } }; require( - AutomationTransfers.transport(ResourceType.ITEMS) - .orElseThrow() - .move( - items, - refusedItems, - 8, - value -> true, + AutomationTransfers.transport(ResourceType.ITEMS).orElseThrow() + .move(items, refusedItems, 8, value -> true, pending(a, input ? 0 : 1, input), h.getLevel().registryAccess()) == 0, "Item delivery refused after extraction"); - require(items.getStackInSlot(0).isEmpty(), "Pending items are owned only by escrow"); + require(items.getStackInSlot(0).isEmpty(), + "Pending items are owned only by escrow"); var fluids = new FluidTank(1000); fluids.setFluid(new FluidStack(Fluids.WATER, 750)); var refusedFluids = new FluidTank(1000) { @@ -96,13 +99,8 @@ public int fill(FluidStack stack, FluidAction action) { } }; require( - AutomationTransfers.transport(ResourceType.FLUIDS) - .orElseThrow() - .move( - fluids, - refusedFluids, - 750, - value -> true, + AutomationTransfers.transport(ResourceType.FLUIDS).orElseThrow() + .move(fluids, refusedFluids, 750, value -> true, pending(a, input ? 2 : 4, input), h.getLevel().registryAccess()) == 0, "Fluid delivery refused after extraction"); @@ -116,45 +114,39 @@ public int receiveEnergy(int amount, boolean simulate) { } }; require( - AutomationTransfers.transport(ResourceType.ENERGY) - .orElseThrow() - .move( - energy, - refusedEnergy, - 750, - value -> true, - pending(a, 6, input), - h.getLevel().registryAccess()) == 0, + AutomationTransfers.transport(ResourceType.ENERGY).orElseThrow() + .move(energy, refusedEnergy, 750, value -> true, + pending(a, 6, input), h.getLevel().registryAccess()) == 0, "Energy delivery refused after extraction"); require(energy.stored() == 0, "Pending energy is owned only by escrow"); } @GameTest(template = "empty") - public static void cachesInvalidateAndUnchangedInventoriesSkipRevalidation(GameTestHelper h) { + public static void cachesInvalidateAndUnchangedInventoriesSkipRevalidation( + GameTestHelper h) { var machine = place(h); var a = machine.automation; var pos = machine.getBlockPos().north(); h.getLevel().setBlock(pos, Blocks.CHEST.defaultBlockState(), 3); TestAutomationCapabilities.LOOKUPS.remove(pos); for (int i = 0; i < 100; i++) - require( - a.neighbour(TestAutomationCapabilities.ITEMS, Direction.NORTH) != null, - "Cached neighbour exists"); - require( - TestAutomationCapabilities.LOOKUPS.get(pos) == 1, + require(a.neighbour(TestAutomationCapabilities.ITEMS, + Direction.NORTH) != null, "Cached neighbour exists"); + require(TestAutomationCapabilities.LOOKUPS.get(pos) == 1, "Repeated queries resolve provider only once"); h.getLevel().invalidateCapabilities(pos); a.neighbour(TestAutomationCapabilities.ITEMS, Direction.NORTH); - require( - TestAutomationCapabilities.LOOKUPS.get(pos) == 2, + require(TestAutomationCapabilities.LOOKUPS.get(pos) == 2, "Explicit invalidation refreshes provider"); h.getLevel().destroyBlock(pos, false); require( - a.neighbour(TestAutomationCapabilities.ITEMS, Direction.NORTH) == null, + a.neighbour(TestAutomationCapabilities.ITEMS, + Direction.NORTH) == null, "Removal invalidates cached neighbour"); h.getLevel().setBlock(pos, Blocks.CHEST.defaultBlockState(), 3); require( - a.neighbour(TestAutomationCapabilities.ITEMS, Direction.NORTH) != null + a.neighbour(TestAutomationCapabilities.ITEMS, + Direction.NORTH) != null && TestAutomationCapabilities.LOOKUPS.get(pos) == 3, "Replacement supplies a fresh capability"); a.slots().setStackInSlot(0, new ItemStack(Items.BLAZE_ROD)); @@ -175,158 +167,160 @@ public static void cachesInvalidateAndUnchangedInventoriesSkipRevalidation(GameT a.group(0).sides().set(RelativeFace.FRONT, IoMode.DISABLED); require( a.viewRevision() > revision - && a.mode(ResourceType.ITEMS, "input", 0, Direction.NORTH) == IoMode.DISABLED - && a.mode(ResourceType.ITEMS, "output", 0, Direction.NORTH) == IoMode.OUTPUT, + && a.mode(ResourceType.ITEMS, "input", 0, + Direction.NORTH) == IoMode.DISABLED + && a.mode(ResourceType.ITEMS, "output", 0, + Direction.NORTH) == IoMode.OUTPUT, "Indexed membership preserves independent modes and notifies menu revision"); h.succeed(); } @GameTest(template = "empty") - public static void unknownRecoveryPayloadAndFailedHandoffAreLossless(GameTestHelper h) { + public static void unknownRecoveryPayloadAndFailedHandoffAreLossless( + GameTestHelper h) { var machine = place(h); var a = machine.automation; var state = a.transferState("removed_group", 0); state.putString("resource", "absent_mod:gas"); state.putInt("amount", 1234); - state.put("item", new ItemStack(Items.DIAMOND).save(h.getLevel().registryAccess())); + state.put("item", + new ItemStack(Items.DIAMOND).save(h.getLevel().registryAccess())); var before = a.save(h.getLevel().registryAccess()); try { - a.dropAll( - payload -> { - throw new IllegalStateException("Recovery sink refused"); - }); + a.dropAll(payload -> { + throw new IllegalStateException("Recovery sink refused"); + }); throw new AssertionError("Expected refused handoff"); } catch (IllegalStateException expected) { - require( - a.save(h.getLevel().registryAccess()).equals(before), + require(a.save(h.getLevel().registryAccess()).equals(before), "Failed handoff leaves pending data intact"); } var captured = new java.util.concurrent.atomic.AtomicReference(); a.dropAll(captured::set); - var entry = captured.get().getList("resources", Tag.TAG_COMPOUND).getCompound(0); + var entry = captured.get().getList("resources", Tag.TAG_COMPOUND) + .getCompound(0); require( entry.getString("type").equals("absent_mod:gas") && entry.getCompound("data").equals(state), "Unknown transport schema is preserved verbatim"); require( h.getLevel() - .getEntitiesOfClass(ItemEntity.class, new AABB(machine.getBlockPos()).inflate(2)) + .getEntitiesOfClass(ItemEntity.class, + new AABB(machine.getBlockPos()).inflate(2)) .isEmpty(), "A custom payload's item field must not also be interpreted as a vanilla item drop"); var capsule = RecoveryCapsuleItem.create(captured.get()); require( - !RecoveryCapsuleItem.restore(capsule, machine) && !capsule.isEmpty(), + !RecoveryCapsuleItem.restore(capsule, machine) + && !capsule.isEmpty(), "Unsupported resources stay in the recovery capsule"); - a.dropAll( - payload -> { - throw new AssertionError("Recovery payload handed off twice"); - }); + a.dropAll(payload -> { + throw new AssertionError("Recovery payload handed off twice"); + }); h.succeed(); } @GameTest(template = "empty") - public static void inboundEscrowCompletesAfterSourceRemovalAndReload(GameTestHelper h) { + public static void inboundEscrowCompletesAfterSourceRemovalAndReload( + GameTestHelper h) { var machine = place(h); var source = machine.getBlockPos().north(); h.getLevel().setBlock(source, Blocks.CHEST.defaultBlockState(), 3); stage(h, machine.automation, true); h.getLevel().destroyBlock(source, false); var a = machine.automation; - a.load(a.save(h.getLevel().registryAccess()), h.getLevel().registryAccess()); + a.load(a.save(h.getLevel().registryAccess()), + h.getLevel().registryAccess()); for (var group : a.groups()) for (var face : RelativeFace.values()) group.sides().set(face, IoMode.DISABLED); AutomationTransfers.tick(a, machine.energy); AutomationTransfers.tick(a, machine.energy); - require( - machine.items.getStackInSlot(0).getCount() == 8, + require(machine.items.getStackInSlot(0).getCount() == 8, "Owned inbound items need no source or enabled external side"); - require( - machine.fluids.getFluidInTank(0).getAmount() == 750, + require(machine.fluids.getFluidInTank(0).getAmount() == 750, "Inbound fluid completes without original neighbour"); - require( - machine.energy.stored() == 750, + require(machine.energy.stored() == 750, "Inbound energy completes once without original neighbour"); h.succeed(); } @GameTest(template = "empty") - public static void outboundEscrowReroutesToAnotherEnabledNeighbour(GameTestHelper h) { + public static void outboundEscrowReroutesToAnotherEnabledNeighbour( + GameTestHelper h) { var machine = place(h); var a = machine.automation; var original = machine.getBlockPos().north(); place(h, original); stage(h, a, false); // Populate caches before breaking the destination. - require( - a.neighbour(Capabilities.ItemHandler.BLOCK, Direction.NORTH) != null, - "Original neighbour cached"); + require(a.neighbour(Capabilities.ItemHandler.BLOCK, + Direction.NORTH) != null, "Original neighbour cached"); h.getLevel().destroyBlock(original, false); AutomationTransfers.tick(a, machine.energy); require(a.transferState("tank_2", 0).contains("fluid"), "No destination retains pending fluid"); - a.load(a.save(h.getLevel().registryAccess()), h.getLevel().registryAccess()); + a.load(a.save(h.getLevel().registryAccess()), + h.getLevel().registryAccess()); var destination = place(h, machine.getBlockPos().east()); for (int g : new int[]{1, 4, 6}) { for (var face : RelativeFace.values()) a.group(g).sides().set(face, IoMode.DISABLED); - a.group(g).sides().set(RelativeFace.of(Direction.NORTH, Direction.EAST), IoMode.OUTPUT); + a.group(g).sides().set( + RelativeFace.of(Direction.NORTH, Direction.EAST), + IoMode.OUTPUT); } AutomationTransfers.tick(a, machine.energy); AutomationTransfers.tick(a, machine.energy); - require( - destination.items.getStackInSlot(0).getCount() == 8, + require(destination.items.getStackInSlot(0).getCount() == 8, "Pending items reroute exactly once"); - require( - destination.fluids.getFluidInTank(0).getAmount() == 750, + require(destination.fluids.getFluidInTank(0).getAmount() == 750, "Pending fluid reroutes exactly once"); - require(destination.energy.stored() == 750, "Pending energy reroutes exactly once"); - require(machine.items.getStackInSlot(1).isEmpty(), "Rerouting never extracts a new batch"); + require(destination.energy.stored() == 750, + "Pending energy reroutes exactly once"); + require(machine.items.getStackInSlot(1).isEmpty(), + "Rerouting never extracts a new batch"); h.succeed(); } @GameTest(template = "empty") - public static void clippedRefundSurvivesSaveAndReturnsExactlyOnce(GameTestHelper h) { + public static void clippedRefundSurvivesSaveAndReturnsExactlyOnce( + GameTestHelper h) { var machine = place(h); var power = new EnergyBuffer(1000, 1000); power.setStored(1000); var destination = new ItemStackHandler(1) { @Override - public ItemStack insertItem(int slot, ItemStack stack, boolean simulate) { + public ItemStack insertItem(int slot, ItemStack stack, + boolean simulate) { if (!simulate) - power.setStored(1000); // Reentrant handler fills the reserved refund space. + power.setStored(1000); // Reentrant handler fills the + // reserved refund space. return stack; } }; - var group = new ResourceGroup<>( - "input", - Component.literal("Input"), - 0, - ResourceType.ITEMS, - IoMode.INPUT, - List.of(0), - List.of(), + var group = new ResourceGroup<>("input", Component.literal("Input"), 0, + ResourceType.ITEMS, IoMode.INPUT, List.of(0), List.of(), new SideConfig(IoMode.INPUT)); group.bind(() -> Direction.NORTH, side -> destination); var a = new AutomationInventory(machine, List.of(group)); var state = pending(a, 0, true); - state.put("item", new ItemStack(Items.DIAMOND, 8).save(h.getLevel().registryAccess())); + state.put("item", new ItemStack(Items.DIAMOND, 8) + .save(h.getLevel().registryAccess())); state.putInt("batch", 8); state.putLong("cost", 4); AutomationTransfers.tick(a, power); - require( - power.stored() == 1000 && a.pendingRefund() == 32, + require(power.stored() == 1000 && a.pendingRefund() == 32, "Rejected refund is retained, not clipped"); - a.load(a.save(h.getLevel().registryAccess()), h.getLevel().registryAccess()); + a.load(a.save(h.getLevel().registryAccess()), + h.getLevel().registryAccess()); require(a.pendingRefund() == 32, "Refund persists through save/reload"); power.extract(100, false); a.settleRefund(power); a.settleRefund(power); - require( - power.stored() == 932 && a.pendingRefund() == 0, + require(power.stored() == 932 && a.pendingRefund() == 0, "Refund returns once when room becomes available"); - require( - power.inserted().signum() == 0 && power.used().signum() == 0, + require(power.inserted().signum() == 0 && power.used().signum() == 0, "Reserved and refunded transfer charges never count as input or processor use"); h.succeed(); } @@ -339,24 +333,24 @@ public static void breakingPreservesFluidsEnergyRefundsAndPartialCapsuleRecovery machine.energy.setStored(machine.energy.capacity()); machine.automation.refund(machine.energy, 50); machine.automation.load( - machine.automation.save(h.getLevel().registryAccess()), h.getLevel().registryAccess()); + machine.automation.save(h.getLevel().registryAccess()), + h.getLevel().registryAccess()); h.getLevel().destroyBlock(machine.getBlockPos(), true); machine.dropContents(h.getLevel(), machine.getBlockPos()); - var drops = h.getLevel() - .getEntitiesOfClass(ItemEntity.class, new AABB(machine.getBlockPos()).inflate(2)); + var drops = h.getLevel().getEntitiesOfClass(ItemEntity.class, + new AABB(machine.getBlockPos()).inflate(2)); var capsules = drops.stream() - .filter(e -> e.getItem().is(CompanionAddons.RECOVERY_CAPSULE.get())).toList(); + .filter(e -> e.getItem().is(CompanionAddons.RECOVERY_CAPSULE.get())) + .toList(); require( - capsules.size() == 1 && capsules.getFirst().getItem().getCount() == 1, + capsules.size() == 1 + && capsules.getFirst().getItem().getCount() == 1, "Exactly one recovery capsule"); require( - drops.stream() - .filter(e -> e.getItem().is(Items.DIAMOND)) - .mapToInt(e -> e.getItem().getCount()) - .sum() == 8, + drops.stream().filter(e -> e.getItem().is(Items.DIAMOND)) + .mapToInt(e -> e.getItem().getCount()).sum() == 8, "Pending items still drop normally exactly once"); - var capsule = ItemStack.parseOptional( - h.getLevel().registryAccess(), + var capsule = ItemStack.parseOptional(h.getLevel().registryAccess(), (CompoundTag) capsules.getFirst().getItem() .saveOptional(h.getLevel().registryAccess())); var target = place(h); @@ -379,13 +373,11 @@ public static void breakingPreservesFluidsEnergyRefundsAndPartialCapsuleRecovery int fluid = 0, energy = 0; for (var entry : remaining.getList("resources", Tag.TAG_COMPOUND)) { var data = ((CompoundTag) entry).getCompound("data"); - fluid += FluidStack - .parseOptional(h.getLevel().registryAccess(), data.getCompound("fluid")) - .getAmount(); + fluid += FluidStack.parseOptional(h.getLevel().registryAccess(), + data.getCompound("fluid")).getAmount(); energy += data.getInt("energy"); } - require( - fluid == 500 && energy == 625, + require(fluid == 500 && energy == 625, "Partial capsule contains exact fluid and energy remainders"); target.fluids.tank(0).setFluid(FluidStack.EMPTY); target.fluids.tank(1).setFluid(FluidStack.EMPTY); @@ -394,48 +386,51 @@ public static void breakingPreservesFluidsEnergyRefundsAndPartialCapsuleRecovery RecoveryCapsuleItem.restore(capsule, target) && capsule.isEmpty(), "Fully restored capsule is consumed"); require( - target.fluids.getFluidInTank(0).getAmount() == 500 && target.energy.stored() == 675, + target.fluids.getFluidInTank(0).getAmount() == 500 + && target.energy.stored() == 675, "All remaining fluid, transferred energy and refund are recoverable"); - require( - !RecoveryCapsuleItem.restore(capsule, target), + require(!RecoveryCapsuleItem.restore(capsule, target), "Consumed capsule cannot duplicate resources"); h.succeed(); } @GameTest(template = "empty") - public static void unsidedEnergyIgnoresFaceModesAndSnapshotsExcludeAddonSlots(GameTestHelper h) - throws ReflectiveOperationException { + public static void unsidedEnergyIgnoresFaceModesAndSnapshotsExcludeAddonSlots( + GameTestHelper h) throws ReflectiveOperationException { var pos = h.absolutePos(new BlockPos(2, 2, 2)); - h.getLevel().setBlock(pos, CompanionMod.CRUSHER.block.get().defaultBlockState(), 3); + h.getLevel().setBlock(pos, + CompanionMod.CRUSHER.block.get().defaultBlockState(), 3); var machine = (ProcessorEntity) h.getLevel().getBlockEntity(pos); machine.energy.setRates(1000, 1000); machine.energy.setStored(1000); - var unsided = h.getLevel().getCapability(Capabilities.EnergyStorage.BLOCK, pos, null); - var sided = h.getLevel().getCapability(Capabilities.EnergyStorage.BLOCK, pos, - Direction.NORTH); + var unsided = h.getLevel() + .getCapability(Capabilities.EnergyStorage.BLOCK, pos, null); + var sided = h.getLevel().getCapability(Capabilities.EnergyStorage.BLOCK, + pos, Direction.NORTH); require( - unsided.extractEnergy(100, false) == 100 && sided.extractEnergy(100, false) == 0, + unsided.extractEnergy(100, false) == 100 + && sided.extractEnergy(100, false) == 0, "Unsided energy preserves buffer operations while sided access honours input role"); - // Exercise a host with more underlying storage than its three displayed slots. + // Exercise a host with more underlying storage than its three displayed + // slots. var field = ProcessorEntity.class.getDeclaredField("items"); field.setAccessible(true); var original = machine.items; try { - field.set( - machine, - zone.moddev.mc.lib.api.capabilities.ItemInventories.partition( - java.util.List.of("input", "output", "output", "hidden", "hidden"))); + field.set(machine, + zone.moddev.mc.lib.api.capabilities.ItemInventories + .partition(java.util.List.of("input", "output", "output", + "hidden", "hidden"))); var player = h.makeMockServerPlayerInLevel(); var menu = new MachineMenu(91, player.getInventory(), machine); menu.components().snapshot(); - require( - menu.components().get(BuiltinMenuComponents.AUTOMATION).slotStart() == 3 - && menu.components() - .state( - menu.components().require(BuiltinMenuComponents.RESERVATIONS), - zone.moddev.mc.lib.gui.runtime.slot.ReservationMenuComponent.PLANS) - .keySet() - .equals(java.util.Set.of(0, 1, 2)), + require(menu.components().get(BuiltinMenuComponents.AUTOMATION) + .slotStart() == 3 + && menu.components().state( + menu.components() + .require(BuiltinMenuComponents.RESERVATIONS), + zone.moddev.mc.lib.gui.runtime.slot.ReservationMenuComponent.PLANS) + .keySet().equals(java.util.Set.of(0, 1, 2)), "Reservation snapshot includes only displayed base slots, not hidden underlying slots or" + " addon indices"); } finally { diff --git a/src/companionTest/java/zone/moddev/mc/companion/test/AutomationRoleTests.java b/src/companionTest/java/zone/moddev/mc/companion/test/AutomationRoleTests.java index 8d5e6f66..8756e371 100644 --- a/src/companionTest/java/zone/moddev/mc/companion/test/AutomationRoleTests.java +++ b/src/companionTest/java/zone/moddev/mc/companion/test/AutomationRoleTests.java @@ -32,7 +32,8 @@ public final class AutomationRoleTests { private static ProcessorEntity place(GameTestHelper h) { var pos = h.absolutePos(new BlockPos(2, 2, 2)); - h.getLevel().setBlock(pos, CompanionMod.LOGISTICS.block.get().defaultBlockState(), 3); + h.getLevel().setBlock(pos, + CompanionMod.LOGISTICS.block.get().defaultBlockState(), 3); return (ProcessorEntity) h.getLevel().getBlockEntity(pos); } @@ -42,27 +43,33 @@ private static void require(boolean condition, String message) { } @GameTest(template = "empty") - public static void duplicateRolesRejectedByStorageAndMenu(GameTestHelper h) { + public static void duplicateRolesRejectedByStorageAndMenu( + GameTestHelper h) { var machine = place(h); var a = machine.automation; var puller = new ItemStack(CompanionAddons.PULLER.get()); - require(a.slots().insertItem(0, puller.copy(), false).isEmpty(), "First puller installs"); - a.slots().setStackInSlot(4, new ItemStack(CompanionAddons.ITEM_FILTER.get())); + require(a.slots().insertItem(0, puller.copy(), false).isEmpty(), + "First puller installs"); + a.slots().setStackInSlot(4, + new ItemStack(CompanionAddons.ITEM_FILTER.get())); for (boolean simulate : new boolean[]{true, false}) require( - a.slots().insertItem(1, puller.copy(), simulate).getCount() == 1, + a.slots().insertItem(1, puller.copy(), simulate) + .getCount() == 1, "Duplicate puller is refused, including simulation"); try { a.slots().setStackInSlot(1, puller.copy()); - throw new AssertionError("Direct assignment accepted a duplicate role"); + throw new AssertionError( + "Direct assignment accepted a duplicate role"); } catch (IllegalArgumentException expected) { - require( - a.addon(0, 1).isEmpty() && !a.filter(0, 0).isEmpty(), + require(a.addon(0, 1).isEmpty() && !a.filter(0, 0).isEmpty(), "Refusal leaves installed addon and filter intact"); } - require(a.accepts(0, puller), "Replacing the existing role in its own slot is allowed"); + require(a.accepts(0, puller), + "Replacing the existing role in its own slot is allowed"); - // The menu's client snapshot must enforce the same rule using its own synced contents. + // The menu's client snapshot must enforce the same rule using its own + // synced contents. var client = new ItemStackHandler(a.slots().getSlots()); client.setStackInSlot(0, puller.copy()); require( @@ -71,32 +78,33 @@ public static void duplicateRolesRejectedByStorageAndMenu(GameTestHelper h) { for (int group : new int[]{2, 3}) { int slot = AutomationInventory.addonSlot(group, 0); var pump = new ItemStack(CompanionAddons.INPUT_PUMP.get()); - require( - a.slots().insertItem(slot, pump.copy(), false).isEmpty(), + require(a.slots().insertItem(slot, pump.copy(), false).isEmpty(), "Independent inventories may each have an input pump"); - require(!a.accepts(slot + 1, pump), "Only one input pump per inventory"); + require(!a.accepts(slot + 1, pump), + "Only one input pump per inventory"); } int energy = AutomationInventory.addonSlot(6, 0); - a.slots().setStackInSlot(energy, new ItemStack(CompanionAddons.IMPORT_RELAY.get())); - a.slots().setStackInSlot(energy + 1, new ItemStack(CompanionAddons.EXPORT_RELAY.get())); + a.slots().setStackInSlot(energy, + new ItemStack(CompanionAddons.IMPORT_RELAY.get())); + a.slots().setStackInSlot(energy + 1, + new ItemStack(CompanionAddons.EXPORT_RELAY.get())); require( - !a.accepts(energy + 2, new ItemStack(CompanionAddons.IMPORT_RELAY.get())) - && !a.accepts(energy + 2, new ItemStack(CompanionAddons.EXPORT_RELAY.get())), + !a.accepts(energy + 2, + new ItemStack(CompanionAddons.IMPORT_RELAY.get())) + && !a.accepts(energy + 2, + new ItemStack(CompanionAddons.EXPORT_RELAY.get())), "Input and output coexist, but neither role may be duplicated"); var player = h.makeMockServerPlayerInLevel(); - player.setPos( - machine.getBlockPos().getX(), machine.getBlockPos().getY(), - machine.getBlockPos().getZ()); + player.setPos(machine.getBlockPos().getX(), + machine.getBlockPos().getY(), machine.getBlockPos().getZ()); var menu = new MachineMenu(71, player.getInventory(), machine); player.containerMenu = menu; - menu.components() - .get(BuiltinMenuComponents.AUTOMATION) - .action( - player, - new AutomationNetwork.Action( - 71, menu.widgetSession(), 1, "select", 0, -1, 0, 0, "", 0, "")); - int destination = menu.components().get(BuiltinMenuComponents.AUTOMATION).slotStart() + 1; + menu.components().get(BuiltinMenuComponents.AUTOMATION).action(player, + new AutomationNetwork.Action(71, menu.widgetSession(), 1, "select", + 0, -1, 0, 0, "", 0, "")); + int destination = menu.components() + .get(BuiltinMenuComponents.AUTOMATION).slotStart() + 1; menu.setCarried(puller.copy()); menu.clicked(destination, 0, ClickType.PICKUP, player); require( @@ -106,132 +114,129 @@ public static void duplicateRolesRejectedByStorageAndMenu(GameTestHelper h) { player.getInventory().setItem(0, puller.copy()); menu.clicked(destination, 0, ClickType.SWAP, player); require( - player.getInventory().getItem(0).is(puller.getItem()) && a.addon(0, 1).isEmpty(), + player.getInventory().getItem(0).is(puller.getItem()) + && a.addon(0, 1).isEmpty(), "Hotbar swap refuses duplicate role"); - // Keep the normal resource inventory full so shift-click only tests addon destinations. + // Keep the normal resource inventory full so shift-click only tests + // addon destinations. machine.items.setStackInSlot(0, new ItemStack(Items.STONE, 64)); var source = menu.slots.stream() - .filter(s -> s.container == player.getInventory() && s.getContainerSlot() == 0) - .findFirst() - .orElseThrow(); + .filter(s -> s.container == player.getInventory() + && s.getContainerSlot() == 0) + .findFirst().orElseThrow(); menu.clicked(source.index, 0, ClickType.QUICK_MOVE, player); for (int c = 1; c < 4; c++) - require(a.addon(0, c).isEmpty(), "Shift-click cannot add a second puller"); - require( - player.getInventory().countItem(puller.getItem()) == 1, + require(a.addon(0, c).isEmpty(), + "Shift-click cannot add a second puller"); + require(player.getInventory().countItem(puller.getItem()) == 1, "Refused addon stays with player"); player.containerMenu = player.inventoryMenu; h.succeed(); } @GameTest(template = "empty") - public static void legacyDuplicateRolesRecoverWithoutLosingFilters(GameTestHelper h) { + public static void legacyDuplicateRolesRecoverWithoutLosingFilters( + GameTestHelper h) { var machine = place(h); var a = machine.automation; - a.slots().setStackInSlot(0, new ItemStack(CompanionAddons.PULLER.get())); - a.slots().setStackInSlot(4, new ItemStack(CompanionAddons.ITEM_FILTER.get())); + a.slots().setStackInSlot(0, + new ItemStack(CompanionAddons.PULLER.get())); + a.slots().setStackInSlot(4, + new ItemStack(CompanionAddons.ITEM_FILTER.get())); var saved = a.save(h.getLevel().registryAccess()); - var contents = saved.getCompound("groups").getCompound("input").getList("contents", - Tag.TAG_COMPOUND); + var contents = saved.getCompound("groups").getCompound("input") + .getList("contents", Tag.TAG_COMPOUND); contents.set(1, contents.getCompound(0).copy()); contents.set(5, contents.getCompound(4).copy()); a.load(saved, h.getLevel().registryAccess()); require( - !a.addon(0, 0).isEmpty() && a.addon(0, 1).isEmpty() && a.filter(0, 1).isEmpty(), + !a.addon(0, 0).isEmpty() && a.addon(0, 1).isEmpty() + && a.filter(0, 1).isEmpty(), "Reload keeps the first addon and detaches the duplicate pair"); - a.load(a.save(h.getLevel().registryAccess()), h.getLevel().registryAccess()); + a.load(a.save(h.getLevel().registryAccess()), + h.getLevel().registryAccess()); var player = h.makeMockServerPlayerInLevel(); a.recover(player); a.recover(player); require( player.getInventory().countItem(CompanionAddons.PULLER.get()) == 1 - && player.getInventory().countItem(CompanionAddons.ITEM_FILTER.get()) == 1, + && player.getInventory() + .countItem(CompanionAddons.ITEM_FILTER.get()) == 1, "Duplicate addon and filter survive save/reload and recover exactly once"); h.succeed(); } @GameTest(template = "empty") - public static void breakingMachineDropsEveryAddonAndConfiguredFilterOnce(GameTestHelper h) { + public static void breakingMachineDropsEveryAddonAndConfiguredFilterOnce( + GameTestHelper h) { var machine = place(h); var a = machine.automation; var expected = new HashMap(); - Item[] modules = { - CompanionAddons.PULLER.get(), - CompanionAddons.PUSHER.get(), - CompanionAddons.INPUT_PUMP.get(), - CompanionAddons.INPUT_PUMP.get(), - CompanionAddons.OUTPUT_PUMP.get(), + Item[] modules = {CompanionAddons.PULLER.get(), + CompanionAddons.PUSHER.get(), CompanionAddons.INPUT_PUMP.get(), + CompanionAddons.INPUT_PUMP.get(), CompanionAddons.OUTPUT_PUMP.get(), CompanionAddons.OUTPUT_PUMP.get(), - CompanionAddons.IMPORT_RELAY.get() - }; + CompanionAddons.IMPORT_RELAY.get()}; for (int g = 0; g < modules.length; g++) { a.slots().setStackInSlot(AutomationInventory.addonSlot(g, 0), new ItemStack(modules[g])); expected.merge(modules[g], 1, Integer::sum); } - a.slots() - .setStackInSlot( - AutomationInventory.addonSlot(6, 1), - new ItemStack(CompanionAddons.EXPORT_RELAY.get())); + a.slots().setStackInSlot(AutomationInventory.addonSlot(6, 1), + new ItemStack(CompanionAddons.EXPORT_RELAY.get())); expected.put(CompanionAddons.EXPORT_RELAY.get(), 1); - // Include a detached filter still in recovery, not just currently visible child slots. - a.slots().setStackInSlot(4, new ItemStack(CompanionAddons.TAG_FILTER.get())); - a.slots().setStackInSlot(0, new ItemStack(CompanionAddons.PULLER.get())); + // Include a detached filter still in recovery, not just currently + // visible child slots. + a.slots().setStackInSlot(4, + new ItemStack(CompanionAddons.TAG_FILTER.get())); + a.slots().setStackInSlot(0, + new ItemStack(CompanionAddons.PULLER.get())); expected.put(CompanionAddons.TAG_FILTER.get(), 1); - Item[] filters = { - CompanionAddons.ITEM_FILTER.get(), + Item[] filters = {CompanionAddons.ITEM_FILTER.get(), CompanionAddons.LARGE_FILTER.get(), - CompanionAddons.FLUID_FILTER.get() - }; + CompanionAddons.FLUID_FILTER.get()}; for (int g = 0; g < filters.length; g++) { a.slots().setStackInSlot(AutomationInventory.filterSlot(g, 0), new ItemStack(filters[g])); expected.put(filters[g], 1); } - require( - a.edit( - 0, - 0, - a.generation(0, 0), - a.revision(0, 0), - "samples", - 0, - "sample", - new ItemStack(Items.DIAMOND), - h.getLevel().registryAccess()), - "Configure installed filter"); + require(a.edit(0, 0, a.generation(0, 0), a.revision(0, 0), "samples", 0, + "sample", new ItemStack(Items.DIAMOND), + h.getLevel().registryAccess()), "Configure installed filter"); machine.items.setStackInSlot(0, new ItemStack(Items.DIAMOND, 3)); - a.transferState("input", 0) - .put("item", new ItemStack(Items.IRON_INGOT, 2).save(h.getLevel().registryAccess())); + a.transferState("input", 0).put("item", + new ItemStack(Items.IRON_INGOT, 2) + .save(h.getLevel().registryAccess())); expected.put(Items.DIAMOND, 3); expected.put(Items.IRON_INGOT, 2); h.getLevel().destroyBlock(machine.getBlockPos(), true); - a.dropAll( - payload -> { - throw new AssertionError("No non-item pending resources in this test"); - }); + a.dropAll(payload -> { + throw new AssertionError( + "No non-item pending resources in this test"); + }); var actual = new HashMap(); - var drops = h.getLevel() - .getEntitiesOfClass(ItemEntity.class, new AABB(machine.getBlockPos()).inflate(2)); + var drops = h.getLevel().getEntitiesOfClass(ItemEntity.class, + new AABB(machine.getBlockPos()).inflate(2)); for (var drop : drops) - actual.merge(drop.getItem().getItem(), drop.getItem().getCount(), Integer::sum); + actual.merge(drop.getItem().getItem(), drop.getItem().getCount(), + Integer::sum); for (var entry : expected.entrySet()) require( Objects.equals(actual.get(entry.getKey()), entry.getValue()), "Exact break drops for " + entry.getKey()); - var filter = drops.stream() - .map(ItemEntity::getItem) - .filter(s -> s.is(CompanionAddons.ITEM_FILTER.get())) - .findFirst() + var filter = drops.stream().map(ItemEntity::getItem) + .filter(s -> s.is(CompanionAddons.ITEM_FILTER.get())).findFirst() .orElseThrow(); var provider = filter.getCapability(AddonCapabilities.FILTER); require( - provider.matches(filter, new ItemStack(Items.DIAMOND), h.getLevel().registryAccess()) + provider.matches(filter, new ItemStack(Items.DIAMOND), + h.getLevel().registryAccess()) && !provider.matches(filter, new ItemStack(Items.DIRT), h.getLevel().registryAccess()), "Broken-out filter retains its configuration"); for (int s = 0; s < a.slots().getSlots(); s++) - require(a.slots().getStackInSlot(s).isEmpty(), "Dropped slots are cleared"); + require(a.slots().getStackInSlot(s).isEmpty(), + "Dropped slots are cleared"); h.succeed(); } } diff --git a/src/companionTest/java/zone/moddev/mc/companion/test/BackpackClientSmoke.java b/src/companionTest/java/zone/moddev/mc/companion/test/BackpackClientSmoke.java index 015b951a..bff1a206 100644 --- a/src/companionTest/java/zone/moddev/mc/companion/test/BackpackClientSmoke.java +++ b/src/companionTest/java/zone/moddev/mc/companion/test/BackpackClientSmoke.java @@ -12,21 +12,18 @@ import zone.moddev.mc.lib.gui.runtime.MachineMenu; import zone.moddev.mc.lib.gui.runtime.component.BuiltinMenuComponents; -/** Real negotiated client/server coverage for the optional Curios and backpack runtime fixtures. */ +/** + * Real negotiated client/server coverage for the optional Curios and backpack + * runtime fixtures. + */ final class BackpackClientSmoke { private static int step; private static volatile boolean verified; static boolean run(MachineScreen screen) { if (!BackpackFixture.installed()) { - require( - screen - .getMenu() - .components() - .get(BuiltinMenuComponents.BACKPACK) - .state() - .icons() - .isEmpty(), + require(screen.getMenu().components() + .get(BuiltinMenuComponents.BACKPACK).state().icons().isEmpty(), "No backpack button without optional mods"); return true; } @@ -50,7 +47,8 @@ static boolean run(MachineScreen screen) { switch (step) { case 0 -> { require( - state.icons().getFirst().getHoverName().getString().equals("Copper supplies"), + state.icons().getFirst().getHoverName().getString() + .equals("Copper supplies"), "Actual named equipped item is the tab icon"); click(screen, layout.tab(), 0); step++; @@ -60,12 +58,12 @@ static boolean run(MachineScreen screen) { return false; require(state.contents().getFirst().getCount() == 24, "Backpack contents reached client"); - require(screen.exclusions().contains(layout.body()), "Drawer is excluded from JEI"); - require( - screen.exclusions().contains(layout.rail()), + require(screen.exclusions().contains(layout.body()), + "Drawer is excluded from JEI"); + require(screen.exclusions().contains(layout.rail()), "Full-height icon rail is excluded from JEI"); - Screenshot.grab( - mc.gameDirectory, "backpack-drawer.png", mc.getMainRenderTarget(), message -> { + Screenshot.grab(mc.gameDirectory, "backpack-drawer.png", + mc.getMainRenderTarget(), message -> { }); click(screen, layout.cell(0), 1); for (int tick = 0; tick < 19; tick++) @@ -73,7 +71,8 @@ static boolean run(MachineScreen screen) { require(drawer.pendingInventory(), "Container transfer stays locked before timeout"); drawer.tickClient(); - require(drawer.pendingInventory(), "Recovery sync preserves the inventory lock"); + require(drawer.pendingInventory(), + "Recovery sync preserves the inventory lock"); step++; } case 2 -> { @@ -83,23 +82,22 @@ static boolean run(MachineScreen screen) { menu.getCarried().is(Items.IRON_INGOT) && state.contents().getFirst().getCount() == 12, "Right click transfers half through the network"); - click( - screen, - new zone.moddev.mc.lib.gui.layout.Rect( - layout.rail().x(), layout.rail().y() + 60, 22, 24), - 0); - require( - menu.getCarried().getCount() == 12 && !drawer.pending(), + click(screen, new zone.moddev.mc.lib.gui.layout.Rect( + layout.rail().x(), layout.rail().y() + 60, 22, 24), 0); + require(menu.getCarried().getCount() == 12 && !drawer.pending(), "Rail clicks retain carried items"); click(screen, layout.cell(0), 0); step++; } case 3 -> { - if (!menu.getCarried().isEmpty() || state.contents().getFirst().getCount() != 24) + if (!menu.getCarried().isEmpty() + || state.contents().getFirst().getCount() != 24) return false; drawer - .request(ContainerDrawerMenu.QUICK_TAKE, 0, state.pageSize()) - .ifPresent(net.neoforged.neoforge.network.PacketDistributor::sendToServer); + .request(ContainerDrawerMenu.QUICK_TAKE, 0, + state.pageSize()) + .ifPresent( + net.neoforged.neoforge.network.PacketDistributor::sendToServer); step++; } case 4 -> { @@ -111,48 +109,46 @@ static boolean run(MachineScreen screen) { mc.player.getInventory().countItem(Items.IRON_INGOT) == 24, "Shift transfer reaches player inventory"); step++; - mc.getSingleplayerServer() - .execute( - () -> { - try { - var player = mc.getSingleplayerServer().getPlayerList().getPlayers() - .getFirst(); - var source = zone.moddev.mc.lib.gui.runtime.EquippedContainers - .find(player).getFirst(); - var saved = source.stack().save(player.registryAccess()); - var restored = ItemStack.parse(player.registryAccess(), saved) - .orElseThrow(); - require( - restored - .getCapability(Capabilities.ItemHandler.ITEM) - .getStackInSlot(0) - .isEmpty(), - "Modified storage survives item serialization"); - require( - restored - .getCapability(Capabilities.ItemHandler.ITEM) - .getStackInSlot(1) - .getCount() == 12, - "Other storage remains intact"); - BackpackFixture.unequip(player); - ((MachineMenu) player.containerMenu) - .components() - .get(BuiltinMenuComponents.BACKPACK) - .broadcast(); - verified = true; - } catch (Throwable error) { - CompanionClientSmoke.fail(error); - } - }); + mc.getSingleplayerServer().execute(() -> { + try { + var player = mc.getSingleplayerServer().getPlayerList() + .getPlayers().getFirst(); + var source = zone.moddev.mc.lib.gui.runtime.EquippedContainers + .find(player).getFirst(); + var saved = source.stack() + .save(player.registryAccess()); + var restored = ItemStack + .parse(player.registryAccess(), saved) + .orElseThrow(); + require( + restored + .getCapability(Capabilities.ItemHandler.ITEM) + .getStackInSlot(0).isEmpty(), + "Modified storage survives item serialization"); + require( + restored + .getCapability(Capabilities.ItemHandler.ITEM) + .getStackInSlot(1).getCount() == 12, + "Other storage remains intact"); + BackpackFixture.unequip(player); + ((MachineMenu) player.containerMenu).components() + .get(BuiltinMenuComponents.BACKPACK).broadcast(); + verified = true; + } catch (Throwable error) { + CompanionClientSmoke.fail(error); + } + }); } - default -> throw new AssertionError("Unexpected backpack test step"); + default -> + throw new AssertionError("Unexpected backpack test step"); } return false; } - private static void click( - MachineScreen screen, zone.moddev.mc.lib.gui.layout.Rect rect, int button) { - require(screen.mouseClicked(rect.x() + 5, rect.y() + 5, button), "Drawer click handled"); + private static void click(MachineScreen screen, + zone.moddev.mc.lib.gui.layout.Rect rect, int button) { + require(screen.mouseClicked(rect.x() + 5, rect.y() + 5, button), + "Drawer click handled"); screen.mouseReleased(rect.x() + 5, rect.y() + 5, button); } diff --git a/src/companionTest/java/zone/moddev/mc/companion/test/BackpackFixture.java b/src/companionTest/java/zone/moddev/mc/companion/test/BackpackFixture.java index 4b8c0f77..84b353b8 100644 --- a/src/companionTest/java/zone/moddev/mc/companion/test/BackpackFixture.java +++ b/src/companionTest/java/zone/moddev/mc/companion/test/BackpackFixture.java @@ -8,61 +8,61 @@ import net.minecraft.world.InteractionHand; import net.minecraft.world.item.ItemStack; import net.minecraft.world.item.Items; + import net.neoforged.fml.ModList; import net.neoforged.neoforge.capabilities.Capabilities; + import top.theillusivec4.curios.api.CuriosApi; -/** Test-only fixture: exercises the real optional runtime mods without linking a backpack API. */ +/** + * Test-only fixture: exercises the real optional runtime mods without linking a + * backpack API. + */ final class BackpackFixture { - static boolean installed() { - return ModList.get().isLoaded("sophisticatedbackpacks"); - } + static boolean installed() { + return ModList.get().isLoaded("sophisticatedbackpacks"); + } - static ItemStack equip(ServerPlayer player) { - var bag = - new ItemStack( - BuiltInRegistries.ITEM.get(ResourceLocation.parse("sophisticatedbackpacks:backpack"))); - require(!bag.isEmpty(), "Runtime backpack fixture registered"); - bag.set(DataComponents.CUSTOM_NAME, Component.literal("Copper supplies")); - var previous = player.getMainHandItem(); - player.setItemInHand(InteractionHand.MAIN_HAND, bag); - // Normal use creates the backpack's own storage identity and initializes its capability. - bag.use(player.level(), player, InteractionHand.MAIN_HAND); - player.closeContainer(); - player.setItemInHand(InteractionHand.MAIN_HAND, previous); - var slots = - CuriosApi.getCuriosInventory(player) - .orElseThrow() - .getStacksHandler("back") - .orElseThrow() - .getStacks(); - slots.setStackInSlot(0, bag); - var handler = bag.getCapability(Capabilities.ItemHandler.ITEM); - require( - handler != null && handler.getSlots() >= 27, - "Initialized backpack exposes generic item storage"); - require( - handler.insertItem(0, new ItemStack(Items.IRON_INGOT, 24), false).isEmpty(), - "Fixture accepts iron"); - require( - handler.insertItem(1, new ItemStack(Items.COPPER_INGOT, 12), false).isEmpty(), - "Fixture accepts copper"); - require( - handler.insertItem(2, new ItemStack(Items.REDSTONE, 32), false).isEmpty(), - "Fixture accepts redstone"); - return bag; - } + static ItemStack equip(ServerPlayer player) { + var bag = new ItemStack(BuiltInRegistries.ITEM + .get(ResourceLocation.parse("sophisticatedbackpacks:backpack"))); + require(!bag.isEmpty(), "Runtime backpack fixture registered"); + bag.set(DataComponents.CUSTOM_NAME, + Component.literal("Copper supplies")); + var previous = player.getMainHandItem(); + player.setItemInHand(InteractionHand.MAIN_HAND, bag); + // Normal use creates the backpack's own storage identity and + // initializes its capability. + bag.use(player.level(), player, InteractionHand.MAIN_HAND); + player.closeContainer(); + player.setItemInHand(InteractionHand.MAIN_HAND, previous); + var slots = CuriosApi.getCuriosInventory(player).orElseThrow() + .getStacksHandler("back").orElseThrow().getStacks(); + slots.setStackInSlot(0, bag); + var handler = bag.getCapability(Capabilities.ItemHandler.ITEM); + require(handler != null && handler.getSlots() >= 27, + "Initialized backpack exposes generic item storage"); + require( + handler.insertItem(0, new ItemStack(Items.IRON_INGOT, 24), false) + .isEmpty(), + "Fixture accepts iron"); + require( + handler.insertItem(1, new ItemStack(Items.COPPER_INGOT, 12), false) + .isEmpty(), + "Fixture accepts copper"); + require(handler.insertItem(2, new ItemStack(Items.REDSTONE, 32), false) + .isEmpty(), "Fixture accepts redstone"); + return bag; + } - static void unequip(ServerPlayer player) { - CuriosApi.getCuriosInventory(player) - .orElseThrow() - .getStacksHandler("back") - .orElseThrow() - .getStacks() - .setStackInSlot(0, ItemStack.EMPTY); - } + static void unequip(ServerPlayer player) { + CuriosApi.getCuriosInventory(player).orElseThrow() + .getStacksHandler("back").orElseThrow().getStacks() + .setStackInSlot(0, ItemStack.EMPTY); + } - private static void require(boolean ok, String message) { - if (!ok) throw new AssertionError(message); - } + private static void require(boolean ok, String message) { + if (!ok) + throw new AssertionError(message); + } } diff --git a/src/companionTest/java/zone/moddev/mc/companion/test/BackpackInteractionClientSmoke.java b/src/companionTest/java/zone/moddev/mc/companion/test/BackpackInteractionClientSmoke.java index 5b9d1a1a..89638586 100644 --- a/src/companionTest/java/zone/moddev/mc/companion/test/BackpackInteractionClientSmoke.java +++ b/src/companionTest/java/zone/moddev/mc/companion/test/BackpackInteractionClientSmoke.java @@ -9,7 +9,10 @@ import zone.moddev.mc.lib.gui.runtime.EquippedContainers; import zone.moddev.mc.lib.gui.runtime.component.BuiltinMenuComponents; -/** Real mouse gestures and hotbar keys, including a double-click before pickup acknowledgement. */ +/** + * Real mouse gestures and hotbar keys, including a double-click before pickup + * acknowledgement. + */ final class BackpackInteractionClientSmoke { private static int step, reported = -1; private static volatile boolean ready; @@ -17,24 +20,26 @@ final class BackpackInteractionClientSmoke { static boolean run(MachineScreen screen) { var mc = Minecraft.getInstance(); var menu = screen.getMenu(); - var state = menu.components().get(BuiltinMenuComponents.BACKPACK).state(); + var state = menu.components().get(BuiltinMenuComponents.BACKPACK) + .state(); if (reported != step) { reported = step; - zone.moddev.mc.lib.MMDLib.LOGGER.info("BACKPACK_INTERACTION_STEP {}", step); + zone.moddev.mc.lib.MMDLib.LOGGER + .info("BACKPACK_INTERACTION_STEP {}", step); } if (menu.components().get(BuiltinMenuComponents.BACKPACK).pending()) return false; var layout = DrawerExtensionClientSmoke.containerLayout(screen, false); switch (step) { - case 0 -> - setup( - player -> { - var handler = EquippedContainers.find(player).getFirst().items(); - for (int i = 0; i < handler.getSlots(); i++) - handler.extractItem(i, Integer.MAX_VALUE, false); - player.getInventory().clearContent(); - player.containerMenu.setCarried(new ItemStack(Items.IRON_INGOT, 10)); - }); + case 0 -> setup(player -> { + var handler = EquippedContainers.find(player).getFirst() + .items(); + for (int i = 0; i < handler.getSlots(); i++) + handler.extractItem(i, Integer.MAX_VALUE, false); + player.getInventory().clearContent(); + player.containerMenu + .setCarried(new ItemStack(Items.IRON_INGOT, 10)); + }); case 1 -> { if (!ready || menu.getCarried().getCount() != 10) return false; @@ -42,10 +47,11 @@ static boolean run(MachineScreen screen) { } case 2 -> { require( - menu.getCarried().getCount() == 1 && state.contents().get(0).getCount() == 9, + menu.getCarried().getCount() == 1 + && state.contents().get(0).getCount() == 9, "Left drag preserves remainder while the capability routes insertions"); - setup( - player -> player.containerMenu.setCarried(new ItemStack(Items.IRON_INGOT, 5))); + setup(player -> player.containerMenu + .setCarried(new ItemStack(Items.IRON_INGOT, 5))); } case 3 -> { if (!ready || menu.getCarried().getCount() != 5) @@ -54,18 +60,20 @@ static boolean run(MachineScreen screen) { } case 4 -> { require( - menu.getCarried().getCount() == 3 && state.contents().get(0).getCount() == 11, + menu.getCarried().getCount() == 3 + && state.contents().get(0).getCount() == 11, "Right drag inserts one per target while respecting capability routing"); - setup( - player -> { - player.containerMenu.setCarried(ItemStack.EMPTY); - var handler = EquippedContainers.find(player).getFirst().items(); - ((net.neoforged.neoforge.items.IItemHandlerModifiable) handler) - .setStackInSlot(7, new ItemStack(Items.IRON_INGOT, 10)); - ((net.neoforged.neoforge.items.IItemHandlerModifiable) handler) - .setStackInSlot(8, new ItemStack(Items.IRON_INGOT, 20)); - player.getInventory().setItem(9, new ItemStack(Items.IRON_INGOT, 12)); - }); + setup(player -> { + player.containerMenu.setCarried(ItemStack.EMPTY); + var handler = EquippedContainers.find(player).getFirst() + .items(); + ((net.neoforged.neoforge.items.IItemHandlerModifiable) handler) + .setStackInSlot(7, new ItemStack(Items.IRON_INGOT, 10)); + ((net.neoforged.neoforge.items.IItemHandlerModifiable) handler) + .setStackInSlot(8, new ItemStack(Items.IRON_INGOT, 20)); + player.getInventory().setItem(9, + new ItemStack(Items.IRON_INGOT, 12)); + }); } case 5 -> { if (!ready || !menu.getCarried().isEmpty() @@ -75,50 +83,50 @@ static boolean run(MachineScreen screen) { click(screen, layout.cell(7)); } case 6 -> { + require(menu.getCarried().getCount() == 53, "Collection cursor=" + + menu.getCarried() + ", contents=" + state.contents()); require( - menu.getCarried().getCount() == 53, - "Collection cursor=" + menu.getCarried() + ", contents=" + state.contents()); - require( - state.contents().stream().noneMatch(s -> s.is(Items.IRON_INGOT)) + state.contents().stream() + .noneMatch(s -> s.is(Items.IRON_INGOT)) && mc.player.getInventory().getItem(9).isEmpty(), "Double click collects backpack and player stacks"); - setup( - player -> { - player.containerMenu.setCarried(ItemStack.EMPTY); - player.getInventory().setItem(0, new ItemStack(Items.DIAMOND, 2)); - ((net.neoforged.neoforge.items.IItemHandlerModifiable) EquippedContainers - .find(player).getFirst().items()) - .setStackInSlot(9, new ItemStack(Items.COPPER_INGOT, 5)); - }); + setup(player -> { + player.containerMenu.setCarried(ItemStack.EMPTY); + player.getInventory().setItem(0, + new ItemStack(Items.DIAMOND, 2)); + ((net.neoforged.neoforge.items.IItemHandlerModifiable) EquippedContainers + .find(player).getFirst().items()).setStackInSlot(9, + new ItemStack(Items.COPPER_INGOT, 5)); + }); } case 7 -> { - if (!ready - || !menu.getCarried().isEmpty() + if (!ready || !menu.getCarried().isEmpty() || !mc.player.getInventory().getItem(0).is(Items.DIAMOND)) return false; - screen.mouseMoved(layout.cell(9).x() + 5, layout.cell(9).y() + 5); - require(screen.keyPressed(49, 0, 0), "Number key handled over backpack"); + screen.mouseMoved(layout.cell(9).x() + 5, + layout.cell(9).y() + 5); + require(screen.keyPressed(49, 0, 0), + "Number key handled over backpack"); } case 8 -> { require( - state.contents().stream() - .filter(s -> s.is(Items.DIAMOND)) - .mapToInt(ItemStack::getCount) - .sum() == 2 - && mc.player.getInventory().getItem(0).is(Items.COPPER_INGOT) + state.contents().stream().filter(s -> s.is(Items.DIAMOND)) + .mapToInt(ItemStack::getCount).sum() == 2 + && mc.player.getInventory().getItem(0) + .is(Items.COPPER_INGOT) && mc.player.getInventory().getItem(0).getCount() == 5, "Number key swaps with hotbar"); - setup( - player -> { - player.getInventory().clearContent(); - player.containerMenu.setCarried(new ItemStack(Items.IRON_INGOT, 10)); - }); + setup(player -> { + player.getInventory().clearContent(); + player.containerMenu + .setCarried(new ItemStack(Items.IRON_INGOT, 10)); + }); } case 9 -> { if (!ready || menu.getCarried().getCount() != 10) return false; - beginDrag(screen, 0, playerCell(screen, 9), playerCell(screen, 10), - playerCell(screen, 11)); + beginDrag(screen, 0, playerCell(screen, 9), + playerCell(screen, 10), playerCell(screen, 11)); } case 10 -> release(screen, 0, playerCell(screen, 11)); case 11 -> { @@ -128,16 +136,17 @@ static boolean run(MachineScreen screen) { && mc.player.getInventory().getItem(10).getCount() == 3 && mc.player.getInventory().getItem(11).getCount() == 3, "Normal inventory left drag survives frames and release with backpack open"); - setup( - player -> { - player.getInventory().clearContent(); - player.containerMenu.setCarried(new ItemStack(Items.IRON_INGOT, 5)); - }); + setup(player -> { + player.getInventory().clearContent(); + player.containerMenu + .setCarried(new ItemStack(Items.IRON_INGOT, 5)); + }); } case 12 -> { if (!ready || menu.getCarried().getCount() != 5) return false; - beginDrag(screen, 1, playerCell(screen, 12), playerCell(screen, 13)); + beginDrag(screen, 1, playerCell(screen, 12), + playerCell(screen, 13)); } case 13 -> release(screen, 1, playerCell(screen, 13)); case 14 -> { @@ -146,17 +155,17 @@ static boolean run(MachineScreen screen) { && mc.player.getInventory().getItem(12).getCount() == 1 && mc.player.getInventory().getItem(13).getCount() == 1, "Normal inventory right drag survives frames and release with backpack open"); - setup( - player -> { - player.containerMenu.setCarried(ItemStack.EMPTY); - player.getInventory().clearContent(); - player.getInventory().setItem(9, new ItemStack(Items.IRON_INGOT, 10)); - player.getInventory().setItem(10, new ItemStack(Items.IRON_INGOT, 20)); - }); + setup(player -> { + player.containerMenu.setCarried(ItemStack.EMPTY); + player.getInventory().clearContent(); + player.getInventory().setItem(9, + new ItemStack(Items.IRON_INGOT, 10)); + player.getInventory().setItem(10, + new ItemStack(Items.IRON_INGOT, 20)); + }); } case 15 -> { - if (!ready - || !menu.getCarried().isEmpty() + if (!ready || !menu.getCarried().isEmpty() || mc.player.getInventory().getItem(9).getCount() != 10) return false; click(screen, playerCell(screen, 9)); @@ -168,17 +177,18 @@ static boolean run(MachineScreen screen) { && mc.player.getInventory().getItem(9).isEmpty() && mc.player.getInventory().getItem(10).isEmpty(), "Normal inventory double click collects with backpack open"); - setup( - player -> player.containerMenu.setCarried(new ItemStack(Items.IRON_INGOT, 10))); + setup(player -> player.containerMenu + .setCarried(new ItemStack(Items.IRON_INGOT, 10))); } case 17 -> { if (!ready || menu.getCarried().getCount() != 10) return false; - beginDrag(screen, 0, layout.cell(3), playerCell(screen, 9), playerCell(screen, 10)); + beginDrag(screen, 0, layout.cell(3), playerCell(screen, 9), + playerCell(screen, 10)); setup( player -> ((net.neoforged.neoforge.items.IItemHandlerModifiable) EquippedContainers - .find(player).getFirst().items()) - .setStackInSlot(1, new ItemStack(Items.COPPER_INGOT, 13))); + .find(player).getFirst().items()).setStackInSlot(1, + new ItemStack(Items.COPPER_INGOT, 13))); } case 18 -> { if (!ready || state.contents().get(1).getCount() != 13) @@ -192,25 +202,26 @@ static boolean run(MachineScreen screen) { && mc.player.getInventory().getItem(10).getCount() == 3 && state.contents().stream() .filter(s -> s.is(Items.IRON_INGOT)) - .mapToInt(ItemStack::getCount) - .sum() == 3, + .mapToInt(ItemStack::getCount).sum() == 3, "Backpack drag into player slots survives frames and release"); - setup( - player -> { - var handler = EquippedContainers.find(player).getFirst().items(); - for (int i = 0; i < handler.getSlots(); i++) - handler.extractItem(i, Integer.MAX_VALUE, false); - // Seed the fixture's exact slots; production intentionally uses only I/O. - ((net.neoforged.neoforge.items.IItemHandlerModifiable) handler) - .setStackInSlot(1, new ItemStack(Items.COPPER_INGOT, 12)); - player.getInventory().clearContent(); - player.getInventory().setItem(0, new ItemStack(Items.IRON_INGOT, 24)); - player.containerMenu.setCarried(ItemStack.EMPTY); - }); + setup(player -> { + var handler = EquippedContainers.find(player).getFirst() + .items(); + for (int i = 0; i < handler.getSlots(); i++) + handler.extractItem(i, Integer.MAX_VALUE, false); + // Seed the fixture's exact slots; production intentionally + // uses only I/O. + ((net.neoforged.neoforge.items.IItemHandlerModifiable) handler) + .setStackInSlot(1, + new ItemStack(Items.COPPER_INGOT, 12)); + player.getInventory().clearContent(); + player.getInventory().setItem(0, + new ItemStack(Items.IRON_INGOT, 24)); + player.containerMenu.setCarried(ItemStack.EMPTY); + }); } default -> { - return ready - && state.contents().get(1).getCount() == 12 + return ready && state.contents().get(1).getCount() == 12 && state.contents().get(9).isEmpty(); } } @@ -222,17 +233,16 @@ private static void setup( java.util.function.Consumer action) { ready = false; var server = Minecraft.getInstance().getSingleplayerServer(); - server.execute( - () -> { - try { - var player = server.getPlayerList().getPlayers().getFirst(); - action.accept(player); - player.containerMenu.broadcastChanges(); - ready = true; - } catch (Throwable error) { - CompanionClientSmoke.fail(error); - } - }); + server.execute(() -> { + try { + var player = server.getPlayerList().getPlayers().getFirst(); + action.accept(player); + player.containerMenu.broadcastChanges(); + ready = true; + } catch (Throwable error) { + CompanionClientSmoke.fail(error); + } + }); } private static void drag(MachineScreen screen, int button, Rect... cells) { @@ -240,24 +250,28 @@ private static void drag(MachineScreen screen, int button, Rect... cells) { release(screen, button, cells[cells.length - 1]); } - private static void beginDrag(MachineScreen screen, int button, Rect... cells) { + private static void beginDrag(MachineScreen screen, int button, + Rect... cells) { var first = cells[0]; - require(screen.mouseClicked(first.x() + 5, first.y() + 5, button), "Drag press handled"); + require(screen.mouseClicked(first.x() + 5, first.y() + 5, button), + "Drag press handled"); for (var cell : cells) screen.mouseDragged(cell.x() + 5, cell.y() + 5, button, 18, 0); } private static void release(MachineScreen screen, int button, Rect last) { - require(screen.mouseReleased(last.x() + 5, last.y() + 5, button), "Drag release handled"); + require(screen.mouseReleased(last.x() + 5, last.y() + 5, button), + "Drag release handled"); } private static Rect playerCell(MachineScreen screen, int index) { var inventory = Minecraft.getInstance().player.getInventory(); var slot = screen.getMenu().slots.stream() - .filter(s -> s.container == inventory && s.getContainerSlot() == index) - .findFirst() - .orElseThrow(); - return new Rect(screen.guiLeft() + slot.x, screen.guiTop() + slot.y, 16, 16); + .filter( + s -> s.container == inventory && s.getContainerSlot() == index) + .findFirst().orElseThrow(); + return new Rect(screen.guiLeft() + slot.x, screen.guiTop() + slot.y, 16, + 16); } private static void click(MachineScreen screen, Rect cell) { diff --git a/src/companionTest/java/zone/moddev/mc/companion/test/BasePoweredMachineTests.java b/src/companionTest/java/zone/moddev/mc/companion/test/BasePoweredMachineTests.java index e9d2568e..9063d04b 100644 --- a/src/companionTest/java/zone/moddev/mc/companion/test/BasePoweredMachineTests.java +++ b/src/companionTest/java/zone/moddev/mc/companion/test/BasePoweredMachineTests.java @@ -51,7 +51,8 @@ private static void require(boolean condition, String message) { } @GameTest(template = "empty") - public static void legacyMappingsRunBeforeFeaturesAndRespectStableIds(GameTestHelper helper) { + public static void legacyMappingsRunBeforeFeaturesAndRespectStableIds( + GameTestHelper helper) { var registries = helper.getLevel().registryAccess(); var probe = new Probe(helper, 1); probe.metadata = 1; @@ -77,7 +78,8 @@ public static void legacyMappingsRunBeforeFeaturesAndRespectStableIds(GameTestHe && restored.tanks.getFluidInTank(1).getAmount() == 500, "Legacy indexes remap once; subsequent identified entries retain their identities"); require( - restored.itemAtRestore.is(Items.EMERALD) && restored.featureValue == 7, + restored.itemAtRestore.is(Items.EMERALD) + && restored.featureValue == 7, "Features see migrated storage before custom world metadata is restored"); saved = restored.saveWithoutMetadata(registries); } @@ -87,39 +89,38 @@ public static void legacyMappingsRunBeforeFeaturesAndRespectStableIds(GameTestHe @GameTest(template = "empty") public static void lifecycleRejectsReservedSaveKeys(GameTestHelper helper) { var probe = new Probe(helper, 1); - for (var key : List.of( - "automation", - "features", - "redstone_control", - "redstone_mode", - "id", - "components", - "mmdlib_features", + for (var key : List.of("automation", "features", "redstone_control", + "redstone_mode", "id", "components", "mmdlib_features", "mmdlib_machine")) { try { probe.lifecycle.items(key, new ItemStorage(1)); - throw new AssertionError("Reserved storage key accepted: " + key); + throw new AssertionError( + "Reserved storage key accepted: " + key); } catch (IllegalArgumentException expected) { - require(expected.getMessage().contains(key), "Diagnostic names the reserved key"); + require(expected.getMessage().contains(key), + "Diagnostic names the reserved key"); } } helper.succeed(); } @GameTest(template = "empty") - public static void ownedPowerSurvivesEvenWhenCustomLifecycleOmitsIt(GameTestHelper helper) { + public static void ownedPowerSurvivesEvenWhenCustomLifecycleOmitsIt( + GameTestHelper helper) { var registries = helper.getLevel().registryAccess(); for (int mode = 0; mode < 3; mode++) { var original = new Probe(helper, mode); original.energy.setStored(1234); original.featureValue = 7; if (mode != 0) - original.items.setStackInSlot(0, new ItemStack(Items.DIAMOND, 3)); + original.items.setStackInSlot(0, + new ItemStack(Items.DIAMOND, 3)); var saved = original.saveWithoutMetadata(registries); var restored = new Probe(helper, mode); restored.loadWithComponents(saved, registries); require( - restored.energy.stored() == 1234 && restored.energy.capacity() == 3210, + restored.energy.stored() == 1234 + && restored.energy.capacity() == 3210, "Base owns power persistence with no lifecycle, a raw lifecycle, or its factory"); require( restored.powerAtRestore == 1234 && restored.featureValue == 7, @@ -129,8 +130,7 @@ public static void ownedPowerSurvivesEvenWhenCustomLifecycleOmitsIt(GameTestHelp "Custom storage also survives"); restored.clearContent(); require( - restored.energy.stored() == 0 - && restored.featureValue == 0 + restored.energy.stored() == 0 && restored.featureValue == 0 && restored.items.getStackInSlot(0).isEmpty(), "Clear includes power even when the subclass omitted it"); } @@ -140,13 +140,15 @@ public static void ownedPowerSurvivesEvenWhenCustomLifecycleOmitsIt(GameTestHelp conflict.saveWithoutMetadata(registries); throw new AssertionError("A conflicting energy binding must fail"); } catch (IllegalArgumentException expected) { - require(expected.getMessage().contains("energy"), "Binding error identifies energy"); + require(expected.getMessage().contains("energy"), + "Binding error identifies energy"); } helper.succeed(); } @GameTest(template = "empty") - public static void customClientUpdatesAreIsolatedFromPersistence(GameTestHelper helper) { + public static void customClientUpdatesAreIsolatedFromPersistence( + GameTestHelper helper) { var registries = helper.getLevel().registryAccess(); var server = new Probe(helper, 1); server.featureValue = 9; @@ -155,25 +157,25 @@ public static void customClientUpdatesAreIsolatedFromPersistence(GameTestHelper client.energy.setStored(2345); client.items.setStackInSlot(0, new ItemStack(Items.EMERALD, 4)); client.metadata = 61; - var packet = (ClientboundBlockEntityDataPacket) server.getUpdatePacket(); + var packet = (ClientboundBlockEntityDataPacket) server + .getUpdatePacket(); client.loadWithComponents(packet.getTag(), registries); require( - client.featureValue == 9 && client.visual == 42 && client.clientReads == 1, + client.featureValue == 9 && client.visual == 42 + && client.clientReads == 1, "Normal update packets carry features and custom renderer state together"); require( client.energy.stored() == 2345 && client.items.getStackInSlot(0).getCount() == 4 - && client.metadata == 61 - && client.worldReads == 0, + && client.metadata == 61 && client.worldReads == 0, "Client hook data cannot overwrite common storage or run world-load hooks"); - client.loadWithComponents(FeatureSync.updateTag(server.features, registries), registries); - require( - client.visual == 42 && client.clientReads == 1, + client.loadWithComponents( + FeatureSync.updateTag(server.features, registries), registries); + require(client.visual == 42 && client.clientReads == 1, "Older feature-only updates do not reset custom client state"); server.visual = 0; client.loadWithComponents(server.getUpdateTag(registries), registries); - require( - client.visual == 0 && client.clientReads == 2, + require(client.visual == 0 && client.clientReads == 2, "Custom state can be cleared by a later update"); var saved = client.saveWithoutMetadata(registries); require(!saved.contains("mmdlib_machine"), @@ -182,43 +184,35 @@ public static void customClientUpdatesAreIsolatedFromPersistence(GameTestHelper restored.visual = 88; restored.loadWithComponents(saved, registries); require( - restored.metadata == 61 - && restored.visual == 88 - && restored.clientReads == 0 - && restored.worldReads == 1, + restored.metadata == 61 && restored.visual == 88 + && restored.clientReads == 0 && restored.worldReads == 1, "World saves use their own metadata hook without invoking the client hook"); server.sendVisualUpdate(); helper.succeed(); } @GameTest(template = "empty") - public static void baseWarningsDescribeProcessorsAndRespectPausePolicy(GameTestHelper helper) { + public static void baseWarningsDescribeProcessorsAndRespectPausePolicy( + GameTestHelper helper) { var probe = new Probe(helper, 0); var resources = new ProcessorEntity( - CompanionMod.DUAL_CRUSHER.type.get(), - probe.getBlockPos(), + CompanionMod.DUAL_CRUSHER.type.get(), probe.getBlockPos(), CompanionMod.DUAL_CRUSHER.block.get().defaultBlockState(), ProcessorEntity.Kind.DUAL); for (var lane : resources.lanes) probe.features.add(lane); - require( - probe.getWarnings().isEmpty(), + require(probe.getWarnings().isEmpty(), "Idle processors and unrelated features produce no warnings"); var first = resources.lanes.getFirst(); for (var status : RecipeProcessor.Status.values()) { - first.restoreOnClient( - new RecipeProcessor.State( - status == RecipeProcessor.Status.PAUSED, - 0, - Optional.empty(), - 0, - Optional.empty(), - Optional.of(status))); + first.restoreOnClient(new RecipeProcessor.State( + status == RecipeProcessor.Status.PAUSED, 0, Optional.empty(), 0, + Optional.empty(), Optional.of(status))); require( - ProcessorButtonState.of(first) - .name() - .equals( - status == RecipeProcessor.Status.RUNNING ? "PROCESSING" : status.name()), + ProcessorButtonState.of(first).name() + .equals(status == RecipeProcessor.Status.RUNNING + ? "PROCESSING" + : status.name()), "Processor button distinguishes every current state, including automatic holds"); var key = switch (status) { case STALLED -> "screen.mmdlib.machine.stalled"; @@ -228,12 +222,14 @@ public static void baseWarningsDescribeProcessorsAndRespectPausePolicy(GameTestH }; var warnings = probe.getWarnings(); if (key == null) - require(warnings.isEmpty(), "Normal/manual states are not automatic holds"); + require(warnings.isEmpty(), + "Normal/manual states are not automatic holds"); else { - require(warnings.size() == 1, "Exactly one warning for the affected processor"); - var text = (TranslatableContents) warnings.getFirst().getContents(); - require( - text.getArgs()[0].equals(first.displayName()), + require(warnings.size() == 1, + "Exactly one warning for the affected processor"); + var text = (TranslatableContents) warnings.getFirst() + .getContents(); + require(text.getArgs()[0].equals(first.displayName()), "Warnings use the registered processor name"); require( ((Component) text.getArgs()[1]) @@ -244,25 +240,24 @@ public static void baseWarningsDescribeProcessorsAndRespectPausePolicy(GameTestH } probe.redstone.migrate(1); probe.redstone.readInputs(); - require( - probe.redstone.paused() && probe.getWarnings().size() == 1, + require(probe.redstone.paused() && probe.getWarnings().size() == 1, "Redstone hold is reported"); probe.policy = MachineInventoryLifecycle.PausePolicy.TRANSFERS_ONLY; - require( - probe.getWarnings().isEmpty(), + require(probe.getWarnings().isEmpty(), "A transfer-only pause does not claim processing is held"); resources.setReservePercent(50); require( resources.getWarnings().stream() - .anyMatch( - c -> c.getContents() instanceof TranslatableContents t - && t.getKey().equals("screen.mmdlib_companion.reserve.waiting")), + .anyMatch(c -> c.getContents() instanceof TranslatableContents t + && t.getKey() + .equals("screen.mmdlib_companion.reserve.waiting")), "Subclasses retain custom warnings"); helper.succeed(); } private static final class Probe extends BasePoweredMachine { - final ItemStorage items = new ItemStorage(2).slotId(0, "second").slotId(1, "first"); + final ItemStorage items = new ItemStorage(2).slotId(0, "second") + .slotId(1, "first"); final FluidStorage tanks = new FluidStorage(); ItemStack itemAtRestore = ItemStack.EMPTY; final MachineInventoryLifecycle lifecycle; @@ -272,62 +267,53 @@ private static final class Probe extends BasePoweredMachine { long powerAtRestore; Probe(GameTestHelper helper, int mode) { - super( - CompanionMod.CRUSHER.type.get(), + super(CompanionMod.CRUSHER.type.get(), helper.absolutePos(new BlockPos(2, 2, 2)), - CompanionMod.CRUSHER.block.get().defaultBlockState(), - 3210, - 40, + CompanionMod.CRUSHER.block.get().defaultBlockState(), 3210, 40, 20); setLevel(helper.getLevel()); tanks.addTank(2000, Role.STORAGE); tanks.addTank(2000, Role.STORAGE); tanks.tankId(0, "second").tankId(1, "first"); var group = new ResourceGroup( - "energy", - Component.literal("Energy"), - 0xffffff, - ResourceType.ENERGY, - IoMode.BOTH, - List.of(0), - List.of("energy"), + "energy", Component.literal("Energy"), 0xffffff, + ResourceType.ENERGY, IoMode.BOTH, List.of(0), List.of("energy"), new SideConfig(IoMode.BOTH)); var automation = new AutomationInventory(this, List.of(group)); lifecycle = mode == 0 ? null : (mode == 1 - ? new MachineInventoryLifecycle(this, features, automation, energy) - : poweredInventory(automation)) - .items("items", items) + ? new MachineInventoryLifecycle(this, features, automation, + energy) + : poweredInventory(automation)).items("items", items) .fluids("fluids", tanks); - features.add( - new MachineFeature() { - public String key() { - return "probe"; - } + features.add(new MachineFeature() { + public String key() { + return "probe"; + } - public Codec codec() { - return Codec.INT; - } + public Codec codec() { + return Codec.INT; + } - public Integer state() { - return featureValue; - } + public Integer state() { + return featureValue; + } - public void restore(Integer value) { - featureValue = value; - powerAtRestore = energy.stored(); - itemAtRestore = items.getStackInSlot(0).copy(); - } + public void restore(Integer value) { + featureValue = value; + powerAtRestore = energy.stored(); + itemAtRestore = items.getStackInSlot(0).copy(); + } - public void reset() { - featureValue = 0; - } + public void reset() { + featureValue = 0; + } - public boolean syncsToClient() { - return true; - } - }); + public boolean syncsToClient() { + return true; + } + }); } @Override @@ -352,35 +338,40 @@ protected MachineInventoryLifecycle.PausePolicy pausePolicy() { @Override protected IntUnaryOperator legacySlots(String key, CompoundTag tag) { - return tag.getInt("metadata") == 1 && (key.equals("items") || key.equals("fluids")) - ? index -> 1 - index - : IntUnaryOperator.identity(); + return tag.getInt("metadata") == 1 + && (key.equals("items") || key.equals("fluids")) + ? index -> 1 - index + : IntUnaryOperator.identity(); } @Override - protected void saveMachineData(CompoundTag tag, HolderLookup.Provider registries) { + protected void saveMachineData(CompoundTag tag, + HolderLookup.Provider registries) { tag.putInt("metadata", metadata); } @Override - protected void loadMachineData(CompoundTag tag, HolderLookup.Provider registries) { + protected void loadMachineData(CompoundTag tag, + HolderLookup.Provider registries) { metadata = tag.getInt("metadata"); worldReads++; } @Override - protected void saveClientData(CompoundTag tag, HolderLookup.Provider registries) { + protected void saveClientData(CompoundTag tag, + HolderLookup.Provider registries) { tag.putInt("visual", visual); tag.putInt("expected_feature", featureValue); - // These names must remain isolated from the base's persistence and sync envelopes. + // These names must remain isolated from the base's persistence and + // sync envelopes. tag.putLong("energy", -1); tag.putString("mmdlib_features", "custom"); } @Override - protected void loadClientData(CompoundTag tag, HolderLookup.Provider registries) { - require( - featureValue == tag.getInt("expected_feature"), + protected void loadClientData(CompoundTag tag, + HolderLookup.Provider registries) { + require(featureValue == tag.getInt("expected_feature"), "Features restore before custom client state"); visual = tag.getInt("visual"); clientReads++; @@ -391,7 +382,8 @@ void sendVisualUpdate() { } @Override - protected void recoverResources(Level level, BlockPos pos, CompoundTag payload) { + protected void recoverResources(Level level, BlockPos pos, + CompoundTag payload) { throw new AssertionError("Unexpected recovery"); } } diff --git a/src/companionTest/java/zone/moddev/mc/companion/test/CompanionClientSmoke.java b/src/companionTest/java/zone/moddev/mc/companion/test/CompanionClientSmoke.java index 43865b67..a2888607 100644 --- a/src/companionTest/java/zone/moddev/mc/companion/test/CompanionClientSmoke.java +++ b/src/companionTest/java/zone/moddev/mc/companion/test/CompanionClientSmoke.java @@ -33,35 +33,36 @@ import zone.moddev.mc.lib.gui.runtime.ScreenRuntime; import zone.moddev.mc.lib.gui.runtime.component.BuiltinMenuComponents; -/** Opens all four networked menus in an isolated world and captures the rendered screens. */ +/** + * Opens all four networked menus in an isolated world and captures the rendered + * screens. + */ @EventBusSubscriber(modid = CompanionMod.ID, value = Dist.CLIENT) public final class CompanionClientSmoke { private static final BlockPos POS = new BlockPos(0, 100, 0); /** - * Time the whole smoke may take once the title screen is up. Launch time is not counted: it - * varies with the machine, and a slow CI runner with software rendering needs most of this - * budget for the menus alone. A menu that never renders still fails within 400 ticks (see - * {@link #tick}). + * Time the whole smoke may take once the title screen is up. Launch time is + * not counted: it varies with the machine, and a slow CI runner with + * software rendering needs most of this budget for the menus alone. A menu + * that never renders still fails within 400 ticks (see {@link #tick}). */ private static final long BUDGET_MILLIS = 360_000; private static long deadline = Long.MAX_VALUE; private static boolean started, opening, menuRequested, finished; - private static int stage, frames, waitingTicks, drawerStage, fluidStage, controlStage; + private static int stage, frames, waitingTicks, drawerStage, fluidStage, + controlStage; private static boolean controlFixtureRequested; private static boolean redstoneSmokeDone; private static volatile boolean controlFixtureReady; @SubscribeEvent public static void render(ScreenEvent.Render.Pre event) { - if (Boolean.getBoolean("mmdlib.companionSmoke") - && !finished - && opening - && stage == 4 - && event.getScreen() instanceof MachineScreen screen - && AutomationClientSmoke.renderTooltipCapture( - screen, event.getGuiGraphics(), event.getPartialTick())) + if (Boolean.getBoolean("mmdlib.companionSmoke") && !finished && opening + && stage == 4 && event.getScreen() instanceof MachineScreen screen + && AutomationClientSmoke.renderTooltipCapture(screen, + event.getGuiGraphics(), event.getPartialTick())) event.setCanceled(true); } @@ -127,71 +128,53 @@ public static void tick(ClientTickEvent.Post event) { } deadline = System.currentTimeMillis() + BUDGET_MILLIS; if (!net.neoforged.fml.ModList.get().isLoaded("jade")) - throw new AssertionError("Jade must be installed in companion runs"); + throw new AssertionError( + "Jade must be installed in companion runs"); MMDLib.LOGGER.info("JADE_COMPANION_PRESENT"); started = true; - var settings = new LevelSettings( - "Companion smoke", - GameType.CREATIVE, - false, - Difficulty.PEACEFUL, - true, - new GameRules(), - WorldDataConfiguration.DEFAULT); - mc.createWorldOpenFlows() - .createFreshLevel( - "companion-" + System.currentTimeMillis(), - settings, - new WorldOptions(1, false, false), - registries -> registries - .registryOrThrow(Registries.WORLD_PRESET) - .getHolderOrThrow(WorldPresets.FLAT) - .value() - .createWorldDimensions(), - mc.screen); + var settings = new LevelSettings("Companion smoke", + GameType.CREATIVE, false, Difficulty.PEACEFUL, true, + new GameRules(), WorldDataConfiguration.DEFAULT); + mc.createWorldOpenFlows().createFreshLevel( + "companion-" + System.currentTimeMillis(), settings, + new WorldOptions(1, false, false), + registries -> registries + .registryOrThrow(Registries.WORLD_PRESET) + .getHolderOrThrow(WorldPresets.FLAT).value() + .createWorldDimensions(), + mc.screen); } if (mc.player == null || mc.level == null) return; if (opening) { if (++waitingTicks > 400) throw new AssertionError( - "Menu " - + stage - + " did not render; screen=" - + mc.screen - + ", block entity=" - + mc.level.getBlockEntity(POS) - + ", control stage=" - + controlStage - + ", fluid stage=" - + fluidStage + "Menu " + stage + " did not render; screen=" + mc.screen + + ", block entity=" + mc.level.getBlockEntity(POS) + + ", control stage=" + controlStage + + ", fluid stage=" + fluidStage + (mc.screen instanceof MachineScreen screen - ? ", cycle time=" - + screen - .getMenu().bound - .screen() - .find("progress") - .map(element -> screen.getMenu().value(element, true)) - .orElse("missing") + ? ", cycle time=" + screen.getMenu().bound + .screen().find("progress") + .map(element -> screen.getMenu() + .value(element, true)) + .orElse("missing") : "")); var entity = mc.level.getBlockEntity(POS); - if (!menuRequested - && entity != null - && entity.getType() == CompanionMod.MACHINES.get(stage).type.get()) { + if (!menuRequested && entity != null && entity + .getType() == CompanionMod.MACHINES.get(stage).type.get()) { menuRequested = true; - mc.getSingleplayerServer() - .execute( - () -> { - try { - var player = mc.getSingleplayerServer().getPlayerList() - .getPlayers().getFirst(); - if (!ScreenRuntime.open(player, - player.serverLevel().getBlockEntity(POS))) - throw new AssertionError("Menu did not open"); - } catch (Throwable error) { - fail(error); - } - }); + mc.getSingleplayerServer().execute(() -> { + try { + var player = mc.getSingleplayerServer() + .getPlayerList().getPlayers().getFirst(); + if (!ScreenRuntime.open(player, + player.serverLevel().getBlockEntity(POS))) + throw new AssertionError("Menu did not open"); + } catch (Throwable error) { + fail(error); + } + }); } return; } @@ -199,103 +182,89 @@ public static void tick(ClientTickEvent.Post event) { if (!DrawerMemoryClientSmoke.run()) return; finished = true; - MMDLib.LOGGER.info( - Boolean.getBoolean("mmdlib.drawerSmokeOnly") - ? "COMPANION_CLIENT_PASS: focused drawers, equipment gestures and cross-machine" - + " restoration" - : "COMPANION_CLIENT_PASS: all five menus rendered and received their values"); + MMDLib.LOGGER.info(Boolean.getBoolean("mmdlib.drawerSmokeOnly") + ? "COMPANION_CLIENT_PASS: focused drawers, equipment gestures and cross-machine" + + " restoration" + : "COMPANION_CLIENT_PASS: all five menus rendered and received their values"); mc.stop(); return; } opening = true; waitingTicks = 0; int current = stage; - mc.getSingleplayerServer() - .execute( - () -> { - try { - var player = mc.getSingleplayerServer().getPlayerList().getPlayers() - .getFirst(); - var level = player.serverLevel(); - player.closeContainer(); - if (current == 0 && BackpackFixture.installed()) { - BackpackFixture.equip(player); - CuriosFixture.setup(player); - } - level.getChunkAt(POS); - level.setBlock( - POS.offset(0, 0, 2), - net.minecraft.world.level.block.Blocks.STONE.defaultBlockState(), + mc.getSingleplayerServer().execute(() -> { + try { + var player = mc.getSingleplayerServer().getPlayerList() + .getPlayers().getFirst(); + var level = player.serverLevel(); + player.closeContainer(); + if (current == 0 && BackpackFixture.installed()) { + BackpackFixture.equip(player); + CuriosFixture.setup(player); + } + level.getChunkAt(POS); + level.setBlock(POS.offset(0, 0, 2), + net.minecraft.world.level.block.Blocks.STONE + .defaultBlockState(), + 3); + level.setBlock(POS, CompanionMod.MACHINES.get(current).block + .get().defaultBlockState(), 3); + if (current == 3) { + // Player stands south of both machines: the western + // neighbour is on + // the left. + level.setBlock(POS, + CompanionMod.CREATIVE_POWER.block.get() + .defaultBlockState().setValue( + zone.moddev.mc.companion.DemoBlock.FACING, + net.minecraft.core.Direction.SOUTH), + 3); + level.setBlock(POS.west(), + CompanionMod.CRUSHER.block.get().defaultBlockState() + .setValue( + zone.moddev.mc.companion.DemoBlock.FACING, + net.minecraft.core.Direction.SOUTH), + 3); + } + var entity = level.getBlockEntity(POS); + if (entity instanceof ProcessorEntity machine) { + machine.energy.setStored(current == 0 + ? 80_000 + : current == 1 ? 40_000 : 10_000); + if (current == 0) + machine.fluids.fill( + new FluidStack(Fluids.WATER, 2_500), + FluidAction.EXECUTE); + machine.items.insertItem(0, new ItemStack( + current == 1 ? Items.IRON_INGOT : Items.COPPER_ORE, + current == 1 ? 1 : 16), false); + for (int i = 0; i < 40; i++) + machine.serverTick(); + if (current == 4) { + level.setBlock(POS.north(), + net.minecraft.world.level.block.Blocks.CHEST + .defaultBlockState(), 3); - level.setBlock( - POS, - CompanionMod.MACHINES.get(current).block.get().defaultBlockState(), + level.setBlock(POS.west(), + net.minecraft.world.level.block.Blocks.FURNACE + .defaultBlockState(), + 3); + level.setBlock(POS.east(), + net.minecraft.world.level.block.Blocks.GLASS + .defaultBlockState(), + 3); + level.setBlock(POS.below(), + net.minecraft.world.level.block.Blocks.HOPPER + .defaultBlockState(), 3); - if (current == 3) { - // Player stands south of both machines: the western neighbour is on - // the left. - level.setBlock( - POS, - CompanionMod.CREATIVE_POWER.block - .get() - .defaultBlockState() - .setValue( - zone.moddev.mc.companion.DemoBlock.FACING, - net.minecraft.core.Direction.SOUTH), - 3); - level.setBlock( - POS.west(), - CompanionMod.CRUSHER.block - .get() - .defaultBlockState() - .setValue( - zone.moddev.mc.companion.DemoBlock.FACING, - net.minecraft.core.Direction.SOUTH), - 3); - } - var entity = level.getBlockEntity(POS); - if (entity instanceof ProcessorEntity machine) { - machine.energy.setStored( - current == 0 ? 80_000 : current == 1 ? 40_000 : 10_000); - if (current == 0) - machine.fluids.fill(new FluidStack(Fluids.WATER, 2_500), - FluidAction.EXECUTE); - machine.items.insertItem( - 0, - new ItemStack( - current == 1 ? Items.IRON_INGOT : Items.COPPER_ORE, - current == 1 ? 1 : 16), - false); - for (int i = 0; i < 40; i++) - machine.serverTick(); - if (current == 4) { - level.setBlock( - POS.north(), - net.minecraft.world.level.block.Blocks.CHEST - .defaultBlockState(), - 3); - level.setBlock( - POS.west(), - net.minecraft.world.level.block.Blocks.FURNACE - .defaultBlockState(), - 3); - level.setBlock( - POS.east(), - net.minecraft.world.level.block.Blocks.GLASS - .defaultBlockState(), - 3); - level.setBlock( - POS.below(), - net.minecraft.world.level.block.Blocks.HOPPER - .defaultBlockState(), - 3); - } - } - player.teleportTo(POS.getX() + 0.5, POS.getY() + 1, POS.getZ() + 2.5); - } catch (Throwable error) { - fail(error); } - }); + } + player.teleportTo(POS.getX() + 0.5, POS.getY() + 1, + POS.getZ() + 2.5); + } catch (Throwable error) { + fail(error); + } + }); } catch (Throwable error) { fail(error); } @@ -303,7 +272,8 @@ public static void tick(ClientTickEvent.Post event) { @SubscribeEvent public static void frame(RenderFrameEvent.Post event) { - if (!Boolean.getBoolean("mmdlib.companionSmoke") || finished || !opening) + if (!Boolean.getBoolean("mmdlib.companionSmoke") || finished + || !opening) return; var mc = Minecraft.getInstance(); if (!(mc.screen instanceof MachineScreen screen)) @@ -401,7 +371,8 @@ public static void frame(RenderFrameEvent.Post event) { if (Boolean.getBoolean("mmdlib.reservationSmokeOnly")) { waitingTicks = 0; frames = 0; - if (!ReservationClientSmoke.run(screen) || !SlotPolicyClientSmoke.run()) + if (!ReservationClientSmoke.run(screen) + || !SlotPolicyClientSmoke.run()) return; finished = true; MMDLib.LOGGER.info( @@ -478,8 +449,8 @@ public static void frame(RenderFrameEvent.Post event) { mc.stop(); return; } - if (stage == 0 - && (!CuriosClientSmoke.run(screen) || !BackpackClientSmoke.run(screen))) { + if (stage == 0 && (!CuriosClientSmoke.run(screen) + || !BackpackClientSmoke.run(screen))) { frames = 0; waitingTicks = 0; return; @@ -501,22 +472,25 @@ public static void frame(RenderFrameEvent.Post event) { } if (menu.components().get(BuiltinMenuComponents.AUTOMATION) != null && menu.bound.screen().playerInventory()) { - var row = screen.layout.region(PlayerInventoryElement.TOOLBAR).orElseThrow(); - var inventory = screen.layout.region(PlayerInventoryElement.INVENTORY) + var row = screen.layout.region(PlayerInventoryElement.TOOLBAR) .orElseThrow(); + var inventory = screen.layout + .region(PlayerInventoryElement.INVENTORY).orElseThrow(); if (row.x() != inventory.x()) - throw new AssertionError("Colours align with inventory left edge"); + throw new AssertionError( + "Colours align with inventory left edge"); var firstStorage = PlayerInventoryElement.slot(inventory, 9); var lastStorage = PlayerInventoryElement.slot(inventory, 17); - if (row.y() != inventory.y() || row.bottom() + 4 != firstStorage.y()) + if (row.y() != inventory.y() + || row.bottom() + 4 != firstStorage.y()) throw new AssertionError( "Toolbar shares the helmet row directly above inventory slots"); - for (String id : new String[]{"redstone", "toggle_0", "toggle_1"}) { + for (String id : new String[]{"redstone", "toggle_0", + "toggle_1"}) { var button = menu.bound.screen().find(id); if (button.isPresent()) { var rect = screen.layout.bounds(button.get()); - if (rect.y() != row.y() - || rect.x() <= row.right() + if (rect.y() != row.y() || rect.x() <= row.right() || rect.right() > lastStorage.right()) throw new AssertionError( "Colour selectors and machine controls share one non-overlapping row"); @@ -527,12 +501,13 @@ public static void frame(RenderFrameEvent.Post event) { if (!(energy instanceof zone.moddev.mc.lib.gui.model.elements.EnergyGaugeElement)) throw new AssertionError( "Every companion screen must use the reusable battery gauge"); - if (screen.layout.bounds(energy).right() != screen.layout.panel().width() - 7) - throw new AssertionError("Energy must sit against the right panel margin"); + if (screen.layout.bounds(energy) + .right() != screen.layout.panel().width() - 7) + throw new AssertionError( + "Energy must sit against the right panel margin"); if (!(menu.value(energy, false) instanceof Number)) throw new AssertionError("Energy was not synced"); - if (stage == 0 - && !redstoneSmokeDone + if (stage == 0 && !redstoneSmokeDone && !(redstoneSmokeDone = RedstoneClientSmoke.run(screen))) { frames = 0; waitingTicks = 0; @@ -562,7 +537,8 @@ public static void frame(RenderFrameEvent.Post event) { return; boolean paused = state .orElseThrow() == zone.moddev.mc.lib.gui.model.ProcessorButtonState.PAUSED; - if (controlStage == 0 && !paused || controlStage == 1 && paused) { + if (controlStage == 0 && !paused + || controlStage == 1 && paused) { var bounds = screen.bounds(dial); screen.mouseClicked(bounds.x() + 10, bounds.y() + 10, 0); screen.mouseReleased(bounds.x() + 10, bounds.y() + 10, 0); @@ -577,13 +553,18 @@ public static void frame(RenderFrameEvent.Post event) { waitingTicks = 0; return; } - // The empty-tank/empty-energy screenshot intentionally leaves the crusher idle. + // The empty-tank/empty-energy screenshot intentionally leaves the + // crusher idle. if (stage < 3 && !(stage == 0 && fluidStage == 5)) { - var progress = menu.bound.screen().find("progress").orElseThrow(); - if (!(menu.value(progress, true) instanceof Number n) || n.intValue() <= 0) - // Resume reserves the cycle on a later server tick. An idle snapshot between + var progress = menu.bound.screen().find("progress") + .orElseThrow(); + if (!(menu.value(progress, true) instanceof Number n) + || n.intValue() <= 0) + // Resume reserves the cycle on a later server tick. An idle + // snapshot between // cycles - // is also valid; render frames are not a barrier for server ticks or network + // is also valid; render frames are not a barrier for server + // ticks or network // updates. return; if (menu.getSlot(0).getItem().isEmpty()) @@ -592,7 +573,9 @@ public static void frame(RenderFrameEvent.Post event) { String name = menu.bound.screen().id().getPath(); if (stage == 0) { var tank = menu.bound.screen().find("tank").orElseThrow(); - var expectedFluid = fluidStage == 0 ? Fluids.WATER : Fluids.LAVA; + var expectedFluid = fluidStage == 0 + ? Fluids.WATER + : Fluids.LAVA; int expectedAmount = fluidStage == 0 ? 2_500 : fluidStage == 3 ? 500 : fluidStage == 5 ? 0 : 1_500; @@ -601,30 +584,32 @@ public static void frame(RenderFrameEvent.Post event) { || fluid.getAmount() != expectedAmount) { if (fluidStage > 0) return; // Wait for fluid changes to reach the menu. - throw new AssertionError("Water was not synced into the tank gauge"); + throw new AssertionError( + "Water was not synced into the tank gauge"); } if (!(menu.value(tank, true) instanceof Number capacity) || capacity.intValue() != 4_000) throw new AssertionError("Tank capacity was not synced"); if (fluidStage == 1) { - Screenshot.grab( - mc.gameDirectory, "crusher-lava.png", mc.getMainRenderTarget(), message -> { + Screenshot.grab(mc.gameDirectory, "crusher-lava.png", + mc.getMainRenderTarget(), message -> { }); fluidStage = 2; frames = 0; - mc.getSingleplayerServer() - .execute( - () -> { - var player = mc.getSingleplayerServer().getPlayerList().getPlayers() - .getFirst(); - player.getAbilities().instabuild = false; - player.containerMenu.setCarried(new ItemStack(Items.BUCKET)); - player.containerMenu.broadcastChanges(); - }); + mc.getSingleplayerServer().execute(() -> { + var player = mc.getSingleplayerServer().getPlayerList() + .getPlayers().getFirst(); + player.getAbilities().instabuild = false; + player.containerMenu + .setCarried(new ItemStack(Items.BUCKET)); + player.containerMenu.broadcastChanges(); + }); return; } if (fluidStage == 2 || fluidStage == 3) { - var expectedItem = fluidStage == 2 ? Items.BUCKET : Items.LAVA_BUCKET; + var expectedItem = fluidStage == 2 + ? Items.BUCKET + : Items.LAVA_BUCKET; if (!menu.getCarried().is(expectedItem)) return; var bounds = screen.bounds(tank); @@ -639,34 +624,32 @@ public static void frame(RenderFrameEvent.Post event) { if (fluidStage == 4) { if (!menu.getCarried().is(Items.BUCKET)) return; - Screenshot.grab( - mc.gameDirectory, "crusher-container.png", mc.getMainRenderTarget(), - message -> { + Screenshot.grab(mc.gameDirectory, "crusher-container.png", + mc.getMainRenderTarget(), message -> { }); fluidStage = 5; frames = 0; - mc.getSingleplayerServer() - .execute( - () -> { - var player = mc.getSingleplayerServer().getPlayerList().getPlayers() - .getFirst(); - var machine = (ProcessorEntity) player.serverLevel() - .getBlockEntity(POS); - machine.fluids.drain(Integer.MAX_VALUE, FluidAction.EXECUTE); - machine.energy.setStored(0); - player.containerMenu.setCarried(ItemStack.EMPTY); - player.getAbilities().instabuild = true; - player.containerMenu.broadcastChanges(); - }); + mc.getSingleplayerServer().execute(() -> { + var player = mc.getSingleplayerServer().getPlayerList() + .getPlayers().getFirst(); + var machine = (ProcessorEntity) player.serverLevel() + .getBlockEntity(POS); + machine.fluids.drain(Integer.MAX_VALUE, + FluidAction.EXECUTE); + machine.energy.setStored(0); + player.containerMenu.setCarried(ItemStack.EMPTY); + player.getAbilities().instabuild = true; + player.containerMenu.broadcastChanges(); + }); return; } if (fluidStage == 5) { if (!menu.getCarried().isEmpty() - || ((Number) menu.value(energy, false)).longValue() != 0) + || ((Number) menu.value(energy, false)) + .longValue() != 0) return; - Screenshot.grab( - mc.gameDirectory, "crusher-empty.png", mc.getMainRenderTarget(), - message -> { + Screenshot.grab(mc.gameDirectory, "crusher-empty.png", + mc.getMainRenderTarget(), message -> { }); frames = 0; stage++; @@ -676,28 +659,32 @@ public static void frame(RenderFrameEvent.Post event) { } } if (stage < 3) { - var info = (zone.moddev.mc.lib.gui.model.elements.PopoutElement) menu.bound.screen() - .find("info").orElseThrow(); + var info = (zone.moddev.mc.lib.gui.model.elements.PopoutElement) menu.bound + .screen().find("info").orElseThrow(); var tab = screen.bounds(info); - var drawer = screen.drawers().entries().stream() - .filter(candidate -> candidate.definition().element().orElse(null) == info) + var drawer = screen + .drawers().entries().stream().filter(candidate -> candidate + .definition().element().orElse(null) == info) .findFirst().orElseThrow(); if (!info.iconOrDefault().toString().equals("mmdlib:icon_info")) - throw new AssertionError("The information drawer must use the info icon"); + throw new AssertionError( + "The information drawer must use the info icon"); if (drawerStage == 0) { - Screenshot.grab(mc.gameDirectory, name + ".png", mc.getMainRenderTarget(), - message -> { + Screenshot.grab(mc.gameDirectory, name + ".png", + mc.getMainRenderTarget(), message -> { }); screen.mouseClicked(tab.x() + 10, tab.y() + 10, 0); var exclusions = drawer.exclusions(); if (!drawer.open() || exclusions.size() != 2 || !screen.exclusions().containsAll(exclusions) || exclusions.getFirst().right() != tab.x() - || !exclusions.get(1).contains(tab.x() + 10, tab.y() + 10)) + || !exclusions.get(1).contains(tab.x() + 10, + tab.y() + 10)) throw new AssertionError( "The drawer must extend left while retaining its icon tab"); if (exclusions.get(0).bottom() != exclusions.get(1).bottom() - || exclusions.get(1).bottom() > screen.panelBounds().bottom() - 4 + || exclusions.get(1) + .bottom() > screen.panelBounds().bottom() - 4 || exclusions.get(1).bottom() > screen.drawerBottom()) throw new AssertionError( "The info drawer must fit its content within the main panel and screen height" @@ -706,57 +693,64 @@ public static void frame(RenderFrameEvent.Post event) { frames = 0; return; } - Screenshot.grab( - mc.gameDirectory, name + "-drawer.png", mc.getMainRenderTarget(), message -> { + Screenshot.grab(mc.gameDirectory, name + "-drawer.png", + mc.getMainRenderTarget(), message -> { }); - // The compact panel puts equipment tabs at the bottom of the same rail. - // Exercise empty rail below the info tab, not another drawer's tab. + // The compact panel puts equipment tabs at the bottom of the + // same rail. + // Exercise empty rail below the info tab, not another drawer's + // tab. int railY = tab.bottom() + 1; // Gap before the Statistics tab. if (!screen.mouseClicked(tab.x() + 10, railY, 0)) throw new AssertionError( "Clicks on the drawer rail must not pass through the screen"); - // Vanilla returns true even for unused buttons. Observe its recorded press to + // Vanilla returns true even for unused buttons. Observe its + // recorded press to // verify - // these clicks reached inventory handling instead of being consumed by the drawer. + // these clicks reached inventory handling instead of being + // consumed by the drawer. var lastButton = net.minecraft.client.gui.screens.inventory.AbstractContainerScreen.class .getDeclaredField("lastClickButton"); lastButton.setAccessible(true); for (int button : new int[]{2, 3, 4}) - for (var point : new double[][]{ - {tab.x() + 10, railY}, - {tab.x() - 10, tab.y() + 10} - }) { + for (var point : new double[][]{{tab.x() + 10, railY}, + {tab.x() - 10, tab.y() + 10}}) { lastButton.setInt(screen, -1); screen.mouseClicked(point[0], point[1], button); if (lastButton.getInt(screen) != button) - throw new AssertionError("Drawer swallowed mouse button " + button); + throw new AssertionError( + "Drawer swallowed mouse button " + button); if (!drawer.open() || drawer.exclusions().size() != 2 - || !screen.exclusions().containsAll(drawer.exclusions())) - throw new AssertionError("Extra mouse button changed the drawer state"); + || !screen.exclusions() + .containsAll(drawer.exclusions())) + throw new AssertionError( + "Extra mouse button changed the drawer state"); } screen.mouseScrolled(tab.x() - 10, tab.y() + 10, 0, -1); screen.mouseClicked(tab.x() + 10, tab.y() + 10, 0); - if (drawer.open() || !drawer.exclusions().equals(java.util.List.of(tab)) + if (drawer.open() + || !drawer.exclusions().equals(java.util.List.of(tab)) || !screen.exclusions().contains(tab)) - throw new AssertionError("The icon tab did not close its drawer"); + throw new AssertionError( + "The icon tab did not close its drawer"); drawerStage = 0; if (stage == 0) { fluidStage = 1; frames = 0; - mc.getSingleplayerServer() - .execute( - () -> { - var machine = (ProcessorEntity) mc.getSingleplayerServer() - .overworld().getBlockEntity(POS); - machine.fluids.drain(Integer.MAX_VALUE, FluidAction.EXECUTE); - machine.fluids.fill(new FluidStack(Fluids.LAVA, 1_500), - FluidAction.EXECUTE); - }); + mc.getSingleplayerServer().execute(() -> { + var machine = (ProcessorEntity) mc + .getSingleplayerServer().overworld() + .getBlockEntity(POS); + machine.fluids.drain(Integer.MAX_VALUE, + FluidAction.EXECUTE); + machine.fluids.fill(new FluidStack(Fluids.LAVA, 1_500), + FluidAction.EXECUTE); + }); return; } } else { - Screenshot.grab(mc.gameDirectory, name + ".png", mc.getMainRenderTarget(), - message -> { + Screenshot.grab(mc.gameDirectory, name + ".png", + mc.getMainRenderTarget(), message -> { }); } frames = 0; @@ -774,37 +768,40 @@ private static boolean prepareControlFixture() { if (!controlFixtureRequested) { controlFixtureRequested = true; var server = Minecraft.getInstance().getSingleplayerServer(); - server.execute( - () -> { - try { - var machine = (ProcessorEntity) server.overworld().getBlockEntity(POS); - // Reproduce a slow inventory/widget prelude without sleeping: its original - // recipe - // inputs must be allowed to run out before we prepare the independent - // control phase. - for (int i = 0; i < 2_000; i++) - machine.serverTick(); - if (!machine.items.getStackInSlot(0).isEmpty()) - throw new AssertionError( - "Slow-prelude regression did not exhaust the original input"); - machine.lanes - .forEach(zone.moddev.mc.lib.machine.feature.RecipeProcessor::reset); - for (int i = 0; i < machine.items.getSlots(); i++) - machine.items.setStackInSlot(i, ItemStack.EMPTY); - machine.items.setStackInSlot(0, new ItemStack(Items.COPPER_ORE, 16)); - machine.energy.setStored(80_000); - for (int i = 0; i < 40; i++) - machine.serverTick(); - if (machine.lanes.getFirst().time() <= 0) - throw new AssertionError( - "Fresh control fixture did not start processing"); - server.getPlayerList().getPlayers().getFirst().containerMenu - .broadcastChanges(); - controlFixtureReady = true; - } catch (Throwable error) { - fail(error); - } - }); + server.execute(() -> { + try { + var machine = (ProcessorEntity) server.overworld() + .getBlockEntity(POS); + // Reproduce a slow inventory/widget prelude without + // sleeping: its original + // recipe + // inputs must be allowed to run out before we prepare the + // independent + // control phase. + for (int i = 0; i < 2_000; i++) + machine.serverTick(); + if (!machine.items.getStackInSlot(0).isEmpty()) + throw new AssertionError( + "Slow-prelude regression did not exhaust the original input"); + machine.lanes.forEach( + zone.moddev.mc.lib.machine.feature.RecipeProcessor::reset); + for (int i = 0; i < machine.items.getSlots(); i++) + machine.items.setStackInSlot(i, ItemStack.EMPTY); + machine.items.setStackInSlot(0, + new ItemStack(Items.COPPER_ORE, 16)); + machine.energy.setStored(80_000); + for (int i = 0; i < 40; i++) + machine.serverTick(); + if (machine.lanes.getFirst().time() <= 0) + throw new AssertionError( + "Fresh control fixture did not start processing"); + server.getPlayerList().getPlayers().getFirst().containerMenu + .broadcastChanges(); + controlFixtureReady = true; + } catch (Throwable error) { + fail(error); + } + }); } return false; } diff --git a/src/companionTest/java/zone/moddev/mc/companion/test/CompanionGameTests.java b/src/companionTest/java/zone/moddev/mc/companion/test/CompanionGameTests.java index 90cb1527..ce8a9594 100644 --- a/src/companionTest/java/zone/moddev/mc/companion/test/CompanionGameTests.java +++ b/src/companionTest/java/zone/moddev/mc/companion/test/CompanionGameTests.java @@ -33,29 +33,31 @@ import zone.moddev.mc.lib.gui.runtime.component.BuiltinMenuComponents; import zone.moddev.mc.lib.machine.feature.RecipeProcessor.Status; -/** Exercises the consumer's registration and wiring with the real recipe files and block types. */ +/** + * Exercises the consumer's registration and wiring with the real recipe files + * and block types. + */ @GameTestHolder(CompanionMod.ID) @PrefixGameTestTemplate(false) public final class CompanionGameTests { private static final BlockPos CENTER = new BlockPos(2, 2, 2); @GameTest(template = "empty") - public static void poweredBasePreservesSavesAndSeparatesFeatureUpdates(GameTestHelper helper) { + public static void poweredBasePreservesSavesAndSeparatesFeatureUpdates( + GameTestHelper helper) { var level = helper.getLevel(); var registries = level.registryAccess(); var pos = helper.absolutePos(CENTER); - for (var definition : java.util.List.of( - CompanionMod.CRUSHER, - CompanionMod.TRANSMUTER, - CompanionMod.DUAL_CRUSHER, - CompanionMod.LOGISTICS, - CompanionMod.CREATIVE_POWER)) { + for (var definition : java.util.List.of(CompanionMod.CRUSHER, + CompanionMod.TRANSMUTER, CompanionMod.DUAL_CRUSHER, + CompanionMod.LOGISTICS, CompanionMod.CREATIVE_POWER)) { level.setBlock(pos, definition.block.get().defaultBlockState(), 3); var machine = (zone.moddev.mc.lib.machine.neoforge.BasePoweredMachine) level .getBlockEntity(pos); machine.energy.setStored(7_321); require( - machine.energy.stored() == 7_321 && machine.energy.capacity() == 100_000, + machine.energy.stored() == 7_321 + && machine.energy.capacity() == 100_000, "The owned buffer preserves its full values"); var bound = Screens.get(definition.type.getId()).orElseThrow(); var gauge = bound.screen().find("energy").orElseThrow(); @@ -72,7 +74,8 @@ public static void poweredBasePreservesSavesAndSeparatesFeatureUpdates(GameTestH && machine.screenStats().getFirst() == machine.energy, "Screen features and statistics use the same owned parts"); if (machine instanceof ProcessorEntity processor) { - processor.items.insertItem(0, new ItemStack(Items.IRON_INGOT), false); + processor.items.insertItem(0, new ItemStack(Items.IRON_INGOT), + false); processor.setReservePercent(25); processor.features.setProcessorPaused("lane_0", true); processor.redstone.migrate(1); @@ -84,7 +87,8 @@ public static void poweredBasePreservesSavesAndSeparatesFeatureUpdates(GameTestH "Power survives a full save/load for every machine"); if (copy instanceof ProcessorEntity processor) { require( - processor.getPaused() && processor.getReservePercent() == 25, + processor.getPaused() + && processor.getReservePercent() == 25, "Base feature state and subclass metadata both survive loading"); require( processor.redstone.save(registries) @@ -92,21 +96,19 @@ public static void poweredBasePreservesSavesAndSeparatesFeatureUpdates(GameTestH "Redstone configuration survives loading"); } var beforeUpdate = copy.saveWithFullMetadata(registries); - copy.loadWithComponents(machine.getUpdateTag(registries), registries); + copy.loadWithComponents(machine.getUpdateTag(registries), + registries); var afterUpdate = copy.saveWithFullMetadata(registries); - for (var key : java.util.List.of( - "energy", - "items", - "fluids", - "automation", - "redstone_control", - "machine_name", + for (var key : java.util.List.of("energy", "items", "fluids", + "automation", "redstone_control", "machine_name", "reserve_percent")) require( - java.util.Objects.equals(beforeUpdate.get(key), afterUpdate.get(key)), + java.util.Objects.equals(beforeUpdate.get(key), + afterUpdate.get(key)), "Feature update preserves " + key); copy.clearContent(); - require(copy.energy.stored() == 0, "Shared clear resets owned power"); + require(copy.energy.stored() == 0, + "Shared clear resets owned power"); machine.clearContent(); level.setBlock(pos, Blocks.AIR.defaultBlockState(), 3); } @@ -115,8 +117,7 @@ public static void poweredBasePreservesSavesAndSeparatesFeatureUpdates(GameTestH @GameTest(template = "empty") public static void keylessProcessorButtonSamplesAndTogglesTheFirstProcessor( - GameTestHelper helper) - throws ReflectiveOperationException { + GameTestHelper helper) throws ReflectiveOperationException { var machine = place(helper, CompanionMod.TRANSMUTER); var player = helper.makeMockServerPlayerInLevel(); var pos = machine.getBlockPos(); @@ -125,33 +126,37 @@ public static void keylessProcessorButtonSamplesAndTogglesTheFirstProcessor( player.containerMenu = menu; var button = (zone.moddev.mc.lib.gui.model.elements.ProcessorButtonElement) menu.bound .screen().find("toggle_0").orElseThrow(); - require(button.processorKey().isEmpty(), "Inherited control uses no machine-specific key"); - // Mock players have no negotiated GUI channel; inspect the actual server snapshot directly. - var read = Class.forName("zone.moddev.mc.lib.gui.runtime.ScreenSnapshots") - .getDeclaredMethod( - "read", BlockEntity.class, zone.moddev.mc.lib.gui.load.ScreenBinder.Bound.class); + require(button.processorKey().isEmpty(), + "Inherited control uses no machine-specific key"); + // Mock players have no negotiated GUI channel; inspect the actual + // server snapshot directly. + var read = Class + .forName("zone.moddev.mc.lib.gui.runtime.ScreenSnapshots") + .getDeclaredMethod("read", BlockEntity.class, + zone.moddev.mc.lib.gui.load.ScreenBinder.Bound.class); read.setAccessible(true); - var snapshot = (java.util.Map) read.invoke(null, machine, menu.bound); + var snapshot = (java.util.Map) read.invoke(null, machine, + menu.bound); require( - zone.moddev.mc.lib.gui.model.ProcessorButtonState.IDLE - .wireValue() + zone.moddev.mc.lib.gui.model.ProcessorButtonState.IDLE.wireValue() .equals(snapshot.get(menu.index(button) * 2)), "Keyless control samples the first processor"); - var action = new ScreenNetwork.Action(43, menu.index(button), 0, 0, 0, 0); + var action = new ScreenNetwork.Action(43, menu.index(button), 0, 0, 0, + 0); menu.action(player, action); snapshot = (java.util.Map) read.invoke(null, machine, menu.bound); require( machine.lanes.getFirst().paused() && zone.moddev.mc.lib.gui.model.ProcessorButtonState.PAUSED - .wireValue() - .equals(snapshot.get(menu.index(button) * 2)), + .wireValue().equals(snapshot.get(menu.index(button) * 2)), "Keyless click pauses and synchronizes the same processor"); menu.action(player, action); - require(!machine.lanes.getFirst().paused(), "Keyless click resumes the same processor"); + require(!machine.lanes.getFirst().paused(), + "Keyless click resumes the same processor"); var dual = place(helper, CompanionMod.DUAL_CRUSHER); - var features = new zone.moddev.mc.lib.machine.feature.FeatureHost(() -> { - }) - .add( + var features = new zone.moddev.mc.lib.machine.feature.FeatureHost( + () -> { + }).add( new zone.moddev.mc.lib.machine.feature.MachineFeature() { public String key() { return "auxiliary"; @@ -168,15 +173,16 @@ public Integer state() { public void restore(Integer value) { } }) - .add(dual.lanes.get(1)) - .add(dual.lanes.getFirst()); + .add(dual.lanes.get(1)).add(dual.lanes.getFirst()); require( - features.processor(java.util.Optional.empty()).orElseThrow() == dual.lanes.get(1), + features.processor(java.util.Optional.empty()) + .orElseThrow() == dual.lanes.get(1), "Default is the first registered recipe processor, skipping other features"); require( - features.processor(java.util.Optional.of("lane_0")).orElseThrow() == dual.lanes - .getFirst() - && features.processor(java.util.Optional.of("missing")).isEmpty(), + features.processor(java.util.Optional.of("lane_0")) + .orElseThrow() == dual.lanes.getFirst() + && features.processor(java.util.Optional.of("missing")) + .isEmpty(), "Explicit keys never fall back to the first processor"); var emptyHost = new zone.moddev.mc.lib.gui.runtime.ScreenHost() { public net.neoforged.neoforge.items.IItemHandlerModifiable screenItems() { @@ -184,8 +190,9 @@ public net.neoforged.neoforge.items.IItemHandlerModifiable screenItems() { } public zone.moddev.mc.lib.machine.feature.FeatureHost screenFeatures() { - return new zone.moddev.mc.lib.machine.feature.FeatureHost(() -> { - }); + return new zone.moddev.mc.lib.machine.feature.FeatureHost( + () -> { + }); } public zone.moddev.mc.lib.api.capabilities.EnergyBuffer screenEnergy() { @@ -193,9 +200,10 @@ public zone.moddev.mc.lib.api.capabilities.EnergyBuffer screenEnergy() { } }; try { - zone.moddev.mc.lib.gui.runtime.ScreenInventoryValidation.validate( - menu.bound.screen(), emptyHost); - throw new AssertionError("A keyless processor control requires a processor"); + zone.moddev.mc.lib.gui.runtime.ScreenInventoryValidation + .validate(menu.bound.screen(), emptyHost); + throw new AssertionError( + "A keyless processor control requires a processor"); } catch (zone.moddev.mc.lib.gui.load.ScreenException expected) { require( expected.getMessage().contains("at least one recipe processor"), @@ -206,7 +214,8 @@ public zone.moddev.mc.lib.api.capabilities.EnergyBuffer screenEnergy() { } @GameTest(template = "empty") - public static void processorActivityTracksWorkAndHostGates(GameTestHelper helper) { + public static void processorActivityTracksWorkAndHostGates( + GameTestHelper helper) { var machine = place(helper, CompanionMod.CRUSHER); var processor = machine.lanes.getFirst(); require(!processor.paused() && !processor.processing(), @@ -214,40 +223,42 @@ public static void processorActivityTracksWorkAndHostGates(GameTestHelper helper machine.items.setStackInSlot(0, new ItemStack(Items.COPPER_ORE, 16)); machine.energy.setStored(100000); tick(machine, 2); - require(processor.processing(), "Available work and energy start processing"); + require(processor.processing(), + "Available work and energy start processing"); machine.energy.setStored(0); machine.serverTick(); - require( - !processor.processing() && !processor.paused(), + require(!processor.processing() && !processor.paused(), "An energy stall stops work without manually pausing"); - require( - zone.moddev.mc.lib.gui.model.ProcessorButtonState - .of(processor) == zone.moddev.mc.lib.gui.model.ProcessorButtonState.STALLED, + require(zone.moddev.mc.lib.gui.model.ProcessorButtonState.of( + processor) == zone.moddev.mc.lib.gui.model.ProcessorButtonState.STALLED, "The processor control shows the energy stall instead of idle"); machine.energy.setStored(100000); machine.serverTick(); machine.setReservePercent(100); machine.serverTick(); - require( - !processor.processing() && !processor.paused(), + require(!processor.processing() && !processor.paused(), "An energy-reserve hold is not active processing"); machine.setReservePercent(0); machine.serverTick(); - machine.redstone.mode(0, zone.moddev.mc.lib.api.redstone.RedstoneControl.Mode.READ); + machine.redstone.mode(0, + zone.moddev.mc.lib.api.redstone.RedstoneControl.Mode.READ); machine.redstone.range(0, 0, 15); machine.serverTick(); require( - !processor.processing() && !processor.paused() && machine.items.hasPlan(0), + !processor.processing() && !processor.paused() + && machine.items.hasPlan(0), "Redstone holds work without manually pausing or releasing its reservation"); processor.pause(); - require(processor.paused() && !processor.processing(), "Manual pause has its own state"); - require( - !machine.items.hasPlan(0) && processor.progress() > 0, + require(processor.paused() && !processor.processing(), + "Manual pause has its own state"); + require(!machine.items.hasPlan(0) && processor.progress() > 0, "Manual pause releases reservations but keeps progress as the tooltip promises"); processor.resume(); - machine.redstone.mode(0, zone.moddev.mc.lib.api.redstone.RedstoneControl.Mode.IGNORE); + machine.redstone.mode(0, + zone.moddev.mc.lib.api.redstone.RedstoneControl.Mode.IGNORE); machine.serverTick(); - for (int ticks = 0; ticks < 200 && processor.progress() < processor.time() - 1; ticks++) + for (int ticks = 0; ticks < 200 + && processor.progress() < processor.time() - 1; ticks++) machine.serverTick(); require(processor.progress() == processor.time() - 1, "Resumed work reaches its final tick"); @@ -259,7 +270,8 @@ public static void processorActivityTracksWorkAndHostGates(GameTestHelper helper } @GameTest(template = "empty") - public static void genericProcessorButtonsRespectRedstoneAndLaneKeys(GameTestHelper helper) { + public static void genericProcessorButtonsRespectRedstoneAndLaneKeys( + GameTestHelper helper) { var machine = place(helper, CompanionMod.DUAL_CRUSHER); var player = helper.makeMockServerPlayerInLevel(); var pos = machine.getBlockPos(); @@ -269,13 +281,16 @@ public static void genericProcessorButtonsRespectRedstoneAndLaneKeys(GameTestHel var button = (zone.moddev.mc.lib.gui.model.elements.ProcessorButtonElement) menu.bound .screen().find("toggle_1").orElseThrow(); require( - button.processorKey().orElseThrow().equals("lane_1") && !button.common().clicks().any(), + button.processorKey().orElseThrow().equals("lane_1") + && !button.common().clicks().any(), "The generic control targets a stable key without callbacks"); int index = menu.index(button); menu.action(player, new ScreenNetwork.Action(42, index, 0, 1, 0, 0)); - require(!machine.lanes.get(1).paused(), "Right click cannot toggle the processor"); + require(!machine.lanes.get(1).paused(), + "Right click cannot toggle the processor"); menu.action(player, new ScreenNetwork.Action(42, index, 0, 0, 0, 1)); - require(!machine.lanes.get(1).paused(), "Invalid options cannot toggle the processor"); + require(!machine.lanes.get(1).paused(), + "Invalid options cannot toggle the processor"); var action = new ScreenNetwork.Action(42, index, 0, 0, 0, 0); menu.action(player, action); require( @@ -283,68 +298,70 @@ public static void genericProcessorButtonsRespectRedstoneAndLaneKeys(GameTestHel "Only the keyed lane pauses"); require( !menu.components().get(BuiltinMenuComponents.REDSTONE).watching() - && !menu.components().get(BuiltinMenuComponents.REDSTONE).conflicts(true), + && !menu.components().get(BuiltinMenuComponents.REDSTONE) + .conflicts(true), "No input watching means no conflict indicator"); - machine.redstone.mode(0, zone.moddev.mc.lib.api.redstone.RedstoneControl.Mode.EMIT); + machine.redstone.mode(0, + zone.moddev.mc.lib.api.redstone.RedstoneControl.Mode.EMIT); require( !menu.components().get(BuiltinMenuComponents.REDSTONE).watching(), "Output-only redstone does not watch inputs"); - machine.redstone.mode(0, zone.moddev.mc.lib.api.redstone.RedstoneControl.Mode.READ); + machine.redstone.mode(0, + zone.moddev.mc.lib.api.redstone.RedstoneControl.Mode.READ); machine.redstone.range(0, 0, 15); machine.redstone.readInputs(); - require( - menu.components().get(BuiltinMenuComponents.REDSTONE).watching() - && menu.components().get(BuiltinMenuComponents.REDSTONE).paused() - && !menu.components().get(BuiltinMenuComponents.REDSTONE).conflicts(true) - && menu.components().get(BuiltinMenuComponents.REDSTONE).conflicts(false), + require(menu.components().get(BuiltinMenuComponents.REDSTONE).watching() + && menu.components().get(BuiltinMenuComponents.REDSTONE).paused() + && !menu.components().get(BuiltinMenuComponents.REDSTONE) + .conflicts(true) + && menu.components().get(BuiltinMenuComponents.REDSTONE) + .conflicts(false), "A redstone hold conflicts only with an enabled manual state"); menu.action(player, action); require( - !machine.lanes.get(1).paused() - && menu.components().get(BuiltinMenuComponents.REDSTONE).paused(), + !machine.lanes.get(1).paused() && menu.components() + .get(BuiltinMenuComponents.REDSTONE).paused(), "Resume changes the manual state while redstone still holds processing"); machine.redstone.outside(0, true); machine.redstone.readInputs(); require( !menu.components().get(BuiltinMenuComponents.REDSTONE).paused() - && menu.components().get(BuiltinMenuComponents.REDSTONE).conflicts(true) - && !menu.components().get(BuiltinMenuComponents.REDSTONE).conflicts(false), + && menu.components().get(BuiltinMenuComponents.REDSTONE) + .conflicts(true) + && !menu.components().get(BuiltinMenuComponents.REDSTONE) + .conflicts(false), "Redstone allowing work conflicts with manual pause"); int[] changes = {0}, syncs = {0}; - var host = new zone.moddev.mc.lib.machine.feature.FeatureHost(() -> changes[0]++, - () -> syncs[0]++) - .add(machine.lanes.get(1)); - require( - host.setProcessorPaused("lane_1", true) && changes[0] == 1 && syncs[0] == 1, - "Manual changes persist and sync without a processing tick"); + var host = new zone.moddev.mc.lib.machine.feature.FeatureHost( + () -> changes[0]++, () -> syncs[0]++).add(machine.lanes.get(1)); require( - !host.setProcessorPaused("lane_1", true) - && !host.setProcessorPaused("missing", true) - && changes[0] == 1 + host.setProcessorPaused("lane_1", true) && changes[0] == 1 && syncs[0] == 1, - "Repeated states and unknown keys are inert"); + "Manual changes persist and sync without a processing tick"); + require(!host.setProcessorPaused("lane_1", true) + && !host.setProcessorPaused("missing", true) && changes[0] == 1 + && syncs[0] == 1, "Repeated states and unknown keys are inert"); machine.lanes.get(1).pause(); machine.lanes.get(1).setPaused(false); - require(!machine.lanes.get(1).paused(), "The public pause setter resumes too"); + require(!machine.lanes.get(1).paused(), + "The public pause setter resumes too"); var invalid = new zone.moddev.mc.lib.gui.model.elements.ProcessorButtonElement( - button.common(), java.util.Optional.of("missing"), button.text(), button.flat()); + button.common(), java.util.Optional.of("missing"), button.text(), + button.flat()); var original = menu.bound.screen(); var invalidScreen = new zone.moddev.mc.lib.gui.model.Screen( - original.id(), - original.source(), - original.title(), - original.blockEntity(), - original.playerInventory(), - java.util.List.of(invalid), - java.util.List.of()); + original.id(), original.source(), original.title(), + original.blockEntity(), original.playerInventory(), + java.util.List.of(invalid), java.util.List.of()); try { - zone.moddev.mc.lib.gui.runtime.ScreenInventoryValidation.validate(invalidScreen, - machine); + zone.moddev.mc.lib.gui.runtime.ScreenInventoryValidation + .validate(invalidScreen, machine); throw new AssertionError("Unknown processor key accepted"); } catch (zone.moddev.mc.lib.gui.load.ScreenException expected) { require( - expected.getMessage().contains("Unknown processor-key: missing"), + expected.getMessage() + .contains("Unknown processor-key: missing"), "An invalid control identifies its missing key"); } helper.succeed(); @@ -364,8 +381,7 @@ public static void heldProcessorsSaveSyncAndResumeWithoutReleasingReservations( var changes = new java.util.concurrent.atomic.AtomicInteger(); var syncs = new java.util.concurrent.atomic.AtomicInteger(); var host = new zone.moddev.mc.lib.machine.feature.FeatureHost( - changes::incrementAndGet, syncs::incrementAndGet) - .add(lane); + changes::incrementAndGet, syncs::incrementAndGet).add(lane); host.tick(helper.getLevel(), false); require(lane.status() == Status.HELD && !lane.paused(), "Host hold differs from manual pause"); @@ -380,18 +396,20 @@ public static void heldProcessorsSaveSyncAndResumeWithoutReleasingReservations( "Hold retains progress and reservations"); for (int i = 0; i < 20; i++) host.tick(helper.getLevel(), false); - require( - changes.get() == 1 && syncs.get() == 1, + require(changes.get() == 1 && syncs.get() == 1, "Unchanged hold does not repeatedly save or sync"); host.tick(helper.getLevel(), true); require( - lane.status() == Status.RUNNING && lane.progress() == progress + 1 && syncs.get() == 2, + lane.status() == Status.RUNNING && lane.progress() == progress + 1 + && syncs.get() == 2, "Releasing hold resumes and synchronizes the running state"); machine.setReservePercent(100); machine.serverTick(); - require(lane.status() == Status.HELD, "Energy reserve gate reports held"); + require(lane.status() == Status.HELD, + "Energy reserve gate reports held"); machine.setReservePercent(0); - machine.redstone.mode(0, zone.moddev.mc.lib.api.redstone.RedstoneControl.Mode.READ); + machine.redstone.mode(0, + zone.moddev.mc.lib.api.redstone.RedstoneControl.Mode.READ); machine.redstone.range(0, 0, 15); machine.redstone.outside(0, false); machine.serverTick(); @@ -405,15 +423,14 @@ public static void heldProcessorsSaveSyncAndResumeWithoutReleasingReservations( } @GameTest(template = "empty") - public static void statisticsProvidersDescribeCurrentRecipes(GameTestHelper helper) { + public static void statisticsProvidersDescribeCurrentRecipes( + GameTestHelper helper) { var machine = place(helper, CompanionMod.DUAL_CRUSHER); var first = machine.lanes.getFirst(); - require( - machine.screenStats().size() == 1 + machine.lanes.size(), + require(machine.screenStats().size() == 1 + machine.lanes.size(), "Only energy and processors contribute statistics"); - require( - first.statistics().rows().get(1) - .value() instanceof zone.moddev.mc.lib.api.stats.Statistics.Text, + require(first.statistics().rows().get(1) + .value() instanceof zone.moddev.mc.lib.api.stats.Statistics.Text, "Idle processor maximum explicitly has no recipe"); machine.items.setStackInSlot(0, new ItemStack(Items.COPPER_ORE, 16)); machine.energy.setStored(100000); @@ -422,16 +439,18 @@ public static void statisticsProvidersDescribeCurrentRecipes(GameTestHelper help for (var lane : machine.lanes) { var section = lane.statistics(); require( - section.title().equals(lane.displayName()) && section.rows().size() == 2, + section.title().equals(lane.displayName()) + && section.rows().size() == 2, "Processor provides a named section with exactly two rows"); - var maximum = (zone.moddev.mc.lib.api.stats.Statistics.Rate) section.rows().get(1) - .value(); + var maximum = (zone.moddev.mc.lib.api.stats.Statistics.Rate) section + .rows().get(1).value(); require( - maximum.amount().equals(java.math.BigInteger.ONE) && maximum.ticks() == 100, + maximum.amount().equals(java.math.BigInteger.ONE) + && maximum.ticks() == 100, "Maximum throughput uses the current copper recipe's duration"); require( - section.maximumUses().size() == 1 - && section.maximumUses().getFirst().resource() == machine.energy, + section.maximumUses().size() == 1 && section.maximumUses() + .getFirst().resource() == machine.energy, "Demand is attributed to the backing buffer, not the group's wrapper"); peak += section.maximumUses().getFirst().perTick().longValueExact(); } @@ -442,12 +461,13 @@ public static void statisticsProvidersDescribeCurrentRecipes(GameTestHelper help && machine.energy.extracted().signum() == 0, "Recipe consumption is used RF, not output RF"); first.reset(); - require(first.statistics().maximumUses().isEmpty(), "Reset removes stale recipe demand"); - require( - first.statistics().rows().get(1) - .value() instanceof zone.moddev.mc.lib.api.stats.Statistics.Text, + require(first.statistics().maximumUses().isEmpty(), + "Reset removes stale recipe demand"); + require(first.statistics().rows().get(1) + .value() instanceof zone.moddev.mc.lib.api.stats.Statistics.Text, "Reset removes stale maximum cycle rate"); - helper.getLevel().setBlock(machine.getBlockPos(), Blocks.AIR.defaultBlockState(), 3); + helper.getLevel().setBlock(machine.getBlockPos(), + Blocks.AIR.defaultBlockState(), 3); helper.succeed(); } @@ -459,29 +479,28 @@ public static void elementStateIsPerMenuAndRejectionsRestorePredictedViews( var pos = machine.getBlockPos(); player.setPos(pos.getX() + 0.5, pos.getY(), pos.getZ() + 0.5); var server = new MachineMenu(55, player.getInventory(), machine); - var client = new MachineMenu(55, player.getInventory(), machine, server.widgetSession()); + var client = new MachineMenu(55, player.getInventory(), machine, + server.widgetSession()); var other = new MachineMenu(56, player.getInventory(), machine); player.containerMenu = server; var button = client.bound.screen().find("open_settings").orElseThrow(); var action = zone.moddev.mc.lib.gui.model.elements.ButtonElement.NAVIGATE; try { var invalid = client - .prepareElementAction( - button, action, new zone.moddev.mc.lib.gui.ElementActions.Click(0, 0, 1)) + .prepareElementAction(button, action, + new zone.moddev.mc.lib.gui.ElementActions.Click(0, 0, 1)) .orElseThrow(); client.navigate("panel", "settings"); var rejected = server.elementAction(player, invalid); - require( - !rejected.accepted() && !rejected.states().isEmpty(), + require(!rejected.accepted() && !rejected.states().isEmpty(), "Rejection includes authoritative element state"); client.receiveWidgetResult(rejected); - require( - client.views().get("panel").equals("machine"), + require(client.views().get("panel").equals("machine"), "Rejected prediction is corrected before the callback"); client.takeElementResult(button); var valid = client - .prepareElementAction( - button, action, new zone.moddev.mc.lib.gui.ElementActions.Click(0, 0, 0)) + .prepareElementAction(button, action, + new zone.moddev.mc.lib.gui.ElementActions.Click(0, 0, 0)) .orElseThrow(); var accepted = server.elementAction(player, valid); require(accepted.accepted(), "Valid navigation accepted"); @@ -493,16 +512,14 @@ public static void elementStateIsPerMenuAndRejectionsRestorePredictedViews( client.takeElementResult(button); var panel = client.bound.screen().find("panel").orElseThrow(); client - .prepareElementAction( - panel, + .prepareElementAction(panel, zone.moddev.mc.lib.gui.model.containers.ContextElement.BACK, new zone.moddev.mc.lib.gui.ElementActions.Click(0, 0, 0)) .orElseThrow(); client.navigate("panel", "machine"); for (int i = 0; i < 100; i++) client.tickWidgetActions(); - require( - client.views().get("panel").equals("settings"), + require(client.views().get("panel").equals("settings"), "Timeout restores the last confirmed view"); } finally { player.containerMenu = player.inventoryMenu; @@ -511,23 +528,18 @@ public static void elementStateIsPerMenuAndRejectionsRestorePredictedViews( helper.succeed(); } - private static ScreenNetwork.Widget widgetPacket( - MachineMenu menu, - String element, - int request, - zone.moddev.mc.lib.gui.ElementAction action, - T value) { - return new ScreenNetwork.Widget( - menu.containerId, - menu.widgetSession(), + private static ScreenNetwork.Widget widgetPacket(MachineMenu menu, + String element, int request, + zone.moddev.mc.lib.gui.ElementAction action, T value) { + return new ScreenNetwork.Widget(menu.containerId, menu.widgetSession(), menu.index(menu.bound.screen().find(element).orElseThrow()), - request, - action.id(), + request, action.id(), action.encode(menu.blockEntity.getLevel().registryAccess(), value)); } @GameTest(template = "empty") - public static void builtinElementsUseAuthorizedTypedActions(GameTestHelper helper) { + public static void builtinElementsUseAuthorizedTypedActions( + GameTestHelper helper) { var machine = place(helper, CompanionMod.CRUSHER); var player = helper.makeMockServerPlayerInLevel(); var pos = machine.getBlockPos(); @@ -539,50 +551,47 @@ public static void builtinElementsUseAuthorizedTypedActions(GameTestHelper helpe var click = new zone.moddev.mc.lib.gui.ElementActions.Click(0, 0, 0); try { require( - !menu.elementAction( - player, - widgetPacket( - menu, - "open_settings", - 1, - navigate, - new zone.moddev.mc.lib.gui.ElementActions.Click(0, 0, 1))) + !menu.elementAction(player, + widgetPacket(menu, "open_settings", 1, navigate, + new zone.moddev.mc.lib.gui.ElementActions.Click(0, 0, + 1))) .accepted(), "Navigation validates its own payload"); - var packet = widgetPacket(menu, "open_settings", 2, navigate, click); + var packet = widgetPacket(menu, "open_settings", 2, navigate, + click); require( menu.elementAction(player, packet).accepted() && menu.views().get("panel").equals("settings"), "Typed navigation selects its target view"); require(menu.elementAction(player, packet) == null, "Built-in actions cannot be replayed"); - require( - !menu.elementAction(player, widgetPacket(menu, "open_settings", 3, navigate, click)) - .accepted(), - "A hidden navigation button cannot act"); + require(!menu + .elementAction(player, + widgetPacket(menu, "open_settings", 3, navigate, click)) + .accepted(), "A hidden navigation button cannot act"); boolean paused = machine.getPaused(); require( - !menu.elementAction( - player, - widgetPacket( - menu, "progress", 4, zone.moddev.mc.lib.gui.ElementActions.CLICK, click)) - .accepted() - && machine.getPaused() == paused, + !menu + .elementAction(player, + widgetPacket(menu, "progress", 4, + zone.moddev.mc.lib.gui.ElementActions.CLICK, click)) + .accepted() && machine.getPaused() == paused, "A hidden common callback cannot mutate the host"); require( - menu.elementAction(player, widgetPacket(menu, "panel", 5, back, click)).accepted() + menu.elementAction(player, + widgetPacket(menu, "panel", 5, back, click)).accepted() && menu.views().get("panel").equals("machine"), "Context owns its typed back action"); require( - menu.elementAction( - player, - widgetPacket( - menu, "progress", 6, zone.moddev.mc.lib.gui.ElementActions.CLICK, click)) - .accepted() - && machine.getPaused() != paused, + menu.elementAction(player, + widgetPacket(menu, "progress", 6, + zone.moddev.mc.lib.gui.ElementActions.CLICK, click)) + .accepted() && machine.getPaused() != paused, "Generic click dispatch preserves common callbacks"); require( - !menu.elementAction(player, widgetPacket(menu, "progress", 7, navigate, click)) + !menu + .elementAction(player, + widgetPacket(menu, "progress", 7, navigate, click)) .accepted(), "A visible element cannot invoke another element type's action"); } finally { @@ -592,7 +601,8 @@ public static void builtinElementsUseAuthorizedTypedActions(GameTestHelper helpe } @GameTest(template = "empty") - public static void typedWidgetActionsAuthorizeBeforeMutation(GameTestHelper helper) { + public static void typedWidgetActionsAuthorizeBeforeMutation( + GameTestHelper helper) { var machine = place(helper, CompanionMod.CRUSHER); var player = helper.makeMockServerPlayerInLevel(); var pos = machine.getBlockPos(); @@ -601,22 +611,16 @@ public static void typedWidgetActionsAuthorizeBeforeMutation(GameTestHelper help player.containerMenu = menu; var rename = zone.moddev.mc.companion.CompanionWidgets.RENAME; try { - require( - !menu.widgetAction(player, widgetPacket(menu, "name_editor", 1, rename, "Hidden")) - .accepted(), - "Hidden widgets cannot mutate the machine"); + require(!menu + .widgetAction(player, + widgetPacket(menu, "name_editor", 1, rename, "Hidden")) + .accepted(), "Hidden widgets cannot mutate the machine"); menu.navigate("panel", "settings"); - var packet = widgetPacket(menu, "name_editor", 2, rename, "Copper crusher"); - require( - menu.widgetAction( - player, - new ScreenNetwork.Widget( - 51, - menu.widgetSession() + 1, - packet.element(), - 2, - rename.id(), - packet.data())) == null, + var packet = widgetPacket(menu, "name_editor", 2, rename, + "Copper crusher"); + require(menu.widgetAction(player, + new ScreenNetwork.Widget(51, menu.widgetSession() + 1, + packet.element(), 2, rename.id(), packet.data())) == null, "Old menu sessions are rejected"); var outsider = helper.makeMockServerPlayerInLevel(); require(menu.widgetAction(outsider, packet) == null, @@ -625,49 +629,41 @@ public static void typedWidgetActionsAuthorizeBeforeMutation(GameTestHelper help require( accepted.accepted() && ((net.minecraft.network.chat.Component) accepted.value()) - .getString() - .equals("Copper crusher"), + .getString().equals("Copper crusher"), "Accept response carries the value after the handler, in the same tick"); - require(menu.widgetAction(player, packet) == null, "Requests cannot be replayed"); + require(menu.widgetAction(player, packet) == null, + "Requests cannot be replayed"); var rejected = menu.widgetAction(player, widgetPacket(menu, "name_editor", 3, rename, " ")); require( - !rejected.accepted() - && machine.getMachineName().getString().equals("Copper crusher"), + !rejected.accepted() && machine.getMachineName().getString() + .equals("Copper crusher"), "Invalid text is rejected without changing the authoritative name"); var data = rename.encode(player.registryAccess(), "Malformed"); - require( - !menu.widgetAction( - player, - new ScreenNetwork.Widget( - 51, - menu.widgetSession(), - packet.element(), - 4, - rename.id(), + require(!menu + .widgetAction(player, + new ScreenNetwork.Widget(51, menu.widgetSession(), + packet.element(), 4, rename.id(), java.util.Arrays.copyOf(data, data.length + 1))) - .accepted(), - "Trailing data is rejected"); - require( - !menu.widgetAction( - player, - widgetPacket( - menu, - "name_editor", - 5, - zone.moddev.mc.companion.CompanionWidgets.RESERVE, - 50)) - .accepted(), - "Actions are bound to widget types"); + .accepted(), "Trailing data is rejected"); + require(!menu + .widgetAction(player, + widgetPacket(menu, "name_editor", 5, + zone.moddev.mc.companion.CompanionWidgets.RESERVE, 50)) + .accepted(), "Actions are bound to widget types"); player.setPos(pos.getX() + 100, pos.getY(), pos.getZ()); require( menu.widgetAction(player, - widgetPacket(menu, "name_editor", 6, rename, "Too far")) == null, + widgetPacket(menu, "name_editor", 6, rename, + "Too far")) == null, "Distance is checked before decoding"); - var saved = machine.saveWithFullMetadata(helper.getLevel().registryAccess()); - var copy = (ProcessorEntity) BlockEntity.loadStatic( - pos, machine.getBlockState(), saved, helper.getLevel().registryAccess()); - require(copy.getMachineName().getString().equals("Copper crusher"), "Name persists"); + var saved = machine + .saveWithFullMetadata(helper.getLevel().registryAccess()); + var copy = (ProcessorEntity) BlockEntity.loadStatic(pos, + machine.getBlockState(), saved, + helper.getLevel().registryAccess()); + require(copy.getMachineName().getString().equals("Copper crusher"), + "Name persists"); } finally { player.containerMenu = player.inventoryMenu; helper.getLevel().setBlock(pos, Blocks.AIR.defaultBlockState(), 3); @@ -688,111 +684,92 @@ public static void structuredWidgetActionsBoundValuesRateAndPendingEdits( var reserve = zone.moddev.mc.companion.CompanionWidgets.RESERVE; var lane = zone.moddev.mc.companion.CompanionWidgets.SET_LANE; try { - require( - !menu.widgetAction(player, widgetPacket(menu, "reserve_slider", 1, reserve, 101)) - .accepted(), - "Reserve range checked"); - require( - menu.widgetAction(player, widgetPacket(menu, "reserve_slider", 2, reserve, 25)) - .accepted(), - "Valid reserve accepted"); - require( - !menu.widgetAction( - player, - widgetPacket( - menu, - "lane_selector", - 3, - lane, - new zone.moddev.mc.companion.CompanionWidgets.LaneSetting(-1, true))) - .accepted(), - "Negative lane rejected"); - require( - !menu.widgetAction( - player, - widgetPacket( - menu, - "lane_selector", - 4, - lane, - new zone.moddev.mc.companion.CompanionWidgets.LaneSetting(1, true))) - .accepted(), - "Unconfigured lane rejected"); - require( - menu.widgetAction( - player, - widgetPacket( - menu, - "lane_selector", - 5, - lane, - new zone.moddev.mc.companion.CompanionWidgets.LaneSetting(0, true))) - .accepted() - && machine.getPaused(), + require(!menu + .widgetAction(player, + widgetPacket(menu, "reserve_slider", 1, reserve, 101)) + .accepted(), "Reserve range checked"); + require(menu + .widgetAction(player, + widgetPacket(menu, "reserve_slider", 2, reserve, 25)) + .accepted(), "Valid reserve accepted"); + require(!menu.widgetAction(player, + widgetPacket(menu, "lane_selector", 3, lane, + new zone.moddev.mc.companion.CompanionWidgets.LaneSetting( + -1, true))) + .accepted(), "Negative lane rejected"); + require(!menu.widgetAction(player, + widgetPacket(menu, "lane_selector", 4, lane, + new zone.moddev.mc.companion.CompanionWidgets.LaneSetting(1, + true))) + .accepted(), "Unconfigured lane rejected"); + require( + menu.widgetAction(player, widgetPacket(menu, "lane_selector", 5, + lane, + new zone.moddev.mc.companion.CompanionWidgets.LaneSetting(0, + true))) + .accepted() && machine.getPaused(), "Structured pause accepted"); require( - menu.widgetAction( - player, - widgetPacket( - menu, - "lane_selector", - 6, - lane, - new zone.moddev.mc.companion.CompanionWidgets.LaneSetting(0, false))) - .accepted() - && !machine.getPaused(), + menu.widgetAction(player, widgetPacket(menu, "lane_selector", 6, + lane, + new zone.moddev.mc.companion.CompanionWidgets.LaneSetting(0, + false))) + .accepted() && !machine.getPaused(), "Structured resume accepted"); - menu.widgetAction(player, widgetPacket(menu, "reserve_slider", 7, reserve, 25)); - menu.widgetAction(player, widgetPacket(menu, "reserve_slider", 8, reserve, 25)); + menu.widgetAction(player, + widgetPacket(menu, "reserve_slider", 7, reserve, 25)); + menu.widgetAction(player, + widgetPacket(menu, "reserve_slider", 8, reserve, 25)); var excess = menu.widgetAction(player, widgetPacket(menu, "reserve_slider", 9, reserve, 26)); require( - excess != null && !excess.accepted() && ((Number) excess.value()).intValue() == 25, + excess != null && !excess.accepted() + && ((Number) excess.value()).intValue() == 25, "Excess edits are immediately rejected with the last sent value"); require(machine.getReservePercent() == 25, "Rate-limited edits do not run their handler"); - var saved = machine.saveWithFullMetadata(helper.getLevel().registryAccess()); - var copy = (ProcessorEntity) BlockEntity.loadStatic( - pos, machine.getBlockState(), saved, helper.getLevel().registryAccess()); + var saved = machine + .saveWithFullMetadata(helper.getLevel().registryAccess()); + var copy = (ProcessorEntity) BlockEntity.loadStatic(pos, + machine.getBlockState(), saved, + helper.getLevel().registryAccess()); require(copy.getReservePercent() == 25, "Reserve persists"); - var name = (zone.moddev.mc.lib.gui.model.elements.CustomElement) menu.bound.screen() - .find("name_editor").orElseThrow(); + var name = (zone.moddev.mc.lib.gui.model.elements.CustomElement) menu.bound + .screen().find("name_editor").orElseThrow(); require( - menu.prepareWidgetAction( - name, zone.moddev.mc.companion.CompanionWidgets.RENAME, "x".repeat(33)) - .isEmpty(), + menu.prepareWidgetAction(name, + zone.moddev.mc.companion.CompanionWidgets.RENAME, + "x".repeat(33)).isEmpty(), "Codec bounds failures are refused without escaping into the client"); + require(menu + .prepareWidgetAction(name, + zone.moddev.mc.companion.CompanionWidgets.RENAME, null) + .isEmpty(), "Other codec runtime failures are also refused"); require( - menu.prepareWidgetAction(name, zone.moddev.mc.companion.CompanionWidgets.RENAME, - null) - .isEmpty(), - "Other codec runtime failures are also refused"); - require( - !menu.widgetPending(name) && menu.takeWidgetResult(name) == null, + !menu.widgetPending(name) + && menu.takeWidgetResult(name) == null, "Failed encoding leaves the widget ready for another edit"); - var pending = menu.prepareWidgetAction( - name, zone.moddev.mc.companion.CompanionWidgets.RENAME, "Pending") + var pending = menu + .prepareWidgetAction(name, + zone.moddev.mc.companion.CompanionWidgets.RENAME, "Pending") .orElseThrow(); - require(pending.request() == 1, "Failed encoding does not consume request numbers"); - require( - menu.prepareWidgetAction(name, zone.moddev.mc.companion.CompanionWidgets.RENAME, - "Second") - .isEmpty(), - "Only one in-flight edit per widget"); - menu.receiveWidgetResult( - new ScreenNetwork.WidgetResult( - 52, - menu.widgetSession() + 1, - pending.element(), - pending.request(), - true, - net.minecraft.network.chat.Component.literal("Stale"), - null)); - require(menu.widgetPending(name), "A stale response cannot settle a current edit"); + require(pending.request() == 1, + "Failed encoding does not consume request numbers"); + require(menu + .prepareWidgetAction(name, + zone.moddev.mc.companion.CompanionWidgets.RENAME, "Second") + .isEmpty(), "Only one in-flight edit per widget"); + menu.receiveWidgetResult(new ScreenNetwork.WidgetResult(52, + menu.widgetSession() + 1, pending.element(), pending.request(), + true, net.minecraft.network.chat.Component.literal("Stale"), + null)); + require(menu.widgetPending(name), + "A stale response cannot settle a current edit"); for (int i = 0; i < 100; i++) menu.tickWidgetActions(); require( - !menu.widgetPending(name) && !menu.takeWidgetResult(name).accepted(), + !menu.widgetPending(name) + && !menu.takeWidgetResult(name).accepted(), "Timed-out edits unlock for retry"); } finally { player.containerMenu = player.inventoryMenu; @@ -809,34 +786,40 @@ public static void widgetRejectionUnlocksImmediatelyAndAcknowledgmentsTrackDelta var pos = machine.getBlockPos(); player.setPos(pos.getX() + 0.5, pos.getY(), pos.getZ() + 0.5); var server = new MachineMenu(54, player.getInventory(), machine); - var client = new MachineMenu(54, player.getInventory(), machine, server.widgetSession()); + var client = new MachineMenu(54, player.getInventory(), machine, + server.widgetSession()); player.containerMenu = server; server.navigate("panel", "settings"); client.navigate("panel", "settings"); - var name = (zone.moddev.mc.lib.gui.model.elements.CustomElement) server.bound.screen() - .find("name_editor").orElseThrow(); + var name = (zone.moddev.mc.lib.gui.model.elements.CustomElement) server.bound + .screen().find("name_editor").orElseThrow(); int key = server.index(name) * 2; var rename = zone.moddev.mc.companion.CompanionWidgets.RENAME; var original = machine.getMachineName(); server.apply(java.util.Map.of(key, original)); client.apply(java.util.Map.of(key, original)); try { - var packet = client.prepareWidgetAction(name, rename, "Temporary").orElseThrow(); + var packet = client.prepareWidgetAction(name, rename, "Temporary") + .orElseThrow(); var result = server.widgetAction(player, packet); client.receiveWidgetResult(result); - require(client.takeWidgetResult(name).accepted(), "First edit accepted"); - // The machine changes back before a regular broadcast has observed Temporary. + require(client.takeWidgetResult(name).accepted(), + "First edit accepted"); + // The machine changes back before a regular broadcast has observed + // Temporary. machine.rename(original.getString()); var delta = zone.moddev.mc.lib.gui.runtime.ScreenValues.changed( - java.util.Map.of(key, server.wireValue(key)), java.util.Map.of(key, original), - false); - require(delta.containsKey(key), "Reversion must be sent after an acknowledgment"); + java.util.Map.of(key, server.wireValue(key)), + java.util.Map.of(key, original), false); + require(delta.containsKey(key), + "Reversion must be sent after an acknowledgment"); client.apply(delta); server.apply(delta); require(original.equals(client.value(name, false)), "Client converges to the reverted value"); for (int i = 2; i <= 9; i++) { - packet = client.prepareWidgetAction(name, rename, "Edit " + i).orElseThrow(); + packet = client.prepareWidgetAction(name, rename, "Edit " + i) + .orElseThrow(); result = server.widgetAction(player, packet); require(result != null && result.accepted() == (i <= 8), "Only eight edits may dispatch"); @@ -857,18 +840,22 @@ public static void widgetRejectionUnlocksImmediatelyAndAcknowledgmentsTrackDelta } @GameTest(template = "empty") - public static void elementActionSessionTimeoutRetryAndClose(GameTestHelper helper) { + public static void elementActionSessionTimeoutRetryAndClose( + GameTestHelper helper) { var machine = place(helper, CompanionMod.CRUSHER); var player = helper.makeMockServerPlayerInLevel(); var pos = machine.getBlockPos(); player.setPos(pos.getX() + .5, pos.getY(), pos.getZ() + .5); var server = new MachineMenu(55, player.getInventory(), machine); - var client = new MachineMenu(55, player.getInventory(), machine, server.sessionId()); + var client = new MachineMenu(55, player.getInventory(), machine, + server.sessionId()); player.containerMenu = server; server.navigate("panel", "settings"); client.navigate("panel", "settings"); - // Establish confirmed navigation just as the initial server snapshot does in a real menu. - client.apply(new java.util.HashMap(server.elementStates().snapshot())); + // Establish confirmed navigation just as the initial server snapshot + // does in a real menu. + client.apply(new java.util.HashMap( + server.elementStates().snapshot())); var element = server.bound.screen().find("name_editor").orElseThrow(); var rename = zone.moddev.mc.companion.CompanionWidgets.RENAME; var original = machine.getMachineName(); @@ -881,67 +868,74 @@ public zone.moddev.mc.lib.gui.model.Common common() { return element.common(); } }; - require(client.prepareElementAction(foreign, rename, "Foreign").isEmpty() - && !client.elementPending(foreign) && client.takeElementResult(foreign) == null, + require( + client.prepareElementAction(foreign, rename, "Foreign") + .isEmpty() && !client.elementPending(foreign) + && client.takeElementResult(foreign) == null, "Foreign elements are refused without throwing or creating pending state"); - var first = client.prepareElementAction(element, rename, "First").orElseThrow(); + var first = client.prepareElementAction(element, rename, "First") + .orElseThrow(); var late = server.elementAction(player, first); require(late.accepted(), "Server accepted the delayed action"); for (int i = 0; i < 99; i++) client.tickElementActions(); - require( - client.elementPending(element), + require(client.elementPending(element), "Request remains pending before the timeout boundary"); client.tickElementActions(); - require(!client.elementPending(element), "Timeout clears pending state"); - require(client.value(element, false).equals(original), "Timeout uses confirmed values"); + require(!client.elementPending(element), + "Timeout clears pending state"); + require(client.value(element, false).equals(original), + "Timeout uses confirmed values"); require( client.prepareElementAction(element, rename, "Retry").isEmpty(), "Timeout result must be consumed before retry"); - require(!client.takeElementResult(element).accepted(), "Timeout reports rejection"); - require(client.visible(element), "Timeout preserves the confirmed Settings view"); - var retry = client.prepareElementAction(element, rename, "Retry").orElseThrow(); - require(retry.request() > first.request(), "Retry never reuses request identity"); + require(!client.takeElementResult(element).accepted(), + "Timeout reports rejection"); + require(client.visible(element), + "Timeout preserves the confirmed Settings view"); + var retry = client.prepareElementAction(element, rename, "Retry") + .orElseThrow(); + require(retry.request() > first.request(), + "Retry never reuses request identity"); client.receiveElementResult(late); require( - client.elementPending(element) && client.value(element, false).equals(original), + client.elementPending(element) + && client.value(element, false).equals(original), "Late acknowledgment cannot settle or overwrite a newer request"); client.receiveElementResult(server.elementAction(player, retry)); require( - !client.elementPending(element) && client.takeElementResult(element).accepted(), + !client.elementPending(element) + && client.takeElementResult(element).accepted(), "Matching retry acknowledgment settles immediately"); require( - ((net.minecraft.network.chat.Component) client.value(element, false)) - .getString() - .equals("Retry"), + ((net.minecraft.network.chat.Component) client.value(element, + false)).getString().equals("Retry"), "Retry carries fresh authoritative values"); - var pending = client.prepareElementAction(element, rename, "After close").orElseThrow(); + var pending = client + .prepareElementAction(element, rename, "After close") + .orElseThrow(); var afterClose = server.elementAction(player, pending); client.removed(player); client.receiveElementResult(afterClose); client.tickElementActions(); require( - !client.elementPending(element) && client.takeElementResult(element) == null, + !client.elementPending(element) + && client.takeElementResult(element) == null, "Closing clears pending requests and results and ignores later acknowledgments"); + require(client.prepareElementAction(element, rename, "Closed") + .isEmpty(), "Closed sessions refuse new requests"); require( - client.prepareElementAction(element, rename, "Closed").isEmpty(), - "Closed sessions refuse new requests"); - require( - ((net.minecraft.network.chat.Component) client.value(element, false)) - .getString() - .equals("Retry"), + ((net.minecraft.network.chat.Component) client.value(element, + false)).getString().equals("Retry"), "Closing prevents late authoritative result application"); server.removed(player); require( - server.elementAction( - player, - new ScreenNetwork.Widget( - server.containerId, - server.sessionId(), - server.index(element), - pending.request() + 1, - rename.id(), - rename.encode(player.registryAccess(), "Closed server"))) == null, + server.elementAction(player, + new ScreenNetwork.Widget(server.containerId, + server.sessionId(), server.index(element), + pending.request() + 1, rename.id(), + rename.encode(player.registryAccess(), + "Closed server"))) == null, "Closed server sessions refuse dispatch even while the owner still references the menu"); } finally { player.containerMenu = player.inventoryMenu; @@ -951,36 +945,41 @@ public zone.moddev.mc.lib.gui.model.Common common() { } @GameTest(template = "empty") - public static void clearedProcessorsRestartWithoutManualResume(GameTestHelper helper) { - for (var type : java.util.List.of( - CompanionMod.CRUSHER, CompanionMod.TRANSMUTER, CompanionMod.DUAL_CRUSHER)) { + public static void clearedProcessorsRestartWithoutManualResume( + GameTestHelper helper) { + for (var type : java.util.List.of(CompanionMod.CRUSHER, + CompanionMod.TRANSMUTER, CompanionMod.DUAL_CRUSHER)) { var machine = place(helper, type); - var input = type == CompanionMod.TRANSMUTER ? Items.IRON_INGOT : Items.COPPER_ORE; + var input = type == CompanionMod.TRANSMUTER + ? Items.IRON_INGOT + : Items.COPPER_ORE; machine.items.setStackInSlot(0, new ItemStack(input)); machine.energy.setStored(100_000); tick(machine, 6); machine.clearContent(); for (var lane : machine.lanes) require( - !lane.paused() && lane.progress() == 0 && lane.state().resumeRecipe().isEmpty(), + !lane.paused() && lane.progress() == 0 + && lane.state().resumeRecipe().isEmpty(), "Clearing resets pause, progress and remembered recipe"); for (int slot = 0; slot < machine.items.getSlots(); slot++) require( - machine.items.getStackInSlot(slot).isEmpty() && !machine.items.hasPlan(slot), + machine.items.getStackInSlot(slot).isEmpty() + && !machine.items.hasPlan(slot), "Clearing empties inventory and reservations"); machine.items.setStackInSlot(0, new ItemStack(input)); machine.energy.setStored(100_000); tick(machine, 202); int output = type == CompanionMod.TRANSMUTER ? 0 : 1; require( - machine.items - .getStackInSlot(output) - .is(type == CompanionMod.TRANSMUTER ? Items.GOLD_INGOT : Items.RAW_COPPER), + machine.items.getStackInSlot(output) + .is(type == CompanionMod.TRANSMUTER + ? Items.GOLD_INGOT + : Items.RAW_COPPER), "Cleared machine processes fresh input without manual resume"); machine.lanes.forEach(lane -> lane.interrupt()); machine.clearContent(); - require( - machine.lanes.stream().noneMatch(lane -> lane.paused()), + require(machine.lanes.stream().noneMatch(lane -> lane.paused()), "Clearing also resets a paused machine"); } helper.succeed(); @@ -996,22 +995,24 @@ public static void registeredScreensAndRecipes(GameTestHelper helper) { require(Screens.get(id).isPresent(), "Missing screen " + id); level.setBlock(pos, machine.block.get().defaultBlockState(), 3); var entity = level.getBlockEntity(pos); - require(entity.getType() == machine.type.get(), "Wrong block entity for " + id); + require(entity.getType() == machine.type.get(), + "Wrong block entity for " + id); var menu = new MachineMenu(41, player.getInventory(), entity); - require(menu.bound.screen().id().equals(id), "Wrong menu for " + id); + require(menu.bound.screen().id().equals(id), + "Wrong menu for " + id); level.setBlock(pos, Blocks.AIR.defaultBlockState(), 3); } require( CompanionMod.CRUSHING.recipes(level.getRecipeManager()).size() == 5, "Five crusher recipes"); - require( - CompanionMod.TRANSMUTING.recipes(level.getRecipeManager()).size() == 1, - "One converter recipe"); + require(CompanionMod.TRANSMUTING.recipes(level.getRecipeManager()) + .size() == 1, "One converter recipe"); helper.succeed(); } @GameTest(template = "empty") - public static void customWidgetUsesValidatedMenuActions(GameTestHelper helper) { + public static void customWidgetUsesValidatedMenuActions( + GameTestHelper helper) { var machine = place(helper, CompanionMod.CRUSHER); var player = helper.makeMockServerPlayerInLevel(); var pos = machine.getBlockPos(); @@ -1024,17 +1025,25 @@ public static void customWidgetUsesValidatedMenuActions(GameTestHelper helper) { dial instanceof zone.moddev.mc.lib.gui.model.elements.CustomElement, "The consuming mod's widget loads on a dedicated server"); int index = menu.index(dial); - menu.action(player, new ScreenNetwork.Action(47, index, 0, 0, 0, 1)); - require(!machine.getPaused(), "Custom widgets reject unrecognized action payloads"); - menu.action(player, new ScreenNetwork.Action(48, index, 0, 0, 0, 0)); - require(!machine.getPaused(), "A stale menu id cannot act on a custom widget"); - menu.action(player, new ScreenNetwork.Action(47, index, 0, 0, 0, 0)); + menu.action(player, + new ScreenNetwork.Action(47, index, 0, 0, 0, 1)); + require(!machine.getPaused(), + "Custom widgets reject unrecognized action payloads"); + menu.action(player, + new ScreenNetwork.Action(48, index, 0, 0, 0, 0)); + require(!machine.getPaused(), + "A stale menu id cannot act on a custom widget"); + menu.action(player, + new ScreenNetwork.Action(47, index, 0, 0, 0, 0)); require(machine.getPaused(), "Custom widget click invokes the registered server handler"); - menu.action(player, new ScreenNetwork.Action(47, index, 0, 0, 0, 0)); - require(!machine.getPaused(), "A second custom widget click resumes the machine"); + menu.action(player, + new ScreenNetwork.Action(47, index, 0, 0, 0, 0)); + require(!machine.getPaused(), + "A second custom widget click resumes the machine"); } finally { - // Mock players have no client connection to receive subsequent menu ticks. + // Mock players have no client connection to receive subsequent menu + // ticks. player.containerMenu = player.inventoryMenu; helper.getLevel().setBlock(pos, Blocks.AIR.defaultBlockState(), 3); } @@ -1042,37 +1051,38 @@ public static void customWidgetUsesValidatedMenuActions(GameTestHelper helper) { } @GameTest(template = "empty") - public static void redstoneControlsHoldProgressAndRespectManualPause(GameTestHelper helper) { + public static void redstoneControlsHoldProgressAndRespectManualPause( + GameTestHelper helper) { RedstoneSmoke.check(helper); helper.succeed(); } @GameTest(template = "empty") - public static void shiftClickMergeMarksIdleMachineForSave(GameTestHelper helper) { + public static void shiftClickMergeMarksIdleMachineForSave( + GameTestHelper helper) { var machine = place(helper, CompanionMod.CRUSHER); var level = helper.getLevel(); var chunk = level.getChunkAt(machine.getBlockPos()); var player = helper.makeMockServerPlayerInLevel(); var inventory = player.getInventory(); machine.items.setStackInSlot(0, new ItemStack(Items.COPPER_ORE, 60)); - // Keep the newly fillable outputs full so this regression isolates an input merge. + // Keep the newly fillable outputs full so this regression isolates an + // input merge. machine.items.setStackInSlot(1, new ItemStack(Items.RAW_COPPER, 64)); machine.items.setStackInSlot(2, new ItemStack(Items.RAW_COPPER, 64)); inventory.setItem(9, new ItemStack(Items.COPPER_ORE, 8)); var menu = new MachineMenu(44, inventory, machine); - int source = menu.slots.stream() - .filter(slot -> slot.container == inventory && slot.getContainerSlot() == 9) - .findFirst() - .orElseThrow().index; - require( - !machine.items.hasPlan(0) && machine.energy.stored() == 0, + int source = menu.slots.stream().filter( + slot -> slot.container == inventory && slot.getContainerSlot() == 9) + .findFirst().orElseThrow().index; + require(!machine.items.hasPlan(0) && machine.energy.stored() == 0, "Regression starts without reservations or power to trigger unrelated saves"); try { - // Model an already-saved chunk; no server ticks run between this and the assertion. + // Model an already-saved chunk; no server ticks run between this + // and the assertion. chunk.setUnsaved(false); require(!chunk.isUnsaved(), "Chunk starts clean"); - require( - !menu.quickMoveStack(player, source).isEmpty(), + require(!menu.quickMoveStack(player, source).isEmpty(), "Shift-click merges into existing input"); require( machine.items.getStackInSlot(0).getCount() == 64 @@ -1082,24 +1092,27 @@ public static void shiftClickMergeMarksIdleMachineForSave(GameTestHelper helper) "Shift-click merge must mark the idle machine chunk for saving"); var saved = machine.saveWithFullMetadata(level.registryAccess()); var copy = (ProcessorEntity) BlockEntity.loadStatic( - machine.getBlockPos(), machine.getBlockState(), saved, level.registryAccess()); + machine.getBlockPos(), machine.getBlockState(), saved, + level.registryAccess()); require(copy.items.getStackInSlot(0).getCount() == 64, "Merged stack survives save/load"); chunk.setUnsaved(false); - require( - menu.quickMoveStack(player, source).isEmpty(), + require(menu.quickMoveStack(player, source).isEmpty(), "Full input rejects another shift-click"); - require(!chunk.isUnsaved(), "Rejected shift-click does not dirty the machine chunk"); + require(!chunk.isUnsaved(), + "Rejected shift-click does not dirty the machine chunk"); } finally { chunk.setUnsaved(true); - level.setBlock(machine.getBlockPos(), Blocks.AIR.defaultBlockState(), 3); + level.setBlock(machine.getBlockPos(), + Blocks.AIR.defaultBlockState(), 3); } helper.succeed(); } @GameTest(template = "empty") - public static void partialShiftClickExtractionMarksMachineForSave(GameTestHelper helper) { + public static void partialShiftClickExtractionMarksMachineForSave( + GameTestHelper helper) { var machine = place(helper, CompanionMod.CRUSHER); var level = helper.getLevel(); var chunk = level.getChunkAt(machine.getBlockPos()); @@ -1112,31 +1125,33 @@ public static void partialShiftClickExtractionMarksMachineForSave(GameTestHelper var menu = new MachineMenu(45, inventory, machine); try { chunk.setUnsaved(false); - require(!chunk.isUnsaved(), "Chunk starts clean before partial extraction"); + require(!chunk.isUnsaved(), + "Chunk starts clean before partial extraction"); require(!menu.quickMoveStack(player, 1).isEmpty(), "Shift-click tops up the player stack"); require( machine.items.getStackInSlot(1).getCount() == 4 && inventory.getItem(9).getCount() == 64, "Partial extraction retains four machine items"); - require( - chunk.isUnsaved(), + require(chunk.isUnsaved(), "Partial shift-click extraction must mark the machine chunk for saving"); var saved = machine.saveWithFullMetadata(level.registryAccess()); var copy = (ProcessorEntity) BlockEntity.loadStatic( - machine.getBlockPos(), machine.getBlockState(), saved, level.registryAccess()); - require( - copy.items.getStackInSlot(1).getCount() == 4, + machine.getBlockPos(), machine.getBlockState(), saved, + level.registryAccess()); + require(copy.items.getStackInSlot(1).getCount() == 4, "Only the remaining output survives save/load"); } finally { chunk.setUnsaved(true); - level.setBlock(machine.getBlockPos(), Blocks.AIR.defaultBlockState(), 3); + level.setBlock(machine.getBlockPos(), + Blocks.AIR.defaultBlockState(), 3); } helper.succeed(); } @GameTest(template = "empty") - public static void placementFacingKeepsNeighboursAndPortsOnTheSameSide(GameTestHelper helper) { + public static void placementFacingKeepsNeighboursAndPortsOnTheSameSide( + GameTestHelper helper) { var level = helper.getLevel(); var pos = helper.absolutePos(CENTER); var player = helper.makeMockServerPlayerInLevel(); @@ -1148,57 +1163,62 @@ public static void placementFacingKeepsNeighboursAndPortsOnTheSameSide(GameTestH var left = look.getCounterClockWise(); for (var machine : CompanionMod.MACHINES) { var context = new net.minecraft.world.item.context.BlockPlaceContext( - player, - InteractionHand.MAIN_HAND, + player, InteractionHand.MAIN_HAND, new ItemStack(machine.block.get()), - new BlockHitResult(Vec3.atCenterOf(pos.below()), Direction.UP, pos.below(), - false)); + new BlockHitResult(Vec3.atCenterOf(pos.below()), + Direction.UP, pos.below(), false)); var state = machine.block.get().getStateForPlacement(context); require( - state.getValue(zone.moddev.mc.companion.DemoBlock.FACING) == look.getOpposite(), + state.getValue( + zone.moddev.mc.companion.DemoBlock.FACING) == look + .getOpposite(), "Placed machine faces its player: " + look); level.setBlock(pos, state, 3); var entity = (zone.moddev.mc.lib.machine.neoforge.BasePoweredMachine) level .getBlockEntity(pos); var host = (zone.moddev.mc.lib.gui.runtime.ScreenHost) entity; for (var group : host.screenAutomation().groups()) - require( - group.world(leftFace) == left, + require(group.world(leftFace) == left, "Every resource group uses placement-relative left"); if (entity instanceof ProcessorEntity processor) require( processor.redstone.world(leftFace.ordinal()) == left, "Redstone and resource drawers agree on left"); - var rotated = state.rotate(net.minecraft.world.level.block.Rotation.CLOCKWISE_90); + var rotated = state.rotate( + net.minecraft.world.level.block.Rotation.CLOCKWISE_90); require( - rotated.getValue(zone.moddev.mc.companion.DemoBlock.FACING) == look - .getOpposite().getClockWise(), + rotated.getValue( + zone.moddev.mc.companion.DemoBlock.FACING) == look + .getOpposite().getClockWise(), "Rotation changes the stored facing"); - var mirrored = state.mirror(net.minecraft.world.level.block.Mirror.LEFT_RIGHT); - var expectedMirror = look.getAxis() == Direction.Axis.Z ? look : look.getOpposite(); - require( - mirrored.getValue(zone.moddev.mc.companion.DemoBlock.FACING) == expectedMirror, + var mirrored = state + .mirror(net.minecraft.world.level.block.Mirror.LEFT_RIGHT); + var expectedMirror = look.getAxis() == Direction.Axis.Z + ? look + : look.getOpposite(); + require(mirrored.getValue( + zone.moddev.mc.companion.DemoBlock.FACING) == expectedMirror, "Mirror changes the stored facing"); if (entity instanceof CreativePowerEntity power) { - level.setBlock( - pos.relative(left), - CompanionMod.CRUSHER.block.get().getStateForPlacement(context), + level.setBlock(pos.relative(left), + CompanionMod.CRUSHER.block.get() + .getStateForPlacement(context), 3); - var receiver = (ProcessorEntity) level.getBlockEntity(pos.relative(left)); - require( - receiver.facing() == power.facing(), + var receiver = (ProcessorEntity) level + .getBlockEntity(pos.relative(left)); + require(receiver.facing() == power.facing(), "Both placements from one position share a facing"); var group = power.automation.group(0); - for (var face : zone.moddev.mc.lib.api.capabilities.RelativeFace.values()) + for (var face : zone.moddev.mc.lib.api.capabilities.RelativeFace + .values()) group.sides().set(face, disabled); group.sides().set(leftFace, output); - var port = level.getCapability(Capabilities.EnergyStorage.BLOCK, pos, left); + var port = level.getCapability( + Capabilities.EnergyStorage.BLOCK, pos, left); require(port.canExtract(), "Left capability is enabled"); require( - !level - .getCapability(Capabilities.EnergyStorage.BLOCK, pos, - left.getOpposite()) - .canExtract(), + !level.getCapability(Capabilities.EnergyStorage.BLOCK, + pos, left.getOpposite()).canExtract(), "Right capability stays disabled"); power.serverTick(); require(receiver.energy.stored() == 1000, @@ -1214,8 +1234,10 @@ public static void placementFacingKeepsNeighboursAndPortsOnTheSameSide(GameTestH level.getBlockEntity(pos) == power && group.world(leftFace) == left.getClockWise(), "Existing group follows block rotation"); - require(!port.canExtract(), "Cached world-side capability follows rotation"); - level.setBlock(pos.relative(left), Blocks.AIR.defaultBlockState(), 3); + require(!port.canExtract(), + "Cached world-side capability follows rotation"); + level.setBlock(pos.relative(left), + Blocks.AIR.defaultBlockState(), 3); } level.setBlock(pos, Blocks.AIR.defaultBlockState(), 3); } @@ -1224,47 +1246,58 @@ public static void placementFacingKeepsNeighboursAndPortsOnTheSameSide(GameTestH } @GameTest(template = "empty") - public static void creativePowerSidesGatePushAndPullAndPersist(GameTestHelper helper) { + public static void creativePowerSidesGatePushAndPullAndPersist( + GameTestHelper helper) { var level = helper.getLevel(); var pos = helper.absolutePos(CENTER); - level.setBlock(pos, CompanionMod.CREATIVE_POWER.block.get().defaultBlockState(), 3); - level.setBlock(pos.above(), CompanionMod.CRUSHER.block.get().defaultBlockState(), 3); + level.setBlock(pos, + CompanionMod.CREATIVE_POWER.block.get().defaultBlockState(), 3); + level.setBlock(pos.above(), + CompanionMod.CRUSHER.block.get().defaultBlockState(), 3); var power = (CreativePowerEntity) level.getBlockEntity(pos); var receiver = (ProcessorEntity) level.getBlockEntity(pos.above()); - var port = level.getCapability(Capabilities.EnergyStorage.BLOCK, pos, Direction.UP); + var port = level.getCapability(Capabilities.EnergyStorage.BLOCK, pos, + Direction.UP); var group = power.automation.group(0); var top = zone.moddev.mc.lib.api.capabilities.RelativeFace.TOP; var output = zone.moddev.mc.lib.api.capabilities.IoMode.OUTPUT; var disabled = zone.moddev.mc.lib.api.capabilities.IoMode.DISABLED; - require(power.screenAutomation() == power.automation, "Generator exposes side drawer"); - var menu = new MachineMenu(46, helper.makeMockServerPlayerInLevel().getInventory(), power); + require(power.screenAutomation() == power.automation, + "Generator exposes side drawer"); + var menu = new MachineMenu(46, + helper.makeMockServerPlayerInLevel().getInventory(), power); require( menu.slots.isEmpty() - && menu.components().get(BuiltinMenuComponents.AUTOMATION).slotCount() == 0 - && !menu.components().get(BuiltinMenuComponents.AUTOMATION).addonsEnabled(), + && menu.components().get(BuiltinMenuComponents.AUTOMATION) + .slotCount() == 0 + && !menu.components().get(BuiltinMenuComponents.AUTOMATION) + .addonsEnabled(), "Generator side configuration exposes no inventory slots"); - require(menu.bound.screen().find("info").isPresent(), "Generator has an Info panel"); + require(menu.bound.screen().find("info").isPresent(), + "Generator has an Info panel"); require(group.cycle(top, output, false), "Output side can be disabled"); - require( - !port.canExtract() && port.extractEnergy(1000, false) == 0, + require(!port.canExtract() && port.extractEnergy(1000, false) == 0, "Existing capability follows disabled side"); power.serverTick(); - require(receiver.energy.stored() == 0, "Disabled side does not push energy"); + require(receiver.energy.stored() == 0, + "Disabled side does not push energy"); var saved = power.saveWithFullMetadata(level.registryAccess()); - var copy = (CreativePowerEntity) BlockEntity.loadStatic(pos, power.getBlockState(), saved, - level.registryAccess()); + var copy = (CreativePowerEntity) BlockEntity.loadStatic(pos, + power.getBlockState(), saved, level.registryAccess()); require(copy.automation.group(0).sides().get(top) == disabled, "Disabled side survives reload"); saved.remove("automation"); - var legacy = (CreativePowerEntity) BlockEntity.loadStatic(pos, power.getBlockState(), saved, - level.registryAccess()); - require( - legacy.automation.group(0).sides().get(top) == output, + var legacy = (CreativePowerEntity) BlockEntity.loadStatic(pos, + power.getBlockState(), saved, level.registryAccess()); + require(legacy.automation.group(0).sides().get(top) == output, "Older generators retain all outputs"); - require(group.cycle(top, disabled, false), "Disabled side cycles back to output"); - require(port.canExtract() && !port.canReceive(), "Generator remains output-only"); + require(group.cycle(top, disabled, false), + "Disabled side cycles back to output"); + require(port.canExtract() && !port.canReceive(), + "Generator remains output-only"); power.serverTick(); - require(receiver.energy.stored() == 1000, "Re-enabled side pushes energy"); + require(receiver.energy.stored() == 1000, + "Re-enabled side pushes energy"); level.setBlock(pos.above(), Blocks.AIR.defaultBlockState(), 3); level.setBlock(pos, Blocks.AIR.defaultBlockState(), 3); helper.succeed(); @@ -1274,32 +1307,38 @@ public static void creativePowerSidesGatePushAndPullAndPersist(GameTestHelper he public static void creativePowerFeedsAllFaces(GameTestHelper helper) { var level = helper.getLevel(); var pos = helper.absolutePos(CENTER); - level.setBlock(pos, CompanionMod.CREATIVE_POWER.block.get().defaultBlockState(), 3); + level.setBlock(pos, + CompanionMod.CREATIVE_POWER.block.get().defaultBlockState(), 3); var power = (CreativePowerEntity) level.getBlockEntity(pos); for (var side : Direction.values()) - level.setBlock(pos.relative(side), CompanionMod.CRUSHER.block.get().defaultBlockState(), - 3); + level.setBlock(pos.relative(side), + CompanionMod.CRUSHER.block.get().defaultBlockState(), 3); power.serverTick(); for (var side : Direction.values()) { - var machine = (ProcessorEntity) level.getBlockEntity(pos.relative(side)); + var machine = (ProcessorEntity) level + .getBlockEntity(pos.relative(side)); require(machine.energy.stored() == 1_000, "Power reaches " + side); - var view = level.getCapability( - Capabilities.EnergyStorage.BLOCK, pos.relative(side), side.getOpposite()); - require(view.canReceive() && !view.canExtract(), "Processor energy is input-only"); - var unsided = level.getCapability(Capabilities.EnergyStorage.BLOCK, pos.relative(side), - null); - require( - unsided != null - && !unsided.canExtract() + var view = level.getCapability(Capabilities.EnergyStorage.BLOCK, + pos.relative(side), side.getOpposite()); + require(view.canReceive() && !view.canExtract(), + "Processor energy is input-only"); + var unsided = level.getCapability(Capabilities.EnergyStorage.BLOCK, + pos.relative(side), null); + require( + unsided != null && !unsided.canExtract() && unsided.extractEnergy(1_000, true) == 0 && unsided.extractEnergy(1_000, false) == 0 && machine.energy.stored() == 1_000, "Unsided Crusher access cannot drain energy"); - level.setBlock(pos.relative(side), Blocks.AIR.defaultBlockState(), 3); + level.setBlock(pos.relative(side), Blocks.AIR.defaultBlockState(), + 3); } - var output = level.getCapability(Capabilities.EnergyStorage.BLOCK, pos, Direction.UP); - require(output.canExtract() && !output.canReceive(), "Source energy is output-only"); - require(output.extractEnergy(2_000, false) == 1_000, "Source limits extraction per call"); + var output = level.getCapability(Capabilities.EnergyStorage.BLOCK, pos, + Direction.UP); + require(output.canExtract() && !output.canReceive(), + "Source energy is output-only"); + require(output.extractEnergy(2_000, false) == 1_000, + "Source limits extraction per call"); level.setBlock(pos, Blocks.AIR.defaultBlockState(), 3); helper.succeed(); } @@ -1309,14 +1348,15 @@ public static void crusherUsesAdjacentPower(GameTestHelper helper) { var level = helper.getLevel(); var pos = helper.absolutePos(CENTER); var crusher = place(helper, CompanionMod.CRUSHER); - level.setBlock(pos.below(), CompanionMod.CREATIVE_POWER.block.get().defaultBlockState(), 3); + level.setBlock(pos.below(), + CompanionMod.CREATIVE_POWER.block.get().defaultBlockState(), 3); var source = (CreativePowerEntity) level.getBlockEntity(pos.below()); - var input = level.getCapability(Capabilities.ItemHandler.BLOCK, pos, Direction.UP); - require(input.insertItem(0, new ItemStack(Items.COPPER_ORE), false).isEmpty(), - "Accepts ore"); - require( - input.insertItem(1, new ItemStack(Items.RAW_COPPER), true).isEmpty(), - "Crusher output accepts insertion for testing"); + var input = level.getCapability(Capabilities.ItemHandler.BLOCK, pos, + Direction.UP); + require(input.insertItem(0, new ItemStack(Items.COPPER_ORE), false) + .isEmpty(), "Accepts ore"); + require(input.insertItem(1, new ItemStack(Items.RAW_COPPER), true) + .isEmpty(), "Crusher output accepts insertion for testing"); for (int i = 0; i <= 100; i++) { source.serverTick(); crusher.serverTick(); @@ -1326,21 +1366,26 @@ public static void crusherUsesAdjacentPower(GameTestHelper helper) { crusher.items.getStackInSlot(1).is(Items.RAW_COPPER) && crusher.items.getStackInSlot(1).getCount() == 2, "Produced two raw copper"); - require(input.extractItem(1, 64, false).getCount() == 2, "Automation extracts output"); + require(input.extractItem(1, 64, false).getCount() == 2, + "Automation extracts output"); level.setBlock(pos, Blocks.AIR.defaultBlockState(), 3); level.setBlock(pos.below(), Blocks.AIR.defaultBlockState(), 3); helper.succeed(); } @GameTest(template = "empty") - public static void crusherSplitsOutputAcrossNearlyFullSlots(GameTestHelper helper) { + public static void crusherSplitsOutputAcrossNearlyFullSlots( + GameTestHelper helper) { var machine = place(helper, CompanionMod.CRUSHER); var level = helper.getLevel(); var pos = machine.getBlockPos(); - require(machine.items.getSlots() == 3, "Crusher has one input and two outputs"); + require(machine.items.getSlots() == 3, + "Crusher has one input and two outputs"); require( - machine.automation.group(1).members().equals(java.util.List.of(0, 1)) - && machine.items.inventory("input") != machine.items.inventory("output"), + machine.automation.group(1).members() + .equals(java.util.List.of(0, 1)) + && machine.items.inventory("input") != machine.items + .inventory("output"), "Both outputs belong to one configurable inventory"); var player = helper.makeMockServerPlayerInLevel(); player.setPos(pos.getX() + .5, pos.getY() + .5, pos.getZ() + .5); @@ -1348,24 +1393,30 @@ public static void crusherSplitsOutputAcrossNearlyFullSlots(GameTestHelper helpe player.containerMenu = menu; for (int slot : new int[]{1, 2}) { menu.setCarried(new ItemStack(Items.DIRT, 7)); - menu.clicked(slot, 0, net.minecraft.world.inventory.ClickType.PICKUP, player); + menu.clicked(slot, 0, + net.minecraft.world.inventory.ClickType.PICKUP, player); require( machine.items.getStackInSlot(slot).is(Items.DIRT) && machine.items.getStackInSlot(slot).getCount() == 7 && menu.getCarried().isEmpty(), "Players can fill either crusher output with arbitrary items"); - menu.clicked(slot, 0, net.minecraft.world.inventory.ClickType.PICKUP, player); + menu.clicked(slot, 0, + net.minecraft.world.inventory.ClickType.PICKUP, player); require( - menu.getCarried().getCount() == 7 && machine.items.getStackInSlot(slot).isEmpty(), + menu.getCarried().getCount() == 7 + && machine.items.getStackInSlot(slot).isEmpty(), "Players can take their test items back"); menu.setCarried(ItemStack.EMPTY); } player.containerMenu = player.inventoryMenu; machine.items.insertItem(0, new ItemStack(Items.IRON_ORE), false); - var fill = level.getCapability(Capabilities.ItemHandler.BLOCK, pos, Direction.UP); + var fill = level.getCapability(Capabilities.ItemHandler.BLOCK, pos, + Direction.UP); require( - fill.insertItem(1, new ItemStack(Items.RAW_IRON, 63), false).isEmpty() - && fill.insertItem(2, new ItemStack(Items.RAW_IRON, 63), false).isEmpty(), + fill.insertItem(1, new ItemStack(Items.RAW_IRON, 63), false) + .isEmpty() + && fill.insertItem(2, new ItemStack(Items.RAW_IRON, 63), false) + .isEmpty(), "Both output slots accept external filling before the recipe starts"); machine.energy.setStored(100000); tick(machine, 1); @@ -1377,29 +1428,33 @@ public static void crusherSplitsOutputAcrossNearlyFullSlots(GameTestHelper helpe && machine.items.plan(2).add().getCount() == 1, "Two raw iron reserve one space in each stack of 63"); require( - machine.items.insertResult(1, new ItemStack(Items.RAW_IRON), true).getCount() == 1 - && machine.items.insertResult(2, new ItemStack(Items.RAW_IRON), true) + machine.items.insertResult(1, new ItemStack(Items.RAW_IRON), true) + .getCount() == 1 + && machine.items + .insertResult(2, new ItemStack(Items.RAW_IRON), true) .getCount() == 1, "Both last spaces are protected by reservations"); var saved = machine.saveWithFullMetadata(level.registryAccess()); - var copy = (ProcessorEntity) BlockEntity.loadStatic(pos, machine.getBlockState(), saved, - level.registryAccess()); + var copy = (ProcessorEntity) BlockEntity.loadStatic(pos, + machine.getBlockState(), saved, level.registryAccess()); copy.setLevel(level); require( - copy.items.plan(1).add().getCount() == 1 && copy.items.plan(2).add().getCount() == 1, + copy.items.plan(1).add().getCount() == 1 + && copy.items.plan(2).add().getCount() == 1, "Split reservations survive saving and loading"); tick(copy, 100); require( copy.items.getStackInSlot(0).isEmpty() && copy.items.getStackInSlot(1).getCount() == 64 && copy.items.getStackInSlot(2).getCount() == 64 - && !copy.items.hasPlan(1) - && !copy.items.hasPlan(2), + && !copy.items.hasPlan(1) && !copy.items.hasPlan(2), "Completion fills both stacks to 64 and releases their plans"); - var capability = level.getCapability(Capabilities.ItemHandler.BLOCK, pos, Direction.UP); + var capability = level.getCapability(Capabilities.ItemHandler.BLOCK, + pos, Direction.UP); require( capability.getSlots() == 3 - && capability.insertItem(2, new ItemStack(Items.RAW_IRON), true).getCount() == 1 + && capability.insertItem(2, new ItemStack(Items.RAW_IRON), true) + .getCount() == 1 && capability.extractItem(2, 1, true).getCount() == 1, "Second output permits extraction but protects its reserved last space"); machine.lanes.forEach(lane -> lane.reset()); @@ -1407,20 +1462,18 @@ public static void crusherSplitsOutputAcrossNearlyFullSlots(GameTestHelper helpe machine.items.setStackInSlot(2, new ItemStack(Items.RAW_IRON, 64)); long energy = machine.energy.stored(); tick(machine, 1); - require( - machine.getTime() == 0 - && machine.items.plan(0).isNone() - && machine.items.plan(1).isNone() - && machine.items.plan(2).isNone() - && machine.energy.stored() == energy, + require(machine.getTime() == 0 && machine.items.plan(0).isNone() + && machine.items.plan(1).isNone() && machine.items.plan(2).isNone() + && machine.energy.stored() == energy, "Only one free space refuses the whole recipe without partial reservations or energy use"); - var legacyItems = new zone.moddev.mc.lib.api.capabilities.ItemStorage(2); + var legacyItems = new zone.moddev.mc.lib.api.capabilities.ItemStorage( + 2); legacyItems.setStackInSlot(0, new ItemStack(Items.IRON_ORE)); legacyItems.setStackInSlot(1, new ItemStack(Items.RAW_IRON, 63)); var legacy = machine.saveWithFullMetadata(level.registryAccess()); legacy.put("items", legacyItems.serializeNBT(level.registryAccess())); - var upgraded = (ProcessorEntity) BlockEntity.loadStatic(pos, machine.getBlockState(), - legacy, level.registryAccess()); + var upgraded = (ProcessorEntity) BlockEntity.loadStatic(pos, + machine.getBlockState(), legacy, level.registryAccess()); require( upgraded.items.getSlots() == 3 && upgraded.items.getStackInSlot(1).getCount() == 63 @@ -1435,68 +1488,75 @@ public static void oneSlotInterruptAndResume(GameTestHelper helper) { var machine = place(helper, CompanionMod.TRANSMUTER); var level = helper.getLevel(); var pos = machine.getBlockPos(); - var automation = level.getCapability(Capabilities.ItemHandler.BLOCK, pos, Direction.UP); + var automation = level.getCapability(Capabilities.ItemHandler.BLOCK, + pos, Direction.UP); require( - automation.insertItem(0, new ItemStack(Items.IRON_INGOT, 64), false).getCount() == 63, + automation.insertItem(0, new ItemStack(Items.IRON_INGOT, 64), false) + .getCount() == 63, "The converter accepts exactly one iron"); machine.energy.setStored(100_000); tick(machine, 51); - require(machine.lanes.getFirst().progress() == 50, "Halfway to interrupt"); - require(automation.extractItem(0, 1, false).isEmpty(), "Running input is reserved"); - require( - automation.insertItem(0, new ItemStack(Items.IRON_INGOT), false).getCount() == 1, - "A hopper cannot refill the reserved slot"); + require(machine.lanes.getFirst().progress() == 50, + "Halfway to interrupt"); + require(automation.extractItem(0, 1, false).isEmpty(), + "Running input is reserved"); + require(automation.insertItem(0, new ItemStack(Items.IRON_INGOT), false) + .getCount() == 1, "A hopper cannot refill the reserved slot"); machine.energy.setStored(0); machine.serverTick(); - require(machine.lanes.getFirst().status() == Status.STALLED, "Stalls without power"); - require( - machine.lanes.getFirst().completedCycles().signum() == 0, + require(machine.lanes.getFirst().status() == Status.STALLED, + "Stalls without power"); + require(machine.lanes.getFirst().completedCycles().signum() == 0, "Progress and energy stalls are not completed cycles"); var player = helper.makeMockServerPlayerInLevel(); player.setPos(pos.getX() + 0.5, pos.getY(), pos.getZ() + 0.5); var menu = new MachineMenu(42, player.getInventory(), machine); player.containerMenu = menu; - int button = menu.index(menu.bound.screen().find("toggle_0").orElseThrow()); + int button = menu + .index(menu.bound.screen().find("toggle_0").orElseThrow()); int action = new ScreenNetwork.Action(42, button, 0, 0, 0, 0).pack(); menu.clickMenuButton(player, action); - require( - machine.getPaused() && !machine.items.hasPlan(0), + require(machine.getPaused() && !machine.items.hasPlan(0), "The screen frees reservations without power"); - require(automation.extractItem(0, 1, true).getCount() == 1, "Paused iron can be taken"); - require( - machine.lanes.getFirst().completedCycles().signum() == 0, + require(automation.extractItem(0, 1, true).getCount() == 1, + "Paused iron can be taken"); + require(machine.lanes.getFirst().completedCycles().signum() == 0, "Interrupts do not count as completed cycles"); menu.clickMenuButton(player, action); machine.energy.setStored(100_000); tick(machine, 150); - require( - machine.items.getStackInSlot(0).is(Items.GOLD_INGOT), + require(machine.items.getStackInSlot(0).is(Items.GOLD_INGOT), "Resume finishes from saved progress"); require( - machine.lanes.getFirst().completedCycles().equals(java.math.BigInteger.ONE), + machine.lanes.getFirst().completedCycles() + .equals(java.math.BigInteger.ONE), "A resumed recipe counts exactly once when it finishes"); - require(automation.extractItem(0, 1, false).is(Items.GOLD_INGOT), "Gold can be extracted"); + require(automation.extractItem(0, 1, false).is(Items.GOLD_INGOT), + "Gold can be extracted"); level.setBlock(pos, Blocks.AIR.defaultBlockState(), 3); helper.succeed(); } @GameTest(template = "empty") - public static void sharedLanesSurviveSaveAndInterrupt(GameTestHelper helper) { + public static void sharedLanesSurviveSaveAndInterrupt( + GameTestHelper helper) { var machine = place(helper, CompanionMod.DUAL_CRUSHER); var level = helper.getLevel(); var pos = machine.getBlockPos(); machine.items.insertItem(0, new ItemStack(Items.COPPER_ORE, 2), false); machine.energy.setStored(100_000); tick(machine, 31); - require(machine.items.plan(0).take() == 2, "Both lanes reserve the shared input"); + require(machine.items.plan(0).take() == 2, + "Both lanes reserve the shared input"); require(machine.items.plan(1).add().getCount() == 4, "Both lanes reserve the shared output"); machine.toggle(0); - require(machine.items.plan(0).take() == 1, "Interrupt releases only lane A's share"); + require(machine.items.plan(0).take() == 1, + "Interrupt releases only lane A's share"); var saved = machine.saveWithFullMetadata(level.registryAccess()); - var copy = (ProcessorEntity) BlockEntity.loadStatic(pos, machine.getBlockState(), saved, - level.registryAccess()); + var copy = (ProcessorEntity) BlockEntity.loadStatic(pos, + machine.getBlockState(), saved, level.registryAccess()); copy.setLevel(level); require( copy.getPaused() && copy.lanes.get(1).status() == Status.RUNNING, @@ -1504,13 +1564,14 @@ public static void sharedLanesSurviveSaveAndInterrupt(GameTestHelper helper) { require(copy.items.plan(0).take() == 1, "Lane B reservation restored"); copy.toggle(0); tick(copy, 70); - require(copy.items.getStackInSlot(0).isEmpty(), "Both lanes consumed their own ore"); + require(copy.items.getStackInSlot(0).isEmpty(), + "Both lanes consumed their own ore"); require(copy.items.getStackInSlot(1).getCount() == 4, "Both lanes produced exactly two copper"); require( - copy.screenStats().containsAll(copy.lanes) - && copy.lanes.stream() - .allMatch(lane -> lane.completedCycles().equals(java.math.BigInteger.ONE)), + copy.screenStats().containsAll(copy.lanes) && copy.lanes.stream() + .allMatch(lane -> lane.completedCycles() + .equals(java.math.BigInteger.ONE)), "Each processor exposes its own completed-cycle count"); machine.clearContent(); level.setBlock(pos, Blocks.AIR.defaultBlockState(), 3); @@ -1518,7 +1579,8 @@ public static void sharedLanesSurviveSaveAndInterrupt(GameTestHelper helper) { } @GameTest(template = "empty") - public static void commandReplacementClearsButDestroyDrops(GameTestHelper helper) { + public static void commandReplacementClearsButDestroyDrops( + GameTestHelper helper) { var level = helper.getLevel(); var pos = helper.absolutePos(CENTER); for (String command : new String[]{"setblock", "fill"}) { @@ -1527,27 +1589,26 @@ public static void commandReplacementClearsButDestroyDrops(GameTestHelper helper new AABB(pos).inflate(2))) drop.discard(); var machine = place(helper, CompanionMod.TRANSMUTER); - machine.items.insertItem(0, new ItemStack(Items.IRON_INGOT), false); + machine.items.insertItem(0, new ItemStack(Items.IRON_INGOT), + false); machine.energy.setStored(100_000); tick(machine, 5); - String coordinates = pos.getX() + " " + pos.getY() + " " + pos.getZ(); + String coordinates = pos.getX() + " " + pos.getY() + " " + + pos.getZ(); String arguments = command.equals("fill") ? coordinates + " " + coordinates : coordinates; - level - .getServer() - .getCommands() - .performPrefixedCommand( - level.getServer().createCommandSourceStack().withLevel(level) - .withPermission(4), - command + " " + arguments + " minecraft:air " + mode); - int iron = level.getEntitiesOfClass(ItemEntity.class, new AABB(pos).inflate(2)) + level.getServer().getCommands().performPrefixedCommand( + level.getServer().createCommandSourceStack() + .withLevel(level).withPermission(4), + command + " " + arguments + " minecraft:air " + mode); + int iron = level + .getEntitiesOfClass(ItemEntity.class, + new AABB(pos).inflate(2)) .stream() .filter(drop -> drop.getItem().is(Items.IRON_INGOT)) - .mapToInt(drop -> drop.getItem().getCount()) - .sum(); - require( - iron == (mode.equals("destroy") ? 1 : 0), + .mapToInt(drop -> drop.getItem().getCount()).sum(); + require(iron == (mode.equals("destroy") ? 1 : 0), command + " " + mode + " dropped " + iron + " iron"); } } @@ -1555,59 +1616,67 @@ public static void commandReplacementClearsButDestroyDrops(GameTestHelper helper } @GameTest(template = "empty") - public static void crusherTankRejectsMixingAndPersists(GameTestHelper helper) { + public static void crusherTankRejectsMixingAndPersists( + GameTestHelper helper) { var machine = place(helper, CompanionMod.CRUSHER); var level = helper.getLevel(); var pos = machine.getBlockPos(); for (var side : Direction.values()) { - var handler = level.getCapability(Capabilities.FluidHandler.BLOCK, pos, side); + var handler = level.getCapability(Capabilities.FluidHandler.BLOCK, + pos, side); require( - handler != null && handler.getTanks() == 1 && handler.getTankCapacity(0) == 4_000, + handler != null && handler.getTanks() == 1 + && handler.getTankCapacity(0) == 4_000, "Crusher exposes one 4-bucket tank on every face"); require( - handler.fill(new FluidStack(Fluids.WATER, 5_000), FluidAction.SIMULATE) == 4_000, + handler.fill(new FluidStack(Fluids.WATER, 5_000), + FluidAction.SIMULATE) == 4_000, "Simulated fill respects capacity"); - require(machine.fluids.getFluidInTank(0).isEmpty(), "Simulation leaves the tank empty"); + require(machine.fluids.getFluidInTank(0).isEmpty(), + "Simulation leaves the tank empty"); } - var handler = level.getCapability(Capabilities.FluidHandler.BLOCK, pos, Direction.UP); + var handler = level.getCapability(Capabilities.FluidHandler.BLOCK, pos, + Direction.UP); require( - handler.fill(new FluidStack(Fluids.WATER, 5_000), FluidAction.EXECUTE) == 4_000, + handler.fill(new FluidStack(Fluids.WATER, 5_000), + FluidAction.EXECUTE) == 4_000, "Water is accepted up to capacity"); - require( - handler.fill(new FluidStack(Fluids.WATER, 1), FluidAction.EXECUTE) == 0, - "Full tank refuses overflow"); + require(handler.fill(new FluidStack(Fluids.WATER, 1), + FluidAction.EXECUTE) == 0, "Full tank refuses overflow"); handler.drain(3_999, FluidAction.EXECUTE); require( - handler.fill(new FluidStack(Fluids.LAVA, 1_000), FluidAction.EXECUTE) == 0, + handler.fill(new FluidStack(Fluids.LAVA, 1_000), + FluidAction.EXECUTE) == 0, "Even one mB of water prevents mixing with lava"); - require( - handler.drain(new FluidStack(Fluids.LAVA, 1_000), FluidAction.EXECUTE).isEmpty(), - "Draining the wrong fluid does not remove water"); + require(handler + .drain(new FluidStack(Fluids.LAVA, 1_000), FluidAction.EXECUTE) + .isEmpty(), "Draining the wrong fluid does not remove water"); handler.drain(1, FluidAction.EXECUTE); - require( - handler.fill(new FluidStack(Fluids.LAVA, 2_000), FluidAction.EXECUTE) == 2_000, - "An emptied tank accepts lava"); - require( - handler.fill(new FluidStack(Fluids.WATER, 1_000), FluidAction.EXECUTE) == 0, - "Water cannot mix into lava either"); + require(handler.fill(new FluidStack(Fluids.LAVA, 2_000), + FluidAction.EXECUTE) == 2_000, "An emptied tank accepts lava"); + require(handler.fill(new FluidStack(Fluids.WATER, 1_000), + FluidAction.EXECUTE) == 0, "Water cannot mix into lava either"); var saved = machine.saveWithFullMetadata(level.registryAccess()); - var copy = (ProcessorEntity) BlockEntity.loadStatic(pos, machine.getBlockState(), saved, - level.registryAccess()); + var copy = (ProcessorEntity) BlockEntity.loadStatic(pos, + machine.getBlockState(), saved, level.registryAccess()); require( copy.fluids.getFluidInTank(0).getFluid() == Fluids.LAVA && copy.fluids.getFluidInTank(0).getAmount() == 2_000, "Fluid type and amount survive save/load"); saved.remove("fluids"); - var legacy = (ProcessorEntity) BlockEntity.loadStatic(pos, machine.getBlockState(), saved, - level.registryAccess()); - require(legacy.fluids.getFluidInTank(0).isEmpty(), "Older saves start with an empty tank"); + var legacy = (ProcessorEntity) BlockEntity.loadStatic(pos, + machine.getBlockState(), saved, level.registryAccess()); + require(legacy.fluids.getFluidInTank(0).isEmpty(), + "Older saves start with an empty tank"); machine.clearContent(); require(machine.fluids.getFluidInTank(0).isEmpty(), "Clearing the machine clears its fluid"); - for (var other : java.util.List.of(CompanionMod.TRANSMUTER, CompanionMod.DUAL_CRUSHER)) { + for (var other : java.util.List.of(CompanionMod.TRANSMUTER, + CompanionMod.DUAL_CRUSHER)) { place(helper, other); require( - level.getCapability(Capabilities.FluidHandler.BLOCK, pos, Direction.UP) == null, + level.getCapability(Capabilities.FluidHandler.BLOCK, pos, + Direction.UP) == null, "Only the single Crusher has a fluid tank"); } level.setBlock(pos, Blocks.AIR.defaultBlockState(), 3); @@ -1615,47 +1684,56 @@ public static void crusherTankRejectsMixingAndPersists(GameTestHelper helper) { } @GameTest(template = "empty") - public static void crusherBucketsFillDrainAndRefuseMixing(GameTestHelper helper) { + public static void crusherBucketsFillDrainAndRefuseMixing( + GameTestHelper helper) { var machine = place(helper, CompanionMod.CRUSHER); var level = helper.getLevel(); var pos = machine.getBlockPos(); var player = helper.makeMockServerPlayerInLevel(); player.getAbilities().instabuild = false; - var hit = new BlockHitResult(Vec3.atCenterOf(pos), Direction.UP, pos, false); + var hit = new BlockHitResult(Vec3.atCenterOf(pos), Direction.UP, pos, + false); var state = machine.getBlockState(); - player.setItemInHand(InteractionHand.MAIN_HAND, new ItemStack(Items.WATER_BUCKET)); - require( - state.useItemOn(player.getMainHandItem(), level, player, InteractionHand.MAIN_HAND, - hit) == ItemInteractionResult.CONSUME, + player.setItemInHand(InteractionHand.MAIN_HAND, + new ItemStack(Items.WATER_BUCKET)); + require(state.useItemOn(player.getMainHandItem(), level, player, + InteractionHand.MAIN_HAND, hit) == ItemInteractionResult.CONSUME, "Water bucket handled by the block"); require( player.getMainHandItem().is(Items.BUCKET) && machine.fluids.getFluidInTank(0).getAmount() == 1_000, "Filling consumes the water and returns an empty bucket"); - player.setItemInHand(InteractionHand.MAIN_HAND, new ItemStack(Items.LAVA_BUCKET)); + player.setItemInHand(InteractionHand.MAIN_HAND, + new ItemStack(Items.LAVA_BUCKET)); require( - state.useItemOn(player.getMainHandItem(), level, player, InteractionHand.MAIN_HAND, - hit) == ItemInteractionResult.FAIL, + state.useItemOn(player.getMainHandItem(), level, player, + InteractionHand.MAIN_HAND, hit) == ItemInteractionResult.FAIL, "Mixed bucket stops before vanilla can place lava"); require( player.getMainHandItem().is(Items.LAVA_BUCKET) && machine.fluids.getFluidInTank(0).getFluid() == Fluids.WATER && machine.fluids.getFluidInTank(0).getAmount() == 1_000, "Rejected transfer leaves the bucket and tank unchanged"); - player.setItemInHand(InteractionHand.MAIN_HAND, new ItemStack(Items.BUCKET)); - state.useItemOn(player.getMainHandItem(), level, player, InteractionHand.MAIN_HAND, hit); + player.setItemInHand(InteractionHand.MAIN_HAND, + new ItemStack(Items.BUCKET)); + state.useItemOn(player.getMainHandItem(), level, player, + InteractionHand.MAIN_HAND, hit); require( player.getMainHandItem().is(Items.WATER_BUCKET) && machine.fluids.getFluidInTank(0).isEmpty(), "An empty bucket drains water"); - player.setItemInHand(InteractionHand.MAIN_HAND, new ItemStack(Items.LAVA_BUCKET)); - state.useItemOn(player.getMainHandItem(), level, player, InteractionHand.MAIN_HAND, hit); + player.setItemInHand(InteractionHand.MAIN_HAND, + new ItemStack(Items.LAVA_BUCKET)); + state.useItemOn(player.getMainHandItem(), level, player, + InteractionHand.MAIN_HAND, hit); require( player.getMainHandItem().is(Items.BUCKET) && machine.fluids.getFluidInTank(0).getFluid() == Fluids.LAVA, "Lava can be put in after draining water"); - player.setItemInHand(InteractionHand.MAIN_HAND, new ItemStack(Items.BUCKET)); - state.useItemOn(player.getMainHandItem(), level, player, InteractionHand.MAIN_HAND, hit); + player.setItemInHand(InteractionHand.MAIN_HAND, + new ItemStack(Items.BUCKET)); + state.useItemOn(player.getMainHandItem(), level, player, + InteractionHand.MAIN_HAND, hit); require( player.getMainHandItem().is(Items.LAVA_BUCKET) && machine.fluids.getFluidInTank(0).isEmpty(), @@ -1665,13 +1743,15 @@ public static void crusherBucketsFillDrainAndRefuseMixing(GameTestHelper helper) } @GameTest(template = "empty") - public static void tankGaugeTransfersCursorContainers(GameTestHelper helper) { + public static void tankGaugeTransfersCursorContainers( + GameTestHelper helper) { var machine = place(helper, CompanionMod.CRUSHER); var player = helper.makeMockServerPlayerInLevel(); var pos = machine.getBlockPos(); player.setPos(pos.getX() + 0.5, pos.getY(), pos.getZ() + 0.5); player.getAbilities().instabuild = false; - // Mock players have no negotiated screen channel; the client smoke checks real + // Mock players have no negotiated screen channel; the client smoke + // checks real // synchronization. var menu = new MachineMenu(43, player.getInventory(), machine) { @Override @@ -1696,11 +1776,11 @@ public void broadcastChanges() { machine.fluids.getFluidInTank(0).getAmount() == 1000 && ((Number) snapshot.maximum()).intValue() == 4000, "Tank component supplies independent snapshots without machine getters"); - var tooltip = component.tooltip(machine, new ItemStack(Items.WATER_BUCKET)); + var tooltip = component.tooltip(machine, + new ItemStack(Items.WATER_BUCKET)); require( - tooltip.size() == 4 - && tooltip.getLast().getStyle().getColor() - .getValue() == net.minecraft.ChatFormatting.BLUE.getColor(), + tooltip.size() == 4 && tooltip.getLast().getStyle().getColor() + .getValue() == net.minecraft.ChatFormatting.BLUE.getColor(), "Handwritten tank uses the component tooltip without a machine callback"); menu.setCarried(new ItemStack(Items.LAVA_BUCKET)); menu.clickMenuButton(player, left); @@ -1712,7 +1792,8 @@ public void broadcastChanges() { menu.setCarried(new ItemStack(Items.BUCKET, 2)); menu.clickMenuButton(player, right); require( - menu.getCarried().is(Items.BUCKET) && menu.getCarried().getCount() == 1, + menu.getCarried().is(Items.BUCKET) + && menu.getCarried().getCount() == 1, "A stacked cursor processes exactly one container"); require( player.getInventory().countItem(Items.WATER_BUCKET) == 1 @@ -1720,59 +1801,58 @@ public void broadcastChanges() { "The filled bucket goes to inventory without duplicating water"); var container = new ItemStack(Items.GLASS_BOTTLE); - FluidUtil.getFluidHandler(container) - .orElseThrow() + FluidUtil.getFluidHandler(container).orElseThrow() .fill(new FluidStack(Fluids.WATER, 1_500), FluidAction.EXECUTE); - machine.fluids.fill(new FluidStack(Fluids.WATER, 3_500), FluidAction.EXECUTE); + machine.fluids.fill(new FluidStack(Fluids.WATER, 3_500), + FluidAction.EXECUTE); menu.setCarried(container); menu.clickMenuButton(player, left); require( machine.fluids.getFluidInTank(0).getAmount() == 4_000 - && FluidUtil.getFluidHandler(menu.getCarried()) - .orElseThrow() - .getFluidInTank(0) - .getAmount() == 1_000, + && FluidUtil.getFluidHandler(menu.getCarried()).orElseThrow() + .getFluidInTank(0).getAmount() == 1_000, "A modded container pours only the available room and retains its remainder"); machine.fluids.drain(Integer.MAX_VALUE, FluidAction.EXECUTE); menu.clickMenuButton(player, left); require( machine.fluids.getFluidInTank(0).getAmount() == 1_000 - && FluidUtil.getFluidHandler(menu.getCarried()) - .orElseThrow() - .getFluidInTank(0) - .isEmpty(), + && FluidUtil.getFluidHandler(menu.getCarried()).orElseThrow() + .getFluidInTank(0).isEmpty(), "The partial container can be completely emptied"); menu.clickMenuButton(player, right); require( machine.fluids.getFluidInTank(0).isEmpty() - && FluidUtil.getFluidHandler(menu.getCarried()) - .orElseThrow() - .getFluidInTank(0) - .getAmount() == 1_000, + && FluidUtil.getFluidHandler(menu.getCarried()).orElseThrow() + .getFluidInTank(0).getAmount() == 1_000, "An empty modded container can drain less than its capacity"); menu.setCarried(new ItemStack(Items.IRON_INGOT)); menu.clickMenuButton(player, left); require( - menu.getCarried().is(Items.IRON_INGOT) && machine.fluids.getFluidInTank(0).isEmpty(), + menu.getCarried().is(Items.IRON_INGOT) + && machine.fluids.getFluidInTank(0).isEmpty(), "Non-container items are unchanged"); menu.setCarried(new ItemStack(Items.WATER_BUCKET)); player.setPos(pos.getX() + 100, pos.getY(), pos.getZ()); menu.clickMenuButton(player, left); require( - machine.fluids.getFluidInTank(0).isEmpty() && menu.getCarried().is(Items.WATER_BUCKET), + machine.fluids.getFluidInTank(0).isEmpty() + && menu.getCarried().is(Items.WATER_BUCKET), "A remote stale menu cannot transfer fluid"); helper.getLevel().setBlock(pos, Blocks.AIR.defaultBlockState(), 3); helper.succeed(); } - private static ProcessorEntity place(GameTestHelper helper, CompanionMod.Machine machine) { + private static ProcessorEntity place(GameTestHelper helper, + CompanionMod.Machine machine) { var pos = helper.absolutePos(CENTER); - helper.getLevel().setBlock(pos, machine.block.get().defaultBlockState(), 3); + helper.getLevel().setBlock(pos, machine.block.get().defaultBlockState(), + 3); return (ProcessorEntity) helper.getLevel().getBlockEntity(pos); } @GameTest(template = "empty") - public static void gaugeTargetsExactlyOneTankAndProtectsOutputSpace(GameTestHelper helper) { + public static void gaugeTargetsExactlyOneTankAndProtectsOutputSpace( + GameTestHelper helper) { var machine = place(helper, CompanionMod.LOGISTICS); var player = helper.makeMockServerPlayerInLevel(); var pos = machine.getBlockPos(); @@ -1784,61 +1864,63 @@ public void broadcastChanges() { } }; player.containerMenu = menu; - int input = menu.index(menu.bound.screen().find("tank_1").orElseThrow()); - int output = menu.index(menu.bound.screen().find("tank_2").orElseThrow()); + int input = menu + .index(menu.bound.screen().find("tank_1").orElseThrow()); + int output = menu + .index(menu.bound.screen().find("tank_2").orElseThrow()); menu.setCarried(new ItemStack(Items.WATER_BUCKET)); menu.action(player, new ScreenNetwork.Action(48, input, 0, 0, 0, 0)); require( machine.fluids.getFluidInTank(0).isEmpty() && machine.fluids.getFluidInTank(1).getAmount() == 1000, "Second gauge fills only second tank"); - machine.fluids.setPlan( - 1, - new zone.moddev.mc.lib.machine.inventory.TankPlan( - 100, zone.moddev.mc.lib.machine.ingredient.FluidAmount.EMPTY)); + machine.fluids.setPlan(1, + new zone.moddev.mc.lib.machine.inventory.TankPlan(100, + zone.moddev.mc.lib.machine.ingredient.FluidAmount.EMPTY)); menu.setCarried(new ItemStack(Items.GLASS_BOTTLE)); menu.action(player, new ScreenNetwork.Action(48, input, 0, 1, 0, 0)); require( machine.fluids.getFluidInTank(1).getAmount() == 100 - && FluidUtil.getFluidHandler(menu.getCarried()) - .orElseThrow() - .getFluidInTank(0) - .getAmount() == 900, + && FluidUtil.getFluidHandler(menu.getCarried()).orElseThrow() + .getFluidInTank(0).getAmount() == 900, "Partial container takes exactly the unreserved input fluid"); menu.action(player, new ScreenNetwork.Action(48, input, 0, 0, 0, 0)); - machine.fluids.setPlan(1, zone.moddev.mc.lib.machine.inventory.TankPlan.NONE); + machine.fluids.setPlan(1, + zone.moddev.mc.lib.machine.inventory.TankPlan.NONE); menu.setCarried(new ItemStack(Items.BUCKET)); menu.action(player, new ScreenNetwork.Action(48, input, 0, 1, 0, 0)); require( - menu.getCarried().is(Items.WATER_BUCKET) && machine.fluids.getFluidInTank(1).isEmpty(), + menu.getCarried().is(Items.WATER_BUCKET) + && machine.fluids.getFluidInTank(1).isEmpty(), "Input tank can be drained manually"); menu.action(player, new ScreenNetwork.Action(48, output, 0, 0, 0, 0)); require( - machine.fluids.getFluidInTank(2).isEmpty() && menu.getCarried().is(Items.WATER_BUCKET), + machine.fluids.getFluidInTank(2).isEmpty() + && menu.getCarried().is(Items.WATER_BUCKET), "Output tank rejects player insertion"); - machine.fluids.setPlan( - 1, - new zone.moddev.mc.lib.machine.inventory.TankPlan( - 0, - zone.moddev.mc.lib.machine.neoforge.FluidStacks.amount( - new FluidStack(Fluids.WATER, 3500)))); + machine.fluids.setPlan(1, + new zone.moddev.mc.lib.machine.inventory.TankPlan(0, + zone.moddev.mc.lib.machine.neoforge.FluidStacks + .amount(new FluidStack(Fluids.WATER, 3500)))); menu.action(player, new ScreenNetwork.Action(48, input, 0, 0, 0, 0)); require( - machine.fluids.getFluidInTank(1).isEmpty() && menu.getCarried().is(Items.WATER_BUCKET), + machine.fluids.getFluidInTank(1).isEmpty() + && menu.getCarried().is(Items.WATER_BUCKET), "Bucket cannot consume reserved capacity"); helper.succeed(); } @GameTest(template = "empty") - public static void ordinaryLabelUsesSharedInteractionAndMenuAuthorization(GameTestHelper helper) - throws Exception { + public static void ordinaryLabelUsesSharedInteractionAndMenuAuthorization( + GameTestHelper helper) throws Exception { var machine = place(helper, CompanionMod.CRUSHER); var player = helper.makeMockServerPlayerInLevel(); var pos = machine.getBlockPos(); player.setPos(pos.getX() + .5, pos.getY(), pos.getZ() + .5); player.getAbilities().instabuild = false; var saved = zone.moddev.mc.lib.gui.Screens.all(); - var id = net.minecraft.resources.ResourceLocation.parse("mmdlib_companion:crusher"); + var id = net.minecraft.resources.ResourceLocation + .parse("mmdlib_companion:crusher"); String xml = """ @@ -1849,13 +1931,11 @@ public static void ordinaryLabelUsesSharedInteractionAndMenuAuthorization(GameTe """; - var screen = zone.moddev.mc.lib.gui.load.ScreenReader.read( - id, - "interaction-test.xml", - new java.io.ByteArrayInputStream( + var screen = zone.moddev.mc.lib.gui.load.ScreenReader.read(id, + "interaction-test.xml", new java.io.ByteArrayInputStream( xml.getBytes(java.nio.charset.StandardCharsets.UTF_8))); - var bound = zone.moddev.mc.lib.gui.load.ScreenBinder.bind( - screen, CompanionGameTests.class.getClassLoader()); + var bound = zone.moddev.mc.lib.gui.load.ScreenBinder.bind(screen, + CompanionGameTests.class.getClassLoader()); var changed = new java.util.HashMap<>(saved); changed.put(id, bound); try { @@ -1869,19 +1949,23 @@ public void broadcastChanges() { int target = menu.index(screen.find("transfer").orElseThrow()); int hidden = menu.index(screen.find("hidden").orElseThrow()); menu.setCarried(new ItemStack(Items.WATER_BUCKET)); - menu.action(player, new ScreenNetwork.Action(49, hidden, 0, 0, 0, 0)); - menu.action(player, new ScreenNetwork.Action(50, target, 0, 0, 0, 0)); - menu.action(player, new ScreenNetwork.Action(49, target, 0, 0, 0, 1)); - require( - machine.fluids.getFluidInTank(0).isEmpty(), + menu.action(player, + new ScreenNetwork.Action(49, hidden, 0, 0, 0, 0)); + menu.action(player, + new ScreenNetwork.Action(50, target, 0, 0, 0, 0)); + menu.action(player, + new ScreenNetwork.Action(49, target, 0, 0, 0, 1)); + require(machine.fluids.getFluidInTank(0).isEmpty(), "Hidden, wrong-menu and malformed clicks are rejected"); - menu.action(player, new ScreenNetwork.Action(49, target, 0, 0, 1, 0)); + menu.action(player, + new ScreenNetwork.Action(49, target, 0, 0, 1, 0)); require( machine.fluids.getFluidInTank(0).getAmount() == 1000 && menu.getCarried().is(Items.BUCKET), "Ordinary label runs shared interaction with modifiers"); player.containerMenu = player.inventoryMenu; - menu.action(player, new ScreenNetwork.Action(49, target, 0, 1, 0, 0)); + menu.action(player, + new ScreenNetwork.Action(49, target, 0, 1, 0, 0)); require(machine.fluids.getFluidInTank(0).getAmount() == 1000, "Closed menu cannot transfer"); } finally { diff --git a/src/companionTest/java/zone/moddev/mc/companion/test/CompanionWidgetSmoke.java b/src/companionTest/java/zone/moddev/mc/companion/test/CompanionWidgetSmoke.java index 8372264a..d055025c 100644 --- a/src/companionTest/java/zone/moddev/mc/companion/test/CompanionWidgetSmoke.java +++ b/src/companionTest/java/zone/moddev/mc/companion/test/CompanionWidgetSmoke.java @@ -12,7 +12,10 @@ import zone.moddev.mc.lib.gui.model.elements.CustomElement; import zone.moddev.mc.lib.gui.model.elements.SlotElement; -/** Real client focus, text editing, captures, resize, navigation and acknowledged actions. */ +/** + * Real client focus, text editing, captures, resize, navigation and + * acknowledged actions. + */ final class CompanionWidgetSmoke { private static int step; private static zone.moddev.mc.lib.gui.layout.Rect compactPanel; @@ -23,7 +26,8 @@ private CompanionWidgetSmoke() { private static void click(MachineScreen s, double x, double y) { if (!s.mouseClicked(x, y, 0)) - throw new AssertionError("Widget click not handled at step " + step); + throw new AssertionError( + "Widget click not handled at step " + step); s.mouseReleased(x, y, 0); } @@ -34,22 +38,29 @@ private static void type(MachineScreen s, String text) { for (char c : text.toCharArray()) { s.keyPressed(Character.toUpperCase(c), 0, 0); if (!s.charTyped(c, 0)) - throw new AssertionError("Text field lost focus at step " + step); + throw new AssertionError( + "Text field lost focus at step " + step); } if (Minecraft.getInstance().screen != s) - throw new AssertionError("Inventory shortcut closed focused editor"); + throw new AssertionError( + "Inventory shortcut closed focused editor"); } - private static boolean nameIs(MachineScreen s, CustomElement e, String name) { - return s.getMenu().value(e, false) instanceof Component c && c.getString().equals(name); + private static boolean nameIs(MachineScreen s, CustomElement e, + String name) { + return s.getMenu().value(e, false) instanceof Component c + && c.getString().equals(name); } static boolean run(MachineScreen s) { var mc = Minecraft.getInstance(); var menu = s.getMenu(); - var name = (CustomElement) menu.bound.screen().find("name_editor").orElseThrow(); - var slider = (CustomElement) menu.bound.screen().find("reserve_slider").orElseThrow(); - var selector = (CustomElement) menu.bound.screen().find("lane_selector").orElseThrow(); + var name = (CustomElement) menu.bound.screen().find("name_editor") + .orElseThrow(); + var slider = (CustomElement) menu.bound.screen().find("reserve_slider") + .orElseThrow(); + var selector = (CustomElement) menu.bound.screen().find("lane_selector") + .orElseThrow(); var n = s.bounds(name); var r = s.bounds(slider); var l = s.bounds(selector); @@ -59,10 +70,11 @@ static boolean run(MachineScreen s) { if (slot != originalSlots.get(i)) throw new AssertionError("View change replaced menu slot"); if (slot.container == mc.player.getInventory()) { - var expected = PlayerInventoryElement.slot( - s.layout.region(PlayerInventoryElement.INVENTORY).orElseThrow(), + var expected = PlayerInventoryElement.slot(s.layout + .region(PlayerInventoryElement.INVENTORY).orElseThrow(), slot.getSlotIndex()); - if (slot.x != expected.x() + 1 || slot.y != expected.y() + 1) + if (slot.x != expected.x() + 1 + || slot.y != expected.y() + 1) throw new AssertionError( "Resized player slot does not match its background"); } @@ -85,18 +97,22 @@ static boolean run(MachineScreen s) { case 0 -> { compactPanel = s.panelBounds(); originalSlots = java.util.List.copyOf(menu.slots); - Screenshot.grab( - mc.gameDirectory, "widget-collapsed.png", mc.getMainRenderTarget(), message -> { + Screenshot.grab(mc.gameDirectory, "widget-collapsed.png", + mc.getMainRenderTarget(), message -> { }); - var open = s.bounds(menu.bound.screen().find("open_settings").orElseThrow()); + var open = s.bounds( + menu.bound.screen().find("open_settings").orElseThrow()); click(s, open.x() + 8, open.y() + 8); } case 1 -> { - if (!s.visible(name) || !(menu.value(name, false) instanceof Component)) + if (!s.visible(name) + || !(menu.value(name, false) instanceof Component)) return false; if (s.panelBounds().height() <= compactPanel.height()) - throw new AssertionError("Settings did not expand the panel"); - // A recipe browser may remove the screen and later reinitialize the same object. + throw new AssertionError( + "Settings did not expand the panel"); + // A recipe browser may remove the screen and later reinitialize + // the same object. s.removed(); s.resize(mc, s.width, s.height); click(s, n.x() + 8, n.y() + 8); @@ -111,22 +127,26 @@ static boolean run(MachineScreen s) { case 2 -> { if (menu.widgetPending(name) || !nameIs(s, name, "draft ex")) return false; - // Bypass local validation to exercise a rejected server edit and its + // Bypass local validation to exercise a rejected server edit + // and its // acknowledgment. PacketDistributor.sendToServer( - menu.prepareWidgetAction(name, CompanionWidgets.RENAME, "").orElseThrow()); + menu.prepareWidgetAction(name, CompanionWidgets.RENAME, "") + .orElseThrow()); } case 3 -> { if (menu.widgetPending(name)) return false; if (!nameIs(s, name, "draft ex")) - throw new AssertionError("Rejected edit changed server name"); - Screenshot.grab( - mc.gameDirectory, "widget-rejection.png", mc.getMainRenderTarget(), message -> { + throw new AssertionError( + "Rejected edit changed server name"); + Screenshot.grab(mc.gameDirectory, "widget-rejection.png", + mc.getMainRenderTarget(), message -> { }); click(s, n.x() + 8, n.y() + 8); type(s, "discard"); - s.keyPressed(256, 0, 0); // Cancel draft, retaining the editor and screen. + s.keyPressed(256, 0, 0); // Cancel draft, retaining the editor + // and screen. s.keyPressed(257, 0, 0); } case 4 -> { @@ -136,7 +156,8 @@ static boolean run(MachineScreen s) { throw new AssertionError("Escape committed a draft"); click(s, n.x() + 8, n.y() + 8); type(s, "hidden draft"); - var context = s.bounds(menu.bound.screen().find("panel").orElseThrow()); + var context = s + .bounds(menu.bound.screen().find("panel").orElseThrow()); click(s, context.x() + 5, context.y() + 5); } case 5 -> { @@ -146,25 +167,30 @@ static boolean run(MachineScreen s) { throw new AssertionError( "Closing Settings did not restore compact panel bounds"); if (s.charTyped('z', 0)) - throw new AssertionError("Hidden widget retained keyboard focus"); - var open = s.bounds(menu.bound.screen().find("open_settings").orElseThrow()); + throw new AssertionError( + "Hidden widget retained keyboard focus"); + var open = s.bounds( + menu.bound.screen().find("open_settings").orElseThrow()); click(s, open.x() + 8, open.y() + 8); } case 6 -> { if (!s.visible(name)) return false; - click(s, n.x() + 20, n.y() + 28); // Hidden draft must have been discarded. + click(s, n.x() + 20, n.y() + 28); // Hidden draft must have been + // discarded. } case 7 -> { if (menu.widgetPending(name)) return false; if (!nameIs(s, name, "draft ex")) - throw new AssertionError("Hidden draft survived cancellation"); + throw new AssertionError( + "Hidden draft survived cancellation"); s.mouseClicked(r.x() + 8, r.y() + 20, 0); s.mouseDragged(r.right() + 200, r.bottom() + 100, 0, 200, 100); if (menu.widgetPending(slider) || ((Number) menu.value(slider, false)).intValue() != 0) - throw new AssertionError("Drag preview was sent before release"); + throw new AssertionError( + "Drag preview was sent before release"); s.mouseReleased(r.right() + 200, r.bottom() + 100, 0); } case 8 -> { @@ -175,9 +201,11 @@ static boolean run(MachineScreen s) { s.resize(mc, s.width, s.height); s.mouseReleased(r.x() + 10, r.y() + 20, 0); if (menu.widgetPending(slider)) - throw new AssertionError("Resize committed a cancelled drag"); + throw new AssertionError( + "Resize committed a cancelled drag"); s.mouseClicked(r.x() + 20, r.y() + 20, 0); - menu.navigate("panel", "machine"); // Simulate a server-driven view change mid-drag. + menu.navigate("panel", "machine"); // Simulate a server-driven + // view change mid-drag. s.mouseReleased(r.x() + 20, r.y() + 20, 0); if (menu.widgetPending(slider)) throw new AssertionError("Hidden drag committed"); @@ -202,8 +230,8 @@ static boolean run(MachineScreen s) { if (menu.widgetPending(selector) || !Boolean.TRUE.equals(menu.value(selector, false))) return false; - Screenshot.grab( - mc.gameDirectory, "widget-settings.png", mc.getMainRenderTarget(), message -> { + Screenshot.grab(mc.gameDirectory, "widget-settings.png", + mc.getMainRenderTarget(), message -> { }); click(s, l.x() + 20, l.y() + 22); } @@ -211,61 +239,67 @@ static boolean run(MachineScreen s) { if (menu.widgetPending(selector) || !Boolean.FALSE.equals(menu.value(selector, false))) return false; - click(s, r.x() + 3, r.y() + 20); // Restore reserve for the remaining machine tests. + click(s, r.x() + 3, r.y() + 20); // Restore reserve for the + // remaining machine tests. } case 14 -> { if (menu.widgetPending(slider) || ((Number) menu.value(slider, false)).intValue() != 0) return false; - var context = s.bounds(menu.bound.screen().find("panel").orElseThrow()); + var context = s + .bounds(menu.bound.screen().find("panel").orElseThrow()); click(s, context.x() + 5, context.y() + 5); } case 15 -> { if (s.visible(name)) return false; - var open = s.bounds(menu.bound.screen().find("open_settings").orElseThrow()); + var open = s.bounds( + menu.bound.screen().find("open_settings").orElseThrow()); click(s, open.x() + 8, open.y() + 8); } case 16 -> { if (!s.visible(name)) return false; - var delayed = menu.prepareWidgetAction(name, CompanionWidgets.RENAME, "Late edit") - .orElseThrow(); - // A recipe browser can cover the machine while its menu remains open. Let real + var delayed = menu.prepareWidgetAction(name, + CompanionWidgets.RENAME, "Late edit").orElseThrow(); + // A recipe browser can cover the machine while its menu remains + // open. Let real // client - // ticks expire the request, then deliver it late to check ordinary value + // ticks expire the request, then deliver it late to check + // ordinary value // convergence. - mc.setScreen( - new net.minecraft.client.gui.screens.Screen(Component.literal("Cover")) { - private int ticks; + mc.setScreen(new net.minecraft.client.gui.screens.Screen( + Component.literal("Cover")) { + private int ticks; - @Override - public boolean isPauseScreen() { - return false; - } + @Override + public boolean isPauseScreen() { + return false; + } - @Override - public void tick() { - if (++ticks < 105) - return; - if (menu.widgetPending(name)) - throw new AssertionError("Covered screen left the editor pending"); - PacketDistributor.sendToServer(delayed); - mc.setScreen(s); - } - }); + @Override + public void tick() { + if (++ticks < 105) + return; + if (menu.widgetPending(name)) + throw new AssertionError( + "Covered screen left the editor pending"); + PacketDistributor.sendToServer(delayed); + mc.setScreen(s); + } + }); } case 17 -> { if (!nameIs(s, name, "Late edit")) return false; - PacketDistributor.sendToServer( - menu.prepareWidgetAction(name, CompanionWidgets.RENAME, "draft ex") - .orElseThrow()); + PacketDistributor.sendToServer(menu.prepareWidgetAction(name, + CompanionWidgets.RENAME, "draft ex").orElseThrow()); } case 18 -> { if (menu.widgetPending(name) || !nameIs(s, name, "draft ex")) return false; - var context = s.bounds(menu.bound.screen().find("panel").orElseThrow()); + var context = s + .bounds(menu.bound.screen().find("panel").orElseThrow()); click(s, context.x() + 5, context.y() + 5); } default -> { diff --git a/src/companionTest/java/zone/moddev/mc/companion/test/ContainerCellClientSmoke.java b/src/companionTest/java/zone/moddev/mc/companion/test/ContainerCellClientSmoke.java index 02812fdc..d8782c2d 100644 --- a/src/companionTest/java/zone/moddev/mc/companion/test/ContainerCellClientSmoke.java +++ b/src/companionTest/java/zone/moddev/mc/companion/test/ContainerCellClientSmoke.java @@ -30,11 +30,15 @@ import zone.moddev.mc.lib.gui.runtime.component.BuiltinMenuComponents; import zone.moddev.mc.lib.gui.runtime.slot.MenuSlots; -/** Uses the real drawer factories and synchronized pages, without replacing their gesture code. */ +/** + * Uses the real drawer factories and synchronized pages, without replacing + * their gesture code. + */ final class ContainerCellClientSmoke { private static int mode, hitCalls, headings, labels; private static boolean inputProbe; - private static int clicks, drags, releases, keys, keyUps, characters, scrolls, cancels, enters; + private static int clicks, drags, releases, keys, keyUps, characters, + scrolls, cancels, enters; private static boolean focused; private static int probeIndex = -1; private static final Set checked = new HashSet<>(); @@ -54,11 +58,14 @@ public Rect cellHitBounds(Rect frame) { : super.cellHitBounds(frame); } - public List nativeTooltip(ItemStack stack, List vanilla) { + public List nativeTooltip(ItemStack stack, + List vanilla) { stack.setCount(1); return mode == 0 ? vanilla - : mode == 1 ? List.of(Component.literal("native cell replacement")) : List.of(); + : mode == 1 + ? List.of(Component.literal("native cell replacement")) + : List.of(); } public void appendTooltip(TooltipContext c, List rows) { @@ -77,7 +84,8 @@ public List headings() { return super.headings(); } - public Component cellLabel(zone.moddev.mc.lib.gui.model.elements.SlotElement slot) { + public Component cellLabel( + zone.moddev.mc.lib.gui.model.elements.SlotElement slot) { labels++; return super.cellLabel(slot); } @@ -89,8 +97,7 @@ public void appendChildTooltip(TooltipContext c, List rows) { static void install() { for (var type : List.of(BuiltinDrawers.BACKPACK, BuiltinDrawers.CURIOS)) - DrawerFactories.GLOBAL.replace( - type, + DrawerFactories.GLOBAL.replace(type, (screen, definition) -> new ContainerDrawer(screen, definition, type.equals(BuiltinDrawers.CURIOS)) { protected ContainerInventoryElement createInventory( @@ -100,11 +107,8 @@ protected ContainerInventoryElement createInventory( }); var base = ElementRenderers.GLOBAL.require(ContainerSlotElement.class); for (var pass : ElementRenderers.Pass.values()) - ElementRenderers.GLOBAL.register( - pass, - Cell.class, - ElementRenderers.Clip.ELEMENT, - (cell, c) -> { + ElementRenderers.GLOBAL.register(pass, Cell.class, + ElementRenderers.Clip.ELEMENT, (cell, c) -> { passes.add(pass); if (pass == ElementRenderers.Pass.BACKGROUND) { int before = hitCalls; @@ -114,16 +118,11 @@ protected ContainerInventoryElement createInventory( "Default hover renderer consults overridable cell hits"); } if (pass == ElementRenderers.Pass.TOP) - c.graphics() - .renderOutline( - c.bounds().x(), - c.bounds().y(), - c.bounds().width(), - c.bounds().height(), - 0xff55aacc); + c.graphics().renderOutline(c.bounds().x(), + c.bounds().y(), c.bounds().width(), + c.bounds().height(), 0xff55aacc); }); - ElementBehaviors.GLOBAL.register( - Cell.class, + ElementBehaviors.GLOBAL.register(Cell.class, (cell, access) -> new SlotBehavior(cell, access) { private boolean probing() { return inputProbe && cell.index() == probeIndex; @@ -144,7 +143,8 @@ public Click mouseClicked(double x, double y, int button) { return Click.CAPTURE; } - public boolean mouseDragged(double x, double y, int button, double dx, double dy) { + public boolean mouseDragged(double x, double y, int button, + double dx, double dy) { drags++; return true; } @@ -169,7 +169,8 @@ public boolean charTyped(char value, int modifiers) { return true; } - public boolean mouseScrolled(double x, double y, double dx, double dy) { + public boolean mouseScrolled(double x, double y, double dx, + double dy) { if (!probing()) return false; scrolls++; @@ -185,31 +186,30 @@ public void hoverChanged(boolean value) { enters++; } }); - ElementBehaviors.GLOBAL.register( - Inventory.class, + ElementBehaviors.GLOBAL.register(Inventory.class, (element, access) -> new DefaultElementBehavior(access) { - public void appendChildTooltip(TooltipContext c, List rows) { + public void appendChildTooltip(TooltipContext c, + List rows) { rows.add(Component.literal("parent behavior row")); } }); } - static void rendered(MachineScreen screen, net.minecraft.client.gui.GuiGraphics graphics) { + static void rendered(MachineScreen screen, + net.minecraft.client.gui.GuiGraphics graphics) { for (boolean equipment : new boolean[]{false, true}) { String id = equipment ? "curios" : "backpack"; if (checked.contains(id)) continue; - var model = screen - .getMenu() - .components() - .require(equipment ? BuiltinMenuComponents.CURIOS : BuiltinMenuComponents.BACKPACK); + var model = screen.getMenu().components() + .require(equipment + ? BuiltinMenuComponents.CURIOS + : BuiltinMenuComponents.BACKPACK); var state = model.state(); if (!state.open() || state.contents().isEmpty() || model.pending()) continue; var drawer = (ContainerDrawer) screen.drawers().entries().stream() - .filter(d -> d.id().equals(id)) - .findFirst() - .orElseThrow(); + .filter(d -> d.id().equals(id)).findFirst().orElseThrow(); var layout = drawer.contentLayout(); if (state.pageSize() != layout.pageSize() || passes.size() != 4) continue; @@ -226,27 +226,28 @@ static void rendered(MachineScreen screen, net.minecraft.client.gui.GuiGraphics var before = state.contents().get(occupied).copy(); try { for (mode = 0; mode < 3; mode++) { - var rows = drawer.cellTooltip(cell.x() + 8, cell.y() + 8).stream() - .map(Component::getString) - .toList(); + var rows = drawer.cellTooltip(cell.x() + 8, cell.y() + 8) + .stream().map(Component::getString).toList(); require( rows.contains("cell row") && rows.contains("parent cell row") && rows.contains("parent behavior row"), "Parent rows survive every native tooltip mode"); require( - mode != 1 || rows.getFirst().equals("native cell replacement"), + mode != 1 || rows.getFirst() + .equals("native cell replacement"), "Native replacement used"); - require( - mode != 2 || !rows.contains(before.getHoverName().getString()), + require(mode != 2 + || !rows.contains(before.getHoverName().getString()), "Native rows suppressed"); require( - ItemStack.matches(before, state.contents().get(occupied)), + ItemStack.matches(before, + state.contents().get(occupied)), "Tooltip mutation cannot alter snapshot"); if (equipment) - for (var row : state.descriptions().get(occupied).tooltip()) - require( - rows.contains(row.getString()), + for (var row : state.descriptions().get(occupied) + .tooltip()) + require(rows.contains(row.getString()), "Provider tooltip retained on occupied cells"); } } finally { @@ -254,8 +255,7 @@ static void rendered(MachineScreen screen, net.minecraft.client.gui.GuiGraphics } var blank = layout.cell(empty); var rows = drawer.cellTooltip(blank.x() + 8, blank.y() + 8).stream() - .map(Component::getString) - .toList(); + .map(Component::getString).toList(); require( rows.contains("parent cell row") && rows.contains("cell row"), "Empty cells compose rows"); @@ -264,7 +264,8 @@ static void rendered(MachineScreen screen, net.minecraft.client.gui.GuiGraphics try { require( drawer.cellTooltip(first.x() - 2, first.y() + 6).stream() - .anyMatch(row -> row.getString().equals("parent cell row")), + .anyMatch( + row -> row.getString().equals("parent cell row")), "Expanded hit outside frame composes parent rows"); require( drawer.cellTooltip(first.x() + 8, first.y() + 8).isEmpty(), @@ -274,19 +275,20 @@ static void rendered(MachineScreen screen, net.minecraft.client.gui.GuiGraphics } input(screen, drawer, model, first); presentation(); - require(headings > 0 && labels > 0, "Renderer consults inventory presentation hooks"); + require(headings > 0 && labels > 0, + "Renderer consults inventory presentation hooks"); nativeHits(); checked.add(id); var mc = Minecraft.getInstance(); graphics.flush(); - Screenshot.grab( - mc.gameDirectory, id + "-cell-elements.png", mc.getMainRenderTarget(), message -> { + Screenshot.grab(mc.gameDirectory, id + "-cell-elements.png", + mc.getMainRenderTarget(), message -> { }); } } - private static void input( - MachineScreen screen, ContainerDrawer drawer, ContainerDrawerMenu model, Rect frame) { + private static void input(MachineScreen screen, ContainerDrawer drawer, + ContainerDrawerMenu model, Rect frame) { var state = model.state(); var carried = screen.getMenu().getCarried().copy(); probeIndex = state.offset(); @@ -297,32 +299,26 @@ private static void input( screen.mouseMoved(frame.x() + 8, frame.y() + 8); screen.mouseMoved(x, y); require(enters == 1, "Generic hover respects expanded cell bounds"); - require( - screen.mouseClicked(x, y, 0) && clicks == 1 && focused, + require(screen.mouseClicked(x, y, 0) && clicks == 1 && focused, "Generic cell click captures and focuses outside its frame"); - require( - screen.mouseDragged(-10, -10, 0, -1, -1) && drags == 1, + require(screen.mouseDragged(-10, -10, 0, -1, -1) && drags == 1, "Capture receives drag outside drawer"); - require( - screen.mouseReleased(-10, -10, 0) && releases == 1, + require(screen.mouseReleased(-10, -10, 0) && releases == 1, "Capture receives release outside drawer"); require( - screen.keyPressed(65, 0, 0) - && screen.keyReleased(65, 0, 0) - && screen.charTyped('a', 0) - && keys == 1 - && keyUps == 1 + screen.keyPressed(65, 0, 0) && screen.keyReleased(65, 0, 0) + && screen.charTyped('a', 0) && keys == 1 && keyUps == 1 && characters == 1, "Focused cells receive keyboard, release and text input"); require(drawer.scroll(x, y, 1) && scrolls == 1, "Cell scrolling precedes page fallback"); require( - !model.pending() && ItemStack.matches(carried, screen.getMenu().getCarried()), + !model.pending() && ItemStack.matches(carried, + screen.getMenu().getCarried()), "Consumed generic input does not trigger item gestures"); screen.mouseClicked(x, y, 0); - var request = model - .request(ContainerDrawerMenu.PICKUP, state.offset() * 2, state.pageSize()) - .orElseThrow(); + var request = model.request(ContainerDrawerMenu.PICKUP, + state.offset() * 2, state.pageSize()).orElseThrow(); try { screen.mouseDragged(x, y, 0, 1, 1); screen.keyPressed(65, 0, 0); @@ -330,41 +326,25 @@ private static void input( screen.charTyped('b', 0); drawer.scroll(x, y, 1); require( - cancels > 0 - && !focused - && drags == 1 - && keys == 1 - && keyUps == 1 - && characters == 1 - && scrolls == 1, + cancels > 0 && !focused && drags == 1 && keys == 1 + && keyUps == 1 && characters == 1 && scrolls == 1, "Pending lock cancels capture and blocks callbacks"); } finally { - model.receive( - new ContainerDrawerNetwork.State( - state.menu(), - state.equipment(), - state.session(), - state.revision(), - request.request(), - state.open(), - state.selected(), - state.offset(), - state.pageSize(), - state.icons(), - state.keys(), - state.sizes(), - state.contents(), - state.descriptions(), - state.presentations())); + model.receive(new ContainerDrawerNetwork.State(state.menu(), + state.equipment(), state.session(), state.revision(), + request.request(), state.open(), state.selected(), + state.offset(), state.pageSize(), state.icons(), + state.keys(), state.sizes(), state.contents(), + state.descriptions(), state.presentations())); } - require( - screen.mouseReleased(x, y, 0) && releases == 1, + require(screen.mouseReleased(x, y, 0) && releases == 1, "Stale release after pending cancellation is consumed without committing"); screen.mouseClicked(x, y, 0); int cancelled = cancels; drawer.resized(); require( - cancels > cancelled && screen.mouseReleased(x, y, 0) && releases == 1, + cancels > cancelled && screen.mouseReleased(x, y, 0) + && releases == 1, "Resize cancels generic capture and consumes stale release"); } finally { drawer.resized(); @@ -374,14 +354,10 @@ private static void input( } private static void presentation() { - var icon = net.minecraft.resources.ResourceLocation.parse("test:override"); - var info = new EquippedContainers.SlotInfo( - "override", - 0, - false, - false, - icon, - Component.literal("overridden label"), + var icon = net.minecraft.resources.ResourceLocation + .parse("test:override"); + var info = new EquippedContainers.SlotInfo("override", 0, false, false, + icon, Component.literal("overridden label"), List.of(Component.literal("overridden row")), Component.literal("overridden placeholder")); var cell = new ContainerSlotElement(0, Optional.empty()) { @@ -390,10 +366,10 @@ public Optional description() { } }; require( - !cell.available() - && cell.icon().orElseThrow().equals(icon) + !cell.available() && cell.icon().orElseThrow().equals(icon) && cell.cellLabel().getString().equals("overridden label") - && cell.placeholder().getString().equals("overridden placeholder"), + && cell.placeholder().getString() + .equals("overridden placeholder"), "Overridden description controls default cell presentation"); } @@ -415,19 +391,23 @@ public Rect hitBounds(Rect original) { MenuSlots.Role.PLAYER_STORAGE); var hits = new NativeSlotRenderer(); require( - hits.hit(slots, 9, 22) == nativeSlot && hits.hit(slots, 24, 24) == null, + hits.hit(slots, 9, 22) == nativeSlot + && hits.hit(slots, 24, 24) == null, "Custom hits and tolerance replace fixed vanilla bounds"); active[0] = false; require( - hits.hit(slots, 9, 22) == null && hits.hit(slots, 9, 22, false) == nativeSlot, + hits.hit(slots, 9, 22) == null + && hits.hit(slots, 9, 22, false) == nativeSlot, "Pending geometry stays blocked without enabling interaction"); } static void verify() { if (BackpackFixture.installed()) - require(checked.contains("backpack"), "Backpack cell policies were checked"); + require(checked.contains("backpack"), + "Backpack cell policies were checked"); if (net.neoforged.fml.ModList.get().isLoaded("curios")) - require(checked.contains("curios"), "Equipment cell policies were checked"); + require(checked.contains("curios"), + "Equipment cell policies were checked"); } private static void require(boolean value, String message) { diff --git a/src/companionTest/java/zone/moddev/mc/companion/test/ContributedHintClientSmoke.java b/src/companionTest/java/zone/moddev/mc/companion/test/ContributedHintClientSmoke.java index 76d4b7ce..9a3682e4 100644 --- a/src/companionTest/java/zone/moddev/mc/companion/test/ContributedHintClientSmoke.java +++ b/src/companionTest/java/zone/moddev/mc/companion/test/ContributedHintClientSmoke.java @@ -28,7 +28,10 @@ import zone.moddev.mc.lib.gui.runtime.component.BuiltinMenuComponents; import zone.moddev.mc.lib.gui.runtime.component.MenuComponentRegistry; -/** Hints for elements outside the screen tree, through the real server snapshot and drawer. */ +/** + * Hints for elements outside the screen tree, through the real server snapshot + * and drawer. + */ final class ContributedHintClientSmoke { private static volatile boolean enabled = true; private static int stage, frames, nativeMode; @@ -40,9 +43,7 @@ private static class ProbeSlot extends AutomationSlotElement { final String inventory; ProbeSlot(int index, boolean client, String inventory) { - super( - AutomationInventoryElement.defaultSlot(index).common(), - index, + super(AutomationInventoryElement.defaultSlot(index).common(), index, AutomationInventoryElement.defaultSlot(index).appearance()); this.client = client; this.inventory = inventory; @@ -51,31 +52,41 @@ private static class ProbeSlot extends AutomationSlotElement { public zone.moddev.mc.lib.gui.layout.Rect hitBounds( zone.moddev.mc.lib.gui.layout.Rect bounds) { if (index() == 0) - return new zone.moddev.mc.lib.gui.layout.Rect(bounds.x() - 6, bounds.y(), 22, 16); + return new zone.moddev.mc.lib.gui.layout.Rect(bounds.x() - 6, + bounds.y(), 22, 16); if (index() == 2) - return new zone.moddev.mc.lib.gui.layout.Rect(bounds.x() + 4, bounds.y() + 4, 6, 6); + return new zone.moddev.mc.lib.gui.layout.Rect(bounds.x() + 4, + bounds.y() + 4, 6, 6); return super.hitBounds(bounds); } public ItemStack hint(ScreenHost host, String name) { - require(!client && name.equals(inventory), "Server-only contributed hint scope"); - return enabled && index() == 0 ? new ItemStack(Items.DIAMOND, 2) : ItemStack.EMPTY; + require(!client && name.equals(inventory), + "Server-only contributed hint scope"); + return enabled && index() == 0 + ? new ItemStack(Items.DIAMOND, 2) + : ItemStack.EMPTY; } public List tooltip(ScreenHost host, ItemStack carried) { - require(!client, "Server slot tooltip is never sampled on the client"); + require(!client, + "Server slot tooltip is never sampled on the client"); return List.of(Component.literal("server drawer slot")); } - public void appendTooltip(TooltipContext context, List rows) { + public void appendTooltip(TooltipContext context, + List rows) { super.appendTooltip(context, rows); rows.add(Component.literal("client drawer slot")); } - public List nativeTooltip(ItemStack stack, List vanilla) { + public List nativeTooltip(ItemStack stack, + List vanilla) { return nativeMode == 0 ? vanilla - : nativeMode == 1 ? List.of(Component.literal("custom native row")) : List.of(); + : nativeMode == 1 + ? List.of(Component.literal("custom native row")) + : List.of(); } } @@ -91,8 +102,8 @@ public boolean slotVisible(SlotElement slot, Presentation context) { return slot.index() != 1 && super.slotVisible(slot, context); } - public Object drawerValue( - zone.moddev.mc.lib.gui.model.Element element, boolean maximum, Presentation context) { + public Object drawerValue(zone.moddev.mc.lib.gui.model.Element element, + boolean maximum, Presentation context) { if (element == slots().get(2)) return maximum ? 888 : 777; if (element == this && !maximum) @@ -101,11 +112,13 @@ public Object drawerValue( } public List tooltip(ScreenHost host, ItemStack carried) { - require(!client, "Server parent tooltip is never sampled on the client"); + require(!client, + "Server parent tooltip is never sampled on the client"); return List.of(Component.literal("server drawer parent")); } - public void appendChildTooltip(TooltipContext context, List rows) { + public void appendChildTooltip(TooltipContext context, + List rows) { super.appendChildTooltip(context, rows); rows.addAll(context.serverLines()); rows.add(Component.literal("client drawer parent")); @@ -113,55 +126,42 @@ public void appendChildTooltip(TooltipContext context, List rows) { } static void install() { - MenuComponentRegistry.GLOBAL.replace( - BuiltinMenuComponents.AUTOMATION, + MenuComponentRegistry.GLOBAL.replace(BuiltinMenuComponents.AUTOMATION, 0, c -> c.host() == null || c.host().screenAutomation() == null ? null - : new AutomationMenu( - c.menu(), - c.host().screenAutomation(), - c.inventory().player, - c.client(), + : new AutomationMenu(c.menu(), c.host().screenAutomation(), + c.inventory().player, c.client(), group -> new ProbeInventory(group.id(), c.client()))); var base = ElementRenderers.GLOBAL.require(SlotElement.class); for (var pass : ElementRenderers.Pass.values()) - ElementRenderers.GLOBAL.register( - pass, - ProbeSlot.class, - ElementRenderers.Clip.ELEMENT, - (slot, context) -> { - rendered - .computeIfAbsent( - slot.index(), ignored -> EnumSet.noneOf(ElementRenderers.Pass.class)) + ElementRenderers.GLOBAL.register(pass, ProbeSlot.class, + ElementRenderers.Clip.ELEMENT, (slot, context) -> { + rendered.computeIfAbsent(slot.index(), + ignored -> EnumSet.noneOf(ElementRenderers.Pass.class)) .add(pass); if (slot.index() == 2) require( - context.value().equals(777) && context.maximum().equals(888), + context.value().equals(777) + && context.maximum().equals(888), "Renderer receives overridden drawer values"); if (pass == ElementRenderers.Pass.BACKGROUND) base.accept(context); if (pass == ElementRenderers.Pass.TOP) - context - .graphics() - .renderOutline( - context.bounds().x(), - context.bounds().y(), - context.bounds().width(), - context.bounds().height(), - 0xffcc44ff); + context.graphics().renderOutline(context.bounds().x(), + context.bounds().y(), context.bounds().width(), + context.bounds().height(), 0xffcc44ff); }); - ElementBehaviors.GLOBAL.register( - ProbeSlot.class, + ElementBehaviors.GLOBAL.register(ProbeSlot.class, (element, access) -> new SlotBehavior(element, access) { public List tooltip(int x, int y) { return List.of(Component.literal("behavior drawer slot")); } }); - ElementBehaviors.GLOBAL.register( - ProbeInventory.class, + ElementBehaviors.GLOBAL.register(ProbeInventory.class, (element, access) -> new DefaultElementBehavior(access) { - public void appendChildTooltip(TooltipContext context, List rows) { + public void appendChildTooltip(TooltipContext context, + List rows) { rows.add(Component.literal("behavior drawer parent")); } }); @@ -171,7 +171,8 @@ static void rendered(MachineScreen screen, GuiGraphics graphics) { frames++; if (stage != 3 || tooltipChecked) return; - var model = screen.getMenu().components().require(BuiltinMenuComponents.AUTOMATION); + var model = screen.getMenu().components() + .require(BuiltinMenuComponents.AUTOMATION); var slot = screen.getMenu().getSlot(model.slotStart()); if (slot.getItem().isEmpty() || model.pending()) return; @@ -180,22 +181,20 @@ static void rendered(MachineScreen screen, GuiGraphics graphics) { screen.render(graphics, x, y, 0); for (nativeMode = 0; nativeMode < 3; nativeMode++) { try { - var method = MachineScreen.class.getDeclaredMethod("getTooltipFromContainerItem", - ItemStack.class); + var method = MachineScreen.class.getDeclaredMethod( + "getTooltipFromContainerItem", ItemStack.class); method.setAccessible(true); @SuppressWarnings("unchecked") - var rows = ((List) method.invoke(screen, slot.getItem())) - .stream().map(Component::getString).toList(); - var suffix = List.of( - "server drawer slot", - "client drawer slot", - "behavior drawer slot", - "server drawer parent", - "client drawer parent", - "behavior drawer parent"); + var rows = ((List) method.invoke(screen, + slot.getItem())).stream().map(Component::getString) + .toList(); + var suffix = List.of("server drawer slot", "client drawer slot", + "behavior drawer slot", "server drawer parent", + "client drawer parent", "behavior drawer parent"); require( - rows.size() >= suffix.size() - && rows.subList(rows.size() - suffix.size(), rows.size()).equals(suffix), + rows.size() >= suffix.size() && rows + .subList(rows.size() - suffix.size(), rows.size()) + .equals(suffix), "Drawer parent/behavior/server rows preserved: " + rows); if (nativeMode == 1) require( @@ -203,15 +202,16 @@ static void rendered(MachineScreen screen, GuiGraphics graphics) { && rows.size() == suffix.size() + 1, "Custom drawer native replacement"); if (nativeMode == 2) - require(rows.equals(suffix), "Native suppression preserves all composed rows"); + require(rows.equals(suffix), + "Native suppression preserves all composed rows"); if (nativeMode == 0) - require(rows.size() > suffix.size(), "Default native rows retained"); + require(rows.size() > suffix.size(), + "Default native rows retained"); if (nativeMode == 1) { graphics.flush(); var mc = Minecraft.getInstance(); - Screenshot.grab( - mc.gameDirectory, "drawer-slot-tooltip.png", mc.getMainRenderTarget(), - ignored -> { + Screenshot.grab(mc.gameDirectory, "drawer-slot-tooltip.png", + mc.getMainRenderTarget(), ignored -> { }); } } catch (ReflectiveOperationException failure) { @@ -224,7 +224,8 @@ static void rendered(MachineScreen screen, GuiGraphics graphics) { static boolean run(MachineScreen screen) { var menu = screen.getMenu(); - var automation = menu.components().require(BuiltinMenuComponents.AUTOMATION); + var automation = menu.components() + .require(BuiltinMenuComponents.AUTOMATION); if (stage == 4) return !automation.pending() && !automation.open(); var element = automation.slotElement(automation.slotStart()); @@ -234,21 +235,18 @@ static boolean run(MachineScreen screen) { if (stage == 0) { if (menu.slotHint(element).isEmpty()) return false; - require( - menu.elements.stream().noneMatch(e -> e == element), + require(menu.elements.stream().noneMatch(e -> e == element), "Hint element is absent from screen tree"); - require( - menu.nativeSlots().slot(element).getItem().isEmpty(), + require(menu.nativeSlots().slot(element).getItem().isEmpty(), "Hints do not populate actual storage"); - require( - menu.slotHint(element).getCount() == 2, + require(menu.slotHint(element).getCount() == 2, "Contributed hint arrives through typed field"); menu.slotHint(element).setCount(1); - require(menu.slotHint(element).getCount() == 2, "Hint lookup returns detached stacks"); + require(menu.slotHint(element).getCount() == 2, + "Hint lookup returns detached stacks"); var drawer = screen.drawers().entries().stream() .filter(d -> d.definition().type().equals(BuiltinDrawers.SIDES)) - .findFirst() - .orElseThrow(); + .findFirst().orElseThrow(); var tab = drawer.tabBounds(); require(screen.mouseClicked(tab.x() + 2, tab.y() + 2, 0), "Open hint drawer through tab"); @@ -259,7 +257,8 @@ static boolean run(MachineScreen screen) { if (!automation.open()) return false; if (automation.selected() != 0) { - require(automation.send("select", 0, -1, "", 0, ""), "Select hinted group"); + require(automation.send("select", 0, -1, "", 0, ""), + "Select hinted group"); return false; } frames = 0; @@ -270,25 +269,27 @@ static boolean run(MachineScreen screen) { if (frames < 3) return false; require( - rendered.getOrDefault(0, EnumSet.noneOf(ElementRenderers.Pass.class)) + rendered + .getOrDefault(0, + EnumSet.noneOf(ElementRenderers.Pass.class)) .size() == ElementRenderers.Pass.values().length, "Custom drawer slot renderer participates in every pass"); - require(!rendered.containsKey(4), "Hidden filter slots are not rendered"); + require(!rendered.containsKey(4), + "Hidden filter slots are not rendered"); require( - !rendered.containsKey(1) && !menu.getSlot(automation.slotStart() + 1).isActive(), + !rendered.containsKey(1) + && !menu.getSlot(automation.slotStart() + 1).isActive(), "Hidden addon is neither drawn nor interactive"); var drawer = screen.drawers().entries().stream() .filter(d -> d.definition().type().equals(BuiltinDrawers.SIDES)) - .findFirst() - .orElseThrow(); + .findFirst().orElseThrow(); try { var field = drawer.getClass().getDeclaredField("slotFragment"); field.setAccessible(true); var fragment = (ElementFragment) field.get(drawer); var bounds = fragment.bounds(element); - var rows = fragment.tooltip(bounds.x() + 2, bounds.y() + 2).stream() - .map(Component::getString) - .toList(); + var rows = fragment.tooltip(bounds.x() + 2, bounds.y() + 2) + .stream().map(Component::getString).toList(); require( rows.contains("server drawer slot") && rows.contains("server drawer parent") @@ -296,62 +297,55 @@ static boolean run(MachineScreen screen) { "Empty drawer slots compose element and parent rows"); require( rows.stream() - .anyMatch( - row -> row.startsWith( - Component.translatable("screen.mmdlib.automation.addon").getString() - + ":")), + .anyMatch(row -> row.startsWith(Component + .translatable("screen.mmdlib.automation.addon") + .getString() + ":")), "Default addon hint is supplied by parent inventory"); } catch (ReflectiveOperationException failure) { throw new AssertionError(failure); } - Screenshot.grab( - mc.gameDirectory, "contributed-slot-hints.png", mc.getMainRenderTarget(), - ignored -> { + Screenshot.grab(mc.gameDirectory, "contributed-slot-hints.png", + mc.getMainRenderTarget(), ignored -> { }); var small = menu.getSlot(automation.slotStart() + 2); - double sx = screen.guiLeft() + small.x + 1, sy = screen.guiTop() + small.y + 1; + double sx = screen.guiLeft() + small.x + 1, + sy = screen.guiTop() + small.y + 1; require( screen.nativeSlotAt(sx, sy) == null && drawer.click(sx, sy, 0), "Shrunken native bounds leave frame clicks to the drawer"); require( - drawer - .nativeTooltip(small, List.of(Component.literal("vanilla")), (int) sx, (int) sy) + drawer.nativeTooltip(small, + List.of(Component.literal("vanilla")), (int) sx, (int) sy) .isEmpty(), "Frame-only hover cannot claim a native tooltip"); - mc.getSingleplayerServer() - .execute( - () -> { - enabled = false; - var player = mc.getSingleplayerServer().getPlayerList().getPlayers() - .getFirst(); - var serverMenu = (zone.moddev.mc.lib.gui.runtime.MachineMenu) player.containerMenu; - serverMenu - .components() - .require(BuiltinMenuComponents.AUTOMATION).inventory - .slots() - .setStackInSlot( - 0, new ItemStack( - zone.moddev.mc.companion.CompanionAddons.PULLER.get())); - serverMenu.broadcastChanges(); - }); + mc.getSingleplayerServer().execute(() -> { + enabled = false; + var player = mc.getSingleplayerServer().getPlayerList() + .getPlayers().getFirst(); + var serverMenu = (zone.moddev.mc.lib.gui.runtime.MachineMenu) player.containerMenu; + serverMenu.components() + .require(BuiltinMenuComponents.AUTOMATION).inventory.slots() + .setStackInSlot(0, new ItemStack( + zone.moddev.mc.companion.CompanionAddons.PULLER.get())); + serverMenu.broadcastChanges(); + }); stage = 3; return false; } var slot = menu.slot(element); - double x = screen.guiLeft() + slot.x - 4, y = screen.guiTop() + slot.y + 8; + double x = screen.guiLeft() + slot.x - 4, + y = screen.guiTop() + slot.y + 8; if (stage == 3) { if (!menu.slotHint(element).isEmpty() || !tooltipChecked) return false; - require(automation.send("select", 0, -1, "", 0, ""), "Begin pending request"); - require( - automation.visible(0, element) && !slot.isActive(), + require(automation.send("select", 0, -1, "", 0, ""), + "Begin pending request"); + require(automation.visible(0, element) && !slot.isActive(), "Pending interaction lock is independent of rendering"); var drawer = screen.drawers().entries().stream() .filter(d -> d.definition().type().equals(BuiltinDrawers.SIDES)) - .findFirst() - .orElseThrow(); - require( - screen.nativeSlotAt(x, y) == null && drawer.click(x, y, 0), + .findFirst().orElseThrow(); + require(screen.nativeSlotAt(x, y) == null && drawer.click(x, y, 0), "Pending expanded slot hit is consumed without native interaction"); stage = 5; return false; @@ -368,7 +362,8 @@ static boolean run(MachineScreen screen) { if (menu.getCarried().isEmpty() || slot.hasItem()) return false; require( - menu.getCarried().is(zone.moddev.mc.companion.CompanionAddons.PULLER.get()), + menu.getCarried() + .is(zone.moddev.mc.companion.CompanionAddons.PULLER.get()), "Click outside frame extracts the actual addon"); screen.mouseClicked(x, y, 0); screen.mouseReleased(x, y, 0); @@ -377,7 +372,8 @@ static boolean run(MachineScreen screen) { } if (!menu.getCarried().isEmpty() || !slot.hasItem()) return false; - require(automation.send("close", 0, -1, "", 0, ""), "Close drawer after native round trip"); + require(automation.send("close", 0, -1, "", 0, ""), + "Close drawer after native round trip"); stage = 4; return false; } diff --git a/src/companionTest/java/zone/moddev/mc/companion/test/CreativePowerClientSmoke.java b/src/companionTest/java/zone/moddev/mc/companion/test/CreativePowerClientSmoke.java index f95f2c6f..b6b2c956 100644 --- a/src/companionTest/java/zone/moddev/mc/companion/test/CreativePowerClientSmoke.java +++ b/src/companionTest/java/zone/moddev/mc/companion/test/CreativePowerClientSmoke.java @@ -16,7 +16,8 @@ final class CreativePowerClientSmoke { static boolean run(MachineScreen screen) { var menu = screen.getMenu(); - var automation = menu.components().get(BuiltinMenuComponents.AUTOMATION); + var automation = menu.components() + .get(BuiltinMenuComponents.AUTOMATION); if (automation == null) throw new AssertionError("Generator must expose its energy drawer"); if (automation.pending()) @@ -24,63 +25,65 @@ static boolean run(MachineScreen screen) { int top = RelativeFace.TOP.ordinal(); switch (step) { case 0 -> { - var left = automation.inventory.group(0).world(RelativeFace.LEFT); + var left = automation.inventory.group(0) + .world(RelativeFace.LEFT); if (left != net.minecraft.core.Direction.WEST - || !menu.blockEntity - .getLevel() - .getBlockState(menu.blockEntity.getBlockPos().relative(left)) - .is(zone.moddev.mc.companion.CompanionMod.CRUSHER.block.get())) + || !menu.blockEntity.getLevel() + .getBlockState( + menu.blockEntity.getBlockPos().relative(left)) + .is(zone.moddev.mc.companion.CompanionMod.CRUSHER.block + .get())) throw new AssertionError( "Generator drawer places the western crusher on its left when facing south"); - if (!menu.slots.isEmpty() - || automation.addonsEnabled() - || screen.layout.region(PlayerInventoryElement.INVENTORY).isPresent() - || screen.layout.region(PlayerInventoryElement.TOOLBAR).isPresent() - || !menu.components().get(BuiltinMenuComponents.BACKPACK).state().icons() - .isEmpty() - || !menu.components().get(BuiltinMenuComponents.CURIOS).state().icons() - .isEmpty()) + if (!menu.slots.isEmpty() || automation.addonsEnabled() + || screen.layout.region(PlayerInventoryElement.INVENTORY) + .isPresent() + || screen.layout.region(PlayerInventoryElement.TOOLBAR) + .isPresent() + || !menu.components().get(BuiltinMenuComponents.BACKPACK) + .state().icons().isEmpty() + || !menu.components().get(BuiltinMenuComponents.CURIOS) + .state().icons().isEmpty()) throw new AssertionError( "Generator has no inventory, addon slots or equipment drawers"); - if (menu.components().require(BuiltinMenuComponents.STATS).pages().get(0).lines() - .size() != 3 - || menu - .components() - .require(BuiltinMenuComponents.STATS) - .pages() - .get(0) - .lines() - .stream() + if (menu.components().require(BuiltinMenuComponents.STATS) + .pages().get(0).lines().size() != 3 + || menu.components().require(BuiltinMenuComponents.STATS) + .pages().get(0).lines().stream() .anyMatch(c -> c.getString().contains("RF used"))) throw new AssertionError( "Generator statistics only show output and output limit"); - if (menu.components().require(BuiltinMenuComponents.STATS).pages().get(2).lines() - .stream() + if (menu.components().require(BuiltinMenuComponents.STATS) + .pages().get(2).lines().stream() .noneMatch(c -> c.getString().contains("1,200,000 RF/min"))) throw new AssertionError( "Generator maximum output follows the minute timescale"); var panel = screen.panelBounds(); - click(screen, panel.right() + 5, panel.y() + AutomationLayout.TOP + 5); + click(screen, panel.right() + 5, + panel.y() + AutomationLayout.TOP + 5); } case 1 -> { if (!automation.open()) throw new AssertionError("Generator side tab opens drawer"); var drawer = screen.exclusions().stream() - .filter(r -> r.x() >= screen.panelBounds().right() && r.width() > 22) - .findFirst() - .orElseThrow(); - if (drawer.height() != AutomationLayout.face(RelativeFace.BOTTOM.ordinal()).bottom() + .filter(r -> r.x() >= screen.panelBounds().right() + && r.width() > 22) + .findFirst().orElseThrow(); + if (drawer.height() != AutomationLayout + .face(RelativeFace.BOTTOM.ordinal()).bottom() + AutomationLayout.PADDING) throw new AssertionError( "Side-only drawer ends below the faces without addon space"); - var face = AutomationLayout.face(top).offset(drawer.x(), drawer.y()); + var face = AutomationLayout.face(top).offset(drawer.x(), + drawer.y()); click(screen, face.x() + 8, face.y() + 8); } case 2 -> { if (automation.mode(0, top) != IoMode.DISABLED) throw new AssertionError( "Generator top face disabled through client/server action"); - var info = screen.bounds(menu.bound.screen().find("info").orElseThrow()); + var info = screen + .bounds(menu.bound.screen().find("info").orElseThrow()); click(screen, info.x() + 5, info.bottom() + 7); } case 3 -> { @@ -90,30 +93,27 @@ static boolean run(MachineScreen screen) { click(screen, x, y); click(screen, x, y); } - case 4 -> - Screenshot.grab( - Minecraft.getInstance().gameDirectory, - "creative-power-sides-stats.png", - Minecraft.getInstance().getMainRenderTarget(), - m -> { - }); - case 5 -> automation.send("face", 0, -1, IoMode.DISABLED.name(), top, ""); + case 4 -> Screenshot.grab(Minecraft.getInstance().gameDirectory, + "creative-power-sides-stats.png", + Minecraft.getInstance().getMainRenderTarget(), m -> { + }); + case 5 -> + automation.send("face", 0, -1, IoMode.DISABLED.name(), top, ""); case 6 -> { if (automation.mode(0, top) != IoMode.OUTPUT) throw new AssertionError("Generator top face re-enabled"); screen.keyPressed(256, 0, 0); - var info = screen.bounds(menu.bound.screen().find("info").orElseThrow()); + var info = screen + .bounds(menu.bound.screen().find("info").orElseThrow()); click(screen, info.x() + 5, info.y() + 5); } - case 7 -> - Screenshot.grab( - Minecraft.getInstance().gameDirectory, - "creative-power-info.png", - Minecraft.getInstance().getMainRenderTarget(), - m -> { - }); + case 7 -> Screenshot.grab(Minecraft.getInstance().gameDirectory, + "creative-power-info.png", + Minecraft.getInstance().getMainRenderTarget(), m -> { + }); case 8 -> { - var info = screen.bounds(menu.bound.screen().find("info").orElseThrow()); + var info = screen + .bounds(menu.bound.screen().find("info").orElseThrow()); click(screen, info.x() + 5, info.y() + 5); automation.send("close", 0, -1, "", 0, ""); } diff --git a/src/companionTest/java/zone/moddev/mc/companion/test/CuriosClientSmoke.java b/src/companionTest/java/zone/moddev/mc/companion/test/CuriosClientSmoke.java index 708a7746..e307a284 100644 --- a/src/companionTest/java/zone/moddev/mc/companion/test/CuriosClientSmoke.java +++ b/src/companionTest/java/zone/moddev/mc/companion/test/CuriosClientSmoke.java @@ -16,7 +16,10 @@ import zone.moddev.mc.lib.gui.runtime.ContainerDrawerMenu; import zone.moddev.mc.lib.gui.runtime.component.BuiltinMenuComponents; -/** Networked functional/cosmetic equipment, optional absence, paging and dynamic-slot checks. */ +/** + * Networked functional/cosmetic equipment, optional absence, paging and + * dynamic-slot checks. + */ final class CuriosClientSmoke { private static int step, originalSize; private static int reportedStep = -1; @@ -28,31 +31,37 @@ static boolean run(MachineScreen screen) { var drawer = menu.components().get(BuiltinMenuComponents.CURIOS); if (reportedStep != step) { reportedStep = step; - zone.moddev.mc.lib.MMDLib.LOGGER.info("CURIOS_DRAWER_STEP {}", step); + zone.moddev.mc.lib.MMDLib.LOGGER.info("CURIOS_DRAWER_STEP {}", + step); } if (!ModList.get().isLoaded("curios")) { - require(drawer.state().icons().isEmpty(), "Curios is optional on the client"); + require(drawer.state().icons().isEmpty(), + "Curios is optional on the client"); return true; } if (step == 20) return !drawer.state().open(); - if (drawer.pending() || menu.components().get(BuiltinMenuComponents.BACKPACK).pending()) + if (drawer.pending() + || menu.components().get(BuiltinMenuComponents.BACKPACK).pending()) return false; var state = drawer.state(); if (state.icons().isEmpty()) return false; var layout = DrawerExtensionClientSmoke.containerLayout(screen, true); require( - layout.pageSize() > 0 && layout.body().height() == screen.panelBounds().height() - 8, + layout.pageSize() > 0 + && layout.body().height() == screen.panelBounds().height() - 8, "Full-height equipment drawer fits"); switch (step) { case 0 -> { - var bag = DrawerExtensionClientSmoke.containerLayout(screen, false); + var bag = DrawerExtensionClientSmoke.containerLayout(screen, + false); click(screen, bag.tab(), 0); step++; } case 1 -> { - if (!menu.components().get(BuiltinMenuComponents.BACKPACK).state().open()) + if (!menu.components().get(BuiltinMenuComponents.BACKPACK) + .state().open()) return false; click(screen, layout.tab(), 0); step++; @@ -61,7 +70,8 @@ static boolean run(MachineScreen screen) { if (!state.open() || state.pageSize() != layout.pageSize()) return false; require( - !menu.components().get(BuiltinMenuComponents.BACKPACK).state().open(), + !menu.components().get(BuiltinMenuComponents.BACKPACK) + .state().open(), "Opening Curios closes backpack storage"); require( state.descriptions().getFirst().identifier().equals("ring") @@ -75,15 +85,14 @@ static boolean run(MachineScreen screen) { !state.descriptions().get(4).available() && !state.descriptions().get(5).available(), "Inactive functional/cosmetic pair is disabled on client"); - require( - !state.descriptions().get(7).available(), + require(!state.descriptions().get(7).available(), "Missing cosmetic counterpart is disabled"); require( screen.exclusions().contains(layout.body()) && screen.exclusions().contains(layout.rail()), "Equipment body and rail are excluded from JEI"); - Screenshot.grab( - mc.gameDirectory, "curios-drawer.png", mc.getMainRenderTarget(), message -> { + Screenshot.grab(mc.gameDirectory, "curios-drawer.png", + mc.getMainRenderTarget(), message -> { }); click(screen, layout.cell(0), 0); originalSize = state.sizes().getFirst(); @@ -92,7 +101,8 @@ static boolean run(MachineScreen screen) { case 3 -> { if (!menu.getCarried().is(Items.DIAMOND)) return false; - require(state.contents().get(0).isEmpty(), "Unequip functional item"); + require(state.contents().get(0).isEmpty(), + "Unequip functional item"); click(screen, layout.cell(1), 0); step++; } @@ -107,23 +117,27 @@ static boolean run(MachineScreen screen) { case 5 -> { if (!menu.getCarried().isEmpty()) return false; - require(state.contents().get(0).is(Items.DIAMOND), "Re-equip functional item"); + require(state.contents().get(0).is(Items.DIAMOND), + "Re-equip functional item"); click(screen, layout.cell(1), 0); step++; } case 6 -> { if (!menu.getCarried().is(Items.EMERALD)) return false; - require(state.contents().get(1).isEmpty(), "Unequip cosmetic item"); + require(state.contents().get(1).isEmpty(), + "Unequip cosmetic item"); click(screen, layout.cell(1), 0); step++; } case 7 -> { if (!menu.getCarried().isEmpty()) return false; - require(state.contents().get(1).is(Items.EMERALD), "Re-equip cosmetic item"); + require(state.contents().get(1).is(Items.EMERALD), + "Re-equip cosmetic item"); drawer - .request(ContainerDrawerMenu.QUICK_TAKE, 0, state.pageSize()) + .request(ContainerDrawerMenu.QUICK_TAKE, 0, + state.pageSize()) .ifPresent(PacketDistributor::sendToServer); step++; } @@ -138,18 +152,18 @@ static boolean run(MachineScreen screen) { } require(index >= 0, "Shift unequip reaches player inventory"); drawer - .request(ContainerDrawerMenu.QUICK_STORE, index, state.pageSize()) + .request(ContainerDrawerMenu.QUICK_STORE, index, + state.pageSize()) .ifPresent(PacketDistributor::sendToServer); step++; } case 9 -> { if (!state.contents().get(0).is(Items.DIAMOND)) return false; - server( - player -> { - player.containerMenu.setCarried(new ItemStack(Items.STONE)); - player.containerMenu.broadcastChanges(); - }); + server(player -> { + player.containerMenu.setCarried(new ItemStack(Items.STONE)); + player.containerMenu.broadcastChanges(); + }); step++; } case 10 -> { @@ -160,13 +174,13 @@ static boolean run(MachineScreen screen) { } case 11 -> { require( - menu.getCarried().is(Items.STONE) && state.contents().get(4).isEmpty(), + menu.getCarried().is(Items.STONE) + && state.contents().get(4).isEmpty(), "Curios slot validation rejects wrong items"); - server( - player -> { - player.containerMenu.setCarried(ItemStack.EMPTY); - player.containerMenu.broadcastChanges(); - }); + server(player -> { + player.containerMenu.setCarried(ItemStack.EMPTY); + player.containerMenu.broadcastChanges(); + }); step++; } case 12 -> { @@ -177,7 +191,8 @@ static boolean run(MachineScreen screen) { } case 13 -> { require( - menu.getCarried().isEmpty() && state.contents().get(2).is(Items.DIAMOND), + menu.getCarried().isEmpty() + && state.contents().get(2).is(Items.DIAMOND), "Curios unequip events can forbid removal"); click(screen, layout.next(), 0); step++; @@ -185,15 +200,17 @@ static boolean run(MachineScreen screen) { case 14 -> { if (state.offset() == 0) return false; - if (state.contents().stream().noneMatch(stack -> stack.is(Items.AMETHYST_SHARD))) { + if (state.contents().stream() + .noneMatch(stack -> stack.is(Items.AMETHYST_SHARD))) { require( - state.offset() + state.pageSize() < state.sizes().getFirst(), + state.offset() + state.pageSize() < state.sizes() + .getFirst(), "Later equipment page contains its actual items"); click(screen, layout.next(), 0); return false; } - Screenshot.grab( - mc.gameDirectory, "curios-page-two.png", mc.getMainRenderTarget(), message -> { + Screenshot.grab(mc.gameDirectory, "curios-page-two.png", + mc.getMainRenderTarget(), message -> { }); click(screen, layout.previous(), 0); step++; @@ -209,12 +226,12 @@ static boolean run(MachineScreen screen) { case 16 -> { if (state.sizes().getFirst() != originalSize + 2) return false; - require(!state.open(), "Changing slot count closes the old equipment view"); - server( - player -> { - CuriosFixture.checkStale(player); - staleChecked = true; - }); + require(!state.open(), + "Changing slot count closes the old equipment view"); + server(player -> { + CuriosFixture.checkStale(player); + staleChecked = true; + }); step++; } case 17 -> { @@ -230,13 +247,15 @@ static boolean run(MachineScreen screen) { state.descriptions().get(6).identifier().equals("ring") && state.descriptions().get(6).index() == 3, "New equipment slots appear"); - require(screen.keyPressed(256, 0, 0), "Escape closes equipment drawer"); + require(screen.keyPressed(256, 0, 0), + "Escape closes equipment drawer"); step++; } case 19 -> { if (state.open()) return false; - require(mc.screen == screen, "Closing drawer retains machine screen"); + require(mc.screen == screen, + "Closing drawer retains machine screen"); step++; } default -> throw new AssertionError("Unexpected Curios smoke step"); @@ -246,18 +265,18 @@ static boolean run(MachineScreen screen) { private static void server(Consumer action) { var server = Minecraft.getInstance().getSingleplayerServer(); - server.execute( - () -> { - try { - action.accept(server.getPlayerList().getPlayers().getFirst()); - } catch (Throwable error) { - CompanionClientSmoke.fail(error); - } - }); + server.execute(() -> { + try { + action.accept(server.getPlayerList().getPlayers().getFirst()); + } catch (Throwable error) { + CompanionClientSmoke.fail(error); + } + }); } private static void click(MachineScreen screen, Rect rect, int button) { - require(screen.mouseClicked(rect.x() + 5, rect.y() + 5, button), "Curios click handled"); + require(screen.mouseClicked(rect.x() + 5, rect.y() + 5, button), + "Curios click handled"); screen.mouseReleased(rect.x() + 5, rect.y() + 5, button); } diff --git a/src/companionTest/java/zone/moddev/mc/companion/test/CuriosFixture.java b/src/companionTest/java/zone/moddev/mc/companion/test/CuriosFixture.java index 5b3c5f5a..de1e27d2 100644 --- a/src/companionTest/java/zone/moddev/mc/companion/test/CuriosFixture.java +++ b/src/companionTest/java/zone/moddev/mc/companion/test/CuriosFixture.java @@ -17,52 +17,48 @@ import zone.moddev.mc.lib.gui.runtime.MachineMenu; import zone.moddev.mc.lib.gui.runtime.component.BuiltinMenuComponents; -/** Only loaded in the real optional-mod client run. Slot definitions/tags are test resources. */ +/** + * Only loaded in the real optional-mod client run. Slot definitions/tags are + * test resources. + */ final class CuriosFixture { private static ContainerDrawerNetwork.Request stale; static void setup(ServerPlayer player) { var inventory = CuriosApi.getCuriosInventory(player).orElseThrow(); var rings = inventory.getStacksHandler("ring").orElseThrow(); - require( - rings.hasCosmetic() && rings.getStacks().getSlots() == 3, + require(rings.hasCosmetic() && rings.getStacks().getSlots() == 3, "Cosmetic fixture slots registered"); - require( - rings.getStacks().insertItem(0, new ItemStack(Items.DIAMOND), false).isEmpty(), + require(rings.getStacks() + .insertItem(0, new ItemStack(Items.DIAMOND), false).isEmpty(), "Functional fixture accepts tagged item"); var locked = new ItemStack(Items.DIAMOND); - locked.set( - net.minecraft.core.component.DataComponents.CUSTOM_NAME, + locked.set(net.minecraft.core.component.DataComponents.CUSTOM_NAME, net.minecraft.network.chat.Component.literal("Locked ring")); rings.getStacks().insertItem(1, locked, false); - rings.getCosmeticStacks().insertItem(0, new ItemStack(Items.EMERALD), false); - inventory - .getStacksHandler("necklace") - .orElseThrow() - .getStacks() + rings.getCosmeticStacks().insertItem(0, new ItemStack(Items.EMERALD), + false); + inventory.getStacksHandler("necklace").orElseThrow().getStacks() .insertItem(0, new ItemStack(Items.GOLD_INGOT), false); - inventory - .getStacksHandler("charm") - .orElseThrow() - .getStacks() + inventory.getStacksHandler("charm").orElseThrow().getStacks() .insertItem(11, new ItemStack(Items.AMETHYST_SHARD), false); checkInactive(player); - // Keep one empty pair inactive for the client presentation/interaction checks. + // Keep one empty pair inactive for the client presentation/interaction + // checks. inventory.setSlotActive("ring", 2, false); - NeoForge.EVENT_BUS.addListener( - (CurioCanUnequipEvent event) -> { - if (event.getSlotContext().entity() == player - && event.getSlotContext().identifier().equals("ring") - && event.getSlotContext().index() == 1) - event.setUnequipResult(TriState.FALSE); - }); + NeoForge.EVENT_BUS.addListener((CurioCanUnequipEvent event) -> { + if (event.getSlotContext().entity() == player + && event.getSlotContext().identifier().equals("ring") + && event.getSlotContext().index() == 1) + event.setUnequipResult(TriState.FALSE); + }); } private static void checkInactive(ServerPlayer player) { var inventory = CuriosApi.getCuriosInventory(player).orElseThrow(); var identity = new ItemStack(Items.CHEST); - var live = zone.moddev.mc.lib.compat.curios.CuriosEquipment.find(player, identity) - .getFirst().items(); + var live = zone.moddev.mc.lib.compat.curios.CuriosEquipment + .find(player, identity).getFirst().items(); inventory.setSlotActive("ring", 0, false); for (int slot : new int[]{0, 1}) { require( @@ -71,72 +67,61 @@ private static void checkInactive(ServerPlayer player) { "Retained handler rejects inactive extraction"); require( !live.isItemValid(slot, new ItemStack(Items.DIAMOND)) - && live.insertItem(slot, new ItemStack(Items.DIAMOND), true).getCount() == 1 - && live.insertItem(slot, new ItemStack(Items.DIAMOND), false).getCount() == 1, + && live.insertItem(slot, new ItemStack(Items.DIAMOND), true) + .getCount() == 1 + && live + .insertItem(slot, new ItemStack(Items.DIAMOND), false) + .getCount() == 1, "Retained handler rejects inactive insertion"); } var fresh = (zone.moddev.mc.lib.gui.runtime.EquippedContainers.DescribedHandler) zone.moddev.mc.lib.compat.curios.CuriosEquipment - .find(player, identity) - .getFirst() - .items(); + .find(player, identity).getFirst().items(); require( !fresh.describe(0).available() && !fresh.describe(1).available(), "Both inactive functional/cosmetic slots are presented as unavailable"); inventory.setSlotActive("ring", 0, true); require( - live.getStackInSlot(0).is(Items.DIAMOND) && live.getStackInSlot(1).is(Items.EMERALD), + live.getStackInSlot(0).is(Items.DIAMOND) + && live.getStackInSlot(1).is(Items.EMERALD), "Inactive attempts preserve functional and cosmetic contents"); inventory.setSlotActive("ring", 2, false); fresh = (zone.moddev.mc.lib.gui.runtime.EquippedContainers.DescribedHandler) zone.moddev.mc.lib.compat.curios.CuriosEquipment - .find(player, identity) - .getFirst() - .items(); + .find(player, identity).getFirst().items(); for (int slot : new int[]{4, 5}) require( - fresh.insertItem(slot, new ItemStack(Items.DIAMOND), false).getCount() == 1, + fresh.insertItem(slot, new ItemStack(Items.DIAMOND), false) + .getCount() == 1, "Inactive empty pair refuses otherwise valid items"); inventory.setSlotActive("ring", 2, true); inventory.setSlotActive("back", 0, false); - require( - zone.moddev.mc.lib.gui.runtime.EquippedContainers.find(player).isEmpty(), - "Inactive backpack is not exposed as storage"); + require(zone.moddev.mc.lib.gui.runtime.EquippedContainers.find(player) + .isEmpty(), "Inactive backpack is not exposed as storage"); inventory.setSlotActive("back", 0, true); } static void resize(ServerPlayer player) { var menu = (MachineMenu) player.containerMenu; var drawer = menu.components().get(BuiltinMenuComponents.CURIOS); - stale = new ContainerDrawerNetwork.Request( - menu.containerId, - true, - menu.widgetSession(), - drawer.state().revision(), - drawer.state().acknowledged() + 1, - ContainerDrawerMenu.PICKUP, - 0, - drawer.state().pageSize(), - ItemStack.EMPTY); - CuriosApi.getCuriosInventory(player) - .orElseThrow() - .addTransientSlotModifier( - "ring", - ResourceLocation.parse("mmdlib_companion:drawer_test"), - 1, + stale = new ContainerDrawerNetwork.Request(menu.containerId, true, + menu.widgetSession(), drawer.state().revision(), + drawer.state().acknowledged() + 1, ContainerDrawerMenu.PICKUP, 0, + drawer.state().pageSize(), ItemStack.EMPTY); + CuriosApi.getCuriosInventory(player).orElseThrow() + .addTransientSlotModifier("ring", + ResourceLocation.parse("mmdlib_companion:drawer_test"), 1, AttributeModifier.Operation.ADD_VALUE); } static void checkStale(ServerPlayer player) { var menu = (MachineMenu) player.containerMenu; - menu.components().get(BuiltinMenuComponents.CURIOS).action(player, stale); - require(menu.getCarried().isEmpty(), "Pre-resize request cannot mutate the new topology"); + menu.components().get(BuiltinMenuComponents.CURIOS).action(player, + stale); + require(menu.getCarried().isEmpty(), + "Pre-resize request cannot mutate the new topology"); require( - CuriosApi.getCuriosInventory(player) - .orElseThrow() - .getStacksHandler("ring") - .orElseThrow() - .getStacks() - .getStackInSlot(0) - .is(Items.DIAMOND), + CuriosApi.getCuriosInventory(player).orElseThrow() + .getStacksHandler("ring").orElseThrow().getStacks() + .getStackInSlot(0).is(Items.DIAMOND), "Stale click left equipment intact"); } diff --git a/src/companionTest/java/zone/moddev/mc/companion/test/DrawerExtensionClientSmoke.java b/src/companionTest/java/zone/moddev/mc/companion/test/DrawerExtensionClientSmoke.java index 32b88590..ab6b53a2 100644 --- a/src/companionTest/java/zone/moddev/mc/companion/test/DrawerExtensionClientSmoke.java +++ b/src/companionTest/java/zone/moddev/mc/companion/test/DrawerExtensionClientSmoke.java @@ -18,21 +18,26 @@ import zone.moddev.mc.lib.gui.drawer.DrawerRegistry; import zone.moddev.mc.lib.gui.model.Edge; -/** A mod drawer receives the generic hooks and supplies its own appearance and local behavior. */ +/** + * A mod drawer receives the generic hooks and supplies its own appearance and + * local behavior. + */ final class DrawerExtensionClientSmoke { private static Probe probe; private static boolean done; - private static final ResourceLocation TYPE = ResourceLocation.parse("test:drawer"); + private static final ResourceLocation TYPE = ResourceLocation + .parse("test:drawer"); static void install() { - DrawerRegistry.GLOBAL.register( - TYPE, + DrawerRegistry.GLOBAL.register(TYPE, context -> context.host() == null ? List.of() : List.of( - new DrawerDefinition(TYPE, "test:drawer", DrawerPlacement.top(Edge.LEFT, 50)), - new DrawerDefinition(TYPE, "test:right", DrawerPlacement.top(Edge.RIGHT, 5)))); + new DrawerDefinition(TYPE, "test:drawer", + DrawerPlacement.top(Edge.LEFT, 50)), + new DrawerDefinition(TYPE, "test:right", + DrawerPlacement.top(Edge.RIGHT, 5)))); DrawerFactories.GLOBAL.register(TYPE, Probe::new); } @@ -41,29 +46,27 @@ static boolean run(MachineScreen screen) { return true; if (probe == null) { probe = (Probe) screen.drawers().entries().stream() - .filter(d -> d.id().equals("test:drawer")) - .findFirst() + .filter(d -> d.id().equals("test:drawer")).findFirst() .orElseThrow(); var right = screen.drawers().entries().stream() - .filter(d -> d.id().equals("test:right")) - .findFirst() + .filter(d -> d.id().equals("test:right")).findFirst() .orElseThrow(); var redstone = screen.drawers().entries().stream() - .filter(RedstoneDrawer.class::isInstance) - .findFirst() + .filter(RedstoneDrawer.class::isInstance).findFirst() .orElseThrow(); var sides = screen.drawers().entries().stream() - .filter(SideConfigDrawer.class::isInstance) - .findFirst() + .filter(SideConfigDrawer.class::isInstance).findFirst() .orElseThrow(); if (right.tabBounds().y() < sides.tabBounds().bottom() || right.tabBounds().bottom() > redstone.tabBounds().y()) - throw new AssertionError("Registered drawer must reserve space between built-ins"); + throw new AssertionError( + "Registered drawer must reserve space between built-ins"); var before = right.tabBounds(); - screen.resize(net.minecraft.client.Minecraft.getInstance(), screen.width, - screen.height); + screen.resize(net.minecraft.client.Minecraft.getInstance(), + screen.width, screen.height); if (!before.equals(right.tabBounds())) - throw new AssertionError("Drawer placement changed on equal resize"); + throw new AssertionError( + "Drawer placement changed on equal resize"); return false; } if (probe.frames == 0) @@ -76,17 +79,17 @@ static boolean run(MachineScreen screen) { screen.mouseReleased(tab.x() + 6, tab.y() + 6, 0); screen.mouseScrolled(tab.x() + 5, tab.y() + 5, 0, 1); screen.charTyped('x', 0); - if (!probe.open || !probe.dragged || !probe.released || !probe.scrolled || !probe.typed) + if (!probe.open || !probe.dragged || !probe.released || !probe.scrolled + || !probe.typed) throw new AssertionError("Generic drawer input dispatch"); var stats = screen.drawers().entries().stream() - .filter(StatsDrawer.class::isInstance) - .findFirst() - .orElseThrow(); + .filter(StatsDrawer.class::isInstance).findFirst().orElseThrow(); var statsTab = stats.tabBounds(); screen.mouseClicked(statsTab.x() + 5, statsTab.y() + 5, 0); screen.mouseReleased(statsTab.x() + 5, statsTab.y() + 5, 0); if (probe.open || !stats.open()) - throw new AssertionError("Opening a sibling closes the custom drawer"); + throw new AssertionError( + "Opening a sibling closes the custom drawer"); screen.keyPressed(256, 0, 0); probe.enabled = false; done = true; @@ -97,18 +100,17 @@ static zone.moddev.mc.lib.gui.layout.ContainerDrawerLayout containerLayout( MachineScreen screen, boolean equipment) { return ((ContainerDrawer) screen.drawers().entries().stream() .filter(d -> d.id().equals(equipment ? "curios" : "backpack")) - .findFirst() - .orElseThrow()) - .contentLayout(); + .findFirst().orElseThrow()).contentLayout(); } private static final class Probe extends Drawer { - private boolean enabled = true, open, dragged, released, scrolled, typed; + private boolean enabled = true, open, dragged, released, scrolled, + typed; private int frames; Probe(MachineScreen screen, DrawerDefinition definition) { - super( - screen, definition, IconSprite.gui(ResourceLocation.parse("mmdlib:icon_info")), + super(screen, definition, + IconSprite.gui(ResourceLocation.parse("mmdlib:icon_info")), 0x92bcb4); } @@ -140,7 +142,8 @@ public boolean click(double x, double y, int button) { return true; } - public boolean drag(double x, double y, int button, double dx, double dy) { + public boolean drag(double x, double y, int button, double dx, + double dy) { return dragged = open; } diff --git a/src/companionTest/java/zone/moddev/mc/companion/test/DrawerMemoryClientSmoke.java b/src/companionTest/java/zone/moddev/mc/companion/test/DrawerMemoryClientSmoke.java index f8355361..88ced14b 100644 --- a/src/companionTest/java/zone/moddev/mc/companion/test/DrawerMemoryClientSmoke.java +++ b/src/companionTest/java/zone/moddev/mc/companion/test/DrawerMemoryClientSmoke.java @@ -9,9 +9,13 @@ import zone.moddev.mc.lib.gui.runtime.ScreenRuntime; import zone.moddev.mc.lib.gui.runtime.component.BuiltinMenuComponents; -/** Reopens different real machine types on the same connection, including an explicit close. */ +/** + * Reopens different real machine types on the same connection, including an + * explicit close. + */ final class DrawerMemoryClientSmoke { - private static final BlockPos FIRST = new BlockPos(4, 100, 0), SECOND = FIRST.east(2); + private static final BlockPos FIRST = new BlockPos(4, 100, 0), + SECOND = FIRST.east(2); private static int step, waitTicks, reported = -1; private static boolean requested; private static volatile BlockPos reopenAfterClose; @@ -23,56 +27,51 @@ static boolean run() { var mc = Minecraft.getInstance(); if (reported != step) { reported = step; - zone.moddev.mc.lib.MMDLib.LOGGER.info("DRAWER_MEMORY_STEP {}", step); + zone.moddev.mc.lib.MMDLib.LOGGER.info("DRAWER_MEMORY_STEP {}", + step); } if (++waitTicks > 160) - throw new AssertionError( - "Drawer memory timed out at step " - + step - + "; reopening=" - + reopenAfterClose - + "; client=" - + describeClient(mc) - + "; server=" - + serverMenu); + throw new AssertionError("Drawer memory timed out at step " + step + + "; reopening=" + reopenAfterClose + "; client=" + + describeClient(mc) + "; server=" + serverMenu); if (waitTicks == 1 || waitTicks % 20 == 0) - server( - player -> serverMenu = player.containerMenu.getClass().getSimpleName() - + "#" - + player.containerMenu.containerId); + server(player -> serverMenu = player.containerMenu.getClass() + .getSimpleName() + "#" + player.containerMenu.containerId); if (reopenAfterClose != null) { var target = reopenAfterClose; - server( - player -> { - // The client close packet and server.execute use different queues. Never open a - // replacement menu until the server has actually handled the vanilla close. - if (reopenAfterClose == target - && player.containerMenu == player.inventoryMenu) { - reopenAfterClose = null; - ScreenRuntime.open(player, player.serverLevel().getBlockEntity(target)); - } - }); + server(player -> { + // The client close packet and server.execute use different + // queues. Never open a + // replacement menu until the server has actually handled the + // vanilla close. + if (reopenAfterClose == target + && player.containerMenu == player.inventoryMenu) { + reopenAfterClose = null; + ScreenRuntime.open(player, + player.serverLevel().getBlockEntity(target)); + } + }); return false; } if (step == 0) { - server( - player -> { - BackpackFixture.equip(player); - player - .serverLevel() - .setBlock(FIRST, - CompanionMod.MACHINES.get(0).block.get().defaultBlockState(), 3); - player - .serverLevel() - .setBlock(SECOND, - CompanionMod.MACHINES.get(1).block.get().defaultBlockState(), 3); - player.teleportTo(5, 101, 1); - }); + server(player -> { + BackpackFixture.equip(player); + player.serverLevel().setBlock(FIRST, + CompanionMod.MACHINES.get(0).block.get() + .defaultBlockState(), + 3); + player.serverLevel().setBlock(SECOND, + CompanionMod.MACHINES.get(1).block.get() + .defaultBlockState(), + 3); + player.teleportTo(5, 101, 1); + }); advance(); return false; } if (step == 1 && !requested) { - if (mc.level.getBlockEntity(FIRST) == null || mc.level.getBlockEntity(SECOND) == null) + if (mc.level.getBlockEntity(FIRST) == null + || mc.level.getBlockEntity(SECOND) == null) return false; requested = true; open(FIRST); @@ -87,13 +86,16 @@ static boolean run() { || menu.components().get(BuiltinMenuComponents.BACKPACK).pending() || menu.components().get(BuiltinMenuComponents.CURIOS).pending() || menu.components().get(BuiltinMenuComponents.AUTOMATION) != null - && menu.components().get(BuiltinMenuComponents.AUTOMATION).pending()) + && menu.components().get(BuiltinMenuComponents.AUTOMATION) + .pending()) return false; var bag = menu.components().get(BuiltinMenuComponents.BACKPACK).state(); - var curios = menu.components().get(BuiltinMenuComponents.CURIOS).state(); + var curios = menu.components().get(BuiltinMenuComponents.CURIOS) + .state(); if (bag.keys().isEmpty()) return false; - var bagLayout = DrawerExtensionClientSmoke.containerLayout(screen, false); + var bagLayout = DrawerExtensionClientSmoke.containerLayout(screen, + false); switch (step) { case 1 -> { if (!first) @@ -111,14 +113,13 @@ static boolean run() { case 3 -> { if (!second || !bag.open()) return false; - require( - bag.keys().get(bag.selected()).equals("back:0"), + require(bag.keys().get(bag.selected()).equals("back:0"), "Same equipped container restored across machines"); screen.keyPressed(256, 0, 0); - require( - menu.components().get(BuiltinMenuComponents.BACKPACK).pending(), - "Explicit close awaits acknowledgment"); - // Close the screen before that acknowledgment: the user's intent must survive + require(menu.components().get(BuiltinMenuComponents.BACKPACK) + .pending(), "Explicit close awaits acknowledgment"); + // Close the screen before that acknowledgment: the user's + // intent must survive // capture. open(FIRST); advance(); @@ -126,16 +127,19 @@ static boolean run() { case 4 -> { if (!first || waitTicks < 15) return false; - require(!bag.open(), "Pending explicit close is remembered on another machine"); + require(!bag.open(), + "Pending explicit close is remembered on another machine"); advance(); } case 5 -> { if (!first || waitTicks < 15) return false; - require(!bag.open(), "Explicit close is remembered on another machine"); + require(!bag.open(), + "Explicit close is remembered on another machine"); if (curios.icons().isEmpty()) return false; - var layout = DrawerExtensionClientSmoke.containerLayout(screen, true); + var layout = DrawerExtensionClientSmoke.containerLayout(screen, + true); click(screen, layout.tab()); advance(); } @@ -148,7 +152,8 @@ static boolean run() { case 7 -> { if (!second || !curios.open()) return false; - require(!bag.open(), "Curios restoration keeps backpack drawer closed"); + require(!bag.open(), + "Curios restoration keeps backpack drawer closed"); screen.keyPressed(256, 0, 0); var panel = screen.panelBounds(); click(screen, new Rect(panel.right(), panel.y() + 4, 22, 24)); @@ -157,70 +162,68 @@ static boolean run() { case 8 -> { if (curios.open()) return false; - if (menu.components().get(BuiltinMenuComponents.AUTOMATION) == null) + if (menu.components() + .get(BuiltinMenuComponents.AUTOMATION) == null) throw new AssertionError("Fixture has automation"); - if (!menu.components().get(BuiltinMenuComponents.AUTOMATION).open()) { + if (!menu.components().get(BuiltinMenuComponents.AUTOMATION) + .open()) { var panel = screen.panelBounds(); - click(screen, new Rect(panel.right(), panel.y() + 4, 22, 24)); + click(screen, + new Rect(panel.right(), panel.y() + 4, 22, 24)); return false; } - server( - player -> { - player - .getInventory() - .setItem( - 9, - new net.minecraft.world.item.ItemStack( - net.minecraft.world.item.Items.DIRT, 16)); - ScreenRuntime.open(player, player.serverLevel().getBlockEntity(FIRST)); - }); + server(player -> { + player.getInventory().setItem(9, + new net.minecraft.world.item.ItemStack( + net.minecraft.world.item.Items.DIRT, 16)); + ScreenRuntime.open(player, + player.serverLevel().getBlockEntity(FIRST)); + }); advance(); } case 9 -> { if (!first - || menu.components().get(BuiltinMenuComponents.AUTOMATION) == null - || !menu.components().get(BuiltinMenuComponents.AUTOMATION).open()) + || menu.components() + .get(BuiltinMenuComponents.AUTOMATION) == null + || !menu.components().get(BuiltinMenuComponents.AUTOMATION) + .open()) return false; - require( - !bag.open() && !curios.open(), + require(!bag.open() && !curios.open(), "Closed left drawers remain closed while right drawer restores"); - // Deliberately drop a configuration request instead of sending it to the server. - menu.components() - .get(BuiltinMenuComponents.BACKPACK) - .request(zone.moddev.mc.lib.gui.runtime.ContainerDrawerMenu.SYNC, 0, - bag.pageSize()) + // Deliberately drop a configuration request instead of sending + // it to the server. + menu.components().get(BuiltinMenuComponents.BACKPACK) + .request( + zone.moddev.mc.lib.gui.runtime.ContainerDrawerMenu.SYNC, + 0, bag.pageSize()) .orElseThrow(); var slot = menu.slots.stream() - .filter( - s -> s.container == mc.player.getInventory() && s.getContainerSlot() == 9) - .findFirst() - .orElseThrow(); - click(screen, - new Rect(screen.guiLeft() + slot.x, screen.guiTop() + slot.y, 16, 16)); - require( - menu.getCarried().getCount() == 16, + .filter(s -> s.container == mc.player.getInventory() + && s.getContainerSlot() == 9) + .findFirst().orElseThrow(); + click(screen, new Rect(screen.guiLeft() + slot.x, + screen.guiTop() + slot.y, 16, 16)); + require(menu.getCarried().getCount() == 16, "Dropped config request does not block vanilla pickup"); - click(screen, - new Rect(screen.guiLeft() + slot.x, screen.guiTop() + slot.y, 16, 16)); + click(screen, new Rect(screen.guiLeft() + slot.x, + screen.guiTop() + slot.y, 16, 16)); advance(); } case 10 -> { - // The normal pending guard above waits for the one-second SYNC recovery. - require(waitTicks >= 15, "Lost acknowledgment exercised timeout recovery"); - menu.components() - .get(BuiltinMenuComponents.BACKPACK) - .request(zone.moddev.mc.lib.gui.runtime.ContainerDrawerMenu.PICKUP, 0, - bag.pageSize()) - .orElseThrow(); + // The normal pending guard above waits for the one-second SYNC + // recovery. + require(waitTicks >= 15, + "Lost acknowledgment exercised timeout recovery"); + menu.components().get(BuiltinMenuComponents.BACKPACK).request( + zone.moddev.mc.lib.gui.runtime.ContainerDrawerMenu.PICKUP, + 0, bag.pageSize()).orElseThrow(); int exclusions = screen.exclusions().size(); var panel = screen.panelBounds(); click(screen, new Rect(panel.x() - 22, panel.y() + 4, 22, 24)); - require( - screen.exclusions().size() > exclusions, + require(screen.exclusions().size() > exclusions, "Pending transfer does not block info drawer"); screen.keyPressed(256, 0, 0); - require( - mc.screen != screen, + require(mc.screen != screen, "Escape closes screen immediately during dropped item request"); MachineScreen.PendingActions.logout(null); reopenAfterClose = SECOND; @@ -230,15 +233,16 @@ static boolean run() { if (!second || waitTicks < 15) return false; require( - !bag.open() - && !curios.open() - && !menu.components().get(BuiltinMenuComponents.AUTOMATION).open(), + !bag.open() && !curios.open() + && !menu.components() + .get(BuiltinMenuComponents.AUTOMATION).open(), "Logout clears drawer preferences before the next screen"); - if (menu.components().get(BuiltinMenuComponents.REDSTONE) != null) { - require( - menu.components() - .get(BuiltinMenuComponents.REDSTONE) - .send(zone.moddev.mc.lib.gui.runtime.RedstoneMenu.SYNC, 0, 0, 0), + if (menu.components() + .get(BuiltinMenuComponents.REDSTONE) != null) { + require(menu.components() + .get(BuiltinMenuComponents.REDSTONE) + .send(zone.moddev.mc.lib.gui.runtime.RedstoneMenu.SYNC, + 0, 0, 0), "Redstone sync is pending before the next client tick"); require(!screen.keyPressed(65, 0, 0), "Pending redstone does not swallow unrelated keys"); @@ -253,18 +257,19 @@ static boolean run() { case 12 -> { if (!first || waitTicks < 15) return false; - if (!menu.components().get(BuiltinMenuComponents.AUTOMATION).open()) { + if (!menu.components().get(BuiltinMenuComponents.AUTOMATION) + .open()) { var panel = screen.panelBounds(); - click(screen, new Rect(panel.right(), panel.y() + 4, 22, 24)); + click(screen, + new Rect(panel.right(), panel.y() + 4, 22, 24)); return false; } require( - menu.components().get(BuiltinMenuComponents.AUTOMATION).send("select", 0, 0, "", - 0, ""), + menu.components().get(BuiltinMenuComponents.AUTOMATION) + .send("select", 0, 0, "", 0, ""), "Automation request is pending"); screen.keyPressed(256, 0, 0); - require( - mc.screen != screen, + require(mc.screen != screen, "Escape closes immediately during a pending automation request"); reopenAfterClose = SECOND; advance(); @@ -272,14 +277,16 @@ static boolean run() { case 13 -> { if (!second || waitTicks < 15) return false; - if (!menu.components().get(BuiltinMenuComponents.AUTOMATION).open()) { + if (!menu.components().get(BuiltinMenuComponents.AUTOMATION) + .open()) { var panel = screen.panelBounds(); - click(screen, new Rect(panel.right(), panel.y() + 4, 22, 24)); + click(screen, + new Rect(panel.right(), panel.y() + 4, 22, 24)); return false; } require( - menu.components().get(BuiltinMenuComponents.AUTOMATION).send("select", 0, 0, "", - 0, ""), + menu.components().get(BuiltinMenuComponents.AUTOMATION) + .send("select", 0, 0, "", 0, ""), "Second automation request is pending"); screen.keyPressed(69, 0, 0); require(mc.screen != screen, @@ -299,11 +306,8 @@ private static String describeClient(Minecraft mc) { if (!(mc.screen instanceof MachineScreen screen)) return String.valueOf(mc.screen); var menu = screen.getMenu(); - return "MachineMenu#" - + menu.containerId - + " at " - + menu.blockEntity.getBlockPos() - + ", backpackPending=" + return "MachineMenu#" + menu.containerId + " at " + + menu.blockEntity.getBlockPos() + ", backpackPending=" + menu.components().get(BuiltinMenuComponents.BACKPACK).pending() + ", curiosPending=" + menu.components().get(BuiltinMenuComponents.CURIOS).pending() @@ -311,9 +315,10 @@ private static String describeClient(Minecraft mc) { + (menu.components().get(BuiltinMenuComponents.AUTOMATION) == null ? "absent" : "open:" - + menu.components().get(BuiltinMenuComponents.AUTOMATION).open() - + ",pending:" - + menu.components().get(BuiltinMenuComponents.AUTOMATION).pending()); + + menu.components().get(BuiltinMenuComponents.AUTOMATION) + .open() + + ",pending:" + menu.components() + .get(BuiltinMenuComponents.AUTOMATION).pending()); } private static void advance() { @@ -327,20 +332,20 @@ private static void click(MachineScreen screen, Rect rect) { } private static void open(BlockPos pos) { - server(player -> ScreenRuntime.open(player, player.serverLevel().getBlockEntity(pos))); + server(player -> ScreenRuntime.open(player, + player.serverLevel().getBlockEntity(pos))); } private static void server( java.util.function.Consumer action) { var server = Minecraft.getInstance().getSingleplayerServer(); - server.execute( - () -> { - try { - action.accept(server.getPlayerList().getPlayers().getFirst()); - } catch (Throwable error) { - CompanionClientSmoke.fail(error); - } - }); + server.execute(() -> { + try { + action.accept(server.getPlayerList().getPlayers().getFirst()); + } catch (Throwable error) { + CompanionClientSmoke.fail(error); + } + }); } private static void require(boolean ok, String message) { diff --git a/src/companionTest/java/zone/moddev/mc/companion/test/ElementBehaviorClientSmoke.java b/src/companionTest/java/zone/moddev/mc/companion/test/ElementBehaviorClientSmoke.java index 2cbefdcc..fbb89bb2 100644 --- a/src/companionTest/java/zone/moddev/mc/companion/test/ElementBehaviorClientSmoke.java +++ b/src/companionTest/java/zone/moddev/mc/companion/test/ElementBehaviorClientSmoke.java @@ -13,27 +13,27 @@ import zone.moddev.mc.lib.gui.model.elements.SideConfigElement; import zone.moddev.mc.lib.gui.sync.ElementSync; -/** A plain Java element uses the same capture and acknowledged action path as custom widgets. */ +/** + * A plain Java element uses the same capture and acknowledged action path as + * custom widgets. + */ final class ElementBehaviorClientSmoke { private static volatile int serverValue; private static int phase, drags, releases, accepted, cancelled; private static int enters, leaves; - static final ElementSync PROBE_STATE = ElementSync.field( - ResourceLocation.parse("mmdlib_companion:probe_state"), - StreamCodec.of( - (buffer, value) -> buffer.writeVarInt(value), buffer -> buffer.readVarInt()), - 5, - element -> 0) + static final ElementSync PROBE_STATE = ElementSync + .field(ResourceLocation.parse("mmdlib_companion:probe_state"), + StreamCodec.of((buffer, value) -> buffer.writeVarInt(value), + buffer -> buffer.readVarInt()), + 5, element -> 0) .sample((source, previous) -> serverValue) - .validate((element, value) -> value >= 0 && value <= 7) - .build(); + .validate((element, value) -> value >= 0 && value <= 7).build(); private static int initialSideMode; static final ElementAction PROBE = new ElementAction<>( ResourceLocation.parse("mmdlib_companion:behavior_probe"), - StreamCodec.of( - (buffer, value) -> buffer.writeVarInt(value), buffer -> buffer.readVarInt()), - 5, - (context, value) -> { + StreamCodec.of((buffer, value) -> buffer.writeVarInt(value), + buffer -> buffer.readVarInt()), + 5, (context, value) -> { if (value != 7) return false; serverValue = value; @@ -57,7 +57,8 @@ public Click mouseClicked(double x, double y, int button) { return Click.CAPTURE; } - public boolean mouseDragged(double x, double y, int button, double dx, double dy) { + public boolean mouseDragged(double x, double y, int button, + double dx, double dy) { drags++; return true; } @@ -65,15 +66,19 @@ public boolean mouseDragged(double x, double y, int button, double dx, double dy public boolean mouseReleased(double x, double y, int button) { releases++; if (!access.submit(PROBE, 7)) - throw new AssertionError("Plain element action was refused locally"); + throw new AssertionError( + "Plain element action was refused locally"); return true; } public void actionResult(boolean ok) { if (!ok) - throw new AssertionError("Plain element action was refused on the server"); - if (access.presentation().state().get(element, PROBE_STATE) != 7) - throw new AssertionError("Typed state was not applied before actionResult"); + throw new AssertionError( + "Plain element action was refused on the server"); + if (access.presentation().state().get(element, + PROBE_STATE) != 7) + throw new AssertionError( + "Typed state was not applied before actionResult"); accepted++; } @@ -88,7 +93,8 @@ public List tooltip(int x, int y) { } static boolean run(MachineScreen screen) { - var element = screen.getMenu().bound.screen().find("renderer_probe").orElseThrow(); + var element = screen.getMenu().bound.screen().find("renderer_probe") + .orElseThrow(); var bounds = screen.bounds(element); if (phase == 0) { screen.mouseMoved(-1, -1); @@ -101,13 +107,16 @@ static boolean run(MachineScreen screen) { screen.mouseMoved(-1, -1); if (enters != 1 || leaves != 1) throw new AssertionError("Screen hover must leave once"); - if (screen.tooltipLines(element, bounds.x() + 2, bounds.y() + 2).stream() - .noneMatch(line -> line.getString().equals("Plain element behavior"))) + if (screen.tooltipLines(element, bounds.x() + 2, bounds.y() + 2) + .stream().noneMatch( + line -> line.getString().equals("Plain element behavior"))) throw new AssertionError("Plain element tooltip missing"); if (!screen.mouseClicked(bounds.x() + 2, bounds.y() + 2, 0)) throw new AssertionError("Plain element did not capture"); - screen.mouseDragged(bounds.right() + 100, bounds.bottom() + 100, 0, 100, 100); - screen.mouseReleased(bounds.right() + 100, bounds.bottom() + 100, 0); + screen.mouseDragged(bounds.right() + 100, bounds.bottom() + 100, 0, + 100, 100); + screen.mouseReleased(bounds.right() + 100, bounds.bottom() + 100, + 0); phase++; return false; } @@ -115,21 +124,24 @@ static boolean run(MachineScreen screen) { if (accepted == 0) return false; if (serverValue != 7 || drags != 1 || releases != 1) - throw new AssertionError("Plain element capture/action did not complete"); + throw new AssertionError( + "Plain element capture/action did not complete"); int previous = cancelled; screen.mouseClicked(bounds.x() + 2, bounds.y() + 2, 0); - screen.resize(net.minecraft.client.Minecraft.getInstance(), screen.width, - screen.height); + screen.resize(net.minecraft.client.Minecraft.getInstance(), + screen.width, screen.height); screen.mouseReleased(bounds.x() + 2, bounds.y() + 2, 0); - if (cancelled <= previous || releases != 1 || screen.getMenu().elementPending(element)) - throw new AssertionError("Resize committed a cancelled plain-element capture"); + if (cancelled <= previous || releases != 1 + || screen.getMenu().elementPending(element)) + throw new AssertionError( + "Resize committed a cancelled plain-element capture"); phase++; } - var sides = (SideConfigElement) screen.getMenu().bound.screen().find("renderer_sides") - .orElseThrow(); + var sides = (SideConfigElement) screen.getMenu().bound.screen() + .find("renderer_sides").orElseThrow(); if (phase == 2) { - initialSideMode = screen.getMenu().elementStates().get(sides, SideConfigElement.MODES) - .getFirst(); + initialSideMode = screen.getMenu().elementStates() + .get(sides, SideConfigElement.MODES).getFirst(); var face = sides.face(screen.bounds(sides), 0); screen.mouseClicked(face.x() + 2, face.y() + 2, 0); phase++; @@ -138,11 +150,14 @@ static boolean run(MachineScreen screen) { if (phase == 3) { if (screen.getMenu().elementPending(sides)) return false; - if (screen.getMenu().elementStates().get(sides, SideConfigElement.MODES) + if (screen.getMenu().elementStates() + .get(sides, SideConfigElement.MODES) .getFirst() == initialSideMode) - throw new AssertionError("Inline side mode did not synchronize"); + throw new AssertionError( + "Inline side mode did not synchronize"); phase++; } - return NavigationClientSmoke.run(screen) && CompanionWidgetSmoke.run(screen); + return NavigationClientSmoke.run(screen) + && CompanionWidgetSmoke.run(screen); } } diff --git a/src/companionTest/java/zone/moddev/mc/companion/test/ElementInheritanceClientSmoke.java b/src/companionTest/java/zone/moddev/mc/companion/test/ElementInheritanceClientSmoke.java index f0568299..4319849a 100644 --- a/src/companionTest/java/zone/moddev/mc/companion/test/ElementInheritanceClientSmoke.java +++ b/src/companionTest/java/zone/moddev/mc/companion/test/ElementInheritanceClientSmoke.java @@ -34,19 +34,18 @@ import zone.moddev.mc.lib.gui.model.elements.ProgressElement; /** - * Subclasses inherit production renderers, snapshots and tank actions without new registrations. + * Subclasses inherit production renderers, snapshots and tank actions without + * new registrations. */ final class ElementInheritanceClientSmoke { - static int energyRenders, progressRenders, tankFills, gaugeFills, stage, attempts; + static int energyRenders, progressRenders, tankFills, gaugeFills, stage, + attempts; static volatile boolean prepared; static class Tank extends FluidTankElement { Tank() { - super( - ElementInheritanceClientSmoke.common("inherited_tank", false), - 0, - Orientation.VERTICAL, - 36); + super(ElementInheritanceClientSmoke.common("inherited_tank", false), + 0, Orientation.VERTICAL, 36); } public GaugeFill fill(Object value) { @@ -55,10 +54,12 @@ public GaugeFill fill(Object value) { } public Component displayText(Object value, boolean detailed) { - return Component.literal("tank " + super.displayText(value, detailed).getString()); + return Component.literal( + "tank " + super.displayText(value, detailed).getString()); } - public void appendTooltip(TooltipContext context, List rows) { + public void appendTooltip(TooltipContext context, + List rows) { super.appendTooltip(context, rows); rows.add(displayText(context.value(), context.shiftDown())); } @@ -68,9 +69,7 @@ static class Energy extends EnergyGaugeElement { Energy() { super( ElementInheritanceClientSmoke.common("inherited_energy", false), - Orientation.VERTICAL, - 36, - 0xff44aa77); + Orientation.VERTICAL, 36, 0xff44aa77); } public double fillRatio(Object value, Object maximum) { @@ -79,16 +78,15 @@ public double fillRatio(Object value, Object maximum) { } public Component displayText(Object value, boolean detailed) { - return Component.literal("custom " + super.displayText(value, detailed).getString()); + return Component.literal( + "custom " + super.displayText(value, detailed).getString()); } } static class Progress extends ProgressElement { Progress() { - super( - ElementInheritanceClientSmoke.common("inherited_progress", true), - Optional.empty(), - ProgressDirection.RIGHT); + super(ElementInheritanceClientSmoke.common("inherited_progress", + true), Optional.empty(), ProgressDirection.RIGHT); } public double fillRatio(Object value, Object maximum) { @@ -99,19 +97,19 @@ public double fillRatio(Object value, Object maximum) { static class Gauge extends GaugeElement { Gauge() { - super( - ElementInheritanceClientSmoke.common("inherited_gauge", true), - Orientation.HORIZONTAL, - 36, - Optional.empty()); + super(ElementInheritanceClientSmoke.common("inherited_gauge", true), + Orientation.HORIZONTAL, 36, Optional.empty()); } public GaugeFill fill(Object value) { if (value != null && !(value instanceof Number)) throw new AssertionError("Expected numeric gauge snapshot"); gaugeFills++; - return GaugeFill.fluid( - new FluidStack(net.minecraft.world.level.material.Fluids.WATER, 1000), 0xff55aaff); + return GaugeFill + .fluid( + new FluidStack( + net.minecraft.world.level.material.Fluids.WATER, 1000), + 0xff55aaff); } public double fillRatio(Object value, Object maximum) { @@ -121,31 +119,22 @@ public double fillRatio(Object value, Object maximum) { static class Row extends BoxElement { Row() { - super( - Common.empty(), - false, - 0, - 8, - Align.START, + super(Common.empty(), false, 0, 8, Align.START, List.of(new Tank(), new Energy(), new Progress(), new Gauge())); } } private static Common common(String id, boolean progress) { var c = Common.empty(); - return new Common( - Optional.of(id), - progress ? Optional.of(ValueRef.parse("#getProgress")) : Optional.empty(), - progress ? Optional.of(ValueRef.parse("#getTime")) : Optional.empty(), - c.tooltip(), - c.tooltipShift(), - c.clicks(), - c.onHover(), - c.hoverShows(), - c.jeiRecipes(), - 0, - 0, - 0); + return new Common(Optional.of(id), + progress + ? Optional.of(ValueRef.parse("#getProgress")) + : Optional.empty(), + progress + ? Optional.of(ValueRef.parse("#getTime")) + : Optional.empty(), + c.tooltip(), c.tooltipShift(), c.clicks(), c.onHover(), + c.hoverShows(), c.jeiRecipes(), 0, 0, 0); } static void install() { @@ -153,17 +142,11 @@ static void install() { var old = Screens.get(id).orElseThrow().screen(); var body = new ArrayList<>(old.body()); body.add(new Row()); - var model = new Screen( - old.id(), - old.source(), - old.title(), - old.blockEntity(), - old.playerInventory(), - body, - old.tabs()); + var model = new Screen(old.id(), old.source(), old.title(), + old.blockEntity(), old.playerInventory(), body, old.tabs()); var screens = new HashMap<>(Screens.all()); - screens.put(id, - ScreenBinder.bind(model, ElementInheritanceClientSmoke.class.getClassLoader())); + screens.put(id, ScreenBinder.bind(model, + ElementInheritanceClientSmoke.class.getClassLoader())); Screens.install(screens); } @@ -173,19 +156,18 @@ static boolean run(MachineScreen screen) { if (!(menu.value(tank, false) instanceof FluidStack fluid)) return false; if (++attempts > 40) - throw new AssertionError( - "Inherited tank interaction stuck at " - + stage - + ": " - + fluid - + ", cursor=" - + menu.getCarried()); + throw new AssertionError("Inherited tank interaction stuck at " + + stage + ": " + fluid + ", cursor=" + menu.getCarried()); if (stage == 0) { if (tankFills == 0 || gaugeFills == 0) - throw new AssertionError("Inherited renderer bypassed gauge fill selection"); - if (!GaugeFill.fluid(FluidStack.EMPTY).equals(GaugeFill.solid(0x777777))) - throw new AssertionError("Empty fluid must select the default solid fill"); - var original = new FluidStack(net.minecraft.world.level.material.Fluids.WATER, 750); + throw new AssertionError( + "Inherited renderer bypassed gauge fill selection"); + if (!GaugeFill.fluid(FluidStack.EMPTY) + .equals(GaugeFill.solid(0x777777))) + throw new AssertionError( + "Empty fluid must select the default solid fill"); + var original = new FluidStack( + net.minecraft.world.level.material.Fluids.WATER, 750); var selected = GaugeFill.fluid(original); original.setAmount(1); var returned = selected.fluid().orElseThrow(); @@ -194,48 +176,51 @@ static boolean run(MachineScreen screen) { returned.setAmount(2); if (selected.fluid().orElseThrow().getAmount() != 750) throw new AssertionError("Fill exposed mutable contents"); - var defaultGauge = new GaugeElement(Common.empty(), Orientation.VERTICAL, 36, - Optional.empty()); + var defaultGauge = new GaugeElement(Common.empty(), + Orientation.VERTICAL, 36, Optional.empty()); if (defaultGauge.fill(original).fluid().isEmpty()) throw new AssertionError("Default fluid fill lost"); - if (new GaugeElement(Common.empty(), Orientation.VERTICAL, 36, Optional.of(0x123456)) - .fill(original) - .fluid() - .isPresent()) - throw new AssertionError("Configured solid must override fluid"); + if (new GaugeElement(Common.empty(), Orientation.VERTICAL, 36, + Optional.of(0x123456)).fill(original).fluid().isPresent()) + throw new AssertionError( + "Configured solid must override fluid"); if (energyRenders == 0 || progressRenders == 0) - throw new AssertionError("Inherited renderer bypassed element fillRatio"); - if (screen.tooltipLines(tank, 0, 0).stream() - .noneMatch(row -> row.getString().equals("tank " + fluid.getAmount()))) - throw new AssertionError("Tank subclass display hook not composed"); - var energy = menu.bound.screen().find("inherited_energy").orElseThrow(); + throw new AssertionError( + "Inherited renderer bypassed element fillRatio"); + if (screen.tooltipLines(tank, 0, 0).stream().noneMatch( + row -> row.getString().equals("tank " + fluid.getAmount()))) + throw new AssertionError( + "Tank subclass display hook not composed"); + var energy = menu.bound.screen().find("inherited_energy") + .orElseThrow(); if (screen.tooltipLines(energy, 0, 0).stream() .noneMatch(row -> row.getString().startsWith("custom "))) - throw new AssertionError("Inherited energy tooltip bypassed displayText"); + throw new AssertionError( + "Inherited energy tooltip bypassed displayText"); var mc = Minecraft.getInstance(); - Screenshot.grab( - mc.gameDirectory, "inherited-elements.png", mc.getMainRenderTarget(), ignored -> { + Screenshot.grab(mc.gameDirectory, "inherited-elements.png", + mc.getMainRenderTarget(), ignored -> { }); - mc.getSingleplayerServer() - .execute( - () -> { - try { - var player = mc.getSingleplayerServer().getPlayerList().getPlayers() - .getFirst(); - var machine = (ProcessorEntity) ((zone.moddev.mc.lib.gui.runtime.MachineMenu) player.containerMenu).blockEntity; - machine.lanes.forEach(lane -> lane.reset()); - for (int i = 0; i < machine.items.getSlots(); i++) - machine.items.setStackInSlot(i, ItemStack.EMPTY); - machine.energy.setStored(0); - machine.fluids.drain(Integer.MAX_VALUE, FluidAction.EXECUTE); - player.getAbilities().instabuild = false; - player.containerMenu.setCarried(new ItemStack(Items.WATER_BUCKET)); - player.containerMenu.broadcastChanges(); - prepared = true; - } catch (Throwable failure) { - CompanionClientSmoke.fail(failure); - } - }); + mc.getSingleplayerServer().execute(() -> { + try { + var player = mc.getSingleplayerServer().getPlayerList() + .getPlayers().getFirst(); + var machine = (ProcessorEntity) ((zone.moddev.mc.lib.gui.runtime.MachineMenu) player.containerMenu).blockEntity; + machine.lanes.forEach(lane -> lane.reset()); + for (int i = 0; i < machine.items.getSlots(); i++) + machine.items.setStackInSlot(i, ItemStack.EMPTY); + machine.energy.setStored(0); + machine.fluids.drain(Integer.MAX_VALUE, + FluidAction.EXECUTE); + player.getAbilities().instabuild = false; + player.containerMenu + .setCarried(new ItemStack(Items.WATER_BUCKET)); + player.containerMenu.broadcastChanges(); + prepared = true; + } catch (Throwable failure) { + CompanionClientSmoke.fail(failure); + } + }); stage = 1; return false; } @@ -251,7 +236,8 @@ static boolean run(MachineScreen screen) { return false; } if (stage == 2) { - if (fluid.getAmount() != 1000 || !menu.getCarried().is(Items.BUCKET)) + if (fluid.getAmount() != 1000 + || !menu.getCarried().is(Items.BUCKET)) return false; screen.mouseClicked(r.x() + 8, r.y() + 8, 1); screen.mouseReleased(r.x() + 8, r.y() + 8, 1); diff --git a/src/companionTest/java/zone/moddev/mc/companion/test/ElementRendererClientSmoke.java b/src/companionTest/java/zone/moddev/mc/companion/test/ElementRendererClientSmoke.java index 805d8931..6035c9d6 100644 --- a/src/companionTest/java/zone/moddev/mc/companion/test/ElementRendererClientSmoke.java +++ b/src/companionTest/java/zone/moddev/mc/companion/test/ElementRendererClientSmoke.java @@ -39,11 +39,15 @@ import zone.moddev.mc.lib.gui.sync.ElementSync; import zone.moddev.mc.lib.gui.sync.SynchronizedElement; -/** A focused real-screen fixture for external element types and the shared drawing passes. */ +/** + * A focused real-screen fixture for external element types and the shared + * drawing passes. + */ final class ElementRendererClientSmoke { private static final List passes = new ArrayList<>(); private static final Set widgets = new HashSet<>(); - private static int playerSlots, machineSlots, playerOverlays, popoutBodies, phase; + private static int playerSlots, machineSlots, playerOverlays, popoutBodies, + phase; record ModElement(Common common) implements SynchronizedElement { public List> synchronizedValues() { @@ -85,77 +89,62 @@ private static Text text(String label) { private static Common common(String id, String value, String maximum) { var empty = Common.empty(); - return new Common( - Optional.of(id), + return new Common(Optional.of(id), Optional.ofNullable(value).map(ValueRef::parse), - Optional.ofNullable(maximum).map(ValueRef::parse), - Optional.empty(), - Optional.empty(), - empty.clicks(), - Optional.empty(), - Optional.empty(), - List.of(), - 0, - 0, - 0); + Optional.ofNullable(maximum).map(ValueRef::parse), Optional.empty(), + Optional.empty(), empty.clicks(), Optional.empty(), + Optional.empty(), List.of(), 0, 0, 0); } static void install() { var registry = ElementRenderers.GLOBAL; - registry.register( - ModElement.class, - (e, c) -> { - passes.clear(); - passes.add(ElementRenderers.Pass.BACKGROUND); - var r = c.bounds(); - c.graphics().fill(r.x(), r.y(), r.right(), r.bottom(), 0xff228855); - if (c.access().find("renderer_probe").orElseThrow() != e - || !c.access().bounds(e).equals(r)) - throw new AssertionError("Mod presentation services lost element identity"); - }); - registry.registerOverlay( - ModElement.class, - (e, c) -> { - passes.add(ElementRenderers.Pass.OVERLAY); - var r = c.bounds(); - c.graphics().renderOutline(r.x(), r.y(), r.width(), r.height(), 0xffffaa44); - }); - registry.registerForeground( - ModElement.class, (e, c) -> passes.add(ElementRenderers.Pass.FOREGROUND)); - registry.registerTop(ModElement.class, (e, c) -> passes.add(ElementRenderers.Pass.TOP)); + registry.register(ModElement.class, (e, c) -> { + passes.clear(); + passes.add(ElementRenderers.Pass.BACKGROUND); + var r = c.bounds(); + c.graphics().fill(r.x(), r.y(), r.right(), r.bottom(), 0xff228855); + if (c.access().find("renderer_probe").orElseThrow() != e + || !c.access().bounds(e).equals(r)) + throw new AssertionError( + "Mod presentation services lost element identity"); + }); + registry.registerOverlay(ModElement.class, (e, c) -> { + passes.add(ElementRenderers.Pass.OVERLAY); + var r = c.bounds(); + c.graphics().renderOutline(r.x(), r.y(), r.width(), r.height(), + 0xffffaa44); + }); + registry.registerForeground(ModElement.class, + (e, c) -> passes.add(ElementRenderers.Pass.FOREGROUND)); + registry.registerTop(ModElement.class, + (e, c) -> passes.add(ElementRenderers.Pass.TOP)); var sharedSlots = registry.require(SlotElement.class); - registry.register( - ItemSlotElement.class, - (e, c) -> { - sharedSlots.accept(c); - if (c.access().parent(e).isEmpty() && e.common().id().isEmpty()) - playerSlots++; - else - machineSlots++; - }); - registry.registerOverlay( - ItemSlotElement.class, - (e, c) -> { - if (c.access().parent(e).isEmpty() && e.common().id().isEmpty()) { - if (!c.access().visible(e) || !c.access().bounds(e).equals(c.bounds())) - throw new AssertionError( - "Implicit player slot has no presentation bounds/visibility"); - playerOverlays++; - } - }); + registry.register(ItemSlotElement.class, (e, c) -> { + sharedSlots.accept(c); + if (c.access().parent(e).isEmpty() && e.common().id().isEmpty()) + playerSlots++; + else + machineSlots++; + }); + registry.registerOverlay(ItemSlotElement.class, (e, c) -> { + if (c.access().parent(e).isEmpty() && e.common().id().isEmpty()) { + if (!c.access().visible(e) + || !c.access().bounds(e).equals(c.bounds())) + throw new AssertionError( + "Implicit player slot has no presentation bounds/visibility"); + playerOverlays++; + } + }); var sharedWidgets = registry.require(CustomElement.class); - registry.replace( - CustomElement.class, - (e, c) -> { - widgets.add(e.common().id().orElse("")); - sharedWidgets.accept(c); - }); - registry.replace( - ElementRenderers.Pass.FOREGROUND, - PopoutElement.class, - ElementRenderers.Clip.VIEWPORT, - (e, c) -> { - if (c.access().drawerPresentation(e, PopoutRenderer.Presentation.class).isPresent()) + registry.replace(CustomElement.class, (e, c) -> { + widgets.add(e.common().id().orElse("")); + sharedWidgets.accept(c); + }); + registry.replace(ElementRenderers.Pass.FOREGROUND, PopoutElement.class, + ElementRenderers.Clip.VIEWPORT, (e, c) -> { + if (c.access() + .drawerPresentation(e, PopoutRenderer.Presentation.class) + .isPresent()) popoutBodies++; PopoutRenderer.body(e, c); }); @@ -164,74 +153,51 @@ static void install() { var radio = new RadioGroupElement( common("renderer_radios", "#getReservePercent", null), Optional.of(text("Modes")), - List.of( - new RadioElement(Common.empty(), text("One")), + List.of(new RadioElement(Common.empty(), text("One")), new RadioElement(Common.empty(), text("Two")))); - var column = new BoxElement( - Common.empty(), - true, - 0, - 3, - Align.START, - List.of( - new ModElement(common("renderer_probe", null, null)), + var column = new BoxElement(Common.empty(), true, 0, 3, Align.START, + List.of(new ModElement(common("renderer_probe", null, null)), new ModButton(Common.empty()), - new CheckboxElement(common("renderer_checkbox", "#hasTank", null), text("Tank")), + new CheckboxElement( + common("renderer_checkbox", "#hasTank", null), + text("Tank")), new MeterElement( common("renderer_meter", "#getProgress", "#getTime"), - Orientation.HORIZONTAL, - 10, - 60, - 0xff55cc88, - 3, - 1, + Orientation.HORIZONTAL, 10, 60, 0xff55cc88, 3, 1, Optional.empty()))); - var row = new BoxElement( - Common.empty(), - false, - 0, - 8, - Align.START, - List.of( - column, - radio, - new SideConfigElement( - common("renderer_sides", null, null), List.of("input", "output")))); - var group = new GroupElement( - common("renderer_group", null, null), - Optional.of(text("Renderer extensions")), - Optional.empty(), - 4, + var row = new BoxElement(Common.empty(), false, 0, 8, Align.START, + List.of(column, radio, + new SideConfigElement(common("renderer_sides", null, null), + List.of("input", "output")))); + var group = new GroupElement(common("renderer_group", null, null), + Optional.of(text("Renderer extensions")), Optional.empty(), 4, List.of(row)); var body = new ArrayList<>(original.body()); body.add(Math.max(0, body.size() - 1), group); - var screen = new zone.moddev.mc.lib.gui.model.Screen( - original.id(), - original.source(), - original.title(), - original.blockEntity(), - original.playerInventory(), - body, - original.tabs()); + var screen = new zone.moddev.mc.lib.gui.model.Screen(original.id(), + original.source(), original.title(), original.blockEntity(), + original.playerInventory(), body, original.tabs()); var screens = new HashMap<>(Screens.all()); - screens.put(id, - ScreenBinder.bind(screen, ElementRendererClientSmoke.class.getClassLoader())); + screens.put(id, ScreenBinder.bind(screen, + ElementRendererClientSmoke.class.getClassLoader())); Screens.install(screens); } static boolean run(MachineScreen screen) { - var radio = (RadioGroupElement) screen.getMenu().bound.screen().find("renderer_radios") - .orElseThrow(); + var radio = (RadioGroupElement) screen.getMenu().bound.screen() + .find("renderer_radios").orElseThrow(); var selected = screen.getMenu().value(radio, false); for (int i = 0; i < radio.options().size(); i++) if (TextControlRenderer.selected(radio.options().get(i), - screen.presentation()) != java.math.BigInteger.valueOf(i).equals(selected)) + screen.presentation()) != java.math.BigInteger.valueOf(i) + .equals(selected)) throw new AssertionError( "Radio drawing disagrees with synchronized group selection"); if (!passes.equals(List.of(ElementRenderers.Pass.values()))) throw new AssertionError("Wrong element drawing passes: " + passes); if (playerSlots == 0 || machineSlots == 0 || playerOverlays == 0) - throw new AssertionError("Slot override did not render both player and machine slots"); + throw new AssertionError( + "Slot override did not render both player and machine slots"); if (!widgets.contains("progress")) throw new AssertionError("Custom widget stopped rendering"); if (phase == 0) { @@ -242,7 +208,8 @@ static boolean run(MachineScreen screen) { } if (phase == 1) { if (popoutBodies == 0) - throw new AssertionError("Popout foreground renderer did not run"); + throw new AssertionError( + "Popout foreground renderer did not run"); capture("renderer-popout.png"); click(screen, "info"); click(screen, "open_settings"); @@ -251,21 +218,25 @@ static boolean run(MachineScreen screen) { } if (!screen.getMenu().views().get("panel").equals("settings")) return false; - if (!widgets.contains("name_editor") || !widgets.contains("reserve_slider")) + if (!widgets.contains("name_editor") + || !widgets.contains("reserve_slider")) throw new AssertionError("Context widgets did not render"); capture("renderer-context.png"); return true; } private static void click(MachineScreen screen, String id) { - var r = screen.bounds(screen.getMenu().bound.screen().find(id).orElseThrow()); - if (!screen.mouseClicked(r.x() + r.width() / 2., r.y() + r.height() / 2., 0)) + var r = screen + .bounds(screen.getMenu().bound.screen().find(id).orElseThrow()); + if (!screen.mouseClicked(r.x() + r.width() / 2., + r.y() + r.height() / 2., 0)) throw new AssertionError("Click not consumed: " + id); } private static void capture(String name) { var mc = Minecraft.getInstance(); - Screenshot.grab(mc.gameDirectory, name, mc.getMainRenderTarget(), message -> { - }); + Screenshot.grab(mc.gameDirectory, name, mc.getMainRenderTarget(), + message -> { + }); } } diff --git a/src/companionTest/java/zone/moddev/mc/companion/test/FilterElementClientSmoke.java b/src/companionTest/java/zone/moddev/mc/companion/test/FilterElementClientSmoke.java index fadda94e..24335326 100644 --- a/src/companionTest/java/zone/moddev/mc/companion/test/FilterElementClientSmoke.java +++ b/src/companionTest/java/zone/moddev/mc/companion/test/FilterElementClientSmoke.java @@ -30,7 +30,8 @@ import zone.moddev.mc.lib.gui.model.elements.SlotElement; /** - * Real-client coverage for a mod filter element using only the shared registries and host service. + * Real-client coverage for a mod filter element using only the shared + * registries and host service. */ final class FilterElementClientSmoke { interface LeftLayer extends Element { @@ -43,15 +44,9 @@ record Probe(String id) implements FilterControl, LeftLayer, RightLayer { } static class ModSamples extends FilterControl.Samples { - private final SlotElement slot = new GhostSlotElement( - Common.empty(), - 0, - new SlotElement.Appearance( - SlotSize.LARGE, - Optional.of(0x55aa88), - Optional.empty(), - ResourceLocation.parse("mmdlib:slot"), - .8f)); + private final SlotElement slot = new GhostSlotElement(Common.empty(), 0, + new SlotElement.Appearance(SlotSize.LARGE, Optional.of(0x55aa88), + Optional.empty(), ResourceLocation.parse("mmdlib:slot"), .8f)); ModSamples() { super("samples", 5, ResourceType.ENERGY); @@ -75,24 +70,26 @@ public ItemStack sampleDisplay(CompoundTag configuration, int index) { return new ItemStack(index == 4 ? Items.DIAMOND : Items.EMERALD); } - public List sampleTooltip(CompoundTag configuration, int index) { - return List.of( - Component.literal("Mod sample " + index), Component.literal("Custom second row")); + public List sampleTooltip(CompoundTag configuration, + int index) { + return List.of(Component.literal("Mod sample " + index), + Component.literal("Custom second row")); } } private static final ModSamples samples = new ModSamples(); - private static int sampleRenders, submittedSample = -1, validations, behaviorCreations; + private static int sampleRenders, submittedSample = -1, validations, + behaviorCreations; private static String submittedValue; private static boolean rejectValidation = true; private static final Probe probe = new Probe("probe"); - private static final FilterControl.Text text = new FilterControl.Text("text", 8, - Component.literal("Text")); - private static final FilterControl.Number number = new FilterControl.Number("number", 0, 3, 1, - Component.literal("Number")); - private static final FilterControl.Custom custom = new FilterControl.Custom("deny", - CompanionAddons.TAG_MODE, 14); + private static final FilterControl.Text text = new FilterControl.Text( + "text", 8, Component.literal("Text")); + private static final FilterControl.Number number = new FilterControl.Number( + "number", 0, 3, 1, Component.literal("Number")); + private static final FilterControl.Custom custom = new FilterControl.Custom( + "deny", CompanionAddons.TAG_MODE, 14); private static ElementFragment fragment; private static int frames, phase, drags, releases, cancels, removals; private static int enters, leaves, hostEnters, hostLeaves, parentEnters; @@ -100,32 +97,26 @@ public List sampleTooltip(CompoundTag configuration, int index) { private static final CompoundTag config = new CompoundTag(); static void install() { - ElementRenderers.GLOBAL.register( - Probe.class, + ElementRenderers.GLOBAL.register(Probe.class, new ElementRenderers.Renderer() { public void validate(Probe element) { validations++; if (rejectValidation) - throw new IllegalArgumentException("Missing probe dependency"); + throw new IllegalArgumentException( + "Missing probe dependency"); } public void render(Probe element, ElementRenderers.Context c) { frames++; - c.graphics() - .fill( - c.bounds().x(), - c.bounds().y(), - c.bounds().right(), - c.bounds().bottom(), - 0xff55aa88); + c.graphics().fill(c.bounds().x(), c.bounds().y(), + c.bounds().right(), c.bounds().bottom(), 0xff55aa88); } }); ElementRenderers.GLOBAL.registerOverlay(LeftLayer.class, (e, c) -> { }); ElementRenderers.GLOBAL.registerOverlay(RightLayer.class, (e, c) -> { }); - ElementBehaviors.GLOBAL.register( - Probe.class, + ElementBehaviors.GLOBAL.register(Probe.class, (element, access) -> new ElementBehavior() { { behaviorCreations++; @@ -146,7 +137,8 @@ public Click mouseClicked(double x, double y, int button) { return Click.CAPTURE; } - public boolean mouseDragged(double x, double y, int button, double dx, double dy) { + public boolean mouseDragged(double x, double y, int button, + double dx, double dy) { drags++; return true; } @@ -171,7 +163,8 @@ public void removed() { static void render(GuiGraphics graphics) { if (fragment != null) { var r = fragment.bounds(probe); - fragment.render(graphics, phase == 1 ? r.x() + 2 : -1, phase == 1 ? r.y() + 2 : -1); + fragment.render(graphics, phase == 1 ? r.x() + 2 : -1, + phase == 1 ? r.y() + 2 : -1); } } @@ -186,11 +179,13 @@ public String source(Element element) { return "mod-filter.xml"; } }; - expectInvalidFragment(validationHost, "mod-filter.xml: Missing probe dependency"); + expectInvalidFragment(validationHost, + "mod-filter.xml: Missing probe dependency"); rejectValidation = false; - expectInvalidFragment( - validationHost, "mod-filter.xml: Ambiguous element renderers for OVERLAY"); - require(behaviorCreations == 0, "Validation must finish before behaviors are created"); + expectInvalidFragment(validationHost, + "mod-filter.xml: Ambiguous element renderers for OVERLAY"); + require(behaviorCreations == 0, + "Validation must finish before behaviors are created"); ElementRenderers.GLOBAL.registerOverlay(Probe.class, (e, c) -> { }); FilterEdits edits = (element, entry, value) -> { @@ -199,13 +194,13 @@ public String source(Element element) { submittedValue = value; return true; } - return element.edit( - new FilterControl.Edit(null, null, config, entry, value, null, null)); + return element.edit(new FilterControl.Edit(null, null, config, + entry, value, null, null)); }; fragment = new ElementFragment( - new FilterControl.Box("root", false, List.of(probe, text, number, custom, samples)), - 100, - new ElementFragment.Host() { + new FilterControl.Box("root", false, + List.of(probe, text, number, custom, samples)), + 100, new ElementFragment.Host() { public ResourceLocation screenId() { return ResourceLocation.parse("test:fragment"); } @@ -243,17 +238,17 @@ public Optional service(Class type) { require(frames > 0, "Mod control uses registered renderer"); require(validations >= 3 && behaviorCreations == 1, "Fragment shares renderer validation"); - require(sampleRenders > 0, "Sample subclass inherits renderer and display hook"); + require(sampleRenders > 0, + "Sample subclass inherits renderer and display hook"); require( - enters == 1 && leaves == 0 && hostEnters == 1 && parentEnters == 0, + enters == 1 && leaves == 0 && hostEnters == 1 + && parentEnters == 0, "Repeated fragment frames enter only the topmost element once"); fragment.place(5, 5, new Rect(5, 5, 1, fragment.height())); - require( - leaves == 1 && hostLeaves == 1, + require(leaves == 1 && hostLeaves == 1, "Clipping leaves even while the pointer is inside element bounds"); fragment.place(5, 5, new Rect(5, 5, 100, fragment.height())); - require( - enters == 2 && hostEnters == 2, + require(enters == 2 && hostEnters == 2, "Expanding the viewport reenters the stationary pointer"); var r = fragment.bounds(probe); fragment.click(r.x() + 2, r.y() + 2, 0); @@ -272,8 +267,7 @@ public Optional service(Class type) { before = cancels; fragment.place(6, 5, new Rect(6, 5, 100, 10)); fragment.release(500, 500, 0); - require( - cancels > before && releases == 1, + require(cancels > before && releases == 1, "Clipping resize cancels a partly visible capture"); fragment.place(6, 5, new Rect(6, 5, 100, fragment.height())); r = fragment.bounds(text); @@ -281,33 +275,39 @@ public Optional service(Class type) { fragment.release(r.x() + 3, r.y() + 5, 0); fragment.character('x', 0); fragment.key(257, 0, 0); - require(config.getString("text").equals("x"), "Text behavior submits its draft"); + require(config.getString("text").equals("x"), + "Text behavior submits its draft"); fragment.key(256, 0, 0); require(!fragment.character('y', 0), "Escape releases text focus"); r = fragment.bounds(number); fragment.click(r.right() - 2, r.y() + 5, 0); - require(config.getInt("number") == 1, "Number behavior edits its own value"); + require(config.getInt("number") == 1, + "Number behavior edits its own value"); pending = true; fragment.click(r.right() - 2, r.y() + 5, 0); - require(config.getInt("number") == 1, "Pending requests suppress edits"); + require(config.getInt("number") == 1, + "Pending requests suppress edits"); pending = false; r = fragment.bounds(custom); fragment.click(r.x() + 3, r.y() + 3, 0); fragment.release(r.x() + 3, r.y() + 3, 0); - require(config.getBoolean("deny"), "Existing filter widget adapts to shared routing"); - var sampleBounds = samples.sampleBounds(fragment.bounds(samples), 4); - var rows = fragment.tooltip(sampleBounds.x() + 1, sampleBounds.y() + 1); + require(config.getBoolean("deny"), + "Existing filter widget adapts to shared routing"); + var sampleBounds = samples.sampleBounds(fragment.bounds(samples), + 4); + var rows = fragment.tooltip(sampleBounds.x() + 1, + sampleBounds.y() + 1); require( - rows.stream().anyMatch(row -> row.getString().equals("Mod sample 4")) - && rows.stream().anyMatch(row -> row.getString().equals("Custom second row")), + rows.stream() + .anyMatch(row -> row.getString().equals("Mod sample 4")) + && rows.stream().anyMatch( + row -> row.getString().equals("Custom second row")), "Custom sample tooltip rows"); fragment.click(sampleBounds.x() + 1, sampleBounds.y() + 1, 0); - require( - submittedSample == 4 && submittedValue.equals("sample"), + require(submittedSample == 4 && submittedValue.equals("sample"), "Custom geometry selects sample index"); fragment.click(sampleBounds.x() + 1, sampleBounds.y() + 1, 1); - require( - submittedSample == 4 && submittedValue.equals("clear"), + require(submittedSample == 4 && submittedValue.equals("clear"), "Custom geometry preserves clearing"); submittedSample = -1; fragment.click(sampleBounds.x() - 1, sampleBounds.y() + 1, 0); @@ -318,15 +318,13 @@ public Optional service(Class type) { } if (phase == 2) { var mc = Minecraft.getInstance(); - Screenshot.grab( - mc.gameDirectory, "filter-element-fragment.png", mc.getMainRenderTarget(), - message -> { + Screenshot.grab(mc.gameDirectory, "filter-element-fragment.png", + mc.getMainRenderTarget(), message -> { }); var r = fragment.bounds(probe); fragment.mouseMoved(r.x() + 2, r.y() + 2); fragment.removed(); - require( - enters == hostEnters && leaves == hostLeaves, + require(enters == hostEnters && leaves == hostLeaves, "Host and behavior observe the same transitions"); fragment = null; require(removals == 1, "Replacement/removal releases mod state"); @@ -335,13 +333,13 @@ public Optional service(Class type) { return true; } - private static void expectInvalidFragment(ElementFragment.Host host, String message) { + private static void expectInvalidFragment(ElementFragment.Host host, + String message) { try { new ElementFragment(probe, 100, host); throw new AssertionError("Invalid fragment was accepted"); } catch (zone.moddev.mc.lib.gui.load.ScreenException expected) { - require( - expected.getMessage().startsWith(message), + require(expected.getMessage().startsWith(message), "Fragment diagnostic: " + expected.getMessage()); } } @@ -354,15 +352,16 @@ private static void verifyDefaultSamples() { entries.add(StringTag.valueOf("invalid id")); tags.put("samples", entries); var item = new FilterControl.Samples("samples", 3, ResourceType.ITEMS); - var fluid = new FilterControl.Samples("samples", 3, ResourceType.FLUIDS); - require(item.sampleDisplay(tags, 0).is(Items.STONE), "Default item display preserved"); + var fluid = new FilterControl.Samples("samples", 3, + ResourceType.FLUIDS); + require(item.sampleDisplay(tags, 0).is(Items.STONE), + "Default item display preserved"); require(fluid.sampleDisplay(tags, 1).is(Items.WATER_BUCKET), "Default fluid bucket preserved"); - require(item.sampleDisplay(tags, 2).isEmpty(), "Invalid sample remains empty"); + require(item.sampleDisplay(tags, 2).isEmpty(), + "Invalid sample remains empty"); require( - item.sampleTooltip(tags, 0) - .getFirst() - .getString() + item.sampleTooltip(tags, 0).getFirst().getString() .equals(new ItemStack(Items.STONE).getHoverName().getString()), "Default tooltip follows displayed sample"); } diff --git a/src/companionTest/java/zone/moddev/mc/companion/test/FragmentNavigationClientSmoke.java b/src/companionTest/java/zone/moddev/mc/companion/test/FragmentNavigationClientSmoke.java index f8a88871..050ffee5 100644 --- a/src/companionTest/java/zone/moddev/mc/companion/test/FragmentNavigationClientSmoke.java +++ b/src/companionTest/java/zone/moddev/mc/companion/test/FragmentNavigationClientSmoke.java @@ -29,7 +29,10 @@ import zone.moddev.mc.lib.gui.sync.ElementStateView; import zone.moddev.mc.lib.gui.sync.ElementStates; -/** Stateful fragments use the same navigation, selection and input rules as machine screens. */ +/** + * Stateful fragments use the same navigation, selection and input rules as + * machine screens. + */ final class FragmentNavigationClientSmoke { private record Control(Common common) implements Element { public Optional navigation() { @@ -46,26 +49,17 @@ public int[] measure(TextMeasure text) { return new int[]{100, height}; } - public void appendTooltip(TooltipContext context, List rows) { + public void appendTooltip(TooltipContext context, + List rows) { rows.add(Component.literal(common.id().orElseThrow())); } } private static Common named(String id) { var c = Common.empty(); - return new Common( - Optional.of(id), - c.value(), - c.max(), - c.tooltip(), - c.tooltipShift(), - c.clicks(), - c.onHover(), - c.hoverShows(), - c.jeiRecipes(), - 0, - 0, - 0); + return new Common(Optional.of(id), c.value(), c.max(), c.tooltip(), + c.tooltipShift(), c.clicks(), c.onHover(), c.hoverShows(), + c.jeiRecipes(), 0, 0, 0); } private static final Control control = new Control(named("navigate")); @@ -73,41 +67,36 @@ private static Common named(String id) { details = new Probe(named("details_probe"), 36); private static final ContextElement pages = new ContextElement( named("pages"), - List.of( - new ViewElement(named("home"), Optional.empty(), List.of(home)), - new ViewElement(named("details"), Optional.empty(), List.of(details)))); + List.of(new ViewElement(named("home"), Optional.empty(), List.of(home)), + new ViewElement(named("details"), Optional.empty(), + List.of(details)))); private static ElementFragment fragment; private static ElementStates state; - private static int phase, submissions, cancellations, results, homeDraws, detailDraws, resizes; + private static int phase, submissions, cancellations, results, homeDraws, + detailDraws, resizes; private static boolean focused, hidden; static void install() { - ElementRenderers.GLOBAL.register( - Control.class, - (e, c) -> { - var r = c.bounds(); - c.graphics().fill(r.x(), r.y(), r.right(), r.bottom(), 0xff406080); - c.graphics().drawString(c.access().font(), "Switch view", r.x() + 2, r.y() + 4, - 0xffffff); - }); - ElementRenderers.GLOBAL.register( - Probe.class, - (e, c) -> { - require(c.access().state() == state, "Renderer receives host state"); - if (e == home) - homeDraws++; - else - detailDraws++; - var r = c.bounds(); - c.graphics() - .fill(r.x(), r.y(), r.right(), r.bottom(), e == home ? 0xff408060 : 0xff805040); - c.graphics() - .drawString( - c.access().font(), e.common().id().orElseThrow(), r.x() + 2, r.y() + 4, - 0xffffff); - }); - ElementBehaviors.GLOBAL.register( - Probe.class, + ElementRenderers.GLOBAL.register(Control.class, (e, c) -> { + var r = c.bounds(); + c.graphics().fill(r.x(), r.y(), r.right(), r.bottom(), 0xff406080); + c.graphics().drawString(c.access().font(), "Switch view", r.x() + 2, + r.y() + 4, 0xffffff); + }); + ElementRenderers.GLOBAL.register(Probe.class, (e, c) -> { + require(c.access().state() == state, + "Renderer receives host state"); + if (e == home) + homeDraws++; + else + detailDraws++; + var r = c.bounds(); + c.graphics().fill(r.x(), r.y(), r.right(), r.bottom(), + e == home ? 0xff408060 : 0xff805040); + c.graphics().drawString(c.access().font(), + e.common().id().orElseThrow(), r.x() + 2, r.y() + 4, 0xffffff); + }); + ElementBehaviors.GLOBAL.register(Probe.class, (e, access) -> new ElementBehavior() { public boolean focusable() { return true; @@ -130,8 +119,7 @@ public void resized() { } public void actionResult(boolean accepted) { - require( - !accepted && pages.selected(state).equals("home"), + require(!accepted && pages.selected(state).equals("home"), "State applied before result"); results++; } @@ -148,16 +136,15 @@ static boolean run() { var root = new BoxElement(Common.empty(), true, 0, 4, Align.START, List.of(control, pages)); state = new ElementStates( - ElementFragmentLayout.of(root, 100, TextMeasure.NONE).elements(), - Minecraft.getInstance().level.registryAccess(), - () -> { + ElementFragmentLayout.of(root, 100, TextMeasure.NONE) + .elements(), + Minecraft.getInstance().level.registryAccess(), () -> { }); - fragment = new ElementFragment( - root, - 100, + fragment = new ElementFragment(root, 100, new ElementFragment.Host() { public ResourceLocation screenId() { - return ResourceLocation.parse("mmdlib_companion:fragment_navigation"); + return ResourceLocation + .parse("mmdlib_companion:fragment_navigation"); } public ElementStateView state() { @@ -169,13 +156,14 @@ public boolean visible(Element e) { } public void navigate(String target, String selection) { - require(target.equals("pages"), "Host receives navigation target"); + require(target.equals("pages"), + "Host receives navigation target"); pages.select(state, selection); } - public boolean submit(Element e, ElementAction action, T value) { - require( - e == control && action == Navigation.NAVIGATE, + public boolean submit(Element e, + ElementAction action, T value) { + require(e == control && action == Navigation.NAVIGATE, "Typed fragment navigation"); submissions++; return true; @@ -188,7 +176,8 @@ public boolean submit(Element e, ElementAction action, T value) { if (phase == 1) { if (homeDraws == 0 && detailDraws == 0) return false; - require(homeDraws > 0 && detailDraws == 0, "Only selected view renders"); + require(homeDraws > 0 && detailDraws == 0, + "Only selected view renders"); int initialHeight = fragment.height(); var r = fragment.bounds(home); fragment.click(r.x() + 2, r.y() + 2, 0); @@ -201,12 +190,11 @@ public boolean submit(Element e, ElementAction action, T value) { "Unchanged layout preserves capture and focus"); pages.select(state, "details"); fragment.refresh(); - require( - cancellations > before && !focused, + require(cancellations > before && !focused, "Hiding captured view cancels interaction and focus"); - require(fragment.height() > initialHeight, "Selection remeasures fragment"); - require( - fragment.bounds(details).y() > fragment.bounds(pages).y(), + require(fragment.height() > initialHeight, + "Selection remeasures fragment"); + require(fragment.bounds(details).y() > fragment.bounds(pages).y(), "Layout preparation reserves context header"); r = fragment.bounds(details); require( @@ -225,8 +213,7 @@ public boolean submit(Element e, ElementAction action, T value) { r = fragment.bounds(control); fragment.click(r.x() + 2, r.y() + 2, 0); fragment.refresh(); - require( - submissions == 1 && pages.selected(state).equals("details"), + require(submissions == 1 && pages.selected(state).equals("details"), "Generic control predicts via host navigation"); homeDraws = detailDraws = 0; phase++; @@ -235,14 +222,16 @@ public boolean submit(Element e, ElementAction action, T value) { if (phase == 2) { if (homeDraws == 0 && detailDraws == 0) return false; - require(homeDraws == 0 && detailDraws > 0, "Navigation changes rendered selection"); + require(homeDraws == 0 && detailDraws > 0, + "Navigation changes rendered selection"); var mc = Minecraft.getInstance(); - Screenshot.grab( - mc.gameDirectory, "fragment-navigation.png", mc.getMainRenderTarget(), message -> { + Screenshot.grab(mc.gameDirectory, "fragment-navigation.png", + mc.getMainRenderTarget(), message -> { }); pages.select(state, "home"); fragment.actionResult(control, false); - require(fragment.height() < 70, "Authoritative correction remeasures layout"); + require(fragment.height() < 70, + "Authoritative correction remeasures layout"); fragment.removed(); fragment = null; phase++; diff --git a/src/companionTest/java/zone/moddev/mc/companion/test/GroupPresentationClientSmoke.java b/src/companionTest/java/zone/moddev/mc/companion/test/GroupPresentationClientSmoke.java index 89d9c3f9..7afbda51 100644 --- a/src/companionTest/java/zone/moddev/mc/companion/test/GroupPresentationClientSmoke.java +++ b/src/companionTest/java/zone/moddev/mc/companion/test/GroupPresentationClientSmoke.java @@ -44,7 +44,8 @@ import zone.moddev.mc.lib.gui.runtime.component.BuiltinMenuComponents; /** - * Real inline and drawer callers consume mod-owned labels/highlights through presentation services. + * Real inline and drawer callers consume mod-owned labels/highlights through + * presentation services. */ final class GroupPresentationClientSmoke { private static int phase, labelReads; @@ -66,14 +67,8 @@ private static class ModPlayerSlot private static Appearance appearance(int index) { var a = player(index).appearance(); - return new Appearance( - a.size(), - Optional.of(0x88bbcc), - a.icon(), - a.background(), - a.ghostAlpha(), - a.iconAtlas(), - a.itemSize()); + return new Appearance(a.size(), Optional.of(0x88bbcc), a.icon(), + a.background(), a.ghostAlpha(), a.iconAtlas(), a.itemSize()); } } @@ -83,7 +78,8 @@ private static Appearance appearance(int index) { .noneOf(ElementHighlight.Kind.class); private record ModGroup(Common common) implements Element { - public List associatedGroups(Screen screen, List groups) { + public List associatedGroups(Screen screen, + List groups) { var ids = new ArrayList(); ids.add("tank_3"); ids.addAll(Element.super.associatedGroups(screen, groups)); @@ -96,9 +92,7 @@ public int[] measure(TextMeasure text) { } private static final class ModBehavior extends DefaultElementBehavior - implements - GroupLabelProvider, - HighlightPresentation { + implements GroupLabelProvider, HighlightPresentation { ModBehavior(ModGroup element, ElementAccess access) { super(access); } @@ -108,55 +102,46 @@ public Optional groupLabel() { return Optional.of(text("Mod tank")); } - public void highlight(GuiGraphics graphics, Rect bounds, ElementHighlight request) { + public void highlight(GuiGraphics graphics, Rect bounds, + ElementHighlight request) { customRequests.add(request.kind()); if (request.kind() == ElementHighlight.Kind.EMPHASIS) customEmphasis++; if (!bounds.equals(access.bounds())) throw new AssertionError("Highlight lost element bounds"); var r = request.outline(bounds); - graphics.renderOutline(r.x(), r.y(), r.width(), r.height(), 0xffff55cc); + graphics.renderOutline(r.x(), r.y(), r.width(), r.height(), + 0xffff55cc); graphics.fill(r.x(), r.y(), r.x() + 4, r.y() + 4, 0xff55ffff); } } private static Text text(String value) { - return new Text("test.group." + value.replace(' ', '_'), Optional.of(value)); + return new Text("test.group." + value.replace(' ', '_'), + Optional.of(value)); } private static Common common(String id, String tooltip) { var e = Common.empty(); - return new Common( - Optional.of(id), - e.value(), - e.max(), - Optional.of(text(tooltip)), - e.tooltipShift(), - e.clicks(), - e.onHover(), - e.hoverShows(), - e.jeiRecipes(), - 0, - 0, - 0); + return new Common(Optional.of(id), e.value(), e.max(), + Optional.of(text(tooltip)), e.tooltipShift(), e.clicks(), + e.onHover(), e.hoverShows(), e.jeiRecipes(), 0, 0, 0); } static void install() { - ElementRenderers.GLOBAL.replace( - PlayerSlotsElement.class, + ElementRenderers.GLOBAL.replace(PlayerSlotsElement.class, (element, context) -> renderedPlayerParts.add(element.part())); ElementBehaviors.GLOBAL.register(ModGroup.class, ModBehavior::new); - ElementRenderers.GLOBAL.register( - ModGroup.class, - (e, c) -> { - var r = c.bounds(); - c.graphics().fill(r.x(), r.y(), r.right(), r.bottom(), 0xff304050); - c.graphics().drawString(c.access().font(), "Mod", r.x() + 4, r.y() + 7, 0xffffff); - }); - ElementBehaviors.GLOBAL.replace( - ColouredGroupElement.class, + ElementRenderers.GLOBAL.register(ModGroup.class, (e, c) -> { + var r = c.bounds(); + c.graphics().fill(r.x(), r.y(), r.right(), r.bottom(), 0xff304050); + c.graphics().drawString(c.access().font(), "Mod", r.x() + 4, + r.y() + 7, 0xffffff); + }); + ElementBehaviors.GLOBAL.replace(ColouredGroupElement.class, (element, access) -> new ColouredGroupBehavior(element, access) { - public void highlight(GuiGraphics graphics, Rect bounds, ElementHighlight request) { + public void highlight(GuiGraphics graphics, Rect bounds, + ElementHighlight request) { colouredRequests.add(request.kind()); super.highlight(graphics, bounds, request); } @@ -164,66 +149,44 @@ public void highlight(GuiGraphics graphics, Rect bounds, ElementHighlight reques var id = ResourceLocation.parse("mmdlib_companion:logistics"); var original = Screens.get(id).orElseThrow().screen(); var body = new ArrayList(); - body.add( - new BoxElement( - Common.empty(), - false, - 0, - 6, - Align.START, - List.of( - new ModGroup(common("tank_1", "Wrong label")), - new GroupElement( - common("labelled_group", "Wrong label"), - Optional.of(text("Panel")), - Optional.empty(), - 3, - List.of(new LabelElement(Common.empty(), text("Body")))), - new ColouredGroupElement( - common("tank_2", "Coloured"), - 0xff55ff88, - new LabelElement(Common.empty(), text("Owned"))), - new LabelElement(common("tank_3", "Plain"), text("Plain")), - new SideConfigElement( - common("group_selector", "Select"), - List.of("tank_1", "labelled_group", "tank_2", "tank_3"))))); - body.add( - new BoxElement( - Common.empty(), - false, - 0, - 6, - Align.START, - List.of( - new ModArmour(common("player_armour", "Armour")), - new PlayerInventoryElement(common("player_items", "Inventory"), - ModPlayerSlot::new), - new PlayerOffhandElement( - common("player_offhand", "Offhand"), ModPlayerSlot::new)))); - var screen = new zone.moddev.mc.lib.gui.model.Screen( - original.id(), - original.source(), - original.title(), - original.blockEntity(), - original.playerInventory(), - body, - original.tabs()); + body.add(new BoxElement(Common.empty(), false, 0, 6, Align.START, + List.of(new ModGroup(common("tank_1", "Wrong label")), + new GroupElement(common("labelled_group", "Wrong label"), + Optional.of(text("Panel")), Optional.empty(), 3, + List.of(new LabelElement(Common.empty(), text("Body")))), + new ColouredGroupElement(common("tank_2", "Coloured"), + 0xff55ff88, + new LabelElement(Common.empty(), text("Owned"))), + new LabelElement(common("tank_3", "Plain"), text("Plain")), + new SideConfigElement(common("group_selector", "Select"), + List.of("tank_1", "labelled_group", "tank_2", "tank_3"))))); + body.add(new BoxElement(Common.empty(), false, 0, 6, Align.START, + List.of(new ModArmour(common("player_armour", "Armour")), + new PlayerInventoryElement(common("player_items", "Inventory"), + ModPlayerSlot::new), + new PlayerOffhandElement(common("player_offhand", "Offhand"), + ModPlayerSlot::new)))); + var screen = new zone.moddev.mc.lib.gui.model.Screen(original.id(), + original.source(), original.title(), original.blockEntity(), + original.playerInventory(), body, original.tabs()); var screens = new HashMap<>(Screens.all()); - screens.put(id, - ScreenBinder.bind(screen, GroupPresentationClientSmoke.class.getClassLoader())); + screens.put(id, ScreenBinder.bind(screen, + GroupPresentationClientSmoke.class.getClassLoader())); Screens.install(screens); } static boolean run(MachineScreen screen) { var menu = screen.getMenu(); - var automation = menu.components().require(BuiltinMenuComponents.AUTOMATION); + var automation = menu.components() + .require(BuiltinMenuComponents.AUTOMATION); if (automation.pending()) return false; var selector = menu.bound.screen().find("group_selector").orElseThrow(); switch (phase) { case 0 -> { require( - labelReads > 0 && customRequests.contains(ElementHighlight.Kind.SELECTION), + labelReads > 0 && customRequests + .contains(ElementHighlight.Kind.SELECTION), "Inline side config reads the mod caption and invokes its highlight capability"); require( customRequests.contains(ElementHighlight.Kind.DECORATION), @@ -235,8 +198,7 @@ static boolean run(MachineScreen screen) { select(automation, "tank_1"); } case 1 -> { - require( - customRequests.contains(ElementHighlight.Kind.EMPHASIS), + require(customRequests.contains(ElementHighlight.Kind.EMPHASIS), "Opened drawer emphasizes the mod group through the same capability"); capture("groups-custom.png"); click(screen, selector); @@ -247,7 +209,8 @@ static boolean run(MachineScreen screen) { } case 3 -> { require( - colouredRequests.containsAll(EnumSet.allOf(ElementHighlight.Kind.class)), + colouredRequests.containsAll( + EnumSet.allOf(ElementHighlight.Kind.class)), "Coloured wrapper handles decoration, emphasis and inline selection through its" + " contract"); capture("groups-coloured.png"); @@ -260,24 +223,25 @@ static boolean run(MachineScreen screen) { } case 5 -> { beforeAssociationClick = customEmphasis; - var bar = screen.layout.region(PlayerInventoryElement.TOOLBAR).orElseThrow(); + var bar = screen.layout.region(PlayerInventoryElement.TOOLBAR) + .orElseThrow(); require( screen.mouseClicked(screen.guiLeft() + bar.x() + 3, screen.guiTop() + bar.y() + 3, 0), "Generic colour bar accepts selection"); - screen.mouseReleased(screen.guiLeft() + bar.x() + 3, screen.guiTop() + bar.y() + 3, - 0); + screen.mouseReleased(screen.guiLeft() + bar.x() + 3, + screen.guiTop() + bar.y() + 3, 0); } case 6 -> { require( - automation.open() - && automation.inventory.group(automation.selected()).id().equals("tank_3"), + automation.open() && automation.inventory + .group(automation.selected()).id().equals("tank_3"), "Mod-owned association orders the bar and selects the original server group"); - require( - customEmphasis > beforeAssociationClick, + require(customEmphasis > beforeAssociationClick, "Mod-owned association also controls the selected group's highlight targets"); require( - renderedPlayerParts.containsAll(List.of("storage", "armour", "offhand")), + renderedPlayerParts + .containsAll(List.of("storage", "armour", "offhand")), "All three player sections render through the registered inherited renderer"); for (var slot : menu.playerSlotElements()) { var nativeSlot = menu.slot(slot); @@ -285,64 +249,50 @@ static boolean run(MachineScreen screen) { require( nativeSlot.x + screen.guiLeft() == bounds.x() + 1 && nativeSlot.y + screen.guiTop() == bounds.y() + 1, - "Native slot follows ordinary element bounds: " + slot.index()); + "Native slot follows ordinary element bounds: " + + slot.index()); } capture("groups-associations.png"); automation.send("close", -1, -1, "", 0, ""); var mc = Minecraft.getInstance(); var id = mc.player.getUUID(); - mc.getSingleplayerServer() - .execute( - () -> { - var player = mc.getSingleplayerServer().getPlayerList().getPlayer(id); - player - .getInventory() - .setItem( - 40, - new net.minecraft.world.item.ItemStack( - net.minecraft.world.item.Items.SHIELD)); - player - .getInventory() - .setItem( - 39, - new net.minecraft.world.item.ItemStack( - net.minecraft.world.item.Items.DIAMOND_HELMET)); - player.containerMenu.broadcastChanges(); - }); + mc.getSingleplayerServer().execute(() -> { + var player = mc.getSingleplayerServer().getPlayerList() + .getPlayer(id); + player.getInventory().setItem(40, + new net.minecraft.world.item.ItemStack( + net.minecraft.world.item.Items.SHIELD)); + player.getInventory().setItem(39, + new net.minecraft.world.item.ItemStack( + net.minecraft.world.item.Items.DIAMOND_HELMET)); + player.containerMenu.broadcastChanges(); + }); } case 7 -> { - if (!Minecraft.getInstance().player - .getInventory() - .getItem(40) + if (!Minecraft.getInstance().player.getInventory().getItem(40) .is(net.minecraft.world.item.Items.SHIELD)) { - require(++inventoryWait < 40, "Player inventory update timed out"); + require(++inventoryWait < 40, + "Player inventory update timed out"); return false; } - click( - screen, + click(screen, menu.playerSlotElements().stream() - .filter(slot -> slot.index() == 40) - .findFirst() + .filter(slot -> slot.index() == 40).findFirst() .orElseThrow()); } case 8 -> { require( menu.getCarried().is(net.minecraft.world.item.Items.SHIELD), "Offhand element click picks up its native item"); - click( - screen, + click(screen, menu.playerSlotElements().stream() - .filter(slot -> slot.index() == 40) - .findFirst() + .filter(slot -> slot.index() == 40).findFirst() .orElseThrow()); } case 9 -> { - require( - menu.getCarried().isEmpty() - && Minecraft.getInstance().player - .getInventory() - .getItem(40) - .is(net.minecraft.world.item.Items.SHIELD), + require(menu.getCarried().isEmpty() + && Minecraft.getInstance().player.getInventory().getItem(40) + .is(net.minecraft.world.item.Items.SHIELD), "Offhand element click returns its native item"); capture("player-sections.png"); } @@ -354,21 +304,22 @@ static boolean run(MachineScreen screen) { return false; } - private static void assertLabel(MachineScreen screen, String id, String expected) { + private static void assertLabel(MachineScreen screen, String id, + String expected) { var access = screen.presentation(); require( - access - .text(access.groupLabel(access.find(id).orElseThrow()).orElseThrow()) - .getString() - .equals(expected), + access.text( + access.groupLabel(access.find(id).orElseThrow()).orElseThrow()) + .getString().equals(expected), "Unexpected presentation caption for " + id); } - private static void select(zone.moddev.mc.lib.gui.runtime.AutomationMenu automation, - String id) { + private static void select( + zone.moddev.mc.lib.gui.runtime.AutomationMenu automation, String id) { for (int i = 0; i < automation.inventory.groups().size(); i++) if (automation.inventory.group(i).id().equals(id)) { - require(automation.send("select", i, -1, "", 0, ""), "Select group " + id); + require(automation.send("select", i, -1, "", 0, ""), + "Select group " + id); return; } throw new AssertionError("Missing resource group " + id); @@ -383,8 +334,9 @@ private static void click(MachineScreen screen, Element element) { private static void capture(String filename) { var mc = Minecraft.getInstance(); - Screenshot.grab(mc.gameDirectory, filename, mc.getMainRenderTarget(), message -> { - }); + Screenshot.grab(mc.gameDirectory, filename, mc.getMainRenderTarget(), + message -> { + }); } private static void require(boolean value, String message) { diff --git a/src/companionTest/java/zone/moddev/mc/companion/test/InventoryLifecycleTests.java b/src/companionTest/java/zone/moddev/mc/companion/test/InventoryLifecycleTests.java index 87cd2722..c07f2e40 100644 --- a/src/companionTest/java/zone/moddev/mc/companion/test/InventoryLifecycleTests.java +++ b/src/companionTest/java/zone/moddev/mc/companion/test/InventoryLifecycleTests.java @@ -17,128 +17,130 @@ @GameTestHolder(CompanionMod.ID) @PrefixGameTestTemplate(false) public final class InventoryLifecycleTests { - private static ProcessorEntity place(GameTestHelper h, boolean logistics) { - var pos = h.absolutePos(new BlockPos(2, 2, 2)); - h.getLevel() - .setBlock( - pos, - (logistics ? CompanionMod.LOGISTICS : CompanionMod.CRUSHER) - .block - .get() - .defaultBlockState(), + private static ProcessorEntity place(GameTestHelper h, boolean logistics) { + var pos = h.absolutePos(new BlockPos(2, 2, 2)); + h.getLevel().setBlock(pos, + (logistics ? CompanionMod.LOGISTICS : CompanionMod.CRUSHER).block + .get().defaultBlockState(), 3); - return (ProcessorEntity) h.getLevel().getBlockEntity(pos); - } + return (ProcessorEntity) h.getLevel().getBlockEntity(pos); + } - private static void require(boolean condition, String message) { - if (!condition) throw new AssertionError(message); - } + private static void require(boolean condition, String message) { + if (!condition) + throw new AssertionError(message); + } - @GameTest(template = "empty") - public static void pausePolicySeparatesProcessingTransfersAndRefunds(GameTestHelper h) { - var machine = place(h, true); - int[] ticks = {0}; - machine.features.add( - new MachineFeature() { - public String key() { - return "policy_probe"; - } + @GameTest(template = "empty") + public static void pausePolicySeparatesProcessingTransfersAndRefunds( + GameTestHelper h) { + var machine = place(h, true); + int[] ticks = {0}; + machine.features.add(new MachineFeature() { + public String key() { + return "policy_probe"; + } - public com.mojang.serialization.Codec codec() { - return com.mojang.serialization.Codec.INT; - } + public com.mojang.serialization.Codec codec() { + return com.mojang.serialization.Codec.INT; + } - public Integer state() { - return ticks[0]; - } + public Integer state() { + return ticks[0]; + } - public void restore(Integer value) { - ticks[0] = value; - } + public void restore(Integer value) { + ticks[0] = value; + } - public void tick(FeatureHost host) { - ticks[0]++; - } + public void tick(FeatureHost host) { + ticks[0]++; + } }); - var state = machine.automation.transferState("input", 0); - state.putBoolean("input", true); - state.putInt("batch", 8); - state.putLong("cost", 0); - state.put("item", new ItemStack(Items.DIAMOND, 3).save(h.getLevel().registryAccess())); - machine.energy.setStored(machine.energy.capacity()); - machine.automation.refund(machine.energy, 5); - machine.energy.setStored(machine.energy.capacity() - 5); - machine.inventoryLifecycle.tick(true, true, PausePolicy.BOTH); - require( - ticks[0] == 0 && machine.items.getStackInSlot(0).isEmpty(), - "Both policy suspends processing and transfers"); - require( - machine.automation.pendingRefund() == 0 - && machine.energy.stored() == machine.energy.capacity(), - "pause still returns already owed energy"); - machine.inventoryLifecycle.tick(true, true, PausePolicy.TRANSFERS_ONLY); - require( - ticks[0] == 1 && machine.items.getStackInSlot(0).isEmpty(), - "Transfer-only policy leaves processing enabled"); - machine.inventoryLifecycle.tick(true, true, PausePolicy.PROCESSING_ONLY); - require( - ticks[0] == 1 && machine.items.getStackInSlot(0).getCount() == 3, - "Processing-only policy still delivers owned escrow"); - machine.inventoryLifecycle.tick(true, true, PausePolicy.NEITHER); - require(ticks[0] == 2, "Neither policy ignores pause"); - machine.inventoryLifecycle.tick(false, false, PausePolicy.NEITHER); - require(ticks[0] == 2, "Independent processing gate always applies"); - h.succeed(); - } - - @GameTest(template = "empty") - public static void lifecycleLoadsStorageBeforeRestoringReservations(GameTestHelper h) { - var machine = place(h, false); - machine.items.insertItem(0, new ItemStack(Items.COPPER_ORE, 3), false); - machine.energy.setStored(100000); - machine.serverTick(); - require(machine.items.hasPlan(0), "recipe started and reserved input"); - var saved = new CompoundTag(); - machine.inventoryLifecycle.save(saved, h.getLevel().registryAccess()); - for (int i = 0; i < 3; i++) { - machine.items.restoreStack(0, ItemStack.EMPTY); - machine.inventoryLifecycle.load(saved, h.getLevel().registryAccess()); - require( - machine.items.getStackInSlot(0).getCount() == 3 && machine.items.plan(0).take() == 1, - "load restores contents before plan and never accumulates reservations"); + var state = machine.automation.transferState("input", 0); + state.putBoolean("input", true); + state.putInt("batch", 8); + state.putLong("cost", 0); + state.put("item", new ItemStack(Items.DIAMOND, 3) + .save(h.getLevel().registryAccess())); + machine.energy.setStored(machine.energy.capacity()); + machine.automation.refund(machine.energy, 5); + machine.energy.setStored(machine.energy.capacity() - 5); + machine.inventoryLifecycle.tick(true, true, PausePolicy.BOTH); + require(ticks[0] == 0 && machine.items.getStackInSlot(0).isEmpty(), + "Both policy suspends processing and transfers"); + require( + machine.automation.pendingRefund() == 0 + && machine.energy.stored() == machine.energy.capacity(), + "pause still returns already owed energy"); + machine.inventoryLifecycle.tick(true, true, PausePolicy.TRANSFERS_ONLY); + require(ticks[0] == 1 && machine.items.getStackInSlot(0).isEmpty(), + "Transfer-only policy leaves processing enabled"); + machine.inventoryLifecycle.tick(true, true, + PausePolicy.PROCESSING_ONLY); + require( + ticks[0] == 1 && machine.items.getStackInSlot(0).getCount() == 3, + "Processing-only policy still delivers owned escrow"); + machine.inventoryLifecycle.tick(true, true, PausePolicy.NEITHER); + require(ticks[0] == 2, "Neither policy ignores pause"); + machine.inventoryLifecycle.tick(false, false, PausePolicy.NEITHER); + require(ticks[0] == 2, "Independent processing gate always applies"); + h.succeed(); } - machine.inventoryLifecycle.load(new CompoundTag(), h.getLevel().registryAccess()); - require( - !machine.items.hasPlan(0) + + @GameTest(template = "empty") + public static void lifecycleLoadsStorageBeforeRestoringReservations( + GameTestHelper h) { + var machine = place(h, false); + machine.items.insertItem(0, new ItemStack(Items.COPPER_ORE, 3), false); + machine.energy.setStored(100000); + machine.serverTick(); + require(machine.items.hasPlan(0), "recipe started and reserved input"); + var saved = new CompoundTag(); + machine.inventoryLifecycle.save(saved, h.getLevel().registryAccess()); + for (int i = 0; i < 3; i++) { + machine.items.restoreStack(0, ItemStack.EMPTY); + machine.inventoryLifecycle.load(saved, + h.getLevel().registryAccess()); + require( + machine.items.getStackInSlot(0).getCount() == 3 + && machine.items.plan(0).take() == 1, + "load restores contents before plan and never accumulates reservations"); + } + machine.inventoryLifecycle.load(new CompoundTag(), + h.getLevel().registryAccess()); + require(!machine.items.hasPlan(0) && machine.lanes.getFirst().status() == RecipeProcessor.Status.IDLE, - "missing feature save does not retain previous running cycle"); - h.succeed(); - } + "missing feature save does not retain previous running cycle"); + h.succeed(); + } - @GameTest(template = "empty") - public static void displacedItemsSurviveBreakAndCapsuleRestoration(GameTestHelper h) { - var machine = place(h, true); - var old = new ItemStorage(3).slotId(0, "input").slotId(1, "output_1").slotId(2, "removed"); - old.restoreStack(2, new ItemStack(Items.DIAMOND, 9)); - machine.items.deserializeNBT( - h.getLevel().registryAccess(), old.serializeNBT(h.getLevel().registryAccess())); - var payloads = new ArrayList(); - machine.inventoryLifecycle.dropContents(payloads::add); - machine.inventoryLifecycle.dropContents(payloads::add); - require(payloads.size() == 1, "removed slot transfers to host once on destruction"); - var capsule = RecoveryCapsuleItem.create(payloads.getFirst()); - require( - RecoveryCapsuleItem.restore(capsule, machine), - "recovery capsule now restores displaced item resources"); - require( - capsule.isEmpty() && machine.items.getStackInSlot(0).getCount() == 9, - "recovery conserves every item and consumes capsule"); - machine.items.deserializeNBT( - h.getLevel().registryAccess(), old.serializeNBT(h.getLevel().registryAccess())); - machine.clearContent(); - require( - machine.items.recoveryContents().isEmpty(), - "explicit clear also removes displaced contents"); - h.succeed(); - } + @GameTest(template = "empty") + public static void displacedItemsSurviveBreakAndCapsuleRestoration( + GameTestHelper h) { + var machine = place(h, true); + var old = new ItemStorage(3).slotId(0, "input").slotId(1, "output_1") + .slotId(2, "removed"); + old.restoreStack(2, new ItemStack(Items.DIAMOND, 9)); + machine.items.deserializeNBT(h.getLevel().registryAccess(), + old.serializeNBT(h.getLevel().registryAccess())); + var payloads = new ArrayList(); + machine.inventoryLifecycle.dropContents(payloads::add); + machine.inventoryLifecycle.dropContents(payloads::add); + require(payloads.size() == 1, + "removed slot transfers to host once on destruction"); + var capsule = RecoveryCapsuleItem.create(payloads.getFirst()); + require(RecoveryCapsuleItem.restore(capsule, machine), + "recovery capsule now restores displaced item resources"); + require( + capsule.isEmpty() + && machine.items.getStackInSlot(0).getCount() == 9, + "recovery conserves every item and consumes capsule"); + machine.items.deserializeNBT(h.getLevel().registryAccess(), + old.serializeNBT(h.getLevel().registryAccess())); + machine.clearContent(); + require(machine.items.recoveryContents().isEmpty(), + "explicit clear also removes displaced contents"); + h.succeed(); + } } diff --git a/src/companionTest/java/zone/moddev/mc/companion/test/MachineRecipeLoadingTests.java b/src/companionTest/java/zone/moddev/mc/companion/test/MachineRecipeLoadingTests.java index 205168f6..8c82040a 100644 --- a/src/companionTest/java/zone/moddev/mc/companion/test/MachineRecipeLoadingTests.java +++ b/src/companionTest/java/zone/moddev/mc/companion/test/MachineRecipeLoadingTests.java @@ -25,193 +25,184 @@ @GameTestHolder("mmdlib_companion") @PrefixGameTestTemplate(false) public final class MachineRecipeLoadingTests { - private static final ResourceLocation TYPE = - ResourceLocation.parse("mmdlib_companion:simple_crushing"); - private static final ResourceLocation MACHINE = - ResourceLocation.parse("mmdlib_companion:simple_crusher"); - private static final String SINGLE = - """ - {"type":"mmdlib_companion:simple_crushing", - "inputs":{"input":[{"items":{"item":"minecraft:cobblestone"}}]}, - "outputs":{"output":[{"item":{"id":"minecraft:gravel"}}]},"time":1} - """; + private static final ResourceLocation TYPE = ResourceLocation + .parse("mmdlib_companion:simple_crushing"); + private static final ResourceLocation MACHINE = ResourceLocation + .parse("mmdlib_companion:simple_crusher"); + private static final String SINGLE = """ + {"type":"mmdlib_companion:simple_crushing", + "inputs":{"input":[{"items":{"item":"minecraft:cobblestone"}}]}, + "outputs":{"output":[{"item":{"id":"minecraft:gravel"}}]},"time":1} + """; - private static RecipeHolder read(GameTestHelper h, String id, String json) { - return new RecipeHolder<>( - ResourceLocation.parse(id), - Recipe.CODEC - .parse( - h.getLevel().registryAccess().createSerializationContext(JsonOps.INSTANCE), - JsonParser.parseString(json)) - .getOrThrow()); - } + private static RecipeHolder read(GameTestHelper h, String id, + String json) { + return new RecipeHolder<>(ResourceLocation.parse(id), + Recipe.CODEC + .parse(h.getLevel().registryAccess().createSerializationContext( + JsonOps.INSTANCE), JsonParser.parseString(json)) + .getOrThrow()); + } - private static List> load(GameTestHelper h, RecipeHolder... recipes) { - return GeneratedMachineRecipes.generate( - List.of(recipes), h.getLevel().registryAccess(), new ReloadTags(Map.of())); - } + private static List> load(GameTestHelper h, + RecipeHolder... recipes) { + return GeneratedMachineRecipes.generate(List.of(recipes), + h.getLevel().registryAccess(), new ReloadTags(Map.of())); + } - private static void require(boolean condition, String message) { - if (!condition) throw new AssertionError(message); - } + private static void require(boolean condition, String message) { + if (!condition) + throw new AssertionError(message); + } - @GameTest(template = "empty") - public static void authoredRecipesValidateGroupsAndResourceKinds(GameTestHelper h) { - for (var json : - List.of( - SINGLE.replace("\"input\":", "\"typo\":"), + @GameTest(template = "empty") + public static void authoredRecipesValidateGroupsAndResourceKinds( + GameTestHelper h) { + for (var json : List.of(SINGLE.replace("\"input\":", "\"typo\":"), SINGLE.replace("\"output\":", "\"typo\":"), SINGLE.replace("\"input\":", "\"energy\":"), SINGLE.replace("\"output\":", "\"energy\":"))) { - try { - load(h, read(h, "mmdlib_companion:bad_recipe", json)); - throw new AssertionError("Invalid authored recipe accepted"); - } catch (IllegalArgumentException expected) { - require( - expected.getMessage().contains("mmdlib_companion:bad_recipe") - && expected.getMessage().contains(MACHINE.toString()), - "Error identifies both recipe and machine"); - } + try { + load(h, read(h, "mmdlib_companion:bad_recipe", json)); + throw new AssertionError("Invalid authored recipe accepted"); + } catch (IllegalArgumentException expected) { + require( + expected.getMessage() + .contains("mmdlib_companion:bad_recipe") + && expected.getMessage().contains(MACHINE.toString()), + "Error identifies both recipe and machine"); + } + } + require(load(h, read(h, "mmdlib_companion:valid_recipe", SINGLE)) + .size() == 1, "Single JSON recipes remain supported"); + h.succeed(); } - require( - load(h, read(h, "mmdlib_companion:valid_recipe", SINGLE)).size() == 1, - "Single JSON recipes remain supported"); - h.succeed(); - } - @GameTest(template = "empty") - public static void bundlesExpandWithStableIdsAndRejectCollisions(GameTestHelper h) { - String prefix = "{\"type\":\"mmdlib:machine_recipes\",\"recipes\":{"; - var first = - load( - h, - read( - h, - "mmdlib_companion:batch", - prefix + "\"first\":" + SINGLE + ",\"second\":" + SINGLE + "}}")); - var reordered = - load( - h, - read( - h, - "mmdlib_companion:batch", - prefix + "\"second\":" + SINGLE + ",\"first\":" + SINGLE + "}}")); - require( - first.stream() - .map(RecipeHolder::id) - .toList() - .equals(reordered.stream().map(RecipeHolder::id).toList()), - "Reordering does not change IDs"); - require( - first.size() == 2 && first.stream().allMatch(r -> r.value() instanceof MachineRecipeEntry), - "Only flattened ordinary machine recipes survive"); - try { - load( - h, - read(h, "mmdlib_companion:batch", prefix + "\"first\":" + SINGLE + "}}"), - read(h, "mmdlib_companion:batch/first", SINGLE)); - throw new AssertionError("Duplicate authored ID accepted"); - } catch (IllegalArgumentException expected) { - require( - expected.getMessage().contains("mmdlib_companion:batch/first"), - "Duplicate diagnostic names stable ID"); - } - var conditional = - SINGLE.replace( - "\"time\":1", "\"neoforge:conditions\":[{\"type\":\"neoforge:false\"}],\"time\":1"); - require( - load( - h, - read( - h, - "mmdlib_companion:conditional", - prefix + "\"skipped\":" + conditional + ",\"kept\":" + SINGLE + "}}")) - .size() - == 1, - "Per-entry conditions are respected"); - require( - load(h, read(h, "mmdlib_companion:empty", prefix + "}}")).isEmpty(), - "Empty replacement removes all bundle entries"); - try { - load( - h, - read( - h, - "mmdlib_companion:bad_bundle", - prefix + "\"bad\":" + SINGLE.replace("\"input\":", "\"typo\":") + "}}")); - throw new AssertionError("Invalid bundled group accepted"); - } catch (IllegalArgumentException expected) { - require( - expected.getMessage().contains("mmdlib_companion:bad_bundle/bad"), - "Bundled diagnostic names file and entry"); + @GameTest(template = "empty") + public static void bundlesExpandWithStableIdsAndRejectCollisions( + GameTestHelper h) { + String prefix = "{\"type\":\"mmdlib:machine_recipes\",\"recipes\":{"; + var first = load(h, read(h, "mmdlib_companion:batch", + prefix + "\"first\":" + SINGLE + ",\"second\":" + SINGLE + "}}")); + var reordered = load(h, read(h, "mmdlib_companion:batch", + prefix + "\"second\":" + SINGLE + ",\"first\":" + SINGLE + "}}")); + require( + first.stream().map(RecipeHolder::id).toList() + .equals(reordered.stream().map(RecipeHolder::id).toList()), + "Reordering does not change IDs"); + require( + first.size() == 2 && first.stream() + .allMatch(r -> r.value() instanceof MachineRecipeEntry), + "Only flattened ordinary machine recipes survive"); + try { + load(h, + read(h, "mmdlib_companion:batch", + prefix + "\"first\":" + SINGLE + "}}"), + read(h, "mmdlib_companion:batch/first", SINGLE)); + throw new AssertionError("Duplicate authored ID accepted"); + } catch (IllegalArgumentException expected) { + require( + expected.getMessage().contains("mmdlib_companion:batch/first"), + "Duplicate diagnostic names stable ID"); + } + var conditional = SINGLE.replace("\"time\":1", + "\"neoforge:conditions\":[{\"type\":\"neoforge:false\"}],\"time\":1"); + require( + load(h, + read(h, "mmdlib_companion:conditional", + prefix + "\"skipped\":" + conditional + ",\"kept\":" + + SINGLE + "}}")) + .size() == 1, + "Per-entry conditions are respected"); + require( + load(h, read(h, "mmdlib_companion:empty", prefix + "}}")).isEmpty(), + "Empty replacement removes all bundle entries"); + try { + load(h, read(h, "mmdlib_companion:bad_bundle", prefix + "\"bad\":" + + SINGLE.replace("\"input\":", "\"typo\":") + "}}")); + throw new AssertionError("Invalid bundled group accepted"); + } catch (IllegalArgumentException expected) { + require( + expected.getMessage() + .contains("mmdlib_companion:bad_bundle/bad"), + "Bundled diagnostic names file and entry"); + } + h.succeed(); } - h.succeed(); - } - - @GameTest(template = "empty") - public static void realBundleFileLoadsIntoTheProcessor(GameTestHelper h) { - var manager = h.getLevel().getRecipeManager(); - require( - manager.byKey(ResourceLocation.parse("mmdlib_companion:simple_crushing/stone")).isEmpty(), - "Bundle container removed before publication"); - for (var name : List.of("cobblestone", "gravel")) - require( - manager - .byKey(ResourceLocation.parse("mmdlib_companion:simple_crushing/stone/" + name)) - .orElseThrow() - .value() - instanceof MachineRecipeEntry, - "Bundled recipe loaded at startup"); - var pos = h.absolutePos(new BlockPos(2, 2, 2)); - h.getLevel().setBlock(pos, BuiltInRegistries.BLOCK.get(MACHINE).defaultBlockState(), 3); - var machine = (DefinedMachineEntity) h.getLevel().getBlockEntity(pos); - machine.energy.setStored(2000); - require( - machine.items.insertItem(0, new ItemStack(Items.COBBLESTONE), false).isEmpty(), - "Bundled input admitted"); - for (int i = 0; i < 101; i++) machine.serverTick(); - require(machine.items.getStackInSlot(1).is(Items.GRAVEL), "Bundled recipe processed normally"); - h.succeed(); - } - @GameTest(template = "empty") - public static void menuHandshakeIncludesMachineChecksum(GameTestHelper h) { - var pos = h.absolutePos(new BlockPos(2, 2, 2)); - h.getLevel().setBlock(pos, BuiltInRegistries.BLOCK.get(MACHINE).defaultBlockState(), 3); - var machine = (DefinedMachineEntity) h.getLevel().getBlockEntity(pos); - var screen = Screens.get(MACHINE).orElseThrow().screen(); - var player = h.makeMockServerPlayerInLevel(); - var expected = ScreenRuntime.fingerprint(screen, machine); - require(!expected.equals(screen.fingerprint()), "Compatibility includes more than the screen"); - require( - machine.screenDefinitionFingerprint().equals(machine.definition.checksum()), - "Host exposes resolved XML CRC"); - for (var fingerprint : List.of(expected, screen.fingerprint())) { - var data = - new RegistryFriendlyByteBuf( - io.netty.buffer.Unpooled.buffer(), h.getLevel().registryAccess()); - try { - data.writeBlockPos(pos); - data.writeUtf(fingerprint); - data.writeLong(42); - if (fingerprint.equals(expected)) { - require( - new MachineMenu(1, player.getInventory(), data).widgetSession() == 42, - "Matching definition opens normally"); - } else { - try { - new MachineMenu(1, player.getInventory(), data); - throw new AssertionError("Mismatched machine definition accepted"); - } catch (IllegalStateException error) { + @GameTest(template = "empty") + public static void realBundleFileLoadsIntoTheProcessor(GameTestHelper h) { + var manager = h.getLevel().getRecipeManager(); + require( + manager.byKey(ResourceLocation + .parse("mmdlib_companion:simple_crushing/stone")).isEmpty(), + "Bundle container removed before publication"); + for (var name : List.of("cobblestone", "gravel")) require( - error.getMessage().contains("machine definitions differ"), - "Clear compatibility diagnostic"); - } + manager + .byKey(ResourceLocation.parse( + "mmdlib_companion:simple_crushing/stone/" + name)) + .orElseThrow().value() instanceof MachineRecipeEntry, + "Bundled recipe loaded at startup"); + var pos = h.absolutePos(new BlockPos(2, 2, 2)); + h.getLevel().setBlock(pos, + BuiltInRegistries.BLOCK.get(MACHINE).defaultBlockState(), 3); + var machine = (DefinedMachineEntity) h.getLevel().getBlockEntity(pos); + machine.energy.setStored(2000); + require(machine.items + .insertItem(0, new ItemStack(Items.COBBLESTONE), false).isEmpty(), + "Bundled input admitted"); + for (int i = 0; i < 101; i++) + machine.serverTick(); + require(machine.items.getStackInSlot(1).is(Items.GRAVEL), + "Bundled recipe processed normally"); + h.succeed(); + } + + @GameTest(template = "empty") + public static void menuHandshakeIncludesMachineChecksum(GameTestHelper h) { + var pos = h.absolutePos(new BlockPos(2, 2, 2)); + h.getLevel().setBlock(pos, + BuiltInRegistries.BLOCK.get(MACHINE).defaultBlockState(), 3); + var machine = (DefinedMachineEntity) h.getLevel().getBlockEntity(pos); + var screen = Screens.get(MACHINE).orElseThrow().screen(); + var player = h.makeMockServerPlayerInLevel(); + var expected = ScreenRuntime.fingerprint(screen, machine); + require(!expected.equals(screen.fingerprint()), + "Compatibility includes more than the screen"); + require( + machine.screenDefinitionFingerprint() + .equals(machine.definition.checksum()), + "Host exposes resolved XML CRC"); + for (var fingerprint : List.of(expected, screen.fingerprint())) { + var data = new RegistryFriendlyByteBuf( + io.netty.buffer.Unpooled.buffer(), + h.getLevel().registryAccess()); + try { + data.writeBlockPos(pos); + data.writeUtf(fingerprint); + data.writeLong(42); + if (fingerprint.equals(expected)) { + require( + new MachineMenu(1, player.getInventory(), data) + .widgetSession() == 42, + "Matching definition opens normally"); + } else { + try { + new MachineMenu(1, player.getInventory(), data); + throw new AssertionError( + "Mismatched machine definition accepted"); + } catch (IllegalStateException error) { + require( + error.getMessage() + .contains("machine definitions differ"), + "Clear compatibility diagnostic"); + } + } + } finally { + data.release(); + } } - } finally { - data.release(); - } + h.succeed(); } - h.succeed(); - } } diff --git a/src/companionTest/java/zone/moddev/mc/companion/test/NavigationClientSmoke.java b/src/companionTest/java/zone/moddev/mc/companion/test/NavigationClientSmoke.java index e4eff7cb..3ea43df7 100644 --- a/src/companionTest/java/zone/moddev/mc/companion/test/NavigationClientSmoke.java +++ b/src/companionTest/java/zone/moddev/mc/companion/test/NavigationClientSmoke.java @@ -31,21 +31,23 @@ import zone.moddev.mc.lib.gui.sync.ElementSync; import zone.moddev.mc.lib.gui.sync.SynchronizedElement; -/** Non-button controls and a non-context destination use the ordinary typed action lifecycle. */ +/** + * Non-button controls and a non-context destination use the ordinary typed + * action lifecycle. + */ final class NavigationClientSmoke { private static int phase; private static volatile boolean reset; - private static final ElementSync SELECTED = ElementSync.field( - ResourceLocation.parse("mmdlib_companion:navigation_selection"), - StreamCodec.of( - (buffer, value) -> buffer.writeVarInt(value), buffer -> buffer.readVarInt()), - 5, - element -> 0) - .validate((element, value) -> value == 0 || value == 1) - .affectsLayout() + private static final ElementSync SELECTED = ElementSync + .field(ResourceLocation.parse("mmdlib_companion:navigation_selection"), + StreamCodec.of((buffer, value) -> buffer.writeVarInt(value), + buffer -> buffer.readVarInt()), + 5, element -> 0) + .validate((element, value) -> value == 0 || value == 1).affectsLayout() .build(); - private record ModControl(Common common, ViewRef target) implements Element { + private record ModControl(Common common, ViewRef target) + implements Element { public Optional navigation() { return Optional.of(new Navigation(target)); } @@ -55,7 +57,8 @@ public int[] measure(TextMeasure text) { } } - private record ModDestination(Common common) implements NavigableElement, SynchronizedElement { + private record ModDestination(Common common) + implements NavigableElement, SynchronizedElement { public List> synchronizedValues() { return List.of(SELECTED); } @@ -84,69 +87,39 @@ public int[] measure(TextMeasure text) { private static Common common(String id) { var e = Common.empty(); - return new Common( - Optional.of(id), - e.value(), - e.max(), - e.tooltip(), - e.tooltipShift(), - e.clicks(), - e.onHover(), - e.hoverShows(), - e.jeiRecipes(), - 0, - 0, - 0); + return new Common(Optional.of(id), e.value(), e.max(), e.tooltip(), + e.tooltipShift(), e.clicks(), e.onHover(), e.hoverShows(), + e.jeiRecipes(), 0, 0, 0); } static void install() { - ElementRenderers.GLOBAL.register( - ModControl.class, - (e, c) -> { - var r = c.bounds(); - c.graphics().fill(r.x(), r.y(), r.right(), r.bottom(), 0xff406080); - c.graphics() - .drawString(c.access().font(), e.target().context(), r.x() + 2, r.y() + 4, - 0xffffff); - }); - ElementRenderers.GLOBAL.register( - ModDestination.class, - (e, c) -> { - var r = c.bounds(); - c.graphics() - .drawString( - c.access().font(), - e.selected(c.access().state()), - r.x() + 2, - r.y() + 4, - 0xffffff); - }); + ElementRenderers.GLOBAL.register(ModControl.class, (e, c) -> { + var r = c.bounds(); + c.graphics().fill(r.x(), r.y(), r.right(), r.bottom(), 0xff406080); + c.graphics().drawString(c.access().font(), e.target().context(), + r.x() + 2, r.y() + 4, 0xffffff); + }); + ElementRenderers.GLOBAL.register(ModDestination.class, (e, c) -> { + var r = c.bounds(); + c.graphics().drawString(c.access().font(), + e.selected(c.access().state()), r.x() + 2, r.y() + 4, 0xffffff); + }); var id = ResourceLocation.parse("mmdlib_companion:crusher"); var original = Screens.get(id).orElseThrow().screen(); var body = new ArrayList<>(original.body()); - body.add( - new BoxElement( - Common.empty(), - false, - 0, - 4, - Align.START, - List.of( - new ModControl( - common("navigation_custom"), - new ViewRef("navigation_destination", "details")), - new ModControl(common("navigation_builtin"), new ViewRef("panel", "settings")), - new ModDestination(common("navigation_destination"))))); - var screen = new zone.moddev.mc.lib.gui.model.Screen( - original.id(), - original.source(), - original.title(), - original.blockEntity(), - original.playerInventory(), - body, - original.tabs()); + body.add(new BoxElement(Common.empty(), false, 0, 4, Align.START, + List.of( + new ModControl(common("navigation_custom"), + new ViewRef("navigation_destination", "details")), + new ModControl(common("navigation_builtin"), + new ViewRef("panel", "settings")), + new ModDestination(common("navigation_destination"))))); + var screen = new zone.moddev.mc.lib.gui.model.Screen(original.id(), + original.source(), original.title(), original.blockEntity(), + original.playerInventory(), body, original.tabs()); var screens = new HashMap<>(Screens.all()); - screens.put(id, ScreenBinder.bind(screen, NavigationClientSmoke.class.getClassLoader())); + screens.put(id, ScreenBinder.bind(screen, + NavigationClientSmoke.class.getClassLoader())); Screens.install(screens); } @@ -154,22 +127,24 @@ static boolean run(MachineScreen screen) { if (phase > 10) return true; var menu = screen.getMenu(); - var custom = menu.bound.screen().find("navigation_custom").orElseThrow(); - var builtin = menu.bound.screen().find("navigation_builtin").orElseThrow(); - var destination = (NavigableElement) menu.bound.screen().find("navigation_destination") + var custom = menu.bound.screen().find("navigation_custom") .orElseThrow(); + var builtin = menu.bound.screen().find("navigation_builtin") + .orElseThrow(); + var destination = (NavigableElement) menu.bound.screen() + .find("navigation_destination").orElseThrow(); if (menu.elementPending(custom) || menu.elementPending(builtin)) return false; var selected = destination.selected(menu.elementStates()); switch (phase) { case 0 -> { click(screen, custom); - require( - destination.selected(menu.elementStates()).equals("details"), - "Custom control predicts selection"); + require(destination.selected(menu.elementStates()) + .equals("details"), "Custom control predicts selection"); } case 1 -> { - require(selected.equals("details"), "Server accepts custom navigation destination"); + require(selected.equals("details"), + "Server accepts custom navigation destination"); click(screen, custom); } case 2 -> { @@ -181,22 +156,21 @@ static boolean run(MachineScreen screen) { require(selected.equals("details"), "Custom selection confirmed before restoration"); var server = Minecraft.getInstance().getSingleplayerServer(); - server.execute( - () -> { - var serverMenu = (MachineMenu) server.getPlayerList().getPlayers() - .getFirst().containerMenu; - serverMenu.navigate("navigation_destination", "home"); - serverMenu.broadcastChanges(); - reset = true; - }); + server.execute(() -> { + var serverMenu = (MachineMenu) server.getPlayerList() + .getPlayers().getFirst().containerMenu; + serverMenu.navigate("navigation_destination", "home"); + serverMenu.broadcastChanges(); + reset = true; + }); } case 4 -> { if (!reset || !selected.equals("home")) return false; screen.removed(); - screen.resize(Minecraft.getInstance(), screen.width, screen.height); - require( - menu.elementPending(custom), + screen.resize(Minecraft.getInstance(), screen.width, + screen.height); + require(menu.elementPending(custom), "Remembered custom destination uses its custom control"); require( destination.selected(menu.elementStates()).equals("home"), @@ -208,20 +182,19 @@ static boolean run(MachineScreen screen) { click(screen, custom); } case 6 -> { - require(selected.equals("home"), "Custom destination reset before rejected action"); - var packet = menu.prepareElementAction( - custom, Navigation.NAVIGATE, new ElementActions.Click(0, 0, 1)) + require(selected.equals("home"), + "Custom destination reset before rejected action"); + var packet = menu.prepareElementAction(custom, + Navigation.NAVIGATE, new ElementActions.Click(0, 0, 1)) .orElseThrow(); menu.navigate("navigation_destination", "details"); PacketDistributor.sendToServer(packet); } case 7 -> { - require( - selected.equals("home"), + require(selected.equals("home"), "Rejected custom navigation restores authoritative selection"); menu.prepareElementAction(custom, Navigation.NAVIGATE, - new ElementActions.Click(0, 0, 0)) - .orElseThrow(); + new ElementActions.Click(0, 0, 0)).orElseThrow(); menu.navigate("navigation_destination", "details"); for (int i = 0; i < 100; i++) menu.tickClientActions(); @@ -232,14 +205,12 @@ static boolean run(MachineScreen screen) { click(screen, builtin); } case 9 -> { - require( - menu.views().get("panel").equals("settings"), + require(menu.views().get("panel").equals("settings"), "Custom control navigates a built-in context"); click(screen, builtin); } default -> { - require( - menu.views().get("panel").equals("machine"), + require(menu.views().get("panel").equals("machine"), "Custom control toggles built-in context back"); phase = 11; return true; @@ -251,8 +222,7 @@ static boolean run(MachineScreen screen) { private static void click(MachineScreen screen, Element element) { var r = screen.bounds(element); - require( - screen.mouseClicked(r.x() + 3, r.y() + 3, 0), + require(screen.mouseClicked(r.x() + 3, r.y() + 3, 0), "Custom navigation control consumes click"); screen.mouseReleased(r.x() + 3, r.y() + 3, 0); } diff --git a/src/companionTest/java/zone/moddev/mc/companion/test/PopoutClientSmoke.java b/src/companionTest/java/zone/moddev/mc/companion/test/PopoutClientSmoke.java index 4ec0326e..fbe5c5fc 100644 --- a/src/companionTest/java/zone/moddev/mc/companion/test/PopoutClientSmoke.java +++ b/src/companionTest/java/zone/moddev/mc/companion/test/PopoutClientSmoke.java @@ -27,19 +27,18 @@ import zone.moddev.mc.lib.gui.value.ValueType; import zone.moddev.mc.lib.gui.value.ValueTypes; -/** Element-owned popout presentation using inherited production drawing and drawer behavior. */ +/** + * Element-owned popout presentation using inherited production drawing and + * drawer behavior. + */ final class PopoutClientSmoke { private static int stage, attempts; private static boolean compact; private static class CustomPopout extends PopoutElement { CustomPopout(PopoutElement original) { - super( - withoutGetter(original.common()), - original.kind(), - original.icon(), - original.edge(), - original.text()); + super(withoutGetter(original.common()), original.kind(), + original.icon(), original.edge(), original.text()); } public int colour() { @@ -54,33 +53,23 @@ public Optional snapshot(ScreenHost host) { return Optional.of(new ElementSnapshot(7, null)); } - public List content(Object value, Function resolveText) { + public List content(Object value, + Function resolveText) { var rows = new ArrayList(); - rows.add(Component.literal("Load: " + value).withStyle(ChatFormatting.BOLD)); + rows.add(Component.literal("Load: " + value) + .withStyle(ChatFormatting.BOLD)); if (!compact) for (int i = 0; i < 60; i++) - rows.add( - Component.literal( - "Paragraph " + i - + ": this long line must wrap inside the popout body.")); + rows.add(Component.literal("Paragraph " + i + + ": this long line must wrap inside the popout body.")); return rows; } } private static Common withoutGetter(Common c) { - return new Common( - c.id(), - Optional.empty(), - c.max(), - c.tooltip(), - c.tooltipShift(), - c.clicks(), - c.onHover(), - c.hoverShows(), - c.jeiRecipes(), - c.offsetX(), - c.offsetY(), - c.line(), + return new Common(c.id(), Optional.empty(), c.max(), c.tooltip(), + c.tooltipShift(), c.clicks(), c.onHover(), c.hoverShows(), + c.jeiRecipes(), c.offsetX(), c.offsetY(), c.line(), c.interaction()); } @@ -90,79 +79,70 @@ static void install() { var tabs = new ArrayList<>(old.tabs()); var info = (PopoutElement) old.find("info").orElseThrow(); tabs.set(tabs.indexOf(info), new CustomPopout(info)); - var model = new Screen( - old.id(), - old.source(), - old.title(), - old.blockEntity(), - old.playerInventory(), - old.body(), - tabs, - old.origins(), - old.implicitElements()); + var model = new Screen(old.id(), old.source(), old.title(), + old.blockEntity(), old.playerInventory(), old.body(), tabs, + old.origins(), old.implicitElements()); var screens = new HashMap<>(Screens.all()); - screens.put(id, ScreenBinder.bind(model, PopoutClientSmoke.class.getClassLoader())); + screens.put(id, + ScreenBinder.bind(model, PopoutClientSmoke.class.getClassLoader())); Screens.install(screens); } static boolean run(MachineScreen screen) { if (++attempts > 40) throw new AssertionError("Popout smoke stuck at " + stage); - var element = screen.getMenu().bound.screen().find("info").orElseThrow(); + var element = screen.getMenu().bound.screen().find("info") + .orElseThrow(); var tab = screen.bounds(element); var drawer = screen.drawers().entries().stream() .filter(d -> d.definition().element().orElse(null) == element) - .findFirst() - .orElseThrow(); + .findFirst().orElseThrow(); if (stage == 0) { - if (!java.math.BigInteger.valueOf(7).equals(screen.getMenu().value(element, false))) + if (!java.math.BigInteger.valueOf(7) + .equals(screen.getMenu().value(element, false))) return false; if (drawer.colour() != 0x699fc4) throw new AssertionError("Element colour was ignored"); drawer.close(); screen.mouseClicked(tab.x() + 5, tab.y() + 5, 0); } else if (stage == 1) { - var presentation = screen - .presentation() + var presentation = screen.presentation() .drawerPresentation(element, PopoutRenderer.Presentation.class) .orElseThrow(); var first = new StringBuilder(); - presentation - .lines() - .getFirst() - .accept( - (index, style, codepoint) -> { - if (!style.isBold()) - throw new AssertionError("Content style was discarded"); - first.appendCodePoint(codepoint); - return true; - }); - if (!first.toString().equals("Load: 7") || presentation.lines().size() <= 61) - throw new AssertionError("Custom numeric content was not consumed and wrapped"); + presentation.lines().getFirst() + .accept((index, style, codepoint) -> { + if (!style.isBold()) + throw new AssertionError("Content style was discarded"); + first.appendCodePoint(codepoint); + return true; + }); + if (!first.toString().equals("Load: 7") + || presentation.lines().size() <= 61) + throw new AssertionError( + "Custom numeric content was not consumed and wrapped"); capture("popout-custom-content.png"); var body = presentation.body(); if (!screen.mouseScrolled(body.x() + 8, body.y() + 8, 0, -5)) throw new AssertionError("Popout did not accept scrolling"); } else if (stage == 2) { - var presentation = screen - .presentation() + var presentation = screen.presentation() .drawerPresentation(element, PopoutRenderer.Presentation.class) .orElseThrow(); if (presentation.scroll() <= 0) throw new AssertionError("Popout did not scroll"); compact = true; } else if (stage == 3) { - var presentation = screen - .presentation() + var presentation = screen.presentation() .drawerPresentation(element, PopoutRenderer.Presentation.class) .orElseThrow(); if (presentation.scroll() != 0 || presentation.lines().size() != 1) - throw new AssertionError("Popout scroll did not clamp after content shrank"); + throw new AssertionError( + "Popout scroll did not clamp after content shrank"); capture("popout-custom-compact.png"); screen.mouseClicked(tab.x() + 5, tab.y() + 5, 0); } else { - if (screen - .presentation() + if (screen.presentation() .drawerPresentation(element, PopoutRenderer.Presentation.class) .isPresent()) throw new AssertionError("Popout did not close"); @@ -174,7 +154,8 @@ static boolean run(MachineScreen screen) { private static void capture(String name) { var mc = Minecraft.getInstance(); - Screenshot.grab(mc.gameDirectory, name, mc.getMainRenderTarget(), message -> { - }); + Screenshot.grab(mc.gameDirectory, name, mc.getMainRenderTarget(), + message -> { + }); } } diff --git a/src/companionTest/java/zone/moddev/mc/companion/test/ProcessorButtonClientSmoke.java b/src/companionTest/java/zone/moddev/mc/companion/test/ProcessorButtonClientSmoke.java index 0648029c..fae0a883 100644 --- a/src/companionTest/java/zone/moddev/mc/companion/test/ProcessorButtonClientSmoke.java +++ b/src/companionTest/java/zone/moddev/mc/companion/test/ProcessorButtonClientSmoke.java @@ -16,7 +16,10 @@ import zone.moddev.mc.lib.gui.runtime.MachineMenu; import zone.moddev.mc.lib.gui.runtime.component.BuiltinMenuComponents; -/** Real clicks and server snapshots for both directions of manual/redstone disagreement. */ +/** + * Real clicks and server snapshots for both directions of manual/redstone + * disagreement. + */ final class ProcessorButtonClientSmoke { private static int step, attempts; private static volatile boolean ready; @@ -29,74 +32,72 @@ static boolean run(MachineScreen screen) { if (step > 7) return true; if (++attempts > 120) - throw new AssertionError("Processor button smoke stalled at step " + step); + throw new AssertionError( + "Processor button smoke stalled at step " + step); if (step > 0 && !ready) return false; var menu = screen.getMenu(); - var button = (ProcessorButtonElement) menu.bound.screen().find("toggle_0").orElseThrow(); + var button = (ProcessorButtonElement) menu.bound.screen() + .find("toggle_0").orElseThrow(); if (!button.interactive()) throw new AssertionError("Companion must use the generic control"); var wireState = menu.value(button, false); var state = ProcessorButtonState.fromWire(wireState).orElse(null); switch (step) { - case 0 -> - server( - machine -> { - savedPause = machine.lanes.getFirst().paused(); - savedReserve = machine.getReservePercent(); - savedEnergy = machine.energy.stored(); - savedRedstone = machine.redstone.save(machine.getLevel().registryAccess()); - machine.lanes.forEach(lane -> lane.reset()); - machine.items.setStackInSlot(0, new ItemStack(Items.COPPER_ORE, 16)); - machine.items.setStackInSlot(1, ItemStack.EMPTY); - machine.items.setStackInSlot(2, ItemStack.EMPTY); - machine.setReservePercent(0); - machine.energy.setStored(100000); - for (int face = 0; face < 6; face++) - machine.redstone.mode(face, RedstoneControl.Mode.IGNORE); - machine.features.setProcessorPaused("lane_0", false); - }); + case 0 -> server(machine -> { + savedPause = machine.lanes.getFirst().paused(); + savedReserve = machine.getReservePercent(); + savedEnergy = machine.energy.stored(); + savedRedstone = machine.redstone + .save(machine.getLevel().registryAccess()); + machine.lanes.forEach(lane -> lane.reset()); + machine.items.setStackInSlot(0, + new ItemStack(Items.COPPER_ORE, 16)); + machine.items.setStackInSlot(1, ItemStack.EMPTY); + machine.items.setStackInSlot(2, ItemStack.EMPTY); + machine.setReservePercent(0); + machine.energy.setStored(100000); + for (int face = 0; face < 6; face++) + machine.redstone.mode(face, RedstoneControl.Mode.IGNORE); + machine.features.setProcessorPaused("lane_0", false); + }); case 1 -> { if (state != ProcessorButtonState.PROCESSING - || menu.components().get(BuiltinMenuComponents.REDSTONE).watching()) + || menu.components().get(BuiltinMenuComponents.REDSTONE) + .watching()) return false; require(!button.conflict(menu.value(button, true)), "No monitoring means no red dot"); - tooltip( - screen, - button, - "Working", + tooltip(screen, button, "Working", "Click to pause and release reservations", "Progress is kept"); require( - button.iconFor(wireState).orElseThrow().getPath().equals("icon_processor_stop"), + button.iconFor(wireState).orElseThrow().getPath() + .equals("icon_processor_stop"), "An actively processing machine shows Stop"); capture("processor-enabled-no-watch.png"); - server( - machine -> { - machine.redstone.mode(0, RedstoneControl.Mode.READ); - machine.redstone.range(0, 0, 15); - machine.redstone.outside(0, false); - }); + server(machine -> { + machine.redstone.mode(0, RedstoneControl.Mode.READ); + machine.redstone.range(0, 0, 15); + machine.redstone.outside(0, false); + }); } case 2 -> { - if (!menu.components().get(BuiltinMenuComponents.REDSTONE).watching() - || !menu.components().get(BuiltinMenuComponents.REDSTONE).paused() + if (!menu.components().get(BuiltinMenuComponents.REDSTONE) + .watching() + || !menu.components().get(BuiltinMenuComponents.REDSTONE) + .paused() || state != ProcessorButtonState.HELD) return false; require( - button.iconFor(wireState).orElseThrow().getPath().equals("icon_processor_stop"), + button.iconFor(wireState).orElseThrow().getPath() + .equals("icon_processor_stop"), "A redstone-held processor retains work and shows Stop"); - require( - button.conflict(menu.value(button, true)), + require(button.conflict(menu.value(button, true)), "Enabled processor conflicts with redstone hold"); - tooltip( - screen, - button, - "Held by machine controls", + tooltip(screen, button, "Held by machine controls", "Click to pause and release reservations", - "Progress is kept", - "Redstone settings conflict:", + "Progress is kept", "Redstone settings conflict:", "Paused by redstone."); capture("processor-redstone-hold.png"); click(screen, button); @@ -105,8 +106,8 @@ static boolean run(MachineScreen screen) { if (state != ProcessorButtonState.PAUSED) return false; require( - menu.components().get(BuiltinMenuComponents.REDSTONE).paused() - && !button.conflict(menu.value(button, true)), + menu.components().get(BuiltinMenuComponents.REDSTONE) + .paused() && !button.conflict(menu.value(button, true)), "Both pause controls agree"); tooltip(screen, button, "Paused", "Click to unpause"); require( @@ -117,17 +118,14 @@ static boolean run(MachineScreen screen) { server(machine -> machine.redstone.outside(0, true)); } case 4 -> { - if (menu.components().get(BuiltinMenuComponents.REDSTONE).paused()) + if (menu.components().get(BuiltinMenuComponents.REDSTONE) + .paused()) return false; require( state == ProcessorButtonState.PAUSED && button.conflict(menu.value(button, true)), "Clearing redstone preserves manual pause and shows the conflict"); - tooltip( - screen, - button, - "Paused", - "Click to unpause", + tooltip(screen, button, "Paused", "Click to unpause", "Redstone settings conflict:", "Manual pause takes priority."); capture("processor-manual-hold.png"); @@ -137,7 +135,8 @@ static boolean run(MachineScreen screen) { if (state != ProcessorButtonState.PROCESSING) return false; require( - menu.components().get(BuiltinMenuComponents.REDSTONE).watching() + menu.components().get(BuiltinMenuComponents.REDSTONE) + .watching() && !button.conflict(menu.value(button, true)), "Both allow controls agree"); capture("processor-redstone-run.png"); @@ -146,29 +145,27 @@ static boolean run(MachineScreen screen) { case 6 -> { if (state != ProcessorButtonState.STALLED) return false; - require( - !button.conflict(menu.value(button, true)), + require(!button.conflict(menu.value(button, true)), "Lack of power is not a manual/redstone disagreement"); - tooltip( - screen, - button, - "Needs power", + tooltip(screen, button, "Needs power", "Click to pause and release reservations", "Progress is kept"); require( - button.iconFor(wireState).orElseThrow().getPath().equals("icon_processor_stop"), + button.iconFor(wireState).orElseThrow().getPath() + .equals("icon_processor_stop"), "A stalled processor without power retains work and shows Stop"); capture("processor-stalled-no-power.png"); - server( - machine -> { - machine.energy.setStored(savedEnergy); - machine.setReservePercent(savedReserve); - machine.redstone.load(savedRedstone, machine.getLevel().registryAccess()); - machine.features.setProcessorPaused("lane_0", savedPause); - }); + server(machine -> { + machine.energy.setStored(savedEnergy); + machine.setReservePercent(savedReserve); + machine.redstone.load(savedRedstone, + machine.getLevel().registryAccess()); + machine.features.setProcessorPaused("lane_0", savedPause); + }); } case 7 -> { - if (state == null || (state == ProcessorButtonState.PAUSED) != savedPause) + if (state == null + || (state == ProcessorButtonState.PAUSED) != savedPause) return false; } } @@ -177,46 +174,49 @@ static boolean run(MachineScreen screen) { return false; } - private static void tooltip( - MachineScreen screen, ProcessorButtonElement button, String... expected) { + private static void tooltip(MachineScreen screen, + ProcessorButtonElement button, String... expected) { var bounds = screen.bounds(button); - var actual = screen.tooltipLines(button, bounds.x(), bounds.y()).stream() - .map(net.minecraft.network.chat.Component::getString) + var actual = screen.tooltipLines(button, bounds.x(), bounds.y()) + .stream().map(net.minecraft.network.chat.Component::getString) .toList(); require(actual.equals(java.util.List.of(expected)), "Unexpected processor tooltip: " + actual); } - private static void click(MachineScreen screen, ProcessorButtonElement button) { + private static void click(MachineScreen screen, + ProcessorButtonElement button) { var bounds = screen.bounds(button); - double x = bounds.x() + bounds.width() / 2., y = bounds.y() + bounds.height() / 2.; - require(screen.mouseClicked(x, y, 0), "Generic button accepts the real click"); + double x = bounds.x() + bounds.width() / 2., + y = bounds.y() + bounds.height() / 2.; + require(screen.mouseClicked(x, y, 0), + "Generic button accepts the real click"); screen.mouseReleased(x, y, 0); } private static void server(Consumer action) { ready = false; var server = Minecraft.getInstance().getSingleplayerServer(); - server.execute( - () -> { - try { - var player = server.getPlayerList().getPlayers().getFirst(); - var menu = (MachineMenu) player.containerMenu; - var machine = (ProcessorEntity) menu.blockEntity; - action.accept(machine); - machine.redstone.readInputs(); - menu.broadcastChanges(); - ready = true; - } catch (Throwable error) { - CompanionClientSmoke.fail(error); - } - }); + server.execute(() -> { + try { + var player = server.getPlayerList().getPlayers().getFirst(); + var menu = (MachineMenu) player.containerMenu; + var machine = (ProcessorEntity) menu.blockEntity; + action.accept(machine); + machine.redstone.readInputs(); + menu.broadcastChanges(); + ready = true; + } catch (Throwable error) { + CompanionClientSmoke.fail(error); + } + }); } private static void capture(String name) { var mc = Minecraft.getInstance(); - Screenshot.grab(mc.gameDirectory, name, mc.getMainRenderTarget(), message -> { - }); + Screenshot.grab(mc.gameDirectory, name, mc.getMainRenderTarget(), + message -> { + }); } private static void require(boolean value, String message) { diff --git a/src/companionTest/java/zone/moddev/mc/companion/test/RedstoneClientSmoke.java b/src/companionTest/java/zone/moddev/mc/companion/test/RedstoneClientSmoke.java index 596ec6ba..7f11c130 100644 --- a/src/companionTest/java/zone/moddev/mc/companion/test/RedstoneClientSmoke.java +++ b/src/companionTest/java/zone/moddev/mc/companion/test/RedstoneClientSmoke.java @@ -11,7 +11,10 @@ import zone.moddev.mc.lib.gui.layout.RedstoneLayout; import zone.moddev.mc.lib.gui.runtime.component.BuiltinMenuComponents; -/** Real face controls, crossing range handles, output cycling and sibling exclusion. */ +/** + * Real face controls, crossing range handles, output cycling and sibling + * exclusion. + */ final class RedstoneClientSmoke { private static int step, reported = -1; private static volatile boolean ready; @@ -22,21 +25,23 @@ static boolean run(MachineScreen screen) { var model = menu.components().get(BuiltinMenuComponents.REDSTONE); if (reported != step) { reported = step; - zone.moddev.mc.lib.MMDLib.LOGGER.info("REDSTONE_DRAWER_STEP {}", step); + zone.moddev.mc.lib.MMDLib.LOGGER.info("REDSTONE_DRAWER_STEP {}", + step); } if (model == null) throw new AssertionError("Processor must expose redstone drawer"); if (model.pending() || menu.components().get(BuiltinMenuComponents.AUTOMATION) != null - && menu.components().get(BuiltinMenuComponents.AUTOMATION).pending()) + && menu.components().get(BuiltinMenuComponents.AUTOMATION) + .pending()) return false; var panel = screen.panelBounds(); - int x = panel.right() + AutomationLayout.RAIL, y = panel.y() + AutomationLayout.TOP; + int x = panel.right() + AutomationLayout.RAIL, + y = panel.y() + AutomationLayout.TOP; var tab = screen.drawers().entries().stream() - .filter(zone.moddev.mc.lib.gui.client.RedstoneDrawer.class::isInstance) - .findFirst() - .orElseThrow() - .tabBounds(); + .filter( + zone.moddev.mc.lib.gui.client.RedstoneDrawer.class::isInstance) + .findFirst().orElseThrow().tabBounds(); switch (step) { case 0 -> { require(menu.bound.screen().find("redstone").isEmpty(), @@ -45,32 +50,36 @@ static boolean run(MachineScreen screen) { click(screen, tab, 0); for (int tick = 0; tick < 99; tick++) model.tick(); - require(model.pending(), "Redstone remains pending before timeout"); + require(model.pending(), + "Redstone remains pending before timeout"); model.tick(); - require(model.pending(), "Redstone timeout starts a resync request"); + require(model.pending(), + "Redstone timeout starts a resync request"); } } case 1 -> { if (!model.open()) return false; - require( - !menu.components().get(BuiltinMenuComponents.AUTOMATION).open(), - "Redstone excludes inventory drawer on right"); - click(screen, RedstoneLayout.face(model.selected()).offset(x, y), 0); + require(!menu.components().get(BuiltinMenuComponents.AUTOMATION) + .open(), "Redstone excludes inventory drawer on right"); + click(screen, + RedstoneLayout.face(model.selected()).offset(x, y), 0); } case 2 -> { - require(model.mode(0) == RedstoneControl.Mode.READ, "Read mode synchronized"); + require(model.mode(0) == RedstoneControl.Mode.READ, + "Read mode synchronized"); range(screen, x, y, false, 15, 6); } case 3 -> { - require(model.lower(0) == 1 && model.upper(0) == 6, "Upper handle sets maximum"); + require(model.lower(0) == 1 && model.upper(0) == 6, + "Upper handle sets maximum"); range(screen, x, y, true, 1, 10); } case 4 -> { require(model.lower(0) == 10 && model.upper(0) == 10, "Lower crossing pushes upper right"); - Screenshot.grab( - mc.gameDirectory, "crusher-redstone-for.png", mc.getMainRenderTarget(), m -> { + Screenshot.grab(mc.gameDirectory, "crusher-redstone-for.png", + mc.getMainRenderTarget(), m -> { }); range(screen, x, y, false, 10, 4); } @@ -81,11 +90,11 @@ static boolean run(MachineScreen screen) { } case 50 -> { require( - model.outside(0) && model.lower(0) == 4 && model.upper(0) == 4, + model.outside(0) && model.lower(0) == 4 + && model.upper(0) == 4, "Outside preserves equal handles"); - Screenshot.grab( - mc.gameDirectory, "crusher-redstone-except.png", mc.getMainRenderTarget(), - m -> { + Screenshot.grab(mc.gameDirectory, "crusher-redstone-except.png", + mc.getMainRenderTarget(), m -> { }); click(screen, RedstoneLayout.intervalMode().offset(x, y), 0); } @@ -93,74 +102,78 @@ static boolean run(MachineScreen screen) { range(screen, x, y, false, 4, 10); } case 6 -> { - require(model.lower(0) == 4 && model.upper(0) == 10, "Interval 4-10 synchronized"); - Screenshot.grab( - mc.gameDirectory, "crusher-redstone-between.png", mc.getMainRenderTarget(), + require(model.lower(0) == 4 && model.upper(0) == 10, + "Interval 4-10 synchronized"); + Screenshot.grab(mc.gameDirectory, + "crusher-redstone-between.png", mc.getMainRenderTarget(), m -> { }); click(screen, RedstoneLayout.intervalMode().offset(x, y), 0); } case 7 -> { require(model.outside(0), "Outside toggle synchronized"); - Screenshot.grab( - mc.gameDirectory, "crusher-redstone-outside.png", mc.getMainRenderTarget(), + Screenshot.grab(mc.gameDirectory, + "crusher-redstone-outside.png", mc.getMainRenderTarget(), m -> { }); click(screen, RedstoneLayout.face(1).offset(x, y), 0); } case 8 -> { require( - model.selected() == 1 && model.mode(1) == RedstoneControl.Mode.IGNORE, + model.selected() == 1 + && model.mode(1) == RedstoneControl.Mode.IGNORE, "First click selects without editing"); click(screen, RedstoneLayout.face(1).offset(x, y), 1); } case 9 -> { - require(model.mode(1) == RedstoneControl.Mode.EMIT, "Reverse cycle chooses output"); + require(model.mode(1) == RedstoneControl.Mode.EMIT, + "Reverse cycle chooses output"); require(model.source(1) == -1, "Output starts unconfigured"); click(screen, RedstoneLayout.group().offset(x, y), 0); } case 10 -> { - require(model.source(1) == 0, "First click selects first inventory"); + require(model.source(1) == 0, + "First click selects first inventory"); click(screen, RedstoneLayout.group().offset(x, y), 0); } case 11 -> { require(model.source(1) == 1, "Click cycles inventory"); - Screenshot.grab( - mc.gameDirectory, "crusher-redstone-output.png", mc.getMainRenderTarget(), - m -> { + Screenshot.grab(mc.gameDirectory, "crusher-redstone-output.png", + mc.getMainRenderTarget(), m -> { }); click(screen, RedstoneLayout.revert().offset(x, y), 0); } case 60 -> { - require(model.inverted(1), "Revert icon enables output inversion"); - Screenshot.grab( - mc.gameDirectory, "crusher-redstone-reverted.png", mc.getMainRenderTarget(), + require(model.inverted(1), + "Revert icon enables output inversion"); + Screenshot.grab(mc.gameDirectory, + "crusher-redstone-reverted.png", mc.getMainRenderTarget(), m -> { }); click(screen, RedstoneLayout.revert().offset(x, y), 0); } case 61 -> { - require(!model.inverted(1), "Revert icon disables output inversion"); + require(!model.inverted(1), + "Revert icon disables output inversion"); click(screen, RedstoneLayout.group().offset(x, y), 1); } case 12 -> { - require(model.source(1) == 0, "Right click cycles inventory backwards"); + require(model.source(1) == 0, + "Right click cycles inventory backwards"); click(screen, RedstoneLayout.group().offset(x, y), 0); } case 70 -> { - if (!model.control - .outputs() - .get(model.source(1)) - .id() + if (!model.control.outputs().get(model.source(1)).id() .equals("processor/lane_0/processing")) { click(screen, RedstoneLayout.group().offset(x, y), 0); return false; } require( - model.control.outputs().get(model.source(1)).sprite().isPresent(), + model.control.outputs().get(model.source(1)).sprite() + .isPresent(), "Processor picker uses its supplied sprite"); - Screenshot.grab( - mc.gameDirectory, "crusher-redstone-processing.png", mc.getMainRenderTarget(), + Screenshot.grab(mc.gameDirectory, + "crusher-redstone-processing.png", mc.getMainRenderTarget(), m -> { }); click(screen, RedstoneLayout.group().offset(x, y), 0); @@ -170,8 +183,8 @@ static boolean run(MachineScreen screen) { model.control.outputs().get(model.source(1)).id() .equals("processor/lane_0/inactive"), "Processor paused/idle entry follows running"); - Screenshot.grab( - mc.gameDirectory, "crusher-redstone-inactive.png", mc.getMainRenderTarget(), + Screenshot.grab(mc.gameDirectory, + "crusher-redstone-inactive.png", mc.getMainRenderTarget(), m -> { }); click(screen, RedstoneLayout.group().offset(x, y), 0); @@ -181,67 +194,78 @@ static boolean run(MachineScreen screen) { model.control.outputs().get(model.source(1)).id() .equals("processor/lane_0/progress"), "Processor progress is an independent picker entry"); - Screenshot.grab( - mc.gameDirectory, "crusher-redstone-progress.png", mc.getMainRenderTarget(), + Screenshot.grab(mc.gameDirectory, + "crusher-redstone-progress.png", mc.getMainRenderTarget(), m -> { }); click(screen, RedstoneLayout.face(0).offset(x, y), 0); } case 13 -> { require( - model.lower(0) == 4 && model.upper(0) == 10 && model.outside(0), + model.lower(0) == 4 && model.upper(0) == 10 + && model.outside(0), "Range stays on its face across selections"); - // Escape cancels an in-progress drag without applying it or closing the drawer. + // Escape cancels an in-progress drag without applying it or + // closing the drawer. var bar = RedstoneLayout.bar().offset(x, y); - screen.mouseClicked(RedstoneLayout.signalX(bar, 4), bar.y() - 3, 0); + screen.mouseClicked(RedstoneLayout.signalX(bar, 4), bar.y() - 3, + 0); screen.mouseDragged(bar.right() + 20, bar.y() - 3, 0, 100, 0); screen.keyPressed(256, 0, 0); screen.mouseReleased(bar.right() + 20, bar.y() - 3, 0); require( - model.open() && !model.pending() && model.lower(0) == 4 && model.upper(0) == 10, + model.open() && !model.pending() && model.lower(0) == 4 + && model.upper(0) == 10, "Escape cancels range preview"); - click( - screen, - new Rect(panel.right(), y, AutomationLayout.RAIL, AutomationLayout.TAB_HEIGHT), - 0); + click(screen, new Rect(panel.right(), y, AutomationLayout.RAIL, + AutomationLayout.TAB_HEIGHT), 0); } case 14 -> { - if (!menu.components().get(BuiltinMenuComponents.AUTOMATION).open() || model.open()) + if (!menu.components().get(BuiltinMenuComponents.AUTOMATION) + .open() || model.open()) return false; screen.keyPressed(256, 0, 0); - server( - player -> { - var machine = (ProcessorEntity) ((zone.moddev.mc.lib.gui.runtime.MachineMenu) player.containerMenu).blockEntity; - for (int face = 0; face < 6; face++) - machine.redstone.mode(face, RedstoneControl.Mode.IGNORE); - machine.redstone.tick(); - }); + server(player -> { + var machine = (ProcessorEntity) ((zone.moddev.mc.lib.gui.runtime.MachineMenu) player.containerMenu).blockEntity; + for (int face = 0; face < 6; face++) + machine.redstone.mode(face, + RedstoneControl.Mode.IGNORE); + machine.redstone.tick(); + }); } default -> { - return ready - && !model.open() - && !menu.components().get(BuiltinMenuComponents.AUTOMATION).open(); + return ready && !model.open() && !menu.components() + .get(BuiltinMenuComponents.AUTOMATION).open(); } } step = step == 5 ? 50 : step == 51 ? 6 - : step == 11 ? 60 : step == 61 ? 12 : step == 12 ? 70 : step == 72 ? 13 : step + 1; + : step == 11 + ? 60 + : step == 61 + ? 12 + : step == 12 ? 70 : step == 72 ? 13 : step + 1; return false; } - private static void range(MachineScreen screen, int x, int y, boolean lower, int from, int to) { + private static void range(MachineScreen screen, int x, int y, boolean lower, + int from, int to) { var bar = RedstoneLayout.bar().offset(x, y); int mouseY = lower ? bar.y() - 3 : bar.bottom() + 3; - int beforeLow = screen.getMenu().components().get(BuiltinMenuComponents.REDSTONE).lower(0), - beforeHigh = screen.getMenu().components().get(BuiltinMenuComponents.REDSTONE).upper(0); + int beforeLow = screen.getMenu().components() + .get(BuiltinMenuComponents.REDSTONE).lower(0), + beforeHigh = screen.getMenu().components() + .get(BuiltinMenuComponents.REDSTONE).upper(0); screen.mouseClicked(RedstoneLayout.signalX(bar, from), mouseY, 0); - screen.mouseDragged(RedstoneLayout.signalX(bar, to), mouseY, 0, (to - from) * 6, 0); + screen.mouseDragged(RedstoneLayout.signalX(bar, to), mouseY, 0, + (to - from) * 6, 0); require( - screen.getMenu().components().get(BuiltinMenuComponents.REDSTONE).lower(0) == beforeLow - && screen.getMenu().components().get(BuiltinMenuComponents.REDSTONE) - .upper(0) == beforeHigh, + screen.getMenu().components().get(BuiltinMenuComponents.REDSTONE) + .lower(0) == beforeLow + && screen.getMenu().components() + .get(BuiltinMenuComponents.REDSTONE).upper(0) == beforeHigh, "Dragging previews without applying early"); screen.mouseReleased(RedstoneLayout.signalX(bar, to), mouseY, 0); } @@ -255,17 +279,16 @@ private static void server( java.util.function.Consumer action) { ready = false; var server = Minecraft.getInstance().getSingleplayerServer(); - server.execute( - () -> { - try { - var player = server.getPlayerList().getPlayers().getFirst(); - action.accept(player); - player.containerMenu.broadcastChanges(); - ready = true; - } catch (Throwable error) { - CompanionClientSmoke.fail(error); - } - }); + server.execute(() -> { + try { + var player = server.getPlayerList().getPlayers().getFirst(); + action.accept(player); + player.containerMenu.broadcastChanges(); + ready = true; + } catch (Throwable error) { + CompanionClientSmoke.fail(error); + } + }); } private static void require(boolean ok, String message) { diff --git a/src/companionTest/java/zone/moddev/mc/companion/test/RedstoneConnectionClientSmoke.java b/src/companionTest/java/zone/moddev/mc/companion/test/RedstoneConnectionClientSmoke.java index 8996ff6c..d4ba0bc9 100644 --- a/src/companionTest/java/zone/moddev/mc/companion/test/RedstoneConnectionClientSmoke.java +++ b/src/companionTest/java/zone/moddev/mc/companion/test/RedstoneConnectionClientSmoke.java @@ -16,7 +16,8 @@ import zone.moddev.mc.lib.gui.runtime.component.BuiltinMenuComponents; /** - * Exercises real server requests and client world packets, without directly loading update tags. + * Exercises real server requests and client world packets, without directly + * loading update tags. */ final class RedstoneConnectionClientSmoke { private static int step, waiting; @@ -29,7 +30,9 @@ static boolean run(MachineScreen screen) { var pos = menu.blockEntity.getBlockPos(); var dust = pos.east(); int face = RelativeFace - .of(RelativeFace.facing(menu.blockEntity.getBlockState()), Direction.EAST).ordinal(); + .of(RelativeFace.facing(menu.blockEntity.getBlockState()), + Direction.EAST) + .ordinal(); if (model.pending()) return false; switch (step) { @@ -37,15 +40,21 @@ static boolean run(MachineScreen screen) { mc.getSingleplayerServer().execute(() -> { try { var level = mc.getSingleplayerServer().overworld(); - var machine = (ProcessorEntity) level.getBlockEntity(pos); + var machine = (ProcessorEntity) level + .getBlockEntity(pos); machine.energy.setStored(0); for (int i = 0; i < 6; i++) - machine.redstone.mode(i, RedstoneControl.Mode.IGNORE); + machine.redstone.mode(i, + RedstoneControl.Mode.IGNORE); var line = Blocks.REDSTONE_WIRE.defaultBlockState() - .setValue(RedStoneWireBlock.NORTH, RedstoneSide.SIDE) - .setValue(RedStoneWireBlock.SOUTH, RedstoneSide.SIDE); - for (var wirePos : List.of(dust.north(), dust, dust.south())) { - level.setBlock(wirePos.below(), Blocks.STONE.defaultBlockState(), 3); + .setValue(RedStoneWireBlock.NORTH, + RedstoneSide.SIDE) + .setValue(RedStoneWireBlock.SOUTH, + RedstoneSide.SIDE); + for (var wirePos : List.of(dust.north(), dust, + dust.south())) { + level.setBlock(wirePos.below(), + Blocks.STONE.defaultBlockState(), 3); level.setBlock(wirePos, line, 3); } ready = true; @@ -55,13 +64,15 @@ static boolean run(MachineScreen screen) { }); } case 1 -> { - if (!ready || !mc.level.getBlockState(dust).is(Blocks.REDSTONE_WIRE)) + if (!ready + || !mc.level.getBlockState(dust).is(Blocks.REDSTONE_WIRE)) return false; if (!model.open()) model.send(RedstoneMenu.TOGGLE, 0, 0, 0); } case 2 -> model.send(RedstoneMenu.SELECT, face, 0, 0); - case 3 -> model.send(RedstoneMenu.MODE, face, 0, RedstoneControl.Mode.EMIT.ordinal()); + case 3 -> model.send(RedstoneMenu.MODE, face, 0, + RedstoneControl.Mode.EMIT.ordinal()); case 4 -> { int source = -1; for (int i = 0; i < model.control.outputs().size(); i++) @@ -75,22 +86,27 @@ static boolean run(MachineScreen screen) { boolean expected = step == 5 || step == 9; var block = mc.level.getBlockState(pos); var wire = mc.level.getBlockState(dust); - boolean connects = block.canRedstoneConnectTo(mc.level, pos, Direction.WEST); - boolean drawn = wire.getValue(RedStoneWireBlock.WEST).isConnected(); + boolean connects = block.canRedstoneConnectTo(mc.level, pos, + Direction.WEST); + boolean drawn = wire.getValue(RedStoneWireBlock.WEST) + .isConnected(); if (connects != expected || drawn != expected) { if (++waiting < 20) return false; throw new AssertionError( - "Client connection mismatch after drawer edit: expected=" + expected - + ", hook=" + connects + ", dust=" + wire + ", mode=" + model.mode(face) - + ", source=" + model.source(face)); + "Client connection mismatch after drawer edit: expected=" + + expected + ", hook=" + connects + ", dust=" + wire + + ", mode=" + model.mode(face) + ", source=" + + model.source(face)); } waiting = 0; } - case 6 -> model.send(RedstoneMenu.MODE, face, 0, RedstoneControl.Mode.IGNORE.ordinal()); - case 8 -> model.send(RedstoneMenu.MODE, face, 0, RedstoneControl.Mode.READ.ordinal()); - case 10 -> - model.send(RedstoneMenu.MODE, face, 0, RedstoneControl.Mode.IGNORE.ordinal()); + case 6 -> model.send(RedstoneMenu.MODE, face, 0, + RedstoneControl.Mode.IGNORE.ordinal()); + case 8 -> model.send(RedstoneMenu.MODE, face, 0, + RedstoneControl.Mode.READ.ordinal()); + case 10 -> model.send(RedstoneMenu.MODE, face, 0, + RedstoneControl.Mode.IGNORE.ordinal()); default -> { return true; } diff --git a/src/companionTest/java/zone/moddev/mc/companion/test/RedstoneConnectionTests.java b/src/companionTest/java/zone/moddev/mc/companion/test/RedstoneConnectionTests.java index 2c8a81e8..78a6282d 100644 --- a/src/companionTest/java/zone/moddev/mc/companion/test/RedstoneConnectionTests.java +++ b/src/companionTest/java/zone/moddev/mc/companion/test/RedstoneConnectionTests.java @@ -27,11 +27,13 @@ @PrefixGameTestTemplate(false) public final class RedstoneConnectionTests { @GameTest(template = "empty") - public static void loadingConnectionsDoesNotPulseObservers(GameTestHelper helper) { + public static void loadingConnectionsDoesNotPulseObservers( + GameTestHelper helper) { var level = helper.getLevel(); var pos = helper.absolutePos(new BlockPos(2, 2, 2)); var observerPos = pos.west(); - level.setBlock(pos, CompanionMod.CRUSHER.block.get().defaultBlockState(), 3); + level.setBlock(pos, + CompanionMod.CRUSHER.block.get().defaultBlockState(), 3); level.setBlock(observerPos, Blocks.OBSERVER.defaultBlockState() .setValue(BlockStateProperties.FACING, Direction.EAST), 2); var machine = (BaseMachine) level.getBlockEntity(pos); @@ -39,24 +41,31 @@ public static void loadingConnectionsDoesNotPulseObservers(GameTestHelper helper int face = RelativeFace.of(Direction.NORTH, Direction.EAST).ordinal(); control.mode(face, RedstoneControl.Mode.READ); control.updateOutputs(); - require(!level.getBlockTicks().hasScheduledTick(observerPos, Blocks.OBSERVER), + require( + !level.getBlockTicks().hasScheduledTick(observerPos, + Blocks.OBSERVER), "First connection tick does not pulse an observer facing the machine"); var saved = machine.saveWithFullMetadata(level.registryAccess()); - var restored = (BaseMachine) BlockEntity.loadStatic(pos, machine.getBlockState(), saved, - level.registryAccess()); + var restored = (BaseMachine) BlockEntity.loadStatic(pos, + machine.getBlockState(), saved, level.registryAccess()); level.setBlockEntity(restored); restored.screenRedstone().updateOutputs(); - require(!level.getBlockTicks().hasScheduledTick(observerPos, Blocks.OBSERVER), + require( + !level.getBlockTicks().hasScheduledTick(observerPos, + Blocks.OBSERVER), "Restoring configured connections does not pulse an observer"); restored.screenRedstone().mode(face, RedstoneControl.Mode.IGNORE); restored.screenRedstone().updateOutputs(); - require(level.getBlockTicks().hasScheduledTick(observerPos, Blocks.OBSERVER), + require( + level.getBlockTicks().hasScheduledTick(observerPos, + Blocks.OBSERVER), "An actual configuration change still updates neighbouring shapes"); helper.succeed(); } @GameTest(template = "empty") - public static void dustConnectsOnlyToConfiguredInputsAndOutputs(GameTestHelper helper) { + public static void dustConnectsOnlyToConfiguredInputsAndOutputs( + GameTestHelper helper) { var level = helper.getLevel(); var pos = helper.absolutePos(new BlockPos(2, 2, 2)); var dust = pos.east(); @@ -64,8 +73,8 @@ public static void dustConnectsOnlyToConfiguredInputsAndOutputs(GameTestHelper h .setValue(RedStoneWireBlock.NORTH, RedstoneSide.SIDE) .setValue(RedStoneWireBlock.SOUTH, RedstoneSide.SIDE); for (var block : List.of(CompanionMod.CRUSHER.block.get(), - BuiltInRegistries.BLOCK - .get(ResourceLocation.parse("mmdlib_companion:simple_crusher")))) { + BuiltInRegistries.BLOCK.get( + ResourceLocation.parse("mmdlib_companion:simple_crusher")))) { for (var facing : Direction.Plane.HORIZONTAL) { level.setBlock(pos, Blocks.AIR.defaultBlockState(), 3); var state = block.defaultBlockState() @@ -75,67 +84,89 @@ public static void dustConnectsOnlyToConfiguredInputsAndOutputs(GameTestHelper h var control = machine.screenRedstone(); int face = RelativeFace.of(facing, Direction.EAST).ordinal(); for (var wirePos : List.of(dust.north(), dust, dust.south())) { - level.setBlock(wirePos.below(), Blocks.STONE.defaultBlockState(), 3); + level.setBlock(wirePos.below(), + Blocks.STONE.defaultBlockState(), 3); level.setBlock(wirePos, line, 3); } require(!state.canRedstoneConnectTo(level, pos, null), "No vertical dust connection"); for (var query : Direction.values()) - require(!state.canRedstoneConnectTo(level, pos, query), "Ignore refuses dust"); - level.setBlock(dust, line.setValue(RedStoneWireBlock.WEST, RedstoneSide.SIDE), 2); + require(!state.canRedstoneConnectTo(level, pos, query), + "Ignore refuses dust"); + level.setBlock(dust, + line.setValue(RedStoneWireBlock.WEST, RedstoneSide.SIDE), + 2); control.updateOutputs(); require( - level.getBlockState(dust).getValue(RedStoneWireBlock.WEST) == RedstoneSide.NONE, + level.getBlockState(dust) + .getValue(RedStoneWireBlock.WEST) == RedstoneSide.NONE, "First tick repairs saved dust geometry beside an Ignore face"); control.mode(face, RedstoneControl.Mode.EMIT); control.updateOutputs(); require(!state.canRedstoneConnectTo(level, pos, Direction.WEST), "Emit without an output selection refuses dust"); - require(control.source(face, 0), "An output source is available"); + require(control.source(face, 0), + "An output source is available"); control.updateOutputs(); - require(control.signal(Direction.WEST) == 0, "Selected idle output is zero"); + require(control.signal(Direction.WEST) == 0, + "Selected idle output is zero"); for (var query : Direction.values()) require( - state.canRedstoneConnectTo(level, pos, query) == (query == Direction.WEST), + state.canRedstoneConnectTo(level, pos, + query) == (query == Direction.WEST), "Only the selected face connects, respecting machine orientation"); require( - level.getBlockState(dust).getValue(RedStoneWireBlock.WEST) == RedstoneSide.SIDE, + level.getBlockState(dust) + .getValue(RedStoneWireBlock.WEST) == RedstoneSide.SIDE, "Selecting a zero-strength output updates existing dust geometry"); control.inverted(face, true); control.updateOutputs(); - require(level.getBlockState(dust).getValue(RedStoneWireBlock.POWER) == 15, + require( + level.getBlockState(dust) + .getValue(RedStoneWireBlock.POWER) == 15, "Connected dust receives power when the output rises"); control.inverted(face, false); control.updateOutputs(); - require(level.getBlockState(dust).getValue(RedStoneWireBlock.POWER) == 0 - && level.getBlockState(dust) - .getValue(RedStoneWireBlock.WEST) == RedstoneSide.SIDE, + require( + level.getBlockState(dust) + .getValue(RedStoneWireBlock.POWER) == 0 + && level.getBlockState(dust).getValue( + RedStoneWireBlock.WEST) == RedstoneSide.SIDE, "Falling output strength keeps the configured connection"); var restored = (BaseMachine) BlockEntity.loadStatic(pos, state, - machine.saveWithFullMetadata(level.registryAccess()), level.registryAccess()); + machine.saveWithFullMetadata(level.registryAccess()), + level.registryAccess()); require(restored.screenRedstone().connects(Direction.WEST), "Configured connection survives save/load"); var client = (BaseMachine) ((EntityBlock) block) .newBlockEntity(pos, state); - client.handleUpdateTag(machine.getUpdateTag(level.registryAccess()), + client.handleUpdateTag( + machine.getUpdateTag(level.registryAccess()), level.registryAccess()); require(client.screenRedstone().connects(Direction.WEST), "Chunk/update tag carries connections without an open menu"); - for (var mode : List.of(RedstoneControl.Mode.READ, RedstoneControl.Mode.IGNORE)) { + for (var mode : List.of(RedstoneControl.Mode.READ, + RedstoneControl.Mode.IGNORE)) { control.mode(face, mode); control.updateOutputs(); boolean connected = mode == RedstoneControl.Mode.READ; - require(state.canRedstoneConnectTo(level, pos, Direction.WEST) == connected, + require( + state.canRedstoneConnectTo(level, pos, + Direction.WEST) == connected, "Read accepts dust and Ignore refuses it while retaining the selection"); require( level.getBlockState(dust) - .getValue(RedStoneWireBlock.WEST).isConnected() == connected, + .getValue(RedStoneWireBlock.WEST) + .isConnected() == connected, "Dust geometry follows configured input/output faces at zero strength"); - client.handleUpdateTag(machine.getUpdateTag(level.registryAccess()), + client.handleUpdateTag( + machine.getUpdateTag(level.registryAccess()), level.registryAccess()); - require(client.screenRedstone().connects(Direction.WEST) == connected, + require( + client.screenRedstone() + .connects(Direction.WEST) == connected, "Client update preserves Read and clears Ignore connections"); control.mode(face, RedstoneControl.Mode.EMIT); control.updateOutputs(); @@ -144,20 +175,25 @@ public static void dustConnectsOnlyToConfiguredInputsAndOutputs(GameTestHelper h missing.putString("source_" + face, "removed/output"); control.load(missing, level.registryAccess()); control.updateOutputs(); - require(!state.canRedstoneConnectTo(level, pos, Direction.WEST) - && level.getBlockState(dust) - .getValue(RedStoneWireBlock.WEST) == RedstoneSide.NONE, + require( + !state.canRedstoneConnectTo(level, pos, Direction.WEST) + && level.getBlockState(dust).getValue( + RedStoneWireBlock.WEST) == RedstoneSide.NONE, "A removed output source disconnects dust"); control.mode(face, RedstoneControl.Mode.READ); control.updateOutputs(); - require(state.canRedstoneConnectTo(level, pos, Direction.WEST) - && level.getBlockState(dust).getValue(RedStoneWireBlock.WEST).isConnected(), + require( + state.canRedstoneConnectTo(level, pos, Direction.WEST) + && level.getBlockState(dust) + .getValue(RedStoneWireBlock.WEST).isConnected(), "Read connects without an available output source"); - level.setBlock(dust.north().north(), Blocks.REDSTONE_BLOCK.defaultBlockState(), 3); + level.setBlock(dust.north().north(), + Blocks.REDSTONE_BLOCK.defaultBlockState(), 3); control.readInputs(); require(control.incoming(face) > 0 && control.paused(), "A connected side branch delivers dust input and gates processing"); - level.setBlock(dust.north().north(), Blocks.AIR.defaultBlockState(), 3); + level.setBlock(dust.north().north(), + Blocks.AIR.defaultBlockState(), 3); for (var wirePos : List.of(dust.north(), dust, dust.south())) level.setBlock(wirePos, Blocks.AIR.defaultBlockState(), 3); } diff --git a/src/companionTest/java/zone/moddev/mc/companion/test/RedstoneSmoke.java b/src/companionTest/java/zone/moddev/mc/companion/test/RedstoneSmoke.java index 1f31c277..7327cf6d 100644 --- a/src/companionTest/java/zone/moddev/mc/companion/test/RedstoneSmoke.java +++ b/src/companionTest/java/zone/moddev/mc/companion/test/RedstoneSmoke.java @@ -31,10 +31,11 @@ static void check(GameTestHelper helper) { var machine = (ProcessorEntity) level.getBlockEntity(pos); var control = machine.redstone; machine.energy.setStored(100000); - machine.items.insertItem( - 0, + machine.items.insertItem(0, new ItemStack( - type == CompanionMod.TRANSMUTER ? Items.IRON_INGOT : Items.COPPER_ORE, + type == CompanionMod.TRANSMUTER + ? Items.IRON_INGOT + : Items.COPPER_ORE, type == CompanionMod.TRANSMUTER ? 1 : 16), false); tick(machine, 6); @@ -49,16 +50,17 @@ static void check(GameTestHelper helper) { && machine.energy.stored() == energy && machine.items.hasPlan(0), "Read gate preserves progress, energy and reservations"); - level.setBlock(pos.east(), Blocks.REDSTONE_BLOCK.defaultBlockState(), 3); + level.setBlock(pos.east(), + Blocks.REDSTONE_BLOCK.defaultBlockState(), 3); tick(machine, 2); require( - machine.getProgress() > progress && control.incoming(east) == 15, + machine.getProgress() > progress + && control.incoming(east) == 15, "Read observes only the configured input face"); machine.toggle(0); progress = machine.getProgress(); tick(machine, 3); - require( - machine.getPaused() && machine.getProgress() == progress, + require(machine.getPaused() && machine.getProgress() == progress, "Signal never overrides manual pause"); machine.toggle(0); control.outside(east, false); @@ -67,23 +69,26 @@ static void check(GameTestHelper helper) { level.setBlock(pos.east(), Blocks.AIR.defaultBlockState(), 3); control.tick(); require(!control.paused(), "No signal releases pause-on mode"); - require(control.range(east, 4, 10), "Built-in interval accepts ordered endpoints"); + require(control.range(east, 4, 10), + "Built-in interval accepts ordered endpoints"); require( !control.range(east, 12, 3) && !control.range(east, -1, 3) && !control.range(east, 2, 16), "Invalid intervals rejected"); for (int signal = 0; signal < 16; signal++) { require( - RedstoneControl.matches(signal, 4, 10, false) == (signal >= 4 && signal <= 10), + RedstoneControl.matches(signal, 4, 10, + false) == (signal >= 4 && signal <= 10), "Between includes both endpoints"); require( - RedstoneControl.matches(signal, 4, 10, true) == (signal < 4 || signal > 10), + RedstoneControl.matches(signal, 4, 10, + true) == (signal < 4 || signal > 10), "Outside is the exact complement"); require( - RedstoneControl.matches(signal, 7, 7, false) == (signal == 7), + RedstoneControl.matches(signal, 7, 7, + false) == (signal == 7), "Equal handles select one level"); - require( - !RedstoneControl.matches(signal, 0, 15, true), + require(!RedstoneControl.matches(signal, 0, 15, true), "Outside full range selects nothing"); } control.outside(east, true); @@ -91,12 +96,15 @@ static void check(GameTestHelper helper) { require(control.paused(), "Signal zero pauses outside 4-10"); control.range(east, 0, 0); control.tick(); - require(!control.paused(), "Signal zero runs outside the single zero interval"); + require(!control.paused(), + "Signal zero runs outside the single zero interval"); control.mode(east, RedstoneControl.Mode.IGNORE); - int west = RelativeFace.of(Direction.NORTH, Direction.WEST).ordinal(); + int west = RelativeFace.of(Direction.NORTH, Direction.WEST) + .ordinal(); control.mode(west, RedstoneControl.Mode.EMIT); control.tick(); - require(control.outgoing(west) == 0, "No selected inventory emits zero"); + require(control.outgoing(west) == 0, + "No selected inventory emits zero"); control.inverted(west, true); control.tick(); require(control.outgoing(west) == 0, @@ -106,8 +114,7 @@ static void check(GameTestHelper helper) { for (int i = 0; i < control.outputs().size(); i++) if (control.outputs().get(i).id().equals("energy")) group = i; - require( - group >= 0 && control.source(west, group), + require(group >= 0 && control.source(west, group), "Output selects resource by stable group ID"); require(!control.source(west, 9999), "Invalid group refused"); machine.energy.setStored(machine.energy.capacity() / 2); @@ -115,10 +122,12 @@ static void check(GameTestHelper helper) { require(control.outgoing(west) == 7, "Half full exports seven"); control.inverted(west, true); control.tick(); - require(control.outgoing(west) == 8, "Reverted half full exports eight"); + require(control.outgoing(west) == 8, + "Reverted half full exports eight"); machine.energy.setStored(0); control.tick(); - require(control.outgoing(west) == 15, "Reverted empty exports fifteen"); + require(control.outgoing(west) == 15, + "Reverted empty exports fifteen"); machine.energy.setStored(machine.energy.capacity()); control.tick(); require(control.outgoing(west) == 0, "Reverted full exports zero"); @@ -138,8 +147,8 @@ static void check(GameTestHelper helper) { control.mode(west, RedstoneControl.Mode.EMIT); control.range(east, 4, 10); var saved = machine.saveWithFullMetadata(level.registryAccess()); - var restored = (ProcessorEntity) BlockEntity.loadStatic(pos, machine.getBlockState(), - saved, level.registryAccess()); + var restored = (ProcessorEntity) BlockEntity.loadStatic(pos, + machine.getBlockState(), saved, level.registryAccess()); require( restored.redstone.mode(west) == RedstoneControl.Mode.EMIT && restored.redstone.source(west) == group @@ -151,17 +160,21 @@ static void check(GameTestHelper helper) { var legacy = saved.copy(); legacy.remove("redstone_control"); legacy.putInt("redstone_mode", 1); - var migrated = (ProcessorEntity) BlockEntity.loadStatic(pos, machine.getBlockState(), - legacy, level.registryAccess()); + var migrated = (ProcessorEntity) BlockEntity.loadStatic(pos, + machine.getBlockState(), legacy, level.registryAccess()); migrated.setLevel(level); migrated.redstone.tick(); - require(migrated.redstone.paused(), "Legacy powered mode waits without input"); - level.setBlock(pos.east(), Blocks.REDSTONE_BLOCK.defaultBlockState(), 3); + require(migrated.redstone.paused(), + "Legacy powered mode waits without input"); + level.setBlock(pos.east(), + Blocks.REDSTONE_BLOCK.defaultBlockState(), 3); migrated.redstone.tick(); - require(!migrated.redstone.paused(), "Legacy powered mode retains any-face behavior"); + require(!migrated.redstone.paused(), + "Legacy powered mode retains any-face behavior"); migrated.redstone.range(0, 2, 8); require( - !migrated.redstone.save(level.registryAccess()).getBoolean("legacy_any"), + !migrated.redstone.save(level.registryAccess()) + .getBoolean("legacy_any"), "Explicit interval edit adopts per-face rules"); level.setBlock(pos.east(), Blocks.AIR.defaultBlockState(), 3); var invalid = control.save(level.registryAccess()); @@ -192,51 +205,54 @@ static void check(GameTestHelper helper) { var stale = request(menu, 3, RedstoneMenu.MODE, 0, 2); control.outside(0, true); drawer.action(player, stale); - require(control.mode(0) == RedstoneControl.Mode.READ, "Stale face edit rejected"); - drawer.action( - player, - new RedstoneNetwork.Action( - menu.containerId, - menu.widgetSession() + 1, - 4, - control.revision(), - RedstoneMenu.MODE, - 0, - 0, - 2)); - require(control.mode(0) == RedstoneControl.Mode.READ, "Foreign session edit rejected"); + require(control.mode(0) == RedstoneControl.Mode.READ, + "Stale face edit rejected"); + drawer.action(player, + new RedstoneNetwork.Action(menu.containerId, + menu.widgetSession() + 1, 4, control.revision(), + RedstoneMenu.MODE, 0, 0, 2)); + require(control.mode(0) == RedstoneControl.Mode.READ, + "Foreign session edit rejected"); drawer.action(player, request(menu, 5, RedstoneMenu.MODE, 1, 2)); require(control.mode(1) == RedstoneControl.Mode.IGNORE, "Cannot edit a nonselected face"); - drawer.action(player, request(menu, 6, RedstoneMenu.RANGE, 0, 4 * 16 + 10)); + drawer.action(player, + request(menu, 6, RedstoneMenu.RANGE, 0, 4 * 16 + 10)); require(control.lower(0) == 4 && control.upper(0) == 10, "Range command applies atomically"); - drawer.action(player, request(menu, 7, RedstoneMenu.RANGE, 0, 12 * 16 + 3)); + drawer.action(player, + request(menu, 7, RedstoneMenu.RANGE, 0, 12 * 16 + 3)); require(control.lower(0) == 4 && control.upper(0) == 10, "Unordered range command refused"); int beforeGroup = control.source(0); - drawer.action(player, request(menu, 8, RedstoneMenu.SOURCE, 0, group)); + drawer.action(player, + request(menu, 8, RedstoneMenu.SOURCE, 0, group)); require(control.source(0) == beforeGroup, "Read face refuses output-only group command"); drawer.action(player, request(menu, 9, RedstoneMenu.INVERT, 0, 1)); require(!control.inverted(0), "Read face refuses output inversion"); drawer.action(player, request(menu, 10, RedstoneMenu.MODE, 0, 2)); drawer.action(player, request(menu, 11, RedstoneMenu.INVERT, 0, 1)); - require(control.inverted(0), "Output inversion applied through validated menu action"); + require(control.inverted(0), + "Output inversion applied through validated menu action"); drawer.action(player, request(menu, 12, RedstoneMenu.INVERT, 0, 2)); require(control.inverted(0), "Invalid inversion value refused"); var staleClose = request(menu, 13, RedstoneMenu.TOGGLE, 0, 0); - control.range(0, 3, 11); // Another viewer changes the shared configuration. + control.range(0, 3, 11); // Another viewer changes the shared + // configuration. drawer.action(player, staleClose); - require(!drawer.open(), "Shared edits cannot block local drawer close"); + require(!drawer.open(), + "Shared edits cannot block local drawer close"); var staleOpen = request(menu, 14, RedstoneMenu.TOGGLE, 0, 0); control.range(0, 4, 11); drawer.action(player, staleOpen); - require(drawer.open(), "Shared edits cannot block local drawer open"); + require(drawer.open(), + "Shared edits cannot block local drawer open"); var staleSelect = request(menu, 15, RedstoneMenu.SELECT, 1, 0); control.range(0, 5, 11); drawer.action(player, staleSelect); - require(drawer.selected() == 1, "Shared edits cannot block local face selection"); + require(drawer.selected() == 1, + "Shared edits cannot block local face selection"); player.containerMenu = player.inventoryMenu; machine.clearContent(); level.setBlock(pos, Blocks.AIR.defaultBlockState(), 3); @@ -247,7 +263,8 @@ static void check(GameTestHelper helper) { private static void checkProcessorOutputs(GameTestHelper helper) { var level = helper.getLevel(); var pos = helper.absolutePos(new net.minecraft.core.BlockPos(1, 1, 1)); - level.setBlock(pos, CompanionMod.DUAL_CRUSHER.block.get().defaultBlockState(), 3); + level.setBlock(pos, + CompanionMod.DUAL_CRUSHER.block.get().defaultBlockState(), 3); var machine = (ProcessorEntity) level.getBlockEntity(pos); var control = machine.redstone; require( @@ -260,33 +277,31 @@ private static void checkProcessorOutputs(GameTestHelper helper) { String id = "processor/lane_" + lane + "/" + kinds[kind]; int index = source(control, id); require( - index >= 0 && control.outputs().get(index).sprite().isPresent(), + index >= 0 + && control.outputs().get(index).sprite().isPresent(), "Each processor source has a stable ID and sprite"); control.mode(face, RedstoneControl.Mode.EMIT); control.source(face, index); } tick(machine, 1); require( - control.outgoing(0) == 0 && control.outgoing(1) == 15 && control.outgoing(2) == 0, + control.outgoing(0) == 0 && control.outgoing(1) == 15 + && control.outgoing(2) == 0, "Idle processor emits 0 running, 15 inactive and 0 progress"); machine.energy.setStored(100000); machine.items.insertItem(0, new ItemStack(Items.COPPER_ORE, 16), false); tick(machine, 15); require( - control.outgoing(0) == 15 - && control.outgoing(1) == 0 - && control.outgoing(2) > 0 - && control.outgoing(3) == 15 + control.outgoing(0) == 15 && control.outgoing(1) == 0 + && control.outgoing(2) > 0 && control.outgoing(3) == 15 && control.outgoing(4) == 0, "Both lanes expose their own live running/progress values"); int progress = control.outgoing(2); machine.toggle(0); tick(machine, 3); require( - control.outgoing(0) == 0 - && control.outgoing(1) == 15 - && control.outgoing(2) == progress - && control.outgoing(3) == 15 + control.outgoing(0) == 0 && control.outgoing(1) == 15 + && control.outgoing(2) == progress && control.outgoing(3) == 15 && control.outgoing(4) == 0, "Pausing lane one retains progress without pausing lane two"); machine.toggle(0); @@ -319,15 +334,17 @@ private static void checkProcessorOutputs(GameTestHelper helper) { "Redstone hold is reflected in processor outputs on the same tick"); control.inverted(0, true); control.updateOutputs(); - require(control.outgoing(0) == 15, "Revert also applies to processor signals"); + require(control.outgoing(0) == 15, + "Revert also applies to processor signals"); var saved = control.save(level.registryAccess()); - var reordered = new java.util.ArrayList(control.outputs()); + var reordered = new java.util.ArrayList( + control.outputs()); java.util.Collections.reverse(reordered); var restored = new RedstoneControl(machine, reordered); restored.load(saved, level.registryAccess()); require( - restored.outputs().get(restored.source(0)).id().equals("processor/lane_0/processing") - && restored.inverted(0), + restored.outputs().get(restored.source(0)).id() + .equals("processor/lane_0/processing") && restored.inverted(0), "Saved source ID survives output reordering"); var old = saved.copy(); old.remove("source_0"); @@ -337,43 +354,42 @@ private static void checkProcessorOutputs(GameTestHelper helper) { restored.outputs().get(restored.source(0)).id().equals("energy"), "Old inventory selections migrate to the source catalog"); int[] calls = {0}, sourceValue = {99}; - var custom = new RedstoneOutput( - "custom", + var custom = new RedstoneOutput("custom", net.minecraft.network.chat.Component.literal("Custom"), net.minecraft.network.chat.Component.literal("Custom output"), - 0x123456, - java.util.Optional.empty(), - () -> { + 0x123456, java.util.Optional.empty(), () -> { calls[0]++; return sourceValue[0]; }); - var discovered = new RedstoneControl(machine, List.of(new Object(), custom)); - require(discovered.outputs().size() == 1, "Non-output parts are ignored"); + var discovered = new RedstoneControl(machine, + List.of(new Object(), custom)); + require(discovered.outputs().size() == 1, + "Non-output parts are ignored"); for (int face = 0; face < 6; face++) { discovered.mode(face, RedstoneControl.Mode.EMIT); discovered.source(face, 0); } discovered.updateOutputs(); - require( - calls[0] == 1 && discovered.outgoing(0) == 15, + require(calls[0] == 1 && discovered.outgoing(0) == 15, "Custom source is clamped and evaluated once for all faces"); sourceValue[0] = 4; discovered.inverted(0, true); discovered.updateOutputs(); require( - calls[0] == 2 && discovered.outgoing(0) == 11 && discovered.outgoing(1) == 4, + calls[0] == 2 && discovered.outgoing(0) == 11 + && discovered.outgoing(1) == 4, "Samples refresh every update and each face inverts the raw value independently"); for (int face = 0; face < 6; face++) discovered.mode(face, RedstoneControl.Mode.IGNORE); discovered.updateOutputs(); require( - calls[0] == 2 && discovered.outgoing(0) == 0 && discovered.outgoing(1) == 0, + calls[0] == 2 && discovered.outgoing(0) == 0 + && discovered.outgoing(1) == 0, "Unused sources are not sampled and old emissions are cleared"); discovered.mode(1, RedstoneControl.Mode.EMIT); sourceValue[0] = -10; discovered.updateOutputs(); - require( - calls[0] == 3 && discovered.outgoing(1) == 0, + require(calls[0] == 3 && discovered.outgoing(1) == 0, "Reenabled source refreshes its sample and clamps negative values"); var missing = discovered.save(level.registryAccess()); missing.putString("source_1", "removed_source"); @@ -381,13 +397,16 @@ private static void checkProcessorOutputs(GameTestHelper helper) { discovered.inverted(1, true); discovered.updateOutputs(); require( - discovered.source(1) == -1 && calls[0] == 3 && discovered.outgoing(1) == 0, + discovered.source(1) == -1 && calls[0] == 3 + && discovered.outgoing(1) == 0, "Loading a missing source clears the cached index and keeps emission at zero"); discovered.source(1, 0); discovered.migrate(0); - require(discovered.source(1) == -1, "Migration clears cached source indexes"); + require(discovered.source(1) == -1, + "Migration clears cached source indexes"); int[] scratch = new int[48]; - var snapshot = new RedstoneNetwork.State(1, 1, 0, 0, false, 0, false, scratch); + var snapshot = new RedstoneNetwork.State(1, 1, 0, 0, false, 0, false, + scratch); scratch[2] = 15; require(snapshot.faces()[2] == 0, "Reusing a snapshot buffer cannot mutate an existing packet"); @@ -397,10 +416,12 @@ private static void checkProcessorOutputs(GameTestHelper helper) { } catch (IllegalArgumentException expected) { duplicateRejected = true; } - require(duplicateRejected, "Ambiguous duplicate source IDs are rejected"); + require(duplicateRejected, + "Ambiguous duplicate source IDs are rejected"); machine.clearContent(); level.setBlock(pos, Blocks.AIR.defaultBlockState(), 3); - level.setBlock(pos, CompanionMod.CRUSHER.block.get().defaultBlockState(), 3); + level.setBlock(pos, + CompanionMod.CRUSHER.block.get().defaultBlockState(), 3); machine = (ProcessorEntity) level.getBlockEntity(pos); control = machine.redstone; control.mode(0, RedstoneControl.Mode.EMIT); @@ -411,9 +432,11 @@ private static void checkProcessorOutputs(GameTestHelper helper) { do { tick(machine, 1); } while (control.outgoing(0) < 15 && ++ticks < 1000); - require(control.outgoing(0) == 15, "Completion emits progress fifteen for its final tick"); + require(control.outgoing(0) == 15, + "Completion emits progress fifteen for its final tick"); tick(machine, 1); - require(control.outgoing(0) == 0, "Completed processor returns progress to zero when idle"); + require(control.outgoing(0) == 0, + "Completed processor returns progress to zero when idle"); machine.clearContent(); level.setBlock(pos, Blocks.AIR.defaultBlockState(), 3); } @@ -425,17 +448,12 @@ private static int source(RedstoneControl control, String id) { return -1; } - private static RedstoneNetwork.Action request( - MachineMenu menu, int seq, int kind, int face, int value) { - return new RedstoneNetwork.Action( - menu.containerId, - menu.widgetSession(), - seq, + private static RedstoneNetwork.Action request(MachineMenu menu, int seq, + int kind, int face, int value) { + return new RedstoneNetwork.Action(menu.containerId, + menu.widgetSession(), seq, menu.components().get(BuiltinMenuComponents.REDSTONE).revision(), - kind, - face, - 0, - value); + kind, face, 0, value); } private static void tick(ProcessorEntity machine, int count) { diff --git a/src/companionTest/java/zone/moddev/mc/companion/test/ReservationClientSmoke.java b/src/companionTest/java/zone/moddev/mc/companion/test/ReservationClientSmoke.java index 0ec39ed3..e86bedab 100644 --- a/src/companionTest/java/zone/moddev/mc/companion/test/ReservationClientSmoke.java +++ b/src/companionTest/java/zone/moddev/mc/companion/test/ReservationClientSmoke.java @@ -13,7 +13,10 @@ import zone.moddev.mc.lib.gui.runtime.MachineMenu; import zone.moddev.mc.lib.machine.inventory.SlotPlan; -/** Deterministic real-menu captures for reserved input, occupied/empty output and mixed plans. */ +/** + * Deterministic real-menu captures for reserved input, occupied/empty output + * and mixed plans. + */ final class ReservationClientSmoke { private static int step; private static volatile boolean ready; @@ -23,19 +26,20 @@ static boolean run(MachineScreen screen) { if (step > 0 && !ready) return false; switch (step) { - case 0 -> - server( - player -> { - var machine = machine(player); - machine.lanes.forEach(lane -> lane.reset()); - machine.setReservePercent(100); - machine.energy.setStored(0); - machine.items.setStackInSlot(0, new ItemStack(Items.COPPER_ORE, 10)); - machine.items.setStackInSlot(1, new ItemStack(Items.RAW_COPPER, 10)); - machine.items.setStackInSlot(2, ItemStack.EMPTY); - machine.items.setPlan(0, new SlotPlan(2, ItemStack.EMPTY)); - machine.items.setPlan(1, new SlotPlan(0, new ItemStack(Items.RAW_COPPER))); - }); + case 0 -> server(player -> { + var machine = machine(player); + machine.lanes.forEach(lane -> lane.reset()); + machine.setReservePercent(100); + machine.energy.setStored(0); + machine.items.setStackInSlot(0, + new ItemStack(Items.COPPER_ORE, 10)); + machine.items.setStackInSlot(1, + new ItemStack(Items.RAW_COPPER, 10)); + machine.items.setStackInSlot(2, ItemStack.EMPTY); + machine.items.setPlan(0, new SlotPlan(2, ItemStack.EMPTY)); + machine.items.setPlan(1, + new SlotPlan(0, new ItemStack(Items.RAW_COPPER))); + }); case 1 -> { if (menu.reservationPlan(0).take() != 2 || menu.getSlot(0).getItem().getCount() != 10 @@ -43,16 +47,20 @@ static boolean run(MachineScreen screen) { || menu.reservationPlan(1).add().getCount() != 1) return false; int playerSlot = menu.slots.stream() - .filter(slot -> slot.container == Minecraft.getInstance().player.getInventory()) - .findFirst() - .orElseThrow().index; - require(menu.reservationPlan(playerSlot).isNone(), "Player slots have no overlay"); + .filter( + slot -> slot.container == Minecraft.getInstance().player + .getInventory()) + .findFirst().orElseThrow().index; + require(menu.reservationPlan(playerSlot).isNone(), + "Player slots have no overlay"); SlotTooltipClientSmoke.check(screen, 0, true); capture("reservations-counts.png"); - server(player -> machine(player).items.setStackInSlot(1, ItemStack.EMPTY)); + server(player -> machine(player).items.setStackInSlot(1, + ItemStack.EMPTY)); } case 2 -> { - if (!menu.getSlot(1).getItem().isEmpty() || menu.reservationPlan(1).add().isEmpty()) + if (!menu.getSlot(1).getItem().isEmpty() + || menu.reservationPlan(1).add().isEmpty()) return false; SlotTooltipClientSmoke.check(screen, 1, false); capture("reservations-ghost.png"); @@ -64,50 +72,50 @@ static boolean run(MachineScreen screen) { } case 3 -> { require( - menu.getCarried().isEmpty() && menu.getSlot(1).getItem().isEmpty(), + menu.getCarried().isEmpty() + && menu.getSlot(1).getItem().isEmpty(), "Clicking a ghost does not create or pick up a real item"); - server( - player -> { - require( - player.containerMenu.getCarried().isEmpty(), - "Server sees no ghost item pickup"); - machine(player).items.setPlan(0, SlotPlan.NONE); - machine(player).items.setPlan(1, SlotPlan.NONE); - }); + server(player -> { + require(player.containerMenu.getCarried().isEmpty(), + "Server sees no ghost item pickup"); + machine(player).items.setPlan(0, SlotPlan.NONE); + machine(player).items.setPlan(1, SlotPlan.NONE); + }); } case 4 -> { - if (!menu.reservationPlan(0).isNone() || !menu.reservationPlan(1).isNone()) + if (!menu.reservationPlan(0).isNone() + || !menu.reservationPlan(1).isNone()) return false; require( menu.getSlot(0).mayPlace(new ItemStack(Items.COPPER_ORE)), "Client recipe sync admits a valid ingredient before a recipe starts"); - require( - !menu.getSlot(0).mayPlace(new ItemStack(Items.DIAMOND)), + require(!menu.getSlot(0).mayPlace(new ItemStack(Items.DIAMOND)), "Client prediction rejects unrelated items without relying on reservations"); capture("reservations-cleared.png"); - server( - player -> machine(player).items - .setPlan(0, new SlotPlan(10, new ItemStack(Items.GOLD_INGOT)))); + server(player -> machine(player).items.setPlan(0, + new SlotPlan(10, new ItemStack(Items.GOLD_INGOT)))); } case 5 -> { if (menu.reservationPlan(0).take() != 10 || !menu.reservationPlan(0).add().is(Items.GOLD_INGOT)) return false; capture("reservations-mixed.png"); - server( - player -> { - var machine = machine(player); - machine.items.setPlan(0, SlotPlan.NONE); - machine.items.setPlan(1, SlotPlan.NONE); - machine.items.setStackInSlot(0, new ItemStack(Items.IRON_ORE)); - machine.items.setStackInSlot(1, new ItemStack(Items.RAW_IRON, 63)); - machine.items.setStackInSlot(2, new ItemStack(Items.RAW_IRON, 63)); - machine.setReservePercent(0); - machine.energy.setStored(80000); - machine.serverTick(); - machine.setReservePercent(100); - machine.energy.setStored(0); - }); + server(player -> { + var machine = machine(player); + machine.items.setPlan(0, SlotPlan.NONE); + machine.items.setPlan(1, SlotPlan.NONE); + machine.items.setStackInSlot(0, + new ItemStack(Items.IRON_ORE)); + machine.items.setStackInSlot(1, + new ItemStack(Items.RAW_IRON, 63)); + machine.items.setStackInSlot(2, + new ItemStack(Items.RAW_IRON, 63)); + machine.setReservePercent(0); + machine.energy.setStored(80000); + machine.serverTick(); + machine.setReservePercent(100); + machine.energy.setStored(0); + }); } case 6 -> { if (menu.reservationPlan(0).take() != 1 @@ -118,20 +126,20 @@ static boolean run(MachineScreen screen) { return false; require(menu.getSlot(2).isActive(), "Second crusher output is visible and interactive"); - var output = ((ProcessorEntity) menu.blockEntity).automation.group(1); + var output = ((ProcessorEntity) menu.blockEntity).automation + .group(1); require( output.inventory().equals("output") && output.members().equals(java.util.List.of(0, 1)), "Both outputs share the same colour group"); capture("crusher-split-output-reservations.png"); - server( - player -> { - var machine = machine(player); - machine.setReservePercent(0); - machine.energy.setStored(80000); - for (int i = 0; i < 100; i++) - machine.serverTick(); - }); + server(player -> { + var machine = machine(player); + machine.setReservePercent(0); + machine.energy.setStored(80000); + for (int i = 0; i < 100; i++) + machine.serverTick(); + }); } case 7 -> { if (!menu.getSlot(0).getItem().isEmpty() @@ -139,16 +147,17 @@ static boolean run(MachineScreen screen) { || menu.getSlot(2).getItem().getCount() != 64) return false; require( - menu.reservationPlan(1).isNone() && menu.reservationPlan(2).isNone(), + menu.reservationPlan(1).isNone() + && menu.reservationPlan(2).isNone(), "Both completed output reservations clear on the client"); capture("crusher-split-output-completed.png"); - server( - player -> { - var machine = machine(player); - machine.items.setStackInSlot(0, new ItemStack(Items.COPPER_ORE, 16)); - machine.items.setStackInSlot(1, ItemStack.EMPTY); - machine.items.setStackInSlot(2, ItemStack.EMPTY); - }); + server(player -> { + var machine = machine(player); + machine.items.setStackInSlot(0, + new ItemStack(Items.COPPER_ORE, 16)); + machine.items.setStackInSlot(1, ItemStack.EMPTY); + machine.items.setStackInSlot(2, ItemStack.EMPTY); + }); } default -> { return true; @@ -165,23 +174,23 @@ private static ProcessorEntity machine(ServerPlayer player) { private static void server(Consumer action) { ready = false; var server = Minecraft.getInstance().getSingleplayerServer(); - server.execute( - () -> { - try { - var player = server.getPlayerList().getPlayers().getFirst(); - action.accept(player); - player.containerMenu.broadcastChanges(); - ready = true; - } catch (Throwable error) { - CompanionClientSmoke.fail(error); - } - }); + server.execute(() -> { + try { + var player = server.getPlayerList().getPlayers().getFirst(); + action.accept(player); + player.containerMenu.broadcastChanges(); + ready = true; + } catch (Throwable error) { + CompanionClientSmoke.fail(error); + } + }); } private static void capture(String file) { var mc = Minecraft.getInstance(); - Screenshot.grab(mc.gameDirectory, file, mc.getMainRenderTarget(), message -> { - }); + Screenshot.grab(mc.gameDirectory, file, mc.getMainRenderTarget(), + message -> { + }); } private static void require(boolean value, String message) { diff --git a/src/companionTest/java/zone/moddev/mc/companion/test/SimpleCrusherClientSmoke.java b/src/companionTest/java/zone/moddev/mc/companion/test/SimpleCrusherClientSmoke.java index 28d5fcb7..18147bf1 100644 --- a/src/companionTest/java/zone/moddev/mc/companion/test/SimpleCrusherClientSmoke.java +++ b/src/companionTest/java/zone/moddev/mc/companion/test/SimpleCrusherClientSmoke.java @@ -30,19 +30,29 @@ import zone.moddev.mc.lib.machine.neoforge.DefinedMachineEntity; import zone.moddev.mc.lib.machine.neoforge.MachineContent; -/** Focused real-client check of XML registration, generated layout, recipes and /reload. */ +/** + * Focused real-client check of XML registration, generated layout, recipes and + * /reload. + */ @EventBusSubscriber(modid = "mmdlib_companion", value = Dist.CLIENT) public final class SimpleCrusherClientSmoke { - private static final boolean LAVA = Boolean.getBoolean("mmdlib.simpleLavaFurnaceSmoke"); - private static final String NAME = LAVA ? "simple-lava-furnace" : "simple-crusher"; - private static final String MARKER = LAVA ? "SIMPLE_LAVA_FURNACE" : "SIMPLE_CRUSHER"; - private static final ResourceLocation ID = ResourceLocation.parse( - "mmdlib_companion:" + (LAVA ? "simple_lava_furnace" : "simple_crusher")); - private static final ResourceLocation TYPE = ResourceLocation - .parse("mmdlib_companion:" + (LAVA ? "lava_smelting" : "simple_crushing")); + private static final boolean LAVA = Boolean + .getBoolean("mmdlib.simpleLavaFurnaceSmoke"); + private static final String NAME = LAVA + ? "simple-lava-furnace" + : "simple-crusher"; + private static final String MARKER = LAVA + ? "SIMPLE_LAVA_FURNACE" + : "SIMPLE_CRUSHER"; + private static final ResourceLocation ID = ResourceLocation + .parse("mmdlib_companion:" + + (LAVA ? "simple_lava_furnace" : "simple_crusher")); + private static final ResourceLocation TYPE = ResourceLocation.parse( + "mmdlib_companion:" + (LAVA ? "lava_smelting" : "simple_crushing")); private static final BlockPos POS = new BlockPos(0, 100, 0); private static final long DEADLINE = System.currentTimeMillis() + 240000; - private static boolean started, placed, requested, finished, reloadRequested; + private static boolean started, placed, requested, finished, + reloadRequested; private static volatile boolean reloaded; private static boolean recipesOpened; private static int frames; @@ -51,7 +61,8 @@ public final class SimpleCrusherClientSmoke { @SubscribeEvent public static void tick(ClientTickEvent.Post event) { - if ((!Boolean.getBoolean("mmdlib.simpleCrusherSmoke") && !LAVA) || finished) + if ((!Boolean.getBoolean("mmdlib.simpleCrusherSmoke") && !LAVA) + || finished) return; var mc = Minecraft.getInstance(); try { @@ -59,76 +70,72 @@ public static void tick(ClientTickEvent.Post event) { throw new AssertionError("Timed out: " + mc.screen); if (!started && mc.screen instanceof TitleScreen) { started = true; - var settings = new LevelSettings( - NAME + " smoke", - GameType.CREATIVE, - false, - Difficulty.PEACEFUL, - true, - new GameRules(), - WorldDataConfiguration.DEFAULT); - mc.createWorldOpenFlows() - .createFreshLevel( - NAME + "-" + System.currentTimeMillis(), - settings, - new WorldOptions(1, false, false), - registries -> registries - .registryOrThrow(Registries.WORLD_PRESET) - .getHolderOrThrow(WorldPresets.FLAT) - .value() - .createWorldDimensions(), - mc.screen); + var settings = new LevelSettings(NAME + " smoke", + GameType.CREATIVE, false, Difficulty.PEACEFUL, true, + new GameRules(), WorldDataConfiguration.DEFAULT); + mc.createWorldOpenFlows().createFreshLevel( + NAME + "-" + System.currentTimeMillis(), settings, + new WorldOptions(1, false, false), + registries -> registries + .registryOrThrow(Registries.WORLD_PRESET) + .getHolderOrThrow(WorldPresets.FLAT).value() + .createWorldDimensions(), + mc.screen); } if (mc.player == null || mc.level == null) return; var server = mc.getSingleplayerServer(); if (!placed) { placed = true; - server.execute( - () -> { - try { - var player = server.getPlayerList().getPlayers().getFirst(); - var level = player.serverLevel(); - level.getChunkAt(POS); - for (int x = -5; x <= 5; x++) - for (int z = -5; z <= 5; z++) - level.setBlock( - new BlockPos(x, 99, z), - net.minecraft.world.level.block.Blocks.STONE - .defaultBlockState(), - 3); - player.teleportTo(.5, 100, 3.5); - level.setBlock(POS, BuiltInRegistries.BLOCK.get(ID).defaultBlockState(), - 3); - var machine = (DefinedMachineEntity) level.getBlockEntity(POS); - if (LAVA) - machine.fluids.fill( - new net.neoforged.neoforge.fluids.FluidStack( - net.minecraft.world.level.material.Fluids.LAVA, 4000), - net.neoforged.neoforge.fluids.capability.IFluidHandler.FluidAction.EXECUTE); - else - machine.energy.setStored(90000); - machine.items.insertItem(0, new ItemStack(Items.IRON_ORE, 32), false); - for (int i = 0; i < 31; i++) - machine.serverTick(); - } catch (Throwable error) { - fail(error); - } - }); + server.execute(() -> { + try { + var player = server.getPlayerList().getPlayers() + .getFirst(); + var level = player.serverLevel(); + level.getChunkAt(POS); + for (int x = -5; x <= 5; x++) + for (int z = -5; z <= 5; z++) + level.setBlock(new BlockPos(x, 99, z), + net.minecraft.world.level.block.Blocks.STONE + .defaultBlockState(), + 3); + player.teleportTo(.5, 100, 3.5); + level.setBlock(POS, + BuiltInRegistries.BLOCK.get(ID).defaultBlockState(), + 3); + var machine = (DefinedMachineEntity) level + .getBlockEntity(POS); + if (LAVA) + machine.fluids.fill( + new net.neoforged.neoforge.fluids.FluidStack( + net.minecraft.world.level.material.Fluids.LAVA, + 4000), + net.neoforged.neoforge.fluids.capability.IFluidHandler.FluidAction.EXECUTE); + else + machine.energy.setStored(90000); + machine.items.insertItem(0, + new ItemStack(Items.IRON_ORE, 32), false); + for (int i = 0; i < 31; i++) + machine.serverTick(); + } catch (Throwable error) { + fail(error); + } + }); } - if (!requested && mc.level.getBlockEntity(POS) instanceof DefinedMachineEntity) { + if (!requested && mc.level + .getBlockEntity(POS) instanceof DefinedMachineEntity) { requested = true; - server.execute( - () -> { - try { - var player = server.getPlayerList().getPlayers().getFirst(); - if (!ScreenRuntime.open(player, - player.serverLevel().getBlockEntity(POS))) - throw new AssertionError("Menu refused"); - } catch (Throwable error) { - fail(error); - } - }); + server.execute(() -> { + try { + var player = server.getPlayerList().getPlayers() + .getFirst(); + if (!ScreenRuntime.open(player, + player.serverLevel().getBlockEntity(POS))) + throw new AssertionError("Menu refused"); + } catch (Throwable error) { + fail(error); + } + }); } } catch (Throwable error) { fail(error); @@ -137,20 +144,20 @@ public static void tick(ClientTickEvent.Post event) { @SubscribeEvent public static void rendered(RenderFrameEvent.Post event) { - if ((!Boolean.getBoolean("mmdlib.simpleCrusherSmoke") && !LAVA) || finished) + if ((!Boolean.getBoolean("mmdlib.simpleCrusherSmoke") && !LAVA) + || finished) return; var mc = Minecraft.getInstance(); if (recipesOpened) { if (++frames < 10) return; finished = true; - Screenshot.grab( - mc.gameDirectory, NAME + "-recipes.png", mc.getMainRenderTarget(), message -> { + Screenshot.grab(mc.gameDirectory, NAME + "-recipes.png", + mc.getMainRenderTarget(), message -> { }); - MMDLib.LOGGER.info( - MARKER - + "_CLIENT_PASS: centered screen, arrow recipe click, assets, synchronized" - + " recipes and reload"); + MMDLib.LOGGER.info(MARKER + + "_CLIENT_PASS: centered screen, arrow recipe click, assets, synchronized" + + " recipes and reload"); mc.stop(); return; } @@ -161,29 +168,31 @@ public static void rendered(RenderFrameEvent.Post event) { if (++frames < 30) return; var menu = screen.getMenu(); - var inventoryViews = menu.bound.screen().elements().stream() - .filter(zone.moddev.mc.lib.gui.model.containers.InventoryElement.class::isInstance) - .map(zone.moddev.mc.lib.gui.model.containers.InventoryElement.class::cast) + var inventoryViews = menu.bound.screen().elements().stream().filter( + zone.moddev.mc.lib.gui.model.containers.InventoryElement.class::isInstance) + .map( + zone.moddev.mc.lib.gui.model.containers.InventoryElement.class::cast) .toList(); if (inventoryViews.size() < 2) - throw new AssertionError("Generated independent inventory elements missing"); + throw new AssertionError( + "Generated independent inventory elements missing"); for (var view : inventoryViews) { - var outline = menu.elements.stream() - .filter( - zone.moddev.mc.lib.gui.model.containers.ColouredGroupElement.class::isInstance) - .map(zone.moddev.mc.lib.gui.model.containers.ColouredGroupElement.class::cast) - .filter(group -> group.child() == view) - .findFirst() + var outline = menu.elements.stream().filter( + zone.moddev.mc.lib.gui.model.containers.ColouredGroupElement.class::isInstance) + .map( + zone.moddev.mc.lib.gui.model.containers.ColouredGroupElement.class::cast) + .filter(group -> group.child() == view).findFirst() .orElseThrow(); - if (!menu.layout().bounds(outline).equals(menu.layout().bounds(view))) - throw new AssertionError("Group decoration changed inventory geometry"); - if (menu.components().get(BuiltinMenuComponents.AUTOMATION).inventory.groups() + if (!menu.layout().bounds(outline) + .equals(menu.layout().bounds(view))) + throw new AssertionError( + "Group decoration changed inventory geometry"); + if (menu.components() + .get(BuiltinMenuComponents.AUTOMATION).inventory.groups() .stream() .filter(group -> group.inventory().equals(view.inventory())) - .noneMatch( - group -> group - .highlights() - .equals(java.util.List.of(outline.common().id().orElseThrow())))) + .noneMatch(group -> group.highlights().equals(java.util.List + .of(outline.common().id().orElseThrow())))) throw new AssertionError( "Automation does not highlight the inventory's outline"); } @@ -191,39 +200,47 @@ public static void rendered(RenderFrameEvent.Post event) { for (var view : inventoryViews) for (var slot : view.slots()) { if (!seenSlots.add(menu.slotIndex(slot))) - throw new AssertionError("Local slots alias one menu index"); + throw new AssertionError( + "Local slots alias one menu index"); var bounds = menu.layout().bounds(slot); int[] expected = slot.itemPosition(bounds); - if (menu.slot(slot).x != expected[0] || menu.slot(slot).y != expected[1]) + if (menu.slot(slot).x != expected[0] + || menu.slot(slot).y != expected[1]) throw new AssertionError( "Inventory element did not position its native slot"); } - if (menu.playerSlotElements().stream().filter(s -> s.icon().isPresent()).count() != 5) - throw new AssertionError("Shared slot appearance lost equipment placeholders"); - var progress = menu.bound.screen().find("generated_progress").orElseThrow(); + if (menu.playerSlotElements().stream() + .filter(s -> s.icon().isPresent()).count() != 5) + throw new AssertionError( + "Shared slot appearance lost equipment placeholders"); + var progress = menu.bound.screen().find("generated_progress") + .orElseThrow(); if (menu.value(progress, true) == null) throw new AssertionError("Progress maximum not synced"); - if (MachineContent.recipeType(TYPE).recipes(mc.level.getRecipeManager()).isEmpty()) + if (MachineContent.recipeType(TYPE) + .recipes(mc.level.getRecipeManager()).isEmpty()) throw new AssertionError("Generated recipes not synced"); if (!LAVA) { var energy = menu.bound.screen().find("energy").orElseThrow(); if (!(energy instanceof zone.moddev.mc.lib.gui.model.elements.EnergyGaugeElement) - || !(menu.value(energy, false) instanceof java.math.BigInteger stored) - || stored.signum() <= 0 - || !java.math.BigInteger.valueOf(100_000).equals(menu.value(energy, true))) - throw new AssertionError("Energy component did not synchronize"); + || !(menu.value(energy, + false) instanceof java.math.BigInteger stored) + || stored.signum() <= 0 || !java.math.BigInteger + .valueOf(100_000).equals(menu.value(energy, true))) + throw new AssertionError( + "Energy component did not synchronize"); var tooltip = screen.tooltipLines(energy, 0, 0); String expected = zone.moddev.mc.lib.gui.client.ScreenText - .number(menu.value(energy, false), false) - + " / 100 k FE"; - if (tooltip.size() != 1 || !tooltip.getFirst().getString().equals(expected)) - throw new AssertionError("Energy component tooltip changed: " + tooltip); + .number(menu.value(energy, false), false) + " / 100 k FE"; + if (tooltip.size() != 1 + || !tooltip.getFirst().getString().equals(expected)) + throw new AssertionError( + "Energy component tooltip changed: " + tooltip); } if (LAVA) { var gauge = menu.bound.screen().find("tank_0").orElseThrow(); var gaugeBounds = screen.bounds(gauge); - org.lwjgl.glfw.GLFW.glfwSetCursorPos( - mc.getWindow().getWindow(), + org.lwjgl.glfw.GLFW.glfwSetCursorPos(mc.getWindow().getWindow(), (gaugeBounds.x() + gaugeBounds.width() / 2.) * mc.getWindow().getScreenWidth() / mc.getWindow().getGuiScaledWidth(), @@ -232,8 +249,8 @@ public static void rendered(RenderFrameEvent.Post event) { / mc.getWindow().getGuiScaledHeight()); if (!(menu.value(gauge, false) instanceof net.neoforged.neoforge.fluids.FluidStack fluid) - || fluid.getAmount() <= 0 - || fluid.getFluid() != net.minecraft.world.level.material.Fluids.LAVA) + || fluid.getAmount() <= 0 || fluid + .getFluid() != net.minecraft.world.level.material.Fluids.LAVA) throw new AssertionError("Lava gauge did not synchronize"); if (menu.bound.screen().find("energy").isPresent()) throw new AssertionError("Unexpected FE gauge"); @@ -242,7 +259,8 @@ public static void rendered(RenderFrameEvent.Post event) { || !tooltip.get(0).getString().equals("fuel") || !tooltip.get(1).getString().contains("mB of Lava") || !tooltip.get(2).getString().equals("Accepts: Lava")) - throw new AssertionError("Tank tooltip did not synchronize: " + tooltip); + throw new AssertionError( + "Tank tooltip did not synchronize: " + tooltip); if (tooltipStage < 3) { if (tooltipStage > 0) { String expected = tooltipStage == 1 @@ -252,26 +270,24 @@ public static void rendered(RenderFrameEvent.Post event) { ? net.minecraft.ChatFormatting.BLUE : net.minecraft.ChatFormatting.RED; if (!tooltip.getLast().getString().equals(expected) - || tooltip.getLast().getStyle().getColor().getValue() != color - .getColor()) - throw new AssertionError("Wrong carried-container tooltip: " + tooltip); - Screenshot.grab( - mc.gameDirectory, + || tooltip.getLast().getStyle().getColor() + .getValue() != color.getColor()) + throw new AssertionError( + "Wrong carried-container tooltip: " + tooltip); + Screenshot.grab(mc.gameDirectory, NAME + "-tooltip-" + tooltipStage + ".png", - mc.getMainRenderTarget(), - message -> { + mc.getMainRenderTarget(), message -> { }); } final ItemStack next = tooltipStage == 0 ? new ItemStack(Items.LAVA_BUCKET) - : tooltipStage == 1 ? new ItemStack(Items.WATER_BUCKET) : ItemStack.EMPTY; - mc.getSingleplayerServer() - .execute( - () -> mc.getSingleplayerServer() - .getPlayerList() - .getPlayers() - .getFirst().containerMenu - .setCarried(next)); + : tooltipStage == 1 + ? new ItemStack(Items.WATER_BUCKET) + : ItemStack.EMPTY; + mc.getSingleplayerServer().execute( + () -> mc.getSingleplayerServer().getPlayerList() + .getPlayers().getFirst().containerMenu + .setCarried(next)); tooltipStage++; frames = 0; return; @@ -279,47 +295,50 @@ public static void rendered(RenderFrameEvent.Post event) { if (transferStage == 0) { transferStage = 1; frames = 0; - mc.getSingleplayerServer() - .execute( - () -> { - var player = mc.getSingleplayerServer().getPlayerList().getPlayers() - .getFirst(); - var machine = (DefinedMachineEntity) player.serverLevel() - .getBlockEntity(POS); - var features = machine.screenFeatures(); - features.setProcessorPaused( - features.processor(java.util.Optional.empty()).orElseThrow() - .key(), - true); - machine.fluids.restoreFluid( - 0, - new net.neoforged.neoforge.fluids.FluidStack( - net.minecraft.world.level.material.Fluids.LAVA, 4000)); - player.getAbilities().instabuild = false; - player.containerMenu.setCarried(new ItemStack(Items.BUCKET)); - player.containerMenu.broadcastChanges(); - }); + mc.getSingleplayerServer().execute(() -> { + var player = mc.getSingleplayerServer().getPlayerList() + .getPlayers().getFirst(); + var machine = (DefinedMachineEntity) player + .serverLevel().getBlockEntity(POS); + var features = machine.screenFeatures(); + features.setProcessorPaused( + features.processor(java.util.Optional.empty()) + .orElseThrow().key(), + true); + machine.fluids.restoreFluid(0, + new net.neoforged.neoforge.fluids.FluidStack( + net.minecraft.world.level.material.Fluids.LAVA, + 4000)); + player.getAbilities().instabuild = false; + player.containerMenu + .setCarried(new ItemStack(Items.BUCKET)); + player.containerMenu.broadcastChanges(); + }); return; } if (transferStage == 1 || transferStage == 2) { - var expected = transferStage == 1 ? Items.BUCKET : Items.LAVA_BUCKET; + var expected = transferStage == 1 + ? Items.BUCKET + : Items.LAVA_BUCKET; int amount = transferStage == 1 ? 4000 : 3000; - if (!menu.getCarried().is(expected) || fluid.getAmount() != amount) + if (!menu.getCarried().is(expected) + || fluid.getAmount() != amount) return; - if (!screen.mouseClicked( - gaugeBounds.x() + 9, gaugeBounds.y() + 24, transferStage == 1 ? 1 : 0)) - throw new AssertionError("Generated gauge did not handle container click"); + if (!screen.mouseClicked(gaugeBounds.x() + 9, + gaugeBounds.y() + 24, transferStage == 1 ? 1 : 0)) + throw new AssertionError( + "Generated gauge did not handle container click"); transferStage++; frames = 0; return; } if (transferStage == 3) { - if (!menu.getCarried().is(Items.BUCKET) || fluid.getAmount() != 4000) + if (!menu.getCarried().is(Items.BUCKET) + || fluid.getAmount() != 4000) return; transferStage = 4; - Screenshot.grab( - mc.gameDirectory, NAME + "-container.png", mc.getMainRenderTarget(), - message -> { + Screenshot.grab(mc.gameDirectory, NAME + "-container.png", + mc.getMainRenderTarget(), message -> { }); MMDLib.LOGGER.info( "SIMPLE_LAVA_FURNACE_CONTAINER_PASS: right drain, left fill, cursor and tank" @@ -328,95 +347,88 @@ public static void rendered(RenderFrameEvent.Post event) { } if (!LAVA) for (var name : java.util.List.of("cobblestone", "gravel")) - if (mc.level - .getRecipeManager() - .byKey(ResourceLocation - .parse("mmdlib_companion:simple_crushing/stone/" + name)) + if (mc.level.getRecipeManager() + .byKey(ResourceLocation.parse( + "mmdlib_companion:simple_crushing/stone/" + name)) .isEmpty()) - throw new AssertionError("Bundled recipe not synchronized: " + name); - var model = mc.getBlockRenderer().getBlockModel(mc.level.getBlockState(POS)); - if (model.getParticleIcon().contents().name().getPath().equals("missingno")) + throw new AssertionError( + "Bundled recipe not synchronized: " + name); + var model = mc.getBlockRenderer() + .getBlockModel(mc.level.getBlockState(POS)); + if (model.getParticleIcon().contents().name().getPath() + .equals("missingno")) throw new AssertionError("Generated block model missing"); if (!reloadRequested) { reloadRequested = true; - Screenshot.grab(mc.gameDirectory, NAME + ".png", mc.getMainRenderTarget(), - message -> { + Screenshot.grab(mc.gameDirectory, NAME + ".png", + mc.getMainRenderTarget(), message -> { }); var server = mc.getSingleplayerServer(); - server.execute( - () -> { - server.getPlayerList().getPlayers().getFirst().closeContainer(); - reloadAfterRejectedCandidate(server) - .whenComplete( - (ignored, error) -> { - if (error != null) { - fail(error); - return; - } - server.execute( - () -> { - try { - if (MachineContent.recipeType(TYPE) - .recipes(server.getRecipeManager()) - .isEmpty()) - throw new AssertionError( - "Recipes missing after reload"); - reloaded = true; - var player = server.getPlayerList().getPlayers() - .getFirst(); - ScreenRuntime.open( - player, - player.serverLevel().getBlockEntity(POS)); - } catch (Throwable failure) { - fail(failure); - } - }); - }); - }); + server.execute(() -> { + server.getPlayerList().getPlayers().getFirst() + .closeContainer(); + reloadAfterRejectedCandidate(server) + .whenComplete((ignored, error) -> { + if (error != null) { + fail(error); + return; + } + server.execute(() -> { + try { + if (MachineContent.recipeType(TYPE) + .recipes(server.getRecipeManager()) + .isEmpty()) + throw new AssertionError( + "Recipes missing after reload"); + reloaded = true; + var player = server.getPlayerList() + .getPlayers().getFirst(); + ScreenRuntime.open(player, player + .serverLevel().getBlockEntity(POS)); + } catch (Throwable failure) { + fail(failure); + } + }); + }); + }); frames = 0; return; } if (!reloaded) return; - Screenshot.grab( - mc.gameDirectory, NAME + "-reloaded.png", mc.getMainRenderTarget(), message -> { + Screenshot.grab(mc.gameDirectory, NAME + "-reloaded.png", + mc.getMainRenderTarget(), message -> { }); - var field = Class.forName("zone.moddev.mc.lib.client.jei.MachineJei") + var field = Class + .forName("zone.moddev.mc.lib.client.jei.MachineJei") .getDeclaredField("runtime"); field.setAccessible(true); var runtime = (mezz.jei.api.runtime.IJeiRuntime) field.get(null); - var type = zone.moddev.mc.lib.client.jei.MachineRecipeCategory.recipeType( - MachineContent.recipeType(TYPE)); - if (runtime == null - || runtime.getRecipeManager().createRecipeLookup(type).get().findAny().isEmpty()) + var type = zone.moddev.mc.lib.client.jei.MachineRecipeCategory + .recipeType(MachineContent.recipeType(TYPE)); + if (runtime == null || runtime.getRecipeManager() + .createRecipeLookup(type).get().findAny().isEmpty()) throw new AssertionError("JEI crusher recipes unavailable"); - if (!LAVA - && runtime - .getRecipeManager() - .createRecipeLookup(type) - .get() - .noneMatch( - holder -> holder - .id() - .equals( - ResourceLocation.parse( - "mmdlib_companion:simple_crushing/stone/cobblestone")))) - throw new AssertionError("Bundled recipe missing from JEI after reload"); + if (!LAVA && runtime.getRecipeManager().createRecipeLookup(type) + .get() + .noneMatch(holder -> holder.id().equals(ResourceLocation.parse( + "mmdlib_companion:simple_crushing/stone/cobblestone")))) + throw new AssertionError( + "Bundled recipe missing from JEI after reload"); var longestExpectedDetails = net.minecraft.network.chat.Component .translatable("jei.mmdlib.machine.energy", "2,900") - .append(" · ") - .append( - net.minecraft.network.chat.Component.translatable( - "jei.mmdlib.machine.time", "7.25")); - if (!LAVA - && runtime.getRecipeManager().getRecipeCategory(type).getWidth() < mc.font - .width(longestExpectedDetails)) - throw new AssertionError("JEI category clips fractional processing time"); + .append(" · ").append(net.minecraft.network.chat.Component + .translatable("jei.mmdlib.machine.time", "7.25")); + if (!LAVA && runtime.getRecipeManager().getRecipeCategory(type) + .getWidth() < mc.font.width(longestExpectedDetails)) + throw new AssertionError( + "JEI category clips fractional processing time"); var bounds = screen.bounds(progress); - if (!screen.mouseClicked( - bounds.x() + bounds.width() / 2., bounds.y() + bounds.height() / 2., 0) + if (!screen.mouseClicked(bounds.x() + bounds.width() / 2., + bounds.y() + bounds.height() / 2., 0) || mc.screen != runtime.getRecipesGui()) - throw new AssertionError("Processor arrow did not open JEI recipes"); + throw new AssertionError( + "Processor arrow did not open JEI recipes"); recipesOpened = true; frames = 0; } catch (Throwable error) { @@ -432,24 +444,25 @@ private static void fail(Throwable error) { private static java.util.concurrent.CompletableFuture reloadAfterRejectedCandidate( net.minecraft.server.MinecraftServer server) { - var selected = java.util.List.copyOf(server.getPackRepository().getSelectedIds()); + var selected = java.util.List + .copyOf(server.getPackRepository().getSelectedIds()); var active = server.getRecipeManager(); var before = java.util.List.copyOf(active.getRecipes()); var pack = server - .getWorldPath(net.minecraft.world.level.storage.LevelResource.DATAPACK_DIR) + .getWorldPath( + net.minecraft.world.level.storage.LevelResource.DATAPACK_DIR) .resolve("mmdlib-invalid-reload"); try { - var recipe = pack.resolve("data/mmdlib_companion/recipe/rejected.json"); + var recipe = pack + .resolve("data/mmdlib_companion/recipe/rejected.json"); java.nio.file.Files.createDirectories(recipe.getParent()); int format = net.minecraft.SharedConstants.getCurrentVersion() - .getPackVersion(net.minecraft.server.packs.PackType.SERVER_DATA); - java.nio.file.Files.writeString( - pack.resolve("pack.mcmeta"), - "{\"pack\":{\"pack_format\":" - + format + .getPackVersion( + net.minecraft.server.packs.PackType.SERVER_DATA); + java.nio.file.Files.writeString(pack.resolve("pack.mcmeta"), + "{\"pack\":{\"pack_format\":" + format + ",\"description\":\"Failed reload regression fixture\"}}"); - java.nio.file.Files.writeString( - recipe, + java.nio.file.Files.writeString(recipe, """ {"type":"mmdlib_companion:simple_crushing", "inputs":{"misspelled_input":[{"items":{"item":"minecraft:stone"}}]},"time":1} @@ -457,22 +470,19 @@ private static java.util.concurrent.CompletableFuture reloadAfterRejectedC server.getPackRepository().reload(); var failing = new java.util.ArrayList<>(selected); failing.add("file/mmdlib-invalid-reload"); - return server - .reloadResources(failing) - .handleAsync( - (ignored, error) -> { - if (error == null) - throw new AssertionError( - "Invalid authored group did not reject reload"); - if (server.getRecipeManager() != active - || !java.util.List.copyOf(active.getRecipes()).equals(before)) - throw new AssertionError("Rejected reload changed the active recipes"); - MMDLib.LOGGER - .info("SIMPLE_CRUSHER_REJECTED_RELOAD_PASS: active recipes preserved"); - return server.reloadResources(selected); - }, - server) - .thenCompose(future -> future); + return server.reloadResources(failing) + .handleAsync((ignored, error) -> { + if (error == null) + throw new AssertionError( + "Invalid authored group did not reject reload"); + if (server.getRecipeManager() != active || !java.util.List + .copyOf(active.getRecipes()).equals(before)) + throw new AssertionError( + "Rejected reload changed the active recipes"); + MMDLib.LOGGER.info( + "SIMPLE_CRUSHER_REJECTED_RELOAD_PASS: active recipes preserved"); + return server.reloadResources(selected); + }, server).thenCompose(future -> future); } catch (java.io.IOException error) { return java.util.concurrent.CompletableFuture.failedFuture(error); } diff --git a/src/companionTest/java/zone/moddev/mc/companion/test/SimpleCrusherTests.java b/src/companionTest/java/zone/moddev/mc/companion/test/SimpleCrusherTests.java index 9f1f54ab..4ed1c640 100644 --- a/src/companionTest/java/zone/moddev/mc/companion/test/SimpleCrusherTests.java +++ b/src/companionTest/java/zone/moddev/mc/companion/test/SimpleCrusherTests.java @@ -26,443 +26,414 @@ @GameTestHolder("mmdlib_companion") @PrefixGameTestTemplate(false) public final class SimpleCrusherTests { - private static final ResourceLocation ID = - ResourceLocation.parse("mmdlib_companion:simple_crusher"); - private static final ResourceLocation TYPE = - ResourceLocation.parse("mmdlib_companion:simple_crushing"); - private static final ResourceLocation IRON = - ResourceLocation.parse("mmdlib_companion:generated/simple_crushing/ores/iron"); + private static final ResourceLocation ID = ResourceLocation + .parse("mmdlib_companion:simple_crusher"); + private static final ResourceLocation TYPE = ResourceLocation + .parse("mmdlib_companion:simple_crushing"); + private static final ResourceLocation IRON = ResourceLocation + .parse("mmdlib_companion:generated/simple_crushing/ores/iron"); - private static DefinedMachineEntity place(GameTestHelper h) { - var pos = h.absolutePos(new BlockPos(2, 2, 2)); - h.getLevel().setBlock(pos, BuiltInRegistries.BLOCK.get(ID).defaultBlockState(), 3); - return (DefinedMachineEntity) h.getLevel().getBlockEntity(pos); - } + private static DefinedMachineEntity place(GameTestHelper h) { + var pos = h.absolutePos(new BlockPos(2, 2, 2)); + h.getLevel().setBlock(pos, + BuiltInRegistries.BLOCK.get(ID).defaultBlockState(), 3); + return (DefinedMachineEntity) h.getLevel().getBlockEntity(pos); + } - private static void require(boolean value, String message) { - if (!value) throw new AssertionError(message); - } + private static void require(boolean value, String message) { + if (!value) + throw new AssertionError(message); + } - @GameTest(template = "empty") - public static void generatedRecipesDriveAClasslessCrusher(GameTestHelper h) { - var machine = place(h); - var type = MachineContent.recipeType(TYPE); - require( - machine.getClass() == DefinedMachineEntity.class, "Generic entity, no crusher subclass"); - var holder = - type.recipes(h.getLevel().getRecipeManager()).stream() - .filter(r -> r.id().equals(ResourceLocation.parse(IRON + "/minecraft/iron_ore"))) - .findFirst() - .orElseThrow(); - require(holder.value().recipe().time() == 130, "XML hardness scaling applied to iron ore"); - machine.energy.setStored(10000); - require( - machine.items.insertItem(0, new ItemStack(Items.IRON_ORE), false).isEmpty(), - "Recipe-linked ore admission"); - for (int i = 0; i < 131; i++) machine.serverTick(); - require(machine.items.getStackInSlot(0).isEmpty(), "Consumed input group"); - require( - machine.items.getStackInSlot(1).is(Items.IRON_INGOT) - && machine.items.getStackInSlot(1).getCount() == 2, - "Default output count and XML group"); - require(machine.energy.stored() == 7400, "XML hardness scaling applied to total energy"); - require( - Screens.get(ID).orElseThrow().screen().find("generated_progress").isPresent(), - "Generated screen installed"); - h.succeed(); - } + @GameTest(template = "empty") + public static void generatedRecipesDriveAClasslessCrusher( + GameTestHelper h) { + var machine = place(h); + var type = MachineContent.recipeType(TYPE); + require(machine.getClass() == DefinedMachineEntity.class, + "Generic entity, no crusher subclass"); + var holder = type.recipes(h.getLevel().getRecipeManager()).stream() + .filter(r -> r.id() + .equals(ResourceLocation.parse(IRON + "/minecraft/iron_ore"))) + .findFirst().orElseThrow(); + require(holder.value().recipe().time() == 130, + "XML hardness scaling applied to iron ore"); + machine.energy.setStored(10000); + require(machine.items + .insertItem(0, new ItemStack(Items.IRON_ORE), false).isEmpty(), + "Recipe-linked ore admission"); + for (int i = 0; i < 131; i++) + machine.serverTick(); + require(machine.items.getStackInSlot(0).isEmpty(), + "Consumed input group"); + require( + machine.items.getStackInSlot(1).is(Items.IRON_INGOT) + && machine.items.getStackInSlot(1).getCount() == 2, + "Default output count and XML group"); + require(machine.energy.stored() == 7400, + "XML hardness scaling applied to total energy"); + require(Screens.get(ID).orElseThrow().screen() + .find("generated_progress").isPresent(), + "Generated screen installed"); + h.succeed(); + } - @GameTest(template = "empty") - public static void capabilitiesSaveAndReservationsUseInheritedStorage(GameTestHelper h) { - var machine = place(h); - var pos = machine.getBlockPos(); - var items = h.getLevel().getCapability(Capabilities.ItemHandler.BLOCK, pos, Direction.NORTH); - var power = h.getLevel().getCapability(Capabilities.EnergyStorage.BLOCK, pos, Direction.NORTH); - require( - items != null && items.getSlots() == 3 && power != null, - "Capabilities registered automatically"); - require( - items.insertItem(1, new ItemStack(Items.IRON_ORE), false).getCount() == 1, - "Outputs refuse automation insertion"); - require( - items.insertItem(0, new ItemStack(Items.DIRT), false).getCount() == 1, - "Recipe admission refuses unrelated items"); - power.receiveEnergy(1000, false); - power.receiveEnergy(1000, false); - power.receiveEnergy(600, false); - items.insertItem(0, new ItemStack(Items.IRON_ORE), false); - for (int i = 0; i < 26; i++) machine.serverTick(); - var saved = machine.saveWithoutMetadata(h.getLevel().registryAccess()); - machine.loadWithComponents(saved, h.getLevel().registryAccess()); - require(machine.getProgress() == 25, "Reserved cycle restored after storage"); - for (int i = 0; i < 105; i++) machine.serverTick(); - require( - machine.items.getStackInSlot(1).getCount() == 2 && machine.energy.stored() == 0, - "Resumed without duplicating inputs or power"); - h.succeed(); - } + @GameTest(template = "empty") + public static void capabilitiesSaveAndReservationsUseInheritedStorage( + GameTestHelper h) { + var machine = place(h); + var pos = machine.getBlockPos(); + var items = h.getLevel().getCapability(Capabilities.ItemHandler.BLOCK, + pos, Direction.NORTH); + var power = h.getLevel().getCapability(Capabilities.EnergyStorage.BLOCK, + pos, Direction.NORTH); + require(items != null && items.getSlots() == 3 && power != null, + "Capabilities registered automatically"); + require(items.insertItem(1, new ItemStack(Items.IRON_ORE), false) + .getCount() == 1, "Outputs refuse automation insertion"); + require(items.insertItem(0, new ItemStack(Items.DIRT), false) + .getCount() == 1, "Recipe admission refuses unrelated items"); + power.receiveEnergy(1000, false); + power.receiveEnergy(1000, false); + power.receiveEnergy(600, false); + items.insertItem(0, new ItemStack(Items.IRON_ORE), false); + for (int i = 0; i < 26; i++) + machine.serverTick(); + var saved = machine.saveWithoutMetadata(h.getLevel().registryAccess()); + machine.loadWithComponents(saved, h.getLevel().registryAccess()); + require(machine.getProgress() == 25, + "Reserved cycle restored after storage"); + for (int i = 0; i < 105; i++) + machine.serverTick(); + require( + machine.items.getStackInSlot(1).getCount() == 2 + && machine.energy.stored() == 0, + "Resumed without duplicating inputs or power"); + h.succeed(); + } - @GameTest(template = "empty") - public static void blockedOutputsDoNotConsumeResources(GameTestHelper h) { - var machine = place(h); - machine.energy.setStored(10000); - machine.items.setStackInSlot(1, new ItemStack(Items.GOLD_INGOT, 64)); - machine.items.setStackInSlot(2, new ItemStack(Items.GOLD_INGOT, 64)); - machine.items.insertItem(0, new ItemStack(Items.IRON_ORE), false); - machine.serverTick(); - require( - machine.items.getStackInSlot(0).getCount() == 1 && machine.energy.stored() == 10000, - "Blocked output holds resources"); - machine.items.setStackInSlot(2, ItemStack.EMPTY); - for (int i = 0; i < 131; i++) machine.serverTick(); - require( - machine.items.getStackInSlot(2).is(Items.IRON_INGOT), - "Products choose available member of output group"); - h.succeed(); - } + @GameTest(template = "empty") + public static void blockedOutputsDoNotConsumeResources(GameTestHelper h) { + var machine = place(h); + machine.energy.setStored(10000); + machine.items.setStackInSlot(1, new ItemStack(Items.GOLD_INGOT, 64)); + machine.items.setStackInSlot(2, new ItemStack(Items.GOLD_INGOT, 64)); + machine.items.insertItem(0, new ItemStack(Items.IRON_ORE), false); + machine.serverTick(); + require( + machine.items.getStackInSlot(0).getCount() == 1 + && machine.energy.stored() == 10000, + "Blocked output holds resources"); + machine.items.setStackInSlot(2, ItemStack.EMPTY); + for (int i = 0; i < 131; i++) + machine.serverTick(); + require(machine.items.getStackInSlot(2).is(Items.IRON_INGOT), + "Products choose available member of output group"); + h.succeed(); + } - @GameTest(template = "empty") - public static void unknownMetalsAndAuthoredOverridesAndFreshGeneration(GameTestHelper h) { - var tags = new TreeMap>>(); - tags.put( - ResourceLocation.parse("c:ores/testium"), - List.of(Items.ANCIENT_DEBRIS.builtInRegistryHolder())); - tags.put( - ResourceLocation.parse("c:ingots/testium"), - List.of( - Items.NETHERITE_INGOT.builtInRegistryHolder(), - Items.IRON_INGOT.builtInRegistryHolder())); - var id = - ResourceLocation.parse( + @GameTest(template = "empty") + public static void unknownMetalsAndAuthoredOverridesAndFreshGeneration( + GameTestHelper h) { + var tags = new TreeMap>>(); + tags.put(ResourceLocation.parse("c:ores/testium"), + List.of(Items.ANCIENT_DEBRIS.builtInRegistryHolder())); + tags.put(ResourceLocation.parse("c:ingots/testium"), + List.of(Items.NETHERITE_INGOT.builtInRegistryHolder(), + Items.IRON_INGOT.builtInRegistryHolder())); + var id = ResourceLocation.parse( "mmdlib_companion:generated/simple_crushing/ores/testium/minecraft/ancient_debris"); - var generated = - GeneratedMachineRecipes.generate( - List.of(), h.getLevel().registryAccess(), new ReloadTags(tags)); - var recipe = - (MachineRecipeEntry) - generated.stream().filter(r -> r.id().equals(id)).findFirst().orElseThrow().value(); - require( - recipe.getResultItem(h.getLevel().registryAccess()).is(Items.IRON_INGOT), - "Deterministic output tie-break"); - var again = - GeneratedMachineRecipes.generate( - List.of(), h.getLevel().registryAccess(), new ReloadTags(tags)); - require(again.size() == generated.size(), "Regeneration does not accumulate entries"); - var authored = - new RecipeHolder<>( - id, - new MachineRecipeEntry( - MachineContent.recipeType(TYPE), + var generated = GeneratedMachineRecipes.generate(List.of(), + h.getLevel().registryAccess(), new ReloadTags(tags)); + var recipe = (MachineRecipeEntry) generated.stream() + .filter(r -> r.id().equals(id)).findFirst().orElseThrow().value(); + require(recipe.getResultItem(h.getLevel().registryAccess()) + .is(Items.IRON_INGOT), "Deterministic output tie-break"); + var again = GeneratedMachineRecipes.generate(List.of(), + h.getLevel().registryAccess(), new ReloadTags(tags)); + require(again.size() == generated.size(), + "Regeneration does not accumulate entries"); + var authored = new RecipeHolder<>(id, + new MachineRecipeEntry(MachineContent.recipeType(TYPE), MachineRecipeBuilder.recipe(MachineContent.recipeType(TYPE)) - .inputItems( - "input", - net.minecraft.world.item.crafting.Ingredient.of(Items.ANCIENT_DEBRIS), + .inputItems("input", + net.minecraft.world.item.crafting.Ingredient + .of(Items.ANCIENT_DEBRIS), 1) - .outputItem("output", new ItemStack(Items.DIAMOND)) - .time(1) + .outputItem("output", new ItemStack(Items.DIAMOND)).time(1) .build())); - var combined = - GeneratedMachineRecipes.generate( - List.of(authored), h.getLevel().registryAccess(), new ReloadTags(tags)); - require( - combined.stream().filter(r -> r.id().equals(id)).findFirst().orElseThrow() == authored, - "Authored ID overrides generated"); - require( - GeneratedMachineRecipes.generate( - List.of(), h.getLevel().registryAccess(), new ReloadTags(Map.of())) - .isEmpty(), - "Removed tags remove recipes"); - h.succeed(); - } + var combined = GeneratedMachineRecipes.generate(List.of(authored), + h.getLevel().registryAccess(), new ReloadTags(tags)); + require(combined.stream().filter(r -> r.id().equals(id)).findFirst() + .orElseThrow() == authored, "Authored ID overrides generated"); + require(GeneratedMachineRecipes.generate(List.of(), + h.getLevel().registryAccess(), new ReloadTags(Map.of())).isEmpty(), + "Removed tags remove recipes"); + h.succeed(); + } - @GameTest(template = "empty") - public static void generatorDefaultsAndOrderedPreferences(GameTestHelper h) { - var modItem = - BuiltInRegistries.ITEM.stream() - .filter( - item -> - BuiltInRegistries.ITEM.getKey(item).getNamespace().equals("mmdlib") - && item.getDefaultInstance().getMaxStackSize() >= 2) - .findFirst() - .orElseThrow(); - var tags = - new ReloadTags( - Map.of( - ResourceLocation.parse("c:ores/testium"), - List.of(Items.IRON_ORE.builtInRegistryHolder()), + @GameTest(template = "empty") + public static void generatorDefaultsAndOrderedPreferences( + GameTestHelper h) { + var modItem = BuiltInRegistries.ITEM.stream() + .filter(item -> BuiltInRegistries.ITEM.getKey(item).getNamespace() + .equals("mmdlib") + && item.getDefaultInstance().getMaxStackSize() >= 2) + .findFirst().orElseThrow(); + var tags = new ReloadTags( + Map.of(ResourceLocation.parse("c:ores/testium"), + List.of(Items.IRON_ORE.builtInRegistryHolder()), ResourceLocation.parse("c:ingots/testium"), - List.of( - modItem.builtInRegistryHolder(), - Items.IRON_INGOT.builtInRegistryHolder()))); - var defaults = direct(h, tags, Map.of()).values().iterator().next(); - require( - defaults.time() == 100 && defaults.inputs().get("energy").getFirst().amount() == 2000, - "Code defaults"); - require( - defaults.inputs().get("input").getFirst().amount() == 1 - && product(defaults).getCount() == 2, - "One ore to two ingots independent of slots"); - require(product(defaults).is(Items.IRON_INGOT), "Minecraft preferred by default"); - var renamed = - direct( - h, - tags, - Map.of("input-group", "ores", "output-group", "ingots", "energy-group", "power")) - .values() - .iterator() - .next(); - require( - renamed.inputs().keySet().equals(Set.of("ores", "power")) - && renamed.outputs().keySet().equals(Set.of("ingots")), - "Optional group name overrides"); - var custom = - direct( - h, - tags, - Map.of( - "preferred-mods", "missing; mmdlib ;minecraft", "time", "17", "energy", "41")) - .values() - .iterator() + List.of(modItem.builtInRegistryHolder(), + Items.IRON_INGOT.builtInRegistryHolder()))); + var defaults = direct(h, tags, Map.of()).values().iterator().next(); + require( + defaults.time() == 100 + && defaults.inputs().get("energy").getFirst().amount() == 2000, + "Code defaults"); + require( + defaults.inputs().get("input").getFirst().amount() == 1 + && product(defaults).getCount() == 2, + "One ore to two ingots independent of slots"); + require(product(defaults).is(Items.IRON_INGOT), + "Minecraft preferred by default"); + var renamed = direct(h, tags, Map.of("input-group", "ores", + "output-group", "ingots", "energy-group", "power")).values() + .iterator().next(); + require( + renamed.inputs().keySet().equals(Set.of("ores", "power")) + && renamed.outputs().keySet().equals(Set.of("ingots")), + "Optional group name overrides"); + var custom = direct(h, tags, Map.of("preferred-mods", + "missing; mmdlib ;minecraft", "time", "17", "energy", "41")) + .values().iterator().next(); + require(product(custom).is(modItem), + "First available preferred namespace wins"); + require( + custom.time() == 17 + && custom.inputs().get("energy").getFirst().amount() == 41, + "Optional cost overrides"); + var reversed = direct(h, tags, + Map.of("preferred-mods", "minecraft;mmdlib")).values().iterator() .next(); - require(product(custom).is(modItem), "First available preferred namespace wins"); - require( - custom.time() == 17 && custom.inputs().get("energy").getFirst().amount() == 41, - "Optional cost overrides"); - var reversed = - direct(h, tags, Map.of("preferred-mods", "minecraft;mmdlib")).values().iterator().next(); - require(product(reversed).is(Items.IRON_INGOT), "Preference order changes output"); - h.succeed(); - } + require(product(reversed).is(Items.IRON_INGOT), + "Preference order changes output"); + h.succeed(); + } - @GameTest(template = "empty") - public static void generatorScalesEachOreUsingStagedBlockTags(GameTestHelper h) { - Map>> items = - Map.of( + @GameTest(template = "empty") + public static void generatorScalesEachOreUsingStagedBlockTags( + GameTestHelper h) { + Map>> items = Map.of( ResourceLocation.parse("c:ores/iron"), - List.of( - Items.IRON_ORE.builtInRegistryHolder(), - Items.DEEPSLATE_IRON_ORE.builtInRegistryHolder()), + List.of(Items.IRON_ORE.builtInRegistryHolder(), + Items.DEEPSLATE_IRON_ORE.builtInRegistryHolder()), ResourceLocation.parse("c:ingots/iron"), - List.of(Items.IRON_INGOT.builtInRegistryHolder())); - List> ores = - List.of( + List.of(Items.IRON_INGOT.builtInRegistryHolder())); + List> ores = List.of( Blocks.IRON_ORE.builtInRegistryHolder(), Blocks.DEEPSLATE_IRON_ORE.builtInRegistryHolder()); - var tags = new ReloadTags(items, Map.of(BlockTags.NEEDS_IRON_TOOL.location(), ores)); - var props = Map.of("tool-level-ratio", "0.25", "hardness-level-ratio", "0.1"); - var generated = direct(h, tags, props); - var iron = generated.get(ResourceLocation.parse(IRON + "/minecraft/iron_ore")); - var deep = generated.get(ResourceLocation.parse(IRON + "/minecraft/deepslate_iron_ore")); - require( - generated.size() == 2 && iron.time() == 180 && deep.time() == 195, - "Per-block hardness and staged tool requirement"); - require( - iron.inputs().get("energy").getFirst().amount() == 3600 - && deep.inputs().get("energy").getFirst().amount() == 3900, - "Scale total energy with same multiplier"); - var ingredient = - (zone.moddev.mc.lib.machine.ingredient.MachineIngredient.ItemPart) - iron.inputs().get("input").getFirst(); - require( - ingredient.items().test(new ItemStack(Items.IRON_ORE)) - && !ingredient.items().test(new ItemStack(Items.DEEPSLATE_IRON_ORE)), - "Each cost matches only its ore variant"); - var changed = - direct( - h, new ReloadTags(items, Map.of(BlockTags.NEEDS_DIAMOND_TOOL.location(), ores)), props); - require( - changed.get(ResourceLocation.parse(IRON + "/minecraft/iron_ore")).time() == 205, - "New staged tags replace old requirements"); - var rounded = - direct(h, tags, Map.of("time", "1", "energy", "1", "hardness-level-ratio", "0.01")); - require( - rounded.values().stream() - .allMatch( - recipe -> - recipe.time() == 2 && recipe.inputs().get("energy").getFirst().amount() == 2), - "Fractional costs round up"); - var perLevel = direct(h, tags, Map.of("tool-level-ratio", ".10")); - require( - perLevel.values().stream() - .allMatch( - recipe -> - recipe.time() == 120 - && recipe.inputs().get("energy").getFirst().amount() == 2400), - ".10 adds ten percent per level without compounding"); - var stoneTags = new ReloadTags(items, Map.of(BlockTags.NEEDS_STONE_TOOL.location(), ores)); - var boundary = direct(h, stoneTags, Map.of("tool-level-ratio", ".10", "energy", "100")); - require( - boundary.values().stream() - .allMatch( - recipe -> - recipe.time() == 110 - && recipe.inputs().get("energy").getFirst().amount() == 110), - "Double roundoff at 100 * 1.1 does not add an extra unit"); - var fraction = direct(h, stoneTags, Map.of("tool-level-ratio", ".100001", "energy", "100")); - require( - fraction.values().stream() - .allMatch( - recipe -> - recipe.time() == 111 - && recipe.inputs().get("energy").getFirst().amount() == 111), - "Real fractional costs still round up"); - var zero = direct(h, tags, Map.of("tool-level-ratio", "0.0", "hardness-level-ratio", "0.0")); - require( - zero.size() == 1 - && zero.get(IRON).time() == 100 - && zero.get(IRON).inputs().get("energy").getFirst().amount() == 2000, - "Explicit zero ratios preserve default costs and tag recipe IDs"); - var unchanged = - direct( - h, - new ReloadTags(items), - Map.of( - "tool-level-ratio", - ".10", - "time", - Integer.toString(Integer.MAX_VALUE), - "energy", + var tags = new ReloadTags(items, + Map.of(BlockTags.NEEDS_IRON_TOOL.location(), ores)); + var props = Map.of("tool-level-ratio", "0.25", "hardness-level-ratio", + "0.1"); + var generated = direct(h, tags, props); + var iron = generated + .get(ResourceLocation.parse(IRON + "/minecraft/iron_ore")); + var deep = generated.get( + ResourceLocation.parse(IRON + "/minecraft/deepslate_iron_ore")); + require( + generated.size() == 2 && iron.time() == 180 && deep.time() == 195, + "Per-block hardness and staged tool requirement"); + require( + iron.inputs().get("energy").getFirst().amount() == 3600 + && deep.inputs().get("energy").getFirst().amount() == 3900, + "Scale total energy with same multiplier"); + var ingredient = (zone.moddev.mc.lib.machine.ingredient.MachineIngredient.ItemPart) iron + .inputs().get("input").getFirst(); + require( + ingredient.items().test(new ItemStack(Items.IRON_ORE)) + && !ingredient.items() + .test(new ItemStack(Items.DEEPSLATE_IRON_ORE)), + "Each cost matches only its ore variant"); + var changed = direct(h, new ReloadTags(items, + Map.of(BlockTags.NEEDS_DIAMOND_TOOL.location(), ores)), props); + require( + changed.get(ResourceLocation.parse(IRON + "/minecraft/iron_ore")) + .time() == 205, + "New staged tags replace old requirements"); + var rounded = direct(h, tags, + Map.of("time", "1", "energy", "1", "hardness-level-ratio", "0.01")); + require( + rounded.values().stream() + .allMatch(recipe -> recipe.time() == 2 + && recipe.inputs().get("energy").getFirst().amount() == 2), + "Fractional costs round up"); + var perLevel = direct(h, tags, Map.of("tool-level-ratio", ".10")); + require( + perLevel.values().stream().allMatch(recipe -> recipe.time() == 120 + && recipe.inputs().get("energy").getFirst().amount() == 2400), + ".10 adds ten percent per level without compounding"); + var stoneTags = new ReloadTags(items, + Map.of(BlockTags.NEEDS_STONE_TOOL.location(), ores)); + var boundary = direct(h, stoneTags, + Map.of("tool-level-ratio", ".10", "energy", "100")); + require( + boundary.values().stream().allMatch(recipe -> recipe.time() == 110 + && recipe.inputs().get("energy").getFirst().amount() == 110), + "Double roundoff at 100 * 1.1 does not add an extra unit"); + var fraction = direct(h, stoneTags, + Map.of("tool-level-ratio", ".100001", "energy", "100")); + require( + fraction.values().stream().allMatch(recipe -> recipe.time() == 111 + && recipe.inputs().get("energy").getFirst().amount() == 111), + "Real fractional costs still round up"); + var zero = direct(h, tags, + Map.of("tool-level-ratio", "0.0", "hardness-level-ratio", "0.0")); + require( + zero.size() == 1 && zero.get(IRON).time() == 100 + && zero.get(IRON).inputs().get("energy").getFirst() + .amount() == 2000, + "Explicit zero ratios preserve default costs and tag recipe IDs"); + var unchanged = direct(h, new ReloadTags(items), + Map.of("tool-level-ratio", ".10", "time", + Integer.toString(Integer.MAX_VALUE), "energy", Long.toString(Long.MAX_VALUE))); - require( - unchanged.values().stream() - .allMatch( - recipe -> - recipe.time() == Integer.MAX_VALUE - && recipe.inputs().get("energy").getFirst().amount() == Long.MAX_VALUE), - "Factor one preserves exact maximum integer costs"); - h.succeed(); - } + require(unchanged.values().stream() + .allMatch(recipe -> recipe.time() == Integer.MAX_VALUE && recipe + .inputs().get("energy").getFirst().amount() == Long.MAX_VALUE), + "Factor one preserves exact maximum integer costs"); + h.succeed(); + } - @GameTest(template = "empty") - public static void generatorRejectsBadOptionsAndOverflow(GameTestHelper h) { - for (var option : - List.of( - Map.of("preferred-mods", "minecraft:mmdlib"), + @GameTest(template = "empty") + public static void generatorRejectsBadOptionsAndOverflow(GameTestHelper h) { + for (var option : List.of(Map.of("preferred-mods", "minecraft:mmdlib"), Map.of("preferred-mods", "minecraft;"), Map.of("tool-level-ratio", "NaN"), Map.of("hardness-level-ratio", "Infinity"), Map.of("hardness-level-ratio", "-0.1"), - Map.of("preferred-mod", "minecraft"), - Map.of("input-count", "2"), + Map.of("preferred-mod", "minecraft"), Map.of("input-count", "2"), Map.of("time", "0"))) { - try { - direct(h, new ReloadTags(Map.of()), option); - throw new AssertionError("Accepted invalid options " + option); - } catch (IllegalArgumentException expected) { - require( - expected.getMessage().contains("test-generator.xml:1"), "Source-aware option error"); - } - } - var tags = - new ReloadTags( - Map.of( - ResourceLocation.parse("c:ores/iron"), - List.of(Items.IRON_ORE.builtInRegistryHolder()), - ResourceLocation.parse("c:ingots/iron"), - List.of(Items.IRON_INGOT.builtInRegistryHolder()))); - try { - direct(h, tags, Map.of("energy", Long.toString(Long.MAX_VALUE), "hardness-level-ratio", "1")); - throw new AssertionError("Accepted energy overflow"); - } catch (IllegalArgumentException expected) { - require( - expected.getMessage().contains("Scaled energy"), "Overflow fails instead of wrapping"); - } - for (var options : - List.of( - Map.of("time", Integer.toString(Integer.MAX_VALUE), "hardness-level-ratio", ".10"), + try { + direct(h, new ReloadTags(Map.of()), option); + throw new AssertionError("Accepted invalid options " + option); + } catch (IllegalArgumentException expected) { + require(expected.getMessage().contains("test-generator.xml:1"), + "Source-aware option error"); + } + } + var tags = new ReloadTags(Map.of(ResourceLocation.parse("c:ores/iron"), + List.of(Items.IRON_ORE.builtInRegistryHolder()), + ResourceLocation.parse("c:ingots/iron"), + List.of(Items.IRON_INGOT.builtInRegistryHolder()))); + try { + direct(h, tags, Map.of("energy", Long.toString(Long.MAX_VALUE), + "hardness-level-ratio", "1")); + throw new AssertionError("Accepted energy overflow"); + } catch (IllegalArgumentException expected) { + require(expected.getMessage().contains("Scaled energy"), + "Overflow fails instead of wrapping"); + } + for (var options : List.of( + Map.of("time", Integer.toString(Integer.MAX_VALUE), + "hardness-level-ratio", ".10"), Map.of("hardness-level-ratio", "1e308"))) { - try { - direct(h, tags, options); - throw new AssertionError("Accepted non-finite or overflowing scaled time"); - } catch (IllegalArgumentException expected) { - require( - expected.getMessage().contains("Scaled time"), - "Reject time overflow and non-finite products"); - } + try { + direct(h, tags, options); + throw new AssertionError( + "Accepted non-finite or overflowing scaled time"); + } catch (IllegalArgumentException expected) { + require(expected.getMessage().contains("Scaled time"), + "Reject time overflow and non-finite products"); + } + } + h.succeed(); } - h.succeed(); - } - private static Map direct( - GameTestHelper h, ReloadTags tags, Map options) { - var result = new TreeMap(); - new OreToIngotGenerator() - .generate( - new RecipeGenerationContext( - MachineContent.recipeType(TYPE), - h.getLevel().registryAccess(), - tags, - List.of(), + private static Map direct(GameTestHelper h, + ReloadTags tags, Map options) { + var result = new TreeMap(); + new OreToIngotGenerator().generate( + new RecipeGenerationContext(MachineContent.recipeType(TYPE), + h.getLevel().registryAccess(), tags, List.of(), new GeneratorProperties(options, "test-generator.xml", 1)), (id, recipe) -> { - require(result.putIfAbsent(id, recipe) == null, "No duplicate generated IDs"); + require(result.putIfAbsent(id, recipe) == null, + "No duplicate generated IDs"); }); - return result; - } + return result; + } - private static ItemStack product(MachineRecipe recipe) { - return ((MachineProduct.ItemProduct) recipe.outputs().get("output").getFirst()).stack(); - } + private static ItemStack product(MachineRecipe recipe) { + return ((MachineProduct.ItemProduct) recipe.outputs().get("output") + .getFirst()).stack(); + } - @GameTest(template = "empty") - public static void recoveryCapsuleRetainsOverflowAndUnknownResources(GameTestHelper h) { - var machine = place(h); - machine.energy.setStored(machine.energy.capacity() - 100); - var entry = new CompoundTag(); - entry.putString("type", ResourceType.ITEMS.id().toString()); - var data = new CompoundTag(); - data.put("item", new ItemStack(Items.IRON_ORE, 2).save(h.getLevel().registryAccess())); - entry.put("data", data); - var unknown = new CompoundTag(); - unknown.putString("type", "example:unknown"); - var unknownData = new CompoundTag(); - unknownData.putInt("amount", 7); - unknown.put("data", unknownData); - var entries = new ListTag(); - entries.add(entry); - entries.add(unknown); - var payload = new CompoundTag(); - payload.put("resources", entries); - payload.putLong("refund", 250); - var capsule = MachineRecoveryItem.create(payload); - require(MachineRecoveryItem.restore(capsule, machine), "Restored compatible resources"); - require(machine.items.getStackInSlot(0).getCount() == 2, "Restored items once"); - var remainder = - capsule.get(DataComponents.CUSTOM_DATA).copyTag().getCompound("mmdlib_recovery"); - require(remainder.getLong("refund") == 150, "Energy overflow retained"); - require( - remainder.getList("resources", Tag.TAG_COMPOUND).getCompound(0).equals(unknown), - "Unknown resource retained"); - require(!MachineRecoveryItem.restore(capsule, machine), "No progress leaves capsule intact"); - machine.energy.setStored(0); - require(MachineRecoveryItem.restore(capsule, machine), "Remaining energy restored"); - require( - machine.energy.stored() == 150 && !capsule.isEmpty(), - "Unknown payload remains recoverable"); - require(machine.items.getStackInSlot(0).getCount() == 2, "No duplicate items on retry"); - h.succeed(); - } + @GameTest(template = "empty") + public static void recoveryCapsuleRetainsOverflowAndUnknownResources( + GameTestHelper h) { + var machine = place(h); + machine.energy.setStored(machine.energy.capacity() - 100); + var entry = new CompoundTag(); + entry.putString("type", ResourceType.ITEMS.id().toString()); + var data = new CompoundTag(); + data.put("item", new ItemStack(Items.IRON_ORE, 2) + .save(h.getLevel().registryAccess())); + entry.put("data", data); + var unknown = new CompoundTag(); + unknown.putString("type", "example:unknown"); + var unknownData = new CompoundTag(); + unknownData.putInt("amount", 7); + unknown.put("data", unknownData); + var entries = new ListTag(); + entries.add(entry); + entries.add(unknown); + var payload = new CompoundTag(); + payload.put("resources", entries); + payload.putLong("refund", 250); + var capsule = MachineRecoveryItem.create(payload); + require(MachineRecoveryItem.restore(capsule, machine), + "Restored compatible resources"); + require(machine.items.getStackInSlot(0).getCount() == 2, + "Restored items once"); + var remainder = capsule.get(DataComponents.CUSTOM_DATA).copyTag() + .getCompound("mmdlib_recovery"); + require(remainder.getLong("refund") == 150, "Energy overflow retained"); + require(remainder.getList("resources", Tag.TAG_COMPOUND).getCompound(0) + .equals(unknown), "Unknown resource retained"); + require(!MachineRecoveryItem.restore(capsule, machine), + "No progress leaves capsule intact"); + machine.energy.setStored(0); + require(MachineRecoveryItem.restore(capsule, machine), + "Remaining energy restored"); + require(machine.energy.stored() == 150 && !capsule.isEmpty(), + "Unknown payload remains recoverable"); + require(machine.items.getStackInSlot(0).getCount() == 2, + "No duplicate items on retry"); + h.succeed(); + } - @GameTest(template = "empty") - public static void resourcePackContainsCompleteCubeAndDropDefaults(GameTestHelper h) { - var client = MachineResourcePack.build(PackType.CLIENT_RESOURCES); - var server = MachineResourcePack.build(PackType.SERVER_DATA); - for (String path : - List.of( - "models/block/simple_crusher.json", + @GameTest(template = "empty") + public static void resourcePackContainsCompleteCubeAndDropDefaults( + GameTestHelper h) { + var client = MachineResourcePack.build(PackType.CLIENT_RESOURCES); + var server = MachineResourcePack.build(PackType.SERVER_DATA); + for (String path : List.of("models/block/simple_crusher.json", "models/item/simple_crusher.json", - "blockstates/simple_crusher.json", - "lang/en_us.json")) - require( - client.containsKey(ResourceLocation.fromNamespaceAndPath("mmdlib_companion", path)), - "Generated client resource " + path); - require( - server.containsKey( - ResourceLocation.parse("mmdlib_companion:loot_table/blocks/simple_crusher.json")), - "Generated loot table"); - require( - server.containsKey(ResourceLocation.parse("minecraft:tags/block/mineable/pickaxe.json")), - "Generated mining tag"); - h.succeed(); - } + "blockstates/simple_crusher.json", "lang/en_us.json")) + require( + client.containsKey(ResourceLocation + .fromNamespaceAndPath("mmdlib_companion", path)), + "Generated client resource " + path); + require( + server.containsKey(ResourceLocation.parse( + "mmdlib_companion:loot_table/blocks/simple_crusher.json")), + "Generated loot table"); + require( + server.containsKey(ResourceLocation + .parse("minecraft:tags/block/mineable/pickaxe.json")), + "Generated mining tag"); + h.succeed(); + } } diff --git a/src/companionTest/java/zone/moddev/mc/companion/test/SimpleLavaFurnaceTests.java b/src/companionTest/java/zone/moddev/mc/companion/test/SimpleLavaFurnaceTests.java index df10bcaa..a97d6c61 100644 --- a/src/companionTest/java/zone/moddev/mc/companion/test/SimpleLavaFurnaceTests.java +++ b/src/companionTest/java/zone/moddev/mc/companion/test/SimpleLavaFurnaceTests.java @@ -51,7 +51,8 @@ public final class SimpleLavaFurnaceTests { private static DefinedMachineEntity place(GameTestHelper h) { var pos = h.absolutePos(new BlockPos(2, 2, 2)); - h.getLevel().setBlock(pos, BuiltInRegistries.BLOCK.get(ID).defaultBlockState(), 3); + h.getLevel().setBlock(pos, + BuiltInRegistries.BLOCK.get(ID).defaultBlockState(), 3); return (DefinedMachineEntity) h.getLevel().getBlockEntity(pos); } @@ -61,50 +62,53 @@ private static void require(boolean value, String message) { } @GameTest(template = "empty") - public static void tankTooltipDescribesContentsAndEachViewersContainer(GameTestHelper h) { + public static void tankTooltipDescribesContentsAndEachViewersContainer( + GameTestHelper h) { var machine = place(h); - var gauge = Screens.get(ID).orElseThrow().screen().find("tank_0").orElseThrow(); + var gauge = Screens.get(ID).orElseThrow().screen().find("tank_0") + .orElseThrow(); var displayFluid = new FluidStack(Fluids.LAVA, 250); - require(gauge.displayValue(displayFluid).equals(250), "Tank owns fluid display conversion"); - require(gauge.fillRatio(displayFluid, 1000) == .25, "Tank owns fluid fill ratio"); + require(gauge.displayValue(displayFluid).equals(250), + "Tank owns fluid display conversion"); + require(gauge.fillRatio(displayFluid, 1000) == .25, + "Tank owns fluid fill ratio"); require( gauge.displayText(displayFluid, false).getString().equals("250"), "Fluid display amount"); - require(displayFluid.getAmount() == 250, "Presentation does not mutate synchronized fluid"); + require(displayFluid.getAmount() == 250, + "Presentation does not mutate synchronized fluid"); var empty = gauge.tooltip(machine, ItemStack.EMPTY); - require(empty.size() == 3 && empty.getFirst().getString().equals("fuel"), + require( + empty.size() == 3 && empty.getFirst().getString().equals("fuel"), "Group name first"); require( - ((net.minecraft.network.chat.contents.TranslatableContents) empty.get(1).getContents()) - .getKey() + ((net.minecraft.network.chat.contents.TranslatableContents) empty + .get(1).getContents()).getKey() .equals("screen.mmdlib.tank.empty"), "Empty tank described"); - machine.fluids.fill(new FluidStack(Fluids.LAVA, 100), FluidAction.EXECUTE); + machine.fluids.fill(new FluidStack(Fluids.LAVA, 100), + FluidAction.EXECUTE); var lava = new ItemStack(Items.LAVA_BUCKET); var water = new ItemStack(Items.WATER_BUCKET); var allowed = gauge.tooltip(machine, lava); var rejected = gauge.tooltip(machine, water); - var contents = (net.minecraft.network.chat.contents.TranslatableContents) allowed.get(1) - .getContents(); - require( - contents.getKey().equals("screen.mmdlib.tank.contents") - && contents.getArgs()[0].equals(100), - "Actual fluid amount"); + var contents = (net.minecraft.network.chat.contents.TranslatableContents) allowed + .get(1).getContents(); + require(contents.getKey().equals("screen.mmdlib.tank.contents") + && contents.getArgs()[0].equals(100), "Actual fluid amount"); require( - allowed.getLast().getStyle().getColor().getValue() == net.minecraft.ChatFormatting.BLUE - .getColor(), + allowed.getLast().getStyle().getColor() + .getValue() == net.minecraft.ChatFormatting.BLUE.getColor(), "Accepted container is blue"); require( - rejected.getLast().getStyle().getColor().getValue() == net.minecraft.ChatFormatting.RED - .getColor(), + rejected.getLast().getStyle().getColor() + .getValue() == net.minecraft.ChatFormatting.RED.getColor(), "Rejected container is red"); require( - lava.is(Items.LAVA_BUCKET) - && water.is(Items.WATER_BUCKET) + lava.is(Items.LAVA_BUCKET) && water.is(Items.WATER_BUCKET) && machine.fluids.getFluidInTank(0).getAmount() == 100, "Tooltip never transfers contents"); - require( - gauge.tooltip(machine, new ItemStack(Items.BUCKET)).size() == 3, + require(gauge.tooltip(machine, new ItemStack(Items.BUCKET)).size() == 3, "Empty containers add no misleading fluid line"); var portable = new ItemStack(Items.GLASS_BOTTLE); portable.set(DataComponents.CUSTOM_NAME, @@ -115,58 +119,54 @@ public static void tankTooltipDescribesContentsAndEachViewersContainer(GameTestH var tankLine = gauge.tooltip(machine, portable).getLast(); var tankText = (net.minecraft.network.chat.contents.TranslatableContents) tankLine .getContents(); - require( - tankText.getKey().equals("screen.mmdlib.tank.container_rejects") - && ((net.minecraft.network.chat.Component) tankText.getArgs()[1]) - .getString() - .equals("Tank"), + require(tankText.getKey().equals("screen.mmdlib.tank.container_rejects") + && ((net.minecraft.network.chat.Component) tankText.getArgs()[1]) + .getString().equals("Tank"), "Modded containers use their own display name"); require( net.neoforged.neoforge.fluids.FluidUtil.getFluidHandler(portable) - .orElseThrow() - .getFluidInTank(0) - .getAmount() == 350, + .orElseThrow().getFluidInTank(0).getAmount() == 350, "Portable tank remains unchanged"); var player = h.makeMockServerPlayerInLevel(); - var first = new zone.moddev.mc.lib.gui.runtime.MachineMenu(81, player.getInventory(), - machine); - var second = new zone.moddev.mc.lib.gui.runtime.MachineMenu(82, player.getInventory(), - machine); + var first = new zone.moddev.mc.lib.gui.runtime.MachineMenu(81, + player.getInventory(), machine); + var second = new zone.moddev.mc.lib.gui.runtime.MachineMenu(82, + player.getInventory(), machine); first.setCarried(lava); second.setCarried(water); first.components().snapshot(); second.components().snapshot(); - var firstTips = first - .components() - .requireCapability(zone.moddev.mc.lib.gui.runtime.component.ServerTooltips.class); - var secondTips = second - .components() - .requireCapability(zone.moddev.mc.lib.gui.runtime.component.ServerTooltips.class); + var firstTips = first.components().requireCapability( + zone.moddev.mc.lib.gui.runtime.component.ServerTooltips.class); + var secondTips = second.components().requireCapability( + zone.moddev.mc.lib.gui.runtime.component.ServerTooltips.class); require( - firstTips.lines(gauge).equals(allowed) && secondTips.lines(gauge).equals(rejected), + firstTips.lines(gauge).equals(allowed) + && secondTips.lines(gauge).equals(rejected), "Viewer-specific cursor is not shared between menus"); - ((net.minecraft.network.chat.MutableComponent) firstTips.lines(gauge).getFirst()) - .withStyle(net.minecraft.ChatFormatting.OBFUSCATED); - require( - firstTips.lines(gauge).equals(allowed), + ((net.minecraft.network.chat.MutableComponent) firstTips.lines(gauge) + .getFirst()).withStyle(net.minecraft.ChatFormatting.OBFUSCATED); + require(firstTips.lines(gauge).equals(allowed), "Reading tooltip rows cannot mutate synchronized state"); first.setCarried(ItemStack.EMPTY); first.components().snapshot(); require( - firstTips.lines(gauge).size() == 3 && secondTips.lines(gauge).equals(rejected), + firstTips.lines(gauge).size() == 3 + && secondTips.lines(gauge).equals(rejected), "Removing cursor clears acceptance line"); h.succeed(); } @GameTest(template = "empty") - public static void generatedGaugeFillsAndManuallyDrainsOnlyUnreservedFluid(GameTestHelper h) { + public static void generatedGaugeFillsAndManuallyDrainsOnlyUnreservedFluid( + GameTestHelper h) { var machine = place(h); var player = h.makeMockServerPlayerInLevel(); var pos = machine.getBlockPos(); player.setPos(pos.getX() + .5, pos.getY(), pos.getZ() + .5); player.getAbilities().instabuild = false; - var menu = new zone.moddev.mc.lib.gui.runtime.MachineMenu(47, player.getInventory(), - machine) { + var menu = new zone.moddev.mc.lib.gui.runtime.MachineMenu(47, + player.getInventory(), machine) { @Override public void broadcastChanges() { } @@ -174,16 +174,20 @@ public void broadcastChanges() { player.containerMenu = menu; var gauge = menu.bound.screen().find("tank_0").orElseThrow(); int index = menu.index(gauge); - var left = new zone.moddev.mc.lib.gui.runtime.ScreenNetwork.Action(47, index, 0, 0, 0, 0); - var right = new zone.moddev.mc.lib.gui.runtime.ScreenNetwork.Action(47, index, 0, 1, 0, 0); + var left = new zone.moddev.mc.lib.gui.runtime.ScreenNetwork.Action(47, + index, 0, 0, 0, 0); + var right = new zone.moddev.mc.lib.gui.runtime.ScreenNetwork.Action(47, + index, 0, 1, 0, 0); menu.setCarried(new ItemStack(Items.WATER_BUCKET)); menu.action(player, left); require( - machine.fluids.getFluidInTank(0).isEmpty() && menu.getCarried().is(Items.WATER_BUCKET), + machine.fluids.getFluidInTank(0).isEmpty() + && menu.getCarried().is(Items.WATER_BUCKET), "Recipe filter rejects wrong fluid"); menu.setCarried(new ItemStack(Items.LAVA_BUCKET)); menu.action(player, right); - require(machine.fluids.getFluidInTank(0).isEmpty(), "Right click never pours into tank"); + require(machine.fluids.getFluidInTank(0).isEmpty(), + "Right click never pours into tank"); menu.action(player, left); require( machine.fluids.getFluidInTank(0).getAmount() == 1000 @@ -192,19 +196,20 @@ public void broadcastChanges() { menu.action(player, left); require(machine.fluids.getFluidInTank(0).getAmount() == 1000, "Left click never drains tank"); - machine.fluids.setPlan( - 0, - new zone.moddev.mc.lib.machine.inventory.TankPlan( - 100, zone.moddev.mc.lib.machine.ingredient.FluidAmount.EMPTY)); + machine.fluids.setPlan(0, + new zone.moddev.mc.lib.machine.inventory.TankPlan(100, + zone.moddev.mc.lib.machine.ingredient.FluidAmount.EMPTY)); menu.action(player, right); require( machine.fluids.getFluidInTank(0).getAmount() == 1000 && menu.getCarried().is(Items.BUCKET), "Bucket cannot take reserved fluid"); - machine.fluids.setPlan(0, zone.moddev.mc.lib.machine.inventory.TankPlan.NONE); + machine.fluids.setPlan(0, + zone.moddev.mc.lib.machine.inventory.TankPlan.NONE); menu.action(player, right); require( - machine.fluids.getFluidInTank(0).isEmpty() && menu.getCarried().is(Items.LAVA_BUCKET), + machine.fluids.getFluidInTank(0).isEmpty() + && menu.getCarried().is(Items.LAVA_BUCKET), "Input tank allows manual bucket extraction"); player.getAbilities().instabuild = true; menu.action(player, left); @@ -218,46 +223,43 @@ public void broadcastChanges() { @GameTest(template = "empty") public static void smeltsWithLavaAndResumesReservedCycle(GameTestHelper h) { var machine = place(h); + require(machine.getClass() == DefinedMachineEntity.class + && machine.energy == null, "Shared runtime without FE storage"); require( - machine.getClass() == DefinedMachineEntity.class && machine.energy == null, - "Shared runtime without FE storage"); - require( - h.getLevel() - .getCapability( - Capabilities.EnergyStorage.BLOCK, machine.getBlockPos(), - Direction.NORTH) == null, + h.getLevel().getCapability(Capabilities.EnergyStorage.BLOCK, + machine.getBlockPos(), Direction.NORTH) == null, "No FE capability"); - var fluid = h.getLevel() - .getCapability(Capabilities.FluidHandler.BLOCK, machine.getBlockPos(), Direction.NORTH); - require(fluid != null && fluid.getTankCapacity(0) == 8000, "Declared fluid capability"); - require( - fluid.fill(new FluidStack(Fluids.WATER, 1000), FluidAction.EXECUTE) == 0, - "Rejects water"); - require( - fluid.fill(new FluidStack(Fluids.LAVA, 1000), FluidAction.EXECUTE) == 1000, - "Accepts lava"); - require( - fluid.drain(1000, FluidAction.EXECUTE).isEmpty(), + var fluid = h.getLevel().getCapability(Capabilities.FluidHandler.BLOCK, + machine.getBlockPos(), Direction.NORTH); + require(fluid != null && fluid.getTankCapacity(0) == 8000, + "Declared fluid capability"); + require(fluid.fill(new FluidStack(Fluids.WATER, 1000), + FluidAction.EXECUTE) == 0, "Rejects water"); + require(fluid.fill(new FluidStack(Fluids.LAVA, 1000), + FluidAction.EXECUTE) == 1000, "Accepts lava"); + require(fluid.drain(1000, FluidAction.EXECUTE).isEmpty(), "Input tank refuses external extraction"); machine.items.insertItem(0, new ItemStack(Items.RAW_IRON), false); for (int i = 0; i < 26; i++) machine.serverTick(); - require(machine.getProgress() == 25 && machine.getTime() == 200, "Preserves smelting time"); + require(machine.getProgress() == 25 && machine.getTime() == 200, + "Preserves smelting time"); var saved = machine.saveWithoutMetadata(h.getLevel().registryAccess()); machine.loadWithComponents(saved, h.getLevel().registryAccess()); for (int i = 0; i < 175; i++) machine.serverTick(); - require(machine.items.getStackInSlot(0).isEmpty(), "Consumed input once"); + require(machine.items.getStackInSlot(0).isEmpty(), + "Consumed input once"); require( machine.items.getStackInSlot(1).is(Items.IRON_INGOT) && machine.items.getStackInSlot(1).getCount() == 1, "Preserves original smelting yield"); - require( - machine.fluids.getFluidInTank(0).getAmount() == 900, + require(machine.fluids.getFluidInTank(0).getAmount() == 900, "Consumes 100 mB once across save/load"); var screen = Screens.get(ID).orElseThrow().screen(); require( - screen.find("tank_0").isPresent() && screen.find("energy").isEmpty(), + screen.find("tank_0").isPresent() + && screen.find("energy").isEmpty(), "Generated tank gauge without FE gauge"); h.succeed(); } @@ -269,24 +271,29 @@ public static void noFuelBlockedOutputAndPauseKeepInputs(GameTestHelper h) { for (int i = 0; i < 10; i++) machine.serverTick(); require( - machine.getProgress() == 0 && machine.items.getStackInSlot(0).getCount() == 1, + machine.getProgress() == 0 + && machine.items.getStackInSlot(0).getCount() == 1, "No fuel leaves input intact"); - machine.fluids.fill(new FluidStack(Fluids.LAVA, 1000), FluidAction.EXECUTE); + machine.fluids.fill(new FluidStack(Fluids.LAVA, 1000), + FluidAction.EXECUTE); machine.items.setStackInSlot(1, new ItemStack(Items.GOLD_INGOT, 64)); for (int i = 0; i < 10; i++) machine.serverTick(); require( - machine.getProgress() == 0 && machine.fluids.getFluidInTank(0).getAmount() == 1000, + machine.getProgress() == 0 + && machine.fluids.getFluidInTank(0).getAmount() == 1000, "Blocked output consumes no lava"); machine.items.setStackInSlot(1, ItemStack.EMPTY); for (int i = 0; i < 21; i++) machine.serverTick(); - var processor = machine.features.processor(Optional.empty()).orElseThrow(); + var processor = machine.features.processor(Optional.empty()) + .orElseThrow(); processor.pause(); for (int i = 0; i < 10; i++) machine.serverTick(); require( - machine.getProgress() == 20 && machine.fluids.getFluidInTank(0).getAmount() == 1000, + machine.getProgress() == 20 + && machine.fluids.getFluidInTank(0).getAmount() == 1000, "Pause releases reservation without consumption"); processor.resume(); for (int i = 0; i < 181; i++) @@ -299,11 +306,14 @@ public static void noFuelBlockedOutputAndPauseKeepInputs(GameTestHelper h) { } @GameTest(template = "empty") - public static void recoveryRestoresFluidAndRetainsEnergyWithoutBuffer(GameTestHelper h) { + public static void recoveryRestoresFluidAndRetainsEnergyWithoutBuffer( + GameTestHelper h) { var machine = place(h); - machine.fluids.fill(new FluidStack(Fluids.LAVA, 7950), FluidAction.EXECUTE); + machine.fluids.fill(new FluidStack(Fluids.LAVA, 7950), + FluidAction.EXECUTE); var data = new CompoundTag(); - data.put("fluid", new FluidStack(Fluids.LAVA, 200).save(h.getLevel().registryAccess())); + data.put("fluid", new FluidStack(Fluids.LAVA, 200) + .save(h.getLevel().registryAccess())); var entry = new CompoundTag(); entry.putString("type", ResourceType.FLUIDS.id().toString()); entry.put("data", data); @@ -313,63 +323,48 @@ public static void recoveryRestoresFluidAndRetainsEnergyWithoutBuffer(GameTestHe payload.put("resources", entries); payload.putLong("refund", 100); var capsule = MachineRecoveryItem.create(payload); - require(MachineRecoveryItem.restore(capsule, machine), "Restores available capacity"); - require(machine.fluids.getFluidInTank(0).getAmount() == 8000, "Filled available space"); + require(MachineRecoveryItem.restore(capsule, machine), + "Restores available capacity"); + require(machine.fluids.getFluidInTank(0).getAmount() == 8000, + "Filled available space"); var remainder = capsule.get(DataComponents.CUSTOM_DATA).copyTag() .getCompound("mmdlib_recovery"); - require(remainder.getLong("refund") == 100, "No power storage preserves energy refund"); - require( - FluidStack.parseOptional( - h.getLevel().registryAccess(), - remainder - .getList("resources", Tag.TAG_COMPOUND) - .getCompound(0) - .getCompound("data") - .getCompound("fluid")) - .getAmount() == 150, - "Fluid overflow retained"); - require( - !MachineRecoveryItem.restore(capsule, machine), + require(remainder.getLong("refund") == 100, + "No power storage preserves energy refund"); + require(FluidStack + .parseOptional(h.getLevel().registryAccess(), + remainder.getList("resources", Tag.TAG_COMPOUND).getCompound(0) + .getCompound("data").getCompound("fluid")) + .getAmount() == 150, "Fluid overflow retained"); + require(!MachineRecoveryItem.restore(capsule, machine), "Repeated restore cannot duplicate fluids"); h.succeed(); } @GameTest(template = "empty") - public static void bucketsFillAndInvalidSavedFluidIsPreservedForRecovery(GameTestHelper h) { + public static void bucketsFillAndInvalidSavedFluidIsPreservedForRecovery( + GameTestHelper h) { var machine = place(h); var player = h.makeMockServerPlayerInLevel(); player.getAbilities().instabuild = false; var hit = new net.minecraft.world.phys.BlockHitResult( net.minecraft.world.phys.Vec3.atCenterOf(machine.getBlockPos()), - Direction.UP, - machine.getBlockPos(), - false); - player.setItemInHand( - net.minecraft.world.InteractionHand.MAIN_HAND, new ItemStack(Items.LAVA_BUCKET)); - var result = machine - .getBlockState() - .useItemOn( - player.getMainHandItem(), - h.getLevel(), - player, - net.minecraft.world.InteractionHand.MAIN_HAND, - hit); + Direction.UP, machine.getBlockPos(), false); + player.setItemInHand(net.minecraft.world.InteractionHand.MAIN_HAND, + new ItemStack(Items.LAVA_BUCKET)); + var result = machine.getBlockState().useItemOn(player.getMainHandItem(), + h.getLevel(), player, net.minecraft.world.InteractionHand.MAIN_HAND, + hit); require( result == net.minecraft.world.ItemInteractionResult.CONSUME && player.getMainHandItem().is(Items.BUCKET) && machine.fluids.getFluidInTank(0).getAmount() == 1000, "Bucket fills tank and returns empty bucket"); - player.setItemInHand( - net.minecraft.world.InteractionHand.MAIN_HAND, new ItemStack(Items.WATER_BUCKET)); - require( - machine - .getBlockState() - .useItemOn( - player.getMainHandItem(), - h.getLevel(), - player, - net.minecraft.world.InteractionHand.MAIN_HAND, - hit) == net.minecraft.world.ItemInteractionResult.FAIL, + player.setItemInHand(net.minecraft.world.InteractionHand.MAIN_HAND, + new ItemStack(Items.WATER_BUCKET)); + require(machine.getBlockState().useItemOn(player.getMainHandItem(), + h.getLevel(), player, net.minecraft.world.InteractionHand.MAIN_HAND, + hit) == net.minecraft.world.ItemInteractionResult.FAIL, "Wrong fluid blocks vanilla bucket placement"); require( player.getMainHandItem().is(Items.WATER_BUCKET) @@ -390,45 +385,38 @@ public static void oreSubclassTransformsBeforeEmission(GameTestHelper h) { var ore = BuiltInRegistries.ITEM.wrapAsHolder(Items.IRON_ORE); var ingot = BuiltInRegistries.ITEM.wrapAsHolder(Items.IRON_INGOT); var tags = new ReloadTags( - Map.of( - ResourceLocation.parse("c:ores/iron"), - List.of(ore), - ResourceLocation.parse("c:ingots/iron"), - List.of(ingot))); + Map.of(ResourceLocation.parse("c:ores/iron"), List.of(ore), + ResourceLocation.parse("c:ingots/iron"), List.of(ingot))); var context = new RecipeGenerationContext( - MachineContent.recipeType(TYPE), - h.getLevel().registryAccess(), - tags, - List.of(), - new GeneratorProperties(Map.of("energy-per-mb", "40"), "test.xml", 1)); + MachineContent.recipeType(TYPE), h.getLevel().registryAccess(), + tags, List.of(), new GeneratorProperties( + Map.of("energy-per-mb", "40"), "test.xml", 1)); var transformed = new ArrayList(); - new LavaOres().generate(context, (id, recipe) -> transformed.add(recipe)); + new LavaOres().generate(context, + (id, recipe) -> transformed.add(recipe)); var result = transformed.getFirst(); - require( - result.time() == 100 && !result.inputs().containsKey("energy"), + require(result.time() == 100 && !result.inputs().containsKey("energy"), "Subclass removes FE and preserves ore time"); + require(((MachineIngredient.FluidPart) result.inputs().get("fuel") + .getFirst()).amount() == 50, "Ore energy converted to lava"); require( - ((MachineIngredient.FluidPart) result.inputs().get("fuel").getFirst()).amount() == 50, - "Ore energy converted to lava"); - require( - ((MachineProduct.ItemProduct) result.outputs().get("output").getFirst()).stack() - .getCount() == 2, + ((MachineProduct.ItemProduct) result.outputs().get("output") + .getFirst()).stack().getCount() == 2, "Ore generator still doubles output"); var mixed = new MachineRecipe( - Map.of( - "input", - List.of( - new MachineIngredient.EnergyPart(41), + Map.of("input", + List.of(new MachineIngredient.EnergyPart(41), new MachineIngredient.ItemPart( - net.minecraft.world.item.crafting.Ingredient.of(Items.RAW_IRON), 1)), - "other", - List.of(new MachineIngredient.EnergyPart(40))), - result.outputs(), - 17); - var converted = RecipeTransforms.energyToFluid( - mixed, "fuel", HolderSet.direct(Fluids.LAVA.builtInRegistryHolder()), 40); - require( - ((MachineIngredient.FluidPart) converted.inputs().get("fuel").getFirst()).amount() == 3 + net.minecraft.world.item.crafting.Ingredient + .of(Items.RAW_IRON), + 1)), + "other", List.of(new MachineIngredient.EnergyPart(40))), + result.outputs(), 17); + var converted = RecipeTransforms.energyToFluid(mixed, "fuel", + HolderSet.direct(Fluids.LAVA.builtInRegistryHolder()), 40); + require( + ((MachineIngredient.FluidPart) converted.inputs().get("fuel") + .getFirst()).amount() == 3 && !converted.inputs().containsKey("other"), "All energy groups summed and rounded up"); require( @@ -441,20 +429,20 @@ public static void oreSubclassTransformsBeforeEmission(GameTestHelper h) { public static final class LavaOres extends OreToIngotGenerator { @Override - public void generate(RecipeGenerationContext ctx, MachineRecipeOutput output) { - long rate = ctx.properties().longValue("energy-per-mb", 40, 1, Long.MAX_VALUE); + public void generate(RecipeGenerationContext ctx, + MachineRecipeOutput output) { + long rate = ctx.properties().longValue("energy-per-mb", 40, 1, + Long.MAX_VALUE); super.generate( ctx.withProperties(ctx.properties().without("energy-per-mb")), - (id, recipe) -> output.accept( - id, - RecipeTransforms.energyToFluid( - recipe, - "fuel", - HolderSet.direct( - ctx.registries() - .lookupOrThrow(net.minecraft.core.registries.Registries.FLUID) - .getOrThrow(Fluids.LAVA.builtInRegistryHolder().key())), - rate))); + (id, recipe) -> output + .accept(id, + RecipeTransforms.energyToFluid(recipe, "fuel", + HolderSet.direct(ctx.registries().lookupOrThrow( + net.minecraft.core.registries.Registries.FLUID) + .getOrThrow( + Fluids.LAVA.builtInRegistryHolder().key())), + rate))); } } } diff --git a/src/companionTest/java/zone/moddev/mc/companion/test/SlotPolicyClientSmoke.java b/src/companionTest/java/zone/moddev/mc/companion/test/SlotPolicyClientSmoke.java index 64366068..c3a9519d 100644 --- a/src/companionTest/java/zone/moddev/mc/companion/test/SlotPolicyClientSmoke.java +++ b/src/companionTest/java/zone/moddev/mc/companion/test/SlotPolicyClientSmoke.java @@ -21,7 +21,10 @@ import zone.moddev.mc.lib.gui.runtime.slot.SlotAdapter; import zone.moddev.mc.lib.machine.inventory.SlotPlan; -/** Real native drawing verifies policy opt-in/out independently of reservation support and role. */ +/** + * Real native drawing verifies policy opt-in/out independently of reservation + * support and role. + */ final class SlotPolicyClientSmoke { private static int phase, frames; private static MenuSlots.Entry[] entries; @@ -35,11 +38,8 @@ private static class Policy extends ItemSlotElement { final int fixture; Policy(int fixture) { - super( - Common.empty(), - fixture, - SlotKind.NORMAL, - Appearance.standard(SlotSize.NORMAL, Optional.empty(), Optional.empty())); + super(Common.empty(), fixture, SlotKind.NORMAL, Appearance + .standard(SlotSize.NORMAL, Optional.empty(), Optional.empty())); this.fixture = fixture; } @@ -70,8 +70,8 @@ public boolean reservationOverlay(MenuSlots.Entry entry) { return fixture != 1; } - public void renderOverlay( - OverlayPainter painter, ItemStack displayed, SlotPlan plan, String count) { + public void renderOverlay(OverlayPainter painter, ItemStack displayed, + SlotPlan plan, String count) { overlays[fixture]++; super.renderOverlay(painter, displayed, plan, count); if (fixture == 2) @@ -81,7 +81,8 @@ public void renderOverlay( private static class Planned extends Slot implements SlotAdapter { Planned(int x) { - super(new SimpleContainer(new ItemStack(Items.DIAMOND, 5)), 0, x, 24); + super(new SimpleContainer(new ItemStack(Items.DIAMOND, 5)), 0, x, + 24); } public SlotPlan plan() { @@ -92,61 +93,60 @@ public SlotPlan plan() { static boolean run() { if (phase == 0) { entries = new MenuSlots.Entry[]{ - new MenuSlots.Entry(new Planned(12), new Policy(0), MenuSlots.Role.AUXILIARY), - new MenuSlots.Entry(new Planned(72), new Policy(1), MenuSlots.Role.MACHINE), - new MenuSlots.Entry( - new Slot(new SimpleContainer(new ItemStack(Items.EMERALD, 3)), 0, 132, 24), - new Policy(2), - MenuSlots.Role.EQUIPMENT) - }; + new MenuSlots.Entry(new Planned(12), new Policy(0), + MenuSlots.Role.AUXILIARY), + new MenuSlots.Entry(new Planned(72), new Policy(1), + MenuSlots.Role.MACHINE), + new MenuSlots.Entry(new Slot( + new SimpleContainer(new ItemStack(Items.EMERALD, 3)), 0, + 132, 24), new Policy(2), MenuSlots.Role.EQUIPMENT)}; var slots = new MenuSlots(); - var registrar = slots.registrar(slot -> slot.index = slots.entries().size()); + var registrar = slots + .registrar(slot -> slot.index = slots.entries().size()); for (var entry : entries) registrar.add(entry.slot(), entry.element(), entry.role()); for (var entry : entries) { var slot = entry.slot(); var vanilla = new Rect(slot.x, slot.y, 16, 16); require( - renderer - .hitBounds(slots, slot.x, slot.y, 16, 16) + renderer.hitBounds(slots, slot.x, slot.y, 16, 16) .equals(entry.element().hitBounds(vanilla)), "Native hit bridge honors element geometry"); } require( - renderer.hitBounds(slots, 12, 24, 40, 9).equals(new Rect(12, 24, 40, 9)), + renderer.hitBounds(slots, 12, 24, 40, 9) + .equals(new Rect(12, 24, 40, 9)), "Non-slot hit tests retain vanilla bounds"); - var ghost = new GhostSlotElement(Common.empty(), 0, entries[0].element().appearance()); - renderer.render( - null, - entries[0].slot(), - new MenuSlots.Entry(entries[0].slot(), ghost, MenuSlots.Role.MACHINE), - () -> { - throw new AssertionError("Default ghost draws native contents"); + var ghost = new GhostSlotElement(Common.empty(), 0, + entries[0].element().appearance()); + renderer.render(null, entries[0].slot(), new MenuSlots.Entry( + entries[0].slot(), ghost, MenuSlots.Role.MACHINE), () -> { + throw new AssertionError( + "Default ghost draws native contents"); }); - var customGhost = new GhostSlotElement(Common.empty(), 0, ghost.appearance()) { + var customGhost = new GhostSlotElement(Common.empty(), 0, + ghost.appearance()) { public boolean renderNative() { return true; } }; int[] draws = {0}; - renderer.render( - null, - entries[0].slot(), - new MenuSlots.Entry(entries[0].slot(), customGhost, MenuSlots.Role.MACHINE), + renderer.render(null, entries[0].slot(), + new MenuSlots.Entry(entries[0].slot(), customGhost, + MenuSlots.Role.MACHINE), () -> draws[0]++); - require(draws[0] == 1, "Ghost subclasses can opt into native drawing"); + require(draws[0] == 1, + "Ghost subclasses can opt into native drawing"); phase++; return false; } if (phase == 1) { if (frames == 0) return false; - require( - overlays[0] > 0 && overlays[1] == 0 && overlays[2] > 0, + require(overlays[0] > 0 && overlays[1] == 0 && overlays[2] > 0, "Slot overlay policy controls actual rendering"); require( - "".equals(vanillaCounts[0]) - && "5".equals(vanillaCounts[1]) + "".equals(vanillaCounts[0]) && "5".equals(vanillaCounts[1]) && "".equals(vanillaCounts[2]), "Disabled overlays retain vanilla quantity rendering"); require( @@ -155,9 +155,8 @@ public boolean renderNative() { require(highlights[1] > 0 && highlights[2] > 0, "Native bridge requests highlight geometry"); var mc = Minecraft.getInstance(); - Screenshot.grab( - mc.gameDirectory, "slot-overlay-policies.png", mc.getMainRenderTarget(), - message -> { + Screenshot.grab(mc.gameDirectory, "slot-overlay-policies.png", + mc.getMainRenderTarget(), message -> { }); entries = null; phase++; @@ -176,17 +175,12 @@ static void render(GuiGraphics graphics) { var slot = entry.slot(); graphics.fill(slot.x - 2, 8, slot.x + 58, 44, 0xee303030); graphics.drawString(font, labels[i], slot.x - 2, 10, 0xffffff); - renderer.contents( - graphics, - font, - slot, - entry, - slot.getItem(), - Integer.toString(slot.getItem().getCount()), - count -> { + renderer.contents(graphics, font, slot, entry, slot.getItem(), + Integer.toString(slot.getItem().getCount()), count -> { vanillaCounts[index] = count; graphics.renderItem(slot.getItem(), slot.x, slot.y); - graphics.renderItemDecorations(font, slot.getItem(), slot.x, slot.y, count); + graphics.renderItemDecorations(font, slot.getItem(), slot.x, + slot.y, count); }); } graphics.fill(8, 48, 190, 106, 0xee303030); @@ -197,49 +191,37 @@ static void render(GuiGraphics graphics) { var original = entries[i].slot(); var slot = new Slot(original.container, 0, original.x, 72); var entry = new MenuSlots.Entry(slot, element, entries[i].role()); - graphics.drawString(font, nativeLabels[i], slot.x - 2, 54, 0xffffff); + graphics.drawString(font, nativeLabels[i], slot.x - 2, 54, + 0xffffff); var before = new org.joml.Matrix4f(graphics.pose().last().pose()); var target = element.nativeBounds(new Rect(slot.x, slot.y, 16, 16)); - renderer.render( - graphics, - slot, - entry, - () -> { - nativeDraws[index]++; - var actual = graphics - .pose() - .last() - .pose() - .transformPosition(slot.x, slot.y, 0, new org.joml.Vector3f()); - var expected = before.transformPosition(target.x(), target.y(), 0, - new org.joml.Vector3f()); - require(actual.distance(expected) < .01f, - "Native drawing applies element offset"); - var edge = graphics - .pose() - .last() - .pose() - .transformPosition(slot.x + 16, slot.y + 16, 0, new org.joml.Vector3f()); - var expectedEdge = before.transformPosition( - target.right(), target.bottom(), 0, new org.joml.Vector3f()); - require( - edge.distance(expectedEdge) < .01f, - "Native drawing applies independent width/height"); - graphics.renderItem(slot.getItem(), slot.x, slot.y); - }); - require(before.equals(graphics.pose().last().pose()), "Native renderer restores pose"); + renderer.render(graphics, slot, entry, () -> { + nativeDraws[index]++; + var actual = graphics.pose().last().pose().transformPosition( + slot.x, slot.y, 0, new org.joml.Vector3f()); + var expected = before.transformPosition(target.x(), target.y(), + 0, new org.joml.Vector3f()); + require(actual.distance(expected) < .01f, + "Native drawing applies element offset"); + var edge = graphics.pose().last().pose().transformPosition( + slot.x + 16, slot.y + 16, 0, new org.joml.Vector3f()); + var expectedEdge = before.transformPosition(target.right(), + target.bottom(), 0, new org.joml.Vector3f()); + require(edge.distance(expectedEdge) < .01f, + "Native drawing applies independent width/height"); + graphics.renderItem(slot.getItem(), slot.x, slot.y); + }); + require(before.equals(graphics.pose().last().pose()), + "Native renderer restores pose"); if (i != 0) - renderer.highlight( - graphics, - slot, - entry, - 0x66ffffff, - () -> { - throw new AssertionError("Custom highlight delegated vanilla geometry"); - }); + renderer.highlight(graphics, slot, entry, 0x66ffffff, () -> { + throw new AssertionError( + "Custom highlight delegated vanilla geometry"); + }); if (i == 2) { var hit = element.hitBounds(new Rect(slot.x, slot.y, 16, 16)); - graphics.renderOutline(hit.x(), hit.y(), hit.width(), hit.height(), 0xff00ff00); + graphics.renderOutline(hit.x(), hit.y(), hit.width(), + hit.height(), 0xff00ff00); } } frames++; diff --git a/src/companionTest/java/zone/moddev/mc/companion/test/SlotTooltipClientSmoke.java b/src/companionTest/java/zone/moddev/mc/companion/test/SlotTooltipClientSmoke.java index a3a33dfc..b678f8bc 100644 --- a/src/companionTest/java/zone/moddev/mc/companion/test/SlotTooltipClientSmoke.java +++ b/src/companionTest/java/zone/moddev/mc/companion/test/SlotTooltipClientSmoke.java @@ -10,7 +10,10 @@ import zone.moddev.mc.lib.gui.client.MachineScreen; import zone.moddev.mc.lib.gui.client.NativeSlotRenderer; -/** Exercises the real screen's vanilla tooltip bridge for occupied and empty reserved slots. */ +/** + * Exercises the real screen's vanilla tooltip bridge for occupied and empty + * reserved slots. + */ final class SlotTooltipClientSmoke { private SlotTooltipClientSmoke() { } @@ -25,19 +28,13 @@ static void check(MachineScreen screen, int index, boolean filled) { bridge.usesItemTooltip(menu.nativeSlots(), slot, element) == filled, "Only filled slots route their lines through vanilla's tooltip"); var other = menu.nativeSlots().entry(index == 0 ? 1 : 0).element(); - require( - !bridge.usesItemTooltip(menu.nativeSlots(), slot, other), + require(!bridge.usesItemTooltip(menu.nativeSlots(), slot, other), "A different element must keep its own tooltip even over a filled slot"); List vanilla = List.of(Component.literal("vanilla")); - require( - bridge.itemTooltip( - menu.nativeSlots(), - slot, - other, - vanilla, - ignored -> { - throw new AssertionError("Unrelated tooltip was evaluated"); - }) == vanilla, + require(bridge.itemTooltip(menu.nativeSlots(), slot, other, vanilla, + ignored -> { + throw new AssertionError("Unrelated tooltip was evaluated"); + }) == vanilla, "Unrelated elements leave the native item tooltip alone"); require(!bridge.usesItemTooltip(menu.nativeSlots(), null, element), "No hovered native slot"); @@ -52,46 +49,51 @@ static void check(MachineScreen screen, int index, boolean filled) { var expected = screen.tooltipLines(element, x, y); require(!expected.isEmpty(), "Reserved slot has element tooltip lines"); if (!filled) - return; // Empty slots render those lines as the element tooltip above. + return; // Empty slots render those lines as the element tooltip + // above. policies(expected); try { - var method = MachineScreen.class.getDeclaredMethod("getTooltipFromContainerItem", - ItemStack.class); + var method = MachineScreen.class.getDeclaredMethod( + "getTooltipFromContainerItem", ItemStack.class); method.setAccessible(true); @SuppressWarnings("unchecked") - var actual = (List) method.invoke(screen, slot.getItem()); - require(actual.size() > expected.size(), "Vanilla item tooltip is retained"); + var actual = (List) method.invoke(screen, + slot.getItem()); + require(actual.size() > expected.size(), + "Vanilla item tooltip is retained"); require( - actual.subList(actual.size() - expected.size(), actual.size()).equals(expected), + actual.subList(actual.size() - expected.size(), actual.size()) + .equals(expected), "Slot lines append to the actual screen's item tooltip"); for (var line : expected) - require( - actual.stream().filter(line::equals).count() == 1, + require(actual.stream().filter(line::equals).count() == 1, "Slot tooltip line appears once"); } catch (ReflectiveOperationException failure) { - throw new AssertionError("Cannot inspect native item tooltip", failure); + throw new AssertionError("Cannot inspect native item tooltip", + failure); } } private static void policies(List parents) { if (Boolean.getBoolean("mmdlib.tooltipSmokeOnly")) require( - parents.stream().anyMatch(row -> row.getString().equals("server outer")), + parents.stream() + .anyMatch(row -> row.getString().equals("server outer")), "Real ancestor rows reach the native bridge"); for (int policy = 0; policy < 3; policy++) { final int mode = policy; var element = new zone.moddev.mc.lib.gui.model.elements.ItemSlotElement( - zone.moddev.mc.lib.gui.model.Common.empty(), - 0, + zone.moddev.mc.lib.gui.model.Common.empty(), 0, zone.moddev.mc.lib.gui.model.SlotKind.NORMAL, - zone.moddev.mc.lib.gui.model.elements.SlotElement.Appearance.standard( - zone.moddev.mc.lib.gui.model.SlotSize.NORMAL, - java.util.Optional.empty(), - java.util.Optional.empty())) { - public List nativeTooltip(ItemStack stack, List vanilla) { + zone.moddev.mc.lib.gui.model.elements.SlotElement.Appearance + .standard(zone.moddev.mc.lib.gui.model.SlotSize.NORMAL, + java.util.Optional.empty(), + java.util.Optional.empty())) { + public List nativeTooltip(ItemStack stack, + List vanilla) { stack.setCount(2); - ((net.minecraft.network.chat.MutableComponent) vanilla.getFirst()) - .append(" edited"); + ((net.minecraft.network.chat.MutableComponent) vanilla + .getFirst()).append(" edited"); return switch (mode) { case 0 -> vanilla; case 1 -> List.of(Component.literal("replacement")); @@ -103,19 +105,16 @@ public List nativeTooltip(ItemStack stack, List vanilla) { var slot = new net.minecraft.world.inventory.Slot( new net.minecraft.world.SimpleContainer( new ItemStack(net.minecraft.world.item.Items.DIAMOND)), - 0, - 0, - 0); - slots.registrar(value -> value.index = 0).add(slot, element, element.role()); + 0, 0, 0); + slots.registrar(value -> value.index = 0).add(slot, element, + element.role()); List vanilla = List.of(Component.literal("vanilla")); - var result = new NativeSlotRenderer().itemTooltip(slots, slot, element, vanilla, - ignored -> parents); + var result = new NativeSlotRenderer().itemTooltip(slots, slot, + element, vanilla, ignored -> parents); int nativeRows = mode == 2 ? 0 : 1; - require( - result.size() == nativeRows + parents.size(), + require(result.size() == nativeRows + parents.size(), "Native tooltip policy controls only its own rows"); - require( - result.subList(nativeRows, result.size()).equals(parents), + require(result.subList(nativeRows, result.size()).equals(parents), "All element and parent rows survive replacement/suppression"); if (nativeRows != 0) require( @@ -123,11 +122,12 @@ public List nativeTooltip(ItemStack stack, List vanilla) { .equals(mode == 0 ? "vanilla edited" : "replacement"), "Native tooltip override applied"); require( - slot.getItem().getCount() == 1 && vanilla.getFirst().getString().equals("vanilla"), + slot.getItem().getCount() == 1 + && vanilla.getFirst().getString().equals("vanilla"), "Native hook receives detached inputs"); - ((net.minecraft.network.chat.MutableComponent) result.getLast()).append(" changed"); - require( - !parents.getLast().getString().endsWith(" changed"), + ((net.minecraft.network.chat.MutableComponent) result.getLast()) + .append(" changed"); + require(!parents.getLast().getString().endsWith(" changed"), "Composed tooltip rows are detached"); } } diff --git a/src/companionTest/java/zone/moddev/mc/companion/test/StatsClientSmoke.java b/src/companionTest/java/zone/moddev/mc/companion/test/StatsClientSmoke.java index e094e1cc..32d51d52 100644 --- a/src/companionTest/java/zone/moddev/mc/companion/test/StatsClientSmoke.java +++ b/src/companionTest/java/zone/moddev/mc/companion/test/StatsClientSmoke.java @@ -8,7 +8,10 @@ import zone.moddev.mc.lib.gui.layout.Rect; import zone.moddev.mc.lib.gui.runtime.component.BuiltinMenuComponents; -/** Real synced throughput rows, left drawer switching and the cycling timescale control. */ +/** + * Real synced throughput rows, left drawer switching and the cycling timescale + * control. + */ final class StatsClientSmoke { private static final java.util.Map progress = new java.util.WeakHashMap<>(); @@ -21,48 +24,50 @@ static boolean run(MachineScreen screen) { return false; var menu = screen.getMenu(); var panel = screen.panelBounds(); - var info = screen.bounds(menu.bound.screen().find("info").orElseThrow()); - var tab = new Rect( - panel.x() - AutomationLayout.RAIL, - info.bottom() + 2, - AutomationLayout.RAIL, - AutomationLayout.TAB_HEIGHT); + var info = screen + .bounds(menu.bound.screen().find("info").orElseThrow()); + var tab = new Rect(panel.x() - AutomationLayout.RAIL, info.bottom() + 2, + AutomationLayout.RAIL, AutomationLayout.TAB_HEIGHT); String name = menu.bound.screen().id().getPath(); var mc = Minecraft.getInstance(); switch (step) { case 0 -> { - if (menu.components().require(BuiltinMenuComponents.STATS).pages().isEmpty() - || menu.components() - .require(BuiltinMenuComponents.STATS) - .pages() - .get(0) - .lines() - .isEmpty() - || menu.components().get(BuiltinMenuComponents.BACKPACK).pending() - || menu.components().get(BuiltinMenuComponents.CURIOS).pending()) + if (menu.components().require(BuiltinMenuComponents.STATS) + .pages().isEmpty() + || menu.components().require(BuiltinMenuComponents.STATS) + .pages().get(0).lines().isEmpty() + || menu.components().get(BuiltinMenuComponents.BACKPACK) + .pending() + || menu.components().get(BuiltinMenuComponents.CURIOS) + .pending()) return false; int processors = name.contains("dual") ? 2 : 1; - boolean consuming = menu.components().require(BuiltinMenuComponents.STATS).pages() - .get(0).lines().stream() + boolean consuming = menu.components() + .require(BuiltinMenuComponents.STATS).pages().get(0).lines() + .stream() .anyMatch(c -> c.getString().startsWith("RF used:")); - if (menu.components().require(BuiltinMenuComponents.STATS).pages().get(0).lines() + if (menu.components().require(BuiltinMenuComponents.STATS) + .pages().get(0).lines() .size() != (consuming ? 5 : 3) + 4 * processors) throw new AssertionError( "Statistics should contain directional energy rows, conditional usage and two rows" + " under each processor header"); - if (menu.components().require(BuiltinMenuComponents.STATS).pages().get(0).lines() - .stream() - .filter( - c -> c.getString().contains("cycles/t") - || c.getString().contains("Max cycles: No recipe")) + if (menu.components().require(BuiltinMenuComponents.STATS) + .pages().get(0).lines().stream() + .filter(c -> c.getString().contains("cycles/t") + || c.getString().contains("Max cycles: No recipe")) .count() != 2 * processors) - throw new AssertionError("Each processor must have an independent cycle rate"); + throw new AssertionError( + "Each processor must have an independent cycle rate"); click(screen, info.x() + 5, info.y() + 5, 0); } case 1 -> { - // Opening Info can close a restored equipment drawer through a server round trip. - if (menu.components().get(BuiltinMenuComponents.BACKPACK).pending() - || menu.components().get(BuiltinMenuComponents.CURIOS).pending()) + // Opening Info can close a restored equipment drawer through a + // server round trip. + if (menu.components().get(BuiltinMenuComponents.BACKPACK) + .pending() + || menu.components().get(BuiltinMenuComponents.CURIOS) + .pending()) return false; click(screen, tab.x() + 5, tab.y() + 5, 0); } @@ -72,50 +77,47 @@ static boolean run(MachineScreen screen) { .noneMatch(r -> r.right() == tab.x() && r.width() == 160)) throw new AssertionError( "Statistics must expand to the left, under Info: " - + name - + " " - + screen.exclusions()); - Screenshot.grab( - mc.gameDirectory, name + "-stats-tick.png", mc.getMainRenderTarget(), m -> { + + name + " " + screen.exclusions()); + Screenshot.grab(mc.gameDirectory, name + "-stats-tick.png", + mc.getMainRenderTarget(), m -> { }); } case 3 -> { clickUnit(screen, 0); requireUnit(screen, 1); } - case 4 -> - Screenshot.grab( - mc.gameDirectory, name + "-stats-second.png", mc.getMainRenderTarget(), m -> { - }); + case 4 -> Screenshot.grab(mc.gameDirectory, + name + "-stats-second.png", mc.getMainRenderTarget(), m -> { + }); case 5 -> { clickUnit(screen, 0); requireUnit(screen, 2); } - case 6 -> - Screenshot.grab( - mc.gameDirectory, name + "-stats-minute.png", mc.getMainRenderTarget(), m -> { - }); + case 6 -> Screenshot.grab(mc.gameDirectory, + name + "-stats-minute.png", mc.getMainRenderTarget(), m -> { + }); case 7 -> { clickUnit(screen, 0); requireUnit(screen, 0); clickUnit(screen, 1); requireUnit(screen, 2); } - case 8 -> - Screenshot.grab( - mc.gameDirectory, name + "-stats-reverse.png", mc.getMainRenderTarget(), m -> { - }); + case 8 -> Screenshot.grab(mc.gameDirectory, + name + "-stats-reverse.png", mc.getMainRenderTarget(), m -> { + }); case 9 -> click(screen, info.x() + 5, info.y() + 5, 0); case 10 -> { if (screen.exclusions().stream() .anyMatch(r -> r.right() == tab.x() && r.width() == 160)) - throw new AssertionError("Info replaces Statistics on the left"); + throw new AssertionError( + "Info replaces Statistics on the left"); click(screen, tab.x() + 5, tab.y() + 5, 0); } case 11 -> { screen.keyPressed(256, 0, 0); if (mc.screen != screen) - throw new AssertionError("Escape closes Statistics before the machine"); + throw new AssertionError( + "Escape closes Statistics before the machine"); } } state[0]++; @@ -126,31 +128,33 @@ static boolean run(MachineScreen screen) { private static void clickUnit(MachineScreen screen, int button) { try { var drawer = screen.drawers().entries().stream() - .filter(zone.moddev.mc.lib.gui.client.StatsDrawer.class::isInstance) - .findFirst() - .orElseThrow(); + .filter( + zone.moddev.mc.lib.gui.client.StatsDrawer.class::isInstance) + .findFirst().orElseThrow(); var layout = drawer.getClass().getDeclaredMethod("layout"); layout.setAccessible(true); layout.invoke(drawer); var bounds = drawer.getClass().getDeclaredMethod("unitBounds"); bounds.setAccessible(true); var rect = (Rect) bounds.invoke(drawer); - click(screen, rect.x() + rect.width() / 2, rect.y() + rect.height() / 2, button); + click(screen, rect.x() + rect.width() / 2, + rect.y() + rect.height() / 2, button); } catch (ReflectiveOperationException error) { throw new AssertionError(error); } } private static void requireUnit(MachineScreen screen, int expected) { - var drawer = (zone.moddev.mc.lib.gui.client.StatsDrawer) screen.drawers().entries().stream() + var drawer = (zone.moddev.mc.lib.gui.client.StatsDrawer) screen + .drawers().entries().stream() .filter(zone.moddev.mc.lib.gui.client.StatsDrawer.class::isInstance) - .findFirst() - .orElseThrow(); + .findFirst().orElseThrow(); var selected = drawer.selectedPage(); - var expectedId = zone.moddev.mc.lib.api.stats.StatsTimescale.DEFAULTS.get(expected).id(); + var expectedId = zone.moddev.mc.lib.api.stats.StatsTimescale.DEFAULTS + .get(expected).id(); if (!selected.equals(java.util.Optional.of(expectedId))) - throw new AssertionError( - "Wrong statistics page: " + selected + ", expected " + expectedId); + throw new AssertionError("Wrong statistics page: " + selected + + ", expected " + expectedId); } private static void click(MachineScreen screen, int x, int y, int button) { diff --git a/src/companionTest/java/zone/moddev/mc/companion/test/StatsExtensionClientSmoke.java b/src/companionTest/java/zone/moddev/mc/companion/test/StatsExtensionClientSmoke.java index 2a5897cb..559aa86d 100644 --- a/src/companionTest/java/zone/moddev/mc/companion/test/StatsExtensionClientSmoke.java +++ b/src/companionTest/java/zone/moddev/mc/companion/test/StatsExtensionClientSmoke.java @@ -26,47 +26,41 @@ import zone.moddev.mc.lib.gui.runtime.component.MenuComponentRegistry; /** - * A host-provided custom type and fourth timescale, through the production sampler and wire field. + * A host-provided custom type and fourth timescale, through the production + * sampler and wire field. */ final class StatsExtensionClientSmoke { private static final StatsTimescale HOUR = new StatsTimescale( - ResourceLocation.parse("test:hour"), - 72000, - Component.literal("Per hour"), - Component.literal("h")); + ResourceLocation.parse("test:hour"), 72000, + Component.literal("Per hour"), Component.literal("h")); private static final List ALL = List.of(StatsTimescale.TICK, StatsTimescale.SECOND, StatsTimescale.MINUTE, HOUR); private static volatile List timescales = ALL; private static int step, attempts; private record CustomValue(int amount) implements Statistics.Value { - public Statistics.Sample sample(StatsSamplingContext context, Statistics.Sample previous) { + public Statistics.Sample sample(StatsSamplingContext context, + Statistics.Sample previous) { var squared = BigInteger.valueOf(amount).pow(2); return format -> format - .number(StatsNumbers.rate(squared, 1, format.ticks(), format.decimals())) - .copy() - .withStyle(ChatFormatting.DARK_PURPLE); + .number(StatsNumbers.rate(squared, 1, format.ticks(), + format.decimals())) + .copy().withStyle(ChatFormatting.DARK_PURPLE); } } private static final StatsProvider CUSTOM = () -> new Statistics( Component.literal("Custom sensor"), - List.of( - new Statistics.Row( - "square", - Component.literal("Squared load"), - new CustomValue(7), - Component.literal("units"), - 0, - true))); + List.of(new Statistics.Row("square", Component.literal("Squared load"), + new CustomValue(7), Component.literal("units"), 0, true))); static void install() { - // Inject only the test host through the existing component factory. Statistic types and - // timescales themselves have no registration, exactly as in a real ScreenHost + // Inject only the test host through the existing component factory. + // Statistic types and + // timescales themselves have no registration, exactly as in a real + // ScreenHost // implementation. - MenuComponentRegistry.GLOBAL.replace( - BuiltinMenuComponents.STATS, - 20, + MenuComponentRegistry.GLOBAL.replace(BuiltinMenuComponents.STATS, 20, c -> { var host = new ScreenHost() { public net.neoforged.neoforge.items.IItemHandlerModifiable screenItems() { @@ -74,7 +68,8 @@ public net.neoforged.neoforge.items.IItemHandlerModifiable screenItems() { } public List screenStats() { - var providers = new ArrayList(c.host().screenStats()); + var providers = new ArrayList( + c.host().screenStats()); providers.add(CUSTOM); return providers; } @@ -83,26 +78,25 @@ public List screenStatsTimescales() { return timescales; } }; - return new StatsMenuComponent( - new MenuComponentRegistry.Context(c.menu(), c.inventory(), host, c.client())); - }, - StatsMenuComponent.PAGES); + return new StatsMenuComponent(new MenuComponentRegistry.Context( + c.menu(), c.inventory(), host, c.client())); + }, StatsMenuComponent.PAGES); } static boolean run(MachineScreen screen) { if (++attempts > 240) throw new AssertionError("Statistics extension stuck at " + step); - var pages = screen.getMenu().components().require(BuiltinMenuComponents.STATS).pages(); + var pages = screen.getMenu().components() + .require(BuiltinMenuComponents.STATS).pages(); var drawer = (StatsDrawer) screen.drawers().entries().stream() - .filter(StatsDrawer.class::isInstance) - .findFirst() - .orElseThrow(); + .filter(StatsDrawer.class::isInstance).findFirst().orElseThrow(); if (step == 0) { if (pages.size() != 4 || screen.drawers().pending()) return false; - if (pages.get(3).lines().stream() - .noneMatch(row -> row.getString().equals("Squared load: 3,528,000 units/h"))) - throw new AssertionError("Custom statistic was not sampled and synchronized"); + if (pages.get(3).lines().stream().noneMatch(row -> row.getString() + .equals("Squared load: 3,528,000 units/h"))) + throw new AssertionError( + "Custom statistic was not sampled and synchronized"); drawer.close(); var tab = drawer.tabBounds(); click(screen, tab, 0); @@ -116,8 +110,8 @@ static boolean run(MachineScreen screen) { requireSelected(drawer, HOUR.id()); } else if (step == 2) { capture("statistics-custom-hour.png"); - timescales = List.of(HOUR, StatsTimescale.MINUTE, StatsTimescale.TICK, - StatsTimescale.SECOND); + timescales = List.of(HOUR, StatsTimescale.MINUTE, + StatsTimescale.TICK, StatsTimescale.SECOND); } else if (step == 3) { if (pages.isEmpty() || !pages.getFirst().id().equals(HOUR.id())) return false; @@ -150,19 +144,23 @@ static boolean run(MachineScreen screen) { requireSelected(drawer, StatsTimescale.TICK.id()); screen.keyPressed(256, 0, 0); if (drawer.open() || Minecraft.getInstance().screen != screen) - throw new AssertionError("Escape failed to close only statistics"); + throw new AssertionError( + "Escape failed to close only statistics"); return true; } step++; return false; } - private static void requireSelected(StatsDrawer drawer, ResourceLocation id) { + private static void requireSelected(StatsDrawer drawer, + ResourceLocation id) { if (!drawer.selectedPage().equals(Optional.of(id))) - throw new AssertionError("Wrong page: " + drawer.selectedPage() + ", expected " + id); + throw new AssertionError( + "Wrong page: " + drawer.selectedPage() + ", expected " + id); } - private static void clickUnit(MachineScreen screen, StatsDrawer drawer, int button) { + private static void clickUnit(MachineScreen screen, StatsDrawer drawer, + int button) { drawer.selectedPage(); // refresh geometry and selection try { var method = StatsDrawer.class.getDeclaredMethod("unitBounds"); @@ -174,7 +172,8 @@ private static void clickUnit(MachineScreen screen, StatsDrawer drawer, int butt } private static void click(MachineScreen screen, Rect bounds, int button) { - int x = bounds.x() + bounds.width() / 2, y = bounds.y() + bounds.height() / 2; + int x = bounds.x() + bounds.width() / 2, + y = bounds.y() + bounds.height() / 2; if (!screen.mouseClicked(x, y, button)) throw new AssertionError("Statistics click was ignored"); screen.mouseReleased(x, y, button); @@ -182,7 +181,8 @@ private static void click(MachineScreen screen, Rect bounds, int button) { private static void capture(String name) { var mc = Minecraft.getInstance(); - Screenshot.grab(mc.gameDirectory, name, mc.getMainRenderTarget(), message -> { - }); + Screenshot.grab(mc.gameDirectory, name, mc.getMainRenderTarget(), + message -> { + }); } } diff --git a/src/companionTest/java/zone/moddev/mc/companion/test/TestAutomationCapabilities.java b/src/companionTest/java/zone/moddev/mc/companion/test/TestAutomationCapabilities.java index 2de2c506..5e8a7dc1 100644 --- a/src/companionTest/java/zone/moddev/mc/companion/test/TestAutomationCapabilities.java +++ b/src/companionTest/java/zone/moddev/mc/companion/test/TestAutomationCapabilities.java @@ -17,38 +17,32 @@ import zone.moddev.mc.lib.api.automation.*; import zone.moddev.mc.lib.api.capabilities.IoMode; -/** Test-only counters prove steady-state work is skipped and invalidation is observed. */ +/** + * Test-only counters prove steady-state work is skipped and invalidation is + * observed. + */ @EventBusSubscriber(modid = CompanionMod.ID, bus = EventBusSubscriber.Bus.MOD) public final class TestAutomationCapabilities { - public static final BlockCapability ITEMS = - BlockCapability.createSided( - ResourceLocation.parse("mmdlib_companion:test_counted_items"), IItemHandler.class); - public static final Map LOOKUPS = new HashMap<>(); - public static final AtomicInteger SPECIFICATIONS = new AtomicInteger(); + public static final BlockCapability ITEMS = BlockCapability + .createSided( + ResourceLocation.parse("mmdlib_companion:test_counted_items"), + IItemHandler.class); + public static final Map LOOKUPS = new HashMap<>(); + public static final AtomicInteger SPECIFICATIONS = new AtomicInteger(); - @SubscribeEvent - public static void register(RegisterCapabilitiesEvent event) { - event.registerBlockEntity( - ITEMS, - BlockEntityType.CHEST, - (chest, side) -> { - LOOKUPS.merge(chest.getBlockPos(), 1, Integer::sum); - return new ItemStackHandler(1); - }); - event.registerItem( - AddonCapabilities.ADDON, - (stack, context) -> - item -> { - SPECIFICATIONS.incrementAndGet(); - return new AddonSpec( - ResourceLocation.parse("mmdlib_companion:test_puller"), - ResourceType.ITEMS, - IoMode.INPUT, - 8, - 20, - 4, - false); - }, - Items.BLAZE_ROD); - } + @SubscribeEvent + public static void register(RegisterCapabilitiesEvent event) { + event.registerBlockEntity(ITEMS, BlockEntityType.CHEST, + (chest, side) -> { + LOOKUPS.merge(chest.getBlockPos(), 1, Integer::sum); + return new ItemStackHandler(1); + }); + event.registerItem(AddonCapabilities.ADDON, + (stack, context) -> item -> { + SPECIFICATIONS.incrementAndGet(); + return new AddonSpec( + ResourceLocation.parse("mmdlib_companion:test_puller"), + ResourceType.ITEMS, IoMode.INPUT, 8, 20, 4, false); + }, Items.BLAZE_ROD); + } } diff --git a/src/companionTest/java/zone/moddev/mc/companion/test/TestFluidContainer.java b/src/companionTest/java/zone/moddev/mc/companion/test/TestFluidContainer.java index 22fcf655..4be2a8b1 100644 --- a/src/companionTest/java/zone/moddev/mc/companion/test/TestFluidContainer.java +++ b/src/companionTest/java/zone/moddev/mc/companion/test/TestFluidContainer.java @@ -16,33 +16,39 @@ import zone.moddev.mc.companion.CompanionMod; -/** Test-only stand-in for a modded container that transfers partial amounts, unlike buckets. */ +/** + * Test-only stand-in for a modded container that transfers partial amounts, + * unlike buckets. + */ @EventBusSubscriber(modid = CompanionMod.ID, bus = EventBusSubscriber.Bus.MOD) -public final class TestFluidContainer extends FluidTank implements IFluidHandlerItem { - private final ItemStack container; - - private TestFluidContainer(ItemStack container) { - super(2_000, fluid -> fluid.getFluid() == Fluids.WATER); - this.container = container; - int amount = container.getOrDefault(DataComponents.CUSTOM_DATA, CustomData.EMPTY) - .copyTag().getInt("test_water"); - setFluid(new FluidStack(Fluids.WATER, amount)); - } - - @SubscribeEvent - public static void capabilities(RegisterCapabilitiesEvent event) { - event.registerItem(Capabilities.FluidHandler.ITEM, - (stack, context) -> new TestFluidContainer(stack), Items.GLASS_BOTTLE); - } - - @Override - public ItemStack getContainer() { - return container; - } - - @Override - protected void onContentsChanged() { - CustomData.update(DataComponents.CUSTOM_DATA, container, - tag -> tag.putInt("test_water", getFluidAmount())); - } +public final class TestFluidContainer extends FluidTank + implements IFluidHandlerItem { + private final ItemStack container; + + private TestFluidContainer(ItemStack container) { + super(2_000, fluid -> fluid.getFluid() == Fluids.WATER); + this.container = container; + int amount = container + .getOrDefault(DataComponents.CUSTOM_DATA, CustomData.EMPTY) + .copyTag().getInt("test_water"); + setFluid(new FluidStack(Fluids.WATER, amount)); + } + + @SubscribeEvent + public static void capabilities(RegisterCapabilitiesEvent event) { + event.registerItem(Capabilities.FluidHandler.ITEM, + (stack, context) -> new TestFluidContainer(stack), + Items.GLASS_BOTTLE); + } + + @Override + public ItemStack getContainer() { + return container; + } + + @Override + protected void onContentsChanged() { + CustomData.update(DataComponents.CUSTOM_DATA, container, + tag -> tag.putInt("test_water", getFluidAmount())); + } } diff --git a/src/companionTest/java/zone/moddev/mc/companion/test/TooltipClientSmoke.java b/src/companionTest/java/zone/moddev/mc/companion/test/TooltipClientSmoke.java index 7d6e02cb..14c052e0 100644 --- a/src/companionTest/java/zone/moddev/mc/companion/test/TooltipClientSmoke.java +++ b/src/companionTest/java/zone/moddev/mc/companion/test/TooltipClientSmoke.java @@ -32,7 +32,10 @@ import zone.moddev.mc.lib.gui.model.containers.BoxElement; import zone.moddev.mc.lib.gui.runtime.ScreenHost; -/** Real leaf/ancestor composition plus existing occupied/empty native slot regression checks. */ +/** + * Real leaf/ancestor composition plus existing occupied/empty native slot + * regression checks. + */ final class TooltipClientSmoke { record Leaf(Common common) implements Element { public int[] measure(TextMeasure text) { @@ -48,7 +51,8 @@ public List tooltip(ScreenHost host, ItemStack carried) { } } - record Group(Common common, Element child, String label) implements Element { + record Group(Common common, Element child, String label) + implements Element { public List children() { return List.of(child); } @@ -67,25 +71,28 @@ public List tooltip(ScreenHost host, ItemStack carried) { public void appendChildTooltip(TooltipContext c, List rows) { rows.addAll(c.serverLines()); - rows.add( - Component.literal(label + " for " + c.target().common().id().orElse("slot")) - .withStyle(ChatFormatting.GOLD)); + rows.add(Component + .literal( + label + " for " + c.target().common().id().orElse("slot")) + .withStyle(ChatFormatting.GOLD)); } } private static boolean checked; private static boolean captureRequested, captured; - static void render(MachineScreen screen, net.minecraft.client.gui.GuiGraphics graphics) { + static void render(MachineScreen screen, + net.minecraft.client.gui.GuiGraphics graphics) { if (!captureRequested || captured) return; - var leaf = screen.getMenu().bound.screen().find("tooltip_probe").orElseThrow(); + var leaf = screen.getMenu().bound.screen().find("tooltip_probe") + .orElseThrow(); var r = screen.bounds(leaf); screen.render(graphics, r.x() + 2, r.y() + 2, 0); graphics.flush(); var mc = Minecraft.getInstance(); - Screenshot.grab( - mc.gameDirectory, "tooltip-composition.png", mc.getMainRenderTarget(), ignored -> { + Screenshot.grab(mc.gameDirectory, "tooltip-composition.png", + mc.getMainRenderTarget(), ignored -> { }); captured = true; } @@ -94,25 +101,18 @@ static void install() { ContributedHintClientSmoke.install(); ElementRenderers.GLOBAL.register(Group.class, (e, c) -> { }); - ElementRenderers.GLOBAL.register( - Leaf.class, - (e, c) -> c.graphics() - .drawString( - c.access().font(), - "Hover for composed tooltip", - c.bounds().x(), - c.bounds().y(), - 0x404040, - false)); - ElementBehaviors.GLOBAL.register( - Group.class, + ElementRenderers.GLOBAL.register(Leaf.class, + (e, c) -> c.graphics().drawString(c.access().font(), + "Hover for composed tooltip", c.bounds().x(), c.bounds().y(), + 0x404040, false)); + ElementBehaviors.GLOBAL.register(Group.class, (e, a) -> new DefaultElementBehavior(a) { - public void appendChildTooltip(TooltipContext c, List rows) { + public void appendChildTooltip(TooltipContext c, + List rows) { rows.add(Component.literal("behavior " + e.label())); } }); - ElementBehaviors.GLOBAL.register( - Leaf.class, + ElementBehaviors.GLOBAL.register(Leaf.class, (e, a) -> new DefaultElementBehavior(a) { public List tooltip(int x, int y) { return List.of(Component.literal("behavior leaf")); @@ -121,34 +121,25 @@ public List tooltip(int x, int y) { var id = ResourceLocation.parse("mmdlib_companion:crusher"); var old = Screens.get(id).orElseThrow().screen(); var c = Common.empty(); - var common = new Common( - Optional.of("tooltip_probe"), - c.value(), + var common = new Common(Optional.of("tooltip_probe"), c.value(), c.max(), - Optional.of(new Text("test.tooltip", Optional.of("Leaf {value}/{max}"))), - c.tooltipShift(), - c.clicks(), - c.onHover(), - c.hoverShows(), - c.jeiRecipes(), - 0, - 0, - 0); + Optional.of( + new Text("test.tooltip", Optional.of("Leaf {value}/{max}"))), + c.tooltipShift(), c.clicks(), c.onHover(), c.hoverShows(), + c.jeiRecipes(), 0, 0, 0); var body = new ArrayList(); body.add(new Leaf(common)); body.addAll(old.body()); - var panel = new BoxElement(Common.empty(), true, 0, 4, Align.START, body); - var wrapper = new Group(Common.empty(), new Group(Common.empty(), panel, "inner"), "outer"); - var screen = new Screen( - old.id(), - old.source(), - old.title(), - old.blockEntity(), - old.playerInventory(), - List.of(wrapper), + var panel = new BoxElement(Common.empty(), true, 0, 4, Align.START, + body); + var wrapper = new Group(Common.empty(), + new Group(Common.empty(), panel, "inner"), "outer"); + var screen = new Screen(old.id(), old.source(), old.title(), + old.blockEntity(), old.playerInventory(), List.of(wrapper), old.tabs()); var screens = new HashMap<>(Screens.all()); - screens.put(id, ScreenBinder.bind(screen, TooltipClientSmoke.class.getClassLoader())); + screens.put(id, ScreenBinder.bind(screen, + TooltipClientSmoke.class.getClassLoader())); Screens.install(screens); } @@ -156,23 +147,16 @@ static boolean run(MachineScreen screen) { if (!ContributedHintClientSmoke.run(screen)) return false; if (!checked) { - var leaf = screen.getMenu().bound.screen().find("tooltip_probe").orElseThrow(); + var leaf = screen.getMenu().bound.screen().find("tooltip_probe") + .orElseThrow(); var r = screen.bounds(leaf); var rows = screen.tooltipLines(leaf, r.x() + 2, r.y() + 2).stream() - .map(Component::getString) - .toList(); + .map(Component::getString).toList(); if (!rows.contains("server outer")) return false; - var expected = List.of( - "server leaf", - "Leaf 7/9", - "behavior leaf", - "server inner", - "inner for tooltip_probe", - "behavior inner", - "server outer", - "outer for tooltip_probe", - "behavior outer"); + var expected = List.of("server leaf", "Leaf 7/9", "behavior leaf", + "server inner", "inner for tooltip_probe", "behavior inner", + "server outer", "outer for tooltip_probe", "behavior outer"); if (!rows.equals(expected)) throw new AssertionError("Tooltip composition: " + rows); if (!captured) { @@ -180,8 +164,7 @@ static boolean run(MachineScreen screen) { return false; } var fragment = new ElementFragment( - new Group(Common.empty(), leaf, "fragment"), - 140, + new Group(Common.empty(), leaf, "fragment"), 140, new ElementFragment.Host() { public ResourceLocation screenId() { return screen.getMenu().bound.screen().id(); @@ -193,15 +176,15 @@ public Object value(Element element, boolean maximum) { }); try { fragment.place(10, 20, new Rect(10, 20, 100, 14)); - var fragmentRows = fragment.tooltip(12, 22).stream().map(Component::getString) - .toList(); - if (!fragmentRows.equals( - List.of( - "Leaf 7/9", "behavior leaf", "fragment for tooltip_probe", - "behavior fragment"))) - throw new AssertionError("Fragment tooltip composition: " + fragmentRows); + var fragmentRows = fragment.tooltip(12, 22).stream() + .map(Component::getString).toList(); + if (!fragmentRows.equals(List.of("Leaf 7/9", "behavior leaf", + "fragment for tooltip_probe", "behavior fragment"))) + throw new AssertionError( + "Fragment tooltip composition: " + fragmentRows); if (!fragment.tooltip(120, 22).isEmpty()) - throw new AssertionError("Tooltip outside clipped fragment"); + throw new AssertionError( + "Tooltip outside clipped fragment"); } finally { fragment.removed(); } diff --git a/src/companionTest/java/zone/moddev/mc/companion/test/ValueTypeClientSmoke.java b/src/companionTest/java/zone/moddev/mc/companion/test/ValueTypeClientSmoke.java index 03cbdd90..3cb5487a 100644 --- a/src/companionTest/java/zone/moddev/mc/companion/test/ValueTypeClientSmoke.java +++ b/src/companionTest/java/zone/moddev/mc/companion/test/ValueTypeClientSmoke.java @@ -20,33 +20,30 @@ import zone.moddev.mc.lib.gui.model.ValueRef; import zone.moddev.mc.lib.gui.runtime.ScreenValues; -/** Custom getter snapshots traverse real delta packets and accepted/rejected action replies. */ +/** + * Custom getter snapshots traverse real delta packets and accepted/rejected + * action replies. + */ final class ValueTypeClientSmoke { private static int phase, drawn, accepted, rejected; private static ValueTypeFixture.Reading first; static void install() { - ElementRenderers.GLOBAL.register( - ValueTypeFixture.Display.class, + ElementRenderers.GLOBAL.register(ValueTypeFixture.Display.class, (e, c) -> { - var value = (ValueTypeFixture.Reading) c.access().value(e, false); + var value = (ValueTypeFixture.Reading) c.access().value(e, + false); if (value != null) drawn++; - c.graphics() - .drawString( - c.access().font(), - "Mod reading: " + (value == null ? "--" : value.amount), - c.bounds().x(), - c.bounds().y(), - 0xffffff); + c.graphics().drawString(c.access().font(), + "Mod reading: " + (value == null ? "--" : value.amount), + c.bounds().x(), c.bounds().y(), 0xffffff); }); - ElementBehaviors.GLOBAL.register( - ValueTypeFixture.Display.class, + ElementBehaviors.GLOBAL.register(ValueTypeFixture.Display.class, (e, access) -> new ElementBehavior() { public void actionResult(boolean ok) { var value = (ValueTypeFixture.Reading) access.value(); - require( - value != null && value.amount == 7, + require(value != null && value.amount == 7, "Action result did not restore the custom value before callbacks"); if (ok) accepted++; @@ -57,31 +54,18 @@ public void actionResult(boolean ok) { var id = ResourceLocation.parse("mmdlib_companion:crusher"); var old = Screens.get(id).orElseThrow().screen(); var c = Common.empty(); - var binding = new Common( - Optional.of("mod_reading"), - Optional.of(ValueRef.parse(ValueTypeFixture.class.getName() + "#reading")), - c.max(), - c.tooltip(), - c.tooltipShift(), - c.clicks(), - c.onHover(), - c.hoverShows(), - c.jeiRecipes(), - 0, - 0, - 0); + var binding = new Common(Optional.of("mod_reading"), + Optional.of( + ValueRef.parse(ValueTypeFixture.class.getName() + "#reading")), + c.max(), c.tooltip(), c.tooltipShift(), c.clicks(), c.onHover(), + c.hoverShows(), c.jeiRecipes(), 0, 0, 0); var body = new ArrayList<>(old.body()); body.add(new ValueTypeFixture.Display(binding)); - var screen = new Screen( - old.id(), - old.source(), - old.title(), - old.blockEntity(), - old.playerInventory(), - body, - old.tabs()); + var screen = new Screen(old.id(), old.source(), old.title(), + old.blockEntity(), old.playerInventory(), body, old.tabs()); var screens = new HashMap<>(Screens.all()); - screens.put(id, ScreenBinder.bind(screen, ValueTypeClientSmoke.class.getClassLoader())); + screens.put(id, ScreenBinder.bind(screen, + ValueTypeClientSmoke.class.getClassLoader())); Screens.install(screens); } @@ -95,53 +79,52 @@ static boolean run(MachineScreen screen) { case 0 -> { if (value == null || drawn == 0) return false; - require( - value.amount == 3 && value != ValueTypeFixture.live, + require(value.amount == 3 && value != ValueTypeFixture.live, "Initial custom snapshot is not detached"); first = value; require( - ScreenValues.changed( - Map.of(0, value), Map.of(0, new ValueTypeFixture.Reading(3)), false) + ScreenValues + .changed(Map.of(0, value), + Map.of(0, new ValueTypeFixture.Reading(3)), false) .isEmpty(), "Registered content equality sends unchanged values"); - require( - screen.submitElementAction(element, ValueTypeFixture.EDIT, 7), - "Custom action was not sent"); + require(screen.submitElementAction(element, + ValueTypeFixture.EDIT, 7), "Custom action was not sent"); } case 1 -> { if (accepted == 0) return false; - require( - value.amount == 7 && first.amount == 3, + require(value.amount == 7 && first.amount == 3, "Mutable getter corrupted its previous snapshot"); - menu.apply(Map.of(menu.index(element) * 2, new ValueTypeFixture.Reading(99))); - require( - screen.submitElementAction(element, ValueTypeFixture.EDIT, -1), - "Rejected action was not sent"); + menu.apply(Map.of(menu.index(element) * 2, + new ValueTypeFixture.Reading(99))); + require(screen.submitElementAction(element, + ValueTypeFixture.EDIT, -1), "Rejected action was not sent"); } case 2 -> { if (rejected == 0) return false; - require(value.amount == 7, "Rejected action did not correct custom value"); - Minecraft.getInstance() - .getSingleplayerServer() + require(value.amount == 7, + "Rejected action did not correct custom value"); + Minecraft.getInstance().getSingleplayerServer() .execute(() -> ValueTypeFixture.live.amount = 11); } case 3 -> { if (value == null || value.amount != 11) return false; var mc = Minecraft.getInstance(); - Screenshot.grab( - mc.gameDirectory, "custom-value.png", mc.getMainRenderTarget(), ignored -> { + Screenshot.grab(mc.gameDirectory, "custom-value.png", + mc.getMainRenderTarget(), ignored -> { }); - mc.getSingleplayerServer().execute(() -> ValueTypeFixture.live = null); + mc.getSingleplayerServer() + .execute(() -> ValueTypeFixture.live = null); } case 4 -> { if (value != null) return false; - Minecraft.getInstance() - .getSingleplayerServer() - .execute(() -> ValueTypeFixture.live = new ValueTypeFixture.Reading(13)); + Minecraft.getInstance().getSingleplayerServer().execute( + () -> ValueTypeFixture.live = new ValueTypeFixture.Reading( + 13)); } case 5 -> { if (value == null || value.amount != 13) @@ -152,7 +135,8 @@ static boolean run(MachineScreen screen) { "Built-in gauge values stopped synchronizing beside custom values"); return true; } - default -> throw new AssertionError("Unexpected value-type smoke phase"); + default -> + throw new AssertionError("Unexpected value-type smoke phase"); } phase++; return false; diff --git a/src/companionTest/java/zone/moddev/mc/companion/test/ValueTypeFixture.java b/src/companionTest/java/zone/moddev/mc/companion/test/ValueTypeFixture.java index 61aee6c0..b79d7f65 100644 --- a/src/companionTest/java/zone/moddev/mc/companion/test/ValueTypeFixture.java +++ b/src/companionTest/java/zone/moddev/mc/companion/test/ValueTypeFixture.java @@ -20,7 +20,10 @@ import zone.moddev.mc.lib.gui.value.ValueType; import zone.moddev.mc.lib.gui.value.ValueTypeRegistry; -/** Common-only fixture registers before screen loading, exactly as an external mod would. */ +/** + * Common-only fixture registers before screen loading, exactly as an external + * mod would. + */ @EventBusSubscriber(modid = CompanionMod.ID, bus = EventBusSubscriber.Bus.MOD) public final class ValueTypeFixture { public static final class Reading { @@ -32,13 +35,14 @@ public static final class Reading { } static Reading live = new Reading(3); - static final ValueType TYPE = ValueType.builder( - ResourceLocation.parse("mmdlib_companion:test_reading"), - Reading.class, - () -> StreamCodec.of((b, v) -> b.writeInt(v.amount), b -> new Reading(b.readInt())), - 4) - .validate(v -> v.amount >= 0) - .equality((a, b) -> a.amount == b.amount) + static final ValueType TYPE = ValueType + .builder( + ResourceLocation.parse("mmdlib_companion:test_reading"), + Reading.class, + () -> StreamCodec.of((b, v) -> b.writeInt(v.amount), + b -> new Reading(b.readInt())), + 4) + .validate(v -> v.amount >= 0).equality((a, b) -> a.amount == b.amount) .build(); @SubscribeEvent @@ -52,8 +56,7 @@ public static Reading reading(BlockEntity ignored) { static final ElementAction EDIT = new ElementAction<>( ResourceLocation.parse("mmdlib_companion:test_reading_edit"), - StreamCodec.of((b, v) -> b.writeInt(v), b -> b.readInt()), - 4, + StreamCodec.of((b, v) -> b.writeInt(v), b -> b.readInt()), 4, (context, value) -> { if (value < 0) return false; diff --git a/src/main/java/zone/moddev/mc/lib/MMDLib.java b/src/main/java/zone/moddev/mc/lib/MMDLib.java index 1dfdd57f..75581dcf 100644 --- a/src/main/java/zone/moddev/mc/lib/MMDLib.java +++ b/src/main/java/zone/moddev/mc/lib/MMDLib.java @@ -1,6 +1,7 @@ package zone.moddev.mc.lib; import java.nio.file.Files; + import java.util.ArrayList; import java.util.Map; @@ -35,185 +36,208 @@ import zone.moddev.mc.lib.compat.CraftTweaker.CraftTweakerInit; /** - * MMDLib's mod entry point. At mod construction it loads every mod's {@code mmdlib/} definitions - * with {@link zone.moddev.mc.lib.common.loading.DefinitionLoader}, queues MMDLib's own materials - * and standalone blocks and items on its {@link - * zone.moddev.mc.lib.common.runtime.RuntimeContentRegistrar}, publishes definitions to MMDLib's - * definition registries and installs the item event bridge. The static accessors expose what was - * loaded. + * MMDLib's mod entry point. At mod construction it loads every mod's + * {@code mmdlib/} definitions with + * {@link zone.moddev.mc.lib.common.loading.DefinitionLoader}, queues MMDLib's + * own materials and standalone blocks and items on its + * {@link zone.moddev.mc.lib.common.runtime.RuntimeContentRegistrar}, publishes + * definitions to MMDLib's definition registries and installs the item event + * bridge. The static accessors expose what was loaded. */ @Mod(MMDLib.MODID) public class MMDLib { - /** MMDLib's mod ID and namespace: {@code mmdlib}. */ - public static final String MODID = "mmdlib"; - - /** MMDLib's logger. */ - public static final Logger LOGGER = LogUtils.getLogger(); - - /** MMDLib's own block register, for blocks registered in code rather than from definitions. */ - public static final DeferredRegister BLOCKS = - DeferredRegister.create(Registries.BLOCK, MODID); - - /** MMDLib's own item register, for items registered in code rather than from definitions. */ - public static final DeferredRegister ITEMS = - DeferredRegister.create(Registries.ITEM, MODID); - - private final zone.moddev.mc.lib.common.runtime.RuntimeContentRegistrar runtime; - private final DeferredRegister> - machineTypes = DeferredRegister.create(Registries.BLOCK_ENTITY_TYPE, MODID); - private static volatile Map loadedMaterials = Map.of(); - private static volatile Map standaloneBlocks = Map.of(); - private static volatile Map standaloneItems = Map.of(); - private static volatile Map itemDefinitions = Map.of(); - private static volatile Map itemMaterials = Map.of(); - private static volatile Map blockDefinitions = Map.of(); - private static volatile Map blockMaterials = Map.of(); - - /** - * Item registry ID to its loaded definition / owning material; complete with {@link - * #loadedMaterials()}. - */ - public static Map itemDefinitions() { - return itemDefinitions; - } - - /** Item registry ID to its owning material; complete with {@link #loadedMaterials()}. */ - public static Map itemMaterials() { - return itemMaterials; - } - - /** Complete after deferred construction, before definition registry events. */ - public static Map loadedMaterials() { - return loadedMaterials; - } - - /** - * Standalone (material-less) blocks by registry ID; each result's material is a stats-free host. - */ - public static Map standaloneBlocks() { - return standaloneBlocks; - } - - /** - * Standalone (material-less) items by registry ID; each result's material is a stats-free host. - */ - public static Map standaloneItems() { - return standaloneItems; - } - - /** Called by NeoForge; registers MMDLib's config, registers and listeners on {@code modBus}. */ - public MMDLib(IEventBus modBus, ModContainer container) { - container.registerConfig(ModConfig.Type.SERVER, MMDLibConfig.SERVER_SPEC); - zone.moddev.mc.lib.gui.runtime.ScreenRuntime.register(modBus); - zone.moddev.mc.lib.machine.neoforge.MachineContent.register(modBus); - BLOCKS.register(modBus); - ITEMS.register(modBus); - machineTypes.register(modBus); - runtime = - new zone.moddev.mc.lib.common.runtime.RuntimeContentRegistrar( + /** MMDLib's mod ID and namespace: {@code mmdlib}. */ + public static final String MODID = "mmdlib"; + + /** MMDLib's logger. */ + public static final Logger LOGGER = LogUtils.getLogger(); + + /** + * MMDLib's own block register, for blocks registered in code rather than + * from definitions. + */ + public static final DeferredRegister BLOCKS = DeferredRegister + .create(Registries.BLOCK, MODID); + + /** + * MMDLib's own item register, for items registered in code rather than from + * definitions. + */ + public static final DeferredRegister ITEMS = DeferredRegister + .create(Registries.ITEM, MODID); + + private final zone.moddev.mc.lib.common.runtime.RuntimeContentRegistrar runtime; + private final DeferredRegister> machineTypes = DeferredRegister + .create(Registries.BLOCK_ENTITY_TYPE, MODID); + private static volatile Map loadedMaterials = Map + .of(); + private static volatile Map standaloneBlocks = Map + .of(); + private static volatile Map standaloneItems = Map + .of(); + private static volatile Map itemDefinitions = Map + .of(); + private static volatile Map itemMaterials = Map + .of(); + private static volatile Map blockDefinitions = Map + .of(); + private static volatile Map blockMaterials = Map + .of(); + + /** + * Item registry ID to its loaded definition / owning material; complete + * with {@link #loadedMaterials()}. + */ + public static Map itemDefinitions() { + return itemDefinitions; + } + + /** + * Item registry ID to its owning material; complete with + * {@link #loadedMaterials()}. + */ + public static Map itemMaterials() { + return itemMaterials; + } + + /** + * Complete after deferred construction, before definition registry events. + */ + public static Map loadedMaterials() { + return loadedMaterials; + } + + /** + * Standalone (material-less) blocks by registry ID; each result's material + * is a stats-free host. + */ + public static Map standaloneBlocks() { + return standaloneBlocks; + } + + /** + * Standalone (material-less) items by registry ID; each result's material + * is a stats-free host. + */ + public static Map standaloneItems() { + return standaloneItems; + } + + /** + * Called by NeoForge; registers MMDLib's config, registers and listeners on + * {@code modBus}. + */ + public MMDLib(IEventBus modBus, ModContainer container) { + container.registerConfig(ModConfig.Type.SERVER, + MMDLibConfig.SERVER_SPEC); + zone.moddev.mc.lib.gui.runtime.ScreenRuntime.register(modBus); + zone.moddev.mc.lib.machine.neoforge.MachineContent.register(modBus); + BLOCKS.register(modBus); + ITEMS.register(modBus); + machineTypes.register(modBus); + runtime = new zone.moddev.mc.lib.common.runtime.RuntimeContentRegistrar( MODID, MaterialExtensions.GLOBAL, modBus); - zone.moddev.mc.lib.common.recipes.CrushingRecipe.TYPES.register(modBus); - zone.moddev.mc.lib.common.recipes.CrushingRecipe.SERIALIZERS.register(modBus); - zone.moddev.mc.lib.machine.neoforge.MachineComponents.TYPES.register(modBus); - NeoForge.EVENT_BUS.addListener( - zone.moddev.mc.lib.machine.neoforge.MachinePartsLoader::register); - modBus.addListener(zone.moddev.mc.lib.util.Registries::register); - zone.moddev.mc.lib.common.fluid.FluidBottles.register(modBus); - - if (ModList.get().isLoaded("crafttweaker")) CraftTweakerInit.register(modBus); - - modBus.addListener(this::publishDefinitions); - modBus.addListener((FMLConstructModEvent event) -> event.enqueueWork(this::loadDefinitions)); - new ItemEventBridge( - MaterialExtensions.GLOBAL.events(), - id -> itemDefinitions.get(id), - id -> itemMaterials.get(id), - id -> blockDefinitions.get(id), - id -> blockMaterials.get(id)) - .register(NeoForge.EVENT_BUS); - } - - private void loadDefinitions() { - var roots = new ArrayList(); - for (var info : ModList.get().getModFiles()) { - var file = info.getFile(); - var root = file.findResource("mmdlib"); - if (!Files.isDirectory(root) || info.getMods().isEmpty()) continue; - // A root-level directory belongs to the first mod declared by this physical mod file. - var namespace = info.getMods().getFirst().getModId(); - var module = - FMLLoader.getGameLayer() - .findModule(info.moduleName()) - .orElseThrow( - () -> - new IllegalStateException( - "Cannot find module for definitions in " + file.getFileName())); - roots.add(new DefinitionLoader.SourceRoot(namespace, root, module.getClassLoader())); + zone.moddev.mc.lib.common.recipes.CrushingRecipe.TYPES.register(modBus); + zone.moddev.mc.lib.common.recipes.CrushingRecipe.SERIALIZERS + .register(modBus); + zone.moddev.mc.lib.machine.neoforge.MachineComponents.TYPES + .register(modBus); + NeoForge.EVENT_BUS.addListener( + zone.moddev.mc.lib.machine.neoforge.MachinePartsLoader::register); + modBus.addListener(zone.moddev.mc.lib.util.Registries::register); + zone.moddev.mc.lib.common.fluid.FluidBottles.register(modBus); + + if (ModList.get().isLoaded("crafttweaker")) + CraftTweakerInit.register(modBus); + + modBus.addListener(this::publishDefinitions); + modBus.addListener((FMLConstructModEvent event) -> event + .enqueueWork(this::loadDefinitions)); + new ItemEventBridge(MaterialExtensions.GLOBAL.events(), + id -> itemDefinitions.get(id), id -> itemMaterials.get(id), + id -> blockDefinitions.get(id), id -> blockMaterials.get(id)) + .register(NeoForge.EVENT_BUS); } - var loaded = - DefinitionLoader.load(roots, MaterialExtensions.GLOBAL, id -> ModList.get().isLoaded(id)); - var definitions = new java.util.HashMap(); - var materials = new java.util.HashMap(); - var blocks = new java.util.HashMap(); - var blockOwners = new java.util.HashMap(); - var results = new ArrayList(loaded.materials().values()); - results.addAll(loaded.standalone().values()); - results.addAll(loaded.standaloneItems().values()); - for (var result : results) { - var plan = result.content(); - var material = result.material(); - // Standard parts and registered custom types; custom blocks contribute their paired items. - var items = new ArrayList(plan.items().values()); - items.addAll(plan.extraItems().values()); - var allBlocks = new ArrayList(plan.blocks().values()); - allBlocks.addAll(plan.extraBlocks().values()); - for (var block : allBlocks) { - blocks.put(block.name(), block); - blockOwners.put(block.name(), material); - if (block.item() != null) items.add(block.item()); - } - for (var item : items) { - definitions.put(item.name(), item); - materials.put(item.name(), material); - } + + private void loadDefinitions() { + var roots = new ArrayList(); + for (var info : ModList.get().getModFiles()) { + var file = info.getFile(); + var root = file.findResource("mmdlib"); + if (!Files.isDirectory(root) || info.getMods().isEmpty()) + continue; + // A root-level directory belongs to the first mod declared by this + // physical mod file. + var namespace = info.getMods().getFirst().getModId(); + var module = FMLLoader.getGameLayer().findModule(info.moduleName()) + .orElseThrow(() -> new IllegalStateException( + "Cannot find module for definitions in " + + file.getFileName())); + roots.add(new DefinitionLoader.SourceRoot(namespace, root, + module.getClassLoader())); + } + var loaded = DefinitionLoader.load(roots, MaterialExtensions.GLOBAL, + id -> ModList.get().isLoaded(id)); + var definitions = new java.util.HashMap(); + var materials = new java.util.HashMap(); + var blocks = new java.util.HashMap(); + var blockOwners = new java.util.HashMap(); + var results = new ArrayList( + loaded.materials().values()); + results.addAll(loaded.standalone().values()); + results.addAll(loaded.standaloneItems().values()); + for (var result : results) { + var plan = result.content(); + var material = result.material(); + // Standard parts and registered custom types; custom blocks + // contribute their paired + // items. + var items = new ArrayList(plan.items().values()); + items.addAll(plan.extraItems().values()); + var allBlocks = new ArrayList(plan.blocks().values()); + allBlocks.addAll(plan.extraBlocks().values()); + for (var block : allBlocks) { + blocks.put(block.name(), block); + blockOwners.put(block.name(), material); + if (block.item() != null) + items.add(block.item()); + } + for (var item : items) { + definitions.put(item.name(), item); + materials.put(item.name(), material); + } + } + itemDefinitions = Map.copyOf(definitions); + itemMaterials = Map.copyOf(materials); + blockDefinitions = Map.copyOf(blocks); + blockMaterials = Map.copyOf(blockOwners); + loadedMaterials = loaded.materials(); + standaloneBlocks = loaded.standalone(); + standaloneItems = loaded.standaloneItems(); + results.stream().filter(result -> result.material().name().orElseThrow() + .getNamespace().equals(MODID)).forEach(runtime::queue); + runtime.blockEntityHandles().values() + .forEach(handle -> handle.register(machineTypes)); + zone.moddev.mc.lib.machine.neoforge.MachineContent.load(roots); + zone.moddev.mc.lib.gui.runtime.ScreenRuntime.validate(); + LOGGER.info( + "Loaded MMDLib definitions: {} item modules, {} block modules, {} fluid modules, {}" + + " extension files, {} materials, {} standalone blocks, {} standalone items, {}" + + " screens", + loaded.modules().items().size(), loaded.modules().blocks().size(), + loaded.modules().fluids().size(), loaded.extensions().size(), + loadedMaterials.size(), standaloneBlocks.size(), + standaloneItems.size(), + zone.moddev.mc.lib.gui.Screens.all().size()); + } + + private void publishDefinitions(RegisterEvent event) { + MaterialRegistration.publishCallbacks(event, MaterialExtensions.GLOBAL); + loadedMaterials.values().forEach( + result -> MaterialRegistration.publishDefinitions(event, result)); + standaloneBlocks.values().forEach( + result -> MaterialRegistration.publishStandalone(event, result)); + standaloneItems.values().forEach( + result -> MaterialRegistration.publishStandalone(event, result)); } - itemDefinitions = Map.copyOf(definitions); - itemMaterials = Map.copyOf(materials); - blockDefinitions = Map.copyOf(blocks); - blockMaterials = Map.copyOf(blockOwners); - loadedMaterials = loaded.materials(); - standaloneBlocks = loaded.standalone(); - standaloneItems = loaded.standaloneItems(); - results.stream() - .filter(result -> result.material().name().orElseThrow().getNamespace().equals(MODID)) - .forEach(runtime::queue); - runtime.blockEntityHandles().values().forEach(handle -> handle.register(machineTypes)); - zone.moddev.mc.lib.machine.neoforge.MachineContent.load(roots); - zone.moddev.mc.lib.gui.runtime.ScreenRuntime.validate(); - LOGGER.info( - "Loaded MMDLib definitions: {} item modules, {} block modules, {} fluid modules, {}" - + " extension files, {} materials, {} standalone blocks, {} standalone items, {}" - + " screens", - loaded.modules().items().size(), - loaded.modules().blocks().size(), - loaded.modules().fluids().size(), - loaded.extensions().size(), - loadedMaterials.size(), - standaloneBlocks.size(), - standaloneItems.size(), - zone.moddev.mc.lib.gui.Screens.all().size()); - } - - private void publishDefinitions(RegisterEvent event) { - MaterialRegistration.publishCallbacks(event, MaterialExtensions.GLOBAL); - loadedMaterials - .values() - .forEach(result -> MaterialRegistration.publishDefinitions(event, result)); - standaloneBlocks - .values() - .forEach(result -> MaterialRegistration.publishStandalone(event, result)); - standaloneItems - .values() - .forEach(result -> MaterialRegistration.publishStandalone(event, result)); - } } diff --git a/src/main/java/zone/moddev/mc/lib/MMDLibConfig.java b/src/main/java/zone/moddev/mc/lib/MMDLibConfig.java index df8d8f2a..6e00c4ba 100644 --- a/src/main/java/zone/moddev/mc/lib/MMDLibConfig.java +++ b/src/main/java/zone/moddev/mc/lib/MMDLibConfig.java @@ -2,42 +2,45 @@ import net.neoforged.neoforge.common.ModConfigSpec; -/** Server-side (per-world) settings; the server is authoritative for world-changing handlers. */ +/** + * Server-side (per-world) settings; the server is authoritative for + * world-changing handlers. + */ public final class MMDLibConfig { - private MMDLibConfig() {} + private MMDLibConfig() { + } - /** The server config spec, registered by {@link MMDLib}. */ - public static final ModConfigSpec SERVER_SPEC; + /** The server config spec, registered by {@link MMDLib}. */ + public static final ModConfigSpec SERVER_SPEC; - /** - * {@code crackhammer.crush_count}: how many items of a dropped stack a crackhammer crushes per - * right-click (1-99, default 8). - */ - public static final ModConfigSpec.IntValue CRACKHAMMER_CRUSH_COUNT; + /** + * {@code crackhammer.crush_count}: how many items of a dropped stack a + * crackhammer crushes per right-click (1-99, default 8). + */ + public static final ModConfigSpec.IntValue CRACKHAMMER_CRUSH_COUNT; - /** Functional Curios slot types searched for equipped item containers, in priority order. */ - public static final ModConfigSpec.ConfigValue> CONTAINER_SLOTS; + /** + * Functional Curios slot types searched for equipped item containers, in + * priority order. + */ + public static final ModConfigSpec.ConfigValue> CONTAINER_SLOTS; - static { - var builder = new ModConfigSpec.Builder(); - builder.push("crackhammer"); - CRACKHAMMER_CRUSH_COUNT = - builder - .comment( - "Items crushed from a dropped stack per right-click, at 1 durability each. Sneaking" - + " crushes the whole stack.") + static { + var builder = new ModConfigSpec.Builder(); + builder.push("crackhammer"); + CRACKHAMMER_CRUSH_COUNT = builder.comment( + "Items crushed from a dropped stack per right-click, at 1 durability each. Sneaking" + + " crushes the whole stack.") .defineInRange("crush_count", 8, 1, 99); - builder.pop(); - builder.push("inventory"); - CONTAINER_SLOTS = - builder - .comment("Curios slot types eligible for the machine's container drawer.") - .defineListAllowEmpty( - "container_slots", - java.util.List.of("back", "backpack"), - () -> "back", - value -> value instanceof String name && !name.isBlank() && name.length() <= 64); - builder.pop(); - SERVER_SPEC = builder.build(); - } + builder.pop(); + builder.push("inventory"); + CONTAINER_SLOTS = builder.comment( + "Curios slot types eligible for the machine's container drawer.") + .defineListAllowEmpty("container_slots", + java.util.List.of("back", "backpack"), () -> "back", + value -> value instanceof String name && !name.isBlank() + && name.length() <= 64); + builder.pop(); + SERVER_SPEC = builder.build(); + } } diff --git a/src/main/java/zone/moddev/mc/lib/api/BlockContext.java b/src/main/java/zone/moddev/mc/lib/api/BlockContext.java index 09056148..ae9cf70a 100644 --- a/src/main/java/zone/moddev/mc/lib/api/BlockContext.java +++ b/src/main/java/zone/moddev/mc/lib/api/BlockContext.java @@ -12,53 +12,63 @@ import zone.moddev.mc.lib.common.types.MaterialPart; /** - * Everything the runtime registrar knows when it creates one material block. Passed to {@link - * BlockImplementation#create(BlockContext)}; MMDLib's own blocks are built from exactly this. + * Everything the runtime registrar knows when it creates one material block. + * Passed to {@link BlockImplementation#create(BlockContext)}; MMDLib's own + * blocks are built from exactly this. */ public interface BlockContext { - /** - * The material the block belongs to; for a standalone block, a stats-free host material named - * after the block. - */ - MMDMaterial material(); + /** + * The material the block belongs to; for a standalone block, a stats-free + * host material named after the block. + */ + MMDMaterial material(); - /** The planned block definition: registry name, template properties and event bindings. */ - MMDBlock definition(); + /** + * The planned block definition: registry name, template properties and + * event bindings. + */ + MMDBlock definition(); - /** The part being built, with its module ID and metadata. */ - MaterialPart part(); + /** The part being built, with its module ID and metadata. */ + MaterialPart part(); - /** The module's {@code factory_data}, merged with any per-use overrides. */ - default FactoryData factoryData() { - return part().factoryData(); - } + /** The module's {@code factory_data}, merged with any per-use overrides. */ + default FactoryData factoryData() { + return part().factoryData(); + } - /** - * Fresh properties from the definition and the material's runtime settings (sound, map color). - */ - BlockBehaviour.Properties properties(); + /** + * Fresh properties from the definition and the material's runtime settings + * (sound, map color). + */ + BlockBehaviour.Properties properties(); - /** {@code factory_data.block_set_type} when present, otherwise the material's block set type. */ - BlockSetType blockSetType(); + /** + * {@code factory_data.block_set_type} when present, otherwise the + * material's block set type. + */ + BlockSetType blockSetType(); - /** The material's own block set type, ignoring {@code factory_data}. */ - BlockSetType materialBlockSetType(); + /** The material's own block set type, ignoring {@code factory_data}. */ + BlockSetType materialBlockSetType(); - /** The material's base block state, resolved lazily (e.g. for stairs). */ - Supplier baseBlock(); + /** The material's base block state, resolved lazily (e.g. for stairs). */ + Supplier baseBlock(); - /** - * The event registry, for blocks that dispatch their definition's bindings (e.g. {@code - * redstone-changed}). - */ - MaterialEvents events(); + /** + * The event registry, for blocks that dispatch their definition's bindings + * (e.g. {@code + * redstone-changed}). + */ + MaterialEvents events(); - /** - * The block entity queued for this block, when its part declares one ({@code part.block_entity}). - * A block implementation for such a part must create an {@code EntityBlock} wired to it, e.g. - * with {@link BlockEntityBinding#newBlock}. - */ - default java.util.Optional> blockEntity() { - return java.util.Optional.empty(); - } + /** + * The block entity queued for this block, when its part declares one + * ({@code part.block_entity}). A block implementation for such a part must + * create an {@code EntityBlock} wired to it, e.g. with + * {@link BlockEntityBinding#newBlock}. + */ + default java.util.Optional> blockEntity() { + return java.util.Optional.empty(); + } } diff --git a/src/main/java/zone/moddev/mc/lib/api/BlockEntityBinding.java b/src/main/java/zone/moddev/mc/lib/api/BlockEntityBinding.java index f34ffab8..cb4596f6 100644 --- a/src/main/java/zone/moddev/mc/lib/api/BlockEntityBinding.java +++ b/src/main/java/zone/moddev/mc/lib/api/BlockEntityBinding.java @@ -11,49 +11,59 @@ import zone.moddev.mc.lib.common.runtime.MaterialEntityBlock; /** - * The block entity the runtime registrar queued for a block, handed to its {@link - * BlockImplementation} through {@link BlockContext#blockEntity()}. The block must implement {@code - * EntityBlock} and delegate {@code newBlockEntity} and {@code getTicker} to {@link #prepared()}, as - * {@link MaterialEntityBlock} does; {@link #newBlock} builds one. + * The block entity the runtime registrar queued for a block, handed to its + * {@link BlockImplementation} through {@link BlockContext#blockEntity()}. The + * block must implement {@code + * EntityBlock} and delegate {@code newBlockEntity} and {@code getTicker} to + * {@link #prepared()}, as {@link MaterialEntityBlock} does; {@link #newBlock} + * builds one. * - * @param prepared the block-entity factory with its decoded settings - * @param type supplies the registered block-entity type; read it only once registration has run, - * e.g. when a block entity is created, never in the block's constructor - * @param the block-entity class + * @param prepared + * the block-entity factory with its decoded settings + * @param type + * supplies the registered block-entity type; read it only once + * registration has run, e.g. when a block entity is created, never + * in the block's constructor + * @param + * the block-entity class */ public record BlockEntityBinding( - BlockEntityFactories.Prepared prepared, Supplier> type) { - /** Requires both components. */ - public BlockEntityBinding { - Objects.requireNonNull(prepared); - Objects.requireNonNull(type); - } + BlockEntityFactories.Prepared prepared, + Supplier> type) { + /** Requires both components. */ + public BlockEntityBinding { + Objects.requireNonNull(prepared); + Objects.requireNonNull(type); + } - /** The class every block entity of this binding has. */ - public Class blockEntityClass() { - return prepared.blockEntityClass(); - } + /** The class every block entity of this binding has. */ + public Class blockEntityClass() { + return prepared.blockEntityClass(); + } - /** - * This binding typed for {@code expected}, for a block class written for one block-entity class. - * - * @throws IllegalArgumentException if the factory creates a different class - */ - @SuppressWarnings("unchecked") - public BlockEntityBinding as(Class expected) { - if (prepared.blockEntityClass() != expected) - throw new IllegalArgumentException( - "Block entity " - + prepared.definition().name() - + " is " - + prepared.blockEntityClass().getName() - + ", not " - + expected.getName()); - return (BlockEntityBinding) this; - } + /** + * This binding typed for {@code expected}, for a block class written for + * one block-entity class. + * + * @throws IllegalArgumentException + * if the factory creates a different class + */ + @SuppressWarnings("unchecked") + public BlockEntityBinding as(Class expected) { + if (prepared.blockEntityClass() != expected) + throw new IllegalArgumentException( + "Block entity " + prepared.definition().name() + " is " + + prepared.blockEntityClass().getName() + ", not " + + expected.getName()); + return (BlockEntityBinding) this; + } - /** A plain machine block with {@code properties} whose block entity comes from this binding. */ - public MaterialEntityBlock newBlock(BlockBehaviour.Properties properties) { - return new MaterialEntityBlock<>(properties, prepared, type); - } + /** + * A plain machine block with {@code properties} whose block entity comes + * from this binding. + */ + public MaterialEntityBlock newBlock( + BlockBehaviour.Properties properties) { + return new MaterialEntityBlock<>(properties, prepared, type); + } } diff --git a/src/main/java/zone/moddev/mc/lib/api/BlockEntityHandle.java b/src/main/java/zone/moddev/mc/lib/api/BlockEntityHandle.java index f77e3829..b4090c68 100644 --- a/src/main/java/zone/moddev/mc/lib/api/BlockEntityHandle.java +++ b/src/main/java/zone/moddev/mc/lib/api/BlockEntityHandle.java @@ -12,56 +12,55 @@ import zone.moddev.mc.lib.common.factories.BlockEntityFactories; /** - * Prepared machine type. Its owning mod explicitly supplies the register; MMDLib wires the result. + * Prepared machine type. Its owning mod explicitly supplies the register; + * MMDLib wires the result. */ public final class BlockEntityHandle implements Supplier> { - private final BlockEntityFactories factories; - private final BlockEntityFactories.Prepared prepared; - private final Supplier block; - private DeferredHolder, BlockEntityType> holder; + private final BlockEntityFactories factories; + private final BlockEntityFactories.Prepared prepared; + private final Supplier block; + private DeferredHolder, BlockEntityType> holder; - public BlockEntityHandle( - BlockEntityFactories factories, - BlockEntityFactories.Prepared prepared, - Supplier block) { - this.factories = Objects.requireNonNull(factories); - this.prepared = Objects.requireNonNull(prepared); - this.block = Objects.requireNonNull(block); - } + public BlockEntityHandle(BlockEntityFactories factories, + BlockEntityFactories.Prepared prepared, + Supplier block) { + this.factories = Objects.requireNonNull(factories); + this.prepared = Objects.requireNonNull(prepared); + this.block = Objects.requireNonNull(block); + } - public BlockEntityFactories.Prepared prepared() { - return prepared; - } + public BlockEntityFactories.Prepared prepared() { + return prepared; + } - public synchronized DeferredHolder, BlockEntityType> register( - DeferredRegister> register) { - if (holder != null) - throw new IllegalStateException( - "Block entity already registered: " + prepared.definition().name()); - holder = factories.queue(register, prepared, block); - return holder; - } + public synchronized DeferredHolder, BlockEntityType> register( + DeferredRegister> register) { + if (holder != null) + throw new IllegalStateException("Block entity already registered: " + + prepared.definition().name()); + holder = factories.queue(register, prepared, block); + return holder; + } - public Optional, BlockEntityType>> holder() { - return Optional.ofNullable(holder); - } + public Optional, BlockEntityType>> holder() { + return Optional.ofNullable(holder); + } - public void verifyRegistered() { - if (holder == null || !holder.isBound()) - throw new IllegalStateException( - "Unregistered block entity handle: " - + prepared.definition().name() - + "; pass the owning mod's block-entity register to handle.register(register)"); - } + public void verifyRegistered() { + if (holder == null || !holder.isBound()) + throw new IllegalStateException("Unregistered block entity handle: " + + prepared.definition().name() + + "; pass the owning mod's block-entity register to handle.register(register)"); + } - @Override - public BlockEntityType get() { - verifyRegistered(); - return holder.get(); - } + @Override + public BlockEntityType get() { + verifyRegistered(); + return holder.get(); + } - public BlockEntityBinding binding() { - return new BlockEntityBinding<>(prepared, this); - } + public BlockEntityBinding binding() { + return new BlockEntityBinding<>(prepared, this); + } } diff --git a/src/main/java/zone/moddev/mc/lib/api/BlockImplementation.java b/src/main/java/zone/moddev/mc/lib/api/BlockImplementation.java index 262674ac..49bdfe7f 100644 --- a/src/main/java/zone/moddev/mc/lib/api/BlockImplementation.java +++ b/src/main/java/zone/moddev/mc/lib/api/BlockImplementation.java @@ -9,57 +9,67 @@ import zone.moddev.mc.lib.common.types.MaterialPart; /** - * The Java code behind a block data module. A module's {@code factory} names an implementation - * registered with {@link MaterialExtensions#registerBlockImplementation}; the runtime registrar - * calls {@link #create(BlockContext)} once for every material (or standalone block) that selects - * the module. + * The Java code behind a block data module. A module's {@code factory} names an + * implementation registered with + * {@link MaterialExtensions#registerBlockImplementation}; the runtime registrar + * calls {@link #create(BlockContext)} once for every material (or standalone + * block) that selects the module. * - *

Override {@link #create(BlockContext)} for new code. The older {@code create} overloads remain - * for implementations written before contexts existed. + *

+ * Override {@link #create(BlockContext)} for new code. The older {@code create} + * overloads remain for implementations written before contexts existed. * - * @param the block class created + * @param + * the block class created */ public interface BlockImplementation { - /** The concrete class every {@code create} call returns. */ - Class blockClass(); + /** The concrete class every {@code create} call returns. */ + Class blockClass(); - /** - * Creates the block from its material and planned definition. Implementations that override - * {@link #create(BlockContext)} may throw here instead. - */ - B create(MMDMaterial material, MMDBlock definition); + /** + * Creates the block from its material and planned definition. + * Implementations that override {@link #create(BlockContext)} may throw + * here instead. + */ + B create(MMDMaterial material, MMDBlock definition); - /** - * As {@link #create(MMDMaterial, MMDBlock)}, with the module's {@code factory_data}. The default - * refuses any non-empty data, so an implementation that takes settings must override this or - * {@link #create(BlockContext)} and decode them. - */ - default B create( - MMDMaterial material, MMDBlock definition, zone.moddev.mc.lib.common.types.FactoryData data) { - if (!data.equals(zone.moddev.mc.lib.common.types.FactoryData.EMPTY)) - throw new IllegalArgumentException("Implementation must explicitly decode its factory_data"); - return create(material, definition); - } + /** + * As {@link #create(MMDMaterial, MMDBlock)}, with the module's + * {@code factory_data}. The default refuses any non-empty data, so an + * implementation that takes settings must override this or + * {@link #create(BlockContext)} and decode them. + */ + default B create(MMDMaterial material, MMDBlock definition, + zone.moddev.mc.lib.common.types.FactoryData data) { + if (!data.equals(zone.moddev.mc.lib.common.types.FactoryData.EMPTY)) + throw new IllegalArgumentException( + "Implementation must explicitly decode its factory_data"); + return create(material, definition); + } - /** - * Called by the runtime registrar; override to use the prepared properties, block set type, or - * base block. - */ - default B create(BlockContext context) { - return create(context.material(), context.definition(), context.factoryData()); - } + /** + * Called by the runtime registrar; override to use the prepared properties, + * block set type, or base block. + */ + default B create(BlockContext context) { + return create(context.material(), context.definition(), + context.factoryData()); + } - /** - * Decode {@code factory_data} eagerly so malformed module data fails at load instead of registry - * construction. - */ - default void validate(MaterialPart part) {} + /** + * Decode {@code factory_data} eagerly so malformed module data fails at + * load instead of registry construction. + */ + default void validate(MaterialPart part) { + } - /** - * The block's item, when its definition has one; the default burns for the material's {@code - * block_burn_time}. - */ - default Item createItem(Block block, ItemContext context) { - return new MaterialBlockItem(block, context.properties(), context.material()); - } + /** + * The block's item, when its definition has one; the default burns for the + * material's {@code + * block_burn_time}. + */ + default Item createItem(Block block, ItemContext context) { + return new MaterialBlockItem(block, context.properties(), + context.material()); + } } diff --git a/src/main/java/zone/moddev/mc/lib/api/BlockTypeFactory.java b/src/main/java/zone/moddev/mc/lib/api/BlockTypeFactory.java index 04ad06be..927fa186 100644 --- a/src/main/java/zone/moddev/mc/lib/api/BlockTypeFactory.java +++ b/src/main/java/zone/moddev/mc/lib/api/BlockTypeFactory.java @@ -7,29 +7,37 @@ import zone.moddev.mc.lib.common.types.MMDMaterial; /** - * A code-registered block type: a material part defined in Java instead of a data module. - * Registered with {@link MaterialExtensions#registerBlockType}, it can then be selected in a - * material's {@code blocks} section like any other part, and is created with {@link - * MaterialExtensions#queueBlock}. + * A code-registered block type: a material part defined in Java instead of a + * data module. Registered with {@link MaterialExtensions#registerBlockType}, it + * can then be selected in a material's {@code blocks} section like any other + * part, and is created with {@link MaterialExtensions#queueBlock}. * - *

New content should normally prefer a data module with a {@link BlockImplementation}; this - * interface is for parts whose template depends on code. + *

+ * New content should normally prefer a data module with a + * {@link BlockImplementation}; this interface is for parts whose template + * depends on code. * - * @param the block class created + * @param + * the block class created */ -public interface BlockTypeFactory extends BlockImplementation { - /** The concrete class every {@link #create} call returns; checked when queueing. */ - Class blockClass(); +public interface BlockTypeFactory + extends BlockImplementation { + /** + * The concrete class every {@link #create} call returns; checked when + * queueing. + */ + Class blockClass(); - /** - * The block definition template for {@code material}, before MMDLib sets its name, material and - * type. May use the material's stats. - */ - BlockBuilder template(MMDMaterial material); + /** + * The block definition template for {@code material}, before MMDLib sets + * its name, material and type. May use the material's stats. + */ + BlockBuilder template(MMDMaterial material); - /** - * Creates a fresh block for {@code material} from its planned {@code definition}. Called by the - * deferred register supplier, once per registration. - */ - B create(MMDMaterial material, MMDBlock definition); + /** + * Creates a fresh block for {@code material} from its planned + * {@code definition}. Called by the deferred register supplier, once per + * registration. + */ + B create(MMDMaterial material, MMDBlock definition); } diff --git a/src/main/java/zone/moddev/mc/lib/api/ItemContext.java b/src/main/java/zone/moddev/mc/lib/api/ItemContext.java index 30b3d1b8..7c3af6be 100644 --- a/src/main/java/zone/moddev/mc/lib/api/ItemContext.java +++ b/src/main/java/zone/moddev/mc/lib/api/ItemContext.java @@ -15,42 +15,49 @@ import zone.moddev.mc.lib.common.types.MaterialPart; /** - * Everything the runtime registrar knows when it creates one material item. Passed to {@link - * ItemImplementation#create(ItemContext)}; MMDLib's own items are built from exactly this. + * Everything the runtime registrar knows when it creates one material item. + * Passed to {@link ItemImplementation#create(ItemContext)}; MMDLib's own items + * are built from exactly this. */ public interface ItemContext { - /** The material the item belongs to. */ - MMDMaterial material(); - - /** The planned item definition: registry name, template properties and event bindings. */ - MMDItem definition(); - - /** The part being built, with its module ID and metadata. */ - MaterialPart part(); - - /** The module's {@code factory_data}. */ - default FactoryData factoryData() { - return part().factoryData(); - } - - /** Fresh properties from the definition (stack size, durability, fire resistance, container). */ - Item.Properties properties(); - - /** - * The material's tool tier: durability, speed, damage, enchantability, repair item, - * incorrect-block tag. - */ - MaterialToolTier tier(); - - /** - * The material's armor material; available only when the implementation reports {@link - * ItemImplementation#usesArmorMaterial()}. - */ - Holder armor(); - - /** - * The projectile type registered for this part's {@code entity_factory}; available only for parts - * that declare one. - */ - Supplier> projectile(); + /** The material the item belongs to. */ + MMDMaterial material(); + + /** + * The planned item definition: registry name, template properties and event + * bindings. + */ + MMDItem definition(); + + /** The part being built, with its module ID and metadata. */ + MaterialPart part(); + + /** The module's {@code factory_data}. */ + default FactoryData factoryData() { + return part().factoryData(); + } + + /** + * Fresh properties from the definition (stack size, durability, fire + * resistance, container). + */ + Item.Properties properties(); + + /** + * The material's tool tier: durability, speed, damage, enchantability, + * repair item, incorrect-block tag. + */ + MaterialToolTier tier(); + + /** + * The material's armor material; available only when the implementation + * reports {@link ItemImplementation#usesArmorMaterial()}. + */ + Holder armor(); + + /** + * The projectile type registered for this part's {@code entity_factory}; + * available only for parts that declare one. + */ + Supplier> projectile(); } diff --git a/src/main/java/zone/moddev/mc/lib/api/ItemImplementation.java b/src/main/java/zone/moddev/mc/lib/api/ItemImplementation.java index bdd1cf26..dcab66c3 100644 --- a/src/main/java/zone/moddev/mc/lib/api/ItemImplementation.java +++ b/src/main/java/zone/moddev/mc/lib/api/ItemImplementation.java @@ -7,56 +7,65 @@ import zone.moddev.mc.lib.common.types.MaterialPart; /** - * The Java code behind an item data module. A module's {@code factory} names an implementation - * registered with {@link MaterialExtensions#registerItemImplementation}; the runtime registrar - * calls {@link #create(ItemContext)} once for every material that selects the module. + * The Java code behind an item data module. A module's {@code factory} names an + * implementation registered with + * {@link MaterialExtensions#registerItemImplementation}; the runtime registrar + * calls {@link #create(ItemContext)} once for every material that selects the + * module. * - *

Override {@link #create(ItemContext)} for new code. The older {@code create} overloads remain - * for implementations written before contexts existed. + *

+ * Override {@link #create(ItemContext)} for new code. The older {@code create} + * overloads remain for implementations written before contexts existed. * - * @param the item class created + * @param + * the item class created */ public interface ItemImplementation { - /** The concrete class every {@code create} call returns. */ - Class itemClass(); - - /** - * Creates the item from its material and planned definition. Implementations that override {@link - * #create(ItemContext)} may throw here instead. - */ - I create(MMDMaterial material, MMDItem definition); - - /** - * As {@link #create(MMDMaterial, MMDItem)}, with the module's {@code factory_data}. The default - * refuses any non-empty data, so an implementation that takes settings must override this or - * {@link #create(ItemContext)} and decode them. - */ - default I create( - MMDMaterial material, MMDItem definition, zone.moddev.mc.lib.common.types.FactoryData data) { - if (!data.equals(zone.moddev.mc.lib.common.types.FactoryData.EMPTY)) - throw new IllegalArgumentException("Implementation must explicitly decode its factory_data"); - return create(material, definition); - } - - /** - * Called by the runtime registrar; override to use the material's tier, armor, projectile, or - * prepared properties. - */ - default I create(ItemContext context) { - return create(context.material(), context.definition(), context.factoryData()); - } - - /** - * Decode {@code factory_data} eagerly so malformed module data fails at load instead of registry - * construction. - */ - default void validate(MaterialPart part) {} - - /** - * Whether {@link ItemContext#armor()} is needed; the material's armor material is registered only - * when some part asks. - */ - default boolean usesArmorMaterial() { - return false; - } + /** The concrete class every {@code create} call returns. */ + Class itemClass(); + + /** + * Creates the item from its material and planned definition. + * Implementations that override {@link #create(ItemContext)} may throw here + * instead. + */ + I create(MMDMaterial material, MMDItem definition); + + /** + * As {@link #create(MMDMaterial, MMDItem)}, with the module's + * {@code factory_data}. The default refuses any non-empty data, so an + * implementation that takes settings must override this or + * {@link #create(ItemContext)} and decode them. + */ + default I create(MMDMaterial material, MMDItem definition, + zone.moddev.mc.lib.common.types.FactoryData data) { + if (!data.equals(zone.moddev.mc.lib.common.types.FactoryData.EMPTY)) + throw new IllegalArgumentException( + "Implementation must explicitly decode its factory_data"); + return create(material, definition); + } + + /** + * Called by the runtime registrar; override to use the material's tier, + * armor, projectile, or prepared properties. + */ + default I create(ItemContext context) { + return create(context.material(), context.definition(), + context.factoryData()); + } + + /** + * Decode {@code factory_data} eagerly so malformed module data fails at + * load instead of registry construction. + */ + default void validate(MaterialPart part) { + } + + /** + * Whether {@link ItemContext#armor()} is needed; the material's armor + * material is registered only when some part asks. + */ + default boolean usesArmorMaterial() { + return false; + } } diff --git a/src/main/java/zone/moddev/mc/lib/api/ItemTypeFactory.java b/src/main/java/zone/moddev/mc/lib/api/ItemTypeFactory.java index 2bc54593..7fb46234 100644 --- a/src/main/java/zone/moddev/mc/lib/api/ItemTypeFactory.java +++ b/src/main/java/zone/moddev/mc/lib/api/ItemTypeFactory.java @@ -7,28 +7,37 @@ import zone.moddev.mc.lib.common.types.MMDMaterial; /** - * A code-registered item type: a material part defined in Java instead of a data module. Registered - * with {@link MaterialExtensions#registerItemType}, it can then be selected in a material's {@code - * items} section like any other part, and is created with {@link MaterialExtensions#queueItem}. + * A code-registered item type: a material part defined in Java instead of a + * data module. Registered with {@link MaterialExtensions#registerItemType}, it + * can then be selected in a material's {@code + * items} section like any other part, and is created with + * {@link MaterialExtensions#queueItem}. * - *

New content should normally prefer a data module with an {@link ItemImplementation}; this - * interface is for parts whose template depends on code. + *

+ * New content should normally prefer a data module with an + * {@link ItemImplementation}; this interface is for parts whose template + * depends on code. * - * @param the item class created + * @param + * the item class created */ public interface ItemTypeFactory extends ItemImplementation { - /** The concrete class every {@link #create} call returns; checked when queueing. */ - Class itemClass(); + /** + * The concrete class every {@link #create} call returns; checked when + * queueing. + */ + Class itemClass(); - /** - * The item definition template for {@code material}, before MMDLib sets its name, material and - * type. May use the material's stats. - */ - ItemBuilder template(MMDMaterial material); + /** + * The item definition template for {@code material}, before MMDLib sets its + * name, material and type. May use the material's stats. + */ + ItemBuilder template(MMDMaterial material); - /** - * Creates a fresh item for {@code material} from its planned {@code definition}. Called by the - * deferred register supplier, once per registration. - */ - I create(MMDMaterial material, MMDItem definition); + /** + * Creates a fresh item for {@code material} from its planned + * {@code definition}. Called by the deferred register supplier, once per + * registration. + */ + I create(MMDMaterial material, MMDItem definition); } diff --git a/src/main/java/zone/moddev/mc/lib/api/MaterialEvent.java b/src/main/java/zone/moddev/mc/lib/api/MaterialEvent.java index 1fb4a90b..77e62f12 100644 --- a/src/main/java/zone/moddev/mc/lib/api/MaterialEvent.java +++ b/src/main/java/zone/moddev/mc/lib/api/MaterialEvent.java @@ -3,25 +3,32 @@ import java.util.Objects; /** - * A typed event key, such as {@code player-tick}. JSON bindings refer to it by {@link #name()}; - * Java code uses the key itself, so the compiler checks that callbacks and dispatches agree on the - * context type. + * A typed event key, such as {@code player-tick}. JSON bindings refer to it by + * {@link #name()}; Java code uses the key itself, so the compiler checks that + * callbacks and dispatches agree on the context type. * - *

Keys are created by {@link MaterialEvents#registerEvent}; the built-in ones are in {@link - * MaterialEventTypes}. + *

+ * Keys are created by {@link MaterialEvents#registerEvent}; the built-in ones + * are in {@link MaterialEventTypes}. * - * @param name the name used in JSON {@code event} maps, e.g. {@code "tool-right-click"} - * @param contextType the class of the object passed to callbacks when the event fires - * @param the context type + * @param name + * the name used in JSON {@code event} maps, e.g. + * {@code "tool-right-click"} + * @param contextType + * the class of the object passed to callbacks when the event fires + * @param + * the context type */ public record MaterialEvent(String name, Class contextType) { - /** - * Requires a nonblank name and a context type. - * - * @throws IllegalArgumentException if {@code name} is null or blank - */ - public MaterialEvent { - if (name == null || name.isBlank()) throw new IllegalArgumentException("Blank event name"); - Objects.requireNonNull(contextType); - } + /** + * Requires a nonblank name and a context type. + * + * @throws IllegalArgumentException + * if {@code name} is null or blank + */ + public MaterialEvent { + if (name == null || name.isBlank()) + throw new IllegalArgumentException("Blank event name"); + Objects.requireNonNull(contextType); + } } diff --git a/src/main/java/zone/moddev/mc/lib/api/MaterialEventContexts.java b/src/main/java/zone/moddev/mc/lib/api/MaterialEventContexts.java index e1142c59..f1fcf157 100644 --- a/src/main/java/zone/moddev/mc/lib/api/MaterialEventContexts.java +++ b/src/main/java/zone/moddev/mc/lib/api/MaterialEventContexts.java @@ -19,478 +19,529 @@ import net.neoforged.neoforge.common.damagesource.DamageContainer; /** - * The context objects passed to the built-in events in {@link MaterialEventTypes}. Each is - * call-scoped: valid only during the dispatch that created it, so handlers must not keep them. A - * tool target can be a block, any entity (dropped items included) or the air; a harvest carries the - * block's drops. + * The context objects passed to the built-in events in + * {@link MaterialEventTypes}. Each is call-scoped: valid only during the + * dispatch that created it, so handlers must not keep them. A tool target can + * be a block, any entity (dropped items included) or the air; a harvest carries + * the block's drops. */ public final class MaterialEventContexts { - private MaterialEventContexts() {} - - /** Context of {@code weapon-hit}: who hit what. */ - public record WeaponUse(Player player, LivingEntity target) { - /** Requires every component. */ - public WeaponUse { - Objects.requireNonNull(player); - Objects.requireNonNull(target); - } - } - - /** - * Context of {@code tool-left-click} and {@code tool-right-click}: who used the tool, and on - * what. - */ - public record ToolUse(Player player, ToolTarget target) { - /** Requires every component. */ - public ToolUse { - Objects.requireNonNull(player); - Objects.requireNonNull(target); - } - } - - /** Context of {@code tool-harvest}: who broke the block, and the harvest itself. */ - public record ToolHarvest(Player player, Harvest harvest) { - /** Requires every component. */ - public ToolHarvest { - Objects.requireNonNull(player); - Objects.requireNonNull(harvest); - } - } - - /** - * A block broken by a player's tool, server side only. Drops is NeoForge's live, mutable drop - * list: handlers may replace, remove, or add entities before they are spawned. Tool is the - * breaking stack. - */ - public record Harvest( - ServerLevel level, - BlockPos position, - BlockState state, - ItemStack tool, - List drops) { - /** Requires every component; stores an immutable copy of {@code position}. */ - public Harvest { - Objects.requireNonNull(level); - position = Objects.requireNonNull(position).immutable(); - Objects.requireNonNull(state); - Objects.requireNonNull(tool); - Objects.requireNonNull(drops); - } - } - - /** - * Server side, before damage mitigation; the container may change the amount or reduction stages. - */ - public record ProjectileDamage( - Projectile projectile, LivingEntity target, ItemStack ammo, DamageContainer damage) { - /** Requires every component. */ - public ProjectileDamage { - Objects.requireNonNull(projectile); - Objects.requireNonNull(target); - Objects.requireNonNull(ammo); - Objects.requireNonNull(damage); - } - } - - /** Server side, after damage was dealt. */ - public record ProjectileHit(Projectile projectile, LivingEntity target, ItemStack ammo) { - /** Requires every component. */ - public ProjectileHit { - Objects.requireNonNull(projectile); - Objects.requireNonNull(target); - Objects.requireNonNull(ammo); - } - } - - /** - * The redstone power a block receives changed, server side only. Previous and power are 0-15 (the - * strongest neighbouring signal); fired once per change, not for every neighbour update. - */ - public record RedstoneChange( - ServerLevel level, BlockPos position, BlockState state, int previous, int power) { - /** - * Requires every object component; stores an immutable copy of {@code position}. - * - * @throws IllegalArgumentException if either power level is outside 0-15 - */ - public RedstoneChange { - Objects.requireNonNull(level); - position = Objects.requireNonNull(position).immutable(); - Objects.requireNonNull(state); - if (previous < 0 || previous > 15 || power < 0 || power > 15) - throw new IllegalArgumentException("Redstone power is 0-15"); + private MaterialEventContexts() { } - /** Whether the block now receives any power. */ - public boolean powered() { - return power > 0; - } - - /** The power arriving at the block's {@code face} (a world direction) right now. */ - public int signal(net.minecraft.core.Direction face) { - return signal(level, position, face); + /** Context of {@code weapon-hit}: who hit what. */ + public record WeaponUse(Player player, LivingEntity target) { + /** Requires every component. */ + public WeaponUse { + Objects.requireNonNull(player); + Objects.requireNonNull(target); + } } /** - * The power arriving at {@code face} (a world direction) of the block at {@code pos}, as {@link - * net.minecraft.world.level.Level#getSignal} reports it from that neighbor. + * Context of {@code tool-left-click} and {@code tool-right-click}: who used + * the tool, and on what. */ - public static int signal( - net.minecraft.world.level.Level level, BlockPos pos, net.minecraft.core.Direction face) { - return level.getSignal(pos.relative(face), face); - } - } - - /** Context of {@code block-right-click}: who clicked, and the click itself. */ - public record BlockClick(Player player, BlockUse use) { - /** Requires every component. */ - public BlockClick { - Objects.requireNonNull(player); - Objects.requireNonNull(use); - } - } - - /** - * A right-click on a block, keyed by the block's own definition. Call-scoped like the other - * contexts. A handler that acts on the click calls {@link #consume()} on both sides, so the held - * item is not also used (e.g. placed against the block); the bridge then cancels the interaction - * as handled. - */ - public static final class BlockUse { - private final Level level; - private final BlockPos position; - private final BlockState state; - private final Direction face; - private final InteractionHand hand; - private final ItemStack stack; - private boolean consumed; - - /** Creates the context for one click; the position is copied to an immutable one. */ - public BlockUse( - Level level, - BlockPos position, - BlockState state, - Direction face, - InteractionHand hand, - ItemStack stack) { - this.level = Objects.requireNonNull(level); - this.position = Objects.requireNonNull(position).immutable(); - this.state = Objects.requireNonNull(state); - this.face = Objects.requireNonNull(face); - this.hand = Objects.requireNonNull(hand); - this.stack = Objects.requireNonNull(stack); - } - - /** The level the block is in; check {@code isClientSide()} before changing the world. */ - public Level level() { - return level; - } - - /** The clicked block's position. */ - public BlockPos position() { - return position; + public record ToolUse(Player player, ToolTarget target) { + /** Requires every component. */ + public ToolUse { + Objects.requireNonNull(player); + Objects.requireNonNull(target); + } } - /** The clicked block's state at the time of the click. */ - public BlockState state() { - return state; - } - - /** The face that was clicked. */ - public Direction face() { - return face; - } - - /** The hand used. */ - public InteractionHand hand() { - return hand; - } - - /** The item in that hand, possibly empty. */ - public ItemStack stack() { - return stack; - } - - /** Marks the click as handled, so the held item is not also used. Call it on both sides. */ - public void consume() { - consumed = true; - } - - /** Whether a handler called {@link #consume()}. */ - public boolean consumed() { - return consumed; - } - } - - /** What a tool was used on: a block, an entity or the air. */ - public sealed interface ToolTarget permits BlockTarget, EntityTarget, AirTarget { - /** The hand holding the tool. */ - InteractionHand hand(); - - /** The tool stack. */ - ItemStack stack(); - } - - /** A tool used on a block, from the clicked face. */ - public record BlockTarget( - Level level, - BlockPos position, - BlockState state, - Direction face, - InteractionHand hand, - ItemStack stack) - implements ToolTarget { - /** Requires every component; stores an immutable copy of {@code position}. */ - public BlockTarget { - Objects.requireNonNull(level); - position = Objects.requireNonNull(position).immutable(); - Objects.requireNonNull(state); - Objects.requireNonNull(face); - Objects.requireNonNull(hand); - Objects.requireNonNull(stack); - } - } - - /** A tool used on an entity, including dropped items and other non-living entities. */ - public record EntityTarget(Entity entity, InteractionHand hand, ItemStack stack) - implements ToolTarget { - /** Requires every component. */ - public EntityTarget { - Objects.requireNonNull(entity); - Objects.requireNonNull(hand); - Objects.requireNonNull(stack); - } - } - - /** A tool used on nothing (the air). */ - public record AirTarget(InteractionHand hand, ItemStack stack) implements ToolTarget { - /** Requires every component. */ - public AirTarget { - Objects.requireNonNull(hand); - Objects.requireNonNull(stack); - } - } - - /** Which part of using an item (eating, drawing a bow, ...) an {@link ItemUse} reports. */ - public enum UsePhase { - /** The entity starts using the item; the duration can be changed, or the use cancelled. */ - START, - /** Each tick of use; the use can be cancelled. */ - TICK, - /** The entity let go before the use finished, like releasing a bow. */ - STOP, - /** The use finished; the stack left in hand can be replaced. */ - FINISH - } - - /** - * Context of the {@code item-use-*} events: an entity using a bound item over time. Both sides. - * Results set here are written back to the NeoForge event after the handler runs. - */ - public static final class ItemUse { - private final UsePhase phase; - private final LivingEntity entity; - private final ItemStack stack; - private int duration; - private ItemStack result; - private boolean cancelled; - /** - * @param duration ticks of use: the total at {@link UsePhase#START}, the ticks left otherwise - * @param result the stack left in hand at {@link UsePhase#FINISH}, else the used stack + * Context of {@code tool-harvest}: who broke the block, and the harvest + * itself. */ - public ItemUse( - UsePhase phase, LivingEntity entity, ItemStack stack, int duration, ItemStack result) { - this.phase = Objects.requireNonNull(phase); - this.entity = Objects.requireNonNull(entity); - this.stack = Objects.requireNonNull(stack); - this.duration = duration; - this.result = Objects.requireNonNull(result); - } - - public UsePhase phase() { - return phase; - } - - public LivingEntity entity() { - return entity; - } - - public ItemStack stack() { - return stack; - } - - /** The total ticks of use at start; the ticks left in later phases. */ - public int duration() { - return duration; + public record ToolHarvest(Player player, Harvest harvest) { + /** Requires every component. */ + public ToolHarvest { + Objects.requireNonNull(player); + Objects.requireNonNull(harvest); + } } /** - * Changes the total ticks of use. - * - * @throws IllegalStateException outside {@link UsePhase#START} + * A block broken by a player's tool, server side only. Drops is NeoForge's + * live, mutable drop list: handlers may replace, remove, or add entities + * before they are spawned. Tool is the breaking stack. */ - public void setDuration(int duration) { - if (phase != UsePhase.START) - throw new IllegalStateException("The use duration can only change when use starts"); - this.duration = Math.max(0, duration); - } - - /** The stack left in hand when use finishes. */ - public ItemStack result() { - return result; + public record Harvest(ServerLevel level, BlockPos position, + BlockState state, ItemStack tool, List drops) { + /** + * Requires every component; stores an immutable copy of + * {@code position}. + */ + public Harvest { + Objects.requireNonNull(level); + position = Objects.requireNonNull(position).immutable(); + Objects.requireNonNull(state); + Objects.requireNonNull(tool); + Objects.requireNonNull(drops); + } } /** - * Replaces the stack left in hand. - * - * @throws IllegalStateException outside {@link UsePhase#FINISH} + * Server side, before damage mitigation; the container may change the + * amount or reduction stages. */ - public void setResult(ItemStack result) { - if (phase != UsePhase.FINISH) - throw new IllegalStateException("The result can only be set when use finishes"); - this.result = Objects.requireNonNull(result); + public record ProjectileDamage(Projectile projectile, LivingEntity target, + ItemStack ammo, DamageContainer damage) { + /** Requires every component. */ + public ProjectileDamage { + Objects.requireNonNull(projectile); + Objects.requireNonNull(target); + Objects.requireNonNull(ammo); + Objects.requireNonNull(damage); + } + } + + /** Server side, after damage was dealt. */ + public record ProjectileHit(Projectile projectile, LivingEntity target, + ItemStack ammo) { + /** Requires every component. */ + public ProjectileHit { + Objects.requireNonNull(projectile); + Objects.requireNonNull(target); + Objects.requireNonNull(ammo); + } } /** - * Stops the use. - * - * @throws IllegalStateException outside {@link UsePhase#START} and {@link UsePhase#TICK} + * The redstone power a block receives changed, server side only. Previous + * and power are 0-15 (the strongest neighbouring signal); fired once per + * change, not for every neighbour update. */ - public void cancel() { - if (phase != UsePhase.START && phase != UsePhase.TICK) - throw new IllegalStateException("Only starting or ongoing use can be cancelled"); - cancelled = true; - } - - public boolean cancelled() { - return cancelled; - } - } - - /** - * Context of {@code item-hit}: a melee hit with a bound item has dealt {@code damage} to {@code - * target}, after armor and other reductions. Server side. - */ - public record ItemHit(LivingEntity attacker, LivingEntity target, ItemStack stack, float damage) { - /** Requires every component. */ - public ItemHit { - Objects.requireNonNull(attacker); - Objects.requireNonNull(target); - Objects.requireNonNull(stack); - } - } - - /** - * Context of {@code item-break-block} (keyed by the held item) and {@code block-broken} (keyed by - * the block): a player is about to break a block. Server side, before the block is removed. - */ - public static final class BlockBreak { - // Blocks broken through mine() are not reported to item-break-block again, so a 3x3 tool does - // not set off 3x3 breaks of its own. - private static final ThreadLocal MINING = ThreadLocal.withInitial(() -> false); - - private final Player player; - private final ServerLevel level; - private final BlockPos position; - private final BlockState state; - private final ItemStack tool; - private final Direction face; - private boolean cancelled; - - /** @param face the face being mined, or null when it is not known */ - public BlockBreak( - Player player, - ServerLevel level, - BlockPos position, - BlockState state, - ItemStack tool, - Direction face) { - this.player = Objects.requireNonNull(player); - this.level = Objects.requireNonNull(level); - this.position = Objects.requireNonNull(position).immutable(); - this.state = Objects.requireNonNull(state); - this.tool = Objects.requireNonNull(tool); - this.face = face; - } - - public Player player() { - return player; - } - - public ServerLevel level() { - return level; + public record RedstoneChange(ServerLevel level, BlockPos position, + BlockState state, int previous, int power) { + /** + * Requires every object component; stores an immutable copy of + * {@code position}. + * + * @throws IllegalArgumentException + * if either power level is outside 0-15 + */ + public RedstoneChange { + Objects.requireNonNull(level); + position = Objects.requireNonNull(position).immutable(); + Objects.requireNonNull(state); + if (previous < 0 || previous > 15 || power < 0 || power > 15) + throw new IllegalArgumentException("Redstone power is 0-15"); + } + + /** Whether the block now receives any power. */ + public boolean powered() { + return power > 0; + } + + /** + * The power arriving at the block's {@code face} (a world direction) + * right now. + */ + public int signal(net.minecraft.core.Direction face) { + return signal(level, position, face); + } + + /** + * The power arriving at {@code face} (a world direction) of the block + * at {@code pos}, as {@link net.minecraft.world.level.Level#getSignal} + * reports it from that neighbor. + */ + public static int signal(net.minecraft.world.level.Level level, + BlockPos pos, net.minecraft.core.Direction face) { + return level.getSignal(pos.relative(face), face); + } } - public BlockPos position() { - return position; + /** + * Context of {@code block-right-click}: who clicked, and the click itself. + */ + public record BlockClick(Player player, BlockUse use) { + /** Requires every component. */ + public BlockClick { + Objects.requireNonNull(player); + Objects.requireNonNull(use); + } } - public BlockState state() { - return state; + /** + * A right-click on a block, keyed by the block's own definition. + * Call-scoped like the other contexts. A handler that acts on the click + * calls {@link #consume()} on both sides, so the held item is not also used + * (e.g. placed against the block); the bridge then cancels the interaction + * as handled. + */ + public static final class BlockUse { + private final Level level; + private final BlockPos position; + private final BlockState state; + private final Direction face; + private final InteractionHand hand; + private final ItemStack stack; + private boolean consumed; + + /** + * Creates the context for one click; the position is copied to an + * immutable one. + */ + public BlockUse(Level level, BlockPos position, BlockState state, + Direction face, InteractionHand hand, ItemStack stack) { + this.level = Objects.requireNonNull(level); + this.position = Objects.requireNonNull(position).immutable(); + this.state = Objects.requireNonNull(state); + this.face = Objects.requireNonNull(face); + this.hand = Objects.requireNonNull(hand); + this.stack = Objects.requireNonNull(stack); + } + + /** + * The level the block is in; check {@code isClientSide()} before + * changing the world. + */ + public Level level() { + return level; + } + + /** The clicked block's position. */ + public BlockPos position() { + return position; + } + + /** The clicked block's state at the time of the click. */ + public BlockState state() { + return state; + } + + /** The face that was clicked. */ + public Direction face() { + return face; + } + + /** The hand used. */ + public InteractionHand hand() { + return hand; + } + + /** The item in that hand, possibly empty. */ + public ItemStack stack() { + return stack; + } + + /** + * Marks the click as handled, so the held item is not also used. Call + * it on both sides. + */ + public void consume() { + consumed = true; + } + + /** Whether a handler called {@link #consume()}. */ + public boolean consumed() { + return consumed; + } + } + + /** What a tool was used on: a block, an entity or the air. */ + public sealed interface ToolTarget + permits BlockTarget, EntityTarget, AirTarget { + /** The hand holding the tool. */ + InteractionHand hand(); + + /** The tool stack. */ + ItemStack stack(); + } + + /** A tool used on a block, from the clicked face. */ + public record BlockTarget(Level level, BlockPos position, BlockState state, + Direction face, InteractionHand hand, ItemStack stack) + implements ToolTarget { + /** + * Requires every component; stores an immutable copy of + * {@code position}. + */ + public BlockTarget { + Objects.requireNonNull(level); + position = Objects.requireNonNull(position).immutable(); + Objects.requireNonNull(state); + Objects.requireNonNull(face); + Objects.requireNonNull(hand); + Objects.requireNonNull(stack); + } } - /** The item in the player's main hand; empty for bare hands. */ - public ItemStack tool() { - return tool; + /** + * A tool used on an entity, including dropped items and other non-living + * entities. + */ + public record EntityTarget(Entity entity, InteractionHand hand, + ItemStack stack) implements ToolTarget { + /** Requires every component. */ + public EntityTarget { + Objects.requireNonNull(entity); + Objects.requireNonNull(hand); + Objects.requireNonNull(stack); + } + } + + /** A tool used on nothing (the air). */ + public record AirTarget(InteractionHand hand, ItemStack stack) + implements ToolTarget { + /** Requires every component. */ + public AirTarget { + Objects.requireNonNull(hand); + Objects.requireNonNull(stack); + } } - /** The face being mined, or null when it is not known. */ - public Direction face() { - return face; + /** + * Which part of using an item (eating, drawing a bow, ...) an + * {@link ItemUse} reports. + */ + public enum UsePhase { + /** + * The entity starts using the item; the duration can be changed, or the + * use cancelled. + */ + START, + /** Each tick of use; the use can be cancelled. */ + TICK, + /** The entity let go before the use finished, like releasing a bow. */ + STOP, + /** The use finished; the stack left in hand can be replaced. */ + FINISH } - /** Keeps the block: it is not broken and drops nothing. */ - public void cancel() { - cancelled = true; + /** + * Context of the {@code item-use-*} events: an entity using a bound item + * over time. Both sides. Results set here are written back to the NeoForge + * event after the handler runs. + */ + public static final class ItemUse { + private final UsePhase phase; + private final LivingEntity entity; + private final ItemStack stack; + private int duration; + private ItemStack result; + private boolean cancelled; + + /** + * @param duration + * ticks of use: the total at {@link UsePhase#START}, the + * ticks left otherwise + * @param result + * the stack left in hand at {@link UsePhase#FINISH}, else + * the used stack + */ + public ItemUse(UsePhase phase, LivingEntity entity, ItemStack stack, + int duration, ItemStack result) { + this.phase = Objects.requireNonNull(phase); + this.entity = Objects.requireNonNull(entity); + this.stack = Objects.requireNonNull(stack); + this.duration = duration; + this.result = Objects.requireNonNull(result); + } + + public UsePhase phase() { + return phase; + } + + public LivingEntity entity() { + return entity; + } + + public ItemStack stack() { + return stack; + } + + /** The total ticks of use at start; the ticks left in later phases. */ + public int duration() { + return duration; + } + + /** + * Changes the total ticks of use. + * + * @throws IllegalStateException + * outside {@link UsePhase#START} + */ + public void setDuration(int duration) { + if (phase != UsePhase.START) + throw new IllegalStateException( + "The use duration can only change when use starts"); + this.duration = Math.max(0, duration); + } + + /** The stack left in hand when use finishes. */ + public ItemStack result() { + return result; + } + + /** + * Replaces the stack left in hand. + * + * @throws IllegalStateException + * outside {@link UsePhase#FINISH} + */ + public void setResult(ItemStack result) { + if (phase != UsePhase.FINISH) + throw new IllegalStateException( + "The result can only be set when use finishes"); + this.result = Objects.requireNonNull(result); + } + + /** + * Stops the use. + * + * @throws IllegalStateException + * outside {@link UsePhase#START} and {@link UsePhase#TICK} + */ + public void cancel() { + if (phase != UsePhase.START && phase != UsePhase.TICK) + throw new IllegalStateException( + "Only starting or ongoing use can be cancelled"); + cancelled = true; + } + + public boolean cancelled() { + return cancelled; + } } - public boolean cancelled() { - return cancelled; + /** + * Context of {@code item-hit}: a melee hit with a bound item has dealt + * {@code damage} to {@code + * target}, after armor and other reductions. Server side. + */ + public record ItemHit(LivingEntity attacker, LivingEntity target, + ItemStack stack, float damage) { + /** Requires every component. */ + public ItemHit { + Objects.requireNonNull(attacker); + Objects.requireNonNull(target); + Objects.requireNonNull(stack); + } } /** - * Breaks another block as this player would, with their tool: drops, experience, tool wear and - * other mods' break checks all apply. The block must be one the player could break; air, - * unbreakable blocks, blocks the tool cannot harvest, and this break's own block (which is - * already being broken) are skipped. - * - * @return whether the block was broken + * Context of {@code item-break-block} (keyed by the held item) and + * {@code block-broken} (keyed by the block): a player is about to break a + * block. Server side, before the block is removed. */ - public boolean mine(BlockPos other) { - if (!(player instanceof net.minecraft.server.level.ServerPlayer server) - || other.equals(position)) return false; - var target = level.getBlockState(other); - if (target.isAir() - || target.getDestroySpeed(level, other) < 0 - || (target.requiresCorrectToolForDrops() && !tool.isCorrectToolForDrops(target))) - return false; - boolean outer = MINING.get(); - MINING.set(true); - try { - return server.gameMode.destroyBlock(other); - } finally { - MINING.set(outer); - } + public static final class BlockBreak { + // Blocks broken through mine() are not reported to item-break-block + // again, so a 3x3 tool + // does + // not set off 3x3 breaks of its own. + private static final ThreadLocal MINING = ThreadLocal + .withInitial(() -> false); + + private final Player player; + private final ServerLevel level; + private final BlockPos position; + private final BlockState state; + private final ItemStack tool; + private final Direction face; + private boolean cancelled; + + /** + * @param face + * the face being mined, or null when it is not known + */ + public BlockBreak(Player player, ServerLevel level, BlockPos position, + BlockState state, ItemStack tool, Direction face) { + this.player = Objects.requireNonNull(player); + this.level = Objects.requireNonNull(level); + this.position = Objects.requireNonNull(position).immutable(); + this.state = Objects.requireNonNull(state); + this.tool = Objects.requireNonNull(tool); + this.face = face; + } + + public Player player() { + return player; + } + + public ServerLevel level() { + return level; + } + + public BlockPos position() { + return position; + } + + public BlockState state() { + return state; + } + + /** The item in the player's main hand; empty for bare hands. */ + public ItemStack tool() { + return tool; + } + + /** The face being mined, or null when it is not known. */ + public Direction face() { + return face; + } + + /** Keeps the block: it is not broken and drops nothing. */ + public void cancel() { + cancelled = true; + } + + public boolean cancelled() { + return cancelled; + } + + /** + * Breaks another block as this player would, with their tool: drops, + * experience, tool wear and other mods' break checks all apply. The + * block must be one the player could break; air, unbreakable blocks, + * blocks the tool cannot harvest, and this break's own block (which is + * already being broken) are skipped. + * + * @return whether the block was broken + */ + public boolean mine(BlockPos other) { + if (!(player instanceof net.minecraft.server.level.ServerPlayer server) + || other.equals(position)) + return false; + var target = level.getBlockState(other); + if (target.isAir() || target.getDestroySpeed(level, other) < 0 + || (target.requiresCorrectToolForDrops() + && !tool.isCorrectToolForDrops(target))) + return false; + boolean outer = MINING.get(); + MINING.set(true); + try { + return server.gameMode.destroyBlock(other); + } finally { + MINING.set(outer); + } + } + + /** + * Whether a break now comes from {@link #mine}, inside another break's + * handler. + */ + public static boolean mining() { + return MINING.get(); + } } - /** Whether a break now comes from {@link #mine}, inside another break's handler. */ - public static boolean mining() { - return MINING.get(); - } - } - - /** - * Context of {@code block-placed}, {@code block-stepped-on} and {@code block-random-tick}: something - * happened to a bound block. {@code entity} is the placer or the entity stepping on it, and null - * for random ticks. - */ - public record BlockAction(Level level, BlockPos position, BlockState state, Entity entity) { - /** Requires the level, position and state. */ - public BlockAction { - Objects.requireNonNull(level); - position = Objects.requireNonNull(position).immutable(); - Objects.requireNonNull(state); + /** + * Context of {@code block-placed}, {@code block-stepped-on} and + * {@code block-random-tick}: something happened to a bound block. + * {@code entity} is the placer or the entity stepping on it, and null for + * random ticks. + */ + public record BlockAction(Level level, BlockPos position, BlockState state, + Entity entity) { + /** Requires the level, position and state. */ + public BlockAction { + Objects.requireNonNull(level); + position = Objects.requireNonNull(position).immutable(); + Objects.requireNonNull(state); + } } - } } diff --git a/src/main/java/zone/moddev/mc/lib/api/MaterialEventTypes.java b/src/main/java/zone/moddev/mc/lib/api/MaterialEventTypes.java index 5d2c538f..f788cc12 100644 --- a/src/main/java/zone/moddev/mc/lib/api/MaterialEventTypes.java +++ b/src/main/java/zone/moddev/mc/lib/api/MaterialEventTypes.java @@ -1,6 +1,7 @@ package zone.moddev.mc.lib.api; import java.lang.invoke.MethodType; + import java.util.List; import net.minecraft.world.entity.LivingEntity; @@ -13,226 +14,224 @@ import zone.moddev.mc.lib.api.MaterialEventContexts.*; /** - * The built-in event contracts. Each is installed in every {@link MaterialEvents} with the method - * signature listed in {@code docs/method-events.md}. MMDLib's {@code ItemEventBridge} fires them - * from NeoForge events; this class itself installs no listeners. Mods can register further - * contracts with {@link MaterialEvents#registerEvent}. + * The built-in event contracts. Each is installed in every + * {@link MaterialEvents} with the method signature listed in + * {@code docs/method-events.md}. MMDLib's {@code ItemEventBridge} fires them + * from NeoForge events; this class itself installs no listeners. Mods can + * register further contracts with {@link MaterialEvents#registerEvent}. */ public final class MaterialEventTypes { - private MaterialEventTypes() {} - - /** - * Each tick, for items worn or held by a player; handler {@code (Player)}. Runs once per bound - * callback per tick, on both sides. - */ - public static final MaterialEvent PLAYER_TICK = - new MaterialEvent<>("player-tick", Player.class); - - /** A player hits a living entity with a bound item; handler {@code (Player, LivingEntity)}. */ - public static final MaterialEvent WEAPON_HIT = - new MaterialEvent<>("weapon-hit", WeaponUse.class); - - /** - * A player left-clicks a block, an entity or the air with a bound item; handler {@code (Player, - * ToolTarget)}. - */ - public static final MaterialEvent TOOL_LEFT_CLICK = - new MaterialEvent<>("tool-left-click", ToolUse.class); - - /** - * A player right-clicks a block, an entity or the air with a bound item; handler {@code (Player, - * ToolTarget)}. - */ - public static final MaterialEvent TOOL_RIGHT_CLICK = - new MaterialEvent<>("tool-right-click", ToolUse.class); - - /** - * A player breaks a block with a bound tool, server side; handler {@code (Player, Harvest)}, - * whose drop list may be changed. - */ - public static final MaterialEvent TOOL_HARVEST = - new MaterialEvent<>("tool-harvest", ToolHarvest.class); - - /** - * A bound projectile is about to damage a living entity, server side, before armor; handler - * {@code (Projectile, LivingEntity, ItemStack, DamageContainer)}. - */ - public static final MaterialEvent PROJECTILE_DAMAGE = - new MaterialEvent<>("projectile-damage", ProjectileDamage.class); - - /** - * A player right-clicks a bound block, keyed by the clicked block's definition, before the held - * item's {@code tool-right-click}; handler {@code (Player, BlockUse)}. - */ - public static final MaterialEvent BLOCK_RIGHT_CLICK = - new MaterialEvent<>("block-right-click", BlockClick.class); - - /** - * A bound projectile has damaged a living entity, server side; handler {@code (Projectile, - * LivingEntity, ItemStack)}. - */ - public static final MaterialEvent PROJECTILE_HIT = - new MaterialEvent<>("projectile-hit", ProjectileHit.class); - - /** - * The redstone power a bound block receives has changed, server side; fired by the block itself; - * handler {@code (RedstoneChange)}. - */ - public static final MaterialEvent REDSTONE_CHANGED = - new MaterialEvent<>("redstone-changed", RedstoneChange.class); - - /** - * A living entity starts using a bound item (eating, drawing a bow...), both sides; handler - * {@code (ItemUse)}, which can change the duration or cancel. - */ - public static final MaterialEvent ITEM_USE_START = - new MaterialEvent<>("item-use-start", ItemUse.class); - - /** Each tick of using a bound item, both sides; handler {@code (ItemUse)}, which can cancel. */ - public static final MaterialEvent ITEM_USE_TICK = - new MaterialEvent<>("item-use-tick", ItemUse.class); - - /** Use of a bound item stopped early, like a released bow, both sides; handler {@code (ItemUse)}. */ - public static final MaterialEvent ITEM_USE_STOP = - new MaterialEvent<>("item-use-stop", ItemUse.class); - - /** - * Use of a bound item finished, both sides; handler {@code (ItemUse)}, which can replace the stack - * left in hand. - */ - public static final MaterialEvent ITEM_USE_FINISH = - new MaterialEvent<>("item-use-finish", ItemUse.class); - - /** - * A melee hit with a bound item dealt damage, server side, after reductions; handler {@code - * (ItemHit)}. - */ - public static final MaterialEvent ITEM_HIT = new MaterialEvent<>("item-hit", ItemHit.class); - - /** - * A player holding a bound item is breaking a block, server side, before it is removed; handler - * {@code (BlockBreak)}, which can cancel or {@code mine} more blocks. - */ - public static final MaterialEvent ITEM_BREAK_BLOCK = - new MaterialEvent<>("item-break-block", BlockBreak.class); - - /** - * A player is breaking a bound block, server side, before it is removed; handler {@code - * (BlockBreak)}, which can cancel. - */ - public static final MaterialEvent BLOCK_BROKEN = - new MaterialEvent<>("block-broken", BlockBreak.class); - - /** An entity placed a bound block, server side; handler {@code (BlockAction)}. */ - public static final MaterialEvent BLOCK_PLACED = - new MaterialEvent<>("block-placed", BlockAction.class); - - /** - * An entity stepped on a bound block, both sides; fired by MMDLib's plain block ({@code - * mmdlib:block}) only; handler {@code (BlockAction)}. - */ - public static final MaterialEvent BLOCK_STEPPED_ON = - new MaterialEvent<>("block-stepped-on", BlockAction.class); - - /** - * A bound block got a random tick, server side; fired by MMDLib's plain block with random ticks - * on; handler {@code (BlockAction)}. - */ - public static final MaterialEvent BLOCK_RANDOM_TICK = - new MaterialEvent<>("block-random-tick", BlockAction.class); - - /** Every gameplay contract whose handler takes its context alone. */ - private static final List> SINGLE_CONTEXT = - List.of( - ITEM_USE_START, - ITEM_USE_TICK, - ITEM_USE_STOP, - ITEM_USE_FINISH, - ITEM_HIT, - ITEM_BREAK_BLOCK, - BLOCK_BROKEN, - BLOCK_PLACED, - BLOCK_STEPPED_ON, - BLOCK_RANDOM_TICK); - - public static final MaterialEvent GUI_HOVER = - new MaterialEvent<>("gui-hover", zone.moddev.mc.lib.gui.runtime.ScreenHover.class); - - public static final MaterialEvent GUI_CLICK = - new MaterialEvent<>("gui-click", zone.moddev.mc.lib.gui.runtime.ScreenClick.class); - - static void install(MaterialEvents events) { - SINGLE_CONTEXT.forEach(event -> events.registerEvent(event.name(), event.contextType())); - events.registerEvent(GUI_CLICK.name(), GUI_CLICK.contextType()); - events.registerEvent(GUI_HOVER.name(), GUI_HOVER.contextType()); - events.registerEvent( - PLAYER_TICK.name(), - Player.class, - MethodType.methodType(void.class, Player.class), - (method, player) -> { - method.invokeExact(player); - }); - events.registerEvent( - WEAPON_HIT.name(), - WeaponUse.class, - MethodType.methodType(void.class, Player.class, LivingEntity.class), - (method, context) -> { - method.invokeExact(context.player(), context.target()); - }); - events.registerEvent( - TOOL_LEFT_CLICK.name(), - ToolUse.class, - MethodType.methodType(void.class, Player.class, ToolTarget.class), - (method, context) -> { - method.invokeExact(context.player(), context.target()); - }); - events.registerEvent( - TOOL_RIGHT_CLICK.name(), - ToolUse.class, - MethodType.methodType(void.class, Player.class, ToolTarget.class), - (method, context) -> { - method.invokeExact(context.player(), context.target()); - }); - events.registerEvent( - TOOL_HARVEST.name(), - ToolHarvest.class, - MethodType.methodType(void.class, Player.class, Harvest.class), - (method, context) -> { - method.invokeExact(context.player(), context.harvest()); - }); - events.registerEvent( - BLOCK_RIGHT_CLICK.name(), - BlockClick.class, - MethodType.methodType(void.class, Player.class, BlockUse.class), - (method, context) -> { - method.invokeExact(context.player(), context.use()); - }); - events.registerEvent( - PROJECTILE_DAMAGE.name(), - ProjectileDamage.class, - MethodType.methodType( - void.class, - Projectile.class, - LivingEntity.class, - ItemStack.class, - DamageContainer.class), - (method, context) -> { - method.invokeExact( - context.projectile(), context.target(), context.ammo(), context.damage()); - }); - events.registerEvent( - PROJECTILE_HIT.name(), - ProjectileHit.class, - MethodType.methodType(void.class, Projectile.class, LivingEntity.class, ItemStack.class), - (method, context) -> { - method.invokeExact(context.projectile(), context.target(), context.ammo()); - }); - events.registerEvent( - REDSTONE_CHANGED.name(), - RedstoneChange.class, - MethodType.methodType(void.class, RedstoneChange.class), - (method, context) -> { - method.invokeExact(context); - }); - } + private MaterialEventTypes() { + } + + /** + * Each tick, for items worn or held by a player; handler {@code (Player)}. + * Runs once per bound callback per tick, on both sides. + */ + public static final MaterialEvent PLAYER_TICK = new MaterialEvent<>( + "player-tick", Player.class); + + /** + * A player hits a living entity with a bound item; handler + * {@code (Player, LivingEntity)}. + */ + public static final MaterialEvent WEAPON_HIT = new MaterialEvent<>( + "weapon-hit", WeaponUse.class); + + /** + * A player left-clicks a block, an entity or the air with a bound item; + * handler {@code (Player, + * ToolTarget)}. + */ + public static final MaterialEvent TOOL_LEFT_CLICK = new MaterialEvent<>( + "tool-left-click", ToolUse.class); + + /** + * A player right-clicks a block, an entity or the air with a bound item; + * handler {@code (Player, + * ToolTarget)}. + */ + public static final MaterialEvent TOOL_RIGHT_CLICK = new MaterialEvent<>( + "tool-right-click", ToolUse.class); + + /** + * A player breaks a block with a bound tool, server side; handler + * {@code (Player, Harvest)}, whose drop list may be changed. + */ + public static final MaterialEvent TOOL_HARVEST = new MaterialEvent<>( + "tool-harvest", ToolHarvest.class); + + /** + * A bound projectile is about to damage a living entity, server side, + * before armor; handler + * {@code (Projectile, LivingEntity, ItemStack, DamageContainer)}. + */ + public static final MaterialEvent PROJECTILE_DAMAGE = new MaterialEvent<>( + "projectile-damage", ProjectileDamage.class); + + /** + * A player right-clicks a bound block, keyed by the clicked block's + * definition, before the held item's {@code tool-right-click}; handler + * {@code (Player, BlockUse)}. + */ + public static final MaterialEvent BLOCK_RIGHT_CLICK = new MaterialEvent<>( + "block-right-click", BlockClick.class); + + /** + * A bound projectile has damaged a living entity, server side; handler + * {@code (Projectile, + * LivingEntity, ItemStack)}. + */ + public static final MaterialEvent PROJECTILE_HIT = new MaterialEvent<>( + "projectile-hit", ProjectileHit.class); + + /** + * The redstone power a bound block receives has changed, server side; fired + * by the block itself; handler {@code (RedstoneChange)}. + */ + public static final MaterialEvent REDSTONE_CHANGED = new MaterialEvent<>( + "redstone-changed", RedstoneChange.class); + + /** + * A living entity starts using a bound item (eating, drawing a bow...), + * both sides; handler {@code (ItemUse)}, which can change the duration or + * cancel. + */ + public static final MaterialEvent ITEM_USE_START = new MaterialEvent<>( + "item-use-start", ItemUse.class); + + /** + * Each tick of using a bound item, both sides; handler {@code (ItemUse)}, + * which can cancel. + */ + public static final MaterialEvent ITEM_USE_TICK = new MaterialEvent<>( + "item-use-tick", ItemUse.class); + + /** + * Use of a bound item stopped early, like a released bow, both sides; + * handler {@code (ItemUse)}. + */ + public static final MaterialEvent ITEM_USE_STOP = new MaterialEvent<>( + "item-use-stop", ItemUse.class); + + /** + * Use of a bound item finished, both sides; handler {@code (ItemUse)}, + * which can replace the stack left in hand. + */ + public static final MaterialEvent ITEM_USE_FINISH = new MaterialEvent<>( + "item-use-finish", ItemUse.class); + + /** + * A melee hit with a bound item dealt damage, server side, after + * reductions; handler {@code + * (ItemHit)}. + */ + public static final MaterialEvent ITEM_HIT = new MaterialEvent<>( + "item-hit", ItemHit.class); + + /** + * A player holding a bound item is breaking a block, server side, before it + * is removed; handler {@code (BlockBreak)}, which can cancel or + * {@code mine} more blocks. + */ + public static final MaterialEvent ITEM_BREAK_BLOCK = new MaterialEvent<>( + "item-break-block", BlockBreak.class); + + /** + * A player is breaking a bound block, server side, before it is removed; + * handler {@code + * (BlockBreak)}, which can cancel. + */ + public static final MaterialEvent BLOCK_BROKEN = new MaterialEvent<>( + "block-broken", BlockBreak.class); + + /** + * An entity placed a bound block, server side; handler + * {@code (BlockAction)}. + */ + public static final MaterialEvent BLOCK_PLACED = new MaterialEvent<>( + "block-placed", BlockAction.class); + + /** + * An entity stepped on a bound block, both sides; fired by MMDLib's plain + * block ({@code + * mmdlib:block}) only; handler {@code (BlockAction)}. + */ + public static final MaterialEvent BLOCK_STEPPED_ON = new MaterialEvent<>( + "block-stepped-on", BlockAction.class); + + /** + * A bound block got a random tick, server side; fired by MMDLib's plain + * block with random ticks on; handler {@code (BlockAction)}. + */ + public static final MaterialEvent BLOCK_RANDOM_TICK = new MaterialEvent<>( + "block-random-tick", BlockAction.class); + + /** Every gameplay contract whose handler takes its context alone. */ + private static final List> SINGLE_CONTEXT = List.of( + ITEM_USE_START, ITEM_USE_TICK, ITEM_USE_STOP, ITEM_USE_FINISH, ITEM_HIT, + ITEM_BREAK_BLOCK, BLOCK_BROKEN, BLOCK_PLACED, BLOCK_STEPPED_ON, + BLOCK_RANDOM_TICK); + + public static final MaterialEvent GUI_HOVER = new MaterialEvent<>( + "gui-hover", zone.moddev.mc.lib.gui.runtime.ScreenHover.class); + + public static final MaterialEvent GUI_CLICK = new MaterialEvent<>( + "gui-click", zone.moddev.mc.lib.gui.runtime.ScreenClick.class); + + static void install(MaterialEvents events) { + SINGLE_CONTEXT.forEach( + event -> events.registerEvent(event.name(), event.contextType())); + events.registerEvent(GUI_CLICK.name(), GUI_CLICK.contextType()); + events.registerEvent(GUI_HOVER.name(), GUI_HOVER.contextType()); + events.registerEvent(PLAYER_TICK.name(), Player.class, + MethodType.methodType(void.class, Player.class), + (method, player) -> { + method.invokeExact(player); + }); + events.registerEvent(WEAPON_HIT.name(), WeaponUse.class, + MethodType.methodType(void.class, Player.class, LivingEntity.class), + (method, context) -> { + method.invokeExact(context.player(), context.target()); + }); + events.registerEvent(TOOL_LEFT_CLICK.name(), ToolUse.class, + MethodType.methodType(void.class, Player.class, ToolTarget.class), + (method, context) -> { + method.invokeExact(context.player(), context.target()); + }); + events.registerEvent(TOOL_RIGHT_CLICK.name(), ToolUse.class, + MethodType.methodType(void.class, Player.class, ToolTarget.class), + (method, context) -> { + method.invokeExact(context.player(), context.target()); + }); + events.registerEvent(TOOL_HARVEST.name(), ToolHarvest.class, + MethodType.methodType(void.class, Player.class, Harvest.class), + (method, context) -> { + method.invokeExact(context.player(), context.harvest()); + }); + events.registerEvent(BLOCK_RIGHT_CLICK.name(), BlockClick.class, + MethodType.methodType(void.class, Player.class, BlockUse.class), + (method, context) -> { + method.invokeExact(context.player(), context.use()); + }); + events.registerEvent(PROJECTILE_DAMAGE.name(), ProjectileDamage.class, + MethodType.methodType(void.class, Projectile.class, + LivingEntity.class, ItemStack.class, DamageContainer.class), + (method, context) -> { + method.invokeExact(context.projectile(), context.target(), + context.ammo(), context.damage()); + }); + events.registerEvent(PROJECTILE_HIT.name(), ProjectileHit.class, + MethodType.methodType(void.class, Projectile.class, + LivingEntity.class, ItemStack.class), + (method, context) -> { + method.invokeExact(context.projectile(), context.target(), + context.ammo()); + }); + events.registerEvent(REDSTONE_CHANGED.name(), RedstoneChange.class, + MethodType.methodType(void.class, RedstoneChange.class), + (method, context) -> { + method.invokeExact(context); + }); + } } diff --git a/src/main/java/zone/moddev/mc/lib/api/MaterialEvents.java b/src/main/java/zone/moddev/mc/lib/api/MaterialEvents.java index f8387e68..c96ce179 100644 --- a/src/main/java/zone/moddev/mc/lib/api/MaterialEvents.java +++ b/src/main/java/zone/moddev/mc/lib/api/MaterialEvents.java @@ -1,11 +1,14 @@ package zone.moddev.mc.lib.api; import java.io.IOException; + import java.lang.invoke.MethodHandle; import java.lang.invoke.MethodHandles; import java.lang.invoke.MethodType; + import java.nio.file.Files; import java.nio.file.Path; + import java.util.List; import java.util.Map; import java.util.Objects; @@ -26,275 +29,315 @@ import zone.moddev.mc.lib.common.types.MMDLibEventCallback; import zone.moddev.mc.lib.common.types.MMDMaterial; -/** Typed event/callback registration and explicit dispatch for material event bindings. */ +/** + * Typed event/callback registration and explicit dispatch for material event + * bindings. + */ public final class MaterialEvents { - private final ConcurrentHashMap> events = new ConcurrentHashMap<>(); - private final ConcurrentHashMap> callbacks = - new ConcurrentHashMap<>(); - private final ConcurrentHashMap> contracts = new ConcurrentHashMap<>(); - private final ConcurrentHashMap> resolved = - new ConcurrentHashMap<>(); + private final ConcurrentHashMap> events = new ConcurrentHashMap<>(); + private final ConcurrentHashMap> callbacks = new ConcurrentHashMap<>(); + private final ConcurrentHashMap> contracts = new ConcurrentHashMap<>(); + private final ConcurrentHashMap> resolved = new ConcurrentHashMap<>(); - private record MethodContract( - MaterialEvent event, MethodType signature, MethodEventInvoker invoker) {} - - /** Creates a registry with the built-in event contracts from {@link MaterialEventTypes}. */ - public MaterialEvents() { - MaterialEventTypes.install(this); - } + private record MethodContract(MaterialEvent event, + MethodType signature, MethodEventInvoker invoker) { + } - /** - * Registers an event whose method handlers take the context object itself as their only - * parameter. - * - * @param name the JSON name; must be unique - * @param contextType the context class passed when the event fires - * @return the key to use for callbacks and dispatch - * @throws IllegalArgumentException if the name is already registered - */ - public MaterialEvent registerEvent(String name, Class contextType) { - return registerEvent( - name, - contextType, - MethodType.methodType(void.class, contextType), - (method, context) -> { - method.invoke(context); - }); - } + /** + * Creates a registry with the built-in event contracts from + * {@link MaterialEventTypes}. + */ + public MaterialEvents() { + MaterialEventTypes.install(this); + } - /** - * Registers an event whose method handlers take parameters other than the context, e.g. {@code - * (Player, LivingEntity)} for {@code weapon-hit}. - * - * @param signature the parameter types an extension file's {@code public static void} method must - * declare; the return type must be {@code void} - * @param invoker unpacks the context into those parameters - * @throws IllegalArgumentException if the name is already registered or the signature does not - * return void - */ - public synchronized MaterialEvent registerEvent( - String name, Class contextType, MethodType signature, MethodEventInvoker invoker) { - Objects.requireNonNull(signature); - Objects.requireNonNull(invoker); - if (signature.returnType() != void.class) - throw new IllegalArgumentException("Event handlers must return void"); - var key = new MaterialEvent<>(name, contextType); - if (events.putIfAbsent(name, key) != null) - throw new IllegalArgumentException("Event already registered: " + name); - contracts.put(name, new MethodContract<>(key, signature, invoker)); - return key; - } + /** + * Registers an event whose method handlers take the context object itself + * as their only parameter. + * + * @param name + * the JSON name; must be unique + * @param contextType + * the context class passed when the event fires + * @return the key to use for callbacks and dispatch + * @throws IllegalArgumentException + * if the name is already registered + */ + public MaterialEvent registerEvent(String name, + Class contextType) { + return registerEvent(name, contextType, + MethodType.methodType(void.class, contextType), + (method, context) -> { + method.invoke(context); + }); + } - /** - * Registers a Java callback under {@code id}, so JSON {@code event} maps can bind it. Each ID - * handles exactly one event. - * - * @throws IllegalArgumentException if the event is not registered here or the ID is taken - */ - public synchronized void registerCallback( - ResourceLocation id, MaterialEvent event, Consumer callback) { - if (!event.equals(events.get(event.name()))) - throw new IllegalArgumentException("Event is not registered here: " + event.name()); - if (callbacks.putIfAbsent( - Objects.requireNonNull(id), new MMDLibEventCallback<>(event, callback)) - != null) throw new IllegalArgumentException("Callback already registered: " + id); - } + /** + * Registers an event whose method handlers take parameters other than the + * context, e.g. {@code + * (Player, LivingEntity)} for {@code weapon-hit}. + * + * @param signature + * the parameter types an extension file's + * {@code public static void} method must declare; the return + * type must be {@code void} + * @param invoker + * unpacks the context into those parameters + * @throws IllegalArgumentException + * if the name is already registered or the signature does not + * return void + */ + public synchronized MaterialEvent registerEvent(String name, + Class contextType, MethodType signature, + MethodEventInvoker invoker) { + Objects.requireNonNull(signature); + Objects.requireNonNull(invoker); + if (signature.returnType() != void.class) + throw new IllegalArgumentException( + "Event handlers must return void"); + var key = new MaterialEvent<>(name, contextType); + if (events.putIfAbsent(name, key) != null) + throw new IllegalArgumentException( + "Event already registered: " + name); + contracts.put(name, new MethodContract<>(key, signature, invoker)); + return key; + } - /** - * As {@link #loadExtension(EventExtension, ClassLoader, String)}, naming the source {@code - * } in errors. - */ - public List> loadExtension(EventExtension extension, ClassLoader loader) { - return loadExtension(extension, loader, ""); - } + /** + * Registers a Java callback under {@code id}, so JSON {@code event} maps + * can bind it. Each ID handles exactly one event. + * + * @throws IllegalArgumentException + * if the event is not registered here or the ID is taken + */ + public synchronized void registerCallback(ResourceLocation id, + MaterialEvent event, Consumer callback) { + if (!event.equals(events.get(event.name()))) + throw new IllegalArgumentException( + "Event is not registered here: " + event.name()); + if (callbacks.putIfAbsent(Objects.requireNonNull(id), + new MMDLibEventCallback<>(event, callback)) != null) + throw new IllegalArgumentException( + "Callback already registered: " + id); + } - /** - * Resolves and registers every handler in an extension file. All methods are resolved before any - * is installed, so a file with one bad handler installs none. - * - * @param loader the class loader used to find handler classes, normally the owning mod's - * @param source names the file in error messages - * @return the handlers installed - * @throws IllegalArgumentException for duplicate IDs, unknown event types, or methods that cannot - * be found with the event's signature - */ - public synchronized List> loadExtension( - EventExtension extension, ClassLoader loader, String source) { - Objects.requireNonNull(extension); - Objects.requireNonNull(loader); - Objects.requireNonNull(source); - var pending = new java.util.LinkedHashMap>(); - for (var definition : extension.events()) { - if (callbacks.containsKey(definition.id()) || pending.containsKey(definition.id())) - throw new IllegalArgumentException(source + ": Duplicate callback ID: " + definition.id()); - var contract = contracts.get(definition.type()); - if (contract == null) - throw new IllegalArgumentException( - source + ": Unknown event type " + definition.type() + " for " + definition.id()); - pending.put(definition.id(), resolve(definition, contract, loader, source)); + /** + * As {@link #loadExtension(EventExtension, ClassLoader, String)}, naming + * the source {@code + * } in errors. + */ + public List> loadExtension(EventExtension extension, + ClassLoader loader) { + return loadExtension(extension, loader, ""); } - pending.forEach( - (id, binding) -> { - callbacks.put(id, binding.callback()); - resolved.put(id, binding); - }); - return List.copyOf(pending.values()); - } - /** - * Reads and loads an extension file from disk. - * - * @throws IOException if the file cannot be read - */ - public List> loadExtension(Path file, ClassLoader loader) - throws IOException { - final EventExtension extension; - try (var reader = Files.newBufferedReader(file)) { - extension = - EventExtension.CODEC.parse(JsonOps.INSTANCE, JsonParser.parseReader(reader)).getOrThrow(); - } catch (RuntimeException ex) { - throw new IllegalArgumentException(file + ": " + ex.getMessage(), ex); + /** + * Resolves and registers every handler in an extension file. All methods + * are resolved before any is installed, so a file with one bad handler + * installs none. + * + * @param loader + * the class loader used to find handler classes, normally the + * owning mod's + * @param source + * names the file in error messages + * @return the handlers installed + * @throws IllegalArgumentException + * for duplicate IDs, unknown event types, or methods that + * cannot be found with the event's signature + */ + public synchronized List> loadExtension( + EventExtension extension, ClassLoader loader, String source) { + Objects.requireNonNull(extension); + Objects.requireNonNull(loader); + Objects.requireNonNull(source); + var pending = new java.util.LinkedHashMap>(); + for (var definition : extension.events()) { + if (callbacks.containsKey(definition.id()) + || pending.containsKey(definition.id())) + throw new IllegalArgumentException( + source + ": Duplicate callback ID: " + definition.id()); + var contract = contracts.get(definition.type()); + if (contract == null) + throw new IllegalArgumentException( + source + ": Unknown event type " + definition.type() + + " for " + definition.id()); + pending.put(definition.id(), + resolve(definition, contract, loader, source)); + } + pending.forEach((id, binding) -> { + callbacks.put(id, binding.callback()); + resolved.put(id, binding); + }); + return List.copyOf(pending.values()); } - return loadExtension(extension, loader, file.toString()); - } - private static ResolvedEventHandler resolve( - EventExtension.Handler definition, - MethodContract contract, - ClassLoader loader, - String source) { - String reference = definition.handler(); - int separator = reference.indexOf('#'); - if (separator <= 0 - || separator == reference.length() - 1 - || separator != reference.lastIndexOf('#')) - throw new IllegalArgumentException(source + ": Expected Class#method for " + definition.id()); - final MethodHandle handle; - try { - var owner = Class.forName(reference.substring(0, separator), false, loader); - handle = - MethodHandles.publicLookup() - .findStatic(owner, reference.substring(separator + 1), contract.signature()); - } catch (ReflectiveOperationException | LinkageError | SecurityException ex) { - throw new IllegalArgumentException( - source - + ": Cannot resolve " - + definition.id() - + " handler " - + reference - + "; expected public static " - + contract.signature(), - ex); + /** + * Reads and loads an extension file from disk. + * + * @throws IOException + * if the file cannot be read + */ + public List> loadExtension(Path file, + ClassLoader loader) throws IOException { + final EventExtension extension; + try (var reader = Files.newBufferedReader(file)) { + extension = EventExtension.CODEC + .parse(JsonOps.INSTANCE, JsonParser.parseReader(reader)) + .getOrThrow(); + } catch (RuntimeException ex) { + throw new IllegalArgumentException(file + ": " + ex.getMessage(), + ex); + } + return loadExtension(extension, loader, file.toString()); } - var callback = - new MMDLibEventCallback( - contract.event(), - context -> { - try { + + private static ResolvedEventHandler resolve( + EventExtension.Handler definition, MethodContract contract, + ClassLoader loader, String source) { + String reference = definition.handler(); + int separator = reference.indexOf('#'); + if (separator <= 0 || separator == reference.length() - 1 + || separator != reference.lastIndexOf('#')) + throw new IllegalArgumentException( + source + ": Expected Class#method for " + definition.id()); + final MethodHandle handle; + try { + var owner = Class.forName(reference.substring(0, separator), false, + loader); + handle = MethodHandles.publicLookup().findStatic(owner, + reference.substring(separator + 1), contract.signature()); + } catch (ReflectiveOperationException | LinkageError + | SecurityException ex) { + throw new IllegalArgumentException(source + ": Cannot resolve " + + definition.id() + " handler " + reference + + "; expected public static " + contract.signature(), ex); + } + var callback = new MMDLibEventCallback(contract.event(), context -> { + try { contract.invoker().invoke(handle, context); - } catch (RuntimeException | Error ex) { + } catch (RuntimeException | Error ex) { throw ex; - } catch (Throwable ex) { - throw new IllegalStateException( - "Event handler failed: " + definition.id() + " (" + reference + ")", ex); - } - }); - return new ResolvedEventHandler<>(definition, handle, callback); - } + } catch (Throwable ex) { + throw new IllegalStateException("Event handler failed: " + + definition.id() + " (" + reference + ")", ex); + } + }); + return new ResolvedEventHandler<>(definition, handle, callback); + } - /** - * Every handler loaded from extension files so far, by ID. Code-registered callbacks are not - * included. - */ - public Map> resolvedHandlers() { - return Map.copyOf(resolved); - } + /** + * Every handler loaded from extension files so far, by ID. Code-registered + * callbacks are not included. + */ + public Map> resolvedHandlers() { + return Map.copyOf(resolved); + } - /** - * The extension-file handler with this ID. - * - * @throws IllegalArgumentException if there is none - */ - public ResolvedEventHandler resolvedHandler(ResourceLocation id) { - var handler = resolved.get(id); - if (handler == null) throw new IllegalArgumentException("Unknown method handler: " + id); - return handler; - } + /** + * The extension-file handler with this ID. + * + * @throws IllegalArgumentException + * if there is none + */ + public ResolvedEventHandler resolvedHandler(ResourceLocation id) { + var handler = resolved.get(id); + if (handler == null) + throw new IllegalArgumentException("Unknown method handler: " + id); + return handler; + } - /** - * Runs the callback {@code id} for {@code event}. The callback was resolved at load, so no method - * lookup happens here. - * - * @throws IllegalArgumentException if the event or callback is unknown, or the callback handles - * another event - */ - public void dispatch(ResourceLocation id, MaterialEvent event, C context) { - if (!event.equals(events.get(event.name()))) - throw new IllegalArgumentException("Unknown event key: " + event.name()); - event.contextType().cast(Objects.requireNonNull(context)); - var callback = callbacks.get(id); - if (callback == null) throw new IllegalArgumentException("Unknown callback: " + id); - callback.invokeChecked(event, context); - } + /** + * Runs the callback {@code id} for {@code event}. The callback was resolved + * at load, so no method lookup happens here. + * + * @throws IllegalArgumentException + * if the event or callback is unknown, or the callback handles + * another event + */ + public void dispatch(ResourceLocation id, MaterialEvent event, + C context) { + if (!event.equals(events.get(event.name()))) + throw new IllegalArgumentException( + "Unknown event key: " + event.name()); + event.contextType().cast(Objects.requireNonNull(context)); + var callback = callbacks.get(id); + if (callback == null) + throw new IllegalArgumentException("Unknown callback: " + id); + callback.invokeChecked(event, context); + } - /** Every registered callback by ID, both code-registered and from extension files. */ - public Map> callbacks() { - return Map.copyOf(callbacks); - } + /** + * Every registered callback by ID, both code-registered and from extension + * files. + */ + public Map> callbacks() { + return Map.copyOf(callbacks); + } - /** - * Checks a JSON {@code event} map: every event name is registered, and every callback exists and - * handles that event. - * - * @throws IllegalArgumentException naming the first bad entry - */ - public void validate(Map bindings) { - bindings.forEach( - (event, id) -> { - var key = events.get(event); - if (key == null) throw new IllegalArgumentException("Unknown material event: " + event); - var callback = callbacks.get(id); - if (callback == null) throw new IllegalArgumentException("Unknown callback: " + id); - if (!key.equals(callback.event())) - throw new IllegalArgumentException("Callback " + id + " does not handle " + event); + /** + * Checks a JSON {@code event} map: every event name is registered, and + * every callback exists and handles that event. + * + * @throws IllegalArgumentException + * naming the first bad entry + */ + public void validate(Map bindings) { + bindings.forEach((event, id) -> { + var key = events.get(event); + if (key == null) + throw new IllegalArgumentException( + "Unknown material event: " + event); + var callback = callbacks.get(id); + if (callback == null) + throw new IllegalArgumentException("Unknown callback: " + id); + if (!key.equals(callback.event())) + throw new IllegalArgumentException( + "Callback " + id + " does not handle " + event); }); - } + } - /** - * Runs the callback {@code material} binds to {@code event}, if any. A material with no binding - * for the event is skipped silently. - */ - public void dispatch(MMDMaterial material, MaterialEvent event, C context) { - if (!event.equals(events.get(event.name()))) - throw new IllegalArgumentException("Unknown event key: " + event.name()); - event.contextType().cast(Objects.requireNonNull(context)); - var id = material.events().get(event.name()); - if (id == null) return; - var callback = callbacks.get(id); - if (callback == null) throw new IllegalArgumentException("Unknown callback: " + id); - callback.invokeChecked(event, context); - } + /** + * Runs the callback {@code material} binds to {@code event}, if any. A + * material with no binding for the event is skipped silently. + */ + public void dispatch(MMDMaterial material, MaterialEvent event, + C context) { + if (!event.equals(events.get(event.name()))) + throw new IllegalArgumentException( + "Unknown event key: " + event.name()); + event.contextType().cast(Objects.requireNonNull(context)); + var id = material.events().get(event.name()); + if (id == null) + return; + var callback = callbacks.get(id); + if (callback == null) + throw new IllegalArgumentException("Unknown callback: " + id); + callback.invokeChecked(event, context); + } - /** - * Bridges a NeoForge event straight to material bindings: whenever {@code event}'s context type - * is posted on {@code bus}, each material from {@code materials} is dispatched once. - * - * @param materials selects the affected materials, e.g. from the player's equipped items - * @return closes the bridge by unregistering the listener - */ - public AutoCloseable listen( - IEventBus bus, - MaterialEvent event, - Function> materials) { - if (!event.equals(events.get(event.name()))) - throw new IllegalArgumentException("Unknown event key"); - Consumer listener = - context -> { - var seen = new java.util.HashSet(); - for (var material : materials.apply(context)) - if (seen.add(material)) dispatch(material, event, context); + /** + * Bridges a NeoForge event straight to material bindings: whenever + * {@code event}'s context type is posted on {@code bus}, each material from + * {@code materials} is dispatched once. + * + * @param materials + * selects the affected materials, e.g. from the player's + * equipped items + * @return closes the bridge by unregistering the listener + */ + public AutoCloseable listen(IEventBus bus, + MaterialEvent event, + Function> materials) { + if (!event.equals(events.get(event.name()))) + throw new IllegalArgumentException("Unknown event key"); + Consumer listener = context -> { + var seen = new java.util.HashSet(); + for (var material : materials.apply(context)) + if (seen.add(material)) + dispatch(material, event, context); }; - bus.addListener(event.contextType(), listener); - return () -> bus.unregister(listener); - } + bus.addListener(event.contextType(), listener); + return () -> bus.unregister(listener); + } } diff --git a/src/main/java/zone/moddev/mc/lib/api/MaterialExtensions.java b/src/main/java/zone/moddev/mc/lib/api/MaterialExtensions.java index 93b16ee0..80240faa 100644 --- a/src/main/java/zone/moddev/mc/lib/api/MaterialExtensions.java +++ b/src/main/java/zone/moddev/mc/lib/api/MaterialExtensions.java @@ -17,654 +17,718 @@ import zone.moddev.mc.lib.common.types.*; /** - * The catalog of everything material definitions can refer to: item, block and fluid modules, the - * Java implementations behind them, code-registered part types, and events. + * The catalog of everything material definitions can refer to: item, block and + * fluid modules, the Java implementations behind them, code-registered part + * types, and events. * - *

{@link #GLOBAL} is the shared catalog. Register extensions during mod construction, before any - * material JSON that names them is decoded. Separate instances can be created for isolated loading - * and tests. Every registration rejects duplicate IDs. + *

+ * {@link #GLOBAL} is the shared catalog. Register extensions during mod + * construction, before any material JSON that names them is decoded. Separate + * instances can be created for isolated loading and tests. Every registration + * rejects duplicate IDs. */ public final class MaterialExtensions { - /** The shared catalog, with MMDLib's bundled modules and built-in implementations installed. */ - public static final MaterialExtensions GLOBAL = new MaterialExtensions(); - - /** - * A registered item part: the factory that builds its template and item, and the mod it requires, - * if any. - */ - public record ItemType(ItemTypeFactory factory, Optional requiredMod) {} - - /** - * A registered block part: the factory that builds its template and block, and the mod it - * requires, if any. - */ - public record BlockType(BlockTypeFactory factory, Optional requiredMod) {} - - /** - * Definitions built in code rather than read from {@code mmdlib/} files. - * - * @param materials named materials, planned like {@code mmdlib/material} files - * @param standaloneBlocks standalone blocks by registry ID, planned like {@code - * mmdlib/standalone/block} files - * @param standaloneItems standalone items by registry ID, planned like {@code - * mmdlib/standalone/item} files - */ - public record Definitions( - java.util.Collection materials, - Map standaloneBlocks, - Map standaloneItems) { - /** Nothing. */ - public static final Definitions EMPTY = new Definitions(java.util.List.of(), Map.of(), Map.of()); - - /** Copies every collection. */ - public Definitions { - materials = java.util.List.copyOf(materials); - standaloneBlocks = Map.copyOf(standaloneBlocks); - standaloneItems = Map.copyOf(standaloneItems); - } - - /** Materials and standalone blocks, without standalone items. */ - public Definitions( - java.util.Collection materials, - Map standaloneBlocks) { - this(materials, standaloneBlocks, Map.of()); - } - } - - /** - * Supplies {@link Definitions} built in code. The definition loader calls it once, after every - * extension file and data module is installed and before any material file is decoded, so it may - * register item and block modules and callbacks on {@code api} that material files then use. - */ - @FunctionalInterface - public interface DefinitionSource { - /** Registers any modules and callbacks on {@code api} and returns the definitions. */ - Definitions load(MaterialExtensions api); - } - - /** - * A registered definition source. - * - * @param name names the source in loading errors, such as {@code "CraftTweaker scripts"} - * @param source the source - */ - public record NamedSource(String name, DefinitionSource source) { - /** Checks that both values are present and the name is not blank. */ - public NamedSource { - Objects.requireNonNull(source); - if (name.isBlank()) throw new IllegalArgumentException("Definition source needs a name"); - } - } - - /** - * An immutable copy of the registered item and block parts at one moment. Decoding takes a - * snapshot, so a definition decoded earlier is not changed by later registrations. - */ - public record Snapshot( - Map items, Map blocks) { - /** Copies both maps. */ - public Snapshot { - items = Map.copyOf(items); - blocks = Map.copyOf(blocks); - } - - /** Every registered part, resolvable by ID or alias. */ - public PartRegistry parts() { - var parts = new java.util.ArrayList(); - items.forEach( - (id, type) -> - parts.add( - type.factory() - instanceof - zone.moddev.mc.lib.common.modules.ModuleFactories.Reference module - ? module.part() - : new MaterialPart( - id, MaterialPart.Kind.ITEM, type.requiredMod(), PartMetadata.EMPTY))); - blocks.forEach( - (id, type) -> - parts.add( - type.factory() - instanceof - zone.moddev.mc.lib.common.modules.ModuleFactories.Reference module - ? module.part() - : new MaterialPart( - id, MaterialPart.Kind.BLOCK, type.requiredMod(), PartMetadata.EMPTY))); - return new PartRegistry(parts); - } - } - - private final ConcurrentHashMap items = new ConcurrentHashMap<>(); - private final ConcurrentHashMap blocks = new ConcurrentHashMap<>(); - private final MaterialEvents events = new MaterialEvents(); - private final java.util.List definitionSources = - new java.util.concurrent.CopyOnWriteArrayList<>(); - private final ConcurrentHashMap> itemImplementations = - new ConcurrentHashMap<>(); - private final ConcurrentHashMap> blockImplementations = - new ConcurrentHashMap<>(); - private final ConcurrentHashMap< - ResourceLocation, zone.moddev.mc.lib.common.modules.DataModules.Item> - itemModules = new ConcurrentHashMap<>(); - private final ConcurrentHashMap< - ResourceLocation, zone.moddev.mc.lib.common.modules.DataModules.Block> - blockModules = new ConcurrentHashMap<>(); - private final ConcurrentHashMap< - ResourceLocation, zone.moddev.mc.lib.common.modules.DataModules.Fluid> - fluids = new ConcurrentHashMap<>(); - - /** A catalog with MMDLib's bundled modules and built-in implementations installed. */ - public MaterialExtensions() { - this(true); - } - - /** - * A catalog, optionally empty. - * - * @param includeBundledModules whether to install MMDLib's bundled modules and built-in - * implementations; {@code false} gives an empty catalog, mainly for tests - */ - public MaterialExtensions(boolean includeBundledModules) { - if (includeBundledModules) { - ModuleCatalog.bundled().install(this); - // MMDLib's own factories, through the same public registration calls an addon uses. - zone.moddev.mc.lib.common.runtime.StandardImplementations.install(this); - } - } - - /** - * Registers the Java code a data module's {@code factory} names for items. - * - * @throws IllegalArgumentException if the ID is already registered - */ - public void registerItemImplementation( - ResourceLocation id, ItemImplementation implementation) { - Objects.requireNonNull(implementation.itemClass()); - if (itemImplementations.putIfAbsent(id, implementation) != null) - throw new IllegalArgumentException("Duplicate item implementation: " + id); - } - - /** - * Registers the Java code a data module's {@code factory} names for blocks. - * - * @throws IllegalArgumentException if the ID is already registered - */ - public void registerBlockImplementation( - ResourceLocation id, BlockImplementation implementation) { - Objects.requireNonNull(implementation.blockClass()); - if (blockImplementations.putIfAbsent(id, implementation) != null) - throw new IllegalArgumentException("Duplicate block implementation: " + id); - } - - /** The item implementation registered under {@code id}, if any. */ - public Optional> findItemImplementation(ResourceLocation id) { - return Optional.ofNullable(itemImplementations.get(id)); - } - - /** The block implementation registered under {@code id}, if any. */ - public Optional> findBlockImplementation(ResourceLocation id) { - return Optional.ofNullable(blockImplementations.get(id)); - } - - /** - * The item implementation registered under {@code id}. - * - * @throws IllegalStateException if none is installed - */ - public ItemImplementation itemImplementation(ResourceLocation id) { - var implementation = itemImplementations.get(id); - if (implementation == null) - throw new IllegalStateException("Runtime item implementation not installed: " + id); - return implementation; - } - - /** - * The block implementation registered under {@code id}. - * - * @throws IllegalStateException if none is installed - */ - public BlockImplementation blockImplementation(ResourceLocation id) { - var implementation = blockImplementations.get(id); - if (implementation == null) - throw new IllegalStateException("Runtime block implementation not installed: " + id); - return implementation; - } - - /** - * Registers an item data module as a selectable item part. - * - * @throws IllegalArgumentException if the ID or one of its aliases is taken - */ - public void registerItemModule( - ResourceLocation id, zone.moddev.mc.lib.common.modules.DataModules.Item module) { - registerItemModule(id, module, false); - } - - /** - * Registers an item data module, optionally replacing an existing item module with the same ID - * (as a later datapack may). - * - * @param replace whether an existing item module with this ID may be replaced - */ - public void registerItemModule( - ResourceLocation id, - zone.moddev.mc.lib.common.modules.DataModules.Item module, - boolean replace) { - validatePart(module.definition(id), replace); - checkMod(module.requiredMod()); - var entry = - new ItemType( - new zone.moddev.mc.lib.common.modules.ModuleFactories.ItemFactory(this, id, module), + /** + * The shared catalog, with MMDLib's bundled modules and built-in + * implementations installed. + */ + public static final MaterialExtensions GLOBAL = new MaterialExtensions(); + + /** + * A registered item part: the factory that builds its template and item, + * and the mod it requires, if any. + */ + public record ItemType(ItemTypeFactory factory, + Optional requiredMod) { + } + + /** + * A registered block part: the factory that builds its template and block, + * and the mod it requires, if any. + */ + public record BlockType(BlockTypeFactory factory, + Optional requiredMod) { + } + + /** + * Definitions built in code rather than read from {@code mmdlib/} files. + * + * @param materials + * named materials, planned like {@code mmdlib/material} files + * @param standaloneBlocks + * standalone blocks by registry ID, planned like {@code + * mmdlib/standalone/block} files + * @param standaloneItems + * standalone items by registry ID, planned like {@code + * mmdlib/standalone/item} files + */ + public record Definitions(java.util.Collection materials, + Map standaloneBlocks, + Map standaloneItems) { + /** Nothing. */ + public static final Definitions EMPTY = new Definitions( + java.util.List.of(), Map.of(), Map.of()); + + /** Copies every collection. */ + public Definitions { + materials = java.util.List.copyOf(materials); + standaloneBlocks = Map.copyOf(standaloneBlocks); + standaloneItems = Map.copyOf(standaloneItems); + } + + /** Materials and standalone blocks, without standalone items. */ + public Definitions(java.util.Collection materials, + Map standaloneBlocks) { + this(materials, standaloneBlocks, Map.of()); + } + } + + /** + * Supplies {@link Definitions} built in code. The definition loader calls + * it once, after every extension file and data module is installed and + * before any material file is decoded, so it may register item and block + * modules and callbacks on {@code api} that material files then use. + */ + @FunctionalInterface + public interface DefinitionSource { + /** + * Registers any modules and callbacks on {@code api} and returns the + * definitions. + */ + Definitions load(MaterialExtensions api); + } + + /** + * A registered definition source. + * + * @param name + * names the source in loading errors, such as + * {@code "CraftTweaker scripts"} + * @param source + * the source + */ + public record NamedSource(String name, DefinitionSource source) { + /** Checks that both values are present and the name is not blank. */ + public NamedSource { + Objects.requireNonNull(source); + if (name.isBlank()) + throw new IllegalArgumentException( + "Definition source needs a name"); + } + } + + /** + * An immutable copy of the registered item and block parts at one moment. + * Decoding takes a snapshot, so a definition decoded earlier is not changed + * by later registrations. + */ + public record Snapshot(Map items, + Map blocks) { + /** Copies both maps. */ + public Snapshot { + items = Map.copyOf(items); + blocks = Map.copyOf(blocks); + } + + /** Every registered part, resolvable by ID or alias. */ + public PartRegistry parts() { + var parts = new java.util.ArrayList(); + items.forEach((id, type) -> parts.add(type + .factory() instanceof zone.moddev.mc.lib.common.modules.ModuleFactories.Reference module + ? module.part() + : new MaterialPart(id, MaterialPart.Kind.ITEM, + type.requiredMod(), PartMetadata.EMPTY))); + blocks.forEach((id, type) -> parts.add(type + .factory() instanceof zone.moddev.mc.lib.common.modules.ModuleFactories.Reference module + ? module.part() + : new MaterialPart(id, MaterialPart.Kind.BLOCK, + type.requiredMod(), PartMetadata.EMPTY))); + return new PartRegistry(parts); + } + } + + private final ConcurrentHashMap items = new ConcurrentHashMap<>(); + private final ConcurrentHashMap blocks = new ConcurrentHashMap<>(); + private final MaterialEvents events = new MaterialEvents(); + private final java.util.List definitionSources = new java.util.concurrent.CopyOnWriteArrayList<>(); + private final ConcurrentHashMap> itemImplementations = new ConcurrentHashMap<>(); + private final ConcurrentHashMap> blockImplementations = new ConcurrentHashMap<>(); + private final ConcurrentHashMap itemModules = new ConcurrentHashMap<>(); + private final ConcurrentHashMap blockModules = new ConcurrentHashMap<>(); + private final ConcurrentHashMap fluids = new ConcurrentHashMap<>(); + + /** + * A catalog with MMDLib's bundled modules and built-in implementations + * installed. + */ + public MaterialExtensions() { + this(true); + } + + /** + * A catalog, optionally empty. + * + * @param includeBundledModules + * whether to install MMDLib's bundled modules and built-in + * implementations; {@code false} gives an empty catalog, mainly + * for tests + */ + public MaterialExtensions(boolean includeBundledModules) { + if (includeBundledModules) { + ModuleCatalog.bundled().install(this); + // MMDLib's own factories, through the same public registration + // calls an addon uses. + zone.moddev.mc.lib.common.runtime.StandardImplementations + .install(this); + } + } + + /** + * Registers the Java code a data module's {@code factory} names for items. + * + * @throws IllegalArgumentException + * if the ID is already registered + */ + public void registerItemImplementation(ResourceLocation id, + ItemImplementation implementation) { + Objects.requireNonNull(implementation.itemClass()); + if (itemImplementations.putIfAbsent(id, implementation) != null) + throw new IllegalArgumentException( + "Duplicate item implementation: " + id); + } + + /** + * Registers the Java code a data module's {@code factory} names for blocks. + * + * @throws IllegalArgumentException + * if the ID is already registered + */ + public void registerBlockImplementation( + ResourceLocation id, BlockImplementation implementation) { + Objects.requireNonNull(implementation.blockClass()); + if (blockImplementations.putIfAbsent(id, implementation) != null) + throw new IllegalArgumentException( + "Duplicate block implementation: " + id); + } + + /** The item implementation registered under {@code id}, if any. */ + public Optional> findItemImplementation( + ResourceLocation id) { + return Optional.ofNullable(itemImplementations.get(id)); + } + + /** The block implementation registered under {@code id}, if any. */ + public Optional> findBlockImplementation( + ResourceLocation id) { + return Optional.ofNullable(blockImplementations.get(id)); + } + + /** + * The item implementation registered under {@code id}. + * + * @throws IllegalStateException + * if none is installed + */ + public ItemImplementation itemImplementation(ResourceLocation id) { + var implementation = itemImplementations.get(id); + if (implementation == null) + throw new IllegalStateException( + "Runtime item implementation not installed: " + id); + return implementation; + } + + /** + * The block implementation registered under {@code id}. + * + * @throws IllegalStateException + * if none is installed + */ + public BlockImplementation blockImplementation(ResourceLocation id) { + var implementation = blockImplementations.get(id); + if (implementation == null) + throw new IllegalStateException( + "Runtime block implementation not installed: " + id); + return implementation; + } + + /** + * Registers an item data module as a selectable item part. + * + * @throws IllegalArgumentException + * if the ID or one of its aliases is taken + */ + public void registerItemModule(ResourceLocation id, + zone.moddev.mc.lib.common.modules.DataModules.Item module) { + registerItemModule(id, module, false); + } + + /** + * Registers an item data module, optionally replacing an existing item + * module with the same ID (as a later datapack may). + * + * @param replace + * whether an existing item module with this ID may be replaced + */ + public void registerItemModule(ResourceLocation id, + zone.moddev.mc.lib.common.modules.DataModules.Item module, + boolean replace) { + validatePart(module.definition(id), replace); + checkMod(module.requiredMod()); + var entry = new ItemType( + new zone.moddev.mc.lib.common.modules.ModuleFactories.ItemFactory( + this, id, module), module.requiredMod()); - if (replace) items.put(id, entry); - else if (items.putIfAbsent(id, entry) != null) - throw new IllegalArgumentException("Duplicate item module: " + id); - itemModules.put(id, module); - } - - /** A registered data item module, e.g. the one a standalone item names. */ - public zone.moddev.mc.lib.common.modules.DataModules.Item itemModule(ResourceLocation id) { - var module = itemModules.get(id); - if (module == null) throw new IllegalArgumentException("Unknown item module: " + id); - return module; - } - - /** - * Registers a block data module as a selectable block part. - * - * @throws IllegalArgumentException if the ID or one of its aliases is taken - */ - public void registerBlockModule( - ResourceLocation id, zone.moddev.mc.lib.common.modules.DataModules.Block module) { - registerBlockModule(id, module, false); - } - - /** - * Registers a block data module, optionally replacing an existing block module with the same ID. - * - * @param replace whether an existing block module with this ID may be replaced - */ - public void registerBlockModule( - ResourceLocation id, - zone.moddev.mc.lib.common.modules.DataModules.Block module, - boolean replace) { - validatePart(module.definition(id), replace); - checkMod(module.requiredMod()); - var entry = - new BlockType( - new zone.moddev.mc.lib.common.modules.ModuleFactories.BlockFactory(this, id, module), + if (replace) + items.put(id, entry); + else if (items.putIfAbsent(id, entry) != null) + throw new IllegalArgumentException("Duplicate item module: " + id); + itemModules.put(id, module); + } + + /** A registered data item module, e.g. the one a standalone item names. */ + public zone.moddev.mc.lib.common.modules.DataModules.Item itemModule( + ResourceLocation id) { + var module = itemModules.get(id); + if (module == null) + throw new IllegalArgumentException("Unknown item module: " + id); + return module; + } + + /** + * Registers a block data module as a selectable block part. + * + * @throws IllegalArgumentException + * if the ID or one of its aliases is taken + */ + public void registerBlockModule(ResourceLocation id, + zone.moddev.mc.lib.common.modules.DataModules.Block module) { + registerBlockModule(id, module, false); + } + + /** + * Registers a block data module, optionally replacing an existing block + * module with the same ID. + * + * @param replace + * whether an existing block module with this ID may be replaced + */ + public void registerBlockModule(ResourceLocation id, + zone.moddev.mc.lib.common.modules.DataModules.Block module, + boolean replace) { + validatePart(module.definition(id), replace); + checkMod(module.requiredMod()); + var entry = new BlockType( + new zone.moddev.mc.lib.common.modules.ModuleFactories.BlockFactory( + this, id, module), module.requiredMod()); - if (replace) blocks.put(id, entry); - else if (blocks.putIfAbsent(id, entry) != null) - throw new IllegalArgumentException("Duplicate block module: " + id); - blockModules.put(id, module); - } - - /** A registered data block module, e.g. the one a standalone block names. */ - public zone.moddev.mc.lib.common.modules.DataModules.Block blockModule(ResourceLocation id) { - var module = blockModules.get(id); - if (module == null) throw new IllegalArgumentException("Unknown block module: " + id); - return module; - } - - /** - * Registers a fluid data module, which a material selects with its {@code fluid} setting. - * - * @throws IllegalArgumentException if the ID is taken - */ - public void registerFluidModule( - ResourceLocation id, zone.moddev.mc.lib.common.modules.DataModules.Fluid module) { - registerFluidModule(id, module, false); - } - - /** - * Registers a fluid data module, optionally replacing an existing one with the same ID. - * - * @param replace whether an existing fluid module with this ID may be replaced - */ - public void registerFluidModule( - ResourceLocation id, - zone.moddev.mc.lib.common.modules.DataModules.Fluid module, - boolean replace) { - checkMod(module.requiredMod()); - if (replace) fluids.put(id, module); - else if (fluids.putIfAbsent(id, module) != null) - throw new IllegalArgumentException("Duplicate fluid module: " + id); - } - - /** - * The fluid module registered under {@code id}. - * - * @throws IllegalArgumentException if there is none - */ - public zone.moddev.mc.lib.common.modules.DataModules.Fluid fluidModule(ResourceLocation id) { - var module = fluids.get(id); - if (module == null) throw new IllegalArgumentException("Unknown fluid module: " + id); - return module; - } - - /** - * Whether {@code part} can be built: true unless it requires a mod that {@code installed} reports - * missing. - * - * @param installed tests whether a mod ID is loaded - */ - public boolean available(MaterialPart part, Predicate installed) { - var id = part.templateId(); - Optional requirement; - if (part.kind() == MaterialPart.Kind.ITEM && items.containsKey(id)) - requirement = items.get(id).requiredMod(); - else if (part.kind() != MaterialPart.Kind.ITEM && blocks.containsKey(id)) - requirement = blocks.get(id).requiredMod(); - else return part.available(installed); - return requirement.isEmpty() || installed.test(requirement.get()); - } - - /** - * A planner holding the templates for every part {@code material} selects and can build with the - * installed mods, plus its fluid when it has one. - * - * @throws IllegalArgumentException if the material selects an unknown part or a fluid module - * whose mod is missing - */ - public zone.moddev.mc.lib.common.builders.MaterialContentPlanner planner( - MMDMaterial material, Predicate installed) { - var planner = new zone.moddev.mc.lib.common.builders.MaterialContentPlanner(); - for (var part : selection(material).parts()) { - if (!available(part, installed)) continue; - if (part.kind() == MaterialPart.Kind.ITEM) { - var entry = items.get(part.templateId()); - if (entry == null) throw new IllegalArgumentException("Unknown item part: " + part); - var template = entry.factory().template(material); - if (!(entry.factory() - instanceof zone.moddev.mc.lib.common.modules.ModuleFactories.Reference)) - template = template.setType(part.templateId().toString()); - planner = planner.withItemTemplate(part, template); - } else { - var entry = blocks.get(part.templateId()); - if (entry == null) throw new IllegalArgumentException("Unknown block part: " + part); - var template = entry.factory().template(material); - if (!(entry.factory() - instanceof zone.moddev.mc.lib.common.modules.ModuleFactories.Reference)) - template = template.setType(part.templateId().toString()); - planner = planner.withBlockTemplate(part, template); - } - if (part.needsEntity()) - planner = - planner.withEntityTemplate( - part, - zone.moddev.mc.lib.common.builders.EntityBuilder.create() - .setFactory(part.entityFactory().orElseThrow()) - .setFactoryData(part.metadata().entityData())); - } - if (material.parts().hasFluid()) { - var id = material.selection().fluidModule(); - var module = fluidModule(id); - if (module.requiredMod().isPresent() && !installed.test(module.requiredMod().get())) - throw new IllegalArgumentException( - "Selected fluid module requires " + module.requiredMod().get()); - planner = planner.withFluidTemplate(module.builder(id, material)); - } - return planner; - } - - /** - * The parts {@code material} selects, resolved against this catalog. Parts that need a world - * entity are kept only when the material selects that entity. - */ - public MaterialParts selection(MMDMaterial material) { - var registry = snapshot().parts(); - var result = new java.util.HashSet(); - for (var part : material.parts().parts()) - result.add(registry.resolve(part.templateId().toString()).getOrThrow()); - for (var id : material.selection().customItems()) { - var part = registry.resolve(id.toString()).getOrThrow(); - if (part.kind() != MaterialPart.Kind.ITEM) - throw new IllegalArgumentException("Not an item part: " + id); - if (!part.needsEntity() || material.selection().entities().contains(part)) result.add(part); - } - for (var id : material.selection().customBlocks()) { - var part = registry.resolve(id.toString()).getOrThrow(); - if (part.kind() == MaterialPart.Kind.ITEM) - throw new IllegalArgumentException("Not a block part: " + id); - result.add(part); - } - result.removeIf(part -> part.needsEntity() && !material.selection().entities().contains(part)); - return new MaterialParts(result, material.parts().hasFluid()); - } - - private void validatePart(MaterialPart part, boolean replace) { - var current = snapshot().parts(); - var existing = current.entries().get(part.templateId()); - if (existing != null - && (!replace - || (existing.kind() == MaterialPart.Kind.ITEM) - != (part.kind() == MaterialPart.Kind.ITEM))) - throw new IllegalArgumentException("Duplicate material part or conflicting section: " + part); - var candidates = new java.util.ArrayList<>(current.entries().values()); - candidates.removeIf(candidate -> candidate.equals(part)); - candidates.add(part); - new PartRegistry(candidates); // Reject aliases that shadow any canonical ID or another alias. - } - - /** The event registry used by this catalog's definitions. */ - public MaterialEvents events() { - return events; - } - - /** - * Adds code-built definitions to definition loading. Register during mod construction. The - * loader calls each source once (see {@link DefinitionSource}) and plans what it returns with the - * same checks as definition files. Content outside MMDLib's namespace is only registered by its - * own mod's {@link zone.moddev.mc.lib.common.runtime.RuntimeContentRegistrar}. - * - * @param name names the source in loading errors; unique among sources - * @param source supplies the definitions - * @throws IllegalArgumentException if a source with this name is already registered - */ - public synchronized void registerDefinitionSource(String name, DefinitionSource source) { - var named = new NamedSource(name, source); - if (definitionSources.stream().anyMatch(existing -> existing.name().equals(name))) - throw new IllegalArgumentException("Duplicate definition source '" + name + "'"); - definitionSources.add(named); - } - - /** The registered definition sources, in registration order. */ - public java.util.List definitionSources() { - return java.util.List.copyOf(definitionSources); - } - - /** - * Registers a code-defined item part. - * - * @throws IllegalArgumentException if the ID or an alias is taken - */ - public void registerItemType(ResourceLocation id, ItemTypeFactory factory) { - registerItemType(id, factory, Optional.empty()); - } - - /** - * Registers a code-defined item part that is planned only when {@code requiredMod} is loaded. Do - * not load classes from that mod while registering. - */ - public void registerItemType( - ResourceLocation id, ItemTypeFactory factory, Optional requiredMod) { - validatePart( - new MaterialPart(id, MaterialPart.Kind.ITEM, requiredMod, PartMetadata.EMPTY), false); - Objects.requireNonNull(factory.itemClass()); - checkMod(requiredMod); - if (items.putIfAbsent(id, new ItemType(factory, requiredMod)) != null) - throw new IllegalArgumentException("Item type already registered: " + id); - } - - /** - * Registers a code-defined block part. - * - * @throws IllegalArgumentException if the ID or an alias is taken - */ - public void registerBlockType( - ResourceLocation id, BlockTypeFactory factory) { - registerBlockType(id, factory, Optional.empty()); - } - - /** - * Registers a code-defined block part that is planned only when {@code requiredMod} is loaded. Do - * not load classes from that mod while registering. - */ - public void registerBlockType( - ResourceLocation id, BlockTypeFactory factory, Optional requiredMod) { - validatePart( - new MaterialPart(id, MaterialPart.Kind.BLOCK, requiredMod, PartMetadata.EMPTY), false); - Objects.requireNonNull(factory.blockClass()); - checkMod(requiredMod); - if (blocks.putIfAbsent(id, new BlockType(factory, requiredMod)) != null) - throw new IllegalArgumentException("Block type already registered: " + id); - } - - private static void checkMod(Optional mod) { - Objects.requireNonNull(mod); - if (mod.isPresent() && !mod.get().matches("[a-z][a-z0-9_]{1,63}")) - throw new IllegalArgumentException("Invalid required mod ID"); - } - - /** An immutable copy of the registered parts. */ - public Snapshot snapshot() { - return new Snapshot(Map.copyOf(items), Map.copyOf(blocks)); - } - - /** - * Adds the extra-definition views ({@code extraItems}, {@code extraBlocks}) for code-registered - * and addon parts to an existing plan, keyed by part ID. Kept for compatibility: plans are - * already complete, so nothing new is planned here. - * - * @throws IllegalArgumentException if two planned items or blocks share an ID - */ - public MaterialContentPlan extend( - MMDMaterial material, MaterialContentPlan base, Predicate installed) { - var itemDefinitions = new java.util.LinkedHashMap(); - var blockDefinitions = new java.util.LinkedHashMap(); - var itemIds = new java.util.HashSet(); - if (base.fluid() != null && base.fluid().bucket() != null) - itemIds.add(base.fluid().bucket().name()); - base.items() - .forEach( - (part, item) -> { - if (!itemIds.add(item.name())) - throw new IllegalArgumentException("Duplicate item ID: " + item.name()); - if (!ModuleCatalog.bundled().parts().entries().containsKey(part.templateId())) + if (replace) + blocks.put(id, entry); + else if (blocks.putIfAbsent(id, entry) != null) + throw new IllegalArgumentException("Duplicate block module: " + id); + blockModules.put(id, module); + } + + /** + * A registered data block module, e.g. the one a standalone block names. + */ + public zone.moddev.mc.lib.common.modules.DataModules.Block blockModule( + ResourceLocation id) { + var module = blockModules.get(id); + if (module == null) + throw new IllegalArgumentException("Unknown block module: " + id); + return module; + } + + /** + * Registers a fluid data module, which a material selects with its + * {@code fluid} setting. + * + * @throws IllegalArgumentException + * if the ID is taken + */ + public void registerFluidModule(ResourceLocation id, + zone.moddev.mc.lib.common.modules.DataModules.Fluid module) { + registerFluidModule(id, module, false); + } + + /** + * Registers a fluid data module, optionally replacing an existing one with + * the same ID. + * + * @param replace + * whether an existing fluid module with this ID may be replaced + */ + public void registerFluidModule(ResourceLocation id, + zone.moddev.mc.lib.common.modules.DataModules.Fluid module, + boolean replace) { + checkMod(module.requiredMod()); + if (replace) + fluids.put(id, module); + else if (fluids.putIfAbsent(id, module) != null) + throw new IllegalArgumentException("Duplicate fluid module: " + id); + } + + /** + * The fluid module registered under {@code id}. + * + * @throws IllegalArgumentException + * if there is none + */ + public zone.moddev.mc.lib.common.modules.DataModules.Fluid fluidModule( + ResourceLocation id) { + var module = fluids.get(id); + if (module == null) + throw new IllegalArgumentException("Unknown fluid module: " + id); + return module; + } + + /** + * Whether {@code part} can be built: true unless it requires a mod that + * {@code installed} reports missing. + * + * @param installed + * tests whether a mod ID is loaded + */ + public boolean available(MaterialPart part, Predicate installed) { + var id = part.templateId(); + Optional requirement; + if (part.kind() == MaterialPart.Kind.ITEM && items.containsKey(id)) + requirement = items.get(id).requiredMod(); + else if (part.kind() != MaterialPart.Kind.ITEM + && blocks.containsKey(id)) + requirement = blocks.get(id).requiredMod(); + else + return part.available(installed); + return requirement.isEmpty() || installed.test(requirement.get()); + } + + /** + * A planner holding the templates for every part {@code material} selects + * and can build with the installed mods, plus its fluid when it has one. + * + * @throws IllegalArgumentException + * if the material selects an unknown part or a fluid module + * whose mod is missing + */ + public zone.moddev.mc.lib.common.builders.MaterialContentPlanner planner( + MMDMaterial material, Predicate installed) { + var planner = new zone.moddev.mc.lib.common.builders.MaterialContentPlanner(); + for (var part : selection(material).parts()) { + if (!available(part, installed)) + continue; + if (part.kind() == MaterialPart.Kind.ITEM) { + var entry = items.get(part.templateId()); + if (entry == null) + throw new IllegalArgumentException( + "Unknown item part: " + part); + var template = entry.factory().template(material); + if (!(entry + .factory() instanceof zone.moddev.mc.lib.common.modules.ModuleFactories.Reference)) + template = template.setType(part.templateId().toString()); + planner = planner.withItemTemplate(part, template); + } else { + var entry = blocks.get(part.templateId()); + if (entry == null) + throw new IllegalArgumentException( + "Unknown block part: " + part); + var template = entry.factory().template(material); + if (!(entry + .factory() instanceof zone.moddev.mc.lib.common.modules.ModuleFactories.Reference)) + template = template.setType(part.templateId().toString()); + planner = planner.withBlockTemplate(part, template); + } + if (part.needsEntity()) + planner = planner.withEntityTemplate(part, + zone.moddev.mc.lib.common.builders.EntityBuilder.create() + .setFactory(part.entityFactory().orElseThrow()) + .setFactoryData(part.metadata().entityData())); + } + if (material.parts().hasFluid()) { + var id = material.selection().fluidModule(); + var module = fluidModule(id); + if (module.requiredMod().isPresent() + && !installed.test(module.requiredMod().get())) + throw new IllegalArgumentException( + "Selected fluid module requires " + + module.requiredMod().get()); + planner = planner.withFluidTemplate(module.builder(id, material)); + } + return planner; + } + + /** + * The parts {@code material} selects, resolved against this catalog. Parts + * that need a world entity are kept only when the material selects that + * entity. + */ + public MaterialParts selection(MMDMaterial material) { + var registry = snapshot().parts(); + var result = new java.util.HashSet(); + for (var part : material.parts().parts()) + result.add( + registry.resolve(part.templateId().toString()).getOrThrow()); + for (var id : material.selection().customItems()) { + var part = registry.resolve(id.toString()).getOrThrow(); + if (part.kind() != MaterialPart.Kind.ITEM) + throw new IllegalArgumentException("Not an item part: " + id); + if (!part.needsEntity() + || material.selection().entities().contains(part)) + result.add(part); + } + for (var id : material.selection().customBlocks()) { + var part = registry.resolve(id.toString()).getOrThrow(); + if (part.kind() == MaterialPart.Kind.ITEM) + throw new IllegalArgumentException("Not a block part: " + id); + result.add(part); + } + result.removeIf(part -> part.needsEntity() + && !material.selection().entities().contains(part)); + return new MaterialParts(result, material.parts().hasFluid()); + } + + private void validatePart(MaterialPart part, boolean replace) { + var current = snapshot().parts(); + var existing = current.entries().get(part.templateId()); + if (existing != null + && (!replace || (existing.kind() == MaterialPart.Kind.ITEM) != (part + .kind() == MaterialPart.Kind.ITEM))) + throw new IllegalArgumentException( + "Duplicate material part or conflicting section: " + part); + var candidates = new java.util.ArrayList<>(current.entries().values()); + candidates.removeIf(candidate -> candidate.equals(part)); + candidates.add(part); + new PartRegistry(candidates); // Reject aliases that shadow any + // canonical ID or another + // alias. + } + + /** The event registry used by this catalog's definitions. */ + public MaterialEvents events() { + return events; + } + + /** + * Adds code-built definitions to definition loading. Register during mod + * construction. The loader calls each source once (see + * {@link DefinitionSource}) and plans what it returns with the same checks + * as definition files. Content outside MMDLib's namespace is only + * registered by its own mod's + * {@link zone.moddev.mc.lib.common.runtime.RuntimeContentRegistrar}. + * + * @param name + * names the source in loading errors; unique among sources + * @param source + * supplies the definitions + * @throws IllegalArgumentException + * if a source with this name is already registered + */ + public synchronized void registerDefinitionSource(String name, + DefinitionSource source) { + var named = new NamedSource(name, source); + if (definitionSources.stream() + .anyMatch(existing -> existing.name().equals(name))) + throw new IllegalArgumentException( + "Duplicate definition source '" + name + "'"); + definitionSources.add(named); + } + + /** The registered definition sources, in registration order. */ + public java.util.List definitionSources() { + return java.util.List.copyOf(definitionSources); + } + + /** + * Registers a code-defined item part. + * + * @throws IllegalArgumentException + * if the ID or an alias is taken + */ + public void registerItemType(ResourceLocation id, + ItemTypeFactory factory) { + registerItemType(id, factory, Optional.empty()); + } + + /** + * Registers a code-defined item part that is planned only when + * {@code requiredMod} is loaded. Do not load classes from that mod while + * registering. + */ + public void registerItemType(ResourceLocation id, + ItemTypeFactory factory, Optional requiredMod) { + validatePart(new MaterialPart(id, MaterialPart.Kind.ITEM, requiredMod, + PartMetadata.EMPTY), false); + Objects.requireNonNull(factory.itemClass()); + checkMod(requiredMod); + if (items.putIfAbsent(id, new ItemType(factory, requiredMod)) != null) + throw new IllegalArgumentException( + "Item type already registered: " + id); + } + + /** + * Registers a code-defined block part. + * + * @throws IllegalArgumentException + * if the ID or an alias is taken + */ + public void registerBlockType(ResourceLocation id, + BlockTypeFactory factory) { + registerBlockType(id, factory, Optional.empty()); + } + + /** + * Registers a code-defined block part that is planned only when + * {@code requiredMod} is loaded. Do not load classes from that mod while + * registering. + */ + public void registerBlockType(ResourceLocation id, + BlockTypeFactory factory, Optional requiredMod) { + validatePart(new MaterialPart(id, MaterialPart.Kind.BLOCK, requiredMod, + PartMetadata.EMPTY), false); + Objects.requireNonNull(factory.blockClass()); + checkMod(requiredMod); + if (blocks.putIfAbsent(id, new BlockType(factory, requiredMod)) != null) + throw new IllegalArgumentException( + "Block type already registered: " + id); + } + + private static void checkMod(Optional mod) { + Objects.requireNonNull(mod); + if (mod.isPresent() && !mod.get().matches("[a-z][a-z0-9_]{1,63}")) + throw new IllegalArgumentException("Invalid required mod ID"); + } + + /** An immutable copy of the registered parts. */ + public Snapshot snapshot() { + return new Snapshot(Map.copyOf(items), Map.copyOf(blocks)); + } + + /** + * Adds the extra-definition views ({@code extraItems}, {@code extraBlocks}) + * for code-registered and addon parts to an existing plan, keyed by part + * ID. Kept for compatibility: plans are already complete, so nothing new is + * planned here. + * + * @throws IllegalArgumentException + * if two planned items or blocks share an ID + */ + public MaterialContentPlan extend(MMDMaterial material, + MaterialContentPlan base, Predicate installed) { + var itemDefinitions = new java.util.LinkedHashMap(); + var blockDefinitions = new java.util.LinkedHashMap(); + var itemIds = new java.util.HashSet(); + if (base.fluid() != null && base.fluid().bucket() != null) + itemIds.add(base.fluid().bucket().name()); + base.items().forEach((part, item) -> { + if (!itemIds.add(item.name())) + throw new IllegalArgumentException( + "Duplicate item ID: " + item.name()); + if (!ModuleCatalog.bundled().parts().entries() + .containsKey(part.templateId())) itemDefinitions.put(part.templateId(), item); - }); - var blockIds = new java.util.HashSet(); - if (base.fluid() != null && base.fluid().block() != null) - blockIds.add(base.fluid().block().name()); - base.blocks() - .forEach( - (part, block) -> { - if (!blockIds.add(block.name())) - throw new IllegalArgumentException("Duplicate block ID: " + block.name()); - if (!ModuleCatalog.bundled().parts().entries().containsKey(part.templateId())) + }); + var blockIds = new java.util.HashSet(); + if (base.fluid() != null && base.fluid().block() != null) + blockIds.add(base.fluid().block().name()); + base.blocks().forEach((part, block) -> { + if (!blockIds.add(block.name())) + throw new IllegalArgumentException( + "Duplicate block ID: " + block.name()); + if (!ModuleCatalog.bundled().parts().entries() + .containsKey(part.templateId())) blockDefinitions.put(part.templateId(), block); - }); - return new MaterialContentPlan( - base.material(), - base.items(), - base.blocks(), - base.entities(), - base.fluid(), - itemDefinitions, - blockDefinitions, - base.blockEntities()); - } - - @SuppressWarnings("unchecked") - private ItemTypeFactory itemFactory(ResourceLocation id, Class expected) { - var entry = items.get(id); - if (entry == null || entry.factory().itemClass() != expected) - throw new IllegalArgumentException("Unknown item type or wrong class: " + id); - return (ItemTypeFactory) entry.factory(); - } - - @SuppressWarnings("unchecked") - private BlockTypeFactory blockFactory( - ResourceLocation id, Class expected) { - var entry = blocks.get(id); - if (entry == null || entry.factory().blockClass() != expected) - throw new IllegalArgumentException("Unknown block type or wrong class: " + id); - return (BlockTypeFactory) entry.factory(); - } - - /** - * Queues a code-registered item type's item for one material on the mod's own register. Creation - * is deferred until the registry supplier runs. - * - * @param registry a register in the material's namespace, attached to the mod bus - * @param type the item type ID - * @param expected the item type's class - * @throws IllegalArgumentException if the type, class, namespace, material or definition do not - * match - */ - public DeferredHolder queueItem( - DeferredRegister registry, - ResourceLocation type, - Class expected, - MMDMaterial material, - MMDItem definition) { - var factory = itemFactory(type, expected); - checkDefinition( - registry.getNamespace(), - material, - definition.name(), - definition.material(), - definition.type(), - factory instanceof zone.moddev.mc.lib.common.modules.ModuleFactories.Reference module - ? module.implementation() - : type); - return registry.register( - definition.name().getPath(), - () -> expected.cast(Objects.requireNonNull(factory.create(material, definition)))); - } - - /** - * Queues a code-registered block type's block for one material on the mod's own register. - * Creation is deferred until the registry supplier runs. The block's item is not registered here. - * - * @param registry a register in the material's namespace, attached to the mod bus - * @param type the block type ID - * @param expected the block type's class - * @throws IllegalArgumentException if the type, class, namespace, material or definition do not - * match - */ - public DeferredHolder queueBlock( - DeferredRegister registry, - ResourceLocation type, - Class expected, - MMDMaterial material, - MMDBlock definition) { - var factory = blockFactory(type, expected); - checkDefinition( - registry.getNamespace(), - material, - definition.name(), - definition.material(), - definition.type(), - factory instanceof zone.moddev.mc.lib.common.modules.ModuleFactories.Reference module - ? module.implementation() - : type); - return registry.register( - definition.name().getPath(), - () -> expected.cast(Objects.requireNonNull(factory.create(material, definition)))); - } - - private static void checkDefinition( - String namespace, - MMDMaterial material, - ResourceLocation name, - ResourceLocation owner, - String declaredType, - ResourceLocation type) { - if (!name.getNamespace().equals(namespace) - || !material.name().orElseThrow().equals(owner) - || !declaredType.equals(type.toString())) - throw new IllegalArgumentException( - "Definition does not belong to this registry, material, or type: " + name); - } + }); + return new MaterialContentPlan(base.material(), base.items(), + base.blocks(), base.entities(), base.fluid(), itemDefinitions, + blockDefinitions, base.blockEntities()); + } + + @SuppressWarnings("unchecked") + private ItemTypeFactory itemFactory(ResourceLocation id, + Class expected) { + var entry = items.get(id); + if (entry == null || entry.factory().itemClass() != expected) + throw new IllegalArgumentException( + "Unknown item type or wrong class: " + id); + return (ItemTypeFactory) entry.factory(); + } + + @SuppressWarnings("unchecked") + private BlockTypeFactory blockFactory( + ResourceLocation id, Class expected) { + var entry = blocks.get(id); + if (entry == null || entry.factory().blockClass() != expected) + throw new IllegalArgumentException( + "Unknown block type or wrong class: " + id); + return (BlockTypeFactory) entry.factory(); + } + + /** + * Queues a code-registered item type's item for one material on the mod's + * own register. Creation is deferred until the registry supplier runs. + * + * @param registry + * a register in the material's namespace, attached to the mod + * bus + * @param type + * the item type ID + * @param expected + * the item type's class + * @throws IllegalArgumentException + * if the type, class, namespace, material or definition do not + * match + */ + public DeferredHolder queueItem( + DeferredRegister registry, ResourceLocation type, + Class expected, MMDMaterial material, MMDItem definition) { + var factory = itemFactory(type, expected); + checkDefinition(registry.getNamespace(), material, definition.name(), + definition.material(), definition.type(), + factory instanceof zone.moddev.mc.lib.common.modules.ModuleFactories.Reference module + ? module.implementation() + : type); + return registry.register(definition.name().getPath(), + () -> expected.cast( + Objects.requireNonNull(factory.create(material, definition)))); + } + + /** + * Queues a code-registered block type's block for one material on the mod's + * own register. Creation is deferred until the registry supplier runs. The + * block's item is not registered here. + * + * @param registry + * a register in the material's namespace, attached to the mod + * bus + * @param type + * the block type ID + * @param expected + * the block type's class + * @throws IllegalArgumentException + * if the type, class, namespace, material or definition do not + * match + */ + public DeferredHolder queueBlock( + DeferredRegister registry, ResourceLocation type, + Class expected, MMDMaterial material, MMDBlock definition) { + var factory = blockFactory(type, expected); + checkDefinition(registry.getNamespace(), material, definition.name(), + definition.material(), definition.type(), + factory instanceof zone.moddev.mc.lib.common.modules.ModuleFactories.Reference module + ? module.implementation() + : type); + return registry.register(definition.name().getPath(), + () -> expected.cast( + Objects.requireNonNull(factory.create(material, definition)))); + } + + private static void checkDefinition(String namespace, MMDMaterial material, + ResourceLocation name, ResourceLocation owner, String declaredType, + ResourceLocation type) { + if (!name.getNamespace().equals(namespace) + || !material.name().orElseThrow().equals(owner) + || !declaredType.equals(type.toString())) + throw new IllegalArgumentException( + "Definition does not belong to this registry, material, or type: " + + name); + } } diff --git a/src/main/java/zone/moddev/mc/lib/api/MaterialRegistration.java b/src/main/java/zone/moddev/mc/lib/api/MaterialRegistration.java index fe170bd7..a825f1a6 100644 --- a/src/main/java/zone/moddev/mc/lib/api/MaterialRegistration.java +++ b/src/main/java/zone/moddev/mc/lib/api/MaterialRegistration.java @@ -6,61 +6,61 @@ import zone.moddev.mc.lib.util.Registries; /** - * Publishes loaded definitions to MMDLib's own definition registries ({@code mmdlib:materials}, - * {@code mmdlib:items}, ...) from a {@link RegisterEvent} handler, so other mods can look them up - * by ID. These registries hold MMDLib's definitions, not Minecraft game objects. + * Publishes loaded definitions to MMDLib's own definition registries + * ({@code mmdlib:materials}, {@code mmdlib:items}, ...) from a + * {@link RegisterEvent} handler, so other mods can look them up by ID. These + * registries hold MMDLib's definitions, not Minecraft game objects. */ public final class MaterialRegistration { - private MaterialRegistration() {} + private MaterialRegistration() { + } - /** Publishes every callback known to {@code extensions} to the event-callback registry. */ - public static void publishCallbacks(RegisterEvent event, MaterialExtensions extensions) { - event.register( - Registries.EVENT_CALLBACK_KEY, - helper -> extensions.events().callbacks().forEach(helper::register)); - } + /** + * Publishes every callback known to {@code extensions} to the + * event-callback registry. + */ + public static void publishCallbacks(RegisterEvent event, + MaterialExtensions extensions) { + event.register(Registries.EVENT_CALLBACK_KEY, helper -> extensions + .events().callbacks().forEach(helper::register)); + } - /** - * A standalone block's or item's definitions: its items, blocks and block entities. Its host - * material is not a material and is not published. - */ - public static void publishStandalone(RegisterEvent event, MaterialFactory.Result result) { - event.register( - Registries.ITEMS_KEY, - helper -> result.content().itemDefinitions().forEach(helper::register)); - event.register( - Registries.BLOCKS_KEY, - helper -> result.content().blockDefinitions().forEach(helper::register)); - event.register( - Registries.BLOCK_ENTITIES_KEY, - helper -> result.content().blockEntityDefinitions().forEach(helper::register)); - } + /** + * A standalone block's or item's definitions: its items, blocks and block + * entities. Its host material is not a material and is not published. + */ + public static void publishStandalone(RegisterEvent event, + MaterialFactory.Result result) { + event.register(Registries.ITEMS_KEY, helper -> result.content() + .itemDefinitions().forEach(helper::register)); + event.register(Registries.BLOCKS_KEY, helper -> result.content() + .blockDefinitions().forEach(helper::register)); + event.register(Registries.BLOCK_ENTITIES_KEY, helper -> result.content() + .blockEntityDefinitions().forEach(helper::register)); + } - /** - * Publishes one material and all of its planned content: items, blocks, entities, block entities - * and fluid. - */ - public static void publishDefinitions(RegisterEvent event, MaterialFactory.Result result) { - var material = result.material(); - var content = result.content(); - event.register( - Registries.MATERIALS_KEY, - helper -> helper.register(material.name().orElseThrow(), material)); - event.register( - Registries.ITEMS_KEY, helper -> content.itemDefinitions().forEach(helper::register)); - event.register( - Registries.BLOCKS_KEY, helper -> content.blockDefinitions().forEach(helper::register)); - event.register( - Registries.ENTITIES_KEY, - helper -> - content.entities().values().forEach(entity -> helper.register(entity.name(), entity))); - event.register( - Registries.BLOCK_ENTITIES_KEY, - helper -> content.blockEntityDefinitions().forEach(helper::register)); - event.register( - Registries.FLUIDS_KEY, - helper -> { - if (content.fluid() != null) helper.register(content.fluid().name(), content.fluid()); + /** + * Publishes one material and all of its planned content: items, blocks, + * entities, block entities and fluid. + */ + public static void publishDefinitions(RegisterEvent event, + MaterialFactory.Result result) { + var material = result.material(); + var content = result.content(); + event.register(Registries.MATERIALS_KEY, + helper -> helper.register(material.name().orElseThrow(), material)); + event.register(Registries.ITEMS_KEY, + helper -> content.itemDefinitions().forEach(helper::register)); + event.register(Registries.BLOCKS_KEY, + helper -> content.blockDefinitions().forEach(helper::register)); + event.register(Registries.ENTITIES_KEY, + helper -> content.entities().values() + .forEach(entity -> helper.register(entity.name(), entity))); + event.register(Registries.BLOCK_ENTITIES_KEY, helper -> content + .blockEntityDefinitions().forEach(helper::register)); + event.register(Registries.FLUIDS_KEY, helper -> { + if (content.fluid() != null) + helper.register(content.fluid().name(), content.fluid()); }); - } + } } diff --git a/src/main/java/zone/moddev/mc/lib/api/MethodEventInvoker.java b/src/main/java/zone/moddev/mc/lib/api/MethodEventInvoker.java index 7f022763..4915dc01 100644 --- a/src/main/java/zone/moddev/mc/lib/api/MethodEventInvoker.java +++ b/src/main/java/zone/moddev/mc/lib/api/MethodEventInvoker.java @@ -2,15 +2,20 @@ import java.lang.invoke.MethodHandle; -/** Adapts an event context to a declared Java method signature, without runtime reflection. */ +/** + * Adapts an event context to a declared Java method signature, without runtime + * reflection. + */ @FunctionalInterface public interface MethodEventInvoker { - /** - * Calls {@code method} with the arguments this event's signature declares, taken from {@code - * context}. Use {@code invokeExact} with the exact parameter types so no reflection or boxing - * happens per call. - * - * @throws Throwable whatever {@code method} throws - */ - void invoke(MethodHandle method, C context) throws Throwable; + /** + * Calls {@code method} with the arguments this event's signature declares, + * taken from {@code + * context}. Use {@code invokeExact} with the exact parameter types so no + * reflection or boxing happens per call. + * + * @throws Throwable + * whatever {@code method} throws + */ + void invoke(MethodHandle method, C context) throws Throwable; } diff --git a/src/main/java/zone/moddev/mc/lib/api/ResolvedEventHandler.java b/src/main/java/zone/moddev/mc/lib/api/ResolvedEventHandler.java index 9fc84c84..754f480f 100644 --- a/src/main/java/zone/moddev/mc/lib/api/ResolvedEventHandler.java +++ b/src/main/java/zone/moddev/mc/lib/api/ResolvedEventHandler.java @@ -1,36 +1,42 @@ package zone.moddev.mc.lib.api; import java.lang.invoke.MethodHandle; + import java.util.Objects; import zone.moddev.mc.lib.common.codecs.EventExtension; import zone.moddev.mc.lib.common.types.MMDLibEventCallback; /** - * A handler from an extension file, resolved at load: its definition, the static method it names, - * and the callback that adapts an event context to that method's parameters. + * A handler from an extension file, resolved at load: its definition, the + * static method it names, and the callback that adapts an event context to that + * method's parameters. * - * @param definition the handler entry as written in the extension file - * @param methodHandle the resolved {@code public static void} method - * @param callback the typed callback registered under the handler's ID - * @param the event's context type + * @param definition + * the handler entry as written in the extension file + * @param methodHandle + * the resolved {@code public static void} method + * @param callback + * the typed callback registered under the handler's ID + * @param + * the event's context type */ -public record ResolvedEventHandler( - EventExtension.Handler definition, MethodHandle methodHandle, MMDLibEventCallback callback) { - /** Requires every component. */ - public ResolvedEventHandler { - Objects.requireNonNull(definition); - Objects.requireNonNull(methodHandle); - Objects.requireNonNull(callback); - } +public record ResolvedEventHandler(EventExtension.Handler definition, + MethodHandle methodHandle, MMDLibEventCallback callback) { + /** Requires every component. */ + public ResolvedEventHandler { + Objects.requireNonNull(definition); + Objects.requireNonNull(methodHandle); + Objects.requireNonNull(callback); + } - /** The event this handler is registered for. */ - public MaterialEvent event() { - return callback.event(); - } + /** The event this handler is registered for. */ + public MaterialEvent event() { + return callback.event(); + } - /** Runs the handler with {@code context}. */ - public void invoke(C context) { - callback.invoke(context); - } + /** Runs the handler with {@code context}. */ + public void invoke(C context) { + callback.invoke(context); + } } diff --git a/src/main/java/zone/moddev/mc/lib/api/automation/AddonCapabilities.java b/src/main/java/zone/moddev/mc/lib/api/automation/AddonCapabilities.java index 2ac7596c..e2edba38 100644 --- a/src/main/java/zone/moddev/mc/lib/api/automation/AddonCapabilities.java +++ b/src/main/java/zone/moddev/mc/lib/api/automation/AddonCapabilities.java @@ -5,15 +5,17 @@ import net.neoforged.neoforge.capabilities.ItemCapability; /** - * Register providers through RegisterCapabilitiesEvent.registerItem, including third-party items. + * Register providers through RegisterCapabilitiesEvent.registerItem, including + * third-party items. */ public final class AddonCapabilities { - public static final ItemCapability ADDON = - ItemCapability.createVoid( - ResourceLocation.parse("mmdlib:inventory_addon"), InventoryAddon.class); - public static final ItemCapability FILTER = - ItemCapability.createVoid( - ResourceLocation.parse("mmdlib:inventory_filter"), InventoryFilter.class); + public static final ItemCapability ADDON = ItemCapability + .createVoid(ResourceLocation.parse("mmdlib:inventory_addon"), + InventoryAddon.class); + public static final ItemCapability FILTER = ItemCapability + .createVoid(ResourceLocation.parse("mmdlib:inventory_filter"), + InventoryFilter.class); - private AddonCapabilities() {} + private AddonCapabilities() { + } } diff --git a/src/main/java/zone/moddev/mc/lib/api/automation/AddonSpec.java b/src/main/java/zone/moddev/mc/lib/api/automation/AddonSpec.java index ffe3df4d..a85d95c2 100644 --- a/src/main/java/zone/moddev/mc/lib/api/automation/AddonSpec.java +++ b/src/main/java/zone/moddev/mc/lib/api/automation/AddonSpec.java @@ -6,31 +6,34 @@ import zone.moddev.mc.lib.api.capabilities.IoMode; -/** The same direction/content predicate is used by the menu, storage and running addon. */ -public record AddonSpec( - ResourceLocation type, - ResourceType resource, - IoMode direction, - int batch, - int interval, - long energyPerUnit, +/** + * The same direction/content predicate is used by the menu, storage and running + * addon. + */ +public record AddonSpec(ResourceLocation type, ResourceType resource, + IoMode direction, int batch, int interval, long energyPerUnit, boolean filters) { - public AddonSpec { - Objects.requireNonNull(type); - Objects.requireNonNull(resource); - Objects.requireNonNull(direction); - if (direction == IoMode.DISABLED || batch <= 0 || interval <= 0 || energyPerUnit < 0) - throw new IllegalArgumentException("Invalid transfer addon specification"); - } + public AddonSpec { + Objects.requireNonNull(type); + Objects.requireNonNull(resource); + Objects.requireNonNull(direction); + if (direction == IoMode.DISABLED || batch <= 0 || interval <= 0 + || energyPerUnit < 0) + throw new IllegalArgumentException( + "Invalid transfer addon specification"); + } - public boolean accepts(ResourceGroup group) { - return group.type() == resource && group.supports(direction); - } + public boolean accepts(ResourceGroup group) { + return group.type() == resource && group.supports(direction); + } - /** Transfer roles are independent of item ID or tier; BOTH occupies both roles. */ - public boolean conflictsWith(AddonSpec other) { - return resource == other.resource - && ((direction.canInsert() && other.direction.canInsert()) - || (direction.canExtract() && other.direction.canExtract())); - } + /** + * Transfer roles are independent of item ID or tier; BOTH occupies both + * roles. + */ + public boolean conflictsWith(AddonSpec other) { + return resource == other.resource + && ((direction.canInsert() && other.direction.canInsert()) + || (direction.canExtract() && other.direction.canExtract())); + } } diff --git a/src/main/java/zone/moddev/mc/lib/api/automation/AutomationInventory.java b/src/main/java/zone/moddev/mc/lib/api/automation/AutomationInventory.java index 0d93e8e2..6611ef7b 100644 --- a/src/main/java/zone/moddev/mc/lib/api/automation/AutomationInventory.java +++ b/src/main/java/zone/moddev/mc/lib/api/automation/AutomationInventory.java @@ -37,7 +37,8 @@ import zone.moddev.mc.lib.api.capabilities.SideConfig; /** - * Persistent group side modes and four addon/filter pairs per group. Selection belongs to menus. + * Persistent group side modes and four addon/filter pairs per group. Selection + * belongs to menus. */ public final class AutomationInventory { public static final int COLUMNS = 4; @@ -59,7 +60,8 @@ public final class AutomationInventory { private boolean loading; private final ItemInventories slots; - public AutomationInventory(BlockEntity owner, List> groups) { + public AutomationInventory(BlockEntity owner, + List> groups) { if (groups.isEmpty() || groups.size() > MAX_GROUPS) throw new IllegalArgumentException("Group count"); this.owner = Objects.requireNonNull(owner); @@ -73,7 +75,8 @@ public AutomationInventory(BlockEntity owner, List> groups) { if (ids.putIfAbsent(group.id(), i) != null) throw new IllegalArgumentException("Duplicate group ID"); for (int member : group.members()) - if (!membership.add(group.type().id() + "/" + group.inventory() + "/" + member)) + if (!membership.add( + group.type().id() + "/" + group.inventory() + "/" + member)) throw new IllegalArgumentException( "Overlapping resource membership: " + group.id()); var indexed = members.computeIfAbsent(group.type(), @@ -91,8 +94,7 @@ public AutomationInventory(BlockEntity owner, List> groups) { var inventories = new LinkedHashMap(); for (int g = 0; g < groups.size(); g++) { final int groupIndex = g; - inventories.put( - groups.get(g).id(), + inventories.put(groups.get(g).id(), new ItemStorage(SLOTS_PER_GROUP) { @Override public int getSlotLimit(int slot) { @@ -101,19 +103,22 @@ public int getSlotLimit(int slot) { @Override public boolean isItemValid(int slot, ItemStack stack) { - return accepts(groupIndex * SLOTS_PER_GROUP + slot, stack); + return accepts(groupIndex * SLOTS_PER_GROUP + slot, + stack); } @Override public void setStackInSlot(int slot, ItemStack stack) { if (stack.getCount() > 1) - throw new IllegalArgumentException("One addon/filter per slot"); - if (!loading - && !stack.isEmpty() - && !accepts(groupIndex * SLOTS_PER_GROUP + slot, stack)) + throw new IllegalArgumentException( + "One addon/filter per slot"); + if (!loading && !stack.isEmpty() + && !accepts(groupIndex * SLOTS_PER_GROUP + slot, + stack)) throw new IllegalArgumentException( "Incompatible or duplicate addon/filter role"); - // Even replacing an identical stack is a new attachment generation. + // Even replacing an identical stack is a new attachment + // generation. if (!loading && slot % SLOTS_PER_GROUP < COLUMNS) detach(groupIndex, slot % COLUMNS); super.setStackInSlot(slot, stack); @@ -128,9 +133,11 @@ protected void onContentsChanged(int slot) { int key = group * COLUMNS + column; if (slot % SLOTS_PER_GROUP < COLUMNS) { generation[key]++; - // extractItem mutates without calling setStackInSlot. + // extractItem mutates without calling + // setStackInSlot. var addon = addon(group, column); - var filter = AutomationInventory.this.filter(group, column); + var filter = AutomationInventory.this.filter(group, + column); if (!filter.isEmpty() && addon.isEmpty()) detach(group, column); } else { @@ -174,7 +181,8 @@ public long viewRevision() { } /** - * Call after external capability/definition changes; normal slot edits invalidate themselves. + * Call after external capability/definition changes; normal slot edits + * invalidate themselves. */ public void invalidateAddons() { dirtyGroups.set(0, groups.size()); @@ -182,13 +190,17 @@ public void invalidateAddons() { viewChanged(); } - /** Persist any refund that cannot currently fit; retry before the next transfer. */ + /** + * Persist any refund that cannot currently fit; retry before the next + * transfer. + */ public void refund(EnergyBuffer power, long amount) { if (amount < 0) throw new IllegalArgumentException("Negative refund"); if (amount == 0) return; - refund = Math.addExact(refund, amount - (power == null ? 0 : power.refund(amount))); + refund = Math.addExact(refund, + amount - (power == null ? 0 : power.refund(amount))); owner.setChanged(); } @@ -205,10 +217,15 @@ public void settleRefund(EnergyBuffer power) { owner.setChanged(); } - /** Per-owner caches follow neighbour removal, capability invalidation and chunk reloads. */ + /** + * Per-owner caches follow neighbour removal, capability invalidation and + * chunk reloads. + */ @SuppressWarnings("unchecked") - public T neighbour(BlockCapability capability, Direction side) { - if (!(owner.getLevel() instanceof ServerLevel level) || owner.isRemoved()) + public T neighbour(BlockCapability capability, + Direction side) { + if (!(owner.getLevel() instanceof ServerLevel level) + || owner.isRemoved()) return null; if (cacheLevel != level) { neighbours.clear(); @@ -218,20 +235,19 @@ public T neighbour(BlockCapability capability, Direction side) ignored -> new EnumMap<>(Direction.class)); var cache = (BlockCapabilityCache) bySide.computeIfAbsent( side, - direction -> BlockCapabilityCache.create( - capability, - level, + direction -> BlockCapabilityCache.create(capability, level, owner.getBlockPos().relative(direction), direction.getOpposite(), - () -> !owner.isRemoved() && owner.getLevel() == level, - () -> { + () -> !owner.isRemoved() && owner.getLevel() == level, () -> { })); return cache.getCapability(); } public CompoundTag existingTransferState(String group, int column) { String key = group + "/" + column; - return transfers.contains(key, Tag.TAG_COMPOUND) ? transfers.getCompound(key) : null; + return transfers.contains(key, Tag.TAG_COMPOUND) + ? transfers.getCompound(key) + : null; } public CompoundTag transferState(String group, int column) { @@ -240,7 +256,8 @@ public CompoundTag transferState(String group, int column) { var state = new CompoundTag(); int index = index(group); if (index >= 0) - state.putString("resource", group(index).type().id().toString()); + state.putString("resource", + group(index).type().id().toString()); transfers.put(key, state); } return transfers.getCompound(key); @@ -279,7 +296,8 @@ public boolean filterSupported(int group, int column) { } public boolean accepts(int slot, ItemStack stack) { - if (slot < 0 || slot >= groups.size() * SLOTS_PER_GROUP || stack.isEmpty()) + if (slot < 0 || slot >= groups.size() * SLOTS_PER_GROUP + || stack.isEmpty()) return false; int group = slot / SLOTS_PER_GROUP, sub = slot % SLOTS_PER_GROUP; if (sub < COLUMNS) @@ -288,13 +306,14 @@ public boolean accepts(int slot, ItemStack stack) { && acceptsFilter(addon(group, sub - COLUMNS), stack); } - /** Shared by authoritative storage and the menu's separate client-side slot snapshot. */ - public static boolean acceptsAddon( - ResourceGroup group, IItemHandler contents, int slot, ItemStack stack) { - if (slot < 0 - || slot >= contents.getSlots() - || slot % SLOTS_PER_GROUP >= COLUMNS - || stack.isEmpty()) + /** + * Shared by authoritative storage and the menu's separate client-side slot + * snapshot. + */ + public static boolean acceptsAddon(ResourceGroup group, + IItemHandler contents, int slot, ItemStack stack) { + if (slot < 0 || slot >= contents.getSlots() + || slot % SLOTS_PER_GROUP >= COLUMNS || stack.isEmpty()) return false; var capability = stack.getCapability(AddonCapabilities.ADDON); if (capability == null) @@ -308,7 +327,8 @@ public static boolean acceptsAddon( continue; var other = contents.getStackInSlot(base + c); var provider = other.getCapability(AddonCapabilities.ADDON); - if (provider != null && spec.conflictsWith(provider.specification(other))) + if (provider != null + && spec.conflictsWith(provider.specification(other))) return false; } return true; @@ -328,33 +348,38 @@ private void detach(int group, int column) { generation[group * COLUMNS + column]++; } - /** At the end of the authoritative menu interaction, after the source slot has been freed. */ + /** + * At the end of the authoritative menu interaction, after the source slot + * has been freed. + */ public void recover(Player player) { if (owner.getLevel() == null || owner.getLevel().isClientSide) return; for (var stack : recovery) { if (player != null) - stack = net.neoforged.neoforge.items.ItemHandlerHelper.insertItemStacked( - new net.neoforged.neoforge.items.wrapper.PlayerMainInvWrapper( - player.getInventory()), - stack, - false); + stack = net.neoforged.neoforge.items.ItemHandlerHelper + .insertItemStacked( + new net.neoforged.neoforge.items.wrapper.PlayerMainInvWrapper( + player.getInventory()), + stack, false); if (!stack.isEmpty()) - Containers.dropItemStack( - owner.getLevel(), + Containers.dropItemStack(owner.getLevel(), owner.getBlockPos().getX() + .5, owner.getBlockPos().getY() + .5, - owner.getBlockPos().getZ() + .5, - stack); + owner.getBlockPos().getZ() + .5, stack); } if (!recovery.isEmpty()) owner.setChanged(); recovery.clear(); } - /** Preserve incompatible items; recover duplicate roles and orphaned filters without loss. */ + /** + * Preserve incompatible items; recover duplicate roles and orphaned filters + * without loss. + */ public void revalidate() { - for (int g = dirtyGroups.nextSetBit(0); g >= 0; g = dirtyGroups.nextSetBit(g + 1)) { + for (int g = dirtyGroups.nextSetBit(0); g >= 0; g = dirtyGroups + .nextSetBit(g + 1)) { var claimed = new ArrayList(); for (int c = 0; c < COLUMNS; c++) { var stack = addon(g, c); @@ -362,7 +387,8 @@ public void revalidate() { if (provider != null) { var spec = provider.specification(stack); if (claimed.stream().anyMatch(spec::conflictsWith)) { - // Keep the first role after reload; recover duplicates and their attached + // Keep the first role after reload; recover duplicates + // and their attached // filters. recovery.add(stack.copy()); slots.setStackInSlot(addonSlot(g, c), ItemStack.EMPTY); @@ -376,19 +402,10 @@ public void revalidate() { } } - public boolean edit( - int group, - int column, - long expectedGeneration, - long expectedRevision, - String control, - int entry, - String value, - ItemStack cursor, - HolderLookup.Provider registries) { - if (group < 0 - || group >= groups.size() - || column < 0 + public boolean edit(int group, int column, long expectedGeneration, + long expectedRevision, String control, int entry, String value, + ItemStack cursor, HolderLookup.Provider registries) { + if (group < 0 || group >= groups.size() || column < 0 || column >= COLUMNS || generation(group, column) != expectedGeneration || revision(group, column) != expectedRevision @@ -396,10 +413,12 @@ public boolean edit( return false; var original = filter(group, column); var capability = original.getCapability(AddonCapabilities.FILTER); - if (capability == null || !acceptsFilter(addon(group, column), original)) + if (capability == null + || !acceptsFilter(addon(group, column), original)) return false; var candidate = original.copy(); - if (!capability.edit(candidate, control, entry, value, cursor.copy(), registries)) + if (!capability.edit(candidate, control, entry, value, cursor.copy(), + registries)) return false; // A config edit is not an identity replacement. loading = true; @@ -414,23 +433,24 @@ public boolean edit( return true; } - public IoMode mode(ResourceType type, int member, net.minecraft.core.Direction side) { + public IoMode mode(ResourceType type, int member, + net.minecraft.core.Direction side) { var indexed = members.get(type); var group = indexed == null ? null : indexed.get(member); return group == null ? IoMode.DISABLED : group.mode(side); } - /** Use a named inventory when several handlers have the same local slot numbers. */ - public IoMode mode( - ResourceType type, String inventory, int member, net.minecraft.core.Direction side) { + /** + * Use a named inventory when several handlers have the same local slot + * numbers. + */ + public IoMode mode(ResourceType type, String inventory, int member, + net.minecraft.core.Direction side) { return groups.stream() - .filter( - group -> group.type() == type - && group.inventory().equals(inventory) - && group.contains(member)) - .findFirst() - .map(group -> group.mode(side)) - .orElse(IoMode.DISABLED); + .filter(group -> group.type() == type + && group.inventory().equals(inventory) + && group.contains(member)) + .findFirst().map(group -> group.mode(side)).orElse(IoMode.DISABLED); } public CompoundTag save(HolderLookup.Provider registries) { @@ -445,8 +465,8 @@ public CompoundTag save(HolderLookup.Provider registries) { saved.put("sides", modes); var contents = new ListTag(); for (int s = 0; s < SLOTS_PER_GROUP; s++) - contents - .add(slots.getStackInSlot(i * SLOTS_PER_GROUP + s).saveOptional(registries)); + contents.add(slots.getStackInSlot(i * SLOTS_PER_GROUP + s) + .saveOptional(registries)); saved.put("contents", contents); data.put(group.id(), saved); } @@ -477,19 +497,22 @@ public void load(CompoundTag data, HolderLookup.Provider registries) { continue; var saved = unknown.getCompound(group.id()); var modes = saved.getCompound("sides"); - var loaded = new EnumMap(RelativeFace.class); + var loaded = new EnumMap( + RelativeFace.class); for (var face : RelativeFace.values()) { IoMode mode = group.sides().get(face); for (var candidate : IoMode.values()) if (candidate.getSerializedName() .equals(modes.getString(face.getSerializedName()))) mode = candidate; - loaded.put(face, ResourceGroup.intersect(mode, group.supported())); + loaded.put(face, + ResourceGroup.intersect(mode, group.supported())); } group.sides().copyFrom(new SideConfig(loaded)); var contents = saved.getList("contents", Tag.TAG_COMPOUND); for (int s = 0; s < SLOTS_PER_GROUP; s++) { - var stack = ItemStack.parseOptional(registries, contents.getCompound(s)); + var stack = ItemStack.parseOptional(registries, + contents.getCompound(s)); if (stack.getCount() > 1) { recovery.add(stack.copyWithCount(stack.getCount() - 1)); stack.setCount(1); @@ -499,7 +522,8 @@ public void load(CompoundTag data, HolderLookup.Provider registries) { unknown.remove(group.id()); } for (var tag : data.getList("recovery", Tag.TAG_COMPOUND)) - recovery.add(ItemStack.parseOptional(registries, (CompoundTag) tag)); + recovery.add( + ItemStack.parseOptional(registries, (CompoundTag) tag)); } finally { loading = false; } @@ -525,7 +549,8 @@ private String transferType(String key, CompoundTag state) { } /** - * The host must preserve non-item payloads in a dropped container before this clears storage. + * The host must preserve non-item payloads in a dropped container before + * this clears storage. */ public void dropAll(Consumer recoverResources) { if (owner.getLevel() == null || owner.getLevel().isClientSide) @@ -550,15 +575,17 @@ public void dropAll(Consumer recoverResources) { payload.put("resources", resources); if (refund > 0) payload.putLong("refund", refund); - // The host must take ownership before anything is cleared or ordinary item drops begin. + // The host must take ownership before anything is cleared or ordinary + // item drops begin. if (!payload.isEmpty()) recoverResources.accept(payload); loading = true; try { for (String id : unknown.getAllKeys()) - for (var tag : unknown.getCompound(id).getList("contents", Tag.TAG_COMPOUND)) { - var item = ItemStack.parseOptional(owner.getLevel().registryAccess(), - (CompoundTag) tag); + for (var tag : unknown.getCompound(id).getList("contents", + Tag.TAG_COMPOUND)) { + var item = ItemStack.parseOptional( + owner.getLevel().registryAccess(), (CompoundTag) tag); if (!item.isEmpty()) recovery.add(item); } diff --git a/src/main/java/zone/moddev/mc/lib/api/automation/AutomationTransfers.java b/src/main/java/zone/moddev/mc/lib/api/automation/AutomationTransfers.java index bf8ab35a..f270a443 100644 --- a/src/main/java/zone/moddev/mc/lib/api/automation/AutomationTransfers.java +++ b/src/main/java/zone/moddev/mc/lib/api/automation/AutomationTransfers.java @@ -19,327 +19,357 @@ import zone.moddev.mc.lib.api.capabilities.*; /** - * Server-only, paid transfer scheduler. Extracted resources remain owned by persistent escrow until - * a destination accepts them, including handlers that disagree with their simulation. + * Server-only, paid transfer scheduler. Extracted resources remain owned by + * persistent escrow until a destination accepts them, including handlers that + * disagree with their simulation. */ public final class AutomationTransfers { - public interface Transport { - BlockCapability capability(); + public interface Transport { + BlockCapability capability(); - /** Override for a custom resource's escrow schema. */ - default boolean pending(CompoundTag state) { - return state.contains("item") || state.contains("fluid") || state.contains("energy"); - } + /** Override for a custom resource's escrow schema. */ + default boolean pending(CompoundTag state) { + return state.contains("item") || state.contains("fluid") + || state.contains("energy"); + } - /** - * Delivers escrow first; otherwise stages one bounded extraction. Returns actual delivery. - * Implementations must preserve every unaccepted resource in escrow and respect max. - */ - int move( - T from, - T to, - int max, - Predicate filter, - CompoundTag escrow, - HolderLookup.Provider registries); + /** + * Delivers escrow first; otherwise stages one bounded extraction. + * Returns actual delivery. Implementations must preserve every + * unaccepted resource in escrow and respect max. + */ + int move(T from, T to, int max, Predicate filter, + CompoundTag escrow, HolderLookup.Provider registries); - /** Deliver already-owned resources without consulting or extracting from the source. */ - int deliver(T to, int max, CompoundTag escrow, HolderLookup.Provider registries); + /** + * Deliver already-owned resources without consulting or extracting from + * the source. + */ + int deliver(T to, int max, CompoundTag escrow, + HolderLookup.Provider registries); - /** - * Resource-only payload handed to the host on break; custom transports may keep their schema. - */ - default CompoundTag recovery(CompoundTag escrow) { - return pending(escrow) ? escrow.copy() : new CompoundTag(); + /** + * Resource-only payload handed to the host on break; custom transports + * may keep their schema. + */ + default CompoundTag recovery(CompoundTag escrow) { + return pending(escrow) ? escrow.copy() : new CompoundTag(); + } } - } - private static final Map, Transport> TRANSPORTS = new IdentityHashMap<>(); + private static final Map, Transport> TRANSPORTS = new IdentityHashMap<>(); - public static synchronized void register(ResourceType type, Transport transport) { - if (TRANSPORTS.putIfAbsent(type, transport) != null) - throw new IllegalArgumentException("Duplicate transport"); - } + public static synchronized void register(ResourceType type, + Transport transport) { + if (TRANSPORTS.putIfAbsent(type, transport) != null) + throw new IllegalArgumentException("Duplicate transport"); + } - @SuppressWarnings("unchecked") - public static Optional> transport(ResourceType type) { - return Optional.ofNullable((Transport) TRANSPORTS.get(type)); - } + @SuppressWarnings("unchecked") + public static Optional> transport(ResourceType type) { + return Optional.ofNullable((Transport) TRANSPORTS.get(type)); + } - static { - register( - ResourceType.ITEMS, - new Transport() { - public BlockCapability capability() { - return Capabilities.ItemHandler.BLOCK; - } + static { + register(ResourceType.ITEMS, new Transport() { + public BlockCapability capability() { + return Capabilities.ItemHandler.BLOCK; + } - public int move( - IItemHandler from, - IItemHandler to, - int max, - Predicate filter, - CompoundTag escrow, - HolderLookup.Provider registries) { - ItemStack staged = ItemStack.parseOptional(registries, escrow.getCompound("item")); - if (staged.isEmpty()) { - int start = escrow.getInt("cursor"); - for (int n = 0; n < from.getSlots(); n++) { - int slot = Math.floorMod(start + n, from.getSlots()); - var offered = from.extractItem(slot, max, true); - if (offered.isEmpty() || !filter.test(offered)) continue; - int accepted = offered.getCount() - Transfer.insert(to, offered, true).getCount(); - if (accepted <= 0) continue; - staged = from.extractItem(slot, Math.min(max, accepted), false); - escrow.putInt("cursor", slot + 1); - break; - } + public int move(IItemHandler from, IItemHandler to, int max, + Predicate filter, CompoundTag escrow, + HolderLookup.Provider registries) { + ItemStack staged = ItemStack.parseOptional(registries, + escrow.getCompound("item")); + if (staged.isEmpty()) { + int start = escrow.getInt("cursor"); + for (int n = 0; n < from.getSlots(); n++) { + int slot = Math.floorMod(start + n, from.getSlots()); + var offered = from.extractItem(slot, max, true); + if (offered.isEmpty() || !filter.test(offered)) + continue; + int accepted = offered.getCount() + - Transfer.insert(to, offered, true).getCount(); + if (accepted <= 0) + continue; + staged = from.extractItem(slot, Math.min(max, accepted), + false); + escrow.putInt("cursor", slot + 1); + break; + } + } + if (staged.isEmpty()) + return 0; + // Persist before invoking an external handler, and keep any + // remainder. + escrow.put("item", staged.saveOptional(registries)); + return deliver(to, max, escrow, registries); } - if (staged.isEmpty()) return 0; - // Persist before invoking an external handler, and keep any remainder. - escrow.put("item", staged.saveOptional(registries)); - return deliver(to, max, escrow, registries); - } - public int deliver( - IItemHandler to, int max, CompoundTag escrow, HolderLookup.Provider registries) { - var staged = ItemStack.parseOptional(registries, escrow.getCompound("item")); - if (staged.isEmpty()) return 0; - var offered = staged.copyWithCount(Math.min(max, staged.getCount())); - int delivered = offered.getCount() - Transfer.insert(to, offered, false).getCount(); - staged.shrink(delivered); - if (staged.isEmpty()) escrow.remove("item"); - else escrow.put("item", staged.saveOptional(registries)); - return delivered; - } + public int deliver(IItemHandler to, int max, CompoundTag escrow, + HolderLookup.Provider registries) { + var staged = ItemStack.parseOptional(registries, + escrow.getCompound("item")); + if (staged.isEmpty()) + return 0; + var offered = staged + .copyWithCount(Math.min(max, staged.getCount())); + int delivered = offered.getCount() + - Transfer.insert(to, offered, false).getCount(); + staged.shrink(delivered); + if (staged.isEmpty()) + escrow.remove("item"); + else + escrow.put("item", staged.saveOptional(registries)); + return delivered; + } - public CompoundTag recovery(CompoundTag escrow) { - return payload("item", escrow); - } + public CompoundTag recovery(CompoundTag escrow) { + return payload("item", escrow); + } }); - register( - ResourceType.FLUIDS, - new Transport() { - public BlockCapability capability() { - return Capabilities.FluidHandler.BLOCK; - } + register(ResourceType.FLUIDS, new Transport() { + public BlockCapability capability() { + return Capabilities.FluidHandler.BLOCK; + } - public int move( - IFluidHandler from, - IFluidHandler to, - int max, - Predicate filter, - CompoundTag escrow, - HolderLookup.Provider registries) { - var staged = FluidStack.parseOptional(registries, escrow.getCompound("fluid")); - if (staged.isEmpty()) { - int start = escrow.getInt("cursor"); - for (int n = 0; n < from.getTanks(); n++) { - int tank = Math.floorMod(start + n, from.getTanks()); - var candidate = from.getFluidInTank(tank).copy(); - if (candidate.isEmpty() || !filter.test(candidate)) continue; - candidate.setAmount(Math.min(max, candidate.getAmount())); - var offered = from.drain(candidate, IFluidHandler.FluidAction.SIMULATE); - int accepted = to.fill(offered, IFluidHandler.FluidAction.SIMULATE); - if (accepted <= 0) continue; - offered.setAmount(Math.min(max, accepted)); - staged = from.drain(offered, IFluidHandler.FluidAction.EXECUTE); - escrow.putInt("cursor", tank + 1); - break; - } + public int move(IFluidHandler from, IFluidHandler to, int max, + Predicate filter, CompoundTag escrow, + HolderLookup.Provider registries) { + var staged = FluidStack.parseOptional(registries, + escrow.getCompound("fluid")); + if (staged.isEmpty()) { + int start = escrow.getInt("cursor"); + for (int n = 0; n < from.getTanks(); n++) { + int tank = Math.floorMod(start + n, from.getTanks()); + var candidate = from.getFluidInTank(tank).copy(); + if (candidate.isEmpty() || !filter.test(candidate)) + continue; + candidate + .setAmount(Math.min(max, candidate.getAmount())); + var offered = from.drain(candidate, + IFluidHandler.FluidAction.SIMULATE); + int accepted = to.fill(offered, + IFluidHandler.FluidAction.SIMULATE); + if (accepted <= 0) + continue; + offered.setAmount(Math.min(max, accepted)); + staged = from.drain(offered, + IFluidHandler.FluidAction.EXECUTE); + escrow.putInt("cursor", tank + 1); + break; + } + } + if (staged.isEmpty()) + return 0; + escrow.put("fluid", staged.save(registries)); + return deliver(to, max, escrow, registries); } - if (staged.isEmpty()) return 0; - escrow.put("fluid", staged.save(registries)); - return deliver(to, max, escrow, registries); - } - public int deliver( - IFluidHandler to, int max, CompoundTag escrow, HolderLookup.Provider registries) { - var staged = FluidStack.parseOptional(registries, escrow.getCompound("fluid")); - if (staged.isEmpty()) return 0; - var offered = staged.copy(); - offered.setAmount(Math.min(max, staged.getAmount())); - int delivered = to.fill(offered, IFluidHandler.FluidAction.EXECUTE); - staged.shrink(delivered); - if (staged.isEmpty()) escrow.remove("fluid"); - else escrow.put("fluid", staged.save(registries)); - return delivered; - } + public int deliver(IFluidHandler to, int max, CompoundTag escrow, + HolderLookup.Provider registries) { + var staged = FluidStack.parseOptional(registries, + escrow.getCompound("fluid")); + if (staged.isEmpty()) + return 0; + var offered = staged.copy(); + offered.setAmount(Math.min(max, staged.getAmount())); + int delivered = to.fill(offered, + IFluidHandler.FluidAction.EXECUTE); + staged.shrink(delivered); + if (staged.isEmpty()) + escrow.remove("fluid"); + else + escrow.put("fluid", staged.save(registries)); + return delivered; + } - public CompoundTag recovery(CompoundTag escrow) { - return payload("fluid", escrow); - } + public CompoundTag recovery(CompoundTag escrow) { + return payload("fluid", escrow); + } }); - register( - ResourceType.ENERGY, - new Transport() { - public BlockCapability capability() { - return Capabilities.EnergyStorage.BLOCK; - } + register(ResourceType.ENERGY, new Transport() { + public BlockCapability capability() { + return Capabilities.EnergyStorage.BLOCK; + } - public int move( - IEnergyStorage from, - IEnergyStorage to, - int max, - Predicate filter, - CompoundTag escrow, - HolderLookup.Provider registries) { - int staged = escrow.getInt("energy"); - if (staged == 0) { - int offered = from.extractEnergy(max, true); - int accepted = to.receiveEnergy(offered, true); - if (accepted > 0) staged = from.extractEnergy(Math.min(max, accepted), false); + public int move(IEnergyStorage from, IEnergyStorage to, int max, + Predicate filter, CompoundTag escrow, + HolderLookup.Provider registries) { + int staged = escrow.getInt("energy"); + if (staged == 0) { + int offered = from.extractEnergy(max, true); + int accepted = to.receiveEnergy(offered, true); + if (accepted > 0) + staged = from.extractEnergy(Math.min(max, accepted), + false); + } + escrow.putInt("energy", staged); + return deliver(to, max, escrow, registries); } - escrow.putInt("energy", staged); - return deliver(to, max, escrow, registries); - } - public int deliver( - IEnergyStorage to, int max, CompoundTag escrow, HolderLookup.Provider registries) { - int staged = escrow.getInt("energy"); - int delivered = to.receiveEnergy(Math.min(max, staged), false); - if (staged == delivered) escrow.remove("energy"); - else escrow.putInt("energy", staged - delivered); - return delivered; - } + public int deliver(IEnergyStorage to, int max, CompoundTag escrow, + HolderLookup.Provider registries) { + int staged = escrow.getInt("energy"); + int delivered = to.receiveEnergy(Math.min(max, staged), false); + if (staged == delivered) + escrow.remove("energy"); + else + escrow.putInt("energy", staged - delivered); + return delivered; + } - public CompoundTag recovery(CompoundTag escrow) { - return escrow.getInt("energy") > 0 ? payload("energy", escrow) : new CompoundTag(); - } + public CompoundTag recovery(CompoundTag escrow) { + return escrow.getInt("energy") > 0 + ? payload("energy", escrow) + : new CompoundTag(); + } }); - } - - private AutomationTransfers() {} - - private static CompoundTag payload(String key, CompoundTag escrow) { - var payload = new CompoundTag(); - if (escrow.contains(key)) payload.put(key, escrow.get(key).copy()); - return payload; - } + } - /** - * Preserve unknown transport state too, so removing a mod cannot destroy its pending resources. - */ - public static CompoundTag recovery(String type, CompoundTag state) { - try { - var transport = - TRANSPORTS.get( - ResourceType.require(net.minecraft.resources.ResourceLocation.parse(type))); - return transport == null ? state.copy() : transport.recovery(state); - } catch (IllegalArgumentException unknown) { - return state.copy(); + private AutomationTransfers() { } - } - private static boolean pending(ResourceType type, CompoundTag state) { - var transport = TRANSPORTS.get(type); - return transport != null && transport.pending(state); - } + private static CompoundTag payload(String key, CompoundTag escrow) { + var payload = new CompoundTag(); + if (escrow.contains(key)) + payload.put(key, escrow.get(key).copy()); + return payload; + } - /** - * Call once per server tick, after recipe processing. A rotating start prevents slot starvation. - */ - public static void tick(AutomationInventory inventory, EnergyBuffer power) { - var level = inventory.owner().getLevel(); - if (level == null || level.isClientSide) return; - inventory.revalidate(); - inventory.recover(null); - inventory.settleRefund(power); - int total = inventory.groups().size() * COLUMNS; - if (total == 0) return; - int start = (int) Math.floorMod(level.getGameTime(), total); - for (int n = 0; n < total; n++) { - int index = (start + n) % total, g = index / COLUMNS, c = index % COLUMNS; - var group = inventory.group(g); - var stack = inventory.addon(g, c); - var addon = stack.getCapability(AddonCapabilities.ADDON); - var state = inventory.existingTransferState(group.id(), c); - boolean pending = state != null && pending(group.type(), state); - AddonSpec spec = addon == null ? null : addon.specification(stack); - if (!pending && (spec == null || !spec.accepts(group))) continue; - if (state == null) state = inventory.transferState(group.id(), c); - long cost = pending ? state.getLong("cost") : spec.energyPerUnit(); - int batch = pending ? state.getInt("batch") : spec.batch(); - if (!pending && Math.floorMod(level.getGameTime() + index, spec.interval()) != 0) continue; - int max = - (int) Math.min(batch, cost == 0 ? batch : power == null ? 0 : power.stored() / cost); - if (max <= 0) continue; - var filter = inventory.filter(g, c); - var predicate = filter.getCapability(AddonCapabilities.FILTER); - if (!pending - && !filter.isEmpty() - && (predicate == null || !addon.acceptsFilter(stack, filter))) continue; - Predicate accepts = - value -> predicate == null || predicate.matches(filter, value, level.registryAccess()); - int first = state.getInt("face"); - boolean both = !pending && spec.direction() == IoMode.BOTH; - for (int offset = 0; offset < (both ? 12 : 6); offset++) { - int face = Math.floorMod(first + offset / (both ? 2 : 1), 6); - var direction = group.world(RelativeFace.values()[face]); - boolean input = - pending - ? state.getBoolean("input") - : both - ? ((level.getGameTime() / spec.interval() + index + offset) & 1) == 0 - : spec.direction().canInsert(); - if (!(pending && input) - && (input ? !group.mode(direction).canInsert() : !group.mode(direction).canExtract())) - continue; - var transport = TRANSPORTS.get(group.type()); - if (transport == null - || (!(pending && input) - && inventory.neighbour(transport.capability(), direction) == null)) continue; - if (!pending) { - state.putBoolean("input", input); - state.putLong("cost", cost); - state.putInt("batch", batch); - state.putInt("face", face); - } - long reserved = cost * max; - if (reserved > 0 && (power == null || !power.reserve(reserved))) break; - int moved; + /** + * Preserve unknown transport state too, so removing a mod cannot destroy + * its pending resources. + */ + public static CompoundTag recovery(String type, CompoundTag state) { try { - moved = move(group, inventory, direction, input, pending, max, accepts, state); - } catch (RuntimeException failure) { - inventory.refund(power, reserved); - inventory.owner().setChanged(); - throw failure; + var transport = TRANSPORTS.get(ResourceType + .require(net.minecraft.resources.ResourceLocation.parse(type))); + return transport == null ? state.copy() : transport.recovery(state); + } catch (IllegalArgumentException unknown) { + return state.copy(); } - inventory.refund(power, reserved - cost * moved); - inventory.owner().setChanged(); - if (moved > 0 || (pending && input) || (!pending && pending(group.type(), state))) { - if (!pending(group.type(), state)) state.putInt("face", (face + 1) % 6); - break; + } + + private static boolean pending(ResourceType type, CompoundTag state) { + var transport = TRANSPORTS.get(type); + return transport != null && transport.pending(state); + } + + /** + * Call once per server tick, after recipe processing. A rotating start + * prevents slot starvation. + */ + public static void tick(AutomationInventory inventory, EnergyBuffer power) { + var level = inventory.owner().getLevel(); + if (level == null || level.isClientSide) + return; + inventory.revalidate(); + inventory.recover(null); + inventory.settleRefund(power); + int total = inventory.groups().size() * COLUMNS; + if (total == 0) + return; + int start = (int) Math.floorMod(level.getGameTime(), total); + for (int n = 0; n < total; n++) { + int index = (start + n) % total, g = index / COLUMNS, + c = index % COLUMNS; + var group = inventory.group(g); + var stack = inventory.addon(g, c); + var addon = stack.getCapability(AddonCapabilities.ADDON); + var state = inventory.existingTransferState(group.id(), c); + boolean pending = state != null && pending(group.type(), state); + AddonSpec spec = addon == null ? null : addon.specification(stack); + if (!pending && (spec == null || !spec.accepts(group))) + continue; + if (state == null) + state = inventory.transferState(group.id(), c); + long cost = pending ? state.getLong("cost") : spec.energyPerUnit(); + int batch = pending ? state.getInt("batch") : spec.batch(); + if (!pending && Math.floorMod(level.getGameTime() + index, + spec.interval()) != 0) + continue; + int max = (int) Math.min(batch, + cost == 0 ? batch : power == null ? 0 : power.stored() / cost); + if (max <= 0) + continue; + var filter = inventory.filter(g, c); + var predicate = filter.getCapability(AddonCapabilities.FILTER); + if (!pending && !filter.isEmpty() + && (predicate == null || !addon.acceptsFilter(stack, filter))) + continue; + Predicate accepts = value -> predicate == null + || predicate.matches(filter, value, level.registryAccess()); + int first = state.getInt("face"); + boolean both = !pending && spec.direction() == IoMode.BOTH; + for (int offset = 0; offset < (both ? 12 : 6); offset++) { + int face = Math.floorMod(first + offset / (both ? 2 : 1), 6); + var direction = group.world(RelativeFace.values()[face]); + boolean input = pending + ? state.getBoolean("input") + : both + ? ((level.getGameTime() / spec.interval() + index + + offset) & 1) == 0 + : spec.direction().canInsert(); + if (!(pending && input) && (input + ? !group.mode(direction).canInsert() + : !group.mode(direction).canExtract())) + continue; + var transport = TRANSPORTS.get(group.type()); + if (transport == null || (!(pending && input) && inventory + .neighbour(transport.capability(), direction) == null)) + continue; + if (!pending) { + state.putBoolean("input", input); + state.putLong("cost", cost); + state.putInt("batch", batch); + state.putInt("face", face); + } + long reserved = cost * max; + if (reserved > 0 && (power == null || !power.reserve(reserved))) + break; + int moved; + try { + moved = move(group, inventory, direction, input, pending, + max, accepts, state); + } catch (RuntimeException failure) { + inventory.refund(power, reserved); + inventory.owner().setChanged(); + throw failure; + } + inventory.refund(power, reserved - cost * moved); + inventory.owner().setChanged(); + if (moved > 0 || (pending && input) + || (!pending && pending(group.type(), state))) { + if (!pending(group.type(), state)) + state.putInt("face", (face + 1) % 6); + break; + } + } } - } } - } - @SuppressWarnings("unchecked") - private static int move( - ResourceGroup group, - AutomationInventory inventory, - Direction direction, - boolean input, - boolean pending, - int max, - Predicate filter, - CompoundTag escrow) { - var transport = (Transport) TRANSPORTS.get(group.type()); - if (transport == null) return 0; - var level = inventory.owner().getLevel(); - if (pending && input) - return transport.deliver(group.port(null), max, escrow, level.registryAccess()); - T neighbour = inventory.neighbour(transport.capability(), direction); - if (neighbour == null) return 0; - if (pending) return transport.deliver(neighbour, max, escrow, level.registryAccess()); - T local = group.port(direction); - return transport.move( - input ? neighbour : local, - input ? local : neighbour, - max, - filter, - escrow, - level.registryAccess()); - } + @SuppressWarnings("unchecked") + private static int move(ResourceGroup group, + AutomationInventory inventory, Direction direction, boolean input, + boolean pending, int max, Predicate filter, + CompoundTag escrow) { + var transport = (Transport) TRANSPORTS.get(group.type()); + if (transport == null) + return 0; + var level = inventory.owner().getLevel(); + if (pending && input) + return transport.deliver(group.port(null), max, escrow, + level.registryAccess()); + T neighbour = inventory.neighbour(transport.capability(), direction); + if (neighbour == null) + return 0; + if (pending) + return transport.deliver(neighbour, max, escrow, + level.registryAccess()); + T local = group.port(direction); + return transport.move(input ? neighbour : local, + input ? local : neighbour, max, filter, escrow, + level.registryAccess()); + } } diff --git a/src/main/java/zone/moddev/mc/lib/api/automation/FilterControl.java b/src/main/java/zone/moddev/mc/lib/api/automation/FilterControl.java index 4becff2c..bd926bb1 100644 --- a/src/main/java/zone/moddev/mc/lib/api/automation/FilterControl.java +++ b/src/main/java/zone/moddev/mc/lib/api/automation/FilterControl.java @@ -29,24 +29,17 @@ import zone.moddev.mc.lib.gui.model.elements.GhostSlotElement; import zone.moddev.mc.lib.gui.model.elements.SlotElement; -/** An open element contract for item-provided editors. Each control validates its own edits. */ +/** + * An open element contract for item-provided editors. Each control validates + * its own edits. + */ public interface FilterControl extends Element { String id(); default Common common() { - return new Common( - Optional.of(id()), - Optional.empty(), - Optional.empty(), - Optional.empty(), - Optional.empty(), - Common.Clicks.NONE, - Optional.empty(), - Optional.empty(), - List.of(), - 0, - 0, - 0); + return new Common(Optional.of(id()), Optional.empty(), Optional.empty(), + Optional.empty(), Optional.empty(), Common.Clicks.NONE, + Optional.empty(), Optional.empty(), List.of(), 0, 0, 0); } default int[] measure(TextMeasure text) { @@ -57,18 +50,19 @@ default boolean stretchX() { return true; } - /** Server-only candidate data, after the host has authorized the attachment and revision. */ - record Edit( - InventoryFilter provider, - ItemStack filter, - CompoundTag configuration, - int index, - String value, - ItemStack cursor, + /** + * Server-only candidate data, after the host has authorized the attachment + * and revision. + */ + record Edit(InventoryFilter provider, ItemStack filter, + CompoundTag configuration, int index, String value, ItemStack cursor, HolderLookup.Provider registries) { } - /** Refuse edits unless this control explicitly validates and applies the proposed value. */ + /** + * Refuse edits unless this control explicitly validates and applies the + * proposed value. + */ default boolean edit(Edit edit) { return false; } @@ -80,11 +74,8 @@ class Toggle implements FilterControl { private final Component tooltip; private final Component selectedTooltip; - public Toggle( - String id, - ResourceLocation icon, - ResourceLocation selectedIcon, - Component tooltip, + public Toggle(String id, ResourceLocation icon, + ResourceLocation selectedIcon, Component tooltip, Component selectedTooltip) { this.id = id; this.icon = icon; @@ -106,7 +97,8 @@ public boolean stretchX() { } public boolean edit(Edit edit) { - edit.configuration().putBoolean(id, !edit.configuration().getBoolean(id)); + edit.configuration().putBoolean(id, + !edit.configuration().getBoolean(id)); return true; } @@ -141,7 +133,8 @@ public boolean equals(Object object) { && java.util.Objects.equals(icon, other.icon) && java.util.Objects.equals(selectedIcon, other.selectedIcon) && java.util.Objects.equals(tooltip, other.tooltip) - && java.util.Objects.equals(selectedTooltip, other.selectedTooltip); + && java.util.Objects.equals(selectedTooltip, + other.selectedTooltip); } @Override @@ -157,22 +150,15 @@ public int hashCode() { @Override public String toString() { - return "Toggle[id=" - + id - + ", icon=" - + icon - + ", selectedIcon=" - + selectedIcon - + ", tooltip=" - + tooltip - + ", selectedTooltip=" - + selectedTooltip - + "]"; + return "Toggle[id=" + id + ", icon=" + icon + ", selectedIcon=" + + selectedIcon + ", tooltip=" + tooltip + ", selectedTooltip=" + + selectedTooltip + "]"; } } /** - * Compatibility element for a registered filter widget; it uses shared behavior/render routing. + * Compatibility element for a registered filter widget; it uses shared + * behavior/render routing. */ class Custom implements FilterControl { private final String id; @@ -232,7 +218,8 @@ public int hashCode() { @Override public String toString() { - return "Custom[id=" + id + ", type=" + type + ", height=" + height + "]"; + return "Custom[id=" + id + ", type=" + type + ", height=" + height + + "]"; } } @@ -293,7 +280,8 @@ public int hashCode() { @Override public String toString() { - return "Text[id=" + id + ", maxLength=" + maxLength + ", tooltip=" + tooltip + "]"; + return "Text[id=" + id + ", maxLength=" + maxLength + ", tooltip=" + + tooltip + "]"; } } @@ -304,7 +292,8 @@ class Number implements FilterControl { private final int step; private final Component tooltip; - public Number(String id, int min, int max, int step, Component tooltip) { + public Number(String id, int min, int max, int step, + Component tooltip) { if (min > max || step < 1) throw new IllegalArgumentException("Number bounds"); @@ -375,25 +364,15 @@ public int hashCode() { @Override public String toString() { - return "Number[id=" - + id - + ", min=" - + min - + ", max=" - + max - + ", step=" - + step - + ", tooltip=" - + tooltip - + "]"; + return "Number[id=" + id + ", min=" + min + ", max=" + max + + ", step=" + step + ", tooltip=" + tooltip + "]"; } } class Samples implements FilterControl { private static final SlotElement DEFAULT_SLOT = new GhostSlotElement( - Common.empty(), - 0, - SlotElement.Appearance.standard(SlotSize.NORMAL, Optional.empty(), Optional.empty())); + Common.empty(), 0, SlotElement.Appearance.standard(SlotSize.NORMAL, + Optional.empty(), Optional.empty())); private final String id; private final int count; private final ResourceType resource; @@ -420,32 +399,39 @@ public int[] measure(TextMeasure text, int width) { } /** - * Grid pitch in pixels; override together with slot appearance for larger or spaced cells. + * Grid pitch in pixels; override together with slot appearance for + * larger or spaced cells. */ public int cellSize() { return 18; } /** - * Slot presentation only: these cells do not create native inventory slots or separate - * actions. + * Slot presentation only: these cells do not create native inventory + * slots or separate actions. */ public SlotElement sampleSlot(int index) { return DEFAULT_SLOT; } - /** Absolute cell bounds, shared by measurement, drawing, tooltips and click targeting. */ + /** + * Absolute cell bounds, shared by measurement, drawing, tooltips and + * click targeting. + */ public Rect sampleBounds(Rect bounds, int index) { int size = cellSize(); if (size < 1) - throw new IllegalArgumentException("Sample cell size must be positive"); + throw new IllegalArgumentException( + "Sample cell size must be positive"); int columns = Math.max(1, bounds.width() / size); - return new Rect( - bounds.x() + index % columns * size, bounds.y() + index / columns * size, size, - size); + return new Rect(bounds.x() + index % columns * size, + bounds.y() + index / columns * size, size, size); } - /** Last matching cell wins; unoccupied space and points outside the control do not hit. */ + /** + * Last matching cell wins; unoccupied space and points outside the + * control do not hit. + */ public int sampleIndex(Rect bounds, double x, double y) { if (!bounds.contains(x, y)) return -1; @@ -456,42 +442,48 @@ public int sampleIndex(Rect bounds, double x, double y) { } /** - * Selects the displayed stack from read-only synchronized configuration; mods may override. + * Selects the displayed stack from read-only synchronized + * configuration; mods may override. */ public ItemStack sampleDisplay(CompoundTag configuration, int index) { - var key = ResourceLocation - .tryParse(configuration.getList(id(), Tag.TAG_STRING).getString(index)); + var key = ResourceLocation.tryParse( + configuration.getList(id(), Tag.TAG_STRING).getString(index)); if (key == null) return ItemStack.EMPTY; if (resource() == ResourceType.ITEMS) return new ItemStack(BuiltInRegistries.ITEM.get(key)); if (resource() == ResourceType.FLUIDS) - return new ItemStack(BuiltInRegistries.FLUID.get(key).getBucket()); + return new ItemStack( + BuiltInRegistries.FLUID.get(key).getBucket()); return ItemStack.EMPTY; } - /** Tooltip rows for one cell; the default follows the element's displayed stack. */ - public List sampleTooltip(CompoundTag configuration, int index) { + /** + * Tooltip rows for one cell; the default follows the element's + * displayed stack. + */ + public List sampleTooltip(CompoundTag configuration, + int index) { var item = sampleDisplay(configuration, index); - return List.of( - item.isEmpty() - ? Component.translatable("screen.mmdlib.automation.sample") - : item.getHoverName()); + return List.of(item.isEmpty() + ? Component.translatable("screen.mmdlib.automation.sample") + : item.getHoverName()); } public boolean edit(Edit edit) { if (!edit.value().equals("sample") && !edit.value().equals("clear")) return false; - if (edit.index() < 0 - || edit.index() >= count + if (edit.index() < 0 || edit.index() >= count || resource != edit.provider().resource(edit.filter())) return false; - var entries = edit.configuration().getList(id, Tag.TAG_STRING).copy(); + var entries = edit.configuration().getList(id, Tag.TAG_STRING) + .copy(); while (entries.size() < count) entries.add(StringTag.valueOf("")); String sample = edit.value().equals("clear") ? "" - : edit.provider().sample(edit.filter(), edit.cursor(), edit.registries()); + : edit.provider().sample(edit.filter(), edit.cursor(), + edit.registries()); if (!edit.value().equals("clear") && sample.isEmpty()) return false; entries.set(edit.index(), StringTag.valueOf(sample)); @@ -534,11 +526,15 @@ public int hashCode() { @Override public String toString() { - return "Samples[id=" + id + ", count=" + count + ", resource=" + resource + "]"; + return "Samples[id=" + id + ", count=" + count + ", resource=" + + resource + "]"; } } - /** A wrapping row or stack; ordinary elements may be composed alongside filter controls. */ + /** + * A wrapping row or stack; ordinary elements may be composed alongside + * filter controls. + */ class Box implements FilterControl { private final String id; private final boolean horizontal; @@ -552,7 +548,8 @@ public Box(String id, boolean horizontal, List children) { this.children = children; } - public Box(String id, boolean horizontal, Collection children) { + public Box(String id, boolean horizontal, + Collection children) { this(id, horizontal, List.copyOf(children)); } @@ -561,11 +558,13 @@ public int[] measure(TextMeasure text) { } public int[] measure(TextMeasure text, int width) { - return new int[]{width, FlowLayout.of(children, horizontal, width, 2, text).height()}; + return new int[]{width, + FlowLayout.of(children, horizontal, width, 2, text).height()}; } public Map childLayout(Rect bounds, TextMeasure text) { - return FlowLayout.of(children, horizontal, bounds.width(), 2, text).children(); + return FlowLayout.of(children, horizontal, bounds.width(), 2, text) + .children(); } public String id() { @@ -603,24 +602,25 @@ public int hashCode() { @Override public String toString() { - return "Box[id=" + id + ", horizontal=" + horizontal + ", children=" + children + "]"; + return "Box[id=" + id + ", horizontal=" + horizontal + ", children=" + + children + "]"; } } static List flatten(FilterControl root) { var result = new ArrayList(); - collect(root, result, new HashSet<>(), Collections.newSetFromMap(new IdentityHashMap<>()), - 0); + collect(root, result, new HashSet<>(), + Collections.newSetFromMap(new IdentityHashMap<>()), 0); return List.copyOf(result); } - private static void collect( - Element element, List result, Set ids, Set seen, - int depth) { + private static void collect(Element element, List result, + Set ids, Set seen, int depth) { if (depth > 8 || seen.size() >= 128 || !seen.add(element)) throw new IllegalArgumentException("Editor too large or cyclic"); if (element instanceof FilterControl control) { - if (!control.id().matches("[a-z0-9_.-]{1,64}") || !ids.add(control.id())) + if (!control.id().matches("[a-z0-9_.-]{1,64}") + || !ids.add(control.id())) throw new IllegalArgumentException( "Invalid/duplicate editor control: " + control.id()); result.add(control); diff --git a/src/main/java/zone/moddev/mc/lib/api/automation/FilterWidgetType.java b/src/main/java/zone/moddev/mc/lib/api/automation/FilterWidgetType.java index e533434c..7637f4ad 100644 --- a/src/main/java/zone/moddev/mc/lib/api/automation/FilterWidgetType.java +++ b/src/main/java/zone/moddev/mc/lib/api/automation/FilterWidgetType.java @@ -12,40 +12,48 @@ import net.minecraft.resources.ResourceLocation; /** - * Common registration for an item-bound custom leaf. The codec and handler validate one bounded - * action against a candidate configuration, which the filter validates again before commit. + * Common registration for an item-bound custom leaf. The codec and handler + * validate one bounded action against a candidate configuration, which the + * filter validates again before commit. */ -public record FilterWidgetType( - ResourceLocation id, Codec action, BiPredicate apply) { - private static final Map> TYPES = new HashMap<>(); - - public FilterWidgetType { - Objects.requireNonNull(id); - Objects.requireNonNull(action); - Objects.requireNonNull(apply); - } - - public static synchronized FilterWidgetType register( - ResourceLocation id, Codec codec, BiPredicate apply) { - var type = new FilterWidgetType<>(id, codec, apply); - if (TYPES.putIfAbsent(id, type) != null) - throw new IllegalArgumentException("Duplicate filter widget: " + id); - return type; - } - - public String encode(T value) { - String encoded = action.encodeStart(JsonOps.INSTANCE, value).getOrThrow().toString(); - if (encoded.length() > 256) throw new IllegalArgumentException("Filter action too large"); - return encoded; - } - - public boolean dispatch(CompoundTag candidate, String encoded) { - if (encoded.length() > 256) return false; - try { - var decoded = action.parse(JsonOps.INSTANCE, JsonParser.parseString(encoded)).result(); - return decoded.isPresent() && apply.test(candidate, decoded.get()); - } catch (RuntimeException malformed) { - return false; +public record FilterWidgetType(ResourceLocation id, Codec action, + BiPredicate apply) { + private static final Map> TYPES = new HashMap<>(); + + public FilterWidgetType { + Objects.requireNonNull(id); + Objects.requireNonNull(action); + Objects.requireNonNull(apply); + } + + public static synchronized FilterWidgetType register( + ResourceLocation id, Codec codec, + BiPredicate apply) { + var type = new FilterWidgetType<>(id, codec, apply); + if (TYPES.putIfAbsent(id, type) != null) + throw new IllegalArgumentException( + "Duplicate filter widget: " + id); + return type; + } + + public String encode(T value) { + String encoded = action.encodeStart(JsonOps.INSTANCE, value) + .getOrThrow().toString(); + if (encoded.length() > 256) + throw new IllegalArgumentException("Filter action too large"); + return encoded; + } + + public boolean dispatch(CompoundTag candidate, String encoded) { + if (encoded.length() > 256) + return false; + try { + var decoded = action + .parse(JsonOps.INSTANCE, JsonParser.parseString(encoded)) + .result(); + return decoded.isPresent() && apply.test(candidate, decoded.get()); + } catch (RuntimeException malformed) { + return false; + } } - } } diff --git a/src/main/java/zone/moddev/mc/lib/api/automation/InventoryAddon.java b/src/main/java/zone/moddev/mc/lib/api/automation/InventoryAddon.java index 90b4da4d..903c4e05 100644 --- a/src/main/java/zone/moddev/mc/lib/api/automation/InventoryAddon.java +++ b/src/main/java/zone/moddev/mc/lib/api/automation/InventoryAddon.java @@ -3,14 +3,16 @@ import net.minecraft.world.item.ItemStack; /** - * Stateless capability provider; all persistent addon/filter data belongs to its installed stack. + * Stateless capability provider; all persistent addon/filter data belongs to + * its installed stack. */ public interface InventoryAddon { - AddonSpec specification(ItemStack stack); + AddonSpec specification(ItemStack stack); - default boolean acceptsFilter(ItemStack addon, ItemStack filter) { - var spec = specification(addon); - var capability = filter.getCapability(AddonCapabilities.FILTER); - return spec.filters() && capability != null && capability.resource(filter) == spec.resource(); - } + default boolean acceptsFilter(ItemStack addon, ItemStack filter) { + var spec = specification(addon); + var capability = filter.getCapability(AddonCapabilities.FILTER); + return spec.filters() && capability != null + && capability.resource(filter) == spec.resource(); + } } diff --git a/src/main/java/zone/moddev/mc/lib/api/automation/InventoryFilter.java b/src/main/java/zone/moddev/mc/lib/api/automation/InventoryFilter.java index dcebd7ab..27b05623 100644 --- a/src/main/java/zone/moddev/mc/lib/api/automation/InventoryFilter.java +++ b/src/main/java/zone/moddev/mc/lib/api/automation/InventoryFilter.java @@ -7,7 +7,10 @@ import net.minecraft.world.item.ItemStack; import net.minecraft.world.item.component.CustomData; -/** A filter's predicate and editor are item-provided, and have no dependency on a client screen. */ +/** + * A filter's predicate and editor are item-provided, and have no dependency on + * a client screen. + */ public interface InventoryFilter { ResourceType resource(ItemStack stack); @@ -15,63 +18,64 @@ public interface InventoryFilter { FilterControl controls(ItemStack stack); - boolean matches(ItemStack filter, Object resource, HolderLookup.Provider registries); + boolean matches(ItemStack filter, Object resource, + HolderLookup.Provider registries); default CompoundTag configuration(ItemStack stack) { - return stack - .getOrDefault(DataComponents.CUSTOM_DATA, CustomData.EMPTY) - .copyTag() - .getCompound("mmdlib_filter"); + return stack.getOrDefault(DataComponents.CUSTOM_DATA, CustomData.EMPTY) + .copyTag().getCompound("mmdlib_filter"); } default void configure(ItemStack stack, CompoundTag configuration) { - var data = stack.getOrDefault(DataComponents.CUSTOM_DATA, CustomData.EMPTY).copyTag(); + var data = stack + .getOrDefault(DataComponents.CUSTOM_DATA, CustomData.EMPTY) + .copyTag(); data.put("mmdlib_filter", configuration.copy()); stack.set(DataComponents.CUSTOM_DATA, CustomData.of(data)); } - /** Derive an approved sample from the authoritative cursor; never trust client item NBT. */ - default String sample(ItemStack filter, ItemStack cursor, HolderLookup.Provider registries) { + /** + * Derive an approved sample from the authoritative cursor; never trust + * client item NBT. + */ + default String sample(ItemStack filter, ItemStack cursor, + HolderLookup.Provider registries) { if (cursor.isEmpty()) return ""; if (resource(filter) == ResourceType.ITEMS) return net.minecraft.core.registries.BuiltInRegistries.ITEM - .getKey(cursor.getItem()) - .toString(); + .getKey(cursor.getItem()).toString(); if (resource(filter) == ResourceType.FLUIDS) { - var fluid = net.neoforged.neoforge.fluids.FluidUtil.getFluidContained(cursor); + var fluid = net.neoforged.neoforge.fluids.FluidUtil + .getFluidContained(cursor); return fluid - .map( - f -> net.minecraft.core.registries.BuiltInRegistries.FLUID - .getKey(f.getFluid()) - .toString()) + .map(f -> net.minecraft.core.registries.BuiltInRegistries.FLUID + .getKey(f.getFluid()).toString()) .orElse(""); } return ""; } - /** Validate the whole candidate before the host atomically replaces its installed stack. */ - default boolean validate( - ItemStack filter, CompoundTag candidate, HolderLookup.Provider registries) { + /** + * Validate the whole candidate before the host atomically replaces its + * installed stack. + */ + default boolean validate(ItemStack filter, CompoundTag candidate, + HolderLookup.Provider registries) { return true; } - /** Standard controls; custom filters can further constrain the proposed configuration. */ - default boolean edit( - ItemStack copy, - String id, - int index, - String value, - ItemStack cursor, - HolderLookup.Provider registries) { + /** + * Standard controls; custom filters can further constrain the proposed + * configuration. + */ + default boolean edit(ItemStack copy, String id, int index, String value, + ItemStack cursor, HolderLookup.Provider registries) { var control = FilterControl.flatten(controls(copy)).stream() - .filter(c -> c.id().equals(id)) - .findFirst() - .orElse(null); + .filter(c -> c.id().equals(id)).findFirst().orElse(null); var config = configuration(copy); - if (control == null - || !control.edit( - new FilterControl.Edit(this, copy, config, index, value, cursor, registries))) + if (control == null || !control.edit(new FilterControl.Edit(this, copy, + config, index, value, cursor, registries))) return false; if (!validate(copy, config, registries)) return false; diff --git a/src/main/java/zone/moddev/mc/lib/api/automation/ResourceGroup.java b/src/main/java/zone/moddev/mc/lib/api/automation/ResourceGroup.java index 90a9e35b..382b380d 100644 --- a/src/main/java/zone/moddev/mc/lib/api/automation/ResourceGroup.java +++ b/src/main/java/zone/moddev/mc/lib/api/automation/ResourceGroup.java @@ -12,7 +12,10 @@ import zone.moddev.mc.lib.api.capabilities.RelativeFace; import zone.moddev.mc.lib.api.capabilities.SideConfig; -/** One independently configurable item inventory, tank, buffer or mod-defined resource. */ +/** + * One independently configurable item inventory, tank, buffer or mod-defined + * resource. + */ public final class ResourceGroup { private final String id; private final Component name; @@ -27,22 +30,17 @@ public final class ResourceGroup { private Function port; private Supplier facing = () -> Direction.NORTH; - public ResourceGroup( - String id, - Component name, - int colour, - ResourceType type, - IoMode supported, - List members, - List highlights, - SideConfig sides) { + public ResourceGroup(String id, Component name, int colour, + ResourceType type, IoMode supported, List members, + List highlights, SideConfig sides) { if (!id.matches("[a-z0-9_.-]{1,64}")) throw new IllegalArgumentException("Invalid group ID: " + id); if ((colour & 0xff000000) != 0) throw new IllegalArgumentException("Group colour must be RGB"); if (members.stream().anyMatch(i -> i < 0) || members.stream().distinct().count() != members.size()) - throw new IllegalArgumentException("Invalid or duplicate group members"); + throw new IllegalArgumentException( + "Invalid or duplicate group members"); this.id = id; this.name = Objects.requireNonNull(name).copy(); this.colour = colour; @@ -51,11 +49,9 @@ public ResourceGroup( this.members = List.copyOf(members); this.highlights = List.copyOf(highlights); this.sides = Objects.requireNonNull(sides); - definitionKey = id - + "/" - + Integer.toHexString( - Objects.hash( - type.id().toString(), supported.name(), colour, this.members, this.highlights)); + definitionKey = id + "/" + + Integer.toHexString(Objects.hash(type.id().toString(), + supported.name(), colour, this.members, this.highlights)); for (var face : RelativeFace.values()) sides.set(face, intersect(sides.get(face), supported)); } @@ -64,14 +60,18 @@ public String id() { return id; } - /** Identity of the backing inventory; member indexes are local to this inventory. */ + /** + * Identity of the backing inventory; member indexes are local to this + * inventory. + */ public String inventory() { return inventory; } public ResourceGroup inventory(String name) { if (port != null) - throw new IllegalStateException("Inventory identity must be set before binding"); + throw new IllegalStateException( + "Inventory identity must be set before binding"); if (name == null || !name.matches("[a-z0-9_.-]{1,64}")) throw new IllegalArgumentException("Invalid inventory identity"); inventory = name; @@ -114,7 +114,8 @@ public boolean contains(int index) { return members.contains(index); } - public ResourceGroup bind(Supplier facing, Function port) { + public ResourceGroup bind(Supplier facing, + Function port) { this.facing = Objects.requireNonNull(facing); this.port = Objects.requireNonNull(port); return this; @@ -130,9 +131,13 @@ public Direction world(RelativeFace face) { return face.toWorld(facing.get()); } - /** Null-side access keeps the declared role; configured directions follow machine rotation. */ + /** + * Null-side access keeps the declared role; configured directions follow + * machine rotation. + */ public IoMode mode(Direction side) { - return intersect(supported, side == null ? IoMode.BOTH : sides.mode(facing.get(), side)); + return intersect(supported, + side == null ? IoMode.BOTH : sides.mode(facing.get(), side)); } public boolean supports(IoMode mode) { @@ -149,7 +154,10 @@ public IoMode next(IoMode current, boolean reverse) { return modes[index]; } - /** Checked compare-and-set: concurrent viewers cannot cycle a newer value accidentally. */ + /** + * Checked compare-and-set: concurrent viewers cannot cycle a newer value + * accidentally. + */ public boolean cycle(RelativeFace face, IoMode expected, boolean reverse) { if (sides.get(face) != expected) return false; @@ -158,7 +166,10 @@ public boolean cycle(RelativeFace face, IoMode expected, boolean reverse) { } public static IoMode intersect(IoMode a, IoMode b) { - boolean in = a.canInsert() && b.canInsert(), out = a.canExtract() && b.canExtract(); - return in ? (out ? IoMode.BOTH : IoMode.INPUT) : (out ? IoMode.OUTPUT : IoMode.DISABLED); + boolean in = a.canInsert() && b.canInsert(), + out = a.canExtract() && b.canExtract(); + return in + ? (out ? IoMode.BOTH : IoMode.INPUT) + : (out ? IoMode.OUTPUT : IoMode.DISABLED); } } diff --git a/src/main/java/zone/moddev/mc/lib/api/automation/ResourceType.java b/src/main/java/zone/moddev/mc/lib/api/automation/ResourceType.java index d5a36a95..556b77e7 100644 --- a/src/main/java/zone/moddev/mc/lib/api/automation/ResourceType.java +++ b/src/main/java/zone/moddev/mc/lib/api/automation/ResourceType.java @@ -10,44 +10,53 @@ import net.neoforged.neoforge.fluids.capability.IFluidHandler; import net.neoforged.neoforge.items.IItemHandler; -/** Extensible resource identity and the capability interface used by its transfer addons. */ +/** + * Extensible resource identity and the capability interface used by its + * transfer addons. + */ public final class ResourceType { - private static final Map> TYPES = new LinkedHashMap<>(); - public static final ResourceType ITEMS = - register("mmdlib:items", IItemHandler.class); - public static final ResourceType FLUIDS = - register("mmdlib:fluids", IFluidHandler.class); - public static final ResourceType ENERGY = - register("mmdlib:energy", IEnergyStorage.class); - private final ResourceLocation id; - private final Class handlerType; - - private ResourceType(ResourceLocation id, Class handlerType) { - this.id = id; - this.handlerType = handlerType; - } - - /** Register during mod construction on both sides; future resource types need no UI changes. */ - public static synchronized ResourceType register(String id, Class handlerType) { - var key = ResourceLocation.parse(id); - if (TYPES.containsKey(key)) - throw new IllegalArgumentException("Duplicate resource type: " + key); - var type = new ResourceType<>(key, Objects.requireNonNull(handlerType)); - TYPES.put(key, type); - return type; - } - - public ResourceLocation id() { - return id; - } - - public Class handlerType() { - return handlerType; - } - - public static ResourceType require(ResourceLocation id) { - var type = TYPES.get(id); - if (type == null) throw new IllegalArgumentException("Unknown resource type: " + id); - return type; - } + private static final Map> TYPES = new LinkedHashMap<>(); + public static final ResourceType ITEMS = register( + "mmdlib:items", IItemHandler.class); + public static final ResourceType FLUIDS = register( + "mmdlib:fluids", IFluidHandler.class); + public static final ResourceType ENERGY = register( + "mmdlib:energy", IEnergyStorage.class); + private final ResourceLocation id; + private final Class handlerType; + + private ResourceType(ResourceLocation id, Class handlerType) { + this.id = id; + this.handlerType = handlerType; + } + + /** + * Register during mod construction on both sides; future resource types + * need no UI changes. + */ + public static synchronized ResourceType register(String id, + Class handlerType) { + var key = ResourceLocation.parse(id); + if (TYPES.containsKey(key)) + throw new IllegalArgumentException( + "Duplicate resource type: " + key); + var type = new ResourceType<>(key, Objects.requireNonNull(handlerType)); + TYPES.put(key, type); + return type; + } + + public ResourceLocation id() { + return id; + } + + public Class handlerType() { + return handlerType; + } + + public static ResourceType require(ResourceLocation id) { + var type = TYPES.get(id); + if (type == null) + throw new IllegalArgumentException("Unknown resource type: " + id); + return type; + } } diff --git a/src/main/java/zone/moddev/mc/lib/api/capabilities/EnergyBuffer.java b/src/main/java/zone/moddev/mc/lib/api/capabilities/EnergyBuffer.java index 2ff3aa87..ca37fca5 100644 --- a/src/main/java/zone/moddev/mc/lib/api/capabilities/EnergyBuffer.java +++ b/src/main/java/zone/moddev/mc/lib/api/capabilities/EnergyBuffer.java @@ -14,246 +14,293 @@ import zone.moddev.mc.lib.api.stats.StatsProvider; /** - * A machine's energy, counted as a {@code long} so large buffers do not overflow. Automation sees - * it through {@link IEnergyStorage}, whose {@code int} values are clamped, and is limited to the - * transfer rates. The owning block entity uses {@link #insert} and {@link #extract}, which ignore + * A machine's energy, counted as a {@code long} so large buffers do not + * overflow. Automation sees it through {@link IEnergyStorage}, whose + * {@code int} values are clamped, and is limited to the transfer rates. The + * owning block entity uses {@link #insert} and {@link #extract}, which ignore * the rates. */ -public class EnergyBuffer implements IEnergyStorage, INBTSerializable, StatsProvider { - private java.math.BigInteger inserted = java.math.BigInteger.ZERO; - private java.math.BigInteger extracted = java.math.BigInteger.ZERO; - private java.math.BigInteger used = java.math.BigInteger.ZERO; - - public java.math.BigInteger used() { - return used; - } - - @Override - public Statistics statistics() { - var rows = new java.util.ArrayList(); - if (canReceive()) rows.add(Statistics.rf("in", new Statistics.Counter(inserted, 20))); - if (canExtract()) rows.add(Statistics.rf("out", new Statistics.Counter(extracted, 20))); - if (canReceive()) rows.add(Statistics.transfer("in_max", maxInsert)); - if (canExtract()) rows.add(Statistics.transfer("out_max", maxExtract)); - rows.add(Statistics.rf("used", new Statistics.Counter(used, 20)).whenUsed(this)); - rows.add(Statistics.rf("used_max", new Statistics.MaximumUse(this)).whenUsed(this)); - return new Statistics(Statistics.label("energy"), rows); - } - - /** Runtime transfer counters. Loading, resizing and direct assignments are not transfers. */ - public java.math.BigInteger inserted() { - return inserted; - } - - public java.math.BigInteger extracted() { - return extracted; - } - - /** Captures stored energy and transfer counters for a synchronous transaction rollback. */ - public Runnable checkpoint() { - long before = stored; - var beforeIn = inserted; - var beforeOut = extracted; - var beforeUsed = used; - return () -> { - inserted = beforeIn; - extracted = beforeOut; - used = beforeUsed; - setStored(before); - }; - } - - private long stored; - private long capacity; - private long maxInsert; - private long maxExtract; - private Runnable onChange = () -> {}; - - /** - * An empty buffer. Negative values are treated as zero. - * - * @param capacity the most energy it holds - * @param maxInsert the most automation may insert per call - * @param maxExtract the most automation may extract per call - */ - public EnergyBuffer(long capacity, long maxInsert, long maxExtract) { - this.capacity = Math.max(0, capacity); - this.maxInsert = Math.max(0, maxInsert); - this.maxExtract = Math.max(0, maxExtract); - } - - /** An empty buffer with the same insert and extract rate. */ - public EnergyBuffer(long capacity, long maxTransfer) { - this(capacity, maxTransfer, maxTransfer); - } - - /** Runs after every change, typically the block entity's {@code setChanged}. */ - public EnergyBuffer onChange(Runnable onChange) { - this.onChange = onChange; - return this; - } - - /** The energy stored, exact. */ - public long stored() { - return stored; - } - - /** The most energy the buffer holds. */ - public long capacity() { - return capacity; - } - - /** How much more energy fits. */ - public long space() { - return capacity - stored; - } - - /** Sets the stored energy, clamped to the capacity. */ - public void setStored(long energy) { - long clamped = Math.clamp(energy, 0, capacity); - if (clamped != stored) { - stored = clamped; - onChange.run(); +public class EnergyBuffer + implements IEnergyStorage, INBTSerializable, StatsProvider { + private java.math.BigInteger inserted = java.math.BigInteger.ZERO; + private java.math.BigInteger extracted = java.math.BigInteger.ZERO; + private java.math.BigInteger used = java.math.BigInteger.ZERO; + + public java.math.BigInteger used() { + return used; + } + + @Override + public Statistics statistics() { + var rows = new java.util.ArrayList(); + if (canReceive()) + rows.add(Statistics.rf("in", new Statistics.Counter(inserted, 20))); + if (canExtract()) + rows.add( + Statistics.rf("out", new Statistics.Counter(extracted, 20))); + if (canReceive()) + rows.add(Statistics.transfer("in_max", maxInsert)); + if (canExtract()) + rows.add(Statistics.transfer("out_max", maxExtract)); + rows.add(Statistics.rf("used", new Statistics.Counter(used, 20)) + .whenUsed(this)); + rows.add(Statistics.rf("used_max", new Statistics.MaximumUse(this)) + .whenUsed(this)); + return new Statistics(Statistics.label("energy"), rows); } - } - - /** Sets the capacity, e.g. after an upgrade; stored energy above it is lost. */ - public void setCapacity(long capacity) { - this.capacity = Math.max(0, capacity); - setStored(stored); - } - - /** - * Sets the per-call limits for automation; the owner's {@link #insert} and {@link #extract} - * ignore them. - */ - public void setRates(long maxInsert, long maxExtract) { - this.maxInsert = Math.max(0, maxInsert); - this.maxExtract = Math.max(0, maxExtract); - } - - /** Adds up to {@code amount}, ignoring the transfer rate. Returns the amount added. */ - public long insert(long amount, boolean simulate) { - long accepted = Math.clamp(amount, 0, space()); - if (!simulate && accepted > 0) { - inserted = inserted.add(java.math.BigInteger.valueOf(accepted)); - setStored(stored + accepted); + + /** + * Runtime transfer counters. Loading, resizing and direct assignments are + * not transfers. + */ + public java.math.BigInteger inserted() { + return inserted; } - return accepted; - } - - /** Removes up to {@code amount}, ignoring the transfer rate. Returns the amount removed. */ - public long extract(long amount, boolean simulate) { - long removed = Math.clamp(amount, 0, stored); - if (!simulate && removed > 0) { - extracted = extracted.add(java.math.BigInteger.valueOf(removed)); - setStored(stored - removed); + + public java.math.BigInteger extracted() { + return extracted; + } + + /** + * Captures stored energy and transfer counters for a synchronous + * transaction rollback. + */ + public Runnable checkpoint() { + long before = stored; + var beforeIn = inserted; + var beforeOut = extracted; + var beforeUsed = used; + return () -> { + inserted = beforeIn; + extracted = beforeOut; + used = beforeUsed; + setStored(before); + }; } - return removed; - } - - /** Removes exactly {@code amount} if that much is stored, e.g. to run one tick of work. */ - public boolean consume(long amount) { - if (amount < 0 || amount > stored) return false; - used = used.add(java.math.BigInteger.valueOf(amount)); - setStored(stored - amount); - return true; - } - - /** Escrows a transfer charge without counting it as processor use or external output. */ - public boolean reserve(long amount) { - if (amount < 0 || amount > stored) return false; - setStored(stored - amount); - return true; - } - - /** Returns unused transfer escrow without counting it as external input. */ - public long refund(long amount) { - long accepted = Math.clamp(amount, 0, space()); - setStored(stored + accepted); - return accepted; - } - - @Override - public int receiveEnergy(int toReceive, boolean simulate) { - return (int) insert(Math.min(toReceive, maxInsert), simulate); - } - - @Override - public int extractEnergy(int toExtract, boolean simulate) { - return (int) extract(Math.min(toExtract, maxExtract), simulate); - } - - @Override - public int getEnergyStored() { - return (int) Math.min(stored, Integer.MAX_VALUE); - } - - @Override - public int getMaxEnergyStored() { - return (int) Math.min(capacity, Integer.MAX_VALUE); - } - - @Override - public boolean canReceive() { - return maxInsert > 0; - } - - @Override - public boolean canExtract() { - return maxExtract > 0; - } - - /** An automation view with a fixed mode, e.g. {@link IoMode#BOTH} for unsided access. */ - public IEnergyStorage view(IoMode mode) { - return view(() -> mode); - } - - /** - * An automation view whose mode is read on every call, so it follows side-configuration changes - * and rotation without invalidating capabilities. - */ - public IEnergyStorage view(Supplier mode) { - return new IEnergyStorage() { - @Override - public int receiveEnergy(int toReceive, boolean simulate) { - return mode.get().canInsert() ? EnergyBuffer.this.receiveEnergy(toReceive, simulate) : 0; - } - - @Override - public int extractEnergy(int toExtract, boolean simulate) { - return mode.get().canExtract() ? EnergyBuffer.this.extractEnergy(toExtract, simulate) : 0; - } - - @Override - public int getEnergyStored() { - return EnergyBuffer.this.getEnergyStored(); - } - - @Override - public int getMaxEnergyStored() { - return EnergyBuffer.this.getMaxEnergyStored(); - } - - @Override - public boolean canReceive() { - return mode.get().canInsert() && EnergyBuffer.this.canReceive(); - } - - @Override - public boolean canExtract() { - return mode.get().canExtract() && EnergyBuffer.this.canExtract(); - } + + private long stored; + private long capacity; + private long maxInsert; + private long maxExtract; + private Runnable onChange = () -> { }; - } - @Override - public Tag serializeNBT(HolderLookup.Provider provider) { - return LongTag.valueOf(stored); - } + /** + * An empty buffer. Negative values are treated as zero. + * + * @param capacity + * the most energy it holds + * @param maxInsert + * the most automation may insert per call + * @param maxExtract + * the most automation may extract per call + */ + public EnergyBuffer(long capacity, long maxInsert, long maxExtract) { + this.capacity = Math.max(0, capacity); + this.maxInsert = Math.max(0, maxInsert); + this.maxExtract = Math.max(0, maxExtract); + } + + /** An empty buffer with the same insert and extract rate. */ + public EnergyBuffer(long capacity, long maxTransfer) { + this(capacity, maxTransfer, maxTransfer); + } + + /** + * Runs after every change, typically the block entity's {@code setChanged}. + */ + public EnergyBuffer onChange(Runnable onChange) { + this.onChange = onChange; + return this; + } + + /** The energy stored, exact. */ + public long stored() { + return stored; + } + + /** The most energy the buffer holds. */ + public long capacity() { + return capacity; + } + + /** How much more energy fits. */ + public long space() { + return capacity - stored; + } + + /** Sets the stored energy, clamped to the capacity. */ + public void setStored(long energy) { + long clamped = Math.clamp(energy, 0, capacity); + if (clamped != stored) { + stored = clamped; + onChange.run(); + } + } + + /** + * Sets the capacity, e.g. after an upgrade; stored energy above it is lost. + */ + public void setCapacity(long capacity) { + this.capacity = Math.max(0, capacity); + setStored(stored); + } + + /** + * Sets the per-call limits for automation; the owner's {@link #insert} and + * {@link #extract} ignore them. + */ + public void setRates(long maxInsert, long maxExtract) { + this.maxInsert = Math.max(0, maxInsert); + this.maxExtract = Math.max(0, maxExtract); + } + + /** + * Adds up to {@code amount}, ignoring the transfer rate. Returns the amount + * added. + */ + public long insert(long amount, boolean simulate) { + long accepted = Math.clamp(amount, 0, space()); + if (!simulate && accepted > 0) { + inserted = inserted.add(java.math.BigInteger.valueOf(accepted)); + setStored(stored + accepted); + } + return accepted; + } + + /** + * Removes up to {@code amount}, ignoring the transfer rate. Returns the + * amount removed. + */ + public long extract(long amount, boolean simulate) { + long removed = Math.clamp(amount, 0, stored); + if (!simulate && removed > 0) { + extracted = extracted.add(java.math.BigInteger.valueOf(removed)); + setStored(stored - removed); + } + return removed; + } + + /** + * Removes exactly {@code amount} if that much is stored, e.g. to run one + * tick of work. + */ + public boolean consume(long amount) { + if (amount < 0 || amount > stored) + return false; + used = used.add(java.math.BigInteger.valueOf(amount)); + setStored(stored - amount); + return true; + } + + /** + * Escrows a transfer charge without counting it as processor use or + * external output. + */ + public boolean reserve(long amount) { + if (amount < 0 || amount > stored) + return false; + setStored(stored - amount); + return true; + } + + /** Returns unused transfer escrow without counting it as external input. */ + public long refund(long amount) { + long accepted = Math.clamp(amount, 0, space()); + setStored(stored + accepted); + return accepted; + } + + @Override + public int receiveEnergy(int toReceive, boolean simulate) { + return (int) insert(Math.min(toReceive, maxInsert), simulate); + } + + @Override + public int extractEnergy(int toExtract, boolean simulate) { + return (int) extract(Math.min(toExtract, maxExtract), simulate); + } + + @Override + public int getEnergyStored() { + return (int) Math.min(stored, Integer.MAX_VALUE); + } - @Override - public void deserializeNBT(HolderLookup.Provider provider, Tag tag) { - if (tag instanceof NumericTag number) stored = Math.clamp(number.getAsLong(), 0, capacity); - } + @Override + public int getMaxEnergyStored() { + return (int) Math.min(capacity, Integer.MAX_VALUE); + } + + @Override + public boolean canReceive() { + return maxInsert > 0; + } + + @Override + public boolean canExtract() { + return maxExtract > 0; + } + + /** + * An automation view with a fixed mode, e.g. {@link IoMode#BOTH} for + * unsided access. + */ + public IEnergyStorage view(IoMode mode) { + return view(() -> mode); + } + + /** + * An automation view whose mode is read on every call, so it follows + * side-configuration changes and rotation without invalidating + * capabilities. + */ + public IEnergyStorage view(Supplier mode) { + return new IEnergyStorage() { + @Override + public int receiveEnergy(int toReceive, boolean simulate) { + return mode.get().canInsert() + ? EnergyBuffer.this.receiveEnergy(toReceive, simulate) + : 0; + } + + @Override + public int extractEnergy(int toExtract, boolean simulate) { + return mode.get().canExtract() + ? EnergyBuffer.this.extractEnergy(toExtract, simulate) + : 0; + } + + @Override + public int getEnergyStored() { + return EnergyBuffer.this.getEnergyStored(); + } + + @Override + public int getMaxEnergyStored() { + return EnergyBuffer.this.getMaxEnergyStored(); + } + + @Override + public boolean canReceive() { + return mode.get().canInsert() && EnergyBuffer.this.canReceive(); + } + + @Override + public boolean canExtract() { + return mode.get().canExtract() + && EnergyBuffer.this.canExtract(); + } + }; + } + + @Override + public Tag serializeNBT(HolderLookup.Provider provider) { + return LongTag.valueOf(stored); + } + + @Override + public void deserializeNBT(HolderLookup.Provider provider, Tag tag) { + if (tag instanceof NumericTag number) + stored = Math.clamp(number.getAsLong(), 0, capacity); + } } diff --git a/src/main/java/zone/moddev/mc/lib/api/capabilities/FluidStorage.java b/src/main/java/zone/moddev/mc/lib/api/capabilities/FluidStorage.java index 1988f5b4..6fa86c9f 100644 --- a/src/main/java/zone/moddev/mc/lib/api/capabilities/FluidStorage.java +++ b/src/main/java/zone/moddev/mc/lib/api/capabilities/FluidStorage.java @@ -23,412 +23,463 @@ import zone.moddev.mc.lib.machine.neoforge.FluidStacks; /** - * A machine's tanks as one {@link IFluidHandler}, each tank with a {@link Role}, capacity and - * filter, and a change callback. The owning block entity works on {@link #tank} directly, which - * ignores roles; players' buckets and automation use the handler or a {@link #view}. + * A machine's tanks as one {@link IFluidHandler}, each tank with a + * {@link Role}, capacity and filter, and a change callback. The owning block + * entity works on {@link #tank} directly, which ignores roles; players' buckets + * and automation use the handler or a {@link #view}. * - *

Filling picks the first tank that already holds the fluid, then the first empty tank that - * accepts it. Draining picks the first tank holding the requested fluid, or for an amount, the - * first tank that is not empty. + *

+ * Filling picks the first tank that already holds the fluid, then the first + * empty tank that accepts it. Draining picks the first tank holding the + * requested fluid, or for an amount, the first tank that is not empty. */ -public class FluidStorage implements IFluidHandler, INBTSerializable { - private final List tanks = new ArrayList<>(); - private final List roles = new ArrayList<>(); - private final List tankIds = new ArrayList<>(); - private final List> insertionRules = new ArrayList<>(); - private final ListTag recovery = new ListTag(); - private final ReservationPlans plans = - new ReservationPlans<>( - 0, - TankPlan.NONE, - TankPlan::plus, - TankPlan::minus, - (tank, plan) -> - plan.legal( - FluidStacks.amount(tanks.get(tank).getFluid()), tanks.get(tank).getCapacity())); - private Runnable onChange = () -> {}; - private long revision; - - /** Change counter for recipe search invalidation. */ - public long revision() { - return revision + plans.revision(); - } - - /** A storage with no tanks; add them with {@link #addTank}. */ - public FluidStorage() {} - - /** Runs after every change, typically the block entity's {@code setChanged}. */ - public FluidStorage onChange(Runnable onChange) { - this.onChange = onChange; - return this; - } - - /** Adds a tank and returns its index. */ - public int addTank(int capacity, Role role, Predicate filter) { - int index = tanks.size(); - tanks.add( - new FluidTank(capacity, filter) { - @Override - public void setFluid(FluidStack stack) { - super.setFluid(stack); - onContentsChanged(); - } - - @Override - public FluidTank setCapacity(int capacity) { - revision++; - return super.setCapacity(capacity); - } - - @Override - public FluidTank setValidator(Predicate validator) { - revision++; - return super.setValidator(validator); - } - - @Override - protected void onContentsChanged() { - revision++; - onChange.run(); - } - - @Override - public int fill(FluidStack resource, FluidAction action) { - var plan = plans.get(index); - if (plan.isNone()) return super.fill(resource, action); - int room = - plan.room( - FluidStacks.amount(getFluid()), FluidStacks.amount(resource), getCapacity()); - if (room <= 0) return 0; - int amount = Math.min(resource.getAmount(), room); - return super.fill(resource.copyWithAmount(amount), action); - } - - @Override - public FluidStack drain(int maxDrain, FluidAction action) { - var plan = plans.get(index); - if (plan.isNone()) return super.drain(maxDrain, action); - int free = plan.free(FluidStacks.amount(getFluid())); - return free <= 0 ? FluidStack.EMPTY : super.drain(Math.min(maxDrain, free), action); - } - }); - roles.add(role); - tankIds.add(null); - insertionRules.add(fluid -> true); - plans.add(); - revision++; - return tanks.size() - 1; - } - - /** Adds a tank that accepts any fluid and returns its index. */ - public int addTank(int capacity, Role role) { - return addTank(capacity, role, fluid -> true); - } - - /** - * The tank itself, for the machine's own work; it ignores roles but keeps its filter and obeys - * its plan. - */ - public FluidTank tank(int index) { - return tanks.get(index); - } - - /** Stable save identity; saves without identities continue to load by index. */ - public FluidStorage tankId(int tank, String id) { - if (!inRange(tank) || id == null || id.isBlank()) - throw new IllegalArgumentException("Invalid tank ID: " + id); - for (int i = 0; i < tankIds.size(); i++) - if (i != tank && id.equals(tankIds.get(i))) - throw new IllegalArgumentException("Duplicate tank ID: " + id); - tankIds.set(tank, id); - return this; - } - - /** Additional external insertion rule; machine products use fillResult and bypass this rule. */ - public FluidStorage restrict(Predicate rule, int... indexes) { - for (int tank : indexes) - if (!inRange(tank)) - throw new IndexOutOfBoundsException("Tank " + tank + " not in [0," + getTanks() + ")"); - for (int tank : indexes) insertionRules.set(tank, insertionRules.get(tank).and(rule)); - revision++; - return this; - } - - /** Machine product insertion, retaining the tank's structural filter and reservation. */ - public int fillResult(int tank, FluidStack fluid, FluidAction action) { - return tank(tank).fill(fluid, action); - } - - /** Unchecked restoration for save loading and transaction rollback. */ - public void restoreFluid(int tank, FluidStack fluid) { - tank(tank).setFluid(fluid.copy()); - } - - public ListTag recoveryContents() { - return recovery.copy(); - } - - /** Explicitly discards contents, displaced fluids and plans. Stop owning processors first. */ - public void clear() { - recovery.clear(); - for (int tank = 0; tank < getTanks(); tank++) { - setPlan(tank, TankPlan.NONE); - restoreFluid(tank, FluidStack.EMPTY); - } - revision++; - onChange.run(); - } - - public void recover(Consumer receiver) { - StorageRecovery.handoff(recovery, "mmdlib:fluids", "fluid", receiver); - revision++; - onChange.run(); - } - - /** Shared plan table, including transaction tracking for groups over this storage. */ - public ReservationPlans reservationPlans() { - return plans; - } - - /** What the machine plans for tank {@code tank}. */ - public TankPlan plan(int tank) { - return plans.get(tank); - } - - /** Replaces the plan with no checks, for undoing and loading; use {@link #reserve} otherwise. */ - public void setPlan(int tank, TankPlan plan) { - plans.set(tank, plan); - } - - /** - * Adds {@code plan} to what is planned for the tank. Returns false and changes nothing if the - * plans cannot be merged or the result is not legal for what the tank holds now. - */ - public boolean reserve(int tank, TankPlan plan) { - return plans.reserve(tank, plan); - } - - /** Takes {@code plan}, which must have been reserved, out of the tank's plan. */ - public void release(int tank, TankPlan plan) { - plans.release(tank, plan); - } - - /** The role of tank {@code tank}. */ - public Role role(int tank) { - return roles.get(tank); - } - - @Override - public int getTanks() { - return tanks.size(); - } - - @Override - public FluidStack getFluidInTank(int tank) { - return inRange(tank) ? tanks.get(tank).getFluid() : FluidStack.EMPTY; - } - - @Override - public int getTankCapacity(int tank) { - return inRange(tank) ? tanks.get(tank).getCapacity() : 0; - } - - @Override - public boolean isFluidValid(int tank, FluidStack stack) { - return inRange(tank) - && roles.get(tank) != Role.OUTPUT - && insertionRules.get(tank).test(stack) - && tanks.get(tank).isFluidValid(stack); - } - - @Override - public int fill(FluidStack resource, FluidAction action) { - return fill(resource, action, tank -> roles.get(tank) != Role.OUTPUT); - } - - @Override - public FluidStack drain(FluidStack resource, FluidAction action) { - return drain(resource, action, role -> true); - } - - @Override - public FluidStack drain(int maxDrain, FluidAction action) { - return drain(maxDrain, action, role -> true); - } - - /** An automation view with a fixed mode, e.g. {@link IoMode#BOTH} for unsided access. */ - public IFluidHandler view(IoMode mode) { - return view(() -> mode); - } - - /** - * An automation view whose mode is read on every call, so it follows side-configuration changes - * and rotation without invalidating capabilities. Every view shows all tanks with the same - * numbering. - */ - public IFluidHandler view(Supplier mode) { - return view(tank -> mode.get()); - } - - /** Per-tank permissions; filling/draining searches only eligible tanks without renumbering. */ - public IFluidHandler view(IntFunction mode) { - return new IFluidHandler() { - @Override - public int getTanks() { - return FluidStorage.this.getTanks(); - } - - @Override - public FluidStack getFluidInTank(int tank) { - return FluidStorage.this.getFluidInTank(tank); - } - - @Override - public int getTankCapacity(int tank) { - return FluidStorage.this.getTankCapacity(tank); - } - - @Override - public boolean isFluidValid(int tank, FluidStack stack) { - return inRange(tank) - && mode.apply(tank).canInsert() - && roles.get(tank).canInsert() - && FluidStorage.this.isFluidValid(tank, stack); - } - - @Override - public int fill(FluidStack resource, FluidAction action) { - return FluidStorage.this.fill( - resource, action, tank -> roles.get(tank).canInsert() && mode.apply(tank).canInsert()); - } - - @Override - public FluidStack drain(FluidStack resource, FluidAction action) { - return FluidStorage.this.drain( - resource, - action, - tank -> roles.get(tank).canExtract() && mode.apply(tank).canExtract()); - } - - @Override - public FluidStack drain(int maxDrain, FluidAction action) { - return FluidStorage.this.drain( - maxDrain, - action, - tank -> roles.get(tank).canExtract() && mode.apply(tank).canExtract()); - } +public class FluidStorage + implements IFluidHandler, INBTSerializable { + private final List tanks = new ArrayList<>(); + private final List roles = new ArrayList<>(); + private final List tankIds = new ArrayList<>(); + private final List> insertionRules = new ArrayList<>(); + private final ListTag recovery = new ListTag(); + private final ReservationPlans plans = new ReservationPlans<>(0, + TankPlan.NONE, TankPlan::plus, TankPlan::minus, + (tank, plan) -> plan.legal( + FluidStacks.amount(tanks.get(tank).getFluid()), + tanks.get(tank).getCapacity())); + private Runnable onChange = () -> { }; - } - - private int fill(FluidStack resource, FluidAction action, IntPredicate allowed) { - if (resource.isEmpty()) return 0; - int target = -1; - for (int i = 0; i < tanks.size() && target < 0; i++) - if (allowed.test(i) - && insertionRules.get(i).test(resource) - && FluidStack.isSameFluidSameComponents(tanks.get(i).getFluid(), resource) - && tanks.get(i).fill(resource, FluidAction.SIMULATE) > 0) target = i; - for (int i = 0; i < tanks.size() && target < 0; i++) - if (allowed.test(i) - && insertionRules.get(i).test(resource) - && tanks.get(i).isEmpty() - && tanks.get(i).fill(resource, FluidAction.SIMULATE) > 0) target = i; - return target < 0 ? 0 : tanks.get(target).fill(resource, action); - } - - private FluidStack drain(FluidStack resource, FluidAction action, IntPredicate allowed) { - if (resource.isEmpty()) return FluidStack.EMPTY; - for (int i = 0; i < tanks.size(); i++) - if (allowed.test(i) - && FluidStack.isSameFluidSameComponents(tanks.get(i).getFluid(), resource) - && !tanks.get(i).drain(resource, FluidAction.SIMULATE).isEmpty()) - return tanks.get(i).drain(resource, action); - return FluidStack.EMPTY; - } - - private FluidStack drain(int maxDrain, FluidAction action, IntPredicate allowed) { - for (int i = 0; i < tanks.size(); i++) - if (allowed.test(i) && !tanks.get(i).drain(maxDrain, FluidAction.SIMULATE).isEmpty()) - return tanks.get(i).drain(maxDrain, action); - return FluidStack.EMPTY; - } - - private boolean inRange(int tank) { - return tank >= 0 && tank < tanks.size(); - } - - @Override - public CompoundTag serializeNBT(HolderLookup.Provider provider) { - ListTag list = new ListTag(); - for (int i = 0; i < tanks.size(); i++) { - var entry = tanks.get(i).writeToNBT(provider, new CompoundTag()); - if (tankIds.get(i) != null) entry.putString("TankId", tankIds.get(i)); - list.add(entry); - } - CompoundTag tag = new CompoundTag(); - tag.put("Tanks", list); - tag.put("Recovery", recovery.copy()); - return tag; - } - - /** Loads by stable identity or legacy index, preserving removed tanks and capacity overflow. */ - @Override - public void deserializeNBT(HolderLookup.Provider provider, CompoundTag tag) { - deserializeNBT(provider, tag, java.util.function.IntUnaryOperator.identity()); - } - - /** Explicit index migration for pre-identity saves. The host selects this by its save version. */ - public void deserializeNBT( - HolderLookup.Provider provider, - CompoundTag tag, - java.util.function.IntUnaryOperator legacyTanks) { - var saved = tag.getList("Tanks", Tag.TAG_COMPOUND); - var list = new ListTag(); - for (int i = 0; i < saved.size(); i++) { - var tank = saved.getCompound(i); - var entry = tank.getCompound("Fluid").copy(); - if (entry.isEmpty()) continue; - entry.putInt("Tank", i); - if (tank.contains("TankId")) entry.putString("TankId", tank.getString("TankId")); - list.add(entry); - } - list.addAll(tag.getList("Recovery", Tag.TAG_COMPOUND).copy()); - recovery.clear(); - for (var tank : tanks) tank.setFluid(FluidStack.EMPTY); - for (var value : list) { - var entry = (CompoundTag) value; - int index = - entry.contains("TankId") - ? tankIds.indexOf(entry.getString("TankId")) - : legacyTanks.applyAsInt(entry.getInt("Tank")); - if (!entry.contains("TankId")) { - entry.putInt("Tank", index); - if (inRange(index) && tankIds.get(index) != null) - entry.putString("TankId", tankIds.get(index)); - } - var fluid = FluidStack.parseOptional(provider, entry); - if (fluid.isEmpty()) { - if (entry.contains("id")) recovery.add(entry.copy()); - continue; - } - if (!inRange(index) - || !tanks.get(index).isFluidValid(fluid) - || (!tanks.get(index).isEmpty() - && !FluidStack.isSameFluidSameComponents(tanks.get(index).getFluid(), fluid))) { - recovery.add(entry.copy()); - continue; - } - int held = tanks.get(index).getFluidAmount(); - int accepted = - Math.min(fluid.getAmount(), Math.max(0, tanks.get(index).getCapacity() - held)); - restoreFluid(index, fluid.copyWithAmount(held + accepted)); - if (accepted < fluid.getAmount()) { - var extra = entry.copy(); - extra.putInt("amount", fluid.getAmount() - accepted); - recovery.add(extra); - } - } - revision++; - onChange.run(); - } + private long revision; + + /** Change counter for recipe search invalidation. */ + public long revision() { + return revision + plans.revision(); + } + + /** A storage with no tanks; add them with {@link #addTank}. */ + public FluidStorage() { + } + + /** + * Runs after every change, typically the block entity's {@code setChanged}. + */ + public FluidStorage onChange(Runnable onChange) { + this.onChange = onChange; + return this; + } + + /** Adds a tank and returns its index. */ + public int addTank(int capacity, Role role, Predicate filter) { + int index = tanks.size(); + tanks.add(new FluidTank(capacity, filter) { + @Override + public void setFluid(FluidStack stack) { + super.setFluid(stack); + onContentsChanged(); + } + + @Override + public FluidTank setCapacity(int capacity) { + revision++; + return super.setCapacity(capacity); + } + + @Override + public FluidTank setValidator(Predicate validator) { + revision++; + return super.setValidator(validator); + } + + @Override + protected void onContentsChanged() { + revision++; + onChange.run(); + } + + @Override + public int fill(FluidStack resource, FluidAction action) { + var plan = plans.get(index); + if (plan.isNone()) + return super.fill(resource, action); + int room = plan.room(FluidStacks.amount(getFluid()), + FluidStacks.amount(resource), getCapacity()); + if (room <= 0) + return 0; + int amount = Math.min(resource.getAmount(), room); + return super.fill(resource.copyWithAmount(amount), action); + } + + @Override + public FluidStack drain(int maxDrain, FluidAction action) { + var plan = plans.get(index); + if (plan.isNone()) + return super.drain(maxDrain, action); + int free = plan.free(FluidStacks.amount(getFluid())); + return free <= 0 + ? FluidStack.EMPTY + : super.drain(Math.min(maxDrain, free), action); + } + }); + roles.add(role); + tankIds.add(null); + insertionRules.add(fluid -> true); + plans.add(); + revision++; + return tanks.size() - 1; + } + + /** Adds a tank that accepts any fluid and returns its index. */ + public int addTank(int capacity, Role role) { + return addTank(capacity, role, fluid -> true); + } + + /** + * The tank itself, for the machine's own work; it ignores roles but keeps + * its filter and obeys its plan. + */ + public FluidTank tank(int index) { + return tanks.get(index); + } + + /** + * Stable save identity; saves without identities continue to load by index. + */ + public FluidStorage tankId(int tank, String id) { + if (!inRange(tank) || id == null || id.isBlank()) + throw new IllegalArgumentException("Invalid tank ID: " + id); + for (int i = 0; i < tankIds.size(); i++) + if (i != tank && id.equals(tankIds.get(i))) + throw new IllegalArgumentException("Duplicate tank ID: " + id); + tankIds.set(tank, id); + return this; + } + + /** + * Additional external insertion rule; machine products use fillResult and + * bypass this rule. + */ + public FluidStorage restrict(Predicate rule, int... indexes) { + for (int tank : indexes) + if (!inRange(tank)) + throw new IndexOutOfBoundsException( + "Tank " + tank + " not in [0," + getTanks() + ")"); + for (int tank : indexes) + insertionRules.set(tank, insertionRules.get(tank).and(rule)); + revision++; + return this; + } + + /** + * Machine product insertion, retaining the tank's structural filter and + * reservation. + */ + public int fillResult(int tank, FluidStack fluid, FluidAction action) { + return tank(tank).fill(fluid, action); + } + + /** Unchecked restoration for save loading and transaction rollback. */ + public void restoreFluid(int tank, FluidStack fluid) { + tank(tank).setFluid(fluid.copy()); + } + + public ListTag recoveryContents() { + return recovery.copy(); + } + + /** + * Explicitly discards contents, displaced fluids and plans. Stop owning + * processors first. + */ + public void clear() { + recovery.clear(); + for (int tank = 0; tank < getTanks(); tank++) { + setPlan(tank, TankPlan.NONE); + restoreFluid(tank, FluidStack.EMPTY); + } + revision++; + onChange.run(); + } + + public void recover(Consumer receiver) { + StorageRecovery.handoff(recovery, "mmdlib:fluids", "fluid", receiver); + revision++; + onChange.run(); + } + + /** + * Shared plan table, including transaction tracking for groups over this + * storage. + */ + public ReservationPlans reservationPlans() { + return plans; + } + + /** What the machine plans for tank {@code tank}. */ + public TankPlan plan(int tank) { + return plans.get(tank); + } + + /** + * Replaces the plan with no checks, for undoing and loading; use + * {@link #reserve} otherwise. + */ + public void setPlan(int tank, TankPlan plan) { + plans.set(tank, plan); + } + + /** + * Adds {@code plan} to what is planned for the tank. Returns false and + * changes nothing if the plans cannot be merged or the result is not legal + * for what the tank holds now. + */ + public boolean reserve(int tank, TankPlan plan) { + return plans.reserve(tank, plan); + } + + /** + * Takes {@code plan}, which must have been reserved, out of the tank's + * plan. + */ + public void release(int tank, TankPlan plan) { + plans.release(tank, plan); + } + + /** The role of tank {@code tank}. */ + public Role role(int tank) { + return roles.get(tank); + } + + @Override + public int getTanks() { + return tanks.size(); + } + + @Override + public FluidStack getFluidInTank(int tank) { + return inRange(tank) ? tanks.get(tank).getFluid() : FluidStack.EMPTY; + } + + @Override + public int getTankCapacity(int tank) { + return inRange(tank) ? tanks.get(tank).getCapacity() : 0; + } + + @Override + public boolean isFluidValid(int tank, FluidStack stack) { + return inRange(tank) && roles.get(tank) != Role.OUTPUT + && insertionRules.get(tank).test(stack) + && tanks.get(tank).isFluidValid(stack); + } + + @Override + public int fill(FluidStack resource, FluidAction action) { + return fill(resource, action, tank -> roles.get(tank) != Role.OUTPUT); + } + + @Override + public FluidStack drain(FluidStack resource, FluidAction action) { + return drain(resource, action, role -> true); + } + + @Override + public FluidStack drain(int maxDrain, FluidAction action) { + return drain(maxDrain, action, role -> true); + } + + /** + * An automation view with a fixed mode, e.g. {@link IoMode#BOTH} for + * unsided access. + */ + public IFluidHandler view(IoMode mode) { + return view(() -> mode); + } + + /** + * An automation view whose mode is read on every call, so it follows + * side-configuration changes and rotation without invalidating + * capabilities. Every view shows all tanks with the same numbering. + */ + public IFluidHandler view(Supplier mode) { + return view(tank -> mode.get()); + } + + /** + * Per-tank permissions; filling/draining searches only eligible tanks + * without renumbering. + */ + public IFluidHandler view(IntFunction mode) { + return new IFluidHandler() { + @Override + public int getTanks() { + return FluidStorage.this.getTanks(); + } + + @Override + public FluidStack getFluidInTank(int tank) { + return FluidStorage.this.getFluidInTank(tank); + } + + @Override + public int getTankCapacity(int tank) { + return FluidStorage.this.getTankCapacity(tank); + } + + @Override + public boolean isFluidValid(int tank, FluidStack stack) { + return inRange(tank) && mode.apply(tank).canInsert() + && roles.get(tank).canInsert() + && FluidStorage.this.isFluidValid(tank, stack); + } + + @Override + public int fill(FluidStack resource, FluidAction action) { + return FluidStorage.this.fill(resource, action, + tank -> roles.get(tank).canInsert() + && mode.apply(tank).canInsert()); + } + + @Override + public FluidStack drain(FluidStack resource, FluidAction action) { + return FluidStorage.this.drain(resource, action, + tank -> roles.get(tank).canExtract() + && mode.apply(tank).canExtract()); + } + + @Override + public FluidStack drain(int maxDrain, FluidAction action) { + return FluidStorage.this.drain(maxDrain, action, + tank -> roles.get(tank).canExtract() + && mode.apply(tank).canExtract()); + } + }; + } + + private int fill(FluidStack resource, FluidAction action, + IntPredicate allowed) { + if (resource.isEmpty()) + return 0; + int target = -1; + for (int i = 0; i < tanks.size() && target < 0; i++) + if (allowed.test(i) && insertionRules.get(i).test(resource) + && FluidStack.isSameFluidSameComponents(tanks.get(i).getFluid(), + resource) + && tanks.get(i).fill(resource, FluidAction.SIMULATE) > 0) + target = i; + for (int i = 0; i < tanks.size() && target < 0; i++) + if (allowed.test(i) && insertionRules.get(i).test(resource) + && tanks.get(i).isEmpty() + && tanks.get(i).fill(resource, FluidAction.SIMULATE) > 0) + target = i; + return target < 0 ? 0 : tanks.get(target).fill(resource, action); + } + + private FluidStack drain(FluidStack resource, FluidAction action, + IntPredicate allowed) { + if (resource.isEmpty()) + return FluidStack.EMPTY; + for (int i = 0; i < tanks.size(); i++) + if (allowed.test(i) + && FluidStack.isSameFluidSameComponents(tanks.get(i).getFluid(), + resource) + && !tanks.get(i).drain(resource, FluidAction.SIMULATE) + .isEmpty()) + return tanks.get(i).drain(resource, action); + return FluidStack.EMPTY; + } + + private FluidStack drain(int maxDrain, FluidAction action, + IntPredicate allowed) { + for (int i = 0; i < tanks.size(); i++) + if (allowed.test(i) && !tanks.get(i) + .drain(maxDrain, FluidAction.SIMULATE).isEmpty()) + return tanks.get(i).drain(maxDrain, action); + return FluidStack.EMPTY; + } + + private boolean inRange(int tank) { + return tank >= 0 && tank < tanks.size(); + } + + @Override + public CompoundTag serializeNBT(HolderLookup.Provider provider) { + ListTag list = new ListTag(); + for (int i = 0; i < tanks.size(); i++) { + var entry = tanks.get(i).writeToNBT(provider, new CompoundTag()); + if (tankIds.get(i) != null) + entry.putString("TankId", tankIds.get(i)); + list.add(entry); + } + CompoundTag tag = new CompoundTag(); + tag.put("Tanks", list); + tag.put("Recovery", recovery.copy()); + return tag; + } + + /** + * Loads by stable identity or legacy index, preserving removed tanks and + * capacity overflow. + */ + @Override + public void deserializeNBT(HolderLookup.Provider provider, + CompoundTag tag) { + deserializeNBT(provider, tag, + java.util.function.IntUnaryOperator.identity()); + } + + /** + * Explicit index migration for pre-identity saves. The host selects this by + * its save version. + */ + public void deserializeNBT(HolderLookup.Provider provider, CompoundTag tag, + java.util.function.IntUnaryOperator legacyTanks) { + var saved = tag.getList("Tanks", Tag.TAG_COMPOUND); + var list = new ListTag(); + for (int i = 0; i < saved.size(); i++) { + var tank = saved.getCompound(i); + var entry = tank.getCompound("Fluid").copy(); + if (entry.isEmpty()) + continue; + entry.putInt("Tank", i); + if (tank.contains("TankId")) + entry.putString("TankId", tank.getString("TankId")); + list.add(entry); + } + list.addAll(tag.getList("Recovery", Tag.TAG_COMPOUND).copy()); + recovery.clear(); + for (var tank : tanks) + tank.setFluid(FluidStack.EMPTY); + for (var value : list) { + var entry = (CompoundTag) value; + int index = entry.contains("TankId") + ? tankIds.indexOf(entry.getString("TankId")) + : legacyTanks.applyAsInt(entry.getInt("Tank")); + if (!entry.contains("TankId")) { + entry.putInt("Tank", index); + if (inRange(index) && tankIds.get(index) != null) + entry.putString("TankId", tankIds.get(index)); + } + var fluid = FluidStack.parseOptional(provider, entry); + if (fluid.isEmpty()) { + if (entry.contains("id")) + recovery.add(entry.copy()); + continue; + } + if (!inRange(index) || !tanks.get(index).isFluidValid(fluid) + || (!tanks.get(index).isEmpty() + && !FluidStack.isSameFluidSameComponents( + tanks.get(index).getFluid(), fluid))) { + recovery.add(entry.copy()); + continue; + } + int held = tanks.get(index).getFluidAmount(); + int accepted = Math.min(fluid.getAmount(), + Math.max(0, tanks.get(index).getCapacity() - held)); + restoreFluid(index, fluid.copyWithAmount(held + accepted)); + if (accepted < fluid.getAmount()) { + var extra = entry.copy(); + extra.putInt("amount", fluid.getAmount() - accepted); + recovery.add(extra); + } + } + revision++; + onChange.run(); + } } diff --git a/src/main/java/zone/moddev/mc/lib/api/capabilities/IoMode.java b/src/main/java/zone/moddev/mc/lib/api/capabilities/IoMode.java index fb6f952e..f94c67a4 100644 --- a/src/main/java/zone/moddev/mc/lib/api/capabilities/IoMode.java +++ b/src/main/java/zone/moddev/mc/lib/api/capabilities/IoMode.java @@ -4,45 +4,49 @@ import net.minecraft.util.StringRepresentable; -/** What one face of a machine lets automation do with one resource (items, fluids or energy). */ +/** + * What one face of a machine lets automation do with one resource (items, + * fluids or energy). + */ public enum IoMode implements StringRepresentable { - /** Automation can neither insert nor extract. */ - DISABLED(false, false), - /** Automation can insert only. */ - INPUT(true, false), - /** Automation can extract only. */ - OUTPUT(false, true), - /** Automation can insert and extract. */ - BOTH(true, true); - - /** Reads and writes the lower-case name, e.g. {@code "input"}. */ - public static final Codec CODEC = StringRepresentable.fromEnum(IoMode::values); - - private final boolean insert; - private final boolean extract; - - IoMode(boolean insert, boolean extract) { - this.insert = insert; - this.extract = extract; - } - - /** Whether automation may insert through a face with this mode. */ - public boolean canInsert() { - return insert; - } - - /** Whether automation may extract through a face with this mode. */ - public boolean canExtract() { - return extract; - } - - /** The next mode in the order used by side-configuration buttons. */ - public IoMode next() { - return values()[(ordinal() + 1) % values().length]; - } - - @Override - public String getSerializedName() { - return name().toLowerCase(java.util.Locale.ROOT); - } + /** Automation can neither insert nor extract. */ + DISABLED(false, false), + /** Automation can insert only. */ + INPUT(true, false), + /** Automation can extract only. */ + OUTPUT(false, true), + /** Automation can insert and extract. */ + BOTH(true, true); + + /** Reads and writes the lower-case name, e.g. {@code "input"}. */ + public static final Codec CODEC = StringRepresentable + .fromEnum(IoMode::values); + + private final boolean insert; + private final boolean extract; + + IoMode(boolean insert, boolean extract) { + this.insert = insert; + this.extract = extract; + } + + /** Whether automation may insert through a face with this mode. */ + public boolean canInsert() { + return insert; + } + + /** Whether automation may extract through a face with this mode. */ + public boolean canExtract() { + return extract; + } + + /** The next mode in the order used by side-configuration buttons. */ + public IoMode next() { + return values()[(ordinal() + 1) % values().length]; + } + + @Override + public String getSerializedName() { + return name().toLowerCase(java.util.Locale.ROOT); + } } diff --git a/src/main/java/zone/moddev/mc/lib/api/capabilities/ItemInventories.java b/src/main/java/zone/moddev/mc/lib/api/capabilities/ItemInventories.java index 8edb8915..a2afe994 100644 --- a/src/main/java/zone/moddev/mc/lib/api/capabilities/ItemInventories.java +++ b/src/main/java/zone/moddev/mc/lib/api/capabilities/ItemInventories.java @@ -25,8 +25,9 @@ import zone.moddev.mc.lib.machine.inventory.SlotPlan; /** - * Named, independent inventories with a stable combined view for capabilities and legacy callers. - * Contents, filters and reservations belong to the individual ItemStorage instances. + * Named, independent inventories with a stable combined view for capabilities + * and legacy callers. Contents, filters and reservations belong to the + * individual ItemStorage instances. */ public final class ItemInventories implements ItemStore { public record Address(String inventory, int slot) { @@ -38,25 +39,25 @@ public record Address(String inventory, int slot) { private ListTag recovery = new ListTag(); public ItemInventories(Map inventories) { - this( - inventories, - inventories.entrySet().stream() - .flatMap( - entry -> java.util.stream.IntStream.range(0, entry.getValue().getSlots()) - .mapToObj(slot -> new Address(entry.getKey(), slot))) - .toList()); + this(inventories, inventories.entrySet().stream() + .flatMap(entry -> java.util.stream.IntStream + .range(0, entry.getValue().getSlots()) + .mapToObj(slot -> new Address(entry.getKey(), slot))) + .toList()); } - private ItemInventories(Map inventories, List

addresses) { + private ItemInventories(Map inventories, + List
addresses) { var copy = new LinkedHashMap(); - inventories.forEach( - (name, storage) -> { - if (name == null || !name.matches("[a-z0-9_.-]{1,64}")) - throw new IllegalArgumentException("Invalid inventory name: " + name); - if (copy.containsValue(storage)) - throw new IllegalArgumentException("Inventory already registered"); - copy.put(name, Objects.requireNonNull(storage)); - }); + inventories.forEach((name, storage) -> { + if (name == null || !name.matches("[a-z0-9_.-]{1,64}")) + throw new IllegalArgumentException( + "Invalid inventory name: " + name); + if (copy.containsValue(storage)) + throw new IllegalArgumentException( + "Inventory already registered"); + copy.put(name, Objects.requireNonNull(storage)); + }); this.inventories = Collections.unmodifiableMap(copy); this.addresses = List.copyOf(addresses); var indexes = new HashMap(); @@ -65,7 +66,10 @@ private ItemInventories(Map inventories, List
addr this.indexes = Map.copyOf(indexes); } - /** Allocate one real inventory per name, retaining the supplied legacy combined ordering. */ + /** + * Allocate one real inventory per name, retaining the supplied legacy + * combined ordering. + */ public static ItemInventories partition(List names) { var sizes = new LinkedHashMap(); var addresses = new ArrayList
(); @@ -175,7 +179,8 @@ public void restoreStack(int slot, ItemStack stack) { } public long revision() { - return inventories.values().stream().mapToLong(ItemStorage::revision).sum(); + return inventories.values().stream().mapToLong(ItemStorage::revision) + .sum(); } public void markChanged() { @@ -191,7 +196,8 @@ public void setPlan(int slot, SlotPlan plan) { } public boolean hasPlan(int slot) { - return slot >= 0 && slot < getSlots() && storage(slot).hasPlan(localSlot(slot)); + return slot >= 0 && slot < getSlots() + && storage(slot).hasPlan(localSlot(slot)); } public int freeToTake(int slot) { @@ -222,27 +228,34 @@ public int getSlots() { } public ItemStack getStackInSlot(int slot) { - return valid(slot) ? ItemInventories.this.getStackInSlot(slot) : ItemStack.EMPTY; + return valid(slot) + ? ItemInventories.this.getStackInSlot(slot) + : ItemStack.EMPTY; } - public ItemStack insertItem(int slot, ItemStack stack, boolean simulate) { + public ItemStack insertItem(int slot, ItemStack stack, + boolean simulate) { return valid(slot) ? port(slot).insertItem(localSlot(slot), stack, simulate) : stack; } - public ItemStack extractItem(int slot, int amount, boolean simulate) { + public ItemStack extractItem(int slot, int amount, + boolean simulate) { return valid(slot) ? port(slot).extractItem(localSlot(slot), amount, simulate) : ItemStack.EMPTY; } public int getSlotLimit(int slot) { - return valid(slot) ? ItemInventories.this.getSlotLimit(slot) : 0; + return valid(slot) + ? ItemInventories.this.getSlotLimit(slot) + : 0; } public boolean isItemValid(int slot, ItemStack stack) { - return valid(slot) && port(slot).isItemValid(localSlot(slot), stack); + return valid(slot) + && port(slot).isItemValid(localSlot(slot), stack); } }; } @@ -251,23 +264,27 @@ public CompoundTag serializeNBT(HolderLookup.Provider registries) { assignDefaultSlotIds(); var result = new CompoundTag(); var named = new CompoundTag(); - inventories.forEach((name, storage) -> named.put(name, storage.serializeNBT(registries))); + inventories.forEach((name, storage) -> named.put(name, + storage.serializeNBT(registries))); result.put("Inventories", named); result.put("Recovery", recovery.copy()); return result; } - public void deserializeNBT(HolderLookup.Provider registries, CompoundTag tag) { + public void deserializeNBT(HolderLookup.Provider registries, + CompoundTag tag) { deserializeNBT(registries, tag, IntUnaryOperator.identity()); } - public void deserializeNBT( - HolderLookup.Provider registries, CompoundTag tag, IntUnaryOperator legacySlots) { + public void deserializeNBT(HolderLookup.Provider registries, + CompoundTag tag, IntUnaryOperator legacySlots) { assignDefaultSlotIds(); - // Resolve the complete old/new save before restoring any processor reservations. + // Resolve the complete old/new save before restoring any processor + // reservations. var entries = tag.getList("Items", Tag.TAG_COMPOUND).copy(); entries.addAll(tag.getList("Recovery", Tag.TAG_COMPOUND).copy()); - // Old saves used globally unique IDs; new saves namespace each inventory's local IDs. + // Old saves used globally unique IDs; new saves namespace each + // inventory's local IDs. for (var value : entries) { var entry = (CompoundTag) value; String id = entry.getString("SlotId"); @@ -282,7 +299,8 @@ public void deserializeNBT( found = i; } if (found >= 0) - entry.putString("SlotId", address(found).inventory() + "::" + id); + entry.putString("SlotId", + address(found).inventory() + "::" + id); } } var named = tag.getCompound("Inventories"); @@ -295,10 +313,8 @@ public void deserializeNBT( int local = entry.getInt("Slot"); int current = index(name, local); entry.putInt("Slot", current); - entry.putString( - "SlotId", - name - + "::" + entry.putString("SlotId", + name + "::" + (entry.contains("SlotId") ? entry.getString("SlotId") : Integer.toString(local))); @@ -314,8 +330,8 @@ public void deserializeNBT( } var flat = new CompoundTag(); flat.put("Items", entries); - migration.deserializeNBT( - registries, flat, named.isEmpty() ? legacySlots : IntUnaryOperator.identity()); + migration.deserializeNBT(registries, flat, + named.isEmpty() ? legacySlots : IntUnaryOperator.identity()); clear(); for (int i = 0; i < getSlots(); i++) restoreStack(i, migration.getStackInSlot(i)); @@ -326,7 +342,10 @@ public ListTag recoveryContents() { return recovery.copy(); } - /** Apply fallbacks only after callers have supplied their explicit definition IDs. */ + /** + * Apply fallbacks only after callers have supplied their explicit + * definition IDs. + */ private void assignDefaultSlotIds() { for (var storage : inventories.values()) { var used = new java.util.HashSet(); @@ -344,8 +363,10 @@ private void assignDefaultSlotIds() { } } - public void dropAll(Level level, BlockPos pos, Consumer receiver) { + public void dropAll(Level level, BlockPos pos, + Consumer receiver) { StorageRecovery.handoff(recovery, "mmdlib:items", "item", receiver); - inventories.values().forEach(storage -> storage.dropAll(level, pos, receiver)); + inventories.values() + .forEach(storage -> storage.dropAll(level, pos, receiver)); } } diff --git a/src/main/java/zone/moddev/mc/lib/api/capabilities/ItemStorage.java b/src/main/java/zone/moddev/mc/lib/api/capabilities/ItemStorage.java index 33375e91..5349c6b2 100644 --- a/src/main/java/zone/moddev/mc/lib/api/capabilities/ItemStorage.java +++ b/src/main/java/zone/moddev/mc/lib/api/capabilities/ItemStorage.java @@ -20,14 +20,15 @@ import net.neoforged.neoforge.items.ItemStackHandler; /** - * A machine inventory: an {@link ItemStackHandler} with a {@link Role}, filter and limit per slot, - * and a change callback. The owning block entity and its screen use it directly; automation gets a - * {@link #view} that also applies roles and the face's {@link IoMode}. + * A machine inventory: an {@link ItemStackHandler} with a {@link Role}, filter + * and limit per slot, and a change callback. The owning block entity and its + * screen use it directly; automation gets a {@link #view} that also applies + * roles and the face's {@link IoMode}. * *

- * Directly, a slot's filter decides what may be placed in it and {@link Role#OUTPUT} slots refuse - * placement, so players cannot put items into outputs. The machine fills outputs with - * {@link #insertResult}. + * Directly, a slot's filter decides what may be placed in it and + * {@link Role#OUTPUT} slots refuse placement, so players cannot put items into + * outputs. The machine fills outputs with {@link #insertResult}. */ public class ItemStorage extends PlannedItemHandler implements ItemStore { @Override @@ -60,7 +61,10 @@ public String slotId(int slot) { private final String[] slotIds; private final ListTag recovery = new ListTag(); - /** Optional stable identity for save migration; legacy saves still load by index. */ + /** + * Optional stable identity for save migration; legacy saves still load by + * index. + */ public ItemStorage slotId(int slot, String id) { checked(slot); if (id == null || id.isBlank()) @@ -72,7 +76,9 @@ public ItemStorage slotId(int slot, String id) { return this; } - /** Adds an acceptance rule without replacing the machine's existing filter. */ + /** + * Adds an acceptance rule without replacing the machine's existing filter. + */ public ItemStorage restrict(Predicate filter, int... slots) { for (int slot : slots) insertionRules[checked(slot)] = insertionRules[slot].and(filter); @@ -80,12 +86,18 @@ public ItemStorage restrict(Predicate filter, int... slots) { return this; } - /** Unchecked restoration, explicitly bypassing placement rules and reservations. */ + /** + * Unchecked restoration, explicitly bypassing placement rules and + * reservations. + */ public void restoreStack(int slot, ItemStack stack) { setStackInSlot(slot, stack.copy()); } - /** Displaced or undecodable stacks, retained verbatim until recovered or loadable again. */ + /** + * Displaced or undecodable stacks, retained verbatim until recovered or + * loadable again. + */ public ListTag recoveryContents() { return recovery.copy(); } @@ -95,7 +107,10 @@ public void recover(Consumer receiver) { markChanged(); } - /** Explicitly discards contents, displaced items and plans. Stop owning processors first. */ + /** + * Explicitly discards contents, displaced items and plans. Stop owning + * processors first. + */ public void clear() { recovery.clear(); for (int slot = 0; slot < getSlots(); slot++) { @@ -119,16 +134,17 @@ public CompoundTag serializeNBT(HolderLookup.Provider provider) { } @Override - public void deserializeNBT(HolderLookup.Provider provider, CompoundTag tag) { - deserializeNBT(provider, tag, java.util.function.IntUnaryOperator.identity()); + public void deserializeNBT(HolderLookup.Provider provider, + CompoundTag tag) { + deserializeNBT(provider, tag, + java.util.function.IntUnaryOperator.identity()); } /** - * Explicit index migration for pre-identity saves. The host selects this by its save version. + * Explicit index migration for pre-identity saves. The host selects this by + * its save version. */ - public void deserializeNBT( - HolderLookup.Provider provider, - CompoundTag tag, + public void deserializeNBT(HolderLookup.Provider provider, CompoundTag tag, java.util.function.IntUnaryOperator legacySlots) { var entries = tag.getList("Items", Tag.TAG_COMPOUND).copy(); entries.addAll(tag.getList("Recovery", Tag.TAG_COMPOUND).copy()); @@ -152,18 +168,16 @@ public void deserializeNBT( entry.putString("SlotId", slotIds[slot]); } var stack = ItemStack.parseOptional(provider, entry); - if (stack.isEmpty() - || !inRange(slot) - || (!stacks.get(slot).isEmpty() - && !ItemStack.isSameItemSameComponents(stacks.get(slot), stack))) { + if (stack.isEmpty() || !inRange(slot) + || (!stacks.get(slot).isEmpty() && !ItemStack + .isSameItemSameComponents(stacks.get(slot), stack))) { if (!entry.isEmpty()) recovery.add(entry.copy()); continue; } int held = stacks.get(slot).getCount(); - int accepted = Math.min( - stack.getCount(), - Math.max(0, Math.min(getSlotLimit(slot), stack.getMaxStackSize()) - held)); + int accepted = Math.min(stack.getCount(), Math.max(0, + Math.min(getSlotLimit(slot), stack.getMaxStackSize()) - held)); stacks.set(slot, stack.copyWithCount(held + accepted)); if (accepted < stack.getCount()) { var extra = entry.copy(); @@ -180,15 +194,18 @@ public long revision() { return revision + reservationPlans().revision(); } - /** Reports an in-place change made by a menu, including persistence and recipe invalidation. */ + /** + * Reports an in-place change made by a menu, including persistence and + * recipe invalidation. + */ public void markChanged() { revision++; onChange.run(); } /** - * An empty inventory of {@code size} slots, all {@link Role#STORAGE} with no filter and a - * full-stack limit. + * An empty inventory of {@code size} slots, all {@link Role#STORAGE} with + * no filter and a full-stack limit. */ @SuppressWarnings("unchecked") public ItemStorage(int size) { @@ -204,7 +221,9 @@ public ItemStorage(int size) { Arrays.fill(insertionRules, (Predicate) stack -> true); } - /** Runs after every change, typically the block entity's {@code setChanged}. */ + /** + * Runs after every change, typically the block entity's {@code setChanged}. + */ public ItemStorage onChange(Runnable onChange) { this.onChange = onChange; return this; @@ -223,7 +242,10 @@ public ItemStorage role(Role role, int... slots) { return this; } - /** Only stacks passing {@code filter} may be inserted into these slots, by anyone. */ + /** + * Only stacks passing {@code filter} may be inserted into these slots, by + * anyone. + */ public ItemStorage filter(Predicate filter, int... slots) { revision++; for (int slot : slots) @@ -231,7 +253,10 @@ public ItemStorage filter(Predicate filter, int... slots) { return this; } - /** At most {@code limit} items in each of these slots (and never more than a full stack). */ + /** + * At most {@code limit} items in each of these slots (and never more than a + * full stack). + */ public ItemStorage limit(int limit, int... slots) { revision++; for (int slot : slots) @@ -245,8 +270,8 @@ public Role role(int slot) { } /** - * Inserts the machine's own product, ignoring roles and filters; plans still apply. Returns - * what did not fit. + * Inserts the machine's own product, ignoring roles and filters; plans + * still apply. Returns what did not fit. */ public ItemStack insertResult(int slot, ItemStack stack, boolean simulate) { bypass = true; @@ -263,8 +288,9 @@ public boolean fits(int slot, ItemStack stack) { } /** - * Keeps the size given to the constructor, so loading a save written with a different slot - * count cannot desynchronize the per-slot settings; displaced stacks are retained in recovery. + * Keeps the size given to the constructor, so loading a save written with a + * different slot count cannot desynchronize the per-slot settings; + * displaced stacks are retained in recovery. */ @Override public void setSize(int size) { @@ -273,10 +299,8 @@ public void setSize(int size) { @Override public boolean isItemValid(int slot, ItemStack stack) { - boolean allowed = bypass - || (roles[slot] != Role.OUTPUT - && filters[slot].test(stack) - && insertionRules[slot].test(stack)); + boolean allowed = bypass || (roles[slot] != Role.OUTPUT + && filters[slot].test(stack) && insertionRules[slot].test(stack)); return allowed && super.isItemValid(slot, stack); } @@ -290,7 +314,10 @@ protected void onContentsChanged(int slot) { markChanged(); } - /** Drops every stack at {@code pos} and empties the storage, e.g. when the block is broken. */ + /** + * Drops every stack at {@code pos} and empties the storage, e.g. when the + * block is broken. + */ public void dropAll(Level level, BlockPos pos) { for (int slot = 0; slot < getSlots(); slot++) { Containers.dropItemStack(level, pos.getX(), pos.getY(), pos.getZ(), @@ -300,27 +327,37 @@ public void dropAll(Level level, BlockPos pos) { markChanged(); } - /** Hands displaced contents to the host before dropping the ordinary slots. */ - public void dropAll(Level level, BlockPos pos, Consumer recoverResources) { + /** + * Hands displaced contents to the host before dropping the ordinary slots. + */ + public void dropAll(Level level, BlockPos pos, + Consumer recoverResources) { recover(recoverResources); dropAll(level, pos); } - /** An automation view with a fixed mode, e.g. {@link IoMode#BOTH} for unsided access. */ + /** + * An automation view with a fixed mode, e.g. {@link IoMode#BOTH} for + * unsided access. + */ public IItemHandler view(IoMode mode) { return view(() -> mode); } /** - * An automation view whose mode is read on every call, so it follows side-configuration changes - * and rotation without invalidating capabilities. Every view shows all slots with the same - * numbering; a slot index out of range is refused rather than thrown. + * An automation view whose mode is read on every call, so it follows + * side-configuration changes and rotation without invalidating + * capabilities. Every view shows all slots with the same numbering; a slot + * index out of range is refused rather than thrown. */ public IItemHandler view(Supplier mode) { return view(slot -> mode.get()); } - /** Per-slot permissions, preserving every slot index and all storage reservations. */ + /** + * Per-slot permissions, preserving every slot index and all storage + * reservations. + */ public IItemHandler view(IntFunction mode) { return new IItemHandler() { @Override @@ -330,19 +367,25 @@ public int getSlots() { @Override public ItemStack getStackInSlot(int slot) { - return inRange(slot) ? ItemStorage.this.getStackInSlot(slot) : ItemStack.EMPTY; + return inRange(slot) + ? ItemStorage.this.getStackInSlot(slot) + : ItemStack.EMPTY; } @Override - public ItemStack insertItem(int slot, ItemStack stack, boolean simulate) { - if (!inRange(slot) || !roles[slot].canInsert() || !mode.apply(slot).canInsert()) + public ItemStack insertItem(int slot, ItemStack stack, + boolean simulate) { + if (!inRange(slot) || !roles[slot].canInsert() + || !mode.apply(slot).canInsert()) return stack; return ItemStorage.this.insertItem(slot, stack, simulate); } @Override - public ItemStack extractItem(int slot, int amount, boolean simulate) { - if (!inRange(slot) || !roles[slot].canExtract() || !mode.apply(slot).canExtract()) + public ItemStack extractItem(int slot, int amount, + boolean simulate) { + if (!inRange(slot) || !roles[slot].canExtract() + || !mode.apply(slot).canExtract()) return ItemStack.EMPTY; return ItemStorage.this.extractItem(slot, amount, simulate); } @@ -354,8 +397,7 @@ public int getSlotLimit(int slot) { @Override public boolean isItemValid(int slot, ItemStack stack) { - return inRange(slot) - && roles[slot].canInsert() + return inRange(slot) && roles[slot].canInsert() && mode.apply(slot).canInsert() && ItemStorage.this.isItemValid(slot, stack); } @@ -368,7 +410,8 @@ private boolean inRange(int slot) { private int checked(int slot) { if (!inRange(slot)) - throw new IndexOutOfBoundsException("Slot " + slot + " not in [0," + getSlots() + ")"); + throw new IndexOutOfBoundsException( + "Slot " + slot + " not in [0," + getSlots() + ")"); return slot; } } diff --git a/src/main/java/zone/moddev/mc/lib/api/capabilities/ItemStore.java b/src/main/java/zone/moddev/mc/lib/api/capabilities/ItemStore.java index 06941154..e371f804 100644 --- a/src/main/java/zone/moddev/mc/lib/api/capabilities/ItemStore.java +++ b/src/main/java/zone/moddev/mc/lib/api/capabilities/ItemStore.java @@ -14,9 +14,15 @@ import net.neoforged.neoforge.items.IItemHandler; import net.neoforged.neoforge.items.IItemHandlerModifiable; -/** Item storage access, including a combined view over independently owned inventories. */ +/** + * Item storage access, including a combined view over independently owned + * inventories. + */ public interface ItemStore extends IItemHandlerModifiable, ReservedSlots { - /** Physical owner and local index, used for reservation and transaction identity. */ + /** + * Physical owner and local index, used for reservation and transaction + * identity. + */ ItemStorage storage(int slot); int localSlot(int slot); @@ -37,8 +43,8 @@ public interface ItemStore extends IItemHandlerModifiable, ReservedSlots { CompoundTag serializeNBT(HolderLookup.Provider registries); - void deserializeNBT( - HolderLookup.Provider registries, CompoundTag tag, IntUnaryOperator legacySlots); + void deserializeNBT(HolderLookup.Provider registries, CompoundTag tag, + IntUnaryOperator legacySlots); void dropAll(Level level, BlockPos pos, Consumer recovery); diff --git a/src/main/java/zone/moddev/mc/lib/api/capabilities/MachineCapabilities.java b/src/main/java/zone/moddev/mc/lib/api/capabilities/MachineCapabilities.java index 27e900d7..1b6a468f 100644 --- a/src/main/java/zone/moddev/mc/lib/api/capabilities/MachineCapabilities.java +++ b/src/main/java/zone/moddev/mc/lib/api/capabilities/MachineCapabilities.java @@ -11,86 +11,102 @@ import net.neoforged.neoforge.capabilities.RegisterCapabilitiesEvent; /** - * Registers a block entity's {@link ItemStorage}, {@link FluidStorage} or {@link EnergyBuffer} as - * its item, fluid or energy capability, from the mod's {@link RegisterCapabilitiesEvent} handler: + * Registers a block entity's {@link ItemStorage}, {@link FluidStorage} or + * {@link EnergyBuffer} as its item, fluid or energy capability, from the mod's + * {@link RegisterCapabilitiesEvent} handler: * *

{@code
  * MachineCapabilities.items(event, CRUSHER.get(), CrusherEntity::items,
- *     (crusher, side) -> crusher.itemSides().mode(crusher.getBlockState(), side));
+ *     (crusher, side) -> crusher.itemSides().mode(crusher.getBlockState(),
+ *         side));
  * MachineCapabilities.energy(event, CRUSHER.get(), CrusherEntity::energy);
  * }
* - *

Each face gets a view of the same storage whose mode is read on every call, so neither - * side-configuration changes nor rotation need {@code invalidateCapabilities}. A null side (unsided - * access, e.g. information mods) gets {@link IoMode#BOTH}. A getter returning null means the block + *

+ * Each face gets a view of the same storage whose mode is read on every call, + * so neither side-configuration changes nor rotation need + * {@code invalidateCapabilities}. A null side (unsided access, e.g. information + * mods) gets {@link IoMode#BOTH}. A getter returning null means the block * entity has no such storage. */ public final class MachineCapabilities { - private MachineCapabilities() {} + private MachineCapabilities() { + } - /** Item capability with a per-side mode. */ - public static void items( - RegisterCapabilitiesEvent event, - BlockEntityType type, - Function storage, - BiFunction sides) { - event.registerBlockEntity( - Capabilities.ItemHandler.BLOCK, - type, - (be, side) -> { - ItemStorage items = storage.apply(be); - if (items == null) return null; - return side == null ? items.view(IoMode.BOTH) : items.view(() -> sides.apply(be, side)); - }); - } + /** Item capability with a per-side mode. */ + public static void items( + RegisterCapabilitiesEvent event, BlockEntityType type, + Function storage, + BiFunction sides) { + event.registerBlockEntity(Capabilities.ItemHandler.BLOCK, type, + (be, side) -> { + ItemStorage items = storage.apply(be); + if (items == null) + return null; + return side == null + ? items.view(IoMode.BOTH) + : items.view(() -> sides.apply(be, side)); + }); + } - /** Item capability, every side {@link IoMode#BOTH} (slot roles still apply). */ - public static void items( - RegisterCapabilitiesEvent event, BlockEntityType type, Function storage) { - items(event, type, storage, (be, side) -> IoMode.BOTH); - } + /** + * Item capability, every side {@link IoMode#BOTH} (slot roles still apply). + */ + public static void items( + RegisterCapabilitiesEvent event, BlockEntityType type, + Function storage) { + items(event, type, storage, (be, side) -> IoMode.BOTH); + } - /** Fluid capability with a per-side mode. */ - public static void fluids( - RegisterCapabilitiesEvent event, - BlockEntityType type, - Function storage, - BiFunction sides) { - event.registerBlockEntity( - Capabilities.FluidHandler.BLOCK, - type, - (be, side) -> { - FluidStorage fluids = storage.apply(be); - if (fluids == null) return null; - return side == null ? fluids.view(IoMode.BOTH) : fluids.view(() -> sides.apply(be, side)); - }); - } + /** Fluid capability with a per-side mode. */ + public static void fluids( + RegisterCapabilitiesEvent event, BlockEntityType type, + Function storage, + BiFunction sides) { + event.registerBlockEntity(Capabilities.FluidHandler.BLOCK, type, + (be, side) -> { + FluidStorage fluids = storage.apply(be); + if (fluids == null) + return null; + return side == null + ? fluids.view(IoMode.BOTH) + : fluids.view(() -> sides.apply(be, side)); + }); + } - /** Fluid capability, every side {@link IoMode#BOTH} (tank roles still apply). */ - public static void fluids( - RegisterCapabilitiesEvent event, BlockEntityType type, Function storage) { - fluids(event, type, storage, (be, side) -> IoMode.BOTH); - } + /** + * Fluid capability, every side {@link IoMode#BOTH} (tank roles still + * apply). + */ + public static void fluids( + RegisterCapabilitiesEvent event, BlockEntityType type, + Function storage) { + fluids(event, type, storage, (be, side) -> IoMode.BOTH); + } - /** Energy capability with a per-side mode. */ - public static void energy( - RegisterCapabilitiesEvent event, - BlockEntityType type, - Function storage, - BiFunction sides) { - event.registerBlockEntity( - Capabilities.EnergyStorage.BLOCK, - type, - (be, side) -> { - EnergyBuffer energy = storage.apply(be); - if (energy == null) return null; - return side == null ? energy.view(IoMode.BOTH) : energy.view(() -> sides.apply(be, side)); - }); - } + /** Energy capability with a per-side mode. */ + public static void energy( + RegisterCapabilitiesEvent event, BlockEntityType type, + Function storage, + BiFunction sides) { + event.registerBlockEntity(Capabilities.EnergyStorage.BLOCK, type, + (be, side) -> { + EnergyBuffer energy = storage.apply(be); + if (energy == null) + return null; + return side == null + ? energy.view(IoMode.BOTH) + : energy.view(() -> sides.apply(be, side)); + }); + } - /** Energy capability, every side {@link IoMode#BOTH} (transfer rates still apply). */ - public static void energy( - RegisterCapabilitiesEvent event, BlockEntityType type, Function storage) { - energy(event, type, storage, (be, side) -> IoMode.BOTH); - } + /** + * Energy capability, every side {@link IoMode#BOTH} (transfer rates still + * apply). + */ + public static void energy( + RegisterCapabilitiesEvent event, BlockEntityType type, + Function storage) { + energy(event, type, storage, (be, side) -> IoMode.BOTH); + } } diff --git a/src/main/java/zone/moddev/mc/lib/api/capabilities/PlannedItemHandler.java b/src/main/java/zone/moddev/mc/lib/api/capabilities/PlannedItemHandler.java index 84f343e1..0312e2b7 100644 --- a/src/main/java/zone/moddev/mc/lib/api/capabilities/PlannedItemHandler.java +++ b/src/main/java/zone/moddev/mc/lib/api/capabilities/PlannedItemHandler.java @@ -7,75 +7,88 @@ import zone.moddev.mc.lib.machine.inventory.ReservationPlans; import zone.moddev.mc.lib.machine.inventory.SlotPlan; -/** Reservation enforcement shared by authoritative storages and client menu prediction. */ -public class PlannedItemHandler extends ItemStackHandler implements ReservedSlots { - private final ReservationPlans plans; - - public PlannedItemHandler(int size) { - super(size); - plans = new ReservationPlans<>(size, SlotPlan.NONE, SlotPlan::plus, SlotPlan::minus, - (slot, plan) -> plan.legal(getStackInSlot(slot), getSlotLimit(slot))); - } - - /** The same plan table is used by all groups over this storage, including rollback keys. */ - public final ReservationPlans reservationPlans() { - return plans; - } - - @Override - public SlotPlan plan(int slot) { - return plans.get(slot); - } - - /** Replaces a plan for synchronization or restoring state. */ - public void setPlan(int slot, SlotPlan plan) { - plans.set(slot, plan); - } - - public boolean reserve(int slot, SlotPlan plan) { - return plans.reserve(slot, plan); - } - - public void release(int slot, SlotPlan plan) { - plans.release(slot, plan); - } - - @Override - public boolean hasPlan(int slot) { - return slot >= 0 && slot < getSlots() && !plan(slot).isNone(); - } - - @Override - public int freeToTake(int slot) { - return plan(slot).free(getStackInSlot(slot)); - } - - @Override - public int maxHeld(int slot, ItemStack stack) { - if (!hasPlan(slot)) return Math.min(getSlotLimit(slot), stack.getMaxStackSize()); - var held = getStackInSlot(slot); - return held.getCount() + plan(slot).room(held, stack, getSlotLimit(slot)); - } - - @Override - public boolean isItemValid(int slot, ItemStack stack) { - return !hasPlan(slot) || plan(slot).accepts(getStackInSlot(slot), stack); - } - - @Override - public ItemStack extractItem(int slot, int amount, boolean simulate) { - validateSlotIndex(slot); - return super.extractItem(slot, Math.min(amount, freeToTake(slot)), simulate); - } - - @Override - protected int getStackLimit(int slot, ItemStack stack) { - return maxHeld(slot, stack); - } - - @Override - public void setSize(int size) { - // ItemStackHandler calls this from its constructor, before the plan table exists. - super.setSize(plans == null ? size : getSlots()); - } +/** + * Reservation enforcement shared by authoritative storages and client menu + * prediction. + */ +public class PlannedItemHandler extends ItemStackHandler + implements ReservedSlots { + private final ReservationPlans plans; + + public PlannedItemHandler(int size) { + super(size); + plans = new ReservationPlans<>(size, SlotPlan.NONE, SlotPlan::plus, + SlotPlan::minus, (slot, plan) -> plan.legal(getStackInSlot(slot), + getSlotLimit(slot))); + } + + /** + * The same plan table is used by all groups over this storage, including + * rollback keys. + */ + public final ReservationPlans reservationPlans() { + return plans; + } + + @Override + public SlotPlan plan(int slot) { + return plans.get(slot); + } + + /** Replaces a plan for synchronization or restoring state. */ + public void setPlan(int slot, SlotPlan plan) { + plans.set(slot, plan); + } + + public boolean reserve(int slot, SlotPlan plan) { + return plans.reserve(slot, plan); + } + + public void release(int slot, SlotPlan plan) { + plans.release(slot, plan); + } + + @Override + public boolean hasPlan(int slot) { + return slot >= 0 && slot < getSlots() && !plan(slot).isNone(); + } + + @Override + public int freeToTake(int slot) { + return plan(slot).free(getStackInSlot(slot)); + } + + @Override + public int maxHeld(int slot, ItemStack stack) { + if (!hasPlan(slot)) + return Math.min(getSlotLimit(slot), stack.getMaxStackSize()); + var held = getStackInSlot(slot); + return held.getCount() + + plan(slot).room(held, stack, getSlotLimit(slot)); + } + + @Override + public boolean isItemValid(int slot, ItemStack stack) { + return !hasPlan(slot) + || plan(slot).accepts(getStackInSlot(slot), stack); + } + + @Override + public ItemStack extractItem(int slot, int amount, boolean simulate) { + validateSlotIndex(slot); + return super.extractItem(slot, Math.min(amount, freeToTake(slot)), + simulate); + } + + @Override + protected int getStackLimit(int slot, ItemStack stack) { + return maxHeld(slot, stack); + } + + @Override + public void setSize(int size) { + // ItemStackHandler calls this from its constructor, before the plan + // table exists. + super.setSize(plans == null ? size : getSlots()); + } } diff --git a/src/main/java/zone/moddev/mc/lib/api/capabilities/RelativeFace.java b/src/main/java/zone/moddev/mc/lib/api/capabilities/RelativeFace.java index 1fa07c0a..2e78af8b 100644 --- a/src/main/java/zone/moddev/mc/lib/api/capabilities/RelativeFace.java +++ b/src/main/java/zone/moddev/mc/lib/api/capabilities/RelativeFace.java @@ -8,72 +8,86 @@ import net.minecraft.world.level.block.state.properties.BlockStateProperties; /** - * A face named relative to a machine's front. Left and right are as seen by a player looking at the - * front. For a machine facing up or down, the top is the north side. + * A face named relative to a machine's front. Left and right are as seen by a + * player looking at the front. For a machine facing up or down, the top is the + * north side. */ public enum RelativeFace implements StringRepresentable { - /** The side the machine faces. */ - FRONT, - /** The side opposite the front. */ - BACK, - /** The left side, as seen looking at the front. */ - LEFT, - /** The right side, as seen looking at the front. */ - RIGHT, - /** The top; north for a machine facing up or down. */ - TOP, - /** The bottom; south for a machine facing up or down. */ - BOTTOM; + /** The side the machine faces. */ + FRONT, + /** The side opposite the front. */ + BACK, + /** The left side, as seen looking at the front. */ + LEFT, + /** The right side, as seen looking at the front. */ + RIGHT, + /** The top; north for a machine facing up or down. */ + TOP, + /** The bottom; south for a machine facing up or down. */ + BOTTOM; - /** Reads and writes the lower-case name, e.g. {@code "front"}. */ - public static final Codec CODEC = - StringRepresentable.fromEnum(RelativeFace::values); + /** Reads and writes the lower-case name, e.g. {@code "front"}. */ + public static final Codec CODEC = StringRepresentable + .fromEnum(RelativeFace::values); - /** The world side this face is on for a machine whose front points {@code facing}. */ - public Direction toWorld(Direction facing) { - if (facing.getAxis().isVertical()) { - // Seen from the front: north is up; looking down (facing up) east is on the right. - Direction left = facing == Direction.UP ? Direction.WEST : Direction.EAST; - return switch (this) { - case FRONT -> facing; - case BACK -> facing.getOpposite(); - case LEFT -> left; - case RIGHT -> left.getOpposite(); - case TOP -> Direction.NORTH; - case BOTTOM -> Direction.SOUTH; - }; + /** + * The world side this face is on for a machine whose front points + * {@code facing}. + */ + public Direction toWorld(Direction facing) { + if (facing.getAxis().isVertical()) { + // Seen from the front: north is up; looking down (facing up) east + // is on the right. + Direction left = facing == Direction.UP + ? Direction.WEST + : Direction.EAST; + return switch (this) { + case FRONT -> facing; + case BACK -> facing.getOpposite(); + case LEFT -> left; + case RIGHT -> left.getOpposite(); + case TOP -> Direction.NORTH; + case BOTTOM -> Direction.SOUTH; + }; + } + return switch (this) { + case FRONT -> facing; + case BACK -> facing.getOpposite(); + case LEFT -> facing.getClockWise(); + case RIGHT -> facing.getCounterClockWise(); + case TOP -> Direction.UP; + case BOTTOM -> Direction.DOWN; + }; } - return switch (this) { - case FRONT -> facing; - case BACK -> facing.getOpposite(); - case LEFT -> facing.getClockWise(); - case RIGHT -> facing.getCounterClockWise(); - case TOP -> Direction.UP; - case BOTTOM -> Direction.DOWN; - }; - } - /** The face on world side {@code side} of a machine whose front points {@code facing}. */ - public static RelativeFace of(Direction facing, Direction side) { - for (RelativeFace face : values()) if (face.toWorld(facing) == side) return face; - throw new IllegalStateException( - "No face for " + side); // unreachable: the mapping is a bijection - } + /** + * The face on world side {@code side} of a machine whose front points + * {@code facing}. + */ + public static RelativeFace of(Direction facing, Direction side) { + for (RelativeFace face : values()) + if (face.toWorld(facing) == side) + return face; + throw new IllegalStateException("No face for " + side); // unreachable: + // the mapping + // is a + // bijection + } - /** - * The front of a block state: its {@code facing} or {@code horizontal_facing} property, or north - * when it has neither. - */ - public static Direction facing(BlockState state) { - if (state.hasProperty(BlockStateProperties.FACING)) - return state.getValue(BlockStateProperties.FACING); - if (state.hasProperty(BlockStateProperties.HORIZONTAL_FACING)) - return state.getValue(BlockStateProperties.HORIZONTAL_FACING); - return Direction.NORTH; - } + /** + * The front of a block state: its {@code facing} or + * {@code horizontal_facing} property, or north when it has neither. + */ + public static Direction facing(BlockState state) { + if (state.hasProperty(BlockStateProperties.FACING)) + return state.getValue(BlockStateProperties.FACING); + if (state.hasProperty(BlockStateProperties.HORIZONTAL_FACING)) + return state.getValue(BlockStateProperties.HORIZONTAL_FACING); + return Direction.NORTH; + } - @Override - public String getSerializedName() { - return name().toLowerCase(java.util.Locale.ROOT); - } + @Override + public String getSerializedName() { + return name().toLowerCase(java.util.Locale.ROOT); + } } diff --git a/src/main/java/zone/moddev/mc/lib/api/capabilities/ReservationReceiver.java b/src/main/java/zone/moddev/mc/lib/api/capabilities/ReservationReceiver.java index 85fcba5b..e8486bbe 100644 --- a/src/main/java/zone/moddev/mc/lib/api/capabilities/ReservationReceiver.java +++ b/src/main/java/zone/moddev/mc/lib/api/capabilities/ReservationReceiver.java @@ -2,7 +2,10 @@ import zone.moddev.mc.lib.machine.inventory.SlotPlan; -/** Optional storage contract for applying authoritative reservation snapshots to a menu mirror. */ +/** + * Optional storage contract for applying authoritative reservation snapshots to + * a menu mirror. + */ public interface ReservationReceiver { void setPlan(int slot, SlotPlan plan); } diff --git a/src/main/java/zone/moddev/mc/lib/api/capabilities/ReservedSlots.java b/src/main/java/zone/moddev/mc/lib/api/capabilities/ReservedSlots.java index 02a6ae7f..b326a898 100644 --- a/src/main/java/zone/moddev/mc/lib/api/capabilities/ReservedSlots.java +++ b/src/main/java/zone/moddev/mc/lib/api/capabilities/ReservedSlots.java @@ -4,17 +4,32 @@ import zone.moddev.mc.lib.machine.inventory.SlotPlan; -/** What a menu needs to know about a slot whose contents a machine has made plans for. */ +/** + * What a menu needs to know about a slot whose contents a machine has made + * plans for. + */ public interface ReservedSlots { - /** The immutable reservation snapshot sent to menus, including {@link SlotPlan#NONE}. */ - SlotPlan plan(int slot); + /** + * The immutable reservation snapshot sent to menus, including + * {@link SlotPlan#NONE}. + */ + SlotPlan plan(int slot); - /** Whether the machine has planned something for this slot; false if it does not exist. */ - boolean hasPlan(int slot); + /** + * Whether the machine has planned something for this slot; false if it does + * not exist. + */ + boolean hasPlan(int slot); - /** How many items a player may take from this slot now without breaking the plan. */ - int freeToTake(int slot); + /** + * How many items a player may take from this slot now without breaking the + * plan. + */ + int freeToTake(int slot); - /** The total this slot may hold of {@code stack}, as {@code Slot.getMaxStackSize(stack)}. */ - int maxHeld(int slot, ItemStack stack); + /** + * The total this slot may hold of {@code stack}, as + * {@code Slot.getMaxStackSize(stack)}. + */ + int maxHeld(int slot, ItemStack stack); } diff --git a/src/main/java/zone/moddev/mc/lib/api/capabilities/Role.java b/src/main/java/zone/moddev/mc/lib/api/capabilities/Role.java index 61f8b9c5..cec5d084 100644 --- a/src/main/java/zone/moddev/mc/lib/api/capabilities/Role.java +++ b/src/main/java/zone/moddev/mc/lib/api/capabilities/Role.java @@ -1,34 +1,35 @@ package zone.moddev.mc.lib.api.capabilities; /** - * What automation may do with one slot or tank, whatever the face allows. The owning block entity - * is never limited by roles; it works on the storage directly. + * What automation may do with one slot or tank, whatever the face allows. The + * owning block entity is never limited by roles; it works on the storage + * directly. */ public enum Role { - /** Automation may insert and extract. */ - STORAGE(true, true), - /** Automation may insert only, e.g. a furnace's ingredient slot. */ - INPUT(true, false), - /** Automation may extract only; players cannot place into it either. */ - OUTPUT(false, true), - /** Only players, through the screen, e.g. upgrade slots. */ - MANUAL(false, false); + /** Automation may insert and extract. */ + STORAGE(true, true), + /** Automation may insert only, e.g. a furnace's ingredient slot. */ + INPUT(true, false), + /** Automation may extract only; players cannot place into it either. */ + OUTPUT(false, true), + /** Only players, through the screen, e.g. upgrade slots. */ + MANUAL(false, false); - private final boolean insert; - private final boolean extract; + private final boolean insert; + private final boolean extract; - Role(boolean insert, boolean extract) { - this.insert = insert; - this.extract = extract; - } + Role(boolean insert, boolean extract) { + this.insert = insert; + this.extract = extract; + } - /** Whether automation may insert into a slot or tank with this role. */ - public boolean canInsert() { - return insert; - } + /** Whether automation may insert into a slot or tank with this role. */ + public boolean canInsert() { + return insert; + } - /** Whether automation may extract from a slot or tank with this role. */ - public boolean canExtract() { - return extract; - } + /** Whether automation may extract from a slot or tank with this role. */ + public boolean canExtract() { + return extract; + } } diff --git a/src/main/java/zone/moddev/mc/lib/api/capabilities/SideConfig.java b/src/main/java/zone/moddev/mc/lib/api/capabilities/SideConfig.java index 1ef8f1a5..29efcb52 100644 --- a/src/main/java/zone/moddev/mc/lib/api/capabilities/SideConfig.java +++ b/src/main/java/zone/moddev/mc/lib/api/capabilities/SideConfig.java @@ -9,68 +9,89 @@ import net.minecraft.world.level.block.state.BlockState; /** - * The per-face {@link IoMode}s of one resource on one machine, stored relative to its front so they - * follow the machine when it is rotated. Changing a mode never renumbers slots or tanks: every face - * sees the same storage, and the mode only allows or refuses operations. + * The per-face {@link IoMode}s of one resource on one machine, stored relative + * to its front so they follow the machine when it is rotated. Changing a mode + * never renumbers slots or tanks: every face sees the same storage, and the + * mode only allows or refuses operations. */ public final class SideConfig { - /** - * A map from {@link RelativeFace} to {@link IoMode}; faces left out are {@link IoMode#DISABLED}. - */ - public static final Codec CODEC = - Codec.unboundedMap(RelativeFace.CODEC, IoMode.CODEC) - .xmap(SideConfig::new, config -> config.modes); + /** + * A map from {@link RelativeFace} to {@link IoMode}; faces left out are + * {@link IoMode#DISABLED}. + */ + public static final Codec CODEC = Codec + .unboundedMap(RelativeFace.CODEC, IoMode.CODEC) + .xmap(SideConfig::new, config -> config.modes); - private final EnumMap modes = new EnumMap<>(RelativeFace.class); - private Runnable onChange = () -> {}; + private final EnumMap modes = new EnumMap<>( + RelativeFace.class); + private Runnable onChange = () -> { + }; - /** Every face set to {@code mode}. */ - public SideConfig(IoMode mode) { - for (RelativeFace face : RelativeFace.values()) modes.put(face, mode); - } + /** Every face set to {@code mode}. */ + public SideConfig(IoMode mode) { + for (RelativeFace face : RelativeFace.values()) + modes.put(face, mode); + } - /** The given faces; any face left out is {@link IoMode#DISABLED}. */ - public SideConfig(Map modes) { - this(IoMode.DISABLED); - this.modes.putAll(modes); - } + /** The given faces; any face left out is {@link IoMode#DISABLED}. */ + public SideConfig(Map modes) { + this(IoMode.DISABLED); + this.modes.putAll(modes); + } - /** Runs after every change, typically the block entity's {@code setChanged}. */ - public SideConfig onChange(Runnable onChange) { - this.onChange = onChange; - return this; - } + /** + * Runs after every change, typically the block entity's {@code setChanged}. + */ + public SideConfig onChange(Runnable onChange) { + this.onChange = onChange; + return this; + } - /** The mode of {@code face}. */ - public IoMode get(RelativeFace face) { - return modes.get(face); - } + /** The mode of {@code face}. */ + public IoMode get(RelativeFace face) { + return modes.get(face); + } - /** Sets the mode of {@code face}, running the change callback if it changed. */ - public SideConfig set(RelativeFace face, IoMode mode) { - if (modes.put(face, mode) != mode) onChange.run(); - return this; - } + /** + * Sets the mode of {@code face}, running the change callback if it changed. + */ + public SideConfig set(RelativeFace face, IoMode mode) { + if (modes.put(face, mode) != mode) + onChange.run(); + return this; + } - /** Advances one face to its next mode, as a side-configuration button does. */ - public IoMode cycle(RelativeFace face) { - IoMode next = get(face).next(); - set(face, next); - return next; - } + /** + * Advances one face to its next mode, as a side-configuration button does. + */ + public IoMode cycle(RelativeFace face) { + IoMode next = get(face).next(); + set(face, next); + return next; + } - /** The mode on world side {@code side}; a null side (internal access) is {@link IoMode#BOTH}. */ - public IoMode mode(Direction facing, Direction side) { - return side == null ? IoMode.BOTH : get(RelativeFace.of(facing, side)); - } + /** + * The mode on world side {@code side}; a null side (internal access) is + * {@link IoMode#BOTH}. + */ + public IoMode mode(Direction facing, Direction side) { + return side == null ? IoMode.BOTH : get(RelativeFace.of(facing, side)); + } - /** As {@link #mode(Direction, Direction)}, taking the front from the block state. */ - public IoMode mode(BlockState state, Direction side) { - return mode(RelativeFace.facing(state), side); - } + /** + * As {@link #mode(Direction, Direction)}, taking the front from the block + * state. + */ + public IoMode mode(BlockState state, Direction side) { + return mode(RelativeFace.facing(state), side); + } - /** Replaces every mode with {@code other}'s, e.g. when loading; does not run the callback. */ - public void copyFrom(SideConfig other) { - modes.putAll(other.modes); - } + /** + * Replaces every mode with {@code other}'s, e.g. when loading; does not run + * the callback. + */ + public void copyFrom(SideConfig other) { + modes.putAll(other.modes); + } } diff --git a/src/main/java/zone/moddev/mc/lib/api/capabilities/StorageRecovery.java b/src/main/java/zone/moddev/mc/lib/api/capabilities/StorageRecovery.java index f478ba5f..3df3eff7 100644 --- a/src/main/java/zone/moddev/mc/lib/api/capabilities/StorageRecovery.java +++ b/src/main/java/zone/moddev/mc/lib/api/capabilities/StorageRecovery.java @@ -5,25 +5,30 @@ import net.minecraft.nbt.CompoundTag; import net.minecraft.nbt.ListTag; -/** Raw displaced contents survive missing registrations and can be handed to a recovery item. */ +/** + * Raw displaced contents survive missing registrations and can be handed to a + * recovery item. + */ public final class StorageRecovery { - private StorageRecovery() {} + private StorageRecovery() { + } - public static void handoff( - ListTag entries, String type, String key, Consumer receiver) { - if (entries.isEmpty()) return; - var resources = new ListTag(); - for (var entry : entries) { - var data = new CompoundTag(); - data.put(key, entry.copy()); - var resource = new CompoundTag(); - resource.putString("type", type); - resource.put("data", data); - resources.add(resource); + public static void handoff(ListTag entries, String type, String key, + Consumer receiver) { + if (entries.isEmpty()) + return; + var resources = new ListTag(); + for (var entry : entries) { + var data = new CompoundTag(); + data.put(key, entry.copy()); + var resource = new CompoundTag(); + resource.putString("type", type); + resource.put("data", data); + resources.add(resource); + } + var payload = new CompoundTag(); + payload.put("resources", resources); + receiver.accept(payload); + entries.clear(); } - var payload = new CompoundTag(); - payload.put("resources", resources); - receiver.accept(payload); - entries.clear(); - } } diff --git a/src/main/java/zone/moddev/mc/lib/api/capabilities/Transfer.java b/src/main/java/zone/moddev/mc/lib/api/capabilities/Transfer.java index 6f14a856..8c890e5a 100644 --- a/src/main/java/zone/moddev/mc/lib/api/capabilities/Transfer.java +++ b/src/main/java/zone/moddev/mc/lib/api/capabilities/Transfer.java @@ -14,96 +14,124 @@ import net.neoforged.neoforge.items.IItemHandler; /** - * Moving items, fluids and energy between handlers, and finding a neighbour's handler. For a - * neighbour checked every tick, keep a NeoForge {@code BlockCapabilityCache} instead of calling - * {@link #neighbour} each time. + * Moving items, fluids and energy between handlers, and finding a neighbour's + * handler. For a neighbour checked every tick, keep a NeoForge + * {@code BlockCapabilityCache} instead of calling {@link #neighbour} each time. */ public final class Transfer { - private Transfer() {} + private Transfer() { + } - /** - * The capability of the block next to {@code pos} on side {@code side}, asked for on the face - * that touches {@code pos}; null when there is none. - */ - public static T neighbour( - BlockCapability capability, Level level, BlockPos pos, Direction side) { - return level.getCapability(capability, pos.relative(side), side.getOpposite()); - } + /** + * The capability of the block next to {@code pos} on side {@code side}, + * asked for on the face that touches {@code pos}; null when there is none. + */ + public static T neighbour(BlockCapability capability, + Level level, BlockPos pos, Direction side) { + return level.getCapability(capability, pos.relative(side), + side.getOpposite()); + } - /** Moves up to {@code max} items from {@code from} to {@code to}. Returns the number moved. */ - public static int items(IItemHandler from, IItemHandler to, int max) { - int moved = 0; - for (int slot = 0; slot < from.getSlots() && moved < max; slot++) { - ItemStack available = from.extractItem(slot, max - moved, true); - if (available.isEmpty()) continue; - int accepted = available.getCount() - insert(to, available, true).getCount(); - if (accepted <= 0) continue; - ItemStack taken = from.extractItem(slot, accepted, false); - ItemStack rest = insert(to, taken, false); - // A handler that accepted in simulation but not for real: give the rest back. - if (!rest.isEmpty()) from.insertItem(slot, rest, false); - moved += taken.getCount() - rest.getCount(); + /** + * Moves up to {@code max} items from {@code from} to {@code to}. Returns + * the number moved. + */ + public static int items(IItemHandler from, IItemHandler to, int max) { + int moved = 0; + for (int slot = 0; slot < from.getSlots() && moved < max; slot++) { + ItemStack available = from.extractItem(slot, max - moved, true); + if (available.isEmpty()) + continue; + int accepted = available.getCount() + - insert(to, available, true).getCount(); + if (accepted <= 0) + continue; + ItemStack taken = from.extractItem(slot, accepted, false); + ItemStack rest = insert(to, taken, false); + // A handler that accepted in simulation but not for real: give the + // rest back. + if (!rest.isEmpty()) + from.insertItem(slot, rest, false); + moved += taken.getCount() - rest.getCount(); + } + return moved; } - return moved; - } - /** Inserts into any slots of {@code to}, existing stacks first. Returns what did not fit. */ - public static ItemStack insert(IItemHandler to, ItemStack stack, boolean simulate) { - ItemStack rest = stack; - for (int pass = 0; pass < 2 && !rest.isEmpty(); pass++) - for (int slot = 0; slot < to.getSlots() && !rest.isEmpty(); slot++) - if (to.getStackInSlot(slot).isEmpty() == (pass == 1)) - rest = to.insertItem(slot, rest, simulate); - return rest; - } + /** + * Inserts into any slots of {@code to}, existing stacks first. Returns what + * did not fit. + */ + public static ItemStack insert(IItemHandler to, ItemStack stack, + boolean simulate) { + ItemStack rest = stack; + for (int pass = 0; pass < 2 && !rest.isEmpty(); pass++) + for (int slot = 0; slot < to.getSlots() && !rest.isEmpty(); slot++) + if (to.getStackInSlot(slot).isEmpty() == (pass == 1)) + rest = to.insertItem(slot, rest, simulate); + return rest; + } - /** Moves up to {@code max} mB from {@code from} to {@code to}. Returns the amount moved. */ - public static int fluids(IFluidHandler from, IFluidHandler to, int max) { - FluidStack moved = FluidUtil.tryFluidTransfer(to, from, max, true); - return moved.isEmpty() ? 0 : moved.getAmount(); - } + /** + * Moves up to {@code max} mB from {@code from} to {@code to}. Returns the + * amount moved. + */ + public static int fluids(IFluidHandler from, IFluidHandler to, int max) { + FluidStack moved = FluidUtil.tryFluidTransfer(to, from, max, true); + return moved.isEmpty() ? 0 : moved.getAmount(); + } - /** Moves up to {@code max} energy from {@code from} to {@code to}. Returns the amount moved. */ - public static int energy(IEnergyStorage from, IEnergyStorage to, int max) { - int offered = from.extractEnergy(max, true); - if (offered <= 0) return 0; - int accepted = to.receiveEnergy(offered, true); - if (accepted <= 0) return 0; - int taken = from.extractEnergy(accepted, false); - return to.receiveEnergy(taken, false); - } + /** + * Moves up to {@code max} energy from {@code from} to {@code to}. Returns + * the amount moved. + */ + public static int energy(IEnergyStorage from, IEnergyStorage to, int max) { + int offered = from.extractEnergy(max, true); + if (offered <= 0) + return 0; + int accepted = to.receiveEnergy(offered, true); + if (accepted <= 0) + return 0; + int taken = from.extractEnergy(accepted, false); + return to.receiveEnergy(taken, false); + } - /** Pushes up to {@code max} items into the neighbour on {@code side}. */ - public static int pushItems( - Level level, BlockPos pos, Direction side, IItemHandler from, int max) { - IItemHandler to = neighbour(Capabilities.ItemHandler.BLOCK, level, pos, side); - return to == null ? 0 : items(from, to, max); - } + /** Pushes up to {@code max} items into the neighbour on {@code side}. */ + public static int pushItems(Level level, BlockPos pos, Direction side, + IItemHandler from, int max) { + IItemHandler to = neighbour(Capabilities.ItemHandler.BLOCK, level, pos, + side); + return to == null ? 0 : items(from, to, max); + } - /** Pulls up to {@code max} items from the neighbour on {@code side}. */ - public static int pullItems(Level level, BlockPos pos, Direction side, IItemHandler to, int max) { - IItemHandler from = neighbour(Capabilities.ItemHandler.BLOCK, level, pos, side); - return from == null ? 0 : items(from, to, max); - } + /** Pulls up to {@code max} items from the neighbour on {@code side}. */ + public static int pullItems(Level level, BlockPos pos, Direction side, + IItemHandler to, int max) { + IItemHandler from = neighbour(Capabilities.ItemHandler.BLOCK, level, + pos, side); + return from == null ? 0 : items(from, to, max); + } - /** Pushes up to {@code max} mB into the neighbour on {@code side}. */ - public static int pushFluids( - Level level, BlockPos pos, Direction side, IFluidHandler from, int max) { - IFluidHandler to = neighbour(Capabilities.FluidHandler.BLOCK, level, pos, side); - return to == null ? 0 : fluids(from, to, max); - } + /** Pushes up to {@code max} mB into the neighbour on {@code side}. */ + public static int pushFluids(Level level, BlockPos pos, Direction side, + IFluidHandler from, int max) { + IFluidHandler to = neighbour(Capabilities.FluidHandler.BLOCK, level, + pos, side); + return to == null ? 0 : fluids(from, to, max); + } - /** Pulls up to {@code max} mB from the neighbour on {@code side}. */ - public static int pullFluids( - Level level, BlockPos pos, Direction side, IFluidHandler to, int max) { - IFluidHandler from = neighbour(Capabilities.FluidHandler.BLOCK, level, pos, side); - return from == null ? 0 : fluids(from, to, max); - } + /** Pulls up to {@code max} mB from the neighbour on {@code side}. */ + public static int pullFluids(Level level, BlockPos pos, Direction side, + IFluidHandler to, int max) { + IFluidHandler from = neighbour(Capabilities.FluidHandler.BLOCK, level, + pos, side); + return from == null ? 0 : fluids(from, to, max); + } - /** Pushes up to {@code max} energy into the neighbour on {@code side}. */ - public static int pushEnergy( - Level level, BlockPos pos, Direction side, IEnergyStorage from, int max) { - IEnergyStorage to = neighbour(Capabilities.EnergyStorage.BLOCK, level, pos, side); - return to == null ? 0 : energy(from, to, max); - } + /** Pushes up to {@code max} energy into the neighbour on {@code side}. */ + public static int pushEnergy(Level level, BlockPos pos, Direction side, + IEnergyStorage from, int max) { + IEnergyStorage to = neighbour(Capabilities.EnergyStorage.BLOCK, level, + pos, side); + return to == null ? 0 : energy(from, to, max); + } } diff --git a/src/main/java/zone/moddev/mc/lib/api/capabilities/package-info.java b/src/main/java/zone/moddev/mc/lib/api/capabilities/package-info.java index 8c07e578..322fd5b9 100644 --- a/src/main/java/zone/moddev/mc/lib/api/capabilities/package-info.java +++ b/src/main/java/zone/moddev/mc/lib/api/capabilities/package-info.java @@ -1,15 +1,19 @@ /** - * Ready-made item, fluid and energy storage for machine block entities, and the glue that exposes - * it through NeoForge capabilities. + * Ready-made item, fluid and energy storage for machine block entities, and the + * glue that exposes it through NeoForge capabilities. * - *

A block entity owns an {@link zone.moddev.mc.lib.api.capabilities.ItemStorage}, {@link - * zone.moddev.mc.lib.api.capabilities.FluidStorage} or {@link - * zone.moddev.mc.lib.api.capabilities.EnergyBuffer} and works on it directly. Automation sees - * per-face views limited by each slot's {@link zone.moddev.mc.lib.api.capabilities.Role} and each - * face's {@link zone.moddev.mc.lib.api.capabilities.IoMode}, set in a {@link - * zone.moddev.mc.lib.api.capabilities.SideConfig}. {@link - * zone.moddev.mc.lib.api.capabilities.MachineCapabilities} registers the views and {@link - * zone.moddev.mc.lib.api.capabilities.Transfer} moves resources between handlers. See {@code + *

+ * A block entity owns an + * {@link zone.moddev.mc.lib.api.capabilities.ItemStorage}, + * {@link zone.moddev.mc.lib.api.capabilities.FluidStorage} or + * {@link zone.moddev.mc.lib.api.capabilities.EnergyBuffer} and works on it + * directly. Automation sees per-face views limited by each slot's + * {@link zone.moddev.mc.lib.api.capabilities.Role} and each face's + * {@link zone.moddev.mc.lib.api.capabilities.IoMode}, set in a + * {@link zone.moddev.mc.lib.api.capabilities.SideConfig}. + * {@link zone.moddev.mc.lib.api.capabilities.MachineCapabilities} registers the + * views and {@link zone.moddev.mc.lib.api.capabilities.Transfer} moves + * resources between handlers. See {@code * docs/capabilities.md} for a worked example. */ package zone.moddev.mc.lib.api.capabilities; diff --git a/src/main/java/zone/moddev/mc/lib/api/package-info.java b/src/main/java/zone/moddev/mc/lib/api/package-info.java index c415c6f3..c3370338 100644 --- a/src/main/java/zone/moddev/mc/lib/api/package-info.java +++ b/src/main/java/zone/moddev/mc/lib/api/package-info.java @@ -1,21 +1,29 @@ /** - * MMDLib's public extension API: what another mod uses to add content to the material system. + * MMDLib's public extension API: what another mod uses to add content to the + * material system. * *

    - *
  • {@link zone.moddev.mc.lib.api.MaterialExtensions} is the catalog of modules, Java - * implementations, part types and events. Register with {@link - * zone.moddev.mc.lib.api.MaterialExtensions#GLOBAL} during mod construction. - *
  • {@link zone.moddev.mc.lib.api.ItemImplementation} and {@link - * zone.moddev.mc.lib.api.BlockImplementation} are the Java code a data module's {@code - * factory} names; they receive an {@link zone.moddev.mc.lib.api.ItemContext} or {@link - * zone.moddev.mc.lib.api.BlockContext} with everything MMDLib's own content is built from. - *
  • {@link zone.moddev.mc.lib.api.MaterialEvents} holds typed events and their callbacks; - * {@link zone.moddev.mc.lib.api.MaterialEventTypes} lists the built-in events and {@link - * zone.moddev.mc.lib.api.MaterialEventContexts} their context objects. - *
  • {@link zone.moddev.mc.lib.api.MaterialRegistration} publishes loaded definitions to - * MMDLib's definition registries. + *
  • {@link zone.moddev.mc.lib.api.MaterialExtensions} is the catalog of + * modules, Java implementations, part types and events. Register with + * {@link zone.moddev.mc.lib.api.MaterialExtensions#GLOBAL} during mod + * construction. + *
  • {@link zone.moddev.mc.lib.api.ItemImplementation} and + * {@link zone.moddev.mc.lib.api.BlockImplementation} are the Java code a data + * module's {@code + * factory} names; they receive an + * {@link zone.moddev.mc.lib.api.ItemContext} or + * {@link zone.moddev.mc.lib.api.BlockContext} with everything MMDLib's own + * content is built from. + *
  • {@link zone.moddev.mc.lib.api.MaterialEvents} holds typed events and + * their callbacks; {@link zone.moddev.mc.lib.api.MaterialEventTypes} lists the + * built-in events and {@link zone.moddev.mc.lib.api.MaterialEventContexts} + * their context objects. + *
  • {@link zone.moddev.mc.lib.api.MaterialRegistration} publishes loaded + * definitions to MMDLib's definition registries. *
* - *

The guides in {@code docs/} (start with {@code mod-author-guide.md}) explain the data side. + *

+ * The guides in {@code docs/} (start with {@code mod-author-guide.md}) explain + * the data side. */ package zone.moddev.mc.lib.api; diff --git a/src/main/java/zone/moddev/mc/lib/api/redstone/IRedstoneOutput.java b/src/main/java/zone/moddev/mc/lib/api/redstone/IRedstoneOutput.java index bf41396c..150cc768 100644 --- a/src/main/java/zone/moddev/mc/lib/api/redstone/IRedstoneOutput.java +++ b/src/main/java/zone/moddev/mc/lib/api/redstone/IRedstoneOutput.java @@ -3,12 +3,13 @@ import java.util.List; /** - * Optional runtime-part capability: contributes named, independently selectable redstone signals. + * Optional runtime-part capability: contributes named, independently selectable + * redstone signals. */ public interface IRedstoneOutput { - /** - * Stable ordered descriptors, identical on server and client; signal suppliers run only on the - * server. - */ - List redstoneOutputs(); + /** + * Stable ordered descriptors, identical on server and client; signal + * suppliers run only on the server. + */ + List redstoneOutputs(); } diff --git a/src/main/java/zone/moddev/mc/lib/api/redstone/RedstoneControl.java b/src/main/java/zone/moddev/mc/lib/api/redstone/RedstoneControl.java index 8ac811c1..f4af0347 100644 --- a/src/main/java/zone/moddev/mc/lib/api/redstone/RedstoneControl.java +++ b/src/main/java/zone/moddev/mc/lib/api/redstone/RedstoneControl.java @@ -19,7 +19,10 @@ import zone.moddev.mc.lib.api.automation.ResourceGroup; import zone.moddev.mc.lib.api.capabilities.RelativeFace; -/** Persistent per-face signal intervals and outputs discovered from machine parts. */ +/** + * Persistent per-face signal intervals and outputs discovered from machine + * parts. + */ public final class RedstoneControl { public enum Mode { IGNORE, READ, EMIT @@ -29,33 +32,30 @@ public enum Mode { private static final RelativeFace[] FACES = RelativeFace.values(); private final BlockEntity owner; private final List outputs; - private final int[] modes = new int[6], - incoming = new int[6], - outgoing = new int[6], - lower = new int[6], - upper = new int[6]; + private final int[] modes = new int[6], incoming = new int[6], + outgoing = new int[6], lower = new int[6], upper = new int[6]; private final boolean[] outside = new boolean[6], inverted = new boolean[6]; private final String[] sourceIds = new String[6]; - // The catalog is immutable. Resolve saved IDs only when loading or changing a selection. + // The catalog is immutable. Resolve saved IDs only when loading or changing + // a selection. private final int[] sourceIndexes = new int[6]; private final int[] sampledLevels = new int[6]; private long revision; - // The first tick repairs only adjacent dust, without notifying unrelated neighbours. + // The first tick repairs only adjacent dust, without notifying unrelated + // neighbours. private int connections = -1; private boolean paused, legacyAny; - public RedstoneControl( - BlockEntity owner, - List> groups, + public RedstoneControl(BlockEntity owner, List> groups, ToDoubleFunction> fullness) { - this( - owner, - groups.stream() - .map(group -> RedstoneOutput.resource(group, () -> fullness.applyAsDouble(group))) - .toList()); + this(owner, groups.stream().map(group -> RedstoneOutput.resource(group, + () -> fullness.applyAsDouble(group))).toList()); } - /** Discover output-capable runtime parts; nonparticipating parts contribute nothing. */ + /** + * Discover output-capable runtime parts; nonparticipating parts contribute + * nothing. + */ public RedstoneControl(BlockEntity owner, Collection parts) { this.owner = Objects.requireNonNull(owner); var found = new LinkedHashMap(); @@ -170,11 +170,15 @@ private void changed() { } /** Both endpoints belong to Between; Outside is its exact complement. */ - public static boolean matches(int signal, int low, int high, boolean outside) { + public static boolean matches(int signal, int low, int high, + boolean outside) { return (signal >= low && signal <= high) != outside; } - /** Convenience for hosts without stateful features; otherwise split around feature ticks. */ + /** + * Convenience for hosts without stateful features; otherwise split around + * feature ticks. + */ public void tick() { readInputs(); updateOutputs(); @@ -195,14 +199,16 @@ public void readInputs() { : 0; powered |= incoming[face] > 0; if (mode(face) == Mode.READ) - paused |= matches(incoming[face], lower[face], upper[face], outside[face]); + paused |= matches(incoming[face], lower[face], upper[face], + outside[face]); } if (legacyAny) paused = !powered; } /** - * Evaluate selected suppliers once each after feature/transfer ticks, then notify neighbours. + * Evaluate selected suppliers once each after feature/transfer ticks, then + * notify neighbours. */ public void updateOutputs() { var level = owner.getLevel(); @@ -216,10 +222,11 @@ public void updateOutputs() { if (connects(face)) newConnections |= 1 << world(face).ordinal(); if (mode(face) == Mode.EMIT && index >= 0) { - // At most six faces: reuse an earlier face's raw sample without a per-tick map. + // At most six faces: reuse an earlier face's raw sample without + // a per-tick map. int earlier = 0; - while (earlier < face - && (mode(earlier) != Mode.EMIT || sourceIndexes[earlier] != index)) + while (earlier < face && (mode(earlier) != Mode.EMIT + || sourceIndexes[earlier] != index)) earlier++; output = sampledLevels[face] = earlier < face ? sampledLevels[earlier] @@ -235,54 +242,67 @@ public void updateOutputs() { connections = newConnections; var state = owner.getBlockState(); if (firstTick) { - // Old saves may still have all-face wire geometry. Updating the machine's - // entire neighbourhood here would pulse observers on every chunk load. + // Old saves may still have all-face wire geometry. Updating the + // machine's + // entire neighbourhood here would pulse observers on every + // chunk load. for (var direction : Direction.Plane.HORIZONTAL) { var neighbour = owner.getBlockPos().relative(direction); if (!level.hasChunkAt(neighbour)) continue; var wire = level.getBlockState(neighbour); if (wire.is(Blocks.REDSTONE_WIRE)) { - var updated = wire.updateShape(direction.getOpposite(), state, level, - neighbour, owner.getBlockPos()); - Block.updateOrDestroy(wire, updated, level, neighbour, Block.UPDATE_ALL); + var updated = wire.updateShape(direction.getOpposite(), + state, level, neighbour, owner.getBlockPos()); + Block.updateOrDestroy(wire, updated, level, neighbour, + Block.UPDATE_ALL); } } } else { - level.sendBlockUpdated(owner.getBlockPos(), state, state, Block.UPDATE_CLIENTS); - state.updateNeighbourShapes(level, owner.getBlockPos(), Block.UPDATE_ALL); + level.sendBlockUpdated(owner.getBlockPos(), state, state, + Block.UPDATE_CLIENTS); + state.updateNeighbourShapes(level, owner.getBlockPos(), + Block.UPDATE_ALL); outputsChanged = true; } } if (outputsChanged) - level.updateNeighborsAt(owner.getBlockPos(), owner.getBlockState().getBlock()); + level.updateNeighborsAt(owner.getBlockPos(), + owner.getBlockState().getBlock()); } public static int level(double fullness) { - return Double.isFinite(fullness) ? (int) Math.floor(Math.clamp(fullness, 0, 1) * 15) : 0; + return Double.isFinite(fullness) + ? (int) Math.floor(Math.clamp(fullness, 0, 1) * 15) + : 0; } - /** Block getSignal receives the direction from the receiver toward this block. */ + /** + * Block getSignal receives the direction from the receiver toward this + * block. + */ public int signal(Direction queryDirection) { return outgoing[RelativeFace - .of(RelativeFace.facing(owner.getBlockState()), queryDirection.getOpposite()) + .of(RelativeFace.facing(owner.getBlockState()), + queryDirection.getOpposite()) .ordinal()]; } /** - * Read faces and configured Emit faces accept dust. Uses the receiver-to-block query direction - * from {@link #signal(Direction)}. + * Read faces and configured Emit faces accept dust. Uses the + * receiver-to-block query direction from {@link #signal(Direction)}. */ public boolean connects(@Nullable Direction queryDirection) { if (queryDirection == null) return false; - int face = RelativeFace.of( - RelativeFace.facing(owner.getBlockState()), queryDirection.getOpposite()).ordinal(); + int face = RelativeFace.of(RelativeFace.facing(owner.getBlockState()), + queryDirection.getOpposite()).ordinal(); return connects(face); } private boolean connects(int face) { - return mode(face) == Mode.READ || mode(face) == Mode.EMIT && source(face) >= 0; + return mode(face) == Mode.READ + || mode(face) == Mode.EMIT && source(face) >= 0; } public CompoundTag save(HolderLookup.Provider registries) { @@ -308,15 +328,20 @@ public void load(CompoundTag tag, HolderLookup.Provider registries) { var saved = tag.getIntArray("modes"); var lows = tag.getIntArray("lower"); var highs = tag.getIntArray("upper"); - int mask = tag.contains("outside") ? tag.getInt("outside") : tag.getInt("without"); + int mask = tag.contains("outside") + ? tag.getInt("outside") + : tag.getInt("without"); for (int i = 0; i < 6; i++) { - modes[i] = saved.length == 6 && saved[i] >= 0 && saved[i] < MODES.length ? saved[i] : 0; + modes[i] = saved.length == 6 && saved[i] >= 0 + && saved[i] < MODES.length ? saved[i] : 0; lower[i] = lows.length == 6 ? Math.clamp(lows[i], 0, 15) : 1; - upper[i] = highs.length == 6 ? Math.clamp(highs[i], lower[i], 15) : 15; + upper[i] = highs.length == 6 + ? Math.clamp(highs[i], lower[i], 15) + : 15; outside[i] = (mask & 1 << i) != 0; inverted[i] = (tag.getInt("inverted") & 1 << i) != 0; - sourceIds[i] = tag - .getString(tag.contains("source_" + i) ? "source_" + i : "group_" + i); + sourceIds[i] = tag.getString( + tag.contains("source_" + i) ? "source_" + i : "group_" + i); sourceIndexes[i] = findSource(sourceIds[i]); } legacyAny = tag.getBoolean("legacy_any"); @@ -325,7 +350,9 @@ public void load(CompoundTag tag, HolderLookup.Provider registries) { public void migrate(int oldMode) { Arrays.fill(modes, - oldMode == 1 || oldMode == 2 ? Mode.READ.ordinal() : Mode.IGNORE.ordinal()); + oldMode == 1 || oldMode == 2 + ? Mode.READ.ordinal() + : Mode.IGNORE.ordinal()); Arrays.fill(lower, 1); Arrays.fill(upper, 15); Arrays.fill(outside, oldMode == 1); diff --git a/src/main/java/zone/moddev/mc/lib/api/redstone/RedstoneOutput.java b/src/main/java/zone/moddev/mc/lib/api/redstone/RedstoneOutput.java index e42aeecd..93983acf 100644 --- a/src/main/java/zone/moddev/mc/lib/api/redstone/RedstoneOutput.java +++ b/src/main/java/zone/moddev/mc/lib/api/redstone/RedstoneOutput.java @@ -2,56 +2,57 @@ import java.util.*; import java.util.function.*; + import net.minecraft.network.chat.Component; import net.minecraft.resources.ResourceLocation; + import zone.moddev.mc.lib.api.automation.ResourceGroup; -/** Common-side metadata and a server signal supplier; contains no client rendering types. */ -public record RedstoneOutput( - String id, - Component name, - Component description, - int colour, - Optional sprite, - IntSupplier signal) +/** + * Common-side metadata and a server signal supplier; contains no client + * rendering types. + */ +public record RedstoneOutput(String id, Component name, Component description, + int colour, Optional sprite, IntSupplier signal) implements IRedstoneOutput { - public RedstoneOutput { - if (id == null || !id.matches("[a-z0-9_.:/-]{1,128}")) - throw new IllegalArgumentException("Invalid redstone output ID: " + id); - if ((colour & 0xff000000) != 0) throw new IllegalArgumentException("Output colour must be RGB"); - name = Objects.requireNonNull(name).copy(); - description = Objects.requireNonNull(description).copy(); - sprite = Objects.requireNonNull(sprite); - signal = Objects.requireNonNull(signal); - } - - @Override - public Component name() { - return name.copy(); - } - - @Override - public Component description() { - return description.copy(); - } - - public int level() { - return Math.clamp(signal.getAsInt(), 0, 15); - } - - @Override - public List redstoneOutputs() { - return List.of(this); - } - - /** Adapter preserving the resource group's ID, label and swatch. */ - public static RedstoneOutput resource(ResourceGroup group, DoubleSupplier fullness) { - return new RedstoneOutput( - group.id(), - group.name(), - Component.translatable("screen.mmdlib.redstone.source.fullness", group.name()), - group.colour(), - Optional.empty(), - () -> RedstoneControl.level(fullness.getAsDouble())); - } + public RedstoneOutput { + if (id == null || !id.matches("[a-z0-9_.:/-]{1,128}")) + throw new IllegalArgumentException( + "Invalid redstone output ID: " + id); + if ((colour & 0xff000000) != 0) + throw new IllegalArgumentException("Output colour must be RGB"); + name = Objects.requireNonNull(name).copy(); + description = Objects.requireNonNull(description).copy(); + sprite = Objects.requireNonNull(sprite); + signal = Objects.requireNonNull(signal); + } + + @Override + public Component name() { + return name.copy(); + } + + @Override + public Component description() { + return description.copy(); + } + + public int level() { + return Math.clamp(signal.getAsInt(), 0, 15); + } + + @Override + public List redstoneOutputs() { + return List.of(this); + } + + /** Adapter preserving the resource group's ID, label and swatch. */ + public static RedstoneOutput resource(ResourceGroup group, + DoubleSupplier fullness) { + return new RedstoneOutput(group.id(), group.name(), + Component.translatable("screen.mmdlib.redstone.source.fullness", + group.name()), + group.colour(), Optional.empty(), + () -> RedstoneControl.level(fullness.getAsDouble())); + } } diff --git a/src/main/java/zone/moddev/mc/lib/api/stats/CounterRate.java b/src/main/java/zone/moddev/mc/lib/api/stats/CounterRate.java index 83b3da4d..4e4c8f1d 100644 --- a/src/main/java/zone/moddev/mc/lib/api/stats/CounterRate.java +++ b/src/main/java/zone/moddev/mc/lib/api/stats/CounterRate.java @@ -18,8 +18,8 @@ public CounterRate(int window) { private final ArrayDeque samples = new ArrayDeque<>(); public void sample(BigInteger count, long tick) { - if (!samples.isEmpty() - && (tick <= samples.getLast().tick() || count.compareTo(samples.getLast().count()) < 0)) + if (!samples.isEmpty() && (tick <= samples.getLast().tick() + || count.compareTo(samples.getLast().count()) < 0)) samples.clear(); samples.addLast(new Sample(tick, count)); while (samples.size() > 1 && samples.getFirst().tick() < tick - window) @@ -31,8 +31,7 @@ public String format(int ticksPerUnit, int decimals) { return "—"; var first = samples.getFirst(); var last = samples.getLast(); - return StatsNumbers.rate( - last.count().subtract(first.count()), last.tick() - first.tick(), ticksPerUnit, - decimals); + return StatsNumbers.rate(last.count().subtract(first.count()), + last.tick() - first.tick(), ticksPerUnit, decimals); } } diff --git a/src/main/java/zone/moddev/mc/lib/api/stats/Statistics.java b/src/main/java/zone/moddev/mc/lib/api/stats/Statistics.java index c2851270..9e7e37f7 100644 --- a/src/main/java/zone/moddev/mc/lib/api/stats/Statistics.java +++ b/src/main/java/zone/moddev/mc/lib/api/stats/Statistics.java @@ -8,47 +8,46 @@ import net.minecraft.network.chat.Component; /** - * Immutable descriptions of a part's statistics. Values own sampling and formatting; the UI - * transports their display pages. + * Immutable descriptions of a part's statistics. Values own sampling and + * formatting; the UI transports their display pages. */ -public record Statistics(Component title, List rows, List maximumUses) { - public static final Statistics EMPTY = new Statistics(Component.empty(), List.of(), List.of()); +public record Statistics(Component title, List rows, + List maximumUses) { + public static final Statistics EMPTY = new Statistics(Component.empty(), + List.of(), List.of()); public Statistics { title = Objects.requireNonNull(title).copy(); rows = List.copyOf(rows); maximumUses = List.copyOf(maximumUses); - if (rows.size() > 64 || rows.stream().map(Row::id).distinct().count() != rows.size()) - throw new IllegalArgumentException("Statistics need at most 64 rows with unique IDs"); + if (rows.size() > 64 + || rows.stream().map(Row::id).distinct().count() != rows.size()) + throw new IllegalArgumentException( + "Statistics need at most 64 rows with unique IDs"); } public Statistics(Component title, List rows) { this(title, rows, List.of()); } - /** Stable IDs keep each row's counter history independent of order and optional rows. */ - public record Row( - String id, - Component label, - Value value, - Component unit, - int decimals, - boolean perTimescale, - Object demandResource) { - public Row( - String id, - Component label, - Value value, - Component unit, - int decimals, - boolean perTimescale) { + /** + * Stable IDs keep each row's counter history independent of order and + * optional rows. + */ + public record Row(String id, Component label, Value value, Component unit, + int decimals, boolean perTimescale, Object demandResource) { + public Row(String id, Component label, Value value, Component unit, + int decimals, boolean perTimescale) { this(id, label, value, unit, decimals, perTimescale, null); } - /** Show this row only while a provider declares positive demand for this resource. */ + /** + * Show this row only while a provider declares positive demand for this + * resource. + */ public Row whenUsed(Object resource) { - return new Row( - id, label, value, unit, decimals, perTimescale, Objects.requireNonNull(resource)); + return new Row(id, label, value, unit, decimals, perTimescale, + Objects.requireNonNull(resource)); } public Row { @@ -61,23 +60,33 @@ public Row whenUsed(Object resource) { } } - /** An open statistic type; no registration or client implementation is required. */ + /** + * An open statistic type; no registration or client implementation is + * required. + */ public interface Value { - /** Immutable equality key for sampling configuration, excluding the current reading. */ + /** + * Immutable equality key for sampling configuration, excluding the + * current reading. + */ default Object samplingKey() { return getClass(); } /** - * Called once per sampling update, independently of the number of timescales. Previous is - * null for a new row, a changed value class/configuration, or after a row/provider was - * removed. Return fresh sampled data or update the supplied history; never store it in the - * definition. + * Called once per sampling update, independently of the number of + * timescales. Previous is null for a new row, a changed value + * class/configuration, or after a row/provider was removed. Return + * fresh sampled data or update the supplied history; never store it in + * the definition. */ Sample sample(StatsSamplingContext context, Sample previous); } - /** Per-row sampled data/history. Formatting must not change history or read live storage. */ + /** + * Per-row sampled data/history. Formatting must not change history or read + * live storage. + */ @FunctionalInterface public interface Sample { Component format(StatsFormatContext context); @@ -85,11 +94,15 @@ public interface Sample { private record CounterSample(CounterRate rate) implements Sample { public Component format(StatsFormatContext context) { - return context.number(rate.format(context.ticks(), context.decimals())); + return context + .number(rate.format(context.ticks(), context.decimals())); } } - /** A lifetime counter, measured over the most recent window (20..1200 game ticks). */ + /** + * A lifetime counter, measured over the most recent window (20..1200 game + * ticks). + */ public record Counter(BigInteger total, int windowTicks) implements Value { public Object samplingKey() { return windowTicks; @@ -105,15 +118,19 @@ public Sample sample(StatsSamplingContext context, Sample previous) { public Counter { if (total.signum() < 0 || windowTicks < 20 || windowTicks > 1200) - throw new IllegalArgumentException("Invalid statistics counter"); + throw new IllegalArgumentException( + "Invalid statistics counter"); } } - /** Exact amount per number of game ticks; a non-timescale row uses this as a fixed quantity. */ + /** + * Exact amount per number of game ticks; a non-timescale row uses this as a + * fixed quantity. + */ public record Rate(BigInteger amount, long ticks) implements Value { public Sample sample(StatsSamplingContext context, Sample previous) { - return format -> format - .number(StatsNumbers.rate(amount, ticks, format.ticks(), format.decimals())); + return format -> format.number(StatsNumbers.rate(amount, ticks, + format.ticks(), format.decimals())); } public Rate { @@ -133,12 +150,15 @@ public Sample sample(StatsSamplingContext context, Sample previous) { } } - /** Resolves the combined demands of all registered providers against this exact resource. */ + /** + * Resolves the combined demands of all registered providers against this + * exact resource. + */ public record MaximumUse(Object resource) implements Value { public Sample sample(StatsSamplingContext context, Sample previous) { var amount = context.demand(resource); - return format -> format - .number(StatsNumbers.rate(amount, 1, format.ticks(), format.decimals())); + return format -> format.number(StatsNumbers.rate(amount, 1, + format.ticks(), format.decimals())); } public MaximumUse { @@ -146,7 +166,10 @@ public Sample sample(StatsSamplingContext context, Sample previous) { } } - /** Maximum recipe demand per working tick. Resources are matched by identity, not equality. */ + /** + * Maximum recipe demand per working tick. Resources are matched by + * identity, not equality. + */ public record EnergyUse(Object resource, BigInteger perTick) { public EnergyUse { Objects.requireNonNull(resource); diff --git a/src/main/java/zone/moddev/mc/lib/api/stats/StatsFormatContext.java b/src/main/java/zone/moddev/mc/lib/api/stats/StatsFormatContext.java index 97fe0f6b..80d96860 100644 --- a/src/main/java/zone/moddev/mc/lib/api/stats/StatsFormatContext.java +++ b/src/main/java/zone/moddev/mc/lib/api/stats/StatsFormatContext.java @@ -4,9 +4,12 @@ import net.minecraft.network.chat.Component; -/** Row formatting options; custom statistics may use these helpers or return their own text. */ -public record StatsFormatContext( - StatsTimescale timescale, Component unit, int decimals, boolean perTimescale) { +/** + * Row formatting options; custom statistics may use these helpers or return + * their own text. + */ +public record StatsFormatContext(StatsTimescale timescale, Component unit, + int decimals, boolean perTimescale) { public StatsFormatContext { Objects.requireNonNull(timescale); unit = Objects.requireNonNull(unit).copy(); diff --git a/src/main/java/zone/moddev/mc/lib/api/stats/StatsNumbers.java b/src/main/java/zone/moddev/mc/lib/api/stats/StatsNumbers.java index 040da237..03ade23d 100644 --- a/src/main/java/zone/moddev/mc/lib/api/stats/StatsNumbers.java +++ b/src/main/java/zone/moddev/mc/lib/api/stats/StatsNumbers.java @@ -8,15 +8,19 @@ import java.util.Locale; -/** Exact rate conversion with fixed precision and comma grouping, independent of server locale. */ +/** + * Exact rate conversion with fixed precision and comma grouping, independent of + * server locale. + */ public final class StatsNumbers { private StatsNumbers() { } - public static String rate(BigInteger amount, long elapsed, int ticksPerUnit, int decimals) { + public static String rate(BigInteger amount, long elapsed, int ticksPerUnit, + int decimals) { var value = new BigDecimal(amount) - .multiply(BigDecimal.valueOf(ticksPerUnit)) - .divide(BigDecimal.valueOf(elapsed), decimals, RoundingMode.HALF_UP); + .multiply(BigDecimal.valueOf(ticksPerUnit)).divide( + BigDecimal.valueOf(elapsed), decimals, RoundingMode.HALF_UP); var format = NumberFormat.getNumberInstance(Locale.US); format.setMinimumFractionDigits(decimals); format.setMaximumFractionDigits(decimals); diff --git a/src/main/java/zone/moddev/mc/lib/api/stats/StatsProvider.java b/src/main/java/zone/moddev/mc/lib/api/stats/StatsProvider.java index 9d957b7e..b3f35ece 100644 --- a/src/main/java/zone/moddev/mc/lib/api/stats/StatsProvider.java +++ b/src/main/java/zone/moddev/mc/lib/api/stats/StatsProvider.java @@ -1,7 +1,10 @@ package zone.moddev.mc.lib.api.stats; -/** A machine part's server-side statistics. Returning an empty section adds no drawer rows. */ +/** + * A machine part's server-side statistics. Returning an empty section adds no + * drawer rows. + */ @FunctionalInterface public interface StatsProvider { - Statistics statistics(); + Statistics statistics(); } diff --git a/src/main/java/zone/moddev/mc/lib/api/stats/StatsSamplingContext.java b/src/main/java/zone/moddev/mc/lib/api/stats/StatsSamplingContext.java index 2cc07aa8..8f708011 100644 --- a/src/main/java/zone/moddev/mc/lib/api/stats/StatsSamplingContext.java +++ b/src/main/java/zone/moddev/mc/lib/api/stats/StatsSamplingContext.java @@ -5,7 +5,10 @@ import java.util.IdentityHashMap; import java.util.Map; -/** One server sample's game time and combined resource demands, matched by identity. */ +/** + * One server sample's game time and combined resource demands, matched by + * identity. + */ public final class StatsSamplingContext { private final long tick; private final Map demands; diff --git a/src/main/java/zone/moddev/mc/lib/api/stats/StatsTimescale.java b/src/main/java/zone/moddev/mc/lib/api/stats/StatsTimescale.java index 135906d9..af2013d9 100644 --- a/src/main/java/zone/moddev/mc/lib/api/stats/StatsTimescale.java +++ b/src/main/java/zone/moddev/mc/lib/api/stats/StatsTimescale.java @@ -6,18 +6,24 @@ import net.minecraft.network.chat.Component; import net.minecraft.resources.ResourceLocation; -/** Data supplied by a host, without registration. Labels remain translatable on the client. */ -public record StatsTimescale(ResourceLocation id, int ticks, Component label, Component unit) { +/** + * Data supplied by a host, without registration. Labels remain translatable on + * the client. + */ +public record StatsTimescale(ResourceLocation id, int ticks, Component label, + Component unit) { public static final int MAX_TIMESCALES = 16; public static final StatsTimescale TICK = builtin("tick", 1); public static final StatsTimescale SECOND = builtin("second", 20); public static final StatsTimescale MINUTE = builtin("minute", 1200); - public static final List DEFAULTS = List.of(TICK, SECOND, MINUTE); + public static final List DEFAULTS = List.of(TICK, SECOND, + MINUTE); public StatsTimescale { Objects.requireNonNull(id); if (ticks <= 0) - throw new IllegalArgumentException("Timescale ticks must be positive"); + throw new IllegalArgumentException( + "Timescale ticks must be positive"); label = Objects.requireNonNull(label).copy(); unit = Objects.requireNonNull(unit).copy(); } @@ -31,19 +37,18 @@ public Component unit() { } private static StatsTimescale builtin(String name, int ticks) { - return new StatsTimescale( - ResourceLocation.parse("mmdlib:" + name), - ticks, - Statistics.label("per_" + name), - Statistics.label(name)); + return new StatsTimescale(ResourceLocation.parse("mmdlib:" + name), + ticks, Statistics.label("per_" + name), Statistics.label(name)); } /** Empty lists are allowed; the drawer then has no selectable pages. */ - public static List checked(List timescales) { + public static List checked( + List timescales) { var result = List.copyOf(timescales); - if (result.size() > MAX_TIMESCALES - || result.stream().map(StatsTimescale::id).distinct().count() != result.size()) - throw new IllegalArgumentException("Too many statistics timescales or duplicate IDs"); + if (result.size() > MAX_TIMESCALES || result.stream() + .map(StatsTimescale::id).distinct().count() != result.size()) + throw new IllegalArgumentException( + "Too many statistics timescales or duplicate IDs"); return result; } } diff --git a/src/main/java/zone/moddev/mc/lib/client/FluidBottleClient.java b/src/main/java/zone/moddev/mc/lib/client/FluidBottleClient.java index b6ed69c8..94a8e21c 100644 --- a/src/main/java/zone/moddev/mc/lib/client/FluidBottleClient.java +++ b/src/main/java/zone/moddev/mc/lib/client/FluidBottleClient.java @@ -9,16 +9,19 @@ import zone.moddev.mc.lib.common.fluid.FluidBottles; /** - * Colors the fluid in a {@code mmdlib:fluid_bottle} with whatever tint its fluid type registers on - * the client ({@code IClientFluidTypeExtensions#getTintColor}); fluids that register none show - * their texture as drawn. + * Colors the fluid in a {@code mmdlib:fluid_bottle} with whatever tint its + * fluid type registers on the client + * ({@code IClientFluidTypeExtensions#getTintColor}); fluids that register none + * show their texture as drawn. */ @EventBusSubscriber(modid = "mmdlib", bus = EventBusSubscriber.Bus.MOD, value = Dist.CLIENT) public final class FluidBottleClient { - private FluidBottleClient() {} + private FluidBottleClient() { + } - @SubscribeEvent - public static void colors(RegisterColorHandlersEvent.Item event) { - event.register(new DynamicFluidContainerModel.Colors(), FluidBottles.BOTTLE.get()); - } + @SubscribeEvent + public static void colors(RegisterColorHandlersEvent.Item event) { + event.register(new DynamicFluidContainerModel.Colors(), + FluidBottles.BOTTLE.get()); + } } diff --git a/src/main/java/zone/moddev/mc/lib/client/RuntimeContentClient.java b/src/main/java/zone/moddev/mc/lib/client/RuntimeContentClient.java index aea8a5ad..e60bbe0a 100644 --- a/src/main/java/zone/moddev/mc/lib/client/RuntimeContentClient.java +++ b/src/main/java/zone/moddev/mc/lib/client/RuntimeContentClient.java @@ -17,197 +17,180 @@ import zone.moddev.mc.lib.common.runtime.*; /** - * Client-side setup for content registered by every {@link RuntimeContentRegistrar}: item and block - * tints from material colors, projectile renderers, fluid textures, render layers, and the bow, - * crossbow, shield and fishing-rod model properties copied from vanilla. + * Client-side setup for content registered by every + * {@link RuntimeContentRegistrar}: item and block tints from material colors, + * projectile renderers, fluid textures, render layers, and the bow, crossbow, + * shield and fishing-rod model properties copied from vanilla. */ @EventBusSubscriber(modid = "mmdlib", bus = EventBusSubscriber.Bus.MOD, value = Dist.CLIENT) public final class RuntimeContentClient { - private RuntimeContentClient() {} + private RuntimeContentClient() { + } - /** Tints material items with their material's color. */ - @SubscribeEvent - public static void items(RegisterColorHandlersEvent.Item event) { - for (var registrar : RuntimeContentRegistrar.instances()) - registrar - .itemMaterials() - .forEach( - (id, material) -> { + /** Tints material items with their material's color. */ + @SubscribeEvent + public static void items(RegisterColorHandlersEvent.Item event) { + for (var registrar : RuntimeContentRegistrar.instances()) + registrar.itemMaterials().forEach((id, material) -> { var item = registrar.items().get(id).get(); - // Fluid container models obtain their tint from the fluid type, not the bucket + // Fluid container models obtain their tint from the fluid type, + // not the + // bucket // shell. - if (!(item instanceof BucketItem) - && material.runtime().tinted() + if (!(item instanceof BucketItem) && material.runtime().tinted() && !StaticAssetRules.INSTANCE.untintedItems().contains(id)) - event.register( - (stack, layer) -> layer == 0 ? material.properties().color() : -1, item); - }); - } + event.register((stack, layer) -> layer == 0 + ? material.properties().color() + : -1, item); + }); + } - /** Tints material blocks with their material's color. */ - @SubscribeEvent - public static void blocks(RegisterColorHandlersEvent.Block event) { - for (var registrar : RuntimeContentRegistrar.instances()) - registrar - .blockMaterials() - .forEach( - (id, material) -> { + /** Tints material blocks with their material's color. */ + @SubscribeEvent + public static void blocks(RegisterColorHandlersEvent.Block event) { + for (var registrar : RuntimeContentRegistrar.instances()) + registrar.blockMaterials().forEach((id, material) -> { if (material.runtime().tinted() && !StaticAssetRules.INSTANCE.untintedBlocks().contains(id)) - event.register( - (state, level, pos, tint) -> material.properties().color(), - registrar.blocks().get(id).get()); - }); - } + event.register((state, level, pos, tint) -> material + .properties().color(), + registrar.blocks().get(id).get()); + }); + } - /** Registers arrow renderers for material projectiles. */ - @SubscribeEvent - public static void entities(EntityRenderersEvent.RegisterRenderers event) { - for (var registrar : RuntimeContentRegistrar.instances()) - registrar - .projectiles() - .values() - .forEach( - type -> - event.registerEntityRenderer( - type.get(), - context -> - new ArrowRenderer(context) { - @Override - public ResourceLocation getTextureLocation(MaterialProjectile entity) { - return ResourceLocation.parse( - "minecraft:textures/entity/projectiles/arrow.png"); - } - })); - } + /** Registers arrow renderers for material projectiles. */ + @SubscribeEvent + public static void entities(EntityRenderersEvent.RegisterRenderers event) { + for (var registrar : RuntimeContentRegistrar.instances()) + registrar.projectiles().values() + .forEach(type -> event.registerEntityRenderer(type.get(), + context -> new ArrowRenderer(context) { + @Override + public ResourceLocation getTextureLocation( + MaterialProjectile entity) { + return ResourceLocation.parse( + "minecraft:textures/entity/projectiles/arrow.png"); + } + })); + } - /** Registers fluid textures and tints, and the shield's item renderer. */ - @SubscribeEvent - public static void extensions(RegisterClientExtensionsEvent event) { - for (var registrar : RuntimeContentRegistrar.instances()) { - for (var family : registrar.fluidFamilies()) { - var appearance = family.definition.appearance(); - event.registerFluidType( - new IClientFluidTypeExtensions() { - @Override - public ResourceLocation getStillTexture() { - return appearance.still(); - } + /** Registers fluid textures and tints, and the shield's item renderer. */ + @SubscribeEvent + public static void extensions(RegisterClientExtensionsEvent event) { + for (var registrar : RuntimeContentRegistrar.instances()) { + for (var family : registrar.fluidFamilies()) { + var appearance = family.definition.appearance(); + event.registerFluidType(new IClientFluidTypeExtensions() { + @Override + public ResourceLocation getStillTexture() { + return appearance.still(); + } - @Override - public ResourceLocation getFlowingTexture() { - return appearance.flowing(); - } + @Override + public ResourceLocation getFlowingTexture() { + return appearance.flowing(); + } - @Override - public ResourceLocation getOverlayTexture() { - return appearance.overlay().orElse(null); - } + @Override + public ResourceLocation getOverlayTexture() { + return appearance.overlay().orElse(null); + } - @Override - public int getTintColor() { - return appearance.tint(); - } - }, - family.type.get()); - } - registrar - .items() - .values() - .forEach( - holder -> { + @Override + public int getTintColor() { + return appearance.tint(); + } + }, family.type.get()); + } + registrar.items().values().forEach(holder -> { var owner = registrar.itemMaterials().get(holder.getId()); - // Precolored shields use their own item model instead of the vanilla shield. + // Precolored shields use their own item model instead of the + // vanilla + // shield. if (holder.get() instanceof ShieldItem && (owner == null || owner.runtime().tinted()) - && !StaticAssetRules.INSTANCE.untintedItems().contains(holder.getId())) - event.registerItem( - new IClientItemExtensions() { + && !StaticAssetRules.INSTANCE.untintedItems() + .contains(holder.getId())) + event.registerItem(new IClientItemExtensions() { private BlockEntityWithoutLevelRenderer renderer; @Override public BlockEntityWithoutLevelRenderer getCustomRenderer() { - if (renderer == null) { - var minecraft = Minecraft.getInstance(); - renderer = - new BlockEntityWithoutLevelRenderer( + if (renderer == null) { + var minecraft = Minecraft.getInstance(); + renderer = new BlockEntityWithoutLevelRenderer( minecraft.getBlockEntityRenderDispatcher(), minecraft.getEntityModels()) { - @Override - public void renderByItem( - ItemStack stack, - ItemDisplayContext context, - com.mojang.blaze3d.vertex.PoseStack pose, - MultiBufferSource buffers, - int light, - int overlay) { - var vanilla = new ItemStack(Items.SHIELD); - vanilla.applyComponents(stack.getComponentsPatch()); - Minecraft.getInstance() - .getItemRenderer() - .getBlockEntityRenderer() - .renderByItem( - vanilla, context, pose, buffers, light, overlay); - } + @Override + public void renderByItem(ItemStack stack, + ItemDisplayContext context, + com.mojang.blaze3d.vertex.PoseStack pose, + MultiBufferSource buffers, int light, + int overlay) { + var vanilla = new ItemStack( + Items.SHIELD); + vanilla.applyComponents( + stack.getComponentsPatch()); + Minecraft.getInstance() + .getItemRenderer() + .getBlockEntityRenderer() + .renderByItem(vanilla, context, + pose, buffers, light, overlay); + } }; - } - return renderer; + } + return renderer; } - }, - holder.get()); - }); + }, holder.get()); + }); + } } - } - /** - * Sets cutout layers for non-occluding blocks, translucent layers for fluids, and copies vanilla - * item model properties. - */ - @SubscribeEvent - public static void setup(FMLClientSetupEvent event) { - event.enqueueWork( - () -> { - for (var registrar : RuntimeContentRegistrar.instances()) { - registrar - .blocks() - .values() - .forEach( - holder -> { - var block = holder.get(); - if (!(block instanceof net.minecraft.world.level.block.LiquidBlock) - && !block.defaultBlockState().canOcclude()) - ItemBlockRenderTypes.setRenderLayer(block, RenderType.cutout()); - }); - for (var family : registrar.fluidFamilies()) { - ItemBlockRenderTypes.setRenderLayer(family.source.get(), RenderType.translucent()); - ItemBlockRenderTypes.setRenderLayer(family.flowing.get(), RenderType.translucent()); - } - registrar - .items() - .values() - .forEach( - holder -> { - var item = holder.get(); - if (item instanceof BowItem) + /** + * Sets cutout layers for non-occluding blocks, translucent layers for + * fluids, and copies vanilla item model properties. + */ + @SubscribeEvent + public static void setup(FMLClientSetupEvent event) { + event.enqueueWork(() -> { + for (var registrar : RuntimeContentRegistrar.instances()) { + registrar.blocks().values().forEach(holder -> { + var block = holder.get(); + if (!(block instanceof net.minecraft.world.level.block.LiquidBlock) + && !block.defaultBlockState().canOcclude()) + ItemBlockRenderTypes.setRenderLayer(block, + RenderType.cutout()); + }); + for (var family : registrar.fluidFamilies()) { + ItemBlockRenderTypes.setRenderLayer(family.source.get(), + RenderType.translucent()); + ItemBlockRenderTypes.setRenderLayer(family.flowing.get(), + RenderType.translucent()); + } + registrar.items().values().forEach(holder -> { + var item = holder.get(); + if (item instanceof BowItem) copyProperties(item, Items.BOW, "pull", "pulling"); - if (item instanceof CrossbowItem) - copyProperties( - item, Items.CROSSBOW, "pull", "pulling", "charged", "firework"); - if (item instanceof FishingRodItem) + if (item instanceof CrossbowItem) + copyProperties(item, Items.CROSSBOW, "pull", "pulling", + "charged", "firework"); + if (item instanceof FishingRodItem) copyProperties(item, Items.FISHING_ROD, "cast"); - if (item instanceof ShieldItem) + if (item instanceof ShieldItem) copyProperties(item, Items.SHIELD, "blocking"); - }); - } + }); + } }); - } + } - private static void copyProperties(Item target, Item vanilla, String... names) { - for (String name : names) { - var id = ResourceLocation.withDefaultNamespace(name); - var property = ItemProperties.getProperty(new ItemStack(vanilla), id); - if (property instanceof ClampedItemPropertyFunction clamped) - ItemProperties.register(target, id, clamped); + private static void copyProperties(Item target, Item vanilla, + String... names) { + for (String name : names) { + var id = ResourceLocation.withDefaultNamespace(name); + var property = ItemProperties.getProperty(new ItemStack(vanilla), + id); + if (property instanceof ClampedItemPropertyFunction clamped) + ItemProperties.register(target, id, clamped); + } } - } } diff --git a/src/main/java/zone/moddev/mc/lib/client/StaticAssetRules.java b/src/main/java/zone/moddev/mc/lib/client/StaticAssetRules.java index 433f501a..dd11dbb5 100644 --- a/src/main/java/zone/moddev/mc/lib/client/StaticAssetRules.java +++ b/src/main/java/zone/moddev/mc/lib/client/StaticAssetRules.java @@ -1,7 +1,9 @@ package zone.moddev.mc.lib.client; import java.io.InputStreamReader; + import java.nio.charset.StandardCharsets; + import java.util.Set; import com.google.gson.JsonParser; @@ -12,33 +14,38 @@ import net.minecraft.resources.ResourceLocation; -/** Authored, precolored artwork must not receive the generic material tint a second time. */ -public record StaticAssetRules( - Set untintedItems, Set untintedBlocks) { - private static final Codec> IDS = - ResourceLocation.CODEC - .listOf() - .xmap(Set::copyOf, values -> values.stream().sorted().toList()); - public static final Codec CODEC = - RecordCodecBuilder.create( - i -> - i.group( - IDS.optionalFieldOf("untinted_items", Set.of()) - .forGetter(StaticAssetRules::untintedItems), - IDS.optionalFieldOf("untinted_blocks", Set.of()) - .forGetter(StaticAssetRules::untintedBlocks)) - .apply(i, StaticAssetRules::new)); - public static final StaticAssetRules INSTANCE = load(); - - private static StaticAssetRules load() { - try (var stream = - StaticAssetRules.class.getResourceAsStream("/assets/mmdlib/imported_assets.json")) { - if (stream == null) return new StaticAssetRules(Set.of(), Set.of()); - try (var reader = new InputStreamReader(stream, StandardCharsets.UTF_8)) { - return CODEC.parse(JsonOps.INSTANCE, JsonParser.parseReader(reader)).getOrThrow(); - } - } catch (java.io.IOException ex) { - throw new java.io.UncheckedIOException(ex); +/** + * Authored, precolored artwork must not receive the generic material tint a + * second time. + */ +public record StaticAssetRules(Set untintedItems, + Set untintedBlocks) { + private static final Codec> IDS = ResourceLocation.CODEC + .listOf() + .xmap(Set::copyOf, values -> values.stream().sorted().toList()); + public static final Codec CODEC = RecordCodecBuilder + .create(i -> i + .group( + IDS.optionalFieldOf("untinted_items", Set.of()) + .forGetter(StaticAssetRules::untintedItems), + IDS.optionalFieldOf("untinted_blocks", Set.of()) + .forGetter(StaticAssetRules::untintedBlocks)) + .apply(i, StaticAssetRules::new)); + public static final StaticAssetRules INSTANCE = load(); + + private static StaticAssetRules load() { + try (var stream = StaticAssetRules.class + .getResourceAsStream("/assets/mmdlib/imported_assets.json")) { + if (stream == null) + return new StaticAssetRules(Set.of(), Set.of()); + try (var reader = new InputStreamReader(stream, + StandardCharsets.UTF_8)) { + return CODEC + .parse(JsonOps.INSTANCE, JsonParser.parseReader(reader)) + .getOrThrow(); + } + } catch (java.io.IOException ex) { + throw new java.io.UncheckedIOException(ex); + } } - } } diff --git a/src/main/java/zone/moddev/mc/lib/client/jei/CrushingCategory.java b/src/main/java/zone/moddev/mc/lib/client/jei/CrushingCategory.java index ee2fd029..5a818d77 100644 --- a/src/main/java/zone/moddev/mc/lib/client/jei/CrushingCategory.java +++ b/src/main/java/zone/moddev/mc/lib/client/jei/CrushingCategory.java @@ -1,11 +1,5 @@ package zone.moddev.mc.lib.client.jei; -import net.minecraft.client.Minecraft; -import net.minecraft.client.gui.GuiGraphics; -import net.minecraft.network.chat.Component; -import net.minecraft.resources.ResourceLocation; -import net.minecraft.world.item.crafting.RecipeHolder; - import mezz.jei.api.gui.builder.IRecipeLayoutBuilder; import mezz.jei.api.gui.drawable.IDrawableAnimated; import mezz.jei.api.gui.drawable.IDrawableStatic; @@ -15,59 +9,71 @@ import mezz.jei.api.recipe.RecipeType; import mezz.jei.api.recipe.category.AbstractRecipeCategory; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.crafting.RecipeHolder; + import zone.moddev.mc.lib.common.recipes.CrushingRecipe; -/** Crackhammer crushing: input slot, a crackhammer filling bottom to top, and the output slot. */ -final class CrushingCategory extends AbstractRecipeCategory> { - static final RecipeType> TYPE = - RecipeType.createRecipeHolderType(ResourceLocation.parse("mmdlib:crushing")); +/** + * Crackhammer crushing: input slot, a crackhammer filling bottom to top, and + * the output slot. + */ +final class CrushingCategory + extends + AbstractRecipeCategory> { + static final RecipeType> TYPE = RecipeType + .createRecipeHolderType(ResourceLocation.parse("mmdlib:crushing")); - /** 256x256 sheet: background panel, tab icon, and the hammer used for the fill animation. */ - private static final ResourceLocation TEXTURE = - ResourceLocation.parse("mmdlib:textures/gui/jei/crackhammer.png"); + /** + * 256x256 sheet: background panel, tab icon, and the hammer used for the + * fill animation. + */ + private static final ResourceLocation TEXTURE = ResourceLocation + .parse("mmdlib:textures/gui/jei/crackhammer.png"); - private static final int WIDTH = 168, HEIGHT = 74; - private final IDrawableStatic background; - private final IDrawableAnimated hammer; + private static final int WIDTH = 168, HEIGHT = 74; + private final IDrawableStatic background; + private final IDrawableAnimated hammer; - CrushingCategory(IGuiHelper gui) { - super( - TYPE, - Component.translatable("jei.mmdlib.crushing"), - gui.drawableBuilder(TEXTURE, 169, 1, 16, 16).setTextureSize(256, 256).build(), - WIDTH, - HEIGHT); - background = gui.drawableBuilder(TEXTURE, 0, 0, WIDTH, HEIGHT).setTextureSize(256, 256).build(); - hammer = - gui.drawableBuilder(TEXTURE, 168, 18, 32, 32) + CrushingCategory(IGuiHelper gui) { + super(TYPE, Component.translatable("jei.mmdlib.crushing"), + gui.drawableBuilder(TEXTURE, 169, 1, 16, 16) + .setTextureSize(256, 256).build(), + WIDTH, HEIGHT); + background = gui.drawableBuilder(TEXTURE, 0, 0, WIDTH, HEIGHT) + .setTextureSize(256, 256).build(); + hammer = gui.drawableBuilder(TEXTURE, 168, 18, 32, 32) .setTextureSize(256, 256) .buildAnimated(60, IDrawableAnimated.StartDirection.BOTTOM, false); - } + } - @Override - public void setRecipe( - IRecipeLayoutBuilder builder, RecipeHolder recipe, IFocusGroup focuses) { - // Slot frames are part of the background texture: an 18x18 input at (41, 14), a 26x26 output at - // (115, 10). - builder.addInputSlot(42, 15).addIngredients(recipe.value().getIngredients().getFirst()); - var level = Minecraft.getInstance().level; - if (level != null) - builder - .addOutputSlot(120, 15) - .addItemStack(recipe.value().getResultItem(level.registryAccess())); - } + @Override + public void setRecipe(IRecipeLayoutBuilder builder, + RecipeHolder recipe, IFocusGroup focuses) { + // Slot frames are part of the background texture: an 18x18 input at + // (41, 14), a 26x26 + // output at + // (115, 10). + builder.addInputSlot(42, 15) + .addIngredients(recipe.value().getIngredients().getFirst()); + var level = Minecraft.getInstance().level; + if (level != null) + builder.addOutputSlot(120, 15).addItemStack( + recipe.value().getResultItem(level.registryAccess())); + } - @Override - public void draw( - RecipeHolder recipe, - IRecipeSlotsView slots, - GuiGraphics graphics, - double mouseX, - double mouseY) { - background.draw(graphics); - hammer.draw(graphics, 71, 8); - var font = Minecraft.getInstance().font; - var note = Component.translatable("jei.mmdlib.crushing.mined"); - graphics.drawString(font, note, (WIDTH - font.width(note)) / 2, 52, 0xFF404040, false); - } + @Override + public void draw(RecipeHolder recipe, + IRecipeSlotsView slots, GuiGraphics graphics, double mouseX, + double mouseY) { + background.draw(graphics); + hammer.draw(graphics, 71, 8); + var font = Minecraft.getInstance().font; + var note = Component.translatable("jei.mmdlib.crushing.mined"); + graphics.drawString(font, note, (WIDTH - font.width(note)) / 2, 52, + 0xFF404040, false); + } } diff --git a/src/main/java/zone/moddev/mc/lib/client/jei/JeiIngredientTypes.java b/src/main/java/zone/moddev/mc/lib/client/jei/JeiIngredientTypes.java index 4d6769dd..93425652 100644 --- a/src/main/java/zone/moddev/mc/lib/client/jei/JeiIngredientTypes.java +++ b/src/main/java/zone/moddev/mc/lib/client/jei/JeiIngredientTypes.java @@ -16,7 +16,10 @@ import zone.moddev.mc.lib.gui.client.IngredientType; import zone.moddev.mc.lib.gui.client.IngredientTypes; -/** Optional JEI bridge. Mods register mappings in their JEI plugin before GUI interaction. */ +/** + * Optional JEI bridge. Mods register mappings in their JEI plugin before GUI + * interaction. + */ public final class JeiIngredientTypes { public static final JeiIngredientTypes GLOBAL = builtins(); private final Map, IngredientType> types = new HashMap<>(); @@ -29,19 +32,23 @@ private static JeiIngredientTypes builtins() { return registry; } - public synchronized void register(IIngredientType source, IngredientType target) { + public synchronized void register(IIngredientType source, + IngredientType target) { Objects.requireNonNull(source); Objects.requireNonNull(target); if (!target.valueClass().isAssignableFrom(source.getIngredientClass())) - throw new IllegalArgumentException("Incompatible ingredient classes: " + target.id()); + throw new IllegalArgumentException( + "Incompatible ingredient classes: " + target.id()); if (types.containsKey(source) || ids.contains(target.id())) - throw new IllegalArgumentException("Duplicate JEI ingredient mapping: " + target.id()); + throw new IllegalArgumentException( + "Duplicate JEI ingredient mapping: " + target.id()); types.put(source, target); ids.add(target.id()); } @SuppressWarnings("unchecked") - public synchronized Optional> find(IIngredientType source) { + public synchronized Optional> find( + IIngredientType source) { return Optional.ofNullable((IngredientType) types.get(source)); } } diff --git a/src/main/java/zone/moddev/mc/lib/client/jei/MMDLibJeiPlugin.java b/src/main/java/zone/moddev/mc/lib/client/jei/MMDLibJeiPlugin.java index bb13cf30..0e32c869 100644 --- a/src/main/java/zone/moddev/mc/lib/client/jei/MMDLibJeiPlugin.java +++ b/src/main/java/zone/moddev/mc/lib/client/jei/MMDLibJeiPlugin.java @@ -2,16 +2,16 @@ import java.util.List; -import net.minecraft.client.Minecraft; -import net.minecraft.core.registries.BuiltInRegistries; -import net.minecraft.resources.ResourceLocation; - import mezz.jei.api.IModPlugin; import mezz.jei.api.JeiPlugin; import mezz.jei.api.registration.IRecipeCatalystRegistration; import mezz.jei.api.registration.IRecipeCategoryRegistration; import mezz.jei.api.registration.IRecipeRegistration; +import net.minecraft.client.Minecraft; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.resources.ResourceLocation; + import zone.moddev.mc.lib.MMDLib; import zone.moddev.mc.lib.api.MaterialEventTypes; import zone.moddev.mc.lib.common.recipes.CrushingRecipe; @@ -20,112 +20,104 @@ import zone.moddev.mc.lib.machine.recipe.MachineRecipeType; /** - * Optional JEI integration. Only JEI loads this class (via {@link JeiPlugin}); nothing else in - * MMDLib references it, so MMDLib runs without JEI installed. + * Optional JEI integration. Only JEI loads this class (via {@link JeiPlugin}); + * nothing else in MMDLib references it, so MMDLib runs without JEI installed. */ @JeiPlugin public final class MMDLibJeiPlugin implements IModPlugin { - /** Called by JEI, which finds the plugin through {@link JeiPlugin}. */ - public MMDLibJeiPlugin() {} + /** Called by JEI, which finds the plugin through {@link JeiPlugin}. */ + public MMDLibJeiPlugin() { + } - @Override - public void registerGuiHandlers(mezz.jei.api.registration.IGuiHandlerRegistration registration) { - MachineJei.gui(registration); - } + @Override + public void registerGuiHandlers( + mezz.jei.api.registration.IGuiHandlerRegistration registration) { + MachineJei.gui(registration); + } - @Override - public void registerRecipeTransferHandlers( - mezz.jei.api.registration.IRecipeTransferRegistration registration) { - MachineJei.transfer(registration); - } + @Override + public void registerRecipeTransferHandlers( + mezz.jei.api.registration.IRecipeTransferRegistration registration) { + MachineJei.transfer(registration); + } - @Override - public void onRuntimeAvailable(mezz.jei.api.runtime.IJeiRuntime runtime) { - MachineJei.runtime(runtime); - } + @Override + public void onRuntimeAvailable(mezz.jei.api.runtime.IJeiRuntime runtime) { + MachineJei.runtime(runtime); + } - @Override - public void onRuntimeUnavailable() { - MachineJei.unavailable(); - } + @Override + public void onRuntimeUnavailable() { + MachineJei.unavailable(); + } - @Override - public ResourceLocation getPluginUid() { - return ResourceLocation.parse("mmdlib:jei"); - } + @Override + public ResourceLocation getPluginUid() { + return ResourceLocation.parse("mmdlib:jei"); + } - @Override - public void registerCategories(IRecipeCategoryRegistration registration) { - registration.addRecipeCategories( - new CrushingCategory(registration.getJeiHelpers().getGuiHelper())); - var level = Minecraft.getInstance().level; - for (var type : machineRecipeTypes()) - registration.addRecipeCategories( - MachineRecipeCategory.create( - type, - level == null - ? List.of() - : MachineRecipeCategory.shown(type.recipes(level.getRecipeManager())))); - } + @Override + public void registerCategories(IRecipeCategoryRegistration registration) { + registration.addRecipeCategories( + new CrushingCategory(registration.getJeiHelpers().getGuiHelper())); + var level = Minecraft.getInstance().level; + for (var type : machineRecipeTypes()) + registration.addRecipeCategories(MachineRecipeCategory.create(type, + level == null + ? List.of() + : MachineRecipeCategory + .shown(type.recipes(level.getRecipeManager())))); + } - @Override - public void registerRecipes(IRecipeRegistration registration) { - var level = Minecraft.getInstance().level; - registration.addRecipes( - CrushingCategory.TYPE, - level == null - ? List.of() - : level.getRecipeManager().getAllRecipesFor(CrushingRecipe.TYPE.get())); - if (level != null) - for (var type : machineRecipeTypes()) - registration.addRecipes( - MachineRecipeCategory.recipeType(type), - MachineRecipeCategory.shown(type.recipes(level.getRecipeManager()))); - } + @Override + public void registerRecipes(IRecipeRegistration registration) { + var level = Minecraft.getInstance().level; + registration.addRecipes(CrushingCategory.TYPE, + level == null + ? List.of() + : level.getRecipeManager() + .getAllRecipesFor(CrushingRecipe.TYPE.get())); + if (level != null) + for (var type : machineRecipeTypes()) + registration.addRecipes(MachineRecipeCategory.recipeType(type), + MachineRecipeCategory + .shown(type.recipes(level.getRecipeManager()))); + } - /** Every registered machine recipe type, whichever mod registered it. */ - private static List machineRecipeTypes() { - return BuiltInRegistries.RECIPE_TYPE.stream() - .filter(MachineRecipeType.class::isInstance) - .map(MachineRecipeType.class::cast) - .toList(); - } + /** Every registered machine recipe type, whichever mod registered it. */ + private static List machineRecipeTypes() { + return BuiltInRegistries.RECIPE_TYPE.stream() + .filter(MachineRecipeType.class::isInstance) + .map(MachineRecipeType.class::cast).toList(); + } - /** - * Catalysts are found by binding, not class: any item bound to a crackhammer handler, addon items - * included. - */ - @Override - public void registerRecipeCatalysts(IRecipeCatalystRegistration registration) { - zone.moddev.mc.lib.machine.neoforge.MachineContent.definitions() - .forEach( - (id, machine) -> - registration.addRecipeCatalyst( - new net.minecraft.world.item.ItemStack(BuiltInRegistries.ITEM.get(id)), - MachineRecipeCategory.recipeType( - zone.moddev.mc.lib.machine.neoforge.MachineContent.recipeType( - machine.recipeType().id())))); - var materials = MMDLib.itemMaterials(); - MMDLib.itemDefinitions() - .forEach( - (id, definition) -> { - var material = materials.get(id); - boolean crackhammer = - CrackhammerEvents.CRUSH.equals( - ItemEventBridge.binding( - definition.events(), - material, - MaterialEventTypes.TOOL_RIGHT_CLICK.name())) - || CrackhammerEvents.HARVEST.equals( - ItemEventBridge.binding( - definition.events(), - material, - MaterialEventTypes.TOOL_HARVEST.name())); - if (crackhammer) - BuiltInRegistries.ITEM - .getOptional(id) - .ifPresent( - item -> registration.addRecipeCatalysts(CrushingCategory.TYPE, item)); - }); - } + /** + * Catalysts are found by binding, not class: any item bound to a + * crackhammer handler, addon items included. + */ + @Override + public void registerRecipeCatalysts( + IRecipeCatalystRegistration registration) { + zone.moddev.mc.lib.machine.neoforge.MachineContent.definitions() + .forEach((id, machine) -> registration.addRecipeCatalyst( + new net.minecraft.world.item.ItemStack( + BuiltInRegistries.ITEM.get(id)), + MachineRecipeCategory.recipeType( + zone.moddev.mc.lib.machine.neoforge.MachineContent + .recipeType(machine.recipeType().id())))); + var materials = MMDLib.itemMaterials(); + MMDLib.itemDefinitions().forEach((id, definition) -> { + var material = materials.get(id); + boolean crackhammer = CrackhammerEvents.CRUSH + .equals(ItemEventBridge.binding(definition.events(), material, + MaterialEventTypes.TOOL_RIGHT_CLICK.name())) + || CrackhammerEvents.HARVEST + .equals(ItemEventBridge.binding(definition.events(), + material, MaterialEventTypes.TOOL_HARVEST.name())); + if (crackhammer) + BuiltInRegistries.ITEM.getOptional(id) + .ifPresent(item -> registration + .addRecipeCatalysts(CrushingCategory.TYPE, item)); + }); + } } diff --git a/src/main/java/zone/moddev/mc/lib/client/jei/MachineJei.java b/src/main/java/zone/moddev/mc/lib/client/jei/MachineJei.java index 81278165..bd712207 100644 --- a/src/main/java/zone/moddev/mc/lib/client/jei/MachineJei.java +++ b/src/main/java/zone/moddev/mc/lib/client/jei/MachineJei.java @@ -49,7 +49,8 @@ private static Rect2i rect(Rect r) { static void runtime(IJeiRuntime value) { runtime = value; - // JEI's fixed bottom search/configuration bar does not avoid container exclusion areas. + // JEI's fixed bottom search/configuration bar does not avoid container + // exclusion areas. MachineScreen.drawerBottomInset = () -> runtime != null && runtime.getIngredientListOverlay().isListDisplayed() ? 28 : 4; MachineScreen.recipes = (screen, e) -> { @@ -59,7 +60,8 @@ static void runtime(IJeiRuntime value) { runtime.getRecipesGui().showTypes(types); return true; }; - MachineScreen.recipeTooltip = (screen, e) -> !types(screen, e).isEmpty(); + MachineScreen.recipeTooltip = (screen, + e) -> !types(screen, e).isEmpty(); } static void unavailable() { @@ -77,17 +79,19 @@ private static List> types(MachineScreen screen, Element e) { var type = runtime.getRecipeManager().getRecipeType(id); if (type.isPresent()) result.add(type.get()); - else if (UNKNOWN.add(screen.getMenu().bound.screen().source() + ":" + id)) - MMDLib.LOGGER.warn( - "Unknown JEI category {} in {}", id, screen.getMenu().bound.screen().source()); + else if (UNKNOWN + .add(screen.getMenu().bound.screen().source() + ":" + id)) + MMDLib.LOGGER.warn("Unknown JEI category {} in {}", id, + screen.getMenu().bound.screen().source()); } return result; } - // JEI draws its own "Show Recipes" tooltip for the area; merging the element's lines into it + // JEI draws its own "Show Recipes" tooltip for the area; merging the + // element's lines into it // keeps the two from being drawn over each other. - private static IGuiClickableArea clickable( - IGuiClickableArea basic, MachineScreen screen, Element e, int mx, int my) { + private static IGuiClickableArea clickable(IGuiClickableArea basic, + MachineScreen screen, Element e, int mx, int my) { return new IGuiClickableArea() { @Override public Rect2i getArea() { @@ -108,12 +112,12 @@ public void getTooltip(ITooltipBuilder tooltip) { } static void gui(IGuiHandlerRegistration registration) { - registration.addGuiContainerHandler( - MachineScreen.class, + registration.addGuiContainerHandler(MachineScreen.class, new IGuiContainerHandler() { @Override public List getGuiExtraAreas(MachineScreen screen) { - return screen.exclusions().stream().map(MachineJei::rect).toList(); + return screen.exclusions().stream().map(MachineJei::rect) + .toList(); } @Override @@ -121,47 +125,46 @@ public Collection getGuiClickableAreas( MachineScreen screen, double mx, double my) { var result = new ArrayList(); for (var e : screen.getMenu().elements) - if (screen.visible(e) && !e.common().jeiRecipes().isEmpty()) { + if (screen.visible(e) + && !e.common().jeiRecipes().isEmpty()) { var types = types(screen, e); var r = screen.layout.bounds(e); if (!types.isEmpty()) { - var basic = IGuiClickableArea.createBasic( - r.x(), r.y(), r.width(), r.height(), + var basic = IGuiClickableArea.createBasic(r.x(), + r.y(), r.width(), r.height(), types.toArray(RecipeType[]::new)); - result.add(clickable(basic, screen, e, (int) mx, (int) my)); + result.add(clickable(basic, screen, e, (int) mx, + (int) my)); } } return result; } }); - registration.addGhostIngredientHandler( - MachineScreen.class, + registration.addGhostIngredientHandler(MachineScreen.class, new IGhostIngredientHandler() { @Override - public List> getTargetsTyped( - MachineScreen screen, ITypedIngredient ingredient, boolean start) { - var type = JeiIngredientTypes.GLOBAL.find(ingredient.getType()).orElse(null); + public List> getTargetsTyped(MachineScreen screen, + ITypedIngredient ingredient, boolean start) { + var type = JeiIngredientTypes.GLOBAL + .find(ingredient.getType()).orElse(null); if (type == null) return List.of(); var result = new ArrayList>(); for (var target : IngredientDrops.targets( - screen.presentation(), - screen.getMenu().elements, - type, + screen.presentation(), screen.getMenu().elements, type, ingredient.getIngredient())) { - result.add( - new Target<>() { - public Rect2i getArea() { - return rect(target.bounds()); - } + result.add(new Target<>() { + public Rect2i getArea() { + return rect(target.bounds()); + } - public void accept(I value) { - if (runtime != null - && net.minecraft.client.Minecraft - .getInstance().screen == screen) - target.accept().test(value); - } - }); + public void accept(I value) { + if (runtime != null + && net.minecraft.client.Minecraft + .getInstance().screen == screen) + target.accept().test(value); + } + }); } return result; } @@ -184,30 +187,24 @@ public Optional> getMenuType() { return Optional.of(ScreenRuntime.MENUS.get()); } - public IRecipeTransferError transferRecipe( - MachineMenu menu, - Object recipe, - IRecipeSlotsView slots, - Player player, - boolean max, - boolean transfer) { - if (runtime == null || !(menu.blockEntity instanceof ScreenHost host)) + public IRecipeTransferError transferRecipe(MachineMenu menu, + Object recipe, IRecipeSlotsView slots, Player player, + boolean max, boolean transfer) { + if (runtime == null + || !(menu.blockEntity instanceof ScreenHost host)) return helper.createInternalError(); var categories = menu.elements.stream() .flatMap(e -> e.common().jeiRecipes().stream()) - .distinct() - .toList(); + .distinct().toList(); for (var id : categories) { var type = runtime.getRecipeManager().getRecipeType(id); if (type.isPresent() && !host.screenRecipeItems(id).isEmpty() - && runtime - .getRecipeManager() - .createRecipeLookup(type.get()) - .get() + && runtime.getRecipeManager() + .createRecipeLookup(type.get()).get() .anyMatch(recipe::equals)) - return delegate(type.get(), helper, menu, recipe, slots, player, max, - transfer); + return delegate(type.get(), helper, menu, recipe, + slots, player, max, transfer); } return helper.createInternalError(); } @@ -215,15 +212,9 @@ public IRecipeTransferError transferRecipe( } @SuppressWarnings("removal") - private static IRecipeTransferError delegate( - RecipeType type, - IRecipeTransferHandlerHelper helper, - MachineMenu menu, - Object recipe, - IRecipeSlotsView slots, - Player player, - boolean max, - boolean transfer) { + private static IRecipeTransferError delegate(RecipeType type, + IRecipeTransferHandlerHelper helper, MachineMenu menu, Object recipe, + IRecipeSlotsView slots, Player player, boolean max, boolean transfer) { var info = new IRecipeTransferInfo() { public Class getContainerClass() { return MachineMenu.class; @@ -248,10 +239,8 @@ public List getRecipeSlots(MachineMenu menu, R recipe) { for (var address : host.screenRecipeItems(type.getUid())) { int menuIndex = menu.menuSlot(address); var element = menu.slotElement(menuIndex); - if (element == null - || element.ghost() - || !menu.visible(element) - || menuIndex < 0 + if (element == null || element.ghost() + || !menu.visible(element) || menuIndex < 0 || menuIndex >= menu.slots.size()) return List.of(); result.add(menu.slots.get(menuIndex)); @@ -261,15 +250,13 @@ public List getRecipeSlots(MachineMenu menu, R recipe) { public List getInventorySlots(MachineMenu menu, R recipe) { return menu.slots.stream() - .filter( - s -> s.container == player.getInventory() - && s.getContainerSlot() < 36 - && s.isActive()) + .filter(s -> s.container == player.getInventory() + && s.getContainerSlot() < 36 && s.isActive()) .toList(); } }; - return helper - .createUnregisteredRecipeTransferHandler(info) - .transferRecipe(menu, type.getRecipeClass().cast(recipe), slots, player, max, transfer); + return helper.createUnregisteredRecipeTransferHandler(info) + .transferRecipe(menu, type.getRecipeClass().cast(recipe), slots, + player, max, transfer); } } diff --git a/src/main/java/zone/moddev/mc/lib/client/jei/MachineRecipeCategory.java b/src/main/java/zone/moddev/mc/lib/client/jei/MachineRecipeCategory.java index 901f0ed6..b7e30cfa 100644 --- a/src/main/java/zone/moddev/mc/lib/client/jei/MachineRecipeCategory.java +++ b/src/main/java/zone/moddev/mc/lib/client/jei/MachineRecipeCategory.java @@ -1,19 +1,13 @@ package zone.moddev.mc.lib.client.jei; import java.text.NumberFormat; + import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.List; import java.util.Locale; -import net.minecraft.client.Minecraft; -import net.minecraft.core.Holder; -import net.minecraft.core.HolderSet; -import net.minecraft.network.chat.Component; -import net.minecraft.world.item.crafting.RecipeHolder; -import net.minecraft.world.level.material.Fluid; - import mezz.jei.api.gui.builder.IRecipeLayoutBuilder; import mezz.jei.api.gui.builder.IRecipeSlotBuilder; import mezz.jei.api.gui.widgets.IRecipeExtrasBuilder; @@ -22,6 +16,13 @@ import mezz.jei.api.recipe.RecipeType; import mezz.jei.api.recipe.category.AbstractRecipeCategory; +import net.minecraft.client.Minecraft; +import net.minecraft.core.Holder; +import net.minecraft.core.HolderSet; +import net.minecraft.network.chat.Component; +import net.minecraft.world.item.crafting.RecipeHolder; +import net.minecraft.world.level.material.Fluid; + import zone.moddev.mc.lib.machine.ingredient.MachineIngredient; import zone.moddev.mc.lib.machine.recipe.MachineProduct; import zone.moddev.mc.lib.machine.recipe.MachineRecipe; @@ -29,195 +30,210 @@ import zone.moddev.mc.lib.machine.recipe.MachineRecipeType; /** - * The JEI category MMDLib registers for every {@link MachineRecipeType}: item and fluid inputs, an - * arrow, the products, then the energy and the time. It is sized for the largest recipe loaded. Its - * title is the translation {@code jei..} of the type's ID. A mod adds its machines - * as catalysts from its own JEI plugin, with {@link #recipeType}. Recipes that can never run, such - * as one whose input is an empty tag from an optional mod, are left out. + * The JEI category MMDLib registers for every {@link MachineRecipeType}: item + * and fluid inputs, an arrow, the products, then the energy and the time. It is + * sized for the largest recipe loaded. Its title is the translation + * {@code jei..} of the type's ID. A mod adds its machines as + * catalysts from its own JEI plugin, with {@link #recipeType}. Recipes that can + * never run, such as one whose input is an empty tag from an optional mod, are + * left out. */ public final class MachineRecipeCategory - extends AbstractRecipeCategory> { - private static final int CELL = 18, COLUMNS = 3, ARROW = 24, GAP = 4, TEXT = 10; - private static final int TEXT_COLOR = 0xFF808080; - - private final int inputColumns, rows, contentX; - - private MachineRecipeCategory( - MachineRecipeType type, int inputColumns, int outputColumns, int rows, int textWidth) { - super( - recipeType(type), - Component.translatableWithFallback( - "jei." + type.id().getNamespace() + "." + type.id().getPath(), type.id().toString()), - null, - Math.max(inputColumns * CELL + GAP + ARROW + GAP + outputColumns * CELL, textWidth), - rows * CELL + TEXT); - this.inputColumns = inputColumns; - this.rows = rows; - contentX = (getWidth() - (inputColumns * CELL + GAP + ARROW + GAP + outputColumns * CELL)) / 2; - } - - /** The JEI recipe type of {@code type}, for catalysts and recipe lookups. */ - public static RecipeType> recipeType(MachineRecipeType type) { - return RecipeType.createRecipeHolderType(type.id()); - } - - /** The recipes to show: those whose every input names at least one item or fluid. */ - static List> shown( - Collection> recipes) { - return recipes.stream() - .filter( - holder -> - holder.value().recipe().inputs().values().stream() - .flatMap(List::stream) - .allMatch( - part -> - switch (part) { - case MachineIngredient.ItemPart items -> !items.items().hasNoItems(); - case MachineIngredient.FluidPart fluids -> fluids.fluids().size() > 0; - case MachineIngredient.EnergyPart energy -> true; - })) - .toList(); - } - - /** A category for {@code type}, wide and tall enough for every recipe in {@code recipes}. */ - static MachineRecipeCategory create( - MachineRecipeType type, Collection> recipes) { - int inputs = 1, outputs = 1, textWidth = 0; - for (var holder : recipes) { - inputs = Math.max(inputs, inputSlots(holder.value().recipe())); - outputs = Math.max(outputs, outputSlots(holder.value().recipe())); - textWidth = - Math.max(textWidth, Minecraft.getInstance().font.width(details(holder.value().recipe()))); + extends + AbstractRecipeCategory> { + private static final int CELL = 18, COLUMNS = 3, ARROW = 24, GAP = 4, + TEXT = 10; + private static final int TEXT_COLOR = 0xFF808080; + + private final int inputColumns, rows, contentX; + + private MachineRecipeCategory(MachineRecipeType type, int inputColumns, + int outputColumns, int rows, int textWidth) { + super(recipeType(type), + Component.translatableWithFallback( + "jei." + type.id().getNamespace() + "." + type.id().getPath(), + type.id().toString()), + null, + Math.max( + inputColumns * CELL + GAP + ARROW + GAP + outputColumns * CELL, + textWidth), + rows * CELL + TEXT); + this.inputColumns = inputColumns; + this.rows = rows; + contentX = (getWidth() + - (inputColumns * CELL + GAP + ARROW + GAP + outputColumns * CELL)) + / 2; } - int inputColumns = Math.min(COLUMNS, inputs), outputColumns = Math.min(COLUMNS, outputs); - int rows = Math.max(ceil(inputs, inputColumns), ceil(outputs, outputColumns)); - return new MachineRecipeCategory(type, inputColumns, outputColumns, rows, textWidth); - } - - @Override - public void setRecipe( - IRecipeLayoutBuilder builder, RecipeHolder holder, IFocusGroup focuses) { - var recipe = holder.value().recipe(); - int index = 0; - for (var parts : recipe.inputs().values()) - for (var part : parts) { - switch (part) { - case MachineIngredient.ItemPart items -> - slot(builder, RecipeIngredientRole.INPUT, contentX, index++) - .addItemStacks( - Arrays.stream(items.items().getItems()) - .map(stack -> stack.copyWithCount(items.count())) - .toList()); - case MachineIngredient.FluidPart fluids -> { - var slot = slot(builder, RecipeIngredientRole.INPUT, contentX, index++); - for (var fluid : sources(fluids.fluids())) - slot.addFluidStack(fluid.value(), fluids.millibuckets()); - slot.setFluidRenderer(fluids.millibuckets(), false, 16, 16); - } - case MachineIngredient.EnergyPart energy -> {} - } - } - int outputX = contentX + inputColumns * CELL + GAP + ARROW + GAP; - index = 0; - for (var products : recipe.outputs().values()) - for (var product : products) { - var slot = slot(builder, RecipeIngredientRole.OUTPUT, outputX, index++); - switch (product) { - case MachineProduct.ItemProduct item -> slot.addItemStack(item.stack()); - case MachineProduct.FluidProduct fluid -> { - slot.addFluidStack( - fluid.fluid().fluid().value(), fluid.fluid().amount(), fluid.fluid().components()); - slot.setFluidRenderer(fluid.fluid().amount(), false, 16, 16); - } + + /** + * The JEI recipe type of {@code type}, for catalysts and recipe lookups. + */ + public static RecipeType> recipeType( + MachineRecipeType type) { + return RecipeType.createRecipeHolderType(type.id()); + } + + /** + * The recipes to show: those whose every input names at least one item or + * fluid. + */ + static List> shown( + Collection> recipes) { + return recipes.stream() + .filter(holder -> holder.value().recipe().inputs().values().stream() + .flatMap(List::stream).allMatch(part -> switch (part) { + case MachineIngredient.ItemPart items -> + !items.items().hasNoItems(); + case MachineIngredient.FluidPart fluids -> + fluids.fluids().size() > 0; + case MachineIngredient.EnergyPart energy -> true; + })) + .toList(); + } + + /** + * A category for {@code type}, wide and tall enough for every recipe in + * {@code recipes}. + */ + static MachineRecipeCategory create(MachineRecipeType type, + Collection> recipes) { + int inputs = 1, outputs = 1, textWidth = 0; + for (var holder : recipes) { + inputs = Math.max(inputs, inputSlots(holder.value().recipe())); + outputs = Math.max(outputs, outputSlots(holder.value().recipe())); + textWidth = Math.max(textWidth, Minecraft.getInstance().font + .width(details(holder.value().recipe()))); } - if (product.chance() < 1) - slot.addRichTooltipCallback( - (view, tooltip) -> - tooltip.add( - Component.translatable( - "jei.mmdlib.machine.chance", percent(product.chance())))); - } - } - - @Override - public void createRecipeExtras( - IRecipeExtrasBuilder builder, RecipeHolder holder, IFocusGroup focuses) { - var recipe = holder.value().recipe(); - builder - .addRecipeArrowWidget() - .setPosition(contentX + inputColumns * CELL + GAP, (rows * CELL - 17) / 2); - builder - .addText(details(recipe), getWidth(), TEXT) - .setPosition(0, rows * CELL + 1) - .setColor(TEXT_COLOR); - } - - private static Component details(MachineRecipe recipe) { - var lines = new ArrayList(); - long energy = - recipe.inputs().values().stream() - .flatMap(List::stream) + int inputColumns = Math.min(COLUMNS, inputs), + outputColumns = Math.min(COLUMNS, outputs); + int rows = Math.max(ceil(inputs, inputColumns), + ceil(outputs, outputColumns)); + return new MachineRecipeCategory(type, inputColumns, outputColumns, + rows, textWidth); + } + + @Override + public void setRecipe(IRecipeLayoutBuilder builder, + RecipeHolder holder, IFocusGroup focuses) { + var recipe = holder.value().recipe(); + int index = 0; + for (var parts : recipe.inputs().values()) + for (var part : parts) { + switch (part) { + case MachineIngredient.ItemPart items -> slot(builder, + RecipeIngredientRole.INPUT, contentX, index++) + .addItemStacks(Arrays.stream(items.items().getItems()) + .map(stack -> stack.copyWithCount(items.count())) + .toList()); + case MachineIngredient.FluidPart fluids -> { + var slot = slot(builder, RecipeIngredientRole.INPUT, + contentX, index++); + for (var fluid : sources(fluids.fluids())) + slot.addFluidStack(fluid.value(), + fluids.millibuckets()); + slot.setFluidRenderer(fluids.millibuckets(), false, 16, + 16); + } + case MachineIngredient.EnergyPart energy -> { + } + } + } + int outputX = contentX + inputColumns * CELL + GAP + ARROW + GAP; + index = 0; + for (var products : recipe.outputs().values()) + for (var product : products) { + var slot = slot(builder, RecipeIngredientRole.OUTPUT, outputX, + index++); + switch (product) { + case MachineProduct.ItemProduct item -> + slot.addItemStack(item.stack()); + case MachineProduct.FluidProduct fluid -> { + slot.addFluidStack(fluid.fluid().fluid().value(), + fluid.fluid().amount(), fluid.fluid().components()); + slot.setFluidRenderer(fluid.fluid().amount(), false, 16, + 16); + } + } + if (product.chance() < 1) + slot.addRichTooltipCallback((view, tooltip) -> tooltip + .add(Component.translatable("jei.mmdlib.machine.chance", + percent(product.chance())))); + } + } + + @Override + public void createRecipeExtras(IRecipeExtrasBuilder builder, + RecipeHolder holder, IFocusGroup focuses) { + var recipe = holder.value().recipe(); + builder.addRecipeArrowWidget().setPosition( + contentX + inputColumns * CELL + GAP, (rows * CELL - 17) / 2); + builder.addText(details(recipe), getWidth(), TEXT) + .setPosition(0, rows * CELL + 1).setColor(TEXT_COLOR); + } + + private static Component details(MachineRecipe recipe) { + var lines = new ArrayList(); + long energy = recipe.inputs().values().stream().flatMap(List::stream) .filter(MachineIngredient.EnergyPart.class::isInstance) - .mapToLong(MachineIngredient::amount) - .sum(); - if (energy > 0) - lines.add( - Component.translatable( - "jei.mmdlib.machine.energy", - NumberFormat.getIntegerInstance(Locale.ROOT).format(energy))); - lines.add(Component.translatable("jei.mmdlib.machine.time", seconds(recipe.time()))); - return joined(lines); - } - - private IRecipeSlotBuilder slot( - IRecipeLayoutBuilder builder, RecipeIngredientRole role, int originX, int index) { - return builder - .addSlot(role, originX + index % COLUMNS * CELL + 1, index / COLUMNS * CELL + 1) - .setStandardSlotBackground(); - } - - /** The source fluids of {@code fluids}, as a tag also lists flowing ones; all if none are. */ - private static List> sources(HolderSet fluids) { - var sources = - fluids.stream().filter(fluid -> fluid.value().isSource(fluid.value().defaultFluidState())); - var list = sources.toList(); - return list.isEmpty() ? fluids.stream().toList() : list; - } - - private static int inputSlots(MachineRecipe recipe) { - return (int) - recipe.inputs().values().stream() - .flatMap(List::stream) + .mapToLong(MachineIngredient::amount).sum(); + if (energy > 0) + lines.add(Component.translatable("jei.mmdlib.machine.energy", + NumberFormat.getIntegerInstance(Locale.ROOT).format(energy))); + lines.add(Component.translatable("jei.mmdlib.machine.time", + seconds(recipe.time()))); + return joined(lines); + } + + private IRecipeSlotBuilder slot(IRecipeLayoutBuilder builder, + RecipeIngredientRole role, int originX, int index) { + return builder.addSlot(role, originX + index % COLUMNS * CELL + 1, + index / COLUMNS * CELL + 1).setStandardSlotBackground(); + } + + /** + * The source fluids of {@code fluids}, as a tag also lists flowing ones; + * all if none are. + */ + private static List> sources(HolderSet fluids) { + var sources = fluids.stream().filter( + fluid -> fluid.value().isSource(fluid.value().defaultFluidState())); + var list = sources.toList(); + return list.isEmpty() ? fluids.stream().toList() : list; + } + + private static int inputSlots(MachineRecipe recipe) { + return (int) recipe.inputs().values().stream().flatMap(List::stream) .filter(part -> !(part instanceof MachineIngredient.EnergyPart)) .count(); - } - - private static int outputSlots(MachineRecipe recipe) { - return recipe.outputs().values().stream().mapToInt(List::size).sum(); - } - - private static int ceil(int count, int columns) { - return Math.max(1, (count + columns - 1) / columns); - } - - private static String percent(float chance) { - var format = NumberFormat.getNumberInstance(Locale.ROOT); - format.setMaximumFractionDigits(1); - return format.format(chance * 100); - } - - private static String seconds(int ticks) { - var format = NumberFormat.getNumberInstance(Locale.ROOT); - format.setMaximumFractionDigits(2); - return format.format(ticks / 20.0); - } - - private static Component joined(List parts) { - var result = Component.empty(); - for (int i = 0; i < parts.size(); i++) { - if (i > 0) result.append(" · "); - result.append(parts.get(i)); } - return result; - } + + private static int outputSlots(MachineRecipe recipe) { + return recipe.outputs().values().stream().mapToInt(List::size).sum(); + } + + private static int ceil(int count, int columns) { + return Math.max(1, (count + columns - 1) / columns); + } + + private static String percent(float chance) { + var format = NumberFormat.getNumberInstance(Locale.ROOT); + format.setMaximumFractionDigits(1); + return format.format(chance * 100); + } + + private static String seconds(int ticks) { + var format = NumberFormat.getNumberInstance(Locale.ROOT); + format.setMaximumFractionDigits(2); + return format.format(ticks / 20.0); + } + + private static Component joined(List parts) { + var result = Component.empty(); + for (int i = 0; i < parts.size(); i++) { + if (i > 0) + result.append(" · "); + result.append(parts.get(i)); + } + return result; + } } diff --git a/src/main/java/zone/moddev/mc/lib/client/package-info.java b/src/main/java/zone/moddev/mc/lib/client/package-info.java index 2970be36..987e76b7 100644 --- a/src/main/java/zone/moddev/mc/lib/client/package-info.java +++ b/src/main/java/zone/moddev/mc/lib/client/package-info.java @@ -1,2 +1,5 @@ -/** Client-only setup for registered material content: tints, renderers, render layers. */ +/** + * Client-only setup for registered material content: tints, renderers, render + * layers. + */ package zone.moddev.mc.lib.client; diff --git a/src/main/java/zone/moddev/mc/lib/common/builders/BlockBuilder.java b/src/main/java/zone/moddev/mc/lib/common/builders/BlockBuilder.java index 8a5c68e1..89a1b9d3 100644 --- a/src/main/java/zone/moddev/mc/lib/common/builders/BlockBuilder.java +++ b/src/main/java/zone/moddev/mc/lib/common/builders/BlockBuilder.java @@ -10,430 +10,291 @@ import zone.moddev.mc.lib.common.types.MMDBlock; /** - * Builds {@link zone.moddev.mc.lib.common.types.MMDBlock} definitions. Start from {@link - * #create()}; every setter returns a new builder. A block is named {@code _