Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 27 additions & 3 deletions .github/workflows/verify.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
name: fabric 26.1.2 build and regression
on:
push:
branches: ['fabric/26.1.2']
branches: ["gui/**", "feature/**", 'fabric/26.1.2']
pull_request:
branches: ['fabric/26.1.2']
workflow_dispatch:
Expand All @@ -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
Expand All @@ -44,8 +44,32 @@ 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.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
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
8 changes: 4 additions & 4 deletions build.gradle
Original file line number Diff line number Diff line change
@@ -1,22 +1,22 @@
plugins { id 'net.fabricmc.fabric-loom' version '1.15.5'; id 'maven-publish' }
version = '0.1.5'
version = '0.2.2'
group='nl.townsandkingdoms.streamcontrol'
base.archivesName='streams-flow-control-fabric-26.1.2'
repositories { mavenCentral() }
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.5-fabric-26.1.2.jar" }
tasks.named("jar") { archiveFileName = "flow-control-0.2.2-fabric-26.1.2.jar" }
Original file line number Diff line number Diff line change
@@ -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<net.minecraft.commands.CommandSourceStack> dispatcher) {
public static void register(CommandDispatcher<CommandSourceStack> 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;
})));
}
}
101 changes: 68 additions & 33 deletions src/main/java/nl/townsandkingdoms/streamcontrol/ControlConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -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<T> { 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<Boolean> ENABLED=new Value<>(true),WAND_ENABLED=new Value<>(true),OP_ONLY=new Value<>(false),PARTICLES=new Value<>(true);
public static final Value<Integer> 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<Double> 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<T> {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<Boolean> ENABLED=new Value<>(true);
public static final Value<Boolean> WAND_ENABLED=new Value<>(true);
public static final Value<Boolean> OP_ONLY=new Value<>(false);
public static final Value<Boolean> PARTICLES=new Value<>(true);
public static final Value<Integer> MAX_RADIUS=new Value<>(16);
public static final Value<Integer> DEFAULT_RADIUS=new Value<>(4);
public static final Value<Integer> VERTICAL=new Value<>(4);
public static final Value<Double> SPEED=new Value<>(1.0);
public static final Value<Double> MAX_SPEED=new Value<>(2.0);
public static final Value<Integer> MAX_REGIONS=new Value<>(2048);
public static final Value<Integer> MAX_PLAYER_REGIONS=new Value<>(256);
public static final Value<Integer> MAX_VERTICAL=new Value<>(16);
public static final Value<Integer> LINK_RANGE=new Value<>(64);
public static final Value<Integer> SENSOR_MAX_RADIUS=new Value<>(16);
public static final Value<Integer> SENSOR_MAX_VERTICAL=new Value<>(16);
public static final Value<Integer> SENSOR_INTERVAL=new Value<>(20);
public static final Value<Integer> MOORING_MAX_RADIUS=new Value<>(8);
public static final Value<Integer> MOORING_MAX_VERTICAL=new Value<>(8);
public static final Value<Double> RPM_FULL_STRENGTH=new Value<>(128.0);
public static final Value<Double> 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);}
}
}
Loading
Loading