Compare commits
2 Commits
v2.0.0
...
python-leg
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
21659c66e0 | ||
|
|
b35b4f83ff |
5
.flake8
Normal file
@@ -0,0 +1,5 @@
|
||||
[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
|
||||
33
.github/workflows/ci-build.yml
vendored
@@ -1,33 +0,0 @@
|
||||
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
|
||||
52
.github/workflows/release.yml
vendored
@@ -1,52 +0,0 @@
|
||||
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
@@ -1,4 +1,167 @@
|
||||
/target
|
||||
Cargo.lock
|
||||
export.json
|
||||
parsed_osm_data.txt
|
||||
# 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
|
||||
25
Cargo.toml
@@ -1,25 +0,0 @@
|
||||
[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"
|
||||
6
Dockerfile
Normal file
@@ -0,0 +1,6 @@
|
||||
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"]
|
||||
11
Makefile
Normal file
@@ -0,0 +1,11 @@
|
||||
# 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/
|
||||
139
README.md
@@ -1,98 +1,123 @@
|
||||
<p align="center">
|
||||
<img width="456" height="125" src="https://github.com/louis-e/arnis/blob/main/gitassets/logo.png?raw=true">
|
||||
<img width="456" height="125" src="https://github.com/louis-e/arnis/blob/python-legacy/gitassets/logo.png?raw=true">
|
||||
</p>
|
||||
|
||||
# Arnis [](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.
|
||||
|
||||
# Arnis - Python Legacy Branch
|
||||
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><br>
|
||||
This branch stores the old Python legacy version (v1.x), which was now replaced by the [Rust port](https://github.com/louis-e/arnis).
|
||||
<br><br>
|
||||
⇒ [Where did you find this project?](https://6okq6xh5jt4.typeform.com/to/rSjZaB41)
|
||||
|
||||
<br>
|
||||
## :desktop_computer: Example
|
||||
<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
|
||||

|
||||

|
||||
|
||||
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:
|
||||
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:
|
||||
|
||||
#### Processing Pipeline
|
||||
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.
|
||||
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.
|
||||
|
||||
## :keyboard: Usage
|
||||
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"```
|
||||
```python3 arnis.py --bbox="min_lng,min_lat,max_lng,max_lat" --path="C:/Users/username/AppData/Roaming/.minecraft/saves/worldname"```
|
||||
|
||||
### 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.
|
||||

|
||||

|
||||
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
|
||||
- *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.
|
||||
- *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.
|
||||
- *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 [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.
|
||||
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.
|
||||
- *Where does the name come from?*<br>
|
||||
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.
|
||||
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.
|
||||
|
||||
## :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
|
||||
## :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
|
||||
- [ ] Tool for mapping real coordinates to Minecraft coordinates
|
||||
- [ ] Setup fork of [https://github.com/aaronr/bboxfinder.com](https://github.com/aaronr/bboxfinder.com) for easy bbox picking
|
||||
- [ ] 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
|
||||
- [ ] Add interior to buildings
|
||||
- [ ] Evaluate and implement elevation
|
||||
- [ ] Save fountain structure in the code (similar to the tree structure)
|
||||
- [ ] Add windows to buildings
|
||||
- [ ] Generate a few big cities using high performance hardware and make them available to download
|
||||
- [x] Fix faulty empty chunks ([https://github.com/owengage/fastnbt/issues/120](https://github.com/owengage/fastnbt/issues/120)) (workaround found)
|
||||
- [ ] 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
|
||||
|
||||
## :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 Rust’s 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.
|
||||
## :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)
|
||||
|
||||
#### 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.
|
||||
## :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!
|
||||
|
||||
#### 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
|
||||
|
||||
[](https://star-history.com/#louis-e/arnis&Date)
|
||||
|
||||
10
arnis.py
Normal file
@@ -0,0 +1,10 @@
|
||||
#!/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()
|
||||
BIN
gitassets/before-after.gif
Normal file
|
After Width: | Height: | Size: 2.8 MiB |
BIN
gitassets/cli-generation.gif
Normal file
|
After Width: | Height: | Size: 79 KiB |
|
Before Width: | Height: | Size: 1.0 MiB |
BIN
gitassets/demo-comp.png
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
BIN
gitassets/mc.gif
|
Before Width: | Height: | Size: 9.7 MiB |
BIN
gitassets/screenshot-1.png
Normal file
|
After Width: | Height: | Size: 2.9 MiB |
BIN
gitassets/screenshot-2.png
Normal file
|
After Width: | Height: | Size: 2.5 MiB |
BIN
gitassets/screenshot-3.png
Normal file
|
After Width: | Height: | Size: 1.3 MiB |
BIN
gitassets/screenshot-4.png
Normal file
|
After Width: | Height: | Size: 2.2 MiB |
34
pyinst-compile.spec
Normal file
@@ -0,0 +1,34 @@
|
||||
# -*- 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
10
requirements.txt
Normal file
@@ -0,0 +1,10 @@
|
||||
anvil-new==1.0.1
|
||||
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
|
||||
0
src/__init__.py
Normal file
80
src/args.rs
@@ -1,80 +0,0 @@
|
||||
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
|
||||
}
|
||||
95
src/blockDefinitions.py
Normal file
@@ -0,0 +1,95 @@
|
||||
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("minecraft", "cauldron")
|
||||
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("minecraft", "grass")
|
||||
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("minecraft", "green_terracotta")
|
||||
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("minecraft", "oak_fence")
|
||||
oak_leaves = anvil.Block("minecraft", "oak_leaves")
|
||||
oak_log = anvil.Block("minecraft", "oak_log")
|
||||
oak_planks = anvil.Block("minecraft", "oak_planks")
|
||||
podzol = anvil.Block("minecraft", "podzol")
|
||||
potatoes = anvil.Block("minecraft", "potatoes", {"age": 7})
|
||||
rail = anvil.Block("minecraft", "rail")
|
||||
red_flower = anvil.Block("minecraft", "poppy")
|
||||
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("minecraft", "stone_slab")
|
||||
stone_brick_slab = anvil.Block("minecraft", "stone_brick_slab")
|
||||
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"),
|
||||
]
|
||||
@@ -1,253 +0,0 @@
|
||||
#![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,
|
||||
]
|
||||
}
|
||||
26
src/bresenham.py
Normal file
@@ -0,0 +1,26 @@
|
||||
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
|
||||
@@ -1,76 +0,0 @@
|
||||
/// 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
|
||||
}
|
||||
@@ -1,137 +0,0 @@
|
||||
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, ®ion_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(())
|
||||
}
|
||||
@@ -1,137 +0,0 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,232 +0,0 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,256 +0,0 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -1,208 +0,0 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,115 +0,0 @@
|
||||
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);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
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;
|
||||
@@ -1,84 +0,0 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,115 +0,0 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
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
|
||||
}
|
||||
166
src/getData.py
Normal file
@@ -0,0 +1,166 @@
|
||||
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
|
||||
150
src/main.py
Normal file
@@ -0,0 +1,150 @@
|
||||
#!/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 validate_bounding_box(bbox):
|
||||
"""
|
||||
Validates a bounding box represented as a string in
|
||||
the format "min_lng,min_lat,max_lng,max_lat".
|
||||
|
||||
Parameters:
|
||||
bbox (str): The bounding box string.
|
||||
|
||||
Returns:
|
||||
bool: True if the bounding box is valid, False otherwise.
|
||||
"""
|
||||
try:
|
||||
# Split the input string into components
|
||||
parts = bbox.split(",")
|
||||
if len(parts) != 4:
|
||||
return False
|
||||
|
||||
# Convert the components to float
|
||||
min_lng, min_lat, max_lng, max_lat = map(float, parts)
|
||||
|
||||
# Validate the longitude range (-180 to 180)
|
||||
if not (-180 <= min_lng <= 180 and -180 <= max_lng <= 180):
|
||||
return False
|
||||
|
||||
# Validate the latitude range (-90 to 90)
|
||||
if not (-90 <= min_lat <= 90 and -90 <= max_lat <= 90):
|
||||
return False
|
||||
|
||||
# Ensure that min_lng is less than max_lng and min_lat is less than max_lat
|
||||
if min_lng >= max_lng or min_lat >= max_lat:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
except ValueError:
|
||||
# In case of conversion error, input was not a valid float
|
||||
return False
|
||||
|
||||
|
||||
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)
|
||||
|
||||
if args.bbox:
|
||||
if validate_bounding_box(args.bbox) is False:
|
||||
print("Error! Invalid bbox input")
|
||||
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
@@ -1,85 +0,0 @@
|
||||
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);
|
||||
}
|
||||
@@ -1,181 +0,0 @@
|
||||
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()
|
||||
}
|
||||
1405
src/processData.py
Normal file
@@ -1,119 +0,0 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
130
src/tests/test_bresenham.py
Normal file
@@ -0,0 +1,130 @@
|
||||
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),
|
||||
)
|
||||
99
src/tree.py
Normal file
@@ -0,0 +1,99 @@
|
||||
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)
|
||||
@@ -1,77 +0,0 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -1,337 +0,0 @@
|
||||
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.");
|
||||
}
|
||||
}
|
||||
}
|
||||