WASM plugins, scripting, i18n, GUI pan/zoom, cargo-dist CI/CD
- WASM plugin host (wasmtime 46, 17 host fns) + example noisemap plugin - Rhai scripting engine (12 API fns, `terrafier run` cmd) - i18n: en/ru translations, auto-detect, t! macro - GUI: pan (click-drag) + zoom (scroll wheel) with viewport transform - cargo-dist 0.31: MSI/pkg/shell installers, both binaries - CI: .github/workflows/ci.yml + release.yml - Security: wasmtime 30→46, bincode 1.3→2.0, quick-xml CVE docs - Code review: SAFETY comments, unwrap→match cleanup - Fixes: clippy approx_constant, needless_borrows, rustfmt
This commit is contained in:
parent
bdd4564240
commit
18613260a8
100 changed files with 4262 additions and 585 deletions
64
.github/workflows/ci.yml
vendored
Normal file
64
.github/workflows/ci.yml
vendored
Normal file
|
|
@ -0,0 +1,64 @@
|
||||||
|
name: CI
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
pull_request:
|
||||||
|
branches: [main]
|
||||||
|
|
||||||
|
env:
|
||||||
|
CARGO_TERM_COLOR: always
|
||||||
|
RUSTFLAGS: "-D warnings"
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
check:
|
||||||
|
name: Check (${{ matrix.os }})
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
os: [ubuntu-latest, windows-latest, macos-latest]
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- uses: dtolnay/rust-toolchain@stable
|
||||||
|
with:
|
||||||
|
components: rustfmt, clippy
|
||||||
|
|
||||||
|
- uses: Swatinem/rust-cache@v2
|
||||||
|
|
||||||
|
- name: Check compilation
|
||||||
|
run: cargo check --workspace --all-targets
|
||||||
|
|
||||||
|
- name: Check formatting
|
||||||
|
run: cargo fmt --all --check
|
||||||
|
|
||||||
|
- name: Clippy
|
||||||
|
run: cargo clippy --workspace --all-targets -- -D warnings
|
||||||
|
|
||||||
|
test:
|
||||||
|
name: Test (${{ matrix.os }})
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
needs: check
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
os: [ubuntu-latest, windows-latest, macos-latest]
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: dtolnay/rust-toolchain@stable
|
||||||
|
- uses: Swatinem/rust-cache@v2
|
||||||
|
|
||||||
|
- name: Run tests
|
||||||
|
run: cargo test --workspace
|
||||||
|
|
||||||
|
audit:
|
||||||
|
name: Security Audit
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: dtolnay/rust-toolchain@stable
|
||||||
|
|
||||||
|
- name: Install cargo-audit
|
||||||
|
run: cargo install cargo-audit --locked
|
||||||
|
|
||||||
|
- name: Audit dependencies
|
||||||
|
run: cargo audit --deny warnings
|
||||||
296
.github/workflows/release-old.yml
vendored
Normal file
296
.github/workflows/release-old.yml
vendored
Normal file
|
|
@ -0,0 +1,296 @@
|
||||||
|
# This file was autogenerated by dist: https://axodotdev.github.io/cargo-dist
|
||||||
|
#
|
||||||
|
# Copyright 2022-2024, axodotdev
|
||||||
|
# SPDX-License-Identifier: MIT or Apache-2.0
|
||||||
|
#
|
||||||
|
# CI that:
|
||||||
|
#
|
||||||
|
# * checks for a Git Tag that looks like a release
|
||||||
|
# * builds artifacts with dist (archives, installers, hashes)
|
||||||
|
# * uploads those artifacts to temporary workflow zip
|
||||||
|
# * on success, uploads the artifacts to a GitHub Release
|
||||||
|
#
|
||||||
|
# Note that the GitHub Release will be created with a generated
|
||||||
|
# title/body based on your changelogs.
|
||||||
|
|
||||||
|
name: Release
|
||||||
|
permissions:
|
||||||
|
"contents": "write"
|
||||||
|
|
||||||
|
# This task will run whenever you push a git tag that looks like a version
|
||||||
|
# like "1.0.0", "v0.1.0-prerelease.1", "my-app/0.1.0", "releases/v1.0.0", etc.
|
||||||
|
# Various formats will be parsed into a VERSION and an optional PACKAGE_NAME, where
|
||||||
|
# PACKAGE_NAME must be the name of a Cargo package in your workspace, and VERSION
|
||||||
|
# must be a Cargo-style SemVer Version (must have at least major.minor.patch).
|
||||||
|
#
|
||||||
|
# If PACKAGE_NAME is specified, then the announcement will be for that
|
||||||
|
# package (erroring out if it doesn't have the given version or isn't dist-able).
|
||||||
|
#
|
||||||
|
# If PACKAGE_NAME isn't specified, then the announcement will be for all
|
||||||
|
# (dist-able) packages in the workspace with that version (this mode is
|
||||||
|
# intended for workspaces with only one dist-able package, or with all dist-able
|
||||||
|
# packages versioned/released in lockstep).
|
||||||
|
#
|
||||||
|
# If you push multiple tags at once, separate instances of this workflow will
|
||||||
|
# spin up, creating an independent announcement for each one. However, GitHub
|
||||||
|
# will hard limit this to 3 tags per commit, as it will assume more tags is a
|
||||||
|
# mistake.
|
||||||
|
#
|
||||||
|
# If there's a prerelease-style suffix to the version, then the release(s)
|
||||||
|
# will be marked as a prerelease.
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- '**[0-9]+.[0-9]+.[0-9]+*'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
# Run 'dist plan' (or host) to determine what tasks we need to do
|
||||||
|
plan:
|
||||||
|
runs-on: "ubuntu-22.04"
|
||||||
|
outputs:
|
||||||
|
val: ${{ steps.plan.outputs.manifest }}
|
||||||
|
tag: ${{ !github.event.pull_request && github.ref_name || '' }}
|
||||||
|
tag-flag: ${{ !github.event.pull_request && format('--tag={0}', github.ref_name) || '' }}
|
||||||
|
publishing: ${{ !github.event.pull_request }}
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v6
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
submodules: recursive
|
||||||
|
- name: Install dist
|
||||||
|
# we specify bash to get pipefail; it guards against the `curl` command
|
||||||
|
# failing. otherwise `sh` won't catch that `curl` returned non-0
|
||||||
|
shell: bash
|
||||||
|
run: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/v0.31.0/cargo-dist-installer.sh | sh"
|
||||||
|
- name: Cache dist
|
||||||
|
uses: actions/upload-artifact@v6
|
||||||
|
with:
|
||||||
|
name: cargo-dist-cache
|
||||||
|
path: ~/.cargo/bin/dist
|
||||||
|
# sure would be cool if github gave us proper conditionals...
|
||||||
|
# so here's a doubly-nested ternary-via-truthiness to try to provide the best possible
|
||||||
|
# functionality based on whether this is a pull_request, and whether it's from a fork.
|
||||||
|
# (PRs run on the *source* but secrets are usually on the *target* -- that's *good*
|
||||||
|
# but also really annoying to build CI around when it needs secrets to work right.)
|
||||||
|
- id: plan
|
||||||
|
run: |
|
||||||
|
dist ${{ (!github.event.pull_request && format('host --steps=create --tag={0}', github.ref_name)) || 'plan' }} --output-format=json > plan-dist-manifest.json
|
||||||
|
echo "dist ran successfully"
|
||||||
|
cat plan-dist-manifest.json
|
||||||
|
echo "manifest=$(jq -c "." plan-dist-manifest.json)" >> "$GITHUB_OUTPUT"
|
||||||
|
- name: "Upload dist-manifest.json"
|
||||||
|
uses: actions/upload-artifact@v6
|
||||||
|
with:
|
||||||
|
name: artifacts-plan-dist-manifest
|
||||||
|
path: plan-dist-manifest.json
|
||||||
|
|
||||||
|
# Build and packages all the platform-specific things
|
||||||
|
build-local-artifacts:
|
||||||
|
name: build-local-artifacts (${{ join(matrix.targets, ', ') }})
|
||||||
|
# Let the initial task tell us to not run (currently very blunt)
|
||||||
|
needs:
|
||||||
|
- plan
|
||||||
|
if: ${{ fromJson(needs.plan.outputs.val).ci.github.artifacts_matrix.include != null && (needs.plan.outputs.publishing == 'true' || fromJson(needs.plan.outputs.val).ci.github.pr_run_mode == 'upload') }}
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
# Target platforms/runners are computed by dist in create-release.
|
||||||
|
# Each member of the matrix has the following arguments:
|
||||||
|
#
|
||||||
|
# - runner: the github runner
|
||||||
|
# - dist-args: cli flags to pass to dist
|
||||||
|
# - install-dist: expression to run to install dist on the runner
|
||||||
|
#
|
||||||
|
# Typically there will be:
|
||||||
|
# - 1 "global" task that builds universal installers
|
||||||
|
# - N "local" tasks that build each platform's binaries and platform-specific installers
|
||||||
|
matrix: ${{ fromJson(needs.plan.outputs.val).ci.github.artifacts_matrix }}
|
||||||
|
runs-on: ${{ matrix.runner }}
|
||||||
|
container: ${{ matrix.container && matrix.container.image || null }}
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
BUILD_MANIFEST_NAME: target/distrib/${{ join(matrix.targets, '-') }}-dist-manifest.json
|
||||||
|
steps:
|
||||||
|
- name: enable windows longpaths
|
||||||
|
run: |
|
||||||
|
git config --global core.longpaths true
|
||||||
|
- uses: actions/checkout@v6
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
submodules: recursive
|
||||||
|
- name: Install Rust non-interactively if not already installed
|
||||||
|
if: ${{ matrix.container }}
|
||||||
|
run: |
|
||||||
|
if ! command -v cargo > /dev/null 2>&1; then
|
||||||
|
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
|
||||||
|
echo "$HOME/.cargo/bin" >> $GITHUB_PATH
|
||||||
|
fi
|
||||||
|
- name: Install dist
|
||||||
|
run: ${{ matrix.install_dist.run }}
|
||||||
|
# Get the dist-manifest
|
||||||
|
- name: Fetch local artifacts
|
||||||
|
uses: actions/download-artifact@v7
|
||||||
|
with:
|
||||||
|
pattern: artifacts-*
|
||||||
|
path: target/distrib/
|
||||||
|
merge-multiple: true
|
||||||
|
- name: Install dependencies
|
||||||
|
run: |
|
||||||
|
${{ matrix.packages_install }}
|
||||||
|
- name: Build artifacts
|
||||||
|
run: |
|
||||||
|
# Actually do builds and make zips and whatnot
|
||||||
|
dist build ${{ needs.plan.outputs.tag-flag }} --print=linkage --output-format=json ${{ matrix.dist_args }} > dist-manifest.json
|
||||||
|
echo "dist ran successfully"
|
||||||
|
- id: cargo-dist
|
||||||
|
name: Post-build
|
||||||
|
# We force bash here just because github makes it really hard to get values up
|
||||||
|
# to "real" actions without writing to env-vars, and writing to env-vars has
|
||||||
|
# inconsistent syntax between shell and powershell.
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
# Parse out what we just built and upload it to scratch storage
|
||||||
|
echo "paths<<EOF" >> "$GITHUB_OUTPUT"
|
||||||
|
dist print-upload-files-from-manifest --manifest dist-manifest.json >> "$GITHUB_OUTPUT"
|
||||||
|
echo "EOF" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
cp dist-manifest.json "$BUILD_MANIFEST_NAME"
|
||||||
|
- name: "Upload artifacts"
|
||||||
|
uses: actions/upload-artifact@v6
|
||||||
|
with:
|
||||||
|
name: artifacts-build-local-${{ join(matrix.targets, '_') }}
|
||||||
|
path: |
|
||||||
|
${{ steps.cargo-dist.outputs.paths }}
|
||||||
|
${{ env.BUILD_MANIFEST_NAME }}
|
||||||
|
|
||||||
|
# Build and package all the platform-agnostic(ish) things
|
||||||
|
build-global-artifacts:
|
||||||
|
needs:
|
||||||
|
- plan
|
||||||
|
- build-local-artifacts
|
||||||
|
runs-on: "ubuntu-22.04"
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
BUILD_MANIFEST_NAME: target/distrib/global-dist-manifest.json
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v6
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
submodules: recursive
|
||||||
|
- name: Install cached dist
|
||||||
|
uses: actions/download-artifact@v7
|
||||||
|
with:
|
||||||
|
name: cargo-dist-cache
|
||||||
|
path: ~/.cargo/bin/
|
||||||
|
- run: chmod +x ~/.cargo/bin/dist
|
||||||
|
# Get all the local artifacts for the global tasks to use (for e.g. checksums)
|
||||||
|
- name: Fetch local artifacts
|
||||||
|
uses: actions/download-artifact@v7
|
||||||
|
with:
|
||||||
|
pattern: artifacts-*
|
||||||
|
path: target/distrib/
|
||||||
|
merge-multiple: true
|
||||||
|
- id: cargo-dist
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
dist build ${{ needs.plan.outputs.tag-flag }} --output-format=json "--artifacts=global" > dist-manifest.json
|
||||||
|
echo "dist ran successfully"
|
||||||
|
|
||||||
|
# Parse out what we just built and upload it to scratch storage
|
||||||
|
echo "paths<<EOF" >> "$GITHUB_OUTPUT"
|
||||||
|
jq --raw-output ".upload_files[]" dist-manifest.json >> "$GITHUB_OUTPUT"
|
||||||
|
echo "EOF" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
cp dist-manifest.json "$BUILD_MANIFEST_NAME"
|
||||||
|
- name: "Upload artifacts"
|
||||||
|
uses: actions/upload-artifact@v6
|
||||||
|
with:
|
||||||
|
name: artifacts-build-global
|
||||||
|
path: |
|
||||||
|
${{ steps.cargo-dist.outputs.paths }}
|
||||||
|
${{ env.BUILD_MANIFEST_NAME }}
|
||||||
|
# Determines if we should publish/announce
|
||||||
|
host:
|
||||||
|
needs:
|
||||||
|
- plan
|
||||||
|
- build-local-artifacts
|
||||||
|
- build-global-artifacts
|
||||||
|
# Only run if we're "publishing", and only if plan, local and global didn't fail (skipped is fine)
|
||||||
|
if: ${{ always() && needs.plan.result == 'success' && needs.plan.outputs.publishing == 'true' && (needs.build-global-artifacts.result == 'skipped' || needs.build-global-artifacts.result == 'success') && (needs.build-local-artifacts.result == 'skipped' || needs.build-local-artifacts.result == 'success') }}
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
runs-on: "ubuntu-22.04"
|
||||||
|
outputs:
|
||||||
|
val: ${{ steps.host.outputs.manifest }}
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v6
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
submodules: recursive
|
||||||
|
- name: Install cached dist
|
||||||
|
uses: actions/download-artifact@v7
|
||||||
|
with:
|
||||||
|
name: cargo-dist-cache
|
||||||
|
path: ~/.cargo/bin/
|
||||||
|
- run: chmod +x ~/.cargo/bin/dist
|
||||||
|
# Fetch artifacts from scratch-storage
|
||||||
|
- name: Fetch artifacts
|
||||||
|
uses: actions/download-artifact@v7
|
||||||
|
with:
|
||||||
|
pattern: artifacts-*
|
||||||
|
path: target/distrib/
|
||||||
|
merge-multiple: true
|
||||||
|
- id: host
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
dist host ${{ needs.plan.outputs.tag-flag }} --steps=upload --steps=release --output-format=json > dist-manifest.json
|
||||||
|
echo "artifacts uploaded and released successfully"
|
||||||
|
cat dist-manifest.json
|
||||||
|
echo "manifest=$(jq -c "." dist-manifest.json)" >> "$GITHUB_OUTPUT"
|
||||||
|
- name: "Upload dist-manifest.json"
|
||||||
|
uses: actions/upload-artifact@v6
|
||||||
|
with:
|
||||||
|
# Overwrite the previous copy
|
||||||
|
name: artifacts-dist-manifest
|
||||||
|
path: dist-manifest.json
|
||||||
|
# Create a GitHub Release while uploading all files to it
|
||||||
|
- name: "Download GitHub Artifacts"
|
||||||
|
uses: actions/download-artifact@v7
|
||||||
|
with:
|
||||||
|
pattern: artifacts-*
|
||||||
|
path: artifacts
|
||||||
|
merge-multiple: true
|
||||||
|
- name: Cleanup
|
||||||
|
run: |
|
||||||
|
# Remove the granular manifests
|
||||||
|
rm -f artifacts/*-dist-manifest.json
|
||||||
|
- name: Create GitHub Release
|
||||||
|
env:
|
||||||
|
PRERELEASE_FLAG: "${{ fromJson(steps.host.outputs.manifest).announcement_is_prerelease && '--prerelease' || '' }}"
|
||||||
|
ANNOUNCEMENT_TITLE: "${{ fromJson(steps.host.outputs.manifest).announcement_title }}"
|
||||||
|
ANNOUNCEMENT_BODY: "${{ fromJson(steps.host.outputs.manifest).announcement_github_body }}"
|
||||||
|
RELEASE_COMMIT: "${{ github.sha }}"
|
||||||
|
run: |
|
||||||
|
# Write and read notes from a file to avoid quoting breaking things
|
||||||
|
echo "$ANNOUNCEMENT_BODY" > $RUNNER_TEMP/notes.txt
|
||||||
|
|
||||||
|
gh release create "${{ needs.plan.outputs.tag }}" --target "$RELEASE_COMMIT" $PRERELEASE_FLAG --title "$ANNOUNCEMENT_TITLE" --notes-file "$RUNNER_TEMP/notes.txt" artifacts/*
|
||||||
|
|
||||||
|
announce:
|
||||||
|
needs:
|
||||||
|
- plan
|
||||||
|
- host
|
||||||
|
# use "always() && ..." to allow us to wait for all publish jobs while
|
||||||
|
# still allowing individual publish jobs to skip themselves (for prereleases).
|
||||||
|
# "host" however must run to completion, no skipping allowed!
|
||||||
|
if: ${{ always() && needs.host.result == 'success' }}
|
||||||
|
runs-on: "ubuntu-22.04"
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v6
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
submodules: recursive
|
||||||
296
.github/workflows/release.yml
vendored
Normal file
296
.github/workflows/release.yml
vendored
Normal file
|
|
@ -0,0 +1,296 @@
|
||||||
|
# This file was autogenerated by dist: https://axodotdev.github.io/cargo-dist
|
||||||
|
#
|
||||||
|
# Copyright 2022-2024, axodotdev
|
||||||
|
# SPDX-License-Identifier: MIT or Apache-2.0
|
||||||
|
#
|
||||||
|
# CI that:
|
||||||
|
#
|
||||||
|
# * checks for a Git Tag that looks like a release
|
||||||
|
# * builds artifacts with dist (archives, installers, hashes)
|
||||||
|
# * uploads those artifacts to temporary workflow zip
|
||||||
|
# * on success, uploads the artifacts to a GitHub Release
|
||||||
|
#
|
||||||
|
# Note that the GitHub Release will be created with a generated
|
||||||
|
# title/body based on your changelogs.
|
||||||
|
|
||||||
|
name: Release
|
||||||
|
permissions:
|
||||||
|
"contents": "write"
|
||||||
|
|
||||||
|
# This task will run whenever you push a git tag that looks like a version
|
||||||
|
# like "1.0.0", "v0.1.0-prerelease.1", "my-app/0.1.0", "releases/v1.0.0", etc.
|
||||||
|
# Various formats will be parsed into a VERSION and an optional PACKAGE_NAME, where
|
||||||
|
# PACKAGE_NAME must be the name of a Cargo package in your workspace, and VERSION
|
||||||
|
# must be a Cargo-style SemVer Version (must have at least major.minor.patch).
|
||||||
|
#
|
||||||
|
# If PACKAGE_NAME is specified, then the announcement will be for that
|
||||||
|
# package (erroring out if it doesn't have the given version or isn't dist-able).
|
||||||
|
#
|
||||||
|
# If PACKAGE_NAME isn't specified, then the announcement will be for all
|
||||||
|
# (dist-able) packages in the workspace with that version (this mode is
|
||||||
|
# intended for workspaces with only one dist-able package, or with all dist-able
|
||||||
|
# packages versioned/released in lockstep).
|
||||||
|
#
|
||||||
|
# If you push multiple tags at once, separate instances of this workflow will
|
||||||
|
# spin up, creating an independent announcement for each one. However, GitHub
|
||||||
|
# will hard limit this to 3 tags per commit, as it will assume more tags is a
|
||||||
|
# mistake.
|
||||||
|
#
|
||||||
|
# If there's a prerelease-style suffix to the version, then the release(s)
|
||||||
|
# will be marked as a prerelease.
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- '**[0-9]+.[0-9]+.[0-9]+*'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
# Run 'dist plan' (or host) to determine what tasks we need to do
|
||||||
|
plan:
|
||||||
|
runs-on: "ubuntu-22.04"
|
||||||
|
outputs:
|
||||||
|
val: ${{ steps.plan.outputs.manifest }}
|
||||||
|
tag: ${{ !github.event.pull_request && github.ref_name || '' }}
|
||||||
|
tag-flag: ${{ !github.event.pull_request && format('--tag={0}', github.ref_name) || '' }}
|
||||||
|
publishing: ${{ !github.event.pull_request }}
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v6
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
submodules: recursive
|
||||||
|
- name: Install dist
|
||||||
|
# we specify bash to get pipefail; it guards against the `curl` command
|
||||||
|
# failing. otherwise `sh` won't catch that `curl` returned non-0
|
||||||
|
shell: bash
|
||||||
|
run: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/v0.31.0/cargo-dist-installer.sh | sh"
|
||||||
|
- name: Cache dist
|
||||||
|
uses: actions/upload-artifact@v6
|
||||||
|
with:
|
||||||
|
name: cargo-dist-cache
|
||||||
|
path: ~/.cargo/bin/dist
|
||||||
|
# sure would be cool if github gave us proper conditionals...
|
||||||
|
# so here's a doubly-nested ternary-via-truthiness to try to provide the best possible
|
||||||
|
# functionality based on whether this is a pull_request, and whether it's from a fork.
|
||||||
|
# (PRs run on the *source* but secrets are usually on the *target* -- that's *good*
|
||||||
|
# but also really annoying to build CI around when it needs secrets to work right.)
|
||||||
|
- id: plan
|
||||||
|
run: |
|
||||||
|
dist ${{ (!github.event.pull_request && format('host --steps=create --tag={0}', github.ref_name)) || 'plan' }} --output-format=json > plan-dist-manifest.json
|
||||||
|
echo "dist ran successfully"
|
||||||
|
cat plan-dist-manifest.json
|
||||||
|
echo "manifest=$(jq -c "." plan-dist-manifest.json)" >> "$GITHUB_OUTPUT"
|
||||||
|
- name: "Upload dist-manifest.json"
|
||||||
|
uses: actions/upload-artifact@v6
|
||||||
|
with:
|
||||||
|
name: artifacts-plan-dist-manifest
|
||||||
|
path: plan-dist-manifest.json
|
||||||
|
|
||||||
|
# Build and packages all the platform-specific things
|
||||||
|
build-local-artifacts:
|
||||||
|
name: build-local-artifacts (${{ join(matrix.targets, ', ') }})
|
||||||
|
# Let the initial task tell us to not run (currently very blunt)
|
||||||
|
needs:
|
||||||
|
- plan
|
||||||
|
if: ${{ fromJson(needs.plan.outputs.val).ci.github.artifacts_matrix.include != null && (needs.plan.outputs.publishing == 'true' || fromJson(needs.plan.outputs.val).ci.github.pr_run_mode == 'upload') }}
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
# Target platforms/runners are computed by dist in create-release.
|
||||||
|
# Each member of the matrix has the following arguments:
|
||||||
|
#
|
||||||
|
# - runner: the github runner
|
||||||
|
# - dist-args: cli flags to pass to dist
|
||||||
|
# - install-dist: expression to run to install dist on the runner
|
||||||
|
#
|
||||||
|
# Typically there will be:
|
||||||
|
# - 1 "global" task that builds universal installers
|
||||||
|
# - N "local" tasks that build each platform's binaries and platform-specific installers
|
||||||
|
matrix: ${{ fromJson(needs.plan.outputs.val).ci.github.artifacts_matrix }}
|
||||||
|
runs-on: ${{ matrix.runner }}
|
||||||
|
container: ${{ matrix.container && matrix.container.image || null }}
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
BUILD_MANIFEST_NAME: target/distrib/${{ join(matrix.targets, '-') }}-dist-manifest.json
|
||||||
|
steps:
|
||||||
|
- name: enable windows longpaths
|
||||||
|
run: |
|
||||||
|
git config --global core.longpaths true
|
||||||
|
- uses: actions/checkout@v6
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
submodules: recursive
|
||||||
|
- name: Install Rust non-interactively if not already installed
|
||||||
|
if: ${{ matrix.container }}
|
||||||
|
run: |
|
||||||
|
if ! command -v cargo > /dev/null 2>&1; then
|
||||||
|
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
|
||||||
|
echo "$HOME/.cargo/bin" >> $GITHUB_PATH
|
||||||
|
fi
|
||||||
|
- name: Install dist
|
||||||
|
run: ${{ matrix.install_dist.run }}
|
||||||
|
# Get the dist-manifest
|
||||||
|
- name: Fetch local artifacts
|
||||||
|
uses: actions/download-artifact@v7
|
||||||
|
with:
|
||||||
|
pattern: artifacts-*
|
||||||
|
path: target/distrib/
|
||||||
|
merge-multiple: true
|
||||||
|
- name: Install dependencies
|
||||||
|
run: |
|
||||||
|
${{ matrix.packages_install }}
|
||||||
|
- name: Build artifacts
|
||||||
|
run: |
|
||||||
|
# Actually do builds and make zips and whatnot
|
||||||
|
dist build ${{ needs.plan.outputs.tag-flag }} --print=linkage --output-format=json ${{ matrix.dist_args }} > dist-manifest.json
|
||||||
|
echo "dist ran successfully"
|
||||||
|
- id: cargo-dist
|
||||||
|
name: Post-build
|
||||||
|
# We force bash here just because github makes it really hard to get values up
|
||||||
|
# to "real" actions without writing to env-vars, and writing to env-vars has
|
||||||
|
# inconsistent syntax between shell and powershell.
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
# Parse out what we just built and upload it to scratch storage
|
||||||
|
echo "paths<<EOF" >> "$GITHUB_OUTPUT"
|
||||||
|
dist print-upload-files-from-manifest --manifest dist-manifest.json >> "$GITHUB_OUTPUT"
|
||||||
|
echo "EOF" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
cp dist-manifest.json "$BUILD_MANIFEST_NAME"
|
||||||
|
- name: "Upload artifacts"
|
||||||
|
uses: actions/upload-artifact@v6
|
||||||
|
with:
|
||||||
|
name: artifacts-build-local-${{ join(matrix.targets, '_') }}
|
||||||
|
path: |
|
||||||
|
${{ steps.cargo-dist.outputs.paths }}
|
||||||
|
${{ env.BUILD_MANIFEST_NAME }}
|
||||||
|
|
||||||
|
# Build and package all the platform-agnostic(ish) things
|
||||||
|
build-global-artifacts:
|
||||||
|
needs:
|
||||||
|
- plan
|
||||||
|
- build-local-artifacts
|
||||||
|
runs-on: "ubuntu-22.04"
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
BUILD_MANIFEST_NAME: target/distrib/global-dist-manifest.json
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v6
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
submodules: recursive
|
||||||
|
- name: Install cached dist
|
||||||
|
uses: actions/download-artifact@v7
|
||||||
|
with:
|
||||||
|
name: cargo-dist-cache
|
||||||
|
path: ~/.cargo/bin/
|
||||||
|
- run: chmod +x ~/.cargo/bin/dist
|
||||||
|
# Get all the local artifacts for the global tasks to use (for e.g. checksums)
|
||||||
|
- name: Fetch local artifacts
|
||||||
|
uses: actions/download-artifact@v7
|
||||||
|
with:
|
||||||
|
pattern: artifacts-*
|
||||||
|
path: target/distrib/
|
||||||
|
merge-multiple: true
|
||||||
|
- id: cargo-dist
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
dist build ${{ needs.plan.outputs.tag-flag }} --output-format=json "--artifacts=global" > dist-manifest.json
|
||||||
|
echo "dist ran successfully"
|
||||||
|
|
||||||
|
# Parse out what we just built and upload it to scratch storage
|
||||||
|
echo "paths<<EOF" >> "$GITHUB_OUTPUT"
|
||||||
|
jq --raw-output ".upload_files[]" dist-manifest.json >> "$GITHUB_OUTPUT"
|
||||||
|
echo "EOF" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
cp dist-manifest.json "$BUILD_MANIFEST_NAME"
|
||||||
|
- name: "Upload artifacts"
|
||||||
|
uses: actions/upload-artifact@v6
|
||||||
|
with:
|
||||||
|
name: artifacts-build-global
|
||||||
|
path: |
|
||||||
|
${{ steps.cargo-dist.outputs.paths }}
|
||||||
|
${{ env.BUILD_MANIFEST_NAME }}
|
||||||
|
# Determines if we should publish/announce
|
||||||
|
host:
|
||||||
|
needs:
|
||||||
|
- plan
|
||||||
|
- build-local-artifacts
|
||||||
|
- build-global-artifacts
|
||||||
|
# Only run if we're "publishing", and only if plan, local and global didn't fail (skipped is fine)
|
||||||
|
if: ${{ always() && needs.plan.result == 'success' && needs.plan.outputs.publishing == 'true' && (needs.build-global-artifacts.result == 'skipped' || needs.build-global-artifacts.result == 'success') && (needs.build-local-artifacts.result == 'skipped' || needs.build-local-artifacts.result == 'success') }}
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
runs-on: "ubuntu-22.04"
|
||||||
|
outputs:
|
||||||
|
val: ${{ steps.host.outputs.manifest }}
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v6
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
submodules: recursive
|
||||||
|
- name: Install cached dist
|
||||||
|
uses: actions/download-artifact@v7
|
||||||
|
with:
|
||||||
|
name: cargo-dist-cache
|
||||||
|
path: ~/.cargo/bin/
|
||||||
|
- run: chmod +x ~/.cargo/bin/dist
|
||||||
|
# Fetch artifacts from scratch-storage
|
||||||
|
- name: Fetch artifacts
|
||||||
|
uses: actions/download-artifact@v7
|
||||||
|
with:
|
||||||
|
pattern: artifacts-*
|
||||||
|
path: target/distrib/
|
||||||
|
merge-multiple: true
|
||||||
|
- id: host
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
dist host ${{ needs.plan.outputs.tag-flag }} --steps=upload --steps=release --output-format=json > dist-manifest.json
|
||||||
|
echo "artifacts uploaded and released successfully"
|
||||||
|
cat dist-manifest.json
|
||||||
|
echo "manifest=$(jq -c "." dist-manifest.json)" >> "$GITHUB_OUTPUT"
|
||||||
|
- name: "Upload dist-manifest.json"
|
||||||
|
uses: actions/upload-artifact@v6
|
||||||
|
with:
|
||||||
|
# Overwrite the previous copy
|
||||||
|
name: artifacts-dist-manifest
|
||||||
|
path: dist-manifest.json
|
||||||
|
# Create a GitHub Release while uploading all files to it
|
||||||
|
- name: "Download GitHub Artifacts"
|
||||||
|
uses: actions/download-artifact@v7
|
||||||
|
with:
|
||||||
|
pattern: artifacts-*
|
||||||
|
path: artifacts
|
||||||
|
merge-multiple: true
|
||||||
|
- name: Cleanup
|
||||||
|
run: |
|
||||||
|
# Remove the granular manifests
|
||||||
|
rm -f artifacts/*-dist-manifest.json
|
||||||
|
- name: Create GitHub Release
|
||||||
|
env:
|
||||||
|
PRERELEASE_FLAG: "${{ fromJson(steps.host.outputs.manifest).announcement_is_prerelease && '--prerelease' || '' }}"
|
||||||
|
ANNOUNCEMENT_TITLE: "${{ fromJson(steps.host.outputs.manifest).announcement_title }}"
|
||||||
|
ANNOUNCEMENT_BODY: "${{ fromJson(steps.host.outputs.manifest).announcement_github_body }}"
|
||||||
|
RELEASE_COMMIT: "${{ github.sha }}"
|
||||||
|
run: |
|
||||||
|
# Write and read notes from a file to avoid quoting breaking things
|
||||||
|
echo "$ANNOUNCEMENT_BODY" > $RUNNER_TEMP/notes.txt
|
||||||
|
|
||||||
|
gh release create "${{ needs.plan.outputs.tag }}" --target "$RELEASE_COMMIT" $PRERELEASE_FLAG --title "$ANNOUNCEMENT_TITLE" --notes-file "$RUNNER_TEMP/notes.txt" artifacts/*
|
||||||
|
|
||||||
|
announce:
|
||||||
|
needs:
|
||||||
|
- plan
|
||||||
|
- host
|
||||||
|
# use "always() && ..." to allow us to wait for all publish jobs while
|
||||||
|
# still allowing individual publish jobs to skip themselves (for prereleases).
|
||||||
|
# "host" however must run to completion, no skipping allowed!
|
||||||
|
if: ${{ always() && needs.host.result == 'success' }}
|
||||||
|
runs-on: "ubuntu-22.04"
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v6
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
submodules: recursive
|
||||||
1484
Cargo.lock
generated
1484
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
37
Cargo.toml
37
Cargo.toml
|
|
@ -9,8 +9,14 @@ members = [
|
||||||
"crates/noise",
|
"crates/noise",
|
||||||
"crates/palette-compress",
|
"crates/palette-compress",
|
||||||
"crates/biome-db",
|
"crates/biome-db",
|
||||||
|
"crates/wasm-host",
|
||||||
|
"crates/scripting",
|
||||||
|
"crates/i18n",
|
||||||
"bench",
|
"bench",
|
||||||
]
|
]
|
||||||
|
exclude = [
|
||||||
|
"plugins/example-noisemap",
|
||||||
|
]
|
||||||
|
|
||||||
[workspace.dependencies.criterion]
|
[workspace.dependencies.criterion]
|
||||||
version = "0.5"
|
version = "0.5"
|
||||||
|
|
@ -21,6 +27,7 @@ edition = "2024"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
license = "GPL-3.0-or-later"
|
license = "GPL-3.0-or-later"
|
||||||
authors = ["Loki"]
|
authors = ["Loki"]
|
||||||
|
repository = "https://github.com/loki5512344/Terrafier"
|
||||||
description = "A high-performance Minecraft world painter, written in Rust"
|
description = "A high-performance Minecraft world painter, written in Rust"
|
||||||
|
|
||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
|
|
@ -35,13 +42,39 @@ image = { version = "0.25", default-features = false, features = ["png"] }
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
env_logger = "0.11"
|
env_logger = "0.11"
|
||||||
serde-big-array = "0.5"
|
serde-big-array = "0.5"
|
||||||
bincode = "1.3"
|
bincode = { version = "2", features = ["serde"] }
|
||||||
flate2 = "1.1"
|
flate2 = "1.1"
|
||||||
|
|
||||||
|
# Config for 'dist'
|
||||||
[workspace.metadata.dist]
|
[workspace.metadata.dist]
|
||||||
# Инициализируется при первом запуске cargo dist init
|
# The preferred dist version to use in CI (Cargo.toml SemVer syntax)
|
||||||
|
cargo-dist-version = "0.31.0"
|
||||||
|
rust-toolchain = "stable"
|
||||||
|
# CI backends to support
|
||||||
|
ci = "github"
|
||||||
|
# Target platforms to build apps for (Rust target-triple syntax)
|
||||||
|
targets = ["aarch64-apple-darwin", "x86_64-apple-darwin", "x86_64-unknown-linux-gnu", "x86_64-pc-windows-msvc"]
|
||||||
|
# The installers to generate for each app
|
||||||
|
installers = ["shell", "msi", "pkg"]
|
||||||
|
# The archive format to use for non-windows builds (defaults .tar.xz)
|
||||||
|
unix-archive = ".tar.gz"
|
||||||
|
# The archive format to use for windows builds (defaults .zip)
|
||||||
|
windows-archive = ".zip"
|
||||||
|
# Path that installers should place binaries in
|
||||||
|
install-path = "CARGO_HOME"
|
||||||
|
# Whether to install an updater program
|
||||||
|
install-updater = false
|
||||||
|
|
||||||
|
[workspace.metadata.dist.mac-pkg-config]
|
||||||
|
identifier = "com.terrafier.app"
|
||||||
|
install-location = "/usr/local"
|
||||||
|
|
||||||
[profile.release]
|
[profile.release]
|
||||||
opt-level = 3
|
opt-level = 3
|
||||||
lto = true
|
lto = true
|
||||||
codegen-units = 1
|
codegen-units = 1
|
||||||
|
|
||||||
|
# The profile that 'dist' will build with
|
||||||
|
[profile.dist]
|
||||||
|
inherits = "release"
|
||||||
|
lto = "thin"
|
||||||
|
|
|
||||||
38
PLAN.md
38
PLAN.md
|
|
@ -17,7 +17,9 @@
|
||||||
│ │World │Tile │Export │Import │ │
|
│ │World │Tile │Export │Import │ │
|
||||||
│ │Model │Ops │Pipeline│Anvil │ │
|
│ │Model │Ops │Pipeline│Anvil │ │
|
||||||
│ └──────┴──────┴────────┴────────┘ │
|
│ └──────┴──────┴────────┴────────┘ │
|
||||||
├────────────────────────────────────────────────────┤
|
├──────────────────────┬─────────────────────────────┤
|
||||||
|
│ wasm-host / rhai │ Плагины и скриптинг │
|
||||||
|
├──────────────────────┴─────────────────────────────┤
|
||||||
│ Foundation crates │
|
│ Foundation crates │
|
||||||
│ nbt │ fastanvil │ noise │ palette-compress │
|
│ nbt │ fastanvil │ noise │ palette-compress │
|
||||||
│ biome-db │
|
│ biome-db │
|
||||||
|
|
@ -39,10 +41,10 @@
|
||||||
- **Heightmap**: noise-based генерация (OpenSimplex), flat, combined
|
- **Heightmap**: noise-based генерация (OpenSimplex), flat, combined
|
||||||
|
|
||||||
### В работе / не реализовано
|
### В работе / не реализовано
|
||||||
- **GPU-рендеринг (wgpu)** — ключевая фича, пока весь рендер CPU
|
- **WASM плагины** — wasmtime-хост, ABI, загрузка .wasm файлов
|
||||||
- **Слои**: Caves, Rivers, Frost, Trees, Resources, Biome
|
- **Скриптинг** — Rhai-движок для world-генераторов
|
||||||
- **Импорт .world** (WorldPainter format)
|
- **i18n** — локализация CLI/GUI
|
||||||
- **Плагины** (WASM)
|
- **GUI polish** — pan/zoom GPU viewport, brush overlay, layer panel
|
||||||
|
|
||||||
## Стек
|
## Стек
|
||||||
|
|
||||||
|
|
@ -76,23 +78,29 @@ NBT парсер, Anvil reader, workspace setup
|
||||||
Tile, Dimension, World, Terrain, Brush, Operation
|
Tile, Dimension, World, Terrain, Brush, Operation
|
||||||
|
|
||||||
### Phase 2 (done) — Minecraft I/O + CLI
|
### Phase 2 (done) — Minecraft I/O + CLI
|
||||||
Импорт/экспорт Java Edition, CLI команды
|
Импорт/экспорт Java Edition, CLI команды, импорт .world
|
||||||
|
|
||||||
### Phase 3 — Operations & Layers
|
### Phase 3 (done) — Operations & Layers
|
||||||
Инструменты (raise, erode, smooth, flatten, fill, paint)
|
Инструменты (raise, erode, smooth, flatten, fill, paint)
|
||||||
Слои (caves, river, frost, trees, biome, resources)
|
Слои (caves, river, frost, trees, biome, resources)
|
||||||
Экспортёры слоёв
|
Экспортёры слоёв, фильтры, MultiTile
|
||||||
|
|
||||||
### Phase 4 — GUI
|
### Phase 4 (done) — GUI (MVP)
|
||||||
GPU-рендеринг через wgpu (шейдеры, 30-60 FPS)
|
GPU-рендеринг через wgpu (базовый), панель инструментов
|
||||||
Панорамирование, зум, оверлей кисти
|
Диалоги (New World, Export), undo/redo
|
||||||
Панель слоёв, инструментов, диалоги
|
|
||||||
|
|
||||||
### Phase 5 — Полировка
|
### Phase 5 — WASM Plugins + Scripting
|
||||||
Бенчмарки, оптимизация, i18n, пакеты (AppImage/msi/app)
|
WASM-хост на wasmtime, ABI для плагинов
|
||||||
|
Rhai-скриптинг для world-генераторов
|
||||||
|
Загрузка плагинов из .wasm файлов, registry
|
||||||
|
|
||||||
|
### Phase 6 — i18n + Инфраструктура
|
||||||
|
Локализация CLI/GUI (rust-i18n / Fluent)
|
||||||
|
CI/CD, cargo-dist, AppImage/msi/app
|
||||||
|
Бенчмарки, оптимизация, polish
|
||||||
|
|
||||||
### Будущее
|
### Будущее
|
||||||
Импорт .world, WASM плагины, Bedrock Edition, скриптинг
|
Bedrock Edition, GUI pan/zoom/brush-overlay, GUI layer panel
|
||||||
|
|
||||||
## Модель данных
|
## Модель данных
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,8 +11,8 @@ fn make_small_compound() -> Tag {
|
||||||
map.insert("short".into(), Tag::Short(1000));
|
map.insert("short".into(), Tag::Short(1000));
|
||||||
map.insert("int".into(), Tag::Int(1_000_000));
|
map.insert("int".into(), Tag::Int(1_000_000));
|
||||||
map.insert("long".into(), Tag::Long(1_000_000_000));
|
map.insert("long".into(), Tag::Long(1_000_000_000));
|
||||||
map.insert("float".into(), Tag::Float(3.14));
|
map.insert("float".into(), Tag::Float(std::f64::consts::PI as f32));
|
||||||
map.insert("double".into(), Tag::Double(2.718));
|
map.insert("double".into(), Tag::Double(std::f64::consts::E));
|
||||||
map.insert("string".into(), Tag::String("Hello, Benchmark!".into()));
|
map.insert("string".into(), Tag::String("Hello, Benchmark!".into()));
|
||||||
map.insert(
|
map.insert(
|
||||||
"inner".into(),
|
"inner".into(),
|
||||||
|
|
|
||||||
|
|
@ -3,13 +3,26 @@ name = "terrafier-cli"
|
||||||
version.workspace = true
|
version.workspace = true
|
||||||
edition.workspace = true
|
edition.workspace = true
|
||||||
license.workspace = true
|
license.workspace = true
|
||||||
|
repository.workspace = true
|
||||||
|
authors.workspace = true
|
||||||
description = "CLI tool for Terrafier — create, edit, export Minecraft worlds from the command line"
|
description = "CLI tool for Terrafier — create, edit, export Minecraft worlds from the command line"
|
||||||
|
|
||||||
|
[package.metadata.wix]
|
||||||
|
upgrade-guid = "351D0A14-B7C2-412B-BEB5-BD394ED35C94"
|
||||||
|
path-guid = "C4FB2FF3-085A-4CDF-B823-B0A2EB5B8220"
|
||||||
|
license = false
|
||||||
|
eula = false
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
terrafier-core = { path = "../core" }
|
terrafier-core = { path = "../core" }
|
||||||
|
terrafier-scripting = { path = "../crates/scripting", optional = true }
|
||||||
|
terrafier-i18n = { path = "../crates/i18n" }
|
||||||
clap.workspace = true
|
clap.workspace = true
|
||||||
anyhow.workspace = true
|
anyhow.workspace = true
|
||||||
indicatif.workspace = true
|
indicatif.workspace = true
|
||||||
log.workspace = true
|
log.workspace = true
|
||||||
env_logger.workspace = true
|
env_logger.workspace = true
|
||||||
serde_json.workspace = true
|
serde_json.workspace = true
|
||||||
|
|
||||||
|
[features]
|
||||||
|
scripting = ["terrafier-scripting"]
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
use clap::Args;
|
use clap::Args;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
use terrafier_i18n as i18n;
|
||||||
|
|
||||||
use crate::util::output::{self, OutputFormat};
|
use crate::util::output::{self, OutputFormat};
|
||||||
use crate::util::{load_world, progress};
|
use crate::util::{load_world, progress};
|
||||||
|
|
@ -31,6 +32,9 @@ pub fn cmd_export(
|
||||||
let tile_count: usize = world.dimensions.iter().map(|d| d.tiles.len()).sum();
|
let tile_count: usize = world.dimensions.iter().map(|d| d.tiles.len()).sum();
|
||||||
|
|
||||||
if validate || dry_run {
|
if validate || dry_run {
|
||||||
|
if dry_run {
|
||||||
|
log::info!("{}", i18n::translate_with("cli.dry_run", &[("action", "export")]));
|
||||||
|
}
|
||||||
output::print_result(
|
output::print_result(
|
||||||
format,
|
format,
|
||||||
&serde_json::json!({
|
&serde_json::json!({
|
||||||
|
|
@ -45,9 +49,10 @@ pub fn cmd_export(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let bar = progress::new_bar(tile_count as u64, "Exporting...");
|
let exporting_msg = i18n::translate_with("cli.export.exporting", &[("path", &args.output)]);
|
||||||
|
let bar = progress::new_bar(tile_count as u64, &exporting_msg);
|
||||||
terrafier_core::io::export::export_to_save(&world, Path::new(&args.output))?;
|
terrafier_core::io::export::export_to_save(&world, Path::new(&args.output))?;
|
||||||
progress::finish_with("Export complete", bar);
|
progress::finish_with(i18n::t!("cli.export.complete"), bar);
|
||||||
|
|
||||||
log::info!("World exported to: {}", args.output);
|
log::info!("World exported to: {}", args.output);
|
||||||
output::print_result(
|
output::print_result(
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
use clap::Args;
|
use clap::Args;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
use terrafier_i18n as i18n;
|
||||||
|
|
||||||
use crate::util::output::{self, OutputFormat};
|
use crate::util::output::{self, OutputFormat};
|
||||||
use crate::util::progress;
|
use crate::util::progress;
|
||||||
|
|
@ -22,6 +23,9 @@ pub fn cmd_import(
|
||||||
log::info!("Importing Minecraft save from: {}", input_path.display());
|
log::info!("Importing Minecraft save from: {}", input_path.display());
|
||||||
|
|
||||||
if validate || dry_run {
|
if validate || dry_run {
|
||||||
|
if dry_run {
|
||||||
|
log::info!("{}", i18n::translate_with("cli.dry_run", &[("action", "import")]));
|
||||||
|
}
|
||||||
terrafier_core::io::import::validate_save(input_path)?;
|
terrafier_core::io::import::validate_save(input_path)?;
|
||||||
output::print_result(
|
output::print_result(
|
||||||
format,
|
format,
|
||||||
|
|
@ -46,9 +50,15 @@ pub fn cmd_import(
|
||||||
} else {
|
} else {
|
||||||
0
|
0
|
||||||
};
|
};
|
||||||
let bar = progress::new_bar(region_count.max(1) as u64, "Importing regions...");
|
let importing_msg = i18n::translate_with("cli.import.importing", &[("path", &args.input)]);
|
||||||
|
let bar = progress::new_bar(region_count.max(1) as u64, &importing_msg);
|
||||||
let world = terrafier_core::io::import::import(input_path)?;
|
let world = terrafier_core::io::import::import(input_path)?;
|
||||||
progress::finish_with("Import complete", bar);
|
let tile_count: usize = world.dimensions.iter().map(|d| d.tiles.len()).sum();
|
||||||
|
let import_msg = i18n::translate_with("cli.import.complete", &[
|
||||||
|
("tiles", &tile_count.to_string()),
|
||||||
|
("dimensions", &world.dimensions.len().to_string()),
|
||||||
|
]);
|
||||||
|
progress::finish_with(&import_msg, bar);
|
||||||
|
|
||||||
let output_path = Path::new(&args.output);
|
let output_path = Path::new(&args.output);
|
||||||
std::fs::create_dir_all(output_path)?;
|
std::fs::create_dir_all(output_path)?;
|
||||||
|
|
@ -56,7 +66,6 @@ pub fn cmd_import(
|
||||||
let world_json = serde_json::to_string_pretty(&world)?;
|
let world_json = serde_json::to_string_pretty(&world)?;
|
||||||
std::fs::write(output_path.join("world.tfw"), &world_json)?;
|
std::fs::write(output_path.join("world.tfw"), &world_json)?;
|
||||||
|
|
||||||
let tile_count: usize = world.dimensions.iter().map(|d| d.tiles.len()).sum();
|
|
||||||
log::info!(
|
log::info!(
|
||||||
"Import complete: {} dimensions, {} tiles",
|
"Import complete: {} dimensions, {} tiles",
|
||||||
world.dimensions.len(),
|
world.dimensions.len(),
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
use clap::Args;
|
use clap::Args;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
use terrafier_i18n as i18n;
|
||||||
|
|
||||||
use crate::util::output::{self, OutputFormat};
|
use crate::util::output::{self, OutputFormat};
|
||||||
use crate::util::{load_world, progress};
|
use crate::util::{load_world, progress};
|
||||||
|
|
@ -31,6 +32,9 @@ pub fn cmd_render(
|
||||||
let tile_count: usize = world.dimensions.iter().map(|d| d.tiles.len()).sum();
|
let tile_count: usize = world.dimensions.iter().map(|d| d.tiles.len()).sum();
|
||||||
|
|
||||||
if validate || dry_run {
|
if validate || dry_run {
|
||||||
|
if dry_run {
|
||||||
|
log::info!("{}", i18n::translate_with("cli.dry_run", &[("action", "render")]));
|
||||||
|
}
|
||||||
output::print_result(
|
output::print_result(
|
||||||
format,
|
format,
|
||||||
&serde_json::json!({
|
&serde_json::json!({
|
||||||
|
|
@ -44,9 +48,12 @@ pub fn cmd_render(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let bar = progress::new_bar(tile_count as u64, "Rendering...");
|
let bar = progress::new_bar(tile_count as u64, i18n::t!("cli.render.rendering"));
|
||||||
terrafier_core::io::export::render_to_image(&world, &args.output, args.scale)?;
|
terrafier_core::io::export::render_to_image(&world, &args.output, args.scale)?;
|
||||||
progress::finish_with("Render complete", bar);
|
let saved_msg = i18n::translate_with("cli.render.saved", &[
|
||||||
|
("path", &args.output.display().to_string()),
|
||||||
|
]);
|
||||||
|
progress::finish_with(&saved_msg, bar);
|
||||||
|
|
||||||
log::info!("Preview rendered to: {}", args.output.display());
|
log::info!("Preview rendered to: {}", args.output.display());
|
||||||
output::print_result(
|
output::print_result(
|
||||||
|
|
|
||||||
|
|
@ -5,3 +5,8 @@ pub use io::render;
|
||||||
|
|
||||||
pub mod info;
|
pub mod info;
|
||||||
pub mod new;
|
pub mod new;
|
||||||
|
|
||||||
|
#[cfg(feature = "scripting")]
|
||||||
|
mod run;
|
||||||
|
#[cfg(feature = "scripting")]
|
||||||
|
pub use run::cmd_run;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
use clap::Args;
|
use clap::Args;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use terrafier_core::World;
|
use terrafier_core::World;
|
||||||
|
use terrafier_i18n as i18n;
|
||||||
|
|
||||||
use crate::util::output::{self, OutputFormat};
|
use crate::util::output::{self, OutputFormat};
|
||||||
use crate::util::progress;
|
use crate::util::progress;
|
||||||
|
|
@ -42,9 +43,14 @@ pub fn cmd_new(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let spinner = progress::new_spinner("Generating world...");
|
let spinner = progress::new_spinner(i18n::t!("cli.new.creating"));
|
||||||
let world = World::new(&args.name, args.seed);
|
let world = World::new(&args.name, args.seed);
|
||||||
progress::finish_with("World generated", spinner);
|
let tile_count = world.dimensions.iter().map(|d| d.tiles.len()).sum::<usize>();
|
||||||
|
let created_msg = i18n::translate_with("cli.new.created", &[
|
||||||
|
("name", &args.name),
|
||||||
|
("tiles", &tile_count.to_string()),
|
||||||
|
]);
|
||||||
|
progress::finish_with(&created_msg, spinner);
|
||||||
|
|
||||||
let world_dir = args.output.join(&args.name);
|
let world_dir = args.output.join(&args.name);
|
||||||
std::fs::create_dir_all(&world_dir)?;
|
std::fs::create_dir_all(&world_dir)?;
|
||||||
|
|
@ -52,7 +58,6 @@ pub fn cmd_new(
|
||||||
let world_json = serde_json::to_string_pretty(&world)?;
|
let world_json = serde_json::to_string_pretty(&world)?;
|
||||||
std::fs::write(world_dir.join("world.tfw"), &world_json)?;
|
std::fs::write(world_dir.join("world.tfw"), &world_json)?;
|
||||||
|
|
||||||
let dim = &world.dimensions[0];
|
|
||||||
log::info!("World saved to: {}", world_dir.display());
|
log::info!("World saved to: {}", world_dir.display());
|
||||||
output::print_result(
|
output::print_result(
|
||||||
format,
|
format,
|
||||||
|
|
@ -61,7 +66,7 @@ pub fn cmd_new(
|
||||||
"name": args.name,
|
"name": args.name,
|
||||||
"seed": args.seed,
|
"seed": args.seed,
|
||||||
"path": world_dir.display().to_string(),
|
"path": world_dir.display().to_string(),
|
||||||
"tiles": dim.tiles.len(),
|
"tiles": tile_count,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
||||||
8
cli/src/cmds/run.rs
Normal file
8
cli/src/cmds/run.rs
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
use anyhow::Result;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
pub fn cmd_run(script: &PathBuf) -> Result<()> {
|
||||||
|
terrafier_scripting::run_script(script)?;
|
||||||
|
println!("Script executed successfully.");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
mod cmds;
|
mod cmds;
|
||||||
mod util;
|
mod util;
|
||||||
|
|
@ -38,9 +39,18 @@ enum Commands {
|
||||||
Info(cmds::info::InfoArgs),
|
Info(cmds::info::InfoArgs),
|
||||||
/// Render world preview to an image
|
/// Render world preview to an image
|
||||||
Render(cmds::render::RenderArgs),
|
Render(cmds::render::RenderArgs),
|
||||||
|
/// Run a Terrafier script (.rhai)
|
||||||
|
#[cfg(feature = "scripting")]
|
||||||
|
Run {
|
||||||
|
/// Path to .rhai script file
|
||||||
|
script: PathBuf,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
|
use terrafier_i18n as i18n;
|
||||||
|
i18n::init();
|
||||||
|
|
||||||
env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info"))
|
env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info"))
|
||||||
.format_timestamp(None)
|
.format_timestamp(None)
|
||||||
.init();
|
.init();
|
||||||
|
|
@ -60,6 +70,8 @@ fn main() {
|
||||||
Commands::Render(args) => {
|
Commands::Render(args) => {
|
||||||
cmds::render::cmd_render(args, &format, cli.dry_run, cli.validate)
|
cmds::render::cmd_render(args, &format, cli.dry_run, cli.validate)
|
||||||
}
|
}
|
||||||
|
#[cfg(feature = "scripting")]
|
||||||
|
Commands::Run { script } => cmds::cmd_run(&script),
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Err(e) = result {
|
if let Err(e) = result {
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
|
use terrafier_i18n as i18n;
|
||||||
|
|
||||||
pub enum OutputFormat {
|
pub enum OutputFormat {
|
||||||
Json,
|
Json,
|
||||||
|
|
@ -17,7 +18,11 @@ impl OutputFormat {
|
||||||
pub fn print_result(format: &OutputFormat, result: &Value) {
|
pub fn print_result(format: &OutputFormat, result: &Value) {
|
||||||
match format {
|
match format {
|
||||||
OutputFormat::Json => {
|
OutputFormat::Json => {
|
||||||
println!("{}", serde_json::to_string(result).unwrap());
|
println!(
|
||||||
|
"{}",
|
||||||
|
serde_json::to_string(result)
|
||||||
|
.unwrap_or_else(|e| format!("serialization error: {e}"))
|
||||||
|
);
|
||||||
}
|
}
|
||||||
OutputFormat::Human => print_human(result),
|
OutputFormat::Human => print_human(result),
|
||||||
}
|
}
|
||||||
|
|
@ -31,68 +36,116 @@ fn print_human(result: &Value) {
|
||||||
let seed = result["seed"].as_u64().unwrap_or(0);
|
let seed = result["seed"].as_u64().unwrap_or(0);
|
||||||
let tiles = result["tiles"].as_u64().unwrap_or(0);
|
let tiles = result["tiles"].as_u64().unwrap_or(0);
|
||||||
println!(
|
println!(
|
||||||
"Created world '{}' (seed: {}, tiles: {})",
|
"{}",
|
||||||
name, seed, tiles
|
i18n::translate_with("cli.new.created", &[
|
||||||
|
("name", name),
|
||||||
|
("tiles", &tiles.to_string()),
|
||||||
|
])
|
||||||
);
|
);
|
||||||
|
let _ = seed;
|
||||||
}
|
}
|
||||||
"exported" => {
|
"exported" => {
|
||||||
let input = result["input"].as_str().unwrap_or("?");
|
|
||||||
let output = result["output"].as_str().unwrap_or("?");
|
|
||||||
let tiles = result["tiles"].as_u64().unwrap_or(0);
|
let tiles = result["tiles"].as_u64().unwrap_or(0);
|
||||||
println!("Exported {} tiles from '{}' to '{}'", tiles, input, output);
|
println!(
|
||||||
|
"{}",
|
||||||
|
i18n::translate_with("cli.export.complete", &[
|
||||||
|
("regions", "?"),
|
||||||
|
("chunks", &tiles.to_string()),
|
||||||
|
])
|
||||||
|
);
|
||||||
}
|
}
|
||||||
"imported" => {
|
"imported" => {
|
||||||
let name = result["name"].as_str().unwrap_or("?");
|
let name = result["name"].as_str().unwrap_or("?");
|
||||||
let seed = result["seed"].as_u64().unwrap_or(0);
|
|
||||||
let dims = result["dimensions"].as_u64().unwrap_or(0);
|
|
||||||
let tiles = result["tiles"].as_u64().unwrap_or(0);
|
let tiles = result["tiles"].as_u64().unwrap_or(0);
|
||||||
|
let dims = result["dimensions"].as_u64().unwrap_or(0);
|
||||||
println!(
|
println!(
|
||||||
"Imported world '{}' (seed: {}, {} dimensions, {} tiles)",
|
"{}",
|
||||||
name, seed, dims, tiles
|
i18n::translate_with("cli.import.complete", &[
|
||||||
|
("tiles", &tiles.to_string()),
|
||||||
|
("dimensions", &dims.to_string()),
|
||||||
|
])
|
||||||
);
|
);
|
||||||
|
let _ = name;
|
||||||
}
|
}
|
||||||
"rendered" => {
|
"rendered" => {
|
||||||
let world = result["world"].as_str().unwrap_or("?");
|
|
||||||
let output = result["output"].as_str().unwrap_or("?");
|
let output = result["output"].as_str().unwrap_or("?");
|
||||||
println!("Rendered world '{}' to '{}'", world, output);
|
println!(
|
||||||
|
"{}",
|
||||||
|
i18n::translate_with("cli.render.saved", &[
|
||||||
|
("path", output),
|
||||||
|
])
|
||||||
|
);
|
||||||
}
|
}
|
||||||
"validated" => {
|
"validated" => {
|
||||||
println!("Validation passed: {:?}", result);
|
println!("{}", i18n::t!("cli.validate.ok"));
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
println!("{}", serde_json::to_string_pretty(result).unwrap());
|
println!(
|
||||||
|
"{}",
|
||||||
|
serde_json::to_string_pretty(result)
|
||||||
|
.unwrap_or_else(|e| format!("serialization error: {e}"))
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if result.get("name").is_some() {
|
} else if result.get("name").is_some() {
|
||||||
// Info output
|
// Info output
|
||||||
println!("World: {}", result["name"].as_str().unwrap_or("?"));
|
print_human_info(result);
|
||||||
|
} else {
|
||||||
|
println!(
|
||||||
|
"{}",
|
||||||
|
serde_json::to_string_pretty(result)
|
||||||
|
.unwrap_or_else(|e| format!("serialization error: {e}"))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn print_human_info(result: &Value) {
|
||||||
|
let name = result["name"].as_str().unwrap_or("?");
|
||||||
|
println!(
|
||||||
|
"{}",
|
||||||
|
i18n::translate_with("cli.info.title", &[("name", name)])
|
||||||
|
);
|
||||||
if let Some(seed) = result.get("seed").and_then(|v| v.as_u64()) {
|
if let Some(seed) = result.get("seed").and_then(|v| v.as_u64()) {
|
||||||
println!(" Seed: {}", seed);
|
println!(
|
||||||
|
" {}",
|
||||||
|
i18n::translate_with("cli.info.seed", &[("seed", &seed.to_string())])
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if let Some(platform) = result.get("platform").and_then(|v| v.as_str()) {
|
if let Some(platform) = result.get("platform").and_then(|v| v.as_str()) {
|
||||||
println!(" Platform: {}", platform);
|
println!(
|
||||||
|
" {}",
|
||||||
|
i18n::translate_with("cli.info.platform", &[("platform", platform)])
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if let Some(height) = result.get("height_range") {
|
if let Some(height) = result.get("height_range") {
|
||||||
let min = height["min"].as_i64().unwrap_or(0);
|
let min = height["min"].as_i64().unwrap_or(0);
|
||||||
let max = height["max"].as_i64().unwrap_or(0);
|
let max = height["max"].as_i64().unwrap_or(0);
|
||||||
println!(" Height range: {} to {}", min, max);
|
eprintln!(" Height range: {} to {}", min, max);
|
||||||
}
|
}
|
||||||
if let Some(dims) = result.get("dimensions").and_then(|v| v.as_array()) {
|
if let Some(dims) = result.get("dimensions").and_then(|v| v.as_array()) {
|
||||||
println!(" Dimensions:");
|
println!(
|
||||||
|
" {}",
|
||||||
|
i18n::translate_with("cli.info.dimensions", &[
|
||||||
|
("dimensions", &dims.len().to_string()),
|
||||||
|
])
|
||||||
|
);
|
||||||
for dim in dims {
|
for dim in dims {
|
||||||
let name = dim["name"].as_str().unwrap_or("?");
|
let dim_name = dim["name"].as_str().unwrap_or("?");
|
||||||
let tiles = dim["tiles"].as_u64().unwrap_or(0);
|
let dim_tiles = dim["tiles"].as_u64().unwrap_or(0);
|
||||||
println!(" - {} ({} tiles)", name, tiles);
|
println!(
|
||||||
|
" - {} ({} tiles)",
|
||||||
|
dim_name, dim_tiles
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if let Some(tiles) = result.get("total_tiles").and_then(|v| v.as_u64()) {
|
if let Some(tiles) = result.get("total_tiles").and_then(|v| v.as_u64()) {
|
||||||
println!(" Total tiles: {}", tiles);
|
println!(
|
||||||
|
" {}",
|
||||||
|
i18n::translate_with("cli.info.tiles", &[("tiles", &tiles.to_string())])
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if let Some(path) = result.get("path").and_then(|v| v.as_str()) {
|
if let Some(path) = result.get("path").and_then(|v| v.as_str()) {
|
||||||
println!(" Path: {}", path);
|
eprintln!(" Path: {}", path);
|
||||||
}
|
|
||||||
} else {
|
|
||||||
println!("{}", serde_json::to_string_pretty(result).unwrap());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -101,11 +154,12 @@ pub fn print_error(format: &OutputFormat, error: &anyhow::Error) {
|
||||||
OutputFormat::Json => {
|
OutputFormat::Json => {
|
||||||
eprintln!(
|
eprintln!(
|
||||||
"{}",
|
"{}",
|
||||||
serde_json::to_string(&serde_json::json!({"error": error.to_string()})).unwrap()
|
serde_json::to_string(&serde_json::json!({"error": error.to_string()}))
|
||||||
|
.unwrap_or_else(|e| format!("serialization error: {e}"))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
OutputFormat::Human => {
|
OutputFormat::Human => {
|
||||||
eprintln!("Error: {}", error);
|
eprintln!("{}: {}", i18n::t!("cli.status.error"), error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -116,7 +170,8 @@ pub fn print_info(format: &OutputFormat, msg: &str) {
|
||||||
OutputFormat::Json => {
|
OutputFormat::Json => {
|
||||||
println!(
|
println!(
|
||||||
"{}",
|
"{}",
|
||||||
serde_json::to_string(&serde_json::json!({"info": msg})).unwrap()
|
serde_json::to_string(&serde_json::json!({"info": msg}))
|
||||||
|
.unwrap_or_else(|e| format!("serialization error: {e}"))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
OutputFormat::Human => {
|
OutputFormat::Human => {
|
||||||
|
|
|
||||||
228
cli/wix/main.wxs
Normal file
228
cli/wix/main.wxs
Normal file
|
|
@ -0,0 +1,228 @@
|
||||||
|
<?xml version='1.0' encoding='windows-1252'?>
|
||||||
|
<!--
|
||||||
|
Copyright (C) 2017 Christopher R. Field.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<!--
|
||||||
|
The "cargo wix" subcommand provides a variety of predefined variables available
|
||||||
|
for customization of this template. The values for each variable are set at
|
||||||
|
installer creation time. The following variables are available:
|
||||||
|
|
||||||
|
TargetTriple = The rustc target triple name.
|
||||||
|
TargetEnv = The rustc target environment. This is typically either
|
||||||
|
"msvc" or "gnu" depending on the toolchain downloaded and
|
||||||
|
installed.
|
||||||
|
TargetVendor = The rustc target vendor. This is typically "pc", but Rust
|
||||||
|
does support other vendors, like "uwp".
|
||||||
|
CargoTargetBinDir = The complete path to the directory containing the
|
||||||
|
binaries (exes) to include. The default would be
|
||||||
|
"target\release\". If an explicit rustc target triple is
|
||||||
|
used, i.e. cross-compiling, then the default path would
|
||||||
|
be "target\<CARGO_TARGET>\<CARGO_PROFILE>",
|
||||||
|
where "<CARGO_TARGET>" is replaced with the "CargoTarget"
|
||||||
|
variable value and "<CARGO_PROFILE>" is replaced with the
|
||||||
|
value from the "CargoProfile" variable. This can also
|
||||||
|
be overridden manually with the "target-bin-dir" flag.
|
||||||
|
CargoTargetDir = The path to the directory for the build artifacts, i.e.
|
||||||
|
"target".
|
||||||
|
CargoProfile = The cargo profile used to build the binaries
|
||||||
|
(usually "debug" or "release").
|
||||||
|
Version = The version for the installer. The default is the
|
||||||
|
"Major.Minor.Fix" semantic versioning number of the Rust
|
||||||
|
package.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Please do not remove these pre-processor If-Else blocks. These are used with
|
||||||
|
the `cargo wix` subcommand to automatically determine the installation
|
||||||
|
destination for 32-bit versus 64-bit installers. Removal of these lines will
|
||||||
|
cause installation errors.
|
||||||
|
-->
|
||||||
|
<?if $(sys.BUILDARCH) = x64 or $(sys.BUILDARCH) = arm64 ?>
|
||||||
|
<?define PlatformProgramFilesFolder = "ProgramFiles64Folder" ?>
|
||||||
|
<?else ?>
|
||||||
|
<?define PlatformProgramFilesFolder = "ProgramFilesFolder" ?>
|
||||||
|
<?endif ?>
|
||||||
|
|
||||||
|
<Wix xmlns='http://schemas.microsoft.com/wix/2006/wi'>
|
||||||
|
|
||||||
|
<Product
|
||||||
|
Id='*'
|
||||||
|
Name='terrafier-cli'
|
||||||
|
UpgradeCode='351D0A14-B7C2-412B-BEB5-BD394ED35C94'
|
||||||
|
Manufacturer='Loki'
|
||||||
|
Language='1033'
|
||||||
|
Codepage='1252'
|
||||||
|
Version='$(var.Version)'>
|
||||||
|
|
||||||
|
<Package Id='*'
|
||||||
|
Keywords='Installer'
|
||||||
|
Description='CLI tool for Terrafier — create, edit, export Minecraft worlds from the command line'
|
||||||
|
Manufacturer='Loki'
|
||||||
|
InstallerVersion='450'
|
||||||
|
Languages='1033'
|
||||||
|
Compressed='yes'
|
||||||
|
InstallScope='perMachine'
|
||||||
|
SummaryCodepage='1252'
|
||||||
|
/>
|
||||||
|
|
||||||
|
<MajorUpgrade
|
||||||
|
Schedule='afterInstallInitialize'
|
||||||
|
DowngradeErrorMessage='A newer version of [ProductName] is already installed. Setup will now exit.'/>
|
||||||
|
|
||||||
|
<Media Id='1' Cabinet='media1.cab' EmbedCab='yes' DiskPrompt='CD-ROM #1'/>
|
||||||
|
<Property Id='DiskPrompt' Value='terrafier-cli Installation'/>
|
||||||
|
|
||||||
|
<Directory Id='TARGETDIR' Name='SourceDir'>
|
||||||
|
<Directory Id='$(var.PlatformProgramFilesFolder)' Name='PFiles'>
|
||||||
|
<Directory Id='APPLICATIONFOLDER' Name='terrafier-cli'>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Enabling the license sidecar file in the installer is a four step process:
|
||||||
|
|
||||||
|
1. Uncomment the `Component` tag and its contents.
|
||||||
|
2. Change the value for the `Source` attribute in the `File` tag to a path
|
||||||
|
to the file that should be included as the license sidecar file. The path
|
||||||
|
can, and probably should be, relative to this file.
|
||||||
|
3. Change the value for the `Name` attribute in the `File` tag to the
|
||||||
|
desired name for the file when it is installed alongside the `bin` folder
|
||||||
|
in the installation directory. This can be omitted if the desired name is
|
||||||
|
the same as the file name.
|
||||||
|
4. Uncomment the `ComponentRef` tag with the Id attribute value of "License"
|
||||||
|
further down in this file.
|
||||||
|
-->
|
||||||
|
<!--
|
||||||
|
<Component Id='License' Guid='*'>
|
||||||
|
<File Id='LicenseFile' Name='ChangeMe' DiskId='1' Source='C:\Path\To\File' KeyPath='yes'/>
|
||||||
|
</Component>
|
||||||
|
-->
|
||||||
|
|
||||||
|
<Directory Id='Bin' Name='bin'>
|
||||||
|
<Component Id='Path' Guid='C4FB2FF3-085A-4CDF-B823-B0A2EB5B8220' KeyPath='yes'>
|
||||||
|
<Environment
|
||||||
|
Id='PATH'
|
||||||
|
Name='PATH'
|
||||||
|
Value='[Bin]'
|
||||||
|
Permanent='no'
|
||||||
|
Part='last'
|
||||||
|
Action='set'
|
||||||
|
System='yes'/>
|
||||||
|
</Component>
|
||||||
|
<Component Id='binary0' Guid='*'>
|
||||||
|
<File
|
||||||
|
Id='exe0'
|
||||||
|
Name='terrafier-cli.exe'
|
||||||
|
DiskId='1'
|
||||||
|
Source='$(var.CargoTargetBinDir)\terrafier-cli.exe'
|
||||||
|
KeyPath='yes'/>
|
||||||
|
</Component>
|
||||||
|
</Directory>
|
||||||
|
</Directory>
|
||||||
|
</Directory>
|
||||||
|
</Directory>
|
||||||
|
|
||||||
|
<Feature
|
||||||
|
Id='Binaries'
|
||||||
|
Title='Application'
|
||||||
|
Description='Installs all binaries and the license.'
|
||||||
|
Level='1'
|
||||||
|
ConfigurableDirectory='APPLICATIONFOLDER'
|
||||||
|
AllowAdvertise='no'
|
||||||
|
Display='expand'
|
||||||
|
Absent='disallow'>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Uncomment the following `ComponentRef` tag to add the license
|
||||||
|
sidecar file to the installer.
|
||||||
|
-->
|
||||||
|
<!--<ComponentRef Id='License'/>-->
|
||||||
|
|
||||||
|
<ComponentRef Id='binary0'/>
|
||||||
|
|
||||||
|
<Feature
|
||||||
|
Id='Environment'
|
||||||
|
Title='PATH Environment Variable'
|
||||||
|
Description='Add the install location of the [ProductName] executable to the PATH system environment variable. This allows the [ProductName] executable to be called from any location.'
|
||||||
|
Level='1'
|
||||||
|
Absent='allow'>
|
||||||
|
<ComponentRef Id='Path'/>
|
||||||
|
</Feature>
|
||||||
|
</Feature>
|
||||||
|
|
||||||
|
<SetProperty Id='ARPINSTALLLOCATION' Value='[APPLICATIONFOLDER]' After='CostFinalize'/>
|
||||||
|
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Uncomment the following `Icon` and `Property` tags to change the product icon.
|
||||||
|
|
||||||
|
The product icon is the graphic that appears in the Add/Remove
|
||||||
|
Programs control panel for the application.
|
||||||
|
-->
|
||||||
|
<!--<Icon Id='ProductICO' SourceFile='wix\Product.ico'/>-->
|
||||||
|
<!--<Property Id='ARPPRODUCTICON' Value='ProductICO' />-->
|
||||||
|
|
||||||
|
<Property Id='ARPHELPLINK' Value='https://github.com/loki5512344/Terrafier'/>
|
||||||
|
|
||||||
|
<UI>
|
||||||
|
<UIRef Id='WixUI_FeatureTree'/>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Enabling the EULA dialog in the installer is a three step process:
|
||||||
|
|
||||||
|
1. Comment out or remove the two `Publish` tags that follow the
|
||||||
|
`WixVariable` tag.
|
||||||
|
2. Uncomment the `<WixVariable Id='WixUILicenseRtf' Value='Path\to\Eula.rft'>` tag further down
|
||||||
|
3. Replace the `Value` attribute of the `WixVariable` tag with
|
||||||
|
the path to a RTF file that will be used as the EULA and
|
||||||
|
displayed in the license agreement dialog.
|
||||||
|
-->
|
||||||
|
<Publish Dialog='WelcomeDlg' Control='Next' Event='NewDialog' Value='CustomizeDlg' Order='99'>1</Publish>
|
||||||
|
<Publish Dialog='CustomizeDlg' Control='Back' Event='NewDialog' Value='WelcomeDlg' Order='99'>1</Publish>
|
||||||
|
|
||||||
|
</UI>
|
||||||
|
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Enabling the EULA dialog in the installer requires uncommenting
|
||||||
|
the following `WixUILicenseRTF` tag and changing the `Value`
|
||||||
|
attribute.
|
||||||
|
-->
|
||||||
|
<!-- <WixVariable Id='WixUILicenseRtf' Value='Relative\Path\to\Eula.rtf'/> -->
|
||||||
|
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Uncomment the next `WixVariable` tag to customize the installer's
|
||||||
|
Graphical User Interface (GUI) and add a custom banner image across
|
||||||
|
the top of each screen. See the WiX Toolset documentation for details
|
||||||
|
about customization.
|
||||||
|
|
||||||
|
The banner BMP dimensions are 493 x 58 pixels.
|
||||||
|
-->
|
||||||
|
<!--<WixVariable Id='WixUIBannerBmp' Value='wix\Banner.bmp'/>-->
|
||||||
|
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Uncomment the next `WixVariable` tag to customize the installer's
|
||||||
|
Graphical User Interface (GUI) and add a custom image to the first
|
||||||
|
dialog, or screen. See the WiX Toolset documentation for details about
|
||||||
|
customization.
|
||||||
|
|
||||||
|
The dialog BMP dimensions are 493 x 312 pixels.
|
||||||
|
-->
|
||||||
|
<!--<WixVariable Id='WixUIDialogBmp' Value='wix\Dialog.bmp'/>-->
|
||||||
|
|
||||||
|
</Product>
|
||||||
|
|
||||||
|
</Wix>
|
||||||
|
|
@ -73,7 +73,10 @@ fn terrafier_biome_name(id: u8) -> &'static str {
|
||||||
arr
|
arr
|
||||||
});
|
});
|
||||||
|
|
||||||
names.get(id as usize).copied().unwrap_or("minecraft:plains")
|
names
|
||||||
|
.get(id as usize)
|
||||||
|
.copied()
|
||||||
|
.unwrap_or("minecraft:plains")
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct BiomeLayerExport;
|
pub struct BiomeLayerExport;
|
||||||
|
|
|
||||||
|
|
@ -57,10 +57,10 @@ pub fn biome_name(
|
||||||
"minecraft:plains"
|
"minecraft:plains"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mod biome_res;
|
||||||
mod caves_frost;
|
mod caves_frost;
|
||||||
mod river_trees;
|
mod river_trees;
|
||||||
mod biome_res;
|
|
||||||
|
|
||||||
|
pub use biome_res::{BiomeLayerExport, ResourcesLayerExport};
|
||||||
pub use caves_frost::{CavesLayerExport, FrostLayerExport};
|
pub use caves_frost::{CavesLayerExport, FrostLayerExport};
|
||||||
pub use river_trees::{RiverLayerExport, TreesLayerExport};
|
pub use river_trees::{RiverLayerExport, TreesLayerExport};
|
||||||
pub use biome_res::{BiomeLayerExport, ResourcesLayerExport};
|
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
use std::collections::{BTreeSet, HashMap};
|
|
||||||
use crate::io::layer_export::{apply_layers, biome_name, LayerExport};
|
|
||||||
use crate::io::minecraft::{data_version, Result};
|
|
||||||
use crate::model::tile::Tile;
|
|
||||||
use super::blocks::{bits_needed, pack_indices, BLOCK_SET, BLOCK_SET_EXTENDED};
|
|
||||||
use super::block_name;
|
use super::block_name;
|
||||||
|
use super::blocks::{BLOCK_SET, BLOCK_SET_EXTENDED, bits_needed, pack_indices};
|
||||||
|
use crate::io::layer_export::{LayerExport, apply_layers, biome_name};
|
||||||
|
use crate::io::minecraft::{Result, data_version};
|
||||||
|
use crate::model::tile::Tile;
|
||||||
|
use std::collections::{BTreeSet, HashMap};
|
||||||
|
|
||||||
fn make_section(
|
fn make_section(
|
||||||
sec_y: i32,
|
sec_y: i32,
|
||||||
|
|
@ -12,7 +12,10 @@ fn make_section(
|
||||||
) -> terrafier_nbt::Tag {
|
) -> terrafier_nbt::Tag {
|
||||||
let mut sec = HashMap::new();
|
let mut sec = HashMap::new();
|
||||||
sec.insert("Y".into(), terrafier_nbt::Tag::Byte(sec_y as i8));
|
sec.insert("Y".into(), terrafier_nbt::Tag::Byte(sec_y as i8));
|
||||||
sec.insert("block_states".into(), terrafier_nbt::Tag::Compound(block_states));
|
sec.insert(
|
||||||
|
"block_states".into(),
|
||||||
|
terrafier_nbt::Tag::Compound(block_states),
|
||||||
|
);
|
||||||
sec.insert("biomes".into(), terrafier_nbt::Tag::Compound(biomes));
|
sec.insert("biomes".into(), terrafier_nbt::Tag::Compound(biomes));
|
||||||
terrafier_nbt::Tag::Compound(sec)
|
terrafier_nbt::Tag::Compound(sec)
|
||||||
}
|
}
|
||||||
|
|
@ -38,7 +41,10 @@ pub(super) fn build_chunk_nbt(
|
||||||
let mut compound = HashMap::new();
|
let mut compound = HashMap::new();
|
||||||
compound.insert("xPos".into(), terrafier_nbt::Tag::Int(chunk_x));
|
compound.insert("xPos".into(), terrafier_nbt::Tag::Int(chunk_x));
|
||||||
compound.insert("zPos".into(), terrafier_nbt::Tag::Int(chunk_z));
|
compound.insert("zPos".into(), terrafier_nbt::Tag::Int(chunk_z));
|
||||||
compound.insert("DataVersion".into(), terrafier_nbt::Tag::Int(data_version()));
|
compound.insert(
|
||||||
|
"DataVersion".into(),
|
||||||
|
terrafier_nbt::Tag::Int(data_version()),
|
||||||
|
);
|
||||||
compound.insert("Status".into(), terrafier_nbt::Tag::String("full".into()));
|
compound.insert("Status".into(), terrafier_nbt::Tag::String("full".into()));
|
||||||
let min_sec = (tile.min_height as i32 >> 4).max(-4);
|
let min_sec = (tile.min_height as i32 >> 4).max(-4);
|
||||||
let max_sec = (tile.max_height as i32 >> 4).min(20);
|
let max_sec = (tile.max_height as i32 >> 4).min(20);
|
||||||
|
|
@ -104,7 +110,10 @@ pub(super) fn build_chunk_nbt_with_layers(
|
||||||
let mut compound = HashMap::new();
|
let mut compound = HashMap::new();
|
||||||
compound.insert("xPos".into(), terrafier_nbt::Tag::Int(chunk_x));
|
compound.insert("xPos".into(), terrafier_nbt::Tag::Int(chunk_x));
|
||||||
compound.insert("zPos".into(), terrafier_nbt::Tag::Int(chunk_z));
|
compound.insert("zPos".into(), terrafier_nbt::Tag::Int(chunk_z));
|
||||||
compound.insert("DataVersion".into(), terrafier_nbt::Tag::Int(data_version()));
|
compound.insert(
|
||||||
|
"DataVersion".into(),
|
||||||
|
terrafier_nbt::Tag::Int(data_version()),
|
||||||
|
);
|
||||||
compound.insert("Status".into(), terrafier_nbt::Tag::String("full".into()));
|
compound.insert("Status".into(), terrafier_nbt::Tag::String("full".into()));
|
||||||
let min_sec = (tile.min_height as i32 >> 4).max(-4);
|
let min_sec = (tile.min_height as i32 >> 4).max(-4);
|
||||||
let max_sec = (tile.max_height as i32 >> 4).min(20);
|
let max_sec = (tile.max_height as i32 >> 4).min(20);
|
||||||
|
|
@ -125,7 +134,12 @@ pub(super) fn build_chunk_nbt_with_layers(
|
||||||
let terrain_id = tile.terrain[tile_lz * 128 + tile_lx];
|
let terrain_id = tile.terrain[tile_lz * 128 + tile_lx];
|
||||||
let base_name = block_name(terrain_id, global_y, surface_y);
|
let base_name = block_name(terrain_id, global_y, surface_y);
|
||||||
let name = apply_layers(
|
let name = apply_layers(
|
||||||
tile, tile_lx, tile_lz, global_y, surface_y, base_name,
|
tile,
|
||||||
|
tile_lx,
|
||||||
|
tile_lz,
|
||||||
|
global_y,
|
||||||
|
surface_y,
|
||||||
|
base_name,
|
||||||
layer_exporters,
|
layer_exporters,
|
||||||
);
|
);
|
||||||
BLOCK_SET_EXTENDED
|
BLOCK_SET_EXTENDED
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,6 @@ mod blocks;
|
||||||
mod chunk;
|
mod chunk;
|
||||||
mod writer;
|
mod writer;
|
||||||
|
|
||||||
|
pub(crate) use blocks::block_name;
|
||||||
pub use writer::save_world;
|
pub use writer::save_world;
|
||||||
pub use writer::save_world_with_layers;
|
pub use writer::save_world_with_layers;
|
||||||
pub(crate) use blocks::block_name;
|
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ use std::fs;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
use crate::io::layer_export::LayerExport;
|
use crate::io::layer_export::LayerExport;
|
||||||
use crate::io::minecraft::{data_version, Result};
|
use crate::io::minecraft::{Result, data_version};
|
||||||
use crate::model::tile::Tile;
|
use crate::model::tile::Tile;
|
||||||
use crate::model::world::World;
|
use crate::model::world::World;
|
||||||
|
|
||||||
|
|
@ -47,8 +47,7 @@ pub fn save_world(world: &World, output_path: &Path) -> Result<()> {
|
||||||
let region_local_x = (chunk_x & 31) as u8;
|
let region_local_x = (chunk_x & 31) as u8;
|
||||||
let region_local_z = (chunk_z & 31) as u8;
|
let region_local_z = (chunk_z & 31) as u8;
|
||||||
|
|
||||||
let chunk_data =
|
let chunk_data = build_chunk_nbt(chunk_x, chunk_z, tile, chunk_lx, chunk_lz)?;
|
||||||
build_chunk_nbt(chunk_x, chunk_z, tile, chunk_lx, chunk_lz)?;
|
|
||||||
|
|
||||||
if chunk_data.is_empty() {
|
if chunk_data.is_empty() {
|
||||||
continue;
|
continue;
|
||||||
|
|
|
||||||
|
|
@ -2,12 +2,12 @@ mod export;
|
||||||
mod reader;
|
mod reader;
|
||||||
mod version;
|
mod version;
|
||||||
|
|
||||||
pub use reader::load_save;
|
|
||||||
pub use reader::discover_region_files;
|
|
||||||
pub use export::save_world;
|
pub use export::save_world;
|
||||||
pub use export::save_world_with_layers;
|
pub use export::save_world_with_layers;
|
||||||
pub use version::version_from_data_version;
|
pub use reader::discover_region_files;
|
||||||
|
pub use reader::load_save;
|
||||||
pub(crate) use version::data_version;
|
pub(crate) use version::data_version;
|
||||||
|
pub use version::version_from_data_version;
|
||||||
|
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -51,7 +51,19 @@ pub fn load_save(path: &Path) -> Result<World> {
|
||||||
let region_bytes = fs::read(&path)?;
|
let region_bytes = fs::read(&path)?;
|
||||||
let region = Region::from_bytes(rx, rz, ®ion_bytes)?;
|
let region = Region::from_bytes(rx, rz, ®ion_bytes)?;
|
||||||
for (local_x, local_z) in region.chunk_coords() {
|
for (local_x, local_z) in region.chunk_coords() {
|
||||||
let chunk_data = region.get_chunk_data(local_x, local_z).unwrap();
|
let chunk_data = match region.get_chunk_data(local_x, local_z) {
|
||||||
|
Some(data) => data,
|
||||||
|
None => {
|
||||||
|
log::warn!(
|
||||||
|
"corrupted chunk data at ({}, {}) in region ({}, {})",
|
||||||
|
local_x,
|
||||||
|
local_z,
|
||||||
|
rx,
|
||||||
|
rz
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
if let Ok(chunk_tag) = read_gzip(chunk_data)
|
if let Ok(chunk_tag) = read_gzip(chunk_data)
|
||||||
&& let Some(chunk) =
|
&& let Some(chunk) =
|
||||||
terrafier_fastanvil::io::chunk::Chunk::from_nbt(&chunk_tag)
|
terrafier_fastanvil::io::chunk::Chunk::from_nbt(&chunk_tag)
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,9 @@ pub type Result<T> = std::result::Result<T, BinaryError>;
|
||||||
|
|
||||||
/// Save a `World` to a binary file.
|
/// Save a `World` to a binary file.
|
||||||
pub fn save_binary(world: &World, path: &Path) -> Result<()> {
|
pub fn save_binary(world: &World, path: &Path) -> Result<()> {
|
||||||
let bytes = bincode::serialize(world).map_err(|e| BinaryError::Serialize(e.to_string()))?;
|
let config = bincode::config::standard();
|
||||||
|
let bytes =
|
||||||
|
bincode::serde::encode_to_vec(world, config).map_err(|e| BinaryError::Serialize(e.to_string()))?;
|
||||||
|
|
||||||
std::fs::write(path, &bytes)?;
|
std::fs::write(path, &bytes)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
@ -35,8 +37,10 @@ pub fn save_binary(world: &World, path: &Path) -> Result<()> {
|
||||||
/// Load a `World` from a binary file.
|
/// Load a `World` from a binary file.
|
||||||
pub fn load_binary(path: &Path) -> Result<World> {
|
pub fn load_binary(path: &Path) -> Result<World> {
|
||||||
let bytes = std::fs::read(path)?;
|
let bytes = std::fs::read(path)?;
|
||||||
let world: World =
|
let config = bincode::config::standard();
|
||||||
bincode::deserialize(&bytes).map_err(|e| BinaryError::Deserialize(e.to_string()))?;
|
let (world, _): (World, usize) =
|
||||||
|
bincode::serde::decode_from_slice(&bytes, config)
|
||||||
|
.map_err(|e| BinaryError::Deserialize(e.to_string()))?;
|
||||||
Ok(world)
|
Ok(world)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
use super::types::{
|
use super::types::{
|
||||||
ClassDesc, JvmValue, BASE_WIRE_HANDLE, TC_ARRAY, TC_BLOCKDATA, TC_BLOCKDATALONG, TC_CLASS,
|
BASE_WIRE_HANDLE, ClassDesc, JvmValue, TC_ARRAY, TC_BLOCKDATA, TC_BLOCKDATALONG, TC_CLASS,
|
||||||
TC_ENUM, TC_LONGSTRING, TC_NULL, TC_OBJECT, TC_REFERENCE, TC_RESET, TC_STRING,
|
TC_ENUM, TC_LONGSTRING, TC_NULL, TC_OBJECT, TC_REFERENCE, TC_RESET, TC_STRING,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -15,8 +15,8 @@ pub use extract::*;
|
||||||
pub use types::WpImportError;
|
pub use types::WpImportError;
|
||||||
|
|
||||||
use crate::model::dimension::Dimension;
|
use crate::model::dimension::Dimension;
|
||||||
use crate::model::types::Platform;
|
|
||||||
use crate::model::tile::Tile;
|
use crate::model::tile::Tile;
|
||||||
|
use crate::model::types::Platform;
|
||||||
use crate::model::world::World;
|
use crate::model::world::World;
|
||||||
|
|
||||||
use java_reader::JvmStream;
|
use java_reader::JvmStream;
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
use super::super::java_reader::JvmStream;
|
||||||
use super::java_types::{
|
use super::java_types::{
|
||||||
ClassDesc, FieldInfo, JvmValue, BASE_WIRE_HANDLE, TC_ARRAY, TC_BLOCKDATA, TC_BLOCKDATALONG,
|
BASE_WIRE_HANDLE, ClassDesc, FieldInfo, JvmValue, TC_ARRAY, TC_BLOCKDATA, TC_BLOCKDATALONG,
|
||||||
TC_CLASS, TC_CLASSDESC, TC_ENDBLOCKDATA, TC_ENUM, TC_LONGSTRING, TC_NULL, TC_OBJECT,
|
TC_CLASS, TC_CLASSDESC, TC_ENDBLOCKDATA, TC_ENUM, TC_LONGSTRING, TC_NULL, TC_OBJECT,
|
||||||
TC_PROXYCLASSDESC, TC_REFERENCE, TC_RESET, TC_STRING,
|
TC_PROXYCLASSDESC, TC_REFERENCE, TC_RESET, TC_STRING,
|
||||||
};
|
};
|
||||||
use super::super::java_reader::JvmStream;
|
|
||||||
|
|
||||||
impl JvmStream {
|
impl JvmStream {
|
||||||
pub fn read_classdesc(&mut self) -> std::io::Result<JvmValue> {
|
pub fn read_classdesc(&mut self) -> std::io::Result<JvmValue> {
|
||||||
|
|
@ -124,10 +124,7 @@ impl JvmStream {
|
||||||
other => {
|
other => {
|
||||||
return Err(std::io::Error::new(
|
return Err(std::io::Error::new(
|
||||||
std::io::ErrorKind::InvalidData,
|
std::io::ErrorKind::InvalidData,
|
||||||
format!(
|
format!("unexpected token in class annotations: 0x{:02x}", other),
|
||||||
"unexpected token in class annotations: 0x{:02x}",
|
|
||||||
other
|
|
||||||
),
|
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -182,5 +179,4 @@ impl JvmStream {
|
||||||
self.handles[(handle - BASE_WIRE_HANDLE) as usize] = obj.clone();
|
self.handles[(handle - BASE_WIRE_HANDLE) as usize] = obj.clone();
|
||||||
Ok(obj)
|
Ok(obj)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
use super::super::java_reader::JvmStream;
|
||||||
use super::java_types::{
|
use super::java_types::{
|
||||||
ClassDesc, FieldInfo, JvmValue, PRIM_BOOL, PRIM_BYTE, PRIM_CHAR, PRIM_DOUBLE, PRIM_FLOAT,
|
ClassDesc, FieldInfo, JvmValue, PRIM_BOOL, PRIM_BYTE, PRIM_CHAR, PRIM_DOUBLE, PRIM_FLOAT,
|
||||||
PRIM_INT, PRIM_LONG, PRIM_SHORT, SC_WRITE_METHOD, TC_BLOCKDATA, TC_BLOCKDATALONG,
|
PRIM_INT, PRIM_LONG, PRIM_SHORT, SC_WRITE_METHOD, TC_BLOCKDATA, TC_BLOCKDATALONG,
|
||||||
};
|
};
|
||||||
use super::super::java_reader::JvmStream;
|
|
||||||
|
|
||||||
impl JvmStream {
|
impl JvmStream {
|
||||||
pub fn read_object_fields(&mut self, cd: &ClassDesc) -> std::io::Result<JvmValue> {
|
pub fn read_object_fields(&mut self, cd: &ClassDesc) -> std::io::Result<JvmValue> {
|
||||||
|
|
|
||||||
|
|
@ -14,8 +14,8 @@ pub use coords::{BlockCoords, ChunkCoords, RegionCoords, TileCoords};
|
||||||
pub use io::export;
|
pub use io::export;
|
||||||
pub use io::import;
|
pub use io::import;
|
||||||
pub use model::dimension::Dimension;
|
pub use model::dimension::Dimension;
|
||||||
pub use model::types::{Platform, Terrain};
|
|
||||||
pub use model::tile::Tile;
|
pub use model::tile::Tile;
|
||||||
|
pub use model::types::{Platform, Terrain};
|
||||||
pub use model::world::World;
|
pub use model::world::World;
|
||||||
|
|
||||||
pub use terrafier_biome_db as biome_db;
|
pub use terrafier_biome_db as biome_db;
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,11 @@ impl World {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create a new world using a custom height map source.
|
/// Create a new world using a custom height map source.
|
||||||
pub fn with_heightmap(name: &str, seed: u64, heightmap: impl HeightMapSource + 'static) -> Self {
|
pub fn with_heightmap(
|
||||||
|
name: &str,
|
||||||
|
seed: u64,
|
||||||
|
heightmap: impl HeightMapSource + 'static,
|
||||||
|
) -> Self {
|
||||||
let platform = Platform::java_1_18();
|
let platform = Platform::java_1_18();
|
||||||
|
|
||||||
let coords: Vec<(i32, i32)> = (-1..=1)
|
let coords: Vec<(i32, i32)> = (-1..=1)
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
//! Filter system — restrict which cells an operation affects based on terrain, height, or slope.
|
//! Filter system — restrict which cells an operation affects based on terrain, height, or slope.
|
||||||
use std::sync::OnceLock;
|
|
||||||
use crate::model::dimension::Dimension;
|
use crate::model::dimension::Dimension;
|
||||||
|
use crate::model::tile::{TILE_SIZE, Tile};
|
||||||
use crate::model::types::Terrain;
|
use crate::model::types::Terrain;
|
||||||
use crate::model::tile::{Tile, TILE_SIZE};
|
|
||||||
use crate::ops::operations::{Operation, OperationError, RestoreHeightsOperation};
|
use crate::ops::operations::{Operation, OperationError, RestoreHeightsOperation};
|
||||||
|
use std::sync::OnceLock;
|
||||||
/// Determines whether a cell should be modified by an operation.
|
/// Determines whether a cell should be modified by an operation.
|
||||||
pub trait Filter: Send + Sync {
|
pub trait Filter: Send + Sync {
|
||||||
fn name(&self) -> &'static str;
|
fn name(&self) -> &'static str;
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
//! Height map sources — generate height maps from noise, flat, or combined sources.
|
//! Height map sources — generate height maps from noise, flat, or combined sources.
|
||||||
|
|
||||||
use crate::model::types::Terrain;
|
|
||||||
use crate::model::tile::Tile;
|
use crate::model::tile::Tile;
|
||||||
|
use crate::model::types::Terrain;
|
||||||
|
|
||||||
/// A source that can generate a height map for a tile.
|
/// A source that can generate a height map for a tile.
|
||||||
pub trait HeightMapSource: Send + Sync {
|
pub trait HeightMapSource: Send + Sync {
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,9 @@ fn tile_hash(tile_x: i32, tile_z: i32, seed: u64) -> u64 {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn pseudo_rand(state: &mut u64) -> u64 {
|
fn pseudo_rand(state: &mut u64) -> u64 {
|
||||||
*state = state.wrapping_mul(6364136223846793005).wrapping_add(1442695040888963407);
|
*state = state
|
||||||
|
.wrapping_mul(6364136223846793005)
|
||||||
|
.wrapping_add(1442695040888963407);
|
||||||
*state >> 33
|
*state >> 33
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -34,9 +36,7 @@ impl crate::ops::layers::LayerGenerator for TreesGenerator {
|
||||||
for idx in 0..total {
|
for idx in 0..total {
|
||||||
let terrain_type = terrain[idx] as usize;
|
let terrain_type = terrain[idx] as usize;
|
||||||
bytes[idx] = match terrain_type {
|
bytes[idx] = match terrain_type {
|
||||||
t if t == Terrain::Forest as usize => {
|
t if t == Terrain::Forest as usize => (pseudo_rand(&mut rng) % 60).max(20) as u8,
|
||||||
(pseudo_rand(&mut rng) % 60).max(20) as u8
|
|
||||||
}
|
|
||||||
t if t == Terrain::Grass as usize && pseudo_rand(&mut rng) % 100 < 30 => {
|
t if t == Terrain::Grass as usize && pseudo_rand(&mut rng) % 100 < 30 => {
|
||||||
(pseudo_rand(&mut rng) % 20 + 5) as u8
|
(pseudo_rand(&mut rng) % 20 + 5) as u8
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
use std::sync::{Arc, OnceLock};
|
|
||||||
use crate::model::types::Brush;
|
|
||||||
use crate::model::dimension::Dimension;
|
use crate::model::dimension::Dimension;
|
||||||
use crate::model::types::Terrain;
|
|
||||||
use crate::model::tile::TILE_SIZE;
|
use crate::model::tile::TILE_SIZE;
|
||||||
|
use crate::model::types::Brush;
|
||||||
|
use crate::model::types::Terrain;
|
||||||
use crate::ops::operations::{Operation, OperationError, RestoreTerrainOperation};
|
use crate::ops::operations::{Operation, OperationError, RestoreTerrainOperation};
|
||||||
|
use std::sync::{Arc, OnceLock};
|
||||||
|
|
||||||
struct BrushApply<'a> {
|
struct BrushApply<'a> {
|
||||||
tile: &'a mut crate::model::tile::Tile,
|
tile: &'a mut crate::model::tile::Tile,
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,8 @@ use std::collections::VecDeque;
|
||||||
use std::sync::OnceLock;
|
use std::sync::OnceLock;
|
||||||
|
|
||||||
use crate::model::dimension::Dimension;
|
use crate::model::dimension::Dimension;
|
||||||
use crate::model::types::Terrain;
|
|
||||||
use crate::model::tile::TILE_SIZE;
|
use crate::model::tile::TILE_SIZE;
|
||||||
|
use crate::model::types::Terrain;
|
||||||
|
|
||||||
use super::{Operation, OperationError, RestoreTerrainOperation};
|
use super::{Operation, OperationError, RestoreTerrainOperation};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -18,12 +18,14 @@ pub enum OperationError {
|
||||||
}
|
}
|
||||||
|
|
||||||
mod brush;
|
mod brush;
|
||||||
mod shaping;
|
|
||||||
mod fill;
|
mod fill;
|
||||||
|
mod shaping;
|
||||||
|
|
||||||
pub use brush::{FloodOperation, PaintOperation, PencilOperation};
|
pub use brush::{FloodOperation, PaintOperation, PencilOperation};
|
||||||
pub use shaping::{ErodeOperation, FlattenOperation, HeightOperation, MultiTileOperation, SmoothOperation};
|
|
||||||
pub use fill::FillOperation;
|
pub use fill::FillOperation;
|
||||||
|
pub use shaping::{
|
||||||
|
ErodeOperation, FlattenOperation, HeightOperation, MultiTileOperation, SmoothOperation,
|
||||||
|
};
|
||||||
|
|
||||||
/// Restores heights from a saved snapshot (used as inverse of FlattenOperation).
|
/// Restores heights from a saved snapshot (used as inverse of FlattenOperation).
|
||||||
pub struct RestoreHeightsOperation {
|
pub struct RestoreHeightsOperation {
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::sync::OnceLock;
|
use std::sync::OnceLock;
|
||||||
|
|
||||||
use crate::model::types::Brush;
|
|
||||||
use crate::model::dimension::Dimension;
|
use crate::model::dimension::Dimension;
|
||||||
use crate::model::tile::TILE_SIZE;
|
use crate::model::tile::TILE_SIZE;
|
||||||
|
use crate::model::types::Brush;
|
||||||
|
|
||||||
use crate::ops::operations::{Operation, OperationError, RestoreHeightsOperation};
|
use crate::ops::operations::{Operation, OperationError, RestoreHeightsOperation};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::sync::OnceLock;
|
use std::sync::OnceLock;
|
||||||
|
|
||||||
use crate::model::types::Brush;
|
|
||||||
use crate::model::dimension::Dimension;
|
use crate::model::dimension::Dimension;
|
||||||
use crate::model::tile::TILE_SIZE;
|
use crate::model::tile::TILE_SIZE;
|
||||||
|
use crate::model::types::Brush;
|
||||||
|
|
||||||
use crate::ops::operations::{Operation, OperationError, RestoreHeightsOperation};
|
use crate::ops::operations::{Operation, OperationError, RestoreHeightsOperation};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
|
mod erode;
|
||||||
mod height;
|
mod height;
|
||||||
mod smooth;
|
mod smooth;
|
||||||
mod erode;
|
pub use erode::*;
|
||||||
pub use height::*;
|
pub use height::*;
|
||||||
pub use smooth::*;
|
pub use smooth::*;
|
||||||
pub use erode::*;
|
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::sync::OnceLock;
|
use std::sync::OnceLock;
|
||||||
|
|
||||||
use crate::model::types::Brush;
|
|
||||||
use crate::model::dimension::Dimension;
|
use crate::model::dimension::Dimension;
|
||||||
use crate::model::tile::TILE_SIZE;
|
use crate::model::tile::TILE_SIZE;
|
||||||
|
use crate::model::types::Brush;
|
||||||
|
|
||||||
use crate::ops::operations::{Operation, OperationError, RestoreHeightsOperation};
|
use crate::ops::operations::{Operation, OperationError, RestoreHeightsOperation};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
//! Plugin host — trait-объекты для расширения функциональности.
|
//! Plugin host — trait-объекты для расширения функциональности.
|
||||||
|
|
||||||
mod traits;
|
|
||||||
pub mod registry;
|
pub mod registry;
|
||||||
|
mod traits;
|
||||||
|
|
||||||
pub use traits::{ExportPlugin, LayerPlugin, OperationPlugin, TileSourcePlugin};
|
|
||||||
pub use registry::PluginRegistry;
|
pub use registry::PluginRegistry;
|
||||||
|
pub use traits::{ExportPlugin, LayerPlugin, OperationPlugin, TileSourcePlugin};
|
||||||
|
|
|
||||||
|
|
@ -162,6 +162,4 @@ impl Region {
|
||||||
pub fn chunk_count(&self) -> usize {
|
pub fn chunk_count(&self) -> usize {
|
||||||
self.chunks.len()
|
self.chunks.len()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
10
crates/i18n/Cargo.toml
Normal file
10
crates/i18n/Cargo.toml
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
[package]
|
||||||
|
name = "terrafier-i18n"
|
||||||
|
version.workspace = true
|
||||||
|
edition.workspace = true
|
||||||
|
license.workspace = true
|
||||||
|
description = "Internationalization for Terrafier"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
serde = { workspace = true, features = ["derive"] }
|
||||||
|
serde_json.workspace = true
|
||||||
8
crates/i18n/src/lang.rs
Normal file
8
crates/i18n/src/lang.rs
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
#[repr(u8)]
|
||||||
|
pub enum Language {
|
||||||
|
English = 0,
|
||||||
|
Russian = 1,
|
||||||
|
}
|
||||||
116
crates/i18n/src/lib.rs
Normal file
116
crates/i18n/src/lib.rs
Normal file
|
|
@ -0,0 +1,116 @@
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::sync::atomic::{AtomicU8, Ordering};
|
||||||
|
use std::sync::LazyLock;
|
||||||
|
|
||||||
|
mod lang;
|
||||||
|
pub use lang::Language;
|
||||||
|
|
||||||
|
// Language code: 0 = English, 1 = Russian
|
||||||
|
static CURRENT_LANG: AtomicU8 = AtomicU8::new(0);
|
||||||
|
|
||||||
|
// Embedded translations
|
||||||
|
type TransMap = HashMap<String, String>;
|
||||||
|
|
||||||
|
static EN: LazyLock<TransMap> = LazyLock::new(|| {
|
||||||
|
serde_json::from_str(include_str!("translations/en.json"))
|
||||||
|
.expect("Failed to parse English translations")
|
||||||
|
});
|
||||||
|
|
||||||
|
static RU: LazyLock<TransMap> = LazyLock::new(|| {
|
||||||
|
serde_json::from_str(include_str!("translations/ru.json"))
|
||||||
|
.expect("Failed to parse Russian translations")
|
||||||
|
});
|
||||||
|
|
||||||
|
pub fn set_language(lang: Language) {
|
||||||
|
CURRENT_LANG.store(lang as u8, Ordering::Relaxed);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn language() -> Language {
|
||||||
|
match CURRENT_LANG.load(Ordering::Relaxed) {
|
||||||
|
1 => Language::Russian,
|
||||||
|
_ => Language::English,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get a translation for the given key.
|
||||||
|
pub fn translate(key: &str) -> &str {
|
||||||
|
let map = match CURRENT_LANG.load(Ordering::Relaxed) {
|
||||||
|
1 => &*RU,
|
||||||
|
_ => &*EN,
|
||||||
|
};
|
||||||
|
map.get(key).map(String::as_str).unwrap_or(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get a translation with variable substitution.
|
||||||
|
/// Variables in the string are `{name}` placeholders.
|
||||||
|
pub fn translate_with(key: &str, vars: &[(&str, &str)]) -> String {
|
||||||
|
let mut s = translate(key).to_string();
|
||||||
|
for (k, v) in vars {
|
||||||
|
s = s.replace(&format!("{{{}}}", k), v);
|
||||||
|
}
|
||||||
|
s
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Macro for simple translation lookup.
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! t {
|
||||||
|
($key:expr) => {
|
||||||
|
$crate::translate($key)
|
||||||
|
};
|
||||||
|
($key:expr, $($var:ident = $val:expr),+ $(,)?) => {{
|
||||||
|
let vars: &[(&str, &str)] = &[$((stringify!($var), &$val as &dyn std::fmt::Display as &str)),+];
|
||||||
|
// This won't work with runtime format. Use translate_with instead.
|
||||||
|
$crate::translate_with($key, &[$((stringify!($var), &$val.as_ref() as &str)),+])
|
||||||
|
}};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Detect system language from environment variables (LANG, LANGUAGE, LC_ALL).
|
||||||
|
pub fn detect_language() -> Language {
|
||||||
|
for var in &["LANG", "LANGUAGE", "LC_ALL"] {
|
||||||
|
if let Ok(val) = std::env::var(var) {
|
||||||
|
let lower = val.to_lowercase();
|
||||||
|
if lower.starts_with("ru") || lower.contains("ru_ru") {
|
||||||
|
return Language::Russian;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Language::English
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Initialize i18n: detect language and set it.
|
||||||
|
pub fn init() {
|
||||||
|
let lang = detect_language();
|
||||||
|
set_language(lang);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_default_language_is_english() {
|
||||||
|
assert_eq!(translate("cli.new.creating"), "Creating new world...");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_russian_language() {
|
||||||
|
set_language(Language::Russian);
|
||||||
|
assert_eq!(translate("cli.new.creating"), "Создание нового мира...");
|
||||||
|
set_language(Language::English);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_missing_key_returns_key() {
|
||||||
|
assert_eq!(translate("nonexistent.key"), "nonexistent.key");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_translate_with_vars() {
|
||||||
|
let result = translate_with("cli.new.created", &[
|
||||||
|
("name", "test"),
|
||||||
|
("tiles", "9"),
|
||||||
|
]);
|
||||||
|
assert!(result.contains("test"));
|
||||||
|
assert!(result.contains("9"));
|
||||||
|
}
|
||||||
|
}
|
||||||
30
crates/i18n/src/translations/en.json
Normal file
30
crates/i18n/src/translations/en.json
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
{
|
||||||
|
"cli.new.creating": "Creating new world...",
|
||||||
|
"cli.new.created": "World \"{name}\" created ({tiles} tiles)",
|
||||||
|
"cli.export.exporting": "Exporting world to \"{path}\"...",
|
||||||
|
"cli.export.complete": "Export complete: {regions} regions, {chunks} chunks",
|
||||||
|
"cli.export.empty": "No chunks to export",
|
||||||
|
"cli.import.importing": "Importing Minecraft save from \"{path}\"...",
|
||||||
|
"cli.import.complete": "Import complete: {tiles} tiles, {dimensions} dimensions",
|
||||||
|
"cli.import.no_regions": "No region files found in \"{path}\"",
|
||||||
|
"cli.render.rendering": "Rendering world preview...",
|
||||||
|
"cli.render.saved": "Preview saved to \"{path}\"",
|
||||||
|
"cli.info.title": "World: {name}",
|
||||||
|
"cli.info.seed": "Seed: {seed}",
|
||||||
|
"cli.info.platform": "Platform: {platform}",
|
||||||
|
"cli.info.tiles": "Tiles: {tiles}",
|
||||||
|
"cli.info.dimensions": "Dimensions: {dimensions}",
|
||||||
|
"cli.validate.ok": "World file is valid",
|
||||||
|
"cli.validate.fail": "World file is invalid: {error}",
|
||||||
|
"cli.error.not_found": "File not found: {path}",
|
||||||
|
"cli.error.io": "I/O error: {error}",
|
||||||
|
"cli.error.format": "Unsupported format: {format}",
|
||||||
|
"cli.dry_run": "[DRY RUN] Would {action}",
|
||||||
|
"cli.status.ok": "Done",
|
||||||
|
"cli.status.error": "Error",
|
||||||
|
"ops.smooth.applying": "Smoothing terrain...",
|
||||||
|
"ops.erode.applying": "Eroding terrain...",
|
||||||
|
"ops.raise.applying": "Raising terrain...",
|
||||||
|
"ops.lower.applying": "Lowering terrain...",
|
||||||
|
"ops.flatten.applying": "Flattening terrain..."
|
||||||
|
}
|
||||||
30
crates/i18n/src/translations/ru.json
Normal file
30
crates/i18n/src/translations/ru.json
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
{
|
||||||
|
"cli.new.creating": "Создание нового мира...",
|
||||||
|
"cli.new.created": "Мир \"{name}\" создан ({tiles} тайлов)",
|
||||||
|
"cli.export.exporting": "Экспорт мира в \"{path}\"...",
|
||||||
|
"cli.export.complete": "Экспорт завершён: {regions} регионов, {chunks} чанков",
|
||||||
|
"cli.export.empty": "Нет чанков для экспорта",
|
||||||
|
"cli.import.importing": "Импорт сохранения Minecraft из \"{path}\"...",
|
||||||
|
"cli.import.complete": "Импорт завершён: {tiles} тайлов, {dimensions} измерений",
|
||||||
|
"cli.import.no_regions": "Файлы регионов не найдены в \"{path}\"",
|
||||||
|
"cli.render.rendering": "Рендер превью мира...",
|
||||||
|
"cli.render.saved": "Превью сохранено в \"{path}\"",
|
||||||
|
"cli.info.title": "Мир: {name}",
|
||||||
|
"cli.info.seed": "Сид: {seed}",
|
||||||
|
"cli.info.platform": "Платформа: {platform}",
|
||||||
|
"cli.info.tiles": "Тайлов: {tiles}",
|
||||||
|
"cli.info.dimensions": "Измерений: {dimensions}",
|
||||||
|
"cli.validate.ok": "Файл мира корректен",
|
||||||
|
"cli.validate.fail": "Файл мира повреждён: {error}",
|
||||||
|
"cli.error.not_found": "Файл не найден: {path}",
|
||||||
|
"cli.error.io": "Ошибка ввода-вывода: {error}",
|
||||||
|
"cli.error.format": "Неподдерживаемый формат: {format}",
|
||||||
|
"cli.dry_run": "[DRY RUN] Будет выполнено: {action}",
|
||||||
|
"cli.status.ok": "Готово",
|
||||||
|
"cli.status.error": "Ошибка",
|
||||||
|
"ops.smooth.applying": "Сглаживание рельефа...",
|
||||||
|
"ops.erode.applying": "Эрозия рельефа...",
|
||||||
|
"ops.raise.applying": "Поднятие рельефа...",
|
||||||
|
"ops.lower.applying": "Опускание рельефа...",
|
||||||
|
"ops.flatten.applying": "Выравнивание рельефа..."
|
||||||
|
}
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
pub mod read;
|
pub mod read;
|
||||||
pub mod write;
|
pub mod write;
|
||||||
|
|
||||||
pub use read::reader;
|
|
||||||
pub use read::read_tag;
|
pub use read::read_tag;
|
||||||
pub use write::writer;
|
pub use read::reader;
|
||||||
pub use write::write_tag;
|
pub use write::write_tag;
|
||||||
|
pub use write::writer;
|
||||||
|
|
|
||||||
|
|
@ -1,2 +1,2 @@
|
||||||
pub mod reader;
|
|
||||||
pub mod read_tag;
|
pub mod read_tag;
|
||||||
|
pub mod reader;
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::io::Read;
|
use std::io::Read;
|
||||||
|
|
||||||
use crate::tag::Tag;
|
|
||||||
use super::reader::{NbtReader, ReadError, Result};
|
use super::reader::{NbtReader, ReadError, Result};
|
||||||
|
use crate::tag::Tag;
|
||||||
|
|
||||||
impl<R: Read> NbtReader<R> {
|
impl<R: Read> NbtReader<R> {
|
||||||
pub(crate) fn read_tag_payload(&mut self, tag_type: u8) -> Result<Tag> {
|
pub(crate) fn read_tag_payload(&mut self, tag_type: u8) -> Result<Tag> {
|
||||||
|
|
|
||||||
|
|
@ -1,2 +1,2 @@
|
||||||
pub mod writer;
|
|
||||||
pub mod write_tag;
|
pub mod write_tag;
|
||||||
|
pub mod writer;
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
|
|
||||||
|
use super::writer::{NbtWriter, Result, WriteError};
|
||||||
use crate::tag::Tag;
|
use crate::tag::Tag;
|
||||||
use super::writer::{NbtWriter, WriteError, Result};
|
|
||||||
|
|
||||||
impl<W: Write> NbtWriter<W> {
|
impl<W: Write> NbtWriter<W> {
|
||||||
pub(crate) fn write_tag(&mut self, tag: &Tag, name: Option<&str>) -> Result<()> {
|
pub(crate) fn write_tag(&mut self, tag: &Tag, name: Option<&str>) -> Result<()> {
|
||||||
|
|
|
||||||
11
crates/scripting/Cargo.toml
Normal file
11
crates/scripting/Cargo.toml
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
[package]
|
||||||
|
name = "terrafier-scripting"
|
||||||
|
version.workspace = true
|
||||||
|
edition.workspace = true
|
||||||
|
license.workspace = true
|
||||||
|
description = "Rhai scripting engine for Terrafier"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
terrafier-core = { path = "../../core" }
|
||||||
|
rhai = "1"
|
||||||
|
anyhow.workspace = true
|
||||||
148
crates/scripting/src/lib.rs
Normal file
148
crates/scripting/src/lib.rs
Normal file
|
|
@ -0,0 +1,148 @@
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
use rhai::{Dynamic, Engine, EvalAltResult};
|
||||||
|
|
||||||
|
use terrafier_core::World;
|
||||||
|
use terrafier_core::ops::heightmap::{FlatHeightMap, HeightMapSource, NoiseHeightMap};
|
||||||
|
use terrafier_core::ops::layers::generate_all_layers;
|
||||||
|
|
||||||
|
pub fn run_script(path: &Path) -> anyhow::Result<()> {
|
||||||
|
let script = std::fs::read_to_string(path)?;
|
||||||
|
let mut engine = ScriptEngine::new();
|
||||||
|
engine
|
||||||
|
.run(&script)
|
||||||
|
.map(|_| ())
|
||||||
|
.map_err(|e| anyhow::anyhow!("Script error: {}", e))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ScriptEngine {
|
||||||
|
engine: Engine,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn new_world_fn(name: &str, seed: i64) -> World {
|
||||||
|
World::new(name, seed as u64)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn save_world_fn(world: &mut World, path: &str) -> Result<(), Box<EvalAltResult>> {
|
||||||
|
terrafier_core::io::save_binary(world, Path::new(path)).map_err(|e| e.to_string().into())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn info_fn(world: &mut World) -> String {
|
||||||
|
let tile_count: usize = world.dimensions.iter().map(|d| d.tiles.len()).sum();
|
||||||
|
let dim_names: Vec<&str> = world.dimensions.iter().map(|d| d.name.as_str()).collect();
|
||||||
|
format!(
|
||||||
|
"World: {} seed: {} dimensions: [{}] tiles: {}",
|
||||||
|
world.name,
|
||||||
|
world.seed,
|
||||||
|
dim_names.join(", "),
|
||||||
|
tile_count
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn generate_heightmap_flat_fn(
|
||||||
|
world: &mut World,
|
||||||
|
height: i64,
|
||||||
|
dim_name: &str,
|
||||||
|
) -> Result<(), Box<EvalAltResult>> {
|
||||||
|
let dim = world
|
||||||
|
.dimensions
|
||||||
|
.iter_mut()
|
||||||
|
.find(|d| d.name == dim_name)
|
||||||
|
.ok_or_else(|| format!("Dimension '{}' not found", dim_name))?;
|
||||||
|
let flat = FlatHeightMap {
|
||||||
|
height: height as i16,
|
||||||
|
};
|
||||||
|
for (_, tile) in &mut dim.tiles {
|
||||||
|
flat.generate(tile, world.seed);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn generate_heightmap_noise_fn(
|
||||||
|
world: &mut World,
|
||||||
|
seed: i64,
|
||||||
|
amplitude: f64,
|
||||||
|
_octaves: i64,
|
||||||
|
dim_name: &str,
|
||||||
|
) -> Result<(), Box<EvalAltResult>> {
|
||||||
|
let dim = world
|
||||||
|
.dimensions
|
||||||
|
.iter_mut()
|
||||||
|
.find(|d| d.name == dim_name)
|
||||||
|
.ok_or_else(|| format!("Dimension '{}' not found", dim_name))?;
|
||||||
|
let noise = NoiseHeightMap {
|
||||||
|
base_height: 63.0,
|
||||||
|
amplitude,
|
||||||
|
frequency: 0.01,
|
||||||
|
scale_x: 1.0,
|
||||||
|
scale_z: 1.0,
|
||||||
|
};
|
||||||
|
for (_, tile) in &mut dim.tiles {
|
||||||
|
noise.generate(tile, seed as u64);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn apply_smooth_fn(
|
||||||
|
world: &mut World,
|
||||||
|
_iterations: i64,
|
||||||
|
_strength: f64,
|
||||||
|
dim_name: &str,
|
||||||
|
) -> Result<(), Box<EvalAltResult>> {
|
||||||
|
world
|
||||||
|
.dimensions
|
||||||
|
.iter_mut()
|
||||||
|
.find(|d| d.name == dim_name)
|
||||||
|
.ok_or_else(|| format!("Dimension '{}' not found", dim_name))?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn apply_erode_fn(
|
||||||
|
world: &mut World,
|
||||||
|
_iterations: i64,
|
||||||
|
_strength: f64,
|
||||||
|
dim_name: &str,
|
||||||
|
) -> Result<(), Box<EvalAltResult>> {
|
||||||
|
world
|
||||||
|
.dimensions
|
||||||
|
.iter_mut()
|
||||||
|
.find(|d| d.name == dim_name)
|
||||||
|
.ok_or_else(|| format!("Dimension '{}' not found", dim_name))?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn generate_layers_fn(world: &mut World, dim_name: &str) -> Result<(), Box<EvalAltResult>> {
|
||||||
|
let dim = world
|
||||||
|
.dimensions
|
||||||
|
.iter_mut()
|
||||||
|
.find(|d| d.name == dim_name)
|
||||||
|
.ok_or_else(|| format!("Dimension '{}' not found", dim_name))?;
|
||||||
|
for (_, tile) in &mut dim.tiles {
|
||||||
|
generate_all_layers(tile, world.seed);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ScriptEngine {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
let mut engine = Engine::new();
|
||||||
|
|
||||||
|
engine.register_type::<World>();
|
||||||
|
|
||||||
|
engine.register_fn("new_world", new_world_fn);
|
||||||
|
engine.register_fn("save_world", save_world_fn);
|
||||||
|
engine.register_fn("info", info_fn);
|
||||||
|
engine.register_fn("generate_heightmap_flat", generate_heightmap_flat_fn);
|
||||||
|
engine.register_fn("generate_heightmap_noise", generate_heightmap_noise_fn);
|
||||||
|
engine.register_fn("apply_smooth", apply_smooth_fn);
|
||||||
|
engine.register_fn("apply_erode", apply_erode_fn);
|
||||||
|
engine.register_fn("generate_layers", generate_layers_fn);
|
||||||
|
|
||||||
|
Self { engine }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn run(&mut self, script: &str) -> Result<Dynamic, Box<EvalAltResult>> {
|
||||||
|
let ast = self.engine.compile(script)?;
|
||||||
|
self.engine.eval_ast(&ast)
|
||||||
|
}
|
||||||
|
}
|
||||||
56
crates/scripting/tests/integration_test.rs
Normal file
56
crates/scripting/tests/integration_test.rs
Normal file
|
|
@ -0,0 +1,56 @@
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
use terrafier_core::World;
|
||||||
|
use terrafier_scripting::ScriptEngine;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_new_world() {
|
||||||
|
let mut engine = ScriptEngine::new();
|
||||||
|
let result = engine
|
||||||
|
.run(
|
||||||
|
r#"
|
||||||
|
let world = new_world("test_world", 12345);
|
||||||
|
let i = info(world);
|
||||||
|
i.contains("test_world") && i.contains("12345")
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert!(result.as_bool().unwrap());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_generate_flat_heightmap() {
|
||||||
|
let mut engine = ScriptEngine::new();
|
||||||
|
let _ = engine
|
||||||
|
.run(
|
||||||
|
r#"
|
||||||
|
let world = new_world("flat_world", 42);
|
||||||
|
generate_heightmap_flat(world, 64, "overworld");
|
||||||
|
save_world(world, "/tmp/test_flat.tfw");
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let loaded: World = terrafier_core::io::load_binary(Path::new("/tmp/test_flat.tfw")).unwrap();
|
||||||
|
if let Some(dim) = loaded.dimensions.first() {
|
||||||
|
for (_, tile) in &dim.tiles {
|
||||||
|
for z in 0..128 {
|
||||||
|
for x in 0..128 {
|
||||||
|
assert_eq!(tile.get_height(x, z), 64);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let _ = std::fs::remove_file("/tmp/test_flat.tfw");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_script_error_reporting() {
|
||||||
|
let mut engine = ScriptEngine::new();
|
||||||
|
let result = engine.run(
|
||||||
|
r#"
|
||||||
|
let world = new_world("bad", 1);
|
||||||
|
generate_heightmap_flat(world, 64, "nonexistent");
|
||||||
|
"#,
|
||||||
|
);
|
||||||
|
assert!(result.is_err());
|
||||||
|
}
|
||||||
17
crates/wasm-host/Cargo.toml
Normal file
17
crates/wasm-host/Cargo.toml
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
[package]
|
||||||
|
name = "terrafier-wasm-host"
|
||||||
|
version.workspace = true
|
||||||
|
edition.workspace = true
|
||||||
|
license.workspace = true
|
||||||
|
description = "WASM plugin host for Terrafier using wasmtime"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
wasmtime = "46"
|
||||||
|
terrafier-core = { path = "../../core" }
|
||||||
|
anyhow.workspace = true
|
||||||
|
thiserror.workspace = true
|
||||||
|
log.workspace = true
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
tempfile = "3"
|
||||||
|
wat = "1"
|
||||||
12
crates/wasm-host/src/context.rs
Normal file
12
crates/wasm-host/src/context.rs
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
use terrafier_core::model::tile::Tile;
|
||||||
|
|
||||||
|
pub struct WorldContext {
|
||||||
|
pub tile: Tile,
|
||||||
|
pub seed: u64,
|
||||||
|
pub tile_x: i32,
|
||||||
|
pub tile_z: i32,
|
||||||
|
pub hm_offset: usize,
|
||||||
|
pub ter_offset: usize,
|
||||||
|
pub wat_offset: usize,
|
||||||
|
pub error: Option<String>,
|
||||||
|
}
|
||||||
13
crates/wasm-host/src/error.rs
Normal file
13
crates/wasm-host/src/error.rs
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
#[derive(Error, Debug)]
|
||||||
|
pub enum WasmHostError {
|
||||||
|
#[error("Failed to load WASM module: {0}")]
|
||||||
|
LoadError(#[from] anyhow::Error),
|
||||||
|
#[error("WASM plugin does not export required function: {0}")]
|
||||||
|
MissingExport(String),
|
||||||
|
#[error("WASM plugin returned error: {0}")]
|
||||||
|
PluginError(String),
|
||||||
|
#[error("Memory access error: {0}")]
|
||||||
|
MemoryError(String),
|
||||||
|
}
|
||||||
195
crates/wasm-host/src/host.rs
Normal file
195
crates/wasm-host/src/host.rs
Normal file
|
|
@ -0,0 +1,195 @@
|
||||||
|
use wasmtime::{Caller, Linker};
|
||||||
|
|
||||||
|
use crate::context::WorldContext;
|
||||||
|
|
||||||
|
const TILE_SIZE: usize = 128;
|
||||||
|
const HEIGHTMAP_BYTES: usize = TILE_SIZE * TILE_SIZE * 2;
|
||||||
|
const TERRAIN_BYTES: usize = TILE_SIZE * TILE_SIZE;
|
||||||
|
const WATER_BYTES: usize = TILE_SIZE * TILE_SIZE;
|
||||||
|
const TOTAL_BUFFER: usize = HEIGHTMAP_BYTES + TERRAIN_BYTES + WATER_BYTES;
|
||||||
|
|
||||||
|
pub fn register_host_functions(linker: &mut Linker<WorldContext>) -> anyhow::Result<()> {
|
||||||
|
linker.func_wrap("env", "host_log", host_log)?;
|
||||||
|
linker.func_wrap("env", "host_get_seed", host_get_seed)?;
|
||||||
|
linker.func_wrap("env", "host_tile_size", host_tile_size)?;
|
||||||
|
|
||||||
|
linker.func_wrap("env", "host_get_height", host_get_height)?;
|
||||||
|
linker.func_wrap("env", "host_set_height", host_set_height)?;
|
||||||
|
linker.func_wrap("env", "host_get_terrain", host_get_terrain)?;
|
||||||
|
linker.func_wrap("env", "host_set_terrain", host_set_terrain)?;
|
||||||
|
linker.func_wrap("env", "host_get_water", host_get_water)?;
|
||||||
|
linker.func_wrap("env", "host_set_water", host_set_water)?;
|
||||||
|
|
||||||
|
linker.func_wrap("env", "host_alloc_buffers", host_alloc_buffers)?;
|
||||||
|
linker.func_wrap("env", "host_load_heightmap", host_load_heightmap)?;
|
||||||
|
linker.func_wrap("env", "host_load_terrain", host_load_terrain)?;
|
||||||
|
linker.func_wrap("env", "host_load_water", host_load_water)?;
|
||||||
|
linker.func_wrap("env", "host_store_heightmap", host_store_heightmap)?;
|
||||||
|
linker.func_wrap("env", "host_store_terrain", host_store_terrain)?;
|
||||||
|
linker.func_wrap("env", "host_store_water", host_store_water)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn host_log(mut caller: Caller<'_, WorldContext>, level: i32, ptr: i32, len: i32) {
|
||||||
|
let Some(mem) = caller.get_export("memory").and_then(|e| e.into_memory()) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let mut buf = vec![0u8; len as usize];
|
||||||
|
let _ = mem.read(&caller, ptr as usize, &mut buf);
|
||||||
|
let msg = String::from_utf8_lossy(&buf);
|
||||||
|
match level {
|
||||||
|
0 => log::error!("[wasm] {}", msg),
|
||||||
|
1 => log::warn!("[wasm] {}", msg),
|
||||||
|
2 => log::info!("[wasm] {}", msg),
|
||||||
|
_ => log::debug!("[wasm] {}", msg),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn host_get_seed(caller: Caller<'_, WorldContext>) -> i64 {
|
||||||
|
let (lo, hi) = (
|
||||||
|
(caller.data().seed & 0xFFFF_FFFF) as i64,
|
||||||
|
((caller.data().seed >> 32) & 0xFFFF_FFFF) as i64,
|
||||||
|
);
|
||||||
|
lo | (hi << 32)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn host_tile_size(_caller: Caller<'_, WorldContext>) -> i32 {
|
||||||
|
TILE_SIZE as i32
|
||||||
|
}
|
||||||
|
|
||||||
|
fn host_get_height(caller: Caller<'_, WorldContext>, x: i32, z: i32) -> i32 {
|
||||||
|
if x < 0 || z < 0 || x >= TILE_SIZE as i32 || z >= TILE_SIZE as i32 {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
caller.data().tile.get_height(x as usize, z as usize) as i32
|
||||||
|
}
|
||||||
|
|
||||||
|
fn host_set_height(mut caller: Caller<'_, WorldContext>, x: i32, z: i32, val: i32) {
|
||||||
|
if x < 0 || z < 0 || x >= TILE_SIZE as i32 || z >= TILE_SIZE as i32 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
caller
|
||||||
|
.data_mut()
|
||||||
|
.tile
|
||||||
|
.set_height(x as usize, z as usize, val as i16);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn host_get_terrain(caller: Caller<'_, WorldContext>, x: i32, z: i32) -> i32 {
|
||||||
|
if x < 0 || z < 0 || x >= TILE_SIZE as i32 || z >= TILE_SIZE as i32 {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
caller.data().tile.get_terrain(x as usize, z as usize) as i32
|
||||||
|
}
|
||||||
|
|
||||||
|
fn host_set_terrain(mut caller: Caller<'_, WorldContext>, x: i32, z: i32, val: i32) {
|
||||||
|
if x < 0 || z < 0 || x >= TILE_SIZE as i32 || z >= TILE_SIZE as i32 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
caller
|
||||||
|
.data_mut()
|
||||||
|
.tile
|
||||||
|
.set_terrain(x as usize, z as usize, val as u8);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn host_get_water(caller: Caller<'_, WorldContext>, x: i32, z: i32) -> i32 {
|
||||||
|
if x < 0 || z < 0 || x >= TILE_SIZE as i32 || z >= TILE_SIZE as i32 {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
caller.data().tile.water_level[(z as usize) * TILE_SIZE + (x as usize)] as i32
|
||||||
|
}
|
||||||
|
|
||||||
|
fn host_set_water(mut caller: Caller<'_, WorldContext>, x: i32, z: i32, val: i32) {
|
||||||
|
if x < 0 || z < 0 || x >= TILE_SIZE as i32 || z >= TILE_SIZE as i32 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
caller.data_mut().tile.water_level[(z as usize) * TILE_SIZE + (x as usize)] = val as u8;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn host_alloc_buffers(mut caller: Caller<'_, WorldContext>) -> i32 {
|
||||||
|
let Some(mem) = caller.get_export("memory").and_then(|e| e.into_memory()) else {
|
||||||
|
return -1;
|
||||||
|
};
|
||||||
|
let needed_bytes = TOTAL_BUFFER;
|
||||||
|
let current_size = mem.data_size(&caller);
|
||||||
|
let pages_needed = (needed_bytes + current_size + 65535) / 65536 - current_size / 65536;
|
||||||
|
if mem.grow(&mut caller, pages_needed as u64).is_err() {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
let hm_offset = current_size;
|
||||||
|
let ter_offset = hm_offset + HEIGHTMAP_BYTES;
|
||||||
|
let wat_offset = ter_offset + TERRAIN_BYTES;
|
||||||
|
let ctx = caller.data_mut();
|
||||||
|
ctx.hm_offset = hm_offset;
|
||||||
|
ctx.ter_offset = ter_offset;
|
||||||
|
ctx.wat_offset = wat_offset;
|
||||||
|
0
|
||||||
|
}
|
||||||
|
|
||||||
|
fn host_load_heightmap(mut caller: Caller<'_, WorldContext>) -> i32 {
|
||||||
|
let Some(mem) = caller.get_export("memory").and_then(|e| e.into_memory()) else {
|
||||||
|
return -1;
|
||||||
|
};
|
||||||
|
let offset = caller.data().hm_offset;
|
||||||
|
let bytes: Vec<u8> = caller
|
||||||
|
.data()
|
||||||
|
.tile
|
||||||
|
.heightmap
|
||||||
|
.iter()
|
||||||
|
.flat_map(|h| h.to_le_bytes())
|
||||||
|
.collect();
|
||||||
|
let _ = mem.write(&mut caller, offset, &bytes);
|
||||||
|
offset as i32
|
||||||
|
}
|
||||||
|
|
||||||
|
fn host_load_terrain(mut caller: Caller<'_, WorldContext>) -> i32 {
|
||||||
|
let Some(mem) = caller.get_export("memory").and_then(|e| e.into_memory()) else {
|
||||||
|
return -1;
|
||||||
|
};
|
||||||
|
let offset = caller.data().ter_offset;
|
||||||
|
let terrain = caller.data().tile.terrain;
|
||||||
|
let _ = mem.write(&mut caller, offset, &terrain);
|
||||||
|
offset as i32
|
||||||
|
}
|
||||||
|
|
||||||
|
fn host_load_water(mut caller: Caller<'_, WorldContext>) -> i32 {
|
||||||
|
let Some(mem) = caller.get_export("memory").and_then(|e| e.into_memory()) else {
|
||||||
|
return -1;
|
||||||
|
};
|
||||||
|
let offset = caller.data().wat_offset;
|
||||||
|
let water = caller.data().tile.water_level;
|
||||||
|
let _ = mem.write(&mut caller, offset, &water);
|
||||||
|
offset as i32
|
||||||
|
}
|
||||||
|
|
||||||
|
fn host_store_heightmap(mut caller: Caller<'_, WorldContext>) {
|
||||||
|
let Some(mem) = caller.get_export("memory").and_then(|e| e.into_memory()) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let offset = caller.data().hm_offset;
|
||||||
|
let mut buf = vec![0u8; HEIGHTMAP_BYTES];
|
||||||
|
let _ = mem.read(&caller, offset, &mut buf);
|
||||||
|
let ctx = caller.data_mut();
|
||||||
|
for (i, chunk) in buf.chunks_exact(2).enumerate() {
|
||||||
|
ctx.tile.heightmap[i] = i16::from_le_bytes([chunk[0], chunk[1]]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn host_store_terrain(mut caller: Caller<'_, WorldContext>) {
|
||||||
|
let Some(mem) = caller.get_export("memory").and_then(|e| e.into_memory()) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let offset = caller.data().ter_offset;
|
||||||
|
let mut buf = [0u8; TERRAIN_BYTES];
|
||||||
|
let _ = mem.read(&caller, offset, &mut buf);
|
||||||
|
caller.data_mut().tile.terrain = buf;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn host_store_water(mut caller: Caller<'_, WorldContext>) {
|
||||||
|
let Some(mem) = caller.get_export("memory").and_then(|e| e.into_memory()) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let offset = caller.data().wat_offset;
|
||||||
|
let mut buf = [0u8; WATER_BYTES];
|
||||||
|
let _ = mem.read(&caller, offset, &mut buf);
|
||||||
|
caller.data_mut().tile.water_level = buf;
|
||||||
|
}
|
||||||
43
crates/wasm-host/src/lib.rs
Normal file
43
crates/wasm-host/src/lib.rs
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
mod context;
|
||||||
|
mod error;
|
||||||
|
mod host;
|
||||||
|
mod plugin;
|
||||||
|
|
||||||
|
use std::path::Path;
|
||||||
|
use terrafier_core::plugins::PluginRegistry;
|
||||||
|
|
||||||
|
pub use context::WorldContext;
|
||||||
|
pub use error::WasmHostError;
|
||||||
|
pub use plugin::{WasmPlugin, WasmTileSourcePlugin};
|
||||||
|
|
||||||
|
/// Load all .wasm plugins from a directory into the registry.
|
||||||
|
pub fn load_wasm_dir(
|
||||||
|
registry: &mut PluginRegistry,
|
||||||
|
dir: &Path,
|
||||||
|
) -> Result<usize, Box<dyn std::error::Error>> {
|
||||||
|
let mut count = 0;
|
||||||
|
if dir.is_dir() {
|
||||||
|
for entry in std::fs::read_dir(dir)? {
|
||||||
|
let entry = entry?;
|
||||||
|
let path = entry.path();
|
||||||
|
if path.extension().map(|e| e == "wasm").unwrap_or(false) {
|
||||||
|
match WasmTileSourcePlugin::from_file(&path) {
|
||||||
|
Ok(plugin) => {
|
||||||
|
log::info!(
|
||||||
|
"Loaded WASM plugin: {} v{} ({})",
|
||||||
|
plugin.name(),
|
||||||
|
plugin.version(),
|
||||||
|
path.display()
|
||||||
|
);
|
||||||
|
registry.sources.push(Box::new(plugin));
|
||||||
|
count += 1;
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
log::error!("Failed to load WASM plugin {}: {}", path.display(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(count)
|
||||||
|
}
|
||||||
187
crates/wasm-host/src/plugin.rs
Normal file
187
crates/wasm-host/src/plugin.rs
Normal file
|
|
@ -0,0 +1,187 @@
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
use terrafier_core::model::tile::Tile;
|
||||||
|
use terrafier_core::plugins::TileSourcePlugin;
|
||||||
|
use wasmtime::*;
|
||||||
|
|
||||||
|
use crate::context::WorldContext;
|
||||||
|
use crate::error::WasmHostError;
|
||||||
|
use crate::host::register_host_functions;
|
||||||
|
|
||||||
|
pub trait WasmPlugin {
|
||||||
|
fn name(&self) -> &str;
|
||||||
|
fn version(&self) -> &str;
|
||||||
|
fn plugin_type(&self) -> u32;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct WasmTileSourcePlugin {
|
||||||
|
engine: Engine,
|
||||||
|
module: Module,
|
||||||
|
name: String,
|
||||||
|
version: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WasmTileSourcePlugin {
|
||||||
|
pub fn from_file(path: &Path) -> Result<Self, WasmHostError> {
|
||||||
|
let engine = Engine::default();
|
||||||
|
let wasm_bytes = std::fs::read(path).map_err(|e| {
|
||||||
|
WasmHostError::LoadError(anyhow::anyhow!("Failed to read .wasm file: {}", e))
|
||||||
|
})?;
|
||||||
|
let module = Module::from_binary(&engine, &wasm_bytes)
|
||||||
|
.map_err(|e| WasmHostError::LoadError(anyhow::anyhow!("Invalid WASM module: {}", e)))?;
|
||||||
|
|
||||||
|
let mut linker = Linker::new(&engine);
|
||||||
|
register_host_functions(&mut linker).map_err(|e| {
|
||||||
|
WasmHostError::LoadError(anyhow::anyhow!("Failed to register host functions: {}", e))
|
||||||
|
})?;
|
||||||
|
let dummy_ctx = WorldContext {
|
||||||
|
tile: Tile::new(0, 0, -64, 320),
|
||||||
|
seed: 0,
|
||||||
|
tile_x: 0,
|
||||||
|
tile_z: 0,
|
||||||
|
hm_offset: 0,
|
||||||
|
ter_offset: 0,
|
||||||
|
wat_offset: 0,
|
||||||
|
error: None,
|
||||||
|
};
|
||||||
|
let mut store = Store::new(&engine, dummy_ctx);
|
||||||
|
let instance = linker.instantiate(&mut store, &module).map_err(|e| {
|
||||||
|
WasmHostError::LoadError(anyhow::anyhow!("Failed to instantiate module: {}", e))
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let memory = instance
|
||||||
|
.get_memory(&mut store, "memory")
|
||||||
|
.ok_or_else(|| WasmHostError::MissingExport("memory".into()))?;
|
||||||
|
|
||||||
|
let name = Self::read_string(&mut store, &instance, &memory, "name_ptr", "name_len")?;
|
||||||
|
let version =
|
||||||
|
Self::read_string(&mut store, &instance, &memory, "version_ptr", "version_len")?;
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
engine,
|
||||||
|
module,
|
||||||
|
name,
|
||||||
|
version,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_string(
|
||||||
|
store: &mut Store<WorldContext>,
|
||||||
|
instance: &Instance,
|
||||||
|
memory: &Memory,
|
||||||
|
ptr_func: &str,
|
||||||
|
len_func: &str,
|
||||||
|
) -> Result<String, WasmHostError> {
|
||||||
|
let ptr = instance
|
||||||
|
.get_typed_func::<(), i32>(&mut *store, ptr_func)
|
||||||
|
.map_err(|_| WasmHostError::MissingExport(ptr_func.into()))?
|
||||||
|
.call(&mut *store, ())
|
||||||
|
.map_err(|e| WasmHostError::PluginError(e.to_string()))?;
|
||||||
|
let len = instance
|
||||||
|
.get_typed_func::<(), i32>(&mut *store, len_func)
|
||||||
|
.map_err(|_| WasmHostError::MissingExport(len_func.into()))?
|
||||||
|
.call(&mut *store, ())
|
||||||
|
.map_err(|e| WasmHostError::PluginError(e.to_string()))?;
|
||||||
|
|
||||||
|
let mut buf = vec![0u8; len as usize];
|
||||||
|
memory
|
||||||
|
.read(&*store, ptr as usize, &mut buf)
|
||||||
|
.map_err(|e| WasmHostError::MemoryError(e.to_string()))?;
|
||||||
|
Ok(String::from_utf8_lossy(&buf).to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn make_instance(
|
||||||
|
&self,
|
||||||
|
tile: &Tile,
|
||||||
|
seed: u64,
|
||||||
|
tile_x: i32,
|
||||||
|
tile_z: i32,
|
||||||
|
) -> Result<(Store<WorldContext>, Instance), WasmHostError> {
|
||||||
|
let mut linker = Linker::new(&self.engine);
|
||||||
|
register_host_functions(&mut linker).map_err(|e| WasmHostError::LoadError(e.into()))?;
|
||||||
|
|
||||||
|
let ctx = WorldContext {
|
||||||
|
tile: tile.clone(),
|
||||||
|
seed,
|
||||||
|
tile_x,
|
||||||
|
tile_z,
|
||||||
|
hm_offset: 0,
|
||||||
|
ter_offset: 0,
|
||||||
|
wat_offset: 0,
|
||||||
|
error: None,
|
||||||
|
};
|
||||||
|
let mut store = Store::new(&self.engine, ctx);
|
||||||
|
let instance = linker
|
||||||
|
.instantiate(&mut store, &self.module)
|
||||||
|
.map_err(|e| WasmHostError::LoadError(e.into()))?;
|
||||||
|
Ok((store, instance))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WasmPlugin for WasmTileSourcePlugin {
|
||||||
|
fn name(&self) -> &str {
|
||||||
|
&self.name
|
||||||
|
}
|
||||||
|
fn version(&self) -> &str {
|
||||||
|
&self.version
|
||||||
|
}
|
||||||
|
fn plugin_type(&self) -> u32 {
|
||||||
|
3
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TileSourcePlugin for WasmTileSourcePlugin {
|
||||||
|
fn name(&self) -> &'static str {
|
||||||
|
Box::leak(self.name.clone().into_boxed_str())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn version(&self) -> &'static str {
|
||||||
|
Box::leak(self.version.clone().into_boxed_str())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn generate_tile(&self, tile: &mut Tile, seed: u64) {
|
||||||
|
let tx = tile.x;
|
||||||
|
let tz = tile.z;
|
||||||
|
let tile_clone = tile.clone();
|
||||||
|
let (mut store, instance) = match self.make_instance(&tile_clone, seed, tx, tz) {
|
||||||
|
Ok(v) => v,
|
||||||
|
Err(e) => {
|
||||||
|
log::error!(
|
||||||
|
"Failed to create WASM instance for plugin '{}': {}",
|
||||||
|
self.name,
|
||||||
|
e
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let Ok(gen_func) =
|
||||||
|
instance.get_typed_func::<(i32, i32, i32, i32), i32>(&mut store, "generate_tile")
|
||||||
|
else {
|
||||||
|
log::error!(
|
||||||
|
"WASM plugin '{}' does not export 'generate_tile'",
|
||||||
|
self.name
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let seed_lo = (seed & 0xFFFF_FFFF) as i32;
|
||||||
|
let seed_hi = ((seed >> 32) & 0xFFFF_FFFF) as i32;
|
||||||
|
|
||||||
|
match gen_func.call(&mut store, (tx, tz, seed_lo, seed_hi)) {
|
||||||
|
Ok(0) => {
|
||||||
|
*tile = store.data().tile.clone();
|
||||||
|
}
|
||||||
|
Ok(code) => {
|
||||||
|
log::error!(
|
||||||
|
"WASM plugin '{}' generate_tile returned error code: {}",
|
||||||
|
self.name,
|
||||||
|
code
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
log::error!("WASM plugin '{}' generate_tile trap: {}", self.name, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
101
crates/wasm-host/tests/integration_test.rs
Normal file
101
crates/wasm-host/tests/integration_test.rs
Normal file
|
|
@ -0,0 +1,101 @@
|
||||||
|
use terrafier_core::model::tile::Tile;
|
||||||
|
use terrafier_core::plugins::{PluginRegistry, TileSourcePlugin};
|
||||||
|
use terrafier_wasm_host::{WasmTileSourcePlugin, load_wasm_dir};
|
||||||
|
|
||||||
|
const MINIMAL_PLUGIN_WAT: &str = r#"
|
||||||
|
(module
|
||||||
|
(import "env" "host_set_height" (func $host_set_height (param i32 i32 i32)))
|
||||||
|
(import "env" "host_set_terrain" (func $host_set_terrain (param i32 i32 i32)))
|
||||||
|
|
||||||
|
(memory (export "memory") 1)
|
||||||
|
|
||||||
|
(data (i32.const 1024) "test_plugin")
|
||||||
|
(data (i32.const 1040) "1.0.0")
|
||||||
|
|
||||||
|
(func (export "name_ptr") (result i32) i32.const 1024)
|
||||||
|
(func (export "name_len") (result i32) i32.const 11)
|
||||||
|
(func (export "version_ptr") (result i32) i32.const 1040)
|
||||||
|
(func (export "version_len") (result i32) i32.const 5)
|
||||||
|
(func (export "plugin_type") (result i32) i32.const 3)
|
||||||
|
|
||||||
|
(func (export "generate_tile")
|
||||||
|
(param $tx i32) (param $tz i32) (param $seed_lo i32) (param $seed_hi i32)
|
||||||
|
(result i32)
|
||||||
|
(local $x i32) (local $z i32)
|
||||||
|
|
||||||
|
(loop $loop_z
|
||||||
|
(local.set $x (i32.const 0))
|
||||||
|
(loop $loop_x
|
||||||
|
(call $host_set_height (local.get $x) (local.get $z)
|
||||||
|
(i32.add (local.get $x) (local.get $z)))
|
||||||
|
(call $host_set_terrain (local.get $x) (local.get $z) (i32.const 1))
|
||||||
|
(br_if $loop_x
|
||||||
|
(i32.lt_u
|
||||||
|
(local.tee $x (i32.add (local.get $x) (i32.const 1)))
|
||||||
|
(i32.const 128)))
|
||||||
|
)
|
||||||
|
(br_if $loop_z
|
||||||
|
(i32.lt_u
|
||||||
|
(local.tee $z (i32.add (local.get $z) (i32.const 1)))
|
||||||
|
(i32.const 128)))
|
||||||
|
)
|
||||||
|
(i32.const 0)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
"#;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_load_plugin_from_file() {
|
||||||
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
let wasm_path = dir.path().join("test_plugin.wasm");
|
||||||
|
let wasm_bytes = wat::parse_str(MINIMAL_PLUGIN_WAT).expect("Failed to parse WAT");
|
||||||
|
std::fs::write(&wasm_path, &wasm_bytes).unwrap();
|
||||||
|
|
||||||
|
let plugin = WasmTileSourcePlugin::from_file(&wasm_path).expect("Failed to load WASM plugin");
|
||||||
|
|
||||||
|
assert_eq!(plugin.name(), "test_plugin");
|
||||||
|
assert_eq!(plugin.version(), "1.0.0");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_generate_tile() {
|
||||||
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
let wasm_path = dir.path().join("test_plugin.wasm");
|
||||||
|
let wasm_bytes = wat::parse_str(MINIMAL_PLUGIN_WAT).expect("Failed to parse WAT");
|
||||||
|
std::fs::write(&wasm_path, &wasm_bytes).unwrap();
|
||||||
|
|
||||||
|
let plugin = WasmTileSourcePlugin::from_file(&wasm_path).unwrap();
|
||||||
|
let mut tile = Tile::new(5, 3, -64, 320);
|
||||||
|
|
||||||
|
plugin.generate_tile(&mut tile, 42);
|
||||||
|
|
||||||
|
assert_eq!(tile.x, 5);
|
||||||
|
assert_eq!(tile.z, 3);
|
||||||
|
|
||||||
|
// Plugin sets height = x + z, terrain = 1
|
||||||
|
for z in 0..128 {
|
||||||
|
for x in 0..128 {
|
||||||
|
assert_eq!(tile.get_height(x, z), (x + z) as i16);
|
||||||
|
assert_eq!(tile.get_terrain(x, z), 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_load_wasm_dir() {
|
||||||
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
|
||||||
|
// Create two .wasm files
|
||||||
|
let wasm_bytes = wat::parse_str(MINIMAL_PLUGIN_WAT).expect("Failed to parse WAT");
|
||||||
|
std::fs::write(dir.path().join("plugin_a.wasm"), &wasm_bytes).unwrap();
|
||||||
|
std::fs::write(dir.path().join("plugin_b.wasm"), &wasm_bytes).unwrap();
|
||||||
|
// Non-wasm file should be ignored
|
||||||
|
std::fs::write(dir.path().join("readme.txt"), b"ignore me").unwrap();
|
||||||
|
|
||||||
|
let mut registry = PluginRegistry::new();
|
||||||
|
assert_eq!(registry.sources.len(), 0);
|
||||||
|
|
||||||
|
let count = load_wasm_dir(&mut registry, dir.path()).unwrap();
|
||||||
|
assert_eq!(count, 2);
|
||||||
|
assert_eq!(registry.sources.len(), 2);
|
||||||
|
}
|
||||||
|
|
@ -3,8 +3,16 @@ name = "terrafier-gui"
|
||||||
version.workspace = true
|
version.workspace = true
|
||||||
edition.workspace = true
|
edition.workspace = true
|
||||||
license.workspace = true
|
license.workspace = true
|
||||||
|
repository.workspace = true
|
||||||
|
authors.workspace = true
|
||||||
description = "Native GUI for Terrafier — interactive world painting with GPU rendering"
|
description = "Native GUI for Terrafier — interactive world painting with GPU rendering"
|
||||||
|
|
||||||
|
[package.metadata.wix]
|
||||||
|
upgrade-guid = "A7365100-6923-4DAC-B4A5-2BD817EB802B"
|
||||||
|
path-guid = "097E8AD4-180B-4B52-8BFE-04AA942D03A6"
|
||||||
|
license = false
|
||||||
|
eula = false
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
terrafier-core = { path = "../core" }
|
terrafier-core = { path = "../core" }
|
||||||
anyhow.workspace = true
|
anyhow.workspace = true
|
||||||
|
|
|
||||||
|
|
@ -74,6 +74,8 @@ pub struct TerrafierApp {
|
||||||
pub renderer: Option<GpuRenderer>,
|
pub renderer: Option<GpuRenderer>,
|
||||||
pub show_gpu_render: bool,
|
pub show_gpu_render: bool,
|
||||||
pub gpu_status: GpuStatus,
|
pub gpu_status: GpuStatus,
|
||||||
|
pub view_offset: egui::Vec2,
|
||||||
|
pub view_zoom: f32,
|
||||||
pub cpu_texture_handle: Option<egui::TextureHandle>,
|
pub cpu_texture_handle: Option<egui::TextureHandle>,
|
||||||
pub layer_visible: [bool; 6],
|
pub layer_visible: [bool; 6],
|
||||||
}
|
}
|
||||||
|
|
@ -96,7 +98,10 @@ impl TerrafierApp {
|
||||||
gpu_renderer.register_texture(&mut renderer_lock);
|
gpu_renderer.register_texture(&mut renderer_lock);
|
||||||
(Some(gpu_renderer), GpuStatus::Enabled)
|
(Some(gpu_renderer), GpuStatus::Enabled)
|
||||||
} else {
|
} else {
|
||||||
(None, GpuStatus::FallbackCpu("GPU init failed — no wgpu adapter found"))
|
(
|
||||||
|
None,
|
||||||
|
GpuStatus::FallbackCpu("GPU init failed — no wgpu adapter found"),
|
||||||
|
)
|
||||||
};
|
};
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
|
|
@ -124,9 +129,10 @@ impl TerrafierApp {
|
||||||
renderer,
|
renderer,
|
||||||
show_gpu_render: true,
|
show_gpu_render: true,
|
||||||
gpu_status,
|
gpu_status,
|
||||||
|
view_offset: egui::Vec2::ZERO,
|
||||||
|
view_zoom: 1.0,
|
||||||
cpu_texture_handle: None,
|
cpu_texture_handle: None,
|
||||||
layer_visible: [false; 6],
|
layer_visible: [false; 6],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ impl TerrafierApp {
|
||||||
pub fn undo(&mut self) {
|
pub fn undo(&mut self) {
|
||||||
if let Some(op) = self.undo_stack.pop_back() {
|
if let Some(op) = self.undo_stack.pop_back() {
|
||||||
if let Some(ref mut world) = self.world {
|
if let Some(ref mut world) = self.world {
|
||||||
let dim = world.overworld_mut().expect("world has no overworld");
|
if let Some(dim) = world.overworld_mut() {
|
||||||
let inv = op.inverse();
|
let inv = op.inverse();
|
||||||
if let Err(e) = inv.apply(dim) {
|
if let Err(e) = inv.apply(dim) {
|
||||||
self.status_message = format!("Undo error: {:?}", e);
|
self.status_message = format!("Undo error: {:?}", e);
|
||||||
|
|
@ -23,7 +23,11 @@ impl TerrafierApp {
|
||||||
}
|
}
|
||||||
self.redo_stack.push(op);
|
self.redo_stack.push(op);
|
||||||
self.status_message = "Undo".to_string();
|
self.status_message = "Undo".to_string();
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
self.undo_stack.push_back(op);
|
||||||
|
self.status_message = "Undo error: world has no overworld".to_string();
|
||||||
} else {
|
} else {
|
||||||
self.status_message = "Nothing to undo".to_string();
|
self.status_message = "Nothing to undo".to_string();
|
||||||
}
|
}
|
||||||
|
|
@ -32,7 +36,7 @@ impl TerrafierApp {
|
||||||
pub fn redo(&mut self) {
|
pub fn redo(&mut self) {
|
||||||
if let Some(op) = self.redo_stack.pop() {
|
if let Some(op) = self.redo_stack.pop() {
|
||||||
if let Some(ref mut world) = self.world {
|
if let Some(ref mut world) = self.world {
|
||||||
let dim = world.overworld_mut().expect("world has no overworld");
|
if let Some(dim) = world.overworld_mut() {
|
||||||
if let Err(e) = op.apply(dim) {
|
if let Err(e) = op.apply(dim) {
|
||||||
self.status_message = format!("Redo error: {:?}", e);
|
self.status_message = format!("Redo error: {:?}", e);
|
||||||
self.redo_stack.push(op);
|
self.redo_stack.push(op);
|
||||||
|
|
@ -40,7 +44,11 @@ impl TerrafierApp {
|
||||||
}
|
}
|
||||||
self.undo_stack.push_back(op);
|
self.undo_stack.push_back(op);
|
||||||
self.status_message = "Redo".to_string();
|
self.status_message = "Redo".to_string();
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
self.redo_stack.push(op);
|
||||||
|
self.status_message = "Redo error: world has no overworld".to_string();
|
||||||
} else {
|
} else {
|
||||||
self.status_message = "Nothing to redo".to_string();
|
self.status_message = "Nothing to redo".to_string();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -71,8 +71,8 @@ impl eframe::App for TerrafierApp {
|
||||||
&& let Some(tile) = world.dimensions[0].tiles.values().next()
|
&& let Some(tile) = world.dimensions[0].tiles.values().next()
|
||||||
{
|
{
|
||||||
renderer.render(
|
renderer.render(
|
||||||
&tile.heightmap.as_slice().try_into().expect("heightmap size"),
|
&tile.heightmap.as_slice().try_into().unwrap_or_else(|_| [0; 16384]),
|
||||||
&tile.terrain.as_slice().try_into().expect("terrain size"),
|
&tile.terrain.as_slice().try_into().unwrap_or_else(|_| [0; 16384]),
|
||||||
self.show_heightmap,
|
self.show_heightmap,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -47,29 +47,61 @@ impl GpuRenderer {
|
||||||
needed_storage,
|
needed_storage,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
let bind_group_layout =
|
let bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
|
||||||
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
|
|
||||||
label: Some("terrain_bind_group_layout"),
|
label: Some("terrain_bind_group_layout"),
|
||||||
entries: &[
|
entries: &[
|
||||||
wgpu::BindGroupLayoutEntry { binding: 0, visibility: wgpu::ShaderStages::COMPUTE, ty: wgpu::BindingType::Buffer { ty: wgpu::BufferBindingType::Uniform, has_dynamic_offset: false, min_binding_size: None }, count: None },
|
wgpu::BindGroupLayoutEntry {
|
||||||
wgpu::BindGroupLayoutEntry { binding: 1, visibility: wgpu::ShaderStages::COMPUTE, ty: wgpu::BindingType::Buffer { ty: wgpu::BufferBindingType::Storage { read_only: true }, has_dynamic_offset: false, min_binding_size: None }, count: None },
|
binding: 0,
|
||||||
wgpu::BindGroupLayoutEntry { binding: 2, visibility: wgpu::ShaderStages::COMPUTE, ty: wgpu::BindingType::Buffer { ty: wgpu::BufferBindingType::Storage { read_only: true }, has_dynamic_offset: false, min_binding_size: None }, count: None },
|
visibility: wgpu::ShaderStages::COMPUTE,
|
||||||
wgpu::BindGroupLayoutEntry { binding: 3, visibility: wgpu::ShaderStages::COMPUTE, ty: wgpu::BindingType::Buffer { ty: wgpu::BufferBindingType::Storage { read_only: false }, has_dynamic_offset: false, min_binding_size: None }, count: None },
|
ty: wgpu::BindingType::Buffer {
|
||||||
|
ty: wgpu::BufferBindingType::Uniform,
|
||||||
|
has_dynamic_offset: false,
|
||||||
|
min_binding_size: None,
|
||||||
|
},
|
||||||
|
count: None,
|
||||||
|
},
|
||||||
|
wgpu::BindGroupLayoutEntry {
|
||||||
|
binding: 1,
|
||||||
|
visibility: wgpu::ShaderStages::COMPUTE,
|
||||||
|
ty: wgpu::BindingType::Buffer {
|
||||||
|
ty: wgpu::BufferBindingType::Storage { read_only: true },
|
||||||
|
has_dynamic_offset: false,
|
||||||
|
min_binding_size: None,
|
||||||
|
},
|
||||||
|
count: None,
|
||||||
|
},
|
||||||
|
wgpu::BindGroupLayoutEntry {
|
||||||
|
binding: 2,
|
||||||
|
visibility: wgpu::ShaderStages::COMPUTE,
|
||||||
|
ty: wgpu::BindingType::Buffer {
|
||||||
|
ty: wgpu::BufferBindingType::Storage { read_only: true },
|
||||||
|
has_dynamic_offset: false,
|
||||||
|
min_binding_size: None,
|
||||||
|
},
|
||||||
|
count: None,
|
||||||
|
},
|
||||||
|
wgpu::BindGroupLayoutEntry {
|
||||||
|
binding: 3,
|
||||||
|
visibility: wgpu::ShaderStages::COMPUTE,
|
||||||
|
ty: wgpu::BindingType::Buffer {
|
||||||
|
ty: wgpu::BufferBindingType::Storage { read_only: false },
|
||||||
|
has_dynamic_offset: false,
|
||||||
|
min_binding_size: None,
|
||||||
|
},
|
||||||
|
count: None,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
let shader_module =
|
let shader_module = device.create_shader_module(wgpu::ShaderModuleDescriptor {
|
||||||
device.create_shader_module(wgpu::ShaderModuleDescriptor {
|
|
||||||
label: Some("terrain_shader"),
|
label: Some("terrain_shader"),
|
||||||
source: wgpu::ShaderSource::Wgsl(shader_source.into()),
|
source: wgpu::ShaderSource::Wgsl(shader_source.into()),
|
||||||
});
|
});
|
||||||
let pipeline_layout =
|
let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
|
||||||
device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
|
|
||||||
label: Some("terrain_pipeline_layout"),
|
label: Some("terrain_pipeline_layout"),
|
||||||
bind_group_layouts: &[&bind_group_layout],
|
bind_group_layouts: &[&bind_group_layout],
|
||||||
push_constant_ranges: &[],
|
push_constant_ranges: &[],
|
||||||
});
|
});
|
||||||
let compute_pipeline =
|
let compute_pipeline = device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
|
||||||
device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
|
|
||||||
label: Some("terrain_compute_pipeline"),
|
label: Some("terrain_compute_pipeline"),
|
||||||
layout: Some(&pipeline_layout),
|
layout: Some(&pipeline_layout),
|
||||||
module: &shader_module,
|
module: &shader_module,
|
||||||
|
|
@ -78,26 +110,40 @@ impl GpuRenderer {
|
||||||
cache: None,
|
cache: None,
|
||||||
});
|
});
|
||||||
let heightmap_buffer = device.create_buffer(&wgpu::BufferDescriptor {
|
let heightmap_buffer = device.create_buffer(&wgpu::BufferDescriptor {
|
||||||
label: Some("heightmap_buffer"), size: 8192 * 4,
|
label: Some("heightmap_buffer"),
|
||||||
usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST, mapped_at_creation: false,
|
size: 8192 * 4,
|
||||||
|
usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
|
||||||
|
mapped_at_creation: false,
|
||||||
});
|
});
|
||||||
let terrain_buffer = device.create_buffer(&wgpu::BufferDescriptor {
|
let terrain_buffer = device.create_buffer(&wgpu::BufferDescriptor {
|
||||||
label: Some("terrain_buffer"), size: 4096 * 4,
|
label: Some("terrain_buffer"),
|
||||||
usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST, mapped_at_creation: false,
|
size: 4096 * 4,
|
||||||
|
usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
|
||||||
|
mapped_at_creation: false,
|
||||||
});
|
});
|
||||||
let uniform_buffer = device.create_buffer(&wgpu::BufferDescriptor {
|
let uniform_buffer = device.create_buffer(&wgpu::BufferDescriptor {
|
||||||
label: Some("uniform_buffer"), size: std::mem::size_of::<Uniforms>() as u64,
|
label: Some("uniform_buffer"),
|
||||||
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, mapped_at_creation: false,
|
size: std::mem::size_of::<Uniforms>() as u64,
|
||||||
|
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
|
||||||
|
mapped_at_creation: false,
|
||||||
});
|
});
|
||||||
let output_size = (width * height * 4) as u64;
|
let output_size = (width * height * 4) as u64;
|
||||||
let output_buffer = device.create_buffer(&wgpu::BufferDescriptor {
|
let output_buffer = device.create_buffer(&wgpu::BufferDescriptor {
|
||||||
label: Some("output_buffer"), size: output_size,
|
label: Some("output_buffer"),
|
||||||
usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_SRC, mapped_at_creation: false,
|
size: output_size,
|
||||||
|
usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_SRC,
|
||||||
|
mapped_at_creation: false,
|
||||||
});
|
});
|
||||||
let output_texture = device.create_texture(&wgpu::TextureDescriptor {
|
let output_texture = device.create_texture(&wgpu::TextureDescriptor {
|
||||||
label: Some("output_texture"),
|
label: Some("output_texture"),
|
||||||
size: wgpu::Extent3d { width, height, depth_or_array_layers: 1 },
|
size: wgpu::Extent3d {
|
||||||
mip_level_count: 1, sample_count: 1, dimension: wgpu::TextureDimension::D2,
|
width,
|
||||||
|
height,
|
||||||
|
depth_or_array_layers: 1,
|
||||||
|
},
|
||||||
|
mip_level_count: 1,
|
||||||
|
sample_count: 1,
|
||||||
|
dimension: wgpu::TextureDimension::D2,
|
||||||
format: wgpu::TextureFormat::Rgba8UnormSrgb,
|
format: wgpu::TextureFormat::Rgba8UnormSrgb,
|
||||||
view_formats: &[wgpu::TextureFormat::Rgba8UnormSrgb],
|
view_formats: &[wgpu::TextureFormat::Rgba8UnormSrgb],
|
||||||
usage: wgpu::TextureUsages::COPY_DST | wgpu::TextureUsages::TEXTURE_BINDING,
|
usage: wgpu::TextureUsages::COPY_DST | wgpu::TextureUsages::TEXTURE_BINDING,
|
||||||
|
|
@ -107,16 +153,54 @@ impl GpuRenderer {
|
||||||
label: Some("terrain_bind_group"),
|
label: Some("terrain_bind_group"),
|
||||||
layout: &bind_group_layout,
|
layout: &bind_group_layout,
|
||||||
entries: &[
|
entries: &[
|
||||||
wgpu::BindGroupEntry { binding: 0, resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding { buffer: &uniform_buffer, offset: 0, size: None }) },
|
wgpu::BindGroupEntry {
|
||||||
wgpu::BindGroupEntry { binding: 1, resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding { buffer: &heightmap_buffer, offset: 0, size: None }) },
|
binding: 0,
|
||||||
wgpu::BindGroupEntry { binding: 2, resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding { buffer: &terrain_buffer, offset: 0, size: None }) },
|
resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
|
||||||
wgpu::BindGroupEntry { binding: 3, resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding { buffer: &output_buffer, offset: 0, size: None }) },
|
buffer: &uniform_buffer,
|
||||||
|
offset: 0,
|
||||||
|
size: None,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
wgpu::BindGroupEntry {
|
||||||
|
binding: 1,
|
||||||
|
resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
|
||||||
|
buffer: &heightmap_buffer,
|
||||||
|
offset: 0,
|
||||||
|
size: None,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
wgpu::BindGroupEntry {
|
||||||
|
binding: 2,
|
||||||
|
resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
|
||||||
|
buffer: &terrain_buffer,
|
||||||
|
offset: 0,
|
||||||
|
size: None,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
wgpu::BindGroupEntry {
|
||||||
|
binding: 3,
|
||||||
|
resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
|
||||||
|
buffer: &output_buffer,
|
||||||
|
offset: 0,
|
||||||
|
size: None,
|
||||||
|
}),
|
||||||
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
Self {
|
Self {
|
||||||
device, queue, compute_pipeline, heightmap_buffer, terrain_buffer,
|
device,
|
||||||
uniform_buffer, output_buffer, output_texture, output_texture_view, bind_group,
|
queue,
|
||||||
texture_id: None, width, height,
|
compute_pipeline,
|
||||||
|
heightmap_buffer,
|
||||||
|
terrain_buffer,
|
||||||
|
uniform_buffer,
|
||||||
|
output_buffer,
|
||||||
|
output_texture,
|
||||||
|
output_texture_view,
|
||||||
|
bind_group,
|
||||||
|
texture_id: None,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
last_heightmap: vec![0i16; 16384],
|
last_heightmap: vec![0i16; 16384],
|
||||||
last_terrain: vec![0u8; 16384],
|
last_terrain: vec![0u8; 16384],
|
||||||
last_show_heightmap: false,
|
last_show_heightmap: false,
|
||||||
|
|
@ -125,16 +209,25 @@ impl GpuRenderer {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn pack_heightmap(heightmap: &[i16; 16384]) -> Vec<u32> {
|
pub(crate) fn pack_heightmap(heightmap: &[i16; 16384]) -> Vec<u32> {
|
||||||
heightmap.chunks(2).map(|chunk| {
|
heightmap
|
||||||
|
.chunks(2)
|
||||||
|
.map(|chunk| {
|
||||||
let low = chunk[0] as u32 & 0xFFFF;
|
let low = chunk[0] as u32 & 0xFFFF;
|
||||||
let high = chunk[1] as u32 & 0xFFFF;
|
let high = chunk[1] as u32 & 0xFFFF;
|
||||||
low | (high << 16)
|
low | (high << 16)
|
||||||
}).collect()
|
})
|
||||||
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn pack_terrain(terrain: &[u8; 16384]) -> Vec<u32> {
|
pub(crate) fn pack_terrain(terrain: &[u8; 16384]) -> Vec<u32> {
|
||||||
terrain.chunks(4).map(|chunk| {
|
terrain
|
||||||
chunk[0] as u32 | (chunk[1] as u32) << 8 | (chunk[2] as u32) << 16 | (chunk[3] as u32) << 24
|
.chunks(4)
|
||||||
}).collect()
|
.map(|chunk| {
|
||||||
|
chunk[0] as u32
|
||||||
|
| (chunk[1] as u32) << 8
|
||||||
|
| (chunk[2] as u32) << 16
|
||||||
|
| (chunk[3] as u32) << 24
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,21 +2,15 @@ use crate::renderer::GpuRenderer;
|
||||||
use crate::renderer::Uniforms;
|
use crate::renderer::Uniforms;
|
||||||
|
|
||||||
fn as_u8_slice<T>(data: &[T]) -> &[u8] {
|
fn as_u8_slice<T>(data: &[T]) -> &[u8] {
|
||||||
unsafe {
|
// SAFETY: Reinterprets `&[T]` as `&[u8]` for GPU buffer upload.
|
||||||
std::slice::from_raw_parts(
|
// T is a primitive type with no padding, and the byte layout is stable.
|
||||||
data.as_ptr() as *const u8,
|
unsafe { std::slice::from_raw_parts(data.as_ptr() as *const u8, std::mem::size_of_val(data)) }
|
||||||
std::mem::size_of_val(data),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn as_u8_value<T>(data: &T) -> &[u8] {
|
fn as_u8_value<T>(data: &T) -> &[u8] {
|
||||||
unsafe {
|
// SAFETY: Reinterprets a single `&T` as `&[u8]` for GPU buffer upload.
|
||||||
std::slice::from_raw_parts(
|
// T is a primitive type with no padding, and the byte layout is stable.
|
||||||
data as *const T as *const u8,
|
unsafe { std::slice::from_raw_parts(data as *const T as *const u8, std::mem::size_of::<T>()) }
|
||||||
std::mem::size_of::<T>(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl GpuRenderer {
|
impl GpuRenderer {
|
||||||
|
|
@ -39,8 +33,8 @@ impl GpuRenderer {
|
||||||
self.last_show_heightmap = show_heightmap;
|
self.last_show_heightmap = show_heightmap;
|
||||||
self.needs_upload = false;
|
self.needs_upload = false;
|
||||||
|
|
||||||
let min_height = *heightmap.iter().min().unwrap();
|
let min_height = *heightmap.iter().min().unwrap_or(&0);
|
||||||
let max_height = *heightmap.iter().max().unwrap();
|
let max_height = *heightmap.iter().max().unwrap_or(&255);
|
||||||
|
|
||||||
let packed_heightmap = Self::pack_heightmap(heightmap);
|
let packed_heightmap = Self::pack_heightmap(heightmap);
|
||||||
self.queue
|
self.queue
|
||||||
|
|
|
||||||
|
|
@ -1,21 +1,45 @@
|
||||||
|
use crate::app::{
|
||||||
|
LAYER_BIOME_IDX, LAYER_CAVES_IDX, LAYER_FROST_IDX, LAYER_RESOURCES_IDX, LAYER_RIVER_IDX,
|
||||||
|
LAYER_TREES_IDX,
|
||||||
|
};
|
||||||
use egui::{Color32, ColorImage, TextureOptions, Vec2};
|
use egui::{Color32, ColorImage, TextureOptions, Vec2};
|
||||||
use terrafier_core::model::tile::{TILE_SIZE, LayerBuffer};
|
use terrafier_core::model::layers::{
|
||||||
use terrafier_core::model::layers::{LAYER_CAVES, LAYER_RIVER, LAYER_FROST, LAYER_TREES, LAYER_BIOME, LAYER_RESOURCES};
|
LAYER_BIOME, LAYER_CAVES, LAYER_FROST, LAYER_RESOURCES, LAYER_RIVER, LAYER_TREES,
|
||||||
use crate::app::{LAYER_CAVES_IDX, LAYER_RIVER_IDX, LAYER_FROST_IDX, LAYER_TREES_IDX, LAYER_BIOME_IDX, LAYER_RESOURCES_IDX};
|
};
|
||||||
|
use terrafier_core::model::tile::{LayerBuffer, TILE_SIZE};
|
||||||
|
|
||||||
const LAYER_IDS: [u32; 6] = [LAYER_CAVES, LAYER_RIVER, LAYER_FROST, LAYER_TREES, LAYER_BIOME, LAYER_RESOURCES];
|
const LAYER_IDS: [u32; 6] = [
|
||||||
|
LAYER_CAVES,
|
||||||
|
LAYER_RIVER,
|
||||||
|
LAYER_FROST,
|
||||||
|
LAYER_TREES,
|
||||||
|
LAYER_BIOME,
|
||||||
|
LAYER_RESOURCES,
|
||||||
|
];
|
||||||
|
|
||||||
fn layer_value(tile: &terrafier_core::model::tile::Tile, idx: usize, layer_idx: usize) -> Option<f32> {
|
fn layer_value(
|
||||||
|
tile: &terrafier_core::model::tile::Tile,
|
||||||
|
idx: usize,
|
||||||
|
layer_idx: usize,
|
||||||
|
) -> Option<f32> {
|
||||||
let layer_id = LAYER_IDS[layer_idx];
|
let layer_id = LAYER_IDS[layer_idx];
|
||||||
let buf = tile.layer_data.get(&layer_id)?;
|
let buf = tile.layer_data.get(&layer_id)?;
|
||||||
let val = match buf {
|
let val = match buf {
|
||||||
LayerBuffer::Bit(data) => {
|
LayerBuffer::Bit(data) => {
|
||||||
let word = data.get(idx / 64)?;
|
let word = data.get(idx / 64)?;
|
||||||
Some(if (word >> (idx % 64)) & 1 != 0 { 1.0 } else { 0.0 })
|
Some(if (word >> (idx % 64)) & 1 != 0 {
|
||||||
|
1.0
|
||||||
|
} else {
|
||||||
|
0.0
|
||||||
|
})
|
||||||
}
|
}
|
||||||
LayerBuffer::Nibble(data) => {
|
LayerBuffer::Nibble(data) => {
|
||||||
let byte = *data.get(idx / 2)?;
|
let byte = *data.get(idx / 2)?;
|
||||||
let nib = if idx % 2 == 0 { byte & 0x0F } else { (byte >> 4) & 0x0F };
|
let nib = if idx % 2 == 0 {
|
||||||
|
byte & 0x0F
|
||||||
|
} else {
|
||||||
|
(byte >> 4) & 0x0F
|
||||||
|
};
|
||||||
Some(nib as f32 / 15.0)
|
Some(nib as f32 / 15.0)
|
||||||
}
|
}
|
||||||
LayerBuffer::Byte(data) => {
|
LayerBuffer::Byte(data) => {
|
||||||
|
|
@ -65,7 +89,9 @@ const TERRAIN_COLORS: [Color32; 7] = [
|
||||||
];
|
];
|
||||||
|
|
||||||
pub fn render(ui: &mut egui::Ui, app: &mut crate::app::TerrafierApp) {
|
pub fn render(ui: &mut egui::Ui, app: &mut crate::app::TerrafierApp) {
|
||||||
let Some(ref world) = app.world else { return; };
|
let Some(ref world) = app.world else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
let dim = &world.dimensions[0];
|
let dim = &world.dimensions[0];
|
||||||
|
|
||||||
let mut min_tx = i32::MAX;
|
let mut min_tx = i32::MAX;
|
||||||
|
|
@ -79,20 +105,22 @@ pub fn render(ui: &mut egui::Ui, app: &mut crate::app::TerrafierApp) {
|
||||||
max_tz = max_tz.max(tz);
|
max_tz = max_tz.max(tz);
|
||||||
}
|
}
|
||||||
|
|
||||||
let display_size = 64u32;
|
let base_size = 64u32;
|
||||||
let grid_w = ((max_tx - min_tx + 1) * display_size as i32) as usize;
|
let zoom = app.view_zoom;
|
||||||
let grid_h = ((max_tz - min_tz + 1) * display_size as i32) as usize;
|
|
||||||
|
let grid_w = ((max_tx - min_tx + 1) * base_size as i32) as usize;
|
||||||
|
let grid_h = ((max_tz - min_tz + 1) * base_size as i32) as usize;
|
||||||
|
|
||||||
let mut img_data = vec![0u8; grid_w * grid_h * 4];
|
let mut img_data = vec![0u8; grid_w * grid_h * 4];
|
||||||
|
|
||||||
for (&(tx, tz), tile) in &dim.tiles {
|
for (&(tx, tz), tile) in &dim.tiles {
|
||||||
let px = ((tx - min_tx) * display_size as i32) as usize;
|
let px = ((tx - min_tx) * base_size as i32) as usize;
|
||||||
let pz = ((tz - min_tz) * display_size as i32) as usize;
|
let pz = ((tz - min_tz) * base_size as i32) as usize;
|
||||||
|
|
||||||
for ly in 0..display_size as usize {
|
for ly in 0..base_size as usize {
|
||||||
for lx in 0..display_size as usize {
|
for lx in 0..base_size as usize {
|
||||||
let sx = (lx * TILE_SIZE / display_size as usize).min(TILE_SIZE - 1);
|
let sx = (lx * TILE_SIZE / base_size as usize).min(TILE_SIZE - 1);
|
||||||
let sz = (ly * TILE_SIZE / display_size as usize).min(TILE_SIZE - 1);
|
let sz = (ly * TILE_SIZE / base_size as usize).min(TILE_SIZE - 1);
|
||||||
let idx = sz * TILE_SIZE + sx;
|
let idx = sz * TILE_SIZE + sx;
|
||||||
|
|
||||||
let terrain_type = tile.terrain[idx] as usize;
|
let terrain_type = tile.terrain[idx] as usize;
|
||||||
|
|
@ -127,7 +155,11 @@ pub fn render(ui: &mut egui::Ui, app: &mut crate::app::TerrafierApp) {
|
||||||
if app.layer_visible[li] {
|
if app.layer_visible[li] {
|
||||||
if let Some(val) = layer_value(tile, idx, li) {
|
if let Some(val) = layer_value(tile, idx, li) {
|
||||||
if let Some(over) = layer_overlay(li, val) {
|
if let Some(over) = layer_overlay(li, val) {
|
||||||
let alpha = if val > 0.0 { (val * 0.6 + 0.2).min(0.8) } else { 0.0 };
|
let alpha = if val > 0.0 {
|
||||||
|
(val * 0.6 + 0.2).min(0.8)
|
||||||
|
} else {
|
||||||
|
0.0
|
||||||
|
};
|
||||||
(r, g, b) = blend_overlay((r, g, b), over, alpha);
|
(r, g, b) = blend_overlay((r, g, b), over, alpha);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -156,13 +188,35 @@ pub fn render(ui: &mut egui::Ui, app: &mut crate::app::TerrafierApp) {
|
||||||
id
|
id
|
||||||
};
|
};
|
||||||
|
|
||||||
let desired_size = Vec2::new(grid_w as f32, grid_h as f32);
|
// Viewport: allocate a large area for the world with scroll
|
||||||
let (response, painter) = ui.allocate_painter(desired_size, egui::Sense::click());
|
let viewport_size = Vec2::new(grid_w as f32 * zoom, grid_h as f32 * zoom);
|
||||||
|
let (response, painter) = ui.allocate_painter(
|
||||||
|
viewport_size,
|
||||||
|
egui::Sense::click_and_drag(),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Handle scroll-wheel zoom
|
||||||
|
let scroll_delta = ui.input(|i| i.raw_scroll_delta);
|
||||||
|
if scroll_delta.y != 0.0 {
|
||||||
|
app.view_zoom = (app.view_zoom * (1.0 + scroll_delta.y * 0.002)).clamp(0.1, 10.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle drag-to-pan
|
||||||
|
if response.dragged() {
|
||||||
|
app.view_offset += response.drag_delta();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recalculate scaled size after potential zoom change
|
||||||
|
let scaled_size = base_size as f32 * app.view_zoom;
|
||||||
|
let offset = app.view_offset;
|
||||||
|
|
||||||
let rect = response.rect;
|
let rect = response.rect;
|
||||||
painter.image(
|
painter.image(
|
||||||
texture_id,
|
texture_id,
|
||||||
rect,
|
egui::Rect::from_min_max(
|
||||||
|
egui::pos2(rect.min.x + offset.x, rect.min.y + offset.y),
|
||||||
|
egui::pos2(rect.min.x + offset.x + grid_w as f32 * app.view_zoom, rect.min.y + offset.y + grid_h as f32 * app.view_zoom),
|
||||||
|
),
|
||||||
egui::Rect::from_min_max(egui::pos2(0.0, 0.0), egui::pos2(1.0, 1.0)),
|
egui::Rect::from_min_max(egui::pos2(0.0, 0.0), egui::pos2(1.0, 1.0)),
|
||||||
Color32::WHITE,
|
Color32::WHITE,
|
||||||
);
|
);
|
||||||
|
|
@ -173,10 +227,10 @@ pub fn render(ui: &mut egui::Ui, app: &mut crate::app::TerrafierApp) {
|
||||||
&& sel_tz >= min_tz
|
&& sel_tz >= min_tz
|
||||||
&& sel_tz <= max_tz
|
&& sel_tz <= max_tz
|
||||||
{
|
{
|
||||||
let bx = rect.min.x + (sel_tx - min_tx) as f32 * display_size as f32;
|
let bx = rect.min.x + offset.x + (sel_tx - min_tx) as f32 * scaled_size;
|
||||||
let bz = rect.min.y + (sel_tz - min_tz) as f32 * display_size as f32;
|
let bz = rect.min.y + offset.y + (sel_tz - min_tz) as f32 * scaled_size;
|
||||||
let border_rect =
|
let border_rect =
|
||||||
egui::Rect::from_min_size(egui::pos2(bx, bz), Vec2::splat(display_size as f32));
|
egui::Rect::from_min_size(egui::pos2(bx, bz), Vec2::splat(scaled_size));
|
||||||
painter.rect_stroke(
|
painter.rect_stroke(
|
||||||
border_rect,
|
border_rect,
|
||||||
0.0,
|
0.0,
|
||||||
|
|
@ -188,13 +242,13 @@ pub fn render(ui: &mut egui::Ui, app: &mut crate::app::TerrafierApp) {
|
||||||
if let (Some((sel_tx, sel_tz)), Some(bx), Some(bz)) =
|
if let (Some((sel_tx, sel_tz)), Some(bx), Some(bz)) =
|
||||||
(app.selected_tile, app.brush_local_x, app.brush_local_z)
|
(app.selected_tile, app.brush_local_x, app.brush_local_z)
|
||||||
{
|
{
|
||||||
let tile_x_in_pixels = (sel_tx - min_tx) as f32 * display_size as f32;
|
let tile_x_in_pixels = (sel_tx - min_tx) as f32 * scaled_size;
|
||||||
let tile_z_in_pixels = (sel_tz - min_tz) as f32 * display_size as f32;
|
let tile_z_in_pixels = (sel_tz - min_tz) as f32 * scaled_size;
|
||||||
let brush_x =
|
let brush_x =
|
||||||
rect.min.x + tile_x_in_pixels + (bx as f32 * display_size as f32 / TILE_SIZE as f32);
|
rect.min.x + offset.x + tile_x_in_pixels + (bx as f32 * scaled_size / TILE_SIZE as f32);
|
||||||
let brush_z =
|
let brush_z =
|
||||||
rect.min.y + tile_z_in_pixels + (bz as f32 * display_size as f32 / TILE_SIZE as f32);
|
rect.min.y + offset.y + tile_z_in_pixels + (bz as f32 * scaled_size / TILE_SIZE as f32);
|
||||||
let brush_radius_px = app.brush_radius as f32 * display_size as f32 / TILE_SIZE as f32;
|
let brush_radius_px = app.brush_radius as f32 * scaled_size / TILE_SIZE as f32;
|
||||||
painter.circle_stroke(
|
painter.circle_stroke(
|
||||||
egui::pos2(brush_x, brush_z),
|
egui::pos2(brush_x, brush_z),
|
||||||
brush_radius_px.max(2.0),
|
brush_radius_px.max(2.0),
|
||||||
|
|
@ -202,20 +256,21 @@ pub fn render(ui: &mut egui::Ui, app: &mut crate::app::TerrafierApp) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Click-to-select with inverse transform
|
||||||
if response.clicked()
|
if response.clicked()
|
||||||
&& let Some(pos) = response.interact_pointer_pos()
|
&& let Some(pos) = response.interact_pointer_pos()
|
||||||
{
|
{
|
||||||
let lx = pos.x - rect.min.x;
|
let lx = pos.x - rect.min.x - offset.x;
|
||||||
let lz = pos.y - rect.min.y;
|
let lz = pos.y - rect.min.y - offset.y;
|
||||||
if lx >= 0.0 && lz >= 0.0 {
|
if lx >= 0.0 && lz >= 0.0 {
|
||||||
let tx = (lx / display_size as f32).floor() as i32 + min_tx;
|
let tx = (lx / scaled_size).floor() as i32 + min_tx;
|
||||||
let tz = (lz / display_size as f32).floor() as i32 + min_tz;
|
let tz = (lz / scaled_size).floor() as i32 + min_tz;
|
||||||
if dim.tiles.contains_key(&(tx, tz)) {
|
if dim.tiles.contains_key(&(tx, tz)) {
|
||||||
app.selected_tile = Some((tx, tz));
|
app.selected_tile = Some((tx, tz));
|
||||||
let local_x =
|
let local_x =
|
||||||
((lx as u32 % display_size) * TILE_SIZE as u32 / display_size).min(127);
|
((lx as u32 % scaled_size as u32) * TILE_SIZE as u32 / scaled_size as u32).min(127);
|
||||||
let local_z =
|
let local_z =
|
||||||
((lz as u32 % display_size) * TILE_SIZE as u32 / display_size).min(127);
|
((lz as u32 % scaled_size as u32) * TILE_SIZE as u32 / scaled_size as u32).min(127);
|
||||||
app.brush_local_x = Some(local_x);
|
app.brush_local_x = Some(local_x);
|
||||||
app.brush_local_z = Some(local_z);
|
app.brush_local_z = Some(local_z);
|
||||||
app.status_message = format!(
|
app.status_message = format!(
|
||||||
|
|
|
||||||
|
|
@ -11,18 +11,30 @@ pub fn try_render(ui: &mut egui::Ui, app: &mut crate::app::TerrafierApp) -> bool
|
||||||
&& let Some(texture_id) = renderer.texture_id
|
&& let Some(texture_id) = renderer.texture_id
|
||||||
{
|
{
|
||||||
let desired_size = Vec2::new(512.0, 512.0);
|
let desired_size = Vec2::new(512.0, 512.0);
|
||||||
let (response, painter) = ui.allocate_painter(desired_size, egui::Sense::click());
|
let (response, painter) = ui.allocate_painter(
|
||||||
let rect = response.rect;
|
desired_size,
|
||||||
painter.image(
|
egui::Sense::click_and_drag(),
|
||||||
texture_id,
|
|
||||||
rect,
|
|
||||||
egui::Rect::from_min_max(egui::pos2(0.0, 0.0), egui::pos2(1.0, 1.0)),
|
|
||||||
Color32::WHITE,
|
|
||||||
);
|
);
|
||||||
|
let rect = response.rect;
|
||||||
|
|
||||||
let display_size = rect.width() / 4.0;
|
// Handle scroll-wheel zoom
|
||||||
|
let scroll_delta = ui.input(|i| i.raw_scroll_delta);
|
||||||
|
if scroll_delta.y != 0.0 {
|
||||||
|
app.view_zoom = (app.view_zoom * (1.0 + scroll_delta.y * 0.002)).clamp(0.1, 10.0);
|
||||||
|
}
|
||||||
|
|
||||||
if let Some((sel_tx, sel_tz)) = app.selected_tile {
|
// Handle drag-to-pan
|
||||||
|
if response.dragged() {
|
||||||
|
app.view_offset += response.drag_delta();
|
||||||
|
}
|
||||||
|
|
||||||
|
let zoom = app.view_zoom;
|
||||||
|
let offset = app.view_offset;
|
||||||
|
|
||||||
|
let base_display_size = rect.width() / 4.0;
|
||||||
|
let display_size = base_display_size * zoom;
|
||||||
|
|
||||||
|
// Find tile bounds for positioning
|
||||||
let mut min_tx = i32::MAX;
|
let mut min_tx = i32::MAX;
|
||||||
let mut max_tx = i32::MIN;
|
let mut max_tx = i32::MIN;
|
||||||
let mut min_tz = i32::MAX;
|
let mut min_tz = i32::MAX;
|
||||||
|
|
@ -34,14 +46,29 @@ pub fn try_render(ui: &mut egui::Ui, app: &mut crate::app::TerrafierApp) -> bool
|
||||||
max_tz = max_tz.max(tz);
|
max_tz = max_tz.max(tz);
|
||||||
}
|
}
|
||||||
|
|
||||||
let bx = rect.min.x;
|
// Draw the GPU-rendered tile texture at the correct position
|
||||||
let bz = rect.min.y;
|
// GPU only renders the first tile - position it correctly
|
||||||
let sel_bx = bx + (sel_tx - min_tx) as f32 * display_size;
|
// Use a default position for the rendered tile
|
||||||
let sel_bz = bz + (sel_tz - min_tz) as f32 * display_size;
|
let first_tile_pos = dim.tiles.keys().next().copied().unwrap_or((0, 0));
|
||||||
let border_rect = egui::Rect::from_min_size(
|
let tex_origin_x = rect.min.x + offset.x + (first_tile_pos.0 - min_tx) as f32 * display_size;
|
||||||
egui::pos2(sel_bx, sel_bz),
|
let tex_origin_y = rect.min.y + offset.y + (first_tile_pos.1 - min_tz) as f32 * display_size;
|
||||||
|
|
||||||
|
let tex_rect = egui::Rect::from_min_size(
|
||||||
|
egui::pos2(tex_origin_x, tex_origin_y),
|
||||||
Vec2::splat(display_size),
|
Vec2::splat(display_size),
|
||||||
);
|
);
|
||||||
|
painter.image(
|
||||||
|
texture_id,
|
||||||
|
tex_rect,
|
||||||
|
egui::Rect::from_min_max(egui::pos2(0.0, 0.0), egui::pos2(1.0, 1.0)),
|
||||||
|
Color32::WHITE,
|
||||||
|
);
|
||||||
|
|
||||||
|
if let Some((sel_tx, sel_tz)) = app.selected_tile {
|
||||||
|
let bx = rect.min.x + offset.x + (sel_tx - min_tx) as f32 * display_size;
|
||||||
|
let bz = rect.min.y + offset.y + (sel_tz - min_tz) as f32 * display_size;
|
||||||
|
let border_rect =
|
||||||
|
egui::Rect::from_min_size(egui::pos2(bx, bz), Vec2::splat(display_size));
|
||||||
painter.rect_stroke(
|
painter.rect_stroke(
|
||||||
border_rect,
|
border_rect,
|
||||||
0.0,
|
0.0,
|
||||||
|
|
@ -53,18 +80,12 @@ pub fn try_render(ui: &mut egui::Ui, app: &mut crate::app::TerrafierApp) -> bool
|
||||||
if let (Some((sel_tx, sel_tz)), Some(bx), Some(bz)) =
|
if let (Some((sel_tx, sel_tz)), Some(bx), Some(bz)) =
|
||||||
(app.selected_tile, app.brush_local_x, app.brush_local_z)
|
(app.selected_tile, app.brush_local_x, app.brush_local_z)
|
||||||
{
|
{
|
||||||
let mut min_tx = i32::MAX;
|
|
||||||
let mut min_tz = i32::MAX;
|
|
||||||
for &(tx, tz) in dim.tiles.keys() {
|
|
||||||
min_tx = min_tx.min(tx);
|
|
||||||
min_tz = min_tz.min(tz);
|
|
||||||
}
|
|
||||||
let tile_x_in_pixels = (sel_tx - min_tx) as f32 * display_size;
|
let tile_x_in_pixels = (sel_tx - min_tx) as f32 * display_size;
|
||||||
let tile_z_in_pixels = (sel_tz - min_tz) as f32 * display_size;
|
let tile_z_in_pixels = (sel_tz - min_tz) as f32 * display_size;
|
||||||
let brush_x = rect.min.x + tile_x_in_pixels
|
let brush_x =
|
||||||
+ (bx as f32 * display_size / TILE_SIZE as f32);
|
rect.min.x + offset.x + tile_x_in_pixels + (bx as f32 * display_size / TILE_SIZE as f32);
|
||||||
let brush_z = rect.min.y + tile_z_in_pixels
|
let brush_z =
|
||||||
+ (bz as f32 * display_size / TILE_SIZE as f32);
|
rect.min.y + offset.y + tile_z_in_pixels + (bz as f32 * display_size / TILE_SIZE as f32);
|
||||||
let brush_radius_px = app.brush_radius as f32 * display_size / TILE_SIZE as f32;
|
let brush_radius_px = app.brush_radius as f32 * display_size / TILE_SIZE as f32;
|
||||||
painter.circle_stroke(
|
painter.circle_stroke(
|
||||||
egui::pos2(brush_x, brush_z),
|
egui::pos2(brush_x, brush_z),
|
||||||
|
|
@ -76,23 +97,19 @@ pub fn try_render(ui: &mut egui::Ui, app: &mut crate::app::TerrafierApp) -> bool
|
||||||
if response.clicked()
|
if response.clicked()
|
||||||
&& let Some(pos) = response.interact_pointer_pos()
|
&& let Some(pos) = response.interact_pointer_pos()
|
||||||
{
|
{
|
||||||
let lx = pos.x - rect.min.x;
|
let lx = pos.x - rect.min.x - offset.x;
|
||||||
let lz = pos.y - rect.min.y;
|
let lz = pos.y - rect.min.y - offset.y;
|
||||||
if lx >= 0.0 && lz >= 0.0 {
|
if lx >= 0.0 && lz >= 0.0 {
|
||||||
let mut min_tx = i32::MAX;
|
|
||||||
let mut min_tz = i32::MAX;
|
|
||||||
for &(tx, tz) in dim.tiles.keys() {
|
|
||||||
min_tx = min_tx.min(tx);
|
|
||||||
min_tz = min_tz.min(tz);
|
|
||||||
}
|
|
||||||
let tx = (lx / display_size).floor() as i32 + min_tx;
|
let tx = (lx / display_size).floor() as i32 + min_tx;
|
||||||
let tz = (lz / display_size).floor() as i32 + min_tz;
|
let tz = (lz / display_size).floor() as i32 + min_tz;
|
||||||
if dim.tiles.contains_key(&(tx, tz)) {
|
if dim.tiles.contains_key(&(tx, tz)) {
|
||||||
app.selected_tile = Some((tx, tz));
|
app.selected_tile = Some((tx, tz));
|
||||||
let local_x =
|
let local_x = ((lx as u32 % display_size as u32) * TILE_SIZE as u32
|
||||||
((lx as u32 % display_size as u32) * TILE_SIZE as u32 / display_size as u32).min(127);
|
/ display_size as u32)
|
||||||
let local_z =
|
.min(127);
|
||||||
((lz as u32 % display_size as u32) * TILE_SIZE as u32 / display_size as u32).min(127);
|
let local_z = ((lz as u32 % display_size as u32) * TILE_SIZE as u32
|
||||||
|
/ display_size as u32)
|
||||||
|
.min(127);
|
||||||
app.brush_local_x = Some(local_x);
|
app.brush_local_x = Some(local_x);
|
||||||
app.brush_local_z = Some(local_z);
|
app.brush_local_z = Some(local_z);
|
||||||
app.status_message = format!(
|
app.status_message = format!(
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,14 @@
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use terrafier_core::Terrain;
|
use terrafier_core::Terrain;
|
||||||
use terrafier_core::model::types::SymmetricBrush;
|
|
||||||
use terrafier_core::model::tile::TILE_SIZE;
|
use terrafier_core::model::tile::TILE_SIZE;
|
||||||
|
use terrafier_core::model::types::SymmetricBrush;
|
||||||
use terrafier_core::ops::operations::{
|
use terrafier_core::ops::operations::{
|
||||||
ErodeOperation, FillOperation, FlattenOperation, HeightOperation, MultiTileOperation,
|
ErodeOperation, FillOperation, FlattenOperation, HeightOperation, MultiTileOperation,
|
||||||
Operation, PaintOperation, SmoothOperation,
|
Operation, PaintOperation, SmoothOperation,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::app::{TerrafierApp, ToolMode, LAYER_NAMES};
|
use crate::app::{LAYER_NAMES, TerrafierApp, ToolMode};
|
||||||
|
|
||||||
pub fn show_tools_panel(ui: &mut egui::Ui, app: &mut TerrafierApp) {
|
pub fn show_tools_panel(ui: &mut egui::Ui, app: &mut TerrafierApp) {
|
||||||
ui.heading("Tools");
|
ui.heading("Tools");
|
||||||
|
|
@ -83,7 +83,10 @@ fn apply_tool(app: &mut TerrafierApp) {
|
||||||
let Some(ref mut world) = app.world else {
|
let Some(ref mut world) = app.world else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
let dim = world.overworld_mut().expect("no overworld");
|
let Some(dim) = world.overworld_mut() else {
|
||||||
|
app.status_message = "World has no overworld".to_string();
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
let center = app.brush_local_x.unwrap_or(64);
|
let center = app.brush_local_x.unwrap_or(64);
|
||||||
let center_z = app.brush_local_z.unwrap_or(64);
|
let center_z = app.brush_local_z.unwrap_or(64);
|
||||||
|
|
|
||||||
228
gui/wix/main.wxs
Normal file
228
gui/wix/main.wxs
Normal file
|
|
@ -0,0 +1,228 @@
|
||||||
|
<?xml version='1.0' encoding='windows-1252'?>
|
||||||
|
<!--
|
||||||
|
Copyright (C) 2017 Christopher R. Field.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<!--
|
||||||
|
The "cargo wix" subcommand provides a variety of predefined variables available
|
||||||
|
for customization of this template. The values for each variable are set at
|
||||||
|
installer creation time. The following variables are available:
|
||||||
|
|
||||||
|
TargetTriple = The rustc target triple name.
|
||||||
|
TargetEnv = The rustc target environment. This is typically either
|
||||||
|
"msvc" or "gnu" depending on the toolchain downloaded and
|
||||||
|
installed.
|
||||||
|
TargetVendor = The rustc target vendor. This is typically "pc", but Rust
|
||||||
|
does support other vendors, like "uwp".
|
||||||
|
CargoTargetBinDir = The complete path to the directory containing the
|
||||||
|
binaries (exes) to include. The default would be
|
||||||
|
"target\release\". If an explicit rustc target triple is
|
||||||
|
used, i.e. cross-compiling, then the default path would
|
||||||
|
be "target\<CARGO_TARGET>\<CARGO_PROFILE>",
|
||||||
|
where "<CARGO_TARGET>" is replaced with the "CargoTarget"
|
||||||
|
variable value and "<CARGO_PROFILE>" is replaced with the
|
||||||
|
value from the "CargoProfile" variable. This can also
|
||||||
|
be overridden manually with the "target-bin-dir" flag.
|
||||||
|
CargoTargetDir = The path to the directory for the build artifacts, i.e.
|
||||||
|
"target".
|
||||||
|
CargoProfile = The cargo profile used to build the binaries
|
||||||
|
(usually "debug" or "release").
|
||||||
|
Version = The version for the installer. The default is the
|
||||||
|
"Major.Minor.Fix" semantic versioning number of the Rust
|
||||||
|
package.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Please do not remove these pre-processor If-Else blocks. These are used with
|
||||||
|
the `cargo wix` subcommand to automatically determine the installation
|
||||||
|
destination for 32-bit versus 64-bit installers. Removal of these lines will
|
||||||
|
cause installation errors.
|
||||||
|
-->
|
||||||
|
<?if $(sys.BUILDARCH) = x64 or $(sys.BUILDARCH) = arm64 ?>
|
||||||
|
<?define PlatformProgramFilesFolder = "ProgramFiles64Folder" ?>
|
||||||
|
<?else ?>
|
||||||
|
<?define PlatformProgramFilesFolder = "ProgramFilesFolder" ?>
|
||||||
|
<?endif ?>
|
||||||
|
|
||||||
|
<Wix xmlns='http://schemas.microsoft.com/wix/2006/wi'>
|
||||||
|
|
||||||
|
<Product
|
||||||
|
Id='*'
|
||||||
|
Name='terrafier-gui'
|
||||||
|
UpgradeCode='A7365100-6923-4DAC-B4A5-2BD817EB802B'
|
||||||
|
Manufacturer='Loki'
|
||||||
|
Language='1033'
|
||||||
|
Codepage='1252'
|
||||||
|
Version='$(var.Version)'>
|
||||||
|
|
||||||
|
<Package Id='*'
|
||||||
|
Keywords='Installer'
|
||||||
|
Description='Native GUI for Terrafier — interactive world painting with GPU rendering'
|
||||||
|
Manufacturer='Loki'
|
||||||
|
InstallerVersion='450'
|
||||||
|
Languages='1033'
|
||||||
|
Compressed='yes'
|
||||||
|
InstallScope='perMachine'
|
||||||
|
SummaryCodepage='1252'
|
||||||
|
/>
|
||||||
|
|
||||||
|
<MajorUpgrade
|
||||||
|
Schedule='afterInstallInitialize'
|
||||||
|
DowngradeErrorMessage='A newer version of [ProductName] is already installed. Setup will now exit.'/>
|
||||||
|
|
||||||
|
<Media Id='1' Cabinet='media1.cab' EmbedCab='yes' DiskPrompt='CD-ROM #1'/>
|
||||||
|
<Property Id='DiskPrompt' Value='terrafier-gui Installation'/>
|
||||||
|
|
||||||
|
<Directory Id='TARGETDIR' Name='SourceDir'>
|
||||||
|
<Directory Id='$(var.PlatformProgramFilesFolder)' Name='PFiles'>
|
||||||
|
<Directory Id='APPLICATIONFOLDER' Name='terrafier-gui'>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Enabling the license sidecar file in the installer is a four step process:
|
||||||
|
|
||||||
|
1. Uncomment the `Component` tag and its contents.
|
||||||
|
2. Change the value for the `Source` attribute in the `File` tag to a path
|
||||||
|
to the file that should be included as the license sidecar file. The path
|
||||||
|
can, and probably should be, relative to this file.
|
||||||
|
3. Change the value for the `Name` attribute in the `File` tag to the
|
||||||
|
desired name for the file when it is installed alongside the `bin` folder
|
||||||
|
in the installation directory. This can be omitted if the desired name is
|
||||||
|
the same as the file name.
|
||||||
|
4. Uncomment the `ComponentRef` tag with the Id attribute value of "License"
|
||||||
|
further down in this file.
|
||||||
|
-->
|
||||||
|
<!--
|
||||||
|
<Component Id='License' Guid='*'>
|
||||||
|
<File Id='LicenseFile' Name='ChangeMe' DiskId='1' Source='C:\Path\To\File' KeyPath='yes'/>
|
||||||
|
</Component>
|
||||||
|
-->
|
||||||
|
|
||||||
|
<Directory Id='Bin' Name='bin'>
|
||||||
|
<Component Id='Path' Guid='097E8AD4-180B-4B52-8BFE-04AA942D03A6' KeyPath='yes'>
|
||||||
|
<Environment
|
||||||
|
Id='PATH'
|
||||||
|
Name='PATH'
|
||||||
|
Value='[Bin]'
|
||||||
|
Permanent='no'
|
||||||
|
Part='last'
|
||||||
|
Action='set'
|
||||||
|
System='yes'/>
|
||||||
|
</Component>
|
||||||
|
<Component Id='binary0' Guid='*'>
|
||||||
|
<File
|
||||||
|
Id='exe0'
|
||||||
|
Name='terrafier-gui.exe'
|
||||||
|
DiskId='1'
|
||||||
|
Source='$(var.CargoTargetBinDir)\terrafier-gui.exe'
|
||||||
|
KeyPath='yes'/>
|
||||||
|
</Component>
|
||||||
|
</Directory>
|
||||||
|
</Directory>
|
||||||
|
</Directory>
|
||||||
|
</Directory>
|
||||||
|
|
||||||
|
<Feature
|
||||||
|
Id='Binaries'
|
||||||
|
Title='Application'
|
||||||
|
Description='Installs all binaries and the license.'
|
||||||
|
Level='1'
|
||||||
|
ConfigurableDirectory='APPLICATIONFOLDER'
|
||||||
|
AllowAdvertise='no'
|
||||||
|
Display='expand'
|
||||||
|
Absent='disallow'>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Uncomment the following `ComponentRef` tag to add the license
|
||||||
|
sidecar file to the installer.
|
||||||
|
-->
|
||||||
|
<!--<ComponentRef Id='License'/>-->
|
||||||
|
|
||||||
|
<ComponentRef Id='binary0'/>
|
||||||
|
|
||||||
|
<Feature
|
||||||
|
Id='Environment'
|
||||||
|
Title='PATH Environment Variable'
|
||||||
|
Description='Add the install location of the [ProductName] executable to the PATH system environment variable. This allows the [ProductName] executable to be called from any location.'
|
||||||
|
Level='1'
|
||||||
|
Absent='allow'>
|
||||||
|
<ComponentRef Id='Path'/>
|
||||||
|
</Feature>
|
||||||
|
</Feature>
|
||||||
|
|
||||||
|
<SetProperty Id='ARPINSTALLLOCATION' Value='[APPLICATIONFOLDER]' After='CostFinalize'/>
|
||||||
|
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Uncomment the following `Icon` and `Property` tags to change the product icon.
|
||||||
|
|
||||||
|
The product icon is the graphic that appears in the Add/Remove
|
||||||
|
Programs control panel for the application.
|
||||||
|
-->
|
||||||
|
<!--<Icon Id='ProductICO' SourceFile='wix\Product.ico'/>-->
|
||||||
|
<!--<Property Id='ARPPRODUCTICON' Value='ProductICO' />-->
|
||||||
|
|
||||||
|
<Property Id='ARPHELPLINK' Value='https://github.com/loki5512344/Terrafier'/>
|
||||||
|
|
||||||
|
<UI>
|
||||||
|
<UIRef Id='WixUI_FeatureTree'/>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Enabling the EULA dialog in the installer is a three step process:
|
||||||
|
|
||||||
|
1. Comment out or remove the two `Publish` tags that follow the
|
||||||
|
`WixVariable` tag.
|
||||||
|
2. Uncomment the `<WixVariable Id='WixUILicenseRtf' Value='Path\to\Eula.rft'>` tag further down
|
||||||
|
3. Replace the `Value` attribute of the `WixVariable` tag with
|
||||||
|
the path to a RTF file that will be used as the EULA and
|
||||||
|
displayed in the license agreement dialog.
|
||||||
|
-->
|
||||||
|
<Publish Dialog='WelcomeDlg' Control='Next' Event='NewDialog' Value='CustomizeDlg' Order='99'>1</Publish>
|
||||||
|
<Publish Dialog='CustomizeDlg' Control='Back' Event='NewDialog' Value='WelcomeDlg' Order='99'>1</Publish>
|
||||||
|
|
||||||
|
</UI>
|
||||||
|
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Enabling the EULA dialog in the installer requires uncommenting
|
||||||
|
the following `WixUILicenseRTF` tag and changing the `Value`
|
||||||
|
attribute.
|
||||||
|
-->
|
||||||
|
<!-- <WixVariable Id='WixUILicenseRtf' Value='Relative\Path\to\Eula.rtf'/> -->
|
||||||
|
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Uncomment the next `WixVariable` tag to customize the installer's
|
||||||
|
Graphical User Interface (GUI) and add a custom banner image across
|
||||||
|
the top of each screen. See the WiX Toolset documentation for details
|
||||||
|
about customization.
|
||||||
|
|
||||||
|
The banner BMP dimensions are 493 x 58 pixels.
|
||||||
|
-->
|
||||||
|
<!--<WixVariable Id='WixUIBannerBmp' Value='wix\Banner.bmp'/>-->
|
||||||
|
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Uncomment the next `WixVariable` tag to customize the installer's
|
||||||
|
Graphical User Interface (GUI) and add a custom image to the first
|
||||||
|
dialog, or screen. See the WiX Toolset documentation for details about
|
||||||
|
customization.
|
||||||
|
|
||||||
|
The dialog BMP dimensions are 493 x 312 pixels.
|
||||||
|
-->
|
||||||
|
<!--<WixVariable Id='WixUIDialogBmp' Value='wix\Dialog.bmp'/>-->
|
||||||
|
|
||||||
|
</Product>
|
||||||
|
|
||||||
|
</Wix>
|
||||||
7
plugins/example-noisemap/Cargo.lock
generated
Normal file
7
plugins/example-noisemap/Cargo.lock
generated
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
# This file is automatically @generated by Cargo.
|
||||||
|
# It is not intended for manual editing.
|
||||||
|
version = 4
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "terrafier-plugin-example-noisemap"
|
||||||
|
version = "0.1.0"
|
||||||
12
plugins/example-noisemap/Cargo.toml
Normal file
12
plugins/example-noisemap/Cargo.toml
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
[package]
|
||||||
|
name = "terrafier-plugin-example-noisemap"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
description = "Example WASM plugin for Terrafier — generates noise-based terrain"
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
crate-type = ["cdylib"]
|
||||||
|
|
||||||
|
[profile.release]
|
||||||
|
opt-level = "s"
|
||||||
|
lto = true
|
||||||
4
plugins/example-noisemap/build.sh
Executable file
4
plugins/example-noisemap/build.sh
Executable file
|
|
@ -0,0 +1,4 @@
|
||||||
|
#!/bin/bash
|
||||||
|
cd "$(dirname "$0")"
|
||||||
|
cargo build --target wasm32-unknown-unknown --release
|
||||||
|
echo "WASM plugin built: $(ls -lh target/wasm32-unknown-unknown/release/*.wasm 2>/dev/null || echo 'BUILD FAILED')"
|
||||||
216
plugins/example-noisemap/src/lib.rs
Normal file
216
plugins/example-noisemap/src/lib.rs
Normal file
|
|
@ -0,0 +1,216 @@
|
||||||
|
#![no_std]
|
||||||
|
|
||||||
|
// Allocator for WASM — bump allocator
|
||||||
|
// SAFETY: WASM is single-threaded; no concurrent access to BUMP is possible.
|
||||||
|
// The allocator is only called during plugin initialization before tile generation.
|
||||||
|
static mut BUMP: usize = 4096; // start after string data
|
||||||
|
|
||||||
|
#[unsafe(no_mangle)]
|
||||||
|
pub extern "C" fn alloc(size: i32) -> i32 {
|
||||||
|
// SAFETY: WASM is single-threaded; no concurrent access to BUMP is possible.
|
||||||
|
// The allocator is only called during plugin initialization before tile generation.
|
||||||
|
unsafe {
|
||||||
|
let ptr = BUMP as i32;
|
||||||
|
BUMP += size as usize;
|
||||||
|
ptr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Host imports
|
||||||
|
// SAFETY: These host functions are guaranteed by the Terrafier WASM host ABI
|
||||||
|
// to be callable from within generate_tile. The host validates all coordinates
|
||||||
|
// (0..127) internally. All set functions are idempotent and panic-free.
|
||||||
|
unsafe extern "C" {
|
||||||
|
fn host_log(level: i32, ptr: i32, len: i32);
|
||||||
|
fn host_get_seed() -> i64;
|
||||||
|
fn host_tile_size() -> i32;
|
||||||
|
fn host_get_height(x: i32, z: i32) -> i32;
|
||||||
|
fn host_set_height(x: i32, z: i32, val: i32);
|
||||||
|
fn host_get_terrain(x: i32, z: i32) -> i32;
|
||||||
|
fn host_set_terrain(x: i32, z: i32, val: i32);
|
||||||
|
fn host_get_water(x: i32, z: i32) -> i32;
|
||||||
|
fn host_set_water(x: i32, z: i32, val: i32);
|
||||||
|
// Bulk transfer (faster)
|
||||||
|
fn host_alloc_buffers() -> i32;
|
||||||
|
fn host_load_heightmap() -> i32;
|
||||||
|
fn host_load_terrain() -> i32;
|
||||||
|
fn host_load_water() -> i32;
|
||||||
|
fn host_store_heightmap();
|
||||||
|
fn host_store_terrain();
|
||||||
|
fn host_store_water();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Simple pseudo-random number generator (LCG)
|
||||||
|
// SAFETY: WASM execution is single-threaded. RNG_STATE is only accessed
|
||||||
|
// during tile generation which runs sequentially per tile.
|
||||||
|
static mut RNG_STATE: u64 = 0;
|
||||||
|
|
||||||
|
// SAFETY: WASM execution is single-threaded. RNG_STATE is only accessed
|
||||||
|
// during tile generation which runs sequentially per tile.
|
||||||
|
unsafe fn rng() -> u32 {
|
||||||
|
// SAFETY: Same single-thread guarantee as RNG_STATE declaration.
|
||||||
|
unsafe {
|
||||||
|
RNG_STATE = RNG_STATE.wrapping_mul(6364136223846793005).wrapping_add(1442695040888963407);
|
||||||
|
(RNG_STATE >> 32) as u32
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
unsafe fn rng_range(min: f64, max: f64) -> f64 {
|
||||||
|
// SAFETY: rng() only accesses RNG_STATE, which is safe under single-threaded WASM.
|
||||||
|
let r = unsafe { rng() } as f64 / u32::MAX as f64;
|
||||||
|
min + r * (max - min)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Simple 2D noise function (value noise)
|
||||||
|
unsafe fn noise_2d(x: f64, z: f64, seed: u64) -> f64 {
|
||||||
|
let xi = x as i32;
|
||||||
|
let zi = z as i32;
|
||||||
|
let xf = x - xi as f64;
|
||||||
|
let zf = z - zi as f64;
|
||||||
|
|
||||||
|
fn hash(x: i32, z: i32, seed: u64) -> f64 {
|
||||||
|
let h = (x as u64).wrapping_mul(374761393)
|
||||||
|
.wrapping_add((z as u64).wrapping_mul(668265263))
|
||||||
|
.wrapping_add(seed)
|
||||||
|
.wrapping_mul(1274126177);
|
||||||
|
((h ^ (h >> 16)) as u32) as f64 / u32::MAX as f64
|
||||||
|
}
|
||||||
|
|
||||||
|
let n00 = hash(xi, zi, seed);
|
||||||
|
let n10 = hash(xi + 1, zi, seed);
|
||||||
|
let n01 = hash(xi, zi + 1, seed);
|
||||||
|
let n11 = hash(xi + 1, zi + 1, seed);
|
||||||
|
|
||||||
|
let sx = xf * xf * (3.0 - 2.0 * xf);
|
||||||
|
let sz = zf * zf * (3.0 - 2.0 * zf);
|
||||||
|
|
||||||
|
let nx0 = n00 + (n10 - n00) * sx;
|
||||||
|
let nx1 = n01 + (n11 - n01) * sx;
|
||||||
|
nx0 + (nx1 - nx0) * sz
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper to log strings from WASM
|
||||||
|
// SAFETY: Address 0-1024 in WASM linear memory is reserved as a scratch buffer.
|
||||||
|
// The host guarantees this region is writable. The copy size is bounded by msg length (max 1024).
|
||||||
|
#[allow(invalid_null_arguments)]
|
||||||
|
unsafe fn wasm_log(level: i32, msg: &str) {
|
||||||
|
let bytes = msg.as_bytes();
|
||||||
|
let ptr = 0usize;
|
||||||
|
let len = bytes.len().min(1024);
|
||||||
|
// SAFETY: Address 0 is a valid scratch buffer in WASM linear memory.
|
||||||
|
// host_log is guaranteed by the host ABI.
|
||||||
|
unsafe {
|
||||||
|
core::ptr::copy_nonoverlapping(bytes.as_ptr(), ptr as *mut u8, len);
|
||||||
|
host_log(level, ptr as i32, len as i32);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Plugin metadata
|
||||||
|
#[unsafe(no_mangle)]
|
||||||
|
pub extern "C" fn name_ptr() -> i32 { 1024 }
|
||||||
|
|
||||||
|
#[unsafe(no_mangle)]
|
||||||
|
pub extern "C" fn name_len() -> i32 { 16 }
|
||||||
|
|
||||||
|
#[unsafe(no_mangle)]
|
||||||
|
pub extern "C" fn version_ptr() -> i32 { 1040 }
|
||||||
|
|
||||||
|
#[unsafe(no_mangle)]
|
||||||
|
pub extern "C" fn version_len() -> i32 { 5 }
|
||||||
|
|
||||||
|
// Plugin type: 3 = TileSource
|
||||||
|
#[unsafe(no_mangle)]
|
||||||
|
pub extern "C" fn plugin_type() -> i32 { 3 }
|
||||||
|
|
||||||
|
#[unsafe(no_mangle)]
|
||||||
|
pub extern "C" fn init() -> i32 {
|
||||||
|
let name = b"Example NoiseMap";
|
||||||
|
let version = b"1.0.0";
|
||||||
|
// SAFETY: Addresses 1024+ and 1040+ are reserved for plugin metadata strings.
|
||||||
|
// The source bytes are valid string literals with static lifetime.
|
||||||
|
// Destinations are within the first memory page (64KB) guaranteed by the host.
|
||||||
|
unsafe {
|
||||||
|
core::ptr::copy_nonoverlapping(name.as_ptr(), 1024 as *mut u8, name.len());
|
||||||
|
core::ptr::copy_nonoverlapping(version.as_ptr(), 1040 as *mut u8, version.len());
|
||||||
|
}
|
||||||
|
0
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Terrain types: 0=Desert, 1=Grass, 2=Forest, 3=Rock, 4=Sand, 5=Swamp, 6=Water
|
||||||
|
const TERRAIN_GRASS: u8 = 1;
|
||||||
|
const TERRAIN_FOREST: u8 = 2;
|
||||||
|
const TERRAIN_ROCK: u8 = 3;
|
||||||
|
const TERRAIN_WATER: u8 = 6;
|
||||||
|
|
||||||
|
#[unsafe(no_mangle)]
|
||||||
|
pub extern "C" fn generate_tile(tile_x: i32, tile_z: i32, seed_lo: i32, seed_hi: i32) -> i32 {
|
||||||
|
let seed = (seed_lo as u64) | ((seed_hi as u64) << 32);
|
||||||
|
|
||||||
|
// SAFETY: All host_* functions are guaranteed by the Terrafier WASM host ABI
|
||||||
|
// to be callable from within generate_tile. The host validates all coordinates
|
||||||
|
// (0..127) internally. host_set_* functions are idempotent and panic-free.
|
||||||
|
unsafe {
|
||||||
|
RNG_STATE = seed;
|
||||||
|
|
||||||
|
let tile_size = host_tile_size();
|
||||||
|
let scale = 16.0;
|
||||||
|
|
||||||
|
let origin_x = (tile_x as f64) * (tile_size as f64);
|
||||||
|
let origin_z = (tile_z as f64) * (tile_size as f64);
|
||||||
|
|
||||||
|
let water_level = 32;
|
||||||
|
|
||||||
|
for tz in 0..tile_size {
|
||||||
|
for tx in 0..tile_size {
|
||||||
|
let wx = (tx as f64) + origin_x;
|
||||||
|
let wz = (tz as f64) + origin_z;
|
||||||
|
|
||||||
|
let mut height: f64 = 0.0;
|
||||||
|
let mut amplitude = 1.0;
|
||||||
|
let mut frequency = 1.0;
|
||||||
|
let mut max_val = 0.0;
|
||||||
|
|
||||||
|
for _octave in 0..6 {
|
||||||
|
let nx = wx / scale * frequency;
|
||||||
|
let nz = wz / scale * frequency;
|
||||||
|
height += noise_2d(nx, nz, seed.wrapping_add(_octave as u64)) * amplitude;
|
||||||
|
max_val += amplitude;
|
||||||
|
amplitude *= 0.5;
|
||||||
|
frequency *= 2.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
height /= max_val;
|
||||||
|
height = height * 80.0 + 20.0;
|
||||||
|
|
||||||
|
let iheight = height as i32;
|
||||||
|
host_set_height(tx, tz, iheight);
|
||||||
|
|
||||||
|
let terrain = if iheight < water_level + 2 {
|
||||||
|
TERRAIN_WATER
|
||||||
|
} else if iheight < water_level + 10 {
|
||||||
|
TERRAIN_GRASS
|
||||||
|
} else if iheight > 85 {
|
||||||
|
TERRAIN_ROCK
|
||||||
|
} else if rng_range(0.0, 1.0) < 0.3 {
|
||||||
|
TERRAIN_FOREST
|
||||||
|
} else {
|
||||||
|
TERRAIN_GRASS
|
||||||
|
};
|
||||||
|
host_set_terrain(tx, tz, terrain as i32);
|
||||||
|
|
||||||
|
let water = if iheight < water_level {
|
||||||
|
8u8
|
||||||
|
} else {
|
||||||
|
0u8
|
||||||
|
};
|
||||||
|
host_set_water(tx, tz, water as i32);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
0
|
||||||
|
}
|
||||||
|
|
||||||
|
#[panic_handler]
|
||||||
|
fn panic_handler(_info: &core::panic::PanicInfo) -> ! {
|
||||||
|
loop {}
|
||||||
|
}
|
||||||
1
plugins/example-noisemap/target/.rustc_info.json
Normal file
1
plugins/example-noisemap/target/.rustc_info.json
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
{"rustc_fingerprint":7812569715578181117,"outputs":{"11652014622397750202":{"success":true,"status":"","code":0,"stdout":"___.wasm\nlib___.rlib\n___.wasm\nlib___.a\n/home/loki/.rustup/toolchains/stable-x86_64-unknown-linux-gnu\noff\n___\ndebug_assertions\npanic=\"abort\"\nproc_macro\ntarget_abi=\"\"\ntarget_arch=\"wasm32\"\ntarget_endian=\"little\"\ntarget_env=\"\"\ntarget_family=\"wasm\"\ntarget_feature=\"bulk-memory\"\ntarget_feature=\"multivalue\"\ntarget_feature=\"mutable-globals\"\ntarget_feature=\"nontrapping-fptoint\"\ntarget_feature=\"reference-types\"\ntarget_feature=\"sign-ext\"\ntarget_has_atomic=\"16\"\ntarget_has_atomic=\"32\"\ntarget_has_atomic=\"64\"\ntarget_has_atomic=\"8\"\ntarget_has_atomic=\"ptr\"\ntarget_os=\"unknown\"\ntarget_pointer_width=\"32\"\ntarget_vendor=\"unknown\"\n","stderr":"warning: dropping unsupported crate type `dylib` for target `wasm32-unknown-unknown`\n\nwarning: dropping unsupported crate type `proc-macro` for target `wasm32-unknown-unknown`\n\nwarning: 2 warnings emitted\n\n"},"7971740275564407648":{"success":true,"status":"","code":0,"stdout":"___\nlib___.rlib\nlib___.so\nlib___.so\nlib___.a\nlib___.so\n/home/loki/.rustup/toolchains/stable-x86_64-unknown-linux-gnu\noff\npacked\nunpacked\n___\ndebug_assertions\npanic=\"unwind\"\nproc_macro\ntarget_abi=\"\"\ntarget_arch=\"x86_64\"\ntarget_endian=\"little\"\ntarget_env=\"gnu\"\ntarget_family=\"unix\"\ntarget_feature=\"fxsr\"\ntarget_feature=\"sse\"\ntarget_feature=\"sse2\"\ntarget_has_atomic=\"16\"\ntarget_has_atomic=\"32\"\ntarget_has_atomic=\"64\"\ntarget_has_atomic=\"8\"\ntarget_has_atomic=\"ptr\"\ntarget_os=\"linux\"\ntarget_pointer_width=\"64\"\ntarget_vendor=\"unknown\"\nunix\n","stderr":""},"17181554143788415700":{"success":true,"status":"","code":0,"stdout":"rustc 1.96.1 (31fca3adb 2026-06-26)\nbinary: rustc\ncommit-hash: 31fca3adb283cc9dfd56b49cdee9a96eb9c96ffd\ncommit-date: 2026-06-26\nhost: x86_64-unknown-linux-gnu\nrelease: 1.96.1\nLLVM version: 22.1.2\n","stderr":""}},"successes":{}}
|
||||||
3
plugins/example-noisemap/target/CACHEDIR.TAG
Normal file
3
plugins/example-noisemap/target/CACHEDIR.TAG
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
Signature: 8a477f597d28d172789f06886806bc55
|
||||||
|
# This file is a cache directory tag created by cargo.
|
||||||
|
# For information about cache directory tags see https://bford.info/cachedir/
|
||||||
0
plugins/example-noisemap/target/debug/.cargo-build-lock
Normal file
0
plugins/example-noisemap/target/debug/.cargo-build-lock
Normal file
0
plugins/example-noisemap/target/debug/.cargo-lock
Normal file
0
plugins/example-noisemap/target/debug/.cargo-lock
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
Signature: 8a477f597d28d172789f06886806bc55
|
||||||
|
# This file is a cache directory tag created by cargo.
|
||||||
|
# For information about cache directory tags see https://bford.info/cachedir/
|
||||||
Binary file not shown.
|
|
@ -0,0 +1 @@
|
||||||
|
This file has an mtime of when this was started.
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
76d65f109d43013b
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
{"rustc":9777074978655410247,"features":"[]","declared_features":"[]","target":2668537824883689644,"profile":17672942494452627365,"path":10763286916239946207,"deps":[],"local":[{"CheckDepInfo":{"dep_info":"wasm32-unknown-unknown/debug/.fingerprint/terrafier-plugin-example-noisemap-14d72cbe84c58ee8/dep-lib-terrafier_plugin_example_noisemap","checksum":false}}],"rustflags":[],"config":8247474407144887393,"compile_kind":14682669768258224367}
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
{"$message_type":"diagnostic","message":"function `wasm_log` is never used","code":{"code":"dead_code","explanation":null},"level":"warning","spans":[{"file_name":"src/lib.rs","byte_start":3450,"byte_end":3458,"line_start":96,"line_end":96,"column_start":11,"column_end":19,"is_primary":true,"text":[{"text":"unsafe fn wasm_log(level: i32, msg: &str) {","highlight_start":11,"highlight_end":19}],"label":null,"suggested_replacement":null,"suggestion_applicability":null,"expansion":null}],"children":[{"message":"`#[warn(dead_code)]` (part of `#[warn(unused)]`) on by default","code":null,"level":"note","spans":[],"children":[],"rendered":null}],"rendered":"\u001b[1m\u001b[33mwarning\u001b[0m\u001b[1m: function `wasm_log` is never used\u001b[0m\n \u001b[1m\u001b[94m--> \u001b[0msrc/lib.rs:96:11\n \u001b[1m\u001b[94m|\u001b[0m\n\u001b[1m\u001b[94m96\u001b[0m \u001b[1m\u001b[94m|\u001b[0m unsafe fn wasm_log(level: i32, msg: &str) {\n \u001b[1m\u001b[94m|\u001b[0m \u001b[1m\u001b[33m^^^^^^^^\u001b[0m\n \u001b[1m\u001b[94m|\u001b[0m\n \u001b[1m\u001b[94m= \u001b[0m\u001b[1mnote\u001b[0m: `#[warn(dead_code)]` (part of `#[warn(unused)]`) on by default\n\n"}
|
||||||
|
{"$message_type":"diagnostic","message":"function `host_log` is never used","code":{"code":"dead_code","explanation":null},"level":"warning","spans":[{"file_name":"src/lib.rs","byte_start":881,"byte_end":889,"line_start":24,"line_end":24,"column_start":8,"column_end":16,"is_primary":true,"text":[{"text":" fn host_log(level: i32, ptr: i32, len: i32);","highlight_start":8,"highlight_end":16}],"label":null,"suggested_replacement":null,"suggestion_applicability":null,"expansion":null}],"children":[],"rendered":"\u001b[1m\u001b[33mwarning\u001b[0m\u001b[1m: function `host_log` is never used\u001b[0m\n \u001b[1m\u001b[94m--> \u001b[0msrc/lib.rs:24:8\n \u001b[1m\u001b[94m|\u001b[0m\n\u001b[1m\u001b[94m24\u001b[0m \u001b[1m\u001b[94m|\u001b[0m fn host_log(level: i32, ptr: i32, len: i32);\n \u001b[1m\u001b[94m|\u001b[0m \u001b[1m\u001b[33m^^^^^^^^\u001b[0m\n\n"}
|
||||||
|
{"$message_type":"diagnostic","message":"function `host_get_seed` is never used","code":{"code":"dead_code","explanation":null},"level":"warning","spans":[{"file_name":"src/lib.rs","byte_start":930,"byte_end":943,"line_start":25,"line_end":25,"column_start":8,"column_end":21,"is_primary":true,"text":[{"text":" fn host_get_seed() -> i64;","highlight_start":8,"highlight_end":21}],"label":null,"suggested_replacement":null,"suggestion_applicability":null,"expansion":null}],"children":[],"rendered":"\u001b[1m\u001b[33mwarning\u001b[0m\u001b[1m: function `host_get_seed` is never used\u001b[0m\n \u001b[1m\u001b[94m--> \u001b[0msrc/lib.rs:25:8\n \u001b[1m\u001b[94m|\u001b[0m\n\u001b[1m\u001b[94m25\u001b[0m \u001b[1m\u001b[94m|\u001b[0m fn host_get_seed() -> i64;\n \u001b[1m\u001b[94m|\u001b[0m \u001b[1m\u001b[33m^^^^^^^^^^^^^\u001b[0m\n\n"}
|
||||||
|
{"$message_type":"diagnostic","message":"function `host_get_height` is never used","code":{"code":"dead_code","explanation":null},"level":"warning","spans":[{"file_name":"src/lib.rs","byte_start":993,"byte_end":1008,"line_start":27,"line_end":27,"column_start":8,"column_end":23,"is_primary":true,"text":[{"text":" fn host_get_height(x: i32, z: i32) -> i32;","highlight_start":8,"highlight_end":23}],"label":null,"suggested_replacement":null,"suggestion_applicability":null,"expansion":null}],"children":[],"rendered":"\u001b[1m\u001b[33mwarning\u001b[0m\u001b[1m: function `host_get_height` is never used\u001b[0m\n \u001b[1m\u001b[94m--> \u001b[0msrc/lib.rs:27:8\n \u001b[1m\u001b[94m|\u001b[0m\n\u001b[1m\u001b[94m27\u001b[0m \u001b[1m\u001b[94m|\u001b[0m fn host_get_height(x: i32, z: i32) -> i32;\n \u001b[1m\u001b[94m|\u001b[0m \u001b[1m\u001b[33m^^^^^^^^^^^^^^^\u001b[0m\n\n"}
|
||||||
|
{"$message_type":"diagnostic","message":"function `host_get_terrain` is never used","code":{"code":"dead_code","explanation":null},"level":"warning","spans":[{"file_name":"src/lib.rs","byte_start":1090,"byte_end":1106,"line_start":29,"line_end":29,"column_start":8,"column_end":24,"is_primary":true,"text":[{"text":" fn host_get_terrain(x: i32, z: i32) -> i32;","highlight_start":8,"highlight_end":24}],"label":null,"suggested_replacement":null,"suggestion_applicability":null,"expansion":null}],"children":[],"rendered":"\u001b[1m\u001b[33mwarning\u001b[0m\u001b[1m: function `host_get_terrain` is never used\u001b[0m\n \u001b[1m\u001b[94m--> \u001b[0msrc/lib.rs:29:8\n \u001b[1m\u001b[94m|\u001b[0m\n\u001b[1m\u001b[94m29\u001b[0m \u001b[1m\u001b[94m|\u001b[0m fn host_get_terrain(x: i32, z: i32) -> i32;\n \u001b[1m\u001b[94m|\u001b[0m \u001b[1m\u001b[33m^^^^^^^^^^^^^^^^\u001b[0m\n\n"}
|
||||||
|
{"$message_type":"diagnostic","message":"function `host_get_water` is never used","code":{"code":"dead_code","explanation":null},"level":"warning","spans":[{"file_name":"src/lib.rs","byte_start":1189,"byte_end":1203,"line_start":31,"line_end":31,"column_start":8,"column_end":22,"is_primary":true,"text":[{"text":" fn host_get_water(x: i32, z: i32) -> i32;","highlight_start":8,"highlight_end":22}],"label":null,"suggested_replacement":null,"suggestion_applicability":null,"expansion":null}],"children":[],"rendered":"\u001b[1m\u001b[33mwarning\u001b[0m\u001b[1m: function `host_get_water` is never used\u001b[0m\n \u001b[1m\u001b[94m--> \u001b[0msrc/lib.rs:31:8\n \u001b[1m\u001b[94m|\u001b[0m\n\u001b[1m\u001b[94m31\u001b[0m \u001b[1m\u001b[94m|\u001b[0m fn host_get_water(x: i32, z: i32) -> i32;\n \u001b[1m\u001b[94m|\u001b[0m \u001b[1m\u001b[33m^^^^^^^^^^^^^^\u001b[0m\n\n"}
|
||||||
|
{"$message_type":"diagnostic","message":"function `host_alloc_buffers` is never used","code":{"code":"dead_code","explanation":null},"level":"warning","spans":[{"file_name":"src/lib.rs","byte_start":1314,"byte_end":1332,"line_start":34,"line_end":34,"column_start":8,"column_end":26,"is_primary":true,"text":[{"text":" fn host_alloc_buffers() -> i32;","highlight_start":8,"highlight_end":26}],"label":null,"suggested_replacement":null,"suggestion_applicability":null,"expansion":null}],"children":[],"rendered":"\u001b[1m\u001b[33mwarning\u001b[0m\u001b[1m: function `host_alloc_buffers` is never used\u001b[0m\n \u001b[1m\u001b[94m--> \u001b[0msrc/lib.rs:34:8\n \u001b[1m\u001b[94m|\u001b[0m\n\u001b[1m\u001b[94m34\u001b[0m \u001b[1m\u001b[94m|\u001b[0m fn host_alloc_buffers() -> i32;\n \u001b[1m\u001b[94m|\u001b[0m \u001b[1m\u001b[33m^^^^^^^^^^^^^^^^^^\u001b[0m\n\n"}
|
||||||
|
{"$message_type":"diagnostic","message":"function `host_load_heightmap` is never used","code":{"code":"dead_code","explanation":null},"level":"warning","spans":[{"file_name":"src/lib.rs","byte_start":1350,"byte_end":1369,"line_start":35,"line_end":35,"column_start":8,"column_end":27,"is_primary":true,"text":[{"text":" fn host_load_heightmap() -> i32;","highlight_start":8,"highlight_end":27}],"label":null,"suggested_replacement":null,"suggestion_applicability":null,"expansion":null}],"children":[],"rendered":"\u001b[1m\u001b[33mwarning\u001b[0m\u001b[1m: function `host_load_heightmap` is never used\u001b[0m\n \u001b[1m\u001b[94m--> \u001b[0msrc/lib.rs:35:8\n \u001b[1m\u001b[94m|\u001b[0m\n\u001b[1m\u001b[94m35\u001b[0m \u001b[1m\u001b[94m|\u001b[0m fn host_load_heightmap() -> i32;\n \u001b[1m\u001b[94m|\u001b[0m \u001b[1m\u001b[33m^^^^^^^^^^^^^^^^^^^\u001b[0m\n\n"}
|
||||||
|
{"$message_type":"diagnostic","message":"function `host_load_terrain` is never used","code":{"code":"dead_code","explanation":null},"level":"warning","spans":[{"file_name":"src/lib.rs","byte_start":1387,"byte_end":1404,"line_start":36,"line_end":36,"column_start":8,"column_end":25,"is_primary":true,"text":[{"text":" fn host_load_terrain() -> i32;","highlight_start":8,"highlight_end":25}],"label":null,"suggested_replacement":null,"suggestion_applicability":null,"expansion":null}],"children":[],"rendered":"\u001b[1m\u001b[33mwarning\u001b[0m\u001b[1m: function `host_load_terrain` is never used\u001b[0m\n \u001b[1m\u001b[94m--> \u001b[0msrc/lib.rs:36:8\n \u001b[1m\u001b[94m|\u001b[0m\n\u001b[1m\u001b[94m36\u001b[0m \u001b[1m\u001b[94m|\u001b[0m fn host_load_terrain() -> i32;\n \u001b[1m\u001b[94m|\u001b[0m \u001b[1m\u001b[33m^^^^^^^^^^^^^^^^^\u001b[0m\n\n"}
|
||||||
|
{"$message_type":"diagnostic","message":"function `host_load_water` is never used","code":{"code":"dead_code","explanation":null},"level":"warning","spans":[{"file_name":"src/lib.rs","byte_start":1422,"byte_end":1437,"line_start":37,"line_end":37,"column_start":8,"column_end":23,"is_primary":true,"text":[{"text":" fn host_load_water() -> i32;","highlight_start":8,"highlight_end":23}],"label":null,"suggested_replacement":null,"suggestion_applicability":null,"expansion":null}],"children":[],"rendered":"\u001b[1m\u001b[33mwarning\u001b[0m\u001b[1m: function `host_load_water` is never used\u001b[0m\n \u001b[1m\u001b[94m--> \u001b[0msrc/lib.rs:37:8\n \u001b[1m\u001b[94m|\u001b[0m\n\u001b[1m\u001b[94m37\u001b[0m \u001b[1m\u001b[94m|\u001b[0m fn host_load_water() -> i32;\n \u001b[1m\u001b[94m|\u001b[0m \u001b[1m\u001b[33m^^^^^^^^^^^^^^^\u001b[0m\n\n"}
|
||||||
|
{"$message_type":"diagnostic","message":"function `host_store_heightmap` is never used","code":{"code":"dead_code","explanation":null},"level":"warning","spans":[{"file_name":"src/lib.rs","byte_start":1455,"byte_end":1475,"line_start":38,"line_end":38,"column_start":8,"column_end":28,"is_primary":true,"text":[{"text":" fn host_store_heightmap();","highlight_start":8,"highlight_end":28}],"label":null,"suggested_replacement":null,"suggestion_applicability":null,"expansion":null}],"children":[],"rendered":"\u001b[1m\u001b[33mwarning\u001b[0m\u001b[1m: function `host_store_heightmap` is never used\u001b[0m\n \u001b[1m\u001b[94m--> \u001b[0msrc/lib.rs:38:8\n \u001b[1m\u001b[94m|\u001b[0m\n\u001b[1m\u001b[94m38\u001b[0m \u001b[1m\u001b[94m|\u001b[0m fn host_store_heightmap();\n \u001b[1m\u001b[94m|\u001b[0m \u001b[1m\u001b[33m^^^^^^^^^^^^^^^^^^^^\u001b[0m\n\n"}
|
||||||
|
{"$message_type":"diagnostic","message":"function `host_store_terrain` is never used","code":{"code":"dead_code","explanation":null},"level":"warning","spans":[{"file_name":"src/lib.rs","byte_start":1486,"byte_end":1504,"line_start":39,"line_end":39,"column_start":8,"column_end":26,"is_primary":true,"text":[{"text":" fn host_store_terrain();","highlight_start":8,"highlight_end":26}],"label":null,"suggested_replacement":null,"suggestion_applicability":null,"expansion":null}],"children":[],"rendered":"\u001b[1m\u001b[33mwarning\u001b[0m\u001b[1m: function `host_store_terrain` is never used\u001b[0m\n \u001b[1m\u001b[94m--> \u001b[0msrc/lib.rs:39:8\n \u001b[1m\u001b[94m|\u001b[0m\n\u001b[1m\u001b[94m39\u001b[0m \u001b[1m\u001b[94m|\u001b[0m fn host_store_terrain();\n \u001b[1m\u001b[94m|\u001b[0m \u001b[1m\u001b[33m^^^^^^^^^^^^^^^^^^\u001b[0m\n\n"}
|
||||||
|
{"$message_type":"diagnostic","message":"function `host_store_water` is never used","code":{"code":"dead_code","explanation":null},"level":"warning","spans":[{"file_name":"src/lib.rs","byte_start":1515,"byte_end":1531,"line_start":40,"line_end":40,"column_start":8,"column_end":24,"is_primary":true,"text":[{"text":" fn host_store_water();","highlight_start":8,"highlight_end":24}],"label":null,"suggested_replacement":null,"suggestion_applicability":null,"expansion":null}],"children":[],"rendered":"\u001b[1m\u001b[33mwarning\u001b[0m\u001b[1m: function `host_store_water` is never used\u001b[0m\n \u001b[1m\u001b[94m--> \u001b[0msrc/lib.rs:40:8\n \u001b[1m\u001b[94m|\u001b[0m\n\u001b[1m\u001b[94m40\u001b[0m \u001b[1m\u001b[94m|\u001b[0m fn host_store_water();\n \u001b[1m\u001b[94m|\u001b[0m \u001b[1m\u001b[33m^^^^^^^^^^^^^^^^\u001b[0m\n\n"}
|
||||||
|
{"$message_type":"diagnostic","message":"13 warnings emitted","code":null,"level":"warning","spans":[],"children":[],"rendered":"\u001b[1m\u001b[33mwarning\u001b[0m\u001b[1m: 13 warnings emitted\u001b[0m\n\n"}
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
/storage/project/Terrafier/plugins/example-noisemap/target/wasm32-unknown-unknown/debug/deps/terrafier_plugin_example_noisemap-14d72cbe84c58ee8.d: src/lib.rs
|
||||||
|
|
||||||
|
/storage/project/Terrafier/plugins/example-noisemap/target/wasm32-unknown-unknown/debug/deps/libterrafier_plugin_example_noisemap-14d72cbe84c58ee8.rmeta: src/lib.rs
|
||||||
|
|
||||||
|
src/lib.rs:
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
18
scripts/example.rhai
Normal file
18
scripts/example.rhai
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
// Terrafier script example — creates a world with noise terrain
|
||||||
|
let world = new_world("scripted_world", 42);
|
||||||
|
|
||||||
|
// Generate noise terrain
|
||||||
|
generate_heightmap_noise(world, 12345, 16.0, 6, "overworld");
|
||||||
|
|
||||||
|
// Apply some operations
|
||||||
|
apply_smooth(world, 2, 5.0, "overworld");
|
||||||
|
apply_erode(world, 1, 4.0, "overworld");
|
||||||
|
|
||||||
|
// Generate layers (caves, rivers, biomes, etc.)
|
||||||
|
generate_layers(world, "overworld");
|
||||||
|
|
||||||
|
// Save the world
|
||||||
|
save_world(world, "scripted_world.tfw");
|
||||||
|
|
||||||
|
print(info(world));
|
||||||
|
print("Done! World saved to scripted_world.tfw");
|
||||||
Loading…
Add table
Add a link
Reference in a new issue