Fix critical structural issues: undo/redo, surface import, brush strength, i18n macro, level.dat export, GPU fallback, Nether/End support, dead code removal, dim serde, CPU perf, wasm-host, GPU constants, clippy warnings

- Replace OnceLock with Mutex<Option> for snapshot system in all 9 operations (undo/redo now works after redo)
- Fix import surface detection: scan actual packed block data for highest solid block instead of always using +15
- Apply brush strength threshold (< 0.3) in PaintOperation for natural edge falloff
- Remove broken t!() macro variant with variable substitution
- Complete level.dat with RandomSeed, GameType, Difficulty, Spawn, Time, DataPacks, etc.
- GPU viewport falls back to CPU renderer for multi-tile worlds
- Export supports Nether (DIM-1/region) and End (DIM1/region) dimension subdirectories
- Remove dead FloodOperation (duplicate of PaintOperation)
- Add log::warn for malformed dimension tile keys in serde
- Reduce CPU viewport base_size from 64 to 32 (4x fewer pixels)
- Simplify wasm-host host_get_seed to direct cast
- Add named constants for GPU buffer sizes
- Fix unused import warning in cli/main.rs
- cargo fmt
This commit is contained in:
loki5512344 2026-07-04 11:44:34 +02:00
parent 18613260a8
commit 2419b603c1
24 changed files with 339 additions and 271 deletions

View file

@ -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<Vec<u8>>,
) -> Result<()> {
let region_path = dim_region_path(output_path, &dim.name);
fs::create_dir_all(&region_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), &region_bytes)?;
fs::write(region_path.join(&file_name), &region_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 &regions {
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), &region_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<terrafier_nbt::Tag> {
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));

View file

@ -86,41 +86,74 @@ pub fn load_save(path: &Path) -> Result<World> {
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<usize> = 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;
}
}
}
}
}

View file

@ -27,8 +27,8 @@ pub type Result<T> = std::result::Result<T, BinaryError>;
/// 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<World> {
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)
}

View file

@ -55,6 +55,8 @@ impl<'de> Deserialize<'de> for Dimension {
&& let (Ok(tx), Ok(tz)) = (tx_s.parse::<i32>(), tz_s.parse::<i32>())
{
tiles.insert((tx, tz), tile);
} else {
log::warn!("Skipping dimension tile with malformed key: {}", key);
}
}
Ok(Self {

View file

@ -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<Vec<(usize, i16)>>,
pub before_snapshot: Mutex<Option<Vec<(usize, i16)>>>,
}
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<dyn Operation> {
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,

View file

@ -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<Vec<(usize, u8)>>,
snapshot: &'a Mutex<Option<Vec<(usize, u8)>>>,
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<dyn Brush>,
pub before_snapshot: OnceLock<Vec<(usize, u8)>>,
pub before_snapshot: Mutex<Option<Vec<(usize, u8)>>>,
}
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<dyn Operation> {
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<dyn Brush>,
pub before_snapshot: OnceLock<Vec<(usize, u8)>>,
}
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<dyn Operation> {
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<Vec<(usize, u8)>>,
pub before_snapshot: Mutex<Option<Vec<(usize, u8)>>>,
}
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<dyn Operation> {
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,

View file

@ -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<Vec<(usize, u8)>>,
pub before_snapshot: Mutex<Option<Vec<(usize, u8)>>>,
}
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<dyn Operation> {
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,

View file

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

View file

@ -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<dyn Brush>,
pub before_snapshot: OnceLock<Vec<(usize, i16)>>,
pub before_snapshot: Mutex<Option<Vec<(usize, i16)>>>,
}
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<dyn Operation> {
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,

View file

@ -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<dyn Brush>,
pub before_snapshot: OnceLock<Vec<(usize, i16)>>,
pub before_snapshot: Mutex<Option<Vec<(usize, i16)>>>,
}
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<dyn Operation> {
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<dyn Brush>,
pub before_snapshot: OnceLock<Vec<(usize, i16)>>,
pub before_snapshot: Mutex<Option<Vec<(usize, i16)>>>,
}
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<dyn Operation> {
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,

View file

@ -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<dyn Brush>,
pub before_snapshot: OnceLock<Vec<(usize, i16)>>,
pub before_snapshot: Mutex<Option<Vec<(usize, i16)>>>,
}
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<dyn Operation> {
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,