├── .cargo └── config.toml ├── .envrc ├── .github ├── FUNDING.yml ├── release.yml └── workflows │ ├── build.yml │ ├── ci.yml │ └── docker.yml ├── .gitignore ├── .vscode └── launch.json ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── crates ├── blocks │ ├── Cargo.toml │ └── src │ │ ├── block_entities.rs │ │ ├── blocks │ │ ├── mod.rs │ │ └── props.rs │ │ ├── items.rs │ │ └── lib.rs ├── core │ ├── Cargo.toml │ └── src │ │ ├── config.rs │ │ ├── interaction.rs │ │ ├── lib.rs │ │ ├── permissions │ │ └── mod.rs │ │ ├── player.rs │ │ ├── plot │ │ ├── commands.rs │ │ ├── data.rs │ │ ├── database.rs │ │ ├── mod.rs │ │ ├── monitor.rs │ │ ├── packet_handlers.rs │ │ ├── scoreboard.rs │ │ └── worldedit │ │ │ ├── execute.rs │ │ │ ├── mod.rs │ │ │ └── schematic.rs │ │ ├── profile.rs │ │ ├── server.rs │ │ └── utils.rs ├── network │ ├── Cargo.toml │ └── src │ │ ├── lib.rs │ │ ├── nbt_util.rs │ │ └── packets │ │ ├── clientbound.rs │ │ ├── mod.rs │ │ └── serverbound.rs ├── proc_macros │ ├── Cargo.toml │ └── src │ │ └── lib.rs ├── redpiler │ ├── Cargo.toml │ └── src │ │ ├── backend │ │ ├── direct │ │ │ ├── compile.rs │ │ │ ├── mod.rs │ │ │ ├── node.rs │ │ │ ├── tick.rs │ │ │ └── update.rs │ │ └── mod.rs │ │ ├── compile_graph.rs │ │ ├── lib.rs │ │ ├── passes │ │ ├── clamp_weights.rs │ │ ├── coalesce.rs │ │ ├── constant_coalesce.rs │ │ ├── constant_fold.rs │ │ ├── dedup_links.rs │ │ ├── export_graph.rs │ │ ├── identify_nodes.rs │ │ ├── input_search.rs │ │ ├── mod.rs │ │ ├── prune_orphans.rs │ │ └── unreachable_output.rs │ │ └── task_monitor.rs ├── redpiler_graph │ ├── Cargo.toml │ └── src │ │ └── lib.rs ├── redstone │ ├── Cargo.toml │ └── src │ │ ├── comparator.rs │ │ ├── lib.rs │ │ ├── noteblock.rs │ │ ├── repeater.rs │ │ └── wire │ │ ├── mod.rs │ │ └── turbo.rs ├── save_data │ ├── Cargo.toml │ └── src │ │ ├── lib.rs │ │ ├── plot_data.rs │ │ └── plot_data │ │ └── fixer.rs ├── text │ ├── Cargo.toml │ └── src │ │ └── lib.rs ├── utils │ ├── Cargo.toml │ └── src │ │ └── lib.rs └── world │ ├── Cargo.toml │ └── src │ ├── lib.rs │ └── storage.rs ├── docker └── Dockerfile ├── docs └── Redpiler.md ├── flake.lock ├── flake.nix ├── rust-toolchain.toml ├── src └── main.rs └── tests ├── common └── mod.rs ├── components.rs └── timings.rs /.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [target.x86_64-unknown-linux-gnu] 2 | linker = "clang" 3 | # The nix flake will handle this instead 4 | # rustflags = ["-C", "link-arg=-fuse-ld=/usr/bin/mold"] 5 | -------------------------------------------------------------------------------- /.envrc: -------------------------------------------------------------------------------- 1 | watch_file rust-toolchain.toml 2 | use flake -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | patreon: stackdoubleflow 2 | -------------------------------------------------------------------------------- /.github/release.yml: -------------------------------------------------------------------------------- 1 | changelog: 2 | exclude: 3 | authors: 4 | - dependabot 5 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | push: 5 | branches: [ "master" ] 6 | tags: [ "v*" ] 7 | pull_request: 8 | branches: [ "master" ] 9 | 10 | jobs: 11 | build: 12 | strategy: 13 | matrix: 14 | target: 15 | - x86_64-unknown-linux-gnu 16 | - x86_64-pc-windows-msvc 17 | - x86_64-apple-darwin 18 | - aarch64-apple-darwin 19 | include: 20 | - target: x86_64-unknown-linux-gnu 21 | os: ubuntu-latest 22 | - target: x86_64-pc-windows-msvc 23 | os: windows-latest 24 | ext: .exe 25 | - target: x86_64-apple-darwin 26 | os: macos-latest 27 | - target: aarch64-apple-darwin 28 | os: macos-latest 29 | fail-fast: false 30 | runs-on: ${{ matrix.os }} 31 | 32 | steps: 33 | - uses: actions/checkout@v4 34 | 35 | - uses: dtolnay/rust-toolchain@stable 36 | with: 37 | targets: ${{ matrix.target }} 38 | 39 | - uses: Swatinem/rust-cache@v2 40 | with: 41 | key: ${{ matrix.target }} 42 | 43 | - name: Build 44 | env: 45 | CARGO_TARGET_X86_64_PC_WINDOWS_MSVC_LINKER: rust-lld.exe 46 | run: cargo build --release --target ${{ matrix.target }} 47 | 48 | - name: Upload artifact 49 | uses: actions/upload-artifact@v4 50 | with: 51 | name: mchprs-${{ matrix.target }} 52 | path: target/${{ matrix.target }}/release/mchprs${{ matrix.ext }} 53 | 54 | build-macos-universal: 55 | needs: build 56 | runs-on: macos-latest 57 | steps: 58 | - uses: actions/download-artifact@v4 59 | with: 60 | name: mchprs-x86_64-apple-darwin 61 | path: x86_64 62 | - uses: actions/download-artifact@v4 63 | with: 64 | name: mchprs-aarch64-apple-darwin 65 | path: aarch64 66 | 67 | - name: Create universal binary 68 | run: lipo -create -output mchprs x86_64/mchprs aarch64/mchprs 69 | 70 | - name: Upload artifact 71 | uses: actions/upload-artifact@v4 72 | with: 73 | name: mchprs-universal-apple-darwin 74 | path: mchprs 75 | 76 | publish: 77 | needs: [build, build-macos-universal] 78 | runs-on: ubuntu-latest 79 | permissions: 80 | contents: write 81 | if: github.event_name == 'push' 82 | steps: 83 | - uses: actions/checkout@v3 84 | - uses: actions/download-artifact@v4 85 | with: 86 | path: artifacts 87 | - name: Reorganize artifacts 88 | run: | 89 | mkdir -p dist/ 90 | find artifacts/ -type f -exec bash -c 'base=$(basename $1); mv $1 dist/$(basename $(dirname $1))${base#${base%.*}}' _ {} \; 91 | 92 | - name: Create preview release 93 | env: 94 | GITHUB_TOKEN: ${{ github.token }} 95 | run: | 96 | gh release delete preview --yes || true 97 | gh release create preview \ 98 | --prerelease \ 99 | --title "${GITHUB_REPOSITORY} Preview Build" \ 100 | --notes "🚧 **This is a preview build of the latest commit.**" \ 101 | dist/* 102 | 103 | - name: Create release 104 | if: startsWith(github.ref, 'refs/tags/') 105 | env: 106 | GITHUB_TOKEN: ${{ github.token }} 107 | tag: ${{ github.ref_name }} 108 | run: | 109 | gh release create "$tag" \ 110 | --title "${GITHUB_REPOSITORY} ${tag#v}" \ 111 | --generate-notes \ 112 | dist/* 113 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [ "master" ] 6 | pull_request: 7 | branches: [ "master" ] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | format: 14 | name: Check formatting 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v4 18 | 19 | - name: Install Rust toolchain 20 | uses: dtolnay/rust-toolchain@stable 21 | with: 22 | components: rustfmt 23 | 24 | - name: Check formatting 25 | run: cargo fmt --check --all 26 | 27 | clippy: 28 | name: Clippy lints 29 | runs-on: ubuntu-latest 30 | steps: 31 | - uses: actions/checkout@v4 32 | 33 | - name: Install Rust toolchain 34 | uses: dtolnay/rust-toolchain@stable 35 | with: 36 | components: clippy 37 | 38 | - name: Rust cache 39 | uses: Swatinem/rust-cache@v2 40 | 41 | - name: Run clippy 42 | # If we ever get around to fix everything clippy complains about we can start failing on warning via: -- -D warnings 43 | run: cargo clippy --all-features --all-targets 44 | 45 | test: 46 | name: Run tests 47 | runs-on: ubuntu-latest 48 | steps: 49 | - uses: actions/checkout@v4 50 | 51 | - name: Install Rust toolchain 52 | uses: dtolnay/rust-toolchain@stable 53 | 54 | - name: Rust cache 55 | uses: Swatinem/rust-cache@v2 56 | 57 | - name: Run tests 58 | run: cargo test --all-features --all-targets 59 | -------------------------------------------------------------------------------- /.github/workflows/docker.yml: -------------------------------------------------------------------------------- 1 | name: Docker 2 | 3 | on: 4 | push: 5 | branches: [ "master" ] 6 | tags: [ "v*" ] 7 | 8 | jobs: 9 | docker: 10 | runs-on: ubuntu-latest 11 | permissions: 12 | contents: read 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v4 16 | 17 | - name: Docker meta 18 | id: meta 19 | uses: docker/metadata-action@v5 20 | with: 21 | images: ${{ secrets.DOCKER_HUB_USERNAME }}/mchprs 22 | tags: | 23 | type=raw,value=latest,enable={{is_default_branch}} 24 | type=semver,pattern={{major}}.{{minor}}.{{patch}} 25 | type=semver,pattern={{major}}.{{minor}} 26 | type=sha,format=short 27 | 28 | - name: Set up QEMU 29 | uses: docker/setup-qemu-action@v3 30 | with: 31 | platforms: arm64 32 | 33 | - name: Set up Docker Buildx 34 | uses: docker/setup-buildx-action@v3 35 | 36 | - name: Login to Docker Hub 37 | uses: docker/login-action@v3 38 | with: 39 | username: ${{ secrets.DOCKER_HUB_USERNAME }} 40 | password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} 41 | 42 | - name: Build and push 43 | uses: docker/build-push-action@v5 44 | with: 45 | context: . 46 | file: ./docker/Dockerfile 47 | platforms: linux/amd64,linux/arm64 48 | push: true 49 | tags: ${{ steps.meta.outputs.tags }} 50 | labels: ${{ steps.meta.outputs.labels }} 51 | cache-from: type=gha 52 | cache-to: type=gha 53 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.idea 2 | /run 3 | /target 4 | /test_proxy 5 | /generators 6 | /.direnv -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "lldb", 9 | "request": "launch", 10 | "name": "Debug executable 'mchprs'", 11 | "cargo": { 12 | "args": [ 13 | "build", 14 | "--bin=mchprs", 15 | "--package=mchprs" 16 | ], 17 | "filter": { 18 | "name": "mchprs", 19 | "kind": "bin" 20 | } 21 | }, 22 | "args": [], 23 | "cwd": "${workspaceFolder}/run" 24 | }, 25 | { 26 | "type": "lldb", 27 | "request": "launch", 28 | "name": "Debug unit tests in executable 'mchprs'", 29 | "cargo": { 30 | "args": [ 31 | "test", 32 | "--no-run", 33 | "--bin=mchprs", 34 | "--package=mchprs" 35 | ], 36 | "filter": { 37 | "name": "mchprs", 38 | "kind": "bin" 39 | } 40 | }, 41 | "args": [], 42 | "cwd": "${workspaceFolder}/run" 43 | } 44 | ] 45 | } -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = ["crates/proc_macros", "crates/redpiler_graph"] 3 | 4 | [package] 5 | name = "mchprs" 6 | authors.workspace = true 7 | description.workspace = true 8 | edition.workspace = true 9 | homepage.workspace = true 10 | keywords.workspace = true 11 | readme.workspace = true 12 | version.workspace = true 13 | license.workspace = true 14 | repository.workspace = true 15 | 16 | [workspace.package] 17 | authors = ["StackDoubleFlow "] 18 | description = "A multithreaded minecraft server built for redstone." 19 | edition = "2021" 20 | homepage = "https://github.com/MCHPR/MCHPRS" 21 | keywords = ["minecraft", "server", "redstone"] 22 | readme = "README.md" 23 | version = "0.4.1" 24 | license = "MIT" 25 | repository = "https://github.com/MCHPR/MCHPRS" 26 | 27 | include = ["**/*.rs", "Cargo.toml"] 28 | 29 | [profile.dev] 30 | # MCHPRS runs far too slow without any optimizations to even be usable for testing 31 | opt-level = 1 32 | 33 | [profile.release] 34 | # This seems to speed up Redpiler compile times 35 | lto = "fat" 36 | 37 | [dependencies] 38 | mchprs_core = { path = "./crates/core" } 39 | tracing-subscriber = { workspace = true, features = ["env-filter"] } 40 | tracing-appender = { workspace = true } 41 | tracing = { workspace = true } 42 | 43 | [dev-dependencies] 44 | mchprs_world = { path = "./crates/world" } 45 | mchprs_blocks = { path = "./crates/blocks" } 46 | mchprs_redpiler = { path = "./crates/redpiler" } 47 | mchprs_redstone = { path = "./crates/redstone" } 48 | paste = { workspace = true } 49 | 50 | [workspace.dependencies] 51 | toml = "0.8" 52 | byteorder = "1.4" 53 | hematite-nbt = { git = "https://github.com/StackDoubleFlow/hematite_nbt" } 54 | bitflags = "2.6" 55 | serde = "1" 56 | serde_json = "1.0" 57 | md5 = "0.7" 58 | bus = "2.2" 59 | ctrlc = "3.1" 60 | tracing = "0.1" 61 | rand = "0.9" 62 | regex = "1.4" 63 | backtrace = "0.3" 64 | rusqlite = "0.33" 65 | anyhow = "1.0" 66 | toml_edit = "0.22" 67 | mysql = "26" 68 | tokio = "1" 69 | reqwest = "0.12" 70 | itertools = "0.14" 71 | bincode = "1.3" 72 | once_cell = "1.14.0" 73 | rustc-hash = "2.0" 74 | hmac = "0.12" 75 | sha2 = "0.10" 76 | bitvec = "1" 77 | flate2 = "1" 78 | smallvec = "1.9.0" 79 | enum_dispatch = "0.3" 80 | petgraph = "0.7" 81 | thiserror = "2" 82 | syn = "2" 83 | quote = "1" 84 | tracing-subscriber = "0.3" 85 | tracing-appender = "0.2" 86 | paste = "1.0" 87 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Ojas Landge 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /crates/blocks/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "mchprs_blocks" 3 | authors.workspace = true 4 | description.workspace = true 5 | edition.workspace = true 6 | homepage.workspace = true 7 | keywords.workspace = true 8 | readme.workspace = true 9 | version.workspace = true 10 | license.workspace = true 11 | repository.workspace = true 12 | 13 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 14 | 15 | [dependencies] 16 | mchprs_utils = { path = "../utils" } 17 | mchprs_proc_macros = { path = "../proc_macros" } 18 | serde = { workspace = true } 19 | hematite-nbt = { workspace = true } 20 | -------------------------------------------------------------------------------- /crates/blocks/src/block_entities.rs: -------------------------------------------------------------------------------- 1 | use crate::items::Item; 2 | use mchprs_utils::{map, nbt_unwrap_val}; 3 | use serde::{Deserialize, Serialize}; 4 | use std::collections::HashMap; 5 | use std::str::FromStr; 6 | 7 | /// A single item in an inventory 8 | #[derive(Clone, Debug, Serialize, Deserialize)] 9 | pub struct InventoryEntry { 10 | pub id: u32, 11 | pub slot: i8, 12 | pub count: i8, 13 | pub nbt: Option>, 14 | } 15 | 16 | #[derive(Default, Debug, Clone, Serialize, Deserialize)] 17 | pub struct SignBlockEntity { 18 | pub front_rows: [String; 4], 19 | pub back_rows: [String; 4], 20 | } 21 | 22 | #[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] 23 | pub enum ContainerType { 24 | Furnace, 25 | Barrel, 26 | Hopper, 27 | } 28 | 29 | impl FromStr for ContainerType { 30 | type Err = (); 31 | 32 | fn from_str(s: &str) -> Result { 33 | Ok(match s { 34 | "barrel" => ContainerType::Barrel, 35 | "furnace" => ContainerType::Furnace, 36 | "hopper" => ContainerType::Hopper, 37 | _ => return Err(()), 38 | }) 39 | } 40 | } 41 | 42 | impl ToString for ContainerType { 43 | fn to_string(&self) -> String { 44 | match self { 45 | ContainerType::Furnace => "minecraft:furnace", 46 | ContainerType::Barrel => "minecraft:barrel", 47 | ContainerType::Hopper => "minecraft:hopper", 48 | } 49 | .to_owned() 50 | } 51 | } 52 | 53 | impl ContainerType { 54 | pub fn num_slots(self) -> u8 { 55 | match self { 56 | ContainerType::Furnace => 3, 57 | ContainerType::Barrel => 27, 58 | ContainerType::Hopper => 5, 59 | } 60 | } 61 | 62 | pub fn window_type(self) -> u8 { 63 | // https://wiki.vg/Inventory 64 | match self { 65 | ContainerType::Furnace => 14, 66 | ContainerType::Barrel => 2, 67 | ContainerType::Hopper => 16, 68 | } 69 | } 70 | } 71 | 72 | #[derive(Debug, Clone, Serialize, Deserialize)] 73 | pub enum BlockEntity { 74 | Comparator { 75 | output_strength: u8, 76 | }, 77 | Container { 78 | comparator_override: u8, 79 | inventory: Vec, 80 | ty: ContainerType, 81 | }, 82 | Sign(Box), 83 | } 84 | 85 | impl BlockEntity { 86 | /// The protocol id for the block entity 87 | pub fn ty(&self) -> i32 { 88 | match self { 89 | BlockEntity::Comparator { .. } => 18, 90 | BlockEntity::Container { ty, .. } => match ty { 91 | ContainerType::Furnace => 0, 92 | ContainerType::Barrel => 26, 93 | ContainerType::Hopper => 17, 94 | }, 95 | BlockEntity::Sign(_) => 7, 96 | } 97 | } 98 | 99 | fn load_container(slots_nbt: &[nbt::Value], ty: ContainerType) -> Option { 100 | use nbt::Value; 101 | let num_slots = ty.num_slots(); 102 | let mut fullness_sum: f32 = 0.0; 103 | let mut inventory = Vec::new(); 104 | for item in slots_nbt { 105 | let item_compound = nbt_unwrap_val!(item, Value::Compound); 106 | let count = nbt_unwrap_val!(item_compound["Count"], Value::Byte); 107 | let slot = nbt_unwrap_val!(item_compound["Slot"], Value::Byte); 108 | let namespaced_name = nbt_unwrap_val!( 109 | item_compound 110 | .get("Id") 111 | .or_else(|| item_compound.get("id"))?, 112 | Value::String 113 | ); 114 | let item_type = Item::from_name(namespaced_name.split(':').last()?); 115 | 116 | let mut blob = nbt::Blob::new(); 117 | for (k, v) in item_compound { 118 | blob.insert(k, v.clone()).unwrap(); 119 | } 120 | let mut data = Vec::new(); 121 | blob.to_writer(&mut data).unwrap(); 122 | 123 | let tag = match item_compound.get("tag") { 124 | Some(nbt::Value::Compound(map)) => { 125 | let mut blob = nbt::Blob::new(); 126 | for (k, v) in map { 127 | blob.insert(k, v.clone()).unwrap(); 128 | } 129 | 130 | let mut data = Vec::new(); 131 | blob.to_writer(&mut data).unwrap(); 132 | Some(data) 133 | } 134 | _ => None, 135 | }; 136 | inventory.push(InventoryEntry { 137 | slot, 138 | count, 139 | id: item_type.unwrap_or(Item::Redstone {}).get_id(), 140 | nbt: tag, 141 | }); 142 | 143 | fullness_sum += count as f32 / item_type.map_or(64, Item::max_stack_size) as f32; 144 | } 145 | Some(BlockEntity::Container { 146 | comparator_override: (if fullness_sum > 0.0 { 1.0 } else { 0.0 } 147 | + (fullness_sum / num_slots as f32) * 14.0) 148 | .floor() as u8, 149 | inventory, 150 | ty, 151 | }) 152 | } 153 | 154 | pub fn from_nbt(id: &str, nbt: &HashMap) -> Option { 155 | use nbt::Value; 156 | match id.trim_start_matches("minecraft:") { 157 | "comparator" => Some(BlockEntity::Comparator { 158 | output_strength: *nbt_unwrap_val!(&nbt["OutputSignal"], Value::Int) as u8, 159 | }), 160 | "furnace" => BlockEntity::load_container( 161 | nbt_unwrap_val!(&nbt["Items"], Value::List), 162 | ContainerType::Furnace, 163 | ), 164 | "barrel" => BlockEntity::load_container( 165 | nbt_unwrap_val!(&nbt["Items"], Value::List), 166 | ContainerType::Barrel, 167 | ), 168 | "hopper" => BlockEntity::load_container( 169 | nbt_unwrap_val!(&nbt["Items"], Value::List), 170 | ContainerType::Hopper, 171 | ), 172 | "sign" => { 173 | let sign = if nbt.contains_key("Text1") { 174 | // This is the pre-1.20 encoding 175 | SignBlockEntity { 176 | front_rows: [ 177 | // This cloning is really dumb 178 | nbt_unwrap_val!(nbt["Text1"].clone(), Value::String), 179 | nbt_unwrap_val!(nbt["Text2"].clone(), Value::String), 180 | nbt_unwrap_val!(nbt["Text3"].clone(), Value::String), 181 | nbt_unwrap_val!(nbt["Text4"].clone(), Value::String), 182 | ], 183 | back_rows: Default::default(), 184 | } 185 | } else { 186 | let get_side = |side| { 187 | let messages = 188 | nbt_unwrap_val!(&nbt[side], Value::Compound).get("messages")?; 189 | let mut messages = nbt_unwrap_val!(messages, Value::List).iter().cloned(); 190 | Some([ 191 | nbt_unwrap_val!(messages.next()?, Value::String), 192 | nbt_unwrap_val!(messages.next()?, Value::String), 193 | nbt_unwrap_val!(messages.next()?, Value::String), 194 | nbt_unwrap_val!(messages.next()?, Value::String), 195 | ]) 196 | }; 197 | SignBlockEntity { 198 | front_rows: get_side("front_text")?, 199 | back_rows: get_side("back_text")?, 200 | } 201 | }; 202 | Some(BlockEntity::Sign(Box::new(sign))) 203 | } 204 | _ => None, 205 | } 206 | } 207 | 208 | pub fn to_nbt(&self, sign_only: bool) -> Option { 209 | if sign_only && !matches!(self, BlockEntity::Sign(_)) { 210 | return None; 211 | } 212 | 213 | use nbt::Value; 214 | match self { 215 | BlockEntity::Sign(sign) => Some({ 216 | let front = sign.front_rows.iter().map(|str| Value::String(str.clone())); 217 | let back = sign.front_rows.iter().map(|str| Value::String(str.clone())); 218 | nbt::Blob::with_content(map! { 219 | "is_waxed" => Value::Byte(0), 220 | "front_text" => Value::Compound(map! { 221 | "has_glowing_text" => Value::Byte(0), 222 | "color" => Value::String("black".into()), 223 | "messages" => Value::List(front.collect()) 224 | }), 225 | "back_text" => Value::Compound(map! { 226 | "has_glowing_text" => Value::Byte(0), 227 | "color" => Value::String("black".into()), 228 | "messages" => Value::List(back.collect()) 229 | }), 230 | "id" => Value::String("minecraft:sign".to_owned()) 231 | }) 232 | }), 233 | BlockEntity::Comparator { output_strength } => Some({ 234 | nbt::Blob::with_content(map! { 235 | "OutputSignal" => Value::Int(*output_strength as i32), 236 | "id" => Value::String("minecraft:comparator".to_owned()) 237 | }) 238 | }), 239 | BlockEntity::Container { inventory, ty, .. } => Some({ 240 | let mut items = Vec::new(); 241 | for entry in inventory { 242 | let nbt = map! { 243 | "Count" => nbt::Value::Byte(entry.count), 244 | "id" => nbt::Value::String("minecraft:".to_string() + Item::from_id(entry.id).get_name()), 245 | "Slot" => nbt::Value::Byte(entry.slot) 246 | }; 247 | // TODO: item nbt data in containers 248 | // if let Some(tag) = &entry.nbt { 249 | // let blob = nbt::Blob::from_reader(&mut Cursor::new(tag)).unwrap(); 250 | // } 251 | items.push(nbt::Value::Compound(nbt)); 252 | } 253 | nbt::Blob::with_content(map! { 254 | "id" => Value::String(ty.to_string()), 255 | "Items" => Value::List(items) 256 | }) 257 | }), 258 | } 259 | } 260 | } 261 | -------------------------------------------------------------------------------- /crates/core/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "mchprs_core" 3 | authors.workspace = true 4 | description.workspace = true 5 | edition.workspace = true 6 | homepage.workspace = true 7 | keywords.workspace = true 8 | readme.workspace = true 9 | version.workspace = true 10 | license.workspace = true 11 | repository.workspace = true 12 | 13 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 14 | 15 | [dependencies] 16 | mchprs_proc_macros = { path = "../proc_macros" } 17 | mchprs_save_data = { path = "../save_data" } 18 | mchprs_blocks = { path = "../blocks" } 19 | mchprs_world = { path = "../world" } 20 | mchprs_utils = { path = "../utils" } 21 | mchprs_network = { path = "../network" } 22 | mchprs_text = { path = "../text" } 23 | mchprs_redpiler = { path = "../redpiler" } 24 | mchprs_redstone = { path = "../redstone" } 25 | toml = { workspace = true } 26 | byteorder = { workspace = true } 27 | hematite-nbt = { workspace = true } 28 | bitflags = { workspace = true } 29 | serde = { workspace = true } 30 | serde_json = { workspace = true } 31 | md5 = { workspace = true } 32 | bus = { workspace = true } 33 | ctrlc = { workspace = true, features = ["termination"] } 34 | tracing = { workspace = true } 35 | rand = { workspace = true } 36 | regex = { workspace = true } 37 | backtrace = { workspace = true } 38 | rusqlite = { workspace = true, features=["bundled"] } 39 | anyhow = { workspace = true } 40 | toml_edit = { workspace = true } 41 | mysql = { workspace = true } 42 | tokio = { workspace = true, features = ["rt-multi-thread"] } 43 | reqwest = { workspace = true, features = ["json"] } 44 | itertools = { workspace = true } 45 | bincode = { workspace = true } 46 | once_cell = { workspace = true } 47 | rustc-hash = { workspace = true } 48 | hmac = { workspace = true } 49 | sha2 = { workspace = true } 50 | -------------------------------------------------------------------------------- /crates/core/src/config.rs: -------------------------------------------------------------------------------- 1 | use crate::permissions::PermissionsConfig; 2 | use once_cell::sync::Lazy; 3 | use serde::{Deserialize, Serialize}; 4 | use std::fs; 5 | use std::io::Write; 6 | use toml_edit::{value, DocumentMut}; 7 | 8 | pub static CONFIG: Lazy = Lazy::new(|| ServerConfig::load("Config.toml")); 9 | 10 | trait ConfigSerializeDefault { 11 | fn fix_config(self, name: &str, doc: &mut DocumentMut); 12 | } 13 | 14 | macro_rules! impl_simple_default { 15 | ( $( $type:ty ),* ) => { 16 | $( 17 | impl ConfigSerializeDefault for $type { 18 | fn fix_config(self, name: &str, doc: &mut DocumentMut) { 19 | doc.entry(name).or_insert_with(|| value(self)); 20 | } 21 | } 22 | )* 23 | } 24 | } 25 | 26 | impl_simple_default!(String, i64, bool); 27 | 28 | impl ConfigSerializeDefault for Option { 29 | fn fix_config(self, _: &str, _: &mut DocumentMut) { 30 | assert!(matches!(self, None), "`Some` as default is unimplemented"); 31 | } 32 | } 33 | 34 | macro_rules! gen_config { 35 | ( 36 | $( $name:ident: $type:ty = $default:expr),* 37 | ) => { 38 | #[derive(Serialize, Deserialize)] 39 | pub struct ServerConfig { 40 | $( 41 | pub $name: $type, 42 | )* 43 | } 44 | 45 | impl ServerConfig { 46 | fn load(config_file: &str) -> ServerConfig { 47 | let str = fs::read_to_string("Config.toml").unwrap_or_default(); 48 | let mut doc = str.parse::().unwrap(); 49 | 50 | $( 51 | <$type as ConfigSerializeDefault>::fix_config($default, stringify!($name), &mut doc); 52 | )* 53 | 54 | let patched = doc.to_string(); 55 | if str != patched { 56 | let mut file = fs::OpenOptions::new().create(true).write(true).open(&config_file).unwrap(); 57 | write!(file, "{}", patched).unwrap(); 58 | } 59 | 60 | toml::from_str(&patched).unwrap() 61 | } 62 | } 63 | }; 64 | } 65 | 66 | gen_config! { 67 | bind_address: String = "0.0.0.0:25565".to_string(), 68 | motd: String = "Minecraft High Performance Redstone Server".to_string(), 69 | chat_format: String = "<{username}> {message}".to_string(), 70 | max_players: i64 = 99999, 71 | view_distance: i64 = 8, 72 | whitelist: bool = false, 73 | schemati: bool = false, 74 | luckperms: Option = None, 75 | block_in_hitbox: bool = true, 76 | auto_redpiler: bool = false, 77 | velocity: Option = None 78 | } 79 | 80 | #[derive(Serialize, Deserialize)] 81 | pub struct VelocityConfig { 82 | pub enabled: bool, 83 | pub secret: String, 84 | } 85 | -------------------------------------------------------------------------------- /crates/core/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![deny(rust_2018_idioms)] 2 | 3 | #[macro_use] 4 | mod utils; 5 | mod config; 6 | mod interaction; 7 | mod permissions; 8 | mod player; 9 | pub mod plot; 10 | mod profile; 11 | pub mod server; 12 | 13 | #[macro_use] 14 | extern crate bitflags; 15 | -------------------------------------------------------------------------------- /crates/core/src/permissions/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::config::CONFIG; 2 | use crate::utils::HyphenatedUUID; 3 | use anyhow::{anyhow, Context, Result}; 4 | use mysql::prelude::*; 5 | use mysql::{OptsBuilder, Pool, PooledConn, Row}; 6 | use once_cell::sync::OnceCell; 7 | use serde::{Deserialize, Serialize}; 8 | 9 | static POOL: OnceCell = OnceCell::new(); 10 | 11 | fn conn() -> Result { 12 | Ok(POOL 13 | .get() 14 | .context("Tried to get conn before permissions init")? 15 | .get_conn()?) 16 | } 17 | 18 | fn config() -> &'static PermissionsConfig { 19 | CONFIG.luckperms.as_ref().unwrap() 20 | } 21 | 22 | #[derive(Debug)] 23 | enum PathSegment { 24 | WildCard, 25 | Named(String), 26 | } 27 | 28 | #[derive(Debug)] 29 | struct PermissionNode { 30 | path: Vec, 31 | value: i32, 32 | server_context: String, 33 | } 34 | 35 | impl PermissionNode { 36 | fn matches(&self, str: &str) -> bool { 37 | if self.server_context != "global" && self.server_context != config().server_context { 38 | return false; 39 | } 40 | 41 | for (i, segment) in str.split('.').enumerate() { 42 | match &self.path[i] { 43 | PathSegment::WildCard => return true, 44 | PathSegment::Named(name) => { 45 | if name != segment { 46 | return false; 47 | } 48 | } 49 | } 50 | } 51 | true 52 | } 53 | } 54 | 55 | #[derive(Debug)] 56 | pub struct PlayerPermissionsCache { 57 | nodes: Vec, 58 | } 59 | 60 | impl PlayerPermissionsCache { 61 | pub fn get_node_val(&self, name: &str) -> Option { 62 | for node in &self.nodes { 63 | if node.matches(name) { 64 | return Some(node.value); 65 | } 66 | } 67 | None 68 | } 69 | } 70 | 71 | #[derive(Serialize, Deserialize, Clone)] 72 | pub struct PermissionsConfig { 73 | host: String, 74 | db_name: String, 75 | username: String, 76 | password: String, 77 | server_context: String, 78 | } 79 | 80 | pub fn init(config: PermissionsConfig) -> Result<()> { 81 | let opts = OptsBuilder::new() 82 | .ip_or_hostname(Some(config.host)) 83 | .db_name(Some(config.db_name)) 84 | .user(Some(config.username)) 85 | .pass(Some(config.password)); 86 | let pool = Pool::new(opts)?; 87 | POOL.set(pool) 88 | .map_err(|_| anyhow!("Tried to init permissions more than once"))?; 89 | 90 | Ok(()) 91 | } 92 | 93 | pub fn load_player_cache(uuid: u128) -> Result { 94 | let uuid = HyphenatedUUID(uuid).to_string(); 95 | let mut conn = conn()?; 96 | let res: Vec = conn.exec( 97 | " 98 | WITH RECURSIVE groups_inherited AS ( 99 | SELECT * 100 | FROM luckperms_user_permissions 101 | WHERE uuid LIKE ? 102 | UNION 103 | SELECT luckperms_group_permissions.* 104 | FROM groups_inherited, luckperms_group_permissions 105 | WHERE luckperms_group_permissions.name = SUBSTR(groups_inherited.permission, 7) 106 | ) 107 | SELECT * 108 | FROM groups_inherited; 109 | ", 110 | (&uuid,), 111 | )?; 112 | 113 | let mut nodes = Vec::new(); 114 | for row in res { 115 | let path_str = String::from_value(row[2].clone()); 116 | let path = path_str 117 | .split('.') 118 | .map(|s| match s { 119 | "*" => PathSegment::WildCard, 120 | s => PathSegment::Named(s.to_owned()), 121 | }) 122 | .collect(); 123 | let node = PermissionNode { 124 | path, 125 | server_context: FromValue::from_value(row[4].clone()), 126 | value: FromValue::from_value(row[3].clone()), 127 | }; 128 | nodes.push(node); 129 | } 130 | 131 | Ok(PlayerPermissionsCache { nodes }) 132 | } 133 | -------------------------------------------------------------------------------- /crates/core/src/plot/data.rs: -------------------------------------------------------------------------------- 1 | use super::{Plot, PlotWorld, PLOT_WIDTH}; 2 | use anyhow::{Context, Result}; 3 | use mchprs_save_data::plot_data::{ChunkData, PlotData, Tps, WorldSendRate}; 4 | use once_cell::sync::Lazy; 5 | use std::path::Path; 6 | use std::time::Duration; 7 | 8 | // TODO: where to put this? 9 | pub fn sleep_time_for_tps(tps: Tps) -> Duration { 10 | match tps { 11 | Tps::Limited(tps) => { 12 | if tps > 10 { 13 | Duration::from_micros(1_000_000 / tps as u64) 14 | } else { 15 | Duration::from_millis(50) 16 | } 17 | } 18 | Tps::Unlimited => Duration::ZERO, 19 | } 20 | } 21 | 22 | pub fn load_plot(path: impl AsRef) -> Result { 23 | let path = path.as_ref(); 24 | if path.exists() { 25 | Ok(PlotData::load_from_file(path) 26 | .with_context(|| format!("error loading plot save file at {}", path.display()))?) 27 | } else { 28 | Ok(EMPTY_PLOT.clone()) 29 | } 30 | } 31 | 32 | pub fn empty_plot() -> PlotData { 33 | EMPTY_PLOT.clone() 34 | } 35 | 36 | static EMPTY_PLOT: Lazy = Lazy::new(|| { 37 | let template_path = Path::new("./world/plots/pTEMPLATE"); 38 | if template_path.exists() { 39 | PlotData::load_from_file(template_path).expect("failed to read template plot") 40 | } else { 41 | let mut chunks = Vec::new(); 42 | for chunk_x in 0..PLOT_WIDTH { 43 | for chunk_z in 0..PLOT_WIDTH { 44 | chunks.push(Plot::generate_chunk(8, chunk_x, chunk_z)); 45 | } 46 | } 47 | let mut world = PlotWorld { 48 | x: 0, 49 | z: 0, 50 | chunks, 51 | to_be_ticked: Vec::new(), 52 | packet_senders: Vec::new(), 53 | }; 54 | let chunk_data: Vec = 55 | world.chunks.iter_mut().map(|c| ChunkData::new(c)).collect(); 56 | PlotData { 57 | tps: Tps::Limited(10), 58 | world_send_rate: WorldSendRate::default(), 59 | chunk_data, 60 | pending_ticks: Vec::new(), 61 | } 62 | } 63 | }); 64 | -------------------------------------------------------------------------------- /crates/core/src/plot/database.rs: -------------------------------------------------------------------------------- 1 | use once_cell::sync::Lazy; 2 | use rusqlite::{params, Connection}; 3 | use std::sync::{Mutex, MutexGuard}; 4 | 5 | static CONN: Lazy> = Lazy::new(|| { 6 | Mutex::new(Connection::open("./world/plots.db").expect("Error opening plot database!")) 7 | }); 8 | 9 | fn lock<'a>() -> MutexGuard<'a, Connection> { 10 | CONN.lock().unwrap() 11 | } 12 | 13 | pub fn get_plot_owner(plot_x: i32, plot_z: i32) -> Option { 14 | lock() 15 | .query_row( 16 | "SELECT 17 | uuid 18 | FROM 19 | plot 20 | JOIN 21 | userplot ON userplot.plot_id = plot.id 22 | JOIN 23 | user ON user.id = userplot.user_id 24 | WHERE 25 | plot_x=?1 26 | AND plot_z=?2 27 | AND is_owner=TRUE", 28 | params![plot_x, plot_z], 29 | |row| row.get::<_, String>(0), 30 | ) 31 | .ok() 32 | } 33 | 34 | pub fn get_cached_username(uuid: String) -> Option { 35 | lock() 36 | .query_row( 37 | "SELECT 38 | name 39 | FROM 40 | user 41 | WHERE 42 | uuid=?1", 43 | params![uuid], 44 | |row| row.get::<_, String>(0), 45 | ) 46 | .ok() 47 | } 48 | 49 | pub fn get_owned_plots(player: &str) -> Vec<(i32, i32)> { 50 | let conn = lock(); 51 | let mut stmt = conn 52 | .prepare_cached( 53 | "SELECT 54 | plot_x, plot_z 55 | FROM 56 | plot 57 | JOIN 58 | userplot ON userplot.plot_id = plot.id 59 | JOIN 60 | user ON user.id = userplot.user_id 61 | WHERE 62 | name=?1 63 | AND is_owner=TRUE", 64 | ) 65 | .unwrap(); 66 | stmt.query_map(params![player], |row| Ok((row.get(0)?, row.get(1)?))) 67 | .unwrap() 68 | .map(Result::unwrap) 69 | .collect() 70 | } 71 | 72 | pub fn is_claimed(plot_x: i32, plot_z: i32) -> Option { 73 | lock() 74 | .query_row( 75 | "SELECT EXISTS(SELECT * FROM plot WHERE plot_x = ?1 AND plot_z = ?2)", 76 | params![plot_x, plot_z], 77 | |row| row.get::<_, bool>(0), 78 | ) 79 | .ok() 80 | } 81 | 82 | pub fn claim_plot(plot_x: i32, plot_z: i32, uuid: &str) { 83 | let conn = lock(); 84 | conn.execute( 85 | "INSERT INTO plot(plot_x, plot_z) VALUES(?1, ?2)", 86 | params![plot_x, plot_z], 87 | ) 88 | .unwrap(); 89 | 90 | conn.execute( 91 | "INSERT INTO userplot(user_id, plot_id, is_owner) 92 | VALUES( 93 | (SELECT id FROM user WHERE user.uuid = ?1), 94 | LAST_INSERT_ROWID(), 95 | TRUE 96 | )", 97 | params![uuid], 98 | ) 99 | .unwrap(); 100 | } 101 | 102 | pub fn ensure_user(uuid: &str, name: &str) { 103 | lock() 104 | .execute( 105 | "INSERT INTO user(uuid, name) 106 | VALUES (?1, ?2) 107 | ON CONFLICT (uuid) DO UPDATE SET name = ?3", 108 | params![uuid, name, name], 109 | ) 110 | .unwrap(); 111 | } 112 | 113 | pub fn init() { 114 | let conn = lock(); 115 | 116 | conn.execute( 117 | "CREATE TABLE IF NOT EXISTS user( 118 | id INTEGER PRIMARY KEY AUTOINCREMENT, 119 | uuid BLOB(16) UNIQUE NOT NULL, 120 | name VARCHAR(16) NOT NULL 121 | )", 122 | [], 123 | ) 124 | .unwrap(); 125 | 126 | conn.execute( 127 | "CREATE TABLE IF NOT EXISTS plot( 128 | id INTEGER PRIMARY KEY AUTOINCREMENT, 129 | plot_x INTEGER NOT NULL, 130 | plot_z INTEGER NOT NULL 131 | )", 132 | [], 133 | ) 134 | .unwrap(); 135 | 136 | conn.execute( 137 | "CREATE TABLE IF NOT EXISTS userplot( 138 | user_id INTEGER NOT NULL, 139 | plot_id INTEGER NOT NULL, 140 | is_owner BOOLEAN NOT NULL DEFAULT FALSE, 141 | FOREIGN KEY(user_id) REFERENCES user(id), 142 | FOREIGN KEY(plot_id) REFERENCES plot(id) 143 | )", 144 | [], 145 | ) 146 | .unwrap(); 147 | } 148 | -------------------------------------------------------------------------------- /crates/core/src/plot/monitor.rs: -------------------------------------------------------------------------------- 1 | use mchprs_save_data::plot_data::Tps; 2 | use std::collections::VecDeque; 3 | use std::sync::atomic::{AtomicBool, AtomicU32, AtomicU64, Ordering}; 4 | use std::sync::{Arc, Mutex}; 5 | use std::thread; 6 | use std::thread::JoinHandle; 7 | use std::time::Duration; 8 | use tracing::warn; 9 | 10 | #[derive(Default)] 11 | struct AtomicTps { 12 | tps: AtomicU32, 13 | unlimited: AtomicBool, 14 | } 15 | 16 | impl AtomicTps { 17 | fn from_tps(tps: Tps) -> Self { 18 | match tps { 19 | Tps::Limited(tps) => AtomicTps { 20 | tps: AtomicU32::new(tps), 21 | unlimited: AtomicBool::new(false), 22 | }, 23 | Tps::Unlimited => AtomicTps { 24 | tps: AtomicU32::new(0), 25 | unlimited: AtomicBool::new(true), 26 | }, 27 | } 28 | } 29 | 30 | fn update(&self, tps: Tps) { 31 | match tps { 32 | Tps::Limited(tps) => { 33 | self.tps.store(tps, Ordering::Relaxed); 34 | self.unlimited.store(false, Ordering::Relaxed); 35 | } 36 | Tps::Unlimited => self.unlimited.store(true, Ordering::Relaxed), 37 | } 38 | } 39 | } 40 | 41 | struct MonitorData { 42 | tps: AtomicTps, 43 | ticks_passed: Arc, 44 | reset_timings: AtomicU32, 45 | too_slow: AtomicBool, 46 | ticking: AtomicBool, 47 | running: AtomicBool, 48 | timings_record: Mutex>, 49 | } 50 | 51 | #[derive(Debug)] 52 | pub struct TimingsReport { 53 | pub ten_s: f32, 54 | pub one_m: f32, 55 | pub five_m: f32, 56 | pub fifteen_m: f32, 57 | } 58 | 59 | pub struct TimingsMonitor { 60 | data: Arc, 61 | monitor_thread: Option>, 62 | } 63 | 64 | impl TimingsMonitor { 65 | pub fn new(tps: Tps) -> TimingsMonitor { 66 | let data = Arc::new(MonitorData { 67 | ticks_passed: Default::default(), 68 | reset_timings: Default::default(), 69 | running: AtomicBool::new(true), 70 | too_slow: Default::default(), 71 | ticking: Default::default(), 72 | timings_record: Default::default(), 73 | tps: AtomicTps::from_tps(tps), 74 | }); 75 | let monitor_thread = Some(Self::run_thread(data.clone())); 76 | TimingsMonitor { 77 | data, 78 | monitor_thread, 79 | } 80 | } 81 | 82 | pub fn stop(&mut self) { 83 | self.data.running.store(false, Ordering::Relaxed); 84 | if let Some(handle) = self.monitor_thread.take() { 85 | if handle.join().is_err() { 86 | warn!("Failed to join monitor thread handle"); 87 | } 88 | } 89 | } 90 | 91 | pub fn generate_report(&self) -> Option { 92 | let records = self.data.timings_record.lock().unwrap(); 93 | if records.is_empty() { 94 | return None; 95 | } 96 | 97 | let mut ticks_10s = 0; 98 | let mut ticks_1m = 0; 99 | let mut ticks_5m = 0; 100 | let mut ticks_15m = 0; 101 | // TODO: https://github.com/rust-lang/rust-clippy/issues/8987 102 | #[allow(clippy::significant_drop_in_scrutinee)] 103 | for (i, ticks) in records.iter().enumerate() { 104 | if i < 20 { 105 | ticks_10s += *ticks; 106 | } 107 | if i < 120 { 108 | ticks_1m += *ticks; 109 | } 110 | if i < 600 { 111 | ticks_5m += *ticks; 112 | } 113 | ticks_15m += *ticks; 114 | } 115 | 116 | Some(TimingsReport { 117 | ten_s: ticks_10s as f32 / records.len().min(20) as f32 * 2.0, 118 | one_m: ticks_1m as f32 / records.len().min(120) as f32 * 2.0, 119 | five_m: ticks_5m as f32 / records.len().min(600) as f32 * 2.0, 120 | fifteen_m: ticks_15m as f32 / records.len() as f32 * 2.0, 121 | }) 122 | } 123 | 124 | pub fn set_tps(&self, new_tps: Tps) { 125 | self.data.tps.update(new_tps); 126 | self.data.too_slow.store(false, Ordering::Relaxed); 127 | } 128 | 129 | pub fn tick(&self) { 130 | self.data.ticks_passed.fetch_add(1, Ordering::Relaxed); 131 | } 132 | 133 | pub fn tickn(&self, ticks: u64) { 134 | self.data.ticks_passed.fetch_add(ticks, Ordering::Relaxed); 135 | } 136 | 137 | pub fn is_running_behind(&self) -> bool { 138 | self.data.too_slow.load(Ordering::Relaxed) 139 | } 140 | 141 | pub fn set_ticking(&self, ticking: bool) { 142 | self.data.ticking.store(ticking, Ordering::Relaxed); 143 | } 144 | 145 | pub fn reset_timings(&self) { 146 | self.data.reset_timings.store(4, Ordering::Relaxed); 147 | } 148 | 149 | fn run_thread(data: Arc) -> JoinHandle<()> { 150 | thread::spawn(move || { 151 | let mut last_tps = data.tps.tps.load(Ordering::Relaxed); 152 | let mut last_ticks_count = data.ticks_passed.load(Ordering::Relaxed); 153 | let mut was_ticking_before = data.ticking.load(Ordering::Relaxed); 154 | 155 | let mut behind_for = 0; 156 | loop { 157 | thread::sleep(Duration::from_millis(500)); 158 | if !data.running.load(Ordering::Relaxed) { 159 | return; 160 | } 161 | 162 | let ticks_count = data.ticks_passed.load(Ordering::Relaxed); 163 | if ticks_count == 0 { 164 | continue; 165 | } 166 | let ticks_passed = (ticks_count - last_ticks_count) as u32; 167 | last_ticks_count = ticks_count; 168 | 169 | let tps = data.tps.tps.load(Ordering::Relaxed); 170 | let ticking = data.ticking.load(Ordering::Relaxed); 171 | if !(ticking && was_ticking_before) 172 | || tps != last_tps 173 | || data.reset_timings.load(Ordering::Relaxed) > 0 174 | { 175 | data.reset_timings.fetch_sub(1, Ordering::Relaxed); 176 | was_ticking_before = ticking; 177 | last_tps = tps; 178 | continue; 179 | } 180 | 181 | // 5% threshold 182 | if data.tps.unlimited.load(Ordering::Relaxed) || ticks_passed < (tps / 2) * 95 / 100 183 | { 184 | behind_for += 1; 185 | } else { 186 | behind_for = 0; 187 | data.too_slow.store(false, Ordering::Relaxed); 188 | } 189 | 190 | if behind_for >= 3 { 191 | data.too_slow.store(true, Ordering::Relaxed); 192 | // warn!( 193 | // "running behind by {} ticks", 194 | // ((tps / 2) * 95 / 100) - ticks_passed 195 | // ); 196 | } 197 | 198 | // The timings record will only go back 15 minutes. 199 | // This means that, with the 500ms interval, the timings record will 200 | // have a max size of 1800 entries. 201 | let mut timings_record = data.timings_record.lock().unwrap(); 202 | if timings_record.len() == 1800 { 203 | timings_record.pop_back(); 204 | } 205 | timings_record.push_front(ticks_passed); 206 | } 207 | }) 208 | } 209 | } 210 | 211 | impl Drop for TimingsMonitor { 212 | fn drop(&mut self) { 213 | // Joining the thread in drop is a bad idea so we just let it detach 214 | self.data.running.store(false, Ordering::Relaxed); 215 | } 216 | } 217 | -------------------------------------------------------------------------------- /crates/core/src/plot/scoreboard.rs: -------------------------------------------------------------------------------- 1 | use crate::player::{PacketSender, Player}; 2 | use mchprs_network::packets::clientbound::{ 3 | CDisplayObjective, CResetScore, CUpdateObjectives, CUpdateScore, ClientBoundPacket, 4 | ObjectiveNumberFormat, 5 | }; 6 | use mchprs_redpiler::CompilerOptions; 7 | use mchprs_text::{ColorCode, TextComponentBuilder}; 8 | 9 | #[derive(PartialEq, Eq, Default, Clone, Copy)] 10 | pub enum RedpilerState { 11 | #[default] 12 | Stopped, 13 | Compiling, 14 | Running, 15 | } 16 | 17 | impl RedpilerState { 18 | fn to_str(self) -> &'static str { 19 | match self { 20 | RedpilerState::Stopped => "§d§lStopped", 21 | RedpilerState::Compiling => "§e§lCompiling", 22 | RedpilerState::Running => "§a§lRunning", 23 | } 24 | } 25 | } 26 | 27 | pub struct Scoreboard { 28 | current_state: Vec, 29 | } 30 | 31 | impl Default for Scoreboard { 32 | fn default() -> Scoreboard { 33 | let mut sb = Scoreboard { 34 | current_state: vec![], 35 | }; 36 | sb.set_redpiler_state(&[], RedpilerState::Stopped); 37 | sb 38 | } 39 | } 40 | 41 | impl Scoreboard { 42 | fn make_update_packet(&self, line: usize) -> CUpdateScore { 43 | CUpdateScore { 44 | entity_name: self.current_state[line].clone(), 45 | objective_name: "redpiler_status".to_string(), 46 | value: (self.current_state.len() - line) as i32, 47 | display_name: None, 48 | number_format: None, 49 | } 50 | } 51 | 52 | fn make_removal_packet(&self, line: usize) -> CResetScore { 53 | CResetScore { 54 | entity_name: self.current_state[line].clone(), 55 | objective_name: Some("redpiler_status".to_string()), 56 | } 57 | } 58 | 59 | fn set_lines(&mut self, players: &[Player], lines: Vec) { 60 | for line in 0..self.current_state.len() { 61 | let removal_packet = self.make_removal_packet(line).encode(); 62 | players.iter().for_each(|p| p.send_packet(&removal_packet)); 63 | } 64 | 65 | self.current_state = lines; 66 | 67 | for line in 0..self.current_state.len() { 68 | let update_packet = self.make_update_packet(line).encode(); 69 | players.iter().for_each(|p| p.send_packet(&update_packet)); 70 | } 71 | } 72 | 73 | fn set_line(&mut self, players: &[Player], line: usize, text: String) { 74 | if line == self.current_state.len() { 75 | self.current_state.push(text); 76 | } else { 77 | let removal_packet = self.make_removal_packet(line).encode(); 78 | players.iter().for_each(|p| p.send_packet(&removal_packet)); 79 | 80 | self.current_state[line] = text; 81 | } 82 | 83 | let update_packet = self.make_update_packet(line).encode(); 84 | players.iter().for_each(|p| p.send_packet(&update_packet)); 85 | } 86 | 87 | pub fn add_player(&self, player: &Player) { 88 | player.send_packet( 89 | &CUpdateObjectives { 90 | objective_name: "redpiler_status".into(), 91 | mode: 0, 92 | objective_value: TextComponentBuilder::new("Redpiler Status".into()) 93 | .color_code(ColorCode::Red) 94 | .finish(), 95 | ty: 0, 96 | number_format: Some(ObjectiveNumberFormat::Blank), 97 | } 98 | .encode(), 99 | ); 100 | player.send_packet( 101 | &CDisplayObjective { 102 | position: 1, 103 | score_name: "redpiler_status".into(), 104 | } 105 | .encode(), 106 | ); 107 | for i in 0..self.current_state.len() { 108 | player.send_packet(&self.make_update_packet(i).encode()); 109 | } 110 | } 111 | 112 | pub fn remove_player(&mut self, player: &Player) { 113 | for i in 0..self.current_state.len() { 114 | player.send_packet(&self.make_removal_packet(i).encode()); 115 | } 116 | } 117 | 118 | pub fn set_redpiler_state(&mut self, players: &[Player], state: RedpilerState) { 119 | self.set_line(players, 0, state.to_str().to_string()); 120 | } 121 | 122 | pub fn set_redpiler_options(&mut self, players: &[Player], options: &CompilerOptions) { 123 | let mut new_lines = vec![self.current_state[0].clone()]; 124 | 125 | let mut flags = Vec::new(); 126 | if options.optimize { 127 | flags.push("§b- optimize"); 128 | } 129 | if options.export { 130 | flags.push("§b- export"); 131 | } 132 | if options.io_only { 133 | flags.push("§b- io only"); 134 | } 135 | if options.update { 136 | flags.push("§b- update"); 137 | } 138 | if options.wire_dot_out { 139 | flags.push("§b- wire dot out"); 140 | } 141 | 142 | if !flags.is_empty() { 143 | new_lines.push("§7Flags:".to_string()); 144 | new_lines.extend(flags.iter().map(|s| s.to_string())); 145 | } 146 | self.set_lines(players, new_lines); 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /crates/core/src/plot/worldedit/schematic.rs: -------------------------------------------------------------------------------- 1 | //! This implements Sponge Schematic Specification ver. 2 2 | //! https://github.com/SpongePowered/Schematic-Specification/blob/master/versions/schematic-2.md 3 | 4 | use super::WorldEditClipboard; 5 | use crate::server::MC_DATA_VERSION; 6 | use anyhow::{bail, Context, Result}; 7 | use itertools::Itertools; 8 | use mchprs_blocks::block_entities::BlockEntity; 9 | use mchprs_blocks::blocks::Block; 10 | use mchprs_blocks::BlockPos; 11 | use mchprs_world::storage::PalettedBitBuffer; 12 | use once_cell::sync::Lazy; 13 | use regex::Regex; 14 | use rustc_hash::FxHashMap; 15 | use serde::Serialize; 16 | use std::fs::{self, File}; 17 | use std::path::PathBuf; 18 | 19 | macro_rules! nbt_as { 20 | // I'm not sure if path is the right type here. 21 | // It works though! 22 | ($e:expr, $p:path) => { 23 | match $e { 24 | $p(val) => val, 25 | _ => bail!(concat!("Could not parse nbt value as ", stringify!($p))), 26 | } 27 | }; 28 | } 29 | 30 | fn parse_block(str: &str) -> Option { 31 | static RE: Lazy = 32 | Lazy::new(|| Regex::new(r"(?:minecraft:)?([a-z_]+)(?:\[([a-z=,0-9]+)\])?").unwrap()); 33 | let captures = RE.captures(str)?; 34 | let mut block = Block::from_name(captures.get(1)?.as_str()).unwrap_or(Block::Air {}); 35 | if let Some(properties_match) = captures.get(2) { 36 | let properties = properties_match 37 | .as_str() 38 | .split(&[',', '='][..]) 39 | .tuples() 40 | .collect(); 41 | block.set_properties(properties); 42 | } 43 | Some(block) 44 | } 45 | 46 | pub fn load_schematic(file_name: &str) -> Result { 47 | let mut file = File::open("./schems/".to_owned() + file_name)?; 48 | let nbt = nbt::Blob::from_gzip_reader(&mut file)?; 49 | 50 | let root = if nbt.content.contains_key("Schematic") { 51 | nbt_as!(&nbt["Schematic"], nbt::Value::Compound) 52 | } else { 53 | &nbt.content 54 | }; 55 | 56 | let version = nbt_as!(root["Version"], nbt::Value::Int); 57 | match version { 58 | 2 | 3 => load_schematic_sponge(root, version), 59 | _ => bail!("unknown schematic version: {}", version), 60 | } 61 | } 62 | 63 | fn read_block_container( 64 | nbt: &nbt::Map, 65 | version: i32, 66 | size_x: u32, 67 | size_y: u32, 68 | size_z: u32, 69 | ) -> Result<(PalettedBitBuffer, FxHashMap)> { 70 | use nbt::Value; 71 | 72 | let nbt_palette = nbt_as!(&nbt["Palette"], Value::Compound); 73 | let mut palette: FxHashMap = FxHashMap::default(); 74 | for (k, v) in nbt_palette { 75 | let id = *nbt_as!(v, Value::Int) as u32; 76 | let block = parse_block(k).with_context(|| format!("error parsing block: {}", k))?; 77 | palette.insert(id, block.get_id()); 78 | } 79 | 80 | let data_name = match version { 81 | 2 => "BlockData", 82 | 3 => "Data", 83 | _ => unreachable!(), 84 | }; 85 | let blocks: Vec = nbt_as!(&nbt[data_name], Value::ByteArray) 86 | .iter() 87 | .map(|b| *b as u8) 88 | .collect(); 89 | 90 | let mut data = PalettedBitBuffer::new((size_x * size_y * size_z) as usize, 9); 91 | let mut i = 0; 92 | for y_offset in (0..size_y).map(|y| y * size_z * size_x) { 93 | for z_offset in (0..size_z).map(|z| z * size_x) { 94 | for x in 0..size_x { 95 | let mut blockstate_id = 0; 96 | // Max varint length is 5 97 | for varint_len in 0..=5 { 98 | blockstate_id |= ((blocks[i] & 127) as u32) << (varint_len * 7); 99 | if (blocks[i] & 128) != 128 { 100 | i += 1; 101 | break; 102 | } 103 | i += 1; 104 | } 105 | let entry = *palette.get(&blockstate_id).unwrap(); 106 | data.set_entry((y_offset + z_offset + x) as usize, entry); 107 | } 108 | } 109 | } 110 | let block_entities = nbt_as!(&nbt["BlockEntities"], Value::List); 111 | let mut parsed_block_entities = FxHashMap::default(); 112 | for block_entity in block_entities { 113 | let val = nbt_as!(block_entity, Value::Compound); 114 | let pos_array = nbt_as!(&val["Pos"], Value::IntArray); 115 | let pos = BlockPos { 116 | x: pos_array[0], 117 | y: pos_array[1], 118 | z: pos_array[2], 119 | }; 120 | let id = nbt_as!(&val.get("Id").unwrap_or_else(|| &val["id"]), Value::String); 121 | let data = match version { 122 | 2 => val, 123 | 3 => nbt_as!(&val["Data"], Value::Compound), 124 | _ => unreachable!(), 125 | }; 126 | if let Some(parsed) = BlockEntity::from_nbt(id, data) { 127 | parsed_block_entities.insert(pos, parsed); 128 | } 129 | } 130 | 131 | Ok((data, parsed_block_entities)) 132 | } 133 | 134 | fn load_schematic_sponge( 135 | nbt: &nbt::Map, 136 | version: i32, 137 | ) -> Result { 138 | use nbt::Value; 139 | 140 | let size_x = nbt_as!(nbt["Width"], Value::Short) as u32; 141 | let size_z = nbt_as!(nbt["Length"], Value::Short) as u32; 142 | let size_y = nbt_as!(nbt["Height"], Value::Short) as u32; 143 | 144 | let (offset_x, offset_y, offset_z) = match version { 145 | 2 => { 146 | let metadata = nbt_as!(&nbt["Metadata"], Value::Compound); 147 | ( 148 | -nbt_as!(metadata["WEOffsetX"], Value::Int), 149 | -nbt_as!(metadata["WEOffsetY"], Value::Int), 150 | -nbt_as!(metadata["WEOffsetZ"], Value::Int), 151 | ) 152 | } 153 | 3 => { 154 | let offset_array = nbt_as!(&nbt["Offset"], Value::IntArray); 155 | (-offset_array[0], -offset_array[1], -offset_array[2]) 156 | } 157 | _ => unreachable!(), 158 | }; 159 | 160 | let (data, block_entities) = read_block_container( 161 | match version { 162 | 2 => nbt, 163 | 3 => nbt_as!(&nbt["Blocks"], Value::Compound), 164 | _ => unreachable!(), 165 | }, 166 | version, 167 | size_x, 168 | size_y, 169 | size_z, 170 | )?; 171 | Ok(WorldEditClipboard { 172 | size_x, 173 | size_y, 174 | size_z, 175 | offset_x, 176 | offset_y, 177 | offset_z, 178 | data, 179 | block_entities, 180 | }) 181 | } 182 | 183 | #[derive(Serialize)] 184 | struct Metadata { 185 | #[serde(rename = "WEOffsetX")] 186 | offset_x: i32, 187 | #[serde(rename = "WEOffsetY")] 188 | offset_y: i32, 189 | #[serde(rename = "WEOffsetZ")] 190 | offset_z: i32, 191 | } 192 | 193 | /// Used to serialize schematics in NBT. This cannot be used for deserialization because of 194 | /// [a bug](https://github.com/PistonDevelopers/hematite_nbt/issues/45) in `hematite-nbt`. 195 | #[derive(Serialize)] 196 | #[serde(rename_all = "PascalCase")] 197 | struct Schematic { 198 | width: i16, 199 | length: i16, 200 | height: i16, 201 | palette: nbt::Blob, 202 | metadata: Metadata, 203 | #[serde(serialize_with = "nbt::i8_array")] 204 | block_data: Vec, 205 | block_entities: Vec, 206 | version: i32, 207 | data_version: i32, 208 | } 209 | 210 | pub fn save_schematic(file_name: &str, clipboard: &WorldEditClipboard) -> Result<()> { 211 | let mut path = PathBuf::from("./schems"); 212 | path.push(file_name); 213 | fs::create_dir_all(path.parent().unwrap())?; 214 | 215 | let mut file = File::create("./schems/".to_owned() + file_name)?; 216 | let size_x = clipboard.size_x; 217 | let size_y = clipboard.size_y; 218 | let size_z = clipboard.size_z; 219 | let offset_x = -clipboard.offset_x; 220 | let offset_y = -clipboard.offset_y; 221 | let offset_z = -clipboard.offset_z; 222 | let blocks = &clipboard.data; 223 | 224 | let mut data = Vec::new(); 225 | let mut pallette = Vec::new(); 226 | for y_offset in (0..size_y).map(|y| y * size_z * size_x) { 227 | for z_offset in (0..size_z).map(|z| z * size_x) { 228 | for x in 0..size_x { 229 | let entry = blocks.get_entry((y_offset + z_offset + x) as usize); 230 | let block = Block::from_id(entry); 231 | 232 | let name = format!("minecraft:{}", block.get_name()); 233 | let props = block.properties(); 234 | let full_name = if !props.is_empty() { 235 | let props_strs: Vec = props 236 | .iter() 237 | .map(|(name, val)| format!("{}={}", name, val)) 238 | .collect(); 239 | format!("{}[{}]", name, props_strs.join(",")) 240 | } else { 241 | name 242 | }; 243 | let mut idx = if let Some(idx) = pallette.iter().position(|s| *s == full_name) { 244 | idx 245 | } else { 246 | let idx = pallette.len(); 247 | pallette.push(full_name); 248 | idx 249 | }; 250 | 251 | loop { 252 | let mut temp = (idx & 0b1111_1111) as u8; 253 | idx >>= 7; 254 | if idx != 0 { 255 | temp |= 0b1000_0000; 256 | } 257 | data.push(temp as i8); 258 | if idx == 0 { 259 | break; 260 | } 261 | } 262 | } 263 | } 264 | } 265 | 266 | let mut encoded_pallete = nbt::Blob::new(); 267 | for (i, entry) in pallette.iter().enumerate() { 268 | encoded_pallete.insert(entry, i as i32)?; 269 | } 270 | 271 | let mut block_entities = Vec::new(); 272 | for (pos, block_entity) in &clipboard.block_entities { 273 | if let Some(mut blob) = block_entity.to_nbt(false) { 274 | blob.insert("Pos", nbt::Value::IntArray(vec![pos.x, pos.y, pos.z]))?; 275 | block_entities.push(blob); 276 | } 277 | } 278 | 279 | let metadata = Metadata { 280 | offset_x, 281 | offset_y, 282 | offset_z, 283 | }; 284 | let schematic = Schematic { 285 | width: size_x as i16, 286 | length: size_z as i16, 287 | height: size_y as i16, 288 | block_data: data, 289 | block_entities, 290 | palette: encoded_pallete, 291 | metadata, 292 | version: 2, 293 | data_version: MC_DATA_VERSION, 294 | }; 295 | nbt::to_gzip_writer(&mut file, &schematic, Some("Schematic"))?; 296 | 297 | Ok(()) 298 | } 299 | -------------------------------------------------------------------------------- /crates/core/src/profile.rs: -------------------------------------------------------------------------------- 1 | use crate::utils::HyphenatedUUID; 2 | use anyhow::Result; 3 | use serde::Deserialize; 4 | 5 | #[derive(Debug, Deserialize)] 6 | pub struct PlayerProfile { 7 | #[serde(rename = "id")] 8 | pub uuid: HyphenatedUUID, 9 | #[serde(rename = "name")] 10 | pub username: String, 11 | } 12 | 13 | impl PlayerProfile { 14 | pub async fn lookup_by_username(username: &str) -> Result { 15 | let url = format!( 16 | "https://api.mojang.com/users/profiles/minecraft/{}", 17 | username 18 | ); 19 | let client = reqwest::Client::new(); 20 | let res = client 21 | .get(url) 22 | .send() 23 | .await? 24 | .json::() 25 | .await?; 26 | Ok(res) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /crates/core/src/utils.rs: -------------------------------------------------------------------------------- 1 | use mchprs_blocks::block_entities::InventoryEntry; 2 | use mchprs_blocks::items::{Item, ItemStack}; 3 | use mchprs_network::packets::SlotData; 4 | use serde::de::Visitor; 5 | use serde::{Deserialize, Serialize}; 6 | use std::io::Cursor; 7 | use std::num::ParseIntError; 8 | use std::str::FromStr; 9 | 10 | #[derive(Debug)] 11 | pub struct HyphenatedUUID(pub u128); 12 | 13 | impl ToString for HyphenatedUUID { 14 | fn to_string(&self) -> String { 15 | let mut hex = format!("{:032x}", self.0); 16 | hex.insert(8, '-'); 17 | hex.insert(13, '-'); 18 | hex.insert(18, '-'); 19 | hex.insert(23, '-'); 20 | hex 21 | } 22 | } 23 | 24 | impl FromStr for HyphenatedUUID { 25 | type Err = ParseIntError; 26 | fn from_str(s: &str) -> Result { 27 | let hex = s.replace('-', ""); 28 | Ok(HyphenatedUUID(u128::from_str_radix(&hex, 16)?)) 29 | } 30 | } 31 | 32 | impl Serialize for HyphenatedUUID { 33 | fn serialize(&self, serializer: S) -> Result 34 | where 35 | S: serde::Serializer, 36 | { 37 | serializer.serialize_str(&self.to_string()) 38 | } 39 | } 40 | 41 | struct HyphenatedUUIDVisitor; 42 | 43 | impl<'de> Visitor<'de> for HyphenatedUUIDVisitor { 44 | type Value = HyphenatedUUID; 45 | 46 | fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 47 | formatter.write_str("a hyphenated uuid string") 48 | } 49 | 50 | fn visit_str(self, v: &str) -> Result 51 | where 52 | E: serde::de::Error, 53 | { 54 | v.parse().map_err(E::custom) 55 | } 56 | } 57 | 58 | impl<'de> Deserialize<'de> for HyphenatedUUID { 59 | fn deserialize(deserializer: D) -> Result 60 | where 61 | D: serde::Deserializer<'de>, 62 | { 63 | deserializer.deserialize_str(HyphenatedUUIDVisitor) 64 | } 65 | } 66 | 67 | pub fn encode_slot_data(item: &ItemStack) -> SlotData { 68 | SlotData { 69 | item_count: item.count as i8, 70 | item_id: item.item_type.get_id() as i32, 71 | nbt: item.nbt.clone().map(|nbt| nbt.content), 72 | } 73 | } 74 | 75 | pub fn inventory_entry_to_stack(entry: &InventoryEntry) -> ItemStack { 76 | let nbt = entry 77 | .nbt 78 | .clone() 79 | .map(|data| nbt::Blob::from_reader(&mut Cursor::new(data)).unwrap()); 80 | ItemStack { 81 | item_type: Item::from_id(entry.id), 82 | count: entry.count as u8, 83 | nbt, 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /crates/network/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "mchprs_network" 3 | authors.workspace = true 4 | description.workspace = true 5 | edition.workspace = true 6 | homepage.workspace = true 7 | keywords.workspace = true 8 | readme.workspace = true 9 | version.workspace = true 10 | license.workspace = true 11 | repository.workspace = true 12 | 13 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 14 | 15 | [dependencies] 16 | mchprs_text = { path = "../text" } 17 | hematite-nbt = { workspace = true } 18 | flate2 = { workspace = true } 19 | serde = { workspace = true } 20 | byteorder = { workspace = true } 21 | tracing = { workspace = true } 22 | bitvec = { workspace = true } 23 | -------------------------------------------------------------------------------- /crates/network/src/lib.rs: -------------------------------------------------------------------------------- 1 | mod nbt_util; 2 | pub mod packets; 3 | 4 | use packets::serverbound::ServerBoundPacket; 5 | use packets::{read_packet, PacketEncoder, PlayerProperty}; 6 | use std::net::{Shutdown, TcpListener, TcpStream}; 7 | use std::sync::atomic::{AtomicBool, Ordering}; 8 | use std::sync::{mpsc, Arc}; 9 | use std::thread; 10 | use tracing::warn; 11 | 12 | pub use nbt_util::NBTCompound; 13 | 14 | #[derive(Debug)] 15 | pub struct PlayerPacketSender { 16 | stream: Option, 17 | } 18 | 19 | impl PlayerPacketSender { 20 | pub fn new(conn: &PlayerConn) -> PlayerPacketSender { 21 | let stream = conn.client.stream.try_clone().ok(); 22 | if stream.is_none() { 23 | warn!("Creating PlayerPacketSender with dead stream") 24 | } 25 | PlayerPacketSender { stream } 26 | } 27 | 28 | pub fn send_packet(&self, data: &PacketEncoder) { 29 | if let Some(stream) = &self.stream { 30 | // Going to assume stream is compressed since it should be after login 31 | let _ = data.write_compressed(stream); 32 | } 33 | } 34 | } 35 | 36 | /// The minecraft protocol has these 4 different states. 37 | #[derive(PartialEq, Eq, Clone)] 38 | pub enum NetworkState { 39 | Handshaking, 40 | Status, 41 | Login, 42 | Configuration, 43 | Play, 44 | } 45 | 46 | pub struct HandshakingConn { 47 | client: NetworkClient, 48 | pub username: Option, 49 | pub uuid: Option, 50 | pub forwarding_message_id: Option, 51 | pub properties: Vec, 52 | } 53 | 54 | impl HandshakingConn { 55 | pub fn send_packet(&self, data: &PacketEncoder) { 56 | self.client.send_packet(data); 57 | } 58 | 59 | pub fn receive_packets(&self) -> Vec> { 60 | self.client.receive_packets(&mut true) 61 | } 62 | 63 | pub fn set_compressed(&self, compressed: bool) { 64 | self.client.compressed.store(compressed, Ordering::Relaxed) 65 | } 66 | 67 | pub fn close_connection(&self) { 68 | self.client.close_connection(); 69 | } 70 | } 71 | 72 | impl From for PlayerConn { 73 | fn from(conn: HandshakingConn) -> Self { 74 | PlayerConn { 75 | client: conn.client, 76 | alive: true, 77 | } 78 | } 79 | } 80 | 81 | pub struct PlayerConn { 82 | client: NetworkClient, 83 | alive: bool, 84 | } 85 | 86 | impl PlayerConn { 87 | pub fn send_packet(&self, data: &PacketEncoder) { 88 | self.client.send_packet(data); 89 | } 90 | 91 | pub fn receive_packets(&mut self) -> Vec> { 92 | self.client.receive_packets(&mut self.alive) 93 | } 94 | 95 | pub fn alive(&self) -> bool { 96 | self.alive 97 | } 98 | 99 | pub fn close_connection(&mut self) { 100 | self.alive = false; 101 | self.client.close_connection(); 102 | } 103 | } 104 | 105 | /// This handles the TCP stream. 106 | pub struct NetworkClient { 107 | /// All NetworkClients are identified by this id. 108 | /// If the client is a player, the player's entitiy id becomes the same. 109 | pub id: u32, 110 | stream: TcpStream, 111 | packets: mpsc::Receiver>, 112 | compressed: Arc, 113 | } 114 | 115 | impl NetworkClient { 116 | fn listen( 117 | mut stream: TcpStream, 118 | sender: mpsc::Sender>, 119 | compressed: Arc, 120 | ) { 121 | let mut state = NetworkState::Handshaking; 122 | loop { 123 | let packet = match read_packet(&mut stream, &compressed, &mut state) { 124 | Ok(packet) => packet, 125 | // This will cause the client to disconnect 126 | Err(_) => return, 127 | }; 128 | if sender.send(packet).is_err() { 129 | return; 130 | } 131 | } 132 | } 133 | 134 | pub fn receive_packets(&self, alive: &mut bool) -> Vec> { 135 | let mut packets = Vec::new(); 136 | loop { 137 | let packet = self.packets.try_recv(); 138 | match packet { 139 | Ok(packet) => packets.push(packet), 140 | Err(mpsc::TryRecvError::Empty) => break, 141 | _ => { 142 | *alive = false; 143 | break; 144 | } 145 | } 146 | } 147 | packets 148 | } 149 | 150 | pub fn send_packet(&self, data: &PacketEncoder) { 151 | // TODO: every call to `send_packet` with the same PacketEncoder will 152 | // lead to re-encoding the packet. It might be good to cache this. 153 | if self.compressed.load(Ordering::Relaxed) { 154 | let _ = data.write_compressed(&self.stream); 155 | } else { 156 | let _ = data.write_uncompressed(&self.stream); 157 | } 158 | } 159 | 160 | pub fn close_connection(&self) { 161 | let _ = self.stream.shutdown(Shutdown::Both); 162 | } 163 | } 164 | 165 | /// This represents the network portion of a minecraft server 166 | pub struct NetworkServer { 167 | client_receiver: mpsc::Receiver, 168 | /// These clients are either in the handshake, login, or ping state, once they shift to play, they will be moved to a plot 169 | pub handshaking_clients: Vec, 170 | } 171 | 172 | impl NetworkServer { 173 | fn listen(bind_address: &str, sender: mpsc::Sender) { 174 | let listener = TcpListener::bind(bind_address).unwrap(); 175 | 176 | for (index, stream) in listener.incoming().enumerate() { 177 | let stream = stream.unwrap(); 178 | let (packet_sender, packet_receiver) = mpsc::channel(); 179 | let compressed = Arc::new(AtomicBool::new(false)); 180 | let client_stream = stream.try_clone().unwrap(); 181 | let client_compressed = compressed.clone(); 182 | thread::spawn(move || { 183 | NetworkClient::listen(client_stream, packet_sender, client_compressed); 184 | }); 185 | sender 186 | .send(NetworkClient { 187 | // The index will increment after each client making it unique. We'll just use this as the enitity id. 188 | id: index as u32, 189 | stream, 190 | packets: packet_receiver, 191 | compressed, 192 | }) 193 | .unwrap(); 194 | } 195 | } 196 | 197 | /// Creates a new `NetworkServer`. The server will then start accepting TCP clients. 198 | pub fn new(bind_address: String) -> NetworkServer { 199 | let (sender, receiver) = mpsc::channel(); 200 | thread::spawn(move || NetworkServer::listen(&bind_address, sender)); 201 | NetworkServer { 202 | client_receiver: receiver, 203 | handshaking_clients: Vec::new(), 204 | } 205 | } 206 | 207 | pub fn update(&mut self) { 208 | loop { 209 | match self.client_receiver.try_recv() { 210 | Ok(client) => self.handshaking_clients.push(HandshakingConn { 211 | client, 212 | username: None, 213 | uuid: None, 214 | forwarding_message_id: None, 215 | properties: vec![], 216 | }), 217 | Err(mpsc::TryRecvError::Empty) => break, 218 | Err(mpsc::TryRecvError::Disconnected) => { 219 | panic!("Client receiver channel disconnected!"); 220 | } 221 | } 222 | } 223 | } 224 | } 225 | -------------------------------------------------------------------------------- /crates/network/src/nbt_util.rs: -------------------------------------------------------------------------------- 1 | use serde::Serialize; 2 | 3 | pub type NBTCompound = nbt::Map; 4 | 5 | #[derive(Serialize, Clone)] 6 | struct NBTMapEntry { 7 | name: String, 8 | id: i32, 9 | element: T, 10 | } 11 | 12 | /// This is a format used in the current network protocol, 13 | /// most notably used in the `JoinGame` packet. 14 | #[derive(Serialize, Clone)] 15 | pub struct NBTMap { 16 | #[serde(rename = "type")] 17 | self_type: String, 18 | value: Vec>, 19 | } 20 | 21 | impl NBTMap { 22 | pub fn new(self_type: String) -> NBTMap { 23 | NBTMap { 24 | self_type, 25 | value: Vec::new(), 26 | } 27 | } 28 | 29 | pub fn push_element(&mut self, name: String, element: T) { 30 | let id = self.value.len() as i32; 31 | self.value.push(NBTMapEntry { name, id, element }); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /crates/proc_macros/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "mchprs_proc_macros" 3 | authors.workspace = true 4 | description.workspace = true 5 | edition.workspace = true 6 | homepage.workspace = true 7 | keywords.workspace = true 8 | readme.workspace = true 9 | version.workspace = true 10 | license.workspace = true 11 | repository.workspace = true 12 | 13 | [lib] 14 | proc-macro = true 15 | 16 | [dependencies] 17 | syn = { workspace = true } 18 | quote = { workspace = true } -------------------------------------------------------------------------------- /crates/proc_macros/src/lib.rs: -------------------------------------------------------------------------------- 1 | use proc_macro::TokenStream; 2 | use quote::quote; 3 | use syn::{parse_macro_input, Data, DeriveInput, Error, Ident, Type}; 4 | 5 | #[proc_macro_derive(BlockProperty)] 6 | pub fn derive_block_property(input: TokenStream) -> TokenStream { 7 | let input = parse_macro_input!(input as DeriveInput); 8 | 9 | match create_block_property_impl(input) { 10 | Ok(ts) => ts, 11 | Err(err) => err.to_compile_error().into(), 12 | } 13 | } 14 | 15 | fn create_block_property_impl(input: DeriveInput) -> Result { 16 | let fields = match input.data { 17 | Data::Struct(ds) => ds.fields, 18 | _ => { 19 | return Err(Error::new_spanned( 20 | input, 21 | "BlockProperty proxy type must be a struct", 22 | )) 23 | } 24 | }; 25 | let field_types: Vec<&Type> = fields.iter().map(|f| &f.ty).collect(); 26 | let field_names: Vec<&Ident> = fields.iter().map(|f| f.ident.as_ref().unwrap()).collect(); 27 | let struct_name = input.ident; 28 | 29 | let tokens = quote! { 30 | impl BlockProperty for #struct_name { 31 | fn encode(self, props: &mut ::std::collections::HashMap<&'static str, String>, _name: &'static str) { 32 | #( 33 | <#field_types as BlockProperty>::encode(self.#field_names, props, stringify!(#field_names)); 34 | )* 35 | } 36 | 37 | fn decode(&mut self, props: &::std::collections::HashMap<&str, &str>, _name: &str) { 38 | #( 39 | <#field_types as BlockProperty>::decode(&mut self.#field_names, props, stringify!(#field_names)); 40 | )* 41 | } 42 | } 43 | }; 44 | Ok(tokens.into()) 45 | } 46 | 47 | #[proc_macro_derive(BlockTransform)] 48 | pub fn derive_block_transform(input: TokenStream) -> TokenStream { 49 | let input = parse_macro_input!(input as DeriveInput); 50 | 51 | match create_block_transform_impl(input) { 52 | Ok(ts) => ts, 53 | Err(err) => err.to_compile_error().into(), 54 | } 55 | } 56 | 57 | fn create_block_transform_impl(input: DeriveInput) -> Result { 58 | let fields = match input.data { 59 | Data::Struct(ds) => ds.fields, 60 | _ => { 61 | return Err(Error::new_spanned( 62 | input, 63 | "BlockTransform proxy type must be a struct", 64 | )) 65 | } 66 | }; 67 | let field_types: Vec<&Type> = fields.iter().map(|f| &f.ty).collect(); 68 | let field_names: Vec<&Ident> = fields.iter().map(|f| f.ident.as_ref().unwrap()).collect(); 69 | let struct_name = input.ident; 70 | 71 | let tokens = quote! { 72 | impl crate::blocks::BlockTransform for #struct_name { 73 | fn rotate90(&mut self) { 74 | #( 75 | <#field_types as crate::blocks::BlockTransform>::rotate90(&mut self.#field_names); 76 | )* 77 | } 78 | 79 | fn flip(&mut self, dir: crate::blocks::FlipDirection) { 80 | #( 81 | <#field_types as crate::blocks::BlockTransform>::flip(&mut self.#field_names, dir); 82 | )* 83 | } 84 | 85 | } 86 | }; 87 | Ok(tokens.into()) 88 | } 89 | -------------------------------------------------------------------------------- /crates/redpiler/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "mchprs_redpiler" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | mchprs_blocks = { path = "../blocks" } 10 | mchprs_world = { path = "../world" } 11 | mchprs_redstone = { path = "../redstone" } 12 | redpiler_graph = { path = "../redpiler_graph" } 13 | serde_json = { workspace = true } 14 | tracing = { workspace = true } 15 | petgraph = { workspace = true } 16 | itertools = { workspace = true } 17 | rustc-hash = { workspace = true } 18 | smallvec = { workspace = true } 19 | enum_dispatch = { workspace = true } 20 | -------------------------------------------------------------------------------- /crates/redpiler/src/backend/direct/compile.rs: -------------------------------------------------------------------------------- 1 | use crate::compile_graph::{CompileGraph, LinkType, NodeIdx}; 2 | use crate::{CompilerOptions, TaskMonitor}; 3 | use itertools::Itertools; 4 | use mchprs_blocks::blocks::{Block, Instrument}; 5 | use mchprs_blocks::BlockPos; 6 | use mchprs_world::TickEntry; 7 | use petgraph::visit::EdgeRef; 8 | use petgraph::Direction; 9 | use rustc_hash::FxHashMap; 10 | use smallvec::SmallVec; 11 | use std::sync::Arc; 12 | use tracing::trace; 13 | 14 | use super::node::{ForwardLink, Node, NodeId, NodeInput, NodeType, Nodes, NonMaxU8}; 15 | use super::DirectBackend; 16 | 17 | #[derive(Debug, Default)] 18 | struct FinalGraphStats { 19 | update_link_count: usize, 20 | side_link_count: usize, 21 | default_link_count: usize, 22 | nodes_bytes: usize, 23 | } 24 | 25 | fn compile_node( 26 | graph: &CompileGraph, 27 | node_idx: NodeIdx, 28 | nodes_len: usize, 29 | nodes_map: &FxHashMap, 30 | noteblock_info: &mut Vec<(BlockPos, Instrument, u32)>, 31 | stats: &mut FinalGraphStats, 32 | ) -> Node { 33 | let node = &graph[node_idx]; 34 | 35 | const MAX_INPUTS: usize = 255; 36 | 37 | let mut default_input_count = 0; 38 | let mut side_input_count = 0; 39 | 40 | let mut default_inputs = NodeInput { ss_counts: [0; 16] }; 41 | let mut side_inputs = NodeInput { ss_counts: [0; 16] }; 42 | for edge in graph.edges_directed(node_idx, Direction::Incoming) { 43 | let weight = edge.weight(); 44 | let distance = weight.ss; 45 | let source = edge.source(); 46 | let ss = graph[source].state.output_strength.saturating_sub(distance); 47 | match weight.ty { 48 | LinkType::Default => { 49 | if default_input_count >= MAX_INPUTS { 50 | panic!( 51 | "Exceeded the maximum number of default inputs {}", 52 | MAX_INPUTS 53 | ); 54 | } 55 | default_input_count += 1; 56 | default_inputs.ss_counts[ss as usize] += 1; 57 | } 58 | LinkType::Side => { 59 | if side_input_count >= MAX_INPUTS { 60 | panic!("Exceeded the maximum number of side inputs {}", MAX_INPUTS); 61 | } 62 | side_input_count += 1; 63 | side_inputs.ss_counts[ss as usize] += 1; 64 | } 65 | } 66 | } 67 | stats.default_link_count += default_input_count; 68 | stats.side_link_count += side_input_count; 69 | 70 | use crate::compile_graph::NodeType as CNodeType; 71 | let updates = if node.ty != CNodeType::Constant { 72 | graph 73 | .edges_directed(node_idx, Direction::Outgoing) 74 | .sorted_by_key(|edge| nodes_map[&edge.target()]) 75 | .into_group_map_by(|edge| std::mem::discriminant(&graph[edge.target()].ty)) 76 | .into_values() 77 | .flatten() 78 | .map(|edge| unsafe { 79 | let idx = edge.target(); 80 | let idx = nodes_map[&idx]; 81 | assert!(idx < nodes_len); 82 | // Safety: bounds checked 83 | let target_id = NodeId::from_index(idx); 84 | 85 | let weight = edge.weight(); 86 | ForwardLink::new(target_id, weight.ty == LinkType::Side, weight.ss) 87 | }) 88 | .collect() 89 | } else { 90 | SmallVec::new() 91 | }; 92 | stats.update_link_count += updates.len(); 93 | 94 | let ty = match &node.ty { 95 | CNodeType::Repeater { 96 | delay, 97 | facing_diode, 98 | } => NodeType::Repeater { 99 | delay: *delay, 100 | facing_diode: *facing_diode, 101 | }, 102 | CNodeType::Torch => NodeType::Torch, 103 | CNodeType::Comparator { 104 | mode, 105 | far_input, 106 | facing_diode, 107 | } => NodeType::Comparator { 108 | mode: *mode, 109 | far_input: far_input.map(|value| NonMaxU8::new(value).unwrap()), 110 | facing_diode: *facing_diode, 111 | }, 112 | CNodeType::Lamp => NodeType::Lamp, 113 | CNodeType::Button => NodeType::Button, 114 | CNodeType::Lever => NodeType::Lever, 115 | CNodeType::PressurePlate => NodeType::PressurePlate, 116 | CNodeType::Trapdoor => NodeType::Trapdoor, 117 | CNodeType::Wire => NodeType::Wire, 118 | CNodeType::Constant => NodeType::Constant, 119 | CNodeType::NoteBlock { instrument, note } => { 120 | let noteblock_id = noteblock_info.len().try_into().unwrap(); 121 | noteblock_info.push((node.block.unwrap().0, *instrument, *note)); 122 | NodeType::NoteBlock { noteblock_id } 123 | } 124 | }; 125 | 126 | Node { 127 | ty, 128 | default_inputs, 129 | side_inputs, 130 | updates, 131 | powered: node.state.powered, 132 | output_power: node.state.output_strength, 133 | locked: node.state.repeater_locked, 134 | pending_tick: false, 135 | changed: false, 136 | is_io: node.is_input || node.is_output, 137 | } 138 | } 139 | 140 | pub fn compile( 141 | backend: &mut DirectBackend, 142 | graph: CompileGraph, 143 | ticks: Vec, 144 | options: &CompilerOptions, 145 | _monitor: Arc, 146 | ) { 147 | // Create a mapping from compile to backend node indices 148 | let mut nodes_map = FxHashMap::with_capacity_and_hasher(graph.node_count(), Default::default()); 149 | for node in graph.node_indices() { 150 | nodes_map.insert(node, nodes_map.len()); 151 | } 152 | let nodes_len = nodes_map.len(); 153 | 154 | // Lower nodes 155 | let mut stats = FinalGraphStats::default(); 156 | let nodes = graph 157 | .node_indices() 158 | .map(|idx| { 159 | compile_node( 160 | &graph, 161 | idx, 162 | nodes_len, 163 | &nodes_map, 164 | &mut backend.noteblock_info, 165 | &mut stats, 166 | ) 167 | }) 168 | .collect(); 169 | stats.nodes_bytes = nodes_len * std::mem::size_of::(); 170 | trace!("{:#?}", stats); 171 | 172 | backend.blocks = graph 173 | .node_weights() 174 | .map(|node| node.block.map(|(pos, id)| (pos, Block::from_id(id)))) 175 | .collect(); 176 | backend.nodes = Nodes::new(nodes); 177 | 178 | // Create a mapping from block pos to backend NodeId 179 | for i in 0..backend.blocks.len() { 180 | if let Some((pos, _)) = backend.blocks[i] { 181 | backend.pos_map.insert(pos, backend.nodes.get(i)); 182 | } 183 | } 184 | 185 | // Schedule backend ticks 186 | for entry in ticks { 187 | if let Some(node) = backend.pos_map.get(&entry.pos) { 188 | backend 189 | .scheduler 190 | .schedule_tick(*node, entry.ticks_left as usize, entry.tick_priority); 191 | backend.nodes[*node].pending_tick = true; 192 | } 193 | } 194 | 195 | // Dot file output 196 | if options.export_dot_graph { 197 | std::fs::write("backend_graph.dot", format!("{}", backend)).unwrap(); 198 | } 199 | } 200 | -------------------------------------------------------------------------------- /crates/redpiler/src/backend/direct/node.rs: -------------------------------------------------------------------------------- 1 | use mchprs_blocks::blocks::ComparatorMode; 2 | use smallvec::SmallVec; 3 | use std::num::NonZeroU8; 4 | use std::ops::{Index, IndexMut}; 5 | 6 | #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] 7 | pub struct NodeId(u32); 8 | 9 | impl NodeId { 10 | pub fn index(self) -> usize { 11 | self.0 as usize 12 | } 13 | 14 | /// Safety: index must be within bounds of nodes array 15 | pub unsafe fn from_index(index: usize) -> NodeId { 16 | NodeId(index as u32) 17 | } 18 | } 19 | 20 | // This is Pretty Bad:tm: because one can create a NodeId using another instance of Nodes, 21 | // but at least some type system protection is better than none. 22 | #[derive(Default)] 23 | pub struct Nodes { 24 | pub nodes: Box<[Node]>, 25 | } 26 | 27 | impl Nodes { 28 | pub fn new(nodes: Box<[Node]>) -> Nodes { 29 | Nodes { nodes } 30 | } 31 | 32 | pub fn get(&self, idx: usize) -> NodeId { 33 | if self.nodes.get(idx).is_some() { 34 | NodeId(idx as u32) 35 | } else { 36 | panic!("node index out of bounds: {}", idx) 37 | } 38 | } 39 | 40 | pub fn inner(&self) -> &[Node] { 41 | &self.nodes 42 | } 43 | 44 | pub fn inner_mut(&mut self) -> &mut [Node] { 45 | &mut self.nodes 46 | } 47 | 48 | pub fn into_inner(self) -> Box<[Node]> { 49 | self.nodes 50 | } 51 | } 52 | 53 | impl Index for Nodes { 54 | type Output = Node; 55 | 56 | // The index here MUST have been created by this instance, otherwise scary things will happen ! 57 | fn index(&self, index: NodeId) -> &Self::Output { 58 | unsafe { self.nodes.get_unchecked(index.0 as usize) } 59 | } 60 | } 61 | 62 | impl IndexMut for Nodes { 63 | fn index_mut(&mut self, index: NodeId) -> &mut Self::Output { 64 | unsafe { self.nodes.get_unchecked_mut(index.0 as usize) } 65 | } 66 | } 67 | 68 | #[derive(Clone, Copy)] 69 | pub struct ForwardLink { 70 | data: u32, 71 | } 72 | 73 | impl ForwardLink { 74 | pub fn new(id: NodeId, side: bool, ss: u8) -> Self { 75 | assert!(id.index() < (1 << 27)); 76 | // the clamp_weights compile pass should ensure ss < 15 77 | assert!(ss < 15); 78 | Self { 79 | data: (id.index() as u32) << 5 | if side { 1 << 4 } else { 0 } | ss as u32, 80 | } 81 | } 82 | 83 | pub fn node(self) -> NodeId { 84 | unsafe { 85 | // safety: ForwardLink is constructed using a NodeId 86 | NodeId::from_index((self.data >> 5) as usize) 87 | } 88 | } 89 | 90 | pub fn side(self) -> bool { 91 | self.data & (1 << 4) != 0 92 | } 93 | 94 | pub fn ss(self) -> u8 { 95 | (self.data & 0b1111) as u8 96 | } 97 | } 98 | 99 | impl std::fmt::Debug for ForwardLink { 100 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 101 | f.debug_struct("ForwardLink") 102 | .field("node", &self.node()) 103 | .field("side", &self.side()) 104 | .field("ss", &self.ss()) 105 | .finish() 106 | } 107 | } 108 | 109 | #[derive(Debug, Clone, Copy)] 110 | pub enum NodeType { 111 | Repeater { 112 | delay: u8, 113 | facing_diode: bool, 114 | }, 115 | Torch, 116 | Comparator { 117 | mode: ComparatorMode, 118 | far_input: Option, 119 | facing_diode: bool, 120 | }, 121 | Lamp, 122 | Button, 123 | Lever, 124 | PressurePlate, 125 | Trapdoor, 126 | Wire, 127 | Constant, 128 | NoteBlock { 129 | noteblock_id: u16, 130 | }, 131 | } 132 | 133 | #[repr(align(16))] 134 | #[derive(Debug, Clone, Default)] 135 | pub struct NodeInput { 136 | pub ss_counts: [u8; 16], 137 | } 138 | 139 | #[derive(Debug, Clone, Copy)] 140 | pub struct NonMaxU8(NonZeroU8); 141 | 142 | impl NonMaxU8 { 143 | pub fn new(value: u8) -> Option { 144 | NonZeroU8::new(value + 1).map(|x| Self(x)) 145 | } 146 | 147 | pub fn get(self) -> u8 { 148 | self.0.get() - 1 149 | } 150 | } 151 | 152 | #[derive(Debug, Clone)] 153 | pub struct Node { 154 | pub ty: NodeType, 155 | pub default_inputs: NodeInput, 156 | pub side_inputs: NodeInput, 157 | pub updates: SmallVec<[ForwardLink; 10]>, 158 | pub is_io: bool, 159 | 160 | /// Powered or lit 161 | pub powered: bool, 162 | /// Only for repeaters 163 | pub locked: bool, 164 | pub output_power: u8, 165 | pub changed: bool, 166 | pub pending_tick: bool, 167 | } 168 | -------------------------------------------------------------------------------- /crates/redpiler/src/backend/direct/tick.rs: -------------------------------------------------------------------------------- 1 | use super::node::NodeId; 2 | use super::*; 3 | 4 | impl DirectBackend { 5 | pub fn tick_node(&mut self, node_id: NodeId) { 6 | let node = &mut self.nodes[node_id]; 7 | node.pending_tick = false; 8 | 9 | match node.ty { 10 | NodeType::Repeater { delay, .. } => { 11 | if node.locked { 12 | return; 13 | } 14 | 15 | let should_be_powered = get_bool_input(node); 16 | if node.powered && !should_be_powered { 17 | self.set_node(node_id, false, 0); 18 | } else if !node.powered { 19 | if !should_be_powered { 20 | schedule_tick( 21 | &mut self.scheduler, 22 | node_id, 23 | node, 24 | delay as usize, 25 | TickPriority::Higher, 26 | ); 27 | } 28 | self.set_node(node_id, true, 15); 29 | } 30 | } 31 | NodeType::Torch => { 32 | let should_be_powered = !get_bool_input(node); 33 | if node.powered != should_be_powered { 34 | self.set_node(node_id, should_be_powered, bool_to_ss(should_be_powered)); 35 | } 36 | } 37 | NodeType::Comparator { 38 | mode, far_input, .. 39 | } => { 40 | let (mut input_power, side_input_power) = get_all_input(node); 41 | if let Some(far_override) = far_input { 42 | if input_power < 15 { 43 | input_power = far_override.get(); 44 | } 45 | } 46 | let old_strength = node.output_power; 47 | let new_strength = calculate_comparator_output(mode, input_power, side_input_power); 48 | if new_strength != old_strength { 49 | self.set_node(node_id, new_strength > 0, new_strength); 50 | } 51 | } 52 | NodeType::Lamp => { 53 | let should_be_lit = get_bool_input(node); 54 | if node.powered && !should_be_lit { 55 | self.set_node(node_id, false, 0); 56 | } 57 | } 58 | NodeType::Button => { 59 | if node.powered { 60 | self.set_node(node_id, false, 0); 61 | } 62 | } 63 | _ => {} //unreachable!("Node {:?} should not be ticked!", node.ty), 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /crates/redpiler/src/backend/direct/update.rs: -------------------------------------------------------------------------------- 1 | use mchprs_world::TickPriority; 2 | 3 | use super::node::{NodeId, NodeType}; 4 | use super::*; 5 | 6 | #[inline(always)] 7 | pub(super) fn update_node( 8 | scheduler: &mut TickScheduler, 9 | events: &mut Vec, 10 | nodes: &mut Nodes, 11 | node_id: NodeId, 12 | ) { 13 | let node = &mut nodes[node_id]; 14 | 15 | match node.ty { 16 | NodeType::Repeater { 17 | delay, 18 | facing_diode, 19 | } => { 20 | let should_be_locked = get_bool_side(node); 21 | if should_be_locked != node.locked { 22 | set_node_locked(node, should_be_locked); 23 | } 24 | if node.locked || node.pending_tick { 25 | return; 26 | } 27 | 28 | let should_be_powered = get_bool_input(node); 29 | if should_be_powered != node.powered { 30 | let priority = if facing_diode { 31 | TickPriority::Highest 32 | } else if !should_be_powered { 33 | TickPriority::Higher 34 | } else { 35 | TickPriority::High 36 | }; 37 | schedule_tick(scheduler, node_id, node, delay as usize, priority); 38 | } 39 | } 40 | NodeType::Torch => { 41 | if node.pending_tick { 42 | return; 43 | } 44 | let should_be_powered = !get_bool_input(node); 45 | if node.powered != should_be_powered { 46 | schedule_tick(scheduler, node_id, node, 1, TickPriority::Normal); 47 | } 48 | } 49 | NodeType::Comparator { 50 | mode, 51 | far_input, 52 | facing_diode, 53 | } => { 54 | if node.pending_tick { 55 | return; 56 | } 57 | let (mut input_power, side_input_power) = get_all_input(node); 58 | if let Some(far_override) = far_input { 59 | if input_power < 15 { 60 | input_power = far_override.get(); 61 | } 62 | } 63 | let old_strength = node.output_power; 64 | let output_power = calculate_comparator_output(mode, input_power, side_input_power); 65 | if output_power != old_strength { 66 | let priority = if facing_diode { 67 | TickPriority::High 68 | } else { 69 | TickPriority::Normal 70 | }; 71 | schedule_tick(scheduler, node_id, node, 1, priority); 72 | } 73 | } 74 | NodeType::Lamp => { 75 | let should_be_lit = get_bool_input(node); 76 | let lit = node.powered; 77 | if lit && !should_be_lit { 78 | schedule_tick(scheduler, node_id, node, 2, TickPriority::Normal); 79 | } else if !lit && should_be_lit { 80 | set_node(node, true); 81 | } 82 | } 83 | NodeType::Trapdoor => { 84 | let should_be_powered = get_bool_input(node); 85 | if node.powered != should_be_powered { 86 | set_node(node, should_be_powered); 87 | } 88 | } 89 | NodeType::Wire => { 90 | let (input_power, _) = get_all_input(node); 91 | if node.output_power != input_power { 92 | node.output_power = input_power; 93 | node.changed = true; 94 | } 95 | } 96 | NodeType::NoteBlock { noteblock_id } => { 97 | let should_be_powered = get_bool_input(node); 98 | if node.powered != should_be_powered { 99 | set_node(node, should_be_powered); 100 | if should_be_powered { 101 | events.push(Event::NoteBlockPlay { noteblock_id }); 102 | } 103 | } 104 | } 105 | _ => {} // unreachable!("Node {:?} should not be updated!", node.ty), 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /crates/redpiler/src/backend/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod direct; 2 | 3 | use std::sync::Arc; 4 | 5 | use super::compile_graph::CompileGraph; 6 | use super::task_monitor::TaskMonitor; 7 | use super::CompilerOptions; 8 | use enum_dispatch::enum_dispatch; 9 | use mchprs_blocks::BlockPos; 10 | use mchprs_world::{TickEntry, World}; 11 | 12 | #[enum_dispatch] 13 | pub trait JITBackend { 14 | fn compile( 15 | &mut self, 16 | graph: CompileGraph, 17 | ticks: Vec, 18 | options: &CompilerOptions, 19 | monitor: Arc, 20 | ); 21 | fn tick(&mut self); 22 | 23 | fn tickn(&mut self, ticks: u64) { 24 | for _ in 0..ticks { 25 | self.tick(); 26 | } 27 | } 28 | 29 | fn on_use_block(&mut self, pos: BlockPos); 30 | fn set_pressure_plate(&mut self, pos: BlockPos, powered: bool); 31 | fn flush(&mut self, world: &mut W, io_only: bool); 32 | fn reset(&mut self, world: &mut W, io_only: bool); 33 | fn has_pending_ticks(&self) -> bool; 34 | /// Inspect block for debugging 35 | fn inspect(&mut self, pos: BlockPos); 36 | } 37 | 38 | use direct::DirectBackend; 39 | 40 | #[enum_dispatch(JITBackend)] 41 | pub enum BackendDispatcher { 42 | DirectBackend, 43 | } 44 | -------------------------------------------------------------------------------- /crates/redpiler/src/compile_graph.rs: -------------------------------------------------------------------------------- 1 | use mchprs_blocks::blocks::{ComparatorMode, Instrument}; 2 | use mchprs_blocks::BlockPos; 3 | use petgraph::stable_graph::{NodeIndex, StableGraph}; 4 | 5 | pub type NodeIdx = NodeIndex; 6 | 7 | #[derive(Debug, Clone, PartialEq, Eq, Hash)] 8 | pub enum NodeType { 9 | Repeater { 10 | delay: u8, 11 | facing_diode: bool, 12 | }, 13 | Torch, 14 | Comparator { 15 | mode: ComparatorMode, 16 | far_input: Option, 17 | facing_diode: bool, 18 | }, 19 | Lamp, 20 | Button, 21 | Lever, 22 | PressurePlate, 23 | Trapdoor, 24 | Wire, 25 | Constant, 26 | NoteBlock { 27 | instrument: Instrument, 28 | note: u32, 29 | }, 30 | } 31 | 32 | #[derive(Debug, Clone, Default)] 33 | pub struct NodeState { 34 | pub powered: bool, 35 | pub repeater_locked: bool, 36 | pub output_strength: u8, 37 | } 38 | 39 | impl NodeState { 40 | pub fn simple(powered: bool) -> NodeState { 41 | NodeState { 42 | powered, 43 | output_strength: if powered { 15 } else { 0 }, 44 | ..Default::default() 45 | } 46 | } 47 | 48 | pub fn repeater(powered: bool, locked: bool) -> NodeState { 49 | NodeState { 50 | powered, 51 | repeater_locked: locked, 52 | output_strength: if powered { 15 } else { 0 }, 53 | } 54 | } 55 | 56 | pub fn ss(ss: u8) -> NodeState { 57 | NodeState { 58 | output_strength: ss, 59 | ..Default::default() 60 | } 61 | } 62 | 63 | pub fn comparator(powered: bool, ss: u8) -> NodeState { 64 | NodeState { 65 | powered, 66 | output_strength: ss, 67 | ..Default::default() 68 | } 69 | } 70 | } 71 | 72 | #[derive(Debug, Default)] 73 | pub struct Annotations {} 74 | 75 | #[derive(Debug)] 76 | pub struct CompileNode { 77 | pub ty: NodeType, 78 | pub block: Option<(BlockPos, u32)>, 79 | pub state: NodeState, 80 | 81 | pub is_input: bool, 82 | pub is_output: bool, 83 | pub annotations: Annotations, 84 | } 85 | 86 | impl CompileNode { 87 | pub fn is_removable(&self) -> bool { 88 | !self.is_input && !self.is_output 89 | } 90 | } 91 | 92 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 93 | pub enum LinkType { 94 | Default, 95 | Side, 96 | } 97 | 98 | #[derive(Debug)] 99 | pub struct CompileLink { 100 | pub ty: LinkType, 101 | pub ss: u8, 102 | } 103 | 104 | impl CompileLink { 105 | pub fn new(ty: LinkType, ss: u8) -> CompileLink { 106 | CompileLink { ty, ss } 107 | } 108 | 109 | pub fn default(ss: u8) -> CompileLink { 110 | CompileLink { 111 | ty: LinkType::Default, 112 | ss, 113 | } 114 | } 115 | 116 | pub fn side(ss: u8) -> CompileLink { 117 | CompileLink { 118 | ty: LinkType::Side, 119 | ss, 120 | } 121 | } 122 | } 123 | 124 | pub type CompileGraph = StableGraph; 125 | -------------------------------------------------------------------------------- /crates/redpiler/src/lib.rs: -------------------------------------------------------------------------------- 1 | mod backend; 2 | mod compile_graph; 3 | mod task_monitor; 4 | // mod debug_graph; 5 | mod passes; 6 | 7 | use backend::{BackendDispatcher, JITBackend}; 8 | use mchprs_blocks::blocks::Block; 9 | use mchprs_blocks::BlockPos; 10 | use mchprs_world::TickEntry; 11 | use mchprs_world::{for_each_block_mut_optimized, World}; 12 | use passes::make_default_pass_manager; 13 | use std::sync::Arc; 14 | use std::time::Instant; 15 | use tracing::{debug, error, trace, warn}; 16 | 17 | pub use task_monitor::TaskMonitor; 18 | 19 | fn block_powered_mut(block: &mut Block) -> Option<&mut bool> { 20 | Some(match block { 21 | Block::RedstoneComparator { comparator } => &mut comparator.powered, 22 | Block::RedstoneTorch { lit } => lit, 23 | Block::RedstoneWallTorch { lit, .. } => lit, 24 | Block::RedstoneRepeater { repeater } => &mut repeater.powered, 25 | Block::Lever { lever } => &mut lever.powered, 26 | Block::StoneButton { button } => &mut button.powered, 27 | Block::StonePressurePlate { powered } => powered, 28 | Block::RedstoneLamp { lit } => lit, 29 | Block::IronTrapdoor { powered, .. } => powered, 30 | Block::NoteBlock { powered, .. } => powered, 31 | _ => return None, 32 | }) 33 | } 34 | 35 | #[derive(Default, PartialEq, Eq, Debug, Clone)] 36 | pub struct CompilerOptions { 37 | /// Enable optimization passes which may significantly increase compile times. 38 | pub optimize: bool, 39 | /// Export the graph to a binary format. See the [`redpiler_graph`] crate. 40 | pub export: bool, 41 | /// Only flush lamp, button, lever, pressure plate, or trapdoor updates. 42 | pub io_only: bool, 43 | /// Update all blocks in the input region after reset. 44 | pub update: bool, 45 | /// Export a dot file of the graph after backend compile (backend dependent) 46 | pub export_dot_graph: bool, 47 | /// Consider a redstone dot to be an output block (for color screens) 48 | pub wire_dot_out: bool, 49 | /// The backend variant to be used after compilation 50 | pub backend_variant: BackendVariant, 51 | } 52 | 53 | #[derive(Debug, Default, PartialEq, Eq, Clone, Copy)] 54 | pub enum BackendVariant { 55 | #[default] 56 | Direct, 57 | } 58 | 59 | impl CompilerOptions { 60 | pub fn parse(str: &str) -> CompilerOptions { 61 | let mut co: CompilerOptions = Default::default(); 62 | let options = str.split_whitespace(); 63 | for option in options { 64 | if option.starts_with("--") { 65 | match option { 66 | "--optimize" => co.optimize = true, 67 | "--export" => co.export = true, 68 | "--io-only" => co.io_only = true, 69 | "--update" => co.update = true, 70 | "--export-dot" => co.export_dot_graph = true, 71 | "--wire-dot-out" => co.wire_dot_out = true, 72 | // FIXME: use actual error handling 73 | _ => warn!("Unrecognized option: {}", option), 74 | } 75 | } else if let Some(str) = option.strip_prefix('-') { 76 | for c in str.chars() { 77 | let lower = c.to_lowercase().to_string(); 78 | match lower.as_str() { 79 | "o" => co.optimize = true, 80 | "e" => co.export = true, 81 | "i" => co.io_only = true, 82 | "u" => co.update = true, 83 | "d" => co.wire_dot_out = true, 84 | // FIXME: use actual error handling 85 | _ => warn!("Unrecognized option: -{}", c), 86 | } 87 | } 88 | } else { 89 | // FIXME: use actual error handling 90 | warn!("Unrecognized option: {}", option); 91 | } 92 | } 93 | co 94 | } 95 | } 96 | 97 | #[derive(Default)] 98 | pub struct Compiler { 99 | is_active: bool, 100 | jit: Option, 101 | options: CompilerOptions, 102 | } 103 | 104 | impl Compiler { 105 | pub fn is_active(&self) -> bool { 106 | self.is_active 107 | } 108 | 109 | pub fn current_flags(&self) -> Option<&CompilerOptions> { 110 | match self.is_active { 111 | true => Some(&self.options), 112 | false => None, 113 | } 114 | } 115 | 116 | /// Use just-in-time compilation with a `JITBackend` such as the `DirectBackend`. 117 | /// Requires recompilation to take effect. 118 | pub fn use_jit(&mut self, jit: BackendDispatcher) { 119 | self.jit = Some(jit); 120 | } 121 | 122 | pub fn compile( 123 | &mut self, 124 | world: &W, 125 | bounds: (BlockPos, BlockPos), 126 | options: CompilerOptions, 127 | ticks: Vec, 128 | monitor: Arc, 129 | ) { 130 | debug!("Starting compile"); 131 | let start = Instant::now(); 132 | 133 | let input = CompilerInput { world, bounds }; 134 | let pass_manager = make_default_pass_manager::(); 135 | let graph = pass_manager.run_passes(&options, &input, monitor.clone()); 136 | 137 | if monitor.cancelled() { 138 | return; 139 | } 140 | 141 | let replace_jit = match self.jit { 142 | Some(BackendDispatcher::DirectBackend(_)) => { 143 | options.backend_variant != BackendVariant::Direct 144 | } 145 | None => true, 146 | }; 147 | if replace_jit { 148 | debug!("Switching jit backend to {:?}", options.backend_variant); 149 | let jit = match options.backend_variant { 150 | BackendVariant::Direct => BackendDispatcher::DirectBackend(Default::default()), 151 | }; 152 | self.use_jit(jit); 153 | } 154 | 155 | if let Some(jit) = &mut self.jit { 156 | trace!("Compiling backend"); 157 | monitor.set_message("Compiling backend".to_string()); 158 | let start = Instant::now(); 159 | 160 | jit.compile(graph, ticks, &options, monitor.clone()); 161 | 162 | monitor.inc_progress(); 163 | trace!("Backend compiled in {:?}", start.elapsed()); 164 | } else { 165 | error!("Cannot compile without JIT variant selected"); 166 | } 167 | 168 | self.options = options; 169 | self.is_active = true; 170 | debug!("Compile completed in {:?}", start.elapsed()); 171 | } 172 | 173 | pub fn reset(&mut self, world: &mut W, bounds: (BlockPos, BlockPos)) { 174 | if self.is_active { 175 | self.is_active = false; 176 | if let Some(jit) = &mut self.jit { 177 | jit.reset(world, self.options.io_only) 178 | } 179 | } 180 | 181 | if self.options.update { 182 | let (first_pos, second_pos) = bounds; 183 | for_each_block_mut_optimized(world, first_pos, second_pos, |world, pos| { 184 | let block = world.get_block(pos); 185 | mchprs_redstone::update(block, world, pos); 186 | }); 187 | } 188 | self.options = Default::default(); 189 | } 190 | 191 | fn backend(&mut self) -> &mut BackendDispatcher { 192 | assert!( 193 | self.is_active, 194 | "tried to get redpiler backend when inactive" 195 | ); 196 | if let Some(jit) = &mut self.jit { 197 | jit 198 | } else { 199 | panic!("redpiler is active but is missing jit backend"); 200 | } 201 | } 202 | 203 | pub fn tick(&mut self) { 204 | self.backend().tick(); 205 | } 206 | 207 | pub fn tickn(&mut self, ticks: u64) { 208 | self.backend().tickn(ticks); 209 | } 210 | 211 | pub fn on_use_block(&mut self, pos: BlockPos) { 212 | self.backend().on_use_block(pos); 213 | } 214 | 215 | pub fn set_pressure_plate(&mut self, pos: BlockPos, powered: bool) { 216 | self.backend().set_pressure_plate(pos, powered); 217 | } 218 | 219 | pub fn flush(&mut self, world: &mut W) { 220 | let io_only = self.options.io_only; 221 | self.backend().flush(world, io_only); 222 | } 223 | 224 | pub fn inspect(&mut self, pos: BlockPos) { 225 | if let Some(backend) = &mut self.jit { 226 | backend.inspect(pos); 227 | } else { 228 | debug!("cannot inspect when backend is not running"); 229 | } 230 | } 231 | 232 | pub fn has_pending_ticks(&mut self) -> bool { 233 | self.backend().has_pending_ticks() 234 | } 235 | } 236 | 237 | pub struct CompilerInput<'w, W: World> { 238 | pub world: &'w W, 239 | pub bounds: (BlockPos, BlockPos), 240 | } 241 | 242 | #[cfg(test)] 243 | mod tests { 244 | use super::*; 245 | 246 | #[test] 247 | fn parse_options() { 248 | let input = "-io -u --export"; 249 | let expected_options = CompilerOptions { 250 | io_only: true, 251 | optimize: true, 252 | export: true, 253 | update: true, 254 | export_dot_graph: false, 255 | wire_dot_out: false, 256 | backend_variant: BackendVariant::default(), 257 | }; 258 | let options = CompilerOptions::parse(input); 259 | 260 | assert_eq!(options, expected_options); 261 | } 262 | } 263 | -------------------------------------------------------------------------------- /crates/redpiler/src/passes/clamp_weights.rs: -------------------------------------------------------------------------------- 1 | use super::Pass; 2 | use crate::compile_graph::CompileGraph; 3 | use crate::{CompilerInput, CompilerOptions}; 4 | use mchprs_world::World; 5 | 6 | pub struct ClampWeights; 7 | 8 | impl Pass for ClampWeights { 9 | fn run_pass(&self, graph: &mut CompileGraph, _: &CompilerOptions, _: &CompilerInput<'_, W>) { 10 | graph.retain_edges(|g, edge| g[edge].ss < 15); 11 | } 12 | 13 | fn should_run(&self, _: &CompilerOptions) -> bool { 14 | // Mandatory 15 | true 16 | } 17 | 18 | fn status_message(&self) -> &'static str { 19 | "Clamping weights" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /crates/redpiler/src/passes/coalesce.rs: -------------------------------------------------------------------------------- 1 | use super::Pass; 2 | use crate::compile_graph::{CompileGraph, LinkType, NodeIdx, NodeType}; 3 | use crate::{CompilerInput, CompilerOptions}; 4 | use itertools::Itertools; 5 | use mchprs_world::World; 6 | use petgraph::visit::{EdgeRef, NodeIndexable}; 7 | use petgraph::Direction; 8 | use tracing::trace; 9 | 10 | pub struct Coalesce; 11 | 12 | impl Pass for Coalesce { 13 | fn run_pass(&self, graph: &mut CompileGraph, _: &CompilerOptions, _: &CompilerInput<'_, W>) { 14 | loop { 15 | let num_coalesced = run_iteration(graph); 16 | trace!("Iteration combined {} nodes", num_coalesced); 17 | if num_coalesced == 0 { 18 | break; 19 | } 20 | } 21 | } 22 | 23 | fn status_message(&self) -> &'static str { 24 | "Combining duplicate logic" 25 | } 26 | } 27 | 28 | fn run_iteration(graph: &mut CompileGraph) -> usize { 29 | let mut num_coalesced = 0; 30 | for i in 0..graph.node_bound() { 31 | let idx = NodeIdx::new(i); 32 | if !graph.contains_node(idx) { 33 | continue; 34 | } 35 | 36 | let node = &graph[idx]; 37 | // Comparators depend on the link weight as well as the type, 38 | // we could implement that later if it's beneficial enough. 39 | if matches!(node.ty, NodeType::Comparator { .. }) || !node.is_removable() { 40 | continue; 41 | } 42 | 43 | let Ok(edge) = graph.edges_directed(idx, Direction::Incoming).exactly_one() else { 44 | continue; 45 | }; 46 | 47 | if edge.weight().ty != LinkType::Default { 48 | continue; 49 | } 50 | 51 | let source = edge.source(); 52 | // Comparators might output less than 15 ss 53 | if matches!(graph[source].ty, NodeType::Comparator { .. }) { 54 | continue; 55 | } 56 | num_coalesced += coalesce_outgoing(graph, source, idx); 57 | } 58 | num_coalesced 59 | } 60 | 61 | fn coalesce_outgoing(graph: &mut CompileGraph, source_idx: NodeIdx, into_idx: NodeIdx) -> usize { 62 | let mut num_coalesced = 0; 63 | let mut walk_outgoing = graph 64 | .neighbors_directed(source_idx, Direction::Outgoing) 65 | .detach(); 66 | while let Some(edge_idx) = walk_outgoing.next_edge(graph) { 67 | let dest_idx = graph.edge_endpoints(edge_idx).unwrap().1; 68 | if dest_idx == into_idx { 69 | continue; 70 | } 71 | 72 | let dest = &graph[dest_idx]; 73 | let into = &graph[into_idx]; 74 | 75 | if dest.ty == into.ty 76 | && dest.is_removable() 77 | && graph 78 | .neighbors_directed(dest_idx, Direction::Incoming) 79 | .count() 80 | == 1 81 | { 82 | coalesce(graph, dest_idx, into_idx); 83 | num_coalesced += 1; 84 | } 85 | } 86 | num_coalesced 87 | } 88 | 89 | fn coalesce(graph: &mut CompileGraph, node: NodeIdx, into: NodeIdx) { 90 | let mut walk_outgoing: petgraph::stable_graph::WalkNeighbors = 91 | graph.neighbors_directed(node, Direction::Outgoing).detach(); 92 | while let Some(edge_idx) = walk_outgoing.next_edge(graph) { 93 | let dest = graph.edge_endpoints(edge_idx).unwrap().1; 94 | let weight = graph.remove_edge(edge_idx).unwrap(); 95 | graph.add_edge(into, dest, weight); 96 | } 97 | graph.remove_node(node); 98 | } 99 | -------------------------------------------------------------------------------- /crates/redpiler/src/passes/constant_coalesce.rs: -------------------------------------------------------------------------------- 1 | use std::collections::hash_map::Entry; 2 | 3 | use super::Pass; 4 | use crate::compile_graph::{CompileGraph, CompileNode, NodeIdx, NodeState, NodeType}; 5 | use crate::{CompilerInput, CompilerOptions}; 6 | use mchprs_world::World; 7 | use petgraph::unionfind::UnionFind; 8 | use petgraph::visit::{EdgeRef, IntoEdgeReferences, NodeIndexable}; 9 | use petgraph::Direction; 10 | use rustc_hash::FxHashMap; 11 | 12 | pub struct ConstantCoalesce; 13 | 14 | impl Pass for ConstantCoalesce { 15 | fn run_pass(&self, graph: &mut CompileGraph, _: &CompilerOptions, _: &CompilerInput<'_, W>) { 16 | let mut vertex_sets = UnionFind::new(graph.node_bound()); 17 | for edge in graph.edge_references() { 18 | let (src, dest) = (edge.source(), edge.target()); 19 | let node = &graph[src]; 20 | if node.ty != NodeType::Constant || !node.is_removable() { 21 | vertex_sets.union(graph.to_index(src), graph.to_index(dest)); 22 | } 23 | } 24 | 25 | let mut constant_nodes = FxHashMap::default(); 26 | for i in 0..graph.node_bound() { 27 | let idx = NodeIdx::new(i); 28 | if !graph.contains_node(idx) { 29 | continue; 30 | } 31 | let node = &graph[idx]; 32 | if node.ty != NodeType::Constant || !node.is_removable() { 33 | continue; 34 | } 35 | let ss = node.state.output_strength; 36 | 37 | let mut neighbors = graph.neighbors_directed(idx, Direction::Outgoing).detach(); 38 | while let Some((edge, dest)) = neighbors.next(graph) { 39 | let weight = graph.remove_edge(edge).unwrap(); 40 | let subgraph_component = vertex_sets.find(graph.to_index(dest)); 41 | 42 | let constant_idx = match constant_nodes.entry((subgraph_component, ss)) { 43 | Entry::Occupied(entry) => *entry.get(), 44 | Entry::Vacant(entry) => { 45 | let constant_idx = graph.add_node(CompileNode { 46 | ty: NodeType::Constant, 47 | block: None, 48 | state: NodeState::ss(ss), 49 | is_input: false, 50 | is_output: false, 51 | annotations: Default::default(), 52 | }); 53 | *entry.insert(constant_idx) 54 | } 55 | }; 56 | graph.add_edge(constant_idx, dest, weight); 57 | } 58 | graph.remove_node(idx); 59 | } 60 | } 61 | 62 | fn status_message(&self) -> &'static str { 63 | "Coalescing constants" 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /crates/redpiler/src/passes/constant_fold.rs: -------------------------------------------------------------------------------- 1 | use super::Pass; 2 | use crate::compile_graph::{CompileGraph, LinkType, NodeIdx, NodeType}; 3 | use crate::{CompilerInput, CompilerOptions}; 4 | use mchprs_blocks::blocks::ComparatorMode; 5 | use mchprs_world::World; 6 | use petgraph::visit::{EdgeRef, NodeIndexable}; 7 | use petgraph::Direction; 8 | use tracing::trace; 9 | 10 | pub struct ConstantFold; 11 | 12 | impl Pass for ConstantFold { 13 | fn run_pass(&self, graph: &mut CompileGraph, _: &CompilerOptions, _: &CompilerInput<'_, W>) { 14 | loop { 15 | let num_folded = fold(graph); 16 | if num_folded == 0 { 17 | break; 18 | } 19 | trace!("Fold iteration: {} nodes", num_folded); 20 | } 21 | } 22 | 23 | fn status_message(&self) -> &'static str { 24 | "Constant folding" 25 | } 26 | } 27 | 28 | fn fold(graph: &mut CompileGraph) -> usize { 29 | let mut num_folded = 0; 30 | 31 | 'nodes: for i in 0..graph.node_bound() { 32 | let idx = NodeIdx::new(i); 33 | if !graph.contains_node(idx) { 34 | continue; 35 | } 36 | 37 | let mut default_power = 0; 38 | let mut side_power = 0; 39 | for edge in graph.edges_directed(idx, Direction::Incoming) { 40 | let constant = &graph[edge.source()]; 41 | if constant.ty != NodeType::Constant { 42 | continue 'nodes; 43 | } 44 | 45 | match edge.weight().ty { 46 | LinkType::Default => { 47 | default_power = default_power.max( 48 | constant 49 | .state 50 | .output_strength 51 | .saturating_sub(edge.weight().ss), 52 | ) 53 | } 54 | LinkType::Side => { 55 | side_power = side_power.max( 56 | constant 57 | .state 58 | .output_strength 59 | .saturating_sub(edge.weight().ss), 60 | ) 61 | } 62 | } 63 | } 64 | 65 | let new_power = match graph[idx].ty { 66 | NodeType::Comparator { 67 | mode, far_input, .. 68 | } => { 69 | if let Some(far_override) = far_input { 70 | if default_power < 15 { 71 | default_power = far_override; 72 | } 73 | } 74 | match mode { 75 | ComparatorMode::Compare => { 76 | if default_power >= side_power { 77 | default_power 78 | } else { 79 | 0 80 | } 81 | } 82 | ComparatorMode::Subtract => default_power.saturating_sub(side_power), 83 | } 84 | } 85 | NodeType::Repeater { .. } => { 86 | if graph[idx].state.repeater_locked { 87 | graph[idx].state.output_strength 88 | } else if default_power > 0 { 89 | 15 90 | } else { 91 | 0 92 | } 93 | } 94 | NodeType::Torch => { 95 | if default_power > 0 { 96 | 0 97 | } else { 98 | 15 99 | } 100 | } 101 | _ => continue, 102 | }; 103 | 104 | graph[idx].ty = NodeType::Constant; 105 | graph[idx].state.output_strength = new_power; 106 | 107 | let mut incoming = graph.neighbors_directed(idx, Direction::Incoming).detach(); 108 | while let Some(edge) = incoming.next_edge(graph) { 109 | graph.remove_edge(edge); 110 | } 111 | 112 | num_folded += 1; 113 | } 114 | 115 | num_folded 116 | } 117 | -------------------------------------------------------------------------------- /crates/redpiler/src/passes/dedup_links.rs: -------------------------------------------------------------------------------- 1 | //! # [`DedupLinks`] 2 | //! 3 | //! This pass removes duplicate edges from the graph, or parallel edges that have higher weight. 4 | //! 5 | //! For example, if two nodes are connected with two links of weights 13 and 15, the link with 6 | //! weight 15 is removed. 7 | 8 | use super::Pass; 9 | use crate::compile_graph::{CompileGraph, NodeIdx}; 10 | use crate::{CompilerInput, CompilerOptions}; 11 | use mchprs_world::World; 12 | use petgraph::visit::{EdgeRef, NodeIndexable}; 13 | use petgraph::Direction; 14 | 15 | pub struct DedupLinks; 16 | 17 | impl Pass for DedupLinks { 18 | fn run_pass(&self, graph: &mut CompileGraph, _: &CompilerOptions, _: &CompilerInput<'_, W>) { 19 | for i in 0..graph.node_bound() { 20 | let idx = NodeIdx::new(i); 21 | if !graph.contains_node(idx) { 22 | continue; 23 | } 24 | 25 | let mut edges = graph.neighbors_directed(idx, Direction::Incoming).detach(); 26 | while let Some(edge_idx) = edges.next_edge(graph) { 27 | let edge = &graph[edge_idx]; 28 | let source_idx = graph.edge_endpoints(edge_idx).unwrap().0; 29 | 30 | let mut should_remove = false; 31 | for other_edge in graph.edges_directed(idx, Direction::Incoming) { 32 | if other_edge.id() != edge_idx 33 | && other_edge.source() == source_idx 34 | && other_edge.weight().ty == edge.ty 35 | && other_edge.weight().ss <= edge.ss 36 | { 37 | should_remove = true; 38 | break; 39 | } 40 | } 41 | 42 | if should_remove { 43 | graph.remove_edge(edge_idx); 44 | } 45 | } 46 | } 47 | } 48 | 49 | fn status_message(&self) -> &'static str { 50 | "Deduplicating links" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /crates/redpiler/src/passes/export_graph.rs: -------------------------------------------------------------------------------- 1 | use super::Pass; 2 | use crate::compile_graph::{CompileGraph, LinkType as CLinkType, NodeIdx, NodeType as CNodeType}; 3 | use crate::{CompilerInput, CompilerOptions}; 4 | use itertools::Itertools; 5 | use mchprs_blocks::blocks::ComparatorMode as CComparatorMode; 6 | use mchprs_world::World; 7 | use petgraph::visit::EdgeRef; 8 | use petgraph::Direction; 9 | use redpiler_graph::{ 10 | serialize, BlockPos, ComparatorMode, Link, LinkType, Node, NodeState, NodeType, 11 | }; 12 | use rustc_hash::FxHashMap; 13 | use std::fs; 14 | 15 | fn convert_node( 16 | graph: &CompileGraph, 17 | node_idx: NodeIdx, 18 | nodes_map: &FxHashMap, 19 | ) -> Node { 20 | let node = &graph[node_idx]; 21 | 22 | let mut inputs = Vec::new(); 23 | for edge in graph.edges_directed(node_idx, Direction::Incoming) { 24 | let idx = nodes_map[&edge.source()]; 25 | let weight = edge.weight(); 26 | inputs.push(Link { 27 | ty: match weight.ty { 28 | CLinkType::Default => LinkType::Default, 29 | CLinkType::Side => LinkType::Side, 30 | }, 31 | weight: weight.ss, 32 | to: idx, 33 | }); 34 | } 35 | 36 | let updates = graph 37 | .neighbors_directed(node_idx, Direction::Outgoing) 38 | .map(|idx| nodes_map[&idx]) 39 | .collect(); 40 | 41 | let facing_diode = match node.ty { 42 | CNodeType::Repeater { facing_diode, .. } | CNodeType::Comparator { facing_diode, .. } => { 43 | facing_diode 44 | } 45 | _ => false, 46 | }; 47 | 48 | let comparator_far_input = match node.ty { 49 | CNodeType::Comparator { far_input, .. } => far_input, 50 | _ => None, 51 | }; 52 | 53 | Node { 54 | ty: match node.ty { 55 | CNodeType::Repeater { delay, .. } => NodeType::Repeater(delay), 56 | CNodeType::Torch => NodeType::Torch, 57 | CNodeType::Comparator { mode, .. } => NodeType::Comparator(match mode { 58 | CComparatorMode::Compare => ComparatorMode::Compare, 59 | CComparatorMode::Subtract => ComparatorMode::Subtract, 60 | }), 61 | CNodeType::Lamp => NodeType::Lamp, 62 | CNodeType::Button => NodeType::Button, 63 | CNodeType::Lever => NodeType::Lever, 64 | CNodeType::PressurePlate => NodeType::PressurePlate, 65 | CNodeType::Trapdoor => NodeType::Trapdoor, 66 | CNodeType::Wire => NodeType::Wire, 67 | CNodeType::Constant => NodeType::Constant, 68 | CNodeType::NoteBlock { .. } => NodeType::NoteBlock, 69 | }, 70 | block: node.block.map(|(pos, id)| { 71 | ( 72 | BlockPos { 73 | x: pos.x, 74 | y: pos.y, 75 | z: pos.z, 76 | }, 77 | id, 78 | ) 79 | }), 80 | state: NodeState { 81 | output_strength: node.state.output_strength, 82 | powered: node.state.powered, 83 | repeater_locked: node.state.repeater_locked, 84 | }, 85 | comparator_far_input, 86 | facing_diode, 87 | inputs, 88 | updates, 89 | } 90 | } 91 | 92 | pub struct ExportGraph; 93 | 94 | impl Pass for ExportGraph { 95 | fn run_pass(&self, graph: &mut CompileGraph, _: &CompilerOptions, _: &CompilerInput<'_, W>) { 96 | let mut nodes_map = 97 | FxHashMap::with_capacity_and_hasher(graph.node_count(), Default::default()); 98 | for node in graph.node_indices() { 99 | nodes_map.insert(node, nodes_map.len()); 100 | } 101 | 102 | let nodes = graph 103 | .node_indices() 104 | .map(|idx| convert_node(graph, idx, &nodes_map)) 105 | .collect_vec(); 106 | 107 | fs::write("redpiler_graph.bc", serialize(nodes.as_slice()).unwrap()).unwrap(); 108 | } 109 | 110 | fn should_run(&self, options: &CompilerOptions) -> bool { 111 | options.export 112 | } 113 | 114 | fn status_message(&self) -> &'static str { 115 | "Exporting graph" 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /crates/redpiler/src/passes/identify_nodes.rs: -------------------------------------------------------------------------------- 1 | //! # [`IdentifyNodes`] 2 | //! 3 | //! This pass populates the graph with nodes using the input given in [`CompilerInput`]. 4 | //! This pass is *mandatory*. Without it, the graph will never be populated. 5 | //! 6 | //! If `optimize` is set in [`CompilerOptions`], redstone wires will not be added to the graph. 7 | //! 8 | //! There are no requirements for this pass. 9 | 10 | use super::Pass; 11 | use crate::compile_graph::{Annotations, CompileGraph, CompileNode, NodeIdx, NodeState, NodeType}; 12 | use crate::{CompilerInput, CompilerOptions}; 13 | use itertools::Itertools; 14 | use mchprs_blocks::block_entities::BlockEntity; 15 | use mchprs_blocks::blocks::Block; 16 | use mchprs_blocks::{BlockDirection, BlockFace, BlockPos}; 17 | use mchprs_redstone::{self, comparator, noteblock, wire}; 18 | use mchprs_world::{for_each_block_optimized, World}; 19 | use rustc_hash::{FxHashMap, FxHashSet}; 20 | use serde_json::Value; 21 | use tracing::warn; 22 | 23 | pub struct IdentifyNodes; 24 | 25 | impl Pass for IdentifyNodes { 26 | fn run_pass( 27 | &self, 28 | graph: &mut CompileGraph, 29 | options: &CompilerOptions, 30 | input: &CompilerInput<'_, W>, 31 | ) { 32 | let ignore_wires = options.optimize; 33 | let plot = input.world; 34 | 35 | let mut first_pass = FxHashMap::default(); 36 | let mut second_pass = FxHashSet::default(); 37 | 38 | let (first_pos, second_pos) = input.bounds; 39 | 40 | for_each_block_optimized(plot, first_pos, second_pos, |pos| { 41 | for_pos( 42 | graph, 43 | &mut first_pass, 44 | &mut second_pass, 45 | ignore_wires, 46 | options.wire_dot_out, 47 | plot, 48 | pos, 49 | ); 50 | }); 51 | 52 | for pos in second_pass { 53 | apply_annotations(graph, options, &first_pass, plot, pos); 54 | } 55 | } 56 | 57 | fn should_run(&self, _: &CompilerOptions) -> bool { 58 | // Mandatory 59 | true 60 | } 61 | 62 | fn status_message(&self) -> &'static str { 63 | "Identifying nodes" 64 | } 65 | } 66 | 67 | fn for_pos( 68 | graph: &mut CompileGraph, 69 | first_pass: &mut FxHashMap, 70 | second_pass: &mut FxHashSet, 71 | ignore_wires: bool, 72 | wire_dot_out: bool, 73 | world: &W, 74 | pos: BlockPos, 75 | ) { 76 | let id = world.get_block_raw(pos); 77 | let block = Block::from_id(id); 78 | 79 | if matches!(block, Block::Sign { .. } | Block::WallSign { .. }) { 80 | second_pass.insert(pos); 81 | return; 82 | } 83 | 84 | let Some((ty, state)) = identify_block(block, pos, world) else { 85 | return; 86 | }; 87 | 88 | let is_input = matches!( 89 | ty, 90 | NodeType::Button | NodeType::Lever | NodeType::PressurePlate 91 | ); 92 | let is_output = matches!( 93 | ty, 94 | NodeType::Trapdoor | NodeType::Lamp | NodeType::NoteBlock { .. } 95 | ) || matches!(block, Block::RedstoneWire { wire } if wire_dot_out && wire::is_dot(wire)); 96 | 97 | if ignore_wires && ty == NodeType::Wire && !(is_input | is_output) { 98 | return; 99 | } 100 | 101 | let node_idx = graph.add_node(CompileNode { 102 | ty, 103 | block: Some((pos, id)), 104 | state, 105 | 106 | is_input, 107 | is_output, 108 | annotations: Annotations::default(), 109 | }); 110 | first_pass.insert(pos, node_idx); 111 | } 112 | 113 | fn identify_block( 114 | block: Block, 115 | pos: BlockPos, 116 | world: &W, 117 | ) -> Option<(NodeType, NodeState)> { 118 | let (ty, state) = match block { 119 | Block::RedstoneRepeater { repeater } => ( 120 | NodeType::Repeater { 121 | delay: repeater.delay, 122 | facing_diode: mchprs_redstone::is_diode( 123 | world.get_block(pos.offset(repeater.facing.opposite().block_face())), 124 | ), 125 | }, 126 | NodeState::repeater(repeater.powered, repeater.locked), 127 | ), 128 | Block::RedstoneComparator { comparator } => ( 129 | NodeType::Comparator { 130 | mode: comparator.mode, 131 | far_input: comparator::get_far_input(world, pos, comparator.facing), 132 | facing_diode: mchprs_redstone::is_diode( 133 | world.get_block(pos.offset(comparator.facing.opposite().block_face())), 134 | ), 135 | }, 136 | NodeState::comparator( 137 | comparator.powered, 138 | if let Some(BlockEntity::Comparator { output_strength }) = 139 | world.get_block_entity(pos) 140 | { 141 | *output_strength 142 | } else { 143 | 0 144 | }, 145 | ), 146 | ), 147 | Block::RedstoneTorch { lit, .. } | Block::RedstoneWallTorch { lit, .. } => { 148 | (NodeType::Torch, NodeState::simple(lit)) 149 | } 150 | Block::RedstoneWire { wire } => (NodeType::Wire, NodeState::ss(wire.power)), 151 | Block::StoneButton { button } => (NodeType::Button, NodeState::simple(button.powered)), 152 | Block::RedstoneLamp { lit } => (NodeType::Lamp, NodeState::simple(lit)), 153 | Block::Lever { lever } => (NodeType::Lever, NodeState::simple(lever.powered)), 154 | Block::StonePressurePlate { powered } => { 155 | (NodeType::PressurePlate, NodeState::simple(powered)) 156 | } 157 | Block::IronTrapdoor { powered, .. } => (NodeType::Trapdoor, NodeState::simple(powered)), 158 | Block::RedstoneBlock {} => (NodeType::Constant, NodeState::ss(15)), 159 | Block::NoteBlock { 160 | instrument: _, 161 | note, 162 | powered, 163 | } if noteblock::is_noteblock_unblocked(world, pos) => { 164 | let instrument = noteblock::get_noteblock_instrument(world, pos); 165 | ( 166 | NodeType::NoteBlock { instrument, note }, 167 | NodeState::simple(powered), 168 | ) 169 | } 170 | block if comparator::has_override(block) => ( 171 | NodeType::Constant, 172 | NodeState::ss(comparator::get_override(block, world, pos)), 173 | ), 174 | _ => return None, 175 | }; 176 | Some((ty, state)) 177 | } 178 | 179 | fn apply_annotations( 180 | graph: &mut CompileGraph, 181 | options: &CompilerOptions, 182 | first_pass: &FxHashMap, 183 | world: &W, 184 | pos: BlockPos, 185 | ) { 186 | let block = world.get_block(pos); 187 | let annotations = parse_sign_annotations(world.get_block_entity(pos)); 188 | if annotations.is_empty() { 189 | return; 190 | } 191 | 192 | let targets = match block { 193 | Block::Sign { rotation, .. } => { 194 | if let Some(facing) = BlockDirection::from_rotation(rotation) { 195 | let behind = pos.offset(facing.opposite().block_face()); 196 | vec![behind] 197 | } else { 198 | warn!("Found sign with annotations, but bad rotation at {}", pos); 199 | return; 200 | } 201 | } 202 | Block::WallSign { facing, .. } => { 203 | let behind = pos.offset(facing.opposite().block_face()); 204 | vec![ 205 | behind, 206 | behind.offset(BlockFace::Top), 207 | behind.offset(BlockFace::Bottom), 208 | ] 209 | } 210 | _ => panic!("Block unimplemented for second pass"), 211 | }; 212 | 213 | let target = targets.iter().flat_map(|pos| first_pass.get(pos)).next(); 214 | if let Some(&node_idx) = target { 215 | for annotation in annotations { 216 | let result = annotation.apply(graph, node_idx, options); 217 | if let Err(msg) = result { 218 | warn!("{} at {}", msg, pos); 219 | } 220 | } 221 | } else { 222 | warn!("Could not find component for annotation at {}", pos); 223 | } 224 | } 225 | 226 | fn parse_sign_annotations(entity: Option<&BlockEntity>) -> Vec { 227 | if let Some(BlockEntity::Sign(sign)) = entity { 228 | sign.front_rows 229 | .iter() 230 | .flat_map(|row| serde_json::from_str(row)) 231 | .flat_map(|json: Value| NodeAnnotation::parse(json.as_object()?.get("text")?.as_str()?)) 232 | .collect_vec() 233 | } else { 234 | vec![] 235 | } 236 | } 237 | 238 | pub enum NodeAnnotation {} 239 | 240 | impl NodeAnnotation { 241 | fn parse(s: &str) -> Option { 242 | let s = s.trim().to_ascii_lowercase(); 243 | if !(s.starts_with('[') && s.ends_with(']')) { 244 | return None; 245 | } 246 | let parts = s[1..s.len() - 1].split(' ').collect_vec(); 247 | match parts.as_slice() { 248 | _ => None, 249 | } 250 | } 251 | 252 | fn apply( 253 | self, 254 | _graph: &mut CompileGraph, 255 | _node_idx: NodeIdx, 256 | _options: &CompilerOptions, 257 | ) -> Result<(), String> { 258 | match self {} 259 | } 260 | } 261 | -------------------------------------------------------------------------------- /crates/redpiler/src/passes/mod.rs: -------------------------------------------------------------------------------- 1 | mod clamp_weights; 2 | mod coalesce; 3 | mod constant_coalesce; 4 | mod constant_fold; 5 | mod dedup_links; 6 | mod export_graph; 7 | mod identify_nodes; 8 | mod input_search; 9 | mod prune_orphans; 10 | mod unreachable_output; 11 | 12 | use mchprs_world::World; 13 | 14 | use super::compile_graph::CompileGraph; 15 | use super::task_monitor::TaskMonitor; 16 | use super::{CompilerInput, CompilerOptions}; 17 | use std::sync::Arc; 18 | use std::time::Instant; 19 | use tracing::trace; 20 | 21 | pub const fn make_default_pass_manager<'w, W: World>() -> PassManager<'w, W> { 22 | PassManager::new(&[ 23 | &identify_nodes::IdentifyNodes, 24 | &input_search::InputSearch, 25 | &clamp_weights::ClampWeights, 26 | &dedup_links::DedupLinks, 27 | &constant_fold::ConstantFold, 28 | &unreachable_output::UnreachableOutput, 29 | &constant_coalesce::ConstantCoalesce, 30 | &coalesce::Coalesce, 31 | &prune_orphans::PruneOrphans, 32 | &export_graph::ExportGraph, 33 | ]) 34 | } 35 | 36 | pub struct PassManager<'p, W: World> { 37 | passes: &'p [&'p dyn Pass], 38 | } 39 | 40 | impl<'p, W: World> PassManager<'p, W> { 41 | pub const fn new(passes: &'p [&dyn Pass]) -> Self { 42 | Self { passes } 43 | } 44 | 45 | pub fn run_passes( 46 | &self, 47 | options: &CompilerOptions, 48 | input: &CompilerInput<'_, W>, 49 | monitor: Arc, 50 | ) -> CompileGraph { 51 | let mut graph = CompileGraph::new(); 52 | 53 | // Add one for the backend compile step 54 | monitor.set_max_progress(self.passes.len() + 1); 55 | 56 | for &pass in self.passes { 57 | if !pass.should_run(options) { 58 | trace!("Skipping pass: {}", pass.name()); 59 | monitor.inc_progress(); 60 | continue; 61 | } 62 | 63 | if monitor.cancelled() { 64 | return graph; 65 | } 66 | 67 | trace!("Running pass: {}", pass.name()); 68 | monitor.set_message(pass.status_message().to_string()); 69 | let start = Instant::now(); 70 | 71 | pass.run_pass(&mut graph, options, input); 72 | 73 | trace!("Completed pass in {:?}", start.elapsed()); 74 | trace!("node_count: {}", graph.node_count()); 75 | trace!("edge_count: {}", graph.edge_count()); 76 | monitor.inc_progress(); 77 | } 78 | 79 | graph 80 | } 81 | } 82 | 83 | pub trait Pass { 84 | fn run_pass( 85 | &self, 86 | graph: &mut CompileGraph, 87 | options: &CompilerOptions, 88 | input: &CompilerInput<'_, W>, 89 | ); 90 | 91 | /// This name should only be use for debugging purposes, 92 | /// it is not a valid identifier of the pass. 93 | fn name(&self) -> &'static str { 94 | std::any::type_name::() 95 | } 96 | 97 | fn should_run(&self, options: &CompilerOptions) -> bool { 98 | // Run passes for optimized builds by default 99 | options.optimize 100 | } 101 | 102 | fn status_message(&self) -> &'static str; 103 | } 104 | -------------------------------------------------------------------------------- /crates/redpiler/src/passes/prune_orphans.rs: -------------------------------------------------------------------------------- 1 | //! # [`PruneOrphans`] 2 | //! 3 | //! This pass removes any nodes in the graph that aren't transitively connected to an output redstone component by using Depth-First-Search. 4 | 5 | use super::Pass; 6 | use crate::compile_graph::CompileGraph; 7 | use crate::{CompilerInput, CompilerOptions}; 8 | use itertools::Itertools; 9 | use mchprs_world::World; 10 | use petgraph::Direction; 11 | use rustc_hash::FxHashSet; 12 | 13 | pub struct PruneOrphans; 14 | 15 | impl Pass for PruneOrphans { 16 | fn run_pass(&self, graph: &mut CompileGraph, _: &CompilerOptions, _: &CompilerInput<'_, W>) { 17 | let mut to_visit = graph 18 | .node_indices() 19 | .filter(|&idx| !graph[idx].is_removable()) 20 | .collect_vec(); 21 | 22 | let mut visited = FxHashSet::default(); 23 | while let Some(idx) = to_visit.pop() { 24 | if visited.insert(idx) { 25 | to_visit.extend(graph.neighbors_directed(idx, Direction::Incoming)); 26 | } 27 | } 28 | 29 | graph.retain_nodes(|_, idx| visited.contains(&idx)); 30 | } 31 | 32 | fn should_run(&self, options: &CompilerOptions) -> bool { 33 | options.io_only && options.optimize 34 | } 35 | 36 | fn status_message(&self) -> &'static str { 37 | "Pruning orphans" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /crates/redpiler/src/passes/unreachable_output.rs: -------------------------------------------------------------------------------- 1 | //! # [`UnreachableOutput`] 2 | //! 3 | //! If the side of a comparator in subtract mode is constant, then the maximum output of the 4 | //! comparator is equal to the difference of the maximum side input and the maximum default input. 5 | //! Outgoing edges that have a weight greater than or equal to the maxiumum output of the 6 | //! comparator can be safely removed. 7 | //! 8 | //! Basically, links from comparators that could never possibly output a signal great enough that 9 | //! it won't be zero'd out by the weight of the link get removed. 10 | 11 | use super::Pass; 12 | use crate::compile_graph::{CompileGraph, LinkType, NodeIdx, NodeType}; 13 | use crate::{CompilerInput, CompilerOptions}; 14 | use mchprs_blocks::blocks::ComparatorMode; 15 | use mchprs_world::World; 16 | use petgraph::visit::{EdgeRef, NodeIndexable}; 17 | use petgraph::Direction; 18 | 19 | pub struct UnreachableOutput; 20 | 21 | impl Pass for UnreachableOutput { 22 | fn run_pass(&self, graph: &mut CompileGraph, _: &CompilerOptions, _: &CompilerInput<'_, W>) { 23 | for i in 0..graph.node_bound() { 24 | let idx = NodeIdx::new(i); 25 | if !graph.contains_node(idx) { 26 | continue; 27 | } 28 | 29 | if !matches!( 30 | graph[idx].ty, 31 | NodeType::Comparator { 32 | mode: ComparatorMode::Subtract, 33 | .. 34 | } 35 | ) { 36 | continue; 37 | } 38 | 39 | // For simiplicity, we always use 15 here. A more complex implementation in the future 40 | // might want to properly calculate this. 41 | let max_input: u8 = 15; 42 | 43 | let mut side_inputs = graph 44 | .edges_directed(idx, Direction::Incoming) 45 | .filter(|e| e.weight().ty == LinkType::Side); 46 | let Some(constant_edge) = side_inputs.next() else { 47 | continue; 48 | }; 49 | let constant_idx = constant_edge.source(); 50 | 51 | // We only accept one constant input for now. In the future we might wan't to coalesce 52 | // multiple constant inputs together to make this work, most likely in another pass. 53 | if side_inputs.next().is_some() { 54 | continue; 55 | } 56 | 57 | if graph[constant_idx].ty != NodeType::Constant { 58 | continue; 59 | } 60 | 61 | let constant = graph[constant_idx].state.output_strength; 62 | let max_output = max_input.saturating_sub(constant); 63 | 64 | // Now we can go through all the outgoing nodes and remove the ones with a weight that 65 | // is too high. 66 | let mut outgoing = graph.neighbors_directed(idx, Direction::Outgoing).detach(); 67 | while let Some((edge_idx, _)) = outgoing.next(graph) { 68 | if graph[edge_idx].ss >= max_output { 69 | graph.remove_edge(edge_idx); 70 | } 71 | } 72 | } 73 | } 74 | 75 | fn status_message(&self) -> &'static str { 76 | "Pruning unreachable comparator outputs" 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /crates/redpiler/src/task_monitor.rs: -------------------------------------------------------------------------------- 1 | use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; 2 | use std::sync::{Arc, Mutex}; 3 | 4 | #[derive(Default)] 5 | pub struct TaskMonitor { 6 | cancelled: AtomicBool, 7 | max_progress: AtomicUsize, 8 | progress: AtomicUsize, 9 | message: Mutex>>, 10 | } 11 | 12 | impl TaskMonitor { 13 | pub fn cancel(&self) { 14 | self.cancelled.store(true, Ordering::Relaxed); 15 | } 16 | 17 | pub fn cancelled(&self) -> bool { 18 | self.cancelled.load(Ordering::Relaxed) 19 | } 20 | 21 | pub fn set_progress(&self, progress: usize) { 22 | self.progress.store(progress, Ordering::Relaxed); 23 | } 24 | 25 | pub fn inc_progress(&self) { 26 | self.progress.fetch_add(1, Ordering::Relaxed); 27 | } 28 | 29 | pub fn set_max_progress(&self, max_progress: usize) { 30 | self.max_progress.store(max_progress, Ordering::Relaxed); 31 | } 32 | 33 | pub fn progress(&self) -> usize { 34 | self.progress.load(Ordering::Relaxed) 35 | } 36 | 37 | pub fn max_progress(&self) -> usize { 38 | self.max_progress.load(Ordering::Relaxed) 39 | } 40 | 41 | pub fn set_message(&self, message: String) { 42 | *self.message.lock().unwrap() = Some(Arc::new(message)); 43 | } 44 | 45 | pub fn message(&self) -> Option> { 46 | self.message.lock().unwrap().clone() 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /crates/redpiler_graph/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "redpiler_graph" 3 | authors.workspace = true 4 | description.workspace = true 5 | edition.workspace = true 6 | homepage.workspace = true 7 | keywords.workspace = true 8 | readme.workspace = true 9 | version.workspace = true 10 | license.workspace = true 11 | repository.workspace = true 12 | 13 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 14 | 15 | [dependencies] 16 | serde = { workspace = true, features = ["derive"] } 17 | bincode = { workspace = true } 18 | -------------------------------------------------------------------------------- /crates/redpiler_graph/src/lib.rs: -------------------------------------------------------------------------------- 1 | use bincode::{BincodeRead, Result}; 2 | use serde::{Deserialize, Serialize}; 3 | 4 | pub type NodeId = usize; 5 | 6 | #[derive(PartialEq, Eq, Copy, Clone, Debug, Serialize, Deserialize, Hash)] 7 | pub struct BlockPos { 8 | pub x: i32, 9 | pub y: i32, 10 | pub z: i32, 11 | } 12 | 13 | #[derive(Serialize, Deserialize, PartialEq, Eq, Clone, Copy, Debug)] 14 | pub enum LinkType { 15 | Default, 16 | Side, 17 | } 18 | 19 | #[derive(Serialize, Deserialize, PartialEq, Eq, Clone, Copy, Debug)] 20 | pub enum ComparatorMode { 21 | Compare, 22 | Subtract, 23 | } 24 | 25 | #[derive(Serialize, Deserialize, PartialEq, Eq, Clone, Debug)] 26 | pub struct Link { 27 | pub ty: LinkType, 28 | pub weight: u8, 29 | pub to: NodeId, 30 | } 31 | 32 | #[derive(Serialize, Deserialize, PartialEq, Eq, Clone, Copy, Debug)] 33 | pub enum NodeType { 34 | Repeater(u8), 35 | Torch, 36 | Comparator(ComparatorMode), 37 | Lamp, 38 | Button, 39 | Lever, 40 | PressurePlate, 41 | Trapdoor, 42 | Wire, 43 | Constant, 44 | NoteBlock, 45 | } 46 | 47 | #[derive(Serialize, Deserialize, Clone, PartialEq, Eq, Debug)] 48 | pub struct NodeState { 49 | pub powered: bool, 50 | pub repeater_locked: bool, 51 | pub output_strength: u8, 52 | } 53 | 54 | #[derive(Serialize, Deserialize, Clone, PartialEq, Eq, Debug)] 55 | pub struct Node { 56 | pub ty: NodeType, 57 | /// Position and protocol id for block 58 | pub block: Option<(BlockPos, u32)>, 59 | pub state: NodeState, 60 | 61 | pub facing_diode: bool, 62 | pub comparator_far_input: Option, 63 | 64 | pub inputs: Vec, 65 | pub updates: Vec, 66 | } 67 | 68 | pub fn serialize(nodes: &[Node]) -> Result> { 69 | bincode::serialize(nodes) 70 | } 71 | 72 | pub fn serialize_into(writer: W, value: &[Node]) -> Result<()> 73 | where 74 | W: std::io::Write, 75 | { 76 | bincode::serialize_into(writer, value) 77 | } 78 | 79 | pub fn deserialize(bytes: &[u8]) -> Result> { 80 | bincode::deserialize(bytes) 81 | } 82 | 83 | pub fn deserialize_from<'a, R>(reader: R) -> Result> 84 | where 85 | R: BincodeRead<'a>, 86 | { 87 | bincode::deserialize_from(reader) 88 | } 89 | -------------------------------------------------------------------------------- /crates/redstone/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "mchprs_redstone" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | mchprs_blocks = { path = "../blocks" } 10 | mchprs_world = { path = "../world" } 11 | tracing = { workspace = true } 12 | rustc-hash = { workspace = true } 13 | -------------------------------------------------------------------------------- /crates/redstone/src/comparator.rs: -------------------------------------------------------------------------------- 1 | use mchprs_blocks::block_entities::BlockEntity; 2 | use mchprs_blocks::blocks::{Block, ComparatorMode, RedstoneComparator}; 3 | use mchprs_blocks::{BlockDirection, BlockFace, BlockPos}; 4 | use mchprs_world::{TickPriority, World}; 5 | use tracing::warn; 6 | 7 | fn get_power_on_side(world: &impl World, pos: BlockPos, side: BlockDirection) -> u8 { 8 | let side_pos = pos.offset(side.block_face()); 9 | let side_block = world.get_block(side_pos); 10 | if super::is_diode(side_block) { 11 | super::get_weak_power(side_block, world, side_pos, side.block_face(), false) 12 | } else if let Block::RedstoneWire { wire } = side_block { 13 | wire.power 14 | } else if let Block::RedstoneBlock {} = side_block { 15 | 15 16 | } else { 17 | 0 18 | } 19 | } 20 | 21 | fn get_power_on_sides(comp: RedstoneComparator, world: &impl World, pos: BlockPos) -> u8 { 22 | std::cmp::max( 23 | get_power_on_side(world, pos, comp.facing.rotate()), 24 | get_power_on_side(world, pos, comp.facing.rotate_ccw()), 25 | ) 26 | } 27 | 28 | pub fn has_override(block: Block) -> bool { 29 | matches!( 30 | block, 31 | Block::Barrel { .. } 32 | | Block::Furnace { .. } 33 | | Block::Hopper { .. } 34 | | Block::Cauldron { .. } 35 | | Block::Composter { .. } 36 | | Block::Cake { .. } 37 | ) 38 | } 39 | 40 | pub fn get_override(block: Block, world: &impl World, pos: BlockPos) -> u8 { 41 | match block { 42 | Block::Barrel { .. } | Block::Furnace { .. } | Block::Hopper { .. } => { 43 | match world.get_block_entity(pos) { 44 | Some(BlockEntity::Container { 45 | comparator_override, 46 | .. 47 | }) => *comparator_override, 48 | Some(other) => { 49 | warn!("Backing container blockentity type is invalid: {other:?}"); 50 | 0 51 | } 52 | // Empty containers may not have any block entity data 53 | None => 0, 54 | } 55 | } 56 | Block::Cauldron { level } => level, 57 | Block::Composter { level } => level, 58 | Block::Cake { bites } => 14 - 2 * bites, 59 | _ => unreachable!("Block does not override comparators"), 60 | } 61 | } 62 | 63 | pub fn get_far_input(world: &impl World, pos: BlockPos, facing: BlockDirection) -> Option { 64 | let face = facing.block_face(); 65 | let input_pos = pos.offset(face); 66 | let input_block = world.get_block(input_pos); 67 | if !input_block.is_solid() || has_override(input_block) { 68 | return None; 69 | } 70 | 71 | let far_input_pos = input_pos.offset(face); 72 | let far_input_block = world.get_block(far_input_pos); 73 | if has_override(far_input_block) { 74 | Some(get_override(far_input_block, world, far_input_pos)) 75 | } else { 76 | None 77 | } 78 | } 79 | 80 | fn calculate_input_strength(comp: RedstoneComparator, world: &impl World, pos: BlockPos) -> u8 { 81 | let base_input_strength = super::diode_get_input_strength(world, pos, comp.facing); 82 | let input_pos = pos.offset(comp.facing.block_face()); 83 | let input_block = world.get_block(input_pos); 84 | if has_override(input_block) { 85 | get_override(input_block, world, input_pos) 86 | } else if base_input_strength < 15 && input_block.is_solid() { 87 | let far_input_pos = input_pos.offset(comp.facing.block_face()); 88 | let far_input_block = world.get_block(far_input_pos); 89 | if has_override(far_input_block) { 90 | get_override(far_input_block, world, far_input_pos) 91 | } else { 92 | base_input_strength 93 | } 94 | } else { 95 | base_input_strength 96 | } 97 | } 98 | 99 | pub fn should_be_powered(comp: RedstoneComparator, world: &impl World, pos: BlockPos) -> bool { 100 | let input_strength = calculate_input_strength(comp, world, pos); 101 | if input_strength == 0 { 102 | false 103 | } else { 104 | let power_on_sides = get_power_on_sides(comp, world, pos); 105 | if input_strength > power_on_sides { 106 | true 107 | } else { 108 | power_on_sides == input_strength && comp.mode == ComparatorMode::Compare 109 | } 110 | } 111 | } 112 | 113 | fn calculate_output_strength( 114 | comp: RedstoneComparator, 115 | world: &mut impl World, 116 | pos: BlockPos, 117 | ) -> u8 { 118 | let input_strength = calculate_input_strength(comp, world, pos); 119 | if comp.mode == ComparatorMode::Subtract { 120 | input_strength.saturating_sub(get_power_on_sides(comp, world, pos)) 121 | } else if input_strength >= get_power_on_sides(comp, world, pos) { 122 | input_strength 123 | } else { 124 | 0 125 | } 126 | } 127 | 128 | // This is exactly the same as it is in the RedstoneRepeater struct. 129 | // Sometime in the future, this needs to be reused. LLVM might optimize 130 | // it way, but te human brane wil not! 131 | fn on_state_change(comp: RedstoneComparator, world: &mut impl World, pos: BlockPos) { 132 | let front_pos = pos.offset(comp.facing.opposite().block_face()); 133 | let front_block = world.get_block(front_pos); 134 | super::update(front_block, world, front_pos); 135 | for direction in &BlockFace::values() { 136 | let neighbor_pos = front_pos.offset(*direction); 137 | let block = world.get_block(neighbor_pos); 138 | super::update(block, world, neighbor_pos); 139 | } 140 | } 141 | 142 | pub fn update(comp: RedstoneComparator, world: &mut impl World, pos: BlockPos) { 143 | if world.pending_tick_at(pos) { 144 | return; 145 | } 146 | let output_strength = calculate_output_strength(comp, world, pos); 147 | let old_strength = 148 | if let Some(BlockEntity::Comparator { output_strength }) = world.get_block_entity(pos) { 149 | *output_strength 150 | } else { 151 | 0 152 | }; 153 | if output_strength != old_strength || comp.powered != should_be_powered(comp, world, pos) { 154 | let front_block = world.get_block(pos.offset(comp.facing.opposite().block_face())); 155 | let priority = if super::is_diode(front_block) { 156 | TickPriority::High 157 | } else { 158 | TickPriority::Normal 159 | }; 160 | world.schedule_tick(pos, 1, priority); 161 | } 162 | } 163 | 164 | pub fn tick(mut comp: RedstoneComparator, world: &mut impl World, pos: BlockPos) { 165 | let new_strength = calculate_output_strength(comp, world, pos); 166 | let old_strength = if let Some(BlockEntity::Comparator { 167 | output_strength: old_output_strength, 168 | }) = world.get_block_entity(pos) 169 | { 170 | *old_output_strength 171 | } else { 172 | 0 173 | }; 174 | if new_strength != old_strength || comp.mode == ComparatorMode::Compare { 175 | world.set_block_entity( 176 | pos, 177 | BlockEntity::Comparator { 178 | output_strength: new_strength, 179 | }, 180 | ); 181 | let should_be_powered = should_be_powered(comp, world, pos); 182 | let powered = comp.powered; 183 | if powered && !should_be_powered { 184 | comp.powered = false; 185 | world.set_block(pos, Block::RedstoneComparator { comparator: comp }); 186 | } else if !powered && should_be_powered { 187 | comp.powered = true; 188 | world.set_block(pos, Block::RedstoneComparator { comparator: comp }); 189 | } 190 | on_state_change(comp, world, pos); 191 | } 192 | } 193 | -------------------------------------------------------------------------------- /crates/redstone/src/noteblock.rs: -------------------------------------------------------------------------------- 1 | use mchprs_blocks::blocks::{Block, Instrument}; 2 | use mchprs_blocks::{BlockFace, BlockPos}; 3 | use mchprs_world::World; 4 | 5 | // LUT generated via f32::powf(2.0, (note as f32 - 12.0) / 12.0) 6 | // This is hardcoded because at this point floating point operations are not allowed in const contexts 7 | #[allow(clippy::approx_constant)] 8 | const PITCHES_TABLE: [f32; 25] = [ 9 | 0.5, 0.5297315, 0.561231, 0.59460354, 0.62996054, 0.6674199, 0.70710677, 0.74915355, 0.7937005, 10 | 0.8408964, 0.8908987, 0.9438743, 1.0, 1.0594631, 1.122462, 1.1892071, 1.2599211, 1.3348398, 11 | 1.4142135, 1.4983071, 1.587401, 1.6817929, 1.7817974, 1.8877486, 2.0, 12 | ]; 13 | 14 | pub fn is_noteblock_unblocked(world: &impl World, pos: BlockPos) -> bool { 15 | matches!(world.get_block(pos.offset(BlockFace::Top)), Block::Air {}) 16 | } 17 | 18 | pub fn get_noteblock_instrument(world: &impl World, pos: BlockPos) -> Instrument { 19 | Instrument::from_block_below(world.get_block(pos.offset(BlockFace::Bottom))) 20 | } 21 | 22 | pub fn play_note(world: &mut impl World, pos: BlockPos, instrument: Instrument, note: u32) { 23 | world.play_sound( 24 | pos, 25 | instrument.to_sound_id(), 26 | 2, // Sound Caregory ID for Records 27 | 3.0, 28 | PITCHES_TABLE[note as usize], 29 | ); 30 | } 31 | -------------------------------------------------------------------------------- /crates/redstone/src/repeater.rs: -------------------------------------------------------------------------------- 1 | use mchprs_blocks::blocks::{Block, RedstoneRepeater}; 2 | use mchprs_blocks::{BlockDirection, BlockFace, BlockPos}; 3 | use mchprs_world::{TickPriority, World}; 4 | 5 | pub fn get_state_for_placement( 6 | world: &impl World, 7 | pos: BlockPos, 8 | facing: BlockDirection, 9 | ) -> RedstoneRepeater { 10 | RedstoneRepeater { 11 | delay: 1, 12 | facing, 13 | locked: should_be_locked(facing, world, pos), 14 | powered: false, 15 | } 16 | } 17 | 18 | fn should_be_locked(facing: BlockDirection, world: &impl World, pos: BlockPos) -> bool { 19 | let right_side = get_power_on_side(world, pos, facing.rotate()); 20 | let left_side = get_power_on_side(world, pos, facing.rotate_ccw()); 21 | std::cmp::max(right_side, left_side) > 0 22 | } 23 | 24 | fn get_power_on_side(world: &impl World, pos: BlockPos, side: BlockDirection) -> u8 { 25 | let side_pos = pos.offset(side.block_face()); 26 | let side_block = world.get_block(side_pos); 27 | if super::is_diode(side_block) { 28 | super::get_weak_power(side_block, world, side_pos, side.block_face(), false) 29 | } else { 30 | 0 31 | } 32 | } 33 | 34 | fn on_state_change(rep: RedstoneRepeater, world: &mut impl World, pos: BlockPos) { 35 | let front_pos = pos.offset(rep.facing.opposite().block_face()); 36 | let front_block = world.get_block(front_pos); 37 | super::update(front_block, world, front_pos); 38 | for direction in &BlockFace::values() { 39 | let neighbor_pos = front_pos.offset(*direction); 40 | let block = world.get_block(neighbor_pos); 41 | super::update(block, world, neighbor_pos); 42 | } 43 | } 44 | 45 | fn schedule_tick( 46 | rep: RedstoneRepeater, 47 | world: &mut impl World, 48 | pos: BlockPos, 49 | should_be_powered: bool, 50 | ) { 51 | let front_block = world.get_block(pos.offset(rep.facing.opposite().block_face())); 52 | let priority = if super::is_diode(front_block) { 53 | TickPriority::Highest 54 | } else if !should_be_powered { 55 | TickPriority::Higher 56 | } else { 57 | TickPriority::High 58 | }; 59 | world.schedule_tick(pos, rep.delay as u32, priority); 60 | } 61 | 62 | fn should_be_powered(rep: RedstoneRepeater, world: &impl World, pos: BlockPos) -> bool { 63 | super::diode_get_input_strength(world, pos, rep.facing) > 0 64 | } 65 | 66 | pub fn on_neighbor_updated(mut rep: RedstoneRepeater, world: &mut impl World, pos: BlockPos) { 67 | let should_be_locked = should_be_locked(rep.facing, world, pos); 68 | if !rep.locked && should_be_locked { 69 | rep.locked = true; 70 | world.set_block(pos, Block::RedstoneRepeater { repeater: rep }); 71 | } else if rep.locked && !should_be_locked { 72 | rep.locked = false; 73 | world.set_block(pos, Block::RedstoneRepeater { repeater: rep }); 74 | } 75 | 76 | if !rep.locked && !world.pending_tick_at(pos) { 77 | let should_be_powered = should_be_powered(rep, world, pos); 78 | if should_be_powered != rep.powered { 79 | schedule_tick(rep, world, pos, should_be_powered); 80 | } 81 | } 82 | } 83 | 84 | pub fn tick(mut rep: RedstoneRepeater, world: &mut impl World, pos: BlockPos) { 85 | if rep.locked { 86 | return; 87 | } 88 | 89 | let should_be_powered = should_be_powered(rep, world, pos); 90 | if rep.powered && !should_be_powered { 91 | rep.powered = false; 92 | world.set_block(pos, Block::RedstoneRepeater { repeater: rep }); 93 | on_state_change(rep, world, pos); 94 | } else if !rep.powered { 95 | if !should_be_powered { 96 | world.schedule_tick(pos, rep.delay as u32, TickPriority::Higher); 97 | } 98 | rep.powered = true; 99 | world.set_block(pos, Block::RedstoneRepeater { repeater: rep }); 100 | on_state_change(rep, world, pos); 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /crates/redstone/src/wire/mod.rs: -------------------------------------------------------------------------------- 1 | mod turbo; 2 | 3 | use mchprs_blocks::blocks::{Block, RedstoneWire, RedstoneWireSide}; 4 | use mchprs_blocks::{BlockDirection, BlockFace, BlockPos}; 5 | use mchprs_world::World; 6 | use turbo::RedstoneWireTurbo; 7 | 8 | pub fn make_cross(power: u8) -> RedstoneWire { 9 | RedstoneWire { 10 | north: RedstoneWireSide::Side, 11 | south: RedstoneWireSide::Side, 12 | east: RedstoneWireSide::Side, 13 | west: RedstoneWireSide::Side, 14 | power, 15 | } 16 | } 17 | 18 | pub fn get_state_for_placement(world: &impl World, pos: BlockPos) -> RedstoneWire { 19 | let mut wire = RedstoneWire { 20 | power: calculate_power(world, pos), 21 | ..Default::default() 22 | }; 23 | wire = get_regulated_sides(wire, world, pos); 24 | if is_dot(wire) { 25 | wire = make_cross(wire.power); 26 | } 27 | wire 28 | } 29 | 30 | pub fn on_neighbor_changed( 31 | mut wire: RedstoneWire, 32 | world: &impl World, 33 | pos: BlockPos, 34 | side: BlockFace, 35 | ) -> RedstoneWire { 36 | let old_state = wire; 37 | let new_side; 38 | match side { 39 | BlockFace::Top => return wire, 40 | BlockFace::Bottom => { 41 | return get_regulated_sides(wire, world, pos); 42 | } 43 | BlockFace::North => { 44 | wire.south = get_side(world, pos, BlockDirection::South); 45 | new_side = wire.south; 46 | } 47 | BlockFace::South => { 48 | wire.north = get_side(world, pos, BlockDirection::North); 49 | new_side = wire.north; 50 | } 51 | 52 | BlockFace::East => { 53 | wire.west = get_side(world, pos, BlockDirection::West); 54 | new_side = wire.west; 55 | } 56 | BlockFace::West => { 57 | wire.east = get_side(world, pos, BlockDirection::East); 58 | new_side = wire.east; 59 | } 60 | } 61 | wire = get_regulated_sides(wire, world, pos); 62 | if is_cross(old_state) && new_side.is_none() { 63 | // Don't mess up the cross 64 | return old_state; 65 | } 66 | if !is_dot(old_state) && is_dot(wire) { 67 | // Save the power until the transformation into cross is complete 68 | let power = wire.power; 69 | // Become the cross it always wanted to be 70 | wire = make_cross(power); 71 | } 72 | wire 73 | } 74 | 75 | pub fn on_neighbor_updated(mut wire: RedstoneWire, world: &mut impl World, pos: BlockPos) { 76 | let new_power = calculate_power(world, pos); 77 | 78 | if wire.power != new_power { 79 | wire.power = new_power; 80 | world.set_block(pos, Block::RedstoneWire { wire }); 81 | RedstoneWireTurbo::update_surrounding_neighbors(world, pos); 82 | } 83 | } 84 | 85 | // pub fn on_use(wire: RedstoneWire, world: &mut impl World, pos: BlockPos) -> ActionResult { 86 | // if is_dot(wire) || is_cross(wire) { 87 | // let mut new_wire = if is_cross(wire) { 88 | // RedstoneWire::default() 89 | // } else { 90 | // make_cross(0) 91 | // }; 92 | // new_wire.power = wire.power; 93 | // new_wire = get_regulated_sides(new_wire, world, pos); 94 | // if wire != new_wire { 95 | // world.set_block(pos, Block::RedstoneWire { wire: new_wire }); 96 | // super::update_wire_neighbors(world, pos); 97 | // return ActionResult::Success; 98 | // } 99 | // } 100 | // ActionResult::Pass 101 | // } 102 | 103 | fn can_connect_to(block: Block, side: BlockDirection) -> bool { 104 | match block { 105 | Block::RedstoneWire { .. } 106 | | Block::RedstoneComparator { .. } 107 | | Block::RedstoneTorch { .. } 108 | | Block::RedstoneBlock { .. } 109 | | Block::RedstoneWallTorch { .. } 110 | | Block::StonePressurePlate { .. } 111 | | Block::TripwireHook { .. } 112 | | Block::StoneButton { .. } 113 | | Block::Target { .. } 114 | | Block::Lever { .. } => true, 115 | Block::RedstoneRepeater { repeater } => { 116 | repeater.facing == side || repeater.facing == side.opposite() 117 | } 118 | Block::Observer { facing } => facing == side.block_facing(), 119 | _ => false, 120 | } 121 | } 122 | 123 | fn can_connect_diagonal_to(block: Block) -> bool { 124 | matches!(block, Block::RedstoneWire { .. }) 125 | } 126 | 127 | pub fn get_current_side(wire: RedstoneWire, side: BlockDirection) -> RedstoneWireSide { 128 | use BlockDirection::*; 129 | match side { 130 | North => wire.north, 131 | South => wire.south, 132 | East => wire.east, 133 | West => wire.west, 134 | } 135 | } 136 | 137 | pub fn get_side(world: &impl World, pos: BlockPos, side: BlockDirection) -> RedstoneWireSide { 138 | let neighbor_pos = pos.offset(side.block_face()); 139 | let neighbor = world.get_block(neighbor_pos); 140 | 141 | if can_connect_to(neighbor, side) { 142 | return RedstoneWireSide::Side; 143 | } 144 | 145 | let up_pos = pos.offset(BlockFace::Top); 146 | let up = world.get_block(up_pos); 147 | 148 | if !up.is_solid() 149 | && can_connect_diagonal_to(world.get_block(neighbor_pos.offset(BlockFace::Top))) 150 | { 151 | RedstoneWireSide::Up 152 | } else if !neighbor.is_solid() 153 | && can_connect_diagonal_to(world.get_block(neighbor_pos.offset(BlockFace::Bottom))) 154 | { 155 | RedstoneWireSide::Side 156 | } else { 157 | RedstoneWireSide::None 158 | } 159 | } 160 | 161 | fn get_all_sides(mut wire: RedstoneWire, world: &impl World, pos: BlockPos) -> RedstoneWire { 162 | wire.north = get_side(world, pos, BlockDirection::North); 163 | wire.south = get_side(world, pos, BlockDirection::South); 164 | wire.east = get_side(world, pos, BlockDirection::East); 165 | wire.west = get_side(world, pos, BlockDirection::West); 166 | wire 167 | } 168 | 169 | pub fn get_regulated_sides(wire: RedstoneWire, world: &impl World, pos: BlockPos) -> RedstoneWire { 170 | let mut state = get_all_sides(wire, world, pos); 171 | if is_dot(wire) && is_dot(state) { 172 | return state; 173 | } 174 | let north_none = state.north.is_none(); 175 | let south_none = state.south.is_none(); 176 | let east_none = state.east.is_none(); 177 | let west_none = state.west.is_none(); 178 | let north_south_none = north_none && south_none; 179 | let east_west_none = east_none && west_none; 180 | if north_none && east_west_none { 181 | state.north = RedstoneWireSide::Side; 182 | } 183 | if south_none && east_west_none { 184 | state.south = RedstoneWireSide::Side; 185 | } 186 | if east_none && north_south_none { 187 | state.east = RedstoneWireSide::Side; 188 | } 189 | if west_none && north_south_none { 190 | state.west = RedstoneWireSide::Side; 191 | } 192 | state 193 | } 194 | 195 | pub fn is_dot(wire: RedstoneWire) -> bool { 196 | wire.north == RedstoneWireSide::None 197 | && wire.south == RedstoneWireSide::None 198 | && wire.east == RedstoneWireSide::None 199 | && wire.west == RedstoneWireSide::None 200 | } 201 | 202 | pub fn is_cross(wire: RedstoneWire) -> bool { 203 | wire.north == RedstoneWireSide::Side 204 | && wire.south == RedstoneWireSide::Side 205 | && wire.east == RedstoneWireSide::Side 206 | && wire.west == RedstoneWireSide::Side 207 | } 208 | 209 | fn max_wire_power(wire_power: u8, world: &impl World, pos: BlockPos) -> u8 { 210 | let block = world.get_block(pos); 211 | if let Block::RedstoneWire { wire } = block { 212 | wire_power.max(wire.power) 213 | } else { 214 | wire_power 215 | } 216 | } 217 | 218 | fn calculate_power(world: &impl World, pos: BlockPos) -> u8 { 219 | let mut block_power = 0; 220 | let mut wire_power = 0; 221 | 222 | let up_pos = pos.offset(BlockFace::Top); 223 | let up_block = world.get_block(up_pos); 224 | 225 | for side in &BlockFace::values() { 226 | let neighbor_pos = pos.offset(*side); 227 | wire_power = max_wire_power(wire_power, world, neighbor_pos); 228 | let neighbor = world.get_block(neighbor_pos); 229 | block_power = block_power.max(super::get_redstone_power_no_dust( 230 | neighbor, 231 | world, 232 | neighbor_pos, 233 | *side, 234 | )); 235 | if side.is_horizontal() { 236 | if !up_block.is_solid() && !neighbor.is_transparent() { 237 | wire_power = max_wire_power(wire_power, world, neighbor_pos.offset(BlockFace::Top)); 238 | } 239 | 240 | if !neighbor.is_solid() { 241 | wire_power = 242 | max_wire_power(wire_power, world, neighbor_pos.offset(BlockFace::Bottom)); 243 | } 244 | } 245 | } 246 | 247 | block_power.max(wire_power.saturating_sub(1)) 248 | } 249 | -------------------------------------------------------------------------------- /crates/save_data/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "mchprs_save_data" 3 | authors.workspace = true 4 | description.workspace = true 5 | edition.workspace = true 6 | homepage.workspace = true 7 | keywords.workspace = true 8 | readme.workspace = true 9 | version.workspace = true 10 | license.workspace = true 11 | repository.workspace = true 12 | 13 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 14 | 15 | [dependencies] 16 | mchprs_world = { path = "../world" } 17 | mchprs_blocks = { path = "../blocks" } 18 | byteorder = { workspace = true } 19 | bincode = { workspace = true } 20 | serde = { workspace = true } 21 | thiserror = { workspace = true } 22 | rustc-hash = { workspace = true } 23 | tracing = { workspace = true } 24 | -------------------------------------------------------------------------------- /crates/save_data/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod plot_data; 2 | -------------------------------------------------------------------------------- /crates/save_data/src/plot_data.rs: -------------------------------------------------------------------------------- 1 | mod fixer; 2 | 3 | use self::fixer::FixInfo; 4 | use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt}; 5 | use mchprs_blocks::block_entities::BlockEntity; 6 | use mchprs_blocks::BlockPos; 7 | use mchprs_world::storage::{Chunk, ChunkSection}; 8 | use mchprs_world::TickEntry; 9 | use rustc_hash::FxHashMap; 10 | use serde::{Deserialize, Serialize}; 11 | use std::fs::{File, OpenOptions}; 12 | use std::io::{Read, Write}; 13 | use std::path::Path; 14 | use std::{fmt, io}; 15 | use thiserror::Error; 16 | 17 | /// Version History: 18 | /// 0: Initial plot data file with header (MC 1.18.2) 19 | /// 1: Add world send rate 20 | /// 2: Update to MC 1.20.4 21 | pub const VERSION: u32 = 2; 22 | 23 | #[derive(Error, Debug)] 24 | pub enum PlotLoadError { 25 | #[error("plot data deserialization error")] 26 | Deserialize(#[from] bincode::Error), 27 | 28 | #[error("invalid plot data header")] 29 | InvalidHeader, 30 | 31 | #[error("plot data version {0} too new to be loaded")] 32 | TooNew(u32), 33 | 34 | #[error("plot data version {0} failed to be converted")] 35 | ConversionFailed(u32), 36 | 37 | #[error(transparent)] 38 | Io(#[from] io::Error), 39 | 40 | #[error("conversion from plot data version {0} is unavailable")] 41 | ConversionUnavailable(u32), 42 | } 43 | 44 | impl From for PlotLoadError { 45 | fn from(e: PlotSaveError) -> Self { 46 | match e { 47 | PlotSaveError::Serialize(err) => err.into(), 48 | PlotSaveError::Io(err) => err.into(), 49 | } 50 | } 51 | } 52 | 53 | #[derive(Error, Debug)] 54 | pub enum PlotSaveError { 55 | #[error("plot data serialization error")] 56 | Serialize(#[from] bincode::Error), 57 | 58 | #[error(transparent)] 59 | Io(#[from] io::Error), 60 | } 61 | 62 | static PLOT_MAGIC: &[u8; 8] = b"\x86MCHPRS\x00"; 63 | 64 | #[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] 65 | pub struct ChunkSectionData { 66 | pub data: Vec, 67 | pub palette: Vec, 68 | pub bits_per_block: u8, 69 | pub block_count: u32, 70 | } 71 | 72 | impl ChunkSectionData { 73 | fn new(section: &ChunkSection) -> Self { 74 | Self { 75 | data: section.data().to_vec(), 76 | palette: section.palette().to_vec(), 77 | bits_per_block: section.bits_per_block(), 78 | block_count: section.block_count(), 79 | } 80 | } 81 | 82 | fn load(self) -> ChunkSection { 83 | ChunkSection::from_raw( 84 | self.data, 85 | self.bits_per_block, 86 | self.palette, 87 | self.block_count, 88 | ) 89 | } 90 | } 91 | 92 | #[derive(Debug, Serialize, Deserialize, Clone)] 93 | pub struct ChunkData { 94 | pub sections: Vec>, 95 | pub block_entities: FxHashMap, 96 | } 97 | 98 | impl ChunkData { 99 | /// Takes a mutable Chunk to flush it first 100 | pub fn new(chunk: &mut Chunk) -> Self { 101 | chunk.flush(); 102 | Self { 103 | sections: chunk 104 | .sections 105 | .iter() 106 | .map(|section| { 107 | if section.block_count() > 0 { 108 | Some(ChunkSectionData::new(section)) 109 | } else { 110 | None 111 | } 112 | }) 113 | .collect(), 114 | block_entities: chunk.block_entities.clone(), 115 | } 116 | } 117 | 118 | pub fn load(self, x: i32, z: i32) -> Chunk { 119 | Chunk { 120 | x, 121 | z, 122 | sections: self 123 | .sections 124 | .into_iter() 125 | .map(|section| match section { 126 | Some(section) => section.load(), 127 | None => Default::default(), 128 | }) 129 | .collect(), 130 | block_entities: self.block_entities, 131 | } 132 | } 133 | } 134 | 135 | #[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)] 136 | pub enum Tps { 137 | Limited(u32), 138 | Unlimited, 139 | } 140 | 141 | #[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)] 142 | pub struct WorldSendRate(pub u32); 143 | 144 | impl Default for WorldSendRate { 145 | fn default() -> Self { 146 | Self(60) 147 | } 148 | } 149 | 150 | impl fmt::Display for Tps { 151 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 152 | match self { 153 | Tps::Limited(tps) => write!(f, "{}", tps), 154 | Tps::Unlimited => write!(f, "unlimited"), 155 | } 156 | } 157 | } 158 | 159 | #[derive(Debug, Serialize, Deserialize, Clone)] 160 | pub struct PlotData { 161 | pub tps: Tps, 162 | pub world_send_rate: WorldSendRate, 163 | pub chunk_data: Vec, 164 | pub pending_ticks: Vec, 165 | } 166 | 167 | impl PlotData { 168 | pub fn load_from_file(path: impl AsRef) -> Result { 169 | let mut file = File::open(&path)?; 170 | 171 | let mut magic = [0; 8]; 172 | file.read_exact(&mut magic)?; 173 | if &magic != PLOT_MAGIC { 174 | return fixer::try_fix(path, FixInfo::InvalidHeader)? 175 | .ok_or(PlotLoadError::InvalidHeader); 176 | } 177 | 178 | let version = file.read_u32::()?; 179 | if version < VERSION { 180 | return fixer::try_fix(path, FixInfo::OldVersion { version })? 181 | .ok_or(PlotLoadError::ConversionFailed(version)); 182 | } 183 | if version > VERSION { 184 | return Err(PlotLoadError::TooNew(version)); 185 | } 186 | 187 | let mut buf = Vec::new(); 188 | file.read_to_end(&mut buf)?; 189 | Ok(bincode::deserialize(&buf)?) 190 | } 191 | 192 | pub fn save_to_file(&self, path: impl AsRef) -> Result<(), PlotSaveError> { 193 | let mut file = OpenOptions::new().write(true).create(true).open(path)?; 194 | 195 | file.write_all(PLOT_MAGIC)?; 196 | file.write_u32::(VERSION)?; 197 | let data = bincode::serialize(self)?; 198 | file.write_all(&data)?; 199 | file.sync_data()?; 200 | Ok(()) 201 | } 202 | } 203 | -------------------------------------------------------------------------------- /crates/save_data/src/plot_data/fixer.rs: -------------------------------------------------------------------------------- 1 | //! The goal of this module is to make upgrading to newer version of mchprs 2 | //! easier by providing automatic conversion from old world data. 3 | //! 4 | //! Eventually this module might help recover currupted plot data. 5 | //! 6 | //! In the future it might be nice to have this as an optional dependency or 7 | //! seperate download. As our save format changes in the future, the fixer 8 | //! module may become quite big. 9 | 10 | use super::{PlotData, PlotLoadError}; 11 | use crate::plot_data::VERSION; 12 | use std::fs; 13 | use std::path::Path; 14 | use tracing::debug; 15 | 16 | #[derive(Debug)] 17 | pub enum FixInfo { 18 | InvalidHeader, 19 | OldVersion { version: u32 }, 20 | } 21 | 22 | fn make_backup(path: impl AsRef) -> Result<(), PlotLoadError> { 23 | let path = path.as_ref(); 24 | let mut backup_path = path.with_extension("bak"); 25 | if backup_path.exists() { 26 | let num = 1; 27 | loop { 28 | backup_path = path.with_extension(format!("bak.{}", num)); 29 | if !backup_path.exists() { 30 | break; 31 | } 32 | } 33 | } 34 | fs::rename(path, backup_path)?; 35 | Ok(()) 36 | } 37 | 38 | pub fn try_fix(path: impl AsRef, info: FixInfo) -> Result, PlotLoadError> { 39 | debug!("Trying to fix plot with {:?}", info); 40 | let result: Option = match info { 41 | FixInfo::OldVersion { 42 | version: version @ 0..=1, 43 | } => return Err(PlotLoadError::ConversionUnavailable(version)), 44 | _ => None, 45 | }; 46 | 47 | Ok(match result { 48 | Some(data) => { 49 | make_backup(&path)?; 50 | data.save_to_file(&path)?; 51 | debug!("Successfully converted plot to version {}", VERSION); 52 | Some(data) 53 | } 54 | None => None, 55 | }) 56 | } 57 | -------------------------------------------------------------------------------- /crates/text/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "mchprs_text" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | serde = { workspace = true, features = ["derive"] } 8 | serde_json = { workspace = true } 9 | regex = { workspace = true } 10 | once_cell = { workspace = true } 11 | -------------------------------------------------------------------------------- /crates/text/src/lib.rs: -------------------------------------------------------------------------------- 1 | use once_cell::sync::Lazy; 2 | use regex::Regex; 3 | use serde::Serialize; 4 | 5 | static URL_REGEX: Lazy = Lazy::new(|| { 6 | Regex::new("([a-zA-Z0-9§\\-:/]+\\.[a-zA-Z/0-9§\\-:_#]+(\\.[a-zA-Z/0-9.§\\-:#\\?\\+=_]+)?)") 7 | .unwrap() 8 | }); 9 | 10 | fn is_valid_hex(ch: char) -> bool { 11 | ch.is_numeric() || ('a'..='f').contains(&ch) || ('A'..='F').contains(&ch) 12 | } 13 | 14 | #[derive(Serialize, Debug, Clone, Copy)] 15 | #[serde(rename_all = "snake_case")] 16 | pub enum ColorCode { 17 | Black, 18 | DarkBlue, 19 | DarkGreen, 20 | DarkAqua, 21 | DarkRed, 22 | DarkPurple, 23 | Gold, 24 | Gray, 25 | DarkGray, 26 | Blue, 27 | Green, 28 | Aqua, 29 | Red, 30 | LightPurple, 31 | Yellow, 32 | White, 33 | Obfuscated, 34 | Bold, 35 | Strikethrough, 36 | Underline, 37 | Italic, 38 | Reset, 39 | } 40 | 41 | impl ColorCode { 42 | fn parse(code: char) -> Option { 43 | Some(match code { 44 | '0' => ColorCode::Black, 45 | '1' => ColorCode::DarkBlue, 46 | '2' => ColorCode::DarkGreen, 47 | '3' => ColorCode::DarkAqua, 48 | '4' => ColorCode::DarkRed, 49 | '5' => ColorCode::DarkPurple, 50 | '6' => ColorCode::Gold, 51 | '7' => ColorCode::Gray, 52 | '8' => ColorCode::DarkGray, 53 | '9' => ColorCode::Blue, 54 | 'a' => ColorCode::Green, 55 | 'b' => ColorCode::Aqua, 56 | 'c' => ColorCode::Red, 57 | 'd' => ColorCode::LightPurple, 58 | 'e' => ColorCode::Yellow, 59 | 'f' => ColorCode::White, 60 | 'k' => ColorCode::Obfuscated, 61 | 'l' => ColorCode::Bold, 62 | 'm' => ColorCode::Strikethrough, 63 | 'n' => ColorCode::Underline, 64 | 'o' => ColorCode::Italic, 65 | 'r' => ColorCode::Reset, 66 | _ => return None, 67 | }) 68 | } 69 | 70 | fn is_formatting(self) -> bool { 71 | use ColorCode::*; 72 | matches!( 73 | self, 74 | Obfuscated | Bold | Strikethrough | Underline | Italic | Reset 75 | ) 76 | } 77 | } 78 | 79 | #[derive(Serialize, Debug, Clone)] 80 | #[serde(untagged)] 81 | pub enum TextColor { 82 | Hex(String), 83 | ColorCode(ColorCode), 84 | } 85 | 86 | #[derive(Serialize, Debug, Clone)] 87 | #[serde(rename_all = "snake_case")] 88 | enum ClickEventType { 89 | OpenUrl, 90 | // RunCommand, 91 | // SuggestCommand, 92 | } 93 | 94 | #[derive(Serialize, Debug, Clone)] 95 | pub struct ClickEvent { 96 | action: ClickEventType, 97 | value: String, 98 | } 99 | 100 | /// This is only used for `TextComponent` serialize 101 | #[allow(clippy::trivially_copy_pass_by_ref)] 102 | fn is_false(field: &bool) -> bool { 103 | !*field 104 | } 105 | 106 | pub struct TextComponentBuilder { 107 | component: TextComponent, 108 | } 109 | 110 | impl TextComponentBuilder { 111 | pub fn new(text: String) -> Self { 112 | let component = TextComponent { 113 | text, 114 | ..Default::default() 115 | }; 116 | Self { component } 117 | } 118 | 119 | pub fn color(mut self, color: TextColor) -> Self { 120 | self.component.color = Some(color); 121 | self 122 | } 123 | 124 | pub fn color_code(mut self, color: ColorCode) -> Self { 125 | self.component.color = Some(TextColor::ColorCode(color)); 126 | self 127 | } 128 | 129 | pub fn strikethrough(mut self, val: bool) -> Self { 130 | self.component.strikethrough = val; 131 | self 132 | } 133 | 134 | pub fn finish(self) -> TextComponent { 135 | self.component 136 | } 137 | } 138 | 139 | #[derive(Serialize, Default, Debug, Clone)] 140 | pub struct TextComponent { 141 | pub text: String, 142 | #[serde(skip_serializing_if = "is_false")] 143 | pub bold: bool, 144 | #[serde(skip_serializing_if = "is_false")] 145 | pub italic: bool, 146 | #[serde(skip_serializing_if = "is_false")] 147 | pub underlined: bool, 148 | #[serde(skip_serializing_if = "is_false")] 149 | pub strikethrough: bool, 150 | #[serde(skip_serializing_if = "is_false")] 151 | pub obfuscated: bool, 152 | #[serde(skip_serializing_if = "Option::is_none")] 153 | pub color: Option, 154 | #[serde(skip_serializing_if = "Option::is_none")] 155 | #[serde(rename = "clickEvent")] 156 | pub click_event: Option, 157 | #[serde(skip_serializing_if = "Vec::is_empty")] 158 | pub extra: Vec, 159 | } 160 | 161 | impl TextComponent { 162 | pub fn from_legacy_text(message: &str) -> Vec { 163 | let mut components = Vec::new(); 164 | 165 | let mut cur_component: TextComponent = Default::default(); 166 | 167 | let mut chars = message.chars(); 168 | 'main_loop: while let Some(c) = chars.next() { 169 | if c == '&' { 170 | if let Some(code) = chars.next() { 171 | if let Some(color) = ColorCode::parse(code) { 172 | let make_new = !cur_component.text.is_empty(); 173 | if color.is_formatting() && make_new { 174 | components.push(cur_component.clone()); 175 | cur_component.text.clear(); 176 | } 177 | match color { 178 | ColorCode::Bold => cur_component.bold = true, 179 | ColorCode::Italic => cur_component.italic = true, 180 | ColorCode::Underline => cur_component.underlined = true, 181 | ColorCode::Strikethrough => cur_component.strikethrough = true, 182 | ColorCode::Obfuscated => cur_component.obfuscated = true, 183 | _ => { 184 | components.push(cur_component); 185 | cur_component = Default::default(); 186 | cur_component.color = Some(TextColor::ColorCode(color)); 187 | } 188 | } 189 | continue; 190 | } 191 | cur_component.text.push(c); 192 | cur_component.text.push(code); 193 | continue; 194 | } 195 | } 196 | if c == '#' { 197 | let mut hex = String::from(c); 198 | for _ in 0..6 { 199 | if let Some(c) = chars.next() { 200 | hex.push(c); 201 | if !is_valid_hex(c) { 202 | cur_component.text += &hex; 203 | continue 'main_loop; 204 | } 205 | } else { 206 | cur_component.text += &hex; 207 | continue 'main_loop; 208 | } 209 | } 210 | components.push(cur_component); 211 | cur_component = Default::default(); 212 | cur_component.color = Some(TextColor::Hex(hex)); 213 | continue; 214 | } 215 | cur_component.text.push(c); 216 | } 217 | components.push(cur_component); 218 | 219 | // This code is stinky 220 | // Find urls and add click action 221 | let mut new_componenets = Vec::with_capacity(components.len()); 222 | for component in components { 223 | let mut last = 0; 224 | let text = &component.text; 225 | 226 | for match_ in URL_REGEX.find_iter(text) { 227 | let index = match_.start(); 228 | let matched = match_.as_str(); 229 | if last != index { 230 | let mut new = component.clone(); 231 | new.text = String::from(&text[last..index]); 232 | new_componenets.push(new); 233 | } 234 | let mut new = component.clone(); 235 | new.text = matched.to_string(); 236 | new.click_event = Some(ClickEvent { 237 | action: ClickEventType::OpenUrl, 238 | value: matched.to_string(), 239 | }); 240 | new_componenets.push(new); 241 | last = index + matched.len(); 242 | } 243 | if last < text.len() { 244 | let mut new = component.clone(); 245 | new.text = String::from(&text[last..]); 246 | new_componenets.push(new); 247 | } 248 | } 249 | 250 | new_componenets 251 | } 252 | 253 | pub fn encode_json(&self) -> String { 254 | serde_json::to_string(self).unwrap() 255 | } 256 | 257 | pub fn is_text_only(&self) -> bool { 258 | !self.bold 259 | && !self.italic 260 | && !self.underlined 261 | && !self.strikethrough 262 | && !self.obfuscated 263 | && self.extra.is_empty() 264 | && self.color.is_none() 265 | && self.click_event.is_none() 266 | } 267 | } 268 | 269 | impl From for TextComponent 270 | where 271 | S: Into, 272 | { 273 | fn from(value: S) -> Self { 274 | let mut tc: TextComponent = Default::default(); 275 | tc.text = value.into(); 276 | tc 277 | } 278 | } 279 | -------------------------------------------------------------------------------- /crates/utils/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "mchprs_utils" 3 | authors.workspace = true 4 | description.workspace = true 5 | edition.workspace = true 6 | homepage.workspace = true 7 | keywords.workspace = true 8 | readme.workspace = true 9 | version.workspace = true 10 | license.workspace = true 11 | repository.workspace = true 12 | 13 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 14 | 15 | [dependencies] 16 | -------------------------------------------------------------------------------- /crates/utils/src/lib.rs: -------------------------------------------------------------------------------- 1 | /// An easy way to create HashMaps 2 | #[macro_export] 3 | macro_rules! map( 4 | { $($key:expr => $value:expr),+ } => { 5 | { 6 | let mut m = ::std::collections::HashMap::new(); 7 | $( 8 | m.insert($key.into(), $value); 9 | )+ 10 | m 11 | } 12 | }; 13 | ); 14 | 15 | #[macro_export] 16 | macro_rules! nbt_unwrap_val { 17 | // I'm not sure if path is the right type here. 18 | // It works though! 19 | ($e:expr, $p:path) => { 20 | match $e { 21 | $p(val) => val, 22 | _ => return None, 23 | } 24 | }; 25 | } 26 | -------------------------------------------------------------------------------- /crates/world/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "mchprs_world" 3 | authors.workspace = true 4 | description.workspace = true 5 | edition.workspace = true 6 | homepage.workspace = true 7 | keywords.workspace = true 8 | readme.workspace = true 9 | version.workspace = true 10 | license.workspace = true 11 | repository.workspace = true 12 | 13 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 14 | 15 | [dependencies] 16 | mchprs_blocks = { path = "../blocks" } 17 | mchprs_network = { path = "../network", optional = true } 18 | serde = { workspace = true } 19 | rustc-hash = { workspace = true } 20 | hematite-nbt = { workspace = true } 21 | 22 | [features] 23 | default = ["networking"] 24 | networking = ["dep:mchprs_network"] 25 | -------------------------------------------------------------------------------- /crates/world/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod storage; 2 | 3 | use mchprs_blocks::block_entities::BlockEntity; 4 | use mchprs_blocks::blocks::Block; 5 | use mchprs_blocks::BlockPos; 6 | use serde::{Deserialize, Serialize}; 7 | use storage::Chunk; 8 | 9 | #[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] 10 | pub enum TickPriority { 11 | Highest = 0, 12 | Higher = 1, 13 | High = 2, 14 | Normal = 3, 15 | } 16 | 17 | #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] 18 | pub struct TickEntry { 19 | pub ticks_left: u32, 20 | pub tick_priority: TickPriority, 21 | pub pos: BlockPos, 22 | } 23 | 24 | pub trait World { 25 | /// Returns the block located at `pos` 26 | fn get_block(&self, pos: BlockPos) -> Block { 27 | Block::from_id(self.get_block_raw(pos)) 28 | } 29 | 30 | /// Returns the block state id of the block at `pos` 31 | fn get_block_raw(&self, pos: BlockPos) -> u32; 32 | 33 | /// Sets the block at `pos`. 34 | /// This function may have side effects such as sending update block packets to the player. 35 | /// Returns true if the block was changed. 36 | fn set_block(&mut self, pos: BlockPos, block: Block) -> bool { 37 | let block_id = Block::get_id(block); 38 | self.set_block_raw(pos, block_id) 39 | } 40 | 41 | /// Sets a block in storage without any other side effects. Returns true if a block was changed. 42 | fn set_block_raw(&mut self, pos: BlockPos, block: u32) -> bool; 43 | 44 | /// Removes a block entity at `pos` if it exists. 45 | fn delete_block_entity(&mut self, pos: BlockPos); 46 | 47 | /// Returns a reference to the block entity at `pos` if it exists. 48 | /// Returns None if there is no block entity at `pos`. 49 | fn get_block_entity(&self, pos: BlockPos) -> Option<&BlockEntity>; 50 | 51 | /// Sets the block entity at `pos`, overwriting any other block entity that was there prior. 52 | fn set_block_entity(&mut self, pos: BlockPos, block_entity: BlockEntity); 53 | 54 | /// Returns an immutable reference to the chunk at `x` and `z` chunk coordinates. 55 | /// Returns None if the chunk does not exist in this world. 56 | fn get_chunk(&self, x: i32, z: i32) -> Option<&Chunk>; 57 | 58 | /// Returns a mutable reference to the chunk at `x` and `z` chunk coordinates. 59 | /// Returns None if the chunk does not exist in this world. 60 | fn get_chunk_mut(&mut self, x: i32, z: i32) -> Option<&mut Chunk>; 61 | 62 | /// Schedules a tick in the world with `delay` and `pritority` 63 | fn schedule_tick(&mut self, pos: BlockPos, delay: u32, priority: TickPriority); 64 | 65 | /// Returns true if there is a tick entry with `pos` 66 | fn pending_tick_at(&mut self, pos: BlockPos) -> bool; 67 | 68 | fn is_cursed(&self) -> bool { 69 | false 70 | } 71 | 72 | #[allow(unused_variables)] 73 | fn play_sound( 74 | &mut self, 75 | pos: BlockPos, 76 | sound_id: i32, 77 | sound_category: i32, 78 | volume: f32, 79 | pitch: f32, 80 | ) { 81 | } 82 | } 83 | 84 | // TODO: I have no idea how to deduplicate this in a sane way 85 | 86 | /// Executes the given function for each block excluding most air blocks 87 | pub fn for_each_block_optimized( 88 | world: &W, 89 | first_pos: BlockPos, 90 | second_pos: BlockPos, 91 | mut f: F, 92 | ) where 93 | F: FnMut(BlockPos), 94 | { 95 | let start_x = i32::min(first_pos.x, second_pos.x); 96 | let end_x = i32::max(first_pos.x, second_pos.x); 97 | 98 | let start_y = i32::min(first_pos.y, second_pos.y); 99 | let end_y = i32::max(first_pos.y, second_pos.y); 100 | 101 | let start_z = i32::min(first_pos.z, second_pos.z); 102 | let end_z = i32::max(first_pos.z, second_pos.z); 103 | 104 | // Iterate over chunks 105 | for chunk_start_x in (start_x..=end_x).step_by(16) { 106 | for chunk_start_z in (start_z..=end_z).step_by(16) { 107 | let chunk = world 108 | .get_chunk(chunk_start_x.div_euclid(16), chunk_start_z.div_euclid(16)) 109 | .unwrap(); 110 | for chunk_start_y in (start_y..=end_y).step_by(16) { 111 | // Check if the chunk even has non air blocks 112 | if chunk.sections[chunk_start_y as usize / 16].block_count() > 0 { 113 | // Calculate the end position of the current chunk 114 | let chunk_end_x = i32::min(chunk_start_x + 16 - 1, end_x); 115 | let chunk_end_y = i32::min(chunk_start_y + 16 - 1, end_y); 116 | let chunk_end_z = i32::min(chunk_start_z + 16 - 1, end_z); 117 | 118 | // Iterate over each position within the current chunk 119 | for y in chunk_start_y..=chunk_end_y { 120 | for z in chunk_start_z..=chunk_end_z { 121 | for x in chunk_start_x..=chunk_end_x { 122 | let pos = BlockPos::new(x, y, z); 123 | f(pos); 124 | } 125 | } 126 | } 127 | } 128 | } 129 | } 130 | } 131 | } 132 | 133 | /// Executes the given function for each block excluding most air blocks 134 | pub fn for_each_block_mut_optimized( 135 | world: &mut W, 136 | first_pos: BlockPos, 137 | second_pos: BlockPos, 138 | mut f: F, 139 | ) where 140 | F: FnMut(&mut W, BlockPos), 141 | { 142 | let start_x = i32::min(first_pos.x, second_pos.x); 143 | let end_x = i32::max(first_pos.x, second_pos.x); 144 | 145 | let start_y = i32::min(first_pos.y, second_pos.y); 146 | let end_y = i32::max(first_pos.y, second_pos.y); 147 | 148 | let start_z = i32::min(first_pos.z, second_pos.z); 149 | let end_z = i32::max(first_pos.z, second_pos.z); 150 | 151 | // Iterate over chunks 152 | for chunk_start_x in (start_x..=end_x).step_by(16) { 153 | for chunk_start_z in (start_z..=end_z).step_by(16) { 154 | for chunk_start_y in (start_y..=end_y).step_by(16) { 155 | // Check if the chunk even has non air blocks 156 | if world 157 | .get_chunk(chunk_start_x.div_euclid(16), chunk_start_z.div_euclid(16)) 158 | .unwrap() 159 | .sections[chunk_start_y as usize / 16] 160 | .block_count() 161 | > 0 162 | { 163 | // Calculate the end position of the current chunk 164 | let chunk_end_x = i32::min(chunk_start_x + 16 - 1, end_x); 165 | let chunk_end_y = i32::min(chunk_start_y + 16 - 1, end_y); 166 | let chunk_end_z = i32::min(chunk_start_z + 16 - 1, end_z); 167 | 168 | // Iterate over each position within the current chunk 169 | for y in chunk_start_y..=chunk_end_y { 170 | for z in chunk_start_z..=chunk_end_z { 171 | for x in chunk_start_x..=chunk_end_x { 172 | let pos = BlockPos::new(x, y, z); 173 | f(world, pos); 174 | } 175 | } 176 | } 177 | } 178 | } 179 | } 180 | } 181 | } 182 | -------------------------------------------------------------------------------- /docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM rust:slim 2 | 3 | RUN apt-get update \ 4 | && apt-get install -y \ 5 | git pkg-config libssl-dev clang 6 | 7 | RUN git clone https://github.com/MCHPR/MCHPRS.git 8 | WORKDIR ./MCHPRS 9 | RUN cargo install --path . \ 10 | && cargo clean 11 | 12 | VOLUME ["/data"] 13 | WORKDIR /data 14 | 15 | CMD ["mchprs"] -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "flake-utils": { 4 | "inputs": { 5 | "systems": "systems" 6 | }, 7 | "locked": { 8 | "lastModified": 1731533236, 9 | "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", 10 | "owner": "numtide", 11 | "repo": "flake-utils", 12 | "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", 13 | "type": "github" 14 | }, 15 | "original": { 16 | "owner": "numtide", 17 | "repo": "flake-utils", 18 | "type": "github" 19 | } 20 | }, 21 | "nixpkgs": { 22 | "locked": { 23 | "lastModified": 1745930157, 24 | "narHash": "sha256-y3h3NLnzRSiUkYpnfvnS669zWZLoqqI6NprtLQ+5dck=", 25 | "owner": "NixOS", 26 | "repo": "nixpkgs", 27 | "rev": "46e634be05ce9dc6d4db8e664515ba10b78151ae", 28 | "type": "github" 29 | }, 30 | "original": { 31 | "owner": "NixOS", 32 | "ref": "nixos-unstable", 33 | "repo": "nixpkgs", 34 | "type": "github" 35 | } 36 | }, 37 | "root": { 38 | "inputs": { 39 | "flake-utils": "flake-utils", 40 | "nixpkgs": "nixpkgs", 41 | "rust-overlay": "rust-overlay" 42 | } 43 | }, 44 | "rust-overlay": { 45 | "inputs": { 46 | "nixpkgs": [ 47 | "nixpkgs" 48 | ] 49 | }, 50 | "locked": { 51 | "lastModified": 1746067100, 52 | "narHash": "sha256-6JeEbboDvRjLwB9kzCnmWj+f+ZnMtKOe5c2F1VBpaTs=", 53 | "owner": "oxalica", 54 | "repo": "rust-overlay", 55 | "rev": "026e8fedefd6b167d92ed04b195c658d95ffc7a5", 56 | "type": "github" 57 | }, 58 | "original": { 59 | "owner": "oxalica", 60 | "repo": "rust-overlay", 61 | "type": "github" 62 | } 63 | }, 64 | "systems": { 65 | "locked": { 66 | "lastModified": 1681028828, 67 | "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", 68 | "owner": "nix-systems", 69 | "repo": "default", 70 | "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", 71 | "type": "github" 72 | }, 73 | "original": { 74 | "owner": "nix-systems", 75 | "repo": "default", 76 | "type": "github" 77 | } 78 | } 79 | }, 80 | "root": "root", 81 | "version": 7 82 | } 83 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | inputs = { 3 | nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; 4 | flake-utils.url = "github:numtide/flake-utils"; 5 | rust-overlay = { 6 | url = "github:oxalica/rust-overlay"; 7 | inputs = { 8 | nixpkgs.follows = "nixpkgs"; 9 | }; 10 | }; 11 | }; 12 | outputs = { self, nixpkgs, flake-utils, rust-overlay }: 13 | flake-utils.lib.eachDefaultSystem 14 | (system: 15 | let 16 | overlays = [ (import rust-overlay) ]; 17 | pkgs = import nixpkgs { 18 | inherit system overlays; 19 | }; 20 | rustToolchain = pkgs.pkgsBuildHost.rust-bin.fromRustupToolchainFile ./rust-toolchain.toml; 21 | nativeBuildInputs = with pkgs; [ 22 | rustToolchain pkg-config 23 | # Use mold for faster linking 24 | mold clang 25 | ]; 26 | buildInputs = with pkgs; [ 27 | openssl 28 | ]; 29 | in 30 | with pkgs; 31 | { 32 | devShells.default = mkShell { 33 | inherit buildInputs nativeBuildInputs; 34 | RUSTFLAGS = "-Clink-arg=-fuse-ld=${pkgs.mold}/bin/mold"; 35 | LD_LIBRARY_PATH = lib.makeLibraryPath buildInputs; 36 | }; 37 | } 38 | ); 39 | } 40 | -------------------------------------------------------------------------------- /rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | channel = "stable" 3 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use mchprs_core::server::MinecraftServer; 2 | use std::fs; 3 | use std::path::Path; 4 | use tracing::debug; 5 | use tracing_subscriber::filter::LevelFilter; 6 | use tracing_subscriber::fmt::writer::MakeWriterExt; 7 | use tracing_subscriber::EnvFilter; 8 | 9 | fn main() { 10 | // Setup logging 11 | let logfile = tracing_appender::rolling::daily("./logs", "mchprs.log"); 12 | let env_filter = EnvFilter::builder() 13 | .with_default_directive(LevelFilter::INFO.into()) 14 | .with_env_var("MCHPRS_LOG") 15 | .from_env_lossy(); 16 | tracing_subscriber::fmt() 17 | .with_writer(logfile.and(std::io::stdout)) 18 | .with_env_filter(env_filter) 19 | .init(); 20 | 21 | // Move old log file into logs folder 22 | let old_log_path = Path::new("./output.log"); 23 | if old_log_path.exists() { 24 | let dest_path = "./logs/old_output.log"; 25 | fs::rename(old_log_path, "./logs/old_output.log").unwrap(); 26 | debug!( 27 | "Moving old log file from {old_log_path} to {dest_path}", 28 | old_log_path = old_log_path.display() 29 | ); 30 | } 31 | 32 | MinecraftServer::run(); 33 | } 34 | -------------------------------------------------------------------------------- /tests/components.rs: -------------------------------------------------------------------------------- 1 | mod common; 2 | use common::*; 3 | 4 | use mchprs_blocks::blocks::Block; 5 | use mchprs_blocks::BlockDirection; 6 | use mchprs_world::World; 7 | 8 | test_all_backends!(lever_on_off); 9 | fn lever_on_off(backend: TestBackend) { 10 | let lever_pos = pos(0, 1, 0); 11 | 12 | let mut world = TestWorld::new(1); 13 | make_lever(&mut world, lever_pos); 14 | 15 | let mut runner = BackendRunner::new(world, backend); 16 | runner.check_block_powered(lever_pos, false); 17 | 18 | runner.use_block(lever_pos); 19 | runner.check_block_powered(lever_pos, true); 20 | 21 | runner.use_block(lever_pos); 22 | runner.check_block_powered(lever_pos, false); 23 | } 24 | 25 | test_all_backends!(trapdoor_on_off); 26 | fn trapdoor_on_off(backend: TestBackend) { 27 | let lever_pos = pos(0, 1, 0); 28 | let trapdoor_pos = pos(1, 0, 0); 29 | 30 | let mut world = TestWorld::new(1); 31 | make_lever(&mut world, lever_pos); 32 | world.set_block(trapdoor_pos, trapdoor()); 33 | 34 | let mut runner = BackendRunner::new(world, backend); 35 | runner.check_block_powered(trapdoor_pos, false); 36 | 37 | runner.use_block(lever_pos); 38 | runner.check_block_powered(trapdoor_pos, true); 39 | 40 | runner.use_block(lever_pos); 41 | runner.check_block_powered(trapdoor_pos, false); 42 | } 43 | 44 | test_all_backends!(lamp_on_off); 45 | fn lamp_on_off(backend: TestBackend) { 46 | let lever_pos = pos(0, 1, 0); 47 | let lamp_pos = pos(1, 0, 0); 48 | 49 | let mut world = TestWorld::new(1); 50 | make_lever(&mut world, lever_pos); 51 | world.set_block(lamp_pos, Block::RedstoneLamp { lit: false }); 52 | 53 | let mut runner = BackendRunner::new(world, backend); 54 | runner.check_block_powered(lamp_pos, false); 55 | 56 | runner.use_block(lever_pos); 57 | runner.check_block_powered(lamp_pos, true); 58 | 59 | runner.use_block(lever_pos); 60 | runner.check_powered_for(lamp_pos, true, 2); 61 | runner.check_block_powered(lamp_pos, false); 62 | } 63 | 64 | test_all_backends!(wall_torch_on_off); 65 | fn wall_torch_on_off(backend: TestBackend) { 66 | let lever_pos = pos(0, 1, 0); 67 | let torch_pos = pos(1, 0, 0); 68 | 69 | let mut world = TestWorld::new(1); 70 | make_lever(&mut world, lever_pos); 71 | world.set_block( 72 | torch_pos, 73 | Block::RedstoneWallTorch { 74 | lit: true, 75 | facing: BlockDirection::East, 76 | }, 77 | ); 78 | 79 | let mut runner = BackendRunner::new(world, backend); 80 | runner.check_block_powered(torch_pos, true); 81 | 82 | runner.use_block(lever_pos); 83 | runner.check_powered_for(torch_pos, true, 1); 84 | runner.check_block_powered(torch_pos, false); 85 | 86 | runner.use_block(lever_pos); 87 | runner.check_powered_for(torch_pos, false, 1); 88 | runner.check_block_powered(torch_pos, true); 89 | } 90 | 91 | test_all_backends!(torch_on_off); 92 | fn torch_on_off(backend: TestBackend) { 93 | let lever_pos = pos(0, 2, 0); 94 | let torch_pos = pos(2, 2, 0); 95 | 96 | let mut world = TestWorld::new(1); 97 | make_lever(&mut world, lever_pos); 98 | make_wire(&mut world, pos(1, 1, 0)); 99 | place_on_block(&mut world, torch_pos, Block::RedstoneTorch { lit: true }); 100 | 101 | let mut runner = BackendRunner::new(world, backend); 102 | runner.check_block_powered(torch_pos, true); 103 | 104 | runner.use_block(lever_pos); 105 | runner.check_powered_for(torch_pos, true, 1); 106 | runner.check_block_powered(torch_pos, false); 107 | 108 | runner.use_block(lever_pos); 109 | runner.check_powered_for(torch_pos, false, 1); 110 | runner.check_block_powered(torch_pos, true); 111 | } 112 | 113 | test_all_backends!(repeater_on_off); 114 | fn repeater_on_off(backend: TestBackend) { 115 | let lever_pos = pos(0, 2, 0); 116 | let trapdoor_pos = pos(2, 1, 0); 117 | 118 | for delay in 1..=4 { 119 | let mut world = TestWorld::new(1); 120 | make_lever(&mut world, lever_pos); 121 | make_repeater(&mut world, pos(1, 1, 0), delay as u8, BlockDirection::West); 122 | world.set_block(trapdoor_pos, trapdoor()); 123 | 124 | let mut runner = BackendRunner::new(world, backend); 125 | runner.check_block_powered(trapdoor_pos, false); 126 | 127 | // Check with a 1 tick pulse 128 | runner.use_block(lever_pos); 129 | runner.check_powered_for(trapdoor_pos, false, delay); 130 | runner.check_block_powered(trapdoor_pos, true); 131 | runner.use_block(lever_pos); 132 | runner.check_powered_for(trapdoor_pos, true, delay); 133 | runner.check_block_powered(trapdoor_pos, false); 134 | 135 | // Now a 0 tick pulse 136 | runner.use_block(lever_pos); 137 | runner.use_block(lever_pos); 138 | runner.check_powered_for(trapdoor_pos, false, delay); 139 | runner.check_powered_for(trapdoor_pos, true, delay); 140 | runner.check_block_powered(trapdoor_pos, false); 141 | } 142 | } 143 | 144 | test_all_backends!(wire_barely_reaches); 145 | fn wire_barely_reaches(backend: TestBackend) { 146 | let lever_pos = pos(0, 1, 0); 147 | let trapdoor_pos = pos(16, 1, 0); 148 | 149 | let mut world = TestWorld::new(2); 150 | make_lever(&mut world, lever_pos); 151 | // 15 wire blocks between lever and trapdoor 152 | for x in 1..=15 { 153 | make_wire(&mut world, pos(x, 1, 0)); 154 | } 155 | world.set_block(trapdoor_pos, trapdoor()); 156 | 157 | let mut runner = BackendRunner::new(world, backend); 158 | runner.check_block_powered(trapdoor_pos, false); 159 | runner.use_block(lever_pos); 160 | runner.check_block_powered(trapdoor_pos, true); 161 | runner.use_block(lever_pos); 162 | runner.check_block_powered(trapdoor_pos, false); 163 | } 164 | 165 | test_all_backends!(wire_no_reach); 166 | fn wire_no_reach(backend: TestBackend) { 167 | let lever_pos = pos(0, 1, 0); 168 | let trapdoor_pos = pos(17, 1, 0); 169 | 170 | let mut world = TestWorld::new(2); 171 | make_lever(&mut world, lever_pos); 172 | // 16 wire blocks between lever and trapdoor 173 | for x in 1..=16 { 174 | make_wire(&mut world, pos(x, 1, 0)); 175 | } 176 | world.set_block(trapdoor_pos, trapdoor()); 177 | 178 | let mut runner = BackendRunner::new(world, backend); 179 | runner.check_block_powered(trapdoor_pos, false); 180 | runner.use_block(lever_pos); 181 | runner.check_block_powered(trapdoor_pos, false); 182 | runner.use_block(lever_pos); 183 | runner.check_block_powered(trapdoor_pos, false); 184 | } 185 | -------------------------------------------------------------------------------- /tests/timings.rs: -------------------------------------------------------------------------------- 1 | mod common; 2 | use common::*; 3 | 4 | use mchprs_blocks::blocks::{Block, ComparatorMode}; 5 | use mchprs_blocks::BlockDirection; 6 | 7 | test_all_backends!(repeater_t_flip_flop); 8 | fn repeater_t_flip_flop(backend: TestBackend) { 9 | // RN -> Repeater North 10 | // Layout: 11 | // W RN W 12 | // W RN RE 13 | // L 14 | 15 | let mut world = TestWorld::new(1); 16 | 17 | let output_pos = pos(1, 1, 2); 18 | let lever_pos = pos(0, 1, 0); 19 | 20 | make_lever(&mut world, lever_pos); 21 | make_wire(&mut world, pos(1, 1, 0)); 22 | make_wire(&mut world, pos(2, 1, 0)); 23 | 24 | make_repeater(&mut world, pos(1, 1, 1), 1, BlockDirection::North); 25 | make_repeater(&mut world, pos(2, 1, 1), 1, BlockDirection::North); 26 | 27 | make_repeater(&mut world, output_pos, 1, BlockDirection::East); 28 | make_wire(&mut world, pos(2, 1, 2)); 29 | 30 | let mut runner = BackendRunner::new(world, backend); 31 | // Set up initial state 32 | runner.use_block(lever_pos); 33 | runner.check_powered_for(output_pos, false, 2); 34 | 35 | // Toggle flip flop on 36 | runner.use_block(lever_pos); 37 | runner.check_powered_for(output_pos, false, 2); 38 | runner.use_block(lever_pos); 39 | runner.check_powered_for(output_pos, true, 10); 40 | 41 | // Toggle flip flop off 42 | runner.use_block(lever_pos); 43 | runner.check_powered_for(output_pos, true, 2); 44 | runner.use_block(lever_pos); 45 | runner.check_powered_for(output_pos, false, 10); 46 | } 47 | 48 | test_all_backends!(pulse_gen_2t); 49 | fn pulse_gen_2t(backend: TestBackend) { 50 | let output_pos = pos(4, 1, 1); 51 | let lever_pos = pos(0, 1, 1); 52 | 53 | let mut world = TestWorld::new(1); 54 | 55 | make_wire(&mut world, pos(1, 1, 0)); 56 | make_repeater(&mut world, pos(2, 1, 0), 2, BlockDirection::West); 57 | make_wire(&mut world, pos(3, 1, 0)); 58 | 59 | make_lever(&mut world, lever_pos); 60 | make_wire(&mut world, pos(1, 1, 1)); 61 | make_wire(&mut world, pos(2, 1, 1)); 62 | make_comparator( 63 | &mut world, 64 | pos(3, 1, 1), 65 | ComparatorMode::Subtract, 66 | BlockDirection::West, 67 | ); 68 | place_on_block(&mut world, output_pos, trapdoor()); 69 | 70 | let mut runner = BackendRunner::new(world, backend); 71 | 72 | runner.use_block(lever_pos); 73 | runner.check_powered_for(output_pos, false, 1); 74 | runner.check_powered_for(output_pos, true, 2); 75 | runner.check_powered_for(output_pos, false, 10); 76 | } 77 | 78 | test_all_backends!(pulse_gen_1t); 79 | fn pulse_gen_1t(backend: TestBackend) { 80 | let output_pos = pos(5, 1, 1); 81 | let lever_pos = pos(0, 1, 1); 82 | 83 | let mut world = TestWorld::new(1); 84 | 85 | make_wire(&mut world, pos(1, 1, 0)); 86 | make_repeater(&mut world, pos(2, 1, 0), 2, BlockDirection::West); 87 | make_wire(&mut world, pos(3, 1, 0)); 88 | make_wire(&mut world, pos(4, 1, 0)); 89 | 90 | make_lever(&mut world, lever_pos); 91 | make_wire(&mut world, pos(1, 1, 1)); 92 | make_wire(&mut world, pos(2, 1, 1)); 93 | make_comparator( 94 | &mut world, 95 | pos(3, 1, 1), 96 | ComparatorMode::Subtract, 97 | BlockDirection::West, 98 | ); 99 | place_on_block(&mut world, pos(4, 1, 1), Block::Sandstone {}); 100 | place_on_block(&mut world, output_pos, trapdoor()); 101 | 102 | let mut runner = BackendRunner::new(world, backend); 103 | 104 | runner.use_block(lever_pos); 105 | runner.check_powered_for(output_pos, false, 1); 106 | runner.check_powered_for(output_pos, true, 1); 107 | runner.check_powered_for(output_pos, false, 10); 108 | } 109 | --------------------------------------------------------------------------------