MVP checks / mvp (push) Canceled after 0s
Add shared Rust/WASM physics, worker meshing and diagnostics, 64-chunk full-height streaming, atlas texture support, and baseline world import. Document the current implementation and include the supplied in-game lobby screenshot.
487 lines
16 KiB
Rust
487 lines
16 KiB
Rust
use shacraft_compat::{
|
|
Mode, export_anvil, export_schem, import_anvil, import_anvil_baseline, import_schem,
|
|
nbt::{self, Tag},
|
|
};
|
|
use shacraft_core::{BlockChange, WorldStore};
|
|
use std::{
|
|
fs,
|
|
io::Read,
|
|
path::{Path, PathBuf},
|
|
};
|
|
fn fixture(name: &str) -> PathBuf {
|
|
Path::new(env!("CARGO_MANIFEST_DIR"))
|
|
.join("fixtures")
|
|
.join(name)
|
|
}
|
|
fn read_gzip(p: &Path) -> Tag {
|
|
let mut b = Vec::new();
|
|
flate2::read::GzDecoder::new(fs::File::open(p).unwrap())
|
|
.read_to_end(&mut b)
|
|
.unwrap();
|
|
nbt::decode(&b).unwrap().1
|
|
}
|
|
fn assert_tree_equal(source: &Path, out: &Path) {
|
|
for e in fs::read_dir(source).unwrap() {
|
|
let e = e.unwrap();
|
|
let to = out.join(e.file_name());
|
|
if e.file_type().unwrap().is_dir() {
|
|
assert_tree_equal(&e.path(), &to)
|
|
} else {
|
|
assert_eq!(
|
|
fs::read(e.path()).unwrap(),
|
|
fs::read(to).unwrap(),
|
|
"{}",
|
|
e.path().display()
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn sponge_import_edits_unknown_states_and_preserves_nbt() {
|
|
let t = tempfile::tempdir().unwrap();
|
|
let store_path = t.path().join("store");
|
|
let source = fixture("sponge-v3.schem");
|
|
let imported = import_schem(&source, &store_path, "main", Mode::Exact).unwrap();
|
|
assert_eq!(imported.nonair_blocks, 4);
|
|
let exact = t.path().join("exact");
|
|
export_schem(&store_path, "main", &exact, None, None, Mode::Exact).unwrap();
|
|
assert_eq!(
|
|
fs::read(&source).unwrap(),
|
|
fs::read(exact.join("world.schem")).unwrap()
|
|
);
|
|
let mut store = WorldStore::open(&store_path, 2).unwrap();
|
|
let unknown = store.get_block("main", [-15, -1, 31]).unwrap();
|
|
assert_eq!(
|
|
store.registry()[unknown as usize],
|
|
"example:unknown[facing=west]"
|
|
);
|
|
let revision = store.revision("main").unwrap();
|
|
store
|
|
.edit(
|
|
"main",
|
|
revision,
|
|
"break-container",
|
|
vec![BlockChange {
|
|
pos: [-17, -1, 31],
|
|
block: 0,
|
|
}],
|
|
)
|
|
.unwrap();
|
|
drop(store);
|
|
let rejected = t.path().join("rejected");
|
|
assert!(
|
|
export_schem(&store_path, "main", &rejected, None, None, Mode::Exact)
|
|
.unwrap_err()
|
|
.to_string()
|
|
.contains("exact_provenance_invalidated")
|
|
);
|
|
assert!(!rejected.exists());
|
|
let edited = t.path().join("edited");
|
|
let r = export_schem(&store_path, "main", &edited, None, None, Mode::BestEffort).unwrap();
|
|
assert!(r.issues.iter().any(|i| i.code == "changed_block_entity"));
|
|
let a = read_gzip(&source);
|
|
let b = read_gzip(&edited.join("world.schem"));
|
|
let a = a.compound().unwrap()["Schematic"].compound().unwrap();
|
|
let b = b.compound().unwrap()["Schematic"].compound().unwrap();
|
|
for k in ["Metadata", "Entities", "Biomes", "UnknownRoot", "Offset"] {
|
|
assert_eq!(a[k], b[k], "{k}")
|
|
}
|
|
let reimport = t.path().join("reimport");
|
|
import_schem(&edited.join("world.schem"), &reimport, "main", Mode::Exact).unwrap();
|
|
let mut s = WorldStore::open(&reimport, 2).unwrap();
|
|
assert_eq!(s.get_block("main", [-17, -1, 31]).unwrap(), 0);
|
|
assert_ne!(s.get_block("main", [-16, -1, 31]).unwrap(), 0);
|
|
}
|
|
|
|
#[test]
|
|
fn genuine_java_world_import_exact_roundtrip_edit_and_new_chunk() {
|
|
let t = tempfile::tempdir().unwrap();
|
|
let store_path = t.path().join("store");
|
|
let source = fixture("java26_2-world");
|
|
let imported = import_anvil(&source, &store_path, Mode::Exact).unwrap();
|
|
assert_eq!(imported.nonair_blocks, 3);
|
|
assert_eq!(imported.chunks, 1);
|
|
let exact = t.path().join("exact");
|
|
export_anvil(&store_path, None, &exact, Mode::Exact).unwrap();
|
|
assert_tree_equal(&source, &exact);
|
|
let mut store = WorldStore::open(&store_path, 2).unwrap();
|
|
let stone = store.register_block("minecraft:stone").unwrap();
|
|
assert_ne!(store.get_block("main", [-1, -1, 31]).unwrap(), 0);
|
|
let revision = store.revision("main").unwrap();
|
|
store
|
|
.edit(
|
|
"main",
|
|
revision,
|
|
"edit",
|
|
vec![
|
|
BlockChange {
|
|
pos: [-1, -1, 31],
|
|
block: 0,
|
|
},
|
|
BlockChange {
|
|
pos: [512, 64, -513],
|
|
block: stone,
|
|
},
|
|
],
|
|
)
|
|
.unwrap();
|
|
drop(store);
|
|
let rejected = t.path().join("rejected");
|
|
assert!(export_anvil(&store_path, None, &rejected, Mode::Exact).is_err());
|
|
assert!(!rejected.exists());
|
|
let edited = t.path().join("edited");
|
|
let report = export_anvil(&store_path, None, &edited, Mode::BestEffort).unwrap();
|
|
assert_eq!(report.chunks, 2);
|
|
assert!(
|
|
report
|
|
.issues
|
|
.iter()
|
|
.any(|i| i.code == "dependent_records_invalidated")
|
|
);
|
|
assert_eq!(
|
|
fs::read(source.join("entities/r.-1.0.mca")).unwrap(),
|
|
fs::read(edited.join("entities/r.-1.0.mca")).unwrap()
|
|
);
|
|
let reimport = t.path().join("reimport");
|
|
import_anvil(&edited, &reimport, Mode::Exact).unwrap();
|
|
let mut store = WorldStore::open(&reimport, 2).unwrap();
|
|
assert_eq!(store.get_block("main", [-1, -1, 31]).unwrap(), 0);
|
|
let id = store.get_block("main", [512, 64, -513]).unwrap();
|
|
assert_eq!(store.registry()[id as usize], "minecraft:stone");
|
|
}
|
|
|
|
#[test]
|
|
fn anvil_baseline_preserves_roundtrip_edits_snapshots_and_reset() {
|
|
let t = tempfile::tempdir().unwrap();
|
|
let path = t.path().join("baseline");
|
|
let source = fixture("java26_2-world");
|
|
let report = import_anvil_baseline(&source, &path, Mode::Exact).unwrap();
|
|
assert_eq!(report.nonair_blocks, 3);
|
|
assert_eq!(report.worlds["main"], 0);
|
|
let exact = t.path().join("exact");
|
|
export_anvil(&path, None, &exact, Mode::Exact).unwrap();
|
|
assert_tree_equal(&source, &exact);
|
|
let db = rusqlite::Connection::open(path.join("worlds.sqlite3")).unwrap();
|
|
let history: i64 = db
|
|
.query_row("SELECT count(*) FROM operation_changes", [], |r| r.get(0))
|
|
.unwrap();
|
|
assert_eq!(history, 0);
|
|
drop(db);
|
|
let mut store = WorldStore::open(&path, 2).unwrap();
|
|
let pos = [-1, -1, 31];
|
|
let original = store.get_block("main", pos).unwrap();
|
|
assert_ne!(original, 0);
|
|
store
|
|
.edit("main", 0, "remove", vec![BlockChange { pos, block: 0 }])
|
|
.unwrap();
|
|
drop(store);
|
|
assert!(export_anvil(&path, None, &t.path().join("rejected"), Mode::Exact).is_err());
|
|
let edited = t.path().join("edited");
|
|
export_anvil(&path, None, &edited, Mode::BestEffort).unwrap();
|
|
let reimport = t.path().join("reimport");
|
|
import_anvil_baseline(&edited, &reimport, Mode::Exact).unwrap();
|
|
assert_eq!(
|
|
WorldStore::open(&reimport, 2)
|
|
.unwrap()
|
|
.get_block("main", pos)
|
|
.unwrap(),
|
|
0
|
|
);
|
|
let mut store = WorldStore::open(&path, 2).unwrap();
|
|
store.reset_world("main", 1, "reset").unwrap();
|
|
assert_eq!(store.get_block("main", pos).unwrap(), original);
|
|
store.create_world("playable", Some("main")).unwrap();
|
|
assert_eq!(store.get_block("playable", pos).unwrap(), original);
|
|
assert!(
|
|
store
|
|
.read_section("playable", [-1, -1, 1])
|
|
.unwrap()
|
|
.is_some()
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn native_new_world_export_and_schematic() {
|
|
let t = tempfile::tempdir().unwrap();
|
|
let store_path = t.path().join("store");
|
|
let mut store = WorldStore::open(&store_path, 2).unwrap();
|
|
store.create_world("native", None).unwrap();
|
|
let stone = store.register_block("minecraft:stone").unwrap();
|
|
let stairs = store
|
|
.register_block(
|
|
"minecraft:oak_stairs[facing=east,half=top,shape=straight,waterlogged=false]",
|
|
)
|
|
.unwrap();
|
|
store
|
|
.edit(
|
|
"native",
|
|
0,
|
|
"build",
|
|
vec![
|
|
BlockChange {
|
|
pos: [-1, 63, 0],
|
|
block: stone,
|
|
},
|
|
BlockChange {
|
|
pos: [0, 64, 0],
|
|
block: stairs,
|
|
},
|
|
],
|
|
)
|
|
.unwrap();
|
|
drop(store);
|
|
let world = t.path().join("world");
|
|
export_anvil(&store_path, Some("native"), &world, Mode::BestEffort).unwrap();
|
|
assert!(
|
|
world
|
|
.join("data/minecraft/world_gen_settings.dat")
|
|
.is_file()
|
|
);
|
|
let reimport = t.path().join("reimport");
|
|
assert_eq!(
|
|
import_anvil(&world, &reimport, Mode::Exact)
|
|
.unwrap()
|
|
.nonair_blocks,
|
|
2
|
|
);
|
|
let schem = t.path().join("schem");
|
|
export_schem(
|
|
&store_path,
|
|
"native",
|
|
&schem,
|
|
Some([-1, 63, 0]),
|
|
Some([0, 64, 0]),
|
|
Mode::Exact,
|
|
)
|
|
.unwrap();
|
|
let s = t.path().join("schem-reimport");
|
|
assert_eq!(
|
|
import_schem(&schem.join("world.schem"), &s, "main", Mode::Exact)
|
|
.unwrap()
|
|
.nonair_blocks,
|
|
2
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn no_overwrite_and_provenance_hash_guard() {
|
|
let t = tempfile::tempdir().unwrap();
|
|
let path = t.path().join("store");
|
|
let source = fixture("sponge-v3.schem");
|
|
import_schem(&source, &path, "main", Mode::Exact).unwrap();
|
|
let before = fs::read(path.join("worlds.sqlite3")).unwrap();
|
|
assert!(import_schem(&source, &path, "other", Mode::Exact).is_err());
|
|
assert_eq!(before, fs::read(path.join("worlds.sqlite3")).unwrap());
|
|
fs::write(path.join("compat/original/source.schem"), b"tampered").unwrap();
|
|
let out = t.path().join("out");
|
|
assert!(export_schem(&path, "main", &out, None, None, Mode::Exact).is_err());
|
|
assert!(!out.exists());
|
|
}
|
|
|
|
#[test]
|
|
fn malformed_input_is_not_published() {
|
|
let t = tempfile::tempdir().unwrap();
|
|
let bad = t.path().join("bad.schem");
|
|
fs::write(&bad, b"not gzip").unwrap();
|
|
let out = t.path().join("out");
|
|
assert!(import_schem(&bad, &out, "main", Mode::Exact).is_err());
|
|
assert!(!out.exists());
|
|
}
|
|
|
|
fn metadata(path: &Path) -> serde_json::Value {
|
|
let db = rusqlite::Connection::open(path.join("server.sqlite3")).unwrap();
|
|
let text: String = db
|
|
.query_row("SELECT json FROM metadata WHERE id=1", [], |r| r.get(0))
|
|
.unwrap();
|
|
serde_json::from_str(&text).unwrap()
|
|
}
|
|
fn save_metadata(path: &Path, v: &serde_json::Value) {
|
|
let db = rusqlite::Connection::open(path.join("server.sqlite3")).unwrap();
|
|
db.execute(
|
|
"UPDATE metadata SET json=?1 WHERE id=1",
|
|
[serde_json::to_string(v).unwrap()],
|
|
)
|
|
.unwrap();
|
|
}
|
|
#[test]
|
|
fn native_entity_edit_move_create_remove_roundtrip() {
|
|
let t = tempfile::tempdir().unwrap();
|
|
let path = t.path().join("store");
|
|
import_anvil(&fixture("java26_2-world"), &path, Mode::Exact).unwrap();
|
|
let mut meta = metadata(&path);
|
|
let id = meta["entities"]
|
|
.as_object()
|
|
.unwrap()
|
|
.keys()
|
|
.next()
|
|
.unwrap()
|
|
.clone();
|
|
meta["entities"][&id]["position"] = serde_json::json!([513.5, 65., -513.5]);
|
|
meta["entities"][&id]["yaw"] = serde_json::json!(1.25);
|
|
meta["entities"][&id]["properties"]["Invisible"] = serde_json::json!(true);
|
|
let new_id = "12345678-1234-4234-8234-123456789abc";
|
|
meta["entities"][new_id] = serde_json::json!({"id":new_id,"world":"main","kind":"minecraft:cow","type":"minecraft:cow","position":[0.5,70.,0.5],"yaw":0.,"properties":{"Health":17.0,"CustomName":"moo","custom_unmapped":{"a":1}}});
|
|
meta["revision"] = serde_json::json!(1);
|
|
save_metadata(&path, &meta);
|
|
let rejected = t.path().join("exact");
|
|
assert!(
|
|
export_anvil(&path, None, &rejected, Mode::Exact)
|
|
.unwrap_err()
|
|
.to_string()
|
|
.contains("exact_entity_revision")
|
|
);
|
|
assert!(!rejected.exists());
|
|
let out = t.path().join("out");
|
|
let r = export_anvil(&path, None, &out, Mode::BestEffort).unwrap();
|
|
assert!(
|
|
r.issues
|
|
.iter()
|
|
.any(|i| i.code == "entity_property_unmapped")
|
|
);
|
|
let again = t.path().join("again");
|
|
import_anvil(&out, &again, Mode::Exact).unwrap();
|
|
let got = metadata(&again);
|
|
assert_eq!(got["entities"].as_object().unwrap().len(), 2);
|
|
assert_eq!(
|
|
got["entities"][&id]["position"],
|
|
meta["entities"][&id]["position"]
|
|
);
|
|
assert!(
|
|
got["entities"][&id]["properties"]["Invisible"]
|
|
.as_bool()
|
|
.unwrap()
|
|
);
|
|
assert_eq!(
|
|
got["entities"][new_id]["properties"]["Health"],
|
|
serde_json::json!(17.0)
|
|
);
|
|
meta["entities"] = serde_json::json!({});
|
|
save_metadata(&path, &meta);
|
|
let removed = t.path().join("removed");
|
|
let r = export_anvil(&path, None, &removed, Mode::BestEffort).unwrap();
|
|
assert!(r.issues.iter().any(|i| i.code == "entity_removed"));
|
|
let final_store = t.path().join("final");
|
|
import_anvil(&removed, &final_store, Mode::Exact).unwrap();
|
|
assert!(
|
|
metadata(&final_store)["entities"]
|
|
.as_object()
|
|
.unwrap()
|
|
.is_empty()
|
|
);
|
|
}
|
|
#[test]
|
|
fn sponge_entity_metadata_guard_and_position_roundtrip() {
|
|
let t = tempfile::tempdir().unwrap();
|
|
let path = t.path().join("store");
|
|
import_schem(&fixture("sponge-v3.schem"), &path, "main", Mode::Exact).unwrap();
|
|
let mut meta = metadata(&path);
|
|
let id = meta["entities"]
|
|
.as_object()
|
|
.unwrap()
|
|
.keys()
|
|
.next()
|
|
.unwrap()
|
|
.clone();
|
|
meta["entities"][&id]["position"] = serde_json::json!([-15., 1., 31.]);
|
|
save_metadata(&path, &meta);
|
|
assert!(
|
|
export_schem(
|
|
&path,
|
|
"main",
|
|
&t.path().join("reject"),
|
|
None,
|
|
None,
|
|
Mode::Exact
|
|
)
|
|
.is_err()
|
|
);
|
|
let out = t.path().join("out");
|
|
export_schem(&path, "main", &out, None, None, Mode::BestEffort).unwrap();
|
|
let again = t.path().join("again");
|
|
import_schem(&out.join("world.schem"), &again, "main", Mode::Exact).unwrap();
|
|
assert_eq!(
|
|
metadata(&again)["entities"][&id]["position"],
|
|
meta["entities"][&id]["position"]
|
|
);
|
|
}
|
|
fn write_gzip(path: &Path, tag: &Tag) {
|
|
use std::io::Write;
|
|
let mut out = flate2::write::GzEncoder::new(
|
|
fs::File::create(path).unwrap(),
|
|
flate2::Compression::default(),
|
|
);
|
|
out.write_all(&nbt::encode("", tag).unwrap()).unwrap();
|
|
out.finish().unwrap();
|
|
}
|
|
#[test]
|
|
fn duplicate_entity_uuid_stays_outside_editable_subset() {
|
|
let t = tempfile::tempdir().unwrap();
|
|
let mut tag = read_gzip(&fixture("sponge-v3.schem"));
|
|
let c = tag
|
|
.compound_mut()
|
|
.unwrap()
|
|
.get_mut("Schematic")
|
|
.unwrap()
|
|
.compound_mut()
|
|
.unwrap();
|
|
if let Tag::List(_, entities) = c.get_mut("Entities").unwrap() {
|
|
entities.push(entities[0].clone());
|
|
} else {
|
|
panic!("fixture entities")
|
|
}
|
|
let source = t.path().join("duplicate.schem");
|
|
write_gzip(&source, &tag);
|
|
let store = t.path().join("store");
|
|
let report = import_schem(&source, &store, "main", Mode::Exact).unwrap();
|
|
assert!(
|
|
report
|
|
.issues
|
|
.iter()
|
|
.any(|i| i.code == "entity_duplicate_uuid")
|
|
);
|
|
assert!(metadata(&store)["entities"].as_object().unwrap().is_empty());
|
|
let out = t.path().join("out");
|
|
export_schem(&store, "main", &out, None, None, Mode::Exact).unwrap();
|
|
assert_eq!(
|
|
fs::read(source).unwrap(),
|
|
fs::read(out.join("world.schem")).unwrap()
|
|
);
|
|
}
|
|
#[test]
|
|
fn unsupported_data_version_rejects_exact_before_publication() {
|
|
let t = tempfile::tempdir().unwrap();
|
|
let mut tag = read_gzip(&fixture("sponge-v3.schem"));
|
|
let c = tag
|
|
.compound_mut()
|
|
.unwrap()
|
|
.get_mut("Schematic")
|
|
.unwrap()
|
|
.compound_mut()
|
|
.unwrap();
|
|
c.insert("DataVersion".into(), Tag::Int(100));
|
|
let source = t.path().join("old.schem");
|
|
write_gzip(&source, &tag);
|
|
let dest = t.path().join("store");
|
|
assert!(import_schem(&source, &dest, "main", Mode::Exact).is_err());
|
|
assert!(!dest.exists());
|
|
let report = import_schem(&source, &dest, "main", Mode::BestEffort).unwrap();
|
|
assert!(
|
|
report
|
|
.issues
|
|
.iter()
|
|
.any(|i| i.code == "unverified_data_version")
|
|
);
|
|
assert!(
|
|
export_schem(
|
|
&dest,
|
|
"main",
|
|
&t.path().join("reject"),
|
|
None,
|
|
None,
|
|
Mode::Exact
|
|
)
|
|
.is_err()
|
|
);
|
|
}
|