73 Commits

Author SHA1 Message Date
Louis Erbkamm
96cbf2ac96 Updated version 2025-01-05 00:38:20 +01:00
Louis Erbkamm
feab8cea98 Updated version 2025-01-05 00:38:10 +01:00
Louis Erbkamm
8128270a57 Merge pull request #169 from louis-e/embed-bootstrapper
Embed bootstrapper
2025-01-05 00:08:29 +01:00
Louis Erbkamm
8ab5745efa Merge branch 'main' into embed-bootstrapper 2025-01-04 23:57:27 +01:00
Louis Erbkamm
8291a94e13 Merge pull request #168 from louis-e/fix-shelter
Fix amenity shelter elements
2025-01-04 23:57:18 +01:00
louis-e
8d5d96f88a Embed WebView2 bootstrapper 2025-01-04 23:52:06 +01:00
louis-e
5008ddd988 Fix amenity shelter elements 2025-01-04 23:45:09 +01:00
Louis Erbkamm
4a200e01f8 Merge pull request #166 from louis-e/no-world-fix
No world selected fix
2025-01-04 23:37:53 +01:00
Louis Erbkamm
eb9e349cdb Update README.md 2025-01-04 23:23:05 +01:00
Louis Erbkamm
4ccb8a492b Merge branch 'main' into no-world-fix 2025-01-04 23:19:57 +01:00
Louis Erbkamm
a7a74fecdb Merge pull request #165 from louis-e/multipolygon-support
Fix: Generate multipolygon buildings from OSM relations
2025-01-04 23:18:44 +01:00
Louis Erbkamm
364e4933ac Added multipolygon support done 2025-01-04 23:18:12 +01:00
louis-e
b7db71209e Fix logic to make sure that generation cannot be started when no world is selected 2025-01-04 23:09:51 +01:00
louis-e
ae4c8371ae Fix: Generate multipolygon buildings from OSM relations 2025-01-04 22:58:47 +01:00
Louis Erbkamm
14aa8e94d5 Merge pull request #133 from louis-e/dependabot/cargo/itertools-0.14.0
Update itertools requirement from 0.13.0 to 0.14.0
2025-01-04 22:03:09 +01:00
Louis Erbkamm
8f739e0189 Merge pull request #134 from louis-e/dependabot/cargo/geo-0.29.3
Update geo requirement from 0.28.0 to 0.29.3
2025-01-04 22:02:47 +01:00
Louis Erbkamm
c9b2899c83 Merge pull request #135 from louis-e/dependabot/cargo/dirs-5.0.1
Update dirs requirement from 4.0.0 to 5.0.1
2025-01-04 22:02:25 +01:00
Louis Erbkamm
155e3e5a9b Merge pull request #158 from adamperkowski/fix/cargolock
fix: include `Cargo.lock`
2025-01-04 17:31:43 +01:00
Adam Perkowski
d364ed1361 fix: include Cargo.lock 2025-01-04 15:33:04 +01:00
Louis Erbkamm
2f2d1c79bb Merge pull request #155 from louis-e/scale-adaption
Adapted minimum scale value to 0.3
2025-01-04 15:19:29 +01:00
Louis Erbkamm
54313019af Added new todos 2025-01-04 15:02:12 +01:00
louis-e
1bd3fb6802 Adapted minimum scale value to 0.3 2025-01-04 14:58:25 +01:00
louis-e
5cbeeb0e01 Adapted minimum scale value to 0.3 2025-01-04 14:57:57 +01:00
Louis Erbkamm
55b522764d Update bug_report.md 2025-01-04 14:46:10 +01:00
Louis Erbkamm
e2fd24ba08 Added bbox example to issue template 2025-01-04 14:24:34 +01:00
Louis Erbkamm
b095c40285 Update README.md 2025-01-04 14:21:16 +01:00
Louis Erbkamm
ffd5a274b0 Merge pull request #127 from benjamin051000/main
Change rfd features to enable manual builds on Linux.
2025-01-03 20:42:42 +01:00
Louis Erbkamm
0097e3b834 Update README.md 2025-01-03 20:38:25 +01:00
Louis Erbkamm
b37aa8185b Merge pull request #138 from louis-e/sign-implementation
Added set_sign in world_editor.rs
2025-01-03 20:25:47 +01:00
louis-e
11c1c18188 CI (clippy) fix: Added set_sign in world_editor.rs 2025-01-03 20:14:02 +01:00
louis-e
6200d4d6ef Added set_sign in world_editor.rs 2025-01-03 20:08:48 +01:00
Louis Erbkamm
2afd508d67 Merge pull request #136 from louis-e/generation-custom-dir
Enable generation without having Minecraft installed
2025-01-03 18:02:00 +01:00
louis-e
62a5a85625 CI (fmt) fix: Enable generation without having Minecraft installed 2025-01-03 17:27:44 +01:00
louis-e
36a17afd9f Enable generation without having Minecraft installed 2025-01-03 17:24:32 +01:00
dependabot[bot]
7c211938f7 Update dirs requirement from 4.0.0 to 5.0.1
---
updated-dependencies:
- dependency-name: dirs
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-01-03 16:05:44 +00:00
dependabot[bot]
3aa66407be Update geo requirement from 0.28.0 to 0.29.3
Updates the requirements on [geo](https://github.com/georust/geo) to permit the latest version.
- [Changelog](https://github.com/georust/geo/blob/main/CHANGES.md)
- [Commits](https://github.com/georust/geo/compare/geo-0.28.0...geo-0.29.3)

---
updated-dependencies:
- dependency-name: geo
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-01-03 16:04:58 +00:00
dependabot[bot]
18f1239215 Update itertools requirement from 0.13.0 to 0.14.0
Updates the requirements on [itertools](https://github.com/rust-itertools/itertools) to permit the latest version.
- [Changelog](https://github.com/rust-itertools/itertools/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rust-itertools/itertools/compare/v0.13.0...v0.14.0)

---
updated-dependencies:
- dependency-name: itertools
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-01-03 16:03:59 +00:00
Louis Erbkamm
f9d7600839 Merge pull request #123 from sebastiaanspeck/add-dependabot
Add dependabot for automatic dependency updates
2025-01-03 17:03:03 +01:00
Benjamin Wheeler
1bee90889c Change rfd features to enable manual builds on Linux.
rfd wasn't building on Linux, since its default features appear to be
for Windows. This commit changes them to work on Linux. TODO test on
Windows.
2025-01-03 10:40:02 -05:00
Louis Erbkamm
ecead36496 Merge pull request #105 from louis-e/longitude-wrapping-fix
Fixed longitude wrapping bug
2025-01-03 16:35:39 +01:00
louis-e
13a48105fe Removed obsolete console log 2025-01-03 16:06:11 +01:00
Sebastiaan Speck
07fdcf7bed Add dependabot for automatic dependency updates 2025-01-03 05:03:19 +01:00
Louis Erbkamm
aae99417dd Update README.md 2025-01-01 00:50:40 +01:00
louis-e
f4e0b66245 Fixed longitude wrapping bug 2024-12-31 12:14:18 +01:00
Louis Erbkamm
a99dd0f907 Updated version 2024-12-30 00:28:03 +01:00
Louis Erbkamm
5b023fc9ce Updated version 2024-12-30 00:27:51 +01:00
Louis Erbkamm
ed57bf1b4e Update README.md 2024-12-30 00:16:13 +01:00
Louis Erbkamm
1f45100707 Merge pull request #100 from louis-e/groundlevel-parameter
Added groundlevel parameter to settings
2024-12-30 00:05:32 +01:00
Louis Erbkamm
aa1c8509a5 Merge branch 'main' into groundlevel-parameter 2024-12-29 23:54:04 +01:00
louis-e
6193a05be7 fmt (clippy): fixed redundant field names in struct initialization 2024-12-29 23:53:33 +01:00
louis-e
b9277e6a60 Added groundlevel parameter to settings 2024-12-29 23:48:17 +01:00
Louis Erbkamm
5ae8c27bfa Merge pull request #98 from louis-e/new-world-generation
Added support for generating new world
2024-12-29 23:28:04 +01:00
Louis Erbkamm
0f47fa1316 Update README.md 2024-12-29 23:20:23 +01:00
louis-e
04c0b5e6f8 fmt (clippy): Removed unneeded return statement required by clippy 2024-12-29 23:17:59 +01:00
louis-e
c339f02d1b fmt (clippy): Removed unneeded return statement required by clippy 2024-12-29 23:02:19 +01:00
louis-e
e2e69d119f fmt: Fixed cargo fmt required formatting 2024-12-29 22:58:05 +01:00
Louis Erbkamm
278c7a2cda Extended bug_report.md 2024-12-29 22:56:47 +01:00
louis-e
fb31cb5a21 Added LastPlayed unix timestamp modification 2024-12-29 22:52:15 +01:00
louis-e
712c7db03e Added support for generating new world 2024-12-29 22:42:53 +01:00
Louis Erbkamm
420852fcb0 Added rotate maps to TODO section 2024-12-29 15:15:23 +01:00
Louis Erbkamm
a3aae00ae0 Updated star history section 2024-12-29 01:23:01 +01:00
Louis Erbkamm
7401036675 Merge pull request #95 from louis-e/floodfill-settings
Add floodfill timeout support
2024-12-29 00:55:24 +01:00
Louis Erbkamm
46cf1bca63 FUNDING.yml 2024-12-29 00:53:10 +01:00
louis-e
ae3a91970f Add floodfill timeout support 2024-12-29 00:41:20 +01:00
Louis Erbkamm
b44b7995d1 Merge pull request #94 from louis-e/session-lock-check
Check session lock before selecting world
2024-12-28 23:55:38 +01:00
louis-e
f3ae449f02 Check session lock before selecting world 2024-12-28 23:41:45 +01:00
Louis Erbkamm
2e319d5ea0 Merge pull request #93 from louis-e/snow-mode
Added winter mode
2024-12-28 23:34:29 +01:00
louis-e
1a259d6dfc Added winter mode 2024-12-28 23:22:07 +01:00
Louis Erbkamm
036e54807e Merge pull request #92 from louis-e/force-dark-theme
Force dark theme
2024-12-28 16:24:13 +01:00
louis-e
a75cffd94b Force dark theme 2024-12-28 16:21:35 +01:00
Louis Erbkamm
135e15077a Merge pull request #91 from louis-e/settings-menu
Added settings menu including scale option and custom bbox input
2024-12-28 03:20:45 +01:00
louis-e
a284f77545 Added settings menu including scale option and custom bbox input 2024-12-28 03:07:47 +01:00
Louis Erbkamm
5cb749ba8f Added .gitattributes 2024-12-25 01:50:30 +01:00
30 changed files with 7172 additions and 251 deletions

1
.gitattributes vendored Normal file
View File

@@ -0,0 +1 @@
gui-src/** linguist-vendored

1
.github/FUNDING.yml vendored Normal file
View File

@@ -0,0 +1 @@
buy_me_a_coffee: louisdev

View File

@@ -10,11 +10,14 @@ assignees: ''
**Describe the bug**
A clear and concise description of what the bug is and what you expected to happen.
**Used bbox parameter**
Please provide your input parameters so we can reproduce the issue.
**Used bbox area**
Please provide your input parameters so we can reproduce the issue. *(For example: 48.133444 11.569462 48.142609 11.584740)*
**Arnis and Minecraft version**
Please tell us what version of Arnis and Minecraft you used.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Additional context**
Add any other context about the problem here. Please also provide the --bbox input parameters you used so we can reproduce the issue.
Add any other context about the problem here. If you used any more custom settings, please provide them here too.

10
.github/dependabot.yml vendored Normal file
View File

@@ -0,0 +1,10 @@
version: 2
updates:
- package-ecosystem: "cargo"
directory: "/"
schedule:
interval: "monthly"
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "monthly"

3
.gitignore vendored
View File

@@ -7,9 +7,6 @@
/target
**/*.rs.bk
# Lock files
Cargo.lock
# IDE/editor files
.idea/
/.vscode/

6149
Cargo.lock generated Normal file
View File

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
[package]
name = "arnis"
version = "2.1.0"
version = "2.1.2"
edition = "2021"
description = "Arnis - Generate real life cities in Minecraft"
homepage = "https://github.com/louis-e/arnis"
@@ -17,18 +17,20 @@ tauri-build = { version = "2", features = [] }
[dependencies]
clap = { version = "4.1", features = ["derive"] }
colored = "2.1.0"
dirs = "4.0.0"
dirs = "5.0.1"
fastanvil = "0.31.0"
fastnbt = "2.5.0"
flate2 = "1.0"
fnv = "1.0.7"
geo = "0.28.0"
fs2 = "0.4"
geo = "0.29.3"
indicatif = "0.17.8"
itertools = "0.13.0"
itertools = "0.14.0"
nalgebra = "0.33.0"
once_cell = "1.19.0"
rand = "0.8.5"
reqwest = { version = "0.12.7", features = ["blocking", "json"] }
rfd = "0.15.0"
rfd = { version = "0.15.1", default-features = false, features = ["tokio"] }
semver = "1.0.23"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"

100
README.md
View File

@@ -11,29 +11,18 @@ This open source project written in Rust generates any chosen location from the
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.
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 generates your vision.
## :keyboard: Usage
<img width="60%" src="https://github.com/louis-e/arnis/blob/main/gitassets/gui.png?raw=true"><br>
Download the [latest release](https://github.com/louis-e/arnis/releases/) or [compile](#trophy-open-source) the project on your own.
Choose your area in Arnis using the rectangle tool and select your Minecraft world - then simply click on 'Start Generation'!
The world will always be generated starting from the Minecraft coordinates 0 0 0 (/tp 0 0 0). This is the top left of your selected area.
Make sure to generate a new flat world in advance in Minecraft. Then choose your area in Arnis using the rectangle tool and select your Minecraft world - then simply click on 'Start Generation'!
The world will always be generated starting from the coordinates 0 0 0.
If you choose to select an own world, make sure to generate a new flat world in advance in Minecraft.
<details>
<summary>Alternatively you can also run Arnis the old fashioned way in the command line.</summary>
```arnis.exe --path="C:/YOUR_PATH/.minecraft/saves/worldname" --bbox="min_lng,min_lat,max_lng,max_lat"```
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.
<img width="60%" src="https://github.com/louis-e/arnis/blob/main/gitassets/bbox-finder.png?raw=true"><br>
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. Try starting with a small area since large areas take a lot of computing power and time to process.<br>
<i>Note: This might not be working right now since the console gets suppressed.</i>
</details>
Minecraft version 1.16.5 and below is currently not supported, but we are working on it! For the best results, use Minecraft version 1.21.4.
## :floppy_disk: How it works
![CLI Generation](https://github.com/louis-e/arnis/blob/main/gitassets/cli.gif?raw=true)
@@ -41,49 +30,63 @@ Use http://bboxfinder.com/ to draw a rectangle of your wanted area. Then copy th
The raw data obtained from the API *[(see FAQ)](#question-faq)* includes each element (buildings, walls, fountains, farmlands, etc.) with its respective corner coordinates (nodes) and descriptive tags. When you run Arnis, the following steps are performed automatically to generate a Minecraft world:
#### Processing Pipeline
1. 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. **Fetching Data from the Overpass API:** The script retrieves geospatial data for the desired bounding box from the Overpass API.
2. **Parsing 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. Relations and ways cluster several nodes into one specific object.
3. **Prioritizing and Sorting 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. **Generating 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. **Generating 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. **Saving the Minecraft World:** All the modified chunks are saved back to the Minecraft region files.
## :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.
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 the language and push the algorithm's performance further. We were nearing the limits of optimization in Python, and Rust's capabilities allow for even better performance and efficiency. The old Python implementation is still available in the python-legacy branch.
- *Where does the data come from?*<br>
The geographic data is sourced from OpenStreetMap (OSM)[^1], a free, collaborative mapping project that serves as an open-source alternative to commercial mapping services. The data is accessed via the Overpass API, which queries OSM's database.
- *How does the Minecraft world generation work?*<br>
The script uses the [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 [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 section 'Processing Pipeline' goes a bit further into the details and steps of the generation process itself.
- *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.
- *I don't have Minecraft installed but want to generate a world for my kids. How?*<br>
When selecting a world, click on 'Select existing world' and choose a directory. The world will be generated there.
- *Arnis instantly closes again or the window is empty!*<br>
If you're on Windows, please install the [Evergreen Bootstrapper from Microsoft](https://developer.microsoft.com/en-us/microsoft-edge/webview2/?form=MA13LH#download).
- *What Minecraft version should I use?*<br>
Please use Minecraft version 1.21.4 for the best results. Minecraft version 1.16.5 and below is currently not supported, but we are working on it!
- *The generation did finish, but there's nothing in the world!*<br>
Make sure to teleport to the generation starting point (/tp 0 0 0). If there is still nothing, you might need to travel a bit further into the positive X and positive Z direction.
## :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!
- [ ] Memory optimization
- [ ] Fix Github Action Workflow for releasing Linux & MacOS Binary
- [ ] Evaluate and implement multithreaded region saving
- [ ] Better code documentation
- [ ] Implement house roof types
- [ ] Refactor railway implementation
- [ ] Refactor bridges implementation
- [ ] Refactor fountain structure implementation
- [ ] Automatic new world creation instead of using an existing world
- [ ] Tool for mapping real coordinates to Minecraft coordinates
- [ ] Setup fork of [https://github.com/aaronr/bboxfinder.com](https://github.com/aaronr/bboxfinder.com) for easy bbox picking
- [ ] Fix compilation for Linux
- [ ] Rotate maps (https://github.com/louis-e/arnis/issues/97)
- [ ] Add street names as signs
- [ ] Add support for older Minecraft versions (<=1.16.5) (https://github.com/louis-e/arnis/issues/124, https://github.com/louis-e/arnis/issues/137)
- [ ] Mapping real coordinates to Minecraft coordinates (https://github.com/louis-e/arnis/issues/29)
- [ ] Add interior to buildings
- [ ] Evaluate and implement elevation
- [ ] Generate a few big cities using high performance hardware and make them available to download
- [ ] Implement memory mapped storing of chunks to reduce memory usage
- [ ] Implement house roof types
- [ ] Evaluate and implement elevation (https://github.com/louis-e/arnis/issues/66)
- [ ] Add support for inner attribute in multipolygons and multipolygon elements other than buildings
- [ ] Fix Github Action Workflow for releasing Linux & MacOS Binary
- [ ] Evaluate and implement faster region saving
- [ ] Refactor bridges implementation
- [ ] Refactor railway implementation
- [ ] Better code documentation
- [ ] Refactor fountain structure implementation
- [ ] Luanti Support (https://github.com/louis-e/arnis/issues/120)
- [ ] Minecraft Bedrock Edition Support (https://github.com/louis-e/arnis/issues/148)
- [x] Support multipolygons (https://github.com/louis-e/arnis/issues/112, https://github.com/louis-e/arnis/issues/114)
- [x] Memory optimization
- [x] Design and implement a GUI
- [x] Automatic new world creation instead of using an existing world
- [x] Fix faulty empty chunks ([https://github.com/owengage/fastnbt/issues/120](https://github.com/owengage/fastnbt/issues/120)) (workaround found)
- [x] Setup fork of [https://github.com/aaronr/bboxfinder.com](https://github.com/aaronr/bboxfinder.com) for easy bbox picking
## :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.
- **User-Friendly Experience**: Focus on making the project easy to use for end users.
- **Cross-Platform Support**: Ensure the project runs smoothly on Windows, macOS, and Linux.
#### How to contribute
@@ -94,21 +97,20 @@ For the GUI: ```cargo run --release```<br>
After your pull request was merged, I will take care of regularly creating update releases which will include your changes.
#### Contributors:
This section is dedicated to recognizing and celebrating the outstanding contributions of individuals who have significantly enhanced this project. Your work and dedication are deeply appreciated!
- louis-e
- scd31
- vfosnar
## :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)
<a href="https://star-history.com/#louis-e/arnis&Date">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=louis-e/arnis&Date&theme=dark" />
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=louis-e/arnis&Date&type=Date" />
<img alt="Star History Chart" src="https://api.star-history.com/svg?repos=louis-e/arnis&Date&type=Date" />
</picture>
</a>
## :copyright: License Information
This project is licensed under the GNU General Public License v3.0 (GPL-3.0).[^3]
Copyright (c) 2022-2024 louis-e
Copyright (c) 2022-2025 Louis Erbkamm (louis-e)
[^1]: https://en.wikipedia.org/wiki/OpenStreetMap

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 174 KiB

After

Width:  |  Height:  |  Size: 198 KiB

129
gui-src/css/styles.css vendored
View File

@@ -55,7 +55,7 @@ a:hover {
display: flex;
gap: 20px;
justify-content: center;
align-items: stretch; /* Ensures both sections take full height */
align-items: stretch;
margin-top: 5px;
}
@@ -73,7 +73,6 @@ a:hover {
padding: 20px;
border-radius: 8px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
/* No display: flex here, so buttons and content aren't stretched */
}
.controls-content {
@@ -83,7 +82,7 @@ a:hover {
}
.controls-box .progress-section {
margin-top: auto; /* Keeps the progress section at the bottom */
margin-top: auto;
}
.map-container {
@@ -112,11 +111,11 @@ button {
transition: border-color 0.25s;
box-shadow: 0 2px 2px rgba(0, 0, 0, 0.2);
margin-top: 10px;
width: auto; /* Ensures buttons dont stretch */
width: auto;
}
button:hover {
border-color: #396cd8;
border-color: #656565;
}
#selected-directory {
@@ -238,4 +237,122 @@ button:hover {
.controls-box button {
width: 100%;
}
}
/* Customization Settings */
.modal {
position: fixed;
z-index: 1000;
left: 0;
top: 0;
width: 100%;
height: 100%;
overflow: auto;
background-color: rgba(0, 0, 0, 0.4);
display: flex;
justify-content: center;
align-items: center;
}
.modal-content {
background-color: #797979;
padding: 20px;
border: 1px solid #797979;
border-radius: 10px;
width: 400px;
box-shadow: 0px 4px 8px rgba(0, 0, 0, 0.2);
}
.close-button {
color: #e9e9e9;
float: right;
font-size: 28px;
font-weight: bold;
cursor: pointer;
}
.close-button:hover {
color: #ffffff;
}
#winter-toggle {
accent-color: #fecc44;
}
.winter-toggle-container, .scale-slider-container {
margin: 15px 0;
}
.scale-slider-container label {
display: block;
margin-bottom: 5px;
}
#scale-value-slider {
accent-color: #fecc44;
}
#slider-value {
margin-left: 10px;
font-weight: bold;
}
.bbox-input-container {
margin-bottom: 20px;
}
.bbox-input-container label {
display: block;
margin-bottom: 5px;
}
#bbox-coords {
width: 100%;
padding: 8px;
border: 1px solid #fecc44;
border-radius: 4px;
font-size: 14px;
}
#bbox-coords:focus {
outline: none;
border-color: #fecc44;
box-shadow: 0 0 5px #fecc44;
}
.button-container {
display: flex;
align-items: center;
justify-content: flex-start;
gap: 5px;
}
.start-button {
padding: 10px 20px;
border: none;
border-radius: 5px;
font-size: 16px;
cursor: pointer;
transition: background-color 0.3s;
}
.start-button:hover {
background-color: #4caf50;
}
.settings-button {
width: 40px !important;
height: 38px;
border-radius: 5px;
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
transition: background-color 0.3s, border-color 0.3s;
}
.settings-button .gear-icon::before {
content: "⚙️";
font-size: 18px;
}

69
gui-src/index.html vendored
View File

@@ -34,10 +34,10 @@
<section class="section controls-box">
<div class="controls-content">
<h2>Select World</h2>
<!-- Updated Tooltip Structure -->
<div class="tooltip" style="width: 100%;">
<button type="button" onclick="pickDirectory()" style="padding: 10px; line-height: 1.2; width: 100%;">
<button type="button" onclick="openWorldPicker()" style="padding: 10px; line-height: 1.2; width: 100%;">
Choose World
<br>
<span id="selected-world" style="font-size: 0.8em; color: #fecc44; display: block; margin-top: 4px;">
@@ -48,12 +48,15 @@
Please select a Minecraft world that can be overwritten, as the generation process will replace existing structures in the chosen world!
</span>
</div>
<br>
<button type="button" onclick="startGeneration()">Start Generation</button>
<div class="button-container">
<button type="button" id="start-button" class="start-button" onclick="startGeneration()">Start Generation</button>
<button type="button" class="settings-button" onclick="openSettings()">
<i class="gear-icon"></i>
</button>
</div>
<br><br>
<div class="progress-section">
<h2>Progress</h2>
<div class="progress-bar-container">
@@ -68,6 +71,56 @@
</section>
</div>
<!-- World Picker Modal -->
<div id="world-modal" class="modal" style="display: none;">
<div class="modal-content">
<span class="close-button" onclick="closeWorldPicker()">&times;</span>
<h2>Choose World</h2>
<button type="button" id="select-world-button" class="select-world-button" onclick="selectWorld(false)">Select existing world</button>
<button type="button" id="generate-world-button" class="generate-world-button" onclick="selectWorld(true)">Generate new world</button>
</div>
</div>
<!-- Settings Modal -->
<div id="settings-modal" class="modal" style="display: none;">
<div class="modal-content">
<span class="close-button" onclick="closeSettings()">&times;</span>
<h2>Customization Settings</h2>
<!-- Winter Mode Toggle Button -->
<div class="winter-toggle-container">
<label for="winter-toggle">Winter Mode:</label>
<input type="checkbox" id="winter-toggle" name="winter-toggle">
</div>
<!-- World Scale Slider -->
<div class="scale-slider-container">
<label for="scale-value-slider">World Scale:</label>
<input type="range" id="scale-value-slider" name="scale-value-slider" min="0.30" max="2.5" step="0.1" value="1">
<span id="slider-value">1.00</span>
</div>
<!-- Bounding Box Input -->
<div class="bbox-input-container">
<label for="bbox-coords">Custom Bounding Box:</label>
<input type="text" id="bbox-coords" name="bbox-coords" maxlength="55" style="width: 280px;" autocomplete="one-time-code" placeholder="Format: lat,lng,lat,lng">
</div>
<!-- Floodfill Timeout Input -->
<div class="timeout-input-container">
<label for="floodfill-timeout">Floodfill Timeout (sec):</label>
<input type="number" id="floodfill-timeout" name="floodfill-timeout" min="0" step="1" value="20" style="width: 100px;" placeholder="Seconds">
</div><br>
<!-- Ground Level Input -->
<div class="ground-level-input-container">
<label for="ground-level">Ground Level:</label>
<input type="number" id="ground-level" name="ground-level" min="-64" max="290" value="-62" style="width: 100px;" placeholder="Ground Level">
</div>
</div>
</div>
<!-- Footer -->
<footer class="footer">
<a href="https://github.com/louis-e/arnis" target="_blank" class="footer-link">
@@ -77,4 +130,4 @@
</main>
</body>
</html>
</html>

208
gui-src/js/main.js vendored
View File

@@ -2,10 +2,22 @@ const { invoke } = window.__TAURI__.core;
// Initialize elements and start the demo progress
window.addEventListener("DOMContentLoaded", async () => {
initFooter();
await checkForUpdates();
registerMessageEvent();
window.selectWorld = selectWorld;
window.startGeneration = startGeneration;
setupProgressListener();
initSettings();
initWorldPicker();
handleBboxInput();
});
// Function to initialize the footer with the current year and version
async function initFooter() {
const currentYear = new Date().getFullYear();
document.getElementById("current-year").textContent = currentYear;
// Update displayed version
try {
const version = await invoke('gui_get_version');
const footerLink = document.querySelector(".footer-link");
@@ -13,8 +25,10 @@ window.addEventListener("DOMContentLoaded", async () => {
} catch (error) {
console.error("Failed to fetch version:", error);
}
}
// Check for updates
// Function to check for updates and display a notification if available
async function checkForUpdates() {
try {
const isUpdateAvailable = await invoke('gui_check_for_updates');
if (isUpdateAvailable) {
@@ -35,8 +49,10 @@ window.addEventListener("DOMContentLoaded", async () => {
} catch (error) {
console.error("Failed to check for updates: ", error);
}
}
// Register bbox update event for iframe map
// Function to register the event listener for bbox updates from iframe
function registerMessageEvent() {
window.addEventListener('message', function (event) {
const bboxText = event.data.bboxText;
@@ -45,15 +61,14 @@ window.addEventListener("DOMContentLoaded", async () => {
displayBboxInfoText(bboxText);
}
});
}
window.pickDirectory = pickDirectory;
window.startGeneration = startGeneration;
// Function to set up the progress bar listener
function setupProgressListener() {
const progressBar = document.getElementById("progress-bar");
const progressMessage = document.getElementById("progress-message");
const progressDetail = document.getElementById("progress-detail");
// Listen for progress-update events
window.__TAURI__.event.listen("progress-update", (event) => {
const { progress, message } = event.payload;
@@ -64,15 +79,122 @@ window.addEventListener("DOMContentLoaded", async () => {
if (message != "") {
progressMessage.textContent = message;
if (message.startsWith("Error!")) {
progressMessage.style.color = "#fa7878";
generationButtonEnabled = true;
} else if (message.startsWith("Done!")) {
progressMessage.style.color = "#7bd864";
generationButtonEnabled = true;
} else {
progressMessage.style.color = "";
}
}
});
});
}
function initSettings() {
// Settings
const settingsModal = document.getElementById("settings-modal");
const slider = document.getElementById("scale-value-slider");
const sliderValue = document.getElementById("slider-value");
// Open settings modal
function openSettings() {
settingsModal.style.display = "flex";
settingsModal.style.justifyContent = "center";
settingsModal.style.alignItems = "center";
}
// Close settings modal
function closeSettings() {
settingsModal.style.display = "none";
}
window.openSettings = openSettings;
window.closeSettings = closeSettings;
// Update slider value display
slider.addEventListener("input", () => {
sliderValue.textContent = parseFloat(slider.value).toFixed(2);
});
}
function initWorldPicker() {
// World Picker
const worldPickerModal = document.getElementById("world-modal");
// Open world picker modal
function openWorldPicker() {
worldPickerModal.style.display = "flex";
worldPickerModal.style.justifyContent = "center";
worldPickerModal.style.alignItems = "center";
}
// Close world picker modal
function closeWorldPicker() {
worldPickerModal.style.display = "none";
}
window.openWorldPicker = openWorldPicker;
window.closeWorldPicker = closeWorldPicker;
}
// Function to validate and handle bbox input
function handleBboxInput() {
const inputBox = document.getElementById("bbox-coords");
const bboxInfo = document.getElementById("bbox-info");
inputBox.addEventListener("input", function () {
const input = inputBox.value.trim();
if (input === "") {
bboxInfo.textContent = "";
bboxInfo.style.color = "";
selectedBBox = "";
return;
}
// Regular expression to validate bbox input (supports both comma and space-separated formats)
const bboxPattern = /^(-?\d+(\.\d+)?)[,\s](-?\d+(\.\d+)?)[,\s](-?\d+(\.\d+)?)[,\s](-?\d+(\.\d+)?)$/;
if (bboxPattern.test(input)) {
const matches = input.match(bboxPattern);
// Extract coordinates (Lat / Lng order expected)
const lat1 = parseFloat(matches[1]);
const lng1 = parseFloat(matches[3]);
const lat2 = parseFloat(matches[5]);
const lng2 = parseFloat(matches[7]);
// Validate latitude and longitude ranges in the expected Lat / Lng order
if (
lat1 >= -90 && lat1 <= 90 &&
lng1 >= -180 && lng1 <= 180 &&
lat2 >= -90 && lat2 <= 90 &&
lng2 >= -180 && lng2 <= 180
) {
// Input is valid; trigger the event
const bboxText = `${lat1} ${lng1} ${lat2} ${lng2}`;
window.dispatchEvent(new MessageEvent('message', { data: { bboxText } }));
// Update the info text
bboxInfo.textContent = "Custom selection confirmed!";
bboxInfo.style.color = "#7bd864";
} else {
// Valid numbers but invalid order or range
bboxInfo.textContent = "Error: Coordinates are out of range or incorrectly ordered (Lat before Lng required).";
bboxInfo.style.color = "#fecc44";
selectedBBox = "";
}
} else {
// Input doesn't match the required format
bboxInfo.textContent = "Invalid format. Please use 'lat,lng,lat,lng' or 'lat lng lat lng'.";
bboxInfo.style.color = "#fecc44";
selectedBBox = "";
}
});
}
// Function to calculate the bounding box "size" in square meters based on latitude and longitude
function calculateBBoxSize(lng1, lat1, lng2, lat2) {
@@ -95,20 +217,30 @@ function calculateBBoxSize(lng1, lat1, lng2, lat2) {
return Math.abs(width * height);
}
// Function to normalize longitude to the range [-180, 180]
function normalizeLongitude(lon) {
return ((lon + 180) % 360 + 360) % 360 - 180;
}
const threshold1 = 12332660.00;
const threshold2 = 36084700.00;
let selectedBBox = "";
// Function to handle incoming bbox data
function displayBboxInfoText(bboxText) {
selectedBBox = bboxText;
let [lng1, lat1, lng2, lat2] = bboxText.split(" ").map(Number);
// Normalize longitudes
lat1 = parseFloat(normalizeLongitude(lat1).toFixed(6));
lat2 = parseFloat(normalizeLongitude(lat2).toFixed(6));
selectedBBox = `${lng1} ${lat1} ${lng2} ${lat2}`;
const [lng1, lat1, lng2, lat2] = bboxText.split(" ").map(Number);
const bboxInfo = document.getElementById("bbox-info");
// Reset the info text if the bbox is 0,0,0,0
if (lng1 === 0 && lat1 === 0 && lng2 === 0 && lat2 === 0) {
bboxInfo.textContent = "";
selectedBBox = "";
return;
}
@@ -128,10 +260,9 @@ function displayBboxInfoText(bboxText) {
}
let worldPath = "";
async function pickDirectory() {
async function selectWorld(generate_new_world) {
try {
const worldName = await invoke('gui_pick_directory');
const worldName = await invoke('gui_select_world', { generateNew: generate_new_world } );
if (worldName) {
worldPath = worldName;
const lastSegment = worldName.split(/[\\/]/).pop();
@@ -139,32 +270,63 @@ async function pickDirectory() {
document.getElementById('selected-world').style.color = "#fecc44";
}
} catch (error) {
worldPath = error;
console.error(error);
document.getElementById('selected-world').textContent = error;
document.getElementById('selected-world').style.color = "#fa7878";
}
closeWorldPicker();
}
let generationButtonEnabled = true;
async function startGeneration() {
try {
if (worldPath === "No world selected" || worldPath == "Invalid Minecraft world" || worldPath === "") {
if (generationButtonEnabled === false) {
return;
}
if (!selectedBBox || selectedBBox == "0.000000 0.000000 0.000000 0.000000") {
document.getElementById('bbox-info').textContent = "Select a location first!";
document.getElementById('bbox-info').style.color = "#fa7878";
return;
}
if (
worldPath === "No world selected" ||
worldPath == "Invalid Minecraft world" ||
worldPath == "The selected world is currently in use" ||
worldPath == "Minecraft directory not found" ||
worldPath === ""
) {
document.getElementById('selected-world').textContent = "Select a Minecraft world first!";
document.getElementById('selected-world').style.color = "#fa7878";
return;
}
if (!selectedBBox || selectedBBox == "0.000000 0.000000 0.000000 0.000000") {
document.getElementById('bbox-info').textContent = "Select a location firsta using the rectangle tool!";
document.getElementById('bbox-info').style.color = "#fa7878";
return;
}
var winter_mode = document.getElementById("winter-toggle").checked;
var scale = parseFloat(document.getElementById("scale-value-slider").value);
var floodfill_timeout = parseInt(document.getElementById("floodfill-timeout").value, 10);
var ground_level = parseInt(document.getElementById("ground-level").value, 10);
// Validate floodfill_timeout and ground_level
floodfill_timeout = isNaN(floodfill_timeout) || floodfill_timeout < 0 ? 20 : floodfill_timeout;
ground_level = isNaN(ground_level) || ground_level < -62 ? 20 : ground_level;
// Pass the bounding box and selected world to the Rust backend
await invoke("gui_start_generation", { bboxText: selectedBBox, selectedWorld: worldPath });
// Update the UI or show a loading/progress message if needed
await invoke("gui_start_generation", {
bboxText: selectedBBox,
selectedWorld: worldPath,
worldScale: scale,
groundLevel: ground_level,
winterMode: winter_mode,
floodfillTimeout: floodfill_timeout,
});
console.log("Generation process started.");
generationButtonEnabled = false;
} catch (error) {
console.error("Error starting generation:", error);
generationButtonEnabled = true;
}
}

BIN
mcassets/icon.png Normal file
View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

BIN
mcassets/level.dat Normal file
View File

Binary file not shown.

View File

@@ -33,11 +33,19 @@ pub struct Args {
#[arg(long, default_value = "1.0")]
pub scale: f64,
/// Ground level to use in the Minecraft world
#[arg(long, default_value_t = -62)]
pub ground_level: i32,
/// Enable winter mode (default: false)
#[arg(long, default_value_t = false)]
pub winter: bool,
/// Enable debug mode (optional)
#[arg(long, default_value_t = false, action = clap::ArgAction::SetTrue)]
pub debug: bool,
/// Set floodfill timeout (seconds) (optional) // TODO
/// Set floodfill timeout (seconds) (optional)
#[arg(long, value_parser = parse_duration)]
pub timeout: Option<Duration>,
}

View File

@@ -138,6 +138,9 @@ impl Block {
108 => "potatoes",
109 => "wheat",
110 => "bedrock",
111 => "snow_block",
112 => "snow",
113 => "oak_sign",
_ => panic!("Invalid id"),
}
}
@@ -174,6 +177,16 @@ impl Block {
map
})),
113 => Some(Value::Compound({
let mut map: HashMap<String, Value> = HashMap::new();
map.insert("rotation".to_string(), Value::String("6".to_string()));
map.insert(
"waterlogged".to_string(),
Value::String("false".to_string()),
);
map
})),
_ => None,
}
}
@@ -285,6 +298,9 @@ pub const MAGENTA_CONCRETE: Block = Block::new(101);
pub const BROWN_WOOL: Block = Block::new(102);
pub const OXIDIZED_COPPER: Block = Block::new(103);
pub const YELLOW_TERRACOTTA: Block = Block::new(104);
pub const SNOW_BLOCK: Block = Block::new(111);
pub const SNOW_LAYER: Block = Block::new(112);
pub const SIGN: Block = Block::new(113);
pub const CARROTS: Block = Block::new(105);
pub const DARK_OAK_DOOR_LOWER: Block = Block::new(106);

View File

@@ -1,5 +1,5 @@
use crate::args::Args;
use crate::block_definitions::{DIRT, GRASS_BLOCK};
use crate::block_definitions::{DIRT, GRASS_BLOCK, SNOW_BLOCK};
use crate::element_processing::*;
use crate::osm_parser::ProcessedElement;
use crate::progress::emit_gui_progress_update;
@@ -7,8 +7,6 @@ use crate::world_editor::WorldEditor;
use colored::Colorize;
use indicatif::{ProgressBar, ProgressStyle};
const GROUND_LEVEL: i32 = -62;
pub fn generate_world(
elements: Vec<ProcessedElement>,
args: &Args,
@@ -18,10 +16,22 @@ pub fn generate_world(
println!("{} Processing data...", "[3/5]".bold());
emit_gui_progress_update(10.0, "Processing data...");
let ground_level: i32 = args.ground_level;
let region_dir: String = format!("{}/region", args.path);
let mut editor: WorldEditor =
WorldEditor::new(&region_dir, scale_factor_x, scale_factor_z, args);
editor.set_sign(
"".to_string(),
"Generated World".to_string(),
"This direction".to_string(),
"".to_string(),
9,
-61,
9,
6,
);
// Process data
let elements_count: usize = elements.len();
let process_pb: ProgressBar = ProgressBar::new(elements_count as u64);
@@ -54,94 +64,56 @@ pub fn generate_world(
match element {
ProcessedElement::Way(way) => {
if way.tags.contains_key("building") || way.tags.contains_key("building:part") {
buildings::generate_buildings(
&mut editor,
way,
GROUND_LEVEL,
args.timeout.as_ref(),
);
buildings::generate_buildings(&mut editor, way, ground_level, args);
} else if way.tags.contains_key("highway") {
highways::generate_highways(
&mut editor,
element,
GROUND_LEVEL,
args.timeout.as_ref(),
);
highways::generate_highways(&mut editor, element, ground_level, args);
} else if way.tags.contains_key("landuse") {
landuse::generate_landuse(
&mut editor,
way,
GROUND_LEVEL,
args.timeout.as_ref(),
);
landuse::generate_landuse(&mut editor, way, ground_level, args);
} else if way.tags.contains_key("natural") {
natural::generate_natural(
&mut editor,
element,
GROUND_LEVEL,
args.timeout.as_ref(),
);
natural::generate_natural(&mut editor, element, ground_level, args);
} else if way.tags.contains_key("amenity") {
amenities::generate_amenities(
&mut editor,
element,
GROUND_LEVEL,
args.timeout.as_ref(),
);
amenities::generate_amenities(&mut editor, element, ground_level, args);
} else if way.tags.contains_key("leisure") {
leisure::generate_leisure(
&mut editor,
way,
GROUND_LEVEL,
args.timeout.as_ref(),
);
leisure::generate_leisure(&mut editor, way, ground_level, args);
} else if way.tags.contains_key("barrier") {
barriers::generate_barriers(&mut editor, element, GROUND_LEVEL);
barriers::generate_barriers(&mut editor, element, ground_level);
} else if way.tags.contains_key("waterway") {
waterways::generate_waterways(&mut editor, way, GROUND_LEVEL);
waterways::generate_waterways(&mut editor, way, ground_level);
} else if way.tags.contains_key("bridge") {
bridges::generate_bridges(&mut editor, way, GROUND_LEVEL);
bridges::generate_bridges(&mut editor, way, ground_level);
} else if way.tags.contains_key("railway") {
railways::generate_railways(&mut editor, way, GROUND_LEVEL);
railways::generate_railways(&mut editor, way, ground_level);
} else if way.tags.get("service") == Some(&"siding".to_string()) {
highways::generate_siding(&mut editor, way, GROUND_LEVEL);
highways::generate_siding(&mut editor, way, ground_level);
}
}
ProcessedElement::Node(node) => {
if node.tags.contains_key("door") || node.tags.contains_key("entrance") {
doors::generate_doors(&mut editor, node, GROUND_LEVEL);
doors::generate_doors(&mut editor, node, ground_level);
} else if node.tags.contains_key("natural")
&& node.tags.get("natural") == Some(&"tree".to_string())
{
natural::generate_natural(
&mut editor,
element,
GROUND_LEVEL,
args.timeout.as_ref(),
);
natural::generate_natural(&mut editor, element, ground_level, args);
} else if node.tags.contains_key("amenity") {
amenities::generate_amenities(
&mut editor,
element,
GROUND_LEVEL,
args.timeout.as_ref(),
);
amenities::generate_amenities(&mut editor, element, ground_level, args);
} else if node.tags.contains_key("barrier") {
barriers::generate_barriers(&mut editor, element, GROUND_LEVEL);
barriers::generate_barriers(&mut editor, element, ground_level);
} else if node.tags.contains_key("highway") {
highways::generate_highways(
&mut editor,
element,
GROUND_LEVEL,
args.timeout.as_ref(),
);
highways::generate_highways(&mut editor, element, ground_level, args);
} else if node.tags.contains_key("tourism") {
tourisms::generate_tourisms(&mut editor, node, GROUND_LEVEL);
tourisms::generate_tourisms(&mut editor, node, ground_level);
}
}
ProcessedElement::Relation(rel) => {
if rel.tags.contains_key("water") {
water_areas::generate_water_areas(&mut editor, rel, GROUND_LEVEL);
if rel.tags.contains_key("building") || rel.tags.contains_key("building:part") {
buildings::generate_building_from_relation(
&mut editor,
rel,
ground_level,
args,
);
} else if rel.tags.contains_key("water") {
water_areas::generate_water_areas(&mut editor, rel, ground_level);
}
}
}
@@ -172,10 +144,12 @@ pub fn generate_world(
let total_iterations_grnd: f64 = (scale_factor_x + 1.0) * (scale_factor_z + 1.0);
let progress_increment_grnd: f64 = 30.0 / total_iterations_grnd;
let groundlayer_block = if args.winter { SNOW_BLOCK } else { GRASS_BLOCK };
for x in 0..=(scale_factor_x as i32) {
for z in 0..=(scale_factor_z as i32) {
editor.set_block(GRASS_BLOCK, x, GROUND_LEVEL, z, None, None);
editor.set_block(DIRT, x, GROUND_LEVEL - 1, z, None, None);
editor.set_block(groundlayer_block, x, ground_level, z, None, None);
editor.set_block(DIRT, x, ground_level - 1, z, None, None);
block_counter += 1;
if block_counter % batch_size == 0 {

View File

@@ -1,5 +1,4 @@
use std::time::Duration;
use crate::args::Args;
use crate::block_definitions::*;
use crate::bresenham::bresenham_line;
use crate::floodfill::flood_fill_area;
@@ -10,7 +9,7 @@ pub fn generate_amenities(
editor: &mut WorldEditor,
element: &ProcessedElement,
ground_level: i32,
floodfill_timeout: Option<&Duration>,
args: &Args,
) {
// Skip if 'layer' or 'level' is negative in the tags
if let Some(layer) = element.tags().get("layer") {
@@ -52,7 +51,7 @@ pub fn generate_amenities(
.map(|n: &crate::osm_parser::ProcessedNode| (n.x, n.z))
.collect();
let floor_area: Vec<(i32, i32)> =
flood_fill_area(&polygon_coords, floodfill_timeout);
flood_fill_area(&polygon_coords, args.timeout.as_ref());
// Fill the floor area
for (x, z) in floor_area.iter() {
@@ -152,7 +151,7 @@ pub fn generate_amenities(
if corner_addup.2 > 0 {
let polygon_coords: Vec<(i32, i32)> = current_amenity.to_vec();
let flood_area: Vec<(i32, i32)> =
flood_fill_area(&polygon_coords, floodfill_timeout);
flood_fill_area(&polygon_coords, args.timeout.as_ref());
for (x, z) in flood_area {
editor.set_block(

View File

@@ -1,8 +1,9 @@
use crate::args::Args;
use crate::block_definitions::*;
use crate::bresenham::bresenham_line;
use crate::colors::{color_text_to_rgb_tuple, rgb_distance, RGBTuple};
use crate::floodfill::flood_fill_area;
use crate::osm_parser::ProcessedWay;
use crate::osm_parser::{ProcessedMemberRole, ProcessedRelation, ProcessedWay};
use crate::world_editor::WorldEditor;
use rand::Rng;
use std::collections::HashSet;
@@ -12,7 +13,7 @@ pub fn generate_buildings(
editor: &mut WorldEditor,
element: &ProcessedWay,
ground_level: i32,
floodfill_timeout: Option<&Duration>,
args: &Args,
) {
let mut previous_node: Option<(i32, i32)> = None;
let mut corner_addup: (i32, i32, i32) = (0, 0, 0);
@@ -79,6 +80,39 @@ pub fn generate_buildings(
}
}
if let Some(amenity_type) = element.tags.get("amenity") {
if amenity_type == "shelter" {
let roof_block: Block = STONE_BRICK_SLAB;
let polygon_coords: Vec<(i32, i32)> = element
.nodes
.iter()
.map(|n: &crate::osm_parser::ProcessedNode| (n.x, n.z))
.collect();
let roof_area: Vec<(i32, i32)> =
flood_fill_area(&polygon_coords, args.timeout.as_ref());
// Place fences and roof slabs at each corner node directly
for node in &element.nodes {
let x: i32 = node.x;
let z: i32 = node.z;
for y in 1..=4 {
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 roof_area.iter() {
editor.set_block(roof_block, *x, roof_height, *z, None, None);
}
return;
}
}
if let Some(building_type) = element.tags.get("building") {
if building_type == "garage" {
building_height = 2;
@@ -95,7 +129,7 @@ pub fn generate_buildings(
.map(|n: &crate::osm_parser::ProcessedNode| (n.x, n.z))
.collect();
let floor_area: Vec<(i32, i32)> =
flood_fill_area(&polygon_coords, floodfill_timeout);
flood_fill_area(&polygon_coords, args.timeout.as_ref());
// Fill the floor area
for (x, z) in floor_area.iter() {
@@ -152,7 +186,8 @@ pub fn generate_buildings(
.iter()
.map(|node: &crate::osm_parser::ProcessedNode| (node.x, node.z))
.collect();
let roof_area: Vec<(i32, i32)> = flood_fill_area(&polygon_coords, floodfill_timeout); // Use flood-fill to determine the area
let roof_area: Vec<(i32, i32)> =
flood_fill_area(&polygon_coords, args.timeout.as_ref()); // Use flood-fill to determine the area
// Fill the interior of the roof with STONE_BRICK_SLAB
for (x, z) in roof_area.iter() {
@@ -172,7 +207,7 @@ pub fn generate_buildings(
building_height = 23
}
} else if building_type == "bridge" {
generate_bridge(editor, element, ground_level, floodfill_timeout);
generate_bridge(editor, element, ground_level, args.timeout.as_ref());
return;
}
}
@@ -201,6 +236,7 @@ pub fn generate_buildings(
}
}
}
// Ceiling cobblestone
editor.set_block(
COBBLESTONE,
bx,
@@ -208,7 +244,19 @@ pub fn generate_buildings(
bz,
None,
None,
); // Ceiling cobblestone
);
if args.winter {
editor.set_block(
SNOW_LAYER,
x,
ground_level + building_height + 2,
z,
None,
None,
);
}
current_building.push((bx, bz));
corner_addup = (corner_addup.0 + bx, corner_addup.1 + bz, corner_addup.2 + 1);
}
@@ -224,7 +272,7 @@ pub fn generate_buildings(
.iter()
.map(|n: &crate::osm_parser::ProcessedNode| (n.x, n.z))
.collect();
let floor_area: Vec<(i32, i32)> = flood_fill_area(&polygon_coords, floodfill_timeout);
let floor_area: Vec<(i32, i32)> = flood_fill_area(&polygon_coords, args.timeout.as_ref());
for (x, z) in floor_area {
if processed_points.insert((x, z)) {
@@ -253,6 +301,46 @@ pub fn generate_buildings(
None,
None,
);
if args.winter {
editor.set_block(
SNOW_LAYER,
x,
ground_level + building_height + 2,
z,
None,
None,
);
}
}
}
}
}
pub fn generate_building_from_relation(
editor: &mut WorldEditor,
relation: &ProcessedRelation,
ground_level: i32,
args: &Args,
) {
// Process the outer way to create the building walls
for member in &relation.members {
if member.role == ProcessedMemberRole::Outer {
generate_buildings(editor, &member.way, ground_level, args);
}
}
// Handle inner ways (holes, courtyards, etc.)
for member in &relation.members {
if member.role == ProcessedMemberRole::Inner {
let polygon_coords: Vec<(i32, i32)> =
member.way.nodes.iter().map(|n| (n.x, n.z)).collect();
let hole_area: Vec<(i32, i32)> =
flood_fill_area(&polygon_coords, args.timeout.as_ref());
for (x, z) in hole_area {
// Remove blocks in the inner area to create a hole
editor.set_block(AIR, x, ground_level, z, None, Some(&[SPONGE]));
}
}
}

View File

@@ -1,16 +1,15 @@
use std::time::Duration;
use crate::args::Args;
use crate::block_definitions::*;
use crate::bresenham::bresenham_line;
use crate::floodfill::flood_fill_area;
use crate::osm_parser::{ProcessedElement, ProcessedWay};
use crate::world_editor::WorldEditor; // Assuming you have a flood fill function for area filling
use crate::world_editor::WorldEditor;
pub fn generate_highways(
editor: &mut WorldEditor,
element: &ProcessedElement,
ground_level: i32,
floodfill_timeout: Option<&Duration>,
args: &Args,
) {
if let Some(highway_type) = element.tags().get("highway") {
if highway_type == "street_lamp" {
@@ -37,6 +36,10 @@ pub fn generate_highways(
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);
if args.winter {
editor.set_block(SNOW_LAYER, x, ground_level + 7, z, None, None);
}
}
}
}
@@ -72,7 +75,13 @@ pub fn generate_highways(
"wood" => OAK_PLANKS,
"asphalt" => BLACK_CONCRETE,
"gravel" | "fine_gravel" => GRAVEL,
"grass" => GRASS_BLOCK,
"grass" => {
if args.winter {
SNOW_BLOCK
} else {
GRASS_BLOCK
}
}
"dirt" => DIRT,
"sand" => SAND,
"concrete" => LIGHT_GRAY_CONCRETE,
@@ -86,7 +95,8 @@ pub fn generate_highways(
.iter()
.map(|n: &crate::osm_parser::ProcessedNode| (n.x, n.z))
.collect();
let filled_area: Vec<(i32, i32)> = flood_fill_area(&polygon_coords, floodfill_timeout);
let filled_area: Vec<(i32, i32)> =
flood_fill_area(&polygon_coords, args.timeout.as_ref());
for (x, z) in filled_area {
editor.set_block(surface_block, x, ground_level, z, None, None);
@@ -95,7 +105,7 @@ pub fn generate_highways(
let mut previous_node: Option<(i32, i32)> = None;
let mut block_type = BLACK_CONCRETE;
let mut block_range: i32 = 2;
let mut add_stripe = false; // Flag for adding stripes
let mut add_stripe = false;
// Skip if 'layer' or 'level' is negative in the tags
if let Some(layer) = element.tags().get("layer") {

View File

@@ -1,5 +1,4 @@
use std::time::Duration;
use crate::args::Args;
use crate::block_definitions::*;
use crate::bresenham::bresenham_line;
use crate::element_processing::tree::create_tree;
@@ -12,7 +11,7 @@ pub fn generate_landuse(
editor: &mut WorldEditor,
element: &ProcessedWay,
ground_level: i32,
floodfill_timeout: Option<&Duration>,
args: &Args,
) {
let mut previous_node: Option<(i32, i32)> = None;
let mut corner_addup: (i32, i32, i32) = (0, 0, 0);
@@ -23,9 +22,21 @@ pub fn generate_landuse(
let landuse_tag: &String = element.tags.get("landuse").unwrap_or(&binding);
let block_type = match landuse_tag.as_str() {
"greenfield" | "meadow" | "grass" => GRASS_BLOCK,
"greenfield" | "meadow" | "grass" => {
if args.winter {
SNOW_BLOCK
} else {
GRASS_BLOCK
}
}
"farmland" => FARMLAND,
"forest" => GRASS_BLOCK,
"forest" => {
if args.winter {
SNOW_BLOCK
} else {
GRASS_BLOCK
}
}
"cemetery" => PODZOL,
"beach" => SAND,
"construction" => DIRT,
@@ -36,9 +47,17 @@ pub fn generate_landuse(
"industrial" => COBBLESTONE,
"military" => GRAY_CONCRETE,
"railway" => GRAVEL,
_ => GRASS_BLOCK,
_ => {
if args.winter {
SNOW_BLOCK
} else {
GRASS_BLOCK
}
}
};
let bresenham_block: Block = if args.winter { SNOW_BLOCK } else { GRASS_BLOCK };
// Process landuse nodes to fill the area
for node in &element.nodes {
let x: i32 = node.x;
@@ -49,7 +68,7 @@ pub fn generate_landuse(
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);
editor.set_block(bresenham_block, bx, ground_level, bz, None, None);
}
current_landuse.push((x, z));
@@ -62,7 +81,7 @@ pub fn generate_landuse(
// If there are landuse nodes, flood-fill the area
if !current_landuse.is_empty() {
let polygon_coords: Vec<(i32, i32)> = element.nodes.iter().map(|n| (n.x, n.z)).collect();
let floor_area: Vec<(i32, i32)> = flood_fill_area(&polygon_coords, floodfill_timeout);
let floor_area: Vec<(i32, i32)> = flood_fill_area(&polygon_coords, args.timeout.as_ref());
let mut rng: rand::prelude::ThreadRng = rand::thread_rng();
@@ -156,7 +175,14 @@ pub fn generate_landuse(
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));
create_tree(
editor,
x,
ground_level + 1,
z,
rng.gen_range(1..=3),
args.winter,
);
}
}
}
@@ -164,7 +190,14 @@ pub fn generate_landuse(
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));
create_tree(
editor,
x,
ground_level + 1,
z,
rng.gen_range(1..=3),
args.winter,
);
} else if random_choice == 2 {
let flower_block: Block = match rng.gen_range(1..=4) {
1 => RED_FLOWER,
@@ -206,6 +239,7 @@ pub fn generate_landuse(
ground_level + 1,
z,
rng.gen_range(1..=3),
args.winter,
);
} else if special_choice <= 6 {
editor.set_block(HAY_BALE, x, ground_level + 1, z, None, None);
@@ -319,7 +353,14 @@ pub fn generate_landuse(
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));
create_tree(
editor,
x,
ground_level + 1,
z,
rng.gen_range(1..=3),
args.winter,
);
} else if random_choice < 800 {
editor.set_block(GRASS, x, ground_level + 1, z, None, None);
}

View File

@@ -1,5 +1,4 @@
use std::time::Duration;
use crate::args::Args;
use crate::block_definitions::*;
use crate::bresenham::bresenham_line;
use crate::element_processing::tree::create_tree;
@@ -12,7 +11,7 @@ pub fn generate_leisure(
editor: &mut WorldEditor,
element: &ProcessedWay,
ground_level: i32,
floodfill_timeout: Option<&Duration>,
args: &Args,
) {
if let Some(leisure_type) = element.tags.get("leisure") {
let mut previous_node: Option<(i32, i32)> = None;
@@ -21,7 +20,13 @@ pub fn generate_leisure(
// Determine block type based on leisure type
let block_type: Block = match leisure_type.as_str() {
"park" => GRASS_BLOCK,
"park" => {
if args.winter {
SNOW_BLOCK
} else {
GRASS_BLOCK
}
}
"playground" | "recreation_ground" | "pitch" => {
if let Some(surface) = element.tags.get("surface") {
match surface.as_str() {
@@ -34,9 +39,21 @@ pub fn generate_leisure(
GREEN_STAINED_HARDENED_CLAY
}
}
"garden" => GRASS_BLOCK,
"garden" => {
if args.winter {
SNOW_BLOCK
} else {
GRASS_BLOCK
}
}
"swimming_pool" => WATER,
_ => GRASS_BLOCK,
_ => {
if args.winter {
SNOW_BLOCK
} else {
GRASS_BLOCK
}
}
};
// Process leisure area nodes
@@ -78,7 +95,8 @@ pub fn generate_leisure(
.iter()
.map(|n: &crate::osm_parser::ProcessedNode| (n.x, n.z))
.collect();
let filled_area: Vec<(i32, i32)> = flood_fill_area(&polygon_coords, floodfill_timeout);
let filled_area: Vec<(i32, i32)> =
flood_fill_area(&polygon_coords, args.timeout.as_ref());
for (x, z) in filled_area {
editor.set_block(block_type, x, ground_level, z, Some(&[GRASS_BLOCK]), None);
@@ -113,7 +131,14 @@ pub fn generate_leisure(
}
71..=80 => {
// Tree
create_tree(editor, x, ground_level + 1, z, rng.gen_range(1..=3));
create_tree(
editor,
x,
ground_level + 1,
z,
rng.gen_range(1..=3),
args.winter,
);
}
_ => {}
}

View File

@@ -1,5 +1,4 @@
use std::time::Duration;
use crate::args::Args;
use crate::block_definitions::*;
use crate::bresenham::bresenham_line;
use crate::element_processing::tree::create_tree;
@@ -12,7 +11,7 @@ pub fn generate_natural(
editor: &mut WorldEditor,
element: &ProcessedElement,
ground_level: i32,
floodfill_timeout: Option<&Duration>,
args: &Args,
) {
if let Some(natural_type) = element.tags().get("natural") {
if natural_type == "tree" {
@@ -21,7 +20,14 @@ pub fn generate_natural(
let z: i32 = node.z;
let mut rng: rand::prelude::ThreadRng = rand::thread_rng();
create_tree(editor, x, ground_level + 1, z, rng.gen_range(1..=3));
create_tree(
editor,
x,
ground_level + 1,
z,
rng.gen_range(1..=3),
args.winter,
);
}
} else {
let mut previous_node: Option<(i32, i32)> = None;
@@ -30,11 +36,29 @@ pub fn generate_natural(
// Determine block type based on natural tag
let block_type: Block = match natural_type.as_str() {
"scrub" | "grassland" | "wood" => GRASS_BLOCK,
"scrub" | "grassland" | "wood" => {
if args.winter {
SNOW_BLOCK
} else {
GRASS_BLOCK
}
}
"beach" | "sand" => SAND,
"tree_row" => GRASS_BLOCK,
"tree_row" => {
if args.winter {
SNOW_BLOCK
} else {
GRASS_BLOCK
}
}
"wetland" | "water" => WATER,
_ => GRASS_BLOCK,
_ => {
if args.winter {
SNOW_BLOCK
} else {
GRASS_BLOCK
}
}
};
let ProcessedElement::Way(way) = element else {
@@ -69,7 +93,7 @@ pub fn generate_natural(
.map(|n: &crate::osm_parser::ProcessedNode| (n.x, n.z))
.collect();
let filled_area: Vec<(i32, i32)> =
flood_fill_area(&polygon_coords, floodfill_timeout);
flood_fill_area(&polygon_coords, args.timeout.as_ref());
let mut rng: rand::prelude::ThreadRng = rand::thread_rng();
@@ -84,7 +108,14 @@ pub fn generate_natural(
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));
create_tree(
editor,
x,
ground_level + 1,
z,
rng.gen_range(1..=3),
args.winter,
);
} else if random_choice == 2 {
let flower_block = match rng.gen_range(1..=4) {
1 => RED_FLOWER,

View File

@@ -46,7 +46,7 @@ fn round3(editor: &mut WorldEditor, material: Block, x: i32, y: i32, z: i32) {
}
/// Function to create different types of trees.
pub fn create_tree(editor: &mut WorldEditor, x: i32, y: i32, z: i32, typetree: u8) {
pub fn create_tree(editor: &mut WorldEditor, x: i32, y: i32, z: i32, typetree: u8, snow: bool) {
let mut blacklist: Vec<Block> = Vec::new();
blacklist.extend(building_corner_variations());
blacklist.extend(building_wall_variations());
@@ -60,7 +60,6 @@ pub fn create_tree(editor: &mut WorldEditor, x: i32, y: i32, z: i32, typetree: u
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);
@@ -79,6 +78,24 @@ pub fn create_tree(editor: &mut WorldEditor, x: i32, y: i32, z: i32, typetree: u
round2(editor, OAK_LEAVES, x, y + 4, z);
round3(editor, OAK_LEAVES, x, y + 6, z);
round3(editor, OAK_LEAVES, x, y + 5, z);
if snow {
editor.set_block(SNOW_LAYER, x, y + 11, z, None, None);
editor.set_block(SNOW_LAYER, x + 1, y + 10, z, None, None);
editor.set_block(SNOW_LAYER, x - 1, y + 10, z, None, None);
editor.set_block(SNOW_LAYER, x, y + 10, z - 1, None, None);
editor.set_block(SNOW_LAYER, x, y + 10, z + 1, None, None);
round1(editor, SNOW_LAYER, x, y + 9, z);
round1(editor, SNOW_LAYER, x, y + 8, z);
round1(editor, SNOW_LAYER, x, y + 7, z);
round1(editor, SNOW_LAYER, x, y + 6, z);
round2(editor, SNOW_LAYER, x, y + 8, z);
round2(editor, SNOW_LAYER, x, y + 7, z);
round2(editor, SNOW_LAYER, x, y + 6, z);
round2(editor, SNOW_LAYER, x, y + 5, z);
round3(editor, SNOW_LAYER, x, y + 7, z);
round3(editor, SNOW_LAYER, x, y + 6, z);
}
}
2 => {
// Spruce tree
@@ -95,6 +112,21 @@ pub fn create_tree(editor: &mut WorldEditor, x: i32, y: i32, z: i32, typetree: u
round1(editor, BIRCH_LEAVES, x, y + 3, z);
round2(editor, BIRCH_LEAVES, x, y + 6, z);
round2(editor, BIRCH_LEAVES, x, y + 3, z);
if snow {
editor.set_block(SNOW_LAYER, x, y + 11, z, None, None);
editor.set_block(SNOW_LAYER, x + 1, y + 11, z, None, None);
editor.set_block(SNOW_LAYER, x - 1, y + 11, z, None, None);
editor.set_block(SNOW_LAYER, x, y + 11, z - 1, None, None);
editor.set_block(SNOW_LAYER, x, y + 11, z + 1, None, None);
round1(editor, SNOW_LAYER, x, y + 10, z);
round1(editor, SNOW_LAYER, x, y + 8, z);
round1(editor, SNOW_LAYER, x, y + 7, z);
round1(editor, SNOW_LAYER, x, y + 5, z);
round1(editor, SNOW_LAYER, x, y + 4, z);
round2(editor, SNOW_LAYER, x, y + 7, z);
round2(editor, SNOW_LAYER, x, y + 4, z);
}
}
3 => {
// Birch tree
@@ -112,6 +144,22 @@ pub fn create_tree(editor: &mut WorldEditor, x: i32, y: i32, z: i32, typetree: u
round2(editor, BIRCH_LEAVES, x, y + 2, z);
round2(editor, BIRCH_LEAVES, x, y + 3, z);
round2(editor, BIRCH_LEAVES, x, y + 4, z);
if snow {
editor.set_block(SNOW_LAYER, x, y + 9, z, None, None);
editor.set_block(SNOW_LAYER, x + 1, y + 8, z, None, None);
editor.set_block(SNOW_LAYER, x - 1, y + 8, z, None, None);
editor.set_block(SNOW_LAYER, x, y + 8, z - 1, None, None);
editor.set_block(SNOW_LAYER, x, y + 8, z + 1, None, None);
round1(editor, SNOW_LAYER, x, y + 7, z);
round1(editor, SNOW_LAYER, x, y + 6, z);
round1(editor, SNOW_LAYER, x, y + 5, z);
round1(editor, SNOW_LAYER, x, y + 4, z);
round1(editor, SNOW_LAYER, x, y + 3, z);
round2(editor, SNOW_LAYER, x, y + 3, z);
round2(editor, SNOW_LAYER, x, y + 4, z);
round2(editor, SNOW_LAYER, x, y + 5, z);
}
}
_ => {} // Do nothing if typetree is not recognized
}

View File

@@ -56,7 +56,7 @@ pub fn flood_fill_area(
while let Some((start_x, start_z)) = candidate_points.pop_front() {
if let Some(timeout) = timeout {
if &start_time.elapsed() > timeout {
eprintln!("Floodfill timeout"); // TODO only print when debug arg is set?
eprintln!("Floodfill timeout");
break;
}
}
@@ -70,7 +70,7 @@ pub fn flood_fill_area(
while let Some((x, z)) = queue.pop_front() {
if let Some(timeout) = timeout {
if &start_time.elapsed() > timeout {
eprintln!("Floodfill timeout"); // TODO only print when debug arg is set?
eprintln!("Floodfill timeout");
break;
}
}

View File

@@ -16,10 +16,16 @@ mod world_editor;
use args::Args;
use clap::Parser;
use colored::*;
use fastnbt::Value;
use flate2::read::GzDecoder;
use fs2::FileExt;
use rfd::FileDialog;
use std::fs::File;
use std::io::Write;
use std::{env, path::PathBuf};
use std::{
env,
fs::{self, File},
io::{Read, Write},
path::{Path, PathBuf},
};
fn print_banner() {
let version: &str = env!("CARGO_PKG_VERSION");
@@ -116,7 +122,7 @@ fn main() {
println!("Launching UI...");
tauri::Builder::default()
.invoke_handler(tauri::generate_handler![
gui_pick_directory,
gui_select_world,
gui_start_generation,
gui_get_version,
gui_check_for_updates
@@ -134,7 +140,7 @@ fn main() {
}
#[tauri::command]
fn gui_pick_directory() -> Result<String, String> {
fn gui_select_world(generate_new: bool) -> Result<String, String> {
// Determine the default Minecraft 'saves' directory based on the OS
let default_dir: Option<PathBuf> = if cfg!(target_os = "windows") {
env::var("APPDATA")
@@ -151,32 +157,133 @@ fn gui_pick_directory() -> Result<String, String> {
None
};
// Check if the default directory exists
let starting_directory: Option<PathBuf> = default_dir.filter(|dir: &PathBuf| dir.exists());
// Open the directory picker dialog
let dialog: FileDialog = FileDialog::new();
let dialog: FileDialog = if let Some(start_dir) = starting_directory {
dialog.set_directory(start_dir)
if generate_new {
// Handle new world generation
if let Some(default_path) = &default_dir {
if default_path.exists() {
// Call create_new_world and return the result
create_new_world(default_path)
} else {
Err("Minecraft directory not found".to_string())
}
} else {
Err("Minecraft directory not found".to_string())
}
} else {
dialog
// Handle existing world selection
// Open the directory picker dialog
let dialog: FileDialog = FileDialog::new();
let dialog: FileDialog = if let Some(start_dir) = default_dir.filter(|dir| dir.exists()) {
dialog.set_directory(start_dir)
} else {
dialog
};
if let Some(path) = dialog.pick_folder() {
// Check if the "region" folder exists within the selected directory
if path.join("region").exists() {
// Check the 'session.lock' file
let session_lock_path = path.join("session.lock");
if session_lock_path.exists() {
// Try to acquire a lock on the session.lock file
if let Ok(file) = File::open(&session_lock_path) {
if file.try_lock_shared().is_err() {
return Err("The selected world is currently in use".to_string());
} else {
// Release the lock immediately
let _ = file.unlock();
}
}
}
return Ok(path.display().to_string());
} else {
// No Minecraft directory found, generating new world in custom user selected directory
return create_new_world(&path);
}
}
// If no folder was selected, return an error message
Err("No world selected".to_string())
}
}
fn create_new_world(base_path: &Path) -> Result<String, String> {
// Generate a unique world name
let mut counter: i32 = 1;
let unique_name: String = loop {
let candidate_name: String = format!("Arnis World {}", counter);
let candidate_path: PathBuf = base_path.join(&candidate_name);
if !candidate_path.exists() {
break candidate_name;
}
counter += 1;
};
if let Some(path) = dialog.pick_folder() {
// Print the full path to the console
println!("Selected world path: {}", path.display());
let new_world_path: PathBuf = base_path.join(&unique_name);
// Check if the "region" folder exists within the selected directory
if path.join("region").exists() {
return Ok(path.display().to_string());
} else {
// Notify the frontend that no valid Minecraft world was found
return Err("Invalid Minecraft world".to_string());
// Create the new world directory structure
fs::create_dir_all(new_world_path.join("region"))
.map_err(|e| format!("Failed to create world directory: {}", e))?;
// Copy the region template file
const REGION_TEMPLATE: &[u8] = include_bytes!("../mcassets/region.template");
let region_path = new_world_path.join("region").join("r.0.0.mca");
fs::write(&region_path, REGION_TEMPLATE)
.map_err(|e| format!("Failed to create region file: {}", e))?;
// Add the level.dat file
const LEVEL_TEMPLATE: &[u8] = include_bytes!("../mcassets/level.dat");
// Decompress the gzipped level.template
let mut decoder = GzDecoder::new(LEVEL_TEMPLATE);
let mut decompressed_data = Vec::new();
decoder
.read_to_end(&mut decompressed_data)
.map_err(|e| format!("Failed to decompress level.template: {}", e))?;
// Parse the decompressed NBT data
let mut level_data: Value = fastnbt::from_bytes(&decompressed_data)
.map_err(|e| format!("Failed to parse level.dat template: {}", e))?;
// Modify the LevelName and LastPlayed fields
if let Value::Compound(ref mut root) = level_data {
if let Some(Value::Compound(ref mut data)) = root.get_mut("Data") {
// Update LevelName
data.insert("LevelName".to_string(), Value::String(unique_name.clone()));
// Update LastPlayed to the current Unix time in milliseconds
let current_time = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map_err(|e| format!("Failed to get current time: {}", e))?;
let current_time_millis = current_time.as_millis() as i64;
data.insert("LastPlayed".to_string(), Value::Long(current_time_millis));
}
}
// If no folder was selected, return an error message
Err("No world selected".to_string())
// Serialize the updated NBT data back to bytes
let serialized_level_data: Vec<u8> = fastnbt::to_bytes(&level_data)
.map_err(|e| format!("Failed to serialize updated level.dat: {}", e))?;
// Compress the serialized data back to gzip
let mut encoder = flate2::write::GzEncoder::new(Vec::new(), flate2::Compression::default());
encoder
.write_all(&serialized_level_data)
.map_err(|e| format!("Failed to compress updated level.dat: {}", e))?;
let compressed_level_data = encoder
.finish()
.map_err(|e| format!("Failed to finalize compression for level.dat: {}", e))?;
// Write the level.dat file
fs::write(new_world_path.join("level.dat"), compressed_level_data)
.map_err(|e| format!("Failed to create level.dat file: {}", e))?;
// Add the icon.png file
const ICON_TEMPLATE: &[u8] = include_bytes!("../mcassets/icon.png");
fs::write(new_world_path.join("icon.png"), ICON_TEMPLATE)
.map_err(|e| format!("Failed to create icon.png file: {}", e))?;
Ok(new_world_path.display().to_string())
}
#[tauri::command]
@@ -193,7 +300,14 @@ fn gui_check_for_updates() -> Result<bool, String> {
}
#[tauri::command]
fn gui_start_generation(bbox_text: String, selected_world: String) -> Result<(), String> {
fn gui_start_generation(
bbox_text: String,
selected_world: String,
world_scale: f64,
ground_level: i32,
winter_mode: bool,
floodfill_timeout: u64,
) -> Result<(), String> {
tauri::async_runtime::spawn(async move {
if let Err(e) = tokio::task::spawn_blocking(move || {
// Utility function to reorder bounding box coordinates
@@ -217,9 +331,11 @@ fn gui_start_generation(bbox_text: String, selected_world: String) -> Result<(),
file: None,
path: selected_world,
downloader: "requests".to_string(),
scale: 1.0,
scale: world_scale,
ground_level,
winter: winter_mode,
debug: false,
timeout: None,
timeout: Some(std::time::Duration::from_secs(floodfill_timeout)),
};
// Reorder bounding box coordinates for further processing

View File

@@ -51,7 +51,7 @@ pub struct ProcessedWay {
pub tags: HashMap<String, String>,
}
#[derive(Debug)]
#[derive(Debug, PartialEq)]
pub enum ProcessedMemberRole {
Outer,
Inner,

View File

@@ -7,6 +7,7 @@ use fastnbt::{LongArray, Value};
use fnv::FnvHashMap;
use indicatif::{ProgressBar, ProgressStyle};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fs::File;
use std::io::Write;
@@ -137,6 +138,7 @@ impl Default for SectionToModify {
#[derive(Default)]
struct ChunkToModify {
sections: FnvHashMap<i8, SectionToModify>,
other: FnvHashMap<String, Value>,
}
impl ChunkToModify {
@@ -248,7 +250,7 @@ impl<'a> WorldEditor<'a> {
fn create_region(&self, region_x: i32, region_z: i32) -> Region<File> {
let out_path: String = format!("{}/r.{}.{}.mca", self.region_dir, region_x, region_z);
const REGION_TEMPLATE: &[u8] = include_bytes!("region.template");
const REGION_TEMPLATE: &[u8] = include_bytes!("../mcassets/region.template");
let mut region_file: File = File::options()
.read(true)
@@ -274,6 +276,65 @@ impl<'a> WorldEditor<'a> {
self.world.get_block(x, y, z).is_some()
}*/
#[allow(clippy::too_many_arguments)]
pub fn set_sign(
&mut self,
line1: String,
line2: String,
line3: String,
line4: String,
x: i32,
y: i32,
z: i32,
_rotation: i8,
) {
let chunk_x = x >> 4;
let chunk_z = z >> 4;
let region_x = chunk_x >> 5;
let region_z = chunk_z >> 5;
let mut block_entities = HashMap::new();
let messages = vec![
Value::String(format!("\"{}\"", line1)),
Value::String(format!("\"{}\"", line2)),
Value::String(format!("\"{}\"", line3)),
Value::String(format!("\"{}\"", line4)),
];
let mut text_data = HashMap::new();
text_data.insert("messages".to_string(), Value::List(messages));
text_data.insert("color".to_string(), Value::String("black".to_string()));
text_data.insert("has_glowing_text".to_string(), Value::Byte(0));
block_entities.insert("front_text".to_string(), Value::Compound(text_data));
block_entities.insert(
"id".to_string(),
Value::String("minecraft:sign".to_string()),
);
block_entities.insert("is_waxed".to_string(), Value::Byte(0));
block_entities.insert("keepPacked".to_string(), Value::Byte(0));
block_entities.insert("x".to_string(), Value::Int(x));
block_entities.insert("y".to_string(), Value::Int(y));
block_entities.insert("z".to_string(), Value::Int(z));
let region: &mut RegionToModify = self.world.get_or_create_region(region_x, region_z);
let chunk: &mut ChunkToModify = region.get_or_create_chunk(chunk_x & 31, chunk_z & 31);
if let Some(chunk_data) = chunk.other.get_mut("block_entities") {
if let Value::List(entities) = chunk_data {
entities.push(Value::Compound(block_entities));
}
} else {
chunk.other.insert(
"block_entities".to_string(),
Value::List(vec![Value::Compound(block_entities)]),
);
}
self.set_block(SIGN, x, y, z, None, None);
}
/// Sets a block of the specified type at the given coordinates.
pub fn set_block(
&mut self,
@@ -389,7 +450,7 @@ impl<'a> WorldEditor<'a> {
.progress_chars("█▓░"),
);
let total_steps: f64 = 10.0;
let total_steps: f64 = 9.0;
let progress_increment_save: f64 = total_steps / total_regions as f64;
let mut current_progress_save: f64 = 90.0;
let mut last_emitted_progress: f64 = current_progress_save;
@@ -408,6 +469,7 @@ impl<'a> WorldEditor<'a> {
if let Some(chunk_to_modify) = region_to_modify.get_chunk(chunk_x, chunk_z) {
chunk.sections = chunk_to_modify.sections().collect();
chunk.other.extend(chunk_to_modify.other.clone());
}
chunk.x_pos = chunk_x + region_x * 32;

View File

@@ -1,7 +1,7 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "Arnis",
"version": "2.1.0",
"version": "2.1.2",
"identifier": "com.louisdev.arnis",
"build": {
"frontendDist": "gui-src"
@@ -15,7 +15,8 @@
"height": 650,
"resizable": false,
"transparent": true,
"center": true
"center": true,
"theme": "Dark"
}
],
"security": {
@@ -31,6 +32,11 @@
"icons/128x128@2x.png",
"icons/icon.icns",
"icons/icon.ico"
]
],
"windows": {
"webviewInstallMode": {
"type": "embedBootstrapper"
}
}
}
}