diff --git a/.envrc b/.envrc new file mode 100644 index 0000000..3550a30 --- /dev/null +++ b/.envrc @@ -0,0 +1 @@ +use flake diff --git a/.github/diagram.png b/.github/diagram.png deleted file mode 100644 index 8f6f84e..0000000 Binary files a/.github/diagram.png and /dev/null differ diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..758be9c --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,38 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + branches: [main] + +env: + CARGO_TERM_COLOR: always + +jobs: + ci: + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macOS-latest, windows-latest] + runs-on: ${{ matrix.os }} + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Rust Toolchain + run: | + rustup toolchain install stable --profile minimal + rustup component add clippy + + - name: Setup Rust cache + uses: Swatinem/rust-cache@v2 + + - name: Build + run: cargo build --verbose --locked + + - name: Test + run: cargo test --verbose + + - name: Clippy + run: cargo clippy -- -Dwarnings diff --git a/.github/workflows/draft.yml b/.github/workflows/draft.yml new file mode 100644 index 0000000..1a7e70e --- /dev/null +++ b/.github/workflows/draft.yml @@ -0,0 +1,103 @@ +name: Draft + +on: + push: + tags: + - "v[0-9]+.[0-9]+.[0-9]+" + workflow_dispatch: + +env: + CARGO_TERM_COLOR: always + +jobs: + build-unix: + strategy: + matrix: + include: + - os: ubuntu-latest + target: x86_64-unknown-linux-gnu + - os: macos-latest + target: x86_64-apple-darwin + runs-on: ${{ matrix.os }} + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Rust Toolchain + run: rustup toolchain install stable --profile minimal --target ${{ matrix.target }} + + - name: Build + run: | + cargo build --release --locked --target ${{ matrix.target }} + + - name: Pack artifact + env: + ARTIFACT_NAME: haze-${{ matrix.target }} + run: | + mkdir "$ARTIFACT_NAME" + cp "target/${{ matrix.target }}/release/haze" "$ARTIFACT_NAME" + cp README.md LICENSE "$ARTIFACT_NAME" + if ! command -v zip &> /dev/null + then + sudo apt-get update && sudo apt-get install -yq zip + fi + zip -r "$ARTIFACT_NAME.zip" "$ARTIFACT_NAME" + + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + name: haze-${{ matrix.target }}.zip + path: haze-${{ matrix.target }}.zip + + build-windows: + strategy: + matrix: + include: + - os: windows-latest + target: x86_64-pc-windows-msvc + runs-on: ${{ matrix.os }} + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Rust Toolchain + run: rustup toolchain install stable --profile minimal + + - name: Add target + run: rustup target add ${{ matrix.target }} + + - name: Build + run: cargo build --release --locked --target ${{ matrix.target }} + + - name: Pack artifact + env: + TARGET_NAME: haze-${{ matrix.target }} + run: | + New-Item -ItemType Directory -Path ${env:TARGET_NAME} + Copy-Item -Path "target\${{ matrix.target }}\release\haze.exe" -Destination ${env:TARGET_NAME} + Copy-Item -Path "README.md", "LICENSE" -Destination ${env:TARGET_NAME} + Compress-Archive -Path ${env:TARGET_NAME} -DestinationPath "${env:TARGET_NAME}.zip" + + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + name: haze-${{ matrix.target }}.zip + path: haze-${{ matrix.target }}.zip + + draft: + permissions: + contents: write + runs-on: ubuntu-latest + needs: [build-unix, build-windows] + steps: + - name: Grab artifact + uses: actions/download-artifact@v4 + with: + merge-multiple: true + + - name: Draft + uses: softprops/action-gh-release@v1 + if: startsWith(github.ref, 'refs/tags/') + with: + draft: true + files: haze-*.zip diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml deleted file mode 100644 index f10322a..0000000 --- a/.github/workflows/release.yml +++ /dev/null @@ -1,33 +0,0 @@ -on: - push: - tags: - - "v*" - -permissions: - contents: write - -name: 🚚 Release - -jobs: - build: - name: πŸ“¦ Build - runs-on: windows-latest - steps: - - name: πŸ”” Checkout - uses: actions/checkout@v3 - - name: πŸ¦€ Setup Rust Toolchain - uses: actions-rs/toolchain@v1 - with: - profile: minimal - toolchain: stable - - name: πŸ—οΈ Build - uses: actions-rs/cargo@v1 - with: - command: build - args: --release - - name: πŸ“¦ Compress binary - run: Compress-Archive -CompressionLevel Optimal -Force -Path target/release/haze.exe -DestinationPath haze.zip - - name: πŸ“€ Upload release binary - uses: softprops/action-gh-release@v1 - with: - files: haze.zip diff --git a/.gitignore b/.gitignore index ea8c4bf..fdf11bc 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,3 @@ /target +/.direnv +/result diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..23ee002 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,67 @@ +# Changelog + +## [2.0.0](https://github.com/salpland/haze/compare/v1.4.0...v2.0.0) + +### Changes + +- Allow importing and exporting multiple worlds at once +- Fix target worlds not being cleaned up during exporting with the `--overwrite` + flag ([#14](https://github.com/salpland/haze/issues/14)) +- Add a pretty printer for `haze list` +- Rename `--path` flag to `--minecraft-version` and restrict setting its value + to `stable`, `preview`, or `education` +- Allow setting `COM_MOJANG` environment variable to define an arbitrary + `com.mojang` path +- Merge `haze_core` and `haze` into a single crate +- Add colored help message for `haze help` +- Support using comments in `config.json` +- Provide binaries for Linux and MacOS + +## [1.4.0](https://github.com/salpland/haze/compare/v1.3.0...v1.4.0) + +### Changes + +- Add the `--path` flag to allow using predefined export/import paths to + `com.mojang` or custom ones + ([#12](https://github.com/salpland/haze/issues/12)) + +## [1.3.0](https://github.com/salpland/haze/compare/v1.2.0...v1.3.0) + +### Changes + +- Add `haze list` subcommand to list the available worlds in the project. By + [@solvedDev](https://github.com/solvedDev) + ([#10](https://github.com/salpland/haze/issues/10)) +- BREAKING: Rename `test` and `save` subcommands to `export` and `import` + respectively. By [@solvedDev](https://github.com/solvedDev) + ([#8](https://github.com/salpland/haze/issues/8)) +- Extract the core of Haze into a library. By + [@solvedDev](https://github.com/solvedDev) + ([#9](https://github.com/salpland/haze/issues/9)) + +## [1.2.0](https://github.com/salpland/haze/compare/v1.1.0...v1.2.0) + +### Changes + +- BREAKING: Use `worlds` property in the configuration + ([#4](https://github.com/salpland/haze/issues/4)) +- Improve error and logging messages +- Use less bold text for errors +- Update the descriptions for some commands +- Remove diagnostic codes + +## [1.1.0](https://github.com/salpland/haze/compare/v1.0.1...v1.1.0) + +### Changes + +- Disable overwriting in `haze test` by default + ([#1](https://github.com/salpland/haze/issues/1)) +- Add `--overwrite` flag to `haze test` to enable overwriting + ([#2](https://github.com/salpland/haze/issues/2)) +- Improve error/info messages ([#3](https://github.com/salpland/haze/issues/2)) + +## [1.0.1](https://github.com/salpland/haze/compare/v1.0.0...v1.0.1) + +### Changes + +- Update the project's description for more clarity diff --git a/Cargo.lock b/Cargo.lock index 9a3a8f2..d52b60a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,76 +4,106 @@ version = 3 [[package]] name = "addr2line" -version = "0.17.0" +version = "0.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9ecd88a8c8378ca913a680cd98f0f13ac67383d35993f86c90a70e3f137816b" +checksum = "f5fb1d8e4442bd405fdfd1dacb42792696b0cf9cb15882e5d097b742a676d375" dependencies = [ "gimli", ] [[package]] -name = "adler" -version = "1.0.2" +name = "adler2" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" +checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" [[package]] -name = "ahash" -version = "0.7.6" +name = "aho-corasick" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" dependencies = [ - "getrandom", - "once_cell", - "version_check", + "memchr", ] [[package]] -name = "aho-corasick" -version = "0.7.19" +name = "anstream" +version = "0.6.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4f55bd91a0978cbfd91c457a164bab8b4001c833b7f323132c0a4e1922dd44e" +checksum = "64e15c1ab1f89faffbf04a634d5e1962e9074f2741eef6d97f3c4e322426d526" dependencies = [ - "memchr", + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bec1de6f59aedf83baf9ff929c98f2ad654b97c9510f4e70cf6f661d49fd5b1" + +[[package]] +name = "anstyle-parse" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb47de1e80c2b463c735db5b217a0ddc39d612e7ac9e2e96a5aed1f57616c1cb" +dependencies = [ + "utf8parse", ] [[package]] -name = "atty" -version = "0.2.14" +name = "anstyle-query" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +checksum = "6d36fc52c7f6c869915e99412912f22093507da8d9e942ceaf66fe4b7c14422a" dependencies = [ - "hermit-abi", - "libc", - "winapi", + "windows-sys 0.52.0", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5bf74e1b6e971609db8ca7a9ce79fd5768ab6ae46441c572e46cf596f59e57f8" +dependencies = [ + "anstyle", + "windows-sys 0.52.0", ] [[package]] name = "backtrace" -version = "0.3.66" +version = "0.3.74" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cab84319d616cfb654d03394f38ab7e6f0919e181b1b57e1fd15e7fb4077d9a7" +checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" dependencies = [ "addr2line", - "cc", "cfg-if", "libc", "miniz_oxide", "object", "rustc-demangle", + "windows-targets 0.52.6", ] [[package]] -name = "bitflags" -version = "1.3.2" +name = "backtrace-ext" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +checksum = "537beee3be4a18fb023b570f80e3ae28003db9167a751266b259926e25539d50" +dependencies = [ + "backtrace", +] [[package]] -name = "cc" -version = "1.0.74" +name = "bitflags" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "581f5dba903aac52ea3feb5ec4810848460ee833876f1f9b0fdeab1f19091574" +checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" [[package]] name = "cfg-if" @@ -83,27 +113,33 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "clap" -version = "4.0.19" +version = "4.5.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e67816e006b17427c9b4386915109b494fec2d929c63e3bd3561234cbf1bf1e" +checksum = "3e5a21b8495e732f1b3c364c9949b201ca7bae518c502c80256c96ad79eaf6ac" dependencies = [ - "atty", - "bitflags", + "clap_builder", "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8cf2dd12af7a047ad9d6da2b6b249759a22a7abc0f474c1dae1777afa4b21a73" +dependencies = [ + "anstream", + "anstyle", "clap_lex", - "once_cell", "strsim", - "termcolor", ] [[package]] name = "clap_derive" -version = "4.0.18" +version = "4.5.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16a1b0f6422af32d5da0c58e2703320f379216ee70198241c84173a8c5ac28f3" +checksum = "501d359d5f3dcaf6ecdeee48833ae73ec6e42723a1e52419c79abf9507eec0a0" dependencies = [ "heck", - "proc-macro-error", "proc-macro2", "quote", "syn", @@ -111,117 +147,235 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.3.0" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1462739cb27611015575c0c11df5df7601141071f07518d56fcc1be504cbec97" + +[[package]] +name = "color-print" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d4198f73e42b4936b35b5bb248d81d2b595ecb170da0bac7655c54eedfa8da8" +checksum = "1ee543c60ff3888934877a5671f45494dd27ed4ba25c6670b9a7576b7ed7a8c0" dependencies = [ - "os_str_bytes", + "color-print-proc-macro", ] [[package]] -name = "getrandom" -version = "0.2.8" +name = "color-print-proc-macro" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c05aeb6a22b8f62540c194aac980f2115af067bfe15a0734d7277a768d396b31" +checksum = "77ff1a80c5f3cb1ca7c06ffdd71b6a6dd6d8f896c42141fbd43f50ed28dcdb93" dependencies = [ - "cfg-if", + "nom", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "colorchoice" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3fd119d74b830634cea2a0f58bbd0d54540518a14397557951e79340abc28c0" + +[[package]] +name = "console" +version = "0.15.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e1f83fc076bd6dd27517eacdf25fef6c4dfe5f1d7448bafaaf3a26f13b5e4eb" +dependencies = [ + "encode_unicode", + "lazy_static", "libc", - "wasi", + "windows-sys 0.52.0", ] [[package]] -name = "gimli" -version = "0.26.2" +name = "encode_unicode" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22030e2c5a68ec659fde1e949a745124b48e6fa8b045b7ed5bd1fe4ccc5c4e5d" +checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" [[package]] -name = "glob" -version = "0.3.0" +name = "env_filter" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574" +checksum = "4f2c92ceda6ceec50f43169f9ee8424fe2db276791afde7b2cd8bc084cb376ab" +dependencies = [ + "log", + "regex", +] [[package]] -name = "hashbrown" -version = "0.12.3" +name = "env_logger" +version = "0.11.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +checksum = "e13fa619b91fb2381732789fc5de83b45675e882f66623b7d8cb4f643017018d" dependencies = [ - "ahash", + "anstream", + "anstyle", + "env_filter", + "humantime", + "log", ] [[package]] -name = "haze" -version = "1.4.0" +name = "errno" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" dependencies = [ - "clap", - "haze_core", - "miette", - "owo-colors", - "serde", - "serde_json", + "libc", + "windows-sys 0.52.0", ] [[package]] -name = "haze_core" -version = "1.4.0" +name = "fs_extra" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" + +[[package]] +name = "gimli" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32085ea23f3234fc7846555e85283ba4de91e21016dc0455a16286d87a292d64" + +[[package]] +name = "glob" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" + +[[package]] +name = "haze" +version = "2.0.0" dependencies = [ + "anstyle", + "clap", + "color-print", + "env_logger", + "fs_extra", "glob", + "insta-cmd", + "json-strip-comments", + "log", "miette", "serde", "serde_json", "thiserror", + "walkdir", ] [[package]] name = "heck" -version = "0.4.0" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "humantime" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9" +checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" [[package]] -name = "hermit-abi" -version = "0.1.19" +name = "insta" +version = "1.40.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +checksum = "6593a41c7a73841868772495db7dc1e8ecab43bb5c0b6da2059246c4b506ab60" dependencies = [ - "libc", + "console", + "lazy_static", + "linked-hash-map", + "serde", + "similar", +] + +[[package]] +name = "insta-cmd" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffeeefa927925cced49ccb01bf3e57c9d4cd132df21e576eb9415baeab2d3de6" +dependencies = [ + "insta", + "serde", + "serde_json", ] [[package]] name = "is_ci" -version = "1.1.1" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "616cde7c720bb2bb5824a224687d8f77bfd38922027f01d825cd7453be5099fb" +checksum = "7655c9839580ee829dfacba1d1278c2b7883e50a277ff7541299489d6bdfdc45" + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" [[package]] name = "itoa" -version = "1.0.5" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" + +[[package]] +name = "json-strip-comments" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b271732a960335e715b6b2ae66a086f115c74eb97360e996d2bd809bfc063bba" +dependencies = [ + "memchr", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fad582f4b9e86b6caa621cabeb0963332d92eea04729ab12892c2533951e6440" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" -version = "0.2.137" +version = "0.2.158" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8adc4bb1803a324070e64a98ae98f38934d91957a99cfb3a43dcbc01bc56439" + +[[package]] +name = "linked-hash-map" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" + +[[package]] +name = "linux-raw-sys" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" + +[[package]] +name = "log" +version = "0.4.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc7fcc620a3bff7cdd7a365be3376c97191aeaccc2a603e600951e452615bf89" +checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" [[package]] name = "memchr" -version = "2.5.0" +version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" [[package]] name = "miette" -version = "5.4.1" +version = "7.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a24c4b4063c21e037dffb4de388ee85e400bff299803aba9513d9c52de8116b" +checksum = "4edc8853320c2a0dab800fbda86253c8938f6ea88510dc92c5f1ed20e794afc1" dependencies = [ - "atty", "backtrace", + "backtrace-ext", + "cfg-if", "miette-derive", - "once_cell", "owo-colors", "supports-color", "supports-hyperlinks", @@ -234,98 +388,90 @@ dependencies = [ [[package]] name = "miette-derive" -version = "5.4.1" +version = "7.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "827d18edee5d43dc309eb0ac565f2b8e2fdc89b986b2d929e924a0f6e7f23835" +checksum = "dcf09caffaac8068c346b6df2a7fc27a177fd20b39421a39ce0a211bde679a6c" dependencies = [ "proc-macro2", "quote", "syn", ] +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + [[package]] name = "miniz_oxide" -version = "0.5.4" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96590ba8f175222643a85693f33d26e9c8a015f599c216509b1a6894af675d34" +checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1" dependencies = [ - "adler", + "adler2", ] [[package]] -name = "object" -version = "0.29.0" +name = "nom" +version = "7.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21158b2c33aa6d4561f1c0a6ea283ca92bc54802a93b263e910746d679a7eb53" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" dependencies = [ "memchr", + "minimal-lexical", ] [[package]] -name = "once_cell" -version = "1.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86f0b0d4bf799edbc74508c1e8bf170ff5f41238e5f8225603ca7caaae2b7860" - -[[package]] -name = "os_str_bytes" -version = "6.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3baf96e39c5359d2eb0dd6ccb42c62b91d9678aa68160d261b9e0ccbf9e9dea9" - -[[package]] -name = "owo-colors" -version = "3.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1b04fb49957986fdce4d6ee7a65027d55d4b6d2265e5848bbb507b58ccfdb6f" - -[[package]] -name = "proc-macro-error" -version = "1.0.4" +name = "object" +version = "0.36.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +checksum = "084f1a5821ac4c651660a94a7153d27ac9d8a53736203f58b31945ded098070a" dependencies = [ - "proc-macro-error-attr", - "proc-macro2", - "quote", - "syn", - "version_check", + "memchr", ] [[package]] -name = "proc-macro-error-attr" -version = "1.0.4" +name = "owo-colors" +version = "4.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" -dependencies = [ - "proc-macro2", - "quote", - "version_check", -] +checksum = "fb37767f6569cd834a413442455e0f066d0d522de8630436e2a1761d9726ba56" [[package]] name = "proc-macro2" -version = "1.0.47" +version = "1.0.86" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ea3d908b0e36316caf9e9e2c4625cdde190a7e6f440d794667ed17a1855e725" +checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" dependencies = [ "unicode-ident", ] [[package]] name = "quote" -version = "1.0.21" +version = "1.0.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbe448f377a7d6961e30f5955f9b8d106c3f5e449d493ee1b125c1d43c2b5179" +checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" dependencies = [ "proc-macro2", ] [[package]] name = "regex" -version = "1.7.0" +version = "1.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4219d74c6b67a3654a9fbebc4b419e22126d13d2f3c4a07ee0cb61ff79a79619" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e076559ef8e241f2ae3479e36f97bd5741c0330689e217ad51ce2c76808b868a" +checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df" dependencies = [ "aho-corasick", "memchr", @@ -334,36 +480,58 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.6.28" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "456c603be3e8d448b072f410900c09faf164fbce2d480456f50eea6e25f9c848" +checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" [[package]] name = "rustc-demangle" -version = "0.1.21" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" + +[[package]] +name = "rustix" +version = "0.38.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ef03e0a2b150c7a90d01faf6254c9c48a41e95fb2a8c2ac1c6f0d2b9aefc342" +checksum = "8acb788b847c24f28525660c4d7758620a7210875711f79e7f663cc152726811" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.52.0", +] [[package]] name = "ryu" -version = "1.0.12" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" + +[[package]] +name = "same-file" +version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b4b9743ed687d4b4bcedf9ff5eaa7398495ae14e61cba0a295704edbc7decde" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] [[package]] name = "serde" -version = "1.0.147" +version = "1.0.210" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d193d69bae983fc11a79df82342761dfbf28a99fc8d203dca4c3c1b590948965" +checksum = "c8e3592472072e6e22e0a54d5904d9febf8508f65fb8552499a1abc7d1078c3a" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.147" +version = "1.0.210" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f1d362ca8fc9c3e3a7484440752472d68a6caa98f1ab81d99b5dfe517cec852" +checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f" dependencies = [ "proc-macro2", "quote", @@ -372,90 +540,81 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.91" +version = "1.0.128" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "877c235533714907a8c2464236f5c4b2a17262ef1bd71f38f35ea592c8da6883" +checksum = "6ff5456707a1de34e7e37f2a6fd3d3f808c318259cbd01ab6377795054b483d8" dependencies = [ "itoa", + "memchr", "ryu", "serde", ] +[[package]] +name = "similar" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1de1d4f81173b03af4c0cbed3c898f6bff5b870e4a7f5d6f4057d62a7a4b686e" + [[package]] name = "smawk" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f67ad224767faa3c7d8b6d91985b78e70a1324408abcb1cfcc2be4c06bc06043" +checksum = "b7c388c1b5e93756d0c740965c41e8822f866621d41acbdf6336a6a168f8840c" [[package]] name = "strsim" -version = "0.10.0" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "supports-color" -version = "1.3.1" +version = "3.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ba6faf2ca7ee42fdd458f4347ae0a9bd6bcc445ad7cb57ad82b383f18870d6f" +checksum = "8775305acf21c96926c900ad056abeef436701108518cf890020387236ac5a77" dependencies = [ - "atty", "is_ci", ] [[package]] name = "supports-hyperlinks" -version = "1.2.0" +version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "590b34f7c5f01ecc9d78dba4b3f445f31df750a67621cf31626f3b7441ce6406" -dependencies = [ - "atty", -] +checksum = "2c0a1e5168041f5f3ff68ff7d95dcb9c8749df29f6e7e89ada40dd4c9de404ee" [[package]] name = "supports-unicode" -version = "1.0.2" +version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8b945e45b417b125a8ec51f1b7df2f8df7920367700d1f98aedd21e5735f8b2" -dependencies = [ - "atty", -] +checksum = "b7401a30af6cb5818bb64852270bb722533397edcfc7344954a38f420819ece2" [[package]] name = "syn" -version = "1.0.103" +version = "2.0.77" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a864042229133ada95abf3b54fdc62ef5ccabe9515b64717bcb9a1919e59445d" +checksum = "9f35bcdf61fd8e7be6caf75f429fdca8beb3ed76584befb503b1569faee373ed" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] -[[package]] -name = "termcolor" -version = "1.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755" -dependencies = [ - "winapi-util", -] - [[package]] name = "terminal_size" -version = "0.1.17" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "633c1a546cee861a1a6d0dc69ebeca693bf4296661ba7852b9d21d159e0506df" +checksum = "21bebf2b7c9e0a515f6e0f8c51dc0f8e4696391e6f1ff30379559f8365fb0df7" dependencies = [ - "libc", - "winapi", + "rustix", + "windows-sys 0.48.0", ] [[package]] name = "textwrap" -version = "0.15.2" +version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7b3e525a49ec206798b40326a44121291b530c963cfb01018f63e135bac543d" +checksum = "23d434d3f8967a09480fb04132ebe0a3e088c173e6d0ee7897abbdf4eab0f8b9" dependencies = [ "smawk", "unicode-linebreak", @@ -464,18 +623,18 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.37" +version = "1.0.63" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10deb33631e3c9018b9baf9dcbbc4f737320d2b576bac10f6aefa048fa407e3e" +checksum = "c0342370b38b6a11b6cc11d6a805569958d54cfa061a29969c3b5ce2ea405724" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.37" +version = "1.0.63" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "982d17546b47146b28f7c22e3d08465f6b8903d0ea13c1660d9d84a6e7adcdbb" +checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261" dependencies = [ "proc-macro2", "quote", @@ -484,65 +643,191 @@ dependencies = [ [[package]] name = "unicode-ident" -version = "1.0.5" +version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ceab39d59e4c9499d4e5a8ee0e2735b891bb7308ac83dfb4e80cad195c9f6f3" +checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe" [[package]] name = "unicode-linebreak" -version = "0.1.4" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f" + +[[package]] +name = "unicode-width" +version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5faade31a542b8b35855fff6e8def199853b2da8da256da52f52f1316ee3137" +checksum = "0336d538f7abc86d282a4189614dfaa90810dfc2c6f6427eaf88e16311dd225d" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" dependencies = [ - "hashbrown", - "regex", + "same-file", + "winapi-util", ] [[package]] -name = "unicode-width" -version = "0.1.10" +name = "winapi-util" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" +checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" +dependencies = [ + "windows-sys 0.59.0", +] [[package]] -name = "version_check" -version = "0.9.4" +name = "windows-sys" +version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] [[package]] -name = "wasi" -version = "0.11.0+wasi-snapshot-preview1" +name = "windows-sys" +version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] [[package]] -name = "winapi" -version = "0.3.9" +name = "windows-sys" +version = "0.59.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" dependencies = [ - "winapi-i686-pc-windows-gnu", - "winapi-x86_64-pc-windows-gnu", + "windows-targets 0.52.6", ] [[package]] -name = "winapi-i686-pc-windows-gnu" -version = "0.4.0" +name = "windows-targets" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] [[package]] -name = "winapi-util" -version = "0.1.5" +name = "windows-targets" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ - "winapi", + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", ] [[package]] -name = "winapi-x86_64-pc-windows-gnu" -version = "0.4.0" +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" diff --git a/Cargo.toml b/Cargo.toml index 146e8f9..4391ca3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,13 +1,29 @@ -[workspace] -members = ["crates/haze", "crates/haze_core"] +[package] +name = "haze" +version = "2.0.0" +edition = "2021" +description = "Dead simple world management tool for Minecraft Bedrock." -[workspace.dependencies] -haze_core = { path = "crates/haze_core" } +[dependencies] +anstyle = "1.0.8" +clap = { version = "4.5.17", features = ["derive"] } +color-print = "0.3.6" +env_logger = "0.11.5" +fs_extra = "1.3.0" +glob = "0.3.1" +json-strip-comments = "1.0.4" +log = "0.4.22" +miette = { version = "7.2.0", features = ["fancy"] } +serde = { version = "1.0.210", features = ["derive"] } +serde_json = "1.0.128" +thiserror = "1.0.63" +walkdir = "2.5.0" -clap = { version = "4.0.19", features = ["derive"] } -glob = "0.3.0" -miette = { version = "5.3.0", features = ["fancy"] } -owo-colors = "3.5.0" -serde = { version = "1.0.147", features = ["derive"] } -serde_json = "1.0.87" -thiserror = "1.0.37" +[dev-dependencies] +insta-cmd = "0.6.0" + +[profile.release] +strip = true +lto = true +opt-level = "s" +codegen-units = 1 diff --git a/LICENSE b/LICENSE index a03a3f5..c4037e9 100644 --- a/LICENSE +++ b/LICENSE @@ -1,7 +1,21 @@ -Copyright (c) 2022 Arexon +MIT License -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: +Copyright (c) 2024 arexon -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index 5c96621..628de7c 100644 --- a/README.md +++ b/README.md @@ -1,61 +1,185 @@ -# ☘️ Haze +

+ Haze + Haze logo +

-> A simple command line tool to manage your Minecraft Bedrock worlds +> Dead simple world management tool for Minecraft Bedrock -Haze allows you to keep your project's worlds out of the `com.mojang` directory and place them in your project's directory instead. This way you can easily work on multiple worlds and move them back and forth between `com.mojang` and your project's directory. +Haze follows +[Regolith's philosophy](https://bedrock-oss.github.io/regolith/guide/what-is-regolith) +of keeping your project files outside of `com.mojang`, and further extends it to +include worlds too. This can be particularly useful for source control when +working within a team. -![Diagram](.github/diagram.png) +![Diagram](assets/diagram.svg) -## πŸ“¦ Installation +An example workflow could be like this: -Open PowerShell on Windows and run: +1. You pull in some changes from a git repository +2. You run `haze export some_wip_world` to export the world to `com.mojang` +3. You modify the world in Minecraft +4. After you're done, you run `haze import some_wip_world` to import the world + into your project +5. You commit and push the changes you made + +Additionally, this workflow allows for operations such as quick +reloading/reverting of a world state to a previous one by running +`haze export --overwrite some_existing_world`. + +## Installation + +### Windows + +A Powershell installation script is available. I'd recommend taking a good look +at this script before running it. ```powershell -irm https://raw.githubusercontent.com/salpland/haze/main/scripts/install.ps1 | iex +irm https://raw.githubusercontent.com/arexon/haze/main/scripts/install.ps1 | iex ``` -**You can also use this same command to update Haze.** +Alternatively, you can download a pre-built binary from the +[releases](https://github.com/arexon/haze/releases) page. + +### Linux & macOS -## 🧩 Usage +There's no installation scripts for either OSs, but pre-built binaries are +available in the [releases](https://github.com/arexon/haze/releases) page. From +there you can try to install Haze manually. -Haze requires your project to include a config file that follows the [Project Config Standard](https://github.com/Bedrock-OSS/project-config-standard). +### Nix -This also means that you can integrate Haze into projects that use [Regolith](https://github.com/Bedrock-OSS/regolith) or [bridge.'s Dash compiler](https://github.com/bridge-core/deno-dash-compiler) seamlessly. +```console +# Try out with +nix run github:arexon/haze -### πŸ—ΊοΈ Setting up worlds +# ..or create a devshell +nix shell github:arexon/haze +``` -Here is the required config: +If you're using flakes: -```jsonc +```nix +# flake.nix { - // Now any world inside the "worlds" directory can be used in the command line argument. - "worlds": ["./worlds/*"], + inputs = { + haze.url = "github:arexon/haze"; + }; } ``` -You can also reference multiple directories that store worlds: +### Building from source + +Depending on which OS you're on, make sure you either have `rustup` or `cargo` +installed. + +1. Clone the repository: + +```console +git clone https://github.com/arexon/haze.git --depth 1 +cd haze +``` -```jsonc +2. Build and install + +```console +cargo install --path . +``` + +Refer to +[`cargo install` docs](https://doc.rust-lang.org/cargo/commands/cargo-install.html) +for more info. + +## Usage + +You must have a `config.json` file at the root of your project and it must +follow the +[Project Config Standard](https://github.com/Bedrock-OSS/project-config-standard). +If you're using [Regolith](https://bedrock-oss.github.io/regolith/), +[bridge.](https://bridge-core.app/), or +[Dash compiler](https://github.com/bridge-core/deno-dash-compiler) you should +already be familiar with it. + +Define where Haze should look for your worlds. This can be a glob pattern or a +direct path. + +```diff +// config.json { - "worlds": ["./worlds/dev/*", "./worlds/demo/*"], + { + "name": "my-project", ++ "worlds": [ ++ "./worlds/*", ++ "./testing_worlds/playground" ++ ], + "packs": { + "behaviorPack": "./packs/BP", + "resourcePack": "./packs/RP" + } + } } ``` -### πŸ–₯️ Running commands +### The `com.mojang` directory + +If you're on Windows, Haze will try to look for the `com.mojang` directory for +stable versions of Minecraft by default. You can change this with +`haze --minecraft-version [VERSION]` and then supplying `preview` or +`education`. + +You can also define an arbitrary path to `com.mojang` by setting the +`COM_MOJANG` environment variable. Doing so will override the +`--minecraft-version` option. -Run `haze --help` or reference the docs below: +On Unix systems, the `--minecraft-version` option isn't available and you must +set `COM_MOJANG` instead. -| Command | Description | -| ------- | ----------- | -| `haze export ` | Copy a world from the project's worlds directory to "minecraftWorlds" | -| `haze export --overwrite ` | Overwrites if a world with the same name is already in "minecraftWorlds" | -| `haze export --path [stable, preview, education, ]` | Predefined or custom export path | -| `haze import ` | Copy a world from "minecraftWorlds" to the project's worlds directory | -| `haze import --path [stable, preview, education, ] ` | Predefined or custom import path | -| `haze list` | Lists the available worlds in the project config | +### Exporting, importing, and listing worlds + +Let's say your project has the following directory structure: + +```yaml +my-project +|-- config.json +|-- packs/ +`-- worlds + |-- bar/ + `-- foo/ +``` + +You can export `foo` like so: + +```console +haze export foo +``` + +And then import it back: + +```console +haze import foo +``` + +If `foo` is also present in `com.mojang`, you'll have to overwrite it: + +```console +haze export --overwrite foo +``` + +You can operate on multiple worlds as well: + +```console +haze export --overwrite foo bar +haze import foo bar +``` + +And lastly, you can list all worlds stored locally in your project _and_ in +`com.mojang`: + +```console +haze list +``` -Note: `` is the world directory name. +You can refer to `haze help` for more info. -## πŸ“ License +## License -Haze is under the MIT license. +Haze is licensed under MIT. Check the [license file](LICENSE) for more info. diff --git a/assets/diagram.excalidraw b/assets/diagram.excalidraw new file mode 100644 index 0000000..720dc87 --- /dev/null +++ b/assets/diagram.excalidraw @@ -0,0 +1,571 @@ +{ + "type": "excalidraw", + "version": 2, + "source": "https://excalidraw.com", + "elements": [ + { + "type": "rectangle", + "version": 475, + "versionNonce": 1304293445, + "index": "a0", + "isDeleted": false, + "id": "p5XdFpVhFWf-s58h9jxrx", + "fillStyle": "hachure", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 2, + "opacity": 100, + "angle": 0, + "x": 933.7885939323685, + "y": -429.3185724944263, + "strokeColor": "#8b6d9c", + "backgroundColor": "#272744", + "width": 114.2371091371395, + "height": 182.16187673219537, + "seed": 812429756, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "boundElements": [ + { + "id": "ZVRSFvmss7JEsSuJw5q64", + "type": "arrow" + }, + { + "id": "fd82c1hM3WvaqKjh-yqUr", + "type": "arrow" + }, + { + "id": "4gsV0vQhI4wiUdrTQkEJV", + "type": "arrow" + }, + { + "id": "2kErL3ntOoYap-Nf1RDMh", + "type": "arrow" + } + ], + "updated": 1726923481116, + "link": null, + "locked": false + }, + { + "type": "text", + "version": 264, + "versionNonce": 1814211749, + "index": "a1", + "isDeleted": false, + "id": "l2dHbaI7nozXDpE_cq3U0", + "fillStyle": "hachure", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 956.7304372163142, + "y": -359.7799075554032, + "strokeColor": "#c69fa5", + "backgroundColor": "#272744", + "width": 66.13333333333333, + "height": 35, + "seed": 130538244, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1726923517339, + "link": null, + "locked": false, + "fontSize": 28, + "fontFamily": 1, + "text": "Haze", + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "Haze", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "type": "rectangle", + "version": 824, + "versionNonce": 1211800645, + "index": "a1V", + "isDeleted": false, + "id": "x1sI2n1tD6z-mIdF5r0sN", + "fillStyle": "hachure", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 2, + "opacity": 100, + "angle": 0, + "x": 552.15781210773, + "y": -401.0579218238574, + "strokeColor": "#8b6d9c", + "backgroundColor": "#272744", + "width": 182.25600478322443, + "height": 98.72200259091322, + "seed": 797278340, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "boundElements": [ + { + "id": "ZVRSFvmss7JEsSuJw5q64", + "type": "arrow" + }, + { + "id": "4gsV0vQhI4wiUdrTQkEJV", + "type": "arrow" + } + ], + "updated": 1726923497526, + "link": null, + "locked": false + }, + { + "type": "text", + "version": 890, + "versionNonce": 1579654187, + "index": "a2", + "isDeleted": false, + "id": "btbt3HlLK3plP5HP7rJkA", + "fillStyle": "hachure", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 2, + "opacity": 100, + "angle": 0, + "x": 577.6739071027429, + "y": -370.6414845459227, + "strokeColor": "#c69fa5", + "backgroundColor": "#272744", + "width": 128.33333333333334, + "height": 31.304046095242796, + "seed": 1189409212, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1726923517339, + "link": null, + "locked": false, + "fontSize": 25.043236876194236, + "fontFamily": 1, + "text": "com.mojang", + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "com.mojang", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "type": "arrow", + "version": 608, + "versionNonce": 2066994187, + "index": "a7", + "isDeleted": false, + "id": "ZVRSFvmss7JEsSuJw5q64", + "fillStyle": "hachure", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 924.5889525194204, + "y": -401.90656754107755, + "strokeColor": "#f2d3ab", + "backgroundColor": "#ced4da", + "width": 281.36960906420813, + "height": 73.72434649543226, + "seed": 1011060740, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "boundElements": [], + "updated": 1726923495256, + "link": null, + "locked": false, + "startBinding": { + "elementId": "p5XdFpVhFWf-s58h9jxrx", + "focus": 0.38249328167948443, + "gap": 9.19964141294804, + "fixedPoint": null + }, + "endBinding": { + "elementId": "x1sI2n1tD6z-mIdF5r0sN", + "focus": -0.450614577237917, + "gap": 15.89838773012275, + "fixedPoint": null + }, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": "arrow", + "points": [ + [ + 0, + 0 + ], + [ + -225.44955573360846, + -73.72434649543226 + ], + [ + -281.36960906420813, + -15.049742012902584 + ] + ] + }, + { + "type": "arrow", + "version": 687, + "versionNonce": 1638630949, + "index": "a8", + "isDeleted": false, + "id": "4gsV0vQhI4wiUdrTQkEJV", + "fillStyle": "hachure", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 639.0353263951105, + "y": -290.25385626970007, + "strokeColor": "#f2d3ab", + "backgroundColor": "#ced4da", + "width": 285.1364716721721, + "height": 56.218990611059326, + "seed": 662339900, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "boundElements": [], + "updated": 1726923495256, + "link": null, + "locked": false, + "startBinding": { + "elementId": "x1sI2n1tD6z-mIdF5r0sN", + "focus": 0.3311240483654274, + "gap": 12.082062963244084, + "fixedPoint": null + }, + "endBinding": { + "elementId": "p5XdFpVhFWf-s58h9jxrx", + "focus": -0.59596541534489, + "gap": 9.616795865085919, + "fixedPoint": null + }, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": "arrow", + "points": [ + [ + 0, + 0 + ], + [ + 32.31666546545114, + 56.218990611059326 + ], + [ + 285.1364716721721, + 20.525162109511427 + ] + ] + }, + { + "type": "arrow", + "version": 1170, + "versionNonce": 1184335531, + "index": "a9", + "isDeleted": false, + "id": "fd82c1hM3WvaqKjh-yqUr", + "fillStyle": "hachure", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 1056.7875232699537, + "y": -271.4897490675508, + "strokeColor": "#f2d3ab", + "backgroundColor": "#ced4da", + "width": 289.5118134758168, + "height": 58.01092904480879, + "seed": 1042309180, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "boundElements": [], + "updated": 1726923478693, + "link": null, + "locked": false, + "startBinding": { + "elementId": "p5XdFpVhFWf-s58h9jxrx", + "focus": 0.5459866037054745, + "gap": 8.761820200445754, + "fixedPoint": null + }, + "endBinding": { + "elementId": "zGZWE--t_YJScwxekjh2g", + "focus": -0.32561488849318254, + "gap": 7.642612648420084, + "fixedPoint": null + }, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": "arrow", + "points": [ + [ + 0, + 0 + ], + [ + 253.99027488370484, + 44.532825058752564 + ], + [ + 289.5118134758168, + -13.478103986056226 + ] + ] + }, + { + "type": "arrow", + "version": 1673, + "versionNonce": 898223493, + "index": "aA", + "isDeleted": false, + "id": "2kErL3ntOoYap-Nf1RDMh", + "fillStyle": "hachure", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 1348.4407290648858, + "y": -399.3774552492514, + "strokeColor": "#f2d3ab", + "backgroundColor": "#ced4da", + "width": 291.9828076628528, + "height": 61.04687995747355, + "seed": 953872260, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "boundElements": [], + "updated": 1726923478693, + "link": null, + "locked": false, + "startBinding": { + "elementId": "zGZWE--t_YJScwxekjh2g", + "focus": 0.32459479731682106, + "gap": 8.04498695631105, + "fixedPoint": null + }, + "endBinding": { + "elementId": "p5XdFpVhFWf-s58h9jxrx", + "focus": -0.4792962435697572, + "gap": 8.432218332525053, + "fixedPoint": null + }, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": "arrow", + "points": [ + [ + 0, + 0 + ], + [ + -33.675602717080665, + -61.04687995747355 + ], + [ + -291.9828076628528, + -3.2924236241359495 + ] + ], + "elbowed": false + }, + { + "type": "text", + "version": 341, + "versionNonce": 803185355, + "index": "aB", + "isDeleted": false, + "id": "FUmP1IuxNidB251kLNVXN", + "fillStyle": "hachure", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 6.065510823833721, + "x": 1135.5677239224513, + "y": -471.9252082220138, + "strokeColor": "#f2d3ab", + "backgroundColor": "#ced4da", + "width": 166.86666870117188, + "height": 24.258016225651716, + "seed": 1107964732, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [ + { + "id": "2kErL3ntOoYap-Nf1RDMh", + "type": "arrow" + } + ], + "updated": 1726923453118, + "link": null, + "locked": false, + "fontSize": 19.406412980521374, + "fontFamily": 1, + "text": "Exporting a world", + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "Exporting a world", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "type": "text", + "version": 299, + "versionNonce": 373804683, + "index": "aC", + "isDeleted": false, + "id": "qMRIANC5Nh_GL_vtRllOX", + "fillStyle": "hachure", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 6.136871706674989, + "x": 676.1915886134323, + "y": -240.91432304062033, + "strokeColor": "#f2d3ab", + "backgroundColor": "#ced4da", + "width": 170.61666666666667, + "height": 25, + "seed": 1175237180, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [ + { + "id": "fd82c1hM3WvaqKjh-yqUr", + "type": "arrow" + } + ], + "updated": 1726923466245, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 1, + "text": "Importing a world", + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "Importing a world", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "type": "rectangle", + "version": 1025, + "versionNonce": 383511205, + "index": "aG", + "isDeleted": false, + "id": "zGZWE--t_YJScwxekjh2g", + "fillStyle": "hachure", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 2, + "opacity": 100, + "angle": 0, + "x": 1250.5617335277793, + "y": -391.33246829294035, + "strokeColor": "#8b6d9c", + "backgroundColor": "#272744", + "width": 182.25600478322443, + "height": 98.72200259091322, + "seed": 2077423205, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "boundElements": [ + { + "id": "fd82c1hM3WvaqKjh-yqUr", + "type": "arrow" + }, + { + "id": "2kErL3ntOoYap-Nf1RDMh", + "type": "arrow" + } + ], + "updated": 1726923487333, + "link": null, + "locked": false + }, + { + "type": "text", + "version": 1141, + "versionNonce": 785371141, + "index": "aH", + "isDeleted": false, + "id": "yUoSfjfr7vKWJYrx5aDjr", + "fillStyle": "hachure", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 2, + "opacity": 100, + "angle": 0, + "x": 1297.5288761529923, + "y": -359.07886862539766, + "strokeColor": "#c69fa5", + "backgroundColor": "#272744", + "width": 89.69999694824219, + "height": 31.304046095242796, + "seed": 2102452677, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1726923517339, + "link": null, + "locked": false, + "fontSize": 25.043236876194236, + "fontFamily": 1, + "text": "Project", + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "Project", + "autoResize": true, + "lineHeight": 1.25 + } + ], + "appState": { + "gridSize": 20, + "gridStep": 5, + "gridModeEnabled": false, + "viewBackgroundColor": "#1b1822" + }, + "files": {} +} \ No newline at end of file diff --git a/assets/diagram.svg b/assets/diagram.svg new file mode 100644 index 0000000..0cbd06b --- /dev/null +++ b/assets/diagram.svg @@ -0,0 +1,13 @@ + + + + + + + + Hazecom.mojangExporting a worldImporting a worldProject \ No newline at end of file diff --git a/assets/logo.aseprite b/assets/logo.aseprite new file mode 100644 index 0000000..ab35414 Binary files /dev/null and b/assets/logo.aseprite differ diff --git a/assets/logo.png b/assets/logo.png new file mode 100644 index 0000000..bef192b Binary files /dev/null and b/assets/logo.png differ diff --git a/crates/haze/Cargo.toml b/crates/haze/Cargo.toml deleted file mode 100644 index e08c576..0000000 --- a/crates/haze/Cargo.toml +++ /dev/null @@ -1,14 +0,0 @@ -[package] -name = "haze" -version = "1.4.0" -description = "A simple command line tool to manage your Minecraft Bedrock worlds" -author = "Arexon " -edition = "2021" - -[dependencies] -clap.workspace = true -haze_core.workspace = true -miette.workspace = true -owo-colors.workspace = true -serde_json.workspace = true -serde.workspace = true diff --git a/crates/haze/src/cli.rs b/crates/haze/src/cli.rs deleted file mode 100644 index 01a62c1..0000000 --- a/crates/haze/src/cli.rs +++ /dev/null @@ -1,45 +0,0 @@ -use clap::{Parser, Subcommand}; - -#[derive(Parser)] -#[command(author, version, about, long_about = None)] -pub struct Cli { - /// Set a path to the config file - #[arg(short, long, value_name = "PATH", default_value = "config.json")] - pub config: String, - - #[command(subcommand)] - pub command: Command, -} - -#[derive(Subcommand)] -pub enum Command { - /// Copy a world from the project's worlds directory to "minecraftWorlds" - Export { - /// The name of the world to export - name: String, - /// Overwrite an already existing world with the same name - #[arg(short, long)] - overwrite: bool, - /// The path to export the world to. - /// - /// Defaults to "stable". - /// - /// A custom path must point to a directory that contains your worlds. - #[arg(short, long, default_value = "stable")] - path: String, - }, - /// Copy a world from "minecraftWorlds" to the project's worlds directory - Import { - /// The name of the world to import - name: String, - /// The path to import the world from. - /// - /// Defaults to "stable". - /// - /// A custom path must point to a directory that contains your worlds. - #[arg(short, long, default_value = "stable")] - path: String, - }, - /// List all worlds in the project's worlds directory - List, -} diff --git a/crates/haze/src/config.rs b/crates/haze/src/config.rs deleted file mode 100644 index db6ae3d..0000000 --- a/crates/haze/src/config.rs +++ /dev/null @@ -1,17 +0,0 @@ -use serde::Deserialize; -use std::fs; - -use haze_core::Error; - -#[derive(Deserialize)] -pub struct Config { - pub worlds: Vec, -} - -pub fn load(path: String) -> Result { - let config = - fs::read_to_string(&path).map_err(|e| Error::CannotReadConfig(e.kind(), path.clone()))?; - let config = serde_json::from_str(&config).map_err(|e| Error::CannotParseConfig(e, path))?; - - Ok(config) -} diff --git a/crates/haze/src/main.rs b/crates/haze/src/main.rs deleted file mode 100644 index 2fd7ecd..0000000 --- a/crates/haze/src/main.rs +++ /dev/null @@ -1,51 +0,0 @@ -mod cli; -mod config; - -use clap::Parser; -use cli::{Cli, Command}; -use miette::Result; -use owo_colors::OwoColorize; - -fn main() -> Result<()> { - let cli = Cli::parse(); - let config = config::load(cli.config)?; - match cli.command { - Command::Export { - name, - overwrite, - path, - } => { - haze_core::export(&name, &config.worlds, &path, overwrite)?; - if overwrite { - println!( - "{} world \"{}\" in the \"minecraftWorlds\" directory ({})", - "Updated".bold().green(), - name, - "overwrite".red() - ); - } else { - println!( - "{} world \"{}\" to the \"minecraftWorlds\" directory for testing", - "Copied".bold().green(), - name - ); - } - } - Command::Import { name, path } => { - haze_core::import(&name, &config.worlds, &path)?; - println!( - "{} world \"{}\" to the local worlds directory", - "Saved".bold().green(), - name - ); - } - Command::List => { - println!("Available worlds:"); - for world in haze_core::all_worlds(&config.worlds)? { - println!(" {} {world}", ">".cyan()); - } - } - } - - Ok(()) -} diff --git a/crates/haze_core/Cargo.toml b/crates/haze_core/Cargo.toml deleted file mode 100644 index 6dd0caf..0000000 --- a/crates/haze_core/Cargo.toml +++ /dev/null @@ -1,15 +0,0 @@ -[package] -name = "haze_core" -version = "1.4.0" -edition = "2021" -description = "A simple command line tool to manage your Minecraft Bedrock worlds" -author = "Arexon " -repository = "https://github.com/salpland/haze/" -license = "MIT" - -[dependencies] -glob.workspace = true -miette.workspace = true -serde_json.workspace = true -serde.workspace = true -thiserror.workspace = true diff --git a/crates/haze_core/src/error.rs b/crates/haze_core/src/error.rs deleted file mode 100644 index 6aa071e..0000000 --- a/crates/haze_core/src/error.rs +++ /dev/null @@ -1,38 +0,0 @@ -use miette::Diagnostic; -use std::{io, path::PathBuf}; -use thiserror::Error; - -/// The library error type. -#[derive(Error, Diagnostic, Debug)] -pub enum Error { - #[error("cannot read the config file at `{1}`")] - #[diagnostic(help("{0}"))] - CannotReadConfig(io::ErrorKind, String), - - #[error("the config file at `{1}` was not parsed")] - #[diagnostic(help("{0}"))] - CannotParseConfig(serde_json::Error, String), - - #[error("the `worlds` config property is empty")] - #[diagnostic(help("the property must include at least one pattern"))] - EmptyWorldsProperty, - - #[error("invalid glob pattern in `worlds` property `{1}`")] - #[diagnostic(help("{0}"))] - InvalidWorldsGlobPattern(glob::PatternError, String), - - #[error("cannot find the world `{1}` in local worlds directory")] - #[diagnostic(help("available worlds: {0:?}"))] - WorldNotFound(Vec, String), - - #[error("unable to find the local appdata directory")] - CannotFindLocalAppData(), - - #[error("cannot copy the world `{1}`")] - #[diagnostic(help("{0}"))] - CannotCopyWorld(io::ErrorKind, String), - - #[error("cannot overwrite the world `{0}` as it already exists in `minecraftWorlds`")] - #[diagnostic(help("do \"haze test --overwrite {0}\" if you want to overwrite it"))] - CannotOverwriteWorld(String), -} diff --git a/crates/haze_core/src/lib.rs b/crates/haze_core/src/lib.rs deleted file mode 100644 index 66e4360..0000000 --- a/crates/haze_core/src/lib.rs +++ /dev/null @@ -1,5 +0,0 @@ -mod error; -mod world; - -pub use error::Error; -pub use world::{all_worlds, export, import}; diff --git a/crates/haze_core/src/world.rs b/crates/haze_core/src/world.rs deleted file mode 100644 index 2c1e567..0000000 --- a/crates/haze_core/src/world.rs +++ /dev/null @@ -1,117 +0,0 @@ -use std::{ - env, fs, io, - path::{Path, PathBuf}, -}; - -use crate::error::Error; - -/// Exports the given world to the `minecraftWorlds` directory. -pub fn export(name: &str, worlds: &[String], target: &str, overwrite: bool) -> Result<(), Error> { - let from = local_worlds_dir(worlds, name)?; - let to = mojang_worlds_dir(name, target)?; - - if to.exists() && !overwrite { - return Err(Error::CannotOverwriteWorld(name.to_string())); - } - - copy_dir(&from, &to).map_err(|e| Error::CannotCopyWorld(e.kind(), name.to_string()))?; - - Ok(()) -} - -/// Imports the given world from the `minecraftWorlds` directory. -pub fn import(name: &str, worlds: &[String], target: &str) -> Result<(), Error> { - let from = mojang_worlds_dir(name, target)?; - let to: PathBuf = local_worlds_dir(worlds, name)?; - - copy_dir(&from, &to).map_err(|e| Error::CannotCopyWorld(e.kind(), name.to_string()))?; - - Ok(()) -} - -/// Returns the list of worlds from the given glob patterns. -pub fn all_worlds(globs: &[String]) -> Result, Error> { - let worlds = globs - .iter() - .map(|pattern| { - glob::glob(pattern).map_err(|e| Error::InvalidWorldsGlobPattern(e, pattern.to_string())) - }) - .collect::, _>>()? - .into_iter() - .flatten() - .filter_map(Result::ok) - .filter(|p| p.is_dir()) - .map(|p| p.file_name().unwrap().to_string_lossy().to_string()) - .collect::>(); - - if worlds.is_empty() { - return Err(Error::EmptyWorldsProperty); - } - - Ok(worlds) -} - -/// Returns local worlds directory from the given glob patterns. -fn local_worlds_dir(globs: &[String], name: &str) -> Result { - let paths = globs - .iter() - .map(|pattern| { - glob::glob(pattern).map_err(|e| Error::InvalidWorldsGlobPattern(e, pattern.to_string())) - }) - .collect::, _>>()? - .into_iter() - .flatten() - .filter_map(Result::ok) - .filter(|p| p.is_dir()) - .collect::>(); - - if paths.is_empty() { - return Err(Error::EmptyWorldsProperty); - } - - match paths.iter().find(|p| p.ends_with(name)) { - Some(path) => Ok(path.clone()), - None => Err(Error::WorldNotFound(paths, name.to_string())), - } -} - -/// Returns the path to the mojang worlds directory. -fn mojang_worlds_dir(name: &str, target: &str) -> Result { - let target = match target { - "stable" => "Microsoft.MinecraftUWP_8wekyb3d8bbwe", - "preview" => "Microsoft.MinecraftWindowsBeta_8wekyb3d8bbwe", - "education" => "Microsoft.MinecraftEducationEdition_8wekyb3d8bbwe", - path => return Ok(PathBuf::from(path)), - }; - - env::var("LOCALAPPDATA") - .map(|base| { - PathBuf::from(&base) - .join("Packages") - .join(target) - .join("LocalState") - .join("games") - .join("com.mojang") - .join("minecraftWorlds") - .join(name) - }) - .ok() - .ok_or_else(Error::CannotFindLocalAppData) -} - -/// Copies a directory recursively. -fn copy_dir(from: &Path, to: &Path) -> Result<(), io::Error> { - fs::create_dir_all(to)?; - - for entry in fs::read_dir(from)? { - let entry = entry?; - let file_type = entry.file_type()?; - if file_type.is_dir() { - copy_dir(&entry.path(), &to.join(entry.file_name()))?; - } else { - fs::copy(&entry.path(), &to.join(entry.file_name()))?; - } - } - - Ok(()) -} diff --git a/dprint.json b/dprint.json new file mode 100644 index 0000000..a3a1908 --- /dev/null +++ b/dprint.json @@ -0,0 +1,8 @@ +{ + "markdown": { + "textWrap": "always" + }, + "plugins": [ + "https://plugins.dprint.dev/markdown-0.17.8.wasm" + ] +} diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..16f44b6 --- /dev/null +++ b/flake.lock @@ -0,0 +1,98 @@ +{ + "nodes": { + "crane": { + "locked": { + "lastModified": 1725409566, + "narHash": "sha256-PrtLmqhM6UtJP7v7IGyzjBFhbG4eOAHT6LPYOFmYfbk=", + "owner": "ipetkov", + "repo": "crane", + "rev": "7e4586bad4e3f8f97a9271def747cf58c4b68f3c", + "type": "github" + }, + "original": { + "owner": "ipetkov", + "repo": "crane", + "type": "github" + } + }, + "flake-utils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1710146030, + "narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1726042813, + "narHash": "sha256-LnNKCCxnwgF+575y0pxUdlGZBO/ru1CtGHIqQVfvjlA=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "159be5db480d1df880a0135ca0bfed84c2f88353", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixpkgs-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "crane": "crane", + "flake-utils": "flake-utils", + "nixpkgs": "nixpkgs", + "rust-overlay": "rust-overlay" + } + }, + "rust-overlay": { + "inputs": { + "nixpkgs": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1726021481, + "narHash": "sha256-4J4E+Fh+77XIYnq2RVtg+ENWXpu6t74P0jKN/f2RQmI=", + "owner": "oxalica", + "repo": "rust-overlay", + "rev": "1c2c120246c51a644c20ba2a36a33d3bd4860d70", + "type": "github" + }, + "original": { + "owner": "oxalica", + "repo": "rust-overlay", + "type": "github" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..e583842 --- /dev/null +++ b/flake.nix @@ -0,0 +1,83 @@ +{ + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable"; + + flake-utils.url = "github:numtide/flake-utils"; + + rust-overlay = { + url = "github:oxalica/rust-overlay"; + inputs.nixpkgs.follows = "nixpkgs"; + }; + + crane.url = "github:ipetkov/crane"; + }; + + outputs = { + nixpkgs, + flake-utils, + rust-overlay, + crane, + ... + }: + flake-utils.lib.eachDefaultSystem (system: let + pkgs = import nixpkgs { + inherit system; + overlays = [(import rust-overlay)]; + }; + inherit (pkgs) lib; + + craneLib = (crane.mkLib pkgs).overrideToolchain (p: p.rust-bin.fromRustupToolchainFile ./rust-toolchain.toml); + + commonArgs = { + src = lib.cleanSourceWith { + src = lib.cleanSource ./.; + filter = path: type: let + pathStr = toString path; + isTestSource = builtins.match ".*/tests/.*" pathStr != null; + isCargoSource = craneLib.filterCargoSources path type; + in + isCargoSource || isTestSource; + }; + strictDeps = true; + }; + + haze = craneLib.buildPackage ( + commonArgs + // { + cargoArtifacts = craneLib.buildDepsOnly commonArgs; + + doCheck = true; + + meta = with pkgs.lib; { + description = "Dead simple world management tool for Minecraft Bedrock."; + homepage = "https://github.com/arexon/haze"; + license = licenses.mit; + mainProgram = "haze"; + }; + } + ); + in { + checks = { + inherit haze; + }; + + formatter = pkgs.alejandra; + + packages = { + inherit haze; + default = haze; + }; + + apps.default = flake-utils.lib.mkApp { + drv = haze; + }; + + devShells.default = craneLib.devShell { + packages = with pkgs; [ + taplo + dprint + cargo-insta + ]; + }; + }); +} diff --git a/rust-toolchain.toml b/rust-toolchain.toml new file mode 100644 index 0000000..32df066 --- /dev/null +++ b/rust-toolchain.toml @@ -0,0 +1,3 @@ +[toolchain] +channel = "stable" +components = ["clippy", "rustfmt", "rust-src", "rust-analyzer"] diff --git a/scripts/install.ps1 b/scripts/install.ps1 old mode 100644 new mode 100755 index e14d481..924e364 --- a/scripts/install.ps1 +++ b/scripts/install.ps1 @@ -3,10 +3,10 @@ $ErrorActionPreference = 'Stop' $IsInstalled = $false -$DownloadUrl = "https://github.com/sedgeland/haze/releases/latest/download/haze.zip" +$ArchiveName = "haze-x86_64-pc-windows-msvc" +$DownloadUrl = "https://github.com/arexon/haze/releases/latest/download/${ArchiveName}" $BinDir = "${Home}\.haze" -$HazeZip = "${BinDir}\haze.zip" -$HazeExe = "${BinDir}\haze.exe" +$BinArchive = "${BinDir}\${ArchiveName}" if (!(Test-Path $BinDir)) { New-Item $BinDir -ItemType Directory | Out-Null @@ -14,9 +14,11 @@ if (!(Test-Path $BinDir)) { $IsInstalled = $true } -curl.exe -Lo $HazeZip $DownloadUrl -tar.exe xf $HazeZip -C $BinDir -Remove-Item $HazeZip +curl.exe -Lo "${BinArchive}.zip" $DownloadUrl +tar.exe xf "${BinArchive}.zip" -C $BinDir +Move-Item -Path "${BinArchive}\*" -Destination $BinDir +Remove-Item $BinArchive +Remove-Item "${BinArchive}.zip" $User = [System.EnvironmentVariableTarget]::User $Path = [System.Environment]::GetEnvironmentVariable('Path', $User) @@ -28,7 +30,6 @@ if (!(";${Path};".ToLower() -like "*;${BinDir};*".ToLower())) { if ($IsInstalled -eq $true) { Write-Output "Haze was updated successfully to the latest version" } else { - Write-Output "Haze was installed successfully to ${HazeExe}" - Write-Output "Run 'haze --help' to get started" + Write-Output "Haze was installed successfully to ${BinDir}\haze.exe" + Write-Output "Run `haze help` to get started!" } - diff --git a/src/cli.rs b/src/cli.rs new file mode 100644 index 0000000..059d565 --- /dev/null +++ b/src/cli.rs @@ -0,0 +1,94 @@ +use anstyle::{AnsiColor, Color, Style}; +use clap::{builder, Parser, Subcommand}; + +#[cfg(windows)] +use clap::ValueEnum; + +#[cfg(windows)] +use crate::com_mojang::MinecraftVersion; + +#[derive(Parser)] +#[command(author, version, about, long_about = None, styles=get_styles())] +pub struct Cli { + /// Set a path to the config file + #[arg(short, long, value_name = "PATH", default_value = "config.json")] + pub config: String, + + /// The Minecraft version to get the `com.mojang` directory from. To define + /// an arbitrary path, set the `COM_MOJANG` environment variable instead + #[cfg(windows)] + #[arg(short = 'm', long, value_enum, default_value = "stable")] + pub minecraft_version: MinecraftVersionWrapper, + + #[command(subcommand)] + pub commands: Commands, +} + +#[derive(Subcommand)] +pub enum Commands { + /// Copy local worlds to `com.mojang` + #[clap(visible_alias("ex"))] + Export { + /// The name of one or more worlds to export + #[arg(required = true)] + names: Vec, + /// Overwrite any already existing worlds in `com.mojang` + #[arg(short, long)] + overwrite: bool, + }, + + /// Copy `com.mojang` worlds to local worlds + #[clap(visible_alias("im"))] + Import { + /// The name of one or more worlds to import + #[arg(required = true)] + names: Vec, + }, + + /// List all worlds stored locally and in `com.mojang`. + #[clap(visible_alias("ls"))] + List, +} + +#[cfg(windows)] +#[derive(Clone)] +pub struct MinecraftVersionWrapper(pub MinecraftVersion); + +#[cfg(windows)] +impl ValueEnum for MinecraftVersionWrapper { + fn value_variants<'a>() -> &'a [Self] { + &[ + Self(MinecraftVersion::Stable), + Self(MinecraftVersion::Preview), + Self(MinecraftVersion::Education), + ] + } + + fn to_possible_value(&self) -> Option { + Some(builder::PossibleValue::new(self.0.as_str())) + } +} + +fn get_styles() -> builder::Styles { + let error_style = Style::new() + .bold() + .fg_color(Some(Color::Ansi(AnsiColor::Red))); + + let heading_style = Style::new() + .bold() + .fg_color(Some(Color::Ansi(AnsiColor::BrightMagenta))); + let literal_style = Style::new().fg_color(Some(Color::Ansi(AnsiColor::Cyan))); + + builder::Styles::styled() + .usage(heading_style) + .header(heading_style) + .literal(literal_style.bold()) + .placeholder(literal_style) + .invalid(error_style) + .error(error_style) + .valid( + Style::new() + .bold() + .fg_color(Some(Color::Ansi(AnsiColor::Green))), + ) +} diff --git a/src/com_mojang.rs b/src/com_mojang.rs new file mode 100644 index 0000000..89a0b8b --- /dev/null +++ b/src/com_mojang.rs @@ -0,0 +1,81 @@ +use std::{ + env, fs, + path::{Path, PathBuf}, +}; + +use crate::error::{Error, Result}; + +const MC_WORLDS: &str = "minecraftWorlds"; + +#[cfg(windows)] +#[derive(Clone)] +pub enum MinecraftVersion { + Stable, + Preview, + Education, +} + +#[cfg(windows)] +impl MinecraftVersion { + pub fn as_str(&self) -> &'static str { + match self { + Self::Stable => "stable", + Self::Preview => "preview", + Self::Education => "education", + } + } +} + +#[cfg(unix)] +pub fn get_and_check() -> Result { + let path = from_env()?; + check_if_exists(&path)?; + Ok(path) +} + +#[cfg(windows)] +pub fn get_and_check(version: &MinecraftVersion) -> Result { + let path = from_env().or_else(|_| from_version(version))?; + check_if_exists(&path)?; + Ok(path) +} + +#[cfg(windows)] +pub fn from_version(version: &MinecraftVersion) -> Result { + let version = match version { + MinecraftVersion::Stable => "UWP", + MinecraftVersion::Preview => "Beta", + MinecraftVersion::Education => "EducationEdition", + }; + let appdata_var = + env::var("LOCALAPPDATA").map_err(|source| Error::CannotFindLocalAppData { source })?; + let com_mojang = PathBuf::from(appdata_var) + .join("Packages") + .join(format!("Microsoft.Minecraft{version}_8wekyb3d8bbwe")) + .join("LocalState") + .join("games") + .join("com.mojang") + .join(MC_WORLDS); + + Ok(com_mojang) +} + +pub fn from_env() -> Result { + let com_mojang_var = + env::var("COM_MOJANG").map_err(|source| Error::NoComMojangEnvVar { source })?; + Ok(PathBuf::from(com_mojang_var).join(MC_WORLDS)) +} + +pub fn check_if_exists(dir: &Path) -> Result<()> { + match fs::exists(dir) { + Ok(true) => Ok(()), + Ok(false) => Err(Error::ComMojangDoesNotExist { + source: None, + path: dir.to_path_buf(), + }), + Err(source) => Err(Error::ComMojangDoesNotExist { + source: Some(source), + path: dir.to_path_buf(), + }), + } +} diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..e912239 --- /dev/null +++ b/src/config.rs @@ -0,0 +1,36 @@ +use std::{env, fs}; + +use json_strip_comments::CommentSettings; +use serde::Deserialize; + +use crate::error::{Error, Result}; + +#[derive(Deserialize)] +pub struct Config { + pub worlds: Vec, +} + +impl Config { + pub fn load(path: String) -> Result { + let mut content = fs::read_to_string(&path).map_err(|source| Error::ConfigNotFound { + path: path.clone(), + cwd: env::current_dir().unwrap(), + source, + })?; + + json_strip_comments::strip_comments_in_place( + &mut content, + CommentSettings::c_style(), + true, + ) + .unwrap(); + + let config = serde_json::from_str(&content).map_err(|source| Error::ConfigFormat { + path, + cwd: env::current_dir().unwrap(), + source, + })?; + + Ok(config) + } +} diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 0000000..105bc78 --- /dev/null +++ b/src/error.rs @@ -0,0 +1,93 @@ +use std::{env, fmt, io, path::PathBuf}; + +use miette::Diagnostic; +use thiserror::Error; + +pub type Result = std::result::Result; + +#[derive(Debug, Error, Diagnostic)] +pub enum Error { + #[error("could not find `{path}` in `{}`", cwd.display())] + ConfigNotFound { + path: String, + cwd: PathBuf, + source: io::Error, + }, + + #[error("could not parse `{path}` in `{}`", cwd.display())] + ConfigFormat { + path: String, + cwd: PathBuf, + source: serde_json::Error, + }, + + #[error("could not find the LOCALAPPDATA environment variable")] + #[cfg(windows)] + CannotFindLocalAppData { source: env::VarError }, + + #[error("could not find the COM_MOJANG environment variable")] + #[diagnostic(help("setting this variable is required on non-Windows systems"))] + NoComMojangEnvVar { source: env::VarError }, + + #[error("the `com.mojang` directory does not exist in `{}`", path.display())] + ComMojangDoesNotExist { + source: Option, + path: PathBuf, + }, + + #[error("invalid world glob pattern `{pattern}`")] + InvalidWorldGlob { + source: glob::PatternError, + pattern: String, + }, + + #[error("two local worlds have conflicting names `{}` <-> `{}`", world_a.display(), world_b.display())] + #[diagnostic(help( + "worlds in different directories must have unique names so they are easily identifiable" + ))] + LocalWorldNameConflict { world_a: PathBuf, world_b: PathBuf }, + + #[error("attempting to export `{name}` when one already exists in `com.mojang`")] + #[diagnostic(help("use --overwrite to bypass"))] + ExportWithoutOverwriteAllowed { name: String }, + + #[error("attempting to import `{name}` when there is no local world matching it")] + #[diagnostic(help( + "worlds must be manually imported to a desired local location for first-time setup" + ))] + ImportWithoutLocalMatch { name: String }, + + #[error(transparent)] + #[diagnostic(transparent)] + NoMatchingWorlds(NoMatchingWorldsError), + + #[error("failed to access a world at `{}`", path.display())] + WorldAccessFailure { source: io::Error, path: PathBuf }, + + #[error("failed to copy world `{}` to `{}`", from.display(), to.display())] + WorldCopyFailure { + source: fs_extra::error::Error, + from: PathBuf, + to: PathBuf, + }, +} + +#[derive(Debug, Error, Diagnostic)] +pub struct NoMatchingWorldsError { + pub names: Vec, +} + +impl fmt::Display for NoMatchingWorldsError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "no worlds matching ")?; + for (index, name) in self.names.iter().enumerate() { + write!(f, "`{name}`")?; + if index < self.names.len() - 1 { + write!(f, " and ")?; + } + } + write!(f, " were found")?; + + Ok(()) + } +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..e6a2500 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,51 @@ +mod cli; +mod com_mojang; +mod config; +mod error; +mod term; +mod world; + +use std::process; + +use clap::Parser; +use miette::Result; + +use crate::{ + cli::{Cli, Commands}, + config::Config, + world::WorldManager, +}; + +fn main() { + let cli = Cli::parse(); + term::init_logger(); + term::init_miette(); + + let run = || -> Result<()> { + let config = Config::load(cli.config)?; + + #[cfg(unix)] + let com_mojang = com_mojang::get_and_check()?; + #[cfg(windows)] + let com_mojang = com_mojang::get_and_check(&cli.minecraft_version.0)?; + + let haze = WorldManager::new(config.worlds, com_mojang)?; + match cli.commands { + Commands::Export { names, overwrite } => haze.export(names, overwrite)?, + Commands::Import { names } => haze.import(names)?, + Commands::List => haze.list()?, + } + + Ok(()) + }; + + match run() { + Ok(_) => process::exit(0), + Err(error) => { + let error = format!("{error:?}"); + // Trim the initial whitespace. + log::error!("{}", &error[13..]); + process::exit(1); + } + } +} diff --git a/src/term.rs b/src/term.rs new file mode 100644 index 0000000..20ec1df --- /dev/null +++ b/src/term.rs @@ -0,0 +1,34 @@ +use std::io::Write; + +use color_print::cstr; +use env_logger::Env; +use log::Level; +use miette::{GraphicalTheme, MietteHandlerOpts, ThemeCharacters}; + +pub fn init_miette() { + miette::set_hook(Box::new(|_| { + Box::new( + MietteHandlerOpts::new() + .graphical_theme(GraphicalTheme { + characters: ThemeCharacters::ascii(), + ..Default::default() + }) + .build(), + ) + })) + .expect("should set miette hook") +} + +pub fn init_logger() { + env_logger::Builder::from_env(Env::new().filter_or("HAZE_LOG", "info")) + .format(|buf, record| match record.level() { + Level::Error => write!(buf, cstr!("error: {}"), record.args()), + Level::Warn => todo!(), + Level::Info => { + writeln!(buf, cstr!("info: {}"), record.args()) + } + Level::Debug => writeln!(buf, cstr!("debug: {}"), record.args()), + Level::Trace => unimplemented!(), + }) + .init(); +} diff --git a/src/world.rs b/src/world.rs new file mode 100644 index 0000000..4e288b0 --- /dev/null +++ b/src/world.rs @@ -0,0 +1,225 @@ +use std::{ + collections::{HashMap, HashSet}, + fmt::Write, + fs, + path::{Path, PathBuf}, +}; + +use color_print::cstr; +use fs_extra::dir::{self, CopyOptions}; +use walkdir::WalkDir; + +use crate::error::{Error, NoMatchingWorldsError, Result}; + +pub type LocalWorldMap = HashMap; +pub type ComMojangWorldSet = HashMap; + +/// Holds info about local and `com.mojang` worlds. +pub struct WorldManager { + local_worlds: LocalWorldMap, + com_mojang_worlds: ComMojangWorldSet, + com_mojang: PathBuf, +} + +impl WorldManager { + pub fn new(patterns: Vec, com_mojang: PathBuf) -> Result { + let local_worlds = patterns + .clone() + .into_iter() + .map(|pattern| { + glob::glob(&pattern).map_err(|source| Error::InvalidWorldGlob { source, pattern }) + }) + .collect::>>()? + .into_iter() + .flatten() + .try_fold( + HashMap::new(), + |mut worlds, path| -> Result { + let path = path.map_err(|e| Error::WorldAccessFailure { + path: e.path().to_path_buf(), + source: e.into_error(), + })?; + let name = world_name_from_path(&path); + match worlds.get(&name) { + Some(old_path) => { + return Err(Error::LocalWorldNameConflict { + world_a: path, + world_b: old_path.clone(), + }); + } + None => worlds.insert(name, path), + }; + Ok(worlds) + }, + )?; + + let com_mojang_worlds = WalkDir::new(&com_mojang) + .min_depth(1) + .max_depth(1) + .into_iter() + .filter_map(|entry| match entry { + Ok(entry) if entry.file_type().is_dir() => { + Some(Ok((world_name_from_path(entry.path()), ()))) + } + Ok(_) => None, + Err(err) => match err.io_error() { + Some(_) => Some(Err(Error::WorldAccessFailure { + path: err.path()?.to_path_buf(), + source: err.into_io_error().unwrap(), + })), + None => None, + }, + }) + .collect::>()?; + + Ok(Self { + local_worlds, + com_mojang_worlds, + com_mojang, + }) + } + + /// Sequentially exports the given local worlds to `com.mojang`. + pub fn export(mut self, names: Vec, overwrite: bool) -> Result<()> { + let names = HashSet::::from_iter(names); + let names_not_found: Vec<_> = names + .iter() + .filter(|&name| (!self.local_worlds.contains_key(name))) + .cloned() + .collect(); + + if !names_not_found.is_empty() { + return Err(Error::NoMatchingWorlds(NoMatchingWorldsError { + names: names_not_found, + })); + } + + for name in names { + // We've already checked that `name` *does* exist in `local_worlds`. + let from = self.local_worlds.remove(&name).unwrap(); + let to = self.com_mojang.join(&name); + + match (self.com_mojang_worlds.contains_key(&name), overwrite) { + // 1. Target world does exist and we can delete it before copying. + (true, true) => { + fs::remove_dir_all(&to).map_err(|source| Error::WorldAccessFailure { + source, + path: to.to_path_buf(), + })?; + copy_world(&from, &to)?; + } + // 2. Target world does exist, but we cannot overwrite it. + (true, false) => return Err(Error::ExportWithoutOverwriteAllowed { name }), + // 3. Target world does not exist, we can copy normally. + _ => copy_world(&from, &to)?, + } + + log::info!("exported `{}` to `{}`", from.display(), to.display()); + } + + Ok(()) + } + + /// Sequentially imports the given worlds from `com.mojang` and stores them + /// locally. + pub fn import(mut self, names: Vec) -> Result<()> { + let names = HashSet::::from_iter(names); + let names_not_found: Vec<_> = names + .iter() + .filter(|&name| (!self.com_mojang_worlds.contains_key(name))) + .cloned() + .collect(); + + if !names_not_found.is_empty() { + return Err(Error::NoMatchingWorlds(NoMatchingWorldsError { + names: names_not_found, + })); + } + + for name in names { + // We've already checked that `name` *does* exist in `com.mojang`. + let (from, _) = self.com_mojang_worlds.remove_entry(&name).unwrap(); + let from = self.com_mojang.join(from); + let to = self + .local_worlds + .remove(&name) + .ok_or_else(|| Error::ImportWithoutLocalMatch { name })?; + + fs::remove_dir_all(&to).map_err(|source| Error::WorldAccessFailure { + source, + path: to.to_path_buf(), + })?; + copy_world(&from, &to)?; + + log::info!("imoprted `{}` to `{}`", from.display(), to.display()); + } + + Ok(()) + } + + /// List worlds stored locally and in `com.mojang`. + pub fn list(self) -> Result<()> { + let mut output = String::new(); + + let has_local_worlds = !self.local_worlds.is_empty(); + let has_com_mojang_worlds = !self.com_mojang_worlds.is_empty(); + + if has_local_worlds { + writeln!( + output, + cstr!("{}-- local project"), + if has_com_mojang_worlds { '|' } else { '`' } + ) + .unwrap(); + for (index, path) in self.local_worlds.values().enumerate() { + let is_last = self.local_worlds.len() - 1 == index; + write!( + output, + cstr!("{} {}-- {}"), + if has_com_mojang_worlds { '|' } else { ' ' }, + if is_last { '`' } else { '|' }, + path.display() + ) + .unwrap(); + if !is_last || has_com_mojang_worlds { + writeln!(output).unwrap(); + } + } + } + + if has_com_mojang_worlds { + writeln!(output, cstr!("`-- com.mojang")).unwrap(); + for (index, path) in self.com_mojang_worlds.keys().enumerate() { + let is_last = self.com_mojang_worlds.len() - 1 == index; + write!( + output, + cstr!(" {}-- {}"), + if is_last { '`' } else { '|' }, + path + ) + .unwrap(); + if !is_last { + writeln!(output).unwrap(); + } + } + } + + log::info!("listing all worlds at..\n{output}"); + + Ok(()) + } +} + +fn copy_world(from: &Path, to: &Path) -> Result<()> { + let options = CopyOptions::new().content_only(true); + dir::copy(from, to, &options).map_err(|source| Error::WorldCopyFailure { + source, + from: from.to_path_buf(), + to: to.to_path_buf(), + })?; + Ok(()) +} + +fn world_name_from_path(path: &Path) -> String { + path.file_name().unwrap().to_string_lossy().to_string() +} diff --git a/taplo.toml b/taplo.toml new file mode 100644 index 0000000..4d1da57 --- /dev/null +++ b/taplo.toml @@ -0,0 +1,7 @@ +include = ["Cargo.toml", "crates/**/Cargo.toml", "rust-toolchain.toml"] + +[[rule]] +keys = ["dependencies", "*-dependencies", "workspace.dependencies"] + +[rule.formatting] +reorder_keys = true diff --git a/tests/integration.rs b/tests/integration.rs new file mode 100644 index 0000000..fc5a33d --- /dev/null +++ b/tests/integration.rs @@ -0,0 +1,313 @@ +use std::{env, fs, path::PathBuf}; + +use fs_extra::dir::{self, CopyOptions}; +use insta_cmd::{assert_cmd_snapshot, get_cargo_bin, Command}; + +const COM_MOJANG: &str = "com.mojang"; +const MINECRAFT_WORLDS: &str = "minecraftWorlds"; + +macro_rules! fn_name { + () => {{ + // Β―\_(ツ)_/Β― + fn f() {} + fn type_name_of(_: T) -> &'static str { + std::any::type_name::() + } + type_name_of(f) + .split("::") + .enumerate() + .find(|(i, _)| *i == 1) + .map(|(_, s)| s) + .unwrap() + }}; +} + +struct HazeTest { + temp_dir: PathBuf, + command: Command, +} + +impl HazeTest { + fn new<'a>( + name: &'static str, + args: impl IntoIterator, + com_mojang: Option<&str>, + ) -> Self { + let temp_dir = env::temp_dir().join(name); + fs::create_dir(&temp_dir).expect("should create temp dir"); + let testdata = env::current_dir() + .unwrap() + .join("tests") + .join("testdata") + .join(name); + let options = CopyOptions::new().content_only(true); + dir::copy(&testdata, &temp_dir, &options).expect("should copy testdata to temp dir"); + + std::fs::read_dir(&testdata).unwrap(); + + let mut command = Command::new(get_cargo_bin("haze")); + command.args(args).current_dir(&temp_dir); + + if let Some(com_mojang) = com_mojang { + command.env("COM_MOJANG", com_mojang); + } + + Self { temp_dir, command } + } +} + +impl Drop for HazeTest { + fn drop(&mut self) { + fs::remove_dir_all(&self.temp_dir).unwrap(); + } +} + +#[test] +#[cfg(unix)] +fn cannot_find_com_mojang_env_var() { + let mut test = HazeTest::new(fn_name!(), ["export", "foo"], None); + + assert_cmd_snapshot!(test.command, @r#" + success: false + exit_code: 1 + ----- stdout ----- + + ----- stderr ----- + error: could not find the COM_MOJANG environment variable + `-> environment variable not found + help: setting this variable is required on non-Windows systems + "#); +} + +#[test] +fn com_mojang_does_not_exist() { + let mut test = HazeTest::new(fn_name!(), ["export", "foo"], Some("other-com.mojang")); + + #[cfg(unix)] + assert_cmd_snapshot!(test.command, @r#" + success: false + exit_code: 1 + ----- stdout ----- + + ----- stderr ----- + error: the `com.mojang` directory does not exist in `other-com.mojang/ + | minecraftWorlds` + "#); + + #[cfg(windows)] + assert_cmd_snapshot!(test.command, @r#" + success: false + exit_code: 1 + ----- stdout ----- + + ----- stderr ----- + error: the `com.mojang` directory does not exist in `other- + | com.mojang\minecraftWorlds` + "#); +} + +#[test] +fn invalid_world_glob() { + let mut test = HazeTest::new(fn_name!(), ["export", "foo"], Some(COM_MOJANG)); + + assert_cmd_snapshot!(test.command, @r#" + success: false + exit_code: 1 + ----- stdout ----- + + ----- stderr ----- + error: invalid world glob pattern `./worlds/***` + `-> Pattern syntax error near position 11: wildcards are either regular `*` + or recursive `**` + "#); +} + +#[test] +fn local_world_name_conflict() { + let mut test = HazeTest::new(fn_name!(), ["export", "foo"], Some(COM_MOJANG)); + + #[cfg(unix)] + assert_cmd_snapshot!(test.command, @r#" + success: false + exit_code: 1 + ----- stdout ----- + + ----- stderr ----- + error: two local worlds have conflicting names `worlds_other/foo` <-> `worlds/ + | foo` + help: worlds in different directories must have unique names so they are + easily identifiable + "#); + + #[cfg(windows)] + assert_cmd_snapshot!(test.command, @r#" + success: false + exit_code: 1 + ----- stdout ----- + + ----- stderr ----- + error: two local worlds have conflicting names `worlds_other\foo` <-> + | `worlds\foo` + help: worlds in different directories must have unique names so they are + easily identifiable + "#); +} + +#[test] +fn no_matching_local_worlds() { + let mut test = HazeTest::new(fn_name!(), ["export", "foo"], Some(COM_MOJANG)); + + assert_cmd_snapshot!(test.command, @r#" + success: false + exit_code: 1 + ----- stdout ----- + + ----- stderr ----- + error: no worlds matching `foo` were found + "#); +} + +#[test] +fn export_without_overwrite_allowed() { + let mut test = HazeTest::new(fn_name!(), ["export", "foo"], Some(COM_MOJANG)); + + assert_cmd_snapshot!(test.command, @r#" + success: false + exit_code: 1 + ----- stdout ----- + + ----- stderr ----- + error: attempting to export `foo` when one already exists in `com.mojang` + help: use --overwrite to bypass + "#); +} + +#[test] +fn export() { + let world_to_export = "foo"; + + let mut test = HazeTest::new(fn_name!(), ["export", world_to_export], Some(COM_MOJANG)); + + #[cfg(unix)] + assert_cmd_snapshot!(test.command, @r#" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + info: exported `worlds/foo` to `com.mojang/minecraftWorlds/foo` + "#); + + #[cfg(windows)] + assert_cmd_snapshot!(test.command, @r#" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + info: exported `worlds\foo` to `com.mojang\minecraftWorlds\foo` + "#); + + let exported_world = test + .temp_dir + .join(COM_MOJANG) + .join(MINECRAFT_WORLDS) + .join(world_to_export); + assert!( + exported_world.exists(), + "expected world `{}` to have been exported", + exported_world.display() + ); +} + +#[test] +fn export_with_overwrite() { + let mut test = HazeTest::new( + fn_name!(), + ["export", "--overwrite", "foo"], + Some(COM_MOJANG), + ); + + #[cfg(unix)] + assert_cmd_snapshot!(test.command, @r#" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + info: exported `worlds/foo` to `com.mojang/minecraftWorlds/foo` + "#); + + #[cfg(windows)] + assert_cmd_snapshot!(test.command, @r#" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + info: exported `worlds\foo` to `com.mojang\minecraftWorlds\foo` + "#); +} + +#[test] +fn no_matching_com_mojang_worlds() { + let mut test = HazeTest::new(fn_name!(), ["import", "foo"], Some(COM_MOJANG)); + + assert_cmd_snapshot!(test.command, @r#" + success: false + exit_code: 1 + ----- stdout ----- + + ----- stderr ----- + error: no worlds matching `foo` were found + "#); +} + +#[test] +fn import_without_local_match() { + let mut test = HazeTest::new(fn_name!(), ["import", "foo"], Some(COM_MOJANG)); + + assert_cmd_snapshot!(test.command, @r#" + success: false + exit_code: 1 + ----- stdout ----- + + ----- stderr ----- + error: attempting to import `foo` when there is no local world matching it + help: worlds must be manually imported to a desired local location for + first-time setup + "#); +} + +#[test] +fn import() { + let world_to_import = "foo"; + let mut test = HazeTest::new(fn_name!(), ["import", world_to_import], Some(COM_MOJANG)); + + #[cfg(unix)] + assert_cmd_snapshot!(test.command, @r#" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + info: imoprted `com.mojang/minecraftWorlds/foo` to `worlds/foo` + "#); + + #[cfg(windows)] + assert_cmd_snapshot!(test.command, @r#" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + info: imoprted `com.mojang\minecraftWorlds\foo` to `worlds\foo` + "#); + + let imported_world = test.temp_dir.join("worlds").join(world_to_import); + assert!( + imported_world.exists(), + "expected world `{}` to have been imported", + imported_world.display() + ); +} diff --git a/tests/testdata/basic/com.mojang/minecraftWorlds/foo/level.dat b/tests/testdata/basic/com.mojang/minecraftWorlds/foo/level.dat new file mode 100644 index 0000000..e69de29 diff --git a/tests/testdata/basic/config.json b/tests/testdata/basic/config.json new file mode 100644 index 0000000..47d27f4 --- /dev/null +++ b/tests/testdata/basic/config.json @@ -0,0 +1,3 @@ +{ + "worlds": ["./worlds/*"] +} diff --git a/tests/testdata/basic/worlds/foo/level.dat b/tests/testdata/basic/worlds/foo/level.dat new file mode 100644 index 0000000..e69de29 diff --git a/tests/testdata/cannot_find_com_mojang_env_var/config.json b/tests/testdata/cannot_find_com_mojang_env_var/config.json new file mode 100644 index 0000000..bbbe064 --- /dev/null +++ b/tests/testdata/cannot_find_com_mojang_env_var/config.json @@ -0,0 +1,3 @@ +{ + "worlds": [] +} diff --git a/tests/testdata/com_mojang_does_not_exist/config.json b/tests/testdata/com_mojang_does_not_exist/config.json new file mode 100644 index 0000000..bbbe064 --- /dev/null +++ b/tests/testdata/com_mojang_does_not_exist/config.json @@ -0,0 +1,3 @@ +{ + "worlds": [] +} diff --git a/tests/testdata/export/com.mojang/minecraftWorlds/.gitignore b/tests/testdata/export/com.mojang/minecraftWorlds/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/tests/testdata/export/com.mojang/minecraftWorlds/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/tests/testdata/export/config.json b/tests/testdata/export/config.json new file mode 100644 index 0000000..47d27f4 --- /dev/null +++ b/tests/testdata/export/config.json @@ -0,0 +1,3 @@ +{ + "worlds": ["./worlds/*"] +} diff --git a/tests/testdata/export/worlds/foo/level.dat b/tests/testdata/export/worlds/foo/level.dat new file mode 100644 index 0000000..e69de29 diff --git a/tests/testdata/export_with_overwrite/com.mojang/minecraftWorlds/foo/level.dat b/tests/testdata/export_with_overwrite/com.mojang/minecraftWorlds/foo/level.dat new file mode 100644 index 0000000..e69de29 diff --git a/tests/testdata/export_with_overwrite/config.json b/tests/testdata/export_with_overwrite/config.json new file mode 100644 index 0000000..47d27f4 --- /dev/null +++ b/tests/testdata/export_with_overwrite/config.json @@ -0,0 +1,3 @@ +{ + "worlds": ["./worlds/*"] +} diff --git a/tests/testdata/export_with_overwrite/worlds/foo/level.dat b/tests/testdata/export_with_overwrite/worlds/foo/level.dat new file mode 100644 index 0000000..e69de29 diff --git a/tests/testdata/export_without_overwrite_allowed/com.mojang/minecraftWorlds/foo/level.dat b/tests/testdata/export_without_overwrite_allowed/com.mojang/minecraftWorlds/foo/level.dat new file mode 100644 index 0000000..e69de29 diff --git a/tests/testdata/export_without_overwrite_allowed/config.json b/tests/testdata/export_without_overwrite_allowed/config.json new file mode 100644 index 0000000..47d27f4 --- /dev/null +++ b/tests/testdata/export_without_overwrite_allowed/config.json @@ -0,0 +1,3 @@ +{ + "worlds": ["./worlds/*"] +} diff --git a/tests/testdata/export_without_overwrite_allowed/worlds/foo/level.dat b/tests/testdata/export_without_overwrite_allowed/worlds/foo/level.dat new file mode 100644 index 0000000..e69de29 diff --git a/tests/testdata/import/com.mojang/minecraftWorlds/foo/level.dat b/tests/testdata/import/com.mojang/minecraftWorlds/foo/level.dat new file mode 100644 index 0000000..e69de29 diff --git a/tests/testdata/import/config.json b/tests/testdata/import/config.json new file mode 100644 index 0000000..47d27f4 --- /dev/null +++ b/tests/testdata/import/config.json @@ -0,0 +1,3 @@ +{ + "worlds": ["./worlds/*"] +} diff --git a/tests/testdata/import/worlds/foo/level.dat b/tests/testdata/import/worlds/foo/level.dat new file mode 100644 index 0000000..e69de29 diff --git a/tests/testdata/import_without_local_match/com.mojang/minecraftWorlds/foo/level.dat b/tests/testdata/import_without_local_match/com.mojang/minecraftWorlds/foo/level.dat new file mode 100644 index 0000000..e69de29 diff --git a/tests/testdata/import_without_local_match/config.json b/tests/testdata/import_without_local_match/config.json new file mode 100644 index 0000000..47d27f4 --- /dev/null +++ b/tests/testdata/import_without_local_match/config.json @@ -0,0 +1,3 @@ +{ + "worlds": ["./worlds/*"] +} diff --git a/tests/testdata/invalid_world_glob/com.mojang/minecraftWorlds/.gitignore b/tests/testdata/invalid_world_glob/com.mojang/minecraftWorlds/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/tests/testdata/invalid_world_glob/com.mojang/minecraftWorlds/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/tests/testdata/invalid_world_glob/config.json b/tests/testdata/invalid_world_glob/config.json new file mode 100644 index 0000000..f368f36 --- /dev/null +++ b/tests/testdata/invalid_world_glob/config.json @@ -0,0 +1,3 @@ +{ + "worlds": ["./worlds/***"] +} diff --git a/tests/testdata/local_world_name_conflict/com.mojang/minecraftWorlds/.gitignore b/tests/testdata/local_world_name_conflict/com.mojang/minecraftWorlds/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/tests/testdata/local_world_name_conflict/com.mojang/minecraftWorlds/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/tests/testdata/local_world_name_conflict/config.json b/tests/testdata/local_world_name_conflict/config.json new file mode 100644 index 0000000..a24aae4 --- /dev/null +++ b/tests/testdata/local_world_name_conflict/config.json @@ -0,0 +1,3 @@ +{ + "worlds": ["./worlds/*", "./worlds_other/*"] +} diff --git a/tests/testdata/local_world_name_conflict/worlds/foo/level.dat b/tests/testdata/local_world_name_conflict/worlds/foo/level.dat new file mode 100644 index 0000000..e69de29 diff --git a/tests/testdata/local_world_name_conflict/worlds_other/foo/level.dat b/tests/testdata/local_world_name_conflict/worlds_other/foo/level.dat new file mode 100644 index 0000000..e69de29 diff --git a/tests/testdata/no_matching_com_mojang_worlds/com.mojang/minecraftWorlds/.gitignore b/tests/testdata/no_matching_com_mojang_worlds/com.mojang/minecraftWorlds/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/tests/testdata/no_matching_com_mojang_worlds/com.mojang/minecraftWorlds/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/tests/testdata/no_matching_com_mojang_worlds/config.json b/tests/testdata/no_matching_com_mojang_worlds/config.json new file mode 100644 index 0000000..47d27f4 --- /dev/null +++ b/tests/testdata/no_matching_com_mojang_worlds/config.json @@ -0,0 +1,3 @@ +{ + "worlds": ["./worlds/*"] +} diff --git a/tests/testdata/no_matching_local_worlds/com.mojang/minecraftWorlds/.gitignore b/tests/testdata/no_matching_local_worlds/com.mojang/minecraftWorlds/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/tests/testdata/no_matching_local_worlds/com.mojang/minecraftWorlds/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/tests/testdata/no_matching_local_worlds/config.json b/tests/testdata/no_matching_local_worlds/config.json new file mode 100644 index 0000000..47d27f4 --- /dev/null +++ b/tests/testdata/no_matching_local_worlds/config.json @@ -0,0 +1,3 @@ +{ + "worlds": ["./worlds/*"] +} diff --git a/tests/testdata/no_matching_local_worlds/worlds/bar/level.dat b/tests/testdata/no_matching_local_worlds/worlds/bar/level.dat new file mode 100644 index 0000000..e69de29