mirror of
https://github.com/louis-e/arnis.git
synced 2026-01-22 04:57:53 -05:00
Compare commits
17 Commits
v2.1.0-men
...
v2.1.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a99dd0f907 | ||
|
|
5b023fc9ce | ||
|
|
ed57bf1b4e | ||
|
|
1f45100707 | ||
|
|
aa1c8509a5 | ||
|
|
6193a05be7 | ||
|
|
b9277e6a60 | ||
|
|
5ae8c27bfa | ||
|
|
0f47fa1316 | ||
|
|
04c0b5e6f8 | ||
|
|
c339f02d1b | ||
|
|
e2e69d119f | ||
|
|
278c7a2cda | ||
|
|
fb31cb5a21 | ||
|
|
712c7db03e | ||
|
|
420852fcb0 | ||
|
|
a3aae00ae0 |
7
.github/ISSUE_TEMPLATE/bug_report.md
vendored
7
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -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**
|
||||
**Used bbox area**
|
||||
Please provide your input parameters so we can reproduce the issue.
|
||||
|
||||
**Used Minecraft version**
|
||||
Please provide the Minecraft version 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 parameters, please provide them here too.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "arnis"
|
||||
version = "2.1.0"
|
||||
version = "2.1.1"
|
||||
edition = "2021"
|
||||
description = "Arnis - Generate real life cities in Minecraft"
|
||||
homepage = "https://github.com/louis-e/arnis"
|
||||
@@ -20,6 +20,7 @@ colored = "2.1.0"
|
||||
dirs = "4.0.0"
|
||||
fastanvil = "0.31.0"
|
||||
fastnbt = "2.5.0"
|
||||
flate2 = "1.0"
|
||||
fnv = "1.0.7"
|
||||
fs2 = "0.4"
|
||||
geo = "0.28.0"
|
||||
|
||||
35
README.md
35
README.md
@@ -16,10 +16,12 @@ Arnis is designed to handle large-scale data and generate rich, immersive enviro
|
||||
## :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.
|
||||
|
||||
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'!
|
||||
|
||||
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>
|
||||
@@ -31,7 +33,7 @@ The --bbox parameter specifies the bounding box coordinates in the format: min_l
|
||||
<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>
|
||||
<i>Note: This might not be working right now since the console gets suppressed. https://github.com/louis-e/arnis/issues/99</i>
|
||||
|
||||
</details>
|
||||
|
||||
@@ -41,16 +43,16 @@ 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>
|
||||
@@ -61,6 +63,7 @@ The project is named after the smallest city in Germany, Arnis[^2]. The city's s
|
||||
## :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!
|
||||
- [ ] Mapping real coordinates to Minecraft coordinates (https://github.com/louis-e/arnis/issues/29)
|
||||
- [ ] Rotate maps (https://github.com/louis-e/arnis/issues/97)
|
||||
- [ ] Evaluate and implement elevation (https://github.com/louis-e/arnis/issues/66)
|
||||
- [ ] Fix Github Action Workflow for releasing Linux & MacOS Binary
|
||||
- [ ] Evaluate and implement faster region saving
|
||||
@@ -81,7 +84,7 @@ Feel free to choose an item from the To-Do or Known Bugs list, or bring your own
|
||||
- **Modularity**: Ensure that all components (e.g., data fetching, processing, and world generation) are cleanly separated into distinct modules for better maintainability and scalability.
|
||||
- **Performance Optimization**: Utilize Rust’s memory safety and concurrency features to optimize the performance of the world generation process.
|
||||
- **Comprehensive Documentation**: Detailed in-code documentation for a clear structure and logic.
|
||||
- **User-Friendly Experience**: Focus on making the project easy to use for end users, with the potential to develop a graphical user interface (GUI) in the future. Suggestions and discussions on UI/UX are welcome.
|
||||
- **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
|
||||
@@ -101,12 +104,18 @@ This section is dedicated to recognizing and celebrating the outstanding contrib
|
||||
|
||||
## :star: Star History
|
||||
|
||||
[](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
|
||||
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 174 KiB After Width: | Height: | Size: 198 KiB |
21
gui-src/index.html
vendored
21
gui-src/index.html
vendored
@@ -37,7 +37,7 @@
|
||||
|
||||
<!-- 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;">
|
||||
@@ -71,6 +71,17 @@
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<!-- World Picker Modal -->
|
||||
<div id="world-modal" class="modal" style="display: none;">
|
||||
<div class="modal-content">
|
||||
<span class="close-button" onclick="closeWorldPicker()">×</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">
|
||||
@@ -79,7 +90,7 @@
|
||||
|
||||
<!-- Winter Mode Toggle Button -->
|
||||
<div class="winter-toggle-container">
|
||||
<label for="winter-toggle">Winter:</label>
|
||||
<label for="winter-toggle">Winter Mode:</label>
|
||||
<input type="checkbox" id="winter-toggle" name="winter-toggle">
|
||||
</div>
|
||||
|
||||
@@ -100,6 +111,12 @@
|
||||
<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>
|
||||
|
||||
53
gui-src/js/main.js
vendored
53
gui-src/js/main.js
vendored
@@ -5,10 +5,11 @@ window.addEventListener("DOMContentLoaded", async () => {
|
||||
initFooter();
|
||||
await checkForUpdates();
|
||||
registerMessageEvent();
|
||||
window.pickDirectory = pickDirectory;
|
||||
window.selectWorld = selectWorld;
|
||||
window.startGeneration = startGeneration;
|
||||
setupProgressListener();
|
||||
initSettings();
|
||||
initWorldPicker();
|
||||
handleBboxInput();
|
||||
});
|
||||
|
||||
@@ -119,6 +120,26 @@ function initSettings() {
|
||||
});
|
||||
}
|
||||
|
||||
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");
|
||||
@@ -229,10 +250,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();
|
||||
@@ -244,6 +264,8 @@ async function pickDirectory() {
|
||||
document.getElementById('selected-world').textContent = error;
|
||||
document.getElementById('selected-world').style.color = "#fa7878";
|
||||
}
|
||||
|
||||
closeWorldPicker();
|
||||
}
|
||||
|
||||
let generationButtonEnabled = true;
|
||||
@@ -259,7 +281,13 @@ async function startGeneration() {
|
||||
return;
|
||||
}
|
||||
|
||||
if (worldPath === "No world selected" || worldPath == "Invalid Minecraft world" || worldPath == "The selected world is currently in use" || worldPath === "") {
|
||||
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;
|
||||
@@ -268,13 +296,22 @@ async function startGeneration() {
|
||||
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 the floodfill timeout
|
||||
// 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, worldScale: scale, winterMode: winter_mode, floodfillTimeout: floodfill_timeout });
|
||||
|
||||
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) {
|
||||
|
||||
BIN
mcassets/icon.png
Normal file
BIN
mcassets/icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 9.4 KiB |
BIN
mcassets/level.dat
Normal file
BIN
mcassets/level.dat
Normal file
Binary file not shown.
@@ -33,6 +33,10 @@ 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,
|
||||
|
||||
@@ -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,6 +16,7 @@ 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(®ion_dir, scale_factor_x, scale_factor_z, args);
|
||||
@@ -54,49 +53,49 @@ 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);
|
||||
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);
|
||||
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);
|
||||
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);
|
||||
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);
|
||||
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);
|
||||
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);
|
||||
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);
|
||||
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);
|
||||
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);
|
||||
water_areas::generate_water_areas(&mut editor, rel, ground_level);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -131,8 +130,8 @@ pub fn generate_world(
|
||||
|
||||
for x in 0..=(scale_factor_x as i32) {
|
||||
for z in 0..=(scale_factor_z as i32) {
|
||||
editor.set_block(groundlayer_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 {
|
||||
|
||||
170
src/main.rs
170
src/main.rs
@@ -16,11 +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");
|
||||
@@ -117,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
|
||||
@@ -135,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")
|
||||
@@ -152,46 +157,139 @@ 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());
|
||||
if generate_new {
|
||||
// Handle new world generation
|
||||
if let Some(default_path) = &default_dir {
|
||||
if default_path.exists() {
|
||||
// Generate a unique world name
|
||||
let mut counter = 1;
|
||||
let unique_name = loop {
|
||||
let candidate_name = format!("Arnis World {}", counter);
|
||||
let candidate_path = default_path.join(&candidate_name);
|
||||
if !candidate_path.exists() {
|
||||
break candidate_name;
|
||||
}
|
||||
counter += 1;
|
||||
};
|
||||
|
||||
// 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)
|
||||
let new_world_path = 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())
|
||||
} 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() {
|
||||
// Print the full path to the console
|
||||
println!("Selected world path: {}", path.display());
|
||||
if let Some(path) = dialog.pick_folder() {
|
||||
// Print the full path to the console
|
||||
println!("Selected world path: {}", path.display());
|
||||
|
||||
// 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();
|
||||
// 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 {
|
||||
// Notify the frontend that no valid Minecraft world was found
|
||||
return Err("Invalid Minecraft world".to_string());
|
||||
return Ok(path.display().to_string());
|
||||
} else {
|
||||
// Notify the frontend that no valid Minecraft world was found
|
||||
return Err("Invalid Minecraft world".to_string());
|
||||
}
|
||||
}
|
||||
|
||||
// If no folder was selected, return an error message
|
||||
Err("No world selected".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
fn create_new_world(world_path: &Path, world_name: &str) -> Result<(), String> {
|
||||
// Create the new world directory structure
|
||||
fs::create_dir_all(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");
|
||||
fs::write(®ion_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(world_name.to_string()),
|
||||
);
|
||||
|
||||
// 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 = 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))?;
|
||||
|
||||
fs::write(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| format!("Failed to create icon.png file: {}", e))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
@@ -212,6 +310,7 @@ fn gui_start_generation(
|
||||
bbox_text: String,
|
||||
selected_world: String,
|
||||
world_scale: f64,
|
||||
ground_level: i32,
|
||||
winter_mode: bool,
|
||||
floodfill_timeout: u64,
|
||||
) -> Result<(), String> {
|
||||
@@ -239,6 +338,7 @@ fn gui_start_generation(
|
||||
path: selected_world,
|
||||
downloader: "requests".to_string(),
|
||||
scale: world_scale,
|
||||
ground_level,
|
||||
winter: winter_mode,
|
||||
debug: false,
|
||||
timeout: Some(std::time::Duration::from_secs(floodfill_timeout)),
|
||||
|
||||
@@ -248,7 +248,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)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://schema.tauri.app/config/2",
|
||||
"productName": "Arnis",
|
||||
"version": "2.1.0",
|
||||
"version": "2.1.1",
|
||||
"identifier": "com.louisdev.arnis",
|
||||
"build": {
|
||||
"frontendDist": "gui-src"
|
||||
|
||||
Reference in New Issue
Block a user