From 25ccdec7bebd26bd0e5069f8a3ef393a79b9cd54 Mon Sep 17 00:00:00 2001 From: Mikaaah <59295501+Mikaaah@users.noreply.github.com> Date: Fri, 30 Oct 2026 16:41:12 +0100 Subject: [PATCH 1/6] Apply TNK slate/copper controller GUI rework (0.1.6) --- .github/workflows/verify.yml | 4 +- CHANGELOG.md | 11 ++ build.gradle | 4 +- .../client/ControllerScreen.java | 113 +++++++++--------- .../assets/streamcontrol/font/menu.json | 35 ++++++ .../streamcontrol/textures/font/menu.png | Bin 0 -> 1375 bytes .../streamcontrol/textures/gui/apply.png | Bin 0 -> 813 bytes .../streamcontrol/textures/gui/cancel.png | Bin 0 -> 739 bytes .../streamcontrol/textures/gui/compass.png | Bin 0 -> 498 bytes .../streamcontrol/textures/gui/controller.png | Bin 0 -> 3558 bytes .../streamcontrol/textures/gui/handle.png | Bin 0 -> 473 bytes .../streamcontrol/textures/gui/toggle.png | Bin 0 -> 915 bytes .../streamcontrol/textures/gui/track.png | Bin 0 -> 223 bytes src/main/resources/fabric.mod.json | 2 +- tools/build_controller_gui.py | 68 +++++++++++ 15 files changed, 176 insertions(+), 61 deletions(-) create mode 100644 src/main/resources/assets/streamcontrol/font/menu.json create mode 100644 src/main/resources/assets/streamcontrol/textures/font/menu.png create mode 100644 src/main/resources/assets/streamcontrol/textures/gui/apply.png create mode 100644 src/main/resources/assets/streamcontrol/textures/gui/cancel.png create mode 100644 src/main/resources/assets/streamcontrol/textures/gui/compass.png create mode 100644 src/main/resources/assets/streamcontrol/textures/gui/controller.png create mode 100644 src/main/resources/assets/streamcontrol/textures/gui/handle.png create mode 100644 src/main/resources/assets/streamcontrol/textures/gui/toggle.png create mode 100644 src/main/resources/assets/streamcontrol/textures/gui/track.png create mode 100644 tools/build_controller_gui.py diff --git a/.github/workflows/verify.yml b/.github/workflows/verify.yml index c2bb435..3530a45 100644 --- a/.github/workflows/verify.yml +++ b/.github/workflows/verify.yml @@ -1,7 +1,7 @@ name: fabric 26.1.2 build and regression on: push: - branches: ['fabric/26.1.2'] + branches: ["gui/**", 'fabric/26.1.2'] pull_request: branches: ['fabric/26.1.2'] workflow_dispatch: @@ -44,7 +44,7 @@ jobs: if: always() uses: actions/upload-artifact@v4 with: - name: flow-control-fabric-26.1.2-0.1.5 + name: flow-control-fabric-26.1.2-0.1.6 path: | build/libs/*.jar run/streamcontrol-integration-results.json diff --git a/CHANGELOG.md b/CHANGELOG.md index 7991b39..7297ce4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,14 @@ +## 0.1.6 — TNK GUI Rework + +![Flow Control](https://i.imgur.com/JGT2lvz.png) + +- Restyled Current Generator and Stillwater Anchor using the latest TNK Loot / Progression menu design. +- Slate-blue frames, white outer outlines, inset copper panels and a raised title plate. +- Matching textured buttons, compass selections and slider controls, with visible keyboard focus. +- Embedded the supplied mixed-case menu font for titles; retained Minecraft text for settings and values. +- Preserved eight compass directions, normalized diagonal current strength, connected-water mode, redstone behavior and Apply/Cancel. +- Existing saves, config limits and network packet formats remain compatible with 0.1.5. + # Changelog ## 0.1.5 — Eight-way current compass diff --git a/build.gradle b/build.gradle index 29cc972..98c2e41 100644 --- a/build.gradle +++ b/build.gradle @@ -1,5 +1,5 @@ plugins { id 'net.fabricmc.fabric-loom' version '1.15.5'; id 'maven-publish' } -version = '0.1.5' +version = '0.1.6' group='nl.townsandkingdoms.streamcontrol' base.archivesName='streams-flow-control-fabric-26.1.2' repositories { mavenCentral() } @@ -19,4 +19,4 @@ dependencies { tasks.withType(JavaCompile).configureEach { options.encoding='UTF-8'; options.release=25 } tasks.named('jar') { from('LICENSE') } -tasks.named("jar") { archiveFileName = "flow-control-0.1.5-fabric-26.1.2.jar" } +tasks.named("jar") { archiveFileName = "flow-control-0.1.6-fabric-26.1.2.jar" } diff --git a/src/main/java/nl/townsandkingdoms/streamcontrol/client/ControllerScreen.java b/src/main/java/nl/townsandkingdoms/streamcontrol/client/ControllerScreen.java index 29e3d36..ec3f06e 100644 --- a/src/main/java/nl/townsandkingdoms/streamcontrol/client/ControllerScreen.java +++ b/src/main/java/nl/townsandkingdoms/streamcontrol/client/ControllerScreen.java @@ -9,13 +9,21 @@ import net.minecraft.client.gui.screens.Screen; import nl.townsandkingdoms.streamcontrol.FlowHeading; import net.minecraft.network.chat.Component; +import net.minecraft.resources.Identifier; +import net.minecraft.client.renderer.RenderPipelines; +import net.minecraft.network.chat.FontDescription; import nl.townsandkingdoms.streamcontrol.ControlNetwork; -/** One parchment-and-brass visual language for every control, with vanilla input/narration. */ +/** Slate/copper TNK menu artwork, with native input, focus, tooltips and narration. */ public final class ControllerScreen extends Screen { private static final int PANEL_W = 316, PANEL_H = 238; - private static final int INK = 0xFF493326, MUTED = 0xFF806246, CREAM = 0xFFF4DFAE; - private static final int EDGE = 0xFF302018, GOLD = 0xFFC69B58, TEAL = 0xFF337F83; + private static final int INK = 0xFFF4EBE5, MUTED = 0xFFA9C3C8, CREAM = 0xFFF4EBE5; + private static final int EDGE = 0xFF20293F, GOLD = 0xFFC09473, TEAL = 0xFFA9D2CD; + private static final Identifier BACKGROUND = texture("controller"); + private static final Identifier MENU_FONT = Identifier.fromNamespaceAndPath("streamcontrol", "menu"); + private static Identifier texture(String name) { + return Identifier.fromNamespaceAndPath("streamcontrol", "textures/gui/" + name + ".png"); + } private final ControlNetwork.Open snapshot; private int radius; private double speed; @@ -52,7 +60,7 @@ private static Component tr(String key, Object... args) { addRenderableWidget(new SpeedSlider(left + 134, top + 110, 166)); for (FlowHeading d : FlowHeading.values()) { final FlowHeading direction = d; - var button = new ParchmentButton(left + 218 + (d.stepX() + 1) * 28, + var button = new SlateButton(left + 218 + (d.stepX() + 1) * 28, top + 125 + (d.stepZ() + 1) * 15, 26, 14, tr("short_" + d.abbreviation()), b -> facing = direction, false) { @Override protected boolean selected() { return facing == direction; } @@ -61,7 +69,7 @@ private static Component tr(String key, Object... args) { addRenderableWidget(button); } } - var waterToggle = new ParchmentButton(left + 14, top + 174, 286, 22, + var waterToggle = new SlateButton(left + 14, top + 174, 286, 22, tr("connected", tr(connected ? "on" : "off")), b -> { connected = !connected; b.setMessage(tr("connected", tr(connected ? "on" : "off"))); @@ -71,15 +79,15 @@ private static Component tr(String key, Object... args) { g.text(font, tr("connected_label"), getX() + 9, getY() + 7, color, false); right(g, tr(connected ? "on" : "off"), getX() + getWidth() - 50, getY() + 7, color); int x = getX() + getWidth() - 43, y = getY() + 6; - bevel(g, x, y, 32, 11, connected ? TEAL : 0xFF8B7456, false); + bevel(g, x, y, 32, 11, connected ? TEAL : 0xFF344759, false); g.fill(x + (connected ? 21 : 3), y + 3, x + (connected ? 29 : 11), y + 8, CREAM); } }; waterToggle.setTooltip(Tooltip.create(tr("connected_help"))); addRenderableWidget(waterToggle); - addRenderableWidget(new ParchmentButton(left + 14, top + 202, 174, 20, + addRenderableWidget(new SlateButton(left + 14, top + 200, 174, 20, tr("apply"), b -> apply(), true)); - addRenderableWidget(new ParchmentButton(left + 196, top + 202, 104, 20, + addRenderableWidget(new SlateButton(left + 196, top + 200, 104, 20, tr("cancel"), b -> onClose(), false)); } @@ -95,20 +103,21 @@ private void apply() { } @Override public void extractRenderState(GuiGraphicsExtractor g, int mouseX, int mouseY, float partialTick) { - g.fill(0, 0, width, height, 0xA0181412); + g.fill(0, 0, width, height, 0xA0141C2E); drawPanel(g); - g.text(font, title, left + 45, top + 14, CREAM, false); + Component heading = title.copy().withStyle(style -> style.withFont(new FontDescription.Resource(MENU_FONT))); + centered(g, heading, left + PANEL_W / 2, top + 12, CREAM); g.text(font, tr(snapshot.still() ? "calm_subtitle" : "current_subtitle"), - left + 45, top + 28, 0xFFBFA47D, false); - drawSeal(g, left + 16, top + 14); + left + 45, top + 31, MUTED, false); + drawSeal(g, left + 16, top + 23); g.text(font, tr("area_preview"), left + 22, top + 54, INK, false); drawWaterPreview(g); - centered(g, tr("preview_diameter", radius * 2 + 1), left + 66, top + 151, INK); - centered(g, tr("preview_height", snapshot.vertical()), left + 66, top + 162, MUTED); + centered(g, tr("preview_diameter", radius * 2 + 1), left + 66, top + 148, INK); + centered(g, tr("preview_height", snapshot.vertical()), left + 66, top + 159, MUTED); g.text(font, tr("radius_label"), left + 134, top + 54, INK, false); right(g, tr("blocks", radius), left + 300, top + 54, TEAL); if (snapshot.still()) { - bevel(g, left + 134, top + 98, 166, 65, 0xFFCAB487, false); + bevel(g, left + 134, top + 98, 166, 65, 0xFF446077, false); g.text(font, tr("moored"), left + 144, top + 108, INK, false); g.text(font, tr("zero_flow"), left + 144, top + 124, TEAL, false); g.text(font, tr("no_direction"), left + 144, top + 141, MUTED, false); @@ -118,38 +127,27 @@ private void apply() { left + 300, top + 96, TEAL); g.text(font, tr("heading_compact"), left + 134, top + 132, INK, false); g.text(font, tr(facing.key()), left + 134, top + 148, MUTED, false); - bevel(g, left + 246, top + 140, 26, 14, 0xFFB79C6C, false); + bevel(g, left + 246, top + 140, 26, 14, 0xFF304358, false); g.fill(left + 257, top + 144, left + 261, top + 150, GOLD); } // Keep status within the frame, including when redstone disables the controller. centered(g, tr(snapshot.powered() ? "powered_status" : "apply_hint"), - left + PANEL_W / 2, top + 224, snapshot.powered() ? 0xFF88432F : MUTED); + left + PANEL_W / 2, top + 222, snapshot.powered() ? 0xFFFFB093 : MUTED); super.extractRenderState(g, mouseX, mouseY, partialTick); } private void drawPanel(GuiGraphicsExtractor g) { - g.fill(left + 3, top + 5, left + PANEL_W + 3, top + PANEL_H + 5, 0x70000000); - bevel(g, left, top, PANEL_W, PANEL_H, 0xFF795137, true); - bevel(g, left + 5, top + 5, PANEL_W - 10, PANEL_H - 10, 0xFFE0C694, true); - g.fill(left + 8, top + 8, left + PANEL_W - 8, top + 44, 0xFF513A2C); - g.fill(left + 8, top + 44, left + PANEL_W - 8, top + 46, GOLD); - // Discrete, stable parchment flecks: no random flicker or non-pixel gradients. - for (int i = 0; i < 110; i++) { - int x = left + 9 + Math.floorMod(i * 67 + 19, PANEL_W - 18); - int y = top + 48 + Math.floorMod(i * 31 + 7, PANEL_H - 59); - g.fill(x, y, x + 1, y + 1, (i & 1) == 0 ? 0xFFCDB383 : 0xFFECD5A4); - } - for (int x : new int[]{left + 3, left + PANEL_W - 6}) - for (int y : new int[]{top + 3, top + PANEL_H - 6}) { - g.fill(x, y, x + 3, y + 3, GOLD); - g.fill(x, y, x + 1, y + 1, CREAM); - } + g.blit(RenderPipelines.GUI_TEXTURED, BACKGROUND, left, top, 0f, 0f, PANEL_W, PANEL_H, PANEL_W, PANEL_H); + } + + private static void control(GuiGraphicsExtractor g, String name, int x, int y, int w, int h, int state) { + g.blit(RenderPipelines.GUI_TEXTURED, texture(name), x, y, 0f, state * h, w, h, w, h * 5); } private static void bevel(GuiGraphicsExtractor g, int x, int y, int w, int h, int fill, boolean raised) { g.fill(x, y, x + w, y + h, EDGE); - g.fill(x + 1, y + 1, x + w - 1, y + h - 1, raised ? 0xFFE2BD7A : 0xFF8F6D45); - g.fill(x + 2, y + 2, x + w - 1, y + h - 1, raised ? 0xFF67472E : 0xFFF0D8A0); + g.fill(x + 1, y + 1, x + w - 1, y + h - 1, raised ? 0xFFA9C3C8 : 0xFF304358); + g.fill(x + 2, y + 2, x + w - 1, y + h - 1, raised ? 0xFF304358 : 0xFF729AA1); g.fill(x + 2, y + 2, x + w - 2, y + h - 2, fill); } @@ -160,27 +158,26 @@ private void right(GuiGraphicsExtractor g, Component text, int x, int y, int col g.text(font, text, x - font.width(text), y, color, false); } private void drawSeal(GuiGraphicsExtractor g, int x, int y) { - bevel(g, x, y, 22, 23, 0xFF284C4F, true); + bevel(g, x, y, 22, 23, 0xFF446077, true); for (int row = 0; row < 3; row++) { int offset = row == 1 ? 2 : 0; - g.fill(x + 4 + offset, y + 6 + row * 4, x + 15 + offset, y + 7 + row * 4, 0xFF8FCBC0); - g.fill(x + 4 + offset, y + 5 + row * 4, x + 7 + offset, y + 6 + row * 4, 0xFF8FCBC0); + g.fill(x + 4 + offset, y + 6 + row * 4, x + 15 + offset, y + 7 + row * 4, 0xFFA9D2CD); + g.fill(x + 4 + offset, y + 5 + row * 4, x + 7 + offset, y + 6 + row * 4, 0xFFA9D2CD); } } private void drawWaterPreview(GuiGraphicsExtractor g) { int x = left + 22, y = top + 68; - bevel(g, x - 3, y - 3, 88, 81, 0xFF67543B, false); // Schematic only: it communicates circular range, direction, and disconnected pools. for (int row = 0; row < 9; row++) for (int col = 0; col < 9; col++) { int dx = col - 4, dz = row - 4; boolean pool = col >= 7 && row < 4; boolean wet = col < 6 || pool; boolean covered = dx * dx + dz * dz <= 16 && (!connected || !pool); - int color = !wet ? 0xFF9B8157 : covered ? 0xFF5DADB0 : 0xFF396B72; + int color = !wet ? 0xFFB88B68 : covered ? 0xFF729AA1 : 0xFF344759; int tx = x + col * 9, ty = y + row * 8; g.fill(tx, ty, tx + 8, ty + 7, color); - if (wet) g.fill(tx + 2, ty + 2, tx + 6, ty + 3, covered ? 0xFF91CFC8 : 0xFF51858A); + if (wet) g.fill(tx + 2, ty + 2, tx + 6, ty + 3, covered ? 0xFFA9D2CD : 0xFF54768B); } int cx = x + 4 * 9 + 4, cy = y + 4 * 8 + 3; if (snapshot.still()) { @@ -201,9 +198,9 @@ private void drawWaterPreview(GuiGraphicsExtractor g) { } } - private class ParchmentButton extends Button { + private class SlateButton extends Button { private final boolean primary; - ParchmentButton(int x, int y, int w, int h, Component label, OnPress action, boolean primary) { + SlateButton(int x, int y, int w, int h, Component label, OnPress action, boolean primary) { super(x, y, w, h, label, action, DEFAULT_NARRATION); this.primary = primary; } @@ -213,29 +210,33 @@ protected void drawLabel(GuiGraphicsExtractor g, int color) { } @Override public void extractContents(GuiGraphicsExtractor g, int mx, int my, float partialTick) { boolean highlight = isHoveredOrFocused(); - int color = primary || selected() ? (highlight ? 0xFF498F89 : TEAL) - : (highlight ? 0xFFEED4A0 : 0xFFD2B47F); - bevel(g, getX(), getY(), getWidth(), getHeight(), active ? color : 0xFFAB9877, true); + int state = !active ? 4 : primary || selected() ? (highlight ? 3 : 2) : (highlight ? 1 : 0); + String name = getWidth() == 26 ? "compass" : getWidth() == 286 ? "toggle" + : getWidth() == 174 ? "apply" : "cancel"; + control(g, name, getX(), getY(), getWidth(), getHeight(), state); if (highlight) { - g.fill(getX() + 3, getY() + 3, getX() + 5, getY() + getHeight() - 3, GOLD); - g.fill(getX() + getWidth() - 5, getY() + 3, getX() + getWidth() - 3, getY() + getHeight() - 3, GOLD); + int x = getX(), y = getY(), w = getWidth(), h = getHeight(); + g.fill(x, y, x + w, y + 1, CREAM); + g.fill(x, y + h - 1, x + w, y + h, CREAM); + g.fill(x, y, x + 1, y + h, CREAM); + g.fill(x + w - 1, y, x + w, y + h, CREAM); } - drawLabel(g, primary || selected() ? CREAM : INK); + drawLabel(g, active ? CREAM : 0xFF91ABB6); } } - private abstract class ParchmentSlider extends AbstractSliderButton { - ParchmentSlider(int x, int y, int w, double value) { super(x, y, w, 16, Component.empty(), value); } + private abstract class SlateSlider extends AbstractSliderButton { + SlateSlider(int x, int y, int w, double value) { super(x, y, w, 16, Component.empty(), value); } @Override public void extractWidgetRenderState(GuiGraphicsExtractor g, int mx, int my, float partialTick) { int x = getX(), y = getY(), w = getWidth(); - bevel(g, x, y + 4, w, 8, 0xFF9E8157, false); + g.blit(RenderPipelines.GUI_TEXTURED, texture("track"), x, y + 4, 0f, 0f, w, 8, w, 8); int handle = x + 3 + (int)Math.round(value * (w - 12)); g.fill(x + 3, y + 7, handle + 4, y + 9, TEAL); - bevel(g, handle, y + 1, 9, 14, isHoveredOrFocused() ? 0xFFF2D18C : GOLD, true); - g.fill(handle + 4, y + 5, handle + 5, y + 11, 0xFF815830); + control(g, "handle", handle, y + 1, 9, 14, !active ? 4 : isHoveredOrFocused() ? 3 : 2); + g.fill(handle + 4, y + 5, handle + 5, y + 11, 0xFF4D2B32); } } - private final class RadiusSlider extends ParchmentSlider { + private final class RadiusSlider extends SlateSlider { RadiusSlider(int x, int y, int w) { super(x, y, w, (double)(radius - 1) / Math.max(1, snapshot.maxRadius() - 1)); updateMessage(); @@ -246,7 +247,7 @@ private final class RadiusSlider extends ParchmentSlider { updateMessage(); } } - private final class SpeedSlider extends ParchmentSlider { + private final class SpeedSlider extends SlateSlider { SpeedSlider(int x, int y, int w) { super(x, y, w, (speed - 0.05) / Math.max(0.001, snapshot.maxStrength() - 0.05)); updateMessage(); diff --git a/src/main/resources/assets/streamcontrol/font/menu.json b/src/main/resources/assets/streamcontrol/font/menu.json new file mode 100644 index 0000000..11838a6 --- /dev/null +++ b/src/main/resources/assets/streamcontrol/font/menu.json @@ -0,0 +1,35 @@ +{ + "providers": [ + { + "type": "space", + "advances": { + " ": 3, + "\u00a0": 3 + } + }, + { + "type": "bitmap", + "file": "streamcontrol:font/menu.png", + "height": 8, + "ascent": 7, + "chars": [ + "!\"#$%&'()*+,-./0", + "123456789:;<=>?@", + "ABCDEFGHIJKLMNOP", + "QRSTUVWXYZ[\\]^_`", + "abcdefghijklmnop", + "qrstuvwxyz{|}~\u00d7\u00f7", + "\u20ac\u00a3\u2192\u2190\u2191\u2193\u2665\u2713\u2715\u2022\u2026\u2013\u2014\u2019\u2018\u201c", + "\u201d\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000" + ] + }, + { + "type": "reference", + "id": "minecraft:include/default" + }, + { + "type": "reference", + "id": "minecraft:include/unifont" + } + ] +} diff --git a/src/main/resources/assets/streamcontrol/textures/font/menu.png b/src/main/resources/assets/streamcontrol/textures/font/menu.png new file mode 100644 index 0000000000000000000000000000000000000000..ca8ff02199c2ed8e7f4ce496a4d200a2628a272d GIT binary patch literal 1375 zcmV-l1)%zgP)002M;1^@s6or`?0000FhNklc$Pm9`$9X;M9Y078EYEw#@({VGUXOnmpz;{q zSLMACwY47wlz96ngODIZ1KRSpZQEM1&gWNyB9~VKs*mx@`F!=jM(p$a2M6HxgZy(% zVg7H9e1lu^wr$&5!Drw10;gsBNStd(seO>10`!1-xGe)RnF`r3bp});x5q189*@zB zltNZ!O4oxgrIdDcMnE%Q&Tnmq<*XK1C~`lb8T#tpYz17ArwqQ7Qrhp@)I39aCEbHM z^Rc}hlA-OPRAsg+GJ54P!MA+ESl+-z@#Ja{cmduLeE*e#R%$IRxWs39wj3$st;!n- zMPJ6W68X=(n~oNDXN_iiG8NQ%XY@PCV2#^zFV@4$-|4-Nt8sh$(rQbN1N+T7R30lA)eB$4jJRj>p66Es zZ)v8nqm_7Grc~bwSo^M`mdURG+57Hq!!T3pQ-ctN zSEh5WTCbvP=GExBbr}9!M57LD^>9zld?!X^1S@;hOgI~VekbJWLPp(7dGdNnW-fci zp+Wt^_|F?wP&s$t(OD`mALuk62s{EqzOKzeC0jZ8u)`K}ZGx1{ZyEAF>Xdqyyq-W% z*vGs(VgmMfZ10@^+>=@3q4IwJIu`02bihF5GQ$dEAM#waUSsV|cD5hmA59NDMm!rF`Ic84gcY^+}xVxabUwrz#+0)=1q+#EP_A)CQ2yCWq z;RzSzs~!~ezSZ(q_w`iRAXfHRtnRl1UF*;8@5O&NiOYWiN?$M78x z!eO|mJexUMS>!4&e`l0lcFm>dD2L^ie2~9`t{2t)DG6-B<1x+Y%4O-aPi3Qhf5nVs zgRQk@)-T@kt{$)@)GlI0M>9CrqC6np#*H|X$V6@Rd6O&Rm!p6ZZ%2${VHYqf2uBPT zrz~XS(0&Gg?}_NqGJ++)r&VFMw2$KjJw-`B)vJNh{A--AWVp<)t8%J&n1+T6oy zy|degtzL$${rP*n&n-vU6SY3K-pzTrKU&UyL&;R*&z&9`fGCg+@y7W-Mmu~~9xAuR z@v1|V=`!FF#0F|nS)ojMJj+)FD#B&UG!kHF8-V~_ua|Z2J`BreK+9zgiGqvDQ}&eK z2Kx7c4+B(5Z8OxHbjm~IB3y)%2OQfYm*m-gvh+C+2m}JJK&1J=o%p(MLwq|ggAGW^*K^x2Ffe`bba4!+nDh4T{_NmHkz*g9 zPZO(em@RPpl7p9E`#1R&f0<)8y}YsT!p4%0$;}oO6NTn*Np9-ck-Fhgl(7ZpTLse_ zPQ3w&VFE|iJ+qpzr?YIwPQ85jA9c$nyyW3;FMj`9E>FpGGxzD448nK)tUcQn`buZM zscMMXx^zDbHEv;;CvtFUNRZQ}M$Ud+Uw{=xBhD~Pivv_bXW<3mE~=^Ku$ zVDMJlk$!*w(KXB`OpAYd{gBn06mA~2Sd_(QRcOA~B%#i$vK24OZk-Hyqtx*K;5o@P zb8cH!|9qAEWKu)T(obLO%g$BEwsF6Eak$az>Fr7XtSlchFxkD>vEj%>eVtx6Rx7=p z0(tRi5qBF^-vOE%ZwQEzS+}XKvisz-r?d6Zdn0+4nYZjlxcJOXY{Keyc51&wF{P|2= z`_`>{PoMDEedG5x-5)U#Xt__Xtiur4!fs@s;s?`*BXvW@9m%_VcoHRmbC% zr?WiSZ5R%8aNNB;)|d9~oV4WQ?L-!FH=tQA1&N&x4hb3S>0AW*705c+s+txPu?XmY zAPX1)XTbLTSt`DnZ-GGBxrEla#y4(R-uV4_?&j}*vw?o*;+*dSN&-Mf0F%IygNGw; zd3=3x@$w2*#>p%4)&OIs@19nQWU`*#wv{t4Ucc;I;98KFa-}dij8Vh|7|B4}lz>qH zj9i;J2VsE@bjV^RosR`<-X)-rn7gmU6yo2?(qz*zU59mHdo?pR6r1trKUi=(Q++>U Y;XO|Yo{0}lf!Tn;)78&qol`;+08FcO{{R30 literal 0 HcmV?d00001 diff --git a/src/main/resources/assets/streamcontrol/textures/gui/cancel.png b/src/main/resources/assets/streamcontrol/textures/gui/cancel.png new file mode 100644 index 0000000000000000000000000000000000000000..53b1d2fd726323763a8da6d7a697b7e37e631bbc GIT binary patch literal 739 zcmeAS@N?(olHy`uVBq!ia0vp^89tRU|?G1>EaktG3V{w{n^2R630G1 zZxg9+m@RPpl7p9E`!o3!f0<);y}aT0BEss*Nkc+U0t8=t9Jk&_lR>|%MM z@oGWdDXR&}ELT!HHXjj4?{jUkWHJ)xEz}W`x#;+eB)9LgLUG9i z%V!G3YaX5XD3j7Rm!sGc6X*$q58qA$IrW0nS@Rfd7mac zLDlMn3?s9kG-vwxcvW4F!(!9K*VpWB3{SeH;lp#UOq5B8F;c1|;FQ*c zZa$ECKpz<%nVCEh7*YKVp;BANn+mWVcoiq|T9PY*QeDC|Y^J@T1#%65<@*+rXIR}6- z8Jo59g*^KQry@`!5vI7khw1!aS(fL>TdhVU5eATj-1QB0v>>gHY_-w-uhF1T>%Hmu z&4a@uz3W~Cr7}yNLjc9?y)Fbgp&F0`+@+u1M<+xC8$0>STpeENsC5*__9 o^gp_RA+6DwL!+XQ|4vT60EYbiouWd)b^rhX07*qoM6N<$f)py_qyPW_ literal 0 HcmV?d00001 diff --git a/src/main/resources/assets/streamcontrol/textures/gui/controller.png b/src/main/resources/assets/streamcontrol/textures/gui/controller.png new file mode 100644 index 0000000000000000000000000000000000000000..fdd47df0504663255026076621c1efe676487528 GIT binary patch literal 3558 zcmds4SvVWo7EVIVEup1~ASlNWV(t-RX-!29Ra4NSR8UkJb5v@qnyF(tj+RoCnwywg z1XV*3G$e>RhnS}t>ZbSMKHZ1=d|&pr_gddx-(G9|>)ZdIXk~HpJjWFd003~_#Msap z001Hw`*k)J#`+3}&tcfBO$_yIF$Hw8<#VTrOMP35b=pmRnR!+j=2vB*Pq7y(9>*BE z3*2Teff398xo{4g=!qM7JYi&?!gDW?8DwYhMvEja&M#wUCuKy=s}ZK+0fgJh z?TNkjFu(%i@Hq3Q7YI=$766{7cQ3&HVj~pR;t%(YXr|ah`2yFpxAw4`;IP}wMg3?}+gVrN zj3;39S~#^J;&3^9##0k~ixF@4gTAv2=xN{%{;Yq|Qm4`-PUZGFWb@Pn42X5VeGYOH z1GteWx2f69q}1IWBQW;e9xqG<%C67hs@j>>5Ax>HV+5>8j5uNX*_@qqjI07|SI(a& zjC5NVzM7(C@f(xhj0j!E9K^fG6g$z_xMf~6IZz}LpZ4Rsb zJLeom%2D$pvzIWX-Rrbm4Z1vvqSAFG{Q^FBX*BjoaT`*L^Q^ zQEBp#DCFP1Z8OE^Ty0Sj@5a_~{`7bV+O^_-#2Co>J3m-XAOo zw0Kvl**$yhd80C14<#@BwI%J^CjYQvOO-_P-K)%nXD#{ucw=ekdk+KC09O1UMJk?hhq(9@E}Mzy5VRJ6!EhK10>Ur=)W$j$=Ks)vpEEQ&51}DlaW$jOn zJp|_ic?74X(|4%AMUg8AB^9;(j$6d)*OTPB(K1~PbT25UW%$Lba7=TqW6<3|X)T0` z=RGckuWp|54O7e~)^sWv=})9yPl^fl9_XE`Nr4}NeWCu9&9A-V3O$>1oo1vuOZ+Qd zxq#LebO^u(rg&H8z`T`dO%F*-3Ii(g%^r6G%Zs%=N{el?vgUjTOxubQTi3Hqt*wuw z+_rTH(>_NsvELkg{8CeVpQ~}ax`c)nj6wPfFOh9r6x}th(hn}Q@Yqs(HXi>GQ~G%z zd`nGIHjHKD3CCO^Y{vqS8it3&!!6dbV|G&&!U zK5Aaybs~bzw3(qzGzmZSKg(%l%y{ToZ0gSBGo1&D1)1FQ5npCMctz`XSce*fIl0i4 z3H_m_ct7d2qtl%+DFMYoLTM@jnyUNb4O-ARA-i@5Oo*x(3+l%*$z;cVZ~&a!0uzw_ zh^9c@Cpf5fIXwg=d|NC^pPZBN!FXplIs1-NS$RuXB3Uz6FRKbiHho*|0y-M$VM0JL zekJh&yu&`}qmN^c_Y8v$HZONUe_RPz23PzUA0L_h)|1W)kY1s>yE^4FI+VcHm3)E_ zp{8RVrBaNLKf*BA6&*WSUAdFgwzE0AR!}51q_D6y0oyn^* zi@SM)q}W^t1S|RpLfG8|8E|t~cz^u|IVtsP5FY(3=*$`HGU zBWpW1un8v%UYS&%Y&7e~L6CZ?!`@RGldshsb{af!mFfmV6t`nPk zya~GZ0}xPOFFrBwD%Ku!QWDQ9&1j0(gX?(a%Qv_cC3Cp9p1;YFzC|ehT%AI9bW6&$ zab#LmFkN!dl%<*UPKfyw zyGW^dR!`ml$%kxjGVuezHl0BB(sUK~|3zqLB1foLxc6+Sk zVUC7z<0D&H(kRtA_PKkbJ)8;ew!9#F=2?|jdpl-lvl25>TtPacJxpnaq(laUD8yay z@KcX43~+THwHqxDL#;SDG)-BS#S5ez_u(b|k90#_?}rk5yLD7me9HJqOSDY>r9TxB zI&N7ssrCVI>1a=~mF0d^$W=fX=C&+N`%(P06W-w1n*>%I-%OU>^C6p?K~K6b{s>)w zpU5b0nTn(%TR4TNCFmJAMU#Dd7PI$m^r zy&9eOPX+>v4Rk-MEDZZebcMHgmF3tn-k!yS{xmqhdr8MkCcV>LT> z?muuIq$x}XT~>t;s5qT+YC<+waA4HFKWcG?$cw&DfQdo3J34zTK8{PwO+Sl(V_Z4X zuD>43wkU7OH(H^V@MN3XGtgt}K}y`L_SD}}n==|(TrNv-(s?^kljmrK@Pe3#uZ=B# z_xY`09Hf*0f$$`C!OyKC;O z=2b|sSXlbFBJ9$2k@Rn~`qZK1a)OKNQ+HgU)9?WlJrsK_?6GOoVaaP3mNaI?)OZeJ zU8zlw4g8U6bDKn5s3AsmUwe-g3(-=S(g=`cd9l#+d9wgB6U>#bI*B{li$>$RUWnw6 z!MX(Hf`!LlLWTA^>rKPI*Yfhh-Ob&b;x(S2O#NkGR-gR@QXoilN|@!0ud~VdXJOm{ zFNHM5*b8D`=I00Ls!Jlh=uIiLzN%qf59@};pE!Q>^{@F&!@sK1Z~jZuczz$LVHV22 zTsKOp_;j_tyrD<9*mvL{DW&_m^ZETwv*2i?yP2C!!Xjz<&EkJ&`A(0Ye^fL2iADJS@SE%wZMBH4(r(0^vDA< zxAWwdc)vaHxqndDec>a*s}yeEFKtgzfQ|o|2;51q^|(M_uoQD-B+VhWv9s!ydg3J)C z2M-32p-%xCv;4mr;CCn@p)2kVC;LM4jR35@1k=Vp*@L=2HA+}JKl~iJ;^9q+-1{UQ zj->Qo=hl+t1bmwzRu-;Z@vx^b)Qa`l88?SEpw{ayNqhg}4*qAfeJ4YgFnD$IJU0-l zRvNUrs;HQ4$imPalo2MZ6&HtAy0|vsT?fH-;mrlTgt%vg{RzqZI0$y+`7c*_!>L7) y&Ummh9{{5!vu4=avV|dWgRn*({D6f1GhjFyra(dk-oems023n%!x{s(r~d+;=&~UI literal 0 HcmV?d00001 diff --git a/src/main/resources/assets/streamcontrol/textures/gui/handle.png b/src/main/resources/assets/streamcontrol/textures/gui/handle.png new file mode 100644 index 0000000000000000000000000000000000000000..af3d4dde5581054a586eaa5365fa83003e573f1b GIT binary patch literal 473 zcmV;~0Ve*5P)VPG z0RXf}Jmr`*69>2cg+H z{>;~sE##yMjY@qC0pBlJR~`CeIu{(DUP@3nx;WtiW((PAw>j*a6I1rYAr{!gZAOqI zRU-C8kQ?;6!olyjzyN?%eW>iG2SCdtk`4guZm&zg=GM_zE|W+)#e5o%w;c&uS&N}u zZF(+X%n|bX!QwCt_%ks~@YD7mNSff5SN1qJ}L>;887=CoBB``iFAH%)Al`?nd|o?mr(YJ7t6f P00000NkvXXu0mjfb&ts$ literal 0 HcmV?d00001 diff --git a/src/main/resources/assets/streamcontrol/textures/gui/toggle.png b/src/main/resources/assets/streamcontrol/textures/gui/toggle.png new file mode 100644 index 0000000000000000000000000000000000000000..0a8810c977cf4dbc38382aa3052801fb3ea8d53d GIT binary patch literal 915 zcmeAS@N?(olHy`uVBq!ia0y~yV3Y&0^ElXm(+;aa>^;ZH>xwoMl^gM7uckQ0%Q#lo$|GXHeyS64Iluv+(_shXzvtoJv35hFN zk3?oM7Fq$_p;vLHqJhU-7vv)E4-Aq(v2#7_9h+Cfm7Loi}eoDmlK{UiJ9&8t@F+y7sZYkJ_p-T!ZPud?Ew zS6!ZeJ>P%6sUD}quG!neULQPsy!d%V&T{7P7cVzo-c^~sb=AM}h7FOn2ToWxa2(mm z!P8+341;G)%Tpx}%!mNGby}3eoja}xx~CbAxg2LcGV>!tVcFIO3!uZ#1#yD|?fL5x zg?0S%F4xC_+&J;ytII%Jx36QB*ul;(AAa?xwy^j^=7OtovbHb9qQn_{`oq;4W|+s# zw5a@;{&-Tu!s`FWCIN-!%&)7An}082mDYS82HqbjOdXejcC9{uKbLh*2~7Yq@vDOX literal 0 HcmV?d00001 diff --git a/src/main/resources/assets/streamcontrol/textures/gui/track.png b/src/main/resources/assets/streamcontrol/textures/gui/track.png new file mode 100644 index 0000000000000000000000000000000000000000..1bbabdf83f73213e3c9a84f874435c8508e36fd0 GIT binary patch literal 223 zcmV<503iQ~P)MLppwh~_jH)w*YA~-aQD5@*Cz&7Fk zpIgT Date: Tue, 3 Nov 2026 10:17:17 +0100 Subject: [PATCH 2/6] Port Flow Control 0.2.2 engineering to fabric-26.1.2 --- .github/workflows/verify.yml | 30 ++- build.gradle | 8 +- .../streamcontrol/ControlCommands.java | 30 +-- .../streamcontrol/ControlConfig.java | 101 +++++--- .../streamcontrol/ControlNetwork.java | 112 +++++---- .../streamcontrol/ControllerBlock.java | 19 +- .../streamcontrol/ControllerEntity.java | 67 ++---- .../streamcontrol/ControllerSettings.java | 147 ++++++++++++ .../streamcontrol/EngineeringBlockItem.java | 13 ++ .../streamcontrol/EngineeringChecks.java | 116 ++++++++++ .../streamcontrol/FlowController.java | 25 ++ .../streamcontrol/FlowHeading.java | 4 + .../streamcontrol/FlowProfile.java | 38 ++++ .../streamcontrol/FlowRegion.java | 2 +- .../streamcontrol/FlowSensorEntity.java | 80 +++++++ .../streamcontrol/FlowWand.java | 145 +++++++----- .../streamcontrol/IntegrationChecks.java | 17 +- .../streamcontrol/MooringEntity.java | 81 +++++++ .../streamcontrol/StreamControl.java | 14 +- .../streamcontrol/UtilityAccess.java | 8 + .../streamcontrol/UtilityBlock.java | 64 ++++++ .../client/ControllerPreview.java | 59 +++++ .../client/ControllerScreen.java | 170 +++++++++----- .../client/EngineeringClient.java | 94 ++++++++ .../client/EngineeringWidgets.java | 56 +++++ .../client/ModelPreviewScreen.java | 37 +++ .../client/StreamControlClient.java | 3 + .../streamcontrol/client/UtilityScreen.java | 68 ++++++ .../streamcontrol/mixin/BakedFlowMixin.java | 14 -- .../streamcontrol/mixin/ClientGridMixin.java | 7 +- .../streamcontrol/mixin/GridFlowMixin.java | 4 +- .../mixin/SurfaceParticlesMixin.java | 5 +- .../blockstates/flow_sensor.json | 7 + .../blockstates/mooring_post.json | 7 + .../streamcontrol/items/flow_sensor.json | 6 + .../streamcontrol/items/mooring_post.json | 6 + .../assets/streamcontrol/lang/en_us.json | 111 +++++++-- .../models/block/flow_sensor.json | 12 + .../models/block/mooring_post.json | 215 ++++++++++++++++++ .../models/item/flow_sensor.json | 3 + .../models/item/mooring_post.json | 3 + .../textures/block/flow_sensor_bottom.png | Bin 0 -> 661 bytes .../textures/block/flow_sensor_side.png | Bin 0 -> 645 bytes .../textures/block/flow_sensor_top.png | Bin 0 -> 703 bytes .../textures/gui/automation_controller.png | Bin 0 -> 1336 bytes .../textures/gui/current_controller.png | Bin 0 -> 1484 bytes .../textures/gui/stillwater_controller.png | Bin 0 -> 1501 bytes .../textures/gui/track_small.png | Bin 0 -> 221 bytes .../streamcontrol/textures/gui/track_wide.png | Bin 0 -> 232 bytes .../loot_table/blocks/flow_sensor.json | 19 ++ .../loot_table/blocks/mooring_post.json | 19 ++ .../streamcontrol/recipe/flow_sensor.json | 26 +++ .../streamcontrol/recipe/mooring_post.json | 23 ++ src/main/resources/fabric.mod.json | 4 +- src/main/resources/streamcontrol.mixins.json | 1 - 55 files changed, 1773 insertions(+), 327 deletions(-) create mode 100644 src/main/java/nl/townsandkingdoms/streamcontrol/ControllerSettings.java create mode 100644 src/main/java/nl/townsandkingdoms/streamcontrol/EngineeringBlockItem.java create mode 100644 src/main/java/nl/townsandkingdoms/streamcontrol/EngineeringChecks.java create mode 100644 src/main/java/nl/townsandkingdoms/streamcontrol/FlowController.java create mode 100644 src/main/java/nl/townsandkingdoms/streamcontrol/FlowProfile.java create mode 100644 src/main/java/nl/townsandkingdoms/streamcontrol/FlowSensorEntity.java create mode 100644 src/main/java/nl/townsandkingdoms/streamcontrol/MooringEntity.java create mode 100644 src/main/java/nl/townsandkingdoms/streamcontrol/UtilityAccess.java create mode 100644 src/main/java/nl/townsandkingdoms/streamcontrol/UtilityBlock.java create mode 100644 src/main/java/nl/townsandkingdoms/streamcontrol/client/ControllerPreview.java create mode 100644 src/main/java/nl/townsandkingdoms/streamcontrol/client/EngineeringClient.java create mode 100644 src/main/java/nl/townsandkingdoms/streamcontrol/client/EngineeringWidgets.java create mode 100644 src/main/java/nl/townsandkingdoms/streamcontrol/client/ModelPreviewScreen.java create mode 100644 src/main/java/nl/townsandkingdoms/streamcontrol/client/UtilityScreen.java delete mode 100644 src/main/java/nl/townsandkingdoms/streamcontrol/mixin/BakedFlowMixin.java create mode 100644 src/main/resources/assets/streamcontrol/blockstates/flow_sensor.json create mode 100644 src/main/resources/assets/streamcontrol/blockstates/mooring_post.json create mode 100644 src/main/resources/assets/streamcontrol/items/flow_sensor.json create mode 100644 src/main/resources/assets/streamcontrol/items/mooring_post.json create mode 100644 src/main/resources/assets/streamcontrol/models/block/flow_sensor.json create mode 100644 src/main/resources/assets/streamcontrol/models/block/mooring_post.json create mode 100644 src/main/resources/assets/streamcontrol/models/item/flow_sensor.json create mode 100644 src/main/resources/assets/streamcontrol/models/item/mooring_post.json create mode 100644 src/main/resources/assets/streamcontrol/textures/block/flow_sensor_bottom.png create mode 100644 src/main/resources/assets/streamcontrol/textures/block/flow_sensor_side.png create mode 100644 src/main/resources/assets/streamcontrol/textures/block/flow_sensor_top.png create mode 100644 src/main/resources/assets/streamcontrol/textures/gui/automation_controller.png create mode 100644 src/main/resources/assets/streamcontrol/textures/gui/current_controller.png create mode 100644 src/main/resources/assets/streamcontrol/textures/gui/stillwater_controller.png create mode 100644 src/main/resources/assets/streamcontrol/textures/gui/track_small.png create mode 100644 src/main/resources/assets/streamcontrol/textures/gui/track_wide.png create mode 100644 src/main/resources/data/streamcontrol/loot_table/blocks/flow_sensor.json create mode 100644 src/main/resources/data/streamcontrol/loot_table/blocks/mooring_post.json create mode 100644 src/main/resources/data/streamcontrol/recipe/flow_sensor.json create mode 100644 src/main/resources/data/streamcontrol/recipe/mooring_post.json diff --git a/.github/workflows/verify.yml b/.github/workflows/verify.yml index 3530a45..2efdcf6 100644 --- a/.github/workflows/verify.yml +++ b/.github/workflows/verify.yml @@ -1,7 +1,7 @@ name: fabric 26.1.2 build and regression on: push: - branches: ["gui/**", 'fabric/26.1.2'] + branches: ["gui/**", "feature/**", 'fabric/26.1.2'] pull_request: branches: ['fabric/26.1.2'] workflow_dispatch: @@ -21,7 +21,7 @@ jobs: - name: Fetch pinned Streams dependency run: | mkdir -p libs - curl --fail --location --retry 3 'https://cdn.modrinth.com/data/oLS8HdJ1/versions/JUwdeyCO/StreamsReflowing-26.1.2-fabric-2.15.2.jar' -o 'libs/StreamsReflowing-26.1.2-fabric-2.15.2.jar' + curl --fail --location --retry 3 'https://cdn.modrinth.com/data/oLS8HdJ1/versions/xNMwfkfl/StreamsReflowing-26.1.2-fabric-2.16.2.jar' -o 'libs/StreamsReflowing-26.1.2-fabric-2.16.2.jar' - name: Build run: bash gradlew --no-daemon clean build - name: Prepare disposable regression world @@ -44,8 +44,32 @@ jobs: if: always() uses: actions/upload-artifact@v4 with: - name: flow-control-fabric-26.1.2-0.1.6 + name: flow-control-fabric-26.1.2-0.2.2 path: | build/libs/*.jar run/streamcontrol-integration-results.json run/logs/latest.log + + - name: Capture client menus and baked block models + run: | + mkdir -p run-client + printf 'onboardAccessibility:false\nguiScale:3\nfullscreen:false\n' > run-client/options.txt + bash gradlew --no-daemon runServer > client-preview-server.log 2>&1 & + SERVER_PID=$! + trap 'kill "$SERVER_PID" $(jobs -pr) 2>/dev/null || true' EXIT + for attempt in $(seq 1 120); do + if rg -q 'Done \(' client-preview-server.log; then break; fi + if ! kill -0 "$SERVER_PID" 2>/dev/null; then tail -n 40 client-preview-server.log; exit 1; fi + sleep 1 + done + timeout 360 xvfb-run -a bash gradlew --no-daemon -Dstreamcontrol.uiPreview=true runClient + test -s run-client/screenshots/flow-control-block-models.png + - name: Upload client screenshots + if: always() + uses: actions/upload-artifact@v4 + with: + name: flow-control-0.2.2-fabric-26.1.2-client + path: | + run-client/screenshots/*.png + run-client/logs/latest.log + client-preview-server.log diff --git a/build.gradle b/build.gradle index 98c2e41..36d1637 100644 --- a/build.gradle +++ b/build.gradle @@ -1,5 +1,5 @@ plugins { id 'net.fabricmc.fabric-loom' version '1.15.5'; id 'maven-publish' } -version = '0.1.6' +version = '0.2.2' group='nl.townsandkingdoms.streamcontrol' base.archivesName='streams-flow-control-fabric-26.1.2' repositories { mavenCentral() } @@ -7,16 +7,16 @@ java.toolchain.languageVersion=JavaLanguageVersion.of(25) java.withSourcesJar() loom { - runs { server { server(); runDir 'run'; vmArg '-Dstreamcontrol.integrationTest='+System.getProperty('streamcontrol.integrationTest','false'); vmArg '-Xmx3G'; programArg '--nogui' } } + runs { client { client(); runDir 'run-client'; vmArg '-Dstreamcontrol.uiPreview='+System.getProperty('streamcontrol.uiPreview','false'); programArg '--width'; programArg '960'; programArg '--height'; programArg '720' }; server { server(); runDir 'run'; vmArg '-Dstreamcontrol.integrationTest='+System.getProperty('streamcontrol.integrationTest','false'); vmArg '-Xmx3G'; programArg '--nogui' } } } dependencies { minecraft 'com.mojang:minecraft:26.1.2' implementation 'net.fabricmc:fabric-loader:0.18.4' implementation 'net.fabricmc.fabric-api:fabric-api:0.155.3+26.1.2' - implementation files('libs/StreamsReflowing-26.1.2-fabric-2.15.2.jar') + implementation files('libs/StreamsReflowing-26.1.2-fabric-2.16.2.jar') } tasks.withType(JavaCompile).configureEach { options.encoding='UTF-8'; options.release=25 } tasks.named('jar') { from('LICENSE') } -tasks.named("jar") { archiveFileName = "flow-control-0.1.6-fabric-26.1.2.jar" } +tasks.named("jar") { archiveFileName = "flow-control-0.2.2-fabric-26.1.2.jar" } diff --git a/src/main/java/nl/townsandkingdoms/streamcontrol/ControlCommands.java b/src/main/java/nl/townsandkingdoms/streamcontrol/ControlCommands.java index 9d9480e..915445e 100644 --- a/src/main/java/nl/townsandkingdoms/streamcontrol/ControlCommands.java +++ b/src/main/java/nl/townsandkingdoms/streamcontrol/ControlCommands.java @@ -1,29 +1,19 @@ package nl.townsandkingdoms.streamcontrol; -import com.mojang.brigadier.arguments.*; import net.minecraft.commands.Commands; -import net.minecraft.core.component.DataComponents; import net.minecraft.network.chat.Component; -import net.minecraft.world.item.component.CustomData; +import com.mojang.brigadier.CommandDispatcher; +import net.minecraft.commands.CommandSourceStack; public final class ControlCommands { - public static void register(com.mojang.brigadier.CommandDispatcher dispatcher) { + public static void register(CommandDispatcher dispatcher) { dispatcher.register(Commands.literal("streamcontrol") - .then(Commands.literal("radius").then(Commands.argument("blocks",IntegerArgumentType.integer(1,32)).executes(c -> { - var p = c.getSource().getPlayerOrException(); var s = p.getMainHandItem(); - if (!s.is(StreamControl.WAND.get())) return 0; - var n = FlowWand.settings(s); n.putInt("Radius",ControlConfig.radius(IntegerArgumentType.getInteger(c,"blocks"))); s.set(DataComponents.CUSTOM_DATA,CustomData.of(n)); return 1; - }))) - .then(Commands.literal("strength").then(Commands.argument("value",DoubleArgumentType.doubleArg(0.05,2)).executes(c -> { - var p = c.getSource().getPlayerOrException(); var s = p.getMainHandItem(); if (!s.is(StreamControl.WAND.get())) return 0; - var n = FlowWand.settings(s); n.putDouble("Strength",ControlConfig.strength(DoubleArgumentType.getDouble(c,"value"))); s.set(DataComponents.CUSTOM_DATA,CustomData.of(n)); return 1; - }))) - .then(Commands.literal("status").executes(c -> { - var p = net.minecraft.core.BlockPos.containing(c.getSource().getPosition()); var data = ControlData.get(c.getSource().getLevel()); var r = data.at(p.getX(),p.getY(),p.getZ()); - c.getSource().sendSuccess(() -> Component.literal("Flow Control: " + data.all().size() + " regions; here=" + (r == null ? "natural" : r.still() ? "still" : "directed")),false); return 1; + .then(Commands.literal("status").executes(c->{ + var p=net.minecraft.core.BlockPos.containing(c.getSource().getPosition());var data=ControlData.get(c.getSource().getLevel());var r=data.at(p.getX(),p.getY(),p.getZ()); + c.getSource().sendSuccess(()->Component.literal("Flow Control: "+data.all().size()+" regions; here="+(r==null?"natural":r.still()?"still":"directed")),false);return 1; })) - .then(Commands.literal("clear").requires(s -> s.permissions().hasPermission(net.minecraft.server.permissions.Permissions.COMMANDS_GAMEMASTER)).executes(c -> { - var level = c.getSource().getLevel(); var data = ControlData.get(level); int count = 0; - for (var r : data.all()) if (!r.controller()) { data.remove(level,r.key()); count++; } - final int removed = count; c.getSource().sendSuccess(() -> Component.literal("Removed " + removed + " wand regions in this dimension."),true); return count; + .then(Commands.literal("clear").requires(s->s.permissions().hasPermission(net.minecraft.server.permissions.Permissions.COMMANDS_GAMEMASTER)).executes(c->{ + var level=c.getSource().getLevel();var data=ControlData.get(level);int count=0; + for(var r:data.all()) if(!r.controller()) { data.remove(level,r.key());count++; } + final int removed=count;c.getSource().sendSuccess(()->Component.literal("Removed "+removed+" legacy wand regions in this dimension."),true);return count; }))); } } diff --git a/src/main/java/nl/townsandkingdoms/streamcontrol/ControlConfig.java b/src/main/java/nl/townsandkingdoms/streamcontrol/ControlConfig.java index cbe1a26..b895eda 100644 --- a/src/main/java/nl/townsandkingdoms/streamcontrol/ControlConfig.java +++ b/src/main/java/nl/townsandkingdoms/streamcontrol/ControlConfig.java @@ -5,42 +5,77 @@ import net.minecraft.world.level.storage.LevelResource; import net.fabricmc.loader.api.FabricLoader; public final class ControlConfig { - public static final class Value { private volatile T value; public Value(T v){value=v;} public T get(){return value;} public void set(T v){value=v;} } - public static final Value ENABLED=new Value<>(true),WAND_ENABLED=new Value<>(true),OP_ONLY=new Value<>(false),PARTICLES=new Value<>(true); - public static final Value MAX_RADIUS=new Value<>(16),DEFAULT_RADIUS=new Value<>(4),VERTICAL=new Value<>(4),MAX_REGIONS=new Value<>(2048),MAX_PLAYER_REGIONS=new Value<>(256); - public static final Value SPEED=new Value<>(1.0),MAX_SPEED=new Value<>(2.0); - public static int radius(int v){return Math.max(1,Math.min(MAX_RADIUS.get(),v));} - public static double strength(double v){return Double.isFinite(v)?Math.max(0.05,Math.min(MAX_SPEED.get(),v)):Math.min(1,MAX_SPEED.get());} - private static boolean flag(JsonObject j,String k,boolean d){return j.has(k)?j.get(k).getAsBoolean():d;} - private static int integer(JsonObject j,String k,int d,int lo,int hi){return Math.max(lo,Math.min(hi,j.has(k)?j.get(k).getAsInt():d));} - private static double number(JsonObject j,String k,double d){double v=j.has(k)?j.get(k).getAsDouble():d;return Double.isFinite(v)?Math.max(0.05,Math.min(2,v)):d;} + public static final class Value {private volatile T value;public Value(T v){value=v;}public T get(){return value;}public void set(T v){value=v;}} + public static final Value ENABLED=new Value<>(true); + public static final Value WAND_ENABLED=new Value<>(true); + public static final Value OP_ONLY=new Value<>(false); + public static final Value PARTICLES=new Value<>(true); + public static final Value MAX_RADIUS=new Value<>(16); + public static final Value DEFAULT_RADIUS=new Value<>(4); + public static final Value VERTICAL=new Value<>(4); + public static final Value SPEED=new Value<>(1.0); + public static final Value MAX_SPEED=new Value<>(2.0); + public static final Value MAX_REGIONS=new Value<>(2048); + public static final Value MAX_PLAYER_REGIONS=new Value<>(256); + public static final Value MAX_VERTICAL=new Value<>(16); + public static final Value LINK_RANGE=new Value<>(64); + public static final Value SENSOR_MAX_RADIUS=new Value<>(16); + public static final Value SENSOR_MAX_VERTICAL=new Value<>(16); + public static final Value SENSOR_INTERVAL=new Value<>(20); + public static final Value MOORING_MAX_RADIUS=new Value<>(8); + public static final Value MOORING_MAX_VERTICAL=new Value<>(8); + public static final Value RPM_FULL_STRENGTH=new Value<>(128.0); + public static final Value KINETIC_STRESS=new Value<>(4.0); + public static int radius(int n){return Math.max(1,Math.min(MAX_RADIUS.get(),n));} + public static double strength(double n){return Double.isFinite(n)?Math.max(.05,Math.min(MAX_SPEED.get(),n)):Math.min(1,MAX_SPEED.get());} public static void load(MinecraftServer server){ - ENABLED.set(true);WAND_ENABLED.set(true);OP_ONLY.set(false);PARTICLES.set(true);MAX_RADIUS.set(16);DEFAULT_RADIUS.set(4);VERTICAL.set(4);MAX_REGIONS.set(2048);MAX_PLAYER_REGIONS.set(256);SPEED.set(1.0);MAX_SPEED.set(2.0); Path path=server.getWorldPath(LevelResource.ROOT).resolve("serverconfig/streamcontrol-server.json"); - Path defaults=FabricLoader.getInstance().getGameDir().resolve("defaultconfigs/streamcontrol-server.json"); try { Files.createDirectories(path.getParent()); - if(!Files.exists(path)){ - if(Files.isRegularFile(defaults))Files.copy(defaults,path); - else Files.writeString(path,""" -{ - "enabled": true, - "wandEnabled": true, - "wandRequiresOperator": false, - "showWandFeedbackParticles": true, - "defaultRadius": 4, - "maxRadius": 16, - "verticalReach": 4, - "defaultStrength": 1.0, - "maxStrength": 2.0, - "maxRegionsPerDimension": 2048, - "maxWandRegionsPerPlayer": 256 -} -"""); - } - JsonObject j=JsonParser.parseString(Files.readString(path)).getAsJsonObject(); - ENABLED.set(flag(j,"enabled",true));WAND_ENABLED.set(flag(j,"wandEnabled",true));OP_ONLY.set(flag(j,"wandRequiresOperator",false));PARTICLES.set(flag(j,"showWandFeedbackParticles",true)); - MAX_RADIUS.set(integer(j,"maxRadius",16,1,32));DEFAULT_RADIUS.set(integer(j,"defaultRadius",4,1,32));VERTICAL.set(integer(j,"verticalReach",4,0,16));MAX_REGIONS.set(integer(j,"maxRegionsPerDimension",2048,1,8192));MAX_PLAYER_REGIONS.set(integer(j,"maxWandRegionsPerPlayer",256,1,2048));SPEED.set(number(j,"defaultStrength",1));MAX_SPEED.set(number(j,"maxStrength",2)); - }catch(Exception e){org.slf4j.LoggerFactory.getLogger("streamcontrol").error("Cannot load Flow Control settings from {}",path,e);} + Path defaults=FabricLoader.getInstance().getGameDir().resolve("defaultconfigs/streamcontrol-server.json"); + if(!Files.exists(path) && Files.isRegularFile(defaults))Files.copy(defaults,path); + JsonObject n=Files.exists(path)?JsonParser.parseString(Files.readString(path)).getAsJsonObject():new JsonObject(); + if(!n.has("enabled")) n.addProperty("enabled",true); + ENABLED.set(n.get("enabled").getAsBoolean()); + if(!n.has("wandEnabled")) n.addProperty("wandEnabled",true); + WAND_ENABLED.set(n.get("wandEnabled").getAsBoolean()); + if(!n.has("wandRequiresOperator")) n.addProperty("wandRequiresOperator",false); + OP_ONLY.set(n.get("wandRequiresOperator").getAsBoolean()); + if(!n.has("showWandFeedbackParticles")) n.addProperty("showWandFeedbackParticles",true); + PARTICLES.set(n.get("showWandFeedbackParticles").getAsBoolean()); + if(!n.has("maxRadius")) n.addProperty("maxRadius",16); + MAX_RADIUS.set(Math.max(1,Math.min(32,n.get("maxRadius").getAsInt()))); + if(!n.has("defaultRadius")) n.addProperty("defaultRadius",4); + DEFAULT_RADIUS.set(Math.max(1,Math.min(32,n.get("defaultRadius").getAsInt()))); + if(!n.has("verticalReach")) n.addProperty("verticalReach",4); + VERTICAL.set(Math.max(0,Math.min(16,n.get("verticalReach").getAsInt()))); + if(!n.has("defaultStrength")) n.addProperty("defaultStrength",1.0); + SPEED.set(Double.isFinite(n.get("defaultStrength").getAsDouble())?Math.max(0.05,Math.min(2.0,n.get("defaultStrength").getAsDouble())):1.0); + if(!n.has("maxStrength")) n.addProperty("maxStrength",2.0); + MAX_SPEED.set(Double.isFinite(n.get("maxStrength").getAsDouble())?Math.max(0.05,Math.min(2.0,n.get("maxStrength").getAsDouble())):2.0); + if(!n.has("maxRegionsPerDimension")) n.addProperty("maxRegionsPerDimension",2048); + MAX_REGIONS.set(Math.max(1,Math.min(8192,n.get("maxRegionsPerDimension").getAsInt()))); + if(!n.has("maxWandRegionsPerPlayer")) n.addProperty("maxWandRegionsPerPlayer",256); + MAX_PLAYER_REGIONS.set(Math.max(1,Math.min(2048,n.get("maxWandRegionsPerPlayer").getAsInt()))); + if(!n.has("maxControllerVerticalReach")) n.addProperty("maxControllerVerticalReach",16); + MAX_VERTICAL.set(Math.max(0,Math.min(16,n.get("maxControllerVerticalReach").getAsInt()))); + if(!n.has("maxControllerLinkDistance")) n.addProperty("maxControllerLinkDistance",64); + LINK_RANGE.set(Math.max(1,Math.min(256,n.get("maxControllerLinkDistance").getAsInt()))); + if(!n.has("maxSensorRadius")) n.addProperty("maxSensorRadius",16); + SENSOR_MAX_RADIUS.set(Math.max(1,Math.min(16,n.get("maxSensorRadius").getAsInt()))); + if(!n.has("maxSensorVerticalReach")) n.addProperty("maxSensorVerticalReach",16); + SENSOR_MAX_VERTICAL.set(Math.max(0,Math.min(16,n.get("maxSensorVerticalReach").getAsInt()))); + if(!n.has("sensorSampleIntervalTicks")) n.addProperty("sensorSampleIntervalTicks",20); + SENSOR_INTERVAL.set(Math.max(5,Math.min(200,n.get("sensorSampleIntervalTicks").getAsInt()))); + if(!n.has("maxMooringRadius")) n.addProperty("maxMooringRadius",8); + MOORING_MAX_RADIUS.set(Math.max(1,Math.min(16,n.get("maxMooringRadius").getAsInt()))); + if(!n.has("maxMooringVerticalReach")) n.addProperty("maxMooringVerticalReach",8); + MOORING_MAX_VERTICAL.set(Math.max(0,Math.min(16,n.get("maxMooringVerticalReach").getAsInt()))); + if(!n.has("kineticRpmForFullStrength")) n.addProperty("kineticRpmForFullStrength",128.0); + RPM_FULL_STRENGTH.set(Double.isFinite(n.get("kineticRpmForFullStrength").getAsDouble())?Math.max(1.0,Math.min(1024.0,n.get("kineticRpmForFullStrength").getAsDouble())):128.0); + if(!n.has("kineticStressImpact")) n.addProperty("kineticStressImpact",4.0); + KINETIC_STRESS.set(Double.isFinite(n.get("kineticStressImpact").getAsDouble())?Math.max(0.0,Math.min(256.0,n.get("kineticStressImpact").getAsDouble())):4.0); + Files.writeString(path,new GsonBuilder().setPrettyPrinting().create().toJson(n)); + }catch(Exception e){throw new IllegalStateException("Cannot load Flow Control settings from "+path,e);} } } diff --git a/src/main/java/nl/townsandkingdoms/streamcontrol/ControlNetwork.java b/src/main/java/nl/townsandkingdoms/streamcontrol/ControlNetwork.java index fdf2b19..e47e8ff 100644 --- a/src/main/java/nl/townsandkingdoms/streamcontrol/ControlNetwork.java +++ b/src/main/java/nl/townsandkingdoms/streamcontrol/ControlNetwork.java @@ -4,72 +4,108 @@ import net.minecraft.network.codec.StreamCodec; import net.minecraft.network.protocol.common.custom.CustomPacketPayload; import net.minecraft.resources.Identifier; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.world.entity.player.Player; import net.minecraft.server.level.*; -import net.fabricmc.fabric.api.networking.v1.*; public final class ControlNetwork { /** Server-authoritative snapshot used to open the controller's client GUI. */ public record Open(long pos, int radius, int vertical, double strength, double maxStrength, - int maxRadius, boolean connectedOnly, boolean still, boolean powered, int facing) + int maxRadius, boolean connectedOnly, boolean still, boolean powered, int facing, CompoundTag settings) implements CustomPacketPayload { - public static final Type TYPE = new Type<>(Identifier.fromNamespaceAndPath(StreamControl.ID, "open_controller_v2")); + public Open(long pos,int radius,int vertical,double strength,double maxStrength,int maxRadius,boolean connectedOnly,boolean still,boolean powered,int facing) { + this(pos,radius,vertical,strength,maxStrength,maxRadius,connectedOnly,still,powered,facing,new CompoundTag()); + } + public static final Type TYPE = new Type<>(Identifier.fromNamespaceAndPath(StreamControl.ID, "open_controller_v3")); public static final StreamCodec CODEC = new StreamCodec<>() { public Open decode(RegistryFriendlyByteBuf b) { return new Open(b.readLong(), b.readVarInt(), b.readVarInt(), b.readDouble(), b.readDouble(), - b.readVarInt(), b.readBoolean(), b.readBoolean(), b.readBoolean(), b.readVarInt()); + b.readVarInt(), b.readBoolean(), b.readBoolean(), b.readBoolean(), b.readVarInt(), b.readNbt()); } public void encode(RegistryFriendlyByteBuf b, Open p) { b.writeLong(p.pos); b.writeVarInt(p.radius); b.writeVarInt(p.vertical); b.writeDouble(p.strength); b.writeDouble(p.maxStrength); b.writeVarInt(p.maxRadius); b.writeBoolean(p.connectedOnly); - b.writeBoolean(p.still); b.writeBoolean(p.powered); b.writeVarInt(p.facing); + b.writeBoolean(p.still); b.writeBoolean(p.powered); b.writeVarInt(p.facing); b.writeNbt(p.settings); } }; @Override public Type type() { return TYPE; } } /** Never trust client-supplied dimensions, speed or access rights. */ - public record Configure(long pos, int radius, double strength, boolean connectedOnly, int facing) + public record Configure(long pos, int radius, double strength, boolean connectedOnly, int facing, CompoundTag settings) implements CustomPacketPayload { - public static final Type TYPE = new Type<>(Identifier.fromNamespaceAndPath(StreamControl.ID, "configure_controller_v2")); + public Configure(long pos,int radius,double strength,boolean connectedOnly,int facing) { + this(pos,radius,strength,connectedOnly,facing,new CompoundTag()); + } + public static final Type TYPE = new Type<>(Identifier.fromNamespaceAndPath(StreamControl.ID, "configure_controller_v3")); public static final StreamCodec CODEC = new StreamCodec<>() { public Configure decode(RegistryFriendlyByteBuf b) { - return new Configure(b.readLong(), b.readVarInt(), b.readDouble(), b.readBoolean(), b.readVarInt()); + return new Configure(b.readLong(), b.readVarInt(), b.readDouble(), b.readBoolean(), b.readVarInt(), b.readNbt()); } public void encode(RegistryFriendlyByteBuf b, Configure p) { b.writeLong(p.pos); b.writeVarInt(p.radius); b.writeDouble(p.strength); - b.writeBoolean(p.connectedOnly); b.writeVarInt(p.facing); + b.writeBoolean(p.connectedOnly); b.writeVarInt(p.facing); b.writeNbt(p.settings); } }; @Override public Type type() { return TYPE; } } - public static void configure(ServerPlayer player, Configure p) { - var pos = net.minecraft.core.BlockPos.of(p.pos()); - if (player.blockPosition().distSqr(pos) > 64 || !player.mayBuild() - || !player.level().mayInteract(player,pos)) return; - if (!(player.level().getBlockEntity(pos) instanceof ControllerEntity be)) return; - var direction = FlowHeading.fromId(p.facing()); - if (direction == null) return; - be.configure(direction, ControlConfig.radius(p.radius()), ControlConfig.strength(p.strength()),p.connectedOnly()); + public record UtilityOpen(long pos,CompoundTag settings) implements CustomPacketPayload { + public static final Type TYPE=new Type<>(Identifier.fromNamespaceAndPath(StreamControl.ID,"open_utility")); + public static final StreamCodec CODEC=new StreamCodec<>() { + public UtilityOpen decode(RegistryFriendlyByteBuf b) { return new UtilityOpen(b.readLong(),b.readNbt()); } + public void encode(RegistryFriendlyByteBuf b,UtilityOpen p) { b.writeLong(p.pos);b.writeNbt(p.settings); } + }; + @Override public Type type() { return TYPE; } } - - public record Update(String dimension,boolean reset,CompoundTag region,long remove,boolean enabled) implements CustomPacketPayload { - public static final Type TYPE=new Type<>(Identifier.fromNamespaceAndPath(StreamControl.ID,"region")); - public static final StreamCodec CODEC=new StreamCodec<>(){ - public Update decode(RegistryFriendlyByteBuf b){return new Update(b.readUtf(256),b.readBoolean(),b.readNbt(),b.readLong(),b.readBoolean());} - public void encode(RegistryFriendlyByteBuf b,Update p){b.writeUtf(p.dimension(),256);b.writeBoolean(p.reset());b.writeNbt(p.region());b.writeLong(p.remove());b.writeBoolean(p.enabled());} - }; - @Override public Type type(){return TYPE;} - } - public static void register(){PayloadTypeRegistry.clientboundPlay().register(Update.TYPE,Update.CODEC); - PayloadTypeRegistry.clientboundPlay().register(Open.TYPE,Open.CODEC); - PayloadTypeRegistry.serverboundPlay().register(Configure.TYPE,Configure.CODEC); - ServerPlayNetworking.registerGlobalReceiver(Configure.TYPE,(packet,context) -> context.server().execute(() -> configure(context.player(),packet)));} - public static void open(ServerPlayer player, ControllerEntity be) { ServerPlayNetworking.send(player,new Open(be.getBlockPos().asLong(),be.radius(),ControlConfig.VERTICAL.get(), - be.strength(),ControlConfig.MAX_SPEED.get(),ControlConfig.MAX_RADIUS.get(),be.connectedOnly(), - ((ControllerBlock)be.getBlockState().getBlock()).still,be.getBlockState().getValue(ControllerBlock.POWERED), - be.heading().id())); } - public static void sendConfigure(Configure packet) { net.fabricmc.fabric.api.client.networking.v1.ClientPlayNetworking.send(packet); } - public static void update(ServerLevel l,FlowRegion r){for(var p:l.players())ServerPlayNetworking.send(p,new Update(l.dimension().identifier().toString(),false,r.save(),0,ControlConfig.ENABLED.get()));} - public static void remove(ServerLevel l,long key){for(var p:l.players())ServerPlayNetworking.send(p,new Update(l.dimension().identifier().toString(),false,null,key,ControlConfig.ENABLED.get()));} - public static void sync(ServerPlayer p){var l=p.level();String dim=l.dimension().identifier().toString();ServerPlayNetworking.send(p,new Update(dim,true,null,0,ControlConfig.ENABLED.get()));for(var r:ControlData.get(l).all())ServerPlayNetworking.send(p,new Update(dim,false,r.save(),0,ControlConfig.ENABLED.get()));} + public record UtilityConfigure(long pos,CompoundTag settings) implements CustomPacketPayload { + public static final Type TYPE=new Type<>(Identifier.fromNamespaceAndPath(StreamControl.ID,"configure_utility")); + public static final StreamCodec CODEC=new StreamCodec<>() { + public UtilityConfigure decode(RegistryFriendlyByteBuf b) { return new UtilityConfigure(b.readLong(),b.readNbt()); } + public void encode(RegistryFriendlyByteBuf b,UtilityConfigure p) { b.writeLong(p.pos);b.writeNbt(p.settings); } + }; + @Override public Type type() { return TYPE; } + } + public record Update(String dimension, boolean reset, CompoundTag region, long remove,boolean enabled) implements CustomPacketPayload { + public Update(String dimension,boolean reset,CompoundTag region,long remove) {this(dimension,reset,region,remove,ControlConfig.ENABLED.get());} + public static final Type TYPE = new Type<>(Identifier.fromNamespaceAndPath(StreamControl.ID, "region")); + public static final StreamCodec CODEC = new StreamCodec<>() { + public Update decode(RegistryFriendlyByteBuf b) { return new Update(b.readUtf(256), b.readBoolean(), b.readNbt(), b.readLong(),b.readBoolean()); } + public void encode(RegistryFriendlyByteBuf b, Update p) { b.writeUtf(p.dimension, 256); b.writeBoolean(p.reset); b.writeNbt(p.region); b.writeLong(p.remove);b.writeBoolean(p.enabled); } + }; + @Override public Type type() { return TYPE; } + } + public static void configure(ServerPlayer player,Configure p) { + var pos=BlockPos.of(p.pos());var level=player.level(); + if(!level.hasChunkAt(pos) || player.blockPosition().distSqr(pos)>64 || !player.mayBuild() || !level.mayInteract(player,pos))return; + if(!(level.getBlockEntity(pos) instanceof FlowController be))return; + var heading=FlowHeading.fromId(p.facing());if(heading==null)return; + if(p.settings()!=null && p.settings().contains("ProfileA") && p.settings().contains("ProfileB"))be.controls().configure(p.settings()); + else be.configure(heading,ControlConfig.radius(p.radius()),ControlConfig.strength(p.strength()),p.connectedOnly()); + } + public static void configureUtility(ServerPlayer player,UtilityConfigure p) { + var pos=BlockPos.of(p.pos());var level=player.level(); + if(!level.hasChunkAt(pos) || player.blockPosition().distSqr(pos)>64 || !player.mayBuild() || !level.mayInteract(player,pos) || p.settings()==null)return; + if(level.getBlockEntity(pos) instanceof UtilityAccess be)be.configure(p.settings()); + } + public static void register() { + net.fabricmc.fabric.api.networking.v1.PayloadTypeRegistry.playS2C().register(Open.TYPE,Open.CODEC); + net.fabricmc.fabric.api.networking.v1.PayloadTypeRegistry.playS2C().register(UtilityOpen.TYPE,UtilityOpen.CODEC); + net.fabricmc.fabric.api.networking.v1.PayloadTypeRegistry.playS2C().register(Update.TYPE,Update.CODEC); + net.fabricmc.fabric.api.networking.v1.PayloadTypeRegistry.playC2S().register(Configure.TYPE,Configure.CODEC); + net.fabricmc.fabric.api.networking.v1.PayloadTypeRegistry.playC2S().register(UtilityConfigure.TYPE,UtilityConfigure.CODEC); + net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking.registerGlobalReceiver(Configure.TYPE,(p,c)->c.server().execute(()->configure(c.player(),p))); + net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking.registerGlobalReceiver(UtilityConfigure.TYPE,(p,c)->c.server().execute(()->configureUtility(c.player(),p))); + } + public static void sendConfigure(Configure p) {net.fabricmc.fabric.api.client.networking.v1.ClientPlayNetworking.send(p);} + public static void sendConfigure(UtilityConfigure p) {net.fabricmc.fabric.api.client.networking.v1.ClientPlayNetworking.send(p);} + public static void open(ServerPlayer player,FlowController be) { + var n=be.controls().save();n.putInt("MaxVertical",ControlConfig.MAX_VERTICAL.get()); + net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking.send(player,new Open(be.blockEntity().getBlockPos().asLong(),be.radius(),be.controls().a().vertical(),be.strength(),ControlConfig.MAX_SPEED.get(),ControlConfig.MAX_RADIUS.get(),be.connectedOnly(),be.isStill(),be.blockEntity().getBlockState().getValue(ControllerBlock.POWERED),be.heading().id(),n)); + } + public static void openUtility(ServerPlayer p,UtilityAccess be) {net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking.send(p,new UtilityOpen(be.blockEntity().getBlockPos().asLong(),be.settings()));} + private static void send(ServerPlayer p,Update u) {net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking.send(p,u);} + public static void update(ServerLevel l,FlowRegion r) {for(var p:l.players())send(p,new Update(l.dimension().identifier().toString(),false,r.save(),0));} + public static void remove(ServerLevel l,long key) {for(var p:l.players())send(p,new Update(l.dimension().identifier().toString(),false,null,key));} + public static void sync(ServerPlayer p) {var l=p.level();String dim=l.dimension().identifier().toString();send(p,new Update(dim,true,null,0));for(var r:ControlData.get(l).all())send(p,new Update(dim,false,r.save(),0));} } diff --git a/src/main/java/nl/townsandkingdoms/streamcontrol/ControllerBlock.java b/src/main/java/nl/townsandkingdoms/streamcontrol/ControllerBlock.java index a8c4cae..5c0b613 100644 --- a/src/main/java/nl/townsandkingdoms/streamcontrol/ControllerBlock.java +++ b/src/main/java/nl/townsandkingdoms/streamcontrol/ControllerBlock.java @@ -13,6 +13,7 @@ import net.minecraft.world.level.material.*; import net.minecraft.world.phys.BlockHitResult; import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; public final class ControllerBlock extends BaseEntityBlock implements SimpleWaterloggedBlock { public static final EnumProperty FACING = BlockStateProperties.HORIZONTAL_FACING; public static final BooleanProperty WATERLOGGED = BlockStateProperties.WATERLOGGED; @@ -32,9 +33,8 @@ public ControllerBlock(boolean still, Properties p) { @Override public BlockEntity newBlockEntity(BlockPos pos, BlockState state) { return new ControllerEntity(pos, state); } @Override public BlockEntityTicker getTicker(Level level, BlockState state, BlockEntityType type) { return level.isClientSide() ? null : createTickerHelper(type, StreamControl.CONTROLLER.get(), ControllerEntity::tick); } @Override protected FluidState getFluidState(BlockState s) { return s.getValue(WATERLOGGED) ? Fluids.WATER.getSource(false) : super.getFluidState(s); } - @Override protected BlockState updateShape(BlockState s, LevelReader level, net.minecraft.world.level.ScheduledTickAccess ticks, BlockPos pos, Direction direction, BlockPos neighborPos, BlockState neighbor, net.minecraft.util.RandomSource random) { - if (s.getValue(WATERLOGGED)) ticks.scheduleTick(pos, Fluids.WATER, Fluids.WATER.getTickDelay(level)); - return super.updateShape(s,level,ticks,pos,direction,neighborPos,neighbor,random); + @Override protected BlockState updateShape(BlockState s,LevelReader l,net.minecraft.world.level.ScheduledTickAccess ticks,BlockPos p,Direction d,BlockPos q,BlockState other,net.minecraft.util.RandomSource random) { + if(s.getValue(WATERLOGGED))ticks.scheduleTick(p,Fluids.WATER,Fluids.WATER.getTickDelay(l));return super.updateShape(s,l,ticks,p,d,q,other,random); } @Override protected void neighborChanged(BlockState s, Level level, BlockPos pos, Block block, net.minecraft.world.level.redstone.Orientation orientation, boolean moving) { if (!level.isClientSide()) { @@ -43,15 +43,18 @@ public ControllerBlock(boolean still, Properties p) { if (level.getBlockEntity(pos) instanceof ControllerEntity be) be.waterChanged(); } } + @Override protected InteractionResult useItemOn(net.minecraft.world.item.ItemStack stack,BlockState state,Level level,BlockPos pos,Player player,InteractionHand hand,BlockHitResult hit) { + // Let the Calibrator's item handler run before the ordinary configure interaction. + return stack.is(StreamControl.WAND.get()) ? InteractionResult.PASS : super.useItemOn(stack,state,level,pos,player,hand,hit); + } @Override protected InteractionResult useWithoutItem(BlockState s, Level level, BlockPos pos, Player p, BlockHitResult hit) { if (!p.mayBuild() || !level.mayInteract(p,pos)) return InteractionResult.FAIL; - if (!level.isClientSide() && p instanceof net.minecraft.server.level.ServerPlayer player - && level.getBlockEntity(pos) instanceof ControllerEntity be) ControlNetwork.open(player, be); + if (!level.isClientSide() && p instanceof ServerPlayer player && level.getBlockEntity(pos) instanceof ControllerEntity be) + ControlNetwork.open(player, be); return InteractionResult.SUCCESS; } - @Override protected void affectNeighborsAfterRemoval(BlockState old, ServerLevel level, BlockPos pos, boolean moving) { - ControlData.get(level).remove(level, pos.asLong()); - super.affectNeighborsAfterRemoval(old,level,pos,moving); + @Override protected void affectNeighborsAfterRemoval(BlockState old,ServerLevel l,BlockPos p,boolean moving) { + ControlData.get(l).remove(l,p.asLong());super.affectNeighborsAfterRemoval(old,l,p,moving); } @Override protected BlockState rotate(BlockState s, Rotation r) { return FlowHeading.fromState(s).rotate(r).apply(s); } @Override protected BlockState mirror(BlockState s, Mirror m) { return FlowHeading.fromState(s).mirror(m).apply(s); } diff --git a/src/main/java/nl/townsandkingdoms/streamcontrol/ControllerEntity.java b/src/main/java/nl/townsandkingdoms/streamcontrol/ControllerEntity.java index 6faa2f8..169a5c1 100644 --- a/src/main/java/nl/townsandkingdoms/streamcontrol/ControllerEntity.java +++ b/src/main/java/nl/townsandkingdoms/streamcontrol/ControllerEntity.java @@ -1,62 +1,21 @@ package nl.townsandkingdoms.streamcontrol; -import java.util.UUID; -import java.util.Arrays; import net.minecraft.core.*; import net.minecraft.nbt.CompoundTag; -import net.minecraft.server.level.ServerLevel; import net.minecraft.world.level.Level; import net.minecraft.world.level.block.entity.BlockEntity; import net.minecraft.world.level.block.state.BlockState; -public final class ControllerEntity extends BlockEntity { - private int radius = -1; - private double strength = -1; - private boolean connectedOnly; - private boolean connectivityDirty = true; - private boolean initialized; - public ControllerEntity(BlockPos p, BlockState s) { super(StreamControl.CONTROLLER.get(), p, s); } - public int radius() { return ControlConfig.radius(radius < 1 ? ControlConfig.DEFAULT_RADIUS.get() : radius); } - public double strength() { return ControlConfig.strength(strength < 0 ? ControlConfig.SPEED.get() : strength); } - public FlowHeading heading() { return FlowHeading.fromState(getBlockState()); } - public boolean connectedOnly() { return connectedOnly; } - public void setRadius(int r) { radius = ControlConfig.radius(r); connectivityDirty = true; setChanged(); refresh(); } - public void configure(Direction d, int r, double speed) { configure(d, r, speed, connectedOnly); } - public void configure(Direction d, int r, double speed, boolean onlyConnected) { - configure(FlowHeading.fromDirection(d), r, speed, onlyConnected); - } - public void configure(FlowHeading heading, int r, double speed, boolean onlyConnected) { - radius = ControlConfig.radius(r); strength = ControlConfig.strength(speed); - connectedOnly = onlyConnected; - connectivityDirty = true; - if (level != null) level.setBlock(worldPosition, heading.apply(getBlockState()),3); - setChanged(); refresh(); - } - @Override protected void saveAdditional(net.minecraft.world.level.storage.ValueOutput tag) { super.saveAdditional(tag); tag.putInt("Radius",radius); tag.putDouble("Strength",strength); tag.putBoolean("ConnectedOnly",connectedOnly); } - @Override protected void loadAdditional(net.minecraft.world.level.storage.ValueInput tag) { super.loadAdditional(tag); radius = tag.getIntOr("Radius", -1); strength = tag.getDoubleOr("Strength", -1); connectedOnly = tag.getBooleanOr("ConnectedOnly", false); connectivityDirty = true; } - public void waterChanged() { connectivityDirty = true; refresh(); } - public static void tick(Level l, BlockPos p, BlockState s, ControllerEntity be) { - if (!be.initialized || l.getGameTime() % 20 == 0) { be.initialized = true; be.refresh(); } - } - public void refresh() { - if (!(level instanceof ServerLevel server)) return; - var data = ControlData.get(server); long key = worldPosition.asLong(); - if (getBlockState().getValue(ControllerBlock.POWERED) || !ControlConfig.ENABLED.get()) { data.remove(server,key); return; } - FlowHeading heading = heading(); - Direction d = heading.cardinal(); - boolean still = ((ControllerBlock)getBlockState().getBlock()).still; - double sp = strength(); - var old = data.byKey(key); - long[] mask = new long[0]; - if (connectedOnly) { - boolean rebuild = connectivityDirty || old == null || !old.connectedOnly() - || old.radius() != radius() || old.vertical() != ControlConfig.VERTICAL.get() - || server.getGameTime() % 100 == 0; - mask = rebuild ? ConnectedWater.scan(server, worldPosition, d, radius(), ControlConfig.VERTICAL.get()) : old.connectedMask(); - } - connectivityDirty = false; - if (old != null && old.radius() == radius() && old.vertical() == ControlConfig.VERTICAL.get() - && old.dx() == heading.dx() && old.dz() == heading.dz() && old.strength() == sp && old.still() == still - && old.connectedOnly() == connectedOnly && Arrays.equals(old.connectedMask(), mask)) return; - data.put(server, new FlowRegion(key, worldPosition.immutable(), radius(), ControlConfig.VERTICAL.get(), - heading.dx(), heading.dz(), sp, still, true, new UUID(0,0), data.next(), connectedOnly, mask)); +public final class ControllerEntity extends BlockEntity implements FlowController { + private final ControllerSettings controls = new ControllerSettings(this); + public ControllerEntity(BlockPos p, BlockState s) { super(StreamControl.CONTROLLER.get(),p,s); } + @Override public BlockEntity blockEntity() { return this; } + @Override public ControllerSettings controls() { return controls; } + @Override protected void saveAdditional(net.minecraft.world.level.storage.ValueOutput out) {super.saveAdditional(out);out.store("Engineering",CompoundTag.CODEC,controls.save());} + @Override protected void loadAdditional(net.minecraft.world.level.storage.ValueInput in) { + super.loadAdditional(in);var n=in.read("Engineering",CompoundTag.CODEC).orElseGet(()->{ + var legacy=new CompoundTag();legacy.putInt("Radius",in.getIntOr("Radius",-1));legacy.putDouble("Strength",in.getDoubleOr("Strength",-1));legacy.putBoolean("ConnectedOnly",in.getBooleanOr("ConnectedOnly",false));return legacy; + });controls.load(n); } + @Override public net.minecraft.network.protocol.game.ClientboundBlockEntityDataPacket getUpdatePacket() { return net.minecraft.network.protocol.game.ClientboundBlockEntityDataPacket.create(this); } + @Override public CompoundTag getUpdateTag(HolderLookup.Provider r) { return saveWithoutMetadata(r); } + public static void tick(Level l,BlockPos p,BlockState s,ControllerEntity be) { be.controls.tick(); } } diff --git a/src/main/java/nl/townsandkingdoms/streamcontrol/ControllerSettings.java b/src/main/java/nl/townsandkingdoms/streamcontrol/ControllerSettings.java new file mode 100644 index 0000000..61af2b4 --- /dev/null +++ b/src/main/java/nl/townsandkingdoms/streamcontrol/ControllerSettings.java @@ -0,0 +1,147 @@ +package nl.townsandkingdoms.streamcontrol; + +import java.util.*; +import net.minecraft.core.BlockPos; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.level.block.entity.BlockEntity; + +/** Persistent profiles and shared execution for normal and optional kinetic controllers. */ +public final class ControllerSettings { + public enum Redstone { PAUSE, IGNORE, ANALOG, REVERSE, SWITCH; + public static Redstone from(int id) { return values()[Math.clamp(id,0,values().length-1)]; } + } + private final FlowController host; + private FlowProfile a, b; + private Redstone redstone = Redstone.PAUSE; + private BlockPos source; + private boolean dirty = true; + private boolean initialized; + public ControllerSettings(FlowController host) { this.host = host; } + public FlowProfile a() { if (a == null) a = new FlowProfile(ControlConfig.DEFAULT_RADIUS.get(),ControlConfig.VERTICAL.get(),ControlConfig.SPEED.get(),host.heading(),false,host.isStill()?FlowProfile.Action.CALM:FlowProfile.Action.CURRENT); return a; } + public FlowProfile b() { + if (b == null) b = new FlowProfile(a().radius(),a().vertical(),a().strength(),a().heading(),a().connectedOnly(), + host.isStill() ? FlowProfile.Action.RELEASE : FlowProfile.Action.REVERSE); + return b; + } + public Redstone redstone() { return redstone; } + public BlockPos source() { return source; } + private static FlowProfile bounded(FlowProfile p) { + return new FlowProfile(ControlConfig.radius(p.radius()),Math.clamp(p.vertical(),0,ControlConfig.MAX_VERTICAL.get()), + ControlConfig.strength(p.strength()),p.heading(),p.connectedOnly(),p.action()); + } + private void changed() { + var be=host.blockEntity();be.setChanged(); + if(be.getLevel() instanceof ServerLevel l) l.sendBlockUpdated(be.getBlockPos(),be.getBlockState(),be.getBlockState(),2); + } + public void invalidate() { dirty = true; } + public void setA(FlowProfile profile) { + a = bounded(profile.forAnchor(host.isStill())); + var be = host.blockEntity(); + if (be.getLevel() != null) be.getLevel().setBlock(be.getBlockPos(),a.heading().apply(be.getBlockState()),3); + dirty = true; changed(); refresh(); + } + public CompoundTag save() { + a=a().withHeading(host.heading()); + var n = new CompoundTag(); n.put("ProfileA",a().save()); n.put("ProfileB",b().save()); + n.putInt("RedstoneMode",redstone.ordinal()); if (source != null) n.putLong("LinkedSource",source.asLong()); + // Older world data keys remain readable for migration and diagnostics. + n.putInt("Radius",a().radius()); n.putDouble("Strength",a().strength()); n.putBoolean("ConnectedOnly",a().connectedOnly()); + return n; + } + public void load(CompoundTag n) { + a = n.contains("ProfileA") ? FlowProfile.load(n.getCompoundOrEmpty("ProfileA"),host.isStill()) : + new FlowProfile(n.contains("Radius") && n.getIntOr("Radius",0) > 0 ? n.getIntOr("Radius",0) : ControlConfig.DEFAULT_RADIUS.get(), + ControlConfig.VERTICAL.get(), n.contains("Strength") && n.getDoubleOr("Strength",0.0) >= 0 ? n.getDoubleOr("Strength",0.0) : ControlConfig.SPEED.get(), + host.heading(), n.getBooleanOr("ConnectedOnly",false),host.isStill() ? FlowProfile.Action.CALM : FlowProfile.Action.CURRENT); + b = n.contains("ProfileB") ? FlowProfile.load(n.getCompoundOrEmpty("ProfileB"),host.isStill()) : null; + redstone = Redstone.from(n.getIntOr("RedstoneMode",0)); + source = n.contains("LinkedSource") ? BlockPos.of(n.getLongOr("LinkedSource",0L)) : null; dirty = true; + } + /** Client edits may change profiles and clear a link, never forge a link to an arbitrary position. */ + public void configure(CompoundTag n) { + a = FlowProfile.load(n.getCompoundOrEmpty("ProfileA"),host.isStill()); + b = bounded(FlowProfile.load(n.getCompoundOrEmpty("ProfileB"),host.isStill())); + redstone = Redstone.from(n.getIntOr("RedstoneMode",0)); + if (!n.contains("LinkedSource")) source = null; + setA(a); + } + public CompoundTag clipboard() { + ControllerSettings effective=host.blockEntity().getLevel() instanceof ServerLevel level ? resolved(level) : this; + var n=(effective==null?this:effective).save();n.remove("LinkedSource");return n; + } + public void paste(CompoundTag n) { source = null; configure(n); } + public void unlink() { source = null; dirty = true; changed(); refresh(); } + public boolean link(BlockPos pos) { + var be = host.blockEntity(); + if (!(be.getLevel() instanceof ServerLevel level) || pos.equals(be.getBlockPos()) + || pos.distSqr(be.getBlockPos()) > (long)ControlConfig.LINK_RANGE.get()*ControlConfig.LINK_RANGE.get()) return false; + Set seen = new HashSet<>(); seen.add(be.getBlockPos()); BlockPos current = pos; + for (int i=0;i<16;i++) { + if (!seen.add(current) || !level.hasChunkAt(current) + || !(level.getBlockEntity(current) instanceof FlowController controller)) return false; + if (controller.controls().source == null) { + source = pos.immutable(); dirty = true; changed(); refresh(); return true; + } + current = controller.controls().source; + } + return false; + } + private ControllerSettings resolved(ServerLevel level) { + ControllerSettings settings = this; Set seen = new HashSet<>(); seen.add(host.blockEntity().getBlockPos()); + for (int i=0;settings.source!=null && i<16;i++) { + var pos = settings.source; + if (!seen.add(pos) || !level.hasChunkAt(pos) || !(level.getBlockEntity(pos) instanceof FlowController next)) return null; + settings = next.controls(); + } + return settings.source == null ? settings : null; + } + public void tick() { + var level = host.blockEntity().getLevel(); + if (level != null && (!initialized || level.getGameTime()%10 == 0)) { initialized = true; refresh(); } + } + public void refresh() { + BlockEntity be = host.blockEntity(); if (!(be.getLevel() instanceof ServerLevel level) || be.isRemoved()) return; + var data = ControlData.get(level); var pos = be.getBlockPos(); long key = pos.asLong(); + ControllerSettings settings = resolved(level); + if (!ControlConfig.ENABLED.get() || settings == null) { data.remove(level,key); return; } + int signal = level.getBestNeighborSignal(pos); + // Read the block property too: preserves explicit powered states and structure/test placements. + boolean powered = signal > 0 || be.getBlockState().getValue(ControllerBlock.POWERED); + Redstone mode = settings.redstone; + if (mode == Redstone.PAUSE && powered) { data.remove(level,key); return; } + FlowProfile p = mode == Redstone.SWITCH && powered ? settings.b() : settings.a(); + p = bounded(p.forAnchor(host.isStill())); + if (p.action() == FlowProfile.Action.RELEASE) { data.remove(level,key); return; } + FlowHeading heading = mode != Redstone.SWITCH || !powered ? settings.host.heading() : p.heading(); + boolean reverse = p.action() == FlowProfile.Action.REVERSE; + boolean still = host.isStill() || p.action() == FlowProfile.Action.CALM; + double speed = p.strength(); + if (mode == Redstone.REVERSE && powered) reverse = !reverse; + if (mode == Redstone.ANALOG) { + if (host.isStill() && signal == 0) { data.remove(level,key); return; } + speed *= signal/15.0; + if (signal == 0) still = true; + } + float rpm = host.rpm(); + if (!Float.isNaN(rpm)) { + if (!Float.isFinite(rpm) || Math.abs(rpm) < 0.001f) { data.remove(level,key); return; } + speed *= Math.abs(rpm)/ControlConfig.RPM_FULL_STRENGTH.get(); + if (rpm < 0) reverse = !reverse; + } + speed = Double.isFinite(speed) ? Math.clamp(speed,0,ControlConfig.MAX_SPEED.get()) : 0; + if (reverse) heading = heading.rotate(net.minecraft.world.level.block.Rotation.CLOCKWISE_180); + var old = data.byKey(key); long[] mask = new long[0]; + if (p.connectedOnly()) { + boolean rebuild = dirty || old == null || !old.connectedOnly() || old.radius()!=p.radius() || old.vertical()!=p.vertical() + || level.getGameTime()%100 == 0; + mask = rebuild ? ConnectedWater.scan(level,pos,heading.cardinal(),p.radius(),p.vertical()) : old.connectedMask(); + } + dirty = false; + if (old != null && old.radius()==p.radius() && old.vertical()==p.vertical() && old.dx()==heading.dx() + && old.dz()==heading.dz() && old.strength()==speed && old.still()==still + && old.connectedOnly()==p.connectedOnly() && Arrays.equals(old.connectedMask(),mask)) return; + data.put(level,new FlowRegion(key,pos.immutable(),p.radius(),p.vertical(),heading.dx(),heading.dz(),speed,still,true, + new UUID(0,0),data.next(),p.connectedOnly(),mask)); + } +} diff --git a/src/main/java/nl/townsandkingdoms/streamcontrol/EngineeringBlockItem.java b/src/main/java/nl/townsandkingdoms/streamcontrol/EngineeringBlockItem.java new file mode 100644 index 0000000..b307965 --- /dev/null +++ b/src/main/java/nl/townsandkingdoms/streamcontrol/EngineeringBlockItem.java @@ -0,0 +1,13 @@ +package nl.townsandkingdoms.streamcontrol; +import java.util.List; +import net.minecraft.network.chat.Component; +import net.minecraft.world.item.*; +import net.minecraft.world.level.block.Block; +public final class EngineeringBlockItem extends BlockItem { + public EngineeringBlockItem(Block block,Properties p) { super(block,p); } + @Override public void appendHoverText(ItemStack stack,TooltipContext context,net.minecraft.world.item.component.TooltipDisplay display,java.util.function.Consumer lines,TooltipFlag flag) { + super.appendHoverText(stack,context,display,lines,flag); + lines.accept(Component.translatable(getDescriptionId()+".tip").withStyle(net.minecraft.ChatFormatting.GRAY)); + lines.accept(Component.translatable("engineering.streamcontrol.configure_tip").withStyle(net.minecraft.ChatFormatting.DARK_GRAY)); + } +} diff --git a/src/main/java/nl/townsandkingdoms/streamcontrol/EngineeringChecks.java b/src/main/java/nl/townsandkingdoms/streamcontrol/EngineeringChecks.java new file mode 100644 index 0000000..c1e3f24 --- /dev/null +++ b/src/main/java/nl/townsandkingdoms/streamcontrol/EngineeringChecks.java @@ -0,0 +1,116 @@ +package nl.townsandkingdoms.streamcontrol; + +import java.util.*; +import com.mojang.authlib.GameProfile; +import io.netty.buffer.Unpooled; +import net.minecraft.core.*; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.entity.vehicle.boat.AbstractBoat; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.component.CustomData; +import net.minecraft.core.component.DataComponents; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.state.properties.BlockStateProperties; + + +/** Opt-in server scenarios. Runs in the disposable integration world only. */ +public final class EngineeringChecks { + private static void check(boolean value,String reason) { if(!value) throw new AssertionError(reason); } + private static CompoundTag options(FlowProfile a,FlowProfile b,ControllerSettings.Redstone redstone) { + var n=new CompoundTag();n.put("ProfileA",a.save());n.put("ProfileB",b.save());n.putInt("RedstoneMode",redstone.ordinal());return n; + } + private static void toolMode(ItemStack tool,FlowWand.Mode mode) { + var n=FlowWand.settings(tool);n.putInt("CalibratorMode",mode.ordinal());tool.set(DataComponents.CUSTOM_DATA,CustomData.of(n)); + } + public static void run(ServerLevel level,Map results) { + BlockPos p=new BlockPos(256,120,256),q=p.east(10);var data=ControlData.get(level); + level.setBlock(p,StreamControl.GENERATOR.get().defaultBlockState(),3); + level.setBlock(q,StreamControl.GENERATOR.get().defaultBlockState(),3); + var a=(ControllerEntity)level.getBlockEntity(p);var b=(ControllerEntity)level.getBlockEntity(q); + var first=new FlowProfile(3,2,1,FlowHeading.EAST,false,FlowProfile.Action.CURRENT); + var second=new FlowProfile(2,1,.5,FlowHeading.SOUTH,false,FlowProfile.Action.CURRENT); + a.controls().configure(options(first,second,ControllerSettings.Redstone.ANALOG)); + check(data.byKey(p.asLong()).still(),"Analog zero must stop its controlled current"); + level.setBlock(p.west(),Blocks.REDSTONE_BLOCK.defaultBlockState(),3);a.refresh(); + check(data.byKey(p.asLong()).dx()>.99 && data.byKey(p.asLong()).strength()==1,"Analog full signal"); + a.controls().configure(options(first,second,ControllerSettings.Redstone.REVERSE)); + check(data.byKey(p.asLong()).dx()<-.99,"Powered reverse mode"); + level.removeBlock(p.west(),false);a.refresh();check(data.byKey(p.asLong()).dx()>.99,"Unpowered reverse mode"); + a.controls().configure(options(first,second,ControllerSettings.Redstone.SWITCH)); + level.setBlock(p.west(),Blocks.REDSTONE_BLOCK.defaultBlockState(),3);a.refresh(); + check(data.byKey(p.asLong()).dz()>.99 && data.byKey(p.asLong()).radius()==2 && data.byKey(p.asLong()).vertical()==1 + && data.byKey(p.asLong()).strength()==.5,"Switch must use every saved Profile B field"); + var release=new FlowProfile(2,1,.5,FlowHeading.SOUTH,false,FlowProfile.Action.RELEASE); + a.controls().configure(options(first,release,ControllerSettings.Redstone.SWITCH));check(data.byKey(p.asLong())==null,"Profile release must restore underlying water"); + level.removeBlock(p.west(),false);a.controls().configure(options(first,second,ControllerSettings.Redstone.IGNORE)); + var saved=a.saveWithoutMetadata(level.registryAccess());var loaded=new ControllerEntity(p,a.getBlockState());loaded.loadWithComponents(net.minecraft.world.level.storage.TagValueInput.create(net.minecraft.util.ProblemReporter.DISCARDING,level.registryAccess(),saved)); + check(loaded.controls().a().equals(first) && loaded.controls().b().equals(second) && loaded.controls().redstone()==ControllerSettings.Redstone.IGNORE,"Controller profiles save/load"); + var old=new CompoundTag();old.putInt("Radius",7);old.putDouble("Strength",.75);old.putBoolean("ConnectedOnly",true); + loaded.loadWithComponents(net.minecraft.world.level.storage.TagValueInput.create(net.minecraft.util.ProblemReporter.DISCARDING,level.registryAccess(),old));check(loaded.radius()==7 && loaded.strength()==.75 && loaded.connectedOnly(),"0.1.x controller settings migration"); + results.put("profileModesPersistenceAndMigration",true); + check(b.controls().link(p),"Valid controller link rejected");b.refresh(); + check(data.byKey(q.asLong()).radius()==3 && data.byKey(q.asLong()).dx()>.99,"Linked controller did not follow source"); + a.configure(FlowHeading.NORTH_WEST,5,.65,false);b.refresh(); + check(data.byKey(q.asLong()).radius()==5 && data.byKey(q.asLong()).dx()<0 && data.byKey(q.asLong()).dz()<0,"Linked changes did not propagate"); + check(!a.controls().link(q) && !b.controls().link(q),"Link cycles and self-links must be rejected"); + check(!b.controls().link(q.east(300)),"Out-of-range link accepted"); + level.removeBlock(p,false);b.refresh();check(data.byKey(q.asLong())==null,"Missing linked source must pause without chunk loading"); + b.controls().unlink();check(data.byKey(q.asLong())!=null,"Unlink did not restore local profile"); + results.put("controllerLinkPropagationCycleAndMissingSource",true); + var player=new net.minecraft.server.level.ServerPlayer(level.getServer(),level,new GameProfile(UUID.randomUUID(),"FlowChecks"),net.minecraft.server.level.ClientInformation.createDefault()) { @Override public void sendSystemMessage(net.minecraft.network.chat.Component message,boolean overlay) {} };player.getAbilities().mayBuild=true; + var tool=new ItemStack(StreamControl.WAND.get());player.setItemInHand(net.minecraft.world.InteractionHand.MAIN_HAND,tool); + toolMode(tool,FlowWand.Mode.COPY);check(FlowWand.operate(player,q,tool),"Calibrator copy failed"); + level.setBlock(p,StreamControl.GENERATOR.get().defaultBlockState(),3);a=(ControllerEntity)level.getBlockEntity(p); + toolMode(tool,FlowWand.Mode.PASTE);check(FlowWand.operate(player,p,tool),"Calibrator paste failed"); + check(a.controls().a().equals(b.controls().a()) && a.controls().b().equals(b.controls().b()),"Calibrator did not copy both profiles"); + toolMode(tool,FlowWand.Mode.LINK);check(FlowWand.operate(player,p,tool) && FlowWand.operate(player,q,tool),"Calibrator linking failed"); + check(p.equals(b.controls().source()),"Calibrator stored wrong source"); + var legacyPos=p.north(20);level.setBlock(legacyPos,Blocks.WATER.defaultBlockState(),3); + data.put(level,new FlowRegion(87654,legacyPos,2,1,1,0,1,false,false,player.getUUID(),data.next())); + toolMode(tool,FlowWand.Mode.RESTORE);check(FlowWand.operate(player,legacyPos,tool) && data.byKey(87654)==null,"Calibrator cannot remove legacy wand edit"); + results.put("calibratorCopyPasteLinkAndLegacyCleanup",true); + var sensorPos=p.east(64);level.setBlock(sensorPos,StreamControl.SENSOR.get().defaultBlockState(),3); + var sensor=(FlowSensorEntity)level.getBlockEntity(sensorPos);var water=sensorPos.offset(2,3,0); + level.setBlock(water,Blocks.WATER.defaultBlockState(),3);data.put(level,new FlowRegion(87655,water,1,0,1,0,1,false,true,player.getUUID(),data.next())); + var sensorOptions=sensor.settings();sensorOptions.putInt("Radius",4);sensorOptions.putInt("Vertical",2);sensor.configure(sensorOptions); + check(sensor.output()==0,"Sensor detected flow above its vertical range"); + sensorOptions.putInt("Vertical",3);sensor.configure(sensorOptions);check(sensor.output()==8,"Sensor did not detect elevated flow"); + check(level.getBlockState(sensorPos).hasAnalogOutputSignal() && level.getBlockState(sensorPos).getAnalogOutputSignal(level,sensorPos,Direction.NORTH)==8,"Comparator output unavailable"); + sensorOptions.putInt("Mode",1);sensor.configure(sensorOptions);check(sensor.output()==3,"East must be direction output 3"); + sensorOptions.putInt("Radius",1);sensor.configure(sensorOptions);check(sensor.output()==0,"Sensor ignored horizontal range"); + sensorOptions.putInt("Radius",4);sensorOptions.putInt("Mode",2);sensorOptions.putBoolean("EmptyOnly",true); + var sensedBoat=net.minecraft.world.entity.EntityType.OAK_BOAT.create(level,net.minecraft.world.entity.EntitySpawnReason.TRIGGERED);sensedBoat.setPos(sensorPos.getX()+.5,sensorPos.getY()+3.5,sensorPos.getZ()+.5);level.addFreshEntity(sensedBoat); + sensor.configure(sensorOptions);check(sensor.output()==1,"Boat count ignored vertical range"); + var rider=new net.minecraft.world.entity.animal.pig.Pig(net.minecraft.world.entity.EntityType.PIG,level);level.addFreshEntity(rider); + sensorOptions.putInt("Vertical",2);sensor.configure(sensorOptions);check(sensor.output()==0,"Boat below/above range was counted"); + sensorOptions.putInt("Vertical",3);sensor.configure(sensorOptions);check(rider.startRiding(sensedBoat,true,true),"Sensor passenger fixture failed to board");sensor.sample();check(sensor.output()==0,"Empty-only sensor counted occupied boat"); + sensorOptions.putBoolean("EmptyOnly",false);sensor.configure(sensorOptions);check(sensor.output()==1,"All-boats sensor omitted occupied boat"); + rider.stopRiding();sensedBoat.discard(); + var reloadedSensor=new FlowSensorEntity(sensorPos,sensor.getBlockState());reloadedSensor.loadWithComponents(net.minecraft.world.level.storage.TagValueInput.create(net.minecraft.util.ProblemReporter.DISCARDING,level.registryAccess(),sensor.saveWithoutMetadata(level.registryAccess()))); + check(reloadedSensor.vertical()==3 && reloadedSensor.radius()==4 && reloadedSensor.mode()==FlowSensorEntity.Mode.BOATS,"Sensor range/mode persistence"); + results.put("sensorVerticalHorizontalDirectionBoatAndComparator",true); + var mooringPos=p.south(64); + while(Math.floorMod(level.getGameTime()+mooringPos.asLong(),10)!=0) mooringPos=mooringPos.above(); + level.setBlock(mooringPos,StreamControl.MOORING.get().defaultBlockState(),3);var mooring=(MooringEntity)level.getBlockEntity(mooringPos); + var boat=net.minecraft.world.entity.EntityType.OAK_BOAT.create(level,net.minecraft.world.entity.EntitySpawnReason.TRIGGERED);boat.setPos(mooringPos.getX()+1.5,mooringPos.getY()+1,mooringPos.getZ()+.5);level.addFreshEntity(boat); + mooring.tick();check(boat.getUUID().equals(mooring.boatId()),"Mooring did not secure empty boat"); + double x=boat.getX(),z=boat.getZ();boat.setPos(x+.2,boat.getY(),z+.2);boat.setDeltaMovement(.2,.03,.2);mooring.tick(); + check(boat.getX()==x && boat.getZ()==z && boat.getDeltaMovement().x==0 && boat.getDeltaMovement().z==0 && boat.getDeltaMovement().y==.03,"Mooring did not hold horizontal position and preserve bobbing"); + check(rider.startRiding(boat,true,true),"Mooring passenger fixture failed to board");mooring.tick();check(mooring.boatId()==null,"Boarding did not immediately release boat");rider.stopRiding(); + for(int i=0;i<42;i++) mooring.tick();check(mooring.boatId()!=null,"Mooring did not resume after release cooldown"); + level.setBlock(mooringPos,mooring.getBlockState().setValue(UtilityBlock.POWERED,true),2);mooring.tick();check(mooring.boatId()==null,"Redstone did not release boat"); + level.removeBlock(mooringPos,false);boat.discard();rider.discard();results.put("mooringHoldBoardingRedstoneAndCooldown",true); + var buf=new RegistryFriendlyByteBuf(Unpooled.buffer(),level.registryAccess()); + try { + var configure=new ControlNetwork.Configure(p.asLong(),3,1,false,FlowHeading.EAST.id(),options(first,second,ControllerSettings.Redstone.SWITCH)); + ControlNetwork.Configure.CODEC.encode(buf,configure);check(configure.equals(ControlNetwork.Configure.CODEC.decode(buf)),"Profile packet round trip");buf.clear(); + var utility=new ControlNetwork.UtilityConfigure(sensorPos.asLong(),sensorOptions);ControlNetwork.UtilityConfigure.CODEC.encode(buf,utility); + check(utility.equals(ControlNetwork.UtilityConfigure.CODEC.decode(buf)),"Sensor packet round trip"); + } finally { buf.release(); } + results.put("engineeringPacketRoundTrips",true); + results.put("standaloneCoreEngineering",true); + level.removeBlock(p,false);level.removeBlock(q,false);level.removeBlock(sensorPos,false);data.remove(level,87655); + } +} diff --git a/src/main/java/nl/townsandkingdoms/streamcontrol/FlowController.java b/src/main/java/nl/townsandkingdoms/streamcontrol/FlowController.java new file mode 100644 index 0000000..0135f72 --- /dev/null +++ b/src/main/java/nl/townsandkingdoms/streamcontrol/FlowController.java @@ -0,0 +1,25 @@ +package nl.townsandkingdoms.streamcontrol; + +import net.minecraft.core.Direction; +import net.minecraft.world.level.block.entity.BlockEntity; + +/** Shared controller contract; the base mod never loads Create classes. */ +public interface FlowController { + BlockEntity blockEntity(); + ControllerSettings controls(); + default boolean isStill() { return blockEntity().getBlockState().getBlock() instanceof ControllerBlock b && b.still; } + /** NaN means a normal controller. A kinetic controller returns signed RPM. */ + default float rpm() { return Float.NaN; } + default int radius() { return controls().a().radius(); } + default double strength() { return controls().a().strength(); } + default boolean connectedOnly() { return controls().a().connectedOnly(); } + default FlowHeading heading() { return FlowHeading.fromState(blockEntity().getBlockState()); } + default void configure(FlowHeading heading, int radius, double strength, boolean connected) { + controls().setA(new FlowProfile(radius,controls().a().vertical(),strength,heading,connected,controls().a().action())); + } + default void configure(Direction d, int r, double speed, boolean connected) { configure(FlowHeading.fromDirection(d),r,speed,connected); } + default void configure(Direction d, int r, double speed) { configure(d,r,speed,connectedOnly()); } + default void setRadius(int r) { configure(heading(),r,strength(),connectedOnly()); } + default void refresh() { controls().refresh(); } + default void waterChanged() { controls().invalidate(); refresh(); } +} diff --git a/src/main/java/nl/townsandkingdoms/streamcontrol/FlowHeading.java b/src/main/java/nl/townsandkingdoms/streamcontrol/FlowHeading.java index e71cc42..1d6433c 100644 --- a/src/main/java/nl/townsandkingdoms/streamcontrol/FlowHeading.java +++ b/src/main/java/nl/townsandkingdoms/streamcontrol/FlowHeading.java @@ -28,6 +28,10 @@ public String abbreviation(){return switch(this){ case NORTH -> "n";case NORTH_EAST -> "ne";case EAST -> "e";case SOUTH_EAST -> "se"; case SOUTH -> "s";case SOUTH_WEST -> "sw";case WEST -> "w";case NORTH_WEST -> "nw"; };} + public static FlowHeading fromVector(double x,double z) { + if(!Double.isFinite(x) || !Double.isFinite(z) || Math.hypot(x,z)<1e-6) return null; + return VALUES[Math.floorMod((int)Math.round(Math.atan2(x,-z)/(Math.PI/4)),8)]; + } public static FlowHeading fromId(int id){return id>=0 && id NORTH;case EAST -> EAST;case SOUTH -> SOUTH;case WEST -> WEST; diff --git a/src/main/java/nl/townsandkingdoms/streamcontrol/FlowProfile.java b/src/main/java/nl/townsandkingdoms/streamcontrol/FlowProfile.java new file mode 100644 index 0000000..9412fad --- /dev/null +++ b/src/main/java/nl/townsandkingdoms/streamcontrol/FlowProfile.java @@ -0,0 +1,38 @@ +package nl.townsandkingdoms.streamcontrol; + +import net.minecraft.nbt.CompoundTag; + +/** A bounded, portable controller preset. No world coordinates or permissions are copied. */ +public record FlowProfile(int radius, int vertical, double strength, FlowHeading heading, + boolean connectedOnly, Action action) { + public enum Action { CURRENT, REVERSE, CALM, RELEASE; + public static Action from(int id) { return values()[Math.clamp(id, 0, values().length-1)]; } + } + public FlowProfile { + radius = Math.clamp(radius,1,32); + vertical = Math.clamp(vertical,0,16); + strength = Double.isFinite(strength) ? Math.clamp(strength,0.05,2.0) : 1.0; + if (heading == null) heading = FlowHeading.NORTH; + if (action == null) action = Action.CURRENT; + } + public static FlowProfile defaults(boolean still) { + return new FlowProfile(4,4,1.0, FlowHeading.NORTH, false, still ? Action.CALM : Action.CURRENT); + } + public FlowProfile forAnchor(boolean anchor) { + return anchor && action != Action.RELEASE ? new FlowProfile(radius,vertical,strength,heading,connectedOnly,Action.CALM) : this; + } + public FlowProfile withHeading(FlowHeading h) { return new FlowProfile(radius,vertical,strength,h,connectedOnly,action); } + public CompoundTag save() { + var n = new CompoundTag(); n.putInt("Radius",radius); n.putInt("Vertical",vertical); + n.putDouble("Strength",strength); n.putInt("Heading",heading.id()); + n.putBoolean("ConnectedOnly",connectedOnly); n.putInt("Action",action.ordinal()); return n; + } + public static FlowProfile load(CompoundTag n, boolean still) { + var d = defaults(still); + return new FlowProfile(n.contains("Radius") ? n.getIntOr("Radius",0) : d.radius, + n.contains("Vertical") ? n.getIntOr("Vertical",0) : d.vertical, + n.contains("Strength") ? n.getDoubleOr("Strength",0.0) : d.strength, + n.contains("Heading") ? FlowHeading.fromId(n.getIntOr("Heading",0)) : d.heading, + n.getBooleanOr("ConnectedOnly",false), n.contains("Action") ? Action.from(n.getIntOr("Action",0)) : d.action).forAnchor(still); + } +} diff --git a/src/main/java/nl/townsandkingdoms/streamcontrol/FlowRegion.java b/src/main/java/nl/townsandkingdoms/streamcontrol/FlowRegion.java index d283c34..8093169 100644 --- a/src/main/java/nl/townsandkingdoms/streamcontrol/FlowRegion.java +++ b/src/main/java/nl/townsandkingdoms/streamcontrol/FlowRegion.java @@ -58,7 +58,7 @@ public static FlowRegion load(CompoundTag n) { double speed = n.getDoubleOr("Strength", 0.0); return new FlowRegion(n.getLongOr("Key", 0L), BlockPos.of(n.getLongOr("Pos", 0L)), Math.clamp(n.getIntOr("Radius", 0), 1, 32), Math.clamp(n.getIntOr("Vertical", 0), 0, 16), dx / length, dz / length, - Double.isFinite(speed) ? Math.clamp(speed, 0.05, 2) : 1, n.getBooleanOr("Still", false), n.getBooleanOr("Controller", false), + Double.isFinite(speed) ? Math.clamp(speed, 0, 2) : 1, n.getBooleanOr("Still", false), n.getBooleanOr("Controller", false), owner(n), n.getLongOr("Sequence", 0L), n.getBooleanOr("ConnectedOnly", false), n.getLongArray("WaterMask").orElseGet(() -> new long[0])); } diff --git a/src/main/java/nl/townsandkingdoms/streamcontrol/FlowSensorEntity.java b/src/main/java/nl/townsandkingdoms/streamcontrol/FlowSensorEntity.java new file mode 100644 index 0000000..b1b9e19 --- /dev/null +++ b/src/main/java/nl/townsandkingdoms/streamcontrol/FlowSensorEntity.java @@ -0,0 +1,80 @@ +package nl.townsandkingdoms.streamcontrol; + +import net.minecraft.core.*; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.network.protocol.game.ClientboundBlockEntityDataPacket; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.tags.FluidTags; +import net.minecraft.world.entity.vehicle.boat.AbstractBoat; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.phys.AABB; + +/** Bounded, staggered sampling of loaded chunks; vertical reach is independent of horizontal range. */ +public final class FlowSensorEntity extends BlockEntity implements UtilityAccess { + public enum Mode { SPEED, DIRECTION, BOATS; + public static Mode from(int id) { return values()[Math.clamp(id,0,values().length-1)]; } + } + private int radius=4, vertical=4, output; + private Mode mode=Mode.SPEED; + private boolean emptyOnly; + private double measuredSpeed; + private FlowHeading measuredHeading; + public FlowSensorEntity(BlockPos p,BlockState s) { super(StreamControl.SENSOR_ENTITY.get(),p,s); } + @Override public BlockEntity blockEntity() { return this; } + public int output() { return output; } + public int radius() { return Math.clamp(radius,1,ControlConfig.SENSOR_MAX_RADIUS.get()); } + public int vertical() { return Math.clamp(vertical,0,ControlConfig.SENSOR_MAX_VERTICAL.get()); } + public Mode mode() { return mode; } + @Override public CompoundTag settings() { + var n=new CompoundTag();n.putString("Kind","sensor");n.putInt("Radius",radius());n.putInt("Vertical",vertical()); + n.putInt("MaxRadius",ControlConfig.SENSOR_MAX_RADIUS.get());n.putInt("MaxVertical",ControlConfig.SENSOR_MAX_VERTICAL.get()); + n.putInt("Mode",mode.ordinal());n.putInt("Output",output);n.putBoolean("EmptyOnly",emptyOnly); + n.putDouble("MeasuredSpeed",measuredSpeed);n.putInt("MeasuredHeading",measuredHeading==null?-1:measuredHeading.id());return n; + } + @Override public void configure(CompoundTag n) { + radius=Math.clamp(n.getIntOr("Radius",0),1,ControlConfig.SENSOR_MAX_RADIUS.get()); + vertical=Math.clamp(n.getIntOr("Vertical",0),0,ControlConfig.SENSOR_MAX_VERTICAL.get()); + mode=Mode.from(n.getIntOr("Mode",0));emptyOnly=n.getBooleanOr("EmptyOnly",false);setChanged();sample(); + if(level instanceof ServerLevel server) server.sendBlockUpdated(worldPosition,getBlockState(),getBlockState(),2); + } + @Override protected void saveAdditional(net.minecraft.world.level.storage.ValueOutput out) { var n=new CompoundTag();n.putInt("Radius",radius);n.putInt("Vertical",vertical);n.putInt("Mode",mode.ordinal());n.putBoolean("EmptyOnly",emptyOnly);n.putInt("Output",output);out.store("Engineering",CompoundTag.CODEC,n); } + @Override protected void loadAdditional(net.minecraft.world.level.storage.ValueInput in) { super.loadAdditional(in);var n=in.read("Engineering",CompoundTag.CODEC).orElseGet(CompoundTag::new);radius=n.contains("Radius")?n.getIntOr("Radius",0):4;vertical=n.contains("Vertical")?n.getIntOr("Vertical",0):4;mode=Mode.from(n.getIntOr("Mode",0));emptyOnly=n.getBooleanOr("EmptyOnly",false);output=Math.clamp(n.getIntOr("Output",0),0,15); } + @Override public ClientboundBlockEntityDataPacket getUpdatePacket() { return ClientboundBlockEntityDataPacket.create(this); } + @Override public CompoundTag getUpdateTag(HolderLookup.Provider r) { return saveWithoutMetadata(r); } + public static void tick(Level l,BlockPos p,BlockState s,FlowSensorEntity be) { + int interval=ControlConfig.SENSOR_INTERVAL.get(); + if(Math.floorMod(l.getGameTime()+p.asLong(),interval)==0) be.sample(); + } + public static int speedSignal(double speed) { return speed<=1e-6 || !Double.isFinite(speed) ? 0 : Math.clamp((int)Math.ceil(speed/ControlConfig.MAX_SPEED.get()*15),1,15); } + public void sample() { + if(!(level instanceof ServerLevel server)) return; + int result=0;double best=0;FlowHeading heading=null;int r=radius(),v=vertical(); + if(ControlConfig.ENABLED.get()) { + if(mode==Mode.BOATS) { + AABB box=new AABB(worldPosition).inflate(r,v,r); + result=Math.min(15,server.getEntitiesOfClass(AbstractBoat.class,box,b->b.isAlive() && (!emptyOnly || !b.isVehicle()) + && inside(b.getX(),b.getY(),b.getZ(),r,v)).size()); + } else { + var p=new BlockPos.MutableBlockPos(); + for(int x=-r;x<=r;x++) for(int z=-r;z<=r;z++) { + if(x*x+z*z>r*r) continue; + for(int y=-v;y<=v;y++) { + p.setWithOffset(worldPosition,x,y,z);if(!server.hasChunkAt(p)) continue; + var fluid=server.getFluidState(p);if(!fluid.is(FluidTags.WATER)) continue; + var flow=fluid.getFlow(server,p);double speed=Math.hypot(flow.x,flow.z); + if(Double.isFinite(speed) && speed>best+1e-6) { best=speed;heading=FlowHeading.fromVector(flow.x,flow.z); } + } + } + result=mode==Mode.SPEED ? speedSignal(best) : heading==null?0:heading.id()+1; + } + } + measuredSpeed=best;measuredHeading=heading; + if(output!=result) { output=result;setChanged();server.updateNeighbourForOutputSignal(worldPosition,getBlockState().getBlock());server.sendBlockUpdated(worldPosition,getBlockState(),getBlockState(),2); } + } + private boolean inside(double x,double y,double z,int r,int v) { + double dx=x-worldPosition.getX()-.5,dz=z-worldPosition.getZ()-.5; + return dx*dx+dz*dz<=r*r && y>=worldPosition.getY()-v && y= ControlConfig.MAX_PLAYER_REGIONS.get()) { message(p, "message.streamcontrol.limit"); return InteractionResult.FAIL; } - var look = p.getLookAngle(); double len = Math.hypot(look.x, look.z); - double dx = len > 1e-4 ? look.x / len : p.getDirection().getStepX(); - double dz = len > 1e-4 ? look.z / len : p.getDirection().getStepZ(); - var region = new FlowRegion(key, pos.immutable(), radius, ControlConfig.VERTICAL.get(), dx, dz, - ControlConfig.strength(n.contains("Strength") ? n.getDoubleOr("Strength", 0) : ControlConfig.SPEED.get()), mode == 1, false, p.getUUID(), data.next()); - if (!data.put(server, region)) { message(p, "message.streamcontrol.limit"); return InteractionResult.FAIL; } - message(p, "message.streamcontrol.applied", Component.translatable("mode.streamcontrol." + mode), radius); + if(mode==Mode.PASTE) { + var clipboard=n.getCompoundOrEmpty("Calibration");String kind=clipboard.getStringOr("Kind",""); + if("controller".equals(kind) && be instanceof FlowController controller) controller.controls().paste(clipboard); + else if(be instanceof UtilityAccess utility && utility.settings().getStringOr("Kind","").equals(kind)) utility.configure(clipboard); + else { message(player,"incompatible");return false; } + message(player,"pasted");return true; } - world.playSound(null, pos, SoundEvents.AMETHYST_BLOCK_CHIME, SoundSource.PLAYERS, 0.6f, 1.2f); - if (ControlConfig.PARTICLES.get()) for (int i = 0; i < 32; i++) { - double a = i * Math.PI / 16; - server.sendParticles(ParticleTypes.END_ROD, pos.getX()+0.5+Math.cos(a)*radius, pos.getY()+1.05, pos.getZ()+0.5+Math.sin(a)*radius, 1, 0, 0, 0, 0); + if(mode==Mode.LINK) { + if(!(be instanceof FlowController controller)) { message(player,"controller_required");return false; } + if(player.isShiftKeyDown()) { controller.controls().unlink();n.remove("LinkPos");n.remove("LinkDimension");store(stack,n);message(player,"unlinked");return true; } + if(!n.contains("LinkPos")) { + n.putLong("LinkPos",pos.asLong());n.putString("LinkDimension",level.dimension().identifier().toString());store(stack,n);message(player,"source_selected");return true; + } + BlockPos source=BlockPos.of(n.getLongOr("LinkPos",0L)); + if(!level.dimension().identifier().toString().equals(n.getStringOr("LinkDimension","")) || !level.hasChunkAt(source) + || !allowed(player,source,stack) || !controller.controls().link(source)) { message(player,"invalid_link");return false; } + message(player,"linked");return true; + } + var data=ControlData.get(level);var old=data.at(pos.getX(),pos.getY(),pos.getZ()); + if(old==null || old.controller()) { message(player,"no_legacy_edit");return false; } + if(!old.owner().equals(player.getUUID()) && !player.permissions().hasPermission(net.minecraft.server.permissions.Permissions.COMMANDS_GAMEMASTER)) { message(player,"denied");return false; } + data.remove(level,old.key());message(player,"restored");return true; + } + private static void inspect(ServerPlayer player,BlockPos pos) { + var level=player.level();var flow=level.getFluidState(pos).getFlow(level,pos); + FlowHeading heading=FlowHeading.fromVector(flow.x,flow.z);var region=ControlData.get(level).at(pos.getX(),pos.getY(),pos.getZ()); + String direction=heading==null?"Still":heading.key();String source=region==null?"Natural":region.controller()?"Controller":"Legacy edit"; + player.sendSystemMessage(Component.literal(String.format(Locale.ROOT,"%s | %s | %.0f%%",source,direction,Math.hypot(flow.x,flow.z)*100)),true); + } + @Override public InteractionResult use(Level world,Player player,InteractionHand hand) { + ItemStack stack=player.getItemInHand(hand);if(world.isClientSide()) return InteractionResult.SUCCESS; + var hit=getPlayerPOVHitResult(world,player,ClipContext.Fluid.ANY); + if(hit.getType()==HitResult.Type.MISS) { + var n=settings(stack); + if(player.isShiftKeyDown()) { + int next=(mode(stack).ordinal()+1)%Mode.values().length;n.putInt("CalibratorMode",next); + n.remove("LinkPos");n.remove("LinkDimension");message(player,"mode",Component.translatable(Mode.values()[next].key())); + } else { n.putBoolean("HideBounds",!n.getBooleanOr("HideBounds",false));message(player,n.getBooleanOr("HideBounds",false)?"bounds_off":"bounds_on"); } + store(stack,n);return InteractionResult.SUCCESS; } - p.getCooldowns().addCooldown(stack, 5); return InteractionResult.SUCCESS; + if(!(player instanceof ServerPlayer server) || !allowed(player,hit.getBlockPos(),stack)) return InteractionResult.FAIL; + if(!world.getFluidState(hit.getBlockPos()).is(FluidTags.WATER)) return InteractionResult.PASS; + return operate(server,hit.getBlockPos(),stack)?InteractionResult.SUCCESS:InteractionResult.FAIL; } - @Override public void appendHoverText(ItemStack stack, TooltipContext context, net.minecraft.world.item.component.TooltipDisplay display, java.util.function.Consumer lines, TooltipFlag flag) { - var n = settings(stack); lines.accept(Component.translatable("tooltip.streamcontrol.mode", Component.translatable("mode.streamcontrol." + Math.floorMod(n.getIntOr("Mode", 0), 3)))); - lines.accept(Component.translatable("tooltip.streamcontrol.wand")); lines.accept(Component.translatable("tooltip.streamcontrol.wand_air")); + @Override public void appendHoverText(ItemStack stack,TooltipContext context,net.minecraft.world.item.component.TooltipDisplay display,java.util.function.Consumer lines,TooltipFlag flag) { + lines.accept(Component.translatable("calibrator.streamcontrol.mode",Component.translatable(mode(stack).key()))); + lines.accept(Component.translatable("calibrator.streamcontrol.tooltip")); + lines.accept(Component.translatable("calibrator.streamcontrol.tooltip_cycle")); + lines.accept(Component.translatable("calibrator.streamcontrol.tooltip_bounds")); + if(settings(stack).contains("Calibration")) lines.accept(Component.translatable("calibrator.streamcontrol.clipboard_ready")); + if(settings(stack).contains("LinkPos")) lines.accept(Component.translatable("calibrator.streamcontrol.source_ready")); } } diff --git a/src/main/java/nl/townsandkingdoms/streamcontrol/IntegrationChecks.java b/src/main/java/nl/townsandkingdoms/streamcontrol/IntegrationChecks.java index 3d9aa10..1f5a00a 100644 --- a/src/main/java/nl/townsandkingdoms/streamcontrol/IntegrationChecks.java +++ b/src/main/java/nl/townsandkingdoms/streamcontrol/IntegrationChecks.java @@ -2,7 +2,6 @@ import com.google.gson.GsonBuilder; import dev.streamsreflowing.flow.StreamFlowGrid; -import dev.streamsreflowing.fabric.fluid.StreamFluids; import io.netty.buffer.Unpooled; import java.nio.file.Files; import java.util.*; @@ -17,6 +16,10 @@ /** Opt-in dedicated-server test; never runs in a player's normal world. */ public final class IntegrationChecks { public static void register() { if(Boolean.getBoolean("streamcontrol.integrationTest")) net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents.SERVER_STARTED.register(IntegrationChecks::run); } + private static net.minecraft.world.level.block.state.BlockState streamState() { + var b=net.minecraft.core.registries.BuiltInRegistries.BLOCK.get(net.minecraft.resources.Identifier.fromNamespaceAndPath("streamsreflowing","stream")); + if(b==Blocks.AIR) throw new AssertionError("Streams fluid block missing");return b.defaultBlockState(); + } private static void check(boolean condition, String message) { if (!condition) throw new AssertionError(message); } private static void run(net.minecraft.server.MinecraftServer server) { var level = server.overworld(); var results = new LinkedHashMap(); @@ -30,8 +33,8 @@ private static void run(net.minecraft.server.MinecraftServer server) { check(data.at(16,122,16) != null && data.at(16,123,16) == null,"Height boundary"); Vec3 flow = level.getFluidState(p).getFlow(level,p); check(flow.x > 0.7 && Math.abs(flow.z) < 0.01,"Vanilla water did not follow direction: " + flow); - check(StreamFlowGrid.angleForEntityPush(level,16,120,16) == StreamFlowGrid.encode(1,0),"Vessel direction hook"); - check(StreamFlowGrid.speedForPhysics(level,16,120,16) == 0.75,"Vessel strength hook"); + check(StreamFlowGrid.noDecompilePermission1(level,16,120,16) == StreamFlowGrid.encode(1,0),"Vessel direction hook"); + check(StreamFlowGrid.sourceGivenToAuthorizedOnly1(level,16,120,16) == 0.75,"Vessel strength hook"); results.put("vanillaWaterAndVesselFlow",true); for(int dx=-4;dx<=4;dx++) if(dx!=0) { int top=dx>0?123:117; @@ -41,19 +44,18 @@ private static void run(net.minecraft.server.MinecraftServer server) { flow=level.getFluidState(p).getFlow(level,p); check(flow.x>0.7 && Math.abs(flow.z)<0.01,"Explicit Flow Wand direction was overridden by automatic downhill correction: "+flow); results.put("manualDirectionRetainsPriorityOverDownhillGuard",true); - level.setBlock(p, StreamFluids.STREAM_BLOCK.defaultBlockState().setValue(dev.streamsreflowing.fabric.fluid.StreamLiquidBlock.FLOW, 4),3); + level.setBlock(p, streamState(),3); flow = level.getFluidState(p).getFlow(level,p); check(flow.x > 0.7 && Math.abs(flow.z) < 0.01,"Baked stream direction: " + flow); var still = new FlowRegion(102,p,3,2,1,0,1,true,true,owner,data.next()); data.put(level,still); check(data.at(16,120,16).still(),"Still block precedence"); check(level.getFluidState(p).getFlow(level,p).lengthSqr() < 1e-10,"Stillness did not cancel baked stream flow"); - check(StreamFlowGrid.speedForPhysics(level,16,120,16) == 0,"Stillness did not cancel vessel drift"); + check(StreamFlowGrid.sourceGivenToAuthorizedOnly1(level,16,120,16) == 0,"Stillness did not cancel vessel drift"); check(data.at(20,120,16).key() == directed.key(),"Stillness leaked beyond radius"); results.put("bakedWaterStillnessAndOverlap",true); for (int x = -2; x <= 2; x++) for (int z = -2; z <= 2; z++) - level.setBlock(p.offset(x,0,z),StreamFluids.STREAM_BLOCK.defaultBlockState() - .setValue(dev.streamsreflowing.fabric.fluid.StreamLiquidBlock.FLOW,4),3); + level.setBlock(p.offset(x,0,z),streamState(),3); var boat = net.minecraft.world.entity.EntityType.OAK_BOAT.create(level,net.minecraft.world.entity.EntitySpawnReason.TRIGGERED); boat.setPos(16.5,120.2,16.5); level.addFreshEntity(boat); for (int tick = 0; tick < 20; tick++) boat.tick(); @@ -92,6 +94,7 @@ private static void run(net.minecraft.server.MinecraftServer server) { check(server.getCommands().getDispatcher().execute("streamcontrol status",server.createCommandSourceStack())==1,"Status command"); ControllerRegressionChecks.run(level); results.put("controllerGuiAndConnectedWaterRegression",true); HeadingRegressionChecks.run(level); results.put("eightHeadingPhysicsPersistenceAndPackets",true); + EngineeringChecks.run(level,results); results.put("passed",true); } catch (Throwable t) { results.put("passed",false);results.put("error",t.toString()); t.printStackTrace(); } finally { data.remove(level,101);data.remove(level,102); } diff --git a/src/main/java/nl/townsandkingdoms/streamcontrol/MooringEntity.java b/src/main/java/nl/townsandkingdoms/streamcontrol/MooringEntity.java new file mode 100644 index 0000000..e4e195a --- /dev/null +++ b/src/main/java/nl/townsandkingdoms/streamcontrol/MooringEntity.java @@ -0,0 +1,81 @@ +package nl.townsandkingdoms.streamcontrol; + +import java.util.*; +import net.minecraft.core.*; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.entity.vehicle.boat.AbstractBoat; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.phys.AABB; + +/** Holds one empty boat in place without loading chunks or pulling occupied boats. */ +public final class MooringEntity extends BlockEntity implements UtilityAccess { + private static final Map> CLAIMS=new WeakHashMap<>(); + private int radius=4, vertical=2, cooldown; + private boolean enabled=true; + private UUID boatId; + private double moorX,moorZ; + public MooringEntity(BlockPos p,BlockState s) { super(StreamControl.MOORING_ENTITY.get(),p,s); } + @Override public BlockEntity blockEntity() { return this; } + public UUID boatId() { return boatId; } + public int radius() { return Math.clamp(radius,1,ControlConfig.MOORING_MAX_RADIUS.get()); } + public int vertical() { return Math.clamp(vertical,0,ControlConfig.MOORING_MAX_VERTICAL.get()); } + @Override public CompoundTag settings() { + var n=new CompoundTag();n.putString("Kind","mooring");n.putInt("Radius",radius());n.putInt("Vertical",vertical()); + n.putInt("MaxRadius",ControlConfig.MOORING_MAX_RADIUS.get());n.putInt("MaxVertical",ControlConfig.MOORING_MAX_VERTICAL.get()); + n.putBoolean("Enabled",enabled);n.putBoolean("Secured",boatId!=null);n.putBoolean("Powered",getBlockState().getValue(UtilityBlock.POWERED));return n; + } + @Override public void configure(CompoundTag n) { + radius=Math.clamp(n.getIntOr("Radius",0),1,ControlConfig.MOORING_MAX_RADIUS.get());vertical=Math.clamp(n.getIntOr("Vertical",0),0,ControlConfig.MOORING_MAX_VERTICAL.get()); + enabled=n.getBooleanOr("Enabled",false);release();changed(); + } + @Override protected void saveAdditional(net.minecraft.world.level.storage.ValueOutput out) { + var n=new CompoundTag();n.putInt("Radius",radius);n.putInt("Vertical",vertical);n.putBoolean("Enabled",enabled); + if(boatId!=null) { n.store("Boat",net.minecraft.core.UUIDUtil.CODEC,boatId);n.putDouble("MoorX",moorX);n.putDouble("MoorZ",moorZ); }out.store("Engineering",CompoundTag.CODEC,n); } + @Override protected void loadAdditional(net.minecraft.world.level.storage.ValueInput in) { + super.loadAdditional(in);var n=in.read("Engineering",CompoundTag.CODEC).orElseGet(CompoundTag::new);radius=n.contains("Radius")?n.getIntOr("Radius",0):4;vertical=n.contains("Vertical")?n.getIntOr("Vertical",0):2;enabled=!n.contains("Enabled")||n.getBooleanOr("Enabled",false); + boatId=n.contains("Boat")?n.read("Boat",net.minecraft.core.UUIDUtil.CODEC).orElse(null):null;moorX=n.getDoubleOr("MoorX",0.0);moorZ=n.getDoubleOr("MoorZ",0.0); + } + private void changed() { + setChanged();if(level instanceof ServerLevel server && !isRemoved()) server.sendBlockUpdated(worldPosition,getBlockState(),getBlockState(),2); + } + @Override public net.minecraft.network.protocol.game.ClientboundBlockEntityDataPacket getUpdatePacket() { return net.minecraft.network.protocol.game.ClientboundBlockEntityDataPacket.create(this); } + @Override public CompoundTag getUpdateTag(HolderLookup.Provider r) { return saveWithoutMetadata(r); } + public void release() { release(true); } + private void release(boolean notify) { + if(level instanceof ServerLevel server && boatId!=null) CLAIMS.computeIfAbsent(server,s->new HashMap<>()).remove(boatId,worldPosition); + if(boatId!=null) { boatId=null;if(notify) changed(); } cooldown=40; + } + @Override public void setRemoved() { release(false);super.setRemoved(); } + private boolean inRange(AbstractBoat boat) { + double dx=boat.getX()-worldPosition.getX()-.5,dz=boat.getZ()-worldPosition.getZ()-.5; + return dx*dx+dz*dz<=radius()*radius() && boat.getY()>=worldPosition.getY()-vertical() && boat.getY()new HashMap<>());BlockPos other=map.get(boat.getUUID()); + if(other!=null && !other.equals(worldPosition) && server.hasChunkAt(other) + && server.getBlockEntity(other) instanceof MooringEntity post && boat.getUUID().equals(post.boatId)) return false; + map.put(boat.getUUID(),worldPosition.immutable());return true; + } + public static void tick(Level l,BlockPos p,BlockState s,MooringEntity be) { be.tick(); } + public void tick() { + if(!(level instanceof ServerLevel server)) return; + if(!ControlConfig.ENABLED.get() || !enabled || getBlockState().getValue(UtilityBlock.POWERED)) { if(boatId!=null) release();return; } + if(cooldown>0) { cooldown--;return; } + if(boatId!=null) { + var entity=server.getEntity(boatId); + if(!(entity instanceof AbstractBoat boat) || !boat.isAlive() || boat.isVehicle() || !inRange(boat) + || Math.hypot(boat.getX()-moorX,boat.getZ()-moorZ)>1.5 || !claim(server,boat)) { release();return; } + // Preserve vertical bobbing; horizontal momentum is canceled while moored. + var velocity=boat.getDeltaMovement();boat.setDeltaMovement(0,velocity.y,0);boat.setPos(moorX,boat.getY(),moorZ);boat.hurtMarked=true; + return; + } + if(Math.floorMod(server.getGameTime()+worldPosition.asLong(),10)!=0) return; + var candidates=server.getEntitiesOfClass(AbstractBoat.class,new AABB(worldPosition).inflate(radius(),vertical(),radius()), + boat->boat.isAlive() && !boat.isVehicle() && inRange(boat)); + candidates.sort(Comparator.comparingDouble(b->b.distanceToSqr(worldPosition.getX()+.5,worldPosition.getY()+.5,worldPosition.getZ()+.5))); + for(AbstractBoat boat:candidates) if(claim(server,boat)) { boatId=boat.getUUID();moorX=boat.getX();moorZ=boat.getZ();changed();break; } + } +} diff --git a/src/main/java/nl/townsandkingdoms/streamcontrol/StreamControl.java b/src/main/java/nl/townsandkingdoms/streamcontrol/StreamControl.java index e7a45db..fc15f5e 100644 --- a/src/main/java/nl/townsandkingdoms/streamcontrol/StreamControl.java +++ b/src/main/java/nl/townsandkingdoms/streamcontrol/StreamControl.java @@ -18,20 +18,30 @@ import net.fabricmc.fabric.api.command.v2.CommandRegistrationCallback; public final class StreamControl implements ModInitializer { public static final String ID="streamcontrol"; + public static final java.util.List> OPTIONAL_ITEMS=new java.util.ArrayList<>(); private static Identifier id(String n){return Identifier.fromNamespaceAndPath(ID,n);} private static ControllerBlock block(String n,boolean still,BlockBehaviour.Properties p){return Registry.register(BuiltInRegistries.BLOCK,id(n),new ControllerBlock(still,p.setId(ResourceKey.create(Registries.BLOCK,id(n)))));} private static T item(String n,java.util.function.Function factory,Item.Properties p){return Registry.register(BuiltInRegistries.ITEM,id(n),factory.apply(p.setId(ResourceKey.create(Registries.ITEM,id(n)))));} private static final ControllerBlock GENERATOR_BLOCK=block("current_generator",false,BlockBehaviour.Properties.ofFullCopy(Blocks.COPPER_BLOCK).strength(3)); private static final ControllerBlock ANCHOR_BLOCK=block("stillwater_anchor",true,BlockBehaviour.Properties.ofFullCopy(Blocks.PRISMARINE).strength(3)); public static final Supplier GENERATOR=()->GENERATOR_BLOCK,ANCHOR=()->ANCHOR_BLOCK; + private static final UtilityBlock SENSOR_BLOCK=Registry.register(BuiltInRegistries.BLOCK,id("flow_sensor"),new UtilityBlock(true,BlockBehaviour.Properties.ofFullCopy(Blocks.COPPER_BLOCK).strength(3).setId(ResourceKey.create(Registries.BLOCK,id("flow_sensor"))))); + private static final UtilityBlock MOORING_BLOCK=Registry.register(BuiltInRegistries.BLOCK,id("mooring_post"),new UtilityBlock(false,BlockBehaviour.Properties.ofFullCopy(Blocks.OAK_PLANKS).strength(3).noOcclusion().setId(ResourceKey.create(Registries.BLOCK,id("mooring_post"))))); + public static final Supplier SENSOR=()->SENSOR_BLOCK,MOORING=()->MOORING_BLOCK; + public static final Supplier SENSOR_ITEM=()->(BlockItem)SENSOR_BLOCK.asItem(),MOORING_ITEM=()->(BlockItem)MOORING_BLOCK.asItem(); + private static final BlockEntityType SENSOR_TYPE=Registry.register(BuiltInRegistries.BLOCK_ENTITY_TYPE,id("flow_sensor"),FabricBlockEntityTypeBuilder.create(FlowSensorEntity::new,SENSOR_BLOCK).build()); + private static final BlockEntityType MOORING_TYPE=Registry.register(BuiltInRegistries.BLOCK_ENTITY_TYPE,id("mooring_post"),FabricBlockEntityTypeBuilder.create(MooringEntity::new,MOORING_BLOCK).build()); + public static final Supplier> SENSOR_ENTITY=()->SENSOR_TYPE; + public static final Supplier> MOORING_ENTITY=()->MOORING_TYPE; private static final FlowWand WAND_ITEM=item("flow_wand",FlowWand::new,new Item.Properties().stacksTo(1)); public static final Supplier WAND=()->WAND_ITEM; public static final Supplier GENERATOR_ITEM=()->(BlockItem)GENERATOR_BLOCK.asItem(),ANCHOR_ITEM=()->(BlockItem)ANCHOR_BLOCK.asItem(); private static final BlockEntityType CONTROLLER_TYPE=Registry.register(BuiltInRegistries.BLOCK_ENTITY_TYPE,id("controller"),FabricBlockEntityTypeBuilder.create(ControllerEntity::new,GENERATOR_BLOCK,ANCHOR_BLOCK).build()); public static final Supplier> CONTROLLER=()->CONTROLLER_TYPE; @Override public void onInitialize(){ - item("current_generator",p->new BlockItem(GENERATOR_BLOCK,p),new Item.Properties());item("stillwater_anchor",p->new BlockItem(ANCHOR_BLOCK,p),new Item.Properties()); - Registry.register(BuiltInRegistries.CREATIVE_MODE_TAB,id("flow_control"),FabricCreativeModeTab.builder().title(Component.translatable("itemGroup.streamcontrol")).icon(()->new ItemStack(WAND_ITEM)).displayItems((p,o)->{o.accept(WAND_ITEM);o.accept(GENERATOR_BLOCK);o.accept(ANCHOR_BLOCK);}).build()); + item("current_generator",p->new EngineeringBlockItem(GENERATOR_BLOCK,p),new Item.Properties());item("stillwater_anchor",p->new EngineeringBlockItem(ANCHOR_BLOCK,p),new Item.Properties()); + item("flow_sensor",p->new EngineeringBlockItem(SENSOR_BLOCK,p),new Item.Properties());item("mooring_post",p->new EngineeringBlockItem(MOORING_BLOCK,p),new Item.Properties()); + Registry.register(BuiltInRegistries.CREATIVE_MODE_TAB,id("flow_control"),FabricCreativeModeTab.builder().title(Component.translatable("itemGroup.streamcontrol")).icon(()->new ItemStack(WAND_ITEM)).displayItems((p,o)->{o.accept(WAND_ITEM);o.accept(GENERATOR_BLOCK);o.accept(ANCHOR_BLOCK);o.accept(SENSOR_BLOCK);o.accept(MOORING_BLOCK);}).build()); ControlNetwork.register();ServerLifecycleEvents.SERVER_STARTING.register(ControlConfig::load); ServerPlayConnectionEvents.JOIN.register((handler,sender,server)->ControlNetwork.sync(handler.player)); ServerEntityLevelChangeEvents.AFTER_PLAYER_CHANGE_LEVEL.register((player,origin,destination)->ControlNetwork.sync(player)); diff --git a/src/main/java/nl/townsandkingdoms/streamcontrol/UtilityAccess.java b/src/main/java/nl/townsandkingdoms/streamcontrol/UtilityAccess.java new file mode 100644 index 0000000..b41a64f --- /dev/null +++ b/src/main/java/nl/townsandkingdoms/streamcontrol/UtilityAccess.java @@ -0,0 +1,8 @@ +package nl.townsandkingdoms.streamcontrol; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.world.level.block.entity.BlockEntity; +public interface UtilityAccess { + BlockEntity blockEntity(); + CompoundTag settings(); + void configure(CompoundTag n); +} diff --git a/src/main/java/nl/townsandkingdoms/streamcontrol/UtilityBlock.java b/src/main/java/nl/townsandkingdoms/streamcontrol/UtilityBlock.java new file mode 100644 index 0000000..d18672f --- /dev/null +++ b/src/main/java/nl/townsandkingdoms/streamcontrol/UtilityBlock.java @@ -0,0 +1,64 @@ +package nl.townsandkingdoms.streamcontrol; + +import com.mojang.serialization.MapCodec; +import net.minecraft.core.*; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.tags.FluidTags; +import net.minecraft.world.*; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.context.BlockPlaceContext; +import net.minecraft.world.level.*; +import net.minecraft.world.level.block.*; +import net.minecraft.world.level.block.entity.*; +import net.minecraft.world.level.block.state.*; +import net.minecraft.world.level.block.state.properties.*; +import net.minecraft.world.level.material.*; +import net.minecraft.world.phys.BlockHitResult; + +/** Same waterlogged model family as the original controllers. */ +public final class UtilityBlock extends BaseEntityBlock implements SimpleWaterloggedBlock { + public static final MapCodec SENSOR_CODEC = simpleCodec(p -> new UtilityBlock(true,p)); + public static final MapCodec MOORING_CODEC = simpleCodec(p -> new UtilityBlock(false,p)); + public static final BooleanProperty WATERLOGGED = BlockStateProperties.WATERLOGGED, POWERED = BlockStateProperties.POWERED; + public final boolean sensor; + public UtilityBlock(boolean sensor,Properties p) { + super(p);this.sensor=sensor; + registerDefaultState(stateDefinition.any().setValue(WATERLOGGED,false).setValue(POWERED,false)); + } + @Override protected MapCodec codec() { return sensor ? SENSOR_CODEC : MOORING_CODEC; } + @Override protected void createBlockStateDefinition(StateDefinition.Builder b) { b.add(WATERLOGGED,POWERED); } + @Override public BlockState getStateForPlacement(BlockPlaceContext c) { return defaultBlockState().setValue(WATERLOGGED,c.getLevel().getFluidState(c.getClickedPos()).is(FluidTags.WATER)).setValue(POWERED,c.getLevel().hasNeighborSignal(c.getClickedPos())); } + @Override protected RenderShape getRenderShape(BlockState s) { return RenderShape.MODEL; } + @Override public BlockEntity newBlockEntity(BlockPos p,BlockState s) { return sensor ? new FlowSensorEntity(p,s) : new MooringEntity(p,s); } + @Override public BlockEntityTicker getTicker(Level l,BlockState s,BlockEntityType t) { + if (l.isClientSide()) return null; + return sensor ? createTickerHelper(t,StreamControl.SENSOR_ENTITY.get(),FlowSensorEntity::tick) + : createTickerHelper(t,StreamControl.MOORING_ENTITY.get(),MooringEntity::tick); + } + @Override protected FluidState getFluidState(BlockState s) { return s.getValue(WATERLOGGED) ? Fluids.WATER.getSource(false) : super.getFluidState(s); } + @Override protected BlockState updateShape(BlockState s,LevelReader l,net.minecraft.world.level.ScheduledTickAccess ticks,BlockPos p,Direction d,BlockPos q,BlockState other,net.minecraft.util.RandomSource random) { + if(s.getValue(WATERLOGGED))ticks.scheduleTick(p,Fluids.WATER,Fluids.WATER.getTickDelay(l));return super.updateShape(s,l,ticks,p,d,q,other,random); + } + @Override protected void neighborChanged(BlockState s,Level l,BlockPos p,Block b,net.minecraft.world.level.redstone.Orientation orientation,boolean moving) { + if(!l.isClientSide()) { + boolean on=l.hasNeighborSignal(p); + if(on!=s.getValue(POWERED)) l.setBlock(p,s.setValue(POWERED,on),3); + if(l.getBlockEntity(p) instanceof MooringEntity mooring && on) mooring.release(); + } + } + @Override protected InteractionResult useItemOn(net.minecraft.world.item.ItemStack stack,BlockState state,Level level,BlockPos pos,Player player,InteractionHand hand,BlockHitResult hit) { + // Let the Calibrator's item handler run before the ordinary configure interaction. + return stack.is(StreamControl.WAND.get()) ? InteractionResult.PASS : super.useItemOn(stack,state,level,pos,player,hand,hit); + } + @Override protected InteractionResult useWithoutItem(BlockState s,Level l,BlockPos p,Player player,BlockHitResult hit) { + if(!player.mayBuild() || !l.mayInteract(player,p)) return InteractionResult.FAIL; + if(player instanceof ServerPlayer server && l.getBlockEntity(p) instanceof UtilityAccess utility) ControlNetwork.openUtility(server,utility); + return InteractionResult.SUCCESS; + } + @Override protected boolean hasAnalogOutputSignal(BlockState s) { return sensor; } + @Override protected int getAnalogOutputSignal(BlockState s,Level l,BlockPos p,Direction side) { return l.getBlockEntity(p) instanceof FlowSensorEntity be ? be.output() : 0; } + @Override protected void affectNeighborsAfterRemoval(BlockState old,ServerLevel l,BlockPos p,boolean moving) { + if(sensor) l.updateNeighbourForOutputSignal(p,this);super.affectNeighborsAfterRemoval(old,l,p,moving); + } +} diff --git a/src/main/java/nl/townsandkingdoms/streamcontrol/client/ControllerPreview.java b/src/main/java/nl/townsandkingdoms/streamcontrol/client/ControllerPreview.java new file mode 100644 index 0000000..8c8f92f --- /dev/null +++ b/src/main/java/nl/townsandkingdoms/streamcontrol/client/ControllerPreview.java @@ -0,0 +1,59 @@ +package nl.townsandkingdoms.streamcontrol.client; + +import java.io.File; +import net.minecraft.client.Minecraft; +import net.minecraft.client.Screenshot; +import net.minecraft.client.gui.components.Button; +import net.minecraft.client.gui.screens.TitleScreen; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.network.chat.Component; +import nl.townsandkingdoms.streamcontrol.*; + +/** Opt-in CI capture and interaction check. Inactive in normal games. */ +public final class ControllerPreview { + private static int phase,ticks; + private static boolean captured,connecting; + private static final String[] NAMES={"current-generator","stillwater-anchor","redstone-paused","automation","flow-sensor","mooring-post","block-models","mechanical-controller"}; + public static void tick() { + if(!Boolean.getBoolean("streamcontrol.uiPreview")) return; + var mc=Minecraft.getInstance(); + if(phase==0) { + if(mc.level==null) { + if(!connecting && mc.screen instanceof TitleScreen && mc.getOverlay()==null) { + connecting=true; + net.minecraft.client.gui.screens.ConnectScreen.startConnecting(mc.screen,mc,net.minecraft.client.multiplayer.resolver.ServerAddress.parseString("127.0.0.1:25565"),new net.minecraft.client.multiplayer.ServerData("Flow preview","127.0.0.1:25565",net.minecraft.client.multiplayer.ServerData.Type.OTHER),false,null); + } + return; + } + if(mc.player==null || mc.getOverlay()!=null) return; + open();phase=1;ticks=0; + } + if(++ticks<25) return; + String name="flow-control-"+NAMES[phase-1]+".png"; + if(!captured) { Screenshot.grab(mc.gameDirectory,name,mc.getMainRenderTarget(),1,message->{});captured=true;return; } + if(!new File(mc.gameDirectory,"screenshots/"+name).isFile()) return; + if(phase==NAMES.length) { mc.stop();return; } + phase++;ticks=0;captured=false;open(); + } + private static void open() { + var mc=Minecraft.getInstance();String name=NAMES[phase==0?0:phase-1]; + if(name.equals("block-models")) { mc.setScreen(new ModelPreviewScreen());return; } + if(name.equals("flow-sensor") || name.equals("mooring-post")) { + var n=new CompoundTag();n.putString("Kind",name.equals("flow-sensor")?"sensor":"mooring");n.putInt("Radius",4);n.putInt("Vertical",4); + n.putInt("MaxRadius",16);n.putInt("MaxVertical",16);n.putBoolean("Enabled",true);n.putInt("Output",8); + UtilityScreen.open(new ControlNetwork.UtilityOpen(0,n));return; + } + boolean still=name.equals("stillwater-anchor");var n=new CompoundTag();n.putInt("MaxVertical",16); + n.putBoolean("Kinetic",name.equals("mechanical-controller"));n.putFloat("RPM",128); + n.put("ProfileA",new FlowProfile(4,4,.75,FlowHeading.NORTH_EAST,false,still?FlowProfile.Action.CALM:FlowProfile.Action.CURRENT).save()); + n.put("ProfileB",new FlowProfile(6,3,.5,FlowHeading.SOUTH_WEST,true,still?FlowProfile.Action.RELEASE:FlowProfile.Action.REVERSE).save()); + n.putInt("RedstoneMode",name.equals("automation")?4:0); + ControllerScreen.open(new ControlNetwork.Open(0,4,4,.75,2,16,false,still,name.equals("redstone-paused"),FlowHeading.NORTH_EAST.id(),n)); + if(name.equals("automation")) { + var button=mc.screen.children().stream().filter(c->c instanceof Button b && b.getMessage().equals(Component.translatable("engineering.streamcontrol.automation_tab"))) + .map(c->(Button)c).findFirst().orElseThrow();button.onPress(null); + var profile=mc.screen.children().stream().filter(c->c instanceof Button b && b.getMessage().equals(Component.translatable("engineering.streamcontrol.profile_b"))) + .map(c->(Button)c).findFirst().orElseThrow();profile.onPress(null); + } + } +} diff --git a/src/main/java/nl/townsandkingdoms/streamcontrol/client/ControllerScreen.java b/src/main/java/nl/townsandkingdoms/streamcontrol/client/ControllerScreen.java index ec3f06e..0fb0bba 100644 --- a/src/main/java/nl/townsandkingdoms/streamcontrol/client/ControllerScreen.java +++ b/src/main/java/nl/townsandkingdoms/streamcontrol/client/ControllerScreen.java @@ -1,4 +1,6 @@ package nl.townsandkingdoms.streamcontrol.client; +import net.minecraft.client.renderer.RenderPipelines; +import net.minecraft.network.chat.FontDescription; import java.util.Locale; import net.minecraft.client.Minecraft; @@ -10,16 +12,19 @@ import nl.townsandkingdoms.streamcontrol.FlowHeading; import net.minecraft.network.chat.Component; import net.minecraft.resources.Identifier; -import net.minecraft.client.renderer.RenderPipelines; -import net.minecraft.network.chat.FontDescription; + import nl.townsandkingdoms.streamcontrol.ControlNetwork; +import nl.townsandkingdoms.streamcontrol.FlowProfile; +import nl.townsandkingdoms.streamcontrol.ControllerSettings; +import net.minecraft.nbt.CompoundTag; /** Slate/copper TNK menu artwork, with native input, focus, tooltips and narration. */ public final class ControllerScreen extends Screen { private static final int PANEL_W = 316, PANEL_H = 238; private static final int INK = 0xFFF4EBE5, MUTED = 0xFFA9C3C8, CREAM = 0xFFF4EBE5; private static final int EDGE = 0xFF20293F, GOLD = 0xFFC09473, TEAL = 0xFFA9D2CD; - private static final Identifier BACKGROUND = texture("controller"); + private static final Identifier CURRENT_BACKGROUND = texture("current_controller"); + private static final Identifier STILL_BACKGROUND = texture("stillwater_controller"); private static final Identifier MENU_FONT = Identifier.fromNamespaceAndPath("streamcontrol", "menu"); private static Identifier texture(String name) { return Identifier.fromNamespaceAndPath("streamcontrol", "textures/gui/" + name + ".png"); @@ -30,6 +35,11 @@ private static Identifier texture(String name) { private boolean connected; private FlowHeading facing; private int left, top; + private final CompoundTag settings; + private FlowProfile profileA, profileB; + private int editProfile, vertical; + private FlowProfile.Action action; + private boolean automation; public static void open(ControlNetwork.Open packet) { Minecraft.getInstance().setScreen(new ControllerScreen(packet)); @@ -37,31 +47,70 @@ public static void open(ControlNetwork.Open packet) { private ControllerScreen(ControlNetwork.Open packet) { super(Component.translatable(packet.still() ? "block.streamcontrol.stillwater_anchor" - : "block.streamcontrol.current_generator")); + : packet.settings()!=null && packet.settings().getBooleanOr("Kinetic",false) ? "block.streamcontrol.mechanical_current_generator" : "block.streamcontrol.current_generator")); snapshot = packet; radius = Math.max(1, Math.min(packet.maxRadius(), packet.radius())); speed = Math.max(0.05, Math.min(packet.maxStrength(), packet.strength())); connected = packet.connectedOnly(); facing = FlowHeading.fromId(packet.facing()); if (facing == null) facing = FlowHeading.NORTH; + settings = packet.settings()==null ? new CompoundTag() : packet.settings().copy(); + profileA = settings.contains("ProfileA") ? FlowProfile.load(settings.getCompoundOrEmpty("ProfileA"),packet.still()) : + new FlowProfile(radius,packet.vertical(),speed,facing,connected,packet.still()?FlowProfile.Action.CALM:FlowProfile.Action.CURRENT); + profileB = settings.contains("ProfileB") ? FlowProfile.load(settings.getCompoundOrEmpty("ProfileB"),packet.still()) : + new FlowProfile(radius,packet.vertical(),speed,facing,connected,packet.still()?FlowProfile.Action.RELEASE:FlowProfile.Action.REVERSE); + readProfile(); } private static Component tr(String key, Object... args) { return Component.translatable("gui.streamcontrol." + key, args); } + private Component engineering(String key,Object...args) { return Component.translatable("engineering.streamcontrol."+key,args); } + private void readProfile() { + var p=editProfile==0?profileA:profileB;radius=p.radius();vertical=p.vertical();speed=p.strength();facing=p.heading();connected=p.connectedOnly();action=p.action(); + } + private void storeProfile() { + var p=new FlowProfile(radius,vertical,speed,facing,connected,action); + if(editProfile==0) profileA=p;else profileB=p; + } + private void rebuild() { clearWidgets();init(); } + private void changeProfile(int selected) { storeProfile();editProfile=selected;readProfile();rebuild(); } + private EngineeringWidgets.ActionButton actionButton(int x,int y,int w,int h,Component label,Button.OnPress press,java.util.function.BooleanSupplier selected) { + return addRenderableWidget(new EngineeringWidgets.ActionButton(left+x,top+y,w,h,label,press,selected)); + } @Override protected void init() { left = (width - PANEL_W) / 2; top = (height - PANEL_H) / 2; - var radiusSlider = addRenderableWidget(new RadiusSlider(left + 134, top + 68, 166)); + actionButton(16,29,138,14,engineering("settings_tab",editProfile==0?"A":"B"),b->{automation=false;rebuild();},()->!automation); + actionButton(162,29,138,14,engineering("automation_tab"),b->{automation=true;rebuild();},()->automation); + if(automation) { + actionButton(26,56,264,22,engineering("redstone_"+settings.getIntOr("RedstoneMode",0)),b->{ + int next=(settings.getIntOr("RedstoneMode",0)+1)%ControllerSettings.Redstone.values().length; + settings.putInt("RedstoneMode",next);b.setMessage(engineering("redstone_"+next)); + },()->false); + actionButton(26,84,100,20,engineering("profile_a"),b->changeProfile(0),()->editProfile==0); + actionButton(134,84,100,20,engineering("profile_b"),b->changeProfile(1),()->editProfile==1); + var unlink=actionButton(242,84,48,20,engineering("unlink"),b->{settings.remove("LinkedSource");rebuild();},()->false); + unlink.active=settings.contains("LinkedSource"); + actionButton(26,111,264,22,engineering("action_"+action.ordinal()),b->{ + action=snapshot.still()?(action==FlowProfile.Action.CALM?FlowProfile.Action.RELEASE:FlowProfile.Action.CALM):FlowProfile.Action.values()[(action.ordinal()+1)%4]; + b.setMessage(engineering("action_"+action.ordinal())); + },()->false); + addRenderableWidget(new EngineeringWidgets.RangeSlider(left+26,top+154,264,0,settings.contains("MaxVertical")?settings.getIntOr("MaxVertical",0):16,vertical,"vertical",n->vertical=n)); + } else { + var radiusSlider = addRenderableWidget(new RadiusSlider(left + (snapshot.still() ? 26 : 182), + top + (snapshot.still() ? 156 : 83), snapshot.still() ? 264 : 108)); radiusSlider.active = snapshot.maxRadius() > 1; radiusSlider.setTooltip(Tooltip.create(tr("radius_help"))); if (!snapshot.still()) { - addRenderableWidget(new SpeedSlider(left + 134, top + 110, 166)); + addRenderableWidget(new SpeedSlider(left + 182, top + 133, 108)); for (FlowHeading d : FlowHeading.values()) { final FlowHeading direction = d; - var button = new SlateButton(left + 218 + (d.stepX() + 1) * 28, - top + 125 + (d.stepZ() + 1) * 15, 26, 14, + // Eight compass points surround the chart. Every hitbox stays inside its card. + int cx = 89 + d.stepX() * (d.diagonal() ? 48 : 56); + int cy = 112 + d.stepZ() * (d.diagonal() ? 28 : 38); + var button = new SlateButton(left + cx - 13, top + cy - 7, 26, 14, tr("short_" + d.abbreviation()), b -> facing = direction, false) { @Override protected boolean selected() { return facing == direction; } }; @@ -69,7 +118,8 @@ private static Component tr(String key, Object... args) { addRenderableWidget(button); } } - var waterToggle = new SlateButton(left + 14, top + 174, 286, 22, + } + var waterToggle = new SlateButton(left + 14, top + 176, 286, 22, tr("connected", tr(connected ? "on" : "off")), b -> { connected = !connected; b.setMessage(tr("connected", tr(connected ? "on" : "off"))); @@ -92,8 +142,9 @@ private static Component tr(String key, Object... args) { } private void apply() { - ControlNetwork.sendConfigure(new ControlNetwork.Configure(snapshot.pos(), radius, speed, - connected, facing.id())); + storeProfile();settings.put("ProfileA",profileA.save());settings.put("ProfileB",profileB.save()); + ControlNetwork.sendConfigure(new ControlNetwork.Configure(snapshot.pos(),profileA.radius(),profileA.strength(), + profileA.connectedOnly(),profileA.heading().id(),settings)); onClose(); } @@ -107,41 +158,47 @@ private void apply() { drawPanel(g); Component heading = title.copy().withStyle(style -> style.withFont(new FontDescription.Resource(MENU_FONT))); centered(g, heading, left + PANEL_W / 2, top + 12, CREAM); - g.text(font, tr(snapshot.still() ? "calm_subtitle" : "current_subtitle"), - left + 45, top + 31, MUTED, false); - drawSeal(g, left + 16, top + 23); - g.text(font, tr("area_preview"), left + 22, top + 54, INK, false); + if(automation) { + g.text(font,engineering("vertical_range"),left+26,top+141,INK,false); + right(g,Component.literal("+/- "+vertical),left+290,top+141,TEAL); + } else { drawWaterPreview(g); - centered(g, tr("preview_diameter", radius * 2 + 1), left + 66, top + 148, INK); - centered(g, tr("preview_height", snapshot.vertical()), left + 66, top + 159, MUTED); - g.text(font, tr("radius_label"), left + 134, top + 54, INK, false); - right(g, tr("blocks", radius), left + 300, top + 54, TEAL); if (snapshot.still()) { - bevel(g, left + 134, top + 98, 166, 65, 0xFF446077, false); - g.text(font, tr("moored"), left + 144, top + 108, INK, false); - g.text(font, tr("zero_flow"), left + 144, top + 124, TEAL, false); - g.text(font, tr("no_direction"), left + 144, top + 141, MUTED, false); + centered(g, tr(action==FlowProfile.Action.RELEASE?"released_area":"calm_area"), left + 158, top + 54, INK); + centered(g, tr("diameter_label"), left + 66, top + 82, MUTED); + centered(g, tr("blocks", radius * 2 + 1), left + 66, top + 98, INK); + centered(g, tr("height_label"), left + 250, top + 82, MUTED); + centered(g, Component.literal("+/- " + vertical), left + 250, top + 98, INK); + centered(g, tr(action==FlowProfile.Action.RELEASE?"release_flow":"zero_flow"), left + 158, top + 129, TEAL); + g.text(font, tr("radius_label"), left + 26, top + 144, INK, false); + right(g, tr("blocks", radius), left + 290, top + 144, TEAL); } else { - g.text(font, tr("speed_label"), left + 134, top + 96, INK, false); - right(g, Component.literal(String.format(Locale.ROOT, "%d%%", Math.round(speed * 100))), - left + 300, top + 96, TEAL); - g.text(font, tr("heading_compact"), left + 134, top + 132, INK, false); - g.text(font, tr(facing.key()), left + 134, top + 148, MUTED, false); - bevel(g, left + 246, top + 140, 26, 14, 0xFF304358, false); - g.fill(left + 257, top + 144, left + 261, top + 150, GOLD); + centered(g, tr("heading_label"), left + 89, top + 54, INK); + centered(g, tr(facing.key()), left + 89, top + 162, MUTED); + centered(g, tr("radius_label"), left + 236, top + 56, INK); + centered(g, tr("blocks", radius), left + 236, top + 70, TEAL); + centered(g, tr("speed_label"), left + 236, top + 106, INK); + centered(g, Component.literal(String.format(Locale.ROOT, "%d%%", Math.round(speed * 100))), + left + 236, top + 120, TEAL); + centered(g, tr("preview_diameter", radius * 2 + 1), left + 236, top + 151, INK); + centered(g, tr("preview_height", vertical), left + 236, top + 162, MUTED); + } } // Keep status within the frame, including when redstone disables the controller. - centered(g, tr(snapshot.powered() ? "powered_status" : "apply_hint"), - left + PANEL_W / 2, top + 222, snapshot.powered() ? 0xFFFFB093 : MUTED); + boolean paused=snapshot.powered() && settings.getIntOr("RedstoneMode",0)==0; + Component status=settings.contains("LinkedSource")?engineering("linked_source",net.minecraft.core.BlockPos.of(settings.getLongOr("LinkedSource",0L)).toShortString()): + settings.getBooleanOr("Kinetic",false)?engineering("drive_rpm",Math.round(settings.getFloatOr("RPM",0f))):tr(paused?"powered_status":"apply_hint"); + centered(g,status,left+PANEL_W/2,top+222,paused?0xFFFFB093:MUTED); super.extractRenderState(g, mouseX, mouseY, partialTick); } private void drawPanel(GuiGraphicsExtractor g) { - g.blit(RenderPipelines.GUI_TEXTURED, BACKGROUND, left, top, 0f, 0f, PANEL_W, PANEL_H, PANEL_W, PANEL_H); + g.blit(RenderPipelines.GUI_TEXTURED,automation ? texture("automation_controller") : snapshot.still() ? STILL_BACKGROUND : CURRENT_BACKGROUND, left, top, + 0f, 0f, PANEL_W, PANEL_H, PANEL_W, PANEL_H); } private static void control(GuiGraphicsExtractor g, String name, int x, int y, int w, int h, int state) { - g.blit(RenderPipelines.GUI_TEXTURED, texture(name), x, y, 0f, state * h, w, h, w, h * 5); + g.blit(RenderPipelines.GUI_TEXTURED,texture(name), x, y, 0f, state * h, w, h, w, h * 5); } private static void bevel(GuiGraphicsExtractor g, int x, int y, int w, int h, int fill, boolean raised) { @@ -157,32 +214,28 @@ private void centered(GuiGraphicsExtractor g, Component text, int x, int y, int private void right(GuiGraphicsExtractor g, Component text, int x, int y, int color) { g.text(font, text, x - font.width(text), y, color, false); } - private void drawSeal(GuiGraphicsExtractor g, int x, int y) { - bevel(g, x, y, 22, 23, 0xFF446077, true); - for (int row = 0; row < 3; row++) { - int offset = row == 1 ? 2 : 0; - g.fill(x + 4 + offset, y + 6 + row * 4, x + 15 + offset, y + 7 + row * 4, 0xFFA9D2CD); - g.fill(x + 4 + offset, y + 5 + row * 4, x + 7 + offset, y + 6 + row * 4, 0xFFA9D2CD); - } - } - private void drawWaterPreview(GuiGraphicsExtractor g) { - int x = left + 22, y = top + 68; - // Schematic only: it communicates circular range, direction, and disconnected pools. + // Odd-sized chart has one exact center shared by the compass and anchor metrics. + int cx = left + (snapshot.still() ? 158 : 89), cy = top + (snapshot.still() ? 96 : 112); + int x = cx - 26, y = cy - 26; for (int row = 0; row < 9; row++) for (int col = 0; col < 9; col++) { int dx = col - 4, dz = row - 4; - boolean pool = col >= 7 && row < 4; - boolean wet = col < 6 || pool; - boolean covered = dx * dx + dz * dz <= 16 && (!connected || !pool); - int color = !wet ? 0xFFB88B68 : covered ? 0xFF729AA1 : 0xFF344759; - int tx = x + col * 9, ty = y + row * 8; - g.fill(tx, ty, tx + 8, ty + 7, color); - if (wet) g.fill(tx + 2, ty + 2, tx + 6, ty + 3, covered ? 0xFFA9D2CD : 0xFF54768B); + if (dx * dx + dz * dz > 20) continue; + boolean isolated = col >= 7 && row <= 2; + boolean covered = !connected || !isolated; + int tx = x + col * 6, ty = y + row * 6; + g.fill(tx, ty, tx + 5, ty + 5, covered ? 0xFF54768B : 0xFF344759); + g.fill(tx + 1, ty + 2, tx + 4, ty + 3, covered ? TEAL : 0xFF54768B); } - int cx = x + 4 * 9 + 4, cy = y + 4 * 8 + 3; if (snapshot.still()) { - g.fill(cx - 4, cy - 4, cx + 4, cy + 4, GOLD); - g.fill(cx - 2, cy - 2, cx + 2, cy + 2, EDGE); + // Anchor glyph; the generator uses a heading arrow instead. + g.fill(cx - 3, cy - 8, cx + 4, cy - 3, CREAM); + g.fill(cx - 1, cy - 7, cx + 2, cy - 5, EDGE); + g.fill(cx, cy - 3, cx + 2, cy + 8, CREAM); + g.fill(cx - 4, cy - 1, cx + 6, cy + 1, CREAM); + g.fill(cx - 6, cy + 4, cx - 4, cy + 8, CREAM); + g.fill(cx + 6, cy + 4, cx + 8, cy + 8, CREAM); + g.fill(cx - 4, cy + 7, cx + 6, cy + 9, CREAM); } else { for (int n = -5; n <= 5; n++) { int px = cx + facing.stepX() * n, py = cy + facing.stepZ() * n; @@ -211,9 +264,8 @@ protected void drawLabel(GuiGraphicsExtractor g, int color) { @Override public void extractContents(GuiGraphicsExtractor g, int mx, int my, float partialTick) { boolean highlight = isHoveredOrFocused(); int state = !active ? 4 : primary || selected() ? (highlight ? 3 : 2) : (highlight ? 1 : 0); - String name = getWidth() == 26 ? "compass" : getWidth() == 286 ? "toggle" - : getWidth() == 174 ? "apply" : "cancel"; - control(g, name, getX(), getY(), getWidth(), getHeight(), state); + if(getWidth()==26) control(g,"compass",getX(),getY(),getWidth(),getHeight(),state); + else EngineeringWidgets.button(g,getX(),getY(),getWidth(),getHeight(),state); if (highlight) { int x = getX(), y = getY(), w = getWidth(), h = getHeight(); g.fill(x, y, x + w, y + 1, CREAM); @@ -229,7 +281,7 @@ private abstract class SlateSlider extends AbstractSliderButton { SlateSlider(int x, int y, int w, double value) { super(x, y, w, 16, Component.empty(), value); } @Override public void extractWidgetRenderState(GuiGraphicsExtractor g, int mx, int my, float partialTick) { int x = getX(), y = getY(), w = getWidth(); - g.blit(RenderPipelines.GUI_TEXTURED, texture("track"), x, y + 4, 0f, 0f, w, 8, w, 8); + g.blit(RenderPipelines.GUI_TEXTURED,texture(w == 264 ? "track_wide" : "track_small"), x, y + 4, 0f, 0f, w, 8, w, 8); int handle = x + 3 + (int)Math.round(value * (w - 12)); g.fill(x + 3, y + 7, handle + 4, y + 9, TEAL); control(g, "handle", handle, y + 1, 9, 14, !active ? 4 : isHoveredOrFocused() ? 3 : 2); diff --git a/src/main/java/nl/townsandkingdoms/streamcontrol/client/EngineeringClient.java b/src/main/java/nl/townsandkingdoms/streamcontrol/client/EngineeringClient.java new file mode 100644 index 0000000..d5eef2f --- /dev/null +++ b/src/main/java/nl/townsandkingdoms/streamcontrol/client/EngineeringClient.java @@ -0,0 +1,94 @@ +package nl.townsandkingdoms.streamcontrol.client; +import net.minecraft.client.renderer.RenderPipelines; +import net.minecraft.network.chat.FontDescription; + +import java.util.*; +import com.mojang.blaze3d.vertex.*; +import net.minecraft.client.*; +import net.minecraft.client.gui.GuiGraphicsExtractor; +import net.minecraft.client.renderer.*; +import net.minecraft.core.BlockPos; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.Identifier; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.ClipContext; +import net.minecraft.world.phys.*; +import nl.townsandkingdoms.streamcontrol.*; + +/** Local inspection of already synchronized data. No per-frame packets or chunk scans. */ +public final class EngineeringClient { + private record Readout(BlockPos center,int radius,int vertical,List lines,boolean bounds) {} + private static Readout readout; + private static long lastTick=Long.MIN_VALUE; + public static void register() { + net.fabricmc.fabric.api.client.rendering.v1.hud.HudElementRegistry.addLast(Identifier.fromNamespaceAndPath(StreamControl.ID,"calibrator"),(g,d)->hud(g)); + net.fabricmc.fabric.api.client.rendering.v1.level.LevelRenderEvents.END_MAIN.register(EngineeringClient::world); + } + private static Component tr(String key,Object...args) { return Component.translatable("engineering.streamcontrol."+key,args); } + private static ItemStack tool(Minecraft mc) { + if(mc.player==null) return ItemStack.EMPTY; + if(mc.player.getMainHandItem().is(StreamControl.WAND.get())) return mc.player.getMainHandItem(); + if(mc.player.getOffhandItem().is(StreamControl.WAND.get())) return mc.player.getOffhandItem();return ItemStack.EMPTY; + } + private static void update() { + var mc=Minecraft.getInstance();var stack=tool(mc); + if(mc.level==null || stack.isEmpty() || mc.screen!=null || mc.options.hideGui) { readout=null;lastTick=Long.MIN_VALUE;return; } + long tick=mc.level.getGameTime();if(tick==lastTick) return;lastTick=tick; + var start=mc.player.getEyePosition();var end=start.add(mc.player.getLookAngle().scale(mc.player.blockInteractionRange())); + var hit=mc.level.clip(new ClipContext(start,end,ClipContext.Block.OUTLINE,ClipContext.Fluid.ANY,mc.player)); + List lines=new ArrayList<>();lines.add(Component.translatable("calibrator.streamcontrol.mode",Component.translatable(FlowWand.mode(stack).key()))); + if(hit.getType()!=HitResult.Type.BLOCK) { readout=new Readout(mc.player.blockPosition(),0,0,lines,false);return; } + var pos=hit.getBlockPos();var be=mc.level.getBlockEntity(pos);int radius=0,vertical=0;BlockPos center=pos; + FlowRegion region=FlowOverrides.clientAt(pos.getX(),pos.getY(),pos.getZ()); + if(be instanceof FlowController controller) { + var override=FlowOverrides.clientByKey(pos.asLong()); + radius=override==null?controller.radius():override.radius();vertical=override==null?controller.controls().a().vertical():override.vertical(); + lines.add(be.getBlockState().getBlock().getName()); + if(controller.controls().source()!=null) lines.add(tr("linked_source",controller.controls().source().toShortString())); + if(!Float.isNaN(controller.rpm())) lines.add(tr("drive_rpm",Math.round(controller.rpm()))); + } else if(be instanceof FlowSensorEntity sensor) { + radius=sensor.radius();vertical=sensor.vertical();lines.add(tr("sensor_output",sensor.output())); + lines.add(tr("sensor_mode",tr("sensor_mode_"+sensor.mode().ordinal()))); + } else if(be instanceof MooringEntity mooring) { + radius=mooring.radius();vertical=mooring.vertical();lines.add(tr(mooring.boatId()==null?"mooring_waiting":"mooring_secured")); + } else if(mc.level.getFluidState(pos).is(net.minecraft.tags.FluidTags.WATER)) { + var flow=mc.level.getFluidState(pos).getFlow(mc.level,pos);var heading=FlowHeading.fromVector(flow.x,flow.z); + lines.add(tr("readout_speed",Math.round(Math.hypot(flow.x,flow.z)*100))); + lines.add(tr("readout_direction",heading==null?tr("still"):Component.translatable("gui.streamcontrol."+heading.key()))); + lines.add(tr(region==null?"source_natural":region.controller()?"source_controller":"source_legacy")); + if(region!=null) { radius=region.radius();vertical=region.vertical();center=region.center(); } + } + if(radius>0) lines.add(tr("readout_range",radius,vertical)); + readout=new Readout(center,radius,vertical,lines,radius>0 && ControlConfig.PARTICLES.get() && !FlowWand.settings(stack).getBooleanOr("HideBounds",false)); + } + private static void hud(GuiGraphicsExtractor g) { + update();if(readout==null) return;var mc=Minecraft.getInstance();int widest=0; + for(var line:readout.lines) widest=Math.max(widest,mc.font.width(line)); + int w=widest+16,x=mc.getWindow().getGuiScaledWidth()-w-8,y=8; + g.fill(x,y,x+w,y+readout.lines.size()*11+12,0xE620293F); + g.fill(x,y,x+2,y+readout.lines.size()*11+12,0xFFC09473); + for(int i=0;imin; + } + private int current() { return min+(int)Math.round(value*(max-min)); } + @Override protected void updateMessage() { setMessage(Component.translatable("engineering.streamcontrol."+key,current())); } + @Override protected void applyValue() { setter.accept(current());updateMessage(); } + @Override public void extractWidgetRenderState(GuiGraphicsExtractor g,int mx,int my,float tick) { + int x=getX(),y=getY(),w=getWidth(); + // Reuse the generated native wide track at its exact width. + g.blit(RenderPipelines.GUI_TEXTURED,texture("track_wide"),x,y+4,0f,0f,w,8,w,8); + int handle=x+3+(int)Math.round(value*(w-12));g.fill(x+3,y+7,handle+4,y+9,TEAL); + int state=!active?4:isHoveredOrFocused()?3:2; + g.blit(RenderPipelines.GUI_TEXTURED,texture("handle"),handle,y+1,0f,state*14,9,14,9,70); + } + } +} diff --git a/src/main/java/nl/townsandkingdoms/streamcontrol/client/ModelPreviewScreen.java b/src/main/java/nl/townsandkingdoms/streamcontrol/client/ModelPreviewScreen.java new file mode 100644 index 0000000..e160e17 --- /dev/null +++ b/src/main/java/nl/townsandkingdoms/streamcontrol/client/ModelPreviewScreen.java @@ -0,0 +1,37 @@ +package nl.townsandkingdoms.streamcontrol.client; +import net.minecraft.client.renderer.RenderPipelines; +import net.minecraft.network.chat.FontDescription; + +import java.util.*; +import net.minecraft.client.gui.GuiGraphicsExtractor; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.network.chat.Component; +import net.minecraft.world.item.ItemStack; +import nl.townsandkingdoms.streamcontrol.StreamControl; + +/** Opt-in verification gallery of Minecraft's real baked item models. */ +final class ModelPreviewScreen extends Screen { + private final List items=new ArrayList<>(); + ModelPreviewScreen() { + super(Component.literal("Flow Control - Block Models")); + items.add(new ItemStack(StreamControl.GENERATOR.get()));items.add(new ItemStack(StreamControl.ANCHOR.get())); + items.add(new ItemStack(StreamControl.SENSOR.get()));items.add(new ItemStack(StreamControl.MOORING.get())); + StreamControl.OPTIONAL_ITEMS.forEach(item->items.add(new ItemStack(item.get()))); + } + @Override public void extractBackground(GuiGraphicsExtractor g,int x,int y,float t) {} + @Override public void extractRenderState(GuiGraphicsExtractor g,int mx,int my,float tick) { + int left=(width-316)/2,top=(height-238)/2; + g.fill(0,0,width,height,0xFF151C2E);g.blit(RenderPipelines.GUI_TEXTURED,EngineeringWidgets.texture("automation_controller"),left,top,0f,0f,316,238,316,238); + g.fill(left+16,top+49,left+300,top+219,0xFF446077); + EngineeringWidgets.text(g,title,left+158,top+12,EngineeringWidgets.TEXT); + EngineeringWidgets.text(g,Component.literal("Actual Minecraft item renders"),left+158,top+32,EngineeringWidgets.MUTED); + for(int i=0;icontext.client().execute(()->ClientUpdates.apply(p))); ClientPlayNetworking.registerGlobalReceiver(ControlNetwork.Open.TYPE,(packet,context)->context.client().execute(()->ControllerScreen.open(packet))); + ClientPlayNetworking.registerGlobalReceiver(ControlNetwork.UtilityOpen.TYPE,(p,context)->context.client().execute(()->UtilityScreen.open(p))); + EngineeringClient.register(); + net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents.END_CLIENT_TICK.register(client->ControllerPreview.tick()); ClientPlayConnectionEvents.DISCONNECT.register((handler,client)->FlowOverrides.receive("",true,null,0)); } } diff --git a/src/main/java/nl/townsandkingdoms/streamcontrol/client/UtilityScreen.java b/src/main/java/nl/townsandkingdoms/streamcontrol/client/UtilityScreen.java new file mode 100644 index 0000000..dc3b71d --- /dev/null +++ b/src/main/java/nl/townsandkingdoms/streamcontrol/client/UtilityScreen.java @@ -0,0 +1,68 @@ +package nl.townsandkingdoms.streamcontrol.client; +import net.minecraft.client.renderer.RenderPipelines; +import net.minecraft.network.chat.FontDescription; + +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiGraphicsExtractor; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.network.chat.Component; + +import nl.townsandkingdoms.streamcontrol.*; +import static nl.townsandkingdoms.streamcontrol.client.EngineeringWidgets.*; + +public final class UtilityScreen extends Screen { + private final ControlNetwork.UtilityOpen snapshot; + private final CompoundTag options; + private final boolean sensor; + private int radius,vertical,mode,left,top; + private boolean toggle; + public static void open(ControlNetwork.UtilityOpen packet) { Minecraft.getInstance().setScreen(new UtilityScreen(packet)); } + private UtilityScreen(ControlNetwork.UtilityOpen packet) { + super(Component.translatable("block.streamcontrol."+("sensor".equals(packet.settings().getStringOr("Kind",""))?"flow_sensor":"mooring_post"))); + snapshot=packet;options=packet.settings().copy();sensor="sensor".equals(options.getStringOr("Kind","")); + radius=options.getIntOr("Radius",0);vertical=options.getIntOr("Vertical",0);mode=Math.clamp(options.getIntOr("Mode",0),0,2); + toggle=options.getBooleanOr(sensor?"EmptyOnly":"Enabled",false); + } + private Component tr(String key,Object...args) { return Component.translatable("engineering.streamcontrol."+key,args); } + @Override protected void init() { + left=(width-316)/2;top=(height-238)/2; + addRenderableWidget(new RangeSlider(left+26,top+72,264,1,Math.max(1,options.getIntOr("MaxRadius",0)),radius,"radius",n->radius=n)); + addRenderableWidget(new RangeSlider(left+26,top+112,264,0,Math.max(0,options.getIntOr("MaxVertical",0)),vertical,"vertical",n->vertical=n)); + var select=new ActionButton(left+26,top+138,264,22,sensor?modeLabel():tr("mooring_automatic"),b->{ mode=(mode+1)%3;b.setMessage(modeLabel()); },()->false); + select.active=sensor;addRenderableWidget(select); + addRenderableWidget(new ActionButton(left+14,top+176,286,22,toggleLabel(),b->{toggle=!toggle;b.setMessage(toggleLabel());},()->toggle)); + addRenderableWidget(new ActionButton(left+14,top+200,174,20,Component.translatable("gui.streamcontrol.apply"),b->apply(),()->true)); + addRenderableWidget(new ActionButton(left+196,top+200,104,20,Component.translatable("gui.streamcontrol.cancel"),b->onClose(),()->false)); + } + private Component modeLabel() { return tr("sensor_mode",tr("sensor_mode_"+mode)); } + private Component toggleLabel() { return tr(sensor?"empty_boats":"auto_moor",Component.translatable("gui.streamcontrol."+(toggle?"on":"off"))); } + private void apply() { + options.putInt("Radius",radius);options.putInt("Vertical",vertical);options.putInt("Mode",mode);options.putBoolean(sensor?"EmptyOnly":"Enabled",toggle); + ControlNetwork.sendConfigure(new ControlNetwork.UtilityConfigure(snapshot.pos(),options));onClose(); + } + @Override public boolean isPauseScreen() { return false; } + @Override public void extractBackground(GuiGraphicsExtractor g,int x,int y,float t) {} + @Override public void extractRenderState(GuiGraphicsExtractor g,int mx,int my,float tick) { + g.fill(0,0,width,height,0xA0141C2E); + g.blit(RenderPipelines.GUI_TEXTURED,texture("automation_controller"),left,top,0f,0f,316,238,316,238); + var menu=net.minecraft.resources.Identifier.fromNamespaceAndPath("streamcontrol","menu"); + text(g,title.copy().withStyle(s->s.withFont(new FontDescription.Resource(menu))),left+158,top+12,TEXT); + text(g,tr(sensor?"sensor_subtitle":"mooring_subtitle"),left+158,top+32,MUTED); + g.text(font,tr("horizontal_range"),left+26,top+57,TEXT,false); + right(g,tr("blocks",radius),left+290,top+57); + g.text(font,tr("vertical_range"),left+26,top+97,TEXT,false); + right(g,Component.literal("+/- "+vertical),left+290,top+97); + Component detail=sensor ? tr("sensor_hint_"+mode) : tr("mooring_release_hint"); + text(g,detail,left+158,top+164,MUTED); + Component status=sensor?tr("sensor_output",options.getIntOr("Output",0)): + tr(options.getBooleanOr("Powered",false)?"mooring_powered":options.getBooleanOr("Secured",false)?"mooring_secured":"mooring_waiting"); + if(minecraft!=null && minecraft.level!=null && minecraft.level.getBlockEntity(net.minecraft.core.BlockPos.of(snapshot.pos())) instanceof FlowSensorEntity be) + status=tr("sensor_output",be.output()); + if(minecraft!=null && minecraft.level!=null && minecraft.level.getBlockEntity(net.minecraft.core.BlockPos.of(snapshot.pos())) instanceof MooringEntity be) { + var live=be.settings();status=tr(live.getBooleanOr("Powered",false)?"mooring_powered":live.getBooleanOr("Secured",false)?"mooring_secured":"mooring_waiting"); + } + text(g,status,left+158,top+222,TEAL);super.extractRenderState(g,mx,my,tick); + } + private void right(GuiGraphicsExtractor g,Component s,int x,int y) { g.text(font,s,x-font.width(s),y,TEAL,false); } +} diff --git a/src/main/java/nl/townsandkingdoms/streamcontrol/mixin/BakedFlowMixin.java b/src/main/java/nl/townsandkingdoms/streamcontrol/mixin/BakedFlowMixin.java deleted file mode 100644 index 26f851a..0000000 --- a/src/main/java/nl/townsandkingdoms/streamcontrol/mixin/BakedFlowMixin.java +++ /dev/null @@ -1,14 +0,0 @@ -package nl.townsandkingdoms.streamcontrol.mixin; -import dev.streamsreflowing.fabric.fluid.StreamFlowingFluid; -import net.minecraft.core.BlockPos; -import net.minecraft.world.level.BlockGetter; -import net.minecraft.world.phys.Vec3; -import nl.townsandkingdoms.streamcontrol.FlowOverrides; -import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.injection.*; -import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; -@Mixin(value=StreamFlowingFluid.class,remap=false,priority=500) -public abstract class BakedFlowMixin { - @Inject(method="resolve",at=@At("RETURN"),cancellable=true,remap=false) - private static void streamcontrol$baked(Vec3 natural,BlockGetter world,BlockPos pos,CallbackInfoReturnable cir){cir.setReturnValue(FlowOverrides.apply(cir.getReturnValue(),world,pos,world.getFluidState(pos)));} -} diff --git a/src/main/java/nl/townsandkingdoms/streamcontrol/mixin/ClientGridMixin.java b/src/main/java/nl/townsandkingdoms/streamcontrol/mixin/ClientGridMixin.java index 133dcc0..549636e 100644 --- a/src/main/java/nl/townsandkingdoms/streamcontrol/mixin/ClientGridMixin.java +++ b/src/main/java/nl/townsandkingdoms/streamcontrol/mixin/ClientGridMixin.java @@ -1,16 +1,15 @@ package nl.townsandkingdoms.streamcontrol.mixin; -import dev.streamsreflowing.flow.*; import nl.townsandkingdoms.streamcontrol.FlowOverrides; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.injection.*; import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; -@Mixin(value = ClientFlowGrids.class, remap = false) +@Mixin(targets = "dev.streamsreflowing.flow.ProprietaryNiceJohn_001", remap = false) public abstract class ClientGridMixin { - @Inject(method = "angleAt(III)B", at = @At("HEAD"), cancellable = true, remap = false) + @Inject(method = "seeAiNotice1(III)B", at = @At("HEAD"), cancellable = true, remap = false) private static void streamcontrol$angle(int x,int y,int z,CallbackInfoReturnable cir) { var r = FlowOverrides.clientAt(x,y,z); if (r != null) cir.setReturnValue(FlowOverrides.visualAngle(r)); } - @Inject(method = "speedAt(III)D", at = @At("HEAD"), cancellable = true, remap = false) + @Inject(method = "proprietary1(III)D", at = @At("HEAD"), cancellable = true, remap = false) private static void streamcontrol$speed(int x,int y,int z,CallbackInfoReturnable cir) { var r = FlowOverrides.clientAt(x,y,z); if (r != null) cir.setReturnValue(r.still() ? 0 : r.strength()); } diff --git a/src/main/java/nl/townsandkingdoms/streamcontrol/mixin/GridFlowMixin.java b/src/main/java/nl/townsandkingdoms/streamcontrol/mixin/GridFlowMixin.java index c2890b4..38f17f3 100644 --- a/src/main/java/nl/townsandkingdoms/streamcontrol/mixin/GridFlowMixin.java +++ b/src/main/java/nl/townsandkingdoms/streamcontrol/mixin/GridFlowMixin.java @@ -7,11 +7,11 @@ import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; @Mixin(value = StreamFlowGrid.class, remap = false) public abstract class GridFlowMixin { - @Inject(target = {@Desc(value="angleForPhysics",args={BlockGetter.class,int.class,int.class,int.class},ret=byte.class),@Desc(value="angleForEntityPush",args={BlockGetter.class,int.class,int.class,int.class},ret=byte.class)}, at = @At("HEAD"), cancellable = true, remap = false) + @Inject(target = {@Desc(value="niceJohn1",args={BlockGetter.class,int.class,int.class,int.class},ret=byte.class),@Desc(value="noDecompilePermission1",args={BlockGetter.class,int.class,int.class,int.class},ret=byte.class)}, at = @At("HEAD"), cancellable = true, remap = false) private static void streamcontrol$angle(BlockGetter level, int x, int y, int z, CallbackInfoReturnable cir) { var r = FlowOverrides.at(level,x,y,z); if (r != null) cir.setReturnValue(FlowOverrides.visualAngle(r)); } - @Inject(target = @Desc(value="speedForPhysics",args={BlockGetter.class,int.class,int.class,int.class},ret=double.class), at = @At("HEAD"), cancellable = true, remap = false) + @Inject(target = @Desc(value="sourceGivenToAuthorizedOnly1",args={BlockGetter.class,int.class,int.class,int.class},ret=double.class), at = @At("HEAD"), cancellable = true, remap = false) private static void streamcontrol$speed(BlockGetter level, int x, int y, int z, CallbackInfoReturnable cir) { var r = FlowOverrides.at(level,x,y,z); if (r != null) cir.setReturnValue(r.still() ? 0 : r.strength()); } diff --git a/src/main/java/nl/townsandkingdoms/streamcontrol/mixin/SurfaceParticlesMixin.java b/src/main/java/nl/townsandkingdoms/streamcontrol/mixin/SurfaceParticlesMixin.java index 3196777..a1dfb1f 100644 --- a/src/main/java/nl/townsandkingdoms/streamcontrol/mixin/SurfaceParticlesMixin.java +++ b/src/main/java/nl/townsandkingdoms/streamcontrol/mixin/SurfaceParticlesMixin.java @@ -1,6 +1,5 @@ package nl.townsandkingdoms.streamcontrol.mixin; -import dev.streamsreflowing.fabric.client.StreamParticleScanner; import net.minecraft.client.multiplayer.ClientLevel; import nl.townsandkingdoms.streamcontrol.FlowOverrides; import org.spongepowered.asm.mixin.Mixin; @@ -13,9 +12,9 @@ * independently of fluid physics. Stop the emitter at the precise water cell, * rather than disabling its particles globally or relying on its y-less grid. */ -@Mixin(value = StreamParticleScanner.class, remap = false) +@Mixin(targets = "dev.streamsreflowing.fabric.client.NoDecompilePermissionEver_001", remap = false) public abstract class SurfaceParticlesMixin { - @Inject(target = @org.spongepowered.asm.mixin.injection.Desc(value="emitSurfaceAt", args={ClientLevel.class,int.class,int.class,int.class,byte.class,double.class,boolean.class}), at = @At("HEAD"), cancellable = true, remap = false) + @Inject(target = @org.spongepowered.asm.mixin.injection.Desc(value="proprietary1",args={ClientLevel.class,int.class,int.class,int.class,byte.class,double.class,boolean.class},ret=void.class), at = @At("HEAD"), cancellable = true, remap = false) private static void streamcontrol$quietSurface(ClientLevel level, int x, int y, int z, byte angle, double drift, boolean debris, CallbackInfo ci) { var region = FlowOverrides.clientAt(x, y, z); diff --git a/src/main/resources/assets/streamcontrol/blockstates/flow_sensor.json b/src/main/resources/assets/streamcontrol/blockstates/flow_sensor.json new file mode 100644 index 0000000..4c3bd3d --- /dev/null +++ b/src/main/resources/assets/streamcontrol/blockstates/flow_sensor.json @@ -0,0 +1,7 @@ +{ + "variants": { + "": { + "model": "streamcontrol:block/flow_sensor" + } + } +} diff --git a/src/main/resources/assets/streamcontrol/blockstates/mooring_post.json b/src/main/resources/assets/streamcontrol/blockstates/mooring_post.json new file mode 100644 index 0000000..c3399fe --- /dev/null +++ b/src/main/resources/assets/streamcontrol/blockstates/mooring_post.json @@ -0,0 +1,7 @@ +{ + "variants": { + "": { + "model": "streamcontrol:block/mooring_post" + } + } +} diff --git a/src/main/resources/assets/streamcontrol/items/flow_sensor.json b/src/main/resources/assets/streamcontrol/items/flow_sensor.json new file mode 100644 index 0000000..f89c9d1 --- /dev/null +++ b/src/main/resources/assets/streamcontrol/items/flow_sensor.json @@ -0,0 +1,6 @@ +{ + "model": { + "type": "minecraft:model", + "model": "streamcontrol:item/flow_sensor" + } +} diff --git a/src/main/resources/assets/streamcontrol/items/mooring_post.json b/src/main/resources/assets/streamcontrol/items/mooring_post.json new file mode 100644 index 0000000..bdf3512 --- /dev/null +++ b/src/main/resources/assets/streamcontrol/items/mooring_post.json @@ -0,0 +1,6 @@ +{ + "model": { + "type": "minecraft:model", + "model": "streamcontrol:item/mooring_post" + } +} diff --git a/src/main/resources/assets/streamcontrol/lang/en_us.json b/src/main/resources/assets/streamcontrol/lang/en_us.json index df0658a..cd1c280 100644 --- a/src/main/resources/assets/streamcontrol/lang/en_us.json +++ b/src/main/resources/assets/streamcontrol/lang/en_us.json @@ -1,6 +1,6 @@ { - "itemGroup.streamcontrol": "Streams Flow Control", - "item.streamcontrol.flow_wand": "Flow Wand", + "itemGroup.streamcontrol": "Flow Control", + "item.streamcontrol.flow_wand": "Flow Calibrator", "block.streamcontrol.current_generator": "Current Generator", "block.streamcontrol.stillwater_anchor": "Stillwater Anchor", "mode.streamcontrol.0": "Direct flow", @@ -9,23 +9,14 @@ "message.streamcontrol.mode": "Wand mode: %s", "message.streamcontrol.radius": "Wand radius: %s blocks", "message.streamcontrol.controller": "Right-click the controller to configure it.", - "message.streamcontrol.applied": "%s applied within %s blocks", - "message.streamcontrol.restored": "Natural flow restored.", - "message.streamcontrol.none": "No flow edit here.", - "message.streamcontrol.remove_block": "Break or power the controller to restore its water.", - "message.streamcontrol.denied": "You cannot edit this area.", - "message.streamcontrol.limit": "Flow edit limit reached. Restore old edits first.", - "tooltip.streamcontrol.mode": "Mode: %s", - "tooltip.streamcontrol.wand": "Use on water: apply the selected mode. Direction follows your view.", - "tooltip.streamcontrol.wand_air": "Aim at sky: use to change radius; sneak-use to change mode.", "gui.streamcontrol.apply": "Apply changes", "gui.streamcontrol.cancel": "Cancel", "gui.streamcontrol.still_description": "A peaceful pocket of motionless water", "gui.streamcontrol.current_description": "Shape the current around your build", - "gui.streamcontrol.radius_label": "INFLUENCE AREA", + "gui.streamcontrol.radius_label": "RADIUS", "gui.streamcontrol.radius": "Radius: %s blocks", "gui.streamcontrol.radius_details": "Diameter: %s blocks | Vertical reach: +/- %s", - "gui.streamcontrol.speed_label": "CURRENT STRENGTH", + "gui.streamcontrol.speed_label": "STRENGTH", "gui.streamcontrol.speed": "Flow speed: %s", "gui.streamcontrol.still_speed": "Flow speed: 0% (still water)", "gui.streamcontrol.still_direction": "No flow direction", @@ -39,6 +30,15 @@ "gui.streamcontrol.off": "OFF", "gui.streamcontrol.connected_hint": "ON: nearby separate pools are unaffected", "gui.streamcontrol.powered": "Disabled by redstone power", + "message.streamcontrol.applied": "%s applied within %s blocks", + "message.streamcontrol.restored": "Natural flow restored.", + "message.streamcontrol.none": "No flow edit here.", + "message.streamcontrol.remove_block": "Break or power the controller to restore its water.", + "message.streamcontrol.denied": "You cannot edit this area.", + "message.streamcontrol.limit": "Flow edit limit reached. Restore old edits first.", + "tooltip.streamcontrol.mode": "Mode: %s", + "tooltip.streamcontrol.wand": "Use on water: apply the selected mode. Direction follows your view.", + "tooltip.streamcontrol.wand_air": "Aim at sky: use to change radius; sneak-use to change mode.", "gui.streamcontrol.calm_subtitle": "A quiet place to moor", "gui.streamcontrol.current_subtitle": "Shape the water around you", "gui.streamcontrol.area_preview": "WATER AREA", @@ -70,5 +70,88 @@ "gui.streamcontrol.short_s": "S", "gui.streamcontrol.short_sw": "SW", "gui.streamcontrol.short_w": "W", - "gui.streamcontrol.short_nw": "NW" + "gui.streamcontrol.short_nw": "NW", + "gui.streamcontrol.calm_area": "CALM WATER", + "gui.streamcontrol.diameter_label": "DIAMETER", + "gui.streamcontrol.height_label": "HEIGHT", + "block.streamcontrol.flow_sensor": "Flow Sensor", + "block.streamcontrol.mooring_post": "Mooring Post", + "block.streamcontrol.mechanical_current_generator": "Mechanical Current Generator", + "block.streamcontrol.current_generator.tip": "Programmable currents with two profiles and redstone control.", + "block.streamcontrol.stillwater_anchor.tip": "Calm water around docks; switch profiles with redstone.", + "block.streamcontrol.flow_sensor.tip": "Measure speed, compass direction or boats with comparator output.", + "block.streamcontrol.mooring_post.tip": "Secure one empty boat; boarding or redstone releases it.", + "block.streamcontrol.mechanical_current_generator.tip": "Bottom shaft input. RPM controls strength; reversing reverses flow.", + "calibrator.streamcontrol.inspect": "Inspect", + "calibrator.streamcontrol.copy": "Copy settings", + "calibrator.streamcontrol.paste": "Paste settings", + "calibrator.streamcontrol.link": "Link controllers", + "calibrator.streamcontrol.restore": "Remove legacy edit", + "calibrator.streamcontrol.mode": "Calibrator: %s", + "calibrator.streamcontrol.tooltip": "Inspect water and configure controllers; copy, paste or link in other modes.", + "calibrator.streamcontrol.tooltip_cycle": "Sneak-use the air to cycle modes; Link: source first, target second.", + "calibrator.streamcontrol.tooltip_bounds": "Use the air to toggle range outlines. Sneak-use a linked controller to unlink.", + "calibrator.streamcontrol.clipboard_ready": "Controller settings copied.", + "calibrator.streamcontrol.source_ready": "Link source selected.", + "calibrator.streamcontrol.controller_required": "Use this mode on a controller.", + "calibrator.streamcontrol.copied": "Settings copied. Switch to Paste to apply them.", + "calibrator.streamcontrol.pasted": "Settings applied.", + "calibrator.streamcontrol.incompatible": "Copy compatible controller, sensor or mooring settings first.", + "calibrator.streamcontrol.unlinked": "Controller unlinked.", + "calibrator.streamcontrol.source_selected": "Source selected. Use another controller to link it.", + "calibrator.streamcontrol.invalid_link": "Link rejected: check dimension, distance, loaded source and loops.", + "calibrator.streamcontrol.linked": "Controller linked. The source profiles are followed while loaded.", + "calibrator.streamcontrol.no_legacy_edit": "No old wand edit here. Controller regions are removed by their blocks.", + "calibrator.streamcontrol.denied": "You cannot remove this player's edit.", + "calibrator.streamcontrol.restored": "Old wand edit removed; natural flow restored.", + "calibrator.streamcontrol.bounds_off": "Calibrator outlines hidden.", + "calibrator.streamcontrol.bounds_on": "Calibrator outlines shown.", + "engineering.streamcontrol.settings_tab": "Settings %s", + "engineering.streamcontrol.automation_tab": "Automation", + "engineering.streamcontrol.redstone_0": "Redstone: pause when powered", + "engineering.streamcontrol.redstone_1": "Redstone: always active", + "engineering.streamcontrol.redstone_2": "Redstone: analog strength 0-15", + "engineering.streamcontrol.redstone_3": "Redstone: reverse when powered", + "engineering.streamcontrol.redstone_4": "Redstone: switch Profile A / B", + "engineering.streamcontrol.profile_a": "Profile A", + "engineering.streamcontrol.profile_b": "Profile B", + "engineering.streamcontrol.unlink": "Unlink", + "engineering.streamcontrol.action_0": "Profile action: directed current", + "engineering.streamcontrol.action_1": "Profile action: reversed current", + "engineering.streamcontrol.action_2": "Profile action: calm water", + "engineering.streamcontrol.action_3": "Profile action: natural flow", + "engineering.streamcontrol.horizontal_range": "HORIZONTAL RADIUS", + "engineering.streamcontrol.vertical_range": "VERTICAL REACH", + "engineering.streamcontrol.radius": "Horizontal radius: %s blocks", + "engineering.streamcontrol.vertical": "Vertical reach: +/- %s blocks", + "engineering.streamcontrol.blocks": "%s blocks", + "engineering.streamcontrol.linked_source": "Linked source: %s", + "engineering.streamcontrol.drive_rpm": "Drive: %s RPM", + "engineering.streamcontrol.sensor_subtitle": "Read the water above and below", + "engineering.streamcontrol.mooring_subtitle": "A secure berth for an empty boat", + "engineering.streamcontrol.sensor_mode": "Measure: %s", + "engineering.streamcontrol.sensor_mode_0": "Current strength", + "engineering.streamcontrol.sensor_mode_1": "Compass direction", + "engineering.streamcontrol.sensor_mode_2": "Nearby boats", + "engineering.streamcontrol.sensor_hint_0": "Comparator: 0 = still, 15 = maximum strength", + "engineering.streamcontrol.sensor_hint_1": "Comparator: N=1, NE=2, E=3 ... NW=8", + "engineering.streamcontrol.sensor_hint_2": "Comparator: one level for each boat, up to 15", + "engineering.streamcontrol.sensor_output": "Comparator output: %s / 15", + "engineering.streamcontrol.empty_boats": "Count empty boats only: %s", + "engineering.streamcontrol.auto_moor": "Automatic mooring: %s", + "engineering.streamcontrol.mooring_automatic": "Automatic empty-boat mooring", + "engineering.streamcontrol.mooring_release_hint": "Board the boat or power the post to release", + "engineering.streamcontrol.mooring_powered": "Redstone powered - boat released", + "engineering.streamcontrol.mooring_secured": "Boat secured", + "engineering.streamcontrol.mooring_waiting": "Waiting for an empty boat", + "engineering.streamcontrol.readout_speed": "Current strength: %s%%", + "engineering.streamcontrol.readout_direction": "Direction: %s", + "engineering.streamcontrol.still": "Still water", + "engineering.streamcontrol.source_natural": "Source: natural water", + "engineering.streamcontrol.source_controller": "Source: controller", + "engineering.streamcontrol.source_legacy": "Source: old wand edit", + "engineering.streamcontrol.readout_range": "Radius: %s | Vertical: +/- %s", + "engineering.streamcontrol.configure_tip": "Right-click to configure. Calibrator: inspect, copy and paste.", + "gui.streamcontrol.released_area": "NATURAL WATER", + "gui.streamcontrol.release_flow": "Controller override released" } diff --git a/src/main/resources/assets/streamcontrol/models/block/flow_sensor.json b/src/main/resources/assets/streamcontrol/models/block/flow_sensor.json new file mode 100644 index 0000000..7e7d3b8 --- /dev/null +++ b/src/main/resources/assets/streamcontrol/models/block/flow_sensor.json @@ -0,0 +1,12 @@ +{ + "parent": "minecraft:block/cube", + "textures": { + "down": "streamcontrol:block/flow_sensor_bottom", + "up": "streamcontrol:block/flow_sensor_top", + "north": "streamcontrol:block/flow_sensor_side", + "south": "streamcontrol:block/flow_sensor_side", + "east": "streamcontrol:block/flow_sensor_side", + "west": "streamcontrol:block/flow_sensor_side", + "particle": "streamcontrol:block/flow_sensor_side" + } +} diff --git a/src/main/resources/assets/streamcontrol/models/block/mooring_post.json b/src/main/resources/assets/streamcontrol/models/block/mooring_post.json new file mode 100644 index 0000000..23b6848 --- /dev/null +++ b/src/main/resources/assets/streamcontrol/models/block/mooring_post.json @@ -0,0 +1,215 @@ +{ + "parent": "minecraft:block/block", + "textures": { + "side": "minecraft:block/barrel_side", + "top": "minecraft:block/barrel_top", + "bottom": "minecraft:block/barrel_bottom", + "particle": "minecraft:block/barrel_side" + }, + "elements": [ + { + "from": [ + 2, + 0, + 1 + ], + "to": [ + 14, + 16, + 15 + ], + "faces": { + "north": { + "texture": "#side", + "uv": [ + 2, + 0, + 14, + 16 + ] + }, + "south": { + "texture": "#side", + "uv": [ + 2, + 0, + 14, + 16 + ] + }, + "west": { + "texture": "#side", + "uv": [ + 1, + 0, + 15, + 16 + ] + }, + "east": { + "texture": "#side", + "uv": [ + 1, + 0, + 15, + 16 + ] + }, + "up": { + "texture": "#top", + "uv": [ + 2, + 1, + 14, + 15 + ] + }, + "down": { + "texture": "#bottom", + "uv": [ + 2, + 1, + 14, + 15 + ] + } + } + }, + { + "from": [ + 1, + 0, + 2 + ], + "to": [ + 2, + 16, + 14 + ], + "faces": { + "north": { + "texture": "#side", + "uv": [ + 14, + 0, + 15, + 16 + ] + }, + "south": { + "texture": "#side", + "uv": [ + 1, + 0, + 2, + 16 + ] + }, + "west": { + "texture": "#side", + "uv": [ + 2, + 0, + 14, + 16 + ] + }, + "east": { + "texture": "#side", + "uv": [ + 2, + 0, + 14, + 16 + ] + }, + "up": { + "texture": "#top", + "uv": [ + 1, + 2, + 2, + 14 + ] + }, + "down": { + "texture": "#bottom", + "uv": [ + 1, + 2, + 2, + 14 + ] + } + } + }, + { + "from": [ + 14, + 0, + 2 + ], + "to": [ + 15, + 16, + 14 + ], + "faces": { + "north": { + "texture": "#side", + "uv": [ + 1, + 0, + 2, + 16 + ] + }, + "south": { + "texture": "#side", + "uv": [ + 14, + 0, + 15, + 16 + ] + }, + "west": { + "texture": "#side", + "uv": [ + 2, + 0, + 14, + 16 + ] + }, + "east": { + "texture": "#side", + "uv": [ + 2, + 0, + 14, + 16 + ] + }, + "up": { + "texture": "#top", + "uv": [ + 14, + 2, + 15, + 14 + ] + }, + "down": { + "texture": "#bottom", + "uv": [ + 14, + 2, + 15, + 14 + ] + } + } + } + ] +} diff --git a/src/main/resources/assets/streamcontrol/models/item/flow_sensor.json b/src/main/resources/assets/streamcontrol/models/item/flow_sensor.json new file mode 100644 index 0000000..96dc116 --- /dev/null +++ b/src/main/resources/assets/streamcontrol/models/item/flow_sensor.json @@ -0,0 +1,3 @@ +{ + "parent": "streamcontrol:block/flow_sensor" +} diff --git a/src/main/resources/assets/streamcontrol/models/item/mooring_post.json b/src/main/resources/assets/streamcontrol/models/item/mooring_post.json new file mode 100644 index 0000000..b91b93f --- /dev/null +++ b/src/main/resources/assets/streamcontrol/models/item/mooring_post.json @@ -0,0 +1,3 @@ +{ + "parent": "streamcontrol:block/mooring_post" +} diff --git a/src/main/resources/assets/streamcontrol/textures/block/flow_sensor_bottom.png b/src/main/resources/assets/streamcontrol/textures/block/flow_sensor_bottom.png new file mode 100644 index 0000000000000000000000000000000000000000..6b9be84b7152ba19490c5553322f3eac3617a2eb GIT binary patch literal 661 zcmV;G0&4w~M=>lA>#?+l<7y=>!)*+%vl7w=7 zhjO(h0a%~k1;A!Gsj-x+HA-Y5-$qsxxICZ9N+HHtY?hN(E0QEZ$=t#XG2EQqW3!CG zd(3rfV~hyEVSB|G!zaW!{f;mU#2C>!<_DUI1{|Z7HhN;^@1U!F%-e zB*rlUFnG^2P1O4x^=^wCcH7xG299;j{_29FS`%ZO#}$TUGV>-Lku+s{@h`?$HCgpoX}~y5C`wE&XJcDzVCVX=*hgwI<-q6$NGSE4xP?rCn`ZH zVelT&dRgb&kimBt>v;I+2>`$S`T=bWF3VYM&X=-%-}BF3fAZkVZ$1_1+7`}OzHd|S vxmSM9YI9DKB-gVq@qIsU;PpEIrG)&u5q>eJ00000NkvXXu0mjf#6CaX literal 0 HcmV?d00001 diff --git a/src/main/resources/assets/streamcontrol/textures/block/flow_sensor_side.png b/src/main/resources/assets/streamcontrol/textures/block/flow_sensor_side.png new file mode 100644 index 0000000000000000000000000000000000000000..de4d89ddebffb18aeb99ed247b794f96379b4625 GIT binary patch literal 645 zcmV;00($+4P)|BT15rXjm1rcLKSpV7wtxH;nt1d#y`!4xG7?(E(-MlJ{F<~ zt&t|JrY&s}Cru{F%w$|la%1&_3->(kckcPlF>c;jNf}{rX_?m1A?4X~yx(|962|}t zDY>z<%;B#+X3t;b&HD546J#7mbb^4Bc0lXskXmIM<>aYVw($x+0G)Qg-24Ld-73m< zs8zNoPEF(bJ|+N$0l@LWK0&KNy|$w(q+}ng?^X$#4eGm9gtPz{CPR$5>kDbnY>=j! z=fC^kFpP=Mab4uS2amAxE5mEw%Ze-%iP%O{jPnAa+=)`}KlEjgo#CR&>n3NJZHpY1S>IKnYpl#F4 zYAD;G-wV0Da#zRVI3E8X!{*v5Qd+oPfns@@^6XhkGiNB2rikMRuUKMpZFMXs08$8{ z351j=$H8$uEX&gVD2lM1JYlzkrIgMEkeRZE7S9xdoKkpx5ouY(QN%DA((SZ~q5)y& zWR#m}Gm&-Frw{_qFX9$_J;r_*VhWQ%zfUMnCMzz)=@y3R5GU{A7JOXKM>!dRlolA! zZZ(NVmBAoM63p>(Byo%s62mmPeDxY%KD;F=pga0x3dD4Aa2L zyR1KZ!u^Mj30jS;mYHefm&yofomwdQ>GcLj2X%5vkq$G~`Ndm&|N8m#exl-OhD$no-e7of{E5xke00000NkvXXu0mjf!(=YV literal 0 HcmV?d00001 diff --git a/src/main/resources/assets/streamcontrol/textures/block/flow_sensor_top.png b/src/main/resources/assets/streamcontrol/textures/block/flow_sensor_top.png new file mode 100644 index 0000000000000000000000000000000000000000..d599501615cd0a538f045790add98a0414e46b75 GIT binary patch literal 703 zcmV;w0zmzVP)Ki=v_!ftm)5qS2TTO*rB~F8&GL{Rg~x_u#>!H&33?s0q<1 zM9?Z42nPtCQW5$=x7+>lPlBB3YqSFTO_=_=V%|c-6UZoX_}pMbmg$D{m6k3Dv4wYtFe!fGBHwSTOcIZb`w=q zDQ|5c6oG9w5sCmsiA1(H*APMlK$RMaWD=Q7nd~$zIyz#wu0y`}IRC@);uqox1JCm! zS0V_4h!Ad+H`h_6jK)G~mfdgVHYo`3eGkuds8#lm^;#9*_p$9JzTXze^&aESz56W9 zmm=|x)34Eu|Xsc*N(q8HUbY0HD86WNE%cf1$|j@lmRKy8uWj zaa|XAa_|hU>k^CW0DPXCVe!*@0EW(9U}?U@lSdEPUS9*CIC2@M)gq?r>{lx&eW!mFHk!-&PjdK$o2$!F9w7n(gi!H)kA>1K zp64Mo4FD06R8=J)Af0Jv0T7GpNKHf24&eJ9*<25f(?V6-ZERW%gp`tWwmZt;P%e*x zf|))0Sk&FR~Wl_i%CVW_;K!!XPi1S l%=PgJo;`WYhqtey^&fx23#QF6vQYp4002ovPDHLkV1nHNN16Zt literal 0 HcmV?d00001 diff --git a/src/main/resources/assets/streamcontrol/textures/gui/automation_controller.png b/src/main/resources/assets/streamcontrol/textures/gui/automation_controller.png new file mode 100644 index 0000000000000000000000000000000000000000..f2423d517e178660098c0dd812d995efd44bcfec GIT binary patch literal 1336 zcmeAS@N?(olHy`uVBq!ia0y~yV6*|U-*K=3$t5kjr!g?F{P%Qm45^s&_RhhqkaUTL zhw+^cT3@&c#E6`c-I2H6A?lWIcx)waT*K^XPVX1IzT*&B5GeIREv{ohg8AGs1LKnx z>62y}zyG}E&A&S3cHjKa&~r1df}4Ndym!?s9+lspeck7N){5Kjx$&LDy3pjHbaGF8-QU{#$_Fm~{I`4kciThf zDl!;TIOHZMRyedhV3cfPPs#Z+!%qVzae|s(Ck2&oQ-X4wZ?~wbK1*w*hQGkpKCOkAI}xpQOn2A zV8p_ILcm7B`9lNGL1tn(YV#}hyn9p85IehS*Uu*pYCm7z9(UpFw_n4T*i=d-K*`|bXC-<>iKx^3&fmc7nax8L&P^VW8;18*ye zeq~(x`tBh(C{e<7l*5ji&}beY$4*``2gQeYF~gl5o(HS{y?VRu8N-gkX!iF9FJF#5 z$5`Utui_mt4i1%sTH%B*#=G_88jL)cnu<`1SDd^JVt32Y4gmu7t7X zKXvXdoh^4@;V!+hbP0l+XkK&{IWM&E zj(6Oiu;F0rs<0zgKbK7Ydj0PA%4G{YPU%1S^l-WLYcEEIgot%lj5Tn$GaA zytZVqx$MnVwhRY$^u3AdJ+KK$`w!JK(>=bXR&^V{d#o!uMi!Wbdr4FLO<89p5zA&D@gpOXSY+i??Tel|FZqvG$tC zUT*o!JiYJ$8AE z5B%C$$pChB6Z;VX8-=z9jD{@yXdc2LwfM7a|Lk|N{2Q`j-{;-4tl*z-`|_6u}dZ=%{<{ir`>FBY+`E=;f4{k2%Oe5RPCTUsF>k*)&801gKekXn3;dClqT z`|DQNI+^;vzXy!uoR%36rUsolobI31zo9HYZXYnUb?5&Vj&B7M z7F)EF9DGpMbnJd?DEaTt>-V>mH!SeC-&y*!zU_gb$Lk;Go&W#v;3hRv2>6lokVy`y zZYbRi_GW|EqNeZ9p1!`N%piJ{<^9XMv$wi4Xcf~iW_ot*J2>~_+t4$N+=X{G_V^vF z{`2bYs%H(?WXg52Up~lv-y3epzU9%5#oW+jMq(1CdT5boV9&7!d+V!roU>#ION!gA z$MPOnDqQ7eG&@_eSxtAh-LAsiPdDlh&aMmi(y(u*PTA6$Ee|{8F1Y6=g?nj=%V{i~ za(VOQ&nMTHmwgYnmpvdlBkoEXYyLCW?q$CG3Bj>qyMv_6_H@4VgTe~DWM4f#x-gk literal 0 HcmV?d00001 diff --git a/src/main/resources/assets/streamcontrol/textures/gui/stillwater_controller.png b/src/main/resources/assets/streamcontrol/textures/gui/stillwater_controller.png new file mode 100644 index 0000000000000000000000000000000000000000..9aaeb9d6d972efa1f8a8251588b2e7dc5a9ce380 GIT binary patch literal 1501 zcmeAS@N?(olHy`uVBq!ia0y~yV6*|U-*K=3$t5kjr!g?FF7b4645^s&_O4-ebhym1 zhr){;PCVZr-h6OEgK4^<llqQ&A5UuLUf8Dd?YRb< z^c~eKSDlla)|K?XtDT>2v}yex{)zQ6ceQ1wGBG&pw^?C(pf)x({@#^&j6d!lKDhW? zNy_4H3=FxFSL6}`qpg2$b#KUcp6ch8`9j0_oetb(=XU*q> z+DmW$XS^U<8xiwGsbcArPtTT{19jc2`uq1aP=RZHQ@EF={r^wVZv_{W@B962HBhj( zTg-vyAafFjoPhI(28ky2BQt($tu=qkJb8_`^s}2s64r^{+x4fJVcE>(!R6~dxRpOT z`|H4t((FS&ZwBt%wQtYNt(Ru+JS1$rK-+EZEFgQwA!+-B67|{&dD#nhthOb5sr$8d z?%$c(d!w~&HZt!@3tFtU_KVrFzYRASV~w(|{?Oc^zj(XhOX+hr8Dq_KtG3U()c1eE zRoM%wkI&B9QZW11rYU;I*7@JQ!O(s4jTYd?O{-^Smpl<&8{D%WUPk#j%(cK_RJ z&CmO%@B4Hku72JnCd@cmyI-gE>z6l2Ki^V5aEepz>EYYuz*HQ@5w|1UU+%gbW7K0A zcw)sB1ikm8o;7RHAOMcnN9#rZqVSTcm=J$w*5M@GMxuVJU|t_N9P zrhNN*dbuwj`;P5u%P&dAZ~5@>utW5$IF+xPM2JX5M2813Z=pE1$MLfD4cR>XWWI)} z+1T894x01^G#1fajapcscnB($ob^G^ukZF8<^;{Ot*^sAoIBt5%t|2PZjX8Pl}+u< z?)+}s|BCPVDDBGsV%F}Y@KfU2HW!(9Wz4%VHR#k@X)n#EuYP?>`||bLxqqLQGeFS$tej52>}kXC}A_DvmEZ_X?PUm3? zn#BBaTA%|P)MLppwh~_jH)w*YA~-aQD5?z!2iO(= zCr3TC*~}P#C@V%pM1Bl?XUz0C*xLH;5s^TYhY|o2WyNS~%jtaaoFpQlNb`iYYFJJK z_jcTGSD#HH5`c9aljaEki)r|A&RdX(glYakyq*tM+l|i_5edw^q}(0%zFR~j5N*`} iU_fV15&1Xto%sSnC?viKl>7hy0000=25", - "streamsreflowing": "=2.15.2" + "streamsreflowing": "=2.16.2" } } diff --git a/src/main/resources/streamcontrol.mixins.json b/src/main/resources/streamcontrol.mixins.json index ccaf5e2..8015471 100644 --- a/src/main/resources/streamcontrol.mixins.json +++ b/src/main/resources/streamcontrol.mixins.json @@ -5,7 +5,6 @@ "compatibilityLevel": "JAVA_25", "mixins": [ "WaterFlowMixin", - "BakedFlowMixin", "GridFlowMixin", "FluidStateFlowMixin" ], From bf7227a1ddf4e6432bd643b66fd77f39bc634838 Mon Sep 17 00:00:00 2001 From: Mikaaah <59295501+Mikaaah@users.noreply.github.com> Date: Tue, 3 Nov 2026 10:22:08 +0100 Subject: [PATCH 3/6] Resolve legacy compile fixtures and verify complete engineering behavior --- .../townsandkingdoms/streamcontrol/ControlNetwork.java | 10 +++++----- .../streamcontrol/IntegrationChecks.java | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/main/java/nl/townsandkingdoms/streamcontrol/ControlNetwork.java b/src/main/java/nl/townsandkingdoms/streamcontrol/ControlNetwork.java index e47e8ff..031f22f 100644 --- a/src/main/java/nl/townsandkingdoms/streamcontrol/ControlNetwork.java +++ b/src/main/java/nl/townsandkingdoms/streamcontrol/ControlNetwork.java @@ -89,11 +89,11 @@ public static void configureUtility(ServerPlayer player,UtilityConfigure p) { if(level.getBlockEntity(pos) instanceof UtilityAccess be)be.configure(p.settings()); } public static void register() { - net.fabricmc.fabric.api.networking.v1.PayloadTypeRegistry.playS2C().register(Open.TYPE,Open.CODEC); - net.fabricmc.fabric.api.networking.v1.PayloadTypeRegistry.playS2C().register(UtilityOpen.TYPE,UtilityOpen.CODEC); - net.fabricmc.fabric.api.networking.v1.PayloadTypeRegistry.playS2C().register(Update.TYPE,Update.CODEC); - net.fabricmc.fabric.api.networking.v1.PayloadTypeRegistry.playC2S().register(Configure.TYPE,Configure.CODEC); - net.fabricmc.fabric.api.networking.v1.PayloadTypeRegistry.playC2S().register(UtilityConfigure.TYPE,UtilityConfigure.CODEC); + net.fabricmc.fabric.api.networking.v1.PayloadTypeRegistry.clientboundPlay().register(Open.TYPE,Open.CODEC); + net.fabricmc.fabric.api.networking.v1.PayloadTypeRegistry.clientboundPlay().register(UtilityOpen.TYPE,UtilityOpen.CODEC); + net.fabricmc.fabric.api.networking.v1.PayloadTypeRegistry.clientboundPlay().register(Update.TYPE,Update.CODEC); + net.fabricmc.fabric.api.networking.v1.PayloadTypeRegistry.serverboundPlay().register(Configure.TYPE,Configure.CODEC); + net.fabricmc.fabric.api.networking.v1.PayloadTypeRegistry.serverboundPlay().register(UtilityConfigure.TYPE,UtilityConfigure.CODEC); net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking.registerGlobalReceiver(Configure.TYPE,(p,c)->c.server().execute(()->configure(c.player(),p))); net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking.registerGlobalReceiver(UtilityConfigure.TYPE,(p,c)->c.server().execute(()->configureUtility(c.player(),p))); } diff --git a/src/main/java/nl/townsandkingdoms/streamcontrol/IntegrationChecks.java b/src/main/java/nl/townsandkingdoms/streamcontrol/IntegrationChecks.java index 1f5a00a..529727a 100644 --- a/src/main/java/nl/townsandkingdoms/streamcontrol/IntegrationChecks.java +++ b/src/main/java/nl/townsandkingdoms/streamcontrol/IntegrationChecks.java @@ -17,7 +17,7 @@ public final class IntegrationChecks { public static void register() { if(Boolean.getBoolean("streamcontrol.integrationTest")) net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents.SERVER_STARTED.register(IntegrationChecks::run); } private static net.minecraft.world.level.block.state.BlockState streamState() { - var b=net.minecraft.core.registries.BuiltInRegistries.BLOCK.get(net.minecraft.resources.Identifier.fromNamespaceAndPath("streamsreflowing","stream")); + var b=net.minecraft.core.registries.BuiltInRegistries.BLOCK.getValue(net.minecraft.resources.Identifier.fromNamespaceAndPath("streamsreflowing","stream")); if(b==Blocks.AIR) throw new AssertionError("Streams fluid block missing");return b.defaultBlockState(); } private static void check(boolean condition, String message) { if (!condition) throw new AssertionError(message); } From c317ab04292093ecdef8ddd8fdb75e4e733d5899 Mon Sep 17 00:00:00 2001 From: Mikaaah <59295501+Mikaaah@users.noreply.github.com> Date: Tue, 3 Nov 2026 10:28:19 +0100 Subject: [PATCH 4/6] Verify loaded survival recipes and loot across Minecraft versions --- .../streamcontrol/EngineeringChecks.java | 3 +++ .../streamcontrol/IntegrationChecks.java | 1 + .../streamcontrol/ReleaseAssetChecks.java | 18 ++++++++++++++++++ .../data/streamcontrol/recipe/flow_sensor.json | 16 ++++------------ .../streamcontrol/recipe/mooring_post.json | 12 +++--------- 5 files changed, 29 insertions(+), 21 deletions(-) create mode 100644 src/main/java/nl/townsandkingdoms/streamcontrol/ReleaseAssetChecks.java diff --git a/src/main/java/nl/townsandkingdoms/streamcontrol/EngineeringChecks.java b/src/main/java/nl/townsandkingdoms/streamcontrol/EngineeringChecks.java index c1e3f24..6bdbcab 100644 --- a/src/main/java/nl/townsandkingdoms/streamcontrol/EngineeringChecks.java +++ b/src/main/java/nl/townsandkingdoms/streamcontrol/EngineeringChecks.java @@ -25,6 +25,9 @@ private static void toolMode(ItemStack tool,FlowWand.Mode mode) { var n=FlowWand.settings(tool);n.putInt("CalibratorMode",mode.ordinal());tool.set(DataComponents.CUSTOM_DATA,CustomData.of(n)); } public static void run(ServerLevel level,Map results) { + for(String name:new String[]{"flow_wand","current_generator","stillwater_anchor","flow_sensor","mooring_post"}) + check(level.getRecipeManager().byKey(net.minecraft.resources.ResourceKey.create(net.minecraft.core.registries.Registries.RECIPE,net.minecraft.resources.Identifier.fromNamespaceAndPath(StreamControl.ID,name))).isPresent(),"Survival recipe missing: "+name); + results.put("allSurvivalRecipesLoaded",true); BlockPos p=new BlockPos(256,120,256),q=p.east(10);var data=ControlData.get(level); level.setBlock(p,StreamControl.GENERATOR.get().defaultBlockState(),3); level.setBlock(q,StreamControl.GENERATOR.get().defaultBlockState(),3); diff --git a/src/main/java/nl/townsandkingdoms/streamcontrol/IntegrationChecks.java b/src/main/java/nl/townsandkingdoms/streamcontrol/IntegrationChecks.java index 529727a..3ac1b75 100644 --- a/src/main/java/nl/townsandkingdoms/streamcontrol/IntegrationChecks.java +++ b/src/main/java/nl/townsandkingdoms/streamcontrol/IntegrationChecks.java @@ -94,6 +94,7 @@ private static void run(net.minecraft.server.MinecraftServer server) { check(server.getCommands().getDispatcher().execute("streamcontrol status",server.createCommandSourceStack())==1,"Status command"); ControllerRegressionChecks.run(level); results.put("controllerGuiAndConnectedWaterRegression",true); HeadingRegressionChecks.run(level); results.put("eightHeadingPhysicsPersistenceAndPackets",true); + ReleaseAssetChecks.run(level,results); EngineeringChecks.run(level,results); results.put("passed",true); } catch (Throwable t) { results.put("passed",false);results.put("error",t.toString()); t.printStackTrace(); } diff --git a/src/main/java/nl/townsandkingdoms/streamcontrol/ReleaseAssetChecks.java b/src/main/java/nl/townsandkingdoms/streamcontrol/ReleaseAssetChecks.java new file mode 100644 index 0000000..445172c --- /dev/null +++ b/src/main/java/nl/townsandkingdoms/streamcontrol/ReleaseAssetChecks.java @@ -0,0 +1,18 @@ +package nl.townsandkingdoms.streamcontrol; +import java.util.Map; +import net.minecraft.core.BlockPos; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.level.block.Block; +/** Opt-in release data-pack check; invoked only by the disposable server harness. */ +public final class ReleaseAssetChecks { + public static void run(ServerLevel level,Map results){ + BlockPos pos=new BlockPos(512,120,512); + for(Block block:new Block[]{StreamControl.GENERATOR.get(),StreamControl.ANCHOR.get(),StreamControl.SENSOR.get(),StreamControl.MOORING.get()}){ + var state=block.defaultBlockState();level.setBlock(pos,state,3); + var drops=Block.getDrops(state,level,pos,level.getBlockEntity(pos)); + if(drops.stream().noneMatch(stack->stack.is(block.asItem())))throw new AssertionError("Survival block drop missing: "+block); + level.removeBlock(pos,false);pos=pos.east(4); + } + results.put("allSurvivalBlockDrops",true); + } +} diff --git a/src/main/resources/data/streamcontrol/recipe/flow_sensor.json b/src/main/resources/data/streamcontrol/recipe/flow_sensor.json index 3e8a2e8..f7cb21e 100644 --- a/src/main/resources/data/streamcontrol/recipe/flow_sensor.json +++ b/src/main/resources/data/streamcontrol/recipe/flow_sensor.json @@ -6,18 +6,10 @@ "CCC" ], "key": { - "C": { - "item": "minecraft:copper_ingot" - }, - "R": { - "item": "minecraft:redstone" - }, - "Q": { - "item": "minecraft:quartz" - }, - "M": { - "item": "minecraft:comparator" - } + "C": "minecraft:copper_ingot", + "R": "minecraft:redstone", + "Q": "minecraft:quartz", + "M": "minecraft:comparator" }, "result": { "id": "streamcontrol:flow_sensor", diff --git a/src/main/resources/data/streamcontrol/recipe/mooring_post.json b/src/main/resources/data/streamcontrol/recipe/mooring_post.json index 36225a5..cbd6867 100644 --- a/src/main/resources/data/streamcontrol/recipe/mooring_post.json +++ b/src/main/resources/data/streamcontrol/recipe/mooring_post.json @@ -6,15 +6,9 @@ "PPP" ], "key": { - "I": { - "item": "minecraft:iron_ingot" - }, - "P": { - "tag": "minecraft:planks" - }, - "C": { - "item": "minecraft:chain" - } + "I": "minecraft:iron_ingot", + "P": "#minecraft:planks", + "C": "minecraft:chain" }, "result": { "id": "streamcontrol:mooring_post", From 3caf0675d0353a9d1e6dd49d1f640e1f473b13e1 Mon Sep 17 00:00:00 2001 From: Mikaaah <59295501+Mikaaah@users.noreply.github.com> Date: Tue, 3 Nov 2026 10:31:35 +0100 Subject: [PATCH 5/6] Access modern recipes through the server during regression --- .../nl/townsandkingdoms/streamcontrol/EngineeringChecks.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/nl/townsandkingdoms/streamcontrol/EngineeringChecks.java b/src/main/java/nl/townsandkingdoms/streamcontrol/EngineeringChecks.java index 6bdbcab..d1d462e 100644 --- a/src/main/java/nl/townsandkingdoms/streamcontrol/EngineeringChecks.java +++ b/src/main/java/nl/townsandkingdoms/streamcontrol/EngineeringChecks.java @@ -26,7 +26,7 @@ private static void toolMode(ItemStack tool,FlowWand.Mode mode) { } public static void run(ServerLevel level,Map results) { for(String name:new String[]{"flow_wand","current_generator","stillwater_anchor","flow_sensor","mooring_post"}) - check(level.getRecipeManager().byKey(net.minecraft.resources.ResourceKey.create(net.minecraft.core.registries.Registries.RECIPE,net.minecraft.resources.Identifier.fromNamespaceAndPath(StreamControl.ID,name))).isPresent(),"Survival recipe missing: "+name); + check(level.getServer().getRecipeManager().byKey(net.minecraft.resources.ResourceKey.create(net.minecraft.core.registries.Registries.RECIPE,net.minecraft.resources.Identifier.fromNamespaceAndPath(StreamControl.ID,name))).isPresent(),"Survival recipe missing: "+name); results.put("allSurvivalRecipesLoaded",true); BlockPos p=new BlockPos(256,120,256),q=p.east(10);var data=ControlData.get(level); level.setBlock(p,StreamControl.GENERATOR.get().defaultBlockState(),3); From aebd54bd81eca14fd5a322e216ba5857fbb3b65b Mon Sep 17 00:00:00 2001 From: Mikaaah <59295501+Mikaaah@users.noreply.github.com> Date: Tue, 3 Nov 2026 10:35:02 +0100 Subject: [PATCH 6/6] Match modern mooring ingredients and optional Create Fabric runtime libraries --- src/main/resources/data/streamcontrol/recipe/mooring_post.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/resources/data/streamcontrol/recipe/mooring_post.json b/src/main/resources/data/streamcontrol/recipe/mooring_post.json index cbd6867..c6ff79e 100644 --- a/src/main/resources/data/streamcontrol/recipe/mooring_post.json +++ b/src/main/resources/data/streamcontrol/recipe/mooring_post.json @@ -8,7 +8,7 @@ "key": { "I": "minecraft:iron_ingot", "P": "#minecraft:planks", - "C": "minecraft:chain" + "C": "minecraft:iron_chain" }, "result": { "id": "streamcontrol:mooring_post",