Compare commits

...

22 Commits

Author SHA1 Message Date
Louis Erbkamm
1bcec3dcc0 Merge branch 'main' into copilot/fix-generation-stop-issue 2025-11-22 14:40:11 +01:00
Louis Erbkamm
56ddea57d0 Merge pull request #638 from louis-e/telemetry-nofeatures-fix
Fix no-default-features build
2025-11-22 14:37:34 +01:00
Louis Erbkamm
430a4970f5 Merge branch 'main' into telemetry-nofeatures-fix 2025-11-22 14:30:34 +01:00
Louis Erbkamm
74fbdabaee Update benchmark command with new bounding box 2025-11-22 14:30:25 +01:00
louis-e
2643155e9a Fix no-default-features build 2025-11-22 13:50:55 +01:00
Louis Erbkamm
d45c360074 Enable pr-benchmark workflow 2025-11-22 13:44:39 +01:00
copilot-swe-agent[bot]
e86af9e006 Add tests for zero-dimension elevation grid handling
Co-authored-by: louis-e <44675238+louis-e@users.noreply.github.com>
2025-11-17 21:17:50 +00:00
copilot-swe-agent[bot]
7575141837 Add defensive checks to prevent crash with zero-dimension elevation grids
Co-authored-by: louis-e <44675238+louis-e@users.noreply.github.com>
2025-11-17 21:16:23 +00:00
copilot-swe-agent[bot]
eb288e9a03 Initial plan 2025-11-17 21:10:11 +00:00
Louis Erbkamm
6277a14d22 Merge pull request #618 from louis-e/dependabot/cargo/tokio-1.48.0
build(deps): bump tokio from 1.47.0 to 1.48.0
2025-11-17 21:57:36 +01:00
Louis Erbkamm
c355f243e3 Merge pull request #619 from louis-e/dependabot/cargo/geo-0.31.0
build(deps): bump geo from 0.30.0 to 0.31.0
2025-11-17 21:57:25 +01:00
Louis Erbkamm
2c31d2659c Merge pull request #630 from louis-e/crash-telemetry
WIP: Crash telemetry collection
2025-11-17 21:56:50 +01:00
louis-e
996e06ab2c Log panic to log file 2025-11-17 21:54:48 +01:00
louis-e
e11231ad0f Fix cargo fmt 2025-11-17 21:39:38 +01:00
louis-e
9adf31121e Send error telemetry data 2025-11-17 21:30:47 +01:00
louis-e
69da18fbfb Extend telemetry by more endpoints 2025-11-17 21:28:00 +01:00
louis-e
5976cc2868 Improve responsive UI layout 2025-11-17 19:15:04 +01:00
louis-e
a85eaed835 Add telemetry toggle and link to privacy policy 2025-11-17 19:09:16 +01:00
louis-e
37c3d85672 Fix clippy errors 2025-11-17 17:01:16 +01:00
louis-e
15b698a1eb WIP: Crash telemetry collection 2025-11-17 00:10:38 +01:00
dependabot[bot]
8fff2d2fb5 build(deps): bump geo from 0.30.0 to 0.31.0
Bumps [geo](https://github.com/georust/geo) from 0.30.0 to 0.31.0.
- [Changelog](https://github.com/georust/geo/blob/main/CHANGES.md)
- [Commits](https://github.com/georust/geo/compare/geo-0.30.0...geo-0.31.0)

---
updated-dependencies:
- dependency-name: geo
  dependency-version: 0.31.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-01 02:13:26 +00:00
dependabot[bot]
8c702a36ff build(deps): bump tokio from 1.47.0 to 1.48.0
Bumps [tokio](https://github.com/tokio-rs/tokio) from 1.47.0 to 1.48.0.
- [Release notes](https://github.com/tokio-rs/tokio/releases)
- [Commits](https://github.com/tokio-rs/tokio/compare/tokio-1.47.0...tokio-1.48.0)

---
updated-dependencies:
- dependency-name: tokio
  dependency-version: 1.48.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-01 02:13:08 +00:00
15 changed files with 643 additions and 110 deletions

View File

@@ -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
View File

@@ -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"

View File

@@ -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"] }

View File

@@ -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);
}
}

View File

@@ -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());
}
}

View File

@@ -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());

View File

@@ -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,

View File

@@ -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
View File

@@ -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()">&times;</span>
<h2>Help improve Arnis</h2>
<p style="text-align:left; margin-top:6px; color:#ececec;">
Wed 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">

View File

@@ -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
View File

@@ -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.");

View File

@@ -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;

View File

@@ -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
View 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);
}));
}));
}

View File

@@ -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
}