Compare commits

...

29 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
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
12 changed files with 6297 additions and 102 deletions

View File

@@ -11,13 +11,13 @@ assignees: ''
A clear and concise description of what the bug is and what you expected to happen.
**Used bbox area**
Please provide your input parameters so we can reproduce the issue.
Please provide your input parameters so we can reproduce the issue. *(For example: 48.133444 11.569462 48.142609 11.584740)*
**Used Minecraft version**
Please provide the Minecraft version you used.
**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. If you used any more custom parameters, please provide them here too.
Add any other context about the problem here. If you used any more custom settings, please provide them here too.

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.1"
version = "2.1.2"
edition = "2021"
description = "Arnis - Generate real life cities in Minecraft"
homepage = "https://github.com/louis-e/arnis"
@@ -17,15 +17,15 @@ 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"
fs2 = "0.4"
geo = "0.28.0"
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"

View File

@@ -18,7 +18,7 @@ Arnis is designed to handle large-scale data and generate rich, immersive enviro
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).
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.
If you choose to select an own world, make sure to generate a new flat world in advance in Minecraft.
@@ -48,6 +48,8 @@ The script uses the [fastnbt](https://github.com/owengage/fastnbt) cargo package
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>
@@ -56,23 +58,26 @@ Make sure to teleport to the generation starting point (/tp 0 0 0). If there is
## :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!
- [ ] Fix compilation for Linux
- [ ] Support multipolygons
- [ ] Add street names as signs
- [ ] Add support for older Minecraft versions (<=1.16.5)
- [ ] Mapping real coordinates to Minecraft coordinates (https://github.com/louis-e/arnis/issues/29)
- [ ] 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
- [ ] 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
- [ ] Automatic new world creation instead of using an existing world
- [ ] Implement house roof types
- [ ] Refactor bridges implementation
- [ ] Refactor railway implementation
- [ ] Better code documentation
- [ ] Refactor fountain structure implementation
- [ ] Add interior to buildings
- [ ] 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
@@ -92,13 +97,6 @@ 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
<a href="https://star-history.com/#louis-e/arnis&Date">

2
gui-src/index.html vendored
View File

@@ -97,7 +97,7 @@
<!-- 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.50" max="2.5" step="0.25" value="1">
<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>

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

@@ -270,6 +270,7 @@ async function selectWorld(generate_new_world) {
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";
@@ -295,7 +296,7 @@ async function startGeneration() {
worldPath === "No world selected" ||
worldPath == "Invalid Minecraft world" ||
worldPath == "The selected world is currently in use" ||
worldPath == "Minecraft directory not found." ||
worldPath == "Minecraft directory not found" ||
worldPath === ""
) {
document.getElementById('selected-world').textContent = "Select a Minecraft world first!";

View File

@@ -105,7 +105,14 @@ pub fn generate_world(
}
}
ProcessedElement::Relation(rel) => {
if rel.tags.contains_key("water") {
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);
}
}

View File

@@ -3,7 +3,7 @@ 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;
@@ -80,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;
@@ -284,6 +317,35 @@ pub fn generate_buildings(
}
}
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]));
}
}
}
}
fn find_nearest_block_in_color_map(
rgb: &RGBTuple,
color_map: Vec<(RGBTuple, Block)>,

View File

@@ -161,27 +161,13 @@ fn gui_select_world(generate_new: bool) -> Result<String, String> {
// Handle new world generation
if let Some(default_path) = &default_dir {
if default_path.exists() {
// 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 = default_path.join(&candidate_name);
if !candidate_path.exists() {
break candidate_name;
}
counter += 1;
};
let new_world_path: PathBuf = default_path.join(&unique_name);
// Create the new world structure
create_new_world(&new_world_path, &unique_name)?;
Ok(new_world_path.display().to_string())
// Call create_new_world and return the result
create_new_world(default_path)
} else {
Err("Minecraft directory not found.".to_string())
Err("Minecraft directory not found".to_string())
}
} else {
Err("Minecraft directory not found.".to_string())
Err("Minecraft directory not found".to_string())
}
} else {
// Handle existing world selection
@@ -212,24 +198,8 @@ fn gui_select_world(generate_new: bool) -> Result<String, String> {
return Ok(path.display().to_string());
} else {
// No Minecraft directory found, generating world in custom user selected directory
// 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 = path.join(&candidate_name);
if !candidate_path.exists() {
break candidate_name;
}
counter += 1;
};
let new_world_path: PathBuf = path.join(&unique_name);
// Create the new world structure
create_new_world(&new_world_path, &unique_name)?;
return Ok(new_world_path.display().to_string());
// No Minecraft directory found, generating new world in custom user selected directory
return create_new_world(&path);
}
}
@@ -238,76 +208,82 @@ fn gui_select_world(generate_new: bool) -> Result<String, String> {
}
}
fn create_new_world(world_path: &Path, world_name: &str) -> Result<(), 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;
};
let new_world_path: PathBuf = base_path.join(&unique_name);
// Create the new world directory structure
fs::create_dir_all(world_path.join("region"))
.map_err(|e: std::io::Error| format!("Failed to create world directory: {}", e))?;
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 = world_path.join("region").join("r.0.0.mca");
let region_path = new_world_path.join("region").join("r.0.0.mca");
fs::write(&region_path, REGION_TEMPLATE)
.map_err(|e: std::io::Error| format!("Failed to create region file: {}", e))?;
.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<&[u8]> = GzDecoder::new(LEVEL_TEMPLATE);
let mut decompressed_data: Vec<u8> = Vec::new();
let mut decoder = GzDecoder::new(LEVEL_TEMPLATE);
let mut decompressed_data = Vec::new();
decoder
.read_to_end(&mut decompressed_data)
.map_err(|e: std::io::Error| format!("Failed to decompress level.template: {}", e))?;
.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: fastnbt::error::Error| format!("Failed to parse level.dat template: {}", e))?;
.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(world_name.to_string()),
);
data.insert("LevelName".to_string(), Value::String(unique_name.clone()));
// Update LastPlayed to the current Unix time in milliseconds
let current_time: std::time::Duration = std::time::SystemTime::now()
let current_time = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map_err(|e: std::time::SystemTimeError| {
format!("Failed to get current time: {}", e)
})?;
let current_time_millis: i64 = current_time.as_millis() as i64;
.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));
}
}
// Serialize the updated NBT data back to bytes
let serialized_level_data: Vec<u8> =
fastnbt::to_bytes(&level_data).map_err(|e: fastnbt::error::Error| {
format!("Failed to serialize updated level.dat: {}", e)
})?;
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<Vec<u8>> =
flate2::write::GzEncoder::new(Vec::new(), flate2::Compression::default());
let mut encoder = flate2::write::GzEncoder::new(Vec::new(), flate2::Compression::default());
encoder
.write_all(&serialized_level_data)
.map_err(|e: std::io::Error| format!("Failed to compress updated level.dat: {}", e))?;
let compressed_level_data: Vec<u8> = encoder.finish().map_err(|e: std::io::Error| {
format!("Failed to finalize compression for level.dat: {}", e)
})?;
.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))?;
fs::write(world_path.join("level.dat"), compressed_level_data)
.map_err(|e: std::io::Error| format!("Failed to create level.dat file: {}", 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(world_path.join("icon.png"), ICON_TEMPLATE)
.map_err(|e: std::io::Error| format!("Failed to create icon.png file: {}", e))?;
fs::write(new_world_path.join("icon.png"), ICON_TEMPLATE)
.map_err(|e| format!("Failed to create icon.png file: {}", e))?;
Ok(())
Ok(new_world_path.display().to_string())
}
#[tauri::command]

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

@@ -1,7 +1,7 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "Arnis",
"version": "2.1.1",
"version": "2.1.2",
"identifier": "com.louisdev.arnis",
"build": {
"frontendDist": "gui-src"
@@ -32,6 +32,11 @@
"icons/128x128@2x.png",
"icons/icon.icns",
"icons/icon.ico"
]
],
"windows": {
"webviewInstallMode": {
"type": "embedBootstrapper"
}
}
}
}