mirror of
https://github.com/louis-e/arnis.git
synced 2026-01-07 13:48:26 -05:00
Compare commits
44 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
96cbf2ac96 | ||
|
|
feab8cea98 | ||
|
|
8128270a57 | ||
|
|
8ab5745efa | ||
|
|
8291a94e13 | ||
|
|
8d5d96f88a | ||
|
|
5008ddd988 | ||
|
|
4a200e01f8 | ||
|
|
eb9e349cdb | ||
|
|
4ccb8a492b | ||
|
|
a7a74fecdb | ||
|
|
364e4933ac | ||
|
|
b7db71209e | ||
|
|
ae4c8371ae | ||
|
|
14aa8e94d5 | ||
|
|
8f739e0189 | ||
|
|
c9b2899c83 | ||
|
|
155e3e5a9b | ||
|
|
d364ed1361 | ||
|
|
2f2d1c79bb | ||
|
|
54313019af | ||
|
|
1bd3fb6802 | ||
|
|
5cbeeb0e01 | ||
|
|
55b522764d | ||
|
|
e2fd24ba08 | ||
|
|
b095c40285 | ||
|
|
ffd5a274b0 | ||
|
|
0097e3b834 | ||
|
|
b37aa8185b | ||
|
|
11c1c18188 | ||
|
|
6200d4d6ef | ||
|
|
2afd508d67 | ||
|
|
62a5a85625 | ||
|
|
36a17afd9f | ||
|
|
7c211938f7 | ||
|
|
3aa66407be | ||
|
|
18f1239215 | ||
|
|
f9d7600839 | ||
|
|
1bee90889c | ||
|
|
ecead36496 | ||
|
|
13a48105fe | ||
|
|
07fdcf7bed | ||
|
|
aae99417dd | ||
|
|
f4e0b66245 |
8
.github/ISSUE_TEMPLATE/bug_report.md
vendored
8
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -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.
|
||||
|
||||
10
.github/dependabot.yml
vendored
Normal file
10
.github/dependabot.yml
vendored
Normal 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
3
.gitignore
vendored
@@ -7,9 +7,6 @@
|
||||
/target
|
||||
**/*.rs.bk
|
||||
|
||||
# Lock files
|
||||
Cargo.lock
|
||||
|
||||
# IDE/editor files
|
||||
.idea/
|
||||
/.vscode/
|
||||
|
||||
6149
Cargo.lock
generated
Normal file
6149
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
10
Cargo.toml
10
Cargo.toml
@@ -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,20 +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"
|
||||
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"
|
||||
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"
|
||||
|
||||
51
README.md
51
README.md
@@ -11,31 +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 coordinates 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.
|
||||
|
||||
<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. https://github.com/louis-e/arnis/issues/99</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
|
||||

|
||||
@@ -56,26 +43,41 @@ Yes! Arnis was initially developed in Python, which benefited from Python's open
|
||||
- *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!
|
||||
- [ ] Mapping real coordinates to Minecraft coordinates (https://github.com/louis-e/arnis/issues/29)
|
||||
- [ ] 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
|
||||
- [ ] 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
|
||||
|
||||
@@ -95,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
2
gui-src/index.html
vendored
@@ -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>
|
||||
|
||||
|
||||
17
gui-src/js/main.js
vendored
17
gui-src/js/main.js
vendored
@@ -217,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;
|
||||
}
|
||||
|
||||
@@ -260,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";
|
||||
@@ -285,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!";
|
||||
|
||||
@@ -140,6 +140,7 @@ impl Block {
|
||||
110 => "bedrock",
|
||||
111 => "snow_block",
|
||||
112 => "snow",
|
||||
113 => "oak_sign",
|
||||
_ => panic!("Invalid id"),
|
||||
}
|
||||
}
|
||||
@@ -176,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,
|
||||
}
|
||||
}
|
||||
@@ -289,6 +300,7 @@ 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);
|
||||
|
||||
@@ -21,6 +21,17 @@ pub fn generate_world(
|
||||
let mut editor: WorldEditor =
|
||||
WorldEditor::new(®ion_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);
|
||||
@@ -94,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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)>,
|
||||
|
||||
62
src/main.rs
62
src/main.rs
@@ -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 = 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;
|
||||
};
|
||||
|
||||
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())
|
||||
// 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
|
||||
@@ -194,9 +180,6 @@ fn gui_select_world(generate_new: bool) -> Result<String, String> {
|
||||
};
|
||||
|
||||
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
|
||||
@@ -215,8 +198,8 @@ fn gui_select_world(generate_new: bool) -> Result<String, 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());
|
||||
// No Minecraft directory found, generating new world in custom user selected directory
|
||||
return create_new_world(&path);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -225,14 +208,27 @@ 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"))
|
||||
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(®ion_path, REGION_TEMPLATE)
|
||||
.map_err(|e| format!("Failed to create region file: {}", e))?;
|
||||
|
||||
@@ -254,10 +250,7 @@ fn create_new_world(world_path: &Path, world_name: &str) -> Result<(), String> {
|
||||
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::SystemTime::now()
|
||||
@@ -269,7 +262,7 @@ fn create_new_world(world_path: &Path, world_name: &str) -> Result<(), String> {
|
||||
}
|
||||
|
||||
// Serialize the updated NBT data back to bytes
|
||||
let serialized_level_data = fastnbt::to_bytes(&level_data)
|
||||
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
|
||||
@@ -281,15 +274,16 @@ fn create_new_world(world_path: &Path, world_name: &str) -> Result<(), String> {
|
||||
.finish()
|
||||
.map_err(|e| format!("Failed to finalize compression for level.dat: {}", e))?;
|
||||
|
||||
fs::write(world_path.join("level.dat"), compressed_level_data)
|
||||
// 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)
|
||||
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]
|
||||
|
||||
@@ -51,7 +51,7 @@ pub struct ProcessedWay {
|
||||
pub tags: HashMap<String, String>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub enum ProcessedMemberRole {
|
||||
Outer,
|
||||
Inner,
|
||||
|
||||
@@ -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 {
|
||||
@@ -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,
|
||||
@@ -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;
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user