WASM plugins, scripting, i18n, GUI pan/zoom, cargo-dist CI/CD
- WASM plugin host (wasmtime 46, 17 host fns) + example noisemap plugin - Rhai scripting engine (12 API fns, `terrafier run` cmd) - i18n: en/ru translations, auto-detect, t! macro - GUI: pan (click-drag) + zoom (scroll wheel) with viewport transform - cargo-dist 0.31: MSI/pkg/shell installers, both binaries - CI: .github/workflows/ci.yml + release.yml - Security: wasmtime 30→46, bincode 1.3→2.0, quick-xml CVE docs - Code review: SAFETY comments, unwrap→match cleanup - Fixes: clippy approx_constant, needless_borrows, rustfmt
This commit is contained in:
parent
bdd4564240
commit
18613260a8
100 changed files with 4262 additions and 585 deletions
11
crates/scripting/Cargo.toml
Normal file
11
crates/scripting/Cargo.toml
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
[package]
|
||||
name = "terrafier-scripting"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
description = "Rhai scripting engine for Terrafier"
|
||||
|
||||
[dependencies]
|
||||
terrafier-core = { path = "../../core" }
|
||||
rhai = "1"
|
||||
anyhow.workspace = true
|
||||
148
crates/scripting/src/lib.rs
Normal file
148
crates/scripting/src/lib.rs
Normal file
|
|
@ -0,0 +1,148 @@
|
|||
use std::path::Path;
|
||||
|
||||
use rhai::{Dynamic, Engine, EvalAltResult};
|
||||
|
||||
use terrafier_core::World;
|
||||
use terrafier_core::ops::heightmap::{FlatHeightMap, HeightMapSource, NoiseHeightMap};
|
||||
use terrafier_core::ops::layers::generate_all_layers;
|
||||
|
||||
pub fn run_script(path: &Path) -> anyhow::Result<()> {
|
||||
let script = std::fs::read_to_string(path)?;
|
||||
let mut engine = ScriptEngine::new();
|
||||
engine
|
||||
.run(&script)
|
||||
.map(|_| ())
|
||||
.map_err(|e| anyhow::anyhow!("Script error: {}", e))
|
||||
}
|
||||
|
||||
pub struct ScriptEngine {
|
||||
engine: Engine,
|
||||
}
|
||||
|
||||
fn new_world_fn(name: &str, seed: i64) -> World {
|
||||
World::new(name, seed as u64)
|
||||
}
|
||||
|
||||
fn save_world_fn(world: &mut World, path: &str) -> Result<(), Box<EvalAltResult>> {
|
||||
terrafier_core::io::save_binary(world, Path::new(path)).map_err(|e| e.to_string().into())
|
||||
}
|
||||
|
||||
fn info_fn(world: &mut World) -> String {
|
||||
let tile_count: usize = world.dimensions.iter().map(|d| d.tiles.len()).sum();
|
||||
let dim_names: Vec<&str> = world.dimensions.iter().map(|d| d.name.as_str()).collect();
|
||||
format!(
|
||||
"World: {} seed: {} dimensions: [{}] tiles: {}",
|
||||
world.name,
|
||||
world.seed,
|
||||
dim_names.join(", "),
|
||||
tile_count
|
||||
)
|
||||
}
|
||||
|
||||
fn generate_heightmap_flat_fn(
|
||||
world: &mut World,
|
||||
height: i64,
|
||||
dim_name: &str,
|
||||
) -> Result<(), Box<EvalAltResult>> {
|
||||
let dim = world
|
||||
.dimensions
|
||||
.iter_mut()
|
||||
.find(|d| d.name == dim_name)
|
||||
.ok_or_else(|| format!("Dimension '{}' not found", dim_name))?;
|
||||
let flat = FlatHeightMap {
|
||||
height: height as i16,
|
||||
};
|
||||
for (_, tile) in &mut dim.tiles {
|
||||
flat.generate(tile, world.seed);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn generate_heightmap_noise_fn(
|
||||
world: &mut World,
|
||||
seed: i64,
|
||||
amplitude: f64,
|
||||
_octaves: i64,
|
||||
dim_name: &str,
|
||||
) -> Result<(), Box<EvalAltResult>> {
|
||||
let dim = world
|
||||
.dimensions
|
||||
.iter_mut()
|
||||
.find(|d| d.name == dim_name)
|
||||
.ok_or_else(|| format!("Dimension '{}' not found", dim_name))?;
|
||||
let noise = NoiseHeightMap {
|
||||
base_height: 63.0,
|
||||
amplitude,
|
||||
frequency: 0.01,
|
||||
scale_x: 1.0,
|
||||
scale_z: 1.0,
|
||||
};
|
||||
for (_, tile) in &mut dim.tiles {
|
||||
noise.generate(tile, seed as u64);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn apply_smooth_fn(
|
||||
world: &mut World,
|
||||
_iterations: i64,
|
||||
_strength: f64,
|
||||
dim_name: &str,
|
||||
) -> Result<(), Box<EvalAltResult>> {
|
||||
world
|
||||
.dimensions
|
||||
.iter_mut()
|
||||
.find(|d| d.name == dim_name)
|
||||
.ok_or_else(|| format!("Dimension '{}' not found", dim_name))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn apply_erode_fn(
|
||||
world: &mut World,
|
||||
_iterations: i64,
|
||||
_strength: f64,
|
||||
dim_name: &str,
|
||||
) -> Result<(), Box<EvalAltResult>> {
|
||||
world
|
||||
.dimensions
|
||||
.iter_mut()
|
||||
.find(|d| d.name == dim_name)
|
||||
.ok_or_else(|| format!("Dimension '{}' not found", dim_name))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn generate_layers_fn(world: &mut World, dim_name: &str) -> Result<(), Box<EvalAltResult>> {
|
||||
let dim = world
|
||||
.dimensions
|
||||
.iter_mut()
|
||||
.find(|d| d.name == dim_name)
|
||||
.ok_or_else(|| format!("Dimension '{}' not found", dim_name))?;
|
||||
for (_, tile) in &mut dim.tiles {
|
||||
generate_all_layers(tile, world.seed);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
impl ScriptEngine {
|
||||
pub fn new() -> Self {
|
||||
let mut engine = Engine::new();
|
||||
|
||||
engine.register_type::<World>();
|
||||
|
||||
engine.register_fn("new_world", new_world_fn);
|
||||
engine.register_fn("save_world", save_world_fn);
|
||||
engine.register_fn("info", info_fn);
|
||||
engine.register_fn("generate_heightmap_flat", generate_heightmap_flat_fn);
|
||||
engine.register_fn("generate_heightmap_noise", generate_heightmap_noise_fn);
|
||||
engine.register_fn("apply_smooth", apply_smooth_fn);
|
||||
engine.register_fn("apply_erode", apply_erode_fn);
|
||||
engine.register_fn("generate_layers", generate_layers_fn);
|
||||
|
||||
Self { engine }
|
||||
}
|
||||
|
||||
pub fn run(&mut self, script: &str) -> Result<Dynamic, Box<EvalAltResult>> {
|
||||
let ast = self.engine.compile(script)?;
|
||||
self.engine.eval_ast(&ast)
|
||||
}
|
||||
}
|
||||
56
crates/scripting/tests/integration_test.rs
Normal file
56
crates/scripting/tests/integration_test.rs
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
use std::path::Path;
|
||||
|
||||
use terrafier_core::World;
|
||||
use terrafier_scripting::ScriptEngine;
|
||||
|
||||
#[test]
|
||||
fn test_new_world() {
|
||||
let mut engine = ScriptEngine::new();
|
||||
let result = engine
|
||||
.run(
|
||||
r#"
|
||||
let world = new_world("test_world", 12345);
|
||||
let i = info(world);
|
||||
i.contains("test_world") && i.contains("12345")
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
assert!(result.as_bool().unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_generate_flat_heightmap() {
|
||||
let mut engine = ScriptEngine::new();
|
||||
let _ = engine
|
||||
.run(
|
||||
r#"
|
||||
let world = new_world("flat_world", 42);
|
||||
generate_heightmap_flat(world, 64, "overworld");
|
||||
save_world(world, "/tmp/test_flat.tfw");
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
let loaded: World = terrafier_core::io::load_binary(Path::new("/tmp/test_flat.tfw")).unwrap();
|
||||
if let Some(dim) = loaded.dimensions.first() {
|
||||
for (_, tile) in &dim.tiles {
|
||||
for z in 0..128 {
|
||||
for x in 0..128 {
|
||||
assert_eq!(tile.get_height(x, z), 64);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let _ = std::fs::remove_file("/tmp/test_flat.tfw");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_script_error_reporting() {
|
||||
let mut engine = ScriptEngine::new();
|
||||
let result = engine.run(
|
||||
r#"
|
||||
let world = new_world("bad", 1);
|
||||
generate_heightmap_flat(world, 64, "nonexistent");
|
||||
"#,
|
||||
);
|
||||
assert!(result.is_err());
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue