Compare commits

..

2 Commits

Author SHA1 Message Date
Louis Erbkamm
21659c66e0 Update README.md 2024-09-20 18:26:10 +02:00
Louis Erbkamm
b35b4f83ff Delete .github/workflows directory 2024-09-20 18:23:24 +02:00
53 changed files with 2396 additions and 2979 deletions

5
.flake8 Normal file
View 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

View File

@@ -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

View File

@@ -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
View File

@@ -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

View File

@@ -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
View 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
View 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
View File

@@ -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 [![CI Build Status](https://github.com/louis-e/arnis/actions/workflows/ci-build.yml/badge.svg)](https://github.com/louis-e/arnis/actions)
This open source project written in Rust generates any chosen location from the real world in Minecraft with a high level of detail.
# 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.
![Minecraft World Demo](https://github.com/louis-e/arnis/blob/python-legacy/gitassets/demo-comp.png?raw=true)
![Minecraft World Demo Before After](https://github.com/louis-e/arnis/blob/python-legacy/gitassets/before-after.gif?raw=true)
## :floppy_disk: How it works
![CLI Generation](https://github.com/louis-e/arnis/blob/main/gitassets/cli.gif?raw=true)
![CLI Generation](https://github.com/louis-e/arnis/blob/python-legacy/gitassets/cli-generation.gif?raw=true)
The raw data obtained from the API *[(see FAQ)](#question-faq)* includes each element (buildings, walls, fountains, farmlands, etc.) with its respective corner coordinates (nodes) and descriptive tags. When you run 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.
![How to find area](https://github.com/louis-e/arnis/blob/main/gitassets/bbox-finder.png?raw=true)
![How to find area](https://github.com/louis-e/arnis/blob/python-legacy/gitassets/bbox-finder.png?raw=true)
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 Rusts memory safety and concurrency features to optimize the performance of the world generation process.
- **Comprehensive Documentation**: Detailed in-code documentation for a clear structure and logic.
- **User-Friendly Experience**: Focus on making the project easy to use for end users, with the potential to develop a graphical user interface (GUI) in the future. Suggestions and discussions on UI/UX are welcome.
- **Cross-Platform Support**: Ensure the project runs smoothly on Windows, macOS, and Linux.
## :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
[![Star History Chart](https://api.star-history.com/svg?repos=louis-e/arnis&type=Date)](https://star-history.com/#louis-e/arnis&Date)

10
arnis.py Normal file
View 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
View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 MiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 MiB

BIN
gitassets/demo-comp.png Normal file
View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.7 MiB

BIN
gitassets/screenshot-1.png Normal file
View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 MiB

BIN
gitassets/screenshot-2.png Normal file
View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 MiB

BIN
gitassets/screenshot-3.png Normal file
View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

BIN
gitassets/screenshot-4.png Normal file
View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

34
pyinst-compile.spec Normal file
View 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 )

View File

Binary file not shown.

10
requirements.txt Normal file
View 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
View File

View File

@@ -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
View 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"),
]

View File

@@ -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
View 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

View File

@@ -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
}

View File

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

View File

@@ -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);
}
}
}
}
_ => {}
}
}
}

View File

@@ -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
}
}
}
}
}
}

View File

@@ -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;
}
}
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}
}
_ => {}
}
}
}
}

View File

@@ -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);
}
_ => {}
}
}
}
}
}
}

View File

@@ -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;

View File

@@ -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);
}
}
}
}
}
}
}

View File

@@ -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);
}
}
}
}
}

View File

@@ -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
}
}

View File

@@ -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);
}
}
}

View File

@@ -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
View 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
View 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)

View File

@@ -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);
}

View File

@@ -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
View File

File diff suppressed because it is too large Load Diff

View 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
View 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
View 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)

View File

@@ -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);
}
}

View File

@@ -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.");
}
}
}