Compare commits

...

53 Commits

Author SHA1 Message Date
Louis Erbkamm
00ab57e1ea Update Cargo.toml 2024-09-20 19:38:31 +02:00
Louis Erbkamm
0ff31781fb Fixed Rust release workflow 2024-09-20 19:38:10 +02:00
Louis Erbkamm
f8bf0c02dd Added reference to releases in README 2024-09-20 19:31:56 +02:00
Louis Erbkamm
89135b8ddd Fixed Rust release workflow 2024-09-20 19:26:32 +02:00
Louis Erbkamm
543c1e551d Update Cargo.toml 2024-09-20 19:15:15 +02:00
Louis Erbkamm
7a17524de9 Fixed Rust release workflow 2024-09-20 19:13:03 +02:00
Louis Erbkamm
444c6f2d50 Fixed region.template download path 2024-09-20 19:02:30 +02:00
Louis Erbkamm
122c5f01a9 Updated README 2024-09-20 18:18:26 +02:00
louis-e
851c46b86d Merge experimental-rust-dev into main, resolve conflicts 2024-09-20 18:14:03 +02:00
Louis Erbkamm
8552b412e6 Merge pull request #59 from louis-e/prepare-rust-release
Prepare rust release
2024-09-20 18:03:09 +02:00
louis-e
f48598e1bc Prepare rust release 2024-09-20 18:01:43 +02:00
Louis Erbkamm
45ace99a9e Merge pull request #58 from louis-e/minor-fixes
Minor optimization
2024-09-18 20:22:43 +02:00
louis-e
540b8db7d9 Minor optimization 2024-09-18 20:17:40 +02:00
Louis Erbkamm
023bd888f2 Merge pull request #57 from louis-e/lighting-fix
Fixed lighting levels (SkyLight)
2024-09-18 18:41:20 +02:00
louis-e
c237978ce9 Fixed lighting levels (SkyLight) 2024-09-18 18:40:31 +02:00
Louis Erbkamm
1b661c1619 Merge pull request #56 from louis-e/fix-overlay
Fixed overlay issue
2024-09-18 00:19:01 +02:00
louis-e
685df7caa0 Fixed overlay issue 2024-09-18 00:17:15 +02:00
Louis Erbkamm
15f4bf0345 Merge pull request #55 from louis-e/fix-properties
Fix block properties
2024-09-17 21:44:41 +02:00
louis-e
eee26aeda5 Fixed block properties 2024-09-17 21:43:28 +02:00
Louis Erbkamm
4cc432b699 Refactored height check 2024-09-16 21:32:57 +02:00
Louis Erbkamm
baa70a7f3c Update README.md 2024-09-16 21:03:27 +02:00
Louis Erbkamm
9e4f743778 Updated Git Assets 2024-09-16 20:48:01 +02:00
Louis Erbkamm
10bcfbfc8a Added Git Assets 2024-09-16 20:38:45 +02:00
Louis Erbkamm
385ee223e0 Merge pull request #54 from louis-e/third-port-bulk
Major third porting bulk
2024-09-15 22:56:41 +02:00
Louis Erbkamm
2b09c18ec9 Merge branch 'experimental-rust-dev' into third-port-bulk 2024-09-15 22:56:27 +02:00
louis-e
ee88e9dc47 Major third porting bulk 2024-09-15 22:52:19 +02:00
Louis Erbkamm
f9ce6f208c Create feature_request.md 2024-09-14 15:53:54 +02:00
Louis Erbkamm
83e1296ce9 Create discussion.md 2024-09-14 15:53:34 +02:00
Louis Erbkamm
72c8cdc3b7 Create bug_report.md 2024-09-14 15:52:53 +02:00
Louis Erbkamm
15837d5050 Added Release Workflow 2024-09-14 15:51:58 +02:00
Louis Erbkamm
53f352dad4 Update README.md 2024-09-11 21:05:18 +02:00
Louis Erbkamm
dbbb6a896a Update README.md 2024-09-11 21:04:31 +02:00
Louis Erbkamm
e9ca4789c0 Merge pull request #52 from louis-e/second-port-bulk
Fine tuning and bug fixing
2024-09-11 20:15:15 +02:00
louis-e
c3b7904286 Fine tuning and bug fixing 2024-09-11 20:13:52 +02:00
Louis Erbkamm
0b385197db Merge pull request #51 from louis-e/core-port-impl
Core feature port
2024-09-01 03:31:19 +02:00
Louis Erbkamm
77fe64c910 Delete Cargo.lock 2024-09-01 03:30:26 +02:00
louis-e
bfb4bdaf6c Core feature port 2024-09-01 03:22:59 +02:00
Louis Erbkamm
8d57da0cd7 Reverted accidental grass_block to grass 2024-08-30 22:02:24 +02:00
Louis Erbkamm
704736f9dd Enhanced modularization and core feature porting
Merge pull request #47 from louis-e/rust-modularization
2024-08-25 03:03:06 +02:00
louis-e
a7c1f7be5e Enhanced modularization and core feature porting
Modularization: Separated core logic into dedicated modules (world_editor, args, data_processing, block_definitions) for better maintainability.
Bresenham Algorithm: Implemented efficient block placement for drawing lines in 3D space.
Block Definitions: Centralized block constants for cleaner and reusable block references.
Input Argument Parsing: Integrated clap for robust command-line argument handling, including dynamic path and bounding box inputs.
File Structure: Refined file organization to support scalability and easier navigation.
2024-08-25 03:00:04 +02:00
Louis Erbkamm
1bdbf83d22 Added Support for Multi-Region Block Modifications and Cuboid Fill Functionality
Multi-Region Support: The editor now handles block modifications across multiple regions, dynamically loading and creating region files as needed.
Cuboid Fill Functionality: Adds a fill_blocks method to allow setting a block type across a defined 3D area, improving bulk modifications.
2024-08-25 01:18:44 +02:00
louis-e
478ffb6709 Initial branch setup 2024-08-24 16:37:06 +02:00
Louis Erbkamm
0d3565e351 Merge pull request #46 from Aeris1One/anvil-new
Upgrade to anvil-new
2024-08-24 15:56:10 +02:00
Louis Erbkamm
96c7d33cb8 Added pull_request trigger to workflow 2024-08-24 15:53:47 +02:00
Charles P.
e549ded318 Upgrade to anvil-new 2024-08-24 14:50:36 +02:00
Louis Erbkamm
2ad01f8298 Added validation for bbox input
Merge pull request #45 from louis-e/bbox-validation
2024-08-18 14:29:23 +02:00
louis-e
6f34b125bf flake8 and black reformatting 2024-08-18 14:28:14 +02:00
louis-e
1036e36c15 Added validation for bbox input 2024-08-18 14:25:04 +02:00
Louis Erbkamm
ee49015291 Update bug_report.md 2024-08-17 16:14:46 +02:00
Louis Erbkamm
121437cfbf Update issue templates
Added bug report, feature request and discussion
2024-08-16 17:31:33 +02:00
Louis Erbkamm
3bb459cdb7 Change license from MIT to GNU General Public License (GPL v3)
Merge pull request #41 from louis-e/license-switch
2024-08-16 17:15:25 +02:00
Louis Erbkamm
87bf75c161 Update LICENSE 2024-08-16 17:14:35 +02:00
louis-e
24bd642839 Updated README license information 2024-08-16 17:12:47 +02:00
59 changed files with 3682 additions and 2498 deletions

View File

@@ -1,5 +0,0 @@
[flake8]
ignore = E203, W503, F405, F403
# line length is intentionally set to 80 here because black uses Bugbear
# See https://black.readthedocs.io/en/stable/the_black_code_style/current_style.html#line-length for more details
max-line-length = 100

View File

@@ -1,8 +1,8 @@
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: ''
title: "[BUG] Your title here"
labels: bug
assignees: ''
---
@@ -17,4 +17,4 @@ Please provide your input parameters so we can reproduce the issue.
If applicable, add screenshots to help explain your problem.
**Additional context**
Add any other context about the problem here.
Add any other context about the problem here. Please also provide the --bbox input parameters you used so we can reproduce the issue.

10
.github/ISSUE_TEMPLATE/discussion.md vendored Normal file
View File

@@ -0,0 +1,10 @@
---
name: Discusssion
about: Discuss a topic or change
title: "[DISCUSSION] Your title here"
labels: discussion
assignees: ''
---

View File

@@ -0,0 +1,17 @@
---
name: Feature request
about: Suggest an idea for this project
title: "[FEATURE] Your title here"
labels: feature
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is.
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Additional context**
Add any other context or screenshots about the feature request here.

View File

@@ -1,45 +0,0 @@
name: Build and Release for Linux
on:
release:
types: [created] # Triggers only when a release is created
jobs:
build-and-release:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.10'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
- name: Create Executable
uses: Martin005/pyinstaller-action@v1.2.0
with:
python_ver: '3.10'
spec: 'pyinst-compile.spec'
requirements: 'requirements.txt'
upload_exe_with_name: 'arnis'
options: '--onefile --name arnis'
- name: Build
run: make build
- name: Upload Release Asset
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ github.event.release.upload_url }}
asset_path: ./dist/arnis
asset_name: arnis
asset_content_type: application/octet-stream

View File

@@ -1,45 +0,0 @@
name: Build and Release for Windows
on:
release:
types: [created] # Triggers only when a release is created
jobs:
build-and-release:
runs-on: windows-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.10'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
- name: Create Executable
uses: Martin005/pyinstaller-action@v1.2.0
with:
python_ver: '3.10'
spec: 'pyinst-compile.spec'
requirements: 'requirements.txt'
upload_exe_with_name: 'arnis.exe'
options: '--onefile --name arnis'
- name: Build
run: make build
- name: Upload Release Asset
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ github.event.release.upload_url }}
asset_path: ./dist/arnis.exe
asset_name: arnis.exe
asset_content_type: application/octet-stream

33
.github/workflows/ci-build.yml vendored Normal file
View File

@@ -0,0 +1,33 @@
name: CI Build
# Trigger on pull request creation, update, and on pushes to the main branch
on:
pull_request:
branches:
- main
push:
branches:
- main
jobs:
build:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Set up Rust
uses: actions-rs/toolchain@v1
with:
toolchain: stable
target: ${{ matrix.os == 'windows-latest' && 'x86_64-pc-windows-msvc' || 'x86_64-unknown-linux-gnu' || 'x86_64-apple-darwin' }}
- name: Install dependencies
run: cargo fetch
- name: Build
run: cargo build --release

View File

@@ -1,32 +0,0 @@
# This workflow will install Python dependencies, run tests and lint with a single version of Python
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions
# Reduced subset of file from https://github.com/actions/starter-workflows/blob/main/ci/python-app.yml
name: Python application
on: [push]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Python 3.10
uses: actions/setup-python@v3
with:
python-version: '3.10'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install flake8 pytest black
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
- name: Check the black styling
run: |
make style-check
- name: Lint with flake8
run: |
make lint
- name: Run unit tests
run: |
python -m pytest

52
.github/workflows/release.yml vendored Normal file
View File

@@ -0,0 +1,52 @@
name: Build and Release Rust Project
on:
release:
types: [created]
jobs:
build:
runs-on: windows-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Set up Rust for Windows
uses: actions-rs/toolchain@v1
with:
toolchain: stable
target: x86_64-pc-windows-msvc
- name: Install dependencies
run: cargo fetch
- name: Build
run: cargo build --release
- name: Upload artifact (Windows)
uses: actions/upload-artifact@v3
with:
name: windows-latest-build
path: target/release/arnis.exe
release:
runs-on: windows-latest
needs: build
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Download Windows build artifact
uses: actions/download-artifact@v3
with:
name: windows-latest-build
path: ./builds/windows
- name: Create GitHub Release
uses: softprops/action-gh-release@v1
with:
files: |
builds/windows/arnis.exe
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

171
.gitignore vendored
View File

@@ -1,167 +1,4 @@
# Original file is from https://github.com/github/gitignore/blob/main/Python.gitignore
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
.pybuilder/
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# .python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# poetry
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
#poetry.lock
# pdm
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
#pdm.lock
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
# in version control.
# https://pdm.fming.dev/#use-with-ide
.pdm.toml
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# pytype static type analyzer
.pytype/
# Cython debug symbols
cython_debug/
# PyCharm
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/
# Debug files
arnis-debug-raw_data.json
arnis-debug-processed_data.json
arnis-debug-map.png
image.img
/target
Cargo.lock
export.json
parsed_osm_data.txt

25
Cargo.toml Normal file
View File

@@ -0,0 +1,25 @@
[package]
name = "arnis"
version = "2.0.0"
edition = "2021"
description = "Arnis - Generate real life cities in Minecraft"
homepage = "https://github.com/louis-e/arnis"
repository = "https://github.com/louis-e/arnis"
license = "GPL-3.0"
readme = "README.md"
[dependencies]
clap = { version = "4.1", features = ["derive"] }
colored = "2.1.0"
fastanvil = "0.31.0"
fastnbt = "2.5.0"
geo = "0.28.0"
indicatif = "0.17.8"
itertools = "0.13.0"
nalgebra = "0.33.0"
once_cell = "1.19.0"
rand = "0.8.5"
reqwest = { version = "0.12.7", features = ["blocking", "json"] }
semver = "1.0.23"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"

View File

@@ -1,6 +0,0 @@
FROM python:3.9
RUN apt-get update && apt-get -y install git ffmpeg libsm6 libxext6
RUN cd /home && mkdir /home/region && git clone https://github.com/louis-e/arnis.git
WORKDIR /home/arnis
RUN pip install -r requirements.txt
ENTRYPOINT ["python", "arnis.py"]

687
LICENSE
View File

@@ -1,21 +1,674 @@
MIT License
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (c) 2022 Louis
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
Preamble
The above copyright notice, the author ("louis-e") and this permission notice shall be included in all
copies or substantial portions of the Software.
The GNU General Public License is a free, copyleft license for
software and other kinds of works.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
the GNU General Public License is intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users. We, the Free Software Foundation, use the
GNU General Public License for most of our software; it applies also to
any other work released this way by its authors. You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
To protect your rights, we need to prevent others from denying you
these rights or asking you to surrender the rights. Therefore, you have
certain responsibilities if you distribute copies of the software, or if
you modify it: responsibilities to respect the freedom of others.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must pass on to the recipients the same
freedoms that you received. You must make sure that they, too, receive
or can get the source code. And you must show them these terms so they
know their rights.
Developers that use the GNU GPL protect your rights with two steps:
(1) assert copyright on the software, and (2) offer you this License
giving you legal permission to copy, distribute and/or modify it.
For the developers' and authors' protection, the GPL clearly explains
that there is no warranty for this free software. For both users' and
authors' sake, the GPL requires that modified versions be marked as
changed, so that their problems will not be attributed erroneously to
authors of previous versions.
Some devices are designed to deny users access to install or run
modified versions of the software inside them, although the manufacturer
can do so. This is fundamentally incompatible with the aim of
protecting users' freedom to change the software. The systematic
pattern of such abuse occurs in the area of products for individuals to
use, which is precisely where it is most unacceptable. Therefore, we
have designed this version of the GPL to prohibit the practice for those
products. If such problems arise substantially in other domains, we
stand ready to extend this provision to those domains in future versions
of the GPL, as needed to protect the freedom of users.
Finally, every program is threatened constantly by software patents.
States should not allow patents to restrict development and use of
software on general-purpose computers, but in those that do, we wish to
avoid the special danger that patents applied to a free program could
make it effectively proprietary. To prevent this, the GPL assures that
patents cannot be used to render the program non-free.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Use with the GNU Affero General Public License.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU Affero General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the special requirements of the GNU Affero General Public License,
section 13, concerning interaction through a network will apply to the
combination as such.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode:
<program> Copyright (C) <year> <name of author>
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, your program's commands
might be different; for a GUI interface, you would use an "about box".
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU GPL, see
<https://www.gnu.org/licenses/>.
The GNU General Public License does not permit incorporating your program
into proprietary programs. If your program is a subroutine library, you
may consider it more useful to permit linking proprietary applications with
the library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License. But first, please read
<https://www.gnu.org/licenses/why-not-lgpl.html>.

View File

@@ -1,11 +0,0 @@
# Check if black formatter will work without any rewrites, and produces an exit code
style-check:
black src/ --check
# This will reformat all python files unders bookdifferent/ into the python black standard
style:
black src/
# Checks that the python source files are compliant regarding errors and style conventions
lint:
flake8 src/

146
README.md
View File

@@ -2,32 +2,37 @@
<img width="456" height="125" src="https://github.com/louis-e/arnis/blob/main/gitassets/logo.png?raw=true">
</p>
# Arnis [![Testing](https://github.com/louis-e/arnis/actions/workflows/python-app.yml/badge.svg)](https://github.com/louis-e/arnis/actions/workflows/python-app.yml)
This open source project generates any chosen location from the real world in Minecraft, allowing users to explore and build in a virtual world that mirrors the real one.
<br>
# Arnis [![CI Build Status](https://github.com/louis-e/arnis/actions/workflows/ci-build.yml/badge.svg)](https://github.com/louis-e/arnis/actions)
This open source project written in Rust generates any chosen location from the real world in Minecraft with a high level of detail.
⇒ [Where did you find this project?](https://6okq6xh5jt4.typeform.com/to/rSjZaB41)
## :desktop_computer: Example
![Minecraft World Demo](https://github.com/louis-e/arnis/blob/main/gitassets/demo-comp.png?raw=true)
![Minecraft World Demo Before After](https://github.com/louis-e/arnis/blob/main/gitassets/before-after.gif?raw=true)
<img width="700" height="400" src="https://github.com/louis-e/arnis/blob/main/gitassets/mc.gif?raw=true">
By leveraging geospatial data from OpenStreetMap and utilizing the powerful capabilities of Rust, Arnis provides an efficient and robust solution for creating complex and accurate Minecraft worlds that reflect real-world geography and architecture.
Arnis is designed to handle large-scale data and generate rich, immersive environments that bring real-world cities, landmarks, and natural features into the Minecraft universe. Whether you're looking to replicate your hometown, explore urban environments, or simply build something unique and realistic, Arnis offers a comprehensive toolset to achieve your vision.
## :floppy_disk: How it works
![CLI Generation](https://github.com/louis-e/arnis/blob/main/gitassets/cli-generation.gif?raw=true)
![CLI Generation](https://github.com/louis-e/arnis/blob/main/gitassets/cli.gif?raw=true)
The raw data obtained from the API *[(see FAQ)](#question-faq)* includes each element (buildings, walls, fountains, farmlands, etc.) with its respective corner coordinates (nodes) and descriptive tags. When you run the script, the following steps are performed automatically to generate a Minecraft world:
The raw data obtained from the API *[(see FAQ)](#question-faq)* includes each element (buildings, walls, fountains, farmlands, etc.) with its respective corner coordinates (nodes) and descriptive tags. When you run Arnis, the following steps are performed automatically to generate a Minecraft world:
#### Processing Pipeline
1. Scraping Data from API: The script fetches geospatial data from the Overpass Turbo API.
2. Determine Coordinate Extremes: Identifies the highest and lowest latitude and longitude values from the dataset.
3. Standardize Coordinate Lengths: Ensures all coordinates are of uniform length and removes the decimal separator.
4. Normalize Data: Adjusts all coordinates to start from zero by subtracting the previously determined lowest values.
5. Parse Data: Transforms the raw data into a standardized structure.
6. Sort elements by priority: Enables a layering system with prioritized elements.
7. Optimize Array Size: Focuses on the outermost buildings to reduce array size.
8. Generate Minecraft World: Iterates through the array to create the Minecraft world, including 3D structures like forests, houses, and rivers.
1. Fetch Data from Overpass API: The script retrieves geospatial data for the desired bounding box from the Overpass API. You can specify the bounding box coordinates using the --bbox parameter.
2. Parse Raw Data: The raw data is parsed to extract essential information like nodes, ways, and relations. Nodes are converted into Minecraft coordinates, and relations are handled similarly to ways, ensuring all relevant elements are processed correctly.
3. Prioritize and Sort Elements: The elements (nodes, ways, relations) are sorted by priority to establish a layering system, which ensures that certain types of elements (e.g., entrances and buildings) are generated in the correct order to avoid conflicts and overlapping structures.
4. Generate Minecraft World: The Minecraft world is generated using a series of element processors (generate_buildings, generate_highways, generate_landuse, etc.) that interpret the tags and nodes of each element to place the appropriate blocks in the Minecraft world. These processors handle the logic for creating 3D structures, roads, natural formations, and more, as specified by the processed data.
5. Generate Ground Layer: A ground layer is generated based on the provided scale factors to provide a base for the entire Minecraft world. This step ensures all areas have an appropriate foundation (e.g., grass and dirt layers).
6. Save the Minecraft World: All the modified chunks are saved back to the Minecraft region files.
## :keyboard: Usage
```python3 arnis.py --bbox="min_lng,min_lat,max_lng,max_lat" --path="C:/Users/username/AppData/Roaming/.minecraft/saves/worldname"```
Get the [latest release](https://github.com/louis-e/arnis/releases/) or [compile](#trophy-open-source) the project on your own.
#### Run: ```arnis.exe --path="C:/YOUR_PATH/.minecraft/saves/worldname" --bbox="min_lng,min_lat,max_lng,max_lat"```
### How to find your bbox coordinates
Use http://bboxfinder.com/ to draw a rectangle of your wanted area. Then copy the four box coordinates as shown below and use them as the input for the --bbox parameter.
![How to find area](https://github.com/louis-e/arnis/blob/main/gitassets/bbox-finder.png?raw=true)
The world will always be generated starting from the coordinates 0 0 0.
@@ -35,103 +40,72 @@ The world will always be generated starting from the coordinates 0 0 0.
Manually generate a new Minecraft world (preferably a flat world) before running the script.
The --bbox parameter specifies the bounding box coordinates in the format: min_lng,min_lat,max_lng,max_lat.
Use --path to specify the location of the Minecraft world.
With the --timeout parameter you can set the timeout for the floodfill algorithm in seconds (default: 2).
You can optionally use the parameter --debug to see processed value outputs during runtime.
#### Experimental City/State/Country Input Method
The following method is experimental and may not perform as expected. Support is limited.
```python3 arnis.py --city="CityName" --state="StateName" --country="CountryName" --path="C:/Users/username/AppData/Roaming/.minecraft/saves/worldname"```
### Docker image (experimental)
If you want to run this project containerized, you can use the Dockerfile provided in this repository. It will automatically scrape the latest source code from the repository. After running the container, you have to manually copy the generated region files from the container to the host machine in order to use them. When running the Docker image, set the ```--path``` parameter to ```/home```.
```
docker build -t arnis .
docker run arnis --city="Arnis" --state="Schleswig Holstein" --country="Deutschland" --path="/home"
docker cp CONTAINER_ID:/home/region DESTINATION_PATH
```
## :cd: Requirements
- Python 3
- ```pip install -r requirements.txt```
- To conform with style guide please format any changes and check the code quality
```black .```
```flake8 src/```
- Functionality should be covered by automated tests.
```python -m pytest```
## :question: FAQ
- *Why do some cities take so long to generate?*<br>
The script's performance can be significantly affected by large elements, such as extensive farmlands. The floodfill algorithm can slow down considerably when dealing with such elements, leading to long processing times. Thus there is also a timeout restriction in place, which can be adjusted by the user *[(see Usage)](#keyboard-usage)*. It is recommended to start with smaller areas to get a sense of the script's performance. Continuous improvements on the algorithm especially focus on effiency improvements.
- *Wasn't this written in Python before?*<br>
Yes! Arnis was initially developed in Python, which benefited from Python's open-source friendliness and ease of readability. This is why we strive for clear, well-documented code in the Rust port of this project to find the right balance. I decided to port the project to Rust to learn more about it and push the algorithm's performance further. We were nearing the limits of optimization in Python, and Rust's capabilities allow for even better performance and efficiency. The old Python implementation is still available in the python-legacy branch.
- *Where does the data come from?*<br>
The geographic data is sourced from OpenStreetMap (OSM)[^1], a free, collaborative mapping project that serves as an open-source alternative to commercial mapping services. The data is accessed via the Overpass API, which queries OSM's database.
- *How does the Minecraft world generation work?*<br>
The script uses the [anvil-parser](https://github.com/matcool/anvil-parser) library to interact with Minecraft's world format. This library allows the script to create and manipulate Minecraft region files, enabling the generation of real-world locations within the game.
The script uses the [fastnbt](https://github.com/owengage/fastnbt) cargo package to interact with Minecraft's world format. This library allows Arnis to manipulate Minecraft region files, enabling the generation of real-world locations.
- *Where does the name come from?*<br>
The project is named after Arnis[^2], the smallest city in Germany. The city's small size made it an ideal test case for developing and debugging the script efficiently.
The project is named after the smallest city in Germany, Arnis[^2]. The city's small size made it an ideal test case for developing and debugging the algorithm efficiently.
## :memo: ToDo
Feel free to choose an item from the To-Do or Known Bugs list, or bring your own idea to the table. Contributions from everyone are welcome and encouraged to help improve this project.
- [ ] Look into https://github.com/Intergalactyc/anvil-new which seems to have a better support
## :memo: ToDo and Known Bugs
Feel free to choose an item from the To-Do or Known Bugs list, or bring your own idea to the table. Bug reports shall be raised as a Github issue. Contributions are highly welcome and appreciated!
- [ ] Design and implement a GUI
- [ ] Evaluate and implement multithreaded region saving
- [ ] Better code documentation
- [ ] Implement house roof types
- [ ] Refactor railway implementation
- [ ] Refactor bridges implementation
- [ ] Refactor fountain structure implementation
- [ ] Automatic new world creation instead of using an existing world
- [ ] Tool for mapping real coordinates to Minecraft coordinates
- [ ] Fix railway orientation
- [ ] Fix gaps in bridges
- [ ] Full refactoring of variable and function names, establish naming conventions
- [ ] Detection of wrong bbox input
- [ ] Evaluate and implement multiprocessing in the ground layer initialization and floodfill algorithm
- [ ] Implement elevation
- [ ] Setup fork of [https://github.com/aaronr/bboxfinder.com](https://github.com/aaronr/bboxfinder.com) for easy bbox picking
- [ ] Add interior to buildings
- [ ] Save fountain structure in the code (similar to the tree structure)
- [ ] Add windows to buildings
- [ ] Evaluate and implement elevation
- [ ] Generate a few big cities using high performance hardware and make them available to download
- [ ] Optimize region file size
- [ ] Street markings
- [ ] Add better code comments
- [x] Alternative reliable city input options
- [x] Split up processData array into several smaller ones for big cities
- [x] Find alternative for CV2 package
- [x] Floodfill timeout parameter
- [x] Automated Tests
- [x] PEP8
- [x] Use f-Strings in print statements
- [x] Add Dockerfile
- [x] Added path check
- [x] Improve RAM usage
- [x] Fix faulty empty chunks ([https://github.com/owengage/fastnbt/issues/120](https://github.com/owengage/fastnbt/issues/120)) (workaround found)
## :bug: Known Bugs
- [ ] Docker image size
- [x] 'Noer' bug (occurs when several different digits appear in coordinates before the decimal point)
- [x] 'Nortorf' bug (occurs when there are several elements with a big distance to each other, e.g. the API returns several different cities with the exact same name)
- [x] Saving step memory overflow
- [x] Non uniform OSM naming standards (dashes) (See name tags at https://overpass-turbo.eu/s/1mMj)
## :trophy: Open Source
#### Key objectives of this project
- **Modularity**: Ensure that all components (e.g., data fetching, processing, and world generation) are cleanly separated into distinct modules for better maintainability and scalability.
- **Performance Optimization**: Utilize Rusts memory safety and concurrency features to optimize the performance of the world generation process.
- **Comprehensive Documentation**: Detailed in-code documentation for a clear structure and logic.
- **User-Friendly Experience**: Focus on making the project easy to use for end users, with the potential to develop a graphical user interface (GUI) in the future. Suggestions and discussions on UI/UX are welcome.
- **Cross-Platform Support**: Ensure the project runs smoothly on Windows, macOS, and Linux.
## :trophy: Hall of Fame Contributors
This section is dedicated to recognizing and celebrating the outstanding contributions of individuals who have significantly enhanced this project. Your work and dedication are deeply appreciated!
#### How to contribute
This project is open source and welcomes contributions from everyone! Whether you're interested in fixing bugs, improving performance, adding new features, or enhancing documentation, your input is valuable. Simply fork the repository, make your changes, and submit a pull request. We encourage discussions and suggestions to ensure the project remains modular, optimized, and easy to use for the community. You can use the parameter --debug to get a more detailed output of the processed values, which can be helpful for debugging and development. Contributions of all levels are appreciated, and your efforts help improve this tool for everyone.
Build and run it using: ```cargo run --release -- --path="C:/YOUR_PATH/.minecraft/saves/worldname" --bbox="min_lng,min_lat,max_lng,max_lat"```<br>
After your pull request was merged, I will take care of regularly creating update releases which will include your changes.
#### Contributors:
This section is dedicated to recognizing and celebrating the outstanding contributions of individuals who have significantly enhanced this project. Your work and dedication are deeply appreciated!
- louis-e
- callumfrance
- amir16yp
- EdwardWeir13579
- daniil2327
*(Including original Python implementation)*
## :star: Star History
[![Star History Chart](https://api.star-history.com/svg?repos=louis-e/arnis&type=Date)](https://star-history.com/#louis-e/arnis&Date)
## :copyright: License
MIT License[^3]
## :copyright: License Information
This project is licensed under the GNU General Public License v3.0 (GPL-3.0).[^3]
Copyright (c) 2022 louis-e
Copyright (c) 2022-2024 louis-e
[^1]: https://en.wikipedia.org/wiki/OpenStreetMap
[^2]: https://en.wikipedia.org/wiki/Arnis,_Germany
[^3]:
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice, the author ("louis-e") and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
For the full license text, see the LICENSE file.

View File

@@ -1,10 +0,0 @@
#!/usr/bin/env python
# Copyright 2022 by louis-e, https://github.com/louis-e/.
# MIT License
# Please see the LICENSE file that should have been included as part of this package.
from src.main import run
if __name__ == "__main__":
run()

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 MiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 79 KiB

BIN
gitassets/cli.gif Normal file
View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

BIN
gitassets/mc.gif Normal file
View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.7 MiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 MiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 MiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 MiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 MiB

View File

@@ -1,34 +0,0 @@
# -*- mode: python ; coding: utf-8 -*-
import os
import site
# Locate the site-packages directory
site_packages_path = next(p for p in site.getsitepackages() if 'site-packages' in p)
# Path to the legacy_blocks.json file
legacy_blocks_path = os.path.join(site_packages_path, 'anvil', 'legacy_blocks.json')
block_cipher = None
a = Analysis(['arnis.py'],
pathex=['.'],
binaries=[],
datas=[(legacy_blocks_path, 'anvil')],
hiddenimports=[],
hookspath=[],
runtime_hooks=[],
excludes=[],
cipher=block_cipher,
noarchive=False)
pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher)
exe = EXE(pyz,
a.scripts,
a.binaries,
a.zipfiles,
a.datas,
name='arnis',
debug=False,
strip=False,
upx=True,
runtime_tmpdir=None,
console=True )

BIN
region.template Normal file
View File

Binary file not shown.

View File

@@ -1,10 +0,0 @@
anvil-parser==0.9.0
matplotlib==3.9.0
numpy==1.26.4
pytest==8.2.1
python-polylabel==0.6
requests==2.32.2
argparse==1.4.0
black==24.4.2
flake8==7.0.0
tqdm==4.66.5

View File

80
src/args.rs Normal file
View File

@@ -0,0 +1,80 @@
use clap::{ArgGroup, Parser};
use colored::Colorize;
use std::path::Path;
use std::process::exit;
/// Command-line arguments parser
#[derive(Parser, Debug)]
#[command(author, version, about)]
#[command(group(
ArgGroup::new("location")
.required(true)
.args(&["bbox", "file"])
))]
pub struct Args {
/// Bounding box of the area (min_lng,min_lat,max_lng,max_lat) (required)
#[arg(long)]
pub bbox: Option<String>,
/// JSON file containing OSM data (optional)
#[arg(long)]
pub file: Option<String>,
/// Path to the Minecraft world (required)
#[arg(long, required = true)]
pub path: String,
/// Downloader method (requests/curl/wget) (optional)
#[arg(long, default_value = "requests")]
pub downloader: String,
/// Enable debug mode (optional)
#[arg(long, default_value_t = false, action = clap::ArgAction::SetTrue)]
pub debug: bool,
/// Set floodfill timeout (seconds) (optional) // TODO
#[arg(long, default_value_t = 2)]
pub timeout: u64,
}
impl Args {
pub fn run(&self) {
// Validating the world path
let mc_world_path: &Path = Path::new(&self.path);
if !mc_world_path.join("region").exists() {
eprintln!("{}", "Error! No Minecraft world found at the given path".red().bold());
exit(1);
}
// Validating bbox if provided
if let Some(bbox) = &self.bbox {
if !validate_bounding_box(bbox) {
eprintln!("{}", "Error! Invalid bbox input".red().bold());
exit(1);
}
}
}
}
/// Validates the bounding box string
fn validate_bounding_box(bbox: &str) -> bool {
let parts: Vec<&str> = bbox.split(',').collect();
if parts.len() != 4 {
return false;
}
let min_lng: f64 = parts[0].parse().ok().unwrap_or(0.0);
let min_lat: f64 = parts[1].parse().ok().unwrap_or(0.0);
let max_lng: f64 = parts[2].parse().ok().unwrap_or(0.0);
let max_lat: f64 = parts[3].parse().ok().unwrap_or(0.0);
if !(min_lng >= -180.0 && min_lng <= 180.0) || !(max_lng >= -180.0 && max_lng <= 180.0) {
return false;
}
if !(min_lat >= -90.0 && min_lat <= 90.0) || !(max_lat >= -90.0 && max_lat <= 90.0) {
return false;
}
min_lng < max_lng && min_lat < max_lat
}

View File

@@ -1,95 +0,0 @@
import anvil
air = anvil.Block("minecraft", "air")
birch_leaves = anvil.Block("minecraft", "birch_leaves")
birch_log = anvil.Block("minecraft", "birch_log")
black_concrete = anvil.Block("minecraft", "black_concrete")
blue_flower = anvil.Block("minecraft", "blue_orchid")
brick = anvil.Block("minecraft", "bricks")
carrots = anvil.Block("minecraft", "carrots", {"age": 7})
cauldron = anvil.Block.from_numeric_id(118, 0)
cobblestone = anvil.Block("minecraft", "cobblestone")
cobblestone_wall = anvil.Block("minecraft", "cobblestone_wall")
dark_oak_door_lower = anvil.Block("minecraft", "dark_oak_door", {"half": "lower"})
dark_oak_door_upper = anvil.Block("minecraft", "dark_oak_door", {"half": "upper"})
dirt = anvil.Block("minecraft", "dirt")
farmland = anvil.Block("minecraft", "farmland")
glass = anvil.Block("minecraft", "glass_pane")
glowstone = anvil.Block("minecraft", "glowstone")
grass = anvil.Block.from_numeric_id(175, 2)
grass_block = anvil.Block("minecraft", "grass_block")
gravel = anvil.Block("minecraft", "gravel")
gray_concrete = anvil.Block("minecraft", "gray_concrete")
green_stained_hardened_clay = anvil.Block.from_numeric_id(159, 5)
hay_bale = anvil.Block("minecraft", "hay_block")
iron_block = anvil.Block("minecraft", "iron_block")
light_gray_concrete = anvil.Block("minecraft", "light_gray_concrete")
oak_fence = anvil.Block.from_numeric_id(85, 0)
oak_leaves = anvil.Block("minecraft", "oak_leaves")
oak_log = anvil.Block.from_numeric_id(17)
oak_planks = anvil.Block("minecraft", "oak_planks")
podzol = anvil.Block.from_numeric_id(3, 2)
potatoes = anvil.Block("minecraft", "potatoes", {"age": 7})
rail = anvil.Block("minecraft", "rail")
red_flower = anvil.Block.from_numeric_id(38)
sand = anvil.Block("minecraft", "sand")
scaffolding = anvil.Block("minecraft", "scaffolding")
sponge = anvil.Block("minecraft", "sponge")
spruce_log = anvil.Block("minecraft", "spruce_log")
stone = anvil.Block("minecraft", "stone")
stone_block_slab = anvil.Block.from_numeric_id(44, 0)
stone_brick_slab = anvil.Block.from_numeric_id(44, 5)
water = anvil.Block("minecraft", "water")
wheat = anvil.Block("minecraft", "wheat", {"age": 7})
white_concrete = anvil.Block("minecraft", "white_concrete")
white_flower = anvil.Block("minecraft", "azure_bluet")
white_stained_glass = anvil.Block("minecraft", "white_stained_glass")
yellow_flower = anvil.Block("minecraft", "dandelion")
# Variations for building corners
building_corner_variations = [
anvil.Block("minecraft", "stone_bricks"),
anvil.Block("minecraft", "cobblestone"),
anvil.Block("minecraft", "bricks"),
anvil.Block("minecraft", "mossy_cobblestone"),
anvil.Block("minecraft", "sandstone"),
anvil.Block("minecraft", "red_nether_bricks"),
anvil.Block("minecraft", "blackstone"),
anvil.Block("minecraft", "smooth_quartz"),
anvil.Block("minecraft", "chiseled_stone_bricks"),
anvil.Block("minecraft", "polished_basalt"),
anvil.Block("minecraft", "cut_sandstone"),
anvil.Block("minecraft", "polished_blackstone_bricks"),
]
# Variations for building walls
building_wall_variations = [
anvil.Block("minecraft", "white_terracotta"),
anvil.Block("minecraft", "gray_terracotta"),
anvil.Block("minecraft", "bricks"),
anvil.Block("minecraft", "smooth_sandstone"),
anvil.Block("minecraft", "red_terracotta"),
anvil.Block("minecraft", "polished_diorite"),
anvil.Block("minecraft", "smooth_stone"),
anvil.Block("minecraft", "polished_andesite"),
anvil.Block("minecraft", "warped_planks"),
anvil.Block("minecraft", "end_stone_bricks"),
anvil.Block("minecraft", "smooth_red_sandstone"),
anvil.Block("minecraft", "nether_bricks"),
]
# Variations for building floors
building_floor_variations = [
anvil.Block("minecraft", "oak_planks"),
anvil.Block("minecraft", "spruce_planks"),
anvil.Block("minecraft", "dark_oak_planks"),
anvil.Block("minecraft", "stone_bricks"),
anvil.Block("minecraft", "polished_granite"),
anvil.Block("minecraft", "polished_diorite"),
anvil.Block("minecraft", "acacia_planks"),
anvil.Block("minecraft", "jungle_planks"),
anvil.Block("minecraft", "warped_planks"),
anvil.Block("minecraft", "purpur_block"),
anvil.Block("minecraft", "smooth_red_sandstone"),
anvil.Block("minecraft", "polished_blackstone"),
]

253
src/block_definitions.rs Normal file
View File

@@ -0,0 +1,253 @@
#![allow(unused)]
use fastnbt::Value;
use once_cell::sync::Lazy;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Serialize, Deserialize, Clone)]
pub struct Block {
pub namespace: String,
pub name: String,
pub properties: Option<Value>,
}
impl Block {
pub fn new(namespace: &str, name: &str, properties: Option<Value>) -> Self {
Self {
namespace: namespace.to_string(),
name: name.to_string(),
properties,
}
}
}
// Lazy static blocks
pub static ACACIA_PLANKS: Lazy<Block> = Lazy::new(|| Block::new("minecraft", "acacia_planks", None));
pub static AIR: Lazy<Block> = Lazy::new(|| Block::new("minecraft", "air", None));
pub static ANDESITE: Lazy<Block> = Lazy::new(|| Block::new("minecraft", "andesite", None));
pub static BIRCH_LEAVES: Lazy<Block> = Lazy::new(|| Block::new("minecraft", "birch_leaves", None));
pub static BIRCH_LOG: Lazy<Block> = Lazy::new(|| Block::new("minecraft", "birch_log", None));
pub static BLACK_CONCRETE: Lazy<Block> = Lazy::new(|| Block::new("minecraft", "black_concrete", None));
pub static BLACKSTONE: Lazy<Block> = Lazy::new(|| Block::new("minecraft", "blackstone", None));
pub static BLUE_FLOWER: Lazy<Block> = Lazy::new(|| Block::new("minecraft", "blue_orchid", None));
pub static BLUE_TERRACOTTA: Lazy<Block> = Lazy::new(|| Block::new("minecraft", "blue_terracotta", None));
pub static BRICK: Lazy<Block> = Lazy::new(|| Block::new("minecraft", "bricks", None));
pub static CAULDRON: Lazy<Block> = Lazy::new(|| Block::new("minecraft", "cauldron", None));
pub static CHISELED_STONE_BRICKS: Lazy<Block> = Lazy::new(|| Block::new("minecraft", "chiseled_stone_bricks", None));
pub static COBBLESTONE_WALL: Lazy<Block> = Lazy::new(|| Block::new("minecraft", "cobblestone_wall", None));
pub static COBBLESTONE: Lazy<Block> = Lazy::new(|| Block::new("minecraft", "cobblestone", None));
pub static CRACKED_POLISHED_BLACKSTONE_BRICKS: Lazy<Block> = Lazy::new(|| Block::new("minecraft", "cracked_polished_blackstone_bricks", None));
pub static CRACKED_STONE_BRICKS: Lazy<Block> = Lazy::new(|| Block::new("minecraft", "cracked_stone_bricks", None));
pub static CRIMSON_PLANKS: Lazy<Block> = Lazy::new(|| Block::new("minecraft", "crimson_planks", None));
pub static CUT_SANDSTONE: Lazy<Block> = Lazy::new(|| Block::new("minecraft", "cut_sandstone", None));
pub static CYAN_CONCRETE: Lazy<Block> = Lazy::new(|| Block::new("minecraft", "cyan_concrete", None));
pub static DARK_OAK_PLANKS: Lazy<Block> = Lazy::new(|| Block::new("minecraft", "dark_oak_planks", None));
pub static DEEPSLATE_BRICKS: Lazy<Block> = Lazy::new(|| Block::new("minecraft", "deepslate_bricks", None));
pub static DIORITE: Lazy<Block> = Lazy::new(|| Block::new("minecraft", "diorite", None));
pub static DIRT: Lazy<Block> = Lazy::new(|| Block::new("minecraft", "dirt", None));
pub static END_STONE_BRICKS: Lazy<Block> = Lazy::new(|| Block::new("minecraft", "end_stone_bricks", None));
pub static FARMLAND: Lazy<Block> = Lazy::new(|| Block::new("minecraft", "farmland", None));
pub static GLASS: Lazy<Block> = Lazy::new(|| Block::new("minecraft", "glass_pane", None));
pub static GLOWSTONE: Lazy<Block> = Lazy::new(|| Block::new("minecraft", "glowstone", None));
pub static GRANITE: Lazy<Block> = Lazy::new(|| Block::new("minecraft", "granite", None));
pub static GRASS_BLOCK: Lazy<Block> = Lazy::new(|| Block::new("minecraft", "grass_block", None));
pub static GRASS: Lazy<Block> = Lazy::new(|| Block::new("minecraft", "tall_grass", None));
pub static GRAVEL: Lazy<Block> = Lazy::new(|| Block::new("minecraft", "gravel", None));
pub static GRAY_CONCRETE: Lazy<Block> = Lazy::new(|| Block::new("minecraft", "gray_concrete", None));
pub static GRAY_TERRACOTTA: Lazy<Block> = Lazy::new(|| Block::new("minecraft", "gray_terracotta", None));
pub static GREEN_STAINED_HARDENED_CLAY: Lazy<Block> = Lazy::new(|| Block::new("minecraft", "green_terracotta", None));
pub static GREEN_WOOL: Lazy<Block> = Lazy::new(|| Block::new("minecraft", "green_wool", None));
pub static HAY_BALE: Lazy<Block> = Lazy::new(|| Block::new("minecraft", "hay_block", None));
pub static IRON_BARS: Lazy<Block> = Lazy::new(|| Block::new("minecraft", "iron_bars", None));
pub static IRON_BLOCK: Lazy<Block> = Lazy::new(|| Block::new("minecraft", "iron_block", None));
pub static JUNGLE_PLANKS: Lazy<Block> = Lazy::new(|| Block::new("minecraft", "jungle_planks", None));
pub static LADDER: Lazy<Block> = Lazy::new(|| Block::new("minecraft", "ladder", None));
pub static LIGHT_BLUE_CONCRETE: Lazy<Block> = Lazy::new(|| Block::new("minecraft", "light_blue_concrete", None));
pub static LIGHT_BLUE_TERRACOTTA: Lazy<Block> = Lazy::new(|| Block::new("minecraft", "light_blue_terracotta", None));
pub static LIGHT_GRAY_CONCRETE: Lazy<Block> = Lazy::new(|| Block::new("minecraft", "light_gray_concrete", None));
pub static MOSS_BLOCK: Lazy<Block> = Lazy::new(|| Block::new("minecraft", "moss_block", None));
pub static MOSSY_COBBLESTONE: Lazy<Block> = Lazy::new(|| Block::new("minecraft", "mossy_cobblestone", None));
pub static MUD_BRICKS: Lazy<Block> = Lazy::new(|| Block::new("minecraft", "mud_bricks", None));
pub static NETHER_BRICK: Lazy<Block> = Lazy::new(|| Block::new("minecraft", "nether_bricks", None));
pub static NETHER_BRICKS: Lazy<Block> = Lazy::new(|| Block::new("minecraft", "nether_bricks", None));
pub static OAK_FENCE: Lazy<Block> = Lazy::new(|| Block::new("minecraft", "oak_fence", None));
pub static OAK_LEAVES: Lazy<Block> = Lazy::new(|| Block::new("minecraft", "oak_leaves", None));
pub static OAK_LOG: Lazy<Block> = Lazy::new(|| Block::new("minecraft", "oak_log", None));
pub static OAK_PLANKS: Lazy<Block> = Lazy::new(|| Block::new("minecraft", "oak_planks", None));
pub static OAK_SLAB: Lazy<Block> = Lazy::new(|| Block::new("minecraft", "oak_slab", None));
pub static ORANGE_TERRACOTTA: Lazy<Block> = Lazy::new(|| Block::new("minecraft", "orange_terracotta", None));
pub static PODZOL: Lazy<Block> = Lazy::new(|| Block::new("minecraft", "podzol", None));
pub static POLISHED_ANDESITE: Lazy<Block> = Lazy::new(|| Block::new("minecraft", "polished_andesite", None));
pub static POLISHED_BASALT: Lazy<Block> = Lazy::new(|| Block::new("minecraft", "polished_basalt", None));
pub static POLISHED_BLACKSTONE_BRICKS: Lazy<Block> = Lazy::new(|| Block::new("minecraft", "polished_blackstone_bricks", None));
pub static POLISHED_BLACKSTONE: Lazy<Block> = Lazy::new(|| Block::new("minecraft", "polished_blackstone", None));
pub static POLISHED_DEEPSLATE: Lazy<Block> = Lazy::new(|| Block::new("minecraft", "polished_deepslate", None));
pub static POLISHED_DIORITE: Lazy<Block> = Lazy::new(|| Block::new("minecraft", "polished_diorite", None));
pub static POLISHED_GRANITE: Lazy<Block> = Lazy::new(|| Block::new("minecraft", "polished_granite", None));
pub static PRISMARINE: Lazy<Block> = Lazy::new(|| Block::new("minecraft", "prismarine", None));
pub static PURPUR_BLOCK: Lazy<Block> = Lazy::new(|| Block::new("minecraft", "purpur_block", None));
pub static PURPUR_PILLAR: Lazy<Block> = Lazy::new(|| Block::new("minecraft", "purpur_pillar", None));
pub static QUARTZ_BRICKS: Lazy<Block> = Lazy::new(|| Block::new("minecraft", "quartz_bricks", None));
pub static RAIL: Lazy<Block> = Lazy::new(|| Block::new("minecraft", "rail", None));
pub static RED_FLOWER: Lazy<Block> = Lazy::new(|| Block::new("minecraft", "poppy", None));
pub static RED_NETHER_BRICKS: Lazy<Block> = Lazy::new(|| Block::new("minecraft", "red_nether_bricks", None));
pub static RED_TERRACOTTA: Lazy<Block> = Lazy::new(|| Block::new("minecraft", "red_terracotta", None));
pub static RED_WOOL: Lazy<Block> = Lazy::new(|| Block::new("minecraft", "red_wool", None));
pub static SAND: Lazy<Block> = Lazy::new(|| Block::new("minecraft", "sand", None));
pub static SANDSTONE: Lazy<Block> = Lazy::new(|| Block::new("minecraft", "sandstone", None));
pub static SCAFFOLDING: Lazy<Block> = Lazy::new(|| Block::new("minecraft", "scaffolding", None));
pub static SMOOTH_QUARTZ: Lazy<Block> = Lazy::new(|| Block::new("minecraft", "smooth_quartz", None));
pub static SMOOTH_RED_SANDSTONE: Lazy<Block> = Lazy::new(|| Block::new("minecraft", "smooth_red_sandstone", None));
pub static SMOOTH_SANDSTONE: Lazy<Block> = Lazy::new(|| Block::new("minecraft", "smooth_sandstone", None));
pub static SMOOTH_STONE: Lazy<Block> = Lazy::new(|| Block::new("minecraft", "smooth_stone", None));
pub static SPONGE: Lazy<Block> = Lazy::new(|| Block::new("minecraft", "sponge", None));
pub static SPRUCE_LOG: Lazy<Block> = Lazy::new(|| Block::new("minecraft", "spruce_log", None));
pub static SPRUCE_PLANKS: Lazy<Block> = Lazy::new(|| Block::new("minecraft", "spruce_planks", None));
pub static STONE_BLOCK_SLAB: Lazy<Block> = Lazy::new(|| Block::new("minecraft", "stone_slab", None));
pub static STONE_BRICK_SLAB: Lazy<Block> = Lazy::new(|| Block::new("minecraft", "stone_brick_slab", None));
pub static STONE_BRICKS: Lazy<Block> = Lazy::new(|| Block::new("minecraft", "stone_bricks", None));
pub static STONE: Lazy<Block> = Lazy::new(|| Block::new("minecraft", "stone", None));
pub static TERRACOTTA: Lazy<Block> = Lazy::new(|| Block::new("minecraft", "terracotta", None));
pub static WARPED_PLANKS: Lazy<Block> = Lazy::new(|| Block::new("minecraft", "warped_planks", None));
pub static WATER: Lazy<Block> = Lazy::new(|| Block::new("minecraft", "water", None));
pub static WHITE_CONCRETE: Lazy<Block> = Lazy::new(|| Block::new("minecraft", "white_concrete", None));
pub static WHITE_FLOWER: Lazy<Block> = Lazy::new(|| Block::new("minecraft", "azure_bluet", None));
pub static WHITE_STAINED_GLASS: Lazy<Block> = Lazy::new(|| Block::new("minecraft", "white_stained_glass", None));
pub static WHITE_TERRACOTTA: Lazy<Block> = Lazy::new(|| Block::new("minecraft", "white_terracotta", None));
pub static WHITE_WOOL: Lazy<Block> = Lazy::new(|| Block::new("minecraft", "white_wool", None));
pub static YELLOW_CONCRETE: Lazy<Block> = Lazy::new(|| Block::new("minecraft", "yellow_concrete", None));
pub static YELLOW_FLOWER: Lazy<Block> = Lazy::new(|| Block::new("minecraft", "dandelion", None));
pub static YELLOW_WOOL: Lazy<Block> = Lazy::new(|| Block::new("minecraft", "yellow_wool", None));
pub static CARROTS: Lazy<Block> = Lazy::new(|| {
Block::new(
"minecraft",
"carrots",
Some(Value::Compound({
let mut map = HashMap::new();
map.insert("age".to_string(), Value::String("7".to_string()));
map
})),
)
});
pub static DARK_OAK_DOOR_LOWER: Lazy<Block> = Lazy::new(|| {
Block::new(
"minecraft",
"dark_oak_door",
Some(Value::Compound({
let mut map = HashMap::new();
map.insert("half".to_string(), Value::String("lower".to_string()));
map
})),
)
});
pub static DARK_OAK_DOOR_UPPER: Lazy<Block> = Lazy::new(|| {
Block::new(
"minecraft",
"dark_oak_door",
Some(Value::Compound({
let mut map = HashMap::new();
map.insert("half".to_string(), Value::String("upper".to_string()));
map
})),
)
});
pub static POTATOES: Lazy<Block> = Lazy::new(|| {
Block::new(
"minecraft",
"potatoes",
Some(Value::Compound({
let mut map = HashMap::new();
map.insert("age".to_string(), Value::String("7".to_string()));
map
})),
)
});
pub static WHEAT: Lazy<Block> = Lazy::new(|| {
Block::new(
"minecraft",
"wheat",
Some(Value::Compound({
let mut map = HashMap::new();
map.insert("age".to_string(), Value::String("7".to_string()));
map
})),
)
});
// Variations for building corners
pub fn building_corner_variations() -> Vec<&'static Lazy<Block>> {
vec![
&STONE_BRICKS,
&COBBLESTONE,
&BRICK,
&MOSSY_COBBLESTONE,
&SANDSTONE,
&RED_NETHER_BRICKS,
&BLACKSTONE,
&SMOOTH_QUARTZ,
&CHISELED_STONE_BRICKS,
&POLISHED_BASALT,
&CUT_SANDSTONE,
&POLISHED_BLACKSTONE_BRICKS,
&ANDESITE,
&GRANITE,
&DIORITE,
&CRACKED_STONE_BRICKS,
&PRISMARINE,
&BLUE_TERRACOTTA,
&NETHER_BRICK,
&QUARTZ_BRICKS,
]
}
// Variations for building walls
pub fn building_wall_variations() -> Vec<&'static Lazy<Block>> {
vec![
&WHITE_TERRACOTTA,
&GRAY_TERRACOTTA,
&BRICK,
&SMOOTH_SANDSTONE,
&RED_TERRACOTTA,
&POLISHED_DIORITE,
&SMOOTH_STONE,
&POLISHED_ANDESITE,
&WARPED_PLANKS,
&END_STONE_BRICKS,
&SMOOTH_RED_SANDSTONE,
&NETHER_BRICKS,
&YELLOW_CONCRETE,
&ORANGE_TERRACOTTA,
&LIGHT_BLUE_TERRACOTTA,
&CYAN_CONCRETE,
&PURPUR_PILLAR,
&CRACKED_POLISHED_BLACKSTONE_BRICKS,
&DEEPSLATE_BRICKS,
&MUD_BRICKS,
]
}
// Variations for building floors
pub fn building_floor_variations() -> Vec<&'static Lazy<Block>> {
vec![
&OAK_PLANKS,
&SPRUCE_PLANKS,
&DARK_OAK_PLANKS,
&STONE_BRICKS,
&POLISHED_GRANITE,
&POLISHED_DIORITE,
&ACACIA_PLANKS,
&JUNGLE_PLANKS,
&WARPED_PLANKS,
&PURPUR_BLOCK,
&SMOOTH_RED_SANDSTONE,
&POLISHED_BLACKSTONE,
&CRIMSON_PLANKS,
&LIGHT_BLUE_CONCRETE,
&MOSS_BLOCK,
&TERRACOTTA,
&BLACKSTONE,
&POLISHED_DEEPSLATE,
]
}

View File

@@ -1,26 +0,0 @@
def bresenham(x1, y1, x2, y2):
"""Bresenham Line Algorithm Credit: encukou/bresenham@Github"""
dx = x2 - x1
dy = y2 - y1
xsign = 1 if dx > 0 else -1
ysign = 1 if dy > 0 else -1
dx = abs(dx)
dy = abs(dy)
if dx > dy:
xx, xy, yx, yy = xsign, 0, 0, ysign
else:
dx, dy = dy, dx
xx, xy, yx, yy = 0, ysign, xsign, 0
D = 2 * dy - dx
y = 0
for x in range(dx + 1):
yield x1 + x * xx + y * yx, y1 + x * xy + y * yy
if D >= 0:
y += 1
D -= 2 * dx
D += 2 * dy

76
src/bresenham.rs Normal file
View File

@@ -0,0 +1,76 @@
/// Generates the coordinates for a line between two points using the Bresenham algorithm.
/// The result is a vector of 3D coordinates (x, y, z).
pub fn bresenham_line(x1: i32, y1: i32, z1: i32, x2: i32, y2: i32, z2: i32) -> Vec<(i32, i32, i32)> {
let mut points: Vec<(i32, i32, i32)> = Vec::new();
let dx: i32 = (x2 - x1).abs();
let dy: i32 = (y2 - y1).abs();
let dz: i32 = (z2 - z1).abs();
let xs: i32 = if x1 < x2 { 1 } else { -1 };
let ys: i32 = if y1 < y2 { 1 } else { -1 };
let zs: i32 = if z1 < z2 { 1 } else { -1 };
let mut x: i32 = x1;
let mut y: i32 = y1;
let mut z: i32 = z1;
if dx >= dy && dx >= dz {
let mut p1: i32 = 2 * dy - dx;
let mut p2: i32 = 2 * dz - dx;
while x != x2 {
points.push((x, y, z));
if p1 >= 0 {
y += ys;
p1 -= 2 * dx;
}
if p2 >= 0 {
z += zs;
p2 -= 2 * dx;
}
p1 += 2 * dy;
p2 += 2 * dz;
x += xs;
}
} else if dy >= dx && dy >= dz {
let mut p1: i32 = 2 * dx - dy;
let mut p2: i32 = 2 * dz - dy;
while y != y2 {
points.push((x, y, z));
if p1 >= 0 {
x += xs;
p1 -= 2 * dy;
}
if p2 >= 0 {
z += zs;
p2 -= 2 * dy;
}
p1 += 2 * dx;
p2 += 2 * dz;
y += ys;
}
} else {
let mut p1: i32 = 2 * dy - dz;
let mut p2: i32 = 2 * dx - dz;
while z != z2 {
points.push((x, y, z));
if p1 >= 0 {
y += ys;
p1 -= 2 * dz;
}
if p2 >= 0 {
x += xs;
p2 -= 2 * dz;
}
p1 += 2 * dy;
p2 += 2 * dx;
z += zs;
}
}
points.push((x2, y2, z2));
points
}

137
src/data_processing.rs Normal file
View File

@@ -0,0 +1,137 @@
use colored::Colorize;
use crate::args::Args;
use crate::element_processing::{*};
use crate::osm_parser::ProcessedElement;
use crate::world_editor::WorldEditor;
use indicatif::{ProgressBar, ProgressStyle};
use reqwest::blocking::get;
use std::fs;
use std::io::Write;
use std::path::Path;
pub fn generate_world(elements: Vec<ProcessedElement>, args: &Args, scale_factor_x: f64, scale_factor_z: f64) {
println!("{} {}", "[3/5]".bold(), "Processing data...");
let region_template_path: &str = "region.template";
let region_dir: String = format!("{}/region", args.path);
let ground_level: i32 = -62;
// Check if the region.template file exists, and download if necessary
if !Path::new(region_template_path).exists() {
let _ = download_region_template(region_template_path);
}
let mut editor: WorldEditor = WorldEditor::new(region_template_path, &region_dir, scale_factor_x, scale_factor_z, &args);
// Process data
let process_pb: ProgressBar = ProgressBar::new(elements.len() as u64);
process_pb.set_style(ProgressStyle::default_bar()
.template("{spinner:.green} [{elapsed_precise}] [{bar:45.white/black}] {pos}/{len} elements ({eta}) {msg}")
.unwrap()
.progress_chars("█▓░"));
for element in &elements {
process_pb.inc(1);
if args.debug {
process_pb.set_message(format!("(Element ID: {} / Type: {})", element.id, element.r#type));
} else {
process_pb.set_message("");
}
match element.r#type.as_str() {
"way" => {
if element.tags.contains_key("building") || element.tags.contains_key("building:part") || element.tags.contains_key("area:highway") {
buildings::generate_buildings(&mut editor, element, ground_level);
} else if element.tags.contains_key("highway") {
highways::generate_highways(&mut editor, element, ground_level);
} else if element.tags.contains_key("landuse") {
landuse::generate_landuse(&mut editor, element, ground_level);
} else if element.tags.contains_key("natural") {
natural::generate_natural(&mut editor, element, ground_level);
} else if element.tags.contains_key("amenity") {
amenities::generate_amenities(&mut editor, element, ground_level);
} else if element.tags.contains_key("leisure") {
leisure::generate_leisure(&mut editor, element, ground_level);
} else if element.tags.contains_key("barrier") {
barriers::generate_barriers(&mut editor, element, ground_level);
} else if element.tags.contains_key("waterway") {
waterways::generate_waterways(&mut editor, element, ground_level);
} else if element.tags.contains_key("bridge") {
bridges::generate_bridges(&mut editor, element, ground_level);
} else if element.tags.contains_key("railway") {
railways::generate_railways(&mut editor, element, ground_level);
} else if element.tags.get("service") == Some(&"siding".to_string()) {
highways::generate_siding(&mut editor, element, ground_level);
}
}
"node" => {
if element.tags.contains_key("door") || element.tags.contains_key("entrance") {
doors::generate_doors(&mut editor, element, ground_level);
} else if element.tags.contains_key("natural") && element.tags.get("natural") == Some(&"tree".to_string()) {
natural::generate_natural(&mut editor, element, ground_level);
} else if element.tags.contains_key("amenity") {
amenities::generate_amenities(&mut editor, element, ground_level);
} else if element.tags.contains_key("barrier") {
barriers::generate_barriers(&mut editor, element, ground_level);
} else if element.tags.contains_key("highway") {
highways::generate_highways(&mut editor, element, ground_level);
}
}
_ => {}
}
}
process_pb.finish();
// Generate ground layer
let total_blocks: u64 = (scale_factor_x as i32 + 1) as u64 * (scale_factor_z as i32 + 1) as u64;
let desired_updates: u64 = 1500;
let batch_size: u64 = (total_blocks / desired_updates).max(1);
let mut block_counter: u64 = 0;
println!("{} {}", "[4/5]".bold(), "Generating ground layer...");
let ground_pb: ProgressBar = ProgressBar::new(total_blocks);
ground_pb.set_style(ProgressStyle::default_bar()
.template("{spinner:.green} [{elapsed_precise}] [{bar:45}] {pos}/{len} blocks ({eta})")
.unwrap()
.progress_chars("█▓░"));
for x in 0..=(scale_factor_x as i32) {
for z in 0..=(scale_factor_z as i32) {
editor.set_block(&crate::block_definitions::GRASS_BLOCK, x, ground_level, z, None, None);
editor.set_block(&crate::block_definitions::DIRT, x, ground_level - 1, z, None, None);
block_counter += 1;
if block_counter % batch_size == 0 {
ground_pb.inc(batch_size);
}
}
}
ground_pb.inc(block_counter % batch_size);
ground_pb.finish();
// Save world
editor.save();
println!("{}", "Done! World generation complete.".green().bold());
}
/// Downloads the region template file from a remote URL and saves it locally.
fn download_region_template(file_path: &str) -> Result<(), Box<dyn std::error::Error>> {
let url = "https://github.com/louis-e/arnis/raw/refs/heads/main/region.template";
// Download the file
let response = get(url)?;
if !response.status().is_success() {
return Err(format!("Failed to download file: HTTP {}", response.status()).into());
}
// Write the file to the specified path
let mut file = fs::File::create(file_path)?;
file.write_all(&response.bytes()?)?;
Ok(())
}

View File

@@ -0,0 +1,137 @@
use crate::world_editor::WorldEditor;
use crate::osm_parser::ProcessedElement;
use crate::block_definitions::*;
use crate::bresenham::bresenham_line;
use crate::floodfill::flood_fill_area;
pub fn generate_amenities(editor: &mut WorldEditor, element: &ProcessedElement, ground_level: i32) {
// Skip if 'layer' or 'level' is negative in the tags
if let Some(layer) = element.tags.get("layer") {
if layer.parse::<i32>().unwrap_or(0) < 0 {
return;
}
}
if let Some(level) = element.tags.get("level") {
if level.parse::<i32>().unwrap_or(0) < 0 {
return;
}
}
if let Some(amenity_type) = element.tags.get("amenity") {
let first_node: Option<&(i32, i32)> = element.nodes.first();
match amenity_type.as_str() {
"waste_disposal" | "waste_basket" => {
// Place a cauldron for waste disposal or waste basket
if let Some(&(x, z)) = first_node {
editor.set_block(&CAULDRON, x, ground_level + 1, z, None, None);
}
return;
}
"vending_machine" | "atm" => {
if let Some(&(x, z)) = first_node {
editor.set_block(&IRON_BLOCK, x, ground_level + 1, z, None, None);
editor.set_block(&IRON_BLOCK, x, ground_level + 2, z, None, None);
}
return;
}
"bicycle_parking" => {
let ground_block: &once_cell::sync::Lazy<Block> = &OAK_PLANKS;
let roof_block: &once_cell::sync::Lazy<Block> = &STONE_BLOCK_SLAB;
let polygon_coords: Vec<(i32, i32)> = element.nodes.iter().copied().collect();
let floor_area: Vec<(i32, i32)> = flood_fill_area(&polygon_coords, 2);
// Fill the floor area
for (x, z) in floor_area.iter() {
editor.set_block(ground_block, *x, ground_level, *z, None, None);
}
// Place fences and roof slabs at each corner node directly
for &(x, z) in &element.nodes {
for y in 1..=4 {
editor.set_block(ground_block, x, ground_level, z, None, None);
editor.set_block(&OAK_FENCE, x, ground_level + y, z, None, None);
}
editor.set_block(roof_block, x, ground_level + 5, z, None, None);
}
// Flood fill the roof area
let roof_height: i32 = ground_level + 5;
for (x, z) in floor_area.iter() {
editor.set_block(roof_block, *x, roof_height, *z, None, None);
}
}
"bench" => {
// Place a bench
if let Some(&(x, z)) = first_node {
editor.set_block(&SMOOTH_STONE, x, ground_level + 1, z, None, None);
editor.set_block(&OAK_LOG, x + 1, ground_level + 1, z, None, None);
editor.set_block(&OAK_LOG, x - 1, ground_level + 1, z, None, None);
}
}
"vending" => {
// Place vending machine blocks
if let Some(&(x, z)) = first_node {
editor.set_block(&IRON_BLOCK, x, ground_level + 1, z, None, None);
editor.set_block(&IRON_BLOCK, x, ground_level + 2, z, None, None);
}
}
"parking" | "fountain" => {
// Process parking or fountain areas
let mut previous_node: Option<(i32, i32)> = None;
let mut corner_addup: (i32, i32, i32) = (0, 0, 0);
let mut current_amenity: Vec<(i32, i32)> = vec![];
let block_type: &once_cell::sync::Lazy<Block> = match amenity_type.as_str() {
"fountain" => &WATER,
"parking" => &GRAY_CONCRETE,
_ => &GRAY_CONCRETE,
};
for &node in &element.nodes {
if let Some(prev) = previous_node {
// Create borders for fountain or parking area
let bresenham_points: Vec<(i32, i32, i32)> = bresenham_line(prev.0, ground_level, prev.1, node.0, ground_level, node.1);
for (bx, _, bz) in bresenham_points {
editor.set_block(block_type, bx, ground_level, bz, Some(&[&BLACK_CONCRETE]), None);
// Decorative border around fountains
if amenity_type == "fountain" {
for dx in [-1, 0, 1].iter() {
for dz in [-1, 0, 1].iter() {
if (*dx, *dz) != (0, 0) {
editor.set_block(&LIGHT_GRAY_CONCRETE, bx + dx, ground_level, bz + dz, None, None);
}
}
}
}
current_amenity.push((node.0, node.1));
corner_addup.0 += node.0;
corner_addup.1 += node.1;
corner_addup.2 += 1;
}
}
previous_node = Some(node);
}
// Flood-fill the interior area for parking or fountains
if corner_addup.2 > 0 {
let polygon_coords: Vec<(i32, i32)> = current_amenity.iter().copied().collect();
let flood_area: Vec<(i32, i32)> = flood_fill_area(&polygon_coords, 2);
for (x, z) in flood_area {
editor.set_block(block_type, x, ground_level, z, Some(&[&BLACK_CONCRETE, &GRAY_CONCRETE]), None);
// Add parking spot markings
if amenity_type == "parking" && (x + z) % 8 == 0 && (x * z) % 32 != 0 {
editor.set_block(&LIGHT_GRAY_CONCRETE, x, ground_level, z, Some(&[&BLACK_CONCRETE, &GRAY_CONCRETE]), None);
}
}
}
}
_ => {}
}
}
}

View File

@@ -0,0 +1,50 @@
use crate::world_editor::WorldEditor;
use crate::osm_parser::ProcessedElement;
use crate::block_definitions::*;
use crate::bresenham::bresenham_line;
pub fn generate_barriers(editor: &mut WorldEditor, element: &ProcessedElement, ground_level: i32) {
if let Some(barrier_type) = element.tags.get("barrier") {
if barrier_type == "bollard" {
if let Some(&(x, z)) = element.nodes.first() {
editor.set_block(&COBBLESTONE_WALL, x, ground_level + 1, z, None, None); // Place bollard
}
} else {
// Determine wall height
let wall_height: i32 = element
.tags
.get("height")
.and_then(|height: &String| height.parse::<f32>().ok())
.map(|height: f32| f32::min(3.0, height).round() as i32)
.unwrap_or(2); // Default height is 2 if not specified or invalid
// Process nodes to create the barrier wall
for i in 1..element.nodes.len() {
let (x1, z1) = element.nodes[i - 1];
let (x2, z2) = element.nodes[i];
// Generate the line of coordinates between the two nodes
let bresenham_points: Vec<(i32, i32, i32)> = bresenham_line(x1, ground_level, z1, x2, ground_level, z2);
for (bx, _, bz) in bresenham_points {
// Build the barrier wall to the specified height
for y in (ground_level + 1)..=(ground_level + wall_height) {
editor.set_block(&COBBLESTONE_WALL, bx, y, bz, None, None); // Barrier wall
}
// Add an optional top to the barrier if the height is more than 1
if wall_height > 1 {
editor.set_block(
&STONE_BRICK_SLAB,
bx,
ground_level + wall_height + 1,
bz,
None,
None,
); // Top of the barrier
}
}
}
}
}
}

View File

@@ -0,0 +1,63 @@
use crate::world_editor::WorldEditor;
use crate::osm_parser::ProcessedElement;
use crate::block_definitions::*;
use crate::bresenham::bresenham_line;
pub fn generate_bridges(editor: &mut WorldEditor, element: &ProcessedElement, ground_level: i32) {
if let Some(_bridge_type) = element.tags.get("bridge") {
let bridge_height: i32 = element
.tags
.get("layer")
.and_then(|layer: &String| layer.parse::<i32>().ok())
.unwrap_or(1); // Default height if not specified
// Calculate the total length of the bridge
let total_steps: usize = element
.nodes
.windows(2)
.map(|nodes: &[(i32, i32)]| {
let (x1, z1) = nodes[0];
let (x2, z2) = nodes[1];
bresenham_line(x1, ground_level, z1, x2, ground_level, z2).len()
})
.sum();
let half_steps = total_steps / 2; // Calculate midpoint for descending after rising
let mut current_step = 0;
for i in 1..element.nodes.len() {
let (x1, z1) = element.nodes[i - 1];
let (x2, z2) = element.nodes[i];
// Generate the line of coordinates between the two nodes
let bresenham_points: Vec<(i32, i32, i32)> = bresenham_line(x1, ground_level, z1, x2, ground_level, z2);
for (bx, _, bz) in bresenham_points {
// Calculate the current height of the bridge
let current_height: i32 = if current_step <= half_steps {
ground_level + bridge_height + current_step as i32 / 5 // Rise for the first half
} else {
ground_level
+ bridge_height
+ (half_steps as i32 / 5)
- ((current_step - half_steps) as i32 / 5) // Descend for the second half
};
// Set bridge blocks
editor.set_block(&LIGHT_GRAY_CONCRETE, bx, current_height, bz, None, None);
for (offset_x, offset_z) in &[(-1, -1), (1, -1), (1, 1), (-1, 1)] {
editor.set_block(
&LIGHT_GRAY_CONCRETE,
bx + offset_x,
current_height,
bz + offset_z,
None,
None,
);
}
current_step += 1;
}
}
}
}

View File

@@ -0,0 +1,232 @@
use crate::world_editor::WorldEditor;
use crate::osm_parser::ProcessedElement;
use crate::block_definitions::*;
use crate::bresenham::bresenham_line;
use crate::floodfill::flood_fill_area;
use rand::Rng;
use std::collections::HashSet;
pub fn generate_buildings(editor: &mut WorldEditor, element: &ProcessedElement, ground_level: i32) {
let mut previous_node: Option<(i32, i32)> = None;
let mut corner_addup: (i32, i32, i32) = (0, 0, 0);
let mut current_building: Vec<(i32, i32)> = vec![];
// Randomly select block variations for corners, walls, and floors
let mut rng: rand::prelude::ThreadRng = rand::thread_rng();
let variation_index_corner: usize = rng.gen_range(0..building_corner_variations().len());
let variation_index_wall: usize = rng.gen_range(0..building_wall_variations().len());
let variation_index_floor: usize = rng.gen_range(0..building_floor_variations().len());
let corner_block: &&once_cell::sync::Lazy<Block> = &building_corner_variations()[variation_index_corner];
let wall_block: &&once_cell::sync::Lazy<Block> = &building_wall_variations()[variation_index_wall];
let floor_block: &&once_cell::sync::Lazy<Block> = &building_floor_variations()[variation_index_floor];
let window_block: &once_cell::sync::Lazy<Block> = &WHITE_STAINED_GLASS;
// Set to store processed flood fill points
let mut processed_points: HashSet<(i32, i32)> = HashSet::new();
let mut building_height: i32 = 6; // Default building height
// Skip if 'layer' or 'level' is negative in the tags
if let Some(layer) = element.tags.get("layer") {
if layer.parse::<i32>().unwrap_or(0) < 0 {
return;
}
}
if let Some(level) = element.tags.get("level") {
if level.parse::<i32>().unwrap_or(0) < 0 {
return;
}
}
// Determine building height from tags
if let Some(levels_str) = element.tags.get("building:levels") {
if let Ok(levels) = levels_str.parse::<i32>() {
if levels >= 1 && (levels * 3) > building_height {
building_height = levels * 3;
}
}
}
if let Some(height_str) = element.tags.get("height") {
if let Ok(height) = height_str.parse::<f64>() {
building_height = height.round() as i32;
}
}
if let Some(building_type) = element.tags.get("building") {
if building_type == "garage" {
building_height = 2;
} else if building_type == "shed" {
building_height = 2;
if element.tags.contains_key("bicycle_parking") {
let ground_block: &once_cell::sync::Lazy<Block> = &OAK_PLANKS;
let roof_block: &once_cell::sync::Lazy<Block> = &STONE_BLOCK_SLAB;
let polygon_coords: Vec<(i32, i32)> = element.nodes.iter().copied().collect();
let floor_area: Vec<(i32, i32)> = flood_fill_area(&polygon_coords, 2);
// Fill the floor area
for (x, z) in floor_area.iter() {
editor.set_block(ground_block, *x, ground_level, *z, None, None);
}
// Place fences and roof slabs at each corner node directly
for &(x, z) in &element.nodes {
for y in 1..=4 {
editor.set_block(ground_block, x, ground_level, z, None, None);
editor.set_block(&OAK_FENCE, x, ground_level + y, z, None, None);
}
editor.set_block(roof_block, x, ground_level + 5, z, None, None);
}
// Flood fill the roof area
let roof_height: i32 = ground_level + 5;
for (x, z) in floor_area.iter() {
editor.set_block(roof_block, *x, roof_height, *z, None, None);
}
return;
}
} else if building_type == "roof" {
let roof_height = ground_level + 5;
// Iterate through the nodes to create the roof edges using Bresenham's line algorithm
for &node in &element.nodes {
let (x, z) = node;
if let Some(prev) = previous_node {
let bresenham_points: Vec<(i32, i32, i32)> = bresenham_line(prev.0, roof_height, prev.1, x, roof_height, z);
for (bx, _, bz) in bresenham_points {
editor.set_block(&STONE_BRICK_SLAB, bx, roof_height, bz, None, None); // Set roof block at edge
}
}
for y in (ground_level + 1)..=(roof_height - 1) {
editor.set_block(&COBBLESTONE_WALL, x, y, z, None, None);
}
previous_node = Some(node);
}
// Use flood-fill to fill the interior of the roof
let polygon_coords: Vec<(i32, i32)> = element.nodes.iter().copied().collect();
let roof_area: Vec<(i32, i32)> = flood_fill_area(&polygon_coords, 2); // Use flood-fill to determine the area
// Fill the interior of the roof with STONE_BRICK_SLAB
for (x, z) in roof_area.iter() {
editor.set_block(&STONE_BRICK_SLAB, *x, roof_height, *z, None, None); // Set roof block
}
return;
} else if building_type == "apartments" {
// If building has no height attribute, assign a defined height
if building_height == 6 {
building_height = 15
}
} else if building_type == "hospital" {
// If building has no height attribute, assign a defined height
if building_height == 6 {
building_height = 23
}
} else if building_type == "bridge" {
generate_bridge(editor, element, ground_level);
return;
}
}
// Process nodes to create walls and corners
for &node in &element.nodes {
let (x, z) = node;
if let Some(prev) = previous_node {
// Calculate walls and corners using Bresenham line
let bresenham_points: Vec<(i32, i32, i32)> = bresenham_line(prev.0, ground_level, prev.1, x, ground_level, z);
for (bx, _, bz) in bresenham_points {
for h in (ground_level + 1)..=(ground_level + building_height) {
if (bx, bz) == element.nodes[0] {
editor.set_block(corner_block, bx, h, bz, None, None); // Corner block
} else {
// Add windows to the walls at intervals
if h > ground_level + 1 && h % 4 != 0 && (bx + bz) % 6 < 3 {
editor.set_block(window_block, bx, h, bz, None, None); // Window block
} else {
editor.set_block(wall_block, bx, h, bz, None, None); // Wall block
}
}
}
editor.set_block(&COBBLESTONE, bx, ground_level + building_height + 1, bz, None, None); // Ceiling cobblestone
current_building.push((bx, bz));
corner_addup = (corner_addup.0 + bx, corner_addup.1 + bz, corner_addup.2 + 1);
}
}
previous_node = Some(node);
}
// Flood-fill interior with floor variation
if corner_addup != (0, 0, 0) {
let polygon_coords: Vec<(i32, i32)> = element.nodes.iter().copied().collect();
let floor_area: Vec<(i32, i32)> = flood_fill_area(&polygon_coords, 2);
for (x, z) in floor_area {
if processed_points.insert((x, z)) {
editor.set_block(floor_block, x, ground_level, z, None, None); // Set floor
// Set level ceilings if height > 4
if building_height > 4 {
for h in (ground_level + 4..ground_level + building_height).step_by(4) {
if x % 6 == 0 && z % 6 == 0 {
editor.set_block(&GLOWSTONE, x, h, z, None, None); // Light fixtures
} else {
editor.set_block(floor_block, x, h, z, None, None);
}
}
} else if x % 6 == 0 && z % 6 == 0 {
editor.set_block(&GLOWSTONE, x, ground_level + building_height, z, None, None); // Light fixtures
}
// Set the house ceiling
editor.set_block(floor_block, x, ground_level + building_height + 1, z, None, None);
}
}
}
}
/// Generates a bridge structure, paying attention to the "level" tag.
fn generate_bridge(editor: &mut WorldEditor, element: &ProcessedElement, base_level: i32) {
// Calculate the bridge level
let mut bridge_level = base_level;
if let Some(level_str) = element.tags.get("level") {
if let Ok(level) = level_str.parse::<i32>() {
bridge_level += (level * 3) + 1; // Adjust height by levels
}
}
let floor_block: &once_cell::sync::Lazy<Block> = &STONE;
let railing_block: &once_cell::sync::Lazy<Block> = &STONE_BRICKS;
// Process the nodes to create bridge pathways and railings
let mut previous_node: Option<(i32, i32)> = None;
for &node in &element.nodes {
let (x, z) = node;
// Create bridge path using Bresenham's line
if let Some(prev) = previous_node {
let bridge_points: Vec<(i32, i32, i32)> = bresenham_line(prev.0, bridge_level, prev.1, x, bridge_level, z);
for (bx, by, bz) in bridge_points {
editor.set_block(railing_block, bx, by + 1, bz, None, None);
editor.set_block(railing_block, bx, by, bz, None, None);
}
}
previous_node = Some(node);
}
// Flood fill the area between the bridge path nodes
let polygon_coords: Vec<(i32, i32)> = element.nodes.iter().copied().collect();
let bridge_area: Vec<(i32, i32)> = flood_fill_area(&polygon_coords, 2);
for (x, z) in bridge_area {
editor.set_block(floor_block, x, bridge_level, z, None, None);
}
}

View File

@@ -0,0 +1,25 @@
use crate::world_editor::WorldEditor;
use crate::osm_parser::ProcessedElement;
use crate::block_definitions::*;
pub fn generate_doors(editor: &mut WorldEditor, element: &ProcessedElement, ground_level: i32) {
// Check if the element is a door or entrance
if element.tags.contains_key("door") || element.tags.contains_key("entrance") {
// Check for the "level" tag and skip doors that are not at ground level
if let Some(level_str) = element.tags.get("level") {
if let Ok(level) = level_str.parse::<i32>() {
if level != 0 {
return; // Skip doors not on ground level
}
}
}
// Process the first node of the door/entrance element
if let Some(&(x, z)) = element.nodes.first() {
// Set the ground block and the door blocks
editor.set_block(&GRAY_CONCRETE, x, ground_level, z, None, None);
editor.set_block(&DARK_OAK_DOOR_LOWER, x, ground_level + 1, z, None, None);
editor.set_block(&DARK_OAK_DOOR_UPPER, x, ground_level + 2, z, None, None);
}
}
}

View File

@@ -0,0 +1,256 @@
use crate::world_editor::WorldEditor;
use crate::osm_parser::ProcessedElement;
use crate::block_definitions::*;
use crate::bresenham::bresenham_line;
use crate::floodfill::flood_fill_area; // Assuming you have a flood fill function for area filling
pub fn generate_highways(editor: &mut WorldEditor, element: &ProcessedElement, ground_level: i32) {
if let Some(highway_type) = element.tags.get("highway") {
if highway_type == "street_lamp" {
// Handle street lamps
if let Some(first_node) = element.nodes.first() {
let (x, z) = *first_node;
for y in 1..=4 {
editor.set_block(&OAK_FENCE, x, ground_level + y, z, None, None);
}
editor.set_block(&GLOWSTONE, x, ground_level + 5, z, None, None);
}
} else if highway_type == "crossing" {
// Handle traffic signals for crossings
if let Some(crossing_type) = element.tags.get("crossing") {
if crossing_type == "traffic_signals" {
if let Some(first_node) = element.nodes.first() {
let (x, z) = *first_node;
for y in 1..=3 {
editor.set_block(&COBBLESTONE_WALL, x, ground_level + y, z, None, None);
}
editor.set_block(&GREEN_WOOL, x, ground_level + 4, z, None, None);
editor.set_block(&YELLOW_WOOL, x, ground_level + 5, z, None, None);
editor.set_block(&RED_WOOL, x, ground_level + 6, z, None, None);
}
}
}
} else if highway_type == "bus_stop" {
// Handle bus stops
if let Some(first_node) = element.nodes.first() {
let (x, z) = *first_node;
for y in 1..=3 {
editor.set_block(&COBBLESTONE_WALL, x, ground_level + y, z, None, None);
}
editor.set_block(&WHITE_WOOL, x, ground_level + 4, z, None, None);
editor.set_block(&WHITE_WOOL, x + 1, ground_level + 4, z, None, None);
}
} else if element.tags.get("area").map_or(false, |v| v == "yes") {
// Handle areas like pedestrian plazas
let mut surface_block: &once_cell::sync::Lazy<Block> = &STONE; // Default block
// Determine the block type based on the 'surface' tag
if let Some(surface) = element.tags.get("surface") {
surface_block = match surface.as_str() {
"paving_stones" | "sett" => &STONE_BRICKS,
"bricks" => &BRICK,
"wood" => &OAK_PLANKS,
"asphalt" => &BLACK_CONCRETE,
"gravel" | "fine_gravel" => &GRAVEL,
"grass" => &GRASS_BLOCK,
"dirt" => &DIRT,
"sand" => &SAND,
"concrete" => &LIGHT_GRAY_CONCRETE,
_ => &STONE, // Default to stone for unknown surfaces
};
}
// Fill the area using flood fill or by iterating through the nodes
let polygon_coords: Vec<(i32, i32)> = element.nodes.iter().copied().collect();
let filled_area = flood_fill_area(&polygon_coords, 2);
for (x, z) in filled_area {
editor.set_block(surface_block, x, ground_level, z, None, None);
}
} else {
let mut previous_node: Option<(i32, i32)> = None;
let mut block_type: &once_cell::sync::Lazy<Block> = &BLACK_CONCRETE;
let mut block_range: i32 = 2;
let mut add_stripe = false; // Flag for adding stripes
// Skip if 'layer' or 'level' is negative in the tags
if let Some(layer) = element.tags.get("layer") {
if layer.parse::<i32>().unwrap_or(0) < 0 {
return;
}
}
if let Some(level) = element.tags.get("level") {
if level.parse::<i32>().unwrap_or(0) < 0 {
return;
}
}
// Determine block type and range based on highway type
match highway_type.as_str() {
"footway" | "pedestrian" => {
block_type = &GRAY_CONCRETE;
block_range = 1;
}
"path" => {
block_type = &LIGHT_GRAY_CONCRETE;
block_range = 1;
}
"motorway" | "primary" => {
block_range = 5;
add_stripe = true; // Add stripes for motorways and primary roads
}
"track" => {
block_range = 1;
}
"service" => {
block_type = &GRAY_CONCRETE;
block_range = 2;
}
_ => {
if let Some(lanes) = element.tags.get("lanes") {
if lanes == "2" {
block_range = 3;
add_stripe = true;
} else if lanes != "1" {
block_range = 4;
add_stripe = true;
}
}
}
}
// Iterate over nodes to create the highway
for &node in &element.nodes {
if let Some(prev) = previous_node {
let (x1, z1) = prev;
let (x2, z2) = node;
// Generate the line of coordinates between the two nodes
let bresenham_points: Vec<(i32, i32, i32)> =
bresenham_line(x1, ground_level, z1, x2, ground_level, z2);
// Variables to manage dashed line pattern
let mut stripe_length = 0;
let dash_length = 5; // Length of the solid part of the stripe
let gap_length = 5; // Length of the gap part of the stripe
for (x, _, z) in bresenham_points {
// Draw the road surface for the entire width
for dx in -block_range..=block_range {
for dz in -block_range..=block_range {
let set_x: i32 = x + dx;
let set_z: i32 = z + dz;
// Zebra crossing logic
if highway_type == "footway"
&& element.tags.get("footway") == Some(&"crossing".to_string())
{
let is_horizontal: bool = (x2 - x1).abs() >= (z2 - z1).abs();
if is_horizontal {
if set_x % 2 < 1 {
editor.set_block(
&WHITE_CONCRETE,
set_x,
ground_level,
set_z,
Some(&[&BLACK_CONCRETE]),
None,
);
} else {
editor.set_block(
&BLACK_CONCRETE,
set_x,
ground_level,
set_z,
None,
None,
);
}
} else {
if set_z % 2 < 1 {
editor.set_block(
&WHITE_CONCRETE,
set_x,
ground_level,
set_z,
Some(&[&BLACK_CONCRETE]),
None,
);
} else {
editor.set_block(
&BLACK_CONCRETE,
set_x,
ground_level,
set_z,
None,
None,
);
}
}
} else {
editor.set_block(
block_type,
set_x,
ground_level,
set_z,
None,
Some(&[&BLACK_CONCRETE, &WHITE_CONCRETE]),
);
}
}
}
// Add a dashed white line in the middle for larger roads
if add_stripe {
if stripe_length < dash_length {
let stripe_x: i32 = x;
let stripe_z: i32 = z;
editor.set_block(
&WHITE_CONCRETE,
stripe_x,
ground_level,
stripe_z,
Some(&[&BLACK_CONCRETE]),
None,
);
}
// Increment stripe_length and reset after completing a dash and gap
stripe_length += 1;
if stripe_length >= dash_length + gap_length {
stripe_length = 0;
}
}
}
}
previous_node = Some(node);
}
}
}
}
/// Generates a siding using stone brick slabs
pub fn generate_siding(editor: &mut WorldEditor, element: &ProcessedElement, ground_level: i32) {
let mut previous_node: Option<(i32, i32)> = None;
let siding_block: &once_cell::sync::Lazy<Block> = &STONE_BRICK_SLAB;
for &node in &element.nodes {
let (x, z) = node;
// Draw the siding using Bresenham's line algorithm between nodes
if let Some(prev) = previous_node {
let bresenham_points: Vec<(i32, i32, i32)> = bresenham_line(prev.0, ground_level + 1, prev.1, x, ground_level + 1, z);
for (bx, by, bz) in bresenham_points {
if !editor.check_for_block(bx, by - 1, bz, None, Some(&[&BLACK_CONCRETE, &WHITE_CONCRETE])) {
editor.set_block(siding_block, bx, by, bz, None, None);
}
}
}
previous_node = Some(node);
}
}

View File

@@ -0,0 +1,208 @@
use crate::world_editor::WorldEditor;
use crate::osm_parser::ProcessedElement;
use crate::block_definitions::*;
use crate::bresenham::bresenham_line;
use crate::floodfill::flood_fill_area;
use crate::element_processing::tree::create_tree;
use rand::Rng;
pub fn generate_landuse(editor: &mut WorldEditor, element: &ProcessedElement, ground_level: i32) {
let mut previous_node: Option<(i32, i32)> = None;
let mut corner_addup: (i32, i32, i32) = (0, 0, 0);
let mut current_landuse: Vec<(i32, i32)> = vec![];
// Determine block type based on landuse tag
let binding: String = "".to_string();
let landuse_tag: &String = element.tags.get("landuse").unwrap_or(&binding);
let block_type: &once_cell::sync::Lazy<Block> = match landuse_tag.as_str() {
"greenfield" | "meadow" | "grass" => &GRASS_BLOCK,
"farmland" => &FARMLAND,
"forest" => &GRASS_BLOCK,
"cemetery" => &PODZOL,
"beach" => &SAND,
"construction" => &DIRT,
"traffic_island" => &STONE_BLOCK_SLAB,
"residential" => &STONE_BRICKS,
"commercial" => &SMOOTH_STONE,
"education" => &LIGHT_GRAY_CONCRETE,
"industrial" => &COBBLESTONE,
"military" => &GRAY_CONCRETE,
"railway" => &GRAVEL,
_ => &GRASS_BLOCK,
};
// Process landuse nodes to fill the area
for &node in &element.nodes {
let (x, z) = node;
if let Some(prev) = previous_node {
// Generate the line of coordinates between the two nodes
let bresenham_points: Vec<(i32, i32, i32)> = bresenham_line(prev.0, ground_level, prev.1, x, ground_level, z);
for (bx, _, bz) in bresenham_points {
editor.set_block(&GRASS_BLOCK, bx, ground_level, bz, None, None);
}
current_landuse.push((x, z));
corner_addup = (corner_addup.0 + x, corner_addup.1 + z, corner_addup.2 + 1);
}
previous_node = Some(node);
}
// If there are landuse nodes, flood-fill the area
if !current_landuse.is_empty() {
let polygon_coords: Vec<(i32, i32)> = element.nodes.iter().copied().collect();
let floor_area: Vec<(i32, i32)> = flood_fill_area(&polygon_coords, 2);
let mut rng: rand::prelude::ThreadRng = rand::thread_rng();
for (x, z) in floor_area {
if landuse_tag == "traffic_island" {
editor.set_block(block_type, x, ground_level + 1, z, None, None);
} else if landuse_tag == "construction" || landuse_tag == "railway" {
editor.set_block(block_type, x, ground_level, z, None, Some(&[&SPONGE]));
} else {
editor.set_block(block_type, x, ground_level, z, None, None);
}
// Add specific features for different landuse types
match landuse_tag.as_str() {
"cemetery" => {
if (x % 3 == 0) && (z % 3 == 0) {
let random_choice: i32 = rng.gen_range(0..100);
if random_choice < 15 {
// Place graves
if editor.check_for_block(x, ground_level, z, Some(&[&PODZOL]), None) {
if rng.gen_bool(0.5) {
editor.set_block(&COBBLESTONE, x - 1, ground_level + 1, z, None, None);
editor.set_block(&STONE_BRICK_SLAB, x - 1, ground_level + 2, z, None, None);
editor.set_block(&STONE_BRICK_SLAB, x, ground_level + 1, z, None, None);
editor.set_block(&STONE_BRICK_SLAB, x + 1, ground_level + 1, z, None, None);
} else {
editor.set_block(&COBBLESTONE, x, ground_level + 1, z - 1, None, None);
editor.set_block(&STONE_BRICK_SLAB, x, ground_level + 2, z - 1, None, None);
editor.set_block(&STONE_BRICK_SLAB, x, ground_level + 1, z, None, None);
editor.set_block(&STONE_BRICK_SLAB, x, ground_level + 1, z + 1, None, None);
}
}
} else if random_choice < 30 {
if editor.check_for_block(x, ground_level, z, Some(&[&PODZOL]), None) {
editor.set_block(&RED_FLOWER, x, ground_level + 1, z, None, None);
}
} else if random_choice < 33 {
create_tree(editor, x, ground_level + 1, z, rng.gen_range(1..=3));
}
}
}
"forest" => {
if !editor.check_for_block(x, ground_level, z, None, Some(&[&WATER])) {
let random_choice: i32 = rng.gen_range(0..21);
if random_choice == 20 {
create_tree(editor, x, ground_level + 1, z, rng.gen_range(1..=3));
} else if random_choice == 2 {
let flower_block: &once_cell::sync::Lazy<Block> = match rng.gen_range(1..=4) {
1 => &RED_FLOWER,
2 => &BLUE_FLOWER,
3 => &YELLOW_FLOWER,
_ => &WHITE_FLOWER,
};
editor.set_block(flower_block, x, ground_level + 1, z, None, None);
} else if random_choice <= 1 {
editor.set_block(&GRASS, x, ground_level + 1, z, None, None);
}
}
}
"farmland" => {
// Check if the current block is not water or another undesired block
if !editor.check_for_block(x, ground_level, z, None, Some(&[&WATER])) {
if x % 15 == 0 || z % 15 == 0 {
// Place water on the edges
editor.set_block(&WATER, x, ground_level, z, Some(&[&FARMLAND]), None);
editor.set_block(&AIR, x, ground_level + 1, z, Some(&[&GRASS, &WHEAT, &CARROTS, &POTATOES]), None);
} else {
// Set the block below as farmland
editor.set_block(&FARMLAND, x, ground_level, z, None, None);
// If a random condition is met, place a special object
if rng.gen_range(0..76) == 0 {
let special_choice = rng.gen_range(1..=10);
if special_choice <= 2 {
create_tree(editor, x, ground_level + 1, z, rng.gen_range(1..=3));
} else if special_choice <= 6 {
editor.set_block(&HAY_BALE, x, ground_level + 1, z, None, None);
} else {
editor.set_block(&OAK_LEAVES, x, ground_level + 1, z, None, None);
}
} else {
// Set crops only if the block below is farmland
if editor.check_for_block(x, ground_level, z, Some(&[&FARMLAND]), None) {
let crop_choice = [&WHEAT, &CARROTS, &POTATOES][rng.gen_range(0..3)];
editor.set_block(crop_choice, x, ground_level + 1, z, None, None);
}
}
}
}
}
"construction" => {
let random_choice: i32 = rng.gen_range(0..1501);
if random_choice < 6 {
editor.set_block(&SCAFFOLDING, x, ground_level + 1, z, None, None);
if random_choice < 2 {
editor.set_block(&SCAFFOLDING, x, ground_level + 2, z, None, None);
editor.set_block(&SCAFFOLDING, x, ground_level + 3, z, None, None);
} else if random_choice < 4 {
editor.set_block(&SCAFFOLDING, x, ground_level + 2, z, None, None);
editor.set_block(&SCAFFOLDING, x, ground_level + 3, z, None, None);
editor.set_block(&SCAFFOLDING, x, ground_level + 4, z, None, None);
editor.set_block(&SCAFFOLDING, x, ground_level + 1, z + 1, None, None);
} else {
editor.set_block(&SCAFFOLDING, x, ground_level + 2, z, None, None);
editor.set_block(&SCAFFOLDING, x, ground_level + 3, z, None, None);
editor.set_block(&SCAFFOLDING, x, ground_level + 4, z, None, None);
editor.set_block(&SCAFFOLDING, x, ground_level + 5, z, None, None);
editor.set_block(&SCAFFOLDING, x - 1, ground_level + 1, z, None, None);
editor.set_block(&SCAFFOLDING, x + 1, ground_level + 1, z - 1, None, None);
}
} else if random_choice < 30 {
let construction_items: [&once_cell::sync::Lazy<Block>; 11] = [
&OAK_LOG, &COBBLESTONE, &GRAVEL, &GLOWSTONE, &STONE,
&COBBLESTONE_WALL, &BLACK_CONCRETE, &SAND, &OAK_PLANKS, &DIRT, &BRICK,
];
editor.set_block(construction_items[rng.gen_range(0..construction_items.len())], x, ground_level + 1, z, None, None);
} else if random_choice < 35 {
if random_choice < 30 {
editor.set_block(&DIRT, x, ground_level + 1, z, None, None);
editor.set_block(&DIRT, x, ground_level + 2, z, None, None);
editor.set_block(&DIRT, x + 1, ground_level + 1, z, None, None);
editor.set_block(&DIRT, x, ground_level + 1, z + 1, None, None);
} else {
editor.set_block(&DIRT, x, ground_level + 1, z, None, None);
editor.set_block(&DIRT, x, ground_level + 2, z, None, None);
editor.set_block(&DIRT, x - 1, ground_level + 1, z, None, None);
editor.set_block(&DIRT, x, ground_level + 1, z - 1, None, None);
}
} else if random_choice < 150 {
editor.set_block(&AIR, x, ground_level, z, None, Some(&[&SPONGE]));
}
}
"grass" => {
if rng.gen_range(1..=7) != 1 && !editor.check_for_block(x, ground_level, z, None, Some(&[&WATER])) {
editor.set_block(&GRASS, x, ground_level + 1, z, None, None);
}
}
"meadow" => {
if !editor.check_for_block(x, ground_level, z, None, Some(&[&WATER])) {
let random_choice: i32 = rng.gen_range(0..1001);
if random_choice < 5 {
create_tree(editor, x, ground_level + 1, z, rng.gen_range(1..=3));
} else if random_choice < 800 {
editor.set_block(&GRASS, x, ground_level + 1, z, None, None);
}
}
}
_ => {}
}
}
}
}

View File

@@ -0,0 +1,115 @@
use crate::world_editor::WorldEditor;
use crate::osm_parser::ProcessedElement;
use crate::block_definitions::*;
use crate::bresenham::bresenham_line;
use crate::floodfill::flood_fill_area;
use crate::element_processing::tree::create_tree;
use rand::Rng;
pub fn generate_leisure(editor: &mut WorldEditor, element: &ProcessedElement, ground_level: i32) {
if let Some(leisure_type) = element.tags.get("leisure") {
let mut previous_node: Option<(i32, i32)> = None;
let mut corner_addup: (i32, i32, i32) = (0, 0, 0);
let mut current_leisure: Vec<(i32, i32)> = vec![];
// Determine block type based on leisure type
let block_type: &once_cell::sync::Lazy<Block> = match leisure_type.as_str() {
"park" => &GRASS_BLOCK,
"playground" | "recreation_ground" | "pitch" => &GREEN_STAINED_HARDENED_CLAY,
"garden" => &GRASS_BLOCK,
"swimming_pool" => &WATER,
_ => &GRASS_BLOCK,
};
// Process leisure area nodes
for &node in &element.nodes {
if let Some(prev) = previous_node {
// Draw a line between the current and previous node
let bresenham_points: Vec<(i32, i32, i32)> = bresenham_line(prev.0, ground_level, prev.1, node.0, ground_level, node.1);
for (bx, _, bz) in bresenham_points {
editor.set_block(block_type, bx, ground_level, bz, Some(&[&GRASS_BLOCK, &STONE_BRICKS, &SMOOTH_STONE, &LIGHT_GRAY_CONCRETE, &COBBLESTONE, &GRAY_CONCRETE]), None);
}
current_leisure.push((node.0, node.1));
corner_addup.0 += node.0;
corner_addup.1 += node.1;
corner_addup.2 += 1;
}
previous_node = Some(node);
}
// Flood-fill the interior of the leisure area
if corner_addup != (0, 0, 0) {
let polygon_coords: Vec<(i32, i32)> = element.nodes.iter().copied().collect();
let filled_area: Vec<(i32, i32)> = flood_fill_area(&polygon_coords, 2);
for (x, z) in filled_area {
editor.set_block(block_type, x, ground_level, z, Some(&[&GRASS_BLOCK]), None);
// Add decorative elements for parks and gardens
if matches!(leisure_type.as_str(), "park" | "garden") {
if editor.check_for_block(x, ground_level, z, Some(&[&GRASS_BLOCK]), None) {
let mut rng: rand::prelude::ThreadRng = rand::thread_rng();
let random_choice: i32 = rng.gen_range(0..1000);
match random_choice {
0 => { // Benches
editor.set_block(&OAK_LOG, x, ground_level + 1, z, None, None);
editor.set_block(&OAK_LOG, x + 1, ground_level + 1, z, None, None);
editor.set_block(&OAK_LOG, x - 1, ground_level + 1, z, None, None);
}
1..=30 => { // Flowers
let flower_choice: &once_cell::sync::Lazy<Block> = match rng.gen_range(0..4) {
0 => &RED_FLOWER,
1 => &YELLOW_FLOWER,
2 => &BLUE_FLOWER,
_ => &WHITE_FLOWER,
};
editor.set_block(flower_choice, x, ground_level + 1, z, None, None);
}
31..=70 => { // Grass
editor.set_block(&GRASS, x, ground_level + 1, z, None, None);
}
71..=80 => { // Tree
create_tree(editor, x, ground_level + 1, z, rng.gen_range(1..=3));
}
_ => {}
}
}
}
// Add playground or recreation ground features
if matches!(leisure_type.as_str(), "playground" | "recreation_ground") {
let mut rng: rand::prelude::ThreadRng = rand::thread_rng();
let random_choice: i32 = rng.gen_range(0..5000);
match random_choice {
0..=10 => { // Swing set
for y in 1..=4 {
editor.set_block(&OAK_FENCE, x - 1, ground_level + y, z, None, None);
editor.set_block(&OAK_FENCE, x + 1, ground_level + y, z, None, None);
}
editor.set_block(&OAK_FENCE, x, ground_level + 4, z, None, None);
editor.set_block(&STONE_BLOCK_SLAB, x, ground_level + 2, z, None, None);
}
11..=20 => { // Slide
editor.set_block(&OAK_SLAB, x, ground_level + 1, z, None, None);
editor.set_block(&OAK_SLAB, x + 1, ground_level + 2, z, None, None);
editor.set_block(&OAK_SLAB, x + 2, ground_level + 3, z, None, None);
editor.set_block(&OAK_PLANKS, x + 2, ground_level + 2, z, None, None);
editor.set_block(&OAK_PLANKS, x + 2, ground_level + 1, z, None, None);
editor.set_block(&LADDER, x + 2, ground_level + 2, z - 1, None, None);
editor.set_block(&LADDER, x + 2, ground_level + 1, z - 1, None, None);
}
21..=30 => { // Sandpit
editor.fill_blocks(&SAND, x - 3, ground_level, z - 3, x + 3, ground_level, z + 3, Some(&[&GREEN_STAINED_HARDENED_CLAY]), None);
}
_ => {}
}
}
}
}
}
}

View File

@@ -0,0 +1,12 @@
pub mod amenities;
pub mod barriers;
pub mod bridges;
pub mod buildings;
pub mod doors;
pub mod highways;
pub mod landuse;
pub mod leisure;
pub mod natural;
pub mod railways;
pub mod tree;
pub mod waterways;

View File

@@ -0,0 +1,84 @@
use crate::world_editor::WorldEditor;
use crate::osm_parser::ProcessedElement;
use crate::block_definitions::*;
use crate::bresenham::bresenham_line;
use crate::floodfill::flood_fill_area;
use crate::element_processing::tree::create_tree;
use rand::Rng;
pub fn generate_natural(editor: &mut WorldEditor, element: &ProcessedElement, ground_level: i32) {
if let Some(natural_type) = element.tags.get("natural") {
if natural_type == "tree" {
if let Some(first_node) = element.nodes.first() {
let (x, z) = *first_node;
let mut rng: rand::prelude::ThreadRng = rand::thread_rng();
create_tree(editor, x, ground_level + 1, z, rng.gen_range(1..=3));
}
} else {
let mut previous_node: Option<(i32, i32)> = None;
let mut corner_addup: (i32, i32, i32) = (0, 0, 0);
let mut current_natural: Vec<(i32, i32)> = vec![];
// Determine block type based on natural tag
let block_type: &once_cell::sync::Lazy<Block> = match natural_type.as_str() {
"scrub" | "grassland" | "wood" => &GRASS_BLOCK,
"beach" | "sand" => &SAND,
"tree_row" => &GRASS_BLOCK,
"wetland" | "water" => &WATER,
_ => &GRASS_BLOCK,
};
// Process natural nodes to fill the area
for &node in &element.nodes {
let (x, z) = node;
if let Some(prev) = previous_node {
// Generate the line of coordinates between the two nodes
let bresenham_points: Vec<(i32, i32, i32)> = bresenham_line(prev.0, ground_level, prev.1, x, ground_level, z);
for (bx, _, bz) in bresenham_points {
editor.set_block(block_type, bx, ground_level, bz, None, None);
}
current_natural.push((x, z));
corner_addup = (corner_addup.0 + x, corner_addup.1 + z, corner_addup.2 + 1);
}
previous_node = Some(node);
}
// If there are natural nodes, flood-fill the area
if corner_addup != (0, 0, 0) {
let polygon_coords: Vec<(i32, i32)> = element.nodes.iter().copied().collect();
let filled_area: Vec<(i32, i32)> = flood_fill_area(&polygon_coords, 2);
let mut rng: rand::prelude::ThreadRng = rand::thread_rng();
for (x, z) in filled_area {
editor.set_block(block_type, x, ground_level, z, None, None);
// Generate elements for "wood" and "tree_row"
if natural_type == "wood" || natural_type == "tree_row" {
if editor.check_for_block(x, ground_level, z, None, Some(&[&WATER])) {
continue;
}
let random_choice: i32 = rng.gen_range(0..26);
if random_choice == 25 {
create_tree(editor, x, ground_level + 1, z, rng.gen_range(1..=3));
} else if random_choice == 2 {
let flower_block = match rng.gen_range(1..=4) {
1 => &RED_FLOWER,
2 => &BLUE_FLOWER,
3 => &YELLOW_FLOWER,
_ => &WHITE_FLOWER,
};
editor.set_block(flower_block, x, ground_level + 1, z, None, None);
} else if random_choice <= 1 {
editor.set_block(&GRASS, x, ground_level + 1, z, None, None);
}
}
}
}
}
}
}

View File

@@ -0,0 +1,42 @@
use crate::world_editor::WorldEditor;
use crate::osm_parser::ProcessedElement;
use crate::block_definitions::*;
use crate::bresenham::bresenham_line;
pub fn generate_railways(editor: &mut WorldEditor, element: &ProcessedElement, ground_level: i32) {
if let Some(railway_type) = element.tags.get("railway") {
if ["proposed", "abandoned", "subway", "construction"].contains(&railway_type.as_str()) {
return;
}
if let Some(subway) = element.tags.get("subway") {
if subway == "yes" {
return;
}
}
if let Some(tunnel) = element.tags.get("tunnel") {
if tunnel == "yes" {
return;
}
}
for i in 1..element.nodes.len() {
let (x1, z1) = element.nodes[i - 1];
let (x2, z2) = element.nodes[i];
// Generate the line of coordinates between the two nodes
let bresenham_points: Vec<(i32, i32, i32)> = bresenham_line(x1, ground_level, z1, x2, ground_level, z2);
for (bx, _, bz) in bresenham_points {
// TODO: Set direction of rail
editor.set_block(&IRON_BLOCK, bx, ground_level, bz, None, None);
editor.set_block(&RAIL, bx, ground_level + 1, bz, None, None);
if bx % 4 == 0 {
editor.set_block(&OAK_LOG, bx, ground_level, bz, None, None);
}
}
}
}
}

View File

@@ -0,0 +1,115 @@
use crate::block_definitions::*;
use crate::world_editor::WorldEditor;
use once_cell::sync::Lazy;
/// Helper function to set blocks in a circular pattern around a central point.
fn round1(editor: &mut WorldEditor, material: &'static Lazy<Block>, x: i32, y: i32, z: i32) {
editor.set_block(material, x - 2, y, z, None, None);
editor.set_block(material, x + 2, y, z, None, None);
editor.set_block(material, x, y, z - 2, None, None);
editor.set_block(material, x, y, z + 2, None, None);
editor.set_block(material, x - 1, y, z - 1, None, None);
editor.set_block(material, x + 1, y, z + 1, None, None);
editor.set_block(material, x + 1, y, z - 1, None, None);
editor.set_block(material, x - 1, y, z + 1, None, None);
}
/// Helper function to set blocks in a wider circular pattern.
fn round2(editor: &mut WorldEditor, material: &'static Lazy<Block>, x: i32, y: i32, z: i32) {
editor.set_block(material, x + 3, y, z, None, None);
editor.set_block(material, x + 2, y, z - 1, None, None);
editor.set_block(material, x + 2, y, z + 1, None, None);
editor.set_block(material, x + 1, y, z - 2, None, None);
editor.set_block(material, x + 1, y, z + 2, None, None);
editor.set_block(material, x - 3, y, z, None, None);
editor.set_block(material, x - 2, y, z - 1, None, None);
editor.set_block(material, x - 2, y, z + 1, None, None);
editor.set_block(material, x - 1, y, z + 2, None, None);
editor.set_block(material, x - 1, y, z - 2, None, None);
editor.set_block(material, x, y, z - 3, None, None);
editor.set_block(material, x, y, z + 3, None, None);
}
/// Helper function to set blocks in a more scattered circular pattern.
fn round3(editor: &mut WorldEditor, material: &'static Lazy<Block>, x: i32, y: i32, z: i32) {
editor.set_block(material, x + 3, y, z - 1, None, None);
editor.set_block(material, x + 3, y, z + 1, None, None);
editor.set_block(material, x + 2, y, z - 2, None, None);
editor.set_block(material, x + 2, y, z + 2, None, None);
editor.set_block(material, x + 1, y, z - 3, None, None);
editor.set_block(material, x + 1, y, z + 3, None, None);
editor.set_block(material, x - 3, y, z - 1, None, None);
editor.set_block(material, x - 3, y, z + 1, None, None);
editor.set_block(material, x - 2, y, z - 2, None, None);
editor.set_block(material, x - 2, y, z + 2, None, None);
editor.set_block(material, x - 1, y, z + 3, None, None);
editor.set_block(material, x - 1, y, z - 3, None, None);
}
/// Function to create different types of trees.
pub fn create_tree(editor: &mut WorldEditor, x: i32, y: i32, z: i32, typetree: u8) {
let mut blacklist: Vec<&'static Lazy<Block>> = Vec::new();
blacklist.extend(building_corner_variations());
blacklist.extend(building_wall_variations());
blacklist.extend(building_floor_variations());
blacklist.push(&WATER);
if editor.check_for_block(x, y - 1, z, None, Some(&blacklist)) {
return;
}
match typetree {
1 => { // Oak tree
editor.fill_blocks(&OAK_LOG, x, y, z, x, y + 8, z, None, None);
editor.fill_blocks(&OAK_LEAVES, x - 1, y + 3, z, x - 1, y + 9, z, None, None);
editor.fill_blocks(&OAK_LEAVES, x + 1, y + 3, z, x + 1, y + 9, z, None, None);
editor.fill_blocks(&OAK_LEAVES, x, y + 3, z - 1, x, y + 9, z - 1, None, None);
editor.fill_blocks(&OAK_LEAVES, x, y + 3, z + 1, x, y + 9, z + 1, None, None);
editor.fill_blocks(&OAK_LEAVES, x, y + 9, z, x, y + 10, z, None, None);
round1(editor, &OAK_LEAVES, x, y + 8, z);
round1(editor, &OAK_LEAVES, x, y + 7, z);
round1(editor, &OAK_LEAVES, x, y + 6, z);
round1(editor, &OAK_LEAVES, x, y + 5, z);
round1(editor, &OAK_LEAVES, x, y + 4, z);
round1(editor, &OAK_LEAVES, x, y + 3, z);
round2(editor, &OAK_LEAVES, x, y + 7, z);
round2(editor, &OAK_LEAVES, x, y + 6, z);
round2(editor, &OAK_LEAVES, x, y + 5, z);
round2(editor, &OAK_LEAVES, x, y + 4, z);
round3(editor, &OAK_LEAVES, x, y + 6, z);
round3(editor, &OAK_LEAVES, x, y + 5, z);
}
2 => { // Spruce tree
editor.fill_blocks(&SPRUCE_LOG, x, y, z, x, y + 9, z, None, None);
editor.fill_blocks(&BIRCH_LEAVES, x - 1, y + 3, z, x - 1, y + 10, z, None, None);
editor.fill_blocks(&BIRCH_LEAVES, x + 1, y + 3, z, x + 1, y + 10, z, None, None);
editor.fill_blocks(&BIRCH_LEAVES, x, y + 3, z - 1, x, y + 10, z - 1, None, None);
editor.fill_blocks(&BIRCH_LEAVES, x, y + 3, z + 1, x, y + 10, z + 1, None, None);
editor.set_block(&BIRCH_LEAVES, x, y + 10, z, None, None);
round1(editor, &BIRCH_LEAVES, x, y + 9, z);
round1(editor, &BIRCH_LEAVES, x, y + 7, z);
round1(editor, &BIRCH_LEAVES, x, y + 6, z);
round1(editor, &BIRCH_LEAVES, x, y + 4, z);
round1(editor, &BIRCH_LEAVES, x, y + 3, z);
round2(editor, &BIRCH_LEAVES, x, y + 6, z);
round2(editor, &BIRCH_LEAVES, x, y + 3, z);
}
3 => { // Birch tree
editor.fill_blocks(&BIRCH_LOG, x, y, z, x, y + 6, z, None, None);
editor.fill_blocks(&BIRCH_LEAVES, x - 1, y + 2, z, x - 1, y + 7, z, None, None);
editor.fill_blocks(&BIRCH_LEAVES, x + 1, y + 2, z, x + 1, y + 7, z, None, None);
editor.fill_blocks(&BIRCH_LEAVES, x, y + 2, z - 1, x, y + 7, z - 1, None, None);
editor.fill_blocks(&BIRCH_LEAVES, x, y + 2, z + 1, x, y + 7, z + 1, None, None);
editor.fill_blocks(&BIRCH_LEAVES, x, y + 7, z, x, y + 8, z, None, None);
round1(editor, &BIRCH_LEAVES, x, y + 6, z);
round1(editor, &BIRCH_LEAVES, x, y + 5, z);
round1(editor, &BIRCH_LEAVES, x, y + 4, z);
round1(editor, &BIRCH_LEAVES, x, y + 3, z);
round1(editor, &BIRCH_LEAVES, x, y + 2, z);
round2(editor, &BIRCH_LEAVES, x, y + 2, z);
round2(editor, &BIRCH_LEAVES, x, y + 3, z);
round2(editor, &BIRCH_LEAVES, x, y + 4, z);
}
_ => {} // Do nothing if typetree is not recognized
}
}

View File

@@ -0,0 +1,45 @@
use crate::world_editor::WorldEditor;
use crate::osm_parser::ProcessedElement;
use crate::block_definitions::*;
use crate::bresenham::bresenham_line;
pub fn generate_waterways(editor: &mut WorldEditor, element: &ProcessedElement, ground_level: i32) {
if let Some(_waterway_type) = element.tags.get("waterway") {
let mut previous_node: Option<(i32, i32)> = None;
let mut waterway_width: i32 = 4; // Default waterway width
// Check for custom width in tags
if let Some(width_str) = element.tags.get("width") {
waterway_width = width_str.parse::<i32>().unwrap_or_else(|_| {
width_str
.parse::<f32>()
.map(|f: f32| f as i32)
.unwrap_or(waterway_width)
});
}
// Process nodes to create waterways
for &node in &element.nodes {
if let Some(prev) = previous_node {
// Skip layers below the ground level
if !matches!(
element.tags.get("layer").map(|s| s.as_str()),
Some("-1") | Some("-2") | Some("-3")
) {
// Draw a line between the current and previous node
let bresenham_points: Vec<(i32, i32, i32)> =
bresenham_line(prev.0, ground_level, prev.1, node.0, ground_level, node.1);
for (bx, _, bz) in bresenham_points {
for x in (bx - waterway_width / 2)..=(bx + waterway_width / 2) {
for z in (bz - waterway_width / 2)..=(bz + waterway_width / 2) {
editor.set_block(&WATER, x, ground_level, z, None, None); // Set water block
editor.set_block(&AIR, x, ground_level + 1, z, Some(&[&GRASS, &WHEAT, &CARROTS, &POTATOES]), None);
}
}
}
}
}
previous_node = Some(node);
}
}
}

79
src/floodfill.rs Normal file
View File

@@ -0,0 +1,79 @@
use std::collections::{HashSet, VecDeque};
use std::time::{Duration, Instant};
use geo::{LineString, Point, Polygon, Contains};
use itertools::Itertools;
/// Perform a flood-fill to find the area inside a polygon.
/// Returns a vector of (x, z) coordinates representing the filled area.
pub fn flood_fill_area(polygon_coords: &[(i32, i32)], timeout_secs: u64) -> Vec<(i32, i32)> {
if polygon_coords.len() < 3 {
return vec![]; // Not a valid polygon
}
let start_time = Instant::now();
let timeout = Duration::from_secs(timeout_secs);
// Calculate bounding box of the polygon using itertools
let (min_x, max_x) = polygon_coords.iter().map(|&(x, _)| x).minmax().into_option().unwrap();
let (min_z, max_z) = polygon_coords.iter().map(|&(_, z)| z).minmax().into_option().unwrap();
let mut filled_area: Vec<(i32, i32)> = Vec::new();
let mut visited: HashSet<(i32, i32)> = HashSet::new();
// Convert input to a geo::Polygon for efficient point-in-polygon testing
let exterior_coords: Vec<(f64, f64)> = polygon_coords.iter().map(|&(x, z)| (x as f64, z as f64)).collect::<Vec<_>>();
let exterior: LineString = LineString::from(exterior_coords); // Create LineString from coordinates
let polygon: Polygon<f64> = Polygon::new(exterior, vec![]); // Create Polygon using LineString
// Determine safe step sizes for grid sampling
let step_x = ((max_x - min_x) / 10).max(1); // Ensure step is at least 1
let step_z = ((max_z - min_z) / 10).max(1); // Ensure step is at least 1
// Sample multiple starting points within the bounding box
let mut candidate_points: VecDeque<(i32, i32)> = VecDeque::new();
for x in (min_x..=max_x).step_by(step_x as usize) {
for z in (min_z..=max_z).step_by(step_z as usize) {
candidate_points.push_back((x, z));
}
}
// Attempt flood-fill from each candidate point
while let Some((start_x, start_z)) = candidate_points.pop_front() {
if start_time.elapsed() > timeout {
eprintln!("Floodfill timeout"); // TODO only print when debug arg is set?
break;
}
if polygon.contains(&Point::new(start_x as f64, start_z as f64)) {
// Start flood-fill from the valid interior point
let mut queue: VecDeque<(i32, i32)> = VecDeque::new();
queue.push_back((start_x, start_z));
visited.insert((start_x, start_z));
while let Some((x, z)) = queue.pop_front() {
if start_time.elapsed() > timeout {
eprintln!("Floodfill timeout"); // TODO only print when debug arg is set?
break;
}
if polygon.contains(&Point::new(x as f64, z as f64)) {
filled_area.push((x, z));
// Check adjacent points
for (nx, nz) in [(x - 1, z), (x + 1, z), (x, z - 1), (x, z + 1)].iter() {
if *nx >= min_x && *nx <= max_x && *nz >= min_z && *nz <= max_z && !visited.contains(&(*nx, *nz)) {
visited.insert((*nx, *nz));
queue.push_back((*nx, *nz));
}
}
}
}
if !filled_area.is_empty() {
break; // Exit if a valid area has been flood-filled
}
}
}
filled_area
}

View File

@@ -1,166 +0,0 @@
from random import choice
import json
import os
import requests
import subprocess
def download_with_requests(url, params, filename):
response = requests.get(url, params=params)
if response.status_code == 200:
with open(filename, "w") as file:
json.dump(response.json(), file)
return filename
else:
print("Failed to download data. Status code:", response.status_code)
return None
def download_with_curl(url, params, filename):
# Prepare curl command with parameters
curl_command = [
"curl",
"-o",
filename,
url + "?" + "&".join([f"{key}={value}" for key, value in params.items()]),
]
subprocess.call(curl_command)
return filename
def download_with_wget(url, params, filename):
# Prepare wget command with parameters
wget_command = [
"wget",
"-O",
filename,
url + "?" + "&".join([f"{key}={value}" for key, value in params.items()]),
]
subprocess.call(wget_command)
return filename
def getData(city, state, country, bbox, file, debug, download_method="requests"):
print("Fetching data...")
api_servers = [
"https://overpass-api.de/api/interpreter",
"https://lz4.overpass-api.de/api/interpreter",
"https://z.overpass-api.de/api/interpreter",
"https://overpass.kumi.systems/api/interpreter",
"https://overpass.private.coffee/api/interpreter",
]
url = choice(api_servers)
if city:
query1 = f"""
[out:json];
area[name="{city}"]->.city;
area[name="{state}"]->.state;
area[name="{country}"]->.country;
(
nwr(area.country)(area.state)(area.city)[building];
nwr(area.country)(area.state)(area.city)[highway];
nwr(area.country)(area.state)(area.city)[landuse];
nwr(area.country)(area.state)(area.city)[natural];
nwr(area.country)(area.state)(area.city)[leisure];
nwr(area.country)(area.state)(area.city)[waterway]["waterway"!="fairway"];
nwr(area.country)(area.state)(area.city)[amenity];
nwr(area.country)(area.state)(area.city)[bridge];
nwr(area.country)(area.state)(area.city)[railway];
nwr(area.country)(area.state)(area.city)[barrier];
);
(._;>;);
out;
"""
elif bbox:
bbox = bbox.split(",")
bbox = [float(i) for i in bbox]
if debug:
print(f"Bbox input: {bbox}")
query1 = f"""
[out:json][bbox:{bbox[1]},{bbox[0]},{bbox[3]},{bbox[2]}];
(
nwr["building"];
nwr["highway"];
nwr["landuse"];
nwr["natural"];
nwr["leisure"];
nwr["waterway"];
nwr["amenity"];
nwr["bridge"];
nwr["railway"];
nwr["barrier"];
nwr["entrance"];
nwr["door"];
)->.waysinbbox;
(
node(w.waysinbbox);
)->.nodesinbbox;
.waysinbbox out body;
.nodesinbbox out skel qt;
"""
elif file:
print("Loading data from file")
else:
query1 = f"""
[out:json];
area[name="{city}"]->.city;
area[name="{country}"]->.country;
(
nwr(area.country)(area.city)[building];
nwr(area.country)(area.city)[highway];
nwr(area.country)(area.city)[landuse];
nwr(area.country)(area.city)[natural];
nwr(area.country)(area.city)[leisure];
nwr(area.country)(area.city)[waterway]["waterway"!="fairway"];
nwr(area.country)(area.city)[amenity];
nwr(area.country)(area.city)[bridge];
nwr(area.country)(area.city)[railway];
nwr(area.country)(area.city)[barrier];
);
(._;>;);
out;
"""
if debug:
print(f"OSM Query: {query1}")
try:
if file:
with open("data.json", encoding="utf8") as dataset:
data = json.load(dataset)
else:
if debug:
print(f"Chosen server: {url}")
filename = "arnis-debug-raw_data.json"
if download_method == "requests":
file_path = download_with_requests(url, {"data": query1}, filename)
elif download_method == "curl":
file_path = download_with_curl(url, {"data": query1}, filename)
elif download_method == "wget":
file_path = download_with_wget(url, {"data": query1}, filename)
else:
print("Invalid download method. Using 'requests' by default.")
file_path = download_with_requests(url, {"data": query1}, filename)
if file_path is None:
os._exit(1)
with open(file_path, "r") as file:
data = json.load(file)
if len(data["elements"]) == 0:
print("Error! No data available")
os._exit(1)
except Exception as e:
if "The server is probably too busy to handle your request." in str(e):
print("Error! OSM server overloaded")
elif "Dispatcher_Client::request_read_and_idx::rate_limited" in str(e):
print("Error! IP rate limited, wait before trying again")
else:
print(f"Error! {e}")
os._exit(1)
return data

View File

@@ -1,106 +0,0 @@
#!/usr/bin/env python
# Copyright 2022 by louis-e, https://github.com/louis-e/.
# MIT License
# Please see the LICENSE file that should have been included as part of this package.
import argparse
import gc
import numpy as np
import os
import sys
from .getData import getData
from .processData import processRawData, generateWorld
parser = argparse.ArgumentParser(
description="Arnis - Generate cities from real life in Minecraft"
)
parser.add_argument("--bbox", dest="bbox", help="Bounding box of the area")
parser.add_argument("--city", dest="city", help="Name of the city (Experimental)")
parser.add_argument("--state", dest="state", help="Name of the state (Experimental)")
parser.add_argument(
"--country", dest="country", help="Name of the country (Experimental)"
)
parser.add_argument("--file", dest="file", help="JSON file containing OSM data")
parser.add_argument(
"--path", dest="path", required=True, help="Path to the minecraft world"
)
parser.add_argument(
"--downloader",
dest="downloader",
choices=["requests", "curl", "wget"],
default="requests",
help="Downloader method (requests/curl/wget)",
)
parser.add_argument(
"--debug",
dest="debug",
default=False,
action="store_true",
help="Enable debug mode",
)
parser.add_argument(
"--timeout",
dest="timeout",
default=2,
action="store_true",
help="Set floodfill timeout (seconds)",
)
args = parser.parse_args()
# Ensure either bbox or city/state/country is provided
if not args.bbox and not (args.city and args.state and args.country):
print(
"""Error! You must provide either a bounding box (bbox) or city/state/country \
(experimental) information."""
)
os._exit(1)
# Ensure file argument is handled correctly
if args.file and args.bbox:
print("Error! You cannot provide both a bounding box (bbox) and a file.")
os._exit(1)
gc.collect()
np.seterr(all="raise")
np.set_printoptions(threshold=sys.maxsize)
mcWorldPath = args.path
if mcWorldPath[-1] == "/":
mcWorldPath = mcWorldPath[:-1]
def run():
print(
"""\n
▄████████ ▄████████ ███▄▄▄▄ ▄█ ▄████████
███ ███ ███ ███ ███▀▀▀██▄ ███ ███ ███
███ ███ ███ ███ ███ ███ ███▌ ███ █▀
███ ███ ▄███▄▄▄▄██▀ ███ ███ ███▌ ███
▀███████████ ▀▀███▀▀▀▀▀ ███ ███ ███▌ ▀███████████
███ ███ ▀███████████ ███ ███ ███ ███
███ ███ ███ ███ ███ ███ ███ ▄█ ███
███ █▀ ███ ███ ▀█ █▀ █▀ ▄████████▀
███ ███
https://github.com/louis-e/arnis
"""
)
if not (os.path.exists(mcWorldPath + "/region")):
print("Error! No Minecraft world found at given path")
os._exit(1)
rawdata = getData(
args.city,
args.state,
args.country,
args.bbox,
args.file,
args.debug,
args.downloader,
)
rawData = processRawData(rawdata, args)
generateWorld(rawData)
os._exit(0)

85
src/main.rs Normal file
View File

@@ -0,0 +1,85 @@
mod args;
mod block_definitions;
mod bresenham;
mod data_processing;
mod element_processing;
mod floodfill;
mod osm_parser;
mod retrieve_data;
mod version_check;
mod world_editor;
use args::Args;
use clap::Parser;
use colored::*;
use std::fs::File;
use std::io::Write;
fn print_banner() {
let version: &str = env!("CARGO_PKG_VERSION");
let repository: &str = env!("CARGO_PKG_REPOSITORY");
println!(
r#"
▄████████ ▄████████ ███▄▄▄▄ ▄█ ▄████████
███ ███ ███ ███ ███▀▀▀██▄ ███ ███ ███
███ ███ ███ ███ ███ ███ ███▌ ███ █▀
███ ███ ▄███▄▄▄▄██▀ ███ ███ ███▌ ███
▀███████████ ▀▀███▀▀▀▀▀ ███ ███ ███▌ ▀███████████
███ ███ ▀███████████ ███ ███ ███ ███
███ ███ ███ ███ ███ ███ ███ ▄█ ███
███ █▀ ███ ███ ▀█ █▀ █▀ ▄████████▀
███ ███
version {}
{}
"#,
version,
repository.bright_white().bold()
);
}
fn main() {
print_banner();
// Check for updates
if let Err(e) = version_check::check_for_updates() {
eprintln!("{}: {}", "Error checking for version updates".red().bold(), e);
}
// Parse input arguments
let args: Args = Args::parse();
args.run();
let bbox: Vec<f64> = args.bbox
.as_ref()
.expect("Bounding box is required")
.split(',')
.map(|s: &str| s.parse::<f64>().expect("Invalid bbox coordinate"))
.collect::<Vec<f64>>();
let bbox_tuple: (f64, f64, f64, f64) = (bbox[0], bbox[1], bbox[2], bbox[3]);
// Fetch data
let raw_data: serde_json::Value = retrieve_data::fetch_data(
bbox_tuple,
args.file.as_deref(),
args.debug,
"requests",
).expect("Failed to fetch data");
// Parse raw data
let (mut parsed_elements, scale_factor_x, scale_factor_z) = osm_parser::parse_osm_data(&raw_data, bbox_tuple, &args);
parsed_elements.sort_by_key(|element: &osm_parser::ProcessedElement| osm_parser::get_priority(element));
// Write the parsed OSM data to a file for inspection
if args.debug {
let mut output_file: File = File::create("parsed_osm_data.txt").expect("Failed to create output file");
for element in &parsed_elements {
writeln!(output_file, "Element ID: {}, Type: {}, Tags: {:?}, Nodes: {:?}", element.id, element.r#type, element.tags, element.nodes)
.expect("Failed to write to output file");
}
}
// Generate world
data_processing::generate_world(parsed_elements, &args, scale_factor_x, scale_factor_z);
}

181
src/osm_parser.rs Normal file
View File

@@ -0,0 +1,181 @@
use crate::args::Args;
use colored::Colorize;
use serde_json::Value;
use serde::Deserialize;
use std::collections::HashMap;
#[derive(Debug, Deserialize)]
pub struct OSMElement {
pub r#type: String,
pub id: u64,
pub lat: Option<f64>,
pub lon: Option<f64>,
pub nodes: Option<Vec<u64>>,
pub tags: Option<HashMap<String, String>>,
}
#[derive(Debug, Deserialize)]
pub struct OSMData {
pub elements: Vec<OSMElement>,
}
pub struct ProcessedElement {
pub id: u64,
pub r#type: String,
pub tags: HashMap<String, String>,
pub nodes: Vec<(i32, i32)>, // Minecraft coordinates (x, z)
}
// Function to convert latitude and longitude to Minecraft coordinates.
fn lat_lon_to_minecraft_coords(
lat: f64,
lon: f64,
bbox: (f64, f64, f64, f64), // (min_lon, min_lat, max_lon, max_lat)
scale_factor_x: f64,
scale_factor_z: f64,
) -> (i32, i32) {
let (min_lon, min_lat, max_lon, max_lat) = bbox;
// Calculate the relative position within the bounding box
let rel_x: f64 = (lon - min_lon) / (max_lon - min_lon);
let rel_z: f64 = (lat - min_lat) / (max_lat - min_lat);
// Apply scaling factors for each dimension and convert to Minecraft coordinates
let x: i32 = (rel_x * scale_factor_x) as i32;
let z: i32 = (rel_z * scale_factor_z) as i32;
(z, x) // Swap x and z coords to avoid a mirrored projection on the Minecraft map
}
/// Function to determine the number of decimal places in a float as a string
fn count_decimal_places(value: f64) -> usize {
let s: String = value.to_string();
if let Some(pos) = s.find('.') {
s.len() - pos - 1 // Number of digits after the decimal point
} else {
0
}
}
/// Function to convert f64 to an integer based on the number of decimal places
fn convert_to_scaled_int(value: f64, max_decimal_places: usize) -> i64 {
let multiplier: i64 = 10_i64.pow(max_decimal_places as u32); // Compute multiplier
(value * multiplier as f64).round() as i64 // Scale and convert to integer
}
pub fn parse_osm_data(
json_data: &Value,
bbox: (f64, f64, f64, f64),
args: &Args,
) -> (Vec<ProcessedElement>, f64, f64) {
println!("{} {}", "[2/5]".bold(), "Parsing data...");
// Deserialize the JSON data into the OSMData structure
let data: OSMData = serde_json::from_value(json_data.clone()).expect("Failed to parse OSM data");
// Calculate the maximum number of decimal places in bbox elements
let max_decimal_places: usize = [
count_decimal_places(bbox.0),
count_decimal_places(bbox.1),
count_decimal_places(bbox.2),
count_decimal_places(bbox.3),
]
.into_iter()
.max()
.unwrap();
// Convert each element to a scaled integer
let bbox_scaled: (i64, i64, i64, i64) = (
convert_to_scaled_int(bbox.0, max_decimal_places),
convert_to_scaled_int(bbox.1, max_decimal_places),
convert_to_scaled_int(bbox.2, max_decimal_places),
convert_to_scaled_int(bbox.3, max_decimal_places),
);
// Determine which dimension is larger and assign scale factors accordingly
let (scale_factor_x, scale_factor_z) = if (bbox_scaled.2 - bbox_scaled.0) > (bbox_scaled.3 - bbox_scaled.1) {
// Longitude difference is greater than latitude difference
(
((bbox_scaled.2 - bbox_scaled.0) * 10 / 100) as f64, // Scale for width (x) is based on longitude difference
((bbox_scaled.3 - bbox_scaled.1) * 14 / 100) as f64, // Scale for length (z) is based on latitude difference
)
} else {
// Latitude difference is greater than or equal to longitude difference
(
((bbox_scaled.3 - bbox_scaled.1) * 10 / 100) as f64, // Scale for width (x) is based on latitude difference
((bbox_scaled.2 - bbox_scaled.0) * 14 / 100) as f64, // Scale for length (z) is based on longitude difference
)
};
if args.debug {
println!("Scale factor X: {}", scale_factor_x);
println!("Scale factor Z: {}", scale_factor_z);
}
let mut nodes_map: HashMap<u64, (i32, i32)> = HashMap::new();
let mut processed_elements: Vec<ProcessedElement> = Vec::new();
// First pass: store all nodes with Minecraft coordinates and process nodes with tags
for element in &data.elements {
if element.r#type == "node" {
if let (Some(lat), Some(lon)) = (element.lat, element.lon) {
let mc_coords: (i32, i32) = lat_lon_to_minecraft_coords(lat, lon, bbox, scale_factor_x, scale_factor_z);
nodes_map.insert(element.id, mc_coords);
// Process nodes with tags
if let Some(tags) = &element.tags {
if !tags.is_empty() {
processed_elements.push(ProcessedElement {
id: element.id,
r#type: element.r#type.clone(),
tags: tags.clone(),
nodes: vec![mc_coords], // Nodes for nodes is just the single coordinate
});
}
}
}
}
}
// Second pass: process ways and relations
for element in data.elements {
match element.r#type.as_str() {
"way" | "relation" => {
let mut nodes: Vec<(i32, i32)> = Vec::new();
if let Some(node_ids) = element.nodes {
for &node_id in &node_ids {
if let Some(&coords) = nodes_map.get(&node_id) {
nodes.push(coords);
}
}
}
if !nodes.is_empty() {
processed_elements.push(ProcessedElement {
id: element.id,
r#type: element.r#type.clone(),
tags: element.tags.unwrap_or_default(),
nodes,
});
}
}
_ => {}
}
}
(processed_elements, scale_factor_z, scale_factor_x)
}
const PRIORITY_ORDER: [&str; 5] = ["entrance", "building", "highway", "waterway", "barrier"];
// Function to determine the priority of each element
pub fn get_priority(element: &ProcessedElement) -> usize {
// Check each tag against the priority order
for (i, &tag) in PRIORITY_ORDER.iter().enumerate() {
if element.tags.contains_key(tag) {
return i;
}
}
// Return a default priority if none of the tags match
PRIORITY_ORDER.len()
}

View File

File diff suppressed because it is too large Load Diff

119
src/retrieve_data.rs Normal file
View File

@@ -0,0 +1,119 @@
use colored::Colorize;
use reqwest::blocking::Client;
use serde_json::Value;
use std::fs::File;
use std::io::{self, BufReader, Write};
use std::process::Command;
use rand::seq::SliceRandom;
/// Function to download data using the `reqwest` crate
fn download_with_reqwest(url: &str, query: &str) -> Result<String, Box<dyn std::error::Error>> {
let client: Client = Client::new();
let response: String = client.get(url).query(&[("data", query)]).send()?.text()?;
Ok(response)
}
/// Function to download data using `curl`
fn download_with_curl(url: &str, query: &str) -> io::Result<String> {
let output: std::process::Output = Command::new("curl")
.arg("-s") // Add silent mode to suppress output
.arg(format!("{}?data={}", url, query))
.output()?;
if !output.status.success() {
Err(io::Error::new(io::ErrorKind::Other, "Curl command failed"))
} else {
Ok(String::from_utf8_lossy(&output.stdout).to_string())
}
}
/// Function to download data using `wget`
fn download_with_wget(url: &str, query: &str) -> io::Result<String> {
let output: std::process::Output = Command::new("wget")
.arg("-qO-") // Use `-qO-` to output the result directly to stdout
.arg(format!("{}?data={}", url, query))
.output()?;
if !output.status.success() {
Err(io::Error::new(io::ErrorKind::Other, "Wget command failed"))
} else {
Ok(String::from_utf8_lossy(&output.stdout).to_string())
}
}
/// Main function to fetch data
pub fn fetch_data(
bbox: (f64, f64, f64, f64),
file: Option<&str>,
debug: bool,
download_method: &str,
) -> Result<Value, Box<dyn std::error::Error>> {
println!("{} {}", "[1/5]".bold(), "Fetching data...");
// List of Overpass API servers
let api_servers: Vec<&str> = vec![
"https://overpass-api.de/api/interpreter",
"https://lz4.overpass-api.de/api/interpreter",
"https://z.overpass-api.de/api/interpreter",
"https://overpass.kumi.systems/api/interpreter",
"https://overpass.private.coffee/api/interpreter",
];
let url: &&str = api_servers.choose(&mut rand::thread_rng()).unwrap();
// Generate Overpass API query for bounding box
let query: String = format!(
r#"[out:json][bbox:{},{},{},{}];
(
nwr["building"];
nwr["highway"];
nwr["landuse"];
nwr["natural"];
nwr["leisure"];
nwr["waterway"];
nwr["amenity"];
nwr["bridge"];
nwr["railway"];
nwr["barrier"];
nwr["entrance"];
nwr["door"];
way;
)->.waysinbbox;
(
node(w.waysinbbox);
)->.nodesinbbox;
.waysinbbox out body;
.nodesinbbox out skel qt;"#,
bbox.1, bbox.0, bbox.3, bbox.2
);
if let Some(file) = file {
// Load data from file
let file: File = File::open(file)?;
let reader: BufReader<File> = BufReader::new(file);
let data: Value = serde_json::from_reader(reader)?;
Ok(data)
} else {
// Fetch data from Overpass API
let response: String = match download_method {
"requests" => download_with_reqwest(url, &query)?,
"curl" => download_with_curl(url, &query)?,
"wget" => download_with_wget(url, &query)?,
_ => download_with_reqwest(url, &query)?, // Default to requests
};
let data: Value = serde_json::from_str(&response)?;
if data["elements"].as_array().map_or(0, |elements: &Vec<Value>| elements.len()) == 0 {
println!("Error! No data available");
std::process::exit(1);
}
// If debug is enabled, write data to file
if debug {
let mut file: File = File::create("export.json")?;
file.write_all(response.as_bytes())?;
}
Ok(data)
}
}

View File

@@ -1,130 +0,0 @@
import collections
import pytest
from src.bresenham import bresenham
TestBresenhamParameters = collections.namedtuple(
"TestBresenhamParameters", ["x1", "y1", "x2", "y2", "result"]
)
@pytest.mark.parametrize(
"parameters",
(
TestBresenhamParameters(x1=0, y1=0, x2=0, y2=0, result=((0, 0),)),
TestBresenhamParameters(
x1=0,
y1=0,
x2=5,
y2=0,
result=((0, 0), (1, 0), (2, 0), (3, 0), (4, 0), (5, 0)),
),
TestBresenhamParameters(
x1=0,
y1=0,
x2=-5,
y2=0,
result=((0, 0), (-1, 0), (-2, 0), (-3, 0), (-4, 0), (-5, 0)),
),
TestBresenhamParameters(
x1=0,
y1=0,
x2=0,
y2=5,
result=((0, 0), (0, 1), (0, 2), (0, 3), (0, 4), (0, 5)),
),
TestBresenhamParameters(
x1=0,
y1=0,
x2=0,
y2=-5,
result=((0, 0), (0, -1), (0, -2), (0, -3), (0, -4), (0, -5)),
),
TestBresenhamParameters(
x1=0, y1=0, x2=2, y2=3, result=((0, 0), (1, 1), (1, 2), (2, 3))
),
TestBresenhamParameters(
x1=0, y1=0, x2=-2, y2=3, result=((0, 0), (-1, 1), (-1, 2), (-2, 3))
),
TestBresenhamParameters(
x1=0, y1=0, x2=2, y2=-3, result=((0, 0), (1, -1), (1, -2), (2, -3))
),
TestBresenhamParameters(
x1=0, y1=0, x2=-2, y2=-3, result=((0, 0), (-1, -1), (-1, -2), (-2, -3))
),
TestBresenhamParameters(
x1=-1,
y1=-3,
x2=3,
y2=3,
result=((-1, -3), (0, -2), (0, -1), (1, 0), (2, 1), (2, 2), (3, 3)),
),
TestBresenhamParameters(
x1=0,
y1=0,
x2=11,
y2=1,
result=(
(0, 0),
(1, 0),
(2, 0),
(3, 0),
(4, 0),
(5, 0),
(6, 1),
(7, 1),
(8, 1),
(9, 1),
(10, 1),
(11, 1),
),
),
),
)
def test_bresenham(parameters: TestBresenhamParameters):
assert (
tuple(
bresenham(
x1=parameters.x1, y1=parameters.y1, x2=parameters.x2, y2=parameters.y2
)
)
== parameters.result
)
assert tuple(
bresenham(
x1=parameters.x2, y1=parameters.y2, x2=parameters.x1, y2=parameters.y1
)
) == tuple(reversed(parameters.result))
def test_min_slope_uphill():
assert tuple(bresenham(x1=0, y1=0, x2=10, y2=1)) == (
(0, 0),
(1, 0),
(2, 0),
(3, 0),
(4, 0),
(5, 1),
(6, 1),
(7, 1),
(8, 1),
(9, 1),
(10, 1),
)
def test_min_slope_downhill():
assert tuple(bresenham(x1=10, y1=1, x2=0, y2=0)) == (
(10, 1),
(9, 1),
(8, 1),
(7, 1),
(6, 1),
(5, 0),
(4, 0),
(3, 0),
(2, 0),
(1, 0),
(0, 0),
)

View File

@@ -1,99 +0,0 @@
from .processData import (
setBlock,
fillBlocks,
)
from .blockDefinitions import *
def round1(material, x, y, z):
setBlock(material, x - 2, y, z)
setBlock(material, x + 2, y, z)
setBlock(material, x, y, z - 2)
setBlock(material, x, y, z + 2)
setBlock(material, x - 1, y, z - 1)
setBlock(material, x + 1, y, z + 1)
setBlock(material, x + 1, y, z - 1)
setBlock(material, x - 1, y, z + 1)
def round2(material, x, y, z):
setBlock(material, x + 3, y, z)
setBlock(material, x + 2, y, z - 1)
setBlock(material, x + 2, y, z + 1)
setBlock(material, x + 1, y, z - 2)
setBlock(material, x + 1, y, z + 2)
setBlock(material, x - 3, y, z)
setBlock(material, x - 2, y, z - 1)
setBlock(material, x - 2, y, z + 1)
setBlock(material, x - 1, y, z + 2)
setBlock(material, x - 1, y, z - 2)
setBlock(material, x, y, z - 3)
setBlock(material, x, y, z + 3)
def round3(material, x, y, z):
setBlock(material, x + 3, y, z - 1)
setBlock(material, x + 3, y, z + 1)
setBlock(material, x + 2, y, z - 2)
setBlock(material, x + 2, y, z + 2)
setBlock(material, x + 1, y, z - 3)
setBlock(material, x + 1, y, z + 3)
setBlock(material, x - 3, y, z - 1)
setBlock(material, x - 3, y, z + 1)
setBlock(material, x - 2, y, z - 2)
setBlock(material, x - 2, y, z + 2)
setBlock(material, x - 1, y, z + 3)
setBlock(material, x - 1, y, z - 3)
def createTree(x, y, z, typetree=1):
if typetree == 1: # Oak tree
fillBlocks(oak_log, x, y, z, x, y + 8, z)
fillBlocks(oak_leaves, x - 1, y + 3, z, x - 1, y + 9, z)
fillBlocks(oak_leaves, x + 1, y + 3, z, x + 1, y + 9, z)
fillBlocks(oak_leaves, x, y + 3, z - 1, x, y + 9, z - 1)
fillBlocks(oak_leaves, x, y + 3, z + 1, x, y + 9, z + 1)
fillBlocks(oak_leaves, x, y + 9, z, x, y + 10, z)
round1(oak_leaves, x, y + 8, z)
round1(oak_leaves, x, y + 7, z)
round1(oak_leaves, x, y + 6, z)
round1(oak_leaves, x, y + 5, z)
round1(oak_leaves, x, y + 4, z)
round1(oak_leaves, x, y + 3, z)
round2(oak_leaves, x, y + 7, z)
round2(oak_leaves, x, y + 6, z)
round2(oak_leaves, x, y + 5, z)
round2(oak_leaves, x, y + 4, z)
round3(oak_leaves, x, y + 6, z)
round3(oak_leaves, x, y + 5, z)
elif typetree == 2: # Spruce tree
fillBlocks(spruce_log, x, y, z, x, y + 9, z)
fillBlocks(birch_leaves, x - 1, y + 3, z, x - 1, y + 10, z)
fillBlocks(birch_leaves, x + 1, y + 3, z, x + 1, y + 10, z)
fillBlocks(birch_leaves, x, y + 3, z - 1, x, y + 10, z - 1)
fillBlocks(birch_leaves, x, y + 3, z + 1, x, y + 10, z + 1)
setBlock(birch_leaves, x, y + 10, z)
round1(birch_leaves, x, y + 9, z)
round1(birch_leaves, x, y + 7, z)
round1(birch_leaves, x, y + 6, z)
round1(birch_leaves, x, y + 4, z)
round1(birch_leaves, x, y + 3, z)
round2(birch_leaves, x, y + 6, z)
round2(birch_leaves, x, y + 3, z)
elif typetree == 3: # Birch tree
fillBlocks(birch_log, x, y, z, x, y + 6, z)
fillBlocks(birch_leaves, x - 1, y + 2, z, x - 1, y + 7, z)
fillBlocks(birch_leaves, x + 1, y + 2, z, x + 1, y + 7, z)
fillBlocks(birch_leaves, x, y + 2, z - 1, x, y + 7, z - 1)
fillBlocks(birch_leaves, x, y + 2, z + 1, x, y + 7, z + 1)
fillBlocks(birch_leaves, x, y + 7, z, x, y + 8, z)
round1(birch_leaves, x, y + 6, z)
round1(birch_leaves, x, y + 5, z)
round1(birch_leaves, x, y + 4, z)
round1(birch_leaves, x, y + 3, z)
round1(birch_leaves, x, y + 2, z)
round2(birch_leaves, x, y + 2, z)
round2(birch_leaves, x, y + 3, z)
round2(birch_leaves, x, y + 4, z)

77
src/version_check.rs Normal file
View File

@@ -0,0 +1,77 @@
use colored::Colorize;
use reqwest::{Error as ReqwestError, StatusCode};
use reqwest::blocking::Client;
use semver::Version;
use std::error::Error;
/// URL to the remote Cargo.toml file to check for the latest version
const REMOTE_CARGO_TOML_URL: &str = "https://raw.githubusercontent.com/louis-e/arnis/experimental-rust-dev/Cargo.toml";
/// Fetches the latest version from the remote Cargo.toml file and compares it with the local version.
/// If a newer version is available, prints a message.
pub fn check_for_updates() -> Result<(), Box<dyn Error>> {
let client: Client = Client::new();
// Fetch the remote Cargo.toml file with a User-Agent header
let response: Result<reqwest::blocking::Response, ReqwestError> = client
.get(REMOTE_CARGO_TOML_URL)
.header("User-Agent", "arnis-client")
.send();
match response {
Ok(res) => {
// If the response status is not 200 OK, handle it as an HTTP error
if !res.status().is_success() {
handle_http_error(res.status());
return Ok(());
}
let response_text: String = res.text()?;
// Extract the version from the remote Cargo.toml
let remote_version: Version = extract_version_from_cargo_toml(&response_text)?;
let local_version: Version = Version::parse(env!("CARGO_PKG_VERSION"))?;
// Compare versions
if remote_version > local_version {
println!(
"{} {} -> {}",
"A new version is available:".yellow().bold(),
local_version,
remote_version
);
}
}
Err(err) => handle_request_error(err),
}
Ok(())
}
/// Extracts the version from the contents of a Cargo.toml file.
fn extract_version_from_cargo_toml(cargo_toml_contents: &str) -> Result<Version, Box<dyn Error>> {
for line in cargo_toml_contents.lines() {
if line.starts_with("version") {
let version_str: &str = line.split('=').nth(1).unwrap().trim().trim_matches('"');
return Ok(Version::parse(version_str)?);
}
}
Err("Failed to find version in Cargo.toml".into())
}
/// Handles HTTP errors by printing the status code and a user-friendly message.
fn handle_http_error(status: StatusCode) {
eprintln!(
"Failed to fetch remote Cargo.toml: HTTP error {}: {}",
status.as_u16(),
status.canonical_reason().unwrap_or("Unknown error")
);
}
/// Handles the error for HTTP requests more gracefully, including printing HTTP status codes when applicable.
fn handle_request_error(err: ReqwestError) {
if err.is_timeout() {
eprintln!("Request timed out. Please check your network connection.");
} else if let Some(status) = err.status() {
handle_http_error(status);
}
}

337
src/world_editor.rs Normal file
View File

@@ -0,0 +1,337 @@
use colored::Colorize;
use crate::args::Args;
use crate::block_definitions::*;
use fastanvil::Region;
use fastnbt::{ByteArray, LongArray, Value};
use indicatif::{ProgressBar, ProgressStyle};
use once_cell::sync::Lazy;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fs::File;
#[derive(Serialize, Deserialize)]
struct Chunk {
sections: Vec<Section>,
#[serde(flatten)]
other: HashMap<String, Value>,
}
#[derive(Serialize, Deserialize)]
struct Section {
block_states: Blockstates,
#[serde(flatten)]
other: HashMap<String, Value>,
}
#[derive(Serialize, Deserialize)]
struct Blockstates {
palette: Vec<PaletteItem>,
#[serde(flatten)]
other: HashMap<String, Value>,
}
#[derive(Serialize, Deserialize)]
struct PaletteItem {
#[serde(rename = "Name")]
name: String,
#[serde(rename = "Properties")]
properties: Option<Value>,
}
pub struct WorldEditor<'a> {
region_template_path: String,
region_dir: String,
regions: HashMap<(i32, i32), Region<File>>,
chunks_to_modify: HashMap<(i32, i32, i32, i32), HashMap<(i32, i32, i32), Block>>,
scale_factor_x: f64,
scale_factor_z: f64,
args: &'a Args,
}
impl<'a> WorldEditor<'a> {
/// Initializes the WorldEditor with the region directory and template region path.
pub fn new(
region_template_path: &str,
region_dir: &str,
scale_factor_x: f64,
scale_factor_z: f64,
args: &'a Args,
) -> Self {
Self {
region_template_path: region_template_path.to_string(),
region_dir: region_dir.to_string(),
regions: HashMap::new(),
chunks_to_modify: HashMap::new(),
scale_factor_x,
scale_factor_z,
args,
}
}
/// Loads or creates a region for the given region coordinates.
fn load_region(&mut self, region_x: i32, region_z: i32) -> &mut Region<File> {
self.regions.entry((region_x, region_z)).or_insert_with(|| {
let out_path: String = format!("{}/r.{}.{}.mca", self.region_dir, region_x, region_z);
std::fs::copy(&self.region_template_path, &out_path)
.expect("Failed to copy region template");
let region_file = File::options().read(true).write(true).open(&out_path)
.expect("Failed to open region file");
Region::from_stream(region_file).expect("Failed to load region")
})
}
/// Sets a block of the specified type at the given coordinates.
pub fn set_block(
&mut self,
block: &'static Lazy<Block>,
x: i32,
y: i32,
z: i32,
override_whitelist: Option<&[&'static Lazy<Block>]>,
override_blacklist: Option<&[&'static Lazy<Block>]>,
) {
let position: (i32, i32, i32) = (x, y, z);
// Check if coordinates are within bounds
if x < 0 || x > self.scale_factor_x as i32 || z < 0 || z > self.scale_factor_z as i32 {
return;
}
let chunk_x: i32 = x >> 4;
let chunk_z: i32 = z >> 4;
let region_x: i32 = chunk_x >> 5;
let region_z: i32 = chunk_z >> 5;
let chunk_x_within_region: i32 = chunk_x & 31;
let chunk_z_within_region: i32 = chunk_z & 31;
let chunk_key: (i32, i32, i32, i32) = (region_x, region_z, chunk_x_within_region, chunk_z_within_region);
let chunk_blocks: &mut HashMap<(i32, i32, i32), Block> = self.chunks_to_modify.entry(chunk_key).or_default();
if let Some(existing_block) = chunk_blocks.get(&position) {
// Check against whitelist and blacklist
if let Some(whitelist) = override_whitelist {
if whitelist.iter().any(|&whitelisted_block| whitelisted_block.name == existing_block.name) {
chunk_blocks.insert(position, (*block).clone());
}
} else if let Some(blacklist) = override_blacklist {
if !blacklist.iter().any(|&blacklisted_block| blacklisted_block.name == existing_block.name) {
chunk_blocks.insert(position, (*block).clone());
}
}
} else {
chunk_blocks.insert(position, (*block).clone());
}
}
/// Fills a cuboid area with the specified block between two coordinates.
pub fn fill_blocks(
&mut self,
block: &'static Lazy<Block>,
x1: i32,
y1: i32,
z1: i32,
x2: i32,
y2: i32,
z2: i32,
override_whitelist: Option<&[&'static Lazy<Block>]>,
override_blacklist: Option<&[&'static Lazy<Block>]>,
) {
let (min_x, max_x) = if x1 < x2 { (x1, x2) } else { (x2, x1) };
let (min_y, max_y) = if y1 < y2 { (y1, y2) } else { (y2, y1) };
let (min_z, max_z) = if z1 < z2 { (z1, z2) } else { (z2, z1) };
for x in min_x..=max_x {
for y in min_y..=max_y {
for z in min_z..=max_z {
self.set_block(block, x, y, z, override_whitelist, override_blacklist);
}
}
}
}
/// Checks for a block at the given coordinates.
pub fn check_for_block(
&self,
x: i32,
y: i32,
z: i32,
whitelist: Option<&[&'static Lazy<Block>]>,
blacklist: Option<&[&'static Lazy<Block>]>,
) -> bool {
let chunk_x: i32 = x >> 4;
let chunk_z: i32 = z >> 4;
let region_x: i32 = chunk_x >> 5;
let region_z: i32 = chunk_z >> 5;
let chunk_x_within_region: i32 = chunk_x & 31;
let chunk_z_within_region: i32 = chunk_z & 31;
let chunk_key: (i32, i32, i32, i32) = (region_x, region_z, chunk_x_within_region, chunk_z_within_region);
// Retrieve the chunk modification map
if let Some(chunk_blocks) = self.chunks_to_modify.get(&chunk_key) {
if let Some(existing_block) = chunk_blocks.get(&(x, y, z)) {
// Check against whitelist and blacklist
if let Some(whitelist) = whitelist {
if whitelist.iter().any(|&whitelisted_block| whitelisted_block.name == existing_block.name) {
return true; // Block is in whitelist
}
}
if let Some(blacklist) = blacklist {
if blacklist.iter().any(|&blacklisted_block| blacklisted_block.name == existing_block.name) {
return true; // Block is in blacklist
}
}
}
}
false
}
/// Saves all changes made to the world by writing modified chunks to the appropriate region files.
pub fn save(&mut self) {
println!("{} {}", "[5/5]".bold(), "Saving world...");
let _debug: bool = self.args.debug;
let total_chunks: u64 = self.chunks_to_modify.len() as u64;
let save_pb: ProgressBar = ProgressBar::new(total_chunks);
save_pb.set_style(ProgressStyle::default_bar()
.template("{spinner:.green} [{elapsed_precise}] [{bar:45}] {pos}/{len} chunks ({eta})")
.unwrap()
.progress_chars("█▓░"));
let mut chunks_to_process = self.chunks_to_modify.clone(); // Clone to prevent modifying while iterating
for ((region_x, region_z, chunk_x, chunk_z), block_list) in &mut chunks_to_process {
let region: &mut Region<File> = self.load_region(*region_x, *region_z);
if let Ok(Some(data)) = region.read_chunk(*chunk_x as usize, *chunk_z as usize) {
let mut chunk: Chunk = fastnbt::from_bytes(&data).unwrap();
for ((x, y, z), block) in block_list {
set_block_in_chunk(&mut chunk, block.clone(), *x, *y, *z);
}
let ser: Vec<u8> = fastnbt::to_bytes(&chunk).unwrap();
// Write chunk data back to the correct location, ensuring correct chunk coordinates
let expected_chunk_location: (usize, usize) = ((*chunk_x as usize) & 31, (*chunk_z as usize) & 31);
region.write_chunk(expected_chunk_location.0, expected_chunk_location.1, &ser).unwrap();
}
// Remove chunk from the modification map after processing it
self.chunks_to_modify.remove(&(*region_x, *region_z, *chunk_x, *chunk_z));
save_pb.inc(1);
}
save_pb.finish();
}
}
fn bits_per_block(palette_size: u32) -> u32 {
(palette_size as f32).log2().ceil().max(4.0).min(8.0) as u32
}
fn set_block_in_chunk(
chunk: &mut Chunk,
block: Block,
x: i32,
y: i32,
z: i32,
) {
let local_x: usize = (x & 15) as usize;
let local_y: usize = y as usize;
let local_z: usize = (z & 15) as usize;
for section in chunk.sections.iter_mut() {
if let Some(Value::Byte(y_byte)) = section.other.get("Y") {
if *y_byte == (local_y >> 4) as i8 {
let palette: &mut Vec<PaletteItem> = &mut section.block_states.palette;
let block_index: usize = (local_y % 16 * 256 + local_z * 16 + local_x) as usize;
// Add SkyLight with 2048 bytes of value 0xFF
let skylight_data = vec![0xFFu8 as i8; 2048];
section.other.insert("SkyLight".to_string(), Value::ByteArray(ByteArray::new(skylight_data)));
// Check if the block is already in the palette with matching properties
let mut palette_index: Option<usize> = palette.iter().position(|item: &PaletteItem| {
item.name == block.name && item.properties == block.properties
});
// If the block is not in the palette and adding it would exceed a reasonable size, skip or replace
if palette_index.is_none() {
if palette.len() >= 16 {
palette_index = Some(0);
} else {
// Add the new block type to the palette with its properties
palette.push(PaletteItem {
name: block.name.clone(),
properties: block.properties.clone(),
});
palette_index = Some(palette.len() - 1);
}
}
// Unwrap because we are sure palette_index is Some after this point
let palette_index: u32 = palette_index.unwrap() as u32;
let bits_per_block: u32 = bits_per_block(palette.len() as u32);
if let Some(Value::LongArray(ref mut data)) = section.block_states.other.get_mut("data") {
// Convert LongArray to Vec<i64>
let mut vec_data: Vec<i64> = data.as_mut().to_vec();
set_block_in_section(&mut vec_data, block_index, palette_index, bits_per_block);
// Update LongArray with modified Vec<i64>
*data = LongArray::new(vec_data);
} else {
// Properly initialize new data array with correct length
let required_longs: usize = ((4096 + (64 / bits_per_block) - 1) / (64 / bits_per_block)) as usize;
let mut new_data: Vec<i64> = vec![0i64; required_longs];
set_block_in_section(&mut new_data, block_index, palette_index, bits_per_block);
section.block_states.other.insert("data".to_string(), Value::LongArray(LongArray::new(new_data)));
}
break;
}
}
}
}
fn set_block_in_section(
data: &mut Vec<i64>,
block_index: usize,
palette_index: u32,
bits_per_block: u32
) {
let blocks_per_long: u32 = 64 / bits_per_block;
let required_longs: usize = ((4096 + blocks_per_long - 1) / blocks_per_long) as usize;
// Ensure data vector is large enough
assert!(data.len() >= required_longs, "Data slice is too small");
let mask: u64 = (1u64 << bits_per_block) - 1;
let long_index: usize = block_index / blocks_per_long as usize;
let start_bit: usize = (block_index % blocks_per_long as usize) * bits_per_block as usize;
let current_value: u64 = data[long_index] as u64;
let new_value: u64 = (current_value & !(mask << start_bit)) | ((palette_index as u64 & mask) << start_bit);
// Update data
data[long_index] = new_value as i64;
// Handle cases where bits spill over into the next long
if start_bit + bits_per_block as usize > 64 {
let overflow_bits: usize = (start_bit + bits_per_block as usize) - 64;
let next_long_index: usize = long_index + 1;
if next_long_index < data.len() {
let next_value: u64 = data[next_long_index] as u64;
let new_next_value: u64 = (next_value & !(mask >> overflow_bits)) | ((palette_index as u64 & mask) >> overflow_bits);
data[next_long_index] = new_next_value as i64;
} else {
panic!("Data slice is too small even after resizing. This should never happen.");
}
}
}