mirror of
https://github.com/louis-e/arnis.git
synced 2026-01-14 09:07:48 -05:00
Compare commits
22 Commits
v2.3.1
...
copilot/fi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1bcec3dcc0 | ||
|
|
56ddea57d0 | ||
|
|
430a4970f5 | ||
|
|
74fbdabaee | ||
|
|
2643155e9a | ||
|
|
d45c360074 | ||
|
|
e86af9e006 | ||
|
|
7575141837 | ||
|
|
eb288e9a03 | ||
|
|
6277a14d22 | ||
|
|
c355f243e3 | ||
|
|
2c31d2659c | ||
|
|
996e06ab2c | ||
|
|
e11231ad0f | ||
|
|
9adf31121e | ||
|
|
69da18fbfb | ||
|
|
5976cc2868 | ||
|
|
a85eaed835 | ||
|
|
37c3d85672 | ||
|
|
15b698a1eb | ||
|
|
8fff2d2fb5 | ||
|
|
8c702a36ff |
@@ -43,7 +43,7 @@ jobs:
|
||||
- name: Run benchmark command with memory tracking
|
||||
id: benchmark
|
||||
run: |
|
||||
/usr/bin/time -v ./target/release/arnis --path="./world" --terrain --bbox="48.101470,11.517792,48.168375,11.626968" 2> benchmark_log.txt
|
||||
/usr/bin/time -v ./target/release/arnis --path="./world" --terrain --bbox="48.125768 11.552296 48.148565 11.593838" 2> benchmark_log.txt
|
||||
grep "Maximum resident set size" benchmark_log.txt | awk '{print $6}' > peak_mem_kb.txt
|
||||
peak_kb=$(cat peak_mem_kb.txt)
|
||||
peak_mb=$((peak_kb / 1024))
|
||||
125
Cargo.lock
generated
125
Cargo.lock
generated
@@ -2,15 +2,6 @@
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "addr2line"
|
||||
version = "0.24.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1"
|
||||
dependencies = [
|
||||
"gimli",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "adler2"
|
||||
version = "2.0.0"
|
||||
@@ -463,21 +454,6 @@ dependencies = [
|
||||
"arrayvec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "backtrace"
|
||||
version = "0.3.74"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a"
|
||||
dependencies = [
|
||||
"addr2line",
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"miniz_oxide",
|
||||
"object",
|
||||
"rustc-demangle",
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "base64"
|
||||
version = "0.21.7"
|
||||
@@ -1804,9 +1780,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "geo"
|
||||
version = "0.30.0"
|
||||
version = "0.31.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4416397671d8997e9a3e7ad99714f4f00a22e9eaa9b966a5985d2194fc9e02e1"
|
||||
checksum = "2fc1a1678e54befc9b4bcab6cd43b8e7f834ae8ea121118b0fd8c42747675b4a"
|
||||
dependencies = [
|
||||
"earcutr",
|
||||
"float_next_after",
|
||||
@@ -1886,12 +1862,6 @@ dependencies = [
|
||||
"weezl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gimli"
|
||||
version = "0.31.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f"
|
||||
|
||||
[[package]]
|
||||
name = "gio"
|
||||
version = "0.18.4"
|
||||
@@ -2263,24 +2233,24 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "i_float"
|
||||
version = "1.7.0"
|
||||
version = "1.15.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "85df3a416829bb955fdc2416c7b73680c8dcea8d731f2c7aa23e1042fe1b8343"
|
||||
checksum = "010025c2c532c8d82e42d0b8bb5184afa449fa6f06c709ea9adcb16c49ae405b"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"libm",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "i_key_sort"
|
||||
version = "0.2.0"
|
||||
version = "0.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "347c253b4748a1a28baf94c9ce133b6b166f08573157e05afe718812bc599fcd"
|
||||
checksum = "9190f86706ca38ac8add223b2aed8b1330002b5cdbbce28fb58b10914d38fc27"
|
||||
|
||||
[[package]]
|
||||
name = "i_overlay"
|
||||
version = "2.0.5"
|
||||
version = "4.0.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0542dfef184afdd42174a03dcc0625b6147fb73e1b974b1a08a2a42ac35cee49"
|
||||
checksum = "0fcccbd4e4274e0f80697f5fbc6540fdac533cce02f2081b328e68629cce24f9"
|
||||
dependencies = [
|
||||
"i_float",
|
||||
"i_key_sort",
|
||||
@@ -2291,19 +2261,18 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "i_shape"
|
||||
version = "1.7.0"
|
||||
version = "1.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0a38f5a42678726718ff924f6d4a0e79b129776aeed298f71de4ceedbd091bce"
|
||||
checksum = "1ea154b742f7d43dae2897fcd5ead86bc7b5eefcedd305a7ebf9f69d44d61082"
|
||||
dependencies = [
|
||||
"i_float",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "i_tree"
|
||||
version = "0.8.3"
|
||||
version = "0.16.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "155181bc97d770181cf9477da51218a19ee92a8e5be642e796661aee2b601139"
|
||||
checksum = "35e6d558e6d4c7b82bc51d9c771e7a927862a161a7d87bf2b0541450e0e20915"
|
||||
|
||||
[[package]]
|
||||
name = "iana-time-zone"
|
||||
@@ -2591,17 +2560,6 @@ dependencies = [
|
||||
"syn 2.0.95",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "io-uring"
|
||||
version = "0.7.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d93587f37623a1a17d94ef2bc9ada592f5465fe7732084ab7beefabe5c77c0c4"
|
||||
dependencies = [
|
||||
"bitflags 2.6.0",
|
||||
"cfg-if",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ipnet"
|
||||
version = "2.10.1"
|
||||
@@ -3499,15 +3457,6 @@ dependencies = [
|
||||
"objc2-security",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "object"
|
||||
version = "0.36.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.21.3"
|
||||
@@ -4462,12 +4411,6 @@ dependencies = [
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustc-demangle"
|
||||
version = "0.1.24"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f"
|
||||
|
||||
[[package]]
|
||||
name = "rustc_version"
|
||||
version = "0.4.1"
|
||||
@@ -5612,29 +5555,26 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
|
||||
|
||||
[[package]]
|
||||
name = "tokio"
|
||||
version = "1.47.0"
|
||||
version = "1.48.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "43864ed400b6043a4757a25c7a64a8efde741aed79a056a2fb348a406701bb35"
|
||||
checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408"
|
||||
dependencies = [
|
||||
"backtrace",
|
||||
"bytes",
|
||||
"io-uring",
|
||||
"libc",
|
||||
"mio",
|
||||
"parking_lot",
|
||||
"pin-project-lite",
|
||||
"signal-hook-registry",
|
||||
"slab",
|
||||
"socket2 0.6.0",
|
||||
"tokio-macros",
|
||||
"windows-sys 0.59.0",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-macros"
|
||||
version = "2.5.0"
|
||||
version = "2.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8"
|
||||
checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -6423,7 +6363,7 @@ dependencies = [
|
||||
"windows-collections",
|
||||
"windows-core 0.61.0",
|
||||
"windows-future",
|
||||
"windows-link",
|
||||
"windows-link 0.1.1",
|
||||
"windows-numerics",
|
||||
]
|
||||
|
||||
@@ -6453,7 +6393,7 @@ checksum = "4763c1de310c86d75a878046489e2e5ba02c649d185f21c67d4cf8a56d098980"
|
||||
dependencies = [
|
||||
"windows-implement",
|
||||
"windows-interface",
|
||||
"windows-link",
|
||||
"windows-link 0.1.1",
|
||||
"windows-result",
|
||||
"windows-strings 0.4.0",
|
||||
]
|
||||
@@ -6465,7 +6405,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7a1d6bbefcb7b60acd19828e1bc965da6fcf18a7e39490c5f8be71e54a19ba32"
|
||||
dependencies = [
|
||||
"windows-core 0.61.0",
|
||||
"windows-link",
|
||||
"windows-link 0.1.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6496,6 +6436,12 @@ version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38"
|
||||
|
||||
[[package]]
|
||||
name = "windows-link"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
|
||||
|
||||
[[package]]
|
||||
name = "windows-numerics"
|
||||
version = "0.2.0"
|
||||
@@ -6503,7 +6449,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1"
|
||||
dependencies = [
|
||||
"windows-core 0.61.0",
|
||||
"windows-link",
|
||||
"windows-link 0.1.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6523,7 +6469,7 @@ version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c64fd11a4fd95df68efcfee5f44a294fe71b8bc6a91993e2791938abcc712252"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
"windows-link 0.1.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6532,7 +6478,7 @@ version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "87fa48cc5d406560701792be122a10132491cff9d0aeb23583cc2dcafc847319"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
"windows-link 0.1.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6541,7 +6487,7 @@ version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7a2ba9642430ee452d5a7aa78d72907ebe8cfda358e8cb7918a2050581322f97"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
"windows-link 0.1.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6571,6 +6517,15 @@ dependencies = [
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.61.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
|
||||
dependencies = [
|
||||
"windows-link 0.2.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-targets"
|
||||
version = "0.42.2"
|
||||
|
||||
@@ -28,7 +28,7 @@ fastnbt = "2.6.0"
|
||||
flate2 = "1.1"
|
||||
fnv = "1.0.7"
|
||||
fs2 = "0.4"
|
||||
geo = "0.30.0"
|
||||
geo = "0.31.0"
|
||||
image = "0.25"
|
||||
indicatif = "0.17.11"
|
||||
itertools = "0.14.0"
|
||||
@@ -44,7 +44,7 @@ serde_json = "1.0"
|
||||
tauri = { version = "2", optional = true }
|
||||
tauri-plugin-log = { version = "2.6.0", optional = true }
|
||||
tauri-plugin-shell = { version = "2", optional = true }
|
||||
tokio = { version = "1.47.0", features = ["full"], optional = true }
|
||||
tokio = { version = "1.48.0", features = ["full"], optional = true }
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
windows = { version = "0.61.1", features = ["Win32_System_Console"] }
|
||||
|
||||
@@ -6,6 +6,8 @@ use crate::element_processing::*;
|
||||
use crate::ground::Ground;
|
||||
use crate::osm_parser::ProcessedElement;
|
||||
use crate::progress::emit_gui_progress_update;
|
||||
#[cfg(feature = "gui")]
|
||||
use crate::telemetry::{send_log, LogLevel};
|
||||
use crate::world_editor::WorldEditor;
|
||||
use colored::Colorize;
|
||||
use indicatif::{ProgressBar, ProgressStyle};
|
||||
@@ -250,7 +252,10 @@ pub fn generate_world(
|
||||
args.scale,
|
||||
&ground,
|
||||
) {
|
||||
eprintln!("Warning: Failed to update spawn point Y coordinate: {e}");
|
||||
let warning_msg = format!("Failed to update spawn point Y coordinate: {}", e);
|
||||
eprintln!("Warning: {}", warning_msg);
|
||||
#[cfg(feature = "gui")]
|
||||
send_log(LogLevel::Warning, &warning_msg);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
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;
|
||||
|
||||
@@ -82,8 +84,9 @@ pub fn fetch_elevation_data(
|
||||
let tiles: Vec<(u32, u32)> = get_tile_coordinates(bbox, zoom);
|
||||
|
||||
// Match grid dimensions with Minecraft world size
|
||||
let grid_width: usize = scale_factor_x as usize;
|
||||
let grid_height: usize = scale_factor_z as usize;
|
||||
// Ensure minimum grid size of 1 to prevent division by zero and indexing errors
|
||||
let grid_width: usize = (scale_factor_x as usize).max(1);
|
||||
let grid_height: usize = (scale_factor_z as usize).max(1);
|
||||
|
||||
// Initialize height grid with proper dimensions
|
||||
let mut height_grid: Vec<Vec<f64>> = vec![vec![f64::NAN; grid_width]; grid_height];
|
||||
@@ -109,8 +112,16 @@ pub fn fetch_elevation_data(
|
||||
};
|
||||
|
||||
if file_size < 1000 {
|
||||
eprintln!("Warning: Cached tile at {} appears to be too small ({} bytes). Refetching tile.",
|
||||
tile_path.display(), file_size);
|
||||
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 to be too small. Refetching tile.",
|
||||
);
|
||||
|
||||
// Remove the potentially corrupted file
|
||||
if let Err(remove_err) = std::fs::remove_file(&tile_path) {
|
||||
@@ -118,6 +129,11 @@ pub fn fetch_elevation_data(
|
||||
"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
|
||||
@@ -132,7 +148,16 @@ pub fn fetch_elevation_data(
|
||||
match image::open(&tile_path) {
|
||||
Ok(img) => img.to_rgb8(),
|
||||
Err(e) => {
|
||||
eprintln!("Warning: Cached tile at {} is corrupted or invalid: {}. Re-downloading...", tile_path.display(), 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) {
|
||||
@@ -140,6 +165,11 @@ pub fn fetch_elevation_data(
|
||||
"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
|
||||
@@ -352,6 +382,11 @@ fn get_tile_coordinates(bbox: &LLBBox, zoom: u8) -> Vec<(u32, u32)> {
|
||||
}
|
||||
|
||||
fn apply_gaussian_blur(heights: &[Vec<f64>], sigma: f64) -> Vec<Vec<f64>> {
|
||||
// Guard against empty input
|
||||
if heights.is_empty() || heights[0].is_empty() {
|
||||
return heights.to_owned();
|
||||
}
|
||||
|
||||
let kernel_size: usize = (sigma * 3.0).ceil() as usize * 2 + 1;
|
||||
let kernel: Vec<f64> = create_gaussian_kernel(kernel_size, sigma);
|
||||
|
||||
@@ -421,6 +456,11 @@ fn create_gaussian_kernel(size: usize, sigma: f64) -> Vec<f64> {
|
||||
}
|
||||
|
||||
fn fill_nan_values(height_grid: &mut [Vec<f64>]) {
|
||||
// Guard against empty grid
|
||||
if height_grid.is_empty() || height_grid[0].is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let height: usize = height_grid.len();
|
||||
let width: usize = height_grid[0].len();
|
||||
|
||||
@@ -461,6 +501,11 @@ fn fill_nan_values(height_grid: &mut [Vec<f64>]) {
|
||||
}
|
||||
|
||||
fn filter_elevation_outliers(height_grid: &mut [Vec<f64>]) {
|
||||
// Guard against empty grid
|
||||
if height_grid.is_empty() || height_grid[0].is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let height = height_grid.len();
|
||||
let width = height_grid[0].len();
|
||||
|
||||
@@ -575,4 +620,46 @@ mod tests {
|
||||
.unwrap()
|
||||
.contains("image"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_empty_grid_handling() {
|
||||
// Test that empty grids don't cause panics
|
||||
let empty_grid: Vec<Vec<f64>> = vec![];
|
||||
let result = apply_gaussian_blur(&empty_grid, 5.0);
|
||||
assert!(result.is_empty());
|
||||
|
||||
// Test grid with empty rows
|
||||
let grid_with_empty_rows: Vec<Vec<f64>> = vec![vec![]];
|
||||
let result = apply_gaussian_blur(&grid_with_empty_rows, 5.0);
|
||||
assert_eq!(result.len(), 1);
|
||||
assert!(result[0].is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fill_nan_values_empty_grid() {
|
||||
// Test that empty grids don't cause panics
|
||||
let mut empty_grid: Vec<Vec<f64>> = vec![];
|
||||
fill_nan_values(&mut empty_grid);
|
||||
assert!(empty_grid.is_empty());
|
||||
|
||||
// Test grid with empty rows
|
||||
let mut grid_with_empty_rows: Vec<Vec<f64>> = vec![vec![]];
|
||||
fill_nan_values(&mut grid_with_empty_rows);
|
||||
assert_eq!(grid_with_empty_rows.len(), 1);
|
||||
assert!(grid_with_empty_rows[0].is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_filter_outliers_empty_grid() {
|
||||
// Test that empty grids don't cause panics
|
||||
let mut empty_grid: Vec<Vec<f64>> = vec![];
|
||||
filter_elevation_outliers(&mut empty_grid);
|
||||
assert!(empty_grid.is_empty());
|
||||
|
||||
// Test grid with empty rows
|
||||
let mut grid_with_empty_rows: Vec<Vec<f64>> = vec![vec![]];
|
||||
filter_elevation_outliers(&mut grid_with_empty_rows);
|
||||
assert_eq!(grid_with_empty_rows.len(), 1);
|
||||
assert!(grid_with_empty_rows[0].is_empty());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,6 +75,10 @@ impl Ground {
|
||||
/// Converts game coordinates to elevation data coordinates
|
||||
#[inline(always)]
|
||||
fn get_data_coordinates(&self, coord: XZPoint, data: &ElevationData) -> (f64, f64) {
|
||||
// Guard against division by zero for edge cases
|
||||
if data.width == 0 || data.height == 0 {
|
||||
return (0.0, 0.0);
|
||||
}
|
||||
let x_ratio: f64 = coord.x as f64 / data.width as f64;
|
||||
let z_ratio: f64 = coord.z as f64 / data.height as f64;
|
||||
(x_ratio.clamp(0.0, 1.0), z_ratio.clamp(0.0, 1.0))
|
||||
@@ -83,8 +87,17 @@ impl Ground {
|
||||
/// Interpolates height value from the elevation grid
|
||||
#[inline(always)]
|
||||
fn interpolate_height(&self, x_ratio: f64, z_ratio: f64, data: &ElevationData) -> i32 {
|
||||
// Guard against out of bounds access
|
||||
if data.width == 0 || data.height == 0 || data.heights.is_empty() {
|
||||
return self.ground_level;
|
||||
}
|
||||
let x: usize = ((x_ratio * (data.width - 1) as f64).round() as usize).min(data.width - 1);
|
||||
let z: usize = ((z_ratio * (data.height - 1) as f64).round() as usize).min(data.height - 1);
|
||||
|
||||
// Additional safety check for row length
|
||||
if z >= data.heights.len() || x >= data.heights[z].len() {
|
||||
return self.ground_level;
|
||||
}
|
||||
data.heights[z][x]
|
||||
}
|
||||
|
||||
@@ -138,6 +151,80 @@ impl Ground {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_ground_level_with_zero_dimensions() {
|
||||
// Test that zero-dimension elevation data doesn't cause panic
|
||||
let elevation_data = ElevationData {
|
||||
heights: vec![],
|
||||
width: 0,
|
||||
height: 0,
|
||||
};
|
||||
|
||||
let ground = Ground {
|
||||
elevation_enabled: true,
|
||||
ground_level: 64,
|
||||
elevation_data: Some(elevation_data),
|
||||
};
|
||||
|
||||
// This should not panic and should return ground_level
|
||||
let level = ground.level(XZPoint::new(10, 10));
|
||||
assert_eq!(level, 64);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_ground_level_with_one_dimension_zero() {
|
||||
// Test that partial zero dimensions don't cause panic
|
||||
let elevation_data = ElevationData {
|
||||
heights: vec![vec![100]],
|
||||
width: 0,
|
||||
height: 1,
|
||||
};
|
||||
|
||||
let ground = Ground {
|
||||
elevation_enabled: true,
|
||||
ground_level: 64,
|
||||
elevation_data: Some(elevation_data),
|
||||
};
|
||||
|
||||
// This should not panic and should return ground_level
|
||||
let level = ground.level(XZPoint::new(5, 5));
|
||||
assert_eq!(level, 64);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_ground_level_normal_case() {
|
||||
// Test that normal elevation data works correctly
|
||||
let elevation_data = ElevationData {
|
||||
heights: vec![vec![80, 85], vec![90, 95]],
|
||||
width: 2,
|
||||
height: 2,
|
||||
};
|
||||
|
||||
let ground = Ground {
|
||||
elevation_enabled: true,
|
||||
ground_level: 64,
|
||||
elevation_data: Some(elevation_data),
|
||||
};
|
||||
|
||||
// This should work normally
|
||||
let level = ground.level(XZPoint::new(0, 0));
|
||||
assert!(level >= 64); // Should be one of the elevation values
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_ground_level_disabled() {
|
||||
// Test that disabled elevation returns ground_level
|
||||
let ground = Ground::new_flat(70);
|
||||
|
||||
let level = ground.level(XZPoint::new(100, 100));
|
||||
assert_eq!(level, 70);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn generate_ground_data(args: &Args) -> Ground {
|
||||
if args.terrain {
|
||||
println!("{} Fetching elevation...", "[3/7]".bold());
|
||||
|
||||
25
src/gui.rs
25
src/gui.rs
@@ -8,15 +8,16 @@ use crate::map_transformation;
|
||||
use crate::osm_parser;
|
||||
use crate::progress;
|
||||
use crate::retrieve_data;
|
||||
use crate::telemetry::{self, send_log, LogLevel};
|
||||
use crate::version_check;
|
||||
use fastnbt::Value;
|
||||
use flate2::read::GzDecoder;
|
||||
use fs2::FileExt;
|
||||
use log::{error, LevelFilter};
|
||||
use log::LevelFilter;
|
||||
use rfd::FileDialog;
|
||||
use std::io::Read;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::{env, fs, io::Write, panic};
|
||||
use std::{env, fs, io::Write};
|
||||
use tauri_plugin_log::{Builder as LogBuilder, Target, TargetKind};
|
||||
|
||||
/// Manages the session.lock file for a Minecraft world directory
|
||||
@@ -63,12 +64,8 @@ pub fn run_gui() {
|
||||
// Launch the UI
|
||||
println!("Launching UI...");
|
||||
|
||||
// Set a custom panic hook to log panic information
|
||||
panic::set_hook(Box::new(|panic_info| {
|
||||
let message = format!("Application panicked: {panic_info:?}");
|
||||
error!("{message}");
|
||||
std::process::exit(1);
|
||||
}));
|
||||
// Install panic hook for crash reporting
|
||||
telemetry::install_panic_hook();
|
||||
|
||||
// Workaround WebKit2GTK issue with NVIDIA drivers and graphics issues
|
||||
// Source: https://github.com/tauri-apps/tauri/issues/10702
|
||||
@@ -400,6 +397,10 @@ fn add_localized_world_name(world_path: PathBuf, bbox: &LLBBox) -> PathBuf {
|
||||
if let Ok(compressed_data) = encoder.finish() {
|
||||
if let Err(e) = std::fs::write(&level_path, compressed_data) {
|
||||
eprintln!("Failed to update level.dat with area name: {e}");
|
||||
send_log(
|
||||
LogLevel::Warning,
|
||||
"Failed to update level.dat with area name",
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -678,10 +679,17 @@ fn gui_start_generation(
|
||||
fillground_enabled: bool,
|
||||
is_new_world: bool,
|
||||
spawn_point: Option<(f64, f64)>,
|
||||
telemetry_consent: bool,
|
||||
) -> Result<(), String> {
|
||||
use progress::emit_gui_error;
|
||||
use LLBBox;
|
||||
|
||||
// Store telemetry consent for crash reporting
|
||||
telemetry::set_telemetry_consent(telemetry_consent);
|
||||
|
||||
// Send generation click telemetry
|
||||
telemetry::send_generation_click();
|
||||
|
||||
// If spawn point was chosen and the world is new, check and set the spawn point
|
||||
if is_new_world && spawn_point.is_some() {
|
||||
// Verify the spawn point is within bounds
|
||||
@@ -810,6 +818,7 @@ fn gui_start_generation(
|
||||
&mut xzbbox,
|
||||
&mut ground,
|
||||
);
|
||||
send_log(LogLevel::Info, "Map transformation completed.");
|
||||
|
||||
let _ = data_processing::generate_world(
|
||||
parsed_elements,
|
||||
|
||||
50
src/gui/css/styles.css
vendored
50
src/gui/css/styles.css
vendored
@@ -63,6 +63,7 @@ a:hover {
|
||||
justify-content: center;
|
||||
align-items: stretch;
|
||||
margin-top: 5px;
|
||||
min-height: 60vh;
|
||||
}
|
||||
|
||||
.section {
|
||||
@@ -79,6 +80,12 @@ a:hover {
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.map-box {
|
||||
min-height: 400px;
|
||||
}
|
||||
|
||||
.controls-content {
|
||||
@@ -94,6 +101,8 @@ a:hover {
|
||||
.map-container {
|
||||
border: 2px solid #e0e0e0;
|
||||
border-radius: 8px;
|
||||
flex-grow: 1;
|
||||
min-height: 300px;
|
||||
}
|
||||
|
||||
.section h2 {
|
||||
@@ -249,6 +258,33 @@ button:hover {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
/* Modal actions/buttons */
|
||||
.modal-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background-color: var(--primary-accent);
|
||||
color: #1a1a1a;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background-color: var(--primary-accent-dark);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background-color: #e0e0e0;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.btn-secondary {
|
||||
background-color: #3a3a3a;
|
||||
color: #ffffff;
|
||||
}
|
||||
}
|
||||
|
||||
#terrain-toggle {
|
||||
accent-color: #fecc44;
|
||||
}
|
||||
@@ -281,6 +317,10 @@ button:hover {
|
||||
margin: 15px 0;
|
||||
}
|
||||
|
||||
#telemetry-toggle {
|
||||
accent-color: #fecc44;
|
||||
}
|
||||
|
||||
.scale-slider-container label {
|
||||
display: block;
|
||||
margin-bottom: 5px;
|
||||
@@ -306,7 +346,7 @@ button:hover {
|
||||
|
||||
#bbox-coords {
|
||||
width: 100%;
|
||||
padding: 8px;
|
||||
padding: 5px;
|
||||
border: 1px solid #fecc44;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
@@ -353,7 +393,7 @@ button:hover {
|
||||
|
||||
.license-button-row {
|
||||
justify-content: center;
|
||||
margin-top: 10px;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.license-button {
|
||||
@@ -393,7 +433,7 @@ button:hover {
|
||||
.generation-mode-dropdown {
|
||||
width: 100%;
|
||||
max-width: 180px;
|
||||
padding: 5px 8px;
|
||||
padding: 3px 8px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #fecc44;
|
||||
background-color: #ffffff;
|
||||
@@ -421,7 +461,7 @@ button:hover {
|
||||
.language-dropdown {
|
||||
width: 100%;
|
||||
max-width: 180px;
|
||||
padding: 5px 8px;
|
||||
padding: 3px 8px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #fecc44;
|
||||
background-color: #ffffff;
|
||||
@@ -449,7 +489,7 @@ button:hover {
|
||||
.theme-dropdown {
|
||||
width: 100%;
|
||||
max-width: 180px;
|
||||
padding: 5px 8px;
|
||||
padding: 3px 8px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #fecc44;
|
||||
background-color: #ffffff;
|
||||
|
||||
24
src/gui/index.html
vendored
24
src/gui/index.html
vendored
@@ -186,6 +186,14 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Telemetry Consent Toggle -->
|
||||
<div class="settings-row">
|
||||
<label for="telemetry-toggle">Anonymous Crash Reports</label>
|
||||
<div class="settings-control">
|
||||
<input type="checkbox" id="telemetry-toggle" name="telemetry-toggle">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- License and Credits Button -->
|
||||
<div class="settings-row license-button-row">
|
||||
<button type="button" id="license-button" class="license-button" onclick="openLicense()" data-localize="license_and_credits">License and Credits</button>
|
||||
@@ -203,6 +211,22 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Telemetry Consent Modal (first run) -->
|
||||
<div id="telemetry-modal" class="modal" style="display: none;">
|
||||
<div class="modal-content">
|
||||
<span class="close-button" onclick="rejectTelemetry()">×</span>
|
||||
<h2>Help improve Arnis</h2>
|
||||
<p style="text-align:left; margin-top:6px; color:#ececec;">
|
||||
We’d like to collect anonymous usage data like crashes and performance to make Arnis more stable and faster.
|
||||
<a href="https://arnismc.com/privacypolicy.html" style="color: inherit;" target="_blank">No personal data or world contents are collected.</a>
|
||||
</p>
|
||||
<div class="modal-actions" style="margin-top:14px;">
|
||||
<button type="button" class="btn-secondary" onclick="rejectTelemetry()">No thanks</button>
|
||||
<button type="button" class="btn-primary" onclick="acceptTelemetry()">Allow anonymous data</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- License Modal -->
|
||||
<div id="license-modal" class="modal" style="display: none;">
|
||||
<div class="modal-content">
|
||||
|
||||
4
src/gui/js/license.js
vendored
4
src/gui/js/license.js
vendored
@@ -24,6 +24,10 @@ export const licenseText = `
|
||||
Elevation data derived from the <a href="https://registry.opendata.aws/terrain-tiles/" style="color: inherit;" target="_blank">AWS Terrain Tiles</a> dataset.
|
||||
<br><br>
|
||||
|
||||
<p><b>Privacy Policy:</b></p>
|
||||
If you consent to telemetry data collection, please review our Privacy Policy at:
|
||||
<a href="https://arnismc.com/privacypolicy.html" style="color: inherit;" target="_blank">https://arnismc.com/privacypolicy.html</a>.
|
||||
|
||||
<p><b>License:</b></p>
|
||||
<pre style="white-space: pre-wrap; font-family: inherit;">
|
||||
Apache License
|
||||
|
||||
64
src/gui/js/main.js
vendored
64
src/gui/js/main.js
vendored
@@ -20,6 +20,7 @@ window.addEventListener("DOMContentLoaded", async () => {
|
||||
setupProgressListener();
|
||||
initSettings();
|
||||
initWorldPicker();
|
||||
initTelemetryConsent();
|
||||
handleBboxInput();
|
||||
const localization = await getLocalization();
|
||||
await applyLocalization(localization);
|
||||
@@ -305,6 +306,20 @@ function initSettings() {
|
||||
}
|
||||
});
|
||||
|
||||
// Telemetry consent toggle
|
||||
const telemetryToggle = document.getElementById("telemetry-toggle");
|
||||
const telemetryKey = 'telemetry-consent';
|
||||
|
||||
// Load saved telemetry consent
|
||||
const savedConsent = localStorage.getItem(telemetryKey);
|
||||
telemetryToggle.checked = savedConsent === 'true';
|
||||
|
||||
// Handle telemetry consent change
|
||||
telemetryToggle.addEventListener("change", () => {
|
||||
const isEnabled = telemetryToggle.checked;
|
||||
localStorage.setItem(telemetryKey, isEnabled ? 'true' : 'false');
|
||||
});
|
||||
|
||||
|
||||
/// License and Credits
|
||||
function openLicense() {
|
||||
@@ -329,6 +344,49 @@ function initSettings() {
|
||||
window.closeLicense = closeLicense;
|
||||
}
|
||||
|
||||
// Telemetry consent (first run only)
|
||||
function initTelemetryConsent() {
|
||||
const key = 'telemetry-consent'; // values: 'true' | 'false'
|
||||
const existing = localStorage.getItem(key);
|
||||
|
||||
const modal = document.getElementById('telemetry-modal');
|
||||
if (!modal) return;
|
||||
|
||||
if (existing === null) {
|
||||
// First run: ask for consent
|
||||
modal.style.display = 'flex';
|
||||
modal.style.justifyContent = 'center';
|
||||
modal.style.alignItems = 'center';
|
||||
}
|
||||
|
||||
// Expose handlers
|
||||
window.acceptTelemetry = () => {
|
||||
localStorage.setItem(key, 'true');
|
||||
modal.style.display = 'none';
|
||||
// Update settings toggle to reflect the consent
|
||||
const telemetryToggle = document.getElementById('telemetry-toggle');
|
||||
if (telemetryToggle) {
|
||||
telemetryToggle.checked = true;
|
||||
}
|
||||
};
|
||||
|
||||
window.rejectTelemetry = () => {
|
||||
localStorage.setItem(key, 'false');
|
||||
modal.style.display = 'none';
|
||||
// Update settings toggle to reflect the consent
|
||||
const telemetryToggle = document.getElementById('telemetry-toggle');
|
||||
if (telemetryToggle) {
|
||||
telemetryToggle.checked = false;
|
||||
}
|
||||
};
|
||||
|
||||
// Utility for other scripts to read consent
|
||||
window.getTelemetryConsent = () => {
|
||||
const v = localStorage.getItem(key);
|
||||
return v === null ? null : v === 'true';
|
||||
};
|
||||
}
|
||||
|
||||
function initWorldPicker() {
|
||||
// World Picker
|
||||
const worldPickerModal = document.getElementById("world-modal");
|
||||
@@ -617,6 +675,9 @@ async function startGeneration() {
|
||||
floodfill_timeout = isNaN(floodfill_timeout) || floodfill_timeout < 0 ? 20 : floodfill_timeout;
|
||||
ground_level = isNaN(ground_level) || ground_level < -62 ? 20 : ground_level;
|
||||
|
||||
// Get telemetry consent (defaults to false if not set)
|
||||
const telemetryConsent = window.getTelemetryConsent ? window.getTelemetryConsent() : false;
|
||||
|
||||
// Pass the selected options to the Rust backend
|
||||
await invoke("gui_start_generation", {
|
||||
bboxText: selectedBBox,
|
||||
@@ -630,7 +691,8 @@ async function startGeneration() {
|
||||
roofEnabled: roof,
|
||||
fillgroundEnabled: fill_ground,
|
||||
isNewWorld: isNewWorld,
|
||||
spawnPoint: spawnPoint
|
||||
spawnPoint: spawnPoint,
|
||||
telemetryConsent: telemetryConsent || false
|
||||
});
|
||||
|
||||
console.log("Generation process started.");
|
||||
|
||||
@@ -14,6 +14,8 @@ mod osm_parser;
|
||||
#[cfg(feature = "gui")]
|
||||
mod progress;
|
||||
mod retrieve_data;
|
||||
#[cfg(feature = "gui")]
|
||||
mod telemetry;
|
||||
#[cfg(test)]
|
||||
mod test_utilities;
|
||||
mod version_check;
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
#[cfg(feature = "gui")]
|
||||
use crate::telemetry::{send_log, LogLevel};
|
||||
use once_cell::sync::OnceCell;
|
||||
use serde_json::json;
|
||||
use tauri::{Emitter, WebviewWindow};
|
||||
@@ -38,7 +40,10 @@ pub fn emit_gui_progress_update(progress: f64, message: &str) {
|
||||
});
|
||||
|
||||
if let Err(e) = window.emit("progress-update", payload) {
|
||||
eprintln!("Failed to emit progress event: {e}");
|
||||
let error_msg = format!("Failed to emit progress event: {}", e);
|
||||
eprintln!("{}", error_msg);
|
||||
#[cfg(feature = "gui")]
|
||||
send_log(LogLevel::Warning, &error_msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
249
src/telemetry.rs
Normal file
249
src/telemetry.rs
Normal file
@@ -0,0 +1,249 @@
|
||||
use log::error;
|
||||
use reqwest::blocking::Client;
|
||||
use serde::Serialize;
|
||||
use std::panic::{self, AssertUnwindSafe};
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
|
||||
/// Telemetry endpoint URL
|
||||
const TELEMETRY_URL: &str = "https://arnismc.com/telemetry/report_telemetry.php";
|
||||
|
||||
/// Global flag to store user's telemetry consent
|
||||
static TELEMETRY_CONSENT: AtomicBool = AtomicBool::new(false);
|
||||
|
||||
/// Sets the user's telemetry consent preference
|
||||
pub fn set_telemetry_consent(consent: bool) {
|
||||
TELEMETRY_CONSENT.store(consent, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
/// Gets the user's telemetry consent preference
|
||||
fn get_telemetry_consent() -> bool {
|
||||
TELEMETRY_CONSENT.load(Ordering::Relaxed)
|
||||
}
|
||||
|
||||
/// Determines the current platform as a string
|
||||
fn get_platform() -> &'static str {
|
||||
match std::env::consts::OS {
|
||||
"windows" => "windows",
|
||||
"linux" => "linux",
|
||||
"macos" => "macos",
|
||||
_ => "unknown",
|
||||
}
|
||||
}
|
||||
|
||||
/// Gets the application version from Cargo.toml
|
||||
fn get_app_version() -> &'static str {
|
||||
env!("CARGO_PKG_VERSION")
|
||||
}
|
||||
|
||||
/// Crash report payload structure
|
||||
#[derive(Serialize)]
|
||||
struct CrashReport<'a> {
|
||||
r#type: &'a str,
|
||||
error_message: &'a str,
|
||||
platform: &'a str,
|
||||
app_version: &'a str,
|
||||
}
|
||||
|
||||
/// Generation click payload structure
|
||||
#[derive(Serialize)]
|
||||
struct GenerationClick<'a> {
|
||||
r#type: &'a str,
|
||||
}
|
||||
|
||||
/// Log entry payload structure
|
||||
#[derive(Serialize)]
|
||||
struct LogEntry<'a> {
|
||||
r#type: &'a str,
|
||||
log_level: &'a str,
|
||||
log_message: &'a str,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
platform: Option<&'a str>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
app_version: Option<&'a str>,
|
||||
}
|
||||
|
||||
/// Sends a crash report to the telemetry server
|
||||
fn send_crash_report(error_message: String, platform: &str, app_version: &str) {
|
||||
// Wrap in catch_unwind to prevent any panics during crash reporting
|
||||
let _ = std::panic::catch_unwind(AssertUnwindSafe(|| {
|
||||
let _ = (|| -> Result<(), Box<dyn std::error::Error>> {
|
||||
let client = Client::new();
|
||||
|
||||
let payload = CrashReport {
|
||||
r#type: "crash",
|
||||
error_message: &error_message,
|
||||
platform,
|
||||
app_version,
|
||||
};
|
||||
|
||||
let _res = client
|
||||
.post(TELEMETRY_URL)
|
||||
.header("Content-Type", "application/json")
|
||||
.json(&payload)
|
||||
.send()?;
|
||||
|
||||
Ok(())
|
||||
})();
|
||||
}));
|
||||
}
|
||||
|
||||
/// Sends a generation click event to the telemetry server
|
||||
pub fn send_generation_click() {
|
||||
// Check user consent
|
||||
if !get_telemetry_consent() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Only send in release builds
|
||||
if cfg!(debug_assertions) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Send in background thread to avoid blocking UI
|
||||
// Wrap in catch_unwind to prevent any panics from escaping
|
||||
let _ = std::thread::spawn(|| {
|
||||
let _ = std::panic::catch_unwind(AssertUnwindSafe(|| {
|
||||
let _ = (|| -> Result<(), Box<dyn std::error::Error>> {
|
||||
let client = Client::new();
|
||||
|
||||
let payload = GenerationClick {
|
||||
r#type: "generation_click",
|
||||
};
|
||||
|
||||
let _res = client
|
||||
.post(TELEMETRY_URL)
|
||||
.header("Content-Type", "application/json")
|
||||
.json(&payload)
|
||||
.send()?;
|
||||
|
||||
Ok(())
|
||||
})();
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
/// Log levels for telemetry
|
||||
#[allow(dead_code)]
|
||||
pub enum LogLevel {
|
||||
Debug,
|
||||
Info,
|
||||
Warning,
|
||||
Error,
|
||||
}
|
||||
|
||||
impl LogLevel {
|
||||
fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
LogLevel::Debug => "debug",
|
||||
LogLevel::Info => "info",
|
||||
LogLevel::Warning => "warning",
|
||||
LogLevel::Error => "error",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Sends a log entry to the telemetry server
|
||||
pub fn send_log(level: LogLevel, message: &str) {
|
||||
// Check user consent
|
||||
if !get_telemetry_consent() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Only send in release builds
|
||||
if cfg!(debug_assertions) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Truncate message to 1000 characters
|
||||
let truncated_message = if message.chars().count() > 1000 {
|
||||
message.chars().take(1000).collect::<String>()
|
||||
} else {
|
||||
message.to_string()
|
||||
};
|
||||
|
||||
let platform = get_platform();
|
||||
let app_version = get_app_version();
|
||||
|
||||
// Send in background thread to avoid blocking
|
||||
// Wrap in catch_unwind to prevent any panics from escaping
|
||||
let _ = std::thread::spawn(move || {
|
||||
let _ = std::panic::catch_unwind(AssertUnwindSafe(|| {
|
||||
let _ = (|| -> Result<(), Box<dyn std::error::Error>> {
|
||||
let client = Client::new();
|
||||
|
||||
let payload = LogEntry {
|
||||
r#type: "log",
|
||||
log_level: level.as_str(),
|
||||
log_message: &truncated_message,
|
||||
platform: Some(platform),
|
||||
app_version: Some(app_version),
|
||||
};
|
||||
|
||||
let _res = client
|
||||
.post(TELEMETRY_URL)
|
||||
.header("Content-Type", "application/json")
|
||||
.json(&payload)
|
||||
.send()?;
|
||||
|
||||
Ok(())
|
||||
})();
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
/// Installs a panic hook that logs panics and sends crash reports
|
||||
pub fn install_panic_hook() {
|
||||
panic::set_hook(Box::new(|panic_info| {
|
||||
// Log the panic to both stderr and log file
|
||||
error!("Application panicked: {:?}", panic_info);
|
||||
|
||||
// Filter out secondary "panic in a function that cannot unwind" panics
|
||||
if let Some(location) = panic_info.location() {
|
||||
if location.file().contains("panicking.rs") {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Check user consent
|
||||
if !get_telemetry_consent() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Only send crash reports in release builds
|
||||
if cfg!(debug_assertions) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Everything else wrapped in catch_unwind to prevent secondary panics
|
||||
let _ = std::panic::catch_unwind(AssertUnwindSafe(|| {
|
||||
// Extract panic payload
|
||||
let payload = if let Some(s) = panic_info.payload().downcast_ref::<&str>() {
|
||||
s.to_string()
|
||||
} else if let Some(s) = panic_info.payload().downcast_ref::<String>() {
|
||||
s.clone()
|
||||
} else {
|
||||
"Unknown panic".to_string()
|
||||
};
|
||||
|
||||
// Extract location
|
||||
let location = panic_info
|
||||
.location()
|
||||
.map(|l| format!("{}:{}:{}", l.file(), l.line(), l.column()))
|
||||
.unwrap_or_else(|| "unknown location".to_string());
|
||||
|
||||
// Combine payload and location
|
||||
let mut error_message = format!("{} @ {}", payload, location);
|
||||
|
||||
// Truncate to 500 Unicode characters
|
||||
if error_message.chars().count() > 500 {
|
||||
error_message = error_message.chars().take(500).collect();
|
||||
}
|
||||
|
||||
let platform = get_platform();
|
||||
let app_version = get_app_version();
|
||||
|
||||
// Send crash report (best-effort, ignore all errors)
|
||||
send_crash_report(error_message, platform, app_version);
|
||||
}));
|
||||
}));
|
||||
}
|
||||
@@ -3,6 +3,8 @@ use crate::coordinate_system::cartesian::{XZBBox, XZPoint};
|
||||
use crate::coordinate_system::geographic::LLBBox;
|
||||
use crate::ground::Ground;
|
||||
use crate::progress::emit_gui_progress_update;
|
||||
#[cfg(feature = "gui")]
|
||||
use crate::telemetry::{send_log, LogLevel};
|
||||
use colored::Colorize;
|
||||
use fastanvil::Region;
|
||||
use fastnbt::{LongArray, Value};
|
||||
@@ -755,7 +757,9 @@ impl<'a> WorldEditor<'a> {
|
||||
|
||||
// Save metadata with error handling
|
||||
if let Err(e) = self.save_metadata() {
|
||||
eprintln!("Warning: Failed to save world metadata: {}", e);
|
||||
eprintln!("Failed to save world metadata: {}", e);
|
||||
#[cfg(feature = "gui")]
|
||||
send_log(LogLevel::Warning, "Failed to save world metadata.");
|
||||
// Continue with world saving even if metadata fails
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user