mirror of
https://github.com/louis-e/arnis.git
synced 2026-01-15 17:47:49 -05:00
Compare commits
30 Commits
v2.4.0
...
codex/refa
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6c454aeb1d | ||
|
|
cc89576828 | ||
|
|
809fa23941 | ||
|
|
77a6041c40 | ||
|
|
8e8d8e0567 | ||
|
|
da6f23c0a2 | ||
|
|
d4a872989c | ||
|
|
2a5a5230c5 | ||
|
|
9018584b1d | ||
|
|
9eda39846c | ||
|
|
5e9d6795df | ||
|
|
54a7a4f2a9 | ||
|
|
d0d65643f5 | ||
|
|
946fd43a5e | ||
|
|
05e5ffdd2a | ||
|
|
0b7e27df7f | ||
|
|
613a410c93 | ||
|
|
faefd29e30 | ||
|
|
9ad6c75440 | ||
|
|
e51f28f067 | ||
|
|
47ddb9b211 | ||
|
|
46415bb002 | ||
|
|
0683dd3343 | ||
|
|
4d304dc978 | ||
|
|
5d97391820 | ||
|
|
bef3cfb090 | ||
|
|
5a898944f7 | ||
|
|
9fdd960009 | ||
|
|
58e4a337d9 | ||
|
|
236a7e5af9 |
14
.github/workflows/release.yml
vendored
14
.github/workflows/release.yml
vendored
@@ -87,7 +87,7 @@ jobs:
|
||||
shell: powershell
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: ${{ matrix.os }}-${{ matrix.target }}-build
|
||||
path: target/release/${{ matrix.asset_name }}
|
||||
@@ -97,13 +97,13 @@ jobs:
|
||||
runs-on: macos-latest
|
||||
steps:
|
||||
- name: Download macOS Intel build
|
||||
uses: actions/download-artifact@v5
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
name: macos-13-x86_64-apple-darwin-build
|
||||
path: ./intel
|
||||
|
||||
- name: Download macOS ARM64 build
|
||||
uses: actions/download-artifact@v5
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
name: macos-latest-aarch64-apple-darwin-build
|
||||
path: ./arm64
|
||||
@@ -114,7 +114,7 @@ jobs:
|
||||
chmod +x arnis-mac-universal
|
||||
|
||||
- name: Upload universal binary
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: macos-universal-build
|
||||
path: arnis-mac-universal
|
||||
@@ -127,19 +127,19 @@ jobs:
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Download Windows build artifact
|
||||
uses: actions/download-artifact@v5
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
name: windows-latest-x86_64-pc-windows-msvc-build
|
||||
path: ./builds/windows
|
||||
|
||||
- name: Download Linux build artifact
|
||||
uses: actions/download-artifact@v5
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
name: ubuntu-latest-x86_64-unknown-linux-gnu-build
|
||||
path: ./builds/linux
|
||||
|
||||
- name: Download macOS universal build artifact
|
||||
uses: actions/download-artifact@v5
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
name: macos-universal-build
|
||||
path: ./builds/macos
|
||||
|
||||
15
Cargo.lock
generated
15
Cargo.lock
generated
@@ -4487,9 +4487,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rfd"
|
||||
version = "0.15.4"
|
||||
version = "0.16.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ef2bee61e6cffa4635c72d7d81a84294e28f0930db0ddcb0f66d10244674ebed"
|
||||
checksum = "a15ad77d9e70a92437d8f74c35d99b4e4691128df018833e99f90bcd36152672"
|
||||
dependencies = [
|
||||
"ashpd",
|
||||
"block2 0.6.1",
|
||||
@@ -4506,7 +4506,7 @@ dependencies = [
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-futures",
|
||||
"web-sys",
|
||||
"windows-sys 0.59.0",
|
||||
"windows-sys 0.60.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6765,6 +6765,15 @@ dependencies = [
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.60.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb"
|
||||
dependencies = [
|
||||
"windows-targets 0.53.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.61.2"
|
||||
|
||||
@@ -40,7 +40,7 @@ once_cell = "1.21.3"
|
||||
rand = "0.8.5"
|
||||
rayon = "1.10.0"
|
||||
reqwest = { version = "0.12.15", features = ["blocking", "json"] }
|
||||
rfd = { version = "0.15.4", optional = true }
|
||||
rfd = { version = "0.16.0", optional = true }
|
||||
semver = "1.0.27"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
|
||||
@@ -13,6 +13,7 @@ use crate::world_editor::{WorldEditor, WorldFormat};
|
||||
use colored::Colorize;
|
||||
use indicatif::{ProgressBar, ProgressStyle};
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
|
||||
pub const MIN_Y: i32 = -64;
|
||||
|
||||
@@ -61,6 +62,7 @@ pub fn generate_world_with_options(
|
||||
options.level_name,
|
||||
options.spawn_point,
|
||||
);
|
||||
let ground = Arc::new(ground);
|
||||
|
||||
println!("{} Processing data...", "[4/7]".bold());
|
||||
|
||||
@@ -68,7 +70,7 @@ pub fn generate_world_with_options(
|
||||
let highway_connectivity = highways::build_highway_connectivity_map(&elements);
|
||||
|
||||
// Set ground reference in the editor to enable elevation-aware block placement
|
||||
editor.set_ground(&ground);
|
||||
editor.set_ground(Arc::clone(&ground));
|
||||
|
||||
println!("{} Processing terrain...", "[5/7]".bold());
|
||||
emit_gui_progress_update(25.0, "Processing terrain...");
|
||||
@@ -275,6 +277,8 @@ pub fn generate_world_with_options(
|
||||
// Save world
|
||||
editor.save();
|
||||
|
||||
emit_gui_progress_update(99.0, "Finalizing world...");
|
||||
|
||||
// Update player spawn Y coordinate based on terrain height after generation
|
||||
#[cfg(feature = "gui")]
|
||||
if world_format == WorldFormat::JavaAnvil {
|
||||
@@ -293,7 +297,7 @@ pub fn generate_world_with_options(
|
||||
Some(*spawn_coords),
|
||||
bbox_string,
|
||||
args.scale,
|
||||
&ground,
|
||||
ground.as_ref(),
|
||||
) {
|
||||
let warning_msg = format!("Failed to update spawn point Y coordinate: {}", e);
|
||||
eprintln!("Warning: {}", warning_msg);
|
||||
@@ -303,8 +307,6 @@ pub fn generate_world_with_options(
|
||||
}
|
||||
}
|
||||
|
||||
emit_gui_progress_update(99.0, "Finalizing world...");
|
||||
|
||||
// For Bedrock format, emit event to open the mcworld file
|
||||
if world_format == WorldFormat::BedrockMcWorld {
|
||||
if let Some(path_str) = output_path.to_str() {
|
||||
@@ -312,41 +314,72 @@ pub fn generate_world_with_options(
|
||||
}
|
||||
}
|
||||
|
||||
// Generate top-down map preview silently in background after completion (Java only)
|
||||
// Skip map preview for very large areas to avoid memory issues
|
||||
const MAX_MAP_PREVIEW_AREA: i64 = 6400 * 6900;
|
||||
let world_width = (xzbbox.max_x() - xzbbox.min_x()) as i64;
|
||||
let world_height = (xzbbox.max_z() - xzbbox.min_z()) as i64;
|
||||
let world_area = world_width * world_height;
|
||||
|
||||
if world_format == WorldFormat::JavaAnvil && world_area <= MAX_MAP_PREVIEW_AREA {
|
||||
let world_path = args.path.clone();
|
||||
let bounds = (
|
||||
xzbbox.min_x(),
|
||||
xzbbox.max_x(),
|
||||
xzbbox.min_z(),
|
||||
xzbbox.max_z(),
|
||||
);
|
||||
std::thread::spawn(move || {
|
||||
// Use catch_unwind to prevent any panic from affecting the application
|
||||
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
|
||||
map_renderer::render_world_map(&world_path, bounds.0, bounds.1, bounds.2, bounds.3)
|
||||
}));
|
||||
|
||||
match result {
|
||||
Ok(Ok(_path)) => {
|
||||
// Notify the GUI that the map preview is ready
|
||||
emit_map_preview_ready();
|
||||
}
|
||||
Ok(Err(e)) => {
|
||||
eprintln!("Warning: Failed to generate map preview: {}", e);
|
||||
}
|
||||
Err(_) => {
|
||||
eprintln!("Warning: Map preview generation panicked unexpectedly");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Ok(output_path)
|
||||
}
|
||||
|
||||
/// Information needed to generate a map preview after world generation is complete
|
||||
#[derive(Clone)]
|
||||
pub struct MapPreviewInfo {
|
||||
pub world_path: PathBuf,
|
||||
pub min_x: i32,
|
||||
pub max_x: i32,
|
||||
pub min_z: i32,
|
||||
pub max_z: i32,
|
||||
pub world_area: i64,
|
||||
}
|
||||
|
||||
impl MapPreviewInfo {
|
||||
/// Create MapPreviewInfo from world bounds
|
||||
pub fn new(world_path: PathBuf, xzbbox: &XZBBox) -> Self {
|
||||
let world_width = (xzbbox.max_x() - xzbbox.min_x()) as i64;
|
||||
let world_height = (xzbbox.max_z() - xzbbox.min_z()) as i64;
|
||||
Self {
|
||||
world_path,
|
||||
min_x: xzbbox.min_x(),
|
||||
max_x: xzbbox.max_x(),
|
||||
min_z: xzbbox.min_z(),
|
||||
max_z: xzbbox.max_z(),
|
||||
world_area: world_width * world_height,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Maximum area for which map preview generation is allowed (to avoid memory issues)
|
||||
pub const MAX_MAP_PREVIEW_AREA: i64 = 6400 * 6900;
|
||||
|
||||
/// Start map preview generation in a background thread.
|
||||
/// This should be called AFTER the world generation is complete, the session lock is released,
|
||||
/// and the GUI has been notified of 100% completion.
|
||||
///
|
||||
/// For Java worlds only, and only if the world area is within limits.
|
||||
pub fn start_map_preview_generation(info: MapPreviewInfo) {
|
||||
if info.world_area > MAX_MAP_PREVIEW_AREA {
|
||||
return;
|
||||
}
|
||||
|
||||
std::thread::spawn(move || {
|
||||
// Use catch_unwind to prevent any panic from affecting the application
|
||||
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
|
||||
map_renderer::render_world_map(
|
||||
&info.world_path,
|
||||
info.min_x,
|
||||
info.max_x,
|
||||
info.min_z,
|
||||
info.max_z,
|
||||
)
|
||||
}));
|
||||
|
||||
match result {
|
||||
Ok(Ok(_path)) => {
|
||||
// Notify the GUI that the map preview is ready
|
||||
emit_map_preview_ready();
|
||||
}
|
||||
Ok(Err(e)) => {
|
||||
eprintln!("Warning: Failed to generate map preview: {}", e);
|
||||
}
|
||||
Err(_) => {
|
||||
eprintln!("Warning: Map preview generation panicked unexpectedly");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2,7 +2,8 @@ use crate::coordinate_system::{geographic::LLBBox, transformation::geo_distance}
|
||||
#[cfg(feature = "gui")]
|
||||
use crate::telemetry::{send_log, LogLevel};
|
||||
use image::Rgb;
|
||||
use std::path::Path;
|
||||
use rayon::prelude::*;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
/// Maximum Y coordinate in Minecraft (build height limit)
|
||||
const MAX_Y: i32 = 319;
|
||||
@@ -17,6 +18,8 @@ const TERRARIUM_OFFSET: f64 = 32768.0;
|
||||
const MIN_ZOOM: u8 = 10;
|
||||
/// Maximum zoom level for terrain tiles
|
||||
const MAX_ZOOM: u8 = 15;
|
||||
/// Maximum concurrent tile downloads to be respectful to AWS
|
||||
const MAX_CONCURRENT_DOWNLOADS: usize = 8;
|
||||
|
||||
/// Holds processed elevation data and metadata
|
||||
#[derive(Clone)]
|
||||
@@ -29,6 +32,11 @@ pub struct ElevationData {
|
||||
pub(crate) height: usize,
|
||||
}
|
||||
|
||||
/// RGB image buffer type for elevation tiles
|
||||
type TileImage = image::ImageBuffer<Rgb<u8>, Vec<u8>>;
|
||||
/// Result type for tile download operations: ((tile_x, tile_y), image) or error
|
||||
type TileDownloadResult = Result<((u32, u32), TileImage), String>;
|
||||
|
||||
/// Calculates appropriate zoom level for the given bounding box
|
||||
fn calculate_zoom_level(bbox: &LLBBox) -> u8 {
|
||||
let lat_diff: f64 = (bbox.max().lat() - bbox.min().lat()).abs();
|
||||
@@ -53,21 +61,103 @@ fn download_tile(
|
||||
tile_y: u32,
|
||||
zoom: u8,
|
||||
tile_path: &Path,
|
||||
) -> Result<image::ImageBuffer<Rgb<u8>, Vec<u8>>, Box<dyn std::error::Error>> {
|
||||
) -> Result<image::ImageBuffer<Rgb<u8>, Vec<u8>>, String> {
|
||||
println!("Fetching tile x={tile_x},y={tile_y},z={zoom} from AWS Terrain Tiles");
|
||||
let url: String = AWS_TERRARIUM_URL
|
||||
.replace("{z}", &zoom.to_string())
|
||||
.replace("{x}", &tile_x.to_string())
|
||||
.replace("{y}", &tile_y.to_string());
|
||||
|
||||
let response: reqwest::blocking::Response = client.get(&url).send()?;
|
||||
response.error_for_status_ref()?;
|
||||
let bytes = response.bytes()?;
|
||||
std::fs::write(tile_path, &bytes)?;
|
||||
let img: image::DynamicImage = image::load_from_memory(&bytes)?;
|
||||
let response = client.get(&url).send().map_err(|e| e.to_string())?;
|
||||
response.error_for_status_ref().map_err(|e| e.to_string())?;
|
||||
let bytes = response.bytes().map_err(|e| e.to_string())?;
|
||||
std::fs::write(tile_path, &bytes).map_err(|e| e.to_string())?;
|
||||
let img = image::load_from_memory(&bytes).map_err(|e| e.to_string())?;
|
||||
Ok(img.to_rgb8())
|
||||
}
|
||||
|
||||
/// Fetches a tile from cache or downloads it if not available
|
||||
/// Note: In parallel execution, multiple threads may attempt to download the same tile
|
||||
/// if it's missing or corrupted. This is harmless (just wastes some bandwidth) as
|
||||
/// file writes are atomic at the OS level.
|
||||
fn fetch_or_load_tile(
|
||||
client: &reqwest::blocking::Client,
|
||||
tile_x: u32,
|
||||
tile_y: u32,
|
||||
zoom: u8,
|
||||
tile_path: &Path,
|
||||
) -> Result<image::ImageBuffer<Rgb<u8>, Vec<u8>>, String> {
|
||||
if tile_path.exists() {
|
||||
// Check if the cached file has a reasonable size (PNG files should be at least a few KB)
|
||||
let file_size = std::fs::metadata(tile_path).map(|m| m.len()).unwrap_or(0);
|
||||
|
||||
if file_size < 1000 {
|
||||
eprintln!(
|
||||
"Warning: Cached tile at {} appears to be too small ({} bytes). Refetching tile.",
|
||||
tile_path.display(),
|
||||
file_size
|
||||
);
|
||||
#[cfg(feature = "gui")]
|
||||
send_log(
|
||||
LogLevel::Warning,
|
||||
"Cached tile appears too small, refetching.",
|
||||
);
|
||||
|
||||
// Remove the potentially corrupted file
|
||||
if let Err(e) = std::fs::remove_file(tile_path) {
|
||||
eprintln!("Warning: Failed to remove corrupted tile file: {e}");
|
||||
#[cfg(feature = "gui")]
|
||||
send_log(
|
||||
LogLevel::Warning,
|
||||
"Failed to remove corrupted tile file during refetching.",
|
||||
);
|
||||
}
|
||||
|
||||
// Re-download the tile
|
||||
return download_tile(client, tile_x, tile_y, zoom, tile_path);
|
||||
}
|
||||
|
||||
// Try to load cached tile, but handle corruption gracefully
|
||||
match image::open(tile_path) {
|
||||
Ok(img) => {
|
||||
println!(
|
||||
"Loading cached tile x={tile_x},y={tile_y},z={zoom} from {}",
|
||||
tile_path.display()
|
||||
);
|
||||
Ok(img.to_rgb8())
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!(
|
||||
"Cached tile at {} is corrupted or invalid: {}. Re-downloading...",
|
||||
tile_path.display(),
|
||||
e
|
||||
);
|
||||
#[cfg(feature = "gui")]
|
||||
send_log(
|
||||
LogLevel::Warning,
|
||||
"Cached tile is corrupted or invalid. Re-downloading...",
|
||||
);
|
||||
|
||||
// Remove the corrupted file
|
||||
if let Err(e) = std::fs::remove_file(tile_path) {
|
||||
eprintln!("Warning: Failed to remove corrupted tile file: {e}");
|
||||
#[cfg(feature = "gui")]
|
||||
send_log(
|
||||
LogLevel::Warning,
|
||||
"Failed to remove corrupted tile file during re-download.",
|
||||
);
|
||||
}
|
||||
|
||||
// Re-download the tile
|
||||
download_tile(client, tile_x, tile_y, zoom, tile_path)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Download the tile for the first time
|
||||
download_tile(client, tile_x, tile_y, zoom, tile_path)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn fetch_elevation_data(
|
||||
bbox: &LLBBox,
|
||||
scale: f64,
|
||||
@@ -91,101 +181,68 @@ pub fn fetch_elevation_data(
|
||||
let mut height_grid: Vec<Vec<f64>> = vec![vec![f64::NAN; grid_width]; grid_height];
|
||||
let mut extreme_values_found = Vec::new(); // Track extreme values for debugging
|
||||
|
||||
let client: reqwest::blocking::Client = reqwest::blocking::Client::new();
|
||||
|
||||
let tile_cache_dir = Path::new("./arnis-tile-cache");
|
||||
let tile_cache_dir = PathBuf::from("./arnis-tile-cache");
|
||||
if !tile_cache_dir.exists() {
|
||||
std::fs::create_dir_all(tile_cache_dir)?;
|
||||
std::fs::create_dir_all(&tile_cache_dir)?;
|
||||
}
|
||||
|
||||
// Fetch and process each tile
|
||||
for (tile_x, tile_y) in &tiles {
|
||||
// Check if tile is already cached
|
||||
let tile_path = tile_cache_dir.join(format!("z{zoom}_x{tile_x}_y{tile_y}.png"));
|
||||
// Create a shared HTTP client for connection pooling
|
||||
let client = reqwest::blocking::Client::new();
|
||||
|
||||
let rgb_img: image::ImageBuffer<Rgb<u8>, Vec<u8>> = if tile_path.exists() {
|
||||
// Check if the cached file has a reasonable size (PNG files should be at least a few KB)
|
||||
let file_size = match std::fs::metadata(&tile_path) {
|
||||
Ok(metadata) => metadata.len(),
|
||||
Err(_) => 0,
|
||||
};
|
||||
// Download tiles in parallel with limited concurrency to be respectful to AWS
|
||||
let num_tiles = tiles.len();
|
||||
println!(
|
||||
"Downloading {num_tiles} elevation tiles (up to {MAX_CONCURRENT_DOWNLOADS} concurrent)..."
|
||||
);
|
||||
|
||||
if file_size < 1000 {
|
||||
eprintln!(
|
||||
"Warning: Cached tile at {} appears to be too small ({} bytes). Refetching tile.",
|
||||
tile_path.display(),
|
||||
file_size
|
||||
// Use a custom thread pool to limit concurrent downloads
|
||||
let thread_pool = rayon::ThreadPoolBuilder::new()
|
||||
.num_threads(MAX_CONCURRENT_DOWNLOADS)
|
||||
.build()
|
||||
.map_err(|e| format!("Failed to create thread pool: {e}"))?;
|
||||
|
||||
let downloaded_tiles: Vec<TileDownloadResult> = thread_pool.install(|| {
|
||||
tiles
|
||||
.par_iter()
|
||||
.map(|(tile_x, tile_y)| {
|
||||
let tile_path = tile_cache_dir.join(format!("z{zoom}_x{tile_x}_y{tile_y}.png"));
|
||||
|
||||
let rgb_img = fetch_or_load_tile(&client, *tile_x, *tile_y, zoom, &tile_path)?;
|
||||
Ok(((*tile_x, *tile_y), rgb_img))
|
||||
})
|
||||
.collect()
|
||||
});
|
||||
|
||||
// Check for any download errors
|
||||
let mut successful_tiles = Vec::new();
|
||||
for result in downloaded_tiles {
|
||||
match result {
|
||||
Ok(tile_data) => successful_tiles.push(tile_data),
|
||||
Err(e) => {
|
||||
eprintln!("Warning: Failed to download tile: {e}");
|
||||
#[cfg(feature = "gui")]
|
||||
send_log(
|
||||
LogLevel::Warning,
|
||||
&format!("Failed to download elevation tile: {e}"),
|
||||
);
|
||||
|
||||
// Remove the potentially corrupted file
|
||||
if let Err(remove_err) = std::fs::remove_file(&tile_path) {
|
||||
eprintln!(
|
||||
"Warning: Failed to remove corrupted tile file: {}",
|
||||
remove_err
|
||||
);
|
||||
#[cfg(feature = "gui")]
|
||||
send_log(
|
||||
LogLevel::Warning,
|
||||
"Failed to remove corrupted tile file during refetching.",
|
||||
);
|
||||
}
|
||||
|
||||
// Re-download the tile
|
||||
download_tile(&client, *tile_x, *tile_y, zoom, &tile_path)?
|
||||
} else {
|
||||
println!(
|
||||
"Loading cached tile x={tile_x},y={tile_y},z={zoom} from {}",
|
||||
tile_path.display()
|
||||
);
|
||||
|
||||
// Try to load cached tile, but handle corruption gracefully
|
||||
match image::open(&tile_path) {
|
||||
Ok(img) => img.to_rgb8(),
|
||||
Err(e) => {
|
||||
eprintln!(
|
||||
"Cached tile at {} is corrupted or invalid: {}. Re-downloading...",
|
||||
tile_path.display(),
|
||||
e
|
||||
);
|
||||
#[cfg(feature = "gui")]
|
||||
send_log(
|
||||
LogLevel::Warning,
|
||||
"Cached tile is corrupted or invalid. Re-downloading...",
|
||||
);
|
||||
|
||||
// Remove the corrupted file
|
||||
if let Err(remove_err) = std::fs::remove_file(&tile_path) {
|
||||
eprintln!(
|
||||
"Warning: Failed to remove corrupted tile file: {}",
|
||||
remove_err
|
||||
);
|
||||
#[cfg(feature = "gui")]
|
||||
send_log(
|
||||
LogLevel::Warning,
|
||||
"Failed to remove corrupted tile file during re-download.",
|
||||
);
|
||||
}
|
||||
|
||||
// Re-download the tile
|
||||
download_tile(&client, *tile_x, *tile_y, zoom, &tile_path)?
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Download the tile for the first time
|
||||
download_tile(&client, *tile_x, *tile_y, zoom, &tile_path)?
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
println!("Processing {} elevation tiles...", successful_tiles.len());
|
||||
|
||||
// Process tiles sequentially (writes to shared height_grid)
|
||||
for ((tile_x, tile_y), rgb_img) in successful_tiles {
|
||||
// Only process pixels that fall within the requested bbox
|
||||
for (y, row) in rgb_img.rows().enumerate() {
|
||||
for (x, pixel) in row.enumerate() {
|
||||
// Convert tile pixel coordinates back to geographic coordinates
|
||||
let pixel_lng = ((*tile_x as f64 + x as f64 / 256.0) / (2.0_f64.powi(zoom as i32)))
|
||||
let pixel_lng = ((tile_x as f64 + x as f64 / 256.0) / (2.0_f64.powi(zoom as i32)))
|
||||
* 360.0
|
||||
- 180.0;
|
||||
let pixel_lat_rad = std::f64::consts::PI
|
||||
* (1.0
|
||||
- 2.0 * (*tile_y as f64 + y as f64 / 256.0) / (2.0_f64.powi(zoom as i32)));
|
||||
- 2.0 * (tile_y as f64 + y as f64 / 256.0) / (2.0_f64.powi(zoom as i32)));
|
||||
let pixel_lat = pixel_lat_rad.sinh().atan().to_degrees();
|
||||
|
||||
// Skip pixels outside the requested bounding box
|
||||
@@ -377,55 +434,59 @@ fn get_tile_coordinates(bbox: &LLBBox, zoom: u8) -> Vec<(u32, u32)> {
|
||||
}
|
||||
|
||||
fn apply_gaussian_blur(heights: &[Vec<f64>], sigma: f64) -> Vec<Vec<f64>> {
|
||||
let height: usize = heights.len();
|
||||
let width: usize = heights.first().map(|row| row.len()).unwrap_or(0);
|
||||
if height == 0 || width == 0 {
|
||||
return Vec::new();
|
||||
}
|
||||
|
||||
let kernel_size: usize = (sigma * 3.0).ceil() as usize * 2 + 1;
|
||||
let kernel: Vec<f64> = create_gaussian_kernel(kernel_size, sigma);
|
||||
let half_kernel: i32 = kernel_size as i32 / 2;
|
||||
|
||||
// Apply blur
|
||||
let mut blurred: Vec<Vec<f64>> = heights.to_owned();
|
||||
let mut flat: Vec<f64> = Vec::with_capacity(width * height);
|
||||
for row in heights {
|
||||
flat.extend_from_slice(row);
|
||||
}
|
||||
|
||||
// Horizontal pass
|
||||
for row in blurred.iter_mut() {
|
||||
let mut temp: Vec<f64> = row.clone();
|
||||
for (i, val) in temp.iter_mut().enumerate() {
|
||||
let mut scratch: Vec<f64> = vec![0.0; width * height];
|
||||
|
||||
// Horizontal pass: flat -> scratch
|
||||
for y in 0..height {
|
||||
let row_offset = y * width;
|
||||
for x in 0..width {
|
||||
let mut sum: f64 = 0.0;
|
||||
let mut weight_sum: f64 = 0.0;
|
||||
for (j, k) in kernel.iter().enumerate() {
|
||||
let idx: i32 = i as i32 + j as i32 - kernel_size as i32 / 2;
|
||||
if idx >= 0 && idx < row.len() as i32 {
|
||||
sum += row[idx as usize] * k;
|
||||
let idx_x = x as i32 + j as i32 - half_kernel;
|
||||
if (0..width as i32).contains(&idx_x) {
|
||||
let idx = row_offset + idx_x as usize;
|
||||
sum += flat[idx] * k;
|
||||
weight_sum += k;
|
||||
}
|
||||
}
|
||||
*val = sum / weight_sum;
|
||||
scratch[row_offset + x] = sum / weight_sum;
|
||||
}
|
||||
*row = temp;
|
||||
}
|
||||
|
||||
// Vertical pass
|
||||
let height: usize = blurred.len();
|
||||
let width: usize = blurred[0].len();
|
||||
for x in 0..width {
|
||||
let temp: Vec<_> = blurred
|
||||
.iter()
|
||||
.take(height)
|
||||
.map(|row: &Vec<f64>| row[x])
|
||||
.collect();
|
||||
|
||||
for (y, row) in blurred.iter_mut().enumerate().take(height) {
|
||||
// Vertical pass: scratch -> flat
|
||||
for y in 0..height {
|
||||
for x in 0..width {
|
||||
let mut sum: f64 = 0.0;
|
||||
let mut weight_sum: f64 = 0.0;
|
||||
for (j, k) in kernel.iter().enumerate() {
|
||||
let idx: i32 = y as i32 + j as i32 - kernel_size as i32 / 2;
|
||||
if idx >= 0 && idx < height as i32 {
|
||||
sum += temp[idx as usize] * k;
|
||||
let idx_y = y as i32 + j as i32 - half_kernel;
|
||||
if (0..height as i32).contains(&idx_y) {
|
||||
let idx = idx_y as usize * width + x;
|
||||
sum += scratch[idx] * k;
|
||||
weight_sum += k;
|
||||
}
|
||||
}
|
||||
row[x] = sum / weight_sum;
|
||||
flat[y * width + x] = sum / weight_sum;
|
||||
}
|
||||
}
|
||||
|
||||
blurred
|
||||
flat.chunks(width).map(|row| row.to_vec()).collect()
|
||||
}
|
||||
|
||||
fn create_gaussian_kernel(size: usize, sigma: f64) -> Vec<f64> {
|
||||
|
||||
79
src/gui.rs
79
src/gui.rs
@@ -848,16 +848,52 @@ fn gui_start_generation(
|
||||
|
||||
tauri::async_runtime::spawn(async move {
|
||||
if let Err(e) = tokio::task::spawn_blocking(move || {
|
||||
// Acquire session lock for the world directory before starting generation
|
||||
let world_path = PathBuf::from(&selected_world);
|
||||
let _session_lock = match SessionLock::acquire(&world_path) {
|
||||
Ok(lock) => lock,
|
||||
Err(e) => {
|
||||
let error_msg = format!("Failed to acquire session lock: {e}");
|
||||
|
||||
// Determine world format from UI selection first (needed for session lock decision)
|
||||
let world_format = if world_format == "bedrock" {
|
||||
WorldFormat::BedrockMcWorld
|
||||
} else {
|
||||
WorldFormat::JavaAnvil
|
||||
};
|
||||
|
||||
// Check available disk space before starting generation (minimum 3GB required)
|
||||
const MIN_DISK_SPACE_BYTES: u64 = 3 * 1024 * 1024 * 1024; // 3 GB
|
||||
let check_path = if world_format == WorldFormat::JavaAnvil {
|
||||
world_path.clone()
|
||||
} else {
|
||||
// For Bedrock, check current directory where .mcworld will be created
|
||||
std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."))
|
||||
};
|
||||
match fs2::available_space(&check_path) {
|
||||
Ok(available) if available < MIN_DISK_SPACE_BYTES => {
|
||||
let error_msg = "Not enough disk space available.".to_string();
|
||||
eprintln!("{error_msg}");
|
||||
emit_gui_error(&error_msg);
|
||||
return Err(error_msg);
|
||||
}
|
||||
Err(e) => {
|
||||
// Log warning but don't block generation if we can't check space
|
||||
eprintln!("Warning: Could not check disk space: {e}");
|
||||
}
|
||||
_ => {} // Sufficient space available
|
||||
}
|
||||
|
||||
// Acquire session lock for Java worlds only
|
||||
// Session lock prevents Minecraft from having the world open during generation
|
||||
// Bedrock worlds are generated as .mcworld files and don't need this lock
|
||||
let _session_lock: Option<SessionLock> = if world_format == WorldFormat::JavaAnvil {
|
||||
match SessionLock::acquire(&world_path) {
|
||||
Ok(lock) => Some(lock),
|
||||
Err(e) => {
|
||||
let error_msg = format!("Failed to acquire session lock: {e}");
|
||||
eprintln!("{error_msg}");
|
||||
emit_gui_error(&error_msg);
|
||||
return Err(error_msg);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// Parse the bounding box from the text with proper error handling
|
||||
@@ -871,13 +907,6 @@ fn gui_start_generation(
|
||||
}
|
||||
};
|
||||
|
||||
// Determine world format from UI selection
|
||||
let world_format = if world_format == "bedrock" {
|
||||
WorldFormat::BedrockMcWorld
|
||||
} else {
|
||||
WorldFormat::JavaAnvil
|
||||
};
|
||||
|
||||
// Determine output path and level name based on format
|
||||
let (generation_path, level_name) = match world_format {
|
||||
WorldFormat::JavaAnvil => {
|
||||
@@ -963,17 +992,27 @@ fn gui_start_generation(
|
||||
|
||||
let _ = data_processing::generate_world_with_options(
|
||||
parsed_elements,
|
||||
xzbbox,
|
||||
xzbbox.clone(),
|
||||
args.bbox,
|
||||
ground,
|
||||
&args,
|
||||
generation_options,
|
||||
generation_options.clone(),
|
||||
);
|
||||
// Explicitly release session lock before showing Done message
|
||||
// so Minecraft can open the world immediately
|
||||
drop(_session_lock);
|
||||
emit_gui_progress_update(100.0, "Done! World generation completed.");
|
||||
println!("{}", "Done! World generation completed.".green().bold());
|
||||
|
||||
// Start map preview generation silently in background (Java only)
|
||||
if world_format == WorldFormat::JavaAnvil {
|
||||
let preview_info = data_processing::MapPreviewInfo::new(
|
||||
generation_options.path.clone(),
|
||||
&xzbbox,
|
||||
);
|
||||
data_processing::start_map_preview_generation(preview_info);
|
||||
}
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
@@ -1006,7 +1045,7 @@ fn gui_start_generation(
|
||||
|
||||
let _ = data_processing::generate_world_with_options(
|
||||
parsed_elements,
|
||||
xzbbox,
|
||||
xzbbox.clone(),
|
||||
args.bbox,
|
||||
ground,
|
||||
&args,
|
||||
@@ -1017,6 +1056,16 @@ fn gui_start_generation(
|
||||
drop(_session_lock);
|
||||
emit_gui_progress_update(100.0, "Done! World generation completed.");
|
||||
println!("{}", "Done! World generation completed.".green().bold());
|
||||
|
||||
// Start map preview generation silently in background (Java only)
|
||||
if world_format == WorldFormat::JavaAnvil {
|
||||
let preview_info = data_processing::MapPreviewInfo::new(
|
||||
generation_options.path.clone(),
|
||||
&xzbbox,
|
||||
);
|
||||
data_processing::start_map_preview_generation(preview_info);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Err(e) => {
|
||||
|
||||
9
src/gui/js/bbox.js
vendored
9
src/gui/js/bbox.js
vendored
@@ -899,6 +899,15 @@ $(document).ready(function () {
|
||||
});
|
||||
}
|
||||
|
||||
// If it's a rectangle, remove any existing rectangles first
|
||||
if (e.layerType === 'rectangle') {
|
||||
drawnItems.eachLayer(function(layer) {
|
||||
if (layer instanceof L.Rectangle) {
|
||||
drawnItems.removeLayer(layer);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Check if it's a rectangle and set proper styles before adding it to the layer
|
||||
if (e.layerType === 'rectangle') {
|
||||
e.layer.setStyle({
|
||||
|
||||
@@ -24,6 +24,7 @@ use std::collections::HashMap as StdHashMap;
|
||||
use std::fs::{self, File};
|
||||
use std::io::{Cursor, Write as IoWrite};
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use vek::Vec2;
|
||||
use zip::write::FileOptions;
|
||||
use zip::CompressionMethod;
|
||||
@@ -122,7 +123,7 @@ pub struct BedrockWriter {
|
||||
output_dir: PathBuf,
|
||||
level_name: String,
|
||||
spawn_point: Option<(i32, i32)>,
|
||||
ground: Option<Box<Ground>>,
|
||||
ground: Option<Arc<Ground>>,
|
||||
}
|
||||
|
||||
impl BedrockWriter {
|
||||
@@ -131,7 +132,7 @@ impl BedrockWriter {
|
||||
output_path: PathBuf,
|
||||
level_name: String,
|
||||
spawn_point: Option<(i32, i32)>,
|
||||
ground: Option<Box<Ground>>,
|
||||
ground: Option<Arc<Ground>>,
|
||||
) -> Self {
|
||||
// If the path ends with .mcworld, use it as the final archive path
|
||||
// and create a temp directory without that extension for working files
|
||||
|
||||
@@ -4,6 +4,11 @@
|
||||
//! before they are written to either Java or Bedrock format.
|
||||
|
||||
use crate::block_definitions::*;
|
||||
|
||||
/// Minimum Y coordinate in Minecraft (1.18+)
|
||||
const MIN_Y: i32 = -64;
|
||||
/// Maximum Y coordinate in Minecraft (1.18+)
|
||||
const MAX_Y: i32 = 319;
|
||||
use fastnbt::{LongArray, Value};
|
||||
use fnv::FnvHashMap;
|
||||
use serde::{Deserialize, Serialize};
|
||||
@@ -186,16 +191,20 @@ pub(crate) struct ChunkToModify {
|
||||
impl ChunkToModify {
|
||||
#[inline]
|
||||
pub fn get_block(&self, x: u8, y: i32, z: u8) -> Option<Block> {
|
||||
let section_idx: i8 = (y >> 4).try_into().unwrap();
|
||||
// Clamp Y to valid Minecraft range to prevent TryFromIntError
|
||||
let y = y.clamp(MIN_Y, MAX_Y);
|
||||
let section_idx: i8 = (y >> 4) as i8;
|
||||
let section = self.sections.get(§ion_idx)?;
|
||||
section.get_block(x, (y & 15).try_into().unwrap(), z)
|
||||
section.get_block(x, (y & 15) as u8, z)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn set_block(&mut self, x: u8, y: i32, z: u8, block: Block) {
|
||||
let section_idx: i8 = (y >> 4).try_into().unwrap();
|
||||
// Clamp Y to valid Minecraft range to prevent TryFromIntError
|
||||
let y = y.clamp(MIN_Y, MAX_Y);
|
||||
let section_idx: i8 = (y >> 4) as i8;
|
||||
let section = self.sections.entry(section_idx).or_default();
|
||||
section.set_block(x, (y & 15).try_into().unwrap(), z, block);
|
||||
section.set_block(x, (y & 15) as u8, z, block);
|
||||
}
|
||||
|
||||
#[inline]
|
||||
@@ -206,9 +215,11 @@ impl ChunkToModify {
|
||||
z: u8,
|
||||
block_with_props: BlockWithProperties,
|
||||
) {
|
||||
let section_idx: i8 = (y >> 4).try_into().unwrap();
|
||||
// Clamp Y to valid Minecraft range to prevent TryFromIntError
|
||||
let y = y.clamp(MIN_Y, MAX_Y);
|
||||
let section_idx: i8 = (y >> 4) as i8;
|
||||
let section = self.sections.entry(section_idx).or_default();
|
||||
section.set_block_with_properties(x, (y & 15).try_into().unwrap(), z, block_with_props);
|
||||
section.set_block_with_properties(x, (y & 15) as u8, z, block_with_props);
|
||||
}
|
||||
|
||||
pub fn sections(&self) -> impl Iterator<Item = Section> + '_ {
|
||||
|
||||
@@ -23,9 +23,11 @@ use crate::telemetry::{send_log, LogLevel};
|
||||
impl<'a> WorldEditor<'a> {
|
||||
/// Creates a region file for the given region coordinates.
|
||||
pub(super) fn create_region(&self, region_x: i32, region_z: i32) -> Region<File> {
|
||||
let out_path = self
|
||||
.world_dir
|
||||
.join(format!("region/r.{}.{}.mca", region_x, region_z));
|
||||
let region_dir = self.world_dir.join("region");
|
||||
let out_path = region_dir.join(format!("r.{}.{}.mca", region_x, region_z));
|
||||
|
||||
// Ensure region directory exists before creating region files
|
||||
std::fs::create_dir_all(®ion_dir).expect("Failed to create region directory");
|
||||
|
||||
const REGION_TEMPLATE: &[u8] = include_bytes!("../../assets/minecraft/region.template");
|
||||
|
||||
|
||||
@@ -33,6 +33,7 @@ use std::collections::HashMap;
|
||||
use std::fs::File;
|
||||
use std::io::Write;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
|
||||
#[cfg(feature = "gui")]
|
||||
use crate::telemetry::{send_log, LogLevel};
|
||||
@@ -71,7 +72,7 @@ pub struct WorldEditor<'a> {
|
||||
world: WorldToModify,
|
||||
xzbbox: &'a XZBBox,
|
||||
llbbox: LLBBox,
|
||||
ground: Option<Box<Ground>>,
|
||||
ground: Option<Arc<Ground>>,
|
||||
format: WorldFormat,
|
||||
/// Optional level name for Bedrock worlds (e.g., "Arnis World: New York City")
|
||||
bedrock_level_name: Option<String>,
|
||||
@@ -122,13 +123,13 @@ impl<'a> WorldEditor<'a> {
|
||||
}
|
||||
|
||||
/// Sets the ground reference for elevation-based block placement
|
||||
pub fn set_ground(&mut self, ground: &Ground) {
|
||||
self.ground = Some(Box::new(ground.clone()));
|
||||
pub fn set_ground(&mut self, ground: Arc<Ground>) {
|
||||
self.ground = Some(ground);
|
||||
}
|
||||
|
||||
/// Gets a reference to the ground data if available
|
||||
pub fn get_ground(&self) -> Option<&Ground> {
|
||||
self.ground.as_ref().map(|g| g.as_ref())
|
||||
self.ground.as_deref()
|
||||
}
|
||||
|
||||
/// Returns the current world format
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
"minWidth": 1000,
|
||||
"minHeight": 650,
|
||||
"resizable": true,
|
||||
"transparent": true,
|
||||
"transparent": false,
|
||||
"center": true,
|
||||
"theme": "Dark",
|
||||
"additionalBrowserArgs": "--disable-features=VizDisplayCompositor"
|
||||
|
||||
Reference in New Issue
Block a user