diff --git a/.github/workflows/neoforge-1.21.1.yml b/.github/workflows/neoforge-1.21.1.yml index 6b08a85..1fe0949 100644 --- a/.github/workflows/neoforge-1.21.1.yml +++ b/.github/workflows/neoforge-1.21.1.yml @@ -9,11 +9,18 @@ on: pull_request: branches: - main + - gui/controller-layout workflow_dispatch: jobs: build: + strategy: + fail-fast: false + matrix: + flavor: [standalone, create] runs-on: ubuntu-latest + env: + CREATE_ARGS: ${{ matrix.flavor == 'create' && '-PwithCreate' || '' }} permissions: contents: read steps: @@ -35,14 +42,16 @@ jobs: sha256sum --check <<'HASH' 70930ad6b943d9a359ddb573450f5114a6a54a676e32c1895f6eb4d2cd634a33 libs/StreamsReflowing-1.21.1-neoforge-2.16.2.jar HASH + - name: Fetch verified optional Create compile fixture + run: python3 tools/fetch_create.py - name: Compile and package addon run: | chmod +x gradlew - ./gradlew --no-daemon clean build + ./gradlew --no-daemon $CREATE_ARGS clean build - name: Upload test JAR uses: actions/upload-artifact@v4 with: - name: flow-control-0.1.7-neoforge-1.21.1 + name: flow-control-0.2.2-neoforge-1.21.1-${{ matrix.flavor }} path: build/libs/flow-control-*.jar if-no-files-found: error @@ -52,7 +61,7 @@ jobs: echo 'eula=true' > run/eula.txt echo 'forceBakedFluid=true' > run/config/streamsreflowing.properties printf 'level-type=minecraft:flat\ngenerate-structures=false\nonline-mode=false\nview-distance=2\nsimulation-distance=2\n' > run/server.properties - timeout 300 ./gradlew --no-daemon -Dstreamcontrol.integrationTest=true runServer + timeout 300 ./gradlew --no-daemon -Dstreamcontrol.integrationTest=true $CREATE_ARGS runServer python3 - <<'CHECK' import json data=json.load(open('run/streamcontrol-integration-results.json')) @@ -63,7 +72,7 @@ jobs: if: always() uses: actions/upload-artifact@v4 with: - name: flow-control-0.1.7-neoforge-1.21.1-checks + name: flow-control-0.2.2-neoforge-1.21.1-${{ matrix.flavor }}-checks path: | run/streamcontrol-integration-results.json run/logs/latest.log @@ -71,16 +80,50 @@ jobs: - name: Capture the actual controller GUIs run: | mkdir -p run-client - printf 'onboardAccessibility:false\nguiScale:3\nfullscreen:false\n' > run-client/options.txt - timeout 360 xvfb-run -a ./gradlew --no-daemon -Dstreamcontrol.uiPreview=true runClient + printf 'onboardAccessibility:false\nguiScale:3\ntutorialStep:none\nfullscreen:false\n' > run-client/options.txt + timeout 360 xvfb-run -a ./gradlew --no-daemon -Dstreamcontrol.uiPreview=true $CREATE_ARGS runClient test -s run-client/screenshots/flow-control-current-generator.png test -s run-client/screenshots/flow-control-stillwater-anchor.png test -s run-client/screenshots/flow-control-redstone-paused.png + test -s run-client/screenshots/flow-control-automation.png + test -s run-client/screenshots/flow-control-flow-sensor.png + test -s run-client/screenshots/flow-control-mooring-post.png + test -s run-client/screenshots/flow-control-block-models.png + test -s run-client/screenshots/flow-control-mechanical-controller.png - name: Upload GUI screenshots if: always() uses: actions/upload-artifact@v4 with: - name: flow-control-0.1.7-client-preview + name: flow-control-0.2.2-${{ matrix.flavor }}-client-preview path: | run-client/screenshots/*.png run-client/logs/latest.log + + - name: Exercise real client gameplay against a dedicated server + run: | + rm -f run/client-test-ready + ./gradlew --no-daemon -Dstreamcontrol.clientTestServer=true $CREATE_ARGS runServer > client-test-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 test -s run/client-test-ready; then break; fi + if ! kill -0 "$SERVER_PID" 2>/dev/null; then tail -n 50 client-test-server.log; exit 1; fi + sleep 1 + done + test -s run/client-test-ready + timeout 600 xvfb-run -a ./gradlew --no-daemon -Dstreamcontrol.clientIntegrationTest=true $CREATE_ARGS runClient + python3 - <<'CHECK' + import json + data=json.load(open('run-client/streamcontrol-client-results.json')) + assert data.get('passed') is True, data + CHECK + - name: Upload live client results and screenshots + if: always() + uses: actions/upload-artifact@v4 + with: + name: flow-control-0.2.2-${{ matrix.flavor }}-client-gameplay + path: | + run-client/streamcontrol-client-results.json + run-client/screenshots/gameplay-*.png + run-client/logs/latest.log + client-test-server.log diff --git a/.gitignore b/.gitignore index 2d2c650..461be13 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,5 @@ validation/ Thumbs.db run-client/ + +libs/create-compile/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 5623832..a34460e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,42 @@ ![Flow Control](https://i.imgur.com/JGT2lvz.png) +## 0.2.2 — Verified Water Engineering and Version Ports + +- Fixed controller and utility block interactions so Copy, Paste and Link reach the Calibrator instead of opening the normal configuration screen. +- Carried the redesigned, centered interfaces and water-engineering functions to the existing Minecraft/loader ports. +- Updated every port to the exact Streams Reflowing 2.16.2 dependency and checked its current-grid and surface-particle APIs against the matching JAR. +- Preserved old wand items, legacy edits and controller settings; clients and servers must use matching 0.2.2 builds. +- Added real client/server checks for GUI Apply and synchronization, all Calibrator modes, links and unlinking, profiles, redstone switching, vertical sensors and comparator output, mooring release/recapture and real-player boarding. +- Verified the optional mechanical controller through actual Create motor input, reversal and stopping. Create's normal water-wheel RPM behavior is unchanged. +- Adapted modern ports to their own persistence, comparator and rendering APIs, including world-bound item components and 26.3 geometry submission. + +The complete feature changelog and exact supported dependency matrix are supplied with the verified release bundle. + +## 0.2.1 — Barrel and Create Model Refinement + +- Kept the original control lid on the Mechanical Current Generator and replaced its entire body with native Create Andesite Casing textures. +- Added a recessed bottom shaft socket and matched the inventory shaft to Create's actual shaft geometry and end texture. +- Rebuilt the Mooring Post as a compact barrel with straight staves, native barrel hoops, a wooden lid and subtly chamfered corners. +- Removed the old recolored machine textures from the Mooring Post. +- Preserved all water-control, sensor, mooring and Create behavior. + + +## 0.2.0 — Water Engineering + +- Repurposed the Flow Wand as the Flow Calibrator: inspect currents, show horizontal/vertical reach, copy and paste settings, and link controllers. +- Preserved the existing tool registry ID, controller settings and legacy wand regions; added a cleanup mode for old edits. +- Added two saved controller profiles with directed, reversed, calm and natural-flow actions. +- Added analog redstone strength, powered reversal, profile switching and an always-active mode alongside the original power-to-pause behavior. +- Added configurable vertical reach to controller profiles. +- Added Flow Sensors with independent horizontal and vertical ranges, comparator output for strength/direction/boats, and empty-boat filtering. +- Added Mooring Posts that secure one empty boat and release it when boarded, powered, disabled or removed. +- Added optional Create support: Mechanical Current Generator, real bottom-shaft input, RPM-based strength, reversal and stress handling. +- Reused the existing block model family and pixel patterns; added steel-blue sensors, wooden mooring posts and an andesite base for the Create variant. +- Added matching Automation, sensor and mooring screens, live utility status, survival recipes and item descriptions. +- Updated networking for the new settings. Client and server must use matching 0.2.0 builds. +- Verified NeoForge 1.21.1 / Java 21 / Streams Reflowing 2.16.2, both with and without Create 6.0.10. Other ports are unchanged. + + ## 0.1.7 - Removed the header corner rivets. diff --git a/README.md b/README.md index 34f399f..8b01174 100644 --- a/README.md +++ b/README.md @@ -8,47 +8,34 @@ Shape currents and create calm water around docks, canals and builds. An addon for **Streams Reflowing**, available for NeoForge, Forge and Fabric. -## Versions and downloads +## Water Engineering — 0.2.1 -The **0.1.7 preview for NeoForge 1.21.1** updates compatibility to **Streams Reflowing 2.16.2** and gives both controllers a distinct slate-and-copper layout. The Current Generator has a centered eight-way navigation chart; the Stillwater Anchor has a central anchor chart and symmetric range readouts. Header rivets are removed. The preview source and its build checks are on [`gui/controller-layout`](https://github.com/Mikaaah/Flow-Control/tree/gui/controller-layout). +Turn waterways into configurable transport and docking systems. The Flow Calibrator inspects currents, displays reach, copies settings and links controllers. Two saved profiles add analog redstone control, reversal and automatic switching. Flow Sensors detect current strength, direction or boats through configurable horizontal and vertical ranges. Mooring Posts secure empty boats and release them when boarded or powered. -The published **0.1.5** downloads below remain available for all eight versions and require Streams Reflowing **2.15.2**. +With **Create** installed, the **Mechanical Current Generator** accepts a bottom shaft: RPM sets strength, rotation direction reverses flow, and stopped or overstressed drives release their override. The standard controllers, sensors, posts and Calibrator work without Create. -| Minecraft | Loader | Streams Reflowing | Java | Download | -| --- | --- | --- | --- | --- | -| 1.21.1 | NeoForge | 2.15.2 | 21 | [JAR](https://github.com/Mikaaah/Flow-Control/raw/refs/heads/main/downloads/flow-control-0.1.5-neoforge-1.21.1.jar) | -| 1.20.1 | Fabric | 2.15.2 | 17 | [JAR](https://github.com/Mikaaah/Flow-Control/raw/refs/heads/fabric/1.20.1/downloads/flow-control-0.1.5-fabric-1.20.1.jar) | -| 1.21.1 | Fabric | 2.15.2 | 21 | [JAR](https://github.com/Mikaaah/Flow-Control/raw/refs/heads/fabric/1.21.1/downloads/flow-control-0.1.5-fabric-1.21.1.jar) | -| 26.1.2 | Fabric | 2.15.2 | 25 | [JAR](https://github.com/Mikaaah/Flow-Control/raw/refs/heads/fabric/26.1.2/downloads/flow-control-0.1.5-fabric-26.1.2.jar) | -| 1.20.1 | Forge | 2.15.2 | 17 | [JAR](https://github.com/Mikaaah/Flow-Control/raw/refs/heads/forge/1.20.1/downloads/flow-control-0.1.5-forge-1.20.1.jar) | -| 1.21.11 | NeoForge | 2.15.2 | 21 | [JAR](https://github.com/Mikaaah/Flow-Control/raw/refs/heads/neoforge/1.21.11/downloads/flow-control-0.1.5-neoforge-1.21.11.jar) | -| 26.1.2 | NeoForge | 2.15.2 | 25 | [JAR](https://github.com/Mikaaah/Flow-Control/raw/refs/heads/neoforge/26.1.2/downloads/flow-control-0.1.5-neoforge-26.1.2.jar) | -| 26.3 | NeoForge | 2.15.2 | 25 | [JAR](https://github.com/Mikaaah/Flow-Control/raw/refs/heads/neoforge/26.3/downloads/flow-control-0.1.5-neoforge-26.3.jar) | +The existing controller model family is retained, with steel-blue sensor colors, barrel-style mooring posts and a full andesite casing body beneath the original mechanical control lid. Existing Flow Wands become Calibrators; saved controller settings and old edits are preserved. -Install matching JARs on client and server. Fabric also requires Fabric API. -All eight builds and dedicated-server regression suites passed, including eight-way flow, equal diagonal speed, persistence and packet checks; the NeoForge 1.21.1 client was launched and all three controller-screen states were visually checked. +| Minecraft | Loader | Java | Streams Reflowing | Optional Create | Download | +| --- | --- | --- | --- | --- | --- | +| 1.21.1 | NeoForge 21.1.219+ within 21.1 | 21 | 2.16.2 | Tested with 6.0.10 | [0.2.1 JAR](https://github.com/Mikaaah/Flow-Control/raw/refs/heads/feature/water-engineering/downloads/flow-control-0.2.1-neoforge-1.21.1.jar) | -## Make water work for your build - -| Item | What it does | -| --- | --- | -| **Flow Wand** | Direct flow, still water or restore natural flow in a chosen area. | -| **Current Generator** | Maintain a directional current; rotate its top arrow to steer the flow. | -| **Stillwater Anchor** | Calm currents around a dock or mooring. | - -All three items have survival recipes. Right-click either controller to open its settings screen: adjust radius (with diameter and vertical reach), choose connected-water-only behavior, and set flow speed and direction on the Current Generator. Redstone power disables a controller. Break it to restore the underlying current. - -## Install and start +[Controls and behavior](docs/WATER-ENGINEERING.md) · [Feature branch](https://github.com/Mikaaah/Flow-Control/tree/feature/water-engineering) · [Build and regression checks](https://github.com/Mikaaah/Flow-Control/actions/runs/41593515484) -Choose a matching Minecraft and loader build from the table above, then install this addon and **Streams Reflowing 2.15.2** on client and server. Use the Java version listed for that build; Fabric also requires Fabric API. **UltraTerraForged is not required.** +The 0.2.1 feature update currently targets NeoForge 1.21.1. Earlier downloads and other loader branches remain unchanged. Use matching client/server versions. -For a dock, place a Stillwater Anchor under or beside the water within four blocks of the boats. Right-click to adjust its radius; if there are multiple nearby ponds, enable *Connected water only*. Use the Flow Wand for temporary or shaped areas. - -Flow changes velocity rather than moving water blocks or changing river terrain. Calm water removes current-induced drift; it does not prevent steering, collisions or momentum. - -## Guides +## Make water work for your build -[Items and controls](https://github.com/Mikaaah/Flow-Control/wiki/Items-and-Controls) · [Overlapping areas](https://github.com/Mikaaah/Flow-Control/wiki/Overlapping-Areas) · [Configuration](https://github.com/Mikaaah/Flow-Control/wiki/Configuration) · [Troubleshooting](https://github.com/Mikaaah/Flow-Control/wiki/Troubleshooting) +| Tool or block | Purpose | +| --- | --- | +| Flow Calibrator | Inspect currents, preview reach, copy/paste settings and link controllers. | +| Current Generator | Maintain an eight-way current with configurable profiles and redstone behavior. | +| Stillwater Anchor | Calm water around docks and switch between calm and natural water. | +| Flow Sensor | Measure strength, compass direction or boats above, below and around the sensor. | +| Mooring Post | Secure one empty boat; release it when boarded or powered. | +| Mechanical Current Generator | Optional Create shaft-driven variant with RPM and stress support. | + +Right-click blocks to configure them. Sneak-use the Calibrator in the air to switch modes. All tools and blocks have survival recipes; the mechanical recipe is available only with Create. ## Credits and license diff --git a/build.gradle b/build.gradle index 8a75322..64839aa 100644 --- a/build.gradle +++ b/build.gradle @@ -2,7 +2,7 @@ plugins { id 'java-library' id 'net.neoforged.moddev' version '2.0.148' } -version = '0.1.7' +version = '0.2.2' group = 'nl.townsandkingdoms.streamcontrol' base.archivesName = 'flow-control' java.toolchain.languageVersion = JavaLanguageVersion.of(21) @@ -18,19 +18,27 @@ neoForge { programArgument '--height' programArgument '720' systemProperty 'streamcontrol.uiPreview', System.getProperty('streamcontrol.uiPreview', 'false') + systemProperty 'streamcontrol.clientIntegrationTest', System.getProperty('streamcontrol.clientIntegrationTest', 'false') + jvmArgument '-Xmx2G' } server { server() programArgument '--nogui' - jvmArgument '-Xmx3G' + jvmArgument '-Xmx2G' gameDirectory = file('run') systemProperty 'streamcontrol.integrationTest', System.getProperty('streamcontrol.integrationTest', 'false') + systemProperty 'streamcontrol.clientTestServer', System.getProperty('streamcontrol.clientTestServer', 'false') } } mods { streamcontrol { sourceSet sourceSets.main } } } dependencies { implementation files('libs/StreamsReflowing-1.21.1-neoforge-2.16.2.jar') + compileOnly files('libs/create-1.21.1-6.0.10.jar') + compileOnly fileTree('libs/create-compile') { include '*.jar' } + if (providers.gradleProperty('withCreate').isPresent()) { + runtimeOnly files('libs/create-1.21.1-6.0.10.jar') + } // Optional local compatibility test fixtures, never included in the published jar. if (providers.gradleProperty('withUtf').isPresent()) { runtimeOnly files('libs/ultraterraforged-1.0.2+1.21.1-neoforge.jar', @@ -40,4 +48,4 @@ dependencies { tasks.withType(JavaCompile).configureEach { options.encoding = 'UTF-8'; options.release = 21 } tasks.named('jar') { from('LICENSE') } -tasks.named("jar") { archiveFileName = "flow-control-0.1.7-neoforge-1.21.1.jar" } +tasks.named("jar") { archiveFileName = "flow-control-0.2.2-neoforge-1.21.1.jar" } diff --git a/docs/WATER-ENGINEERING.md b/docs/WATER-ENGINEERING.md new file mode 100644 index 0000000..2438565 --- /dev/null +++ b/docs/WATER-ENGINEERING.md @@ -0,0 +1,73 @@ +# Flow Control 0.2.1 — Water Engineering + +| Minecraft | Loader | Java | Required | Optional integration tested | +| --- | --- | --- | --- | --- | +| 1.21.1 | NeoForge 21.1.219+ within 21.1 | 21 | Streams Reflowing 2.16.2 | Create 6.0.10 | + +The same Flow Control JAR works with and without Create. The Mechanical Current Generator and its crafting recipe are available only when Create is installed. Other loader/version branches have not received this feature update. + +## Flow Calibrator + +Existing Flow Wands become Calibrators automatically because their registry ID is retained. Calibrators inspect and configure water engineering; freehand current painting is left to Streams Reflowing's own tool. Existing wand-made regions remain in the world. + +| Input or mode | Action | +| --- | --- | +| Hold the Calibrator and aim at water | Read direction, current strength and whether the source is natural, a controller or a legacy edit. | +| Aim at a controller, sensor or mooring post | Read its range; a cylinder outline shows horizontal and vertical reach. | +| Use while aiming at empty air | Toggle range outlines. | +| Sneak-use while aiming at empty air | Cycle Inspect, Copy, Paste, Link and Remove legacy edit. | +| Inspect: use a controller or utility block | Open its settings. | +| Copy, then Paste | Copy both controller profiles or compatible sensor/mooring settings. Paste does not copy links. | +| Link: use source, then target controller | Target follows the source's profiles and redstone mode. Each target reads its own local redstone signal and, if mechanical, its own drive. | +| Link: sneak-use a controller | Unlink it and clear the selected source. | +| Remove legacy edit: use edited water | Remove your old wand region. Operators can also remove other players' old edits. | + +Links are restricted to the same dimension and default to a maximum of 64 blocks. Self-links and loops are rejected. A linked controller pauses if the source is unloaded or missing; linking never keeps chunks loaded. Unlinking restores its own saved profiles. The outline shows the reach cylinder; connected-water filtering can exclude water inside it. + +## Controller profiles + +The Settings tab edits radius, current strength, direction and connected-water filtering for the selected profile. In Automation, select Profile A or B, choose its action and vertical reach, and select the redstone mode. Changes are saved only with Apply changes. + +| Redstone mode | Behavior | +| --- | --- | +| Pause when powered | Original behavior: power releases the controller's override. This is the migration default. | +| Always active | Ignore the redstone signal. | +| Analog strength 0–15 | Scale the selected strength by signal / 15. Signal 0 calms a Current Generator's region. On a Stillwater Anchor, 0 releases the override and positive signals calm the region. | +| Reverse when powered | Reverse the selected current while powered. | +| Switch Profile A / B | No power uses A; positive power uses B. Each profile saves its own settings. | + +Profile actions are directed current, reversed current, calm water or natural flow. Stillwater Anchors offer calm water and natural flow. Natural flow releases this controller's override, allowing underlying controls or natural currents to resume. Eight compass headings keep equal cardinal and diagonal strength. + +## Flow Sensor + +Horizontal radius and vertical reach are independent. Defaults are radius 4 and height ±4; server limits default to 16 for each. The sensor samples a cylinder, skips unloaded chunks and updates periodically rather than scanning every tick. Place a comparator against it to read the result. + +| Measurement | Comparator output | +| --- | --- | +| Current strength | Strongest horizontal current in range. 0 means still; 15 means the configured maximum current strength. At the default maximum of 200%, a 100% current gives output 8. | +| Compass direction | Direction of the strongest sampled current: N=1, NE=2, E=3, SE=4, S=5, SW=6, W=7, NW=8. Still water gives 0. | +| Nearby boats | Boat count, capped at 15. The empty-boats option excludes boats with passengers in this mode. | + +Vertical reach applies to both water sampling and boat detection. Comparator output and range/mode settings persist; live status is synchronized to clients. + +## Mooring Post + +The barrel-style post uses native wooden staves and barrel hoops. It automatically secures one empty boat already inside its configured cylinder. It holds horizontal position while preserving vertical bobbing. It does not pull boats from outside its range or keep an occupied boat captive. + +Boarding, redstone power, disabling automatic mooring or breaking the post releases the boat. A short cooldown prevents immediate recapture. Multiple posts cannot hold the same boat. Defaults are radius 4 and vertical reach ±2; both are configurable in the GUI within server limits. + +## Optional Create support + +The Mechanical Current Generator keeps the original control lid over a fully Create-textured Andesite Casing body, with a recessed rotating shaft input on its bottom face. Its recipe uses a Current Generator, Andesite Casing and Shaft. + +Connect a real Create shaft or drive below it. The default calibration is 128 RPM for 100% of the configured profile strength; 64 RPM gives half strength and 256 RPM gives double, limited by the server maximum. Negative RPM reverses the current. Low RPM remains proportionally weak rather than jumping to the manual 5% minimum. The default stress impact is 4 SU per RPM. + +A stopped or overstressed drive releases its override. The controller supports the same profiles, connected-water filtering, links and redstone modes as the standard generator. Create is optional; its own dependencies are supplied by Create rather than bundled in Flow Control. + +## Migration and validation + +Existing controller settings are loaded into Profile A. Old wand regions remain editable through the cleanup mode and `/streamcontrol clear` for operators. `/streamcontrol status` remains available. The old wand radius/strength commands are replaced by GUI profile editing. + +Use matching Flow Control 0.2.1 versions on client and server because the settings packet protocol changed. The original `wandEnabled`, `wandRequiresOperator` and `showWandFeedbackParticles` configuration keys are retained for compatibility; they now control the Calibrator and its range outlines. + +Both standalone and Create 6.0.10 builds passed compilation and the dedicated-server suite. Tests cover legacy flow behavior, profiles and migration, linking and loop rejection, Calibrator copy/paste/link/cleanup, sensor vertical and horizontal limits, comparator output, occupied boats, mooring release and cooldown, packet round trips, optional registration/recipes, and a real Create motor network including reversal, low RPM, stress and stopping. Actual Minecraft screenshots cover eight GUI/model states with and without Create. This is not a full TNK pack or interactive client-world playtest. diff --git a/docs/validation-0.2.0.json b/docs/validation-0.2.0.json new file mode 100644 index 0000000..7fd53ac --- /dev/null +++ b/docs/validation-0.2.0.json @@ -0,0 +1,60 @@ +{ + "source_commit": "69056a64d69ba46f6c22cdfc9421b5f978ffae7e", + "workflow_run": "https://github.com/Mikaaah/Flow-Control/actions/runs/41593515484", + "jar_sha256": "6fb829305e9382af6f5a28efaa786e4ac2eee0a481b859f72b50cbb6d45fc1cd", + "checks": { + "standalone": { + "vanillaWaterAndVesselFlow": true, + "manualDirectionRetainsPriorityOverDownhillGuard": true, + "bakedWaterStillnessAndOverlap": true, + "mooredBoatTwentyTicks": true, + "savedDataRoundTrip": true, + "connectedWaterIsolationAndPersistence": true, + "networkCodecRoundTrip": true, + "dimensionIsolation": true, + "masterSwitch": true, + "controllerRedstoneAndRemoval": true, + "eightHeadingPhysicsPersistenceAndPackets": true, + "profileModesPersistenceAndMigration": true, + "controllerLinkPropagationCycleAndMissingSource": true, + "calibratorCopyPasteLinkAndLegacyCleanup": true, + "sensorVerticalHorizontalDirectionBoatAndComparator": true, + "mooringHoldBoardingRedstoneAndCooldown": true, + "engineeringPacketRoundTrips": true, + "standaloneWithoutCreate": true, + "passed": true + }, + "create_6_0_10": { + "vanillaWaterAndVesselFlow": true, + "manualDirectionRetainsPriorityOverDownhillGuard": true, + "bakedWaterStillnessAndOverlap": true, + "mooredBoatTwentyTicks": true, + "savedDataRoundTrip": true, + "connectedWaterIsolationAndPersistence": true, + "networkCodecRoundTrip": true, + "dimensionIsolation": true, + "masterSwitch": true, + "controllerRedstoneAndRemoval": true, + "eightHeadingPhysicsPersistenceAndPackets": true, + "profileModesPersistenceAndMigration": true, + "controllerLinkPropagationCycleAndMissingSource": true, + "calibratorCopyPasteLinkAndLegacyCleanup": true, + "sensorVerticalHorizontalDirectionBoatAndComparator": true, + "mooringHoldBoardingRedstoneAndCooldown": true, + "engineeringPacketRoundTrips": true, + "createRealNetworkRpmReverseStressAndStop": true, + "passed": true + } + }, + "client_captures": [ + "current-generator", + "stillwater-anchor", + "redstone-paused", + "automation", + "flow-sensor", + "mooring-post", + "block-models", + "mechanical-controller" + ], + "full_modpack_playtest": false +} diff --git a/docs/validation-0.2.1.json b/docs/validation-0.2.1.json new file mode 100644 index 0000000..5676be2 --- /dev/null +++ b/docs/validation-0.2.1.json @@ -0,0 +1,51 @@ +{ + "source_commit": "d1aae5dd0d8596bff754d92b59faa03b8ff975e8", + "workflow_run": "https://github.com/Mikaaah/Flow-Control/actions/runs/41600440210", + "jar_sha256": "87191ee4b24f56f8e360553d415391345d9dd9b119223fd8272f96c7a4b0fdcc", + "checks": { + "standalone": { + "vanillaWaterAndVesselFlow": true, + "manualDirectionRetainsPriorityOverDownhillGuard": true, + "bakedWaterStillnessAndOverlap": true, + "mooredBoatTwentyTicks": true, + "savedDataRoundTrip": true, + "connectedWaterIsolationAndPersistence": true, + "networkCodecRoundTrip": true, + "dimensionIsolation": true, + "masterSwitch": true, + "controllerRedstoneAndRemoval": true, + "eightHeadingPhysicsPersistenceAndPackets": true, + "profileModesPersistenceAndMigration": true, + "controllerLinkPropagationCycleAndMissingSource": true, + "calibratorCopyPasteLinkAndLegacyCleanup": true, + "sensorVerticalHorizontalDirectionBoatAndComparator": true, + "mooringHoldBoardingRedstoneAndCooldown": true, + "engineeringPacketRoundTrips": true, + "standaloneWithoutCreate": true, + "passed": true + }, + "create_6_0_10": { + "vanillaWaterAndVesselFlow": true, + "manualDirectionRetainsPriorityOverDownhillGuard": true, + "bakedWaterStillnessAndOverlap": true, + "mooredBoatTwentyTicks": true, + "savedDataRoundTrip": true, + "connectedWaterIsolationAndPersistence": true, + "networkCodecRoundTrip": true, + "dimensionIsolation": true, + "masterSwitch": true, + "controllerRedstoneAndRemoval": true, + "eightHeadingPhysicsPersistenceAndPackets": true, + "profileModesPersistenceAndMigration": true, + "controllerLinkPropagationCycleAndMissingSource": true, + "calibratorCopyPasteLinkAndLegacyCleanup": true, + "sensorVerticalHorizontalDirectionBoatAndComparator": true, + "mooringHoldBoardingRedstoneAndCooldown": true, + "engineeringPacketRoundTrips": true, + "createRealNetworkRpmReverseStressAndStop": true, + "passed": true + } + }, + "model_capture_checked": true, + "full_modpack_playtest": false +} diff --git a/downloads/SHA256SUMS-0.2.0.txt b/downloads/SHA256SUMS-0.2.0.txt new file mode 100644 index 0000000..9c26097 --- /dev/null +++ b/downloads/SHA256SUMS-0.2.0.txt @@ -0,0 +1 @@ +6fb829305e9382af6f5a28efaa786e4ac2eee0a481b859f72b50cbb6d45fc1cd flow-control-0.2.0-neoforge-1.21.1.jar diff --git a/downloads/SHA256SUMS-0.2.1.txt b/downloads/SHA256SUMS-0.2.1.txt new file mode 100644 index 0000000..22db6de --- /dev/null +++ b/downloads/SHA256SUMS-0.2.1.txt @@ -0,0 +1 @@ +87191ee4b24f56f8e360553d415391345d9dd9b119223fd8272f96c7a4b0fdcc flow-control-0.2.1-neoforge-1.21.1.jar diff --git a/downloads/flow-control-0.2.0-neoforge-1.21.1.jar b/downloads/flow-control-0.2.0-neoforge-1.21.1.jar new file mode 100644 index 0000000..1551889 Binary files /dev/null and b/downloads/flow-control-0.2.0-neoforge-1.21.1.jar differ diff --git a/downloads/flow-control-0.2.1-neoforge-1.21.1.jar b/downloads/flow-control-0.2.1-neoforge-1.21.1.jar new file mode 100644 index 0000000..8252d15 Binary files /dev/null and b/downloads/flow-control-0.2.1-neoforge-1.21.1.jar differ diff --git a/src/main/java/nl/townsandkingdoms/streamcontrol/ClientTestWorld.java b/src/main/java/nl/townsandkingdoms/streamcontrol/ClientTestWorld.java new file mode 100644 index 0000000..c8c8277 --- /dev/null +++ b/src/main/java/nl/townsandkingdoms/streamcontrol/ClientTestWorld.java @@ -0,0 +1,75 @@ +package nl.townsandkingdoms.streamcontrol; + +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.GameType; +import net.minecraft.world.entity.ai.attributes.Attributes; +import net.minecraft.world.entity.vehicle.Boat; +import net.minecraft.world.item.ItemStack; +import net.neoforged.neoforge.common.NeoForge; +import net.neoforged.neoforge.event.server.ServerStartedEvent; +import net.neoforged.neoforge.event.entity.player.PlayerEvent; +import net.neoforged.neoforge.event.RegisterCommandsEvent; +import static net.minecraft.commands.Commands.literal; + +/** Test fixture only; disabled unless explicitly requested by the CI launcher. */ +public final class ClientTestWorld { + public static final BlockPos GENERATOR=new BlockPos(0,100,0), TARGET=new BlockPos(4,100,0), + SENSOR=new BlockPos(-4,100,0), MOORING=new BlockPos(0,100,3), ANCHOR=new BlockPos(0,100,6), + MECHANICAL=new BlockPos(4,100,3); + public static void register() { + if(!Boolean.getBoolean("streamcontrol.clientTestServer")) return; + NeoForge.EVENT_BUS.addListener(ClientTestWorld::prepare); + NeoForge.EVENT_BUS.addListener(ClientTestWorld::login); + NeoForge.EVENT_BUS.addListener(ClientTestWorld::commands); + NeoForge.EVENT_BUS.addListener(ClientTestWorld::interaction); + } + private static void prepare(ServerStartedEvent e) { + var l=e.getServer().overworld();l.setDayTime(6000);l.setWeatherParameters(100000,0,false,false); + for(int x=-8;x<=8;x++) for(int z=-4;z<=9;z++) { + l.setBlock(new BlockPos(x,99,z),Blocks.STONE.defaultBlockState(),3); + l.setBlock(new BlockPos(x,100,z),(z<-1 || Math.abs(x)==8 || z==9?Blocks.STONE:Blocks.WATER).defaultBlockState(),3); + } + l.setBlock(GENERATOR,StreamControl.GENERATOR.get().defaultBlockState(),3); + l.setBlock(TARGET,StreamControl.GENERATOR.get().defaultBlockState(),3); + l.setBlock(SENSOR,StreamControl.SENSOR.get().defaultBlockState(),3); + l.setBlock(MOORING,StreamControl.MOORING.get().defaultBlockState(),3); + l.setBlock(ANCHOR,StreamControl.ANCHOR.get().defaultBlockState(),3); + ((ControllerEntity)l.getBlockEntity(ANCHOR)).configure(FlowHeading.NORTH,3,1,false); + // A separate high pool makes the sensor's independent vertical range observable. + l.setBlock(SENSOR.above(6),Blocks.WATER.defaultBlockState(),3); + for(var d:net.minecraft.core.Direction.Plane.HORIZONTAL) l.setBlock(SENSOR.above(6).relative(d),Blocks.GLASS.defaultBlockState(),3); + l.setBlock(SENSOR.above(5),Blocks.GLASS.defaultBlockState(),3); + ControlData.get(l).put(l,new FlowRegion(9001,SENSOR.above(6),1,0,1,0,2,false,false,new java.util.UUID(0,0),ControlData.get(l).next())); + var boat=new Boat(l,.5,100.8,4.5);l.addFreshEntity(boat); + if(net.neoforged.fml.ModList.get().isLoaded("create")) nl.townsandkingdoms.streamcontrol.compat.create.CreateClientFixture.prepare(l); + try { java.nio.file.Files.writeString(net.neoforged.fml.loading.FMLPaths.GAMEDIR.get().resolve("client-test-ready"),"ready"); } + catch(java.io.IOException ex) { throw new RuntimeException(ex); } + } + private static void login(PlayerEvent.PlayerLoggedInEvent e) { + if(!(e.getEntity() instanceof net.minecraft.server.level.ServerPlayer p)) return; + p.setGameMode(GameType.CREATIVE);p.teleportTo(.5,101,-2.5);p.setYRot(0);p.setXRot(20); + p.getAttribute(Attributes.BLOCK_INTERACTION_RANGE).setBaseValue(8); + p.getAttribute(Attributes.ENTITY_INTERACTION_RANGE).setBaseValue(8); + p.getInventory().setItem(0,new ItemStack(StreamControl.WAND.get())); + p.getInventory().selected=0;p.inventoryMenu.broadcastChanges(); + } + private static void interaction(net.neoforged.neoforge.event.entity.player.PlayerInteractEvent.EntityInteract e) { + if(e.getTarget() instanceof Boat b) System.out.println("FLOW_BOARD_INPUT boat="+b.position()+" underwater="+b.isUnderWater()+" eyeFluid="+b.getEyeInFluidType()+" passengers="+b.getPassengers().size()+" hand="+e.getItemStack()+" crouch="+e.getEntity().isShiftKeyDown()+" player="+e.getEntity().position()); + } + private static void commands(RegisterCommandsEvent e) { + e.getDispatcher().register(literal("flowclient").then(literal("power").executes(c->{ + var l=c.getSource().getLevel();l.setBlock(GENERATOR.below(),Blocks.REDSTONE_BLOCK.defaultBlockState(),3);return 1; + })).then(literal("unpower").executes(c->{ + var l=c.getSource().getLevel();l.setBlock(GENERATOR.below(),Blocks.STONE.defaultBlockState(),3);return 1; + })).then(literal("dock").executes(c->{var p=c.getSource().getPlayerOrException();p.teleportTo(.5,101,3.5);p.setDeltaMovement(net.minecraft.world.phys.Vec3.ZERO);return 1; + })).then(literal("view").executes(c->{ + var l=c.getSource().getLevel();l.setBlock(new BlockPos(4,102,-4),Blocks.STONE.defaultBlockState(),3); + var p=c.getSource().getPlayerOrException();p.teleportTo(4.5,103,-3.5);p.setDeltaMovement(net.minecraft.world.phys.Vec3.ZERO);return 1; + })).then(literal("reverse").executes(c->{ + if(net.neoforged.fml.ModList.get().isLoaded("create")) nl.townsandkingdoms.streamcontrol.compat.create.CreateClientFixture.speed(c.getSource().getLevel(),-128);return 1; + })).then(literal("stop").executes(c->{ + if(net.neoforged.fml.ModList.get().isLoaded("create")) nl.townsandkingdoms.streamcontrol.compat.create.CreateClientFixture.speed(c.getSource().getLevel(),0);return 1; + }))); + } +} diff --git a/src/main/java/nl/townsandkingdoms/streamcontrol/ControlCommands.java b/src/main/java/nl/townsandkingdoms/streamcontrol/ControlCommands.java index a4e20d4..f813f7c 100644 --- a/src/main/java/nl/townsandkingdoms/streamcontrol/ControlCommands.java +++ b/src/main/java/nl/townsandkingdoms/streamcontrol/ControlCommands.java @@ -1,30 +1,18 @@ 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 net.neoforged.neoforge.event.RegisterCommandsEvent; public final class ControlCommands { public static void register(RegisterCommandsEvent e) { e.getDispatcher().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.hasPermission(2)).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.hasPermission(2)).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 6c2622a..fe32669 100644 --- a/src/main/java/nl/townsandkingdoms/streamcontrol/ControlConfig.java +++ b/src/main/java/nl/townsandkingdoms/streamcontrol/ControlConfig.java @@ -3,14 +3,14 @@ public final class ControlConfig { public static final ModConfigSpec SPEC; public static final ModConfigSpec.BooleanValue ENABLED, WAND_ENABLED, OP_ONLY, PARTICLES; - public static final ModConfigSpec.IntValue MAX_RADIUS, DEFAULT_RADIUS, VERTICAL, MAX_REGIONS, MAX_PLAYER_REGIONS; - public static final ModConfigSpec.DoubleValue SPEED, MAX_SPEED; + public static final ModConfigSpec.IntValue MAX_RADIUS, DEFAULT_RADIUS, VERTICAL, MAX_REGIONS, MAX_PLAYER_REGIONS, MAX_VERTICAL, LINK_RANGE, SENSOR_MAX_RADIUS, SENSOR_MAX_VERTICAL, SENSOR_INTERVAL, MOORING_MAX_RADIUS, MOORING_MAX_VERTICAL; + public static final ModConfigSpec.DoubleValue SPEED, MAX_SPEED, RPM_FULL_STRENGTH, KINETIC_STRESS; static { var b = new ModConfigSpec.Builder(); ENABLED = b.comment("Master switch. Overrides remain saved when disabled.").define("enabled", true); - WAND_ENABLED = b.define("wandEnabled", true); + WAND_ENABLED = b.comment("Enable the Flow Calibrator; legacy config key retained.").define("wandEnabled", true); OP_ONLY = b.define("wandRequiresOperator", false); - PARTICLES = b.define("showWandFeedbackParticles", true); + PARTICLES = b.comment("Show Calibrator range outlines; legacy config key retained.").define("showWandFeedbackParticles", true); MAX_RADIUS = b.defineInRange("maxRadius", 16, 1, 32); DEFAULT_RADIUS = b.defineInRange("defaultRadius", 4, 1, 32); VERTICAL = b.comment("Blocks above and below the selected water or controller that can be affected.").defineInRange("verticalReach", 4, 0, 16); @@ -18,6 +18,15 @@ public final class ControlConfig { MAX_SPEED = b.defineInRange("maxStrength", 2.0, 0.05, 2.0); MAX_REGIONS = b.defineInRange("maxRegionsPerDimension", 2048, 1, 8192); MAX_PLAYER_REGIONS = b.defineInRange("maxWandRegionsPerPlayer", 256, 1, 2048); + MAX_VERTICAL = b.defineInRange("maxControllerVerticalReach",16,0,16); + LINK_RANGE = b.defineInRange("maxControllerLinkDistance",64,1,256); + SENSOR_MAX_RADIUS = b.defineInRange("maxSensorRadius",16,1,16); + SENSOR_MAX_VERTICAL = b.defineInRange("maxSensorVerticalReach",16,0,16); + SENSOR_INTERVAL = b.defineInRange("sensorSampleIntervalTicks",20,5,200); + MOORING_MAX_RADIUS = b.defineInRange("maxMooringRadius",8,1,16); + MOORING_MAX_VERTICAL = b.defineInRange("maxMooringVerticalReach",8,0,16); + RPM_FULL_STRENGTH = b.defineInRange("kineticRpmForFullStrength",128.0,1.0,1024.0); + KINETIC_STRESS = b.defineInRange("kineticStressImpact",4.0,0.0,256.0); SPEC = b.build(); } public static int radius(int value) { return Math.clamp(value, 1, MAX_RADIUS.get()); } diff --git a/src/main/java/nl/townsandkingdoms/streamcontrol/ControlNetwork.java b/src/main/java/nl/townsandkingdoms/streamcontrol/ControlNetwork.java index 64e41e6..a606c30 100644 --- a/src/main/java/nl/townsandkingdoms/streamcontrol/ControlNetwork.java +++ b/src/main/java/nl/townsandkingdoms/streamcontrol/ControlNetwork.java @@ -14,39 +14,61 @@ 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<>(ResourceLocation.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<>(ResourceLocation.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<>(ResourceLocation.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<>(ResourceLocation.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 record UtilityOpen(long pos,CompoundTag settings) implements CustomPacketPayload { + public static final Type TYPE=new Type<>(ResourceLocation.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 UtilityConfigure(long pos,CompoundTag settings) implements CustomPacketPayload { + public static final Type TYPE=new Type<>(ResourceLocation.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) implements CustomPacketPayload { public static final Type TYPE = new Type<>(ResourceLocation.fromNamespaceAndPath(StreamControl.ID, "region")); public static final StreamCodec CODEC = new StreamCodec<>() { @@ -56,7 +78,15 @@ public record Update(String dimension, boolean reset, CompoundTag region, long r @Override public Type type() { return TYPE; } } public static void register(RegisterPayloadHandlersEvent event) { - var registrar = event.registrar("3"); + var registrar = event.registrar("4"); + registrar.playToClient(UtilityOpen.TYPE,UtilityOpen.CODEC,(p,context)->context.enqueueWork(()-> + nl.townsandkingdoms.streamcontrol.client.UtilityScreen.open(p))); + registrar.playToServer(UtilityConfigure.TYPE,UtilityConfigure.CODEC,(p,context)->context.enqueueWork(()->{ + Player player=context.player();BlockPos pos=BlockPos.of(p.pos()); + if(!(player.level() instanceof ServerLevel level) || !level.hasChunkAt(pos) || player.blockPosition().distSqr(pos)>64 + || !player.mayBuild() || !level.mayInteract(player,pos) || p.settings()==null) return; + if(level.getBlockEntity(pos) instanceof UtilityAccess utility) utility.configure(p.settings()); + })); registrar.playToClient(Update.TYPE, Update.CODEC, (p, context) -> context.enqueueWork(() -> { nl.townsandkingdoms.streamcontrol.client.ClientUpdates.apply(p); })); @@ -67,18 +97,26 @@ public static void register(RegisterPayloadHandlersEvent event) { BlockPos pos = BlockPos.of(p.pos()); if (!(player.level() instanceof ServerLevel) || player.blockPosition().distSqr(pos) > 64 || !player.mayBuild() || !player.level().mayInteract(player, pos)) return; - if (!(player.level().getBlockEntity(pos) instanceof ControllerEntity be)) return; + if (!(player.level().getBlockEntity(pos) instanceof FlowController be)) return; FlowHeading d = FlowHeading.fromId(p.facing()); if (d == null) return; - be.configure(d, ControlConfig.radius(p.radius()), ControlConfig.strength(p.strength()), p.connectedOnly()); + if (p.settings()!=null && p.settings().contains("ProfileA") && p.settings().contains("ProfileB")) be.controls().configure(p.settings()); + else be.configure(d,ControlConfig.radius(p.radius()),ControlConfig.strength(p.strength()),p.connectedOnly()); })); } - public static void open(ServerPlayer player, ControllerEntity be) { - var state = be.getBlockState(); - PacketDistributor.sendToPlayer(player, new Open(be.getBlockPos().asLong(), be.radius(), - ControlConfig.VERTICAL.get(), be.strength(), ControlConfig.MAX_SPEED.get(), - ControlConfig.MAX_RADIUS.get(), be.connectedOnly(), ((ControllerBlock)state.getBlock()).still, - state.getValue(ControllerBlock.POWERED), be.heading().id())); + public static void open(ServerPlayer player, FlowController be) { + var state = be.blockEntity().getBlockState(); + PacketDistributor.sendToPlayer(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(), + state.getValue(ControllerBlock.POWERED), be.heading().id(), snapshot(be))); + } + private static CompoundTag snapshot(FlowController controller) { + var n=controller.controls().save();n.putBoolean("Kinetic",!Float.isNaN(controller.rpm())); + n.putFloat("RPM",Float.isFinite(controller.rpm())?controller.rpm():0);n.putInt("MaxVertical",ControlConfig.MAX_VERTICAL.get());return n; + } + public static void openUtility(ServerPlayer player,UtilityAccess be) { + PacketDistributor.sendToPlayer(player,new UtilityOpen(be.blockEntity().getBlockPos().asLong(),be.settings())); } public static void update(ServerLevel level, FlowRegion r) { PacketDistributor.sendToPlayersInDimension(level, new Update(level.dimension().location().toString(), false, r.save(), 0)); } public static void remove(ServerLevel level, long key) { PacketDistributor.sendToPlayersInDimension(level, new Update(level.dimension().location().toString(), false, null, key)); } diff --git a/src/main/java/nl/townsandkingdoms/streamcontrol/ControllerBlock.java b/src/main/java/nl/townsandkingdoms/streamcontrol/ControllerBlock.java index 6da958a..495bd67 100644 --- a/src/main/java/nl/townsandkingdoms/streamcontrol/ControllerBlock.java +++ b/src/main/java/nl/townsandkingdoms/streamcontrol/ControllerBlock.java @@ -41,6 +41,10 @@ public ControllerBlock(boolean still, Properties p) { if (level.getBlockEntity(pos) instanceof ControllerEntity be) be.waterChanged(); } } + @Override protected ItemInteractionResult 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()) ? ItemInteractionResult.SKIP_DEFAULT_BLOCK_INTERACTION : 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 ServerPlayer player && level.getBlockEntity(pos) instanceof ControllerEntity be) diff --git a/src/main/java/nl/townsandkingdoms/streamcontrol/ControllerEntity.java b/src/main/java/nl/townsandkingdoms/streamcontrol/ControllerEntity.java index dec879b..18a2f8a 100644 --- a/src/main/java/nl/townsandkingdoms/streamcontrol/ControllerEntity.java +++ b/src/main/java/nl/townsandkingdoms/streamcontrol/ControllerEntity.java @@ -1,62 +1,17 @@ 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(CompoundTag tag, HolderLookup.Provider registry) { super.saveAdditional(tag,registry); tag.putInt("Radius",radius); tag.putDouble("Strength",strength); tag.putBoolean("ConnectedOnly",connectedOnly); } - @Override protected void loadAdditional(CompoundTag tag, HolderLookup.Provider registry) { super.loadAdditional(tag,registry); radius = tag.contains("Radius") ? tag.getInt("Radius") : -1; strength = tag.contains("Strength") ? tag.getDouble("Strength") : -1; connectedOnly = tag.getBoolean("ConnectedOnly"); 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(CompoundTag n,HolderLookup.Provider registry) { super.saveAdditional(n,registry); n.merge(controls.save()); } + @Override protected void loadAdditional(CompoundTag n,HolderLookup.Provider registry) { super.loadAdditional(n,registry); 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..0e94edb --- /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.getCompound("ProfileA"),host.isStill()) : + new FlowProfile(n.contains("Radius") && n.getInt("Radius") > 0 ? n.getInt("Radius") : ControlConfig.DEFAULT_RADIUS.get(), + ControlConfig.VERTICAL.get(), n.contains("Strength") && n.getDouble("Strength") >= 0 ? n.getDouble("Strength") : ControlConfig.SPEED.get(), + host.heading(), n.getBoolean("ConnectedOnly"),host.isStill() ? FlowProfile.Action.CALM : FlowProfile.Action.CURRENT); + b = n.contains("ProfileB") ? FlowProfile.load(n.getCompound("ProfileB"),host.isStill()) : null; + redstone = Redstone.from(n.getInt("RedstoneMode")); + source = n.contains("LinkedSource") ? BlockPos.of(n.getLong("LinkedSource")) : 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.getCompound("ProfileA"),host.isStill()); + b = bounded(FlowProfile.load(n.getCompound("ProfileB"),host.isStill())); + redstone = Redstone.from(n.getInt("RedstoneMode")); + 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..ffda32f --- /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,List lines,TooltipFlag flag) { + super.appendHoverText(stack,context,lines,flag); + lines.add(Component.translatable(getDescriptionId()+".tip").withStyle(net.minecraft.ChatFormatting.GRAY)); + lines.add(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..2512b2a --- /dev/null +++ b/src/main/java/nl/townsandkingdoms/streamcontrol/EngineeringChecks.java @@ -0,0 +1,124 @@ +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; +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; +import net.neoforged.neoforge.common.util.FakePlayerFactory; + +/** 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(saved,level.registryAccess()); + 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(old,level.registryAccess());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=FakePlayerFactory.get(level,new GameProfile(UUID.randomUUID(),"FlowChecks"));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)==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=new Boat(level,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(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),"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(sensor.saveWithoutMetadata(level.registryAccess()),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=new Boat(level,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),"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); + boolean create=net.neoforged.fml.ModList.get().isLoaded("create"); + boolean registered=net.minecraft.core.registries.BuiltInRegistries.BLOCK.containsKey(net.minecraft.resources.ResourceLocation.fromNamespaceAndPath(StreamControl.ID,"mechanical_current_generator")); + check(create==registered,"Create block registration is not optional"); + boolean recipe=level.getRecipeManager().byKey(net.minecraft.resources.ResourceLocation.fromNamespaceAndPath(StreamControl.ID,"mechanical_current_generator")).isPresent(); + check(create==recipe,"Mechanical recipe condition does not match optional Create presence"); + check(level.getRecipeManager().byKey(net.minecraft.resources.ResourceLocation.fromNamespaceAndPath(StreamControl.ID,"flow_sensor")).isPresent(),"Flow sensor recipe missing"); + check(level.getRecipeManager().byKey(net.minecraft.resources.ResourceLocation.fromNamespaceAndPath(StreamControl.ID,"mooring_post")).isPresent(),"Mooring recipe missing"); + if(create) nl.townsandkingdoms.streamcontrol.compat.create.CreateChecks.run(level,results); + else results.put("standaloneWithoutCreate",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..eeec8d5 --- /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.getInt("Radius") : d.radius, + n.contains("Vertical") ? n.getInt("Vertical") : d.vertical, + n.contains("Strength") ? n.getDouble("Strength") : d.strength, + n.contains("Heading") ? FlowHeading.fromId(n.getInt("Heading")) : d.heading, + n.getBoolean("ConnectedOnly"), n.contains("Action") ? Action.from(n.getInt("Action")) : 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 b07dcae..38af0a4 100644 --- a/src/main/java/nl/townsandkingdoms/streamcontrol/FlowRegion.java +++ b/src/main/java/nl/townsandkingdoms/streamcontrol/FlowRegion.java @@ -57,7 +57,7 @@ public static FlowRegion load(CompoundTag n) { double speed = n.getDouble("Strength"); return new FlowRegion(n.getLong("Key"), BlockPos.of(n.getLong("Pos")), Math.clamp(n.getInt("Radius"), 1, 32), Math.clamp(n.getInt("Vertical"), 0, 16), dx / length, dz / length, - Double.isFinite(speed) ? Math.clamp(speed, 0.05, 2) : 1, n.getBoolean("Still"), n.getBoolean("Controller"), + Double.isFinite(speed) ? Math.clamp(speed, 0, 2) : 1, n.getBoolean("Still"), n.getBoolean("Controller"), n.hasUUID("Owner") ? n.getUUID("Owner") : new UUID(0, 0), n.getLong("Sequence"), n.getBoolean("ConnectedOnly"), n.getLongArray("WaterMask")); } 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..fa4afb7 --- /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; +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.getInt("Radius"),1,ControlConfig.SENSOR_MAX_RADIUS.get()); + vertical=Math.clamp(n.getInt("Vertical"),0,ControlConfig.SENSOR_MAX_VERTICAL.get()); + mode=Mode.from(n.getInt("Mode"));emptyOnly=n.getBoolean("EmptyOnly");setChanged();sample(); + if(level instanceof ServerLevel server) server.sendBlockUpdated(worldPosition,getBlockState(),getBlockState(),2); + } + @Override protected void saveAdditional(CompoundTag n,HolderLookup.Provider r) { super.saveAdditional(n,r);n.putInt("Radius",radius);n.putInt("Vertical",vertical);n.putInt("Mode",mode.ordinal());n.putBoolean("EmptyOnly",emptyOnly);n.putInt("Output",output); } + @Override protected void loadAdditional(CompoundTag n,HolderLookup.Provider r) { super.loadAdditional(n,r);radius=n.contains("Radius")?n.getInt("Radius"):4;vertical=n.contains("Vertical")?n.getInt("Vertical"):4;mode=Mode.from(n.getInt("Mode"));emptyOnly=n.getBoolean("EmptyOnly");output=Math.clamp(n.getInt("Output"),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(Boat.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 use(Level world, Player p, InteractionHand hand) { - ItemStack stack = p.getItemInHand(hand); - if (world.isClientSide) return InteractionResultHolder.success(stack); - var hit = getPlayerPOVHitResult(world, p, ClipContext.Fluid.ANY); - var n = settings(stack); - int mode = Math.floorMod(n.getInt("Mode"), 3); - int radius = ControlConfig.radius(n.contains("Radius") ? n.getInt("Radius") : ControlConfig.DEFAULT_RADIUS.get()); - if (hit.getType() == HitResult.Type.MISS) { - if (p.isShiftKeyDown()) { mode = (mode + 1) % 3; n.putInt("Mode", mode); message(p, "message.streamcontrol.mode", Component.translatable("mode.streamcontrol." + mode)); } - else { radius = radius % ControlConfig.MAX_RADIUS.get() + 1; n.putInt("Radius", radius); message(p, "message.streamcontrol.radius", radius); } - stack.set(DataComponents.CUSTOM_DATA, CustomData.of(n)); return InteractionResultHolder.success(stack); + @Override public InteractionResult useOn(UseOnContext c) { + var player=c.getPlayer();if(player==null) return InteractionResult.PASS; + var be=c.getLevel().getBlockEntity(c.getClickedPos()); + if(!(be instanceof FlowController) && !(be instanceof UtilityAccess)) return InteractionResult.PASS; + if(c.getLevel().isClientSide) return InteractionResult.SUCCESS; + if(!(player instanceof ServerPlayer server) || !allowed(player,c.getClickedPos(),c.getItemInHand())) return InteractionResult.FAIL; + operate(server,c.getClickedPos(),c.getItemInHand());return InteractionResult.CONSUME; + } + public static boolean operate(ServerPlayer player,BlockPos pos,ItemStack stack) { + var level=player.serverLevel(); + if(!level.hasChunkAt(pos) || !allowed(player,pos,stack)) return false; + var be=level.getBlockEntity(pos);var n=settings(stack);Mode mode=mode(stack); + if(mode==Mode.INSPECT) { + if(be instanceof FlowController controller) ControlNetwork.open(player,controller); + else if(be instanceof UtilityAccess utility) ControlNetwork.openUtility(player,utility); + else inspect(player,pos); + return true; + } + if(mode==Mode.COPY) { + CompoundTag clipboard; + if(be instanceof FlowController controller) { clipboard=controller.controls().clipboard();clipboard.putString("Kind","controller"); } + else if(be instanceof UtilityAccess utility) clipboard=utility.settings(); + else { message(player,"controller_required");return false; } + n.put("Calibration",clipboard);store(stack,n);message(player,"copied");return true; } - BlockPos pos = hit.getBlockPos(); - if (!world.getFluidState(pos).is(FluidTags.WATER)) return InteractionResultHolder.pass(stack); - if (!allowed(p, pos, stack)) { message(p, "message.streamcontrol.denied"); return InteractionResultHolder.fail(stack); } - var server = (ServerLevel)world; var data = ControlData.get(server); - if (mode == 2) { - var old = data.at(pos.getX(), pos.getY(), pos.getZ()); - if (old == null) { message(p, "message.streamcontrol.none"); return InteractionResultHolder.success(stack); } - if (old.controller()) { message(p, "message.streamcontrol.remove_block"); return InteractionResultHolder.fail(stack); } - if (!old.owner().equals(p.getUUID()) && !p.hasPermissions(2)) { message(p, "message.streamcontrol.denied"); return InteractionResultHolder.fail(stack); } - data.remove(server, old.key()); message(p, "message.streamcontrol.restored"); - } else { - long key = pos.asLong() ^ Long.MIN_VALUE; - var existing = data.byKey(key); - if (existing != null && !existing.owner().equals(p.getUUID()) && !p.hasPermissions(2)) { message(p, "message.streamcontrol.denied"); return InteractionResultHolder.fail(stack); } - if (existing == null && data.owned(p.getUUID()) >= ControlConfig.MAX_PLAYER_REGIONS.get()) { message(p, "message.streamcontrol.limit"); return InteractionResultHolder.fail(stack); } - 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.getDouble("Strength") : ControlConfig.SPEED.get()), mode == 1, false, p.getUUID(), data.next()); - if (!data.put(server, region)) { message(p, "message.streamcontrol.limit"); return InteractionResultHolder.fail(stack); } - message(p, "message.streamcontrol.applied", Component.translatable("mode.streamcontrol." + mode), radius); + if(mode==Mode.PASTE) { + var clipboard=n.getCompound("Calibration");String kind=clipboard.getString("Kind"); + if("controller".equals(kind) && be instanceof FlowController controller) controller.controls().paste(clipboard); + else if(be instanceof UtilityAccess utility && utility.settings().getString("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().location().toString());store(stack,n);message(player,"source_selected");return true; + } + BlockPos source=BlockPos.of(n.getLong("LinkPos")); + if(!level.dimension().location().toString().equals(n.getString("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.hasPermissions(2)) { 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.serverLevel();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.displayClientMessage(Component.literal(String.format(Locale.ROOT,"%s | %s | %.0f%%",source,direction,Math.hypot(flow.x,flow.z)*100)),true); + } + @Override public InteractionResultHolder use(Level world,Player player,InteractionHand hand) { + ItemStack stack=player.getItemInHand(hand);if(world.isClientSide) return InteractionResultHolder.success(stack); + 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.getBoolean("HideBounds"));message(player,n.getBoolean("HideBounds")?"bounds_off":"bounds_on"); } + store(stack,n);return InteractionResultHolder.success(stack); } - p.getCooldowns().addCooldown(this, 5); return InteractionResultHolder.success(stack); + if(!(player instanceof ServerPlayer server) || !allowed(player,hit.getBlockPos(),stack)) return InteractionResultHolder.fail(stack); + if(!world.getFluidState(hit.getBlockPos()).is(FluidTags.WATER)) return InteractionResultHolder.pass(stack); + return operate(server,hit.getBlockPos(),stack)?InteractionResultHolder.success(stack):InteractionResultHolder.fail(stack); } - @Override public void appendHoverText(ItemStack stack, TooltipContext context, List lines, TooltipFlag flag) { - var n = settings(stack); lines.add(Component.translatable("tooltip.streamcontrol.mode", Component.translatable("mode.streamcontrol." + Math.floorMod(n.getInt("Mode"), 3)))); - lines.add(Component.translatable("tooltip.streamcontrol.wand")); lines.add(Component.translatable("tooltip.streamcontrol.wand_air")); + @Override public void appendHoverText(ItemStack stack,TooltipContext context,List lines,TooltipFlag flag) { + lines.add(Component.translatable("calibrator.streamcontrol.mode",Component.translatable(mode(stack).key()))); + lines.add(Component.translatable("calibrator.streamcontrol.tooltip")); + lines.add(Component.translatable("calibrator.streamcontrol.tooltip_cycle")); + lines.add(Component.translatable("calibrator.streamcontrol.tooltip_bounds")); + if(settings(stack).contains("Calibration")) lines.add(Component.translatable("calibrator.streamcontrol.clipboard_ready")); + if(settings(stack).contains("LinkPos")) lines.add(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 599c277..bf1f5a9 100644 --- a/src/main/java/nl/townsandkingdoms/streamcontrol/IntegrationChecks.java +++ b/src/main/java/nl/townsandkingdoms/streamcontrol/IntegrationChecks.java @@ -137,6 +137,7 @@ private static void run(ServerStartedEvent event) { results.put("controllerRedstoneAndRemoval",true); check(server.getCommands().getDispatcher().execute("streamcontrol status",server.createCommandSourceStack())==1,"Status command"); 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);data.remove(level,103); } 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..383f3f6 --- /dev/null +++ b/src/main/java/nl/townsandkingdoms/streamcontrol/MooringEntity.java @@ -0,0 +1,82 @@ +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; +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.getInt("Radius"),1,ControlConfig.MOORING_MAX_RADIUS.get());vertical=Math.clamp(n.getInt("Vertical"),0,ControlConfig.MOORING_MAX_VERTICAL.get()); + enabled=n.getBoolean("Enabled");release();changed(); + } + @Override protected void saveAdditional(CompoundTag n,HolderLookup.Provider r) { + super.saveAdditional(n,r);n.putInt("Radius",radius);n.putInt("Vertical",vertical);n.putBoolean("Enabled",enabled); + if(boatId!=null) { n.putUUID("Boat",boatId);n.putDouble("MoorX",moorX);n.putDouble("MoorZ",moorZ); } + } + @Override protected void loadAdditional(CompoundTag n,HolderLookup.Provider r) { + super.loadAdditional(n,r);radius=n.contains("Radius")?n.getInt("Radius"):4;vertical=n.contains("Vertical")?n.getInt("Vertical"):2;enabled=!n.contains("Enabled")||n.getBoolean("Enabled"); + boatId=n.hasUUID("Boat")?n.getUUID("Boat"):null;moorX=n.getDouble("MoorX");moorZ=n.getDouble("MoorZ"); + } + 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(Boat 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 Boat 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.hasImpulse=true; + return; + } + if(Math.floorMod(server.getGameTime()+worldPosition.asLong(),10)!=0) return; + var candidates=server.getEntitiesOfClass(Boat.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(Boat 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 996509e..f7d93f7 100644 --- a/src/main/java/nl/townsandkingdoms/streamcontrol/StreamControl.java +++ b/src/main/java/nl/townsandkingdoms/streamcontrol/StreamControl.java @@ -22,12 +22,22 @@ public final class StreamControl { public static final DeferredRegister TABS = DeferredRegister.create(Registries.CREATIVE_MODE_TAB, ID); public static final DeferredBlock GENERATOR = BLOCKS.register("current_generator", () -> new ControllerBlock(false, BlockBehaviour.Properties.ofFullCopy(Blocks.COPPER_BLOCK).strength(3))); public static final DeferredBlock ANCHOR = BLOCKS.register("stillwater_anchor", () -> new ControllerBlock(true, BlockBehaviour.Properties.ofFullCopy(Blocks.PRISMARINE).strength(3))); + public static final DeferredBlock SENSOR = BLOCKS.register("flow_sensor", () -> new UtilityBlock(true,BlockBehaviour.Properties.ofFullCopy(Blocks.COPPER_BLOCK).strength(3))); + public static final DeferredBlock MOORING = BLOCKS.register("mooring_post", () -> new UtilityBlock(false,BlockBehaviour.Properties.ofFullCopy(Blocks.OAK_PLANKS).strength(3).noOcclusion())); + public static final DeferredItem SENSOR_ITEM = ITEMS.register("flow_sensor",()->new EngineeringBlockItem(SENSOR.get(),new Item.Properties())); + public static final DeferredItem MOORING_ITEM = ITEMS.register("mooring_post",()->new EngineeringBlockItem(MOORING.get(),new Item.Properties())); + public static final java.util.function.Supplier> SENSOR_ENTITY = ENTITIES.register("flow_sensor", () -> BlockEntityType.Builder.of(FlowSensorEntity::new,SENSOR.get()).build(null)); + public static final java.util.function.Supplier> MOORING_ENTITY = ENTITIES.register("mooring_post", () -> BlockEntityType.Builder.of(MooringEntity::new,MOORING.get()).build(null)); + public static final java.util.List> OPTIONAL_ITEMS = new java.util.ArrayList<>(); public static final DeferredItem WAND = ITEMS.register("flow_wand", () -> new FlowWand(new Item.Properties().stacksTo(1))); - public static final DeferredItem GENERATOR_ITEM = ITEMS.registerSimpleBlockItem(GENERATOR); - public static final DeferredItem ANCHOR_ITEM = ITEMS.registerSimpleBlockItem(ANCHOR); + public static final DeferredItem GENERATOR_ITEM = ITEMS.register("current_generator",()->new EngineeringBlockItem(GENERATOR.get(),new Item.Properties())); + public static final DeferredItem ANCHOR_ITEM = ITEMS.register("stillwater_anchor",()->new EngineeringBlockItem(ANCHOR.get(),new Item.Properties())); public static final java.util.function.Supplier> CONTROLLER = ENTITIES.register("controller", () -> BlockEntityType.Builder.of(ControllerEntity::new, GENERATOR.get(), ANCHOR.get()).build(null)); - static { TABS.register("flow_control", () -> CreativeModeTab.builder().title(Component.translatable("itemGroup.streamcontrol")).icon(() -> new ItemStack(WAND.get())).displayItems((params, output) -> { output.accept(WAND.get()); output.accept(GENERATOR.get()); output.accept(ANCHOR.get()); }).build()); } + static { TABS.register("flow_control", () -> CreativeModeTab.builder().title(Component.translatable("itemGroup.streamcontrol")).icon(() -> new ItemStack(WAND.get())).displayItems((params, output) -> { output.accept(WAND.get()); output.accept(GENERATOR.get()); output.accept(ANCHOR.get()); output.accept(SENSOR.get()); output.accept(MOORING.get()); OPTIONAL_ITEMS.forEach(item -> output.accept(item.get())); }).build()); } public StreamControl(IEventBus bus, ModContainer container) { + if (net.neoforged.fml.ModList.get().isLoaded("create")) nl.townsandkingdoms.streamcontrol.compat.create.CreateIntegration.register(); + if (net.neoforged.fml.loading.FMLEnvironment.dist == net.neoforged.api.distmarker.Dist.CLIENT) + nl.townsandkingdoms.streamcontrol.client.EngineeringClient.register(bus); BLOCKS.register(bus); ITEMS.register(bus); ENTITIES.register(bus); TABS.register(bus); container.registerConfig(ModConfig.Type.SERVER, ControlConfig.SPEC); bus.addListener(ControlNetwork::register); @@ -36,5 +46,6 @@ public StreamControl(IEventBus bus, ModContainer container) { NeoForge.EVENT_BUS.addListener(ControlNetwork::respawn); NeoForge.EVENT_BUS.addListener(ControlCommands::register); IntegrationChecks.register(); + ClientTestWorld.register(); } } 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..0e3fb54 --- /dev/null +++ b/src/main/java/nl/townsandkingdoms/streamcontrol/UtilityBlock.java @@ -0,0 +1,67 @@ +package nl.townsandkingdoms.streamcontrol; + +import com.mojang.serialization.MapCodec; +import net.minecraft.core.*; +import net.minecraft.server.level.ServerPlayer; +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,Direction d,BlockState other,LevelAccessor l,BlockPos p,BlockPos q) { + if(s.getValue(WATERLOGGED)) l.scheduleTick(p,Fluids.WATER,Fluids.WATER.getTickDelay(l));return super.updateShape(s,d,other,l,p,q); + } + @Override protected void neighborChanged(BlockState s,Level l,BlockPos p,Block b,BlockPos q,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 ItemInteractionResult 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()) ? ItemInteractionResult.SKIP_DEFAULT_BLOCK_INTERACTION : 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.sidedSuccess(l.isClientSide); + } + @Override protected boolean hasAnalogOutputSignal(BlockState s) { return sensor; } + @Override protected int getAnalogOutputSignal(BlockState s,Level l,BlockPos p) { return l.getBlockEntity(p) instanceof FlowSensorEntity be ? be.output() : 0; } + @Override protected void onRemove(BlockState old,Level l,BlockPos p,BlockState next,boolean moving) { + if(!old.is(next.getBlock())) { + if(l.getBlockEntity(p) instanceof MooringEntity m) m.release(); + if(sensor) l.updateNeighbourForOutputSignal(p,this); + } + super.onRemove(old,l,p,next,moving); + } +} diff --git a/src/main/java/nl/townsandkingdoms/streamcontrol/client/ClientGameplayChecks.java b/src/main/java/nl/townsandkingdoms/streamcontrol/client/ClientGameplayChecks.java new file mode 100644 index 0000000..70259e4 --- /dev/null +++ b/src/main/java/nl/townsandkingdoms/streamcontrol/client/ClientGameplayChecks.java @@ -0,0 +1,147 @@ +package nl.townsandkingdoms.streamcontrol.client; + +import java.nio.file.Files; +import java.util.*; +import java.util.function.BooleanSupplier; +import com.google.gson.GsonBuilder; +import net.minecraft.client.Minecraft; +import net.minecraft.client.Screenshot; +import net.minecraft.client.gui.components.Button; +import net.minecraft.client.gui.components.AbstractSliderButton; +import net.minecraft.client.gui.screens.TitleScreen; +import net.minecraft.client.gui.screens.ConnectScreen; +import net.minecraft.client.multiplayer.ServerData; +import net.minecraft.client.multiplayer.resolver.ServerAddress; +import net.minecraft.core.*; +import net.minecraft.network.chat.Component; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.phys.*; +import net.minecraft.world.entity.vehicle.Boat; +import net.neoforged.api.distmarker.Dist; +import net.neoforged.bus.api.SubscribeEvent; +import net.neoforged.fml.common.EventBusSubscriber; +import net.neoforged.neoforge.client.event.ClientTickEvent; +import nl.townsandkingdoms.streamcontrol.*; +import static nl.townsandkingdoms.streamcontrol.ClientTestWorld.*; + +/** Real client, dedicated server, real input handlers and packets. Explicit CI opt-in only. */ +@EventBusSubscriber(modid="streamcontrol",value=Dist.CLIENT) +public final class ClientGameplayChecks { + private record Step(String name,Runnable action,BooleanSupplier ready) {} + private static final List steps=new ArrayList<>(); + private static final Map results=new LinkedHashMap<>(); + private static int step=-1,ticks,total;private static boolean connecting,done; + private static Minecraft mc() { return Minecraft.getInstance(); } + private static FlowRegion region(BlockPos p) { return FlowOverrides.clientByKey(p.asLong()); } + private static FlowController controller(BlockPos p) { return (FlowController)mc().level.getBlockEntity(p); } + private static UtilityAccess utility(BlockPos p) { return (UtilityAccess)mc().level.getBlockEntity(p); } + private static void add(String n,Runnable a,BooleanSupplier r) { steps.add(new Step(n,a,r)); } + private static void use(BlockPos p) { + mc().gameMode.useItemOn(mc().player,InteractionHand.MAIN_HAND,new BlockHitResult(Vec3.atCenterOf(p).add(0,.5,0),Direction.UP,p,false)); + } + private static void click(String key) { + var label=Component.translatable(key).getString(); + var b=mc().screen.children().stream().filter(c->c instanceof Button button && button.getMessage().getString().equals(label)).map(c->(Button)c).findFirst().orElseThrow(()->new AssertionError("Missing button "+key)); + mc().screen.mouseClicked(b.getX()+b.getWidth()/2.0,b.getY()+b.getHeight()/2.0,0); + } + private static void at(int x,int y) { mc().screen.mouseClicked((mc().screen.width-316)/2.0+x,(mc().screen.height-238)/2.0+y,0); } + private static void slider(int index,double fraction) { + var sliders=mc().screen.children().stream().filter(c->c instanceof AbstractSliderButton).map(c->(AbstractSliderButton)c).toList(); + var s=sliders.get(index);double x=s.getX()+4+fraction*(s.getWidth()-8),y=s.getY()+8; + mc().screen.mouseClicked(x,y,0);mc().screen.mouseReleased(x,y,0); + } + private static void screenshot(String name) { Screenshot.grab(mc().gameDirectory,"gameplay-"+name+".png",mc().getMainRenderTarget(),m->{}); } + private static void command(String name) { mc().player.connection.sendCommand("flowclient "+name); } + private static void cycle(FlowWand.Mode mode) { + add("calibrator "+mode,()->{mc().player.setXRot(-90);mc().options.keyShift.setDown(true);},()->{ + if(ticks==4) mc().gameMode.useItem(mc().player,InteractionHand.MAIN_HAND); + if(ticks==7) mc().options.keyShift.setDown(false); + return ticks>8 && FlowWand.mode(mc().player.getMainHandItem())==mode; + }); + } + private static void setup() { + add("client world and region login sync",()->{},()->mc().level.getBlockEntity(GENERATOR)!=null && region(GENERATOR)!=null && mc().player.getMainHandItem().is(StreamControl.WAND.get())); + add("open controller through block interaction",()->use(GENERATOR),()->mc().screen instanceof ControllerScreen); + add("GUI direction and radius apply round trip",()->{ + screenshot("controller");click("gui.streamcontrol.short_e");slider(0,4.0/15);click("gui.streamcontrol.apply"); + },()->region(GENERATOR)!=null && region(GENERATOR).dx()==1 && region(GENERATOR).radius()==5); + cycle(FlowWand.Mode.COPY); + add("copy settings with real Calibrator item use",()->use(GENERATOR),()->FlowWand.settings(mc().player.getMainHandItem()).contains("Calibration")); + cycle(FlowWand.Mode.PASTE); + add("paste settings through server and sync",()->use(TARGET),()->region(TARGET)!=null && region(TARGET).radius()==5 && region(TARGET).dx()==1); + cycle(FlowWand.Mode.LINK); + add("select linked source",()->use(GENERATOR),()->FlowWand.settings(mc().player.getMainHandItem()).contains("LinkPos")); + add("link target through server and block entity sync",()->use(TARGET),()->GENERATOR.equals(controller(TARGET).controls().source())); + cycle(FlowWand.Mode.RESTORE);cycle(FlowWand.Mode.INSPECT); + add("open linked target",()->use(TARGET),()->mc().screen instanceof ControllerScreen); + add("unlink using GUI",()->{click("engineering.streamcontrol.automation_tab");click("engineering.streamcontrol.unlink");click("gui.streamcontrol.apply");},()->controller(TARGET).controls().source()==null); + add("open automation",()->use(GENERATOR),()->mc().screen instanceof ControllerScreen); + add("edit redstone and Profile B through GUI",()->{ + click("engineering.streamcontrol.automation_tab");for(int i=0;i<4;i++) at(150,66); + click("engineering.streamcontrol.profile_b");slider(0,3.0/16);screenshot("automation");click("gui.streamcontrol.apply"); + },()->controller(GENERATOR).controls().redstone()==ControllerSettings.Redstone.SWITCH && controller(GENERATOR).controls().b().vertical()==3); + add("redstone switches to reversed Profile B",()->command("power"),()->region(GENERATOR)!=null && region(GENERATOR).dz()==1 && region(GENERATOR).vertical()==3); + add("unpowered restores Profile A",()->command("unpower"),()->region(GENERATOR)!=null && region(GENERATOR).dx()==1 && region(GENERATOR).vertical()!=3); + add("open sensor through block interaction",()->use(SENSOR),()->mc().screen instanceof UtilityScreen); + add("sensor GUI independent vertical range and live speed output",()->{ + slider(0,0);slider(1,6.0/16);screenshot("sensor");click("gui.streamcontrol.apply"); + },()->((FlowSensorEntity)utility(SENSOR)).radius()==1 && ((FlowSensorEntity)utility(SENSOR)).vertical()==6 && ((FlowSensorEntity)utility(SENSOR)).output()==15); + add("reopen sensor",()->use(SENSOR),()->mc().screen instanceof UtilityScreen); + add("sensor direction comparator and settings sync",()->{at(150,149);click("gui.streamcontrol.apply");},()->((FlowSensorEntity)utility(SENSOR)).mode()==FlowSensorEntity.Mode.DIRECTION && ((FlowSensorEntity)utility(SENSOR)).output()==3); + add("reopen sensor boat mode",()->use(SENSOR),()->mc().screen instanceof UtilityScreen); + add("sensor boat mode and zero vertical range",()->{at(150,149);slider(0,7.0/15);slider(1,0);click("gui.streamcontrol.apply");},()->((FlowSensorEntity)utility(SENSOR)).mode()==FlowSensorEntity.Mode.BOATS && ((FlowSensorEntity)utility(SENSOR)).output()==1); + add("mooring live boat capture sync",()->command("dock"),()->utility(MOORING).settings().getBoolean("Secured") && mc().player.blockPosition().distSqr(MOORING)<=2); + add("open mooring",()->use(MOORING),()->mc().screen instanceof UtilityScreen); + add("disable mooring from GUI releases boat",()->{screenshot("mooring");at(150,186);click("gui.streamcontrol.apply");},()->!utility(MOORING).settings().getBoolean("Enabled") && !utility(MOORING).settings().getBoolean("Secured")); + add("reopen mooring",()->use(MOORING),()->mc().screen instanceof UtilityScreen); + add("enable mooring recaptures after cooldown",()->{at(150,186);click("gui.streamcontrol.apply");},()->utility(MOORING).settings().getBoolean("Secured")); + add("real player boarding releases mooring",()->{ + mc().player.getInventory().selected=1; + var boat=mc().level.getEntitiesOfClass(Boat.class,new AABB(MOORING).inflate(5)).getFirst(); + System.out.println("FLOW_BOARD_CLIENT boat="+boat.position()+" underwater="+boat.isUnderWater()+" result="+mc().gameMode.interact(mc().player,boat,InteractionHand.MAIN_HAND)); + },()->mc().player.isPassenger() && !utility(MOORING).settings().getBoolean("Secured")); + add("dismount",()->mc().options.keyShift.setDown(true),()->{if(ticks>10)mc().options.keyShift.setDown(false);return ticks>12 && !mc().player.isPassenger();}); + add("stillwater anchor live GUI",()->{mc().player.getInventory().selected=0;use(ANCHOR);},()->mc().screen instanceof ControllerScreen); + add("stillwater GUI release action removes client override",()->{ + screenshot("stillwater");click("engineering.streamcontrol.automation_tab");at(150,122);click("gui.streamcontrol.apply"); + },()->region(ANCHOR)==null); + if(net.neoforged.fml.ModList.get().isLoaded("create")) { + add("Create live kinetic region",()->use(MECHANICAL),()->mc().screen instanceof ControllerScreen && region(MECHANICAL)!=null); + add("Create GUI direction apply",()->{screenshot("mechanical");click("gui.streamcontrol.short_e");click("gui.streamcontrol.apply");},()->region(MECHANICAL)!=null && region(MECHANICAL).dx()==1); + add("Create actual motor reversal reaches client",()->command("reverse"),()->region(MECHANICAL)!=null && region(MECHANICAL).dx()==-1); + add("Create stopped motor clears client override",()->command("stop"),()->region(MECHANICAL)==null); + } + add("render Calibrator HUD and world bounds",()->{ + mc().setScreen(null);command("view");mc().player.setXRot(36);mc().player.setYRot(45); + },()->{if(ticks==200)screenshot("world");return ticks>210;}); + } + @SubscribeEvent public static void tick(ClientTickEvent.Post event) { + if(!Boolean.getBoolean("streamcontrol.clientIntegrationTest") || done) return; + try { + if(++total>12000) throw new AssertionError("Client overall timeout; screen="+mc().screen); + if(!connecting) { + if(!(mc().screen instanceof TitleScreen) || mc().getOverlay()!=null) return; + connecting=true; + ConnectScreen.startConnecting(mc().screen,mc(),ServerAddress.parseString("127.0.0.1:25565"),new ServerData("Flow test","127.0.0.1:25565",ServerData.Type.OTHER),false,null); + return; + } + if(mc().level==null || mc().player==null) return; + if(steps.isEmpty()) setup(); + if(step<0) {step=0;ticks=0;} + var s=steps.get(step); + if(ticks==0) {System.out.println("FLOW_CLIENT_BEGIN "+s.name);s.action.run();} + ticks++; + if(ticks>1 && s.ready.getAsBoolean()) { + results.put(s.name,true);System.out.println("FLOW_CLIENT_PASS "+s.name);step++;ticks=0; + if(step==steps.size()) finish(null); + } else if(ticks>240) throw new AssertionError("Timeout: "+s.name+"; screen="+mc().screen+"; player="+mc().player.position()+"; passenger="+mc().player.isPassenger()+"; crouching="+mc().player.isShiftKeyDown()); + } catch(Throwable t) {t.printStackTrace();finish(t);} + } + private static void finish(Throwable error) { + done=true;mc().options.keyShift.setDown(false);results.put("passed",error==null); + if(error!=null) {results.put("error",error.toString());screenshot("failure");} + try {Files.writeString(mc().gameDirectory.toPath().resolve("streamcontrol-client-results.json"),new GsonBuilder().setPrettyPrinting().create().toJson(results));} + catch(Exception e) {e.printStackTrace();} + System.out.println("STREAMCONTROL_CLIENT_TEST "+results);mc().stop(); + } +} diff --git a/src/main/java/nl/townsandkingdoms/streamcontrol/client/ControllerPreview.java b/src/main/java/nl/townsandkingdoms/streamcontrol/client/ControllerPreview.java index f3b0ff7..84751a1 100644 --- a/src/main/java/nl/townsandkingdoms/streamcontrol/client/ControllerPreview.java +++ b/src/main/java/nl/townsandkingdoms/streamcontrol/client/ControllerPreview.java @@ -3,41 +3,55 @@ 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 nl.townsandkingdoms.streamcontrol.FlowHeading; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.network.chat.Component; +import nl.townsandkingdoms.streamcontrol.*; import net.neoforged.api.distmarker.Dist; import net.neoforged.bus.api.SubscribeEvent; import net.neoforged.fml.common.EventBusSubscriber; import net.neoforged.neoforge.client.event.ClientTickEvent; -import nl.townsandkingdoms.streamcontrol.ControlNetwork; -/** Opt-in CI capture of the actual client screen. Inactive in normal games. */ -@EventBusSubscriber(modid = "streamcontrol", value = Dist.CLIENT) +/** Opt-in CI capture and interaction check. Inactive in normal games. */ +@EventBusSubscriber(modid="streamcontrol",value=Dist.CLIENT) public final class ControllerPreview { - private static int phase, ticks; + private static int phase,ticks; private static boolean captured; - private static final String[] NAMES = {"current-generator", "stillwater-anchor", "redstone-paused"}; + private static final String[] NAMES={"current-generator","stillwater-anchor","redstone-paused","automation","flow-sensor","mooring-post","block-models","mechanical-controller"}; @SubscribeEvent public static void tick(ClientTickEvent.Post event) { - if (!Boolean.getBoolean("streamcontrol.uiPreview")) return; - var mc = Minecraft.getInstance(); - if (phase == 0) { - if (!(mc.screen instanceof TitleScreen) || mc.getOverlay() != null) return; - open(); phase = 1; ticks = 0; + if(!Boolean.getBoolean("streamcontrol.uiPreview")) return; + var mc=Minecraft.getInstance(); + if(phase==0) { + if(!(mc.screen instanceof TitleScreen) || 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(), 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(); + if(++ticks<25) return; + String name="flow-control-"+NAMES[phase-1]+".png"; + if(!captured) { Screenshot.grab(mc.gameDirectory,name,mc.getMainRenderTarget(),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() { - boolean still = phase == 2; - ControllerScreen.open(new ControlNetwork.Open(0, still ? 6 : 4, 4, 0.75, 2, 16, - phase != 0 && phase != 1, still, phase == 3, FlowHeading.NORTH_EAST.id())); + 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(); + 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(); + } } } diff --git a/src/main/java/nl/townsandkingdoms/streamcontrol/client/ControllerScreen.java b/src/main/java/nl/townsandkingdoms/streamcontrol/client/ControllerScreen.java index c5deda1..3fef065 100644 --- a/src/main/java/nl/townsandkingdoms/streamcontrol/client/ControllerScreen.java +++ b/src/main/java/nl/townsandkingdoms/streamcontrol/client/ControllerScreen.java @@ -12,6 +12,9 @@ import net.minecraft.resources.ResourceLocation; import net.neoforged.neoforge.network.PacketDistributor; 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 { @@ -30,6 +33,11 @@ private static ResourceLocation 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,22 +45,58 @@ 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().getBoolean("Kinetic") ? "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.getCompound("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.getCompound("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; + 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.getInt("RedstoneMode")),b->{ + int next=(settings.getInt("RedstoneMode")+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.getInt("MaxVertical"):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; @@ -72,6 +116,7 @@ private static Component tr(String key, Object... args) { addRenderableWidget(button); } } + } var waterToggle = new SlateButton(left + 14, top + 176, 286, 22, tr("connected", tr(connected ? "on" : "off")), b -> { connected = !connected; @@ -95,8 +140,9 @@ private static Component tr(String key, Object... args) { } private void apply() { - PacketDistributor.sendToServer(new ControlNetwork.Configure(snapshot.pos(), radius, speed, - connected, facing.id())); + storeProfile();settings.put("ProfileA",profileA.save());settings.put("ProfileB",profileB.save()); + PacketDistributor.sendToServer(new ControlNetwork.Configure(snapshot.pos(),profileA.radius(),profileA.strength(), + profileA.connectedOnly(),profileA.heading().id(),settings)); onClose(); } @@ -110,16 +156,18 @@ private void apply() { drawPanel(g); Component heading = title.copy().withStyle(style -> style.withFont(MENU_FONT)); centered(g, heading, left + PANEL_W / 2, top + 12, CREAM); - centered(g, tr(snapshot.still() ? "calm_subtitle" : "current_subtitle"), - left + PANEL_W / 2, top + 32, MUTED); + if(automation) { + g.drawString(font,engineering("vertical_range"),left+26,top+141,INK,false); + right(g,Component.literal("+/- "+vertical),left+290,top+141,TEAL); + } else { drawWaterPreview(g); if (snapshot.still()) { - centered(g, tr("calm_area"), left + 158, top + 54, INK); + 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("+/- " + snapshot.vertical()), left + 250, top + 98, INK); - centered(g, tr("zero_flow"), left + 158, top + 129, TEAL); + 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.drawString(font, tr("radius_label"), left + 26, top + 144, INK, false); right(g, tr("blocks", radius), left + 290, top + 144, TEAL); } else { @@ -131,16 +179,19 @@ private void apply() { 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", snapshot.vertical()), left + 236, top + 162, MUTED); + 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.getInt("RedstoneMode")==0; + Component status=settings.contains("LinkedSource")?engineering("linked_source",net.minecraft.core.BlockPos.of(settings.getLong("LinkedSource")).toShortString()): + settings.getBoolean("Kinetic")?engineering("drive_rpm",Math.round(settings.getFloat("RPM"))):tr(paused?"powered_status":"apply_hint"); + centered(g,status,left+PANEL_W/2,top+222,paused?0xFFFFB093:MUTED); super.render(g, mouseX, mouseY, partialTick); } private void drawPanel(GuiGraphics g) { - g.blit(snapshot.still() ? STILL_BACKGROUND : CURRENT_BACKGROUND, left, top, + g.blit(automation ? texture("automation_controller") : snapshot.still() ? STILL_BACKGROUND : CURRENT_BACKGROUND, left, top, 0f, 0f, PANEL_W, PANEL_H, PANEL_W, PANEL_H); } @@ -211,9 +262,8 @@ protected void drawLabel(GuiGraphics g, int color) { @Override public void renderWidget(GuiGraphics 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); 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..62d2f61 --- /dev/null +++ b/src/main/java/nl/townsandkingdoms/streamcontrol/client/EngineeringClient.java @@ -0,0 +1,100 @@ +package nl.townsandkingdoms.streamcontrol.client; + +import java.util.*; +import com.mojang.blaze3d.vertex.*; +import net.minecraft.client.*; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.renderer.*; +import net.minecraft.core.BlockPos; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.ClipContext; +import net.minecraft.world.phys.*; +import net.neoforged.bus.api.IEventBus; +import net.neoforged.fml.ModList; +import net.neoforged.neoforge.client.event.*; +import net.neoforged.neoforge.common.NeoForge; +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(IEventBus bus) { + bus.addListener(EngineeringClient::layers); + if(ModList.get().isLoaded("create")) bus.addListener(nl.townsandkingdoms.streamcontrol.compat.create.CreateClient::renderers); + NeoForge.EVENT_BUS.addListener(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).getBoolean("HideBounds")); + } + private static void layers(RegisterGuiLayersEvent e) { + e.registerAboveAll(ResourceLocation.fromNamespaceAndPath(StreamControl.ID,"calibrator"),(g,delta)->hud(g)); + } + private static void hud(GuiGraphics 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 renderWidget(GuiGraphics 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(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(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..716011b --- /dev/null +++ b/src/main/java/nl/townsandkingdoms/streamcontrol/client/ModelPreviewScreen.java @@ -0,0 +1,35 @@ +package nl.townsandkingdoms.streamcontrol.client; + +import java.util.*; +import net.minecraft.client.gui.GuiGraphics; +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 renderBackground(GuiGraphics g,int x,int y,float t) {} + @Override public void render(GuiGraphics 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(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;iradius=n)); + addRenderableWidget(new RangeSlider(left+26,top+112,264,0,Math.max(0,options.getInt("MaxVertical")),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); + PacketDistributor.sendToServer(new ControlNetwork.UtilityConfigure(snapshot.pos(),options));onClose(); + } + @Override public boolean isPauseScreen() { return false; } + @Override public void renderBackground(GuiGraphics g,int x,int y,float t) {} + @Override public void render(GuiGraphics g,int mx,int my,float tick) { + g.fill(0,0,width,height,0xA0141C2E); + g.blit(texture("automation_controller"),left,top,0f,0f,316,238,316,238); + var menu=net.minecraft.resources.ResourceLocation.fromNamespaceAndPath("streamcontrol","menu"); + text(g,title.copy().withStyle(s->s.withFont(menu)),left+158,top+12,TEXT); + text(g,tr(sensor?"sensor_subtitle":"mooring_subtitle"),left+158,top+32,MUTED); + g.drawString(font,tr("horizontal_range"),left+26,top+57,TEXT,false); + right(g,tr("blocks",radius),left+290,top+57); + g.drawString(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.getInt("Output")): + tr(options.getBoolean("Powered")?"mooring_powered":options.getBoolean("Secured")?"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.getBoolean("Powered")?"mooring_powered":live.getBoolean("Secured")?"mooring_secured":"mooring_waiting"); + } + text(g,status,left+158,top+222,TEAL);super.render(g,mx,my,tick); + } + private void right(GuiGraphics g,Component s,int x,int y) { g.drawString(font,s,x-font.width(s),y,TEAL,false); } +} diff --git a/src/main/java/nl/townsandkingdoms/streamcontrol/compat/create/CreateChecks.java b/src/main/java/nl/townsandkingdoms/streamcontrol/compat/create/CreateChecks.java new file mode 100644 index 0000000..0a7f3bf --- /dev/null +++ b/src/main/java/nl/townsandkingdoms/streamcontrol/compat/create/CreateChecks.java @@ -0,0 +1,37 @@ +package nl.townsandkingdoms.streamcontrol.compat.create; + +import java.util.Map; +import com.simibubi.create.AllBlocks; +import com.simibubi.create.content.kinetics.motor.CreativeMotorBlockEntity; +import net.minecraft.core.*; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.state.properties.BlockStateProperties; +import nl.townsandkingdoms.streamcontrol.*; + +/** Opt-in checks against the actual Create 6.0.10 kinetic network. */ +public final class CreateChecks { + private static void check(boolean b,String why) { if(!b) throw new AssertionError(why); } + public static void run(ServerLevel level,Map results) { + var pos=new BlockPos(400,120,400);var motorPos=pos.below(); + level.setBlock(motorPos,AllBlocks.CREATIVE_MOTOR.getDefaultState().setValue(BlockStateProperties.FACING,Direction.UP),3); + var motor=(CreativeMotorBlockEntity)level.getBlockEntity(motorPos);motor.initialize(); + level.setBlock(pos,CreateIntegration.BLOCK.get().defaultBlockState(),3);var be=(MechanicalCurrentEntity)level.getBlockEntity(pos);be.initialize(); + be.configure(FlowHeading.EAST,4,1,false); + for(var direction:Direction.values()) check(CreateIntegration.BLOCK.get().hasShaftTowards(level,pos,be.getBlockState(),direction)==(direction==Direction.DOWN),"Unexpected kinetic connection: "+direction); + motor.generatedSpeed.setValue(128);motor.updateGeneratedRotation();for(int i=0;i<10;i++) { motor.tick();be.tick(); } + check(Math.abs(be.getSpeed())==128 && be.hasNetwork(),"Mechanical controller did not join the motor network: "+be.getSpeed());be.refresh(); + var region=ControlData.get(level).byKey(pos.asLong());check(region!=null && region.strength()==1,"128 RPM did not produce configured strength"); + double originalX=region.dx(); + motor.generatedSpeed.setValue(1);motor.updateGeneratedRotation();for(int i=0;i<10;i++) { motor.tick();be.tick(); }be.refresh(); + var slow=ControlData.get(level).byKey(pos.asLong());check(slow!=null && Math.abs(slow.strength()-1.0/128)<1e-8,"Low RPM was clamped to manual minimum strength"); + var loaded=FlowRegion.load(slow.save());check(loaded.strength()==slow.strength(),"Sub-5-percent flow did not survive sync/persistence"); + motor.generatedSpeed.setValue(-128);motor.updateGeneratedRotation();for(int i=0;i<10;i++) { motor.tick();be.tick(); }be.refresh(); + region=ControlData.get(level).byKey(pos.asLong());check(region!=null && region.dx()==-originalX,"Motor reversal did not reverse flow"); + check(be.calculateStressApplied()==ControlConfig.KINETIC_STRESS.get().floatValue(),"Kinetic stress consumer value"); + be.updateFromNetwork(0,512,2);be.refresh();check(ControlData.get(level).byKey(pos.asLong())==null,"Overstressed drive left an active flow override"); + motor.generatedSpeed.setValue(0);motor.updateGeneratedRotation();for(int i=0;i<10;i++) { motor.tick();be.tick(); }be.refresh(); + check(ControlData.get(level).byKey(pos.asLong())==null,"Stopped motor left an active override"); + level.removeBlock(pos,false);level.removeBlock(motorPos,false);results.put("createRealNetworkRpmReverseStressAndStop",true); + } +} diff --git a/src/main/java/nl/townsandkingdoms/streamcontrol/compat/create/CreateClient.java b/src/main/java/nl/townsandkingdoms/streamcontrol/compat/create/CreateClient.java new file mode 100644 index 0000000..c6bae86 --- /dev/null +++ b/src/main/java/nl/townsandkingdoms/streamcontrol/compat/create/CreateClient.java @@ -0,0 +1,19 @@ +package nl.townsandkingdoms.streamcontrol.compat.create; + +import com.mojang.blaze3d.vertex.PoseStack; +import com.simibubi.create.content.kinetics.base.KineticBlockEntityRenderer; +import net.minecraft.client.renderer.*; +import net.minecraft.client.renderer.blockentity.BlockEntityRendererProvider; +import net.minecraft.core.Direction; +import net.neoforged.neoforge.client.event.EntityRenderersEvent; + +public final class CreateClient { + public static void renderers(EntityRenderersEvent.RegisterRenderers event) { event.registerBlockEntityRenderer(CreateIntegration.ENTITY.get(),ShaftRenderer::new); } + private static final class ShaftRenderer extends KineticBlockEntityRenderer { + ShaftRenderer(BlockEntityRendererProvider.Context context) { super(context); } + @Override protected void renderSafe(MechanicalCurrentEntity be,float partial,PoseStack pose,MultiBufferSource buffers,int light,int overlay) { + // Always render this un-instanced consumer's shaft, including when Flywheel is active. + renderRotatingKineticBlock(be,shaft(Direction.Axis.Y),pose,buffers.getBuffer(RenderType.solid()),light); + } + } +} diff --git a/src/main/java/nl/townsandkingdoms/streamcontrol/compat/create/CreateClientFixture.java b/src/main/java/nl/townsandkingdoms/streamcontrol/compat/create/CreateClientFixture.java new file mode 100644 index 0000000..71ab424 --- /dev/null +++ b/src/main/java/nl/townsandkingdoms/streamcontrol/compat/create/CreateClientFixture.java @@ -0,0 +1,22 @@ +package nl.townsandkingdoms.streamcontrol.compat.create; + +import com.simibubi.create.AllBlocks; +import com.simibubi.create.content.kinetics.motor.CreativeMotorBlockEntity; +import net.minecraft.core.Direction; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.level.block.state.properties.BlockStateProperties; +import nl.townsandkingdoms.streamcontrol.ClientTestWorld; + +/** Loaded only by the opt-in test fixture with Create present. */ +public final class CreateClientFixture { + public static void prepare(ServerLevel level) { + var p=ClientTestWorld.MECHANICAL; + level.setBlock(p.below(),AllBlocks.CREATIVE_MOTOR.getDefaultState().setValue(BlockStateProperties.FACING,Direction.UP),3); + level.setBlock(p,CreateIntegration.BLOCK.get().defaultBlockState(),3); + speed(level,128); + } + public static void speed(ServerLevel level,int rpm) { + var motor=(CreativeMotorBlockEntity)level.getBlockEntity(ClientTestWorld.MECHANICAL.below()); + motor.generatedSpeed.setValue(rpm);motor.updateGeneratedRotation(); + } +} diff --git a/src/main/java/nl/townsandkingdoms/streamcontrol/compat/create/CreateIntegration.java b/src/main/java/nl/townsandkingdoms/streamcontrol/compat/create/CreateIntegration.java new file mode 100644 index 0000000..0580fef --- /dev/null +++ b/src/main/java/nl/townsandkingdoms/streamcontrol/compat/create/CreateIntegration.java @@ -0,0 +1,22 @@ +package nl.townsandkingdoms.streamcontrol.compat.create; + +import java.util.function.Supplier; +import net.minecraft.world.item.BlockItem; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.entity.BlockEntityType; +import net.minecraft.world.level.block.state.BlockBehaviour; +import net.neoforged.neoforge.registries.*; +import nl.townsandkingdoms.streamcontrol.StreamControl; + +/** Loaded only after ModList confirms Create is installed. */ +public final class CreateIntegration { + public static DeferredBlock BLOCK; + public static DeferredItem ITEM; + public static Supplier> ENTITY; + public static void register() { + BLOCK=StreamControl.BLOCKS.register("mechanical_current_generator",()->new MechanicalCurrentBlock(BlockBehaviour.Properties.ofFullCopy(Blocks.COPPER_BLOCK).strength(3).noOcclusion())); + ITEM=StreamControl.ITEMS.register("mechanical_current_generator",()->new nl.townsandkingdoms.streamcontrol.EngineeringBlockItem(BLOCK.get(),new net.minecraft.world.item.Item.Properties())); + ENTITY=StreamControl.ENTITIES.register("mechanical_current_generator",()->BlockEntityType.Builder.of(MechanicalCurrentEntity::new,BLOCK.get()).build(null)); + StreamControl.OPTIONAL_ITEMS.add(BLOCK); + } +} diff --git a/src/main/java/nl/townsandkingdoms/streamcontrol/compat/create/MechanicalCurrentBlock.java b/src/main/java/nl/townsandkingdoms/streamcontrol/compat/create/MechanicalCurrentBlock.java new file mode 100644 index 0000000..2ce8fe6 --- /dev/null +++ b/src/main/java/nl/townsandkingdoms/streamcontrol/compat/create/MechanicalCurrentBlock.java @@ -0,0 +1,56 @@ +package nl.townsandkingdoms.streamcontrol.compat.create; + +import com.mojang.serialization.MapCodec; +import com.simibubi.create.content.kinetics.base.KineticBlock; +import com.simibubi.create.foundation.block.IBE; +import net.minecraft.core.*; +import net.minecraft.server.level.*; +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.BlockEntityType; +import net.minecraft.world.level.block.state.*; +import net.minecraft.world.level.material.*; +import net.minecraft.world.phys.BlockHitResult; +import nl.townsandkingdoms.streamcontrol.*; +import static nl.townsandkingdoms.streamcontrol.ControllerBlock.*; + +/** A real kinetic consumer with a bottom shaft, not an adjacent-block RPM probe. */ +public final class MechanicalCurrentBlock extends KineticBlock implements IBE, SimpleWaterloggedBlock { + private static final MapCodec CODEC=simpleCodec(MechanicalCurrentBlock::new); + public MechanicalCurrentBlock(Properties p) { super(p);registerDefaultState(stateDefinition.any().setValue(FACING,Direction.NORTH).setValue(DIAGONAL,false).setValue(POWERED,false).setValue(WATERLOGGED,false)); } + @Override protected MapCodec codec() { return CODEC; } + @Override protected void createBlockStateDefinition(StateDefinition.Builder b) { b.add(FACING,DIAGONAL,POWERED,WATERLOGGED); } + @Override public BlockState getStateForPlacement(BlockPlaceContext c) { return defaultBlockState().setValue(FACING,c.getHorizontalDirection()).setValue(WATERLOGGED,c.getLevel().getFluidState(c.getClickedPos()).is(net.minecraft.tags.FluidTags.WATER)).setValue(POWERED,c.getLevel().hasNeighborSignal(c.getClickedPos())); } + @Override public Class getBlockEntityClass() { return MechanicalCurrentEntity.class; } + @Override public BlockEntityType getBlockEntityType() { return CreateIntegration.ENTITY.get(); } + @Override public Direction.Axis getRotationAxis(BlockState state) { return Direction.Axis.Y; } + @Override public boolean hasShaftTowards(LevelReader l,BlockPos p,BlockState s,Direction face) { return face==Direction.DOWN; } + @Override protected FluidState getFluidState(BlockState s) { return s.getValue(WATERLOGGED)?Fluids.WATER.getSource(false):super.getFluidState(s); } + @Override public BlockState updateShape(BlockState s,Direction d,BlockState n,LevelAccessor l,BlockPos p,BlockPos q) { + if(s.getValue(WATERLOGGED)) l.scheduleTick(p,Fluids.WATER,Fluids.WATER.getTickDelay(l));return super.updateShape(s,d,n,l,p,q); + } + @Override public void neighborChanged(BlockState s,Level l,BlockPos p,Block b,BlockPos q,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 FlowController controller) controller.waterChanged(); + } + } + @Override protected ItemInteractionResult 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()) ? ItemInteractionResult.SKIP_DEFAULT_BLOCK_INTERACTION : 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 FlowController controller) ControlNetwork.open(server,controller); + return InteractionResult.sidedSuccess(l.isClientSide); + } + @Override public void onRemove(BlockState old,Level l,BlockPos p,BlockState next,boolean moving) { + if(!old.is(next.getBlock()) && l instanceof ServerLevel server) ControlData.get(server).remove(server,p.asLong()); + super.onRemove(old,l,p,next,moving); + } + @Override public BlockState rotate(BlockState s,Rotation r) { return FlowHeading.fromState(s).rotate(r).apply(s); } + @Override public BlockState mirror(BlockState s,Mirror m) { return FlowHeading.fromState(s).mirror(m).apply(s); } +} diff --git a/src/main/java/nl/townsandkingdoms/streamcontrol/compat/create/MechanicalCurrentEntity.java b/src/main/java/nl/townsandkingdoms/streamcontrol/compat/create/MechanicalCurrentEntity.java new file mode 100644 index 0000000..d2ce921 --- /dev/null +++ b/src/main/java/nl/townsandkingdoms/streamcontrol/compat/create/MechanicalCurrentEntity.java @@ -0,0 +1,21 @@ +package nl.townsandkingdoms.streamcontrol.compat.create; + +import com.simibubi.create.content.kinetics.base.KineticBlockEntity; +import net.minecraft.core.*; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.state.BlockState; +import nl.townsandkingdoms.streamcontrol.*; + +public final class MechanicalCurrentEntity extends KineticBlockEntity implements FlowController { + private final ControllerSettings controls=new ControllerSettings(this); + public MechanicalCurrentEntity(BlockPos p,BlockState s) { super(CreateIntegration.ENTITY.get(),p,s); } + @Override public BlockEntity blockEntity() { return this; } + @Override public ControllerSettings controls() { return controls; } + @Override public float rpm() { return getSpeed(); } + @Override public void tick() { super.tick();if(level!=null && !level.isClientSide) controls.tick(); } + @Override public void onSpeedChanged(float previous) { super.onSpeedChanged(previous);if(controls!=null) controls.refresh(); } + @Override public float calculateStressApplied() { return lastStressApplied=ControlConfig.KINETIC_STRESS.get().floatValue(); } + @Override protected void write(CompoundTag n,HolderLookup.Provider r,boolean packet) { super.write(n,r,packet);n.put("FlowControl",controls.save()); } + @Override protected void read(CompoundTag n,HolderLookup.Provider r,boolean packet) { super.read(n,r,packet);if(n.contains("FlowControl")) controls.load(n.getCompound("FlowControl")); } +} diff --git a/src/main/resources/META-INF/neoforge.mods.toml b/src/main/resources/META-INF/neoforge.mods.toml index 5a80b2c..41c596b 100644 --- a/src/main/resources/META-INF/neoforge.mods.toml +++ b/src/main/resources/META-INF/neoforge.mods.toml @@ -4,11 +4,11 @@ license="GPL-3.0-only" issueTrackerURL="https://github.com/Mikaaah/Streams-Flow-Control/issues" [[mods]] modId="streamcontrol" -version="0.1.7" +version="0.2.2" displayName="Streams Flow Control" authors="Mikaaah / Towns & Kingdoms" displayURL="https://github.com/Mikaaah/Streams-Flow-Control" -description="Shape water currents with a Flow Wand, Current Generator and Stillwater Anchor. Persistent, height-bounded controls for Streams Reflowing." +description="Calibrate currents, automate controller profiles, monitor water and secure boats. Optional Create-powered current generation." [[mixins]] config="streamcontrol.mixins.json" [[dependencies.streamcontrol]] @@ -29,3 +29,10 @@ type="required" versionRange="[2.16.2,2.16.3)" ordering="AFTER" side="BOTH" + +[[dependencies.streamcontrol]] +modId="create" +type="optional" +versionRange="[6.0.10,6.1)" +ordering="AFTER" +side="BOTH" 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/mechanical_current_generator.json b/src/main/resources/assets/streamcontrol/blockstates/mechanical_current_generator.json new file mode 100644 index 0000000..1f4798c --- /dev/null +++ b/src/main/resources/assets/streamcontrol/blockstates/mechanical_current_generator.json @@ -0,0 +1,36 @@ +{ + "variants": { + "facing=north,diagonal=false": { + "model": "streamcontrol:block/mechanical_current_generator", + "y": 270 + }, + "facing=north,diagonal=true": { + "model": "streamcontrol:block/mechanical_current_generator_diagonal", + "y": 270 + }, + "facing=east,diagonal=false": { + "model": "streamcontrol:block/mechanical_current_generator", + "y": 0 + }, + "facing=east,diagonal=true": { + "model": "streamcontrol:block/mechanical_current_generator_diagonal", + "y": 0 + }, + "facing=south,diagonal=false": { + "model": "streamcontrol:block/mechanical_current_generator", + "y": 90 + }, + "facing=south,diagonal=true": { + "model": "streamcontrol:block/mechanical_current_generator_diagonal", + "y": 90 + }, + "facing=west,diagonal=false": { + "model": "streamcontrol:block/mechanical_current_generator", + "y": 180 + }, + "facing=west,diagonal=true": { + "model": "streamcontrol:block/mechanical_current_generator_diagonal", + "y": 180 + } + } +} 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/lang/en_us.json b/src/main/resources/assets/streamcontrol/lang/en_us.json index 0056679..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", @@ -73,5 +73,85 @@ "gui.streamcontrol.short_nw": "NW", "gui.streamcontrol.calm_area": "CALM WATER", "gui.streamcontrol.diameter_label": "DIAMETER", - "gui.streamcontrol.height_label": "HEIGHT" + "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/mechanical_current_generator.json b/src/main/resources/assets/streamcontrol/models/block/mechanical_current_generator.json new file mode 100644 index 0000000..9e62e4f --- /dev/null +++ b/src/main/resources/assets/streamcontrol/models/block/mechanical_current_generator.json @@ -0,0 +1,419 @@ +{ + "parent": "minecraft:block/block", + "textures": { + "lid": "streamcontrol:block/current_generator_side", + "top": "streamcontrol:block/current_generator_top", + "casing": "create:block/andesite_casing", + "particle": "create:block/andesite_casing" + }, + "elements": [ + { + "from": [ + 0, + 2, + 0 + ], + "to": [ + 16, + 12, + 16 + ], + "faces": { + "north": { + "texture": "#casing", + "uv": [ + 0, + 4, + 16, + 14 + ] + }, + "south": { + "texture": "#casing", + "uv": [ + 0, + 4, + 16, + 14 + ] + }, + "west": { + "texture": "#casing", + "uv": [ + 0, + 4, + 16, + 14 + ] + }, + "east": { + "texture": "#casing", + "uv": [ + 0, + 4, + 16, + 14 + ] + }, + "up": { + "texture": "#casing", + "uv": [ + 0, + 0, + 16, + 16 + ] + }, + "down": { + "texture": "#casing", + "uv": [ + 0, + 0, + 16, + 16 + ] + } + } + }, + { + "from": [ + 0, + 0, + 0 + ], + "to": [ + 16, + 2, + 5 + ], + "faces": { + "north": { + "texture": "#casing", + "uv": [ + 0, + 14, + 16, + 16 + ] + }, + "south": { + "texture": "#casing", + "uv": [ + 0, + 14, + 16, + 16 + ] + }, + "west": { + "texture": "#casing", + "uv": [ + 0, + 14, + 5, + 16 + ] + }, + "east": { + "texture": "#casing", + "uv": [ + 11, + 14, + 16, + 16 + ] + }, + "up": { + "texture": "#casing", + "uv": [ + 0, + 0, + 16, + 5 + ] + }, + "down": { + "texture": "#casing", + "uv": [ + 0, + 11, + 16, + 16 + ] + } + } + }, + { + "from": [ + 0, + 0, + 11 + ], + "to": [ + 16, + 2, + 16 + ], + "faces": { + "north": { + "texture": "#casing", + "uv": [ + 0, + 14, + 16, + 16 + ] + }, + "south": { + "texture": "#casing", + "uv": [ + 0, + 14, + 16, + 16 + ] + }, + "west": { + "texture": "#casing", + "uv": [ + 11, + 14, + 16, + 16 + ] + }, + "east": { + "texture": "#casing", + "uv": [ + 0, + 14, + 5, + 16 + ] + }, + "up": { + "texture": "#casing", + "uv": [ + 0, + 11, + 16, + 16 + ] + }, + "down": { + "texture": "#casing", + "uv": [ + 0, + 0, + 16, + 5 + ] + } + } + }, + { + "from": [ + 0, + 0, + 5 + ], + "to": [ + 5, + 2, + 11 + ], + "faces": { + "north": { + "texture": "#casing", + "uv": [ + 11, + 14, + 16, + 16 + ] + }, + "south": { + "texture": "#casing", + "uv": [ + 0, + 14, + 5, + 16 + ] + }, + "west": { + "texture": "#casing", + "uv": [ + 5, + 14, + 11, + 16 + ] + }, + "east": { + "texture": "#casing", + "uv": [ + 5, + 14, + 11, + 16 + ] + }, + "up": { + "texture": "#casing", + "uv": [ + 0, + 5, + 5, + 11 + ] + }, + "down": { + "texture": "#casing", + "uv": [ + 0, + 5, + 5, + 11 + ] + } + } + }, + { + "from": [ + 11, + 0, + 5 + ], + "to": [ + 16, + 2, + 11 + ], + "faces": { + "north": { + "texture": "#casing", + "uv": [ + 0, + 14, + 5, + 16 + ] + }, + "south": { + "texture": "#casing", + "uv": [ + 11, + 14, + 16, + 16 + ] + }, + "west": { + "texture": "#casing", + "uv": [ + 5, + 14, + 11, + 16 + ] + }, + "east": { + "texture": "#casing", + "uv": [ + 5, + 14, + 11, + 16 + ] + }, + "up": { + "texture": "#casing", + "uv": [ + 11, + 5, + 16, + 11 + ] + }, + "down": { + "texture": "#casing", + "uv": [ + 11, + 5, + 16, + 11 + ] + } + } + }, + { + "from": [ + 0, + 12, + 0 + ], + "to": [ + 16, + 16, + 16 + ], + "faces": { + "north": { + "texture": "#lid", + "uv": [ + 0, + 0, + 16, + 4 + ] + }, + "south": { + "texture": "#lid", + "uv": [ + 0, + 0, + 16, + 4 + ] + }, + "west": { + "texture": "#lid", + "uv": [ + 0, + 0, + 16, + 4 + ] + }, + "east": { + "texture": "#lid", + "uv": [ + 0, + 0, + 16, + 4 + ] + }, + "up": { + "texture": "#top", + "uv": [ + 0, + 0, + 16, + 16 + ] + }, + "down": { + "texture": "#casing", + "uv": [ + 0, + 0, + 16, + 16 + ] + } + } + } + ] +} diff --git a/src/main/resources/assets/streamcontrol/models/block/mechanical_current_generator_diagonal.json b/src/main/resources/assets/streamcontrol/models/block/mechanical_current_generator_diagonal.json new file mode 100644 index 0000000..a3465b6 --- /dev/null +++ b/src/main/resources/assets/streamcontrol/models/block/mechanical_current_generator_diagonal.json @@ -0,0 +1,419 @@ +{ + "parent": "minecraft:block/block", + "textures": { + "lid": "streamcontrol:block/current_generator_side", + "top": "streamcontrol:block/current_generator_top_diagonal", + "casing": "create:block/andesite_casing", + "particle": "create:block/andesite_casing" + }, + "elements": [ + { + "from": [ + 0, + 2, + 0 + ], + "to": [ + 16, + 12, + 16 + ], + "faces": { + "north": { + "texture": "#casing", + "uv": [ + 0, + 4, + 16, + 14 + ] + }, + "south": { + "texture": "#casing", + "uv": [ + 0, + 4, + 16, + 14 + ] + }, + "west": { + "texture": "#casing", + "uv": [ + 0, + 4, + 16, + 14 + ] + }, + "east": { + "texture": "#casing", + "uv": [ + 0, + 4, + 16, + 14 + ] + }, + "up": { + "texture": "#casing", + "uv": [ + 0, + 0, + 16, + 16 + ] + }, + "down": { + "texture": "#casing", + "uv": [ + 0, + 0, + 16, + 16 + ] + } + } + }, + { + "from": [ + 0, + 0, + 0 + ], + "to": [ + 16, + 2, + 5 + ], + "faces": { + "north": { + "texture": "#casing", + "uv": [ + 0, + 14, + 16, + 16 + ] + }, + "south": { + "texture": "#casing", + "uv": [ + 0, + 14, + 16, + 16 + ] + }, + "west": { + "texture": "#casing", + "uv": [ + 0, + 14, + 5, + 16 + ] + }, + "east": { + "texture": "#casing", + "uv": [ + 11, + 14, + 16, + 16 + ] + }, + "up": { + "texture": "#casing", + "uv": [ + 0, + 0, + 16, + 5 + ] + }, + "down": { + "texture": "#casing", + "uv": [ + 0, + 11, + 16, + 16 + ] + } + } + }, + { + "from": [ + 0, + 0, + 11 + ], + "to": [ + 16, + 2, + 16 + ], + "faces": { + "north": { + "texture": "#casing", + "uv": [ + 0, + 14, + 16, + 16 + ] + }, + "south": { + "texture": "#casing", + "uv": [ + 0, + 14, + 16, + 16 + ] + }, + "west": { + "texture": "#casing", + "uv": [ + 11, + 14, + 16, + 16 + ] + }, + "east": { + "texture": "#casing", + "uv": [ + 0, + 14, + 5, + 16 + ] + }, + "up": { + "texture": "#casing", + "uv": [ + 0, + 11, + 16, + 16 + ] + }, + "down": { + "texture": "#casing", + "uv": [ + 0, + 0, + 16, + 5 + ] + } + } + }, + { + "from": [ + 0, + 0, + 5 + ], + "to": [ + 5, + 2, + 11 + ], + "faces": { + "north": { + "texture": "#casing", + "uv": [ + 11, + 14, + 16, + 16 + ] + }, + "south": { + "texture": "#casing", + "uv": [ + 0, + 14, + 5, + 16 + ] + }, + "west": { + "texture": "#casing", + "uv": [ + 5, + 14, + 11, + 16 + ] + }, + "east": { + "texture": "#casing", + "uv": [ + 5, + 14, + 11, + 16 + ] + }, + "up": { + "texture": "#casing", + "uv": [ + 0, + 5, + 5, + 11 + ] + }, + "down": { + "texture": "#casing", + "uv": [ + 0, + 5, + 5, + 11 + ] + } + } + }, + { + "from": [ + 11, + 0, + 5 + ], + "to": [ + 16, + 2, + 11 + ], + "faces": { + "north": { + "texture": "#casing", + "uv": [ + 0, + 14, + 5, + 16 + ] + }, + "south": { + "texture": "#casing", + "uv": [ + 11, + 14, + 16, + 16 + ] + }, + "west": { + "texture": "#casing", + "uv": [ + 5, + 14, + 11, + 16 + ] + }, + "east": { + "texture": "#casing", + "uv": [ + 5, + 14, + 11, + 16 + ] + }, + "up": { + "texture": "#casing", + "uv": [ + 11, + 5, + 16, + 11 + ] + }, + "down": { + "texture": "#casing", + "uv": [ + 11, + 5, + 16, + 11 + ] + } + } + }, + { + "from": [ + 0, + 12, + 0 + ], + "to": [ + 16, + 16, + 16 + ], + "faces": { + "north": { + "texture": "#lid", + "uv": [ + 0, + 0, + 16, + 4 + ] + }, + "south": { + "texture": "#lid", + "uv": [ + 0, + 0, + 16, + 4 + ] + }, + "west": { + "texture": "#lid", + "uv": [ + 0, + 0, + 16, + 4 + ] + }, + "east": { + "texture": "#lid", + "uv": [ + 0, + 0, + 16, + 4 + ] + }, + "up": { + "texture": "#top", + "uv": [ + 0, + 0, + 16, + 16 + ] + }, + "down": { + "texture": "#casing", + "uv": [ + 0, + 0, + 16, + 16 + ] + } + } + } + ] +} 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/mechanical_current_generator.json b/src/main/resources/assets/streamcontrol/models/item/mechanical_current_generator.json new file mode 100644 index 0000000..c8fa4da --- /dev/null +++ b/src/main/resources/assets/streamcontrol/models/item/mechanical_current_generator.json @@ -0,0 +1,489 @@ +{ + "parent": "minecraft:block/block", + "textures": { + "lid": "streamcontrol:block/current_generator_side", + "top": "streamcontrol:block/current_generator_top", + "casing": "create:block/andesite_casing", + "particle": "create:block/andesite_casing", + "shaft": "create:block/axis", + "shaft_end": "create:block/axis_top" + }, + "elements": [ + { + "from": [ + 6, + 0, + 6 + ], + "to": [ + 10, + 2, + 10 + ], + "faces": { + "north": { + "texture": "#shaft", + "uv": [ + 6, + 14, + 10, + 16 + ] + }, + "south": { + "texture": "#shaft", + "uv": [ + 6, + 14, + 10, + 16 + ] + }, + "west": { + "texture": "#shaft", + "uv": [ + 6, + 14, + 10, + 16 + ] + }, + "east": { + "texture": "#shaft", + "uv": [ + 6, + 14, + 10, + 16 + ] + }, + "up": { + "texture": "#shaft_end", + "uv": [ + 6, + 6, + 10, + 10 + ] + }, + "down": { + "texture": "#shaft_end", + "uv": [ + 6, + 6, + 10, + 10 + ] + } + } + }, + { + "from": [ + 0, + 2, + 0 + ], + "to": [ + 16, + 12, + 16 + ], + "faces": { + "north": { + "texture": "#casing", + "uv": [ + 0, + 4, + 16, + 14 + ] + }, + "south": { + "texture": "#casing", + "uv": [ + 0, + 4, + 16, + 14 + ] + }, + "west": { + "texture": "#casing", + "uv": [ + 0, + 4, + 16, + 14 + ] + }, + "east": { + "texture": "#casing", + "uv": [ + 0, + 4, + 16, + 14 + ] + }, + "up": { + "texture": "#casing", + "uv": [ + 0, + 0, + 16, + 16 + ] + }, + "down": { + "texture": "#casing", + "uv": [ + 0, + 0, + 16, + 16 + ] + } + } + }, + { + "from": [ + 0, + 0, + 0 + ], + "to": [ + 16, + 2, + 5 + ], + "faces": { + "north": { + "texture": "#casing", + "uv": [ + 0, + 14, + 16, + 16 + ] + }, + "south": { + "texture": "#casing", + "uv": [ + 0, + 14, + 16, + 16 + ] + }, + "west": { + "texture": "#casing", + "uv": [ + 0, + 14, + 5, + 16 + ] + }, + "east": { + "texture": "#casing", + "uv": [ + 11, + 14, + 16, + 16 + ] + }, + "up": { + "texture": "#casing", + "uv": [ + 0, + 0, + 16, + 5 + ] + }, + "down": { + "texture": "#casing", + "uv": [ + 0, + 11, + 16, + 16 + ] + } + } + }, + { + "from": [ + 0, + 0, + 11 + ], + "to": [ + 16, + 2, + 16 + ], + "faces": { + "north": { + "texture": "#casing", + "uv": [ + 0, + 14, + 16, + 16 + ] + }, + "south": { + "texture": "#casing", + "uv": [ + 0, + 14, + 16, + 16 + ] + }, + "west": { + "texture": "#casing", + "uv": [ + 11, + 14, + 16, + 16 + ] + }, + "east": { + "texture": "#casing", + "uv": [ + 0, + 14, + 5, + 16 + ] + }, + "up": { + "texture": "#casing", + "uv": [ + 0, + 11, + 16, + 16 + ] + }, + "down": { + "texture": "#casing", + "uv": [ + 0, + 0, + 16, + 5 + ] + } + } + }, + { + "from": [ + 0, + 0, + 5 + ], + "to": [ + 5, + 2, + 11 + ], + "faces": { + "north": { + "texture": "#casing", + "uv": [ + 11, + 14, + 16, + 16 + ] + }, + "south": { + "texture": "#casing", + "uv": [ + 0, + 14, + 5, + 16 + ] + }, + "west": { + "texture": "#casing", + "uv": [ + 5, + 14, + 11, + 16 + ] + }, + "east": { + "texture": "#casing", + "uv": [ + 5, + 14, + 11, + 16 + ] + }, + "up": { + "texture": "#casing", + "uv": [ + 0, + 5, + 5, + 11 + ] + }, + "down": { + "texture": "#casing", + "uv": [ + 0, + 5, + 5, + 11 + ] + } + } + }, + { + "from": [ + 11, + 0, + 5 + ], + "to": [ + 16, + 2, + 11 + ], + "faces": { + "north": { + "texture": "#casing", + "uv": [ + 0, + 14, + 5, + 16 + ] + }, + "south": { + "texture": "#casing", + "uv": [ + 11, + 14, + 16, + 16 + ] + }, + "west": { + "texture": "#casing", + "uv": [ + 5, + 14, + 11, + 16 + ] + }, + "east": { + "texture": "#casing", + "uv": [ + 5, + 14, + 11, + 16 + ] + }, + "up": { + "texture": "#casing", + "uv": [ + 11, + 5, + 16, + 11 + ] + }, + "down": { + "texture": "#casing", + "uv": [ + 11, + 5, + 16, + 11 + ] + } + } + }, + { + "from": [ + 0, + 12, + 0 + ], + "to": [ + 16, + 16, + 16 + ], + "faces": { + "north": { + "texture": "#lid", + "uv": [ + 0, + 0, + 16, + 4 + ] + }, + "south": { + "texture": "#lid", + "uv": [ + 0, + 0, + 16, + 4 + ] + }, + "west": { + "texture": "#lid", + "uv": [ + 0, + 0, + 16, + 4 + ] + }, + "east": { + "texture": "#lid", + "uv": [ + 0, + 0, + 16, + 4 + ] + }, + "up": { + "texture": "#top", + "uv": [ + 0, + 0, + 16, + 16 + ] + }, + "down": { + "texture": "#casing", + "uv": [ + 0, + 0, + 16, + 16 + ] + } + } + } + ] +} 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 0000000..6b9be84 Binary files /dev/null and b/src/main/resources/assets/streamcontrol/textures/block/flow_sensor_bottom.png differ 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 0000000..de4d89d Binary files /dev/null and b/src/main/resources/assets/streamcontrol/textures/block/flow_sensor_side.png differ 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 0000000..d599501 Binary files /dev/null and b/src/main/resources/assets/streamcontrol/textures/block/flow_sensor_top.png differ 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 0000000..f2423d5 Binary files /dev/null and b/src/main/resources/assets/streamcontrol/textures/gui/automation_controller.png differ diff --git a/src/main/resources/data/minecraft/tags/block/mineable/axe.json b/src/main/resources/data/minecraft/tags/block/mineable/axe.json new file mode 100644 index 0000000..83667fb --- /dev/null +++ b/src/main/resources/data/minecraft/tags/block/mineable/axe.json @@ -0,0 +1,6 @@ +{ + "replace": false, + "values": [ + "streamcontrol:mooring_post" + ] +} diff --git a/src/main/resources/data/minecraft/tags/block/mineable/pickaxe.json b/src/main/resources/data/minecraft/tags/block/mineable/pickaxe.json index 438c7e4..89f2c1f 100644 --- a/src/main/resources/data/minecraft/tags/block/mineable/pickaxe.json +++ b/src/main/resources/data/minecraft/tags/block/mineable/pickaxe.json @@ -2,6 +2,11 @@ "replace": false, "values": [ "streamcontrol:current_generator", - "streamcontrol:stillwater_anchor" + "streamcontrol:stillwater_anchor", + "streamcontrol:flow_sensor", + { + "id": "streamcontrol:mechanical_current_generator", + "required": false + } ] } diff --git a/src/main/resources/data/streamcontrol/loot_table/blocks/flow_sensor.json b/src/main/resources/data/streamcontrol/loot_table/blocks/flow_sensor.json new file mode 100644 index 0000000..7e21690 --- /dev/null +++ b/src/main/resources/data/streamcontrol/loot_table/blocks/flow_sensor.json @@ -0,0 +1,19 @@ +{ + "type": "minecraft:block", + "pools": [ + { + "rolls": 1, + "entries": [ + { + "type": "minecraft:item", + "name": "streamcontrol:flow_sensor" + } + ], + "conditions": [ + { + "condition": "minecraft:survives_explosion" + } + ] + } + ] +} diff --git a/src/main/resources/data/streamcontrol/loot_table/blocks/mechanical_current_generator.json b/src/main/resources/data/streamcontrol/loot_table/blocks/mechanical_current_generator.json new file mode 100644 index 0000000..1cf0583 --- /dev/null +++ b/src/main/resources/data/streamcontrol/loot_table/blocks/mechanical_current_generator.json @@ -0,0 +1,25 @@ +{ + "type": "minecraft:block", + "pools": [ + { + "rolls": 1, + "entries": [ + { + "type": "minecraft:item", + "name": "streamcontrol:mechanical_current_generator" + } + ], + "conditions": [ + { + "condition": "minecraft:survives_explosion" + } + ] + } + ], + "neoforge:conditions": [ + { + "type": "neoforge:mod_loaded", + "modid": "create" + } + ] +} diff --git a/src/main/resources/data/streamcontrol/loot_table/blocks/mooring_post.json b/src/main/resources/data/streamcontrol/loot_table/blocks/mooring_post.json new file mode 100644 index 0000000..0533091 --- /dev/null +++ b/src/main/resources/data/streamcontrol/loot_table/blocks/mooring_post.json @@ -0,0 +1,19 @@ +{ + "type": "minecraft:block", + "pools": [ + { + "rolls": 1, + "entries": [ + { + "type": "minecraft:item", + "name": "streamcontrol:mooring_post" + } + ], + "conditions": [ + { + "condition": "minecraft:survives_explosion" + } + ] + } + ] +} diff --git a/src/main/resources/data/streamcontrol/recipe/flow_sensor.json b/src/main/resources/data/streamcontrol/recipe/flow_sensor.json new file mode 100644 index 0000000..3e8a2e8 --- /dev/null +++ b/src/main/resources/data/streamcontrol/recipe/flow_sensor.json @@ -0,0 +1,26 @@ +{ + "type": "minecraft:crafting_shaped", + "pattern": [ + "CRC", + "QMQ", + "CCC" + ], + "key": { + "C": { + "item": "minecraft:copper_ingot" + }, + "R": { + "item": "minecraft:redstone" + }, + "Q": { + "item": "minecraft:quartz" + }, + "M": { + "item": "minecraft:comparator" + } + }, + "result": { + "id": "streamcontrol:flow_sensor", + "count": 1 + } +} diff --git a/src/main/resources/data/streamcontrol/recipe/mechanical_current_generator.json b/src/main/resources/data/streamcontrol/recipe/mechanical_current_generator.json new file mode 100644 index 0000000..5fb0170 --- /dev/null +++ b/src/main/resources/data/streamcontrol/recipe/mechanical_current_generator.json @@ -0,0 +1,29 @@ +{ + "type": "minecraft:crafting_shaped", + "pattern": [ + " G ", + " A ", + " S " + ], + "key": { + "G": { + "item": "streamcontrol:current_generator" + }, + "A": { + "item": "create:andesite_casing" + }, + "S": { + "item": "create:shaft" + } + }, + "result": { + "id": "streamcontrol:mechanical_current_generator", + "count": 1 + }, + "neoforge:conditions": [ + { + "type": "neoforge:mod_loaded", + "modid": "create" + } + ] +} diff --git a/src/main/resources/data/streamcontrol/recipe/mooring_post.json b/src/main/resources/data/streamcontrol/recipe/mooring_post.json new file mode 100644 index 0000000..36225a5 --- /dev/null +++ b/src/main/resources/data/streamcontrol/recipe/mooring_post.json @@ -0,0 +1,23 @@ +{ + "type": "minecraft:crafting_shaped", + "pattern": [ + " I ", + "PCP", + "PPP" + ], + "key": { + "I": { + "item": "minecraft:iron_ingot" + }, + "P": { + "tag": "minecraft:planks" + }, + "C": { + "item": "minecraft:chain" + } + }, + "result": { + "id": "streamcontrol:mooring_post", + "count": 1 + } +} diff --git a/tools/fetch_create.py b/tools/fetch_create.py new file mode 100644 index 0000000..afe2236 --- /dev/null +++ b/tools/fetch_create.py @@ -0,0 +1,23 @@ +"""Fetch the exact compile/test fixture; never bundle Create in the addon.""" +import hashlib +import pathlib +import urllib.request +import zipfile + +root = pathlib.Path(__file__).resolve().parents[1] / 'libs' +root.mkdir(exist_ok=True) +jar = root / 'create-1.21.1-6.0.10.jar' +url = 'https://cdn.modrinth.com/data/LNytGWDc/versions/UjX6dr61/create-1.21.1-6.0.10.jar' +if not jar.exists(): + with urllib.request.urlopen(url, timeout=90) as response: + jar.write_bytes(response.read()) +assert hashlib.sha1(jar.read_bytes()).hexdigest() == '0e97e49837bed766e6f28a4c95b04885d6acc353', 'Unexpected Create fixture' +compile_dir = root / 'create-compile' +compile_dir.mkdir(exist_ok=True) +for stale in compile_dir.glob('*.jar'): + stale.unlink() +with zipfile.ZipFile(jar) as archive: + for entry in archive.namelist(): + if entry.startswith('META-INF/jarjar/') and entry.endswith('.jar'): + (compile_dir / pathlib.PurePosixPath(entry).name).write_bytes(archive.read(entry)) +print('Verified Create 6.0.10 and extracted its compile-only dependencies.')