diff --git a/PLAN.md b/PLAN.md index 33f66d6..0fbd490 100755 --- a/PLAN.md +++ b/PLAN.md @@ -125,3 +125,26 @@ struct World { seed: u64, } ``` + +## Известные проблемы (требуют фикса) + +### Критические +1. **GPU рендерит только 1 тайл** — `GpuRenderer` 128×128, а вьюпорт показывает мульти-тайловый мир. `gpu.rs` рисует один тайл, остальные пустые. +2. **Undo/redo сломан** — `OnceLock` в операциях не позволяет повторно захватить снепшот при redo. После undo→redo мир ломается. +3. **Импорт высот неверный** — `reader.rs:122` всегда ставит surface_y = section_y * 16 + 15, игнорируя реальную высоту поверхности. +4. **Brush strength не влияет на Paint** — `apply_brush_terrain()` игнорирует силу кисти, кроме проверки на 0. +5. **Макрос `t!()` сломан** — `i18n/src/lib.rs:61`: невалидный каст, макрос с переменными не компилируется. + +### Существенные +6. **level.dat при экспорте неполный** — нет `GameRules`, `Difficulty`, `Spawn`, `Time`, `RandomSeed`. +7. **Нет поддержки Nether/End** — экспорт всегда в `region/`, импорт создаёт только `"overworld"`. +8. **`FloodOperation` — мёртвый код** — дубликат `PaintOperation`, нигде не используется. +9. **`Dimension` сериализация хрупкая** — строковые ключи `"{tx},{tz}"`, битые ключи молча дропаются. +10. **CPU-вьюпорт рендерит всё каждый кадр** — полный перерендер при каждом `update()`. +11. **`wasm-host` `host_get_seed` бессмысленный** — разбивает u64 на lo/hi 32-bit и собирает обратно. + +### Архитектурные +12. **Только 1 тест на 93 .rs файла** — операции, импорт, экспорт, слои без покрытия. +13. **Экспорт блоков примитивный** — 9 типов блоков, без свойств (snowy, waterlogged). +14. **`data_version` хардкод** — всегда экспортит 3954, игнорируя версию исходного мира. +15. **GPU buffer sizes хардкод** — `8192 * 4` и `4096 * 4` без Named constants. diff --git a/cli/src/cmds/io/export.rs b/cli/src/cmds/io/export.rs index 03ea5ca..15f1234 100755 --- a/cli/src/cmds/io/export.rs +++ b/cli/src/cmds/io/export.rs @@ -33,7 +33,10 @@ pub fn cmd_export( if validate || dry_run { if dry_run { - log::info!("{}", i18n::translate_with("cli.dry_run", &[("action", "export")])); + log::info!( + "{}", + i18n::translate_with("cli.dry_run", &[("action", "export")]) + ); } output::print_result( format, diff --git a/cli/src/cmds/io/import.rs b/cli/src/cmds/io/import.rs index bff4b18..a94da7d 100755 --- a/cli/src/cmds/io/import.rs +++ b/cli/src/cmds/io/import.rs @@ -24,7 +24,10 @@ pub fn cmd_import( if validate || dry_run { if dry_run { - log::info!("{}", i18n::translate_with("cli.dry_run", &[("action", "import")])); + log::info!( + "{}", + i18n::translate_with("cli.dry_run", &[("action", "import")]) + ); } terrafier_core::io::import::validate_save(input_path)?; output::print_result( @@ -54,10 +57,13 @@ pub fn cmd_import( let bar = progress::new_bar(region_count.max(1) as u64, &importing_msg); let world = terrafier_core::io::import::import(input_path)?; let tile_count: usize = world.dimensions.iter().map(|d| d.tiles.len()).sum(); - let import_msg = i18n::translate_with("cli.import.complete", &[ - ("tiles", &tile_count.to_string()), - ("dimensions", &world.dimensions.len().to_string()), - ]); + let import_msg = i18n::translate_with( + "cli.import.complete", + &[ + ("tiles", &tile_count.to_string()), + ("dimensions", &world.dimensions.len().to_string()), + ], + ); progress::finish_with(&import_msg, bar); let output_path = Path::new(&args.output); diff --git a/cli/src/cmds/io/render.rs b/cli/src/cmds/io/render.rs index 26c16ea..9592b66 100755 --- a/cli/src/cmds/io/render.rs +++ b/cli/src/cmds/io/render.rs @@ -33,7 +33,10 @@ pub fn cmd_render( if validate || dry_run { if dry_run { - log::info!("{}", i18n::translate_with("cli.dry_run", &[("action", "render")])); + log::info!( + "{}", + i18n::translate_with("cli.dry_run", &[("action", "render")]) + ); } output::print_result( format, @@ -50,9 +53,10 @@ pub fn cmd_render( let bar = progress::new_bar(tile_count as u64, i18n::t!("cli.render.rendering")); terrafier_core::io::export::render_to_image(&world, &args.output, args.scale)?; - let saved_msg = i18n::translate_with("cli.render.saved", &[ - ("path", &args.output.display().to_string()), - ]); + let saved_msg = i18n::translate_with( + "cli.render.saved", + &[("path", &args.output.display().to_string())], + ); progress::finish_with(&saved_msg, bar); log::info!("Preview rendered to: {}", args.output.display()); diff --git a/cli/src/cmds/new.rs b/cli/src/cmds/new.rs index 4ecaf40..fed9381 100755 --- a/cli/src/cmds/new.rs +++ b/cli/src/cmds/new.rs @@ -45,11 +45,15 @@ pub fn cmd_new( let spinner = progress::new_spinner(i18n::t!("cli.new.creating")); let world = World::new(&args.name, args.seed); - let tile_count = world.dimensions.iter().map(|d| d.tiles.len()).sum::(); - let created_msg = i18n::translate_with("cli.new.created", &[ - ("name", &args.name), - ("tiles", &tile_count.to_string()), - ]); + let tile_count = world + .dimensions + .iter() + .map(|d| d.tiles.len()) + .sum::(); + let created_msg = i18n::translate_with( + "cli.new.created", + &[("name", &args.name), ("tiles", &tile_count.to_string())], + ); progress::finish_with(&created_msg, spinner); let world_dir = args.output.join(&args.name); diff --git a/cli/src/main.rs b/cli/src/main.rs index 24501aa..89b3252 100755 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -1,5 +1,4 @@ use clap::Parser; -use std::path::PathBuf; mod cmds; mod util; @@ -43,7 +42,7 @@ enum Commands { #[cfg(feature = "scripting")] Run { /// Path to .rhai script file - script: PathBuf, + script: std::path::PathBuf, }, } diff --git a/cli/src/util/output.rs b/cli/src/util/output.rs index 11e3d72..0eba950 100755 --- a/cli/src/util/output.rs +++ b/cli/src/util/output.rs @@ -37,10 +37,10 @@ fn print_human(result: &Value) { let tiles = result["tiles"].as_u64().unwrap_or(0); println!( "{}", - i18n::translate_with("cli.new.created", &[ - ("name", name), - ("tiles", &tiles.to_string()), - ]) + i18n::translate_with( + "cli.new.created", + &[("name", name), ("tiles", &tiles.to_string()),] + ) ); let _ = seed; } @@ -48,10 +48,10 @@ fn print_human(result: &Value) { let tiles = result["tiles"].as_u64().unwrap_or(0); println!( "{}", - i18n::translate_with("cli.export.complete", &[ - ("regions", "?"), - ("chunks", &tiles.to_string()), - ]) + i18n::translate_with( + "cli.export.complete", + &[("regions", "?"), ("chunks", &tiles.to_string()),] + ) ); } "imported" => { @@ -60,10 +60,13 @@ fn print_human(result: &Value) { let dims = result["dimensions"].as_u64().unwrap_or(0); println!( "{}", - i18n::translate_with("cli.import.complete", &[ - ("tiles", &tiles.to_string()), - ("dimensions", &dims.to_string()), - ]) + i18n::translate_with( + "cli.import.complete", + &[ + ("tiles", &tiles.to_string()), + ("dimensions", &dims.to_string()), + ] + ) ); let _ = name; } @@ -71,9 +74,7 @@ fn print_human(result: &Value) { let output = result["output"].as_str().unwrap_or("?"); println!( "{}", - i18n::translate_with("cli.render.saved", &[ - ("path", output), - ]) + i18n::translate_with("cli.render.saved", &[("path", output),]) ); } "validated" => { @@ -125,17 +126,15 @@ fn print_human_info(result: &Value) { if let Some(dims) = result.get("dimensions").and_then(|v| v.as_array()) { println!( " {}", - i18n::translate_with("cli.info.dimensions", &[ - ("dimensions", &dims.len().to_string()), - ]) + i18n::translate_with( + "cli.info.dimensions", + &[("dimensions", &dims.len().to_string()),] + ) ); for dim in dims { let dim_name = dim["name"].as_str().unwrap_or("?"); let dim_tiles = dim["tiles"].as_u64().unwrap_or(0); - println!( - " - {} ({} tiles)", - dim_name, dim_tiles - ); + println!(" - {} ({} tiles)", dim_name, dim_tiles); } } if let Some(tiles) = result.get("total_tiles").and_then(|v| v.as_u64()) { diff --git a/core/src/io/minecraft/export/writer.rs b/core/src/io/minecraft/export/writer.rs index 3fd5c79..a85fa4f 100644 --- a/core/src/io/minecraft/export/writer.rs +++ b/core/src/io/minecraft/export/writer.rs @@ -14,18 +14,21 @@ use super::chunk::{build_chunk_nbt, build_chunk_nbt_with_layers}; #[allow(clippy::type_complexity)] type RegionTiles<'a> = BTreeMap<(i32, i32), Vec<(&'a (i32, i32), &'a Tile)>>; -/// Save a Terrafier World to a Minecraft save directory. -pub fn save_world(world: &World, output_path: &Path) -> Result<()> { - fs::create_dir_all(output_path.join("region"))?; +fn dim_region_path(output_path: &Path, dim_name: &str) -> std::path::PathBuf { + match dim_name { + "the_nether" => output_path.join("DIM-1").join("region"), + "the_end" => output_path.join("DIM1").join("region"), + _ => output_path.join("region"), + } +} - let level_tag = build_level_dat(world)?; - let level_bytes = terrafier_nbt::io::writer::to_gzip_bytes(&level_tag)?; - fs::write(output_path.join("level.dat"), &level_bytes)?; - - let dim = match world.dimensions.first() { - Some(d) => d, - None => return Ok(()), - }; +fn write_dimension( + dim: &crate::model::dimension::Dimension, + output_path: &Path, + build_chunk: &dyn Fn(i32, i32, &Tile, usize, usize) -> Result>, +) -> Result<()> { + let region_path = dim_region_path(output_path, &dim.name); + fs::create_dir_all(®ion_path)?; let mut regions: RegionTiles = BTreeMap::new(); for (key, tile) in &dim.tiles { @@ -47,7 +50,7 @@ pub fn save_world(world: &World, output_path: &Path) -> Result<()> { let region_local_x = (chunk_x & 31) as u8; let region_local_z = (chunk_z & 31) as u8; - let chunk_data = build_chunk_nbt(chunk_x, chunk_z, tile, chunk_lx, chunk_lz)?; + let chunk_data = build_chunk(chunk_x, chunk_z, tile, chunk_lx, chunk_lz)?; if chunk_data.is_empty() { continue; @@ -60,7 +63,22 @@ pub fn save_world(world: &World, output_path: &Path) -> Result<()> { let region_bytes = region.to_bytes()?; let file_name = format!("r.{}.{}.mca", rx, rz); - fs::write(output_path.join("region").join(&file_name), ®ion_bytes)?; + fs::write(region_path.join(&file_name), ®ion_bytes)?; + } + + Ok(()) +} + +/// Save a Terrafier World to a Minecraft save directory. +pub fn save_world(world: &World, output_path: &Path) -> Result<()> { + let level_tag = build_level_dat(world)?; + let level_bytes = terrafier_nbt::io::writer::to_gzip_bytes(&level_tag)?; + fs::write(output_path.join("level.dat"), &level_bytes)?; + + for dim in &world.dimensions { + write_dimension(dim, output_path, &|cx, cz, tile, clx, clz| { + build_chunk_nbt(cx, cz, tile, clx, clz) + })?; } Ok(()) @@ -72,58 +90,14 @@ pub fn save_world_with_layers( output_path: &Path, layer_exporters: &[&dyn LayerExport], ) -> Result<()> { - fs::create_dir_all(output_path.join("region"))?; - let level_tag = build_level_dat(world)?; let level_bytes = terrafier_nbt::io::writer::to_gzip_bytes(&level_tag)?; fs::write(output_path.join("level.dat"), &level_bytes)?; - let dim = match world.dimensions.first() { - Some(d) => d, - None => return Ok(()), - }; - - let mut regions: RegionTiles = BTreeMap::new(); - for (key, tile) in &dim.tiles { - let (tx, tz) = key; - let rx = tx >> 2; - let rz = tz >> 2; - regions.entry((rx, rz)).or_default().push((key, tile)); - } - - for ((rx, rz), tile_refs) in ®ions { - let mut region = terrafier_fastanvil::io::region::Region::new(*rx, *rz); - - for (_key, tile) in tile_refs { - for chunk_lx in 0..8usize { - for chunk_lz in 0..8usize { - let chunk_x = tile.x * 8 + chunk_lx as i32; - let chunk_z = tile.z * 8 + chunk_lz as i32; - - let region_local_x = (chunk_x & 31) as u8; - let region_local_z = (chunk_z & 31) as u8; - - let chunk_data = build_chunk_nbt_with_layers( - chunk_x, - chunk_z, - tile, - chunk_lx, - chunk_lz, - layer_exporters, - )?; - - if chunk_data.is_empty() { - continue; - } - - region.set_chunk_data(region_local_x, region_local_z, chunk_data); - } - } - } - - let region_bytes = region.to_bytes()?; - let file_name = format!("r.{}.{}.mca", rx, rz); - fs::write(output_path.join("region").join(&file_name), ®ion_bytes)?; + for dim in &world.dimensions { + write_dimension(dim, output_path, &|cx, cz, tile, clx, clz| { + build_chunk_nbt_with_layers(cx, cz, tile, clx, clz, layer_exporters) + })?; } Ok(()) @@ -135,6 +109,24 @@ fn build_level_dat(world: &World) -> Result { data.insert("LevelName".into(), Tag::String(world.name.clone())); data.insert("DataVersion".into(), Tag::Int(data_version())); data.insert("version".into(), Tag::Int(19133)); + data.insert("RandomSeed".into(), Tag::Long(world.seed as i64)); + data.insert("GameType".into(), Tag::Int(1)); + data.insert("hardcore".into(), Tag::Byte(0)); + data.insert("Difficulty".into(), Tag::Byte(2)); + data.insert("DifficultyLocked".into(), Tag::Byte(0)); + data.insert("SpawnX".into(), Tag::Int(0)); + data.insert("SpawnY".into(), Tag::Int(64)); + data.insert("SpawnZ".into(), Tag::Int(0)); + data.insert("Time".into(), Tag::Long(0)); + data.insert("DayTime".into(), Tag::Long(1000)); + data.insert("allowCommands".into(), Tag::Byte(1)); + data.insert("initialized".into(), Tag::Byte(1)); + data.insert("LastPlayed".into(), Tag::Long(0)); + + let mut data_packs = HashMap::new(); + data_packs.insert("Enabled".into(), Tag::List(vec![])); + data_packs.insert("Disabled".into(), Tag::List(vec![])); + data.insert("DataPacks".into(), Tag::Compound(data_packs)); let mut world_gen = HashMap::new(); world_gen.insert("seed".into(), Tag::Long(world.seed as i64)); diff --git a/core/src/io/minecraft/reader.rs b/core/src/io/minecraft/reader.rs index dd2f43f..9f40828 100644 --- a/core/src/io/minecraft/reader.rs +++ b/core/src/io/minecraft/reader.rs @@ -86,41 +86,74 @@ pub fn load_save(path: &Path) -> Result { let mut sorted: Vec<_> = chunk.sections.iter().collect(); sorted.sort_by_key(|b| std::cmp::Reverse(b.section_y)); - for section in &sorted { + 'section_loop: for section in &sorted { if section.palette.is_empty() { continue; } - let has_blocks = section.palette.iter().any(|p| { - p.get("Name").is_some_and(|n| { - matches!( - n, - terrafier_nbt::Tag::String(s) - if s != "minecraft:air" - ) + let non_air_indices: Vec = section + .palette + .iter() + .enumerate() + .filter(|(_, p)| { + p.get("Name").is_some_and(|n| { + matches!( + n, + terrafier_nbt::Tag::String(s) + if s != "minecraft:air" + ) + }) }) - }); + .map(|(i, _)| i) + .collect(); - if !has_blocks { + if non_air_indices.is_empty() { continue; } if section.block_data.is_empty() { - if section.palette[0].get("Name").is_some_and(|n| { - matches!( - n, - terrafier_nbt::Tag::String(s) - if s == "minecraft:air" - ) - }) { - continue; - } surface_y = Some((section.section_y as i32) * 16 + 15); break; } - surface_y = Some((section.section_y as i32) * 16 + 15); - break; + let bpe = std::cmp::max( + 4, + (section.palette.len() as f64).log2().ceil() as usize, + ); + + for dy in (0..16usize).rev() { + let block_index = dy * 256 + lz * 16 + lx; + let bit_pos = block_index * bpe; + let long_idx = bit_pos / 64; + let bit_off = bit_pos % 64; + + if let Some(&word) = section.block_data.get(long_idx) { + let palette_index = if bit_off + bpe <= 64 { + let mask = (1u64 << bpe) - 1; + ((word as u64 >> bit_off) & mask) as usize + } else { + let low_bits = 64 - bit_off; + let low = ((word as u64) >> bit_off) as usize; + let high_bits = bit_off + bpe - 64; + let high = section + .block_data + .get(long_idx + 1) + .map(|&w| { + (w as u64 & ((1u64 << high_bits) - 1)) + as usize + }) + .unwrap_or(0); + low | (high << low_bits) + }; + + if non_air_indices.contains(&palette_index) { + surface_y = Some( + (section.section_y as i32) * 16 + dy as i32, + ); + break 'section_loop; + } + } + } } } diff --git a/core/src/io/mod.rs b/core/src/io/mod.rs index 7cfd0ad..4b83bcd 100755 --- a/core/src/io/mod.rs +++ b/core/src/io/mod.rs @@ -27,8 +27,8 @@ pub type Result = std::result::Result; /// Save a `World` to a binary file. pub fn save_binary(world: &World, path: &Path) -> Result<()> { let config = bincode::config::standard(); - let bytes = - bincode::serde::encode_to_vec(world, config).map_err(|e| BinaryError::Serialize(e.to_string()))?; + let bytes = bincode::serde::encode_to_vec(world, config) + .map_err(|e| BinaryError::Serialize(e.to_string()))?; std::fs::write(path, &bytes)?; Ok(()) @@ -38,9 +38,8 @@ pub fn save_binary(world: &World, path: &Path) -> Result<()> { pub fn load_binary(path: &Path) -> Result { let bytes = std::fs::read(path)?; let config = bincode::config::standard(); - let (world, _): (World, usize) = - bincode::serde::decode_from_slice(&bytes, config) - .map_err(|e| BinaryError::Deserialize(e.to_string()))?; + let (world, _): (World, usize) = bincode::serde::decode_from_slice(&bytes, config) + .map_err(|e| BinaryError::Deserialize(e.to_string()))?; Ok(world) } diff --git a/core/src/model/world/dimension.rs b/core/src/model/world/dimension.rs index cbe14d9..0553460 100755 --- a/core/src/model/world/dimension.rs +++ b/core/src/model/world/dimension.rs @@ -55,6 +55,8 @@ impl<'de> Deserialize<'de> for Dimension { && let (Ok(tx), Ok(tz)) = (tx_s.parse::(), tz_s.parse::()) { tiles.insert((tx, tz), tile); + } else { + log::warn!("Skipping dimension tile with malformed key: {}", key); } } Ok(Self { diff --git a/core/src/ops/filters.rs b/core/src/ops/filters.rs index d2bf156..2eb5fce 100644 --- a/core/src/ops/filters.rs +++ b/core/src/ops/filters.rs @@ -3,7 +3,7 @@ use crate::model::dimension::Dimension; use crate::model::tile::{TILE_SIZE, Tile}; use crate::model::types::Terrain; use crate::ops::operations::{Operation, OperationError, RestoreHeightsOperation}; -use std::sync::OnceLock; +use std::sync::Mutex; /// Determines whether a cell should be modified by an operation. pub trait Filter: Send + Sync { fn name(&self) -> &'static str; @@ -121,7 +121,7 @@ pub struct FilteredOperation { pub center_x: u32, pub center_z: u32, pub radius: u32, - pub before_snapshot: OnceLock>, + pub before_snapshot: Mutex>>, } impl Operation for FilteredOperation { @@ -130,7 +130,7 @@ impl Operation for FilteredOperation { } fn apply(&self, dim: &mut Dimension) -> Result<(), OperationError> { - let is_first_apply = self.before_snapshot.get().is_none(); + let is_first_apply = self.before_snapshot.lock().unwrap().is_none(); let snapshot: Vec<(usize, i16)> = if is_first_apply { let tile = @@ -159,10 +159,14 @@ impl Operation for FilteredOperation { snap.push((idx, tile.heightmap[idx])); } } - let _ = self.before_snapshot.set(snap.clone()); + self.before_snapshot.lock().unwrap().replace(snap.clone()); snap } else { - self.before_snapshot.get().cloned().unwrap_or_default() + self.before_snapshot + .lock() + .unwrap() + .clone() + .unwrap_or_default() }; self.operation.apply(dim)?; @@ -189,7 +193,12 @@ impl Operation for FilteredOperation { } fn inverse(&self) -> Box { - let snapshot = self.before_snapshot.get().cloned().unwrap_or_default(); + let snapshot = self + .before_snapshot + .lock() + .unwrap() + .clone() + .unwrap_or_default(); Box::new(RestoreHeightsOperation { tile_x: self.tile_x, tile_z: self.tile_z, diff --git a/core/src/ops/operations/brush/paint.rs b/core/src/ops/operations/brush/paint.rs index 2072298..c7de4ba 100644 --- a/core/src/ops/operations/brush/paint.rs +++ b/core/src/ops/operations/brush/paint.rs @@ -3,7 +3,7 @@ use crate::model::tile::TILE_SIZE; use crate::model::types::Brush; use crate::model::types::Terrain; use crate::ops::operations::{Operation, OperationError, RestoreTerrainOperation}; -use std::sync::{Arc, OnceLock}; +use std::sync::{Arc, Mutex}; struct BrushApply<'a> { tile: &'a mut crate::model::tile::Tile, @@ -12,7 +12,7 @@ struct BrushApply<'a> { radius: u32, terrain_id: u8, brush: &'a dyn Brush, - snapshot: &'a OnceLock>, + snapshot: &'a Mutex>>, snapshot_buf: &'a mut Vec<(usize, u8)>, } @@ -25,11 +25,11 @@ fn apply_brush_terrain(args: BrushApply) { if ax < 0 || az < 0 || ax >= TILE_SIZE as i32 || az >= TILE_SIZE as i32 { continue; } - if args.brush.get_strength(dx as f64, dz as f64) == 0.0 { + if args.brush.get_strength(dx as f64, dz as f64) < 0.3 { continue; } let idx = (az as usize) * TILE_SIZE + (ax as usize); - if args.snapshot.get().is_none() { + if args.snapshot.lock().unwrap().is_none() { args.snapshot_buf.push((idx, args.tile.terrain[idx])); } args.tile.terrain[idx] = args.terrain_id; @@ -46,7 +46,7 @@ pub struct PaintOperation { pub radius: u32, pub terrain: Terrain, pub brush: Arc, - pub before_snapshot: OnceLock>, + pub before_snapshot: Mutex>>, } impl Operation for PaintOperation { @@ -77,66 +77,19 @@ impl Operation for PaintOperation { snapshot_buf: &mut snapshot_buf, }); - let _ = self.before_snapshot.get_or_init(|| snapshot_buf); + if self.before_snapshot.lock().unwrap().is_none() { + self.before_snapshot.lock().unwrap().replace(snapshot_buf); + } Ok(()) } fn inverse(&self) -> Box { - let snapshot = self.before_snapshot.get().cloned().unwrap_or_default(); - Box::new(RestoreTerrainOperation { - tile_x: self.tile_x, - tile_z: self.tile_z, - snapshot, - }) - } -} - -/// Set all terrain within brush area to a single type (hard replace regardless of strength). -pub struct FloodOperation { - pub tile_x: i32, - pub tile_z: i32, - pub center_x: u32, - pub center_z: u32, - pub radius: u32, - pub terrain: Terrain, - pub brush: Arc, - pub before_snapshot: OnceLock>, -} - -impl Operation for FloodOperation { - fn name(&self) -> &'static str { - "Flood" - } - - fn apply(&self, dim: &mut Dimension) -> Result<(), OperationError> { - let tile = - dim.tiles - .get_mut(&(self.tile_x, self.tile_z)) - .ok_or(OperationError::OutOfBounds { - tx: self.tile_x, - tz: self.tile_z, - x: 0, - z: 0, - })?; - - let mut snapshot_buf = Vec::new(); - apply_brush_terrain(BrushApply { - tile, - cx: self.center_x as i32, - cz: self.center_z as i32, - radius: self.radius, - terrain_id: self.terrain as u8, - brush: self.brush.as_ref(), - snapshot: &self.before_snapshot, - snapshot_buf: &mut snapshot_buf, - }); - - let _ = self.before_snapshot.get_or_init(|| snapshot_buf); - Ok(()) - } - - fn inverse(&self) -> Box { - let snapshot = self.before_snapshot.get().cloned().unwrap_or_default(); + let snapshot = self + .before_snapshot + .lock() + .unwrap() + .clone() + .unwrap_or_default(); Box::new(RestoreTerrainOperation { tile_x: self.tile_x, tile_z: self.tile_z, @@ -152,7 +105,7 @@ pub struct PencilOperation { pub center_x: u32, pub center_z: u32, pub terrain: Terrain, - pub before_snapshot: OnceLock>, + pub before_snapshot: Mutex>>, } impl Operation for PencilOperation { @@ -183,8 +136,11 @@ impl Operation for PencilOperation { } let idx = cz * TILE_SIZE + cx; - if self.before_snapshot.get().is_none() { - let _ = self.before_snapshot.set(vec![(idx, tile.terrain[idx])]); + if self.before_snapshot.lock().unwrap().is_none() { + self.before_snapshot + .lock() + .unwrap() + .replace(vec![(idx, tile.terrain[idx])]); } tile.terrain[idx] = self.terrain as u8; @@ -192,7 +148,12 @@ impl Operation for PencilOperation { } fn inverse(&self) -> Box { - let snapshot = self.before_snapshot.get().cloned().unwrap_or_default(); + let snapshot = self + .before_snapshot + .lock() + .unwrap() + .clone() + .unwrap_or_default(); Box::new(RestoreTerrainOperation { tile_x: self.tile_x, tile_z: self.tile_z, diff --git a/core/src/ops/operations/fill.rs b/core/src/ops/operations/fill.rs index 8480c71..de26988 100644 --- a/core/src/ops/operations/fill.rs +++ b/core/src/ops/operations/fill.rs @@ -1,5 +1,5 @@ use std::collections::VecDeque; -use std::sync::OnceLock; +use std::sync::Mutex; use crate::model::dimension::Dimension; use crate::model::tile::TILE_SIZE; @@ -14,7 +14,7 @@ pub struct FillOperation { pub center_x: u32, pub center_z: u32, pub terrain: Terrain, - pub before_snapshot: OnceLock>, + pub before_snapshot: Mutex>>, } impl Operation for FillOperation { @@ -60,7 +60,7 @@ impl Operation for FillOperation { while let Some((x, z)) = queue.pop_front() { let i = z * TILE_SIZE + x; - if self.before_snapshot.get().is_none() { + if self.before_snapshot.lock().unwrap().is_none() { snapshot.push((i, tile.terrain[i])); } tile.terrain[i] = target_terrain; @@ -80,15 +80,20 @@ impl Operation for FillOperation { } } - if self.before_snapshot.get().is_none() { - let _ = self.before_snapshot.set(snapshot); + if self.before_snapshot.lock().unwrap().is_none() { + self.before_snapshot.lock().unwrap().replace(snapshot); } Ok(()) } fn inverse(&self) -> Box { - let snapshot = self.before_snapshot.get().cloned().unwrap_or_default(); + let snapshot = self + .before_snapshot + .lock() + .unwrap() + .clone() + .unwrap_or_default(); Box::new(RestoreTerrainOperation { tile_x: self.tile_x, tile_z: self.tile_z, diff --git a/core/src/ops/operations/mod.rs b/core/src/ops/operations/mod.rs index 6bc4956..8e9f3e6 100644 --- a/core/src/ops/operations/mod.rs +++ b/core/src/ops/operations/mod.rs @@ -21,7 +21,7 @@ mod brush; mod fill; mod shaping; -pub use brush::{FloodOperation, PaintOperation, PencilOperation}; +pub use brush::{PaintOperation, PencilOperation}; pub use fill::FillOperation; pub use shaping::{ ErodeOperation, FlattenOperation, HeightOperation, MultiTileOperation, SmoothOperation, diff --git a/core/src/ops/operations/shaping/erode.rs b/core/src/ops/operations/shaping/erode.rs index 8e3aefb..5b35850 100644 --- a/core/src/ops/operations/shaping/erode.rs +++ b/core/src/ops/operations/shaping/erode.rs @@ -1,5 +1,4 @@ -use std::sync::Arc; -use std::sync::OnceLock; +use std::sync::{Arc, Mutex}; use crate::model::dimension::Dimension; use crate::model::tile::TILE_SIZE; @@ -17,7 +16,7 @@ pub struct ErodeOperation { pub iterations: u32, pub talus_angle: f64, pub brush: Arc, - pub before_snapshot: OnceLock>, + pub before_snapshot: Mutex>>, } impl Operation for ErodeOperation { @@ -42,7 +41,7 @@ impl Operation for ErodeOperation { // Snapshot original heights on first apply let mut snapshot: Vec<(usize, i16)> = Vec::new(); - let is_first_apply = self.before_snapshot.get().is_none(); + let is_first_apply = self.before_snapshot.lock().unwrap().is_none(); if is_first_apply { for dz in -r..=r { for dx in -r..=r { @@ -60,7 +59,7 @@ impl Operation for ErodeOperation { } } if is_first_apply { - let _ = self.before_snapshot.set(snapshot); + self.before_snapshot.lock().unwrap().replace(snapshot); } // Apply erosion for `iterations` passes @@ -142,7 +141,12 @@ impl Operation for ErodeOperation { } fn inverse(&self) -> Box { - let snapshot = self.before_snapshot.get().cloned().unwrap_or_default(); + let snapshot = self + .before_snapshot + .lock() + .unwrap() + .clone() + .unwrap_or_default(); Box::new(RestoreHeightsOperation { tile_x: self.tile_x, tile_z: self.tile_z, diff --git a/core/src/ops/operations/shaping/height.rs b/core/src/ops/operations/shaping/height.rs index dbdb858..a833b7b 100644 --- a/core/src/ops/operations/shaping/height.rs +++ b/core/src/ops/operations/shaping/height.rs @@ -1,5 +1,4 @@ -use std::sync::Arc; -use std::sync::OnceLock; +use std::sync::{Arc, Mutex}; use crate::model::dimension::Dimension; use crate::model::tile::TILE_SIZE; @@ -16,7 +15,7 @@ pub struct HeightOperation { pub radius: u32, pub delta: i16, pub brush: Arc, - pub before_snapshot: OnceLock>, + pub before_snapshot: Mutex>>, } impl Operation for HeightOperation { @@ -55,7 +54,7 @@ impl Operation for HeightOperation { let idx = (az as usize) * TILE_SIZE + (ax as usize); - if self.before_snapshot.get().is_none() { + if self.before_snapshot.lock().unwrap().is_none() { heights.push((idx, tile.heightmap[idx])); } @@ -66,15 +65,20 @@ impl Operation for HeightOperation { } } - if self.before_snapshot.get().is_none() { - let _ = self.before_snapshot.set(heights); + if self.before_snapshot.lock().unwrap().is_none() { + self.before_snapshot.lock().unwrap().replace(heights); } Ok(()) } fn inverse(&self) -> Box { - let snapshot = self.before_snapshot.get().cloned().unwrap_or_default(); + let snapshot = self + .before_snapshot + .lock() + .unwrap() + .clone() + .unwrap_or_default(); Box::new(RestoreHeightsOperation { tile_x: self.tile_x, tile_z: self.tile_z, @@ -92,7 +96,7 @@ pub struct FlattenOperation { pub radius: u32, pub target_height: i16, pub brush: Arc, - pub before_snapshot: OnceLock>, + pub before_snapshot: Mutex>>, } impl Operation for FlattenOperation { @@ -131,7 +135,7 @@ impl Operation for FlattenOperation { let idx = (az as usize) * TILE_SIZE + (ax as usize); - if self.before_snapshot.get().is_none() { + if self.before_snapshot.lock().unwrap().is_none() { heights.push((idx, tile.heightmap[idx])); } @@ -145,15 +149,20 @@ impl Operation for FlattenOperation { } } - if self.before_snapshot.get().is_none() { - let _ = self.before_snapshot.set(heights); + if self.before_snapshot.lock().unwrap().is_none() { + self.before_snapshot.lock().unwrap().replace(heights); } Ok(()) } fn inverse(&self) -> Box { - let snapshot = self.before_snapshot.get().cloned().unwrap_or_default(); + let snapshot = self + .before_snapshot + .lock() + .unwrap() + .clone() + .unwrap_or_default(); Box::new(RestoreHeightsOperation { tile_x: self.tile_x, tile_z: self.tile_z, diff --git a/core/src/ops/operations/shaping/smooth.rs b/core/src/ops/operations/shaping/smooth.rs index 7fee191..d90b1db 100644 --- a/core/src/ops/operations/shaping/smooth.rs +++ b/core/src/ops/operations/shaping/smooth.rs @@ -1,5 +1,4 @@ -use std::sync::Arc; -use std::sync::OnceLock; +use std::sync::{Arc, Mutex}; use crate::model::dimension::Dimension; use crate::model::tile::TILE_SIZE; @@ -16,7 +15,7 @@ pub struct SmoothOperation { pub radius: u32, pub iterations: u32, pub brush: Arc, - pub before_snapshot: OnceLock>, + pub before_snapshot: Mutex>>, } impl Operation for SmoothOperation { @@ -51,7 +50,7 @@ impl Operation for SmoothOperation { if self.brush.get_strength(dx as f64, dz as f64) <= 0.0 { continue; } - if self.before_snapshot.get().is_none() { + if self.before_snapshot.lock().unwrap().is_none() { snapshot.push(( (az as usize) * TILE_SIZE + (ax as usize), tile.heightmap[(az as usize) * TILE_SIZE + (ax as usize)], @@ -59,8 +58,8 @@ impl Operation for SmoothOperation { } } } - if self.before_snapshot.get().is_none() { - let _ = self.before_snapshot.set(snapshot); + if self.before_snapshot.lock().unwrap().is_none() { + self.before_snapshot.lock().unwrap().replace(snapshot); } // Apply smoothing for `iterations` passes @@ -110,7 +109,12 @@ impl Operation for SmoothOperation { } fn inverse(&self) -> Box { - let snapshot = self.before_snapshot.get().cloned().unwrap_or_default(); + let snapshot = self + .before_snapshot + .lock() + .unwrap() + .clone() + .unwrap_or_default(); Box::new(RestoreHeightsOperation { tile_x: self.tile_x, tile_z: self.tile_z, diff --git a/crates/i18n/src/lib.rs b/crates/i18n/src/lib.rs index e126236..ea21616 100644 --- a/crates/i18n/src/lib.rs +++ b/crates/i18n/src/lib.rs @@ -1,6 +1,6 @@ use std::collections::HashMap; -use std::sync::atomic::{AtomicU8, Ordering}; use std::sync::LazyLock; +use std::sync::atomic::{AtomicU8, Ordering}; mod lang; pub use lang::Language; @@ -57,11 +57,6 @@ macro_rules! t { ($key:expr) => { $crate::translate($key) }; - ($key:expr, $($var:ident = $val:expr),+ $(,)?) => {{ - let vars: &[(&str, &str)] = &[$((stringify!($var), &$val as &dyn std::fmt::Display as &str)),+]; - // This won't work with runtime format. Use translate_with instead. - $crate::translate_with($key, &[$((stringify!($var), &$val.as_ref() as &str)),+]) - }}; } /// Detect system language from environment variables (LANG, LANGUAGE, LC_ALL). @@ -106,10 +101,7 @@ mod tests { #[test] fn test_translate_with_vars() { - let result = translate_with("cli.new.created", &[ - ("name", "test"), - ("tiles", "9"), - ]); + let result = translate_with("cli.new.created", &[("name", "test"), ("tiles", "9")]); assert!(result.contains("test")); assert!(result.contains("9")); } diff --git a/crates/wasm-host/src/host.rs b/crates/wasm-host/src/host.rs index 5761780..4bf3969 100644 --- a/crates/wasm-host/src/host.rs +++ b/crates/wasm-host/src/host.rs @@ -46,11 +46,7 @@ fn host_log(mut caller: Caller<'_, WorldContext>, level: i32, ptr: i32, len: i32 } fn host_get_seed(caller: Caller<'_, WorldContext>) -> i64 { - let (lo, hi) = ( - (caller.data().seed & 0xFFFF_FFFF) as i64, - ((caller.data().seed >> 32) & 0xFFFF_FFFF) as i64, - ); - lo | (hi << 32) + caller.data().seed as i64 } fn host_tile_size(_caller: Caller<'_, WorldContext>) -> i32 { diff --git a/gui/src/app/update.rs b/gui/src/app/update.rs index dd8ff8d..b79c94a 100644 --- a/gui/src/app/update.rs +++ b/gui/src/app/update.rs @@ -71,8 +71,16 @@ impl eframe::App for TerrafierApp { && let Some(tile) = world.dimensions[0].tiles.values().next() { renderer.render( - &tile.heightmap.as_slice().try_into().unwrap_or_else(|_| [0; 16384]), - &tile.terrain.as_slice().try_into().unwrap_or_else(|_| [0; 16384]), + &tile + .heightmap + .as_slice() + .try_into() + .unwrap_or_else(|_| [0; 16384]), + &tile + .terrain + .as_slice() + .try_into() + .unwrap_or_else(|_| [0; 16384]), self.show_heightmap, ); diff --git a/gui/src/renderer/pipeline.rs b/gui/src/renderer/pipeline.rs index 814f732..e2d0af3 100644 --- a/gui/src/renderer/pipeline.rs +++ b/gui/src/renderer/pipeline.rs @@ -1,5 +1,10 @@ use std::sync::Arc; +const TILE_SIZE: u32 = 128; +const TILE_CELLS: u32 = TILE_SIZE * TILE_SIZE; +const HEIGHTMAP_PACKED_SIZE: u64 = (TILE_CELLS / 2) as u64 * 4; +const TERRAIN_PACKED_SIZE: u64 = (TILE_CELLS / 4) as u64 * 4; + #[repr(C)] #[derive(Clone, Copy)] pub struct Uniforms { @@ -111,13 +116,13 @@ impl GpuRenderer { }); let heightmap_buffer = device.create_buffer(&wgpu::BufferDescriptor { label: Some("heightmap_buffer"), - size: 8192 * 4, + size: HEIGHTMAP_PACKED_SIZE, usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST, mapped_at_creation: false, }); let terrain_buffer = device.create_buffer(&wgpu::BufferDescriptor { label: Some("terrain_buffer"), - size: 4096 * 4, + size: TERRAIN_PACKED_SIZE, usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST, mapped_at_creation: false, }); diff --git a/gui/src/view/cpu.rs b/gui/src/view/cpu.rs index 740c8e3..8c882ce 100644 --- a/gui/src/view/cpu.rs +++ b/gui/src/view/cpu.rs @@ -105,7 +105,7 @@ pub fn render(ui: &mut egui::Ui, app: &mut crate::app::TerrafierApp) { max_tz = max_tz.max(tz); } - let base_size = 64u32; + let base_size = 32u32; let zoom = app.view_zoom; let grid_w = ((max_tx - min_tx + 1) * base_size as i32) as usize; @@ -190,10 +190,7 @@ pub fn render(ui: &mut egui::Ui, app: &mut crate::app::TerrafierApp) { // Viewport: allocate a large area for the world with scroll let viewport_size = Vec2::new(grid_w as f32 * zoom, grid_h as f32 * zoom); - let (response, painter) = ui.allocate_painter( - viewport_size, - egui::Sense::click_and_drag(), - ); + let (response, painter) = ui.allocate_painter(viewport_size, egui::Sense::click_and_drag()); // Handle scroll-wheel zoom let scroll_delta = ui.input(|i| i.raw_scroll_delta); @@ -215,7 +212,10 @@ pub fn render(ui: &mut egui::Ui, app: &mut crate::app::TerrafierApp) { texture_id, egui::Rect::from_min_max( egui::pos2(rect.min.x + offset.x, rect.min.y + offset.y), - egui::pos2(rect.min.x + offset.x + grid_w as f32 * app.view_zoom, rect.min.y + offset.y + grid_h as f32 * app.view_zoom), + egui::pos2( + rect.min.x + offset.x + grid_w as f32 * app.view_zoom, + rect.min.y + offset.y + grid_h as f32 * app.view_zoom, + ), ), egui::Rect::from_min_max(egui::pos2(0.0, 0.0), egui::pos2(1.0, 1.0)), Color32::WHITE, @@ -229,8 +229,7 @@ pub fn render(ui: &mut egui::Ui, app: &mut crate::app::TerrafierApp) { { let bx = rect.min.x + offset.x + (sel_tx - min_tx) as f32 * scaled_size; let bz = rect.min.y + offset.y + (sel_tz - min_tz) as f32 * scaled_size; - let border_rect = - egui::Rect::from_min_size(egui::pos2(bx, bz), Vec2::splat(scaled_size)); + let border_rect = egui::Rect::from_min_size(egui::pos2(bx, bz), Vec2::splat(scaled_size)); painter.rect_stroke( border_rect, 0.0, @@ -267,10 +266,12 @@ pub fn render(ui: &mut egui::Ui, app: &mut crate::app::TerrafierApp) { let tz = (lz / scaled_size).floor() as i32 + min_tz; if dim.tiles.contains_key(&(tx, tz)) { app.selected_tile = Some((tx, tz)); - let local_x = - ((lx as u32 % scaled_size as u32) * TILE_SIZE as u32 / scaled_size as u32).min(127); - let local_z = - ((lz as u32 % scaled_size as u32) * TILE_SIZE as u32 / scaled_size as u32).min(127); + let local_x = ((lx as u32 % scaled_size as u32) * TILE_SIZE as u32 + / scaled_size as u32) + .min(127); + let local_z = ((lz as u32 % scaled_size as u32) * TILE_SIZE as u32 + / scaled_size as u32) + .min(127); app.brush_local_x = Some(local_x); app.brush_local_z = Some(local_z); app.status_message = format!( diff --git a/gui/src/view/gpu.rs b/gui/src/view/gpu.rs index fd7c3f0..10890e3 100644 --- a/gui/src/view/gpu.rs +++ b/gui/src/view/gpu.rs @@ -5,16 +5,20 @@ pub fn try_render(ui: &mut egui::Ui, app: &mut crate::app::TerrafierApp) -> bool let Some(ref world) = app.world else { return false; }; - let dim = &world.dimensions[0]; + let Some(dim) = world.dimensions.first() else { + return false; + }; + + // If world has more than one tile, fall back to CPU renderer + if dim.tiles.len() > 1 { + return false; + } if let Some(ref renderer) = app.renderer && let Some(texture_id) = renderer.texture_id { let desired_size = Vec2::new(512.0, 512.0); - let (response, painter) = ui.allocate_painter( - desired_size, - egui::Sense::click_and_drag(), - ); + let (response, painter) = ui.allocate_painter(desired_size, egui::Sense::click_and_drag()); let rect = response.rect; // Handle scroll-wheel zoom @@ -50,8 +54,10 @@ pub fn try_render(ui: &mut egui::Ui, app: &mut crate::app::TerrafierApp) -> bool // GPU only renders the first tile - position it correctly // Use a default position for the rendered tile let first_tile_pos = dim.tiles.keys().next().copied().unwrap_or((0, 0)); - let tex_origin_x = rect.min.x + offset.x + (first_tile_pos.0 - min_tx) as f32 * display_size; - let tex_origin_y = rect.min.y + offset.y + (first_tile_pos.1 - min_tz) as f32 * display_size; + let tex_origin_x = + rect.min.x + offset.x + (first_tile_pos.0 - min_tx) as f32 * display_size; + let tex_origin_y = + rect.min.y + offset.y + (first_tile_pos.1 - min_tz) as f32 * display_size; let tex_rect = egui::Rect::from_min_size( egui::pos2(tex_origin_x, tex_origin_y), @@ -82,10 +88,14 @@ pub fn try_render(ui: &mut egui::Ui, app: &mut crate::app::TerrafierApp) -> bool { let tile_x_in_pixels = (sel_tx - min_tx) as f32 * display_size; let tile_z_in_pixels = (sel_tz - min_tz) as f32 * display_size; - let brush_x = - rect.min.x + offset.x + tile_x_in_pixels + (bx as f32 * display_size / TILE_SIZE as f32); - let brush_z = - rect.min.y + offset.y + tile_z_in_pixels + (bz as f32 * display_size / TILE_SIZE as f32); + let brush_x = rect.min.x + + offset.x + + tile_x_in_pixels + + (bx as f32 * display_size / TILE_SIZE as f32); + let brush_z = rect.min.y + + offset.y + + tile_z_in_pixels + + (bz as f32 * display_size / TILE_SIZE as f32); let brush_radius_px = app.brush_radius as f32 * display_size / TILE_SIZE as f32; painter.circle_stroke( egui::pos2(brush_x, brush_z),