mirror of
https://github.com/LCE-Hub/LCE-Emerald-Launcher.git
synced 2026-09-23 08:32:51 +00:00
feat: xbox360+ps3+java->windows64 world conversion (#145)
Co-authored-by: str1k3r <[email protected]>
This commit is contained in:
Generated
+9
-9
@@ -1043,7 +1043,7 @@ checksum = "4ef6b89e5b37196644d8796de5268852ff179b44e96276cf4290264843743bb7"
|
||||
|
||||
[[package]]
|
||||
name = "emerald-legacy-launcher"
|
||||
version = "1.5.1"
|
||||
version = "1.6.0"
|
||||
dependencies = [
|
||||
"base64 0.21.7",
|
||||
"byteorder",
|
||||
@@ -2027,7 +2027,7 @@ dependencies = [
|
||||
"libc",
|
||||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
"socket2 0.6.3",
|
||||
"socket2 0.5.10",
|
||||
"tokio",
|
||||
"tower-service",
|
||||
"tracing",
|
||||
@@ -4708,9 +4708,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tao-macros"
|
||||
version = "0.1.3"
|
||||
version = "0.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f4e16beb8b2ac17db28eab8bca40e62dbfbb34c0fcdc6d9826b11b7b5d047dfd"
|
||||
checksum = "5f7eeb6d99155545da6150a1795945f16ac9c178deb2a5f2e74d776107bd5849"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -5402,18 +5402,18 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "toml_parser"
|
||||
version = "1.1.2+spec-1.1.0"
|
||||
version = "1.1.3+spec-1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526"
|
||||
checksum = "1d38ac1cf9b95face32296c0a3ede1fdc270627c9d9c02a7274dd6d960dc4d56"
|
||||
dependencies = [
|
||||
"winnow 1.0.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_writer"
|
||||
version = "1.1.1+spec-1.1.0"
|
||||
version = "1.1.2+spec-1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db"
|
||||
checksum = "7d56353a2a665ad0f41a421187180aab746c8c325620617ad883a99a1cbe66d2"
|
||||
|
||||
[[package]]
|
||||
name = "tower"
|
||||
@@ -6098,7 +6098,7 @@ version = "0.1.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
|
||||
dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
[package]
|
||||
name = "emerald-legacy-launcher"
|
||||
version = "1.5.1"
|
||||
version = "1.6.0"
|
||||
description = "A FOSS, cross-platform launcher for Minecraft Legacy Edition"
|
||||
authors = ["Emerald Team"]
|
||||
edition = "2021"
|
||||
edition = "2024"
|
||||
|
||||
[lib]
|
||||
name = "emerald_lib"
|
||||
@@ -13,7 +13,7 @@ crate-type = ["staticlib", "cdylib", "rlib"]
|
||||
tauri-build = { version = "2", features = [] }
|
||||
|
||||
[dependencies]
|
||||
tauri = { version = "2", features = [ "tray-icon", "image-png"] }
|
||||
tauri = { version = "2", features = ["tray-icon", "image-png"] }
|
||||
tauri-plugin-opener = "2"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
@@ -43,3 +43,8 @@ tauri-plugin-single-instance = "2"
|
||||
|
||||
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
|
||||
tauri-plugin-updater = "2"
|
||||
|
||||
#neo: enable portable appimages. revert back to crates.io once the PR is merged
|
||||
#[patch.crates-io]
|
||||
#tauri = { git = "https://github.com/tauri-apps/tauri", branch = "feat/truly-portable-appimage" }
|
||||
#end neo
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
+106
-86
@@ -1,8 +1,14 @@
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
use std::process::Stdio;
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::sync::Arc;
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
use tauri::{AppHandle, Emitter, Manager, State};
|
||||
use tauri_plugin_opener::OpenerExt;
|
||||
use tokio::io::AsyncRead;
|
||||
use tokio::io::AsyncReadExt;
|
||||
use tokio::sync::Mutex;
|
||||
use crate::commands::runners;
|
||||
use crate::config;
|
||||
use crate::types::AppConfig;
|
||||
@@ -24,6 +30,7 @@ pub async fn launch_game(
|
||||
mut servers: Vec<McServer>,
|
||||
extra_args: Vec<String>,
|
||||
) -> Result<(), String> {
|
||||
state.manual_stop.store(false, Ordering::SeqCst);
|
||||
perform_instance_sync(&app, &instance_id).await?;
|
||||
let working_dir = util::get_instance_working_dir(&app, &instance_id);
|
||||
let config_val = config::load_config_raw(app.clone());
|
||||
@@ -117,42 +124,18 @@ pub async fn launch_game(
|
||||
|
||||
apply_launch_env_vars(&mut cmd, &config_val);
|
||||
cmd.current_dir(&working_dir);
|
||||
cmd.env("WINEDEBUG", "+debugstr");
|
||||
let playtime_start = std::time::Instant::now();
|
||||
let child = cmd.spawn().map_err(|e| e.to_string())?;
|
||||
{
|
||||
let mut lock = state.child.lock().await;
|
||||
*lock = Some(child);
|
||||
}
|
||||
|
||||
let status = loop {
|
||||
{
|
||||
let mut lock = state.child.lock().await;
|
||||
if let Some(ref mut c) = *lock {
|
||||
if let Some(s) = c.try_wait().map_err(|e| e.to_string())? {
|
||||
break s;
|
||||
}
|
||||
} else {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
|
||||
let Some(result) = run_game_and_capture(&state, cmd).await? else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
{
|
||||
let mut lock = state.child.lock().await;
|
||||
*lock = None;
|
||||
}
|
||||
|
||||
let duration = playtime_start.elapsed();
|
||||
let now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs();
|
||||
let start = now - duration.as_secs();
|
||||
playtime::record_session(&app, &instance_id, start, now);
|
||||
|
||||
return if status.success() || status.code() == Some(253) || status.code() == Some(96) {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(format!("Game exited with status: {}", status))
|
||||
};
|
||||
return handle_game_exit(&app, &state, result);
|
||||
}
|
||||
}
|
||||
Err("No Linux runner selected in settings.".into())
|
||||
@@ -207,7 +190,7 @@ pub async fn launch_game(
|
||||
apply_launch_env_vars(&mut cmd, &config_val);
|
||||
cmd.current_dir(&working_dir);
|
||||
cmd.env("WINEPREFIX", &prefix_dir);
|
||||
cmd.env("WINEDEBUG", "-all");
|
||||
cmd.env("WINEDEBUG", "+debugstr");
|
||||
let perf_boost = config_val.apple_silicon_performance_boost.unwrap_or(false);
|
||||
if perf_boost {
|
||||
#[cfg(target_arch = "aarch64")]
|
||||
@@ -233,46 +216,19 @@ pub async fn launch_game(
|
||||
std::env::var("PATH").unwrap_or_default()
|
||||
),
|
||||
);
|
||||
cmd.stdin(std::process::Stdio::null())
|
||||
.stdout(std::process::Stdio::null())
|
||||
.stderr(std::process::Stdio::null());
|
||||
cmd.stdin(std::process::Stdio::null());
|
||||
|
||||
let playtime_start = std::time::Instant::now();
|
||||
let child = cmd.spawn().map_err(|e| e.to_string())?;
|
||||
{
|
||||
let mut lock = state.child.lock().await;
|
||||
*lock = Some(child);
|
||||
}
|
||||
|
||||
let status = loop {
|
||||
{
|
||||
let mut lock = state.child.lock().await;
|
||||
if let Some(ref mut c) = *lock {
|
||||
if let Some(s) = c.try_wait().map_err(|e| e.to_string())? {
|
||||
break s;
|
||||
}
|
||||
} else {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
|
||||
let Some(result) = run_game_and_capture(&state, cmd).await? else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
{
|
||||
let mut lock = state.child.lock().await;
|
||||
*lock = None;
|
||||
}
|
||||
|
||||
let duration = playtime_start.elapsed();
|
||||
let now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs();
|
||||
let start = now - duration.as_secs();
|
||||
playtime::record_session(&app, &instance_id, start, now);
|
||||
|
||||
return if status.success() || status.code() == Some(253) || status.code() == Some(96) {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(format!("Game exited with status: {}", status))
|
||||
};
|
||||
return handle_game_exit(&app, &state, result);
|
||||
}
|
||||
|
||||
#[cfg(all(not(target_os = "macos"), not(target_os = "linux")))]
|
||||
@@ -289,37 +245,14 @@ pub async fn launch_game(
|
||||
apply_launch_env_vars(&mut cmd, &config_val);
|
||||
cmd.current_dir(&working_dir);
|
||||
let playtime_start = std::time::Instant::now();
|
||||
let child = cmd.spawn().map_err(|e| e.to_string())?;
|
||||
{
|
||||
let mut lock = state.child.lock().await;
|
||||
*lock = Some(child);
|
||||
}
|
||||
let status = loop {
|
||||
{
|
||||
let mut lock = state.child.lock().await;
|
||||
if let Some(ref mut c) = *lock {
|
||||
if let Some(s) = c.try_wait().map_err(|e| e.to_string())? {
|
||||
break s;
|
||||
}
|
||||
} else {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
|
||||
let Some(result) = run_game_and_capture(&state, cmd).await? else {
|
||||
return Ok(());
|
||||
};
|
||||
{
|
||||
let mut lock = state.child.lock().await;
|
||||
*lock = None;
|
||||
}
|
||||
let duration = playtime_start.elapsed();
|
||||
let now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs();
|
||||
let start = now - duration.as_secs();
|
||||
playtime::record_session(&app, &instance_id, start, now);
|
||||
return if status.success() || status.code() == Some(253) || status.code() == Some(96) {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(format!("Game exited with status: {}", status))
|
||||
};
|
||||
return handle_game_exit(&app, &state, result);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -330,6 +263,7 @@ pub async fn stop_game(
|
||||
instance_id: String,
|
||||
state: State<'_, GameState>,
|
||||
) -> Result<(), String> {
|
||||
state.manual_stop.store(true, Ordering::SeqCst);
|
||||
let mut lock = state.child.lock().await;
|
||||
if let Some(mut child) = lock.take() {
|
||||
#[cfg(unix)]
|
||||
@@ -631,6 +565,92 @@ fn apply_launch_env_vars(cmd: &mut tokio::process::Command, config: &AppConfig)
|
||||
}
|
||||
}
|
||||
|
||||
const MAX_LOG_BYTES: usize = 1024 * 1024;
|
||||
struct GameRunResult {
|
||||
log: String,
|
||||
}
|
||||
|
||||
fn spawn_log_reader<R>(mut reader: R, log: Arc<Mutex<Vec<u8>>>) -> tokio::task::JoinHandle<()> where R: AsyncRead + Unpin + Send + 'static, {
|
||||
tokio::spawn(async move {
|
||||
let mut buf = [0u8; 8192];
|
||||
loop {
|
||||
match reader.read(&mut buf).await {
|
||||
Ok(0) | Err(_) => break,
|
||||
Ok(n) => {
|
||||
let mut guard = log.lock().await;
|
||||
guard.extend_from_slice(&buf[..n]);
|
||||
if guard.len() > MAX_LOG_BYTES {
|
||||
let remove = guard.len() - MAX_LOG_BYTES;
|
||||
guard.drain(..remove);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async fn run_game_and_capture(
|
||||
state: &State<'_, GameState>,
|
||||
mut cmd: tokio::process::Command,
|
||||
) -> Result<Option<GameRunResult>, String> {
|
||||
cmd.stdout(Stdio::piped()).stderr(Stdio::piped());
|
||||
let mut child = cmd.spawn().map_err(|e| e.to_string())?;
|
||||
let log = Arc::new(Mutex::new(Vec::new()));
|
||||
let mut handles = Vec::new();
|
||||
if let Some(stdout) = child.stdout.take() {
|
||||
handles.push(spawn_log_reader(stdout, log.clone()));
|
||||
}
|
||||
if let Some(stderr) = child.stderr.take() {
|
||||
handles.push(spawn_log_reader(stderr, log.clone()));
|
||||
}
|
||||
{
|
||||
let mut lock = state.child.lock().await;
|
||||
*lock = Some(child);
|
||||
}
|
||||
loop {
|
||||
{
|
||||
let mut lock = state.child.lock().await;
|
||||
if let Some(ref mut c) = *lock {
|
||||
if c.try_wait().map_err(|e| e.to_string())?.is_some() {
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
return Ok(None);
|
||||
}
|
||||
}
|
||||
tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
|
||||
}
|
||||
{
|
||||
let mut lock = state.child.lock().await;
|
||||
*lock = None;
|
||||
}
|
||||
for handle in handles {
|
||||
let _ = handle.await;
|
||||
}
|
||||
let bytes = log.lock().await.clone();
|
||||
let log_str = String::from_utf8_lossy(&bytes).to_string();
|
||||
Ok(Some(GameRunResult { log: log_str }))
|
||||
}
|
||||
|
||||
fn game_exited_ok(log: &str) -> bool {
|
||||
log.lines()
|
||||
.rev()
|
||||
.take(3)
|
||||
.any(|line| line.contains("AppPolicyGetProcessTerminationMethod"))
|
||||
}
|
||||
|
||||
fn handle_game_exit(
|
||||
app: &AppHandle,
|
||||
state: &State<'_, GameState>,
|
||||
result: GameRunResult,
|
||||
) -> Result<(), String> {
|
||||
if state.manual_stop.swap(false, Ordering::SeqCst) || game_exited_ok(&result.log) {
|
||||
return Ok(());
|
||||
}
|
||||
let _ = app.emit("game-log", result.log);
|
||||
Err("The game exited unexpectedly. Check the crash log for details.".into())
|
||||
}
|
||||
|
||||
async fn perform_instance_sync(app: &AppHandle, instance_id: &str) -> Result<(), String> {
|
||||
let target_dir = util::get_instance_working_dir(app, instance_id);
|
||||
if !target_dir.exists() {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,261 @@
|
||||
use super::nbt::{self, NbtCompound, NbtValue};
|
||||
pub struct SpawnPoint {
|
||||
pub x: i32,
|
||||
pub y: i32,
|
||||
pub z: i32,
|
||||
}
|
||||
|
||||
pub fn read_spawn(root: &NbtCompound) -> SpawnPoint {
|
||||
let data = match root.compound("Data") {
|
||||
Some(d) => d,
|
||||
None => {
|
||||
return SpawnPoint { x: 0, y: 64, z: 0 };
|
||||
}
|
||||
};
|
||||
|
||||
if let (Some(sx), Some(sz)) = (data.int("SpawnX"), data.int("SpawnZ")) {
|
||||
return SpawnPoint {
|
||||
x: sx,
|
||||
y: data.int("SpawnY").unwrap_or(64),
|
||||
z: sz,
|
||||
};
|
||||
}
|
||||
|
||||
if let Some(spawn_comp) = data.compound("spawn") {
|
||||
if let Some(pos) = spawn_comp.int_array("pos") {
|
||||
if pos.len() >= 3 {
|
||||
return SpawnPoint {
|
||||
x: pos[0],
|
||||
y: pos[1],
|
||||
z: pos[2],
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SpawnPoint { x: 0, y: 64, z: 0 }
|
||||
}
|
||||
|
||||
pub fn convert_java_to_lce(
|
||||
java_root: &NbtCompound,
|
||||
spawn_chunk_x: i32,
|
||||
spawn_chunk_z: i32,
|
||||
xz_size: i32,
|
||||
flat_world: bool,
|
||||
override_spawn_y: Option<i32>,
|
||||
) -> Vec<u8> {
|
||||
let java_data = java_root
|
||||
.compound("Data")
|
||||
.expect("Java level.dat missing 'Data' compound tag");
|
||||
|
||||
let data_version = java_data
|
||||
.int("DataVersion")
|
||||
.or_else(|| java_root.int("DataVersion"))
|
||||
.unwrap_or(0);
|
||||
let is_modern_world = data_version >= 1519;
|
||||
let hell_scale: i32 = 3;
|
||||
let spawn = read_spawn(java_root);
|
||||
let spawn_x = spawn.x;
|
||||
let spawn_y = if let Some(oy) = override_spawn_y {
|
||||
oy.clamp(1, 127)
|
||||
} else if is_modern_world {
|
||||
64
|
||||
} else {
|
||||
spawn.y.clamp(1, 127)
|
||||
};
|
||||
let spawn_z = spawn.z;
|
||||
let new_spawn_x = spawn_x - (spawn_chunk_x * 16);
|
||||
let new_spawn_z = spawn_z - (spawn_chunk_z * 16);
|
||||
let safe_generator_name = if flat_world {
|
||||
"flat".to_string()
|
||||
} else if is_modern_world {
|
||||
"default".to_string()
|
||||
} else {
|
||||
let name = get_string(java_data, "generatorName", "default");
|
||||
match name.as_str() {
|
||||
"flat" | "superflat" => "flat".to_string(),
|
||||
"largeBiomes" => "largeBiomes".to_string(),
|
||||
_ => "default".to_string(),
|
||||
}
|
||||
};
|
||||
let safe_generator_version = if flat_world {
|
||||
0
|
||||
} else if is_modern_world {
|
||||
1
|
||||
} else {
|
||||
let version = get_int(java_data, "generatorVersion");
|
||||
if safe_generator_name == "default" {
|
||||
1
|
||||
} else {
|
||||
version
|
||||
}
|
||||
};
|
||||
let safe_generator_options = if flat_world {
|
||||
get_string(java_data, "generatorOptions", "2;7,2x3,2;1;")
|
||||
} else if is_modern_world {
|
||||
"".to_string()
|
||||
} else {
|
||||
get_string(java_data, "generatorOptions", "")
|
||||
};
|
||||
|
||||
let mut lce_data = NbtCompound::new("Data");
|
||||
lce_data.insert("RandomSeed", NbtValue::Long(get_long(java_data, "RandomSeed")));
|
||||
lce_data.insert("generatorName", NbtValue::String(safe_generator_name));
|
||||
lce_data.insert("generatorVersion", NbtValue::Int(safe_generator_version));
|
||||
lce_data.insert("generatorOptions", NbtValue::String(safe_generator_options));
|
||||
lce_data.insert("GameType", NbtValue::Int(get_int(java_data, "GameType")));
|
||||
lce_data.insert("MapFeatures", NbtValue::Byte(get_bool(java_data, "MapFeatures", true)));
|
||||
lce_data.insert("SpawnX", NbtValue::Int(new_spawn_x));
|
||||
lce_data.insert("SpawnY", NbtValue::Int(spawn_y));
|
||||
lce_data.insert("SpawnZ", NbtValue::Int(new_spawn_z));
|
||||
lce_data.insert("Time", NbtValue::Long(get_long(java_data, "Time")));
|
||||
lce_data.insert("DayTime", NbtValue::Long(get_long(java_data, "DayTime")));
|
||||
lce_data.insert("SizeOnDisk", NbtValue::Long(0));
|
||||
lce_data.insert("LastPlayed", NbtValue::Long(unix_timestamp_millis()));
|
||||
lce_data.insert(
|
||||
"LevelName",
|
||||
NbtValue::String(get_string(java_data, "LevelName", "Converted World")),
|
||||
);
|
||||
lce_data.insert("version", NbtValue::Int(19133));
|
||||
lce_data.insert("rainTime", NbtValue::Int(get_int(java_data, "rainTime")));
|
||||
lce_data.insert("raining", NbtValue::Byte(get_bool(java_data, "raining", false)));
|
||||
lce_data.insert("thunderTime", NbtValue::Int(get_int(java_data, "thunderTime")));
|
||||
lce_data.insert(
|
||||
"thundering",
|
||||
NbtValue::Byte(get_bool(java_data, "thundering", false)),
|
||||
);
|
||||
lce_data.insert(
|
||||
"hardcore",
|
||||
NbtValue::Byte(get_bool(java_data, "hardcore", false)),
|
||||
);
|
||||
lce_data.insert(
|
||||
"allowCommands",
|
||||
NbtValue::Byte(get_bool(java_data, "allowCommands", false)),
|
||||
);
|
||||
lce_data.insert(
|
||||
"initialized",
|
||||
NbtValue::Byte(get_bool(java_data, "initialized", true)),
|
||||
);
|
||||
|
||||
lce_data.insert("newSeaLevel", NbtValue::Byte(1));
|
||||
lce_data.insert(
|
||||
"hasBeenInCreative",
|
||||
NbtValue::Byte(get_bool(java_data, "hasBeenInCreative", false)),
|
||||
);
|
||||
lce_data.insert(
|
||||
"spawnBonusChest",
|
||||
NbtValue::Byte(get_bool(java_data, "spawnBonusChest", false)),
|
||||
);
|
||||
|
||||
lce_data.insert("hasStronghold", NbtValue::Byte(0));
|
||||
lce_data.insert("StrongholdX", NbtValue::Int(0));
|
||||
lce_data.insert("StrongholdY", NbtValue::Int(0));
|
||||
lce_data.insert("StrongholdZ", NbtValue::Int(0));
|
||||
lce_data.insert("hasStrongholdEndPortal", NbtValue::Byte(0));
|
||||
lce_data.insert("StrongholdEndPortalX", NbtValue::Int(0));
|
||||
lce_data.insert("StrongholdEndPortalZ", NbtValue::Int(0));
|
||||
lce_data.insert("XZSize", NbtValue::Int(xz_size));
|
||||
lce_data.insert("HellScale", NbtValue::Int(hell_scale));
|
||||
if !is_modern_world {
|
||||
if let Some(game_rules) = java_data.get("GameRules") {
|
||||
lce_data.insert("GameRules", game_rules.clone());
|
||||
}
|
||||
}
|
||||
|
||||
let mut root = NbtCompound::new("");
|
||||
root.insert("Data", NbtValue::Compound(lce_data));
|
||||
nbt::write_nbt(&root)
|
||||
}
|
||||
|
||||
pub fn convert_lce_to_java(
|
||||
lce_root: &NbtCompound,
|
||||
override_spawn_x: Option<i32>,
|
||||
override_spawn_y: Option<i32>,
|
||||
override_spawn_z: Option<i32>,
|
||||
embedded_player: Option<&NbtCompound>,
|
||||
) -> Vec<u8> {
|
||||
let lce_data = lce_root.compound("Data").unwrap_or(lce_root);
|
||||
let mut java_data = lce_data.clone();
|
||||
let lce_only_fields = [
|
||||
"newSeaLevel",
|
||||
"hasBeenInCreative",
|
||||
"spawnBonusChest",
|
||||
"XZSize",
|
||||
"xzSize",
|
||||
"HellScale",
|
||||
"hellScale",
|
||||
"hasStronghold",
|
||||
"StrongholdX",
|
||||
"StrongholdY",
|
||||
"StrongholdZ",
|
||||
"hasStrongholdEndPortal",
|
||||
"StrongholdEndPortalX",
|
||||
"StrongholdEndPortalZ",
|
||||
"xStronghold",
|
||||
"yStronghold",
|
||||
"zStronghold",
|
||||
"hasStrongholdEP",
|
||||
"xStrongholdEP",
|
||||
"zStrongholdEP",
|
||||
];
|
||||
|
||||
for field in lce_only_fields {
|
||||
java_data.remove(field);
|
||||
}
|
||||
|
||||
if let Some(sx) = override_spawn_x {
|
||||
java_data.insert("SpawnX", NbtValue::Int(sx));
|
||||
}
|
||||
if let Some(sy) = override_spawn_y {
|
||||
java_data.insert("SpawnY", NbtValue::Int(sy));
|
||||
}
|
||||
if let Some(sz) = override_spawn_z {
|
||||
java_data.insert("SpawnZ", NbtValue::Int(sz));
|
||||
}
|
||||
|
||||
java_data.insert("version", NbtValue::Int(19133));
|
||||
java_data.insert("DataVersion", NbtValue::Int(1343));
|
||||
let mut version_compound = NbtCompound::new("Version");
|
||||
version_compound.insert("Id", NbtValue::Int(1343));
|
||||
version_compound.insert("Name", NbtValue::String("1.12.2".to_string()));
|
||||
version_compound.insert("Snapshot", NbtValue::Byte(0));
|
||||
java_data.insert("Version", NbtValue::Compound(version_compound));
|
||||
if let Some(player) = embedded_player {
|
||||
let mut player_tag = player.clone();
|
||||
player_tag.name = "Player".to_string();
|
||||
java_data.insert("Player", NbtValue::Compound(player_tag));
|
||||
}
|
||||
|
||||
java_data.insert("LastPlayed", NbtValue::Long(unix_timestamp_millis()));
|
||||
let mut java_root = NbtCompound::new("");
|
||||
let mut data_tag = NbtCompound::new("Data");
|
||||
for (name, value) in &java_data.tags {
|
||||
data_tag.insert(name, value.clone());
|
||||
}
|
||||
java_root.insert("Data", NbtValue::Compound(data_tag));
|
||||
|
||||
nbt::write_gzip_nbt(&java_root)
|
||||
}
|
||||
|
||||
fn get_long(compound: &NbtCompound, name: &str) -> i64 {
|
||||
compound.long(name).unwrap_or(0)
|
||||
}
|
||||
|
||||
fn get_int(compound: &NbtCompound, name: &str) -> i32 {
|
||||
compound.int(name).unwrap_or(0)
|
||||
}
|
||||
|
||||
fn get_string(compound: &NbtCompound, name: &str, default: &str) -> String {
|
||||
compound.string(name).unwrap_or(default).to_string()
|
||||
}
|
||||
|
||||
fn get_bool(compound: &NbtCompound, name: &str, default: bool) -> i8 {
|
||||
compound.byte(name).unwrap_or(if default { 1 } else { 0 })
|
||||
}
|
||||
|
||||
fn unix_timestamp_millis() -> i64 {
|
||||
std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap_or_default()
|
||||
.as_millis() as i64
|
||||
}
|
||||
@@ -0,0 +1,404 @@
|
||||
pub mod block_mapping;
|
||||
pub mod chunk;
|
||||
pub mod payload;
|
||||
pub mod level_dat;
|
||||
pub mod nbt;
|
||||
pub mod region;
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
use tauri::AppHandle;
|
||||
use std::collections::HashMap;
|
||||
use region::{JavaWorldReader, JavaRegionFileWriter, LceRegionFile, SaveDataContainer};
|
||||
use chunk::{convert_chunk, build_modern_anvil_level};
|
||||
use level_dat::{read_spawn, convert_java_to_lce, convert_lce_to_java};
|
||||
const DEFAULT_XZ_SIZE: i32 = 54;
|
||||
fn floor_div(a: i32, b: i32) -> i32 {
|
||||
let q = a / b;
|
||||
let r = a % b;
|
||||
if r != 0 && ((r < 0) != (b < 0)) {
|
||||
q - 1
|
||||
} else {
|
||||
q
|
||||
}
|
||||
}
|
||||
fn estimate_safe_spawn_y(
|
||||
reader: &mut JavaWorldReader,
|
||||
spawn_x: i32,
|
||||
source_spawn_y: i32,
|
||||
spawn_z: i32,
|
||||
spawn_chunk_x: i32,
|
||||
spawn_chunk_z: i32,
|
||||
) -> Option<i32> {
|
||||
let region_x = floor_div(spawn_chunk_x, 32);
|
||||
let region_z = floor_div(spawn_chunk_z, 32);
|
||||
let region_path = reader
|
||||
.get_region_files("")
|
||||
.into_iter()
|
||||
.find(|(rx, rz, _)| *rx == region_x && *rz == region_z)
|
||||
.map(|(_, _, path)| path)?;
|
||||
|
||||
let local_chunk_x = ((spawn_chunk_x % 32) + 32) % 32;
|
||||
let local_chunk_z = ((spawn_chunk_z % 32) + 32) % 32;
|
||||
let root = match reader.read_chunk_nbt(®ion_path, local_chunk_x, local_chunk_z) {
|
||||
Ok(Some(r)) => r,
|
||||
_ => return None,
|
||||
};
|
||||
|
||||
let level = root.compound("Level").unwrap_or(&root);
|
||||
let lx = ((spawn_x % 16) + 16) % 16;
|
||||
let lz = ((spawn_z % 16) + 16) % 16;
|
||||
let idx2d = (lx + lz * 16) as usize;
|
||||
if let Some(hm) = level.byte_array("HeightMap") {
|
||||
if hm.len() >= 256 {
|
||||
let height = hm[idx2d];
|
||||
if height > 0 {
|
||||
return Some((height as i32 + 1).clamp(1, 127));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(hm) = level.int_array("HeightMap") {
|
||||
if hm.len() >= 256 {
|
||||
let height = hm[idx2d];
|
||||
if height > 0 {
|
||||
return Some((height + 1).clamp(1, 127));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(blocks) = level.byte_array("Blocks") {
|
||||
if blocks.len() >= 32768 {
|
||||
for y in (1..=127).rev() {
|
||||
let flat_index = (y * 256 + lz * 16 + lx) as usize;
|
||||
if blocks[flat_index] != 0 {
|
||||
return Some((y + 1).clamp(1, 127));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(sections) = level.list("Sections").or_else(|| level.list("sections")) {
|
||||
let mut max_y = -1;
|
||||
for tag in sections {
|
||||
if let nbt::NbtValue::Compound(section) = tag {
|
||||
let section_y = match chunk::read_section_y(section) {
|
||||
Some(y) if (0..=7).contains(&y) => y,
|
||||
_ => continue,
|
||||
};
|
||||
if let Some(section_blocks) = section.byte_array("Blocks") {
|
||||
if section_blocks.len() >= 4096 {
|
||||
for y in (0..=15).rev() {
|
||||
let index = (lx + lz * 16 + y * 256) as usize;
|
||||
if section_blocks[index] != 0 {
|
||||
let global_y = section_y * 16 + y;
|
||||
if global_y > max_y {
|
||||
max_y = global_y;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if max_y >= 0 {
|
||||
return Some((max_y + 1).clamp(1, 127));
|
||||
}
|
||||
}
|
||||
|
||||
Some(source_spawn_y.clamp(1, 127))
|
||||
}
|
||||
|
||||
fn convert_dimension(
|
||||
reader: &mut JavaWorldReader,
|
||||
container: &mut SaveDataContainer,
|
||||
dimension: &str,
|
||||
half_size: i32,
|
||||
offset_chunk_x: i32,
|
||||
offset_chunk_z: i32,
|
||||
global_section_shift: &mut Option<i32>,
|
||||
errors: &mut Vec<String>,
|
||||
) -> usize {
|
||||
let region_files = reader.get_region_files(dimension);
|
||||
let region_lookup: HashMap<(i32, i32), String> = region_files
|
||||
.into_iter()
|
||||
.map(|(rx, rz, path)| ((rx, rz), path))
|
||||
.collect();
|
||||
|
||||
let lce_prefix = match dimension {
|
||||
"DIM-1" => "DIM-1",
|
||||
"DIM1" => "DIM1/",
|
||||
_ => "",
|
||||
};
|
||||
|
||||
let mut lce_regions: HashMap<(i32, i32), LceRegionFile> = HashMap::new();
|
||||
let mut converted = 0usize;
|
||||
for lcx in -half_size..half_size {
|
||||
for lcz in -half_size..half_size {
|
||||
let jx = lcx + offset_chunk_x;
|
||||
let jz = lcz + offset_chunk_z;
|
||||
let jrx = floor_div(jx, 32);
|
||||
let jrz = floor_div(jz, 32);
|
||||
let region_path = match region_lookup.get(&(jrx, jrz)) {
|
||||
Some(p) => p,
|
||||
None => continue,
|
||||
};
|
||||
let local_x = ((jx % 32) + 32) % 32;
|
||||
let local_z = ((jz % 32) + 32) % 32;
|
||||
let root = match reader.read_chunk_nbt(region_path, local_x, local_z) {
|
||||
Ok(Some(r)) => r,
|
||||
Ok(None) => continue,
|
||||
Err(e) => {
|
||||
errors.push(format!("{} ({},{}): {}", dimension, lcx, lcz, e));
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
let payload = convert_chunk(&root, lcx, lcz, false, global_section_shift);
|
||||
if payload.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let lrx = floor_div(lcx, 32);
|
||||
let lrz = floor_div(lcz, 32);
|
||||
let lce_local_x = ((lcx % 32) + 32) % 32;
|
||||
let lce_local_z = ((lcz % 32) + 32) % 32;
|
||||
let region = lce_regions.entry((lrx, lrz)).or_insert_with(|| {
|
||||
LceRegionFile::new(&format!("{}r.{}.{}.mcr", lce_prefix, lrx, lrz))
|
||||
});
|
||||
region.write_chunk(lce_local_x, lce_local_z, payload);
|
||||
converted += 1;
|
||||
}
|
||||
}
|
||||
|
||||
for (_, region) in lce_regions.iter_mut() {
|
||||
region.write_to_container(container);
|
||||
}
|
||||
|
||||
converted
|
||||
}
|
||||
#[tauri::command]
|
||||
#[allow(non_snake_case)]
|
||||
pub async fn java_to_lce(
|
||||
_app: AppHandle,
|
||||
java_world_path: String,
|
||||
output_ms_path: String,
|
||||
) -> Result<String, String> {
|
||||
let mut reader = JavaWorldReader::new(&java_world_path);
|
||||
let java_root = reader.read_level_dat().map_err(|e| format!("Failed to read level.dat: {}", e))?;
|
||||
let spawn = read_spawn(&java_root);
|
||||
let spawn_chunk_x = spawn.x >> 4;
|
||||
let spawn_chunk_z = spawn.z >> 4;
|
||||
let xz_size = DEFAULT_XZ_SIZE;
|
||||
let half_size = xz_size / 2;
|
||||
let hell_scale = 3;
|
||||
let hell_half_size = (xz_size / hell_scale) / 2;
|
||||
let end_half_size = 9;
|
||||
let mut container = SaveDataContainer::new(7, 9);
|
||||
let default_region_order = [
|
||||
"DIM-1r.-1.-1.mcr", "DIM-1r.0.-1.mcr", "DIM-1r.0.0.mcr", "DIM-1r.-1.0.mcr",
|
||||
"DIM1/r.-1.-1.mcr", "DIM1/r.0.-1.mcr", "DIM1/r.0.0.mcr", "DIM1/r.-1.0.mcr",
|
||||
"r.-1.-1.mcr", "r.0.-1.mcr", "r.0.0.mcr", "r.-1.0.mcr",
|
||||
];
|
||||
for name in default_region_order {
|
||||
container.create_file(name);
|
||||
}
|
||||
|
||||
let estimated_spawn_y = estimate_safe_spawn_y(
|
||||
&mut reader,
|
||||
spawn.x,
|
||||
spawn.y,
|
||||
spawn.z,
|
||||
spawn_chunk_x,
|
||||
spawn_chunk_z,
|
||||
);
|
||||
|
||||
let mut global_section_shift: Option<i32> = None;
|
||||
let mut errors: Vec<String> = Vec::new();
|
||||
let ow = convert_dimension(
|
||||
&mut reader,
|
||||
&mut container,
|
||||
"",
|
||||
half_size,
|
||||
spawn_chunk_x,
|
||||
spawn_chunk_z,
|
||||
&mut global_section_shift,
|
||||
&mut errors,
|
||||
);
|
||||
let nether = convert_dimension(
|
||||
&mut reader,
|
||||
&mut container,
|
||||
"DIM-1",
|
||||
hell_half_size,
|
||||
floor_div(spawn_chunk_x, 8),
|
||||
floor_div(spawn_chunk_z, 8),
|
||||
&mut global_section_shift,
|
||||
&mut errors,
|
||||
);
|
||||
let end = convert_dimension(
|
||||
&mut reader,
|
||||
&mut container,
|
||||
"DIM1",
|
||||
end_half_size,
|
||||
0,
|
||||
0,
|
||||
&mut global_section_shift,
|
||||
&mut errors,
|
||||
);
|
||||
|
||||
let level_dat_bytes = convert_java_to_lce(
|
||||
&java_root,
|
||||
spawn_chunk_x,
|
||||
spawn_chunk_z,
|
||||
xz_size,
|
||||
false,
|
||||
estimated_spawn_y,
|
||||
);
|
||||
let ld_idx = container.create_file("level.dat");
|
||||
container.write_to_file(ld_idx, &level_dat_bytes);
|
||||
container.save(&output_ms_path).map_err(|e| format!("Failed to save output: {}", e))?;
|
||||
let chunks_written = ow + nether + end;
|
||||
let mut msg = format!(
|
||||
"Conversion complete!\nChunks: {}\nOverworld: {}\nNether: {}\nEnd: {}",
|
||||
chunks_written, ow, nether, end
|
||||
);
|
||||
if !errors.is_empty() {
|
||||
msg.push_str(&format!("\nErrors: {}", errors.len()));
|
||||
for err in errors.iter().take(5) {
|
||||
msg.push_str(&format!("\n {}", err));
|
||||
}
|
||||
if errors.len() > 5 {
|
||||
msg.push_str(&format!("\n ... and {} more", errors.len() - 5));
|
||||
}
|
||||
}
|
||||
eprintln!("{}",msg);
|
||||
Ok(msg)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[allow(non_snake_case)]
|
||||
pub async fn lce_to_java(
|
||||
_app: AppHandle,
|
||||
input_ms_path: String,
|
||||
java_world_output: String,
|
||||
) -> Result<String, String> {
|
||||
let ms_data = fs::read(&input_ms_path).map_err(|e| format!("Failed to read saveData.ms: {}", e))?;
|
||||
let decompressed = region::decompress_zlib(&ms_data[8..]).map_err(|e| format!("Failed to decompress save data: {}", e))?;
|
||||
let footer_offset = u32::from_le_bytes([
|
||||
decompressed[0], decompressed[1], decompressed[2], decompressed[3],
|
||||
]) as usize;
|
||||
let entry_count = u32::from_le_bytes([
|
||||
decompressed[4], decompressed[5], decompressed[6], decompressed[7],
|
||||
]) as usize;
|
||||
let mut files: Vec<(String, Vec<u8>)> = Vec::new();
|
||||
for i in 0..entry_count {
|
||||
let base = footer_offset + i * 144;
|
||||
if base + 144 > decompressed.len() {
|
||||
break;
|
||||
}
|
||||
|
||||
let name_bytes = &decompressed[base..base + 128];
|
||||
let name: String = name_bytes
|
||||
.chunks_exact(2)
|
||||
.map(|c| u16::from_le_bytes([c[0], c[1]]))
|
||||
.take_while(|&c| c != 0)
|
||||
.filter_map(|c| char::from_u32(c as u32))
|
||||
.collect();
|
||||
|
||||
let length = u32::from_le_bytes([
|
||||
decompressed[base + 128],
|
||||
decompressed[base + 129],
|
||||
decompressed[base + 130],
|
||||
decompressed[base + 131],
|
||||
]) as usize;
|
||||
let start_offset = u32::from_le_bytes([
|
||||
decompressed[base + 132],
|
||||
decompressed[base + 133],
|
||||
decompressed[base + 134],
|
||||
decompressed[base + 135],
|
||||
]) as usize;
|
||||
if !name.is_empty() && length > 0 && start_offset + length <= decompressed.len() {
|
||||
let data = decompressed[start_offset..start_offset + length].to_vec();
|
||||
files.push((name, data));
|
||||
}
|
||||
}
|
||||
|
||||
let out_dir = Path::new(&java_world_output);
|
||||
fs::create_dir_all(out_dir).map_err(|e| format!("Failed to create output directory: {}", e))?;
|
||||
fs::create_dir_all(out_dir.join("region")).map_err(|e| format!("Failed to create region directory: {}", e))?;
|
||||
let mut chunks_written: usize = 0;
|
||||
let mut errors: Vec<String> = Vec::new();
|
||||
let mut region_writers: HashMap<String, JavaRegionFileWriter> = HashMap::new();
|
||||
for (name, data) in &files {
|
||||
if name == "level.dat" {
|
||||
let lce_root = nbt::read_nbt(data).map_err(|e| format!("Failed to parse level.dat: {}", e))?;
|
||||
let java_level_bytes = convert_lce_to_java(&lce_root, None, None, None, None);
|
||||
fs::write(out_dir.join("level.dat"), &java_level_bytes).map_err(|e| format!("Failed to write level.dat: {}", e))?;
|
||||
continue;
|
||||
}
|
||||
|
||||
if name.ends_with(".mcr") || name.ends_with(".mca") {
|
||||
let mut reader = region::JavaRegionReader::open_from_bytes(data);
|
||||
let parts: Vec<&str> = name.split('.').collect();
|
||||
let region_x: i32 = if parts.len() >= 4 { parts[1].parse().unwrap_or(0) } else { 0 };
|
||||
let region_z: i32 = if parts.len() >= 4 { parts[2].parse().unwrap_or(0) } else { 0 };
|
||||
for local_z in 0..32 {
|
||||
for local_x in 0..32 {
|
||||
let index = (local_x & 31) + (local_z & 31) * 32;
|
||||
if reader.offsets[index as usize] == 0 {
|
||||
continue;
|
||||
}
|
||||
|
||||
match reader.read_chunk(local_x, local_z) {
|
||||
Ok(Some(lce_chunk_data)) => {
|
||||
if let Some(legacy_nbt) = payload::try_decode_to_legacy_nbt(&lce_chunk_data) {
|
||||
let root = nbt::read_nbt(&legacy_nbt).unwrap_or_default();
|
||||
let source_level = root.compound("Level").unwrap_or(&root);
|
||||
let cx = region_x * 32 + local_x;
|
||||
let cz = region_z * 32 + local_z;
|
||||
let modern_root = build_modern_anvil_level(source_level, cx, cz);
|
||||
let modern_nbt = nbt::write_nbt(&modern_root);
|
||||
let out_region_x = cx >> 5;
|
||||
let out_region_z = cz >> 5;
|
||||
let out_local_x = cx & 31;
|
||||
let out_local_z = cz & 31;
|
||||
let section_region = format!("r.{}.{}.mca", out_region_x, out_region_z);
|
||||
let region_path = out_dir.join("region").join(§ion_region);
|
||||
let region_path_str = region_path.to_string_lossy().to_string();
|
||||
let writer = region_writers.entry(section_region.clone()).or_insert_with(|| {
|
||||
JavaRegionFileWriter::load_from_file(®ion_path_str)
|
||||
});
|
||||
writer.write_chunk(out_local_x, out_local_z, &modern_nbt);
|
||||
chunks_written += 1;
|
||||
}
|
||||
}
|
||||
Ok(None) => {}
|
||||
Err(e) => {
|
||||
errors.push(format!("{}/{}: {}", name, local_x * 32 + local_z, e));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
for (region_name, writer) in &mut region_writers {
|
||||
if let Err(e) = writer.save() {
|
||||
errors.push(format!("Failed to save region {}: {}", region_name, e));
|
||||
}
|
||||
}
|
||||
|
||||
let mut msg = format!(
|
||||
"Conversion complete!\nChunks: {}\nFiles: {}",
|
||||
chunks_written, files.len()
|
||||
);
|
||||
if !errors.is_empty() {
|
||||
msg.push_str(&format!("\nErrors: {}", errors.len()));
|
||||
for err in errors.iter().take(5) {
|
||||
msg.push_str(&format!("\n {}", err));
|
||||
}
|
||||
}
|
||||
eprintln!("{}", msg);
|
||||
Ok(msg)
|
||||
}
|
||||
@@ -0,0 +1,496 @@
|
||||
use std::io::{Read, Write};
|
||||
use flate2::read::GzDecoder;
|
||||
use flate2::write::GzEncoder;
|
||||
use flate2::Compression;
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum NbtValue {
|
||||
Byte(i8),
|
||||
Short(i16),
|
||||
Int(i32),
|
||||
Long(i64),
|
||||
Float(f32),
|
||||
Double(f64),
|
||||
ByteArray(Vec<u8>),
|
||||
String(String),
|
||||
List(Vec<NbtValue>),
|
||||
Compound(NbtCompound),
|
||||
IntArray(Vec<i32>),
|
||||
LongArray(Vec<i64>),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, PartialEq)]
|
||||
pub struct NbtCompound {
|
||||
pub name: String,
|
||||
pub tags: Vec<(String, NbtValue)>,
|
||||
}
|
||||
|
||||
impl NbtCompound {
|
||||
pub fn new(name: &str) -> Self {
|
||||
Self {
|
||||
name: name.to_string(),
|
||||
tags: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn insert(&mut self, name: &str, value: NbtValue) {
|
||||
self.tags.retain(|(n, _)| n != name);
|
||||
self.tags.push((name.to_string(), value));
|
||||
}
|
||||
|
||||
pub fn get(&self, name: &str) -> Option<&NbtValue> {
|
||||
self.tags.iter().find(|(n, _)| n == name).map(|(_, v)| v)
|
||||
}
|
||||
|
||||
pub fn get_mut(&mut self, name: &str) -> Option<&mut NbtValue> {
|
||||
self.tags.iter_mut().find(|(n, _)| n == name).map(|(_, v)| v)
|
||||
}
|
||||
|
||||
pub fn contains(&self, name: &str) -> bool {
|
||||
self.tags.iter().any(|(n, _)| n == name)
|
||||
}
|
||||
|
||||
pub fn remove(&mut self, name: &str) {
|
||||
self.tags.retain(|(n, _)| n != name);
|
||||
}
|
||||
|
||||
pub fn compound(&self, name: &str) -> Option<&NbtCompound> {
|
||||
self.get(name).and_then(|v| match v {
|
||||
NbtValue::Compound(c) => Some(c),
|
||||
_ => None,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn compound_mut(&mut self, name: &str) -> Option<&mut NbtCompound> {
|
||||
self.get_mut(name).and_then(|v| match v {
|
||||
NbtValue::Compound(c) => Some(c),
|
||||
_ => None,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn byte(&self, name: &str) -> Option<i8> {
|
||||
self.get(name).and_then(|v| match v {
|
||||
NbtValue::Byte(b) => Some(*b),
|
||||
_ => None,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn short(&self, name: &str) -> Option<i16> {
|
||||
self.get(name).and_then(|v| match v {
|
||||
NbtValue::Short(s) => Some(*s),
|
||||
_ => None,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn int(&self, name: &str) -> Option<i32> {
|
||||
self.get(name).and_then(|v| match v {
|
||||
NbtValue::Int(i) => Some(*i),
|
||||
_ => None,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn long(&self, name: &str) -> Option<i64> {
|
||||
self.get(name).and_then(|v| match v {
|
||||
NbtValue::Long(l) => Some(*l),
|
||||
_ => None,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn float(&self, name: &str) -> Option<f32> {
|
||||
self.get(name).and_then(|v| match v {
|
||||
NbtValue::Float(f) => Some(*f),
|
||||
_ => None,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn double(&self, name: &str) -> Option<f64> {
|
||||
self.get(name).and_then(|v| match v {
|
||||
NbtValue::Double(d) => Some(*d),
|
||||
_ => None,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn string(&self, name: &str) -> Option<&str> {
|
||||
self.get(name).and_then(|v| match v {
|
||||
NbtValue::String(s) => Some(s.as_str()),
|
||||
_ => None,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn byte_array(&self, name: &str) -> Option<&[u8]> {
|
||||
self.get(name).and_then(|v| match v {
|
||||
NbtValue::ByteArray(b) => Some(b.as_slice()),
|
||||
_ => None,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn int_array(&self, name: &str) -> Option<&[i32]> {
|
||||
self.get(name).and_then(|v| match v {
|
||||
NbtValue::IntArray(a) => Some(a.as_slice()),
|
||||
_ => None,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn long_array(&self, name: &str) -> Option<&[i64]> {
|
||||
self.get(name).and_then(|v| match v {
|
||||
NbtValue::LongArray(a) => Some(a.as_slice()),
|
||||
_ => None,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn list(&self, name: &str) -> Option<&[NbtValue]> {
|
||||
self.get(name).and_then(|v| match v {
|
||||
NbtValue::List(l) => Some(l.as_slice()),
|
||||
_ => None,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn list_compounds(&self, name: &str) -> Vec<&NbtCompound> {
|
||||
self.list(name)
|
||||
.unwrap_or(&[])
|
||||
.iter()
|
||||
.filter_map(|v| match v {
|
||||
NbtValue::Compound(c) => Some(c),
|
||||
_ => None,
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
fn tag_type_id(v: &NbtValue) -> u8 {
|
||||
match v {
|
||||
NbtValue::Byte(_) => 1,
|
||||
NbtValue::Short(_) => 2,
|
||||
NbtValue::Int(_) => 3,
|
||||
NbtValue::Long(_) => 4,
|
||||
NbtValue::Float(_) => 5,
|
||||
NbtValue::Double(_) => 6,
|
||||
NbtValue::ByteArray(_) => 7,
|
||||
NbtValue::String(_) => 8,
|
||||
NbtValue::List(_) => 9,
|
||||
NbtValue::Compound(_) => 10,
|
||||
NbtValue::IntArray(_) => 11,
|
||||
NbtValue::LongArray(_) => 12,
|
||||
}
|
||||
}
|
||||
|
||||
fn read_be_i16(data: &[u8], off: usize) -> i16 {
|
||||
i16::from_be_bytes([data[off], data[off + 1]])
|
||||
}
|
||||
|
||||
fn read_be_i32(data: &[u8], off: usize) -> i32 {
|
||||
i32::from_be_bytes([data[off], data[off + 1], data[off + 2], data[off + 3]])
|
||||
}
|
||||
|
||||
fn read_be_i64(data: &[u8], off: usize) -> i64 {
|
||||
i64::from_be_bytes([
|
||||
data[off],
|
||||
data[off + 1],
|
||||
data[off + 2],
|
||||
data[off + 3],
|
||||
data[off + 4],
|
||||
data[off + 5],
|
||||
data[off + 6],
|
||||
data[off + 7],
|
||||
])
|
||||
}
|
||||
|
||||
fn read_be_f32(data: &[u8], off: usize) -> f32 {
|
||||
f32::from_be_bytes([data[off], data[off + 1], data[off + 2], data[off + 3]])
|
||||
}
|
||||
|
||||
fn read_be_f64(data: &[u8], off: usize) -> f64 {
|
||||
f64::from_be_bytes([
|
||||
data[off],
|
||||
data[off + 1],
|
||||
data[off + 2],
|
||||
data[off + 3],
|
||||
data[off + 4],
|
||||
data[off + 5],
|
||||
data[off + 6],
|
||||
data[off + 7],
|
||||
])
|
||||
}
|
||||
|
||||
fn read_string(data: &[u8], off: usize) -> Result<(String, usize), String> {
|
||||
let len = read_be_i16(data, off) as usize;
|
||||
let start = off + 2;
|
||||
if start + len > data.len() {
|
||||
return Err("string extends past end of data".into());
|
||||
}
|
||||
let s = String::from_utf8_lossy(&data[start..start + len]).to_string();
|
||||
Ok((s, start + len))
|
||||
}
|
||||
|
||||
pub fn read_nbt(data: &[u8]) -> Result<NbtCompound, String> {
|
||||
if data.is_empty() {
|
||||
return Err("empty NBT data".into());
|
||||
}
|
||||
let mut pos = 0;
|
||||
let tag_type = data[pos];
|
||||
pos += 1;
|
||||
if tag_type != 10 {
|
||||
return Err(format!("root tag is not compound (type={})", tag_type));
|
||||
}
|
||||
let (name, new_pos) = read_string(data, pos)?;
|
||||
pos = new_pos;
|
||||
let (compound, _new_pos) = read_compound_payload(data, pos)?;
|
||||
let mut result = compound;
|
||||
result.name = name;
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
fn read_compound_payload(data: &[u8], mut pos: usize) -> Result<(NbtCompound, usize), String> {
|
||||
let mut compound = NbtCompound::default();
|
||||
loop {
|
||||
if pos >= data.len() {
|
||||
return Err("unexpected end in compound".into());
|
||||
}
|
||||
let tag_type = data[pos];
|
||||
pos += 1;
|
||||
if tag_type == 0 {
|
||||
break;
|
||||
}
|
||||
let (name, new_pos) = read_string(data, pos)?;
|
||||
pos = new_pos;
|
||||
let (value, new_pos) = read_value(data, pos, tag_type)?;
|
||||
pos = new_pos;
|
||||
compound.tags.push((name, value));
|
||||
}
|
||||
Ok((compound, pos))
|
||||
}
|
||||
|
||||
fn read_value(data: &[u8], pos: usize, tag_type: u8) -> Result<(NbtValue, usize), String> {
|
||||
match tag_type {
|
||||
1 => Ok((NbtValue::Byte(data[pos] as i8), pos + 1)),
|
||||
2 => Ok((NbtValue::Short(read_be_i16(data, pos)), pos + 2)),
|
||||
3 => Ok((NbtValue::Int(read_be_i32(data, pos)), pos + 4)),
|
||||
4 => Ok((NbtValue::Long(read_be_i64(data, pos)), pos + 8)),
|
||||
5 => Ok((NbtValue::Float(read_be_f32(data, pos)), pos + 4)),
|
||||
6 => Ok((NbtValue::Double(read_be_f64(data, pos)), pos + 8)),
|
||||
7 => {
|
||||
let len = read_be_i32(data, pos) as usize;
|
||||
let start = pos + 4;
|
||||
if start + len > data.len() {
|
||||
return Err("byte array extends past end".into());
|
||||
}
|
||||
Ok((NbtValue::ByteArray(data[start..start + len].to_vec()), start + len))
|
||||
}
|
||||
8 => {
|
||||
let (s, new_pos) = read_string(data, pos)?;
|
||||
Ok((NbtValue::String(s), new_pos))
|
||||
}
|
||||
9 => {
|
||||
let elem_type = data[pos];
|
||||
let count = read_be_i32(data, pos + 1) as usize;
|
||||
let mut list_pos = pos + 5;
|
||||
let mut list = Vec::with_capacity(count);
|
||||
for _ in 0..count {
|
||||
let (val, new_pos) = read_value(data, list_pos, elem_type)?;
|
||||
list_pos = new_pos;
|
||||
list.push(val);
|
||||
}
|
||||
Ok((NbtValue::List(list), list_pos))
|
||||
}
|
||||
10 => {
|
||||
let (compound, new_pos) = read_compound_payload(data, pos)?;
|
||||
Ok((NbtValue::Compound(compound), new_pos))
|
||||
}
|
||||
11 => {
|
||||
let len = read_be_i32(data, pos) as usize;
|
||||
let start = pos + 4;
|
||||
let mut arr = Vec::with_capacity(len);
|
||||
for i in 0..len {
|
||||
let off = start + i * 4;
|
||||
if off + 4 > data.len() {
|
||||
return Err("int array extends past end".into());
|
||||
}
|
||||
arr.push(read_be_i32(data, off));
|
||||
}
|
||||
Ok((NbtValue::IntArray(arr), start + len * 4))
|
||||
}
|
||||
12 => {
|
||||
let len = read_be_i32(data, pos) as usize;
|
||||
let start = pos + 4;
|
||||
let mut arr = Vec::with_capacity(len);
|
||||
for i in 0..len {
|
||||
let off = start + i * 8;
|
||||
if off + 8 > data.len() {
|
||||
return Err("long array extends past end".into());
|
||||
}
|
||||
arr.push(read_be_i64(data, off));
|
||||
}
|
||||
Ok((NbtValue::LongArray(arr), start + len * 8))
|
||||
}
|
||||
_ => Err(format!("unknown NBT tag type: {}", tag_type)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn write_nbt(compound: &NbtCompound) -> Vec<u8> {
|
||||
let mut out = Vec::new();
|
||||
out.push(10);
|
||||
write_string(&mut out, &compound.name);
|
||||
write_compound_payload(&mut out, compound);
|
||||
out.push(0);
|
||||
out
|
||||
}
|
||||
|
||||
fn write_string(out: &mut Vec<u8>, s: &str) {
|
||||
let bytes = s.as_bytes();
|
||||
let len = (bytes.len() as i16).to_be_bytes();
|
||||
out.extend_from_slice(&len);
|
||||
out.extend_from_slice(bytes);
|
||||
}
|
||||
|
||||
fn write_compound_payload(out: &mut Vec<u8>, compound: &NbtCompound) {
|
||||
for (name, value) in &compound.tags {
|
||||
out.push(tag_type_id(value));
|
||||
write_string(out, name);
|
||||
write_value(out, value);
|
||||
}
|
||||
}
|
||||
|
||||
fn write_value(out: &mut Vec<u8>, value: &NbtValue) {
|
||||
match value {
|
||||
NbtValue::Byte(b) => out.push(*b as u8),
|
||||
NbtValue::Short(s) => out.extend_from_slice(&s.to_be_bytes()),
|
||||
NbtValue::Int(i) => out.extend_from_slice(&i.to_be_bytes()),
|
||||
NbtValue::Long(l) => out.extend_from_slice(&l.to_be_bytes()),
|
||||
NbtValue::Float(f) => out.extend_from_slice(&f.to_be_bytes()),
|
||||
NbtValue::Double(d) => out.extend_from_slice(&d.to_be_bytes()),
|
||||
NbtValue::ByteArray(arr) => {
|
||||
out.extend_from_slice(&(arr.len() as i32).to_be_bytes());
|
||||
out.extend_from_slice(arr);
|
||||
}
|
||||
NbtValue::String(s) => write_string(out, s),
|
||||
NbtValue::List(list) => {
|
||||
let elem_type = list.first().map(|v| tag_type_id(v)).unwrap_or(0);
|
||||
out.push(elem_type);
|
||||
out.extend_from_slice(&(list.len() as i32).to_be_bytes());
|
||||
for item in list {
|
||||
write_value(out, item);
|
||||
}
|
||||
}
|
||||
NbtValue::Compound(c) => {
|
||||
write_compound_payload(out, c);
|
||||
out.push(0);
|
||||
}
|
||||
NbtValue::IntArray(arr) => {
|
||||
out.extend_from_slice(&(arr.len() as i32).to_be_bytes());
|
||||
for i in arr {
|
||||
out.extend_from_slice(&i.to_be_bytes());
|
||||
}
|
||||
}
|
||||
NbtValue::LongArray(arr) => {
|
||||
out.extend_from_slice(&(arr.len() as i32).to_be_bytes());
|
||||
for l in arr {
|
||||
out.extend_from_slice(&l.to_be_bytes());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn read_gzip_nbt(data: &[u8]) -> Result<NbtCompound, String> {
|
||||
let mut decoder = GzDecoder::new(data);
|
||||
let mut buf = Vec::new();
|
||||
decoder
|
||||
.read_to_end(&mut buf)
|
||||
.map_err(|e| format!("gzip decompress failed: {}", e))?;
|
||||
read_nbt(&buf)
|
||||
}
|
||||
|
||||
pub fn write_gzip_nbt(compound: &NbtCompound) -> Vec<u8> {
|
||||
let raw = write_nbt(compound);
|
||||
let mut encoder = GzEncoder::new(Vec::new(), Compression::best());
|
||||
encoder.write_all(&raw).unwrap();
|
||||
encoder.finish().unwrap()
|
||||
}
|
||||
|
||||
pub fn read_zlib_nbt(data: &[u8]) -> Result<NbtCompound, String> {
|
||||
use flate2::read::ZlibDecoder;
|
||||
let mut decoder = ZlibDecoder::new(data);
|
||||
let mut buf = Vec::new();
|
||||
decoder
|
||||
.read_to_end(&mut buf)
|
||||
.map_err(|e| format!("zlib decompress failed: {}", e))?;
|
||||
read_nbt(&buf)
|
||||
}
|
||||
|
||||
pub fn write_zlib_nbt(compound: &NbtCompound) -> Vec<u8> {
|
||||
use flate2::write::ZlibEncoder;
|
||||
let raw = write_nbt(compound);
|
||||
let mut encoder = ZlibEncoder::new(Vec::new(), Compression::best());
|
||||
encoder.write_all(&raw).unwrap();
|
||||
encoder.finish().unwrap()
|
||||
}
|
||||
|
||||
pub fn get_byte_array_or(compound: &NbtCompound, name: &str, default_len: usize) -> Vec<u8> {
|
||||
compound
|
||||
.byte_array(name)
|
||||
.map(|b| {
|
||||
let mut v = b.to_vec();
|
||||
if v.len() < default_len {
|
||||
v.resize(default_len, 0);
|
||||
}
|
||||
v
|
||||
})
|
||||
.unwrap_or_else(|| vec![0u8; default_len])
|
||||
}
|
||||
|
||||
pub fn get_int_array_or(compound: &NbtCompound, name: &str, default_len: usize) -> Vec<i32> {
|
||||
compound
|
||||
.int_array(name)
|
||||
.map(|a| {
|
||||
let mut v = a.to_vec();
|
||||
if v.len() < default_len {
|
||||
v.resize(default_len, 0);
|
||||
}
|
||||
v
|
||||
})
|
||||
.unwrap_or_else(|| vec![0i32; default_len])
|
||||
}
|
||||
|
||||
pub fn get_long_array_or(compound: &NbtCompound, name: &str, default_len: usize) -> Vec<i64> {
|
||||
compound
|
||||
.long_array(name)
|
||||
.map(|a| {
|
||||
let mut v = a.to_vec();
|
||||
if v.len() < default_len {
|
||||
v.resize(default_len, 0);
|
||||
}
|
||||
v
|
||||
})
|
||||
.unwrap_or_else(|| vec![0i64; default_len])
|
||||
}
|
||||
|
||||
pub fn get_nibble(data: &[u8], index: usize) -> u8 {
|
||||
let byte_index = index >> 1;
|
||||
if byte_index >= data.len() {
|
||||
return 0;
|
||||
}
|
||||
let b = data[byte_index];
|
||||
if index & 1 == 0 {
|
||||
b & 0x0F
|
||||
} else {
|
||||
(b >> 4) & 0x0F
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_nibble(data: &mut [u8], index: usize, value: u8) {
|
||||
let byte_index = index >> 1;
|
||||
if byte_index >= data.len() {
|
||||
return;
|
||||
}
|
||||
let val = value & 0x0F;
|
||||
if index & 1 == 0 {
|
||||
data[byte_index] = (data[byte_index] & 0xF0) | val;
|
||||
} else {
|
||||
data[byte_index] = (data[byte_index] & 0x0F) | (val << 4);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn clone_or_empty_list(compound: &NbtCompound, name: &str) -> Vec<NbtValue> {
|
||||
compound
|
||||
.list(name)
|
||||
.map(|l| l.to_vec())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
@@ -0,0 +1,683 @@
|
||||
use super::nbt;
|
||||
const SAVE_FILE_VERSION_COMPRESSED_CHUNK_STORAGE: i16 = 8;
|
||||
const SAVE_FILE_VERSION_CHUNK_INHABITED_TIME: i16 = 9;
|
||||
const COMPRESSED_CHUNK_SECTION_HEIGHT: usize = 128;
|
||||
const BLOCKS_PER_SECTION: usize = COMPRESSED_CHUNK_SECTION_HEIGHT * 16 * 16;
|
||||
const NIBBLES_PER_SECTION: usize = BLOCKS_PER_SECTION / 2;
|
||||
const FULL_CHUNK_BLOCKS: usize = 256 * 16 * 16;
|
||||
const FULL_CHUNK_NIBBLES: usize = FULL_CHUNK_BLOCKS / 2;
|
||||
const INDEX_TYPE_MASK: u16 = 0x0003;
|
||||
const INDEX_TYPE_1BIT: u16 = 0x0000;
|
||||
const INDEX_TYPE_2BIT: u16 = 0x0001;
|
||||
const INDEX_TYPE_4BIT: u16 = 0x0002;
|
||||
const INDEX_TYPE_0_OR_8BIT: u16 = 0x0003;
|
||||
const INDEX_TYPE_0BIT_FLAG: u16 = 0x0004;
|
||||
const SPARSE_ALL_ZERO_INDEX: u8 = 128;
|
||||
const SPARSE_ALL_FIFTEEN_INDEX: u8 = 129;
|
||||
fn read_be_i16(data: &[u8], off: usize) -> i16 {
|
||||
i16::from_be_bytes([data[off], data[off + 1]])
|
||||
}
|
||||
|
||||
fn read_be_i32(data: &[u8], off: usize) -> i32 {
|
||||
i32::from_be_bytes([data[off], data[off + 1], data[off + 2], data[off + 3]])
|
||||
}
|
||||
|
||||
fn read_be_i64(data: &[u8], off: usize) -> i64 {
|
||||
i64::from_be_bytes([
|
||||
data[off], data[off + 1], data[off + 2], data[off + 3], data[off + 4], data[off + 5],
|
||||
data[off + 6], data[off + 7],
|
||||
])
|
||||
}
|
||||
|
||||
fn write_be_i16(out: &mut Vec<u8>, v: i16) {
|
||||
out.extend_from_slice(&v.to_be_bytes());
|
||||
}
|
||||
|
||||
fn write_be_i32(out: &mut Vec<u8>, v: i32) {
|
||||
out.extend_from_slice(&v.to_be_bytes());
|
||||
}
|
||||
|
||||
fn write_be_i64(out: &mut Vec<u8>, v: i64) {
|
||||
out.extend_from_slice(&v.to_be_bytes());
|
||||
}
|
||||
|
||||
fn get_compressed_tile_index(block: usize, tile: usize) -> usize {
|
||||
let mut index = ((block & 0x180) << 6) | ((block & 0x060) << 4) | ((block & 0x01F) << 2);
|
||||
index |= ((tile & 0x30) << 7) | ((tile & 0x0C) << 5) | (tile & 0x03);
|
||||
index
|
||||
}
|
||||
|
||||
fn get_nibble_value(nibble_data: &[u8], xz: usize, y: usize) -> u8 {
|
||||
let pos = (xz << 7) | y;
|
||||
let slot = pos >> 1;
|
||||
let part = pos & 1;
|
||||
if slot >= nibble_data.len() {
|
||||
return 0;
|
||||
}
|
||||
let b = nibble_data[slot];
|
||||
if part == 0 {
|
||||
b & 0x0F
|
||||
} else {
|
||||
(b >> 4) & 0x0F
|
||||
}
|
||||
}
|
||||
|
||||
fn set_nibble_value(nibble_data: &mut [u8], xz: usize, y: usize, value: u8) {
|
||||
let pos = (xz << 7) | y;
|
||||
let slot = pos >> 1;
|
||||
let part = pos & 1;
|
||||
if slot >= nibble_data.len() {
|
||||
return;
|
||||
}
|
||||
let val = value & 0x0F;
|
||||
if part == 0 {
|
||||
nibble_data[slot] = (nibble_data[slot] & 0xF0) | val;
|
||||
} else {
|
||||
nibble_data[slot] = (nibble_data[slot] & 0x0F) | (val << 4);
|
||||
}
|
||||
}
|
||||
|
||||
fn ensure_length(input: &[u8], expected_len: usize) -> Vec<u8> {
|
||||
if input.len() == expected_len {
|
||||
return input.to_vec();
|
||||
}
|
||||
let mut v = vec![0u8; expected_len];
|
||||
let copy_len = input.len().min(expected_len);
|
||||
v[..copy_len].copy_from_slice(&input[..copy_len]);
|
||||
v
|
||||
}
|
||||
|
||||
fn extract_lower_block_section(full_blocks: &[u8]) -> Vec<u8> {
|
||||
if full_blocks.len() == BLOCKS_PER_SECTION {
|
||||
return full_blocks.to_vec();
|
||||
}
|
||||
let mut lower = vec![0u8; BLOCKS_PER_SECTION];
|
||||
for xz in 0..256 {
|
||||
let src_start = xz * 256;
|
||||
let dst_start = xz * COMPRESSED_CHUNK_SECTION_HEIGHT;
|
||||
let copy_len = COMPRESSED_CHUNK_SECTION_HEIGHT.min(full_blocks.len() - src_start.min(full_blocks.len()));
|
||||
if src_start + copy_len <= full_blocks.len() && dst_start + copy_len <= BLOCKS_PER_SECTION {
|
||||
lower[dst_start..dst_start + copy_len]
|
||||
.copy_from_slice(&full_blocks[src_start..src_start + copy_len]);
|
||||
}
|
||||
}
|
||||
lower
|
||||
}
|
||||
|
||||
fn extract_upper_block_section(full_blocks: &[u8]) -> Vec<u8> {
|
||||
if full_blocks.len() < FULL_CHUNK_BLOCKS {
|
||||
return vec![0u8; BLOCKS_PER_SECTION];
|
||||
}
|
||||
let mut upper = vec![0u8; BLOCKS_PER_SECTION];
|
||||
for xz in 0..256 {
|
||||
let src_start = xz * 256 + COMPRESSED_CHUNK_SECTION_HEIGHT;
|
||||
let dst_start = xz * COMPRESSED_CHUNK_SECTION_HEIGHT;
|
||||
upper[dst_start..dst_start + COMPRESSED_CHUNK_SECTION_HEIGHT].copy_from_slice(&full_blocks[src_start..src_start + COMPRESSED_CHUNK_SECTION_HEIGHT]);
|
||||
}
|
||||
upper
|
||||
}
|
||||
|
||||
fn extract_lower_nibble_section(full_nibbles: &[u8]) -> Vec<u8> {
|
||||
let nibble_height = COMPRESSED_CHUNK_SECTION_HEIGHT / 2;
|
||||
if full_nibbles.len() == NIBBLES_PER_SECTION {
|
||||
return full_nibbles.to_vec();
|
||||
}
|
||||
let mut lower = vec![0u8; NIBBLES_PER_SECTION];
|
||||
for xz in 0..256 {
|
||||
let src_start = xz * nibble_height * 2;
|
||||
let dst_start = xz * nibble_height;
|
||||
let copy_len = nibble_height.min(
|
||||
full_nibbles.len().saturating_sub(src_start),
|
||||
);
|
||||
if copy_len > 0 {
|
||||
lower[dst_start..dst_start + copy_len].copy_from_slice(&full_nibbles[src_start..src_start + copy_len]);
|
||||
}
|
||||
}
|
||||
lower
|
||||
}
|
||||
|
||||
fn extract_upper_nibble_section(full_nibbles: &[u8]) -> Vec<u8> {
|
||||
let nibble_height = COMPRESSED_CHUNK_SECTION_HEIGHT / 2;
|
||||
if full_nibbles.len() < FULL_CHUNK_NIBBLES {
|
||||
return vec![0u8; NIBBLES_PER_SECTION];
|
||||
}
|
||||
let mut upper = vec![0u8; NIBBLES_PER_SECTION];
|
||||
for xz in 0..256 {
|
||||
let src_start = xz * nibble_height * 2 + nibble_height;
|
||||
let dst_start = xz * nibble_height;
|
||||
upper[dst_start..dst_start + nibble_height]
|
||||
.copy_from_slice(&full_nibbles[src_start..src_start + nibble_height]);
|
||||
}
|
||||
upper
|
||||
}
|
||||
|
||||
fn combine_block_sections(lower: &[u8], upper: &[u8]) -> Vec<u8> {
|
||||
if lower.len() == FULL_CHUNK_BLOCKS {
|
||||
return lower.to_vec();
|
||||
}
|
||||
let mut combined = vec![0u8; FULL_CHUNK_BLOCKS];
|
||||
for xz in 0..256 {
|
||||
let lower_start = xz * COMPRESSED_CHUNK_SECTION_HEIGHT;
|
||||
let out_start = xz * 256;
|
||||
let copy_len = COMPRESSED_CHUNK_SECTION_HEIGHT.min(lower.len().saturating_sub(lower_start));
|
||||
if copy_len > 0 {
|
||||
combined[out_start..out_start + copy_len].copy_from_slice(&lower[lower_start..lower_start + copy_len]);
|
||||
}
|
||||
let upper_start = xz * COMPRESSED_CHUNK_SECTION_HEIGHT;
|
||||
let out_upper = out_start + COMPRESSED_CHUNK_SECTION_HEIGHT;
|
||||
let copy_len2 = COMPRESSED_CHUNK_SECTION_HEIGHT.min(upper.len().saturating_sub(upper_start));
|
||||
if copy_len2 > 0 {
|
||||
combined[out_upper..out_upper + copy_len2]
|
||||
.copy_from_slice(&upper[upper_start..upper_start + copy_len2]);
|
||||
}
|
||||
}
|
||||
combined
|
||||
}
|
||||
|
||||
fn combine_nibble_sections(lower: &[u8], upper: &[u8]) -> Vec<u8> {
|
||||
let nibble_height = COMPRESSED_CHUNK_SECTION_HEIGHT / 2;
|
||||
if lower.len() == FULL_CHUNK_NIBBLES {
|
||||
return lower.to_vec();
|
||||
}
|
||||
let mut combined = vec![0u8; FULL_CHUNK_NIBBLES];
|
||||
for xz in 0..256 {
|
||||
let lower_start = xz * nibble_height;
|
||||
let out_start = xz * nibble_height * 2;
|
||||
let copy_len = nibble_height.min(lower.len().saturating_sub(lower_start));
|
||||
if copy_len > 0 {
|
||||
combined[out_start..out_start + copy_len]
|
||||
.copy_from_slice(&lower[lower_start..lower_start + copy_len]);
|
||||
}
|
||||
let upper_start = xz * nibble_height;
|
||||
let out_upper = out_start + nibble_height;
|
||||
let copy_len2 = nibble_height.min(upper.len().saturating_sub(upper_start));
|
||||
if copy_len2 > 0 {
|
||||
combined[out_upper..out_upper + copy_len2]
|
||||
.copy_from_slice(&upper[upper_start..upper_start + copy_len2]);
|
||||
}
|
||||
}
|
||||
combined
|
||||
}
|
||||
|
||||
pub fn encode_legacy_nbt(level: &nbt::NbtCompound) -> Vec<u8> {
|
||||
let mut root = nbt::NbtCompound::new("");
|
||||
root.insert("Level", nbt::NbtValue::Compound(level.clone()));
|
||||
nbt::write_nbt(&root)
|
||||
}
|
||||
|
||||
fn write_compressed_tile_storage(out: &mut Vec<u8>, blocks: &[u8]) {
|
||||
let normalized = ensure_length(blocks, BLOCKS_PER_SECTION);
|
||||
let mut blob = vec![0u8; 1024 + BLOCKS_PER_SECTION];
|
||||
let mut data_offset: usize = 0;
|
||||
for block in 0..512 {
|
||||
let index = (INDEX_TYPE_0_OR_8BIT | ((data_offset as u16) << 1)) as u16;
|
||||
blob[block * 2..block * 2 + 2].copy_from_slice(&index.to_le_bytes());
|
||||
for tile in 0..64 {
|
||||
blob[1024 + data_offset + tile] = normalized[get_compressed_tile_index(block, tile)];
|
||||
}
|
||||
data_offset += 64;
|
||||
}
|
||||
|
||||
write_be_i32(out, blob.len() as i32);
|
||||
out.extend_from_slice(&blob);
|
||||
}
|
||||
|
||||
fn write_empty_compressed_tile_storage(out: &mut Vec<u8>) {
|
||||
let mut blob = vec![0u8; 1024];
|
||||
for block in 0..512 {
|
||||
let index = (INDEX_TYPE_0_OR_8BIT | INDEX_TYPE_0BIT_FLAG) as u16;
|
||||
blob[block * 2..block * 2 + 2].copy_from_slice(&index.to_le_bytes());
|
||||
}
|
||||
write_be_i32(out, blob.len() as i32);
|
||||
out.extend_from_slice(&blob);
|
||||
}
|
||||
|
||||
fn write_sparse_nibble_storage(out: &mut Vec<u8>, nibble_data: &[u8], supports_all_fifteen: bool) {
|
||||
let normalized = ensure_length(nibble_data, NIBBLES_PER_SECTION);
|
||||
let mut plane_indices = [0u8; 128];
|
||||
let mut planes: Vec<Vec<u8>> = Vec::new();
|
||||
for y in 0..128 {
|
||||
let mut all_zero = true;
|
||||
let mut all_fifteen = supports_all_fifteen;
|
||||
let mut plane = vec![0u8; 128];
|
||||
let mut plane_cursor = 0;
|
||||
for xz in 0..128 {
|
||||
let first = get_nibble_value(&normalized, xz * 2, y);
|
||||
let second = get_nibble_value(&normalized, xz * 2 + 1, y);
|
||||
|
||||
if first != 0 || second != 0 {
|
||||
all_zero = false;
|
||||
}
|
||||
if supports_all_fifteen && (first != 15 || second != 15) {
|
||||
all_fifteen = false;
|
||||
}
|
||||
|
||||
plane[plane_cursor] = first | (second << 4);
|
||||
plane_cursor += 1;
|
||||
}
|
||||
|
||||
if all_zero {
|
||||
plane_indices[y] = SPARSE_ALL_ZERO_INDEX;
|
||||
continue;
|
||||
}
|
||||
if supports_all_fifteen && all_fifteen {
|
||||
plane_indices[y] = SPARSE_ALL_FIFTEEN_INDEX;
|
||||
continue;
|
||||
}
|
||||
|
||||
plane_indices[y] = planes.len() as u8;
|
||||
planes.push(plane);
|
||||
}
|
||||
|
||||
write_be_i32(out, planes.len() as i32);
|
||||
out.extend_from_slice(&plane_indices);
|
||||
for plane in &planes {
|
||||
out.extend_from_slice(plane);
|
||||
}
|
||||
}
|
||||
|
||||
fn write_empty_sparse_nibble_storage(out: &mut Vec<u8>, supports_all_fifteen: bool, fill_with_fifteen: bool) {
|
||||
write_be_i32(out, 0);
|
||||
let fill = if supports_all_fifteen && fill_with_fifteen {
|
||||
SPARSE_ALL_FIFTEEN_INDEX
|
||||
} else {
|
||||
SPARSE_ALL_ZERO_INDEX
|
||||
};
|
||||
let plane_indices = [fill; 128];
|
||||
out.extend_from_slice(&plane_indices);
|
||||
}
|
||||
|
||||
pub fn encode_compressed_storage(level: &nbt::NbtCompound) -> Vec<u8> {
|
||||
let blocks = level.byte_array("Blocks").unwrap_or(&[]);
|
||||
let data = level.byte_array("Data").unwrap_or(&[]);
|
||||
let sky_light_raw = level.byte_array("SkyLight").unwrap_or(&[]);
|
||||
let block_light_raw = level.byte_array("BlockLight").unwrap_or(&[]);
|
||||
let height_map = level.byte_array("HeightMap").unwrap_or(&[]);
|
||||
let biomes = level.byte_array("Biomes").unwrap_or(&[]);
|
||||
let default_sky = vec![0xFF; NIBBLES_PER_SECTION];
|
||||
let default_light = vec![0u8; NIBBLES_PER_SECTION];
|
||||
let sky_light = if sky_light_raw.is_empty() { default_sky.as_slice() } else { sky_light_raw };
|
||||
let block_light = if block_light_raw.is_empty() { default_light.as_slice() } else { block_light_raw };
|
||||
let mut out = Vec::new();
|
||||
write_be_i16(&mut out, SAVE_FILE_VERSION_CHUNK_INHABITED_TIME);
|
||||
write_be_i32(&mut out, level.int("xPos").unwrap_or(0));
|
||||
write_be_i32(&mut out, level.int("zPos").unwrap_or(0));
|
||||
write_be_i64(&mut out, level.long("LastUpdate").unwrap_or(0));
|
||||
write_be_i64(&mut out, level.long("InhabitedTime").unwrap_or(0));
|
||||
write_compressed_tile_storage(&mut out, &extract_lower_block_section(blocks));
|
||||
write_compressed_tile_storage(&mut out, &extract_upper_block_section(blocks));
|
||||
write_sparse_nibble_storage(&mut out, &extract_lower_nibble_section(data), false);
|
||||
write_sparse_nibble_storage(&mut out, &extract_upper_nibble_section(data), false);
|
||||
write_sparse_nibble_storage(&mut out, &extract_lower_nibble_section(sky_light), true);
|
||||
write_sparse_nibble_storage(&mut out, &extract_upper_nibble_section(sky_light), true);
|
||||
write_sparse_nibble_storage(&mut out, &extract_lower_nibble_section(block_light), true);
|
||||
write_sparse_nibble_storage(&mut out, &extract_upper_nibble_section(block_light), true);
|
||||
let hm = ensure_length(height_map, 256);
|
||||
out.extend_from_slice(&hm[..256]);
|
||||
write_be_i16(&mut out, level.short("TerrainPopulatedFlags").unwrap_or(0));
|
||||
let bio = ensure_length(biomes, 256);
|
||||
out.extend_from_slice(&bio[..256]);
|
||||
let dynamic_root = nbt::NbtCompound::new("");
|
||||
let dynamic_bytes = nbt::write_nbt(&dynamic_root);
|
||||
out.extend_from_slice(&dynamic_bytes);
|
||||
out
|
||||
}
|
||||
|
||||
fn is_compressed_chunk_storage(data: &[u8]) -> bool {
|
||||
if data.len() < 2 + 4 + 4 + 8 {
|
||||
return false;
|
||||
}
|
||||
let version = read_be_i16(data, 0);
|
||||
version == SAVE_FILE_VERSION_COMPRESSED_CHUNK_STORAGE || version == SAVE_FILE_VERSION_CHUNK_INHABITED_TIME
|
||||
}
|
||||
|
||||
pub fn try_read_chunk_coordinates(
|
||||
data: &[u8],
|
||||
) -> Option<(i32, i32, bool)> {
|
||||
if let Some((cx, cz, wrapped)) = try_read_legacy_level_coords(data) {
|
||||
return Some((cx, cz, wrapped));
|
||||
}
|
||||
|
||||
if is_compressed_chunk_storage(data) {
|
||||
let cx = read_be_i32(data, 2);
|
||||
let cz = read_be_i32(data, 6);
|
||||
return Some((cx, cz, false));
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
fn try_read_legacy_level_coords(data: &[u8]) -> Option<(i32, i32, bool)> {
|
||||
if data.is_empty() || data[0] != 10 {
|
||||
return None;
|
||||
}
|
||||
let compound = nbt::read_nbt(data).ok()?;
|
||||
let (level, has_wrapper) = if let Some(nbt::NbtValue::Compound(c)) = compound.get("Level") {
|
||||
(c, true)
|
||||
} else {
|
||||
(&compound, false)
|
||||
};
|
||||
let cx = level.int("xPos")?;
|
||||
let cz = level.int("zPos")?;
|
||||
Some((cx, cz, has_wrapper))
|
||||
}
|
||||
|
||||
pub fn force_chunk_coordinates(data: &[u8], expected_x: i32, expected_z: i32) -> Vec<u8> {
|
||||
if data.is_empty() {
|
||||
return Vec::new();
|
||||
}
|
||||
|
||||
if let Ok((mut level, _)) = read_legacy_level(data) {
|
||||
level.insert("xPos", nbt::NbtValue::Int(expected_x));
|
||||
level.insert("zPos", nbt::NbtValue::Int(expected_z));
|
||||
return encode_legacy_nbt(&level);
|
||||
}
|
||||
|
||||
if is_compressed_chunk_storage(data) {
|
||||
let mut patched = data.to_vec();
|
||||
patched[2..6].copy_from_slice(&expected_x.to_be_bytes());
|
||||
patched[6..10].copy_from_slice(&expected_z.to_be_bytes());
|
||||
return patched;
|
||||
}
|
||||
|
||||
Vec::new()
|
||||
}
|
||||
|
||||
fn read_compressed_tile_storage(data: &[u8], offset: &mut usize) -> Result<Vec<u8>, String> {
|
||||
let allocated_size = read_be_i32(data, *offset) as usize;
|
||||
*offset += 4;
|
||||
if allocated_size < 1024 || *offset + allocated_size > data.len() {
|
||||
return Err("Invalid CompressedTileStorage payload.".into());
|
||||
}
|
||||
|
||||
let blob = &data[*offset..*offset + allocated_size];
|
||||
*offset += allocated_size;
|
||||
let data_region = &blob[1024..];
|
||||
let mut blocks = vec![0u8; BLOCKS_PER_SECTION];
|
||||
for block in 0..512 {
|
||||
let block_index = u16::from_le_bytes([blob[block * 2], blob[block * 2 + 1]]);
|
||||
let index_type = block_index & INDEX_TYPE_MASK;
|
||||
if index_type == INDEX_TYPE_0_OR_8BIT {
|
||||
if (block_index & INDEX_TYPE_0BIT_FLAG) != 0 {
|
||||
let value = ((block_index >> 8) & 0xFF) as u8;
|
||||
for tile in 0..64 {
|
||||
blocks[get_compressed_tile_index(block, tile)] = value;
|
||||
}
|
||||
} else {
|
||||
let data_offset = ((block_index >> 1) & 0x7FFE) as usize;
|
||||
if data_offset + 64 > data_region.len() {
|
||||
return Err("Invalid 8-bit CompressedTileStorage offset.".into());
|
||||
}
|
||||
for tile in 0..64 {
|
||||
blocks[get_compressed_tile_index(block, tile)] = data_region[data_offset + tile];
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
let bits_per_tile = match index_type {
|
||||
INDEX_TYPE_1BIT => 1,
|
||||
INDEX_TYPE_2BIT => 2,
|
||||
INDEX_TYPE_4BIT => 4,
|
||||
_ => return Err("Unsupported CompressedTileStorage index type.".into()),
|
||||
};
|
||||
|
||||
let tile_type_count = 1usize << bits_per_tile;
|
||||
let tile_type_mask = (tile_type_count - 1) as u8;
|
||||
let index_shift = 3 - index_type as usize;
|
||||
let index_mask_bits = (7 >> index_type) as usize;
|
||||
let index_mask_bytes = (62 >> index_shift) as usize;
|
||||
let packed_data_size = (8usize << index_type) as usize;
|
||||
let data_offset_packed = ((block_index >> 1) & 0x7FFE) as usize;
|
||||
if data_offset_packed + tile_type_count + packed_data_size > data_region.len() {
|
||||
return Err("Invalid packed CompressedTileStorage offset.".into());
|
||||
}
|
||||
|
||||
let tile_types = &data_region[data_offset_packed..data_offset_packed + tile_type_count];
|
||||
let packed = &data_region[data_offset_packed + tile_type_count..data_offset_packed + tile_type_count + packed_data_size];
|
||||
for tile in 0..64 {
|
||||
let idx = (tile >> index_shift) & index_mask_bytes;
|
||||
let bit = (tile & index_mask_bits) * bits_per_tile;
|
||||
let palette_index = (packed[idx] >> bit) & tile_type_mask;
|
||||
blocks[get_compressed_tile_index(block, tile)] = tile_types[palette_index as usize];
|
||||
}
|
||||
}
|
||||
|
||||
Ok(blocks)
|
||||
}
|
||||
|
||||
fn skip_compressed_tile_storage(data: &[u8], offset: &mut usize) -> Result<(), String> {
|
||||
let allocated_size = read_be_i32(data, *offset) as usize;
|
||||
*offset += 4;
|
||||
if *offset + allocated_size > data.len() {
|
||||
return Err("Invalid CompressedTileStorage payload.".into());
|
||||
}
|
||||
*offset += allocated_size;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn read_sparse_nibble_storage(data: &[u8], offset: &mut usize, supports_all_fifteen: bool) -> Result<Vec<u8>, String> {
|
||||
let count = read_be_i32(data, *offset) as usize;
|
||||
*offset += 4;
|
||||
let storage_bytes = 128 + count * 128;
|
||||
if count > data.len() || *offset + storage_bytes > data.len() {
|
||||
return Err("Invalid SparseStorage payload.".into());
|
||||
}
|
||||
|
||||
let blob = &data[*offset..*offset + storage_bytes];
|
||||
*offset += storage_bytes;
|
||||
let plane_indices = &blob[..128];
|
||||
let plane_data = &blob[128..];
|
||||
let mut nibble_data = vec![0u8; NIBBLES_PER_SECTION];
|
||||
for y in 0..128 {
|
||||
let plane_index = plane_indices[y];
|
||||
if plane_index == SPARSE_ALL_ZERO_INDEX {
|
||||
continue;
|
||||
}
|
||||
if supports_all_fifteen && plane_index == SPARSE_ALL_FIFTEEN_INDEX {
|
||||
for xz in 0..256 {
|
||||
set_nibble_value(&mut nibble_data, xz, y, 15);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
let plane_offset = plane_index as usize * 128;
|
||||
if plane_offset + 128 > plane_data.len() {
|
||||
return Err("Invalid sparse plane index.".into());
|
||||
}
|
||||
|
||||
let plane = &plane_data[plane_offset..plane_offset + 128];
|
||||
for xz in 0..128 {
|
||||
let packed = plane[xz];
|
||||
set_nibble_value(&mut nibble_data, xz * 2, y, packed & 0x0F);
|
||||
set_nibble_value(&mut nibble_data, xz * 2 + 1, y, (packed >> 4) & 0x0F);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(nibble_data)
|
||||
}
|
||||
|
||||
fn skip_sparse_nibble_storage(data: &[u8], offset: &mut usize) -> Result<(), String> {
|
||||
let count = read_be_i32(data, *offset) as usize;
|
||||
*offset += 4;
|
||||
let storage_bytes = 128 + count * 128;
|
||||
if count > data.len() || *offset + storage_bytes > data.len() {
|
||||
return Err("Invalid SparseStorage payload.".into());
|
||||
}
|
||||
*offset += storage_bytes;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn read_sized_bytes(data: &[u8], offset: &mut usize, length: usize) -> Vec<u8> {
|
||||
let end = (*offset + length).min(data.len());
|
||||
let result = data[*offset..end].to_vec();
|
||||
*offset += length;
|
||||
result
|
||||
}
|
||||
|
||||
fn read_legacy_level(data: &[u8]) -> Result<(nbt::NbtCompound, bool), String> {
|
||||
if data.is_empty() || data[0] != 10 {
|
||||
return Err("Not NBT compound".into());
|
||||
}
|
||||
let file = nbt::read_nbt(data)?;
|
||||
let has_level = file.get("Level").is_some();
|
||||
let level = if let Some(nbt::NbtValue::Compound(c)) = file.get("Level") {
|
||||
let mut cloned = c.clone();
|
||||
cloned.name = "Level".to_string();
|
||||
cloned
|
||||
} else {
|
||||
let mut cloned = file.clone();
|
||||
cloned.name = "Level".to_string();
|
||||
cloned
|
||||
};
|
||||
Ok((level, has_level))
|
||||
}
|
||||
|
||||
pub fn try_decode_to_legacy_nbt(data: &[u8]) -> Option<Vec<u8>> {
|
||||
if let Ok((mut level, _)) = read_legacy_level(data) {
|
||||
level.name = "Level".to_string();
|
||||
return Some(encode_legacy_nbt(&level));
|
||||
}
|
||||
|
||||
if !is_compressed_chunk_storage(data) {
|
||||
return None;
|
||||
}
|
||||
|
||||
decode_compressed_chunk_to_legacy_root(data).ok().map(|root| nbt::write_nbt(&root))
|
||||
}
|
||||
|
||||
fn decode_compressed_chunk_to_legacy_root(data: &[u8]) -> Result<nbt::NbtCompound, String> {
|
||||
let mut offset = 0;
|
||||
let version = read_be_i16(data, offset);
|
||||
offset = 2;
|
||||
if version != SAVE_FILE_VERSION_COMPRESSED_CHUNK_STORAGE
|
||||
&& version != SAVE_FILE_VERSION_CHUNK_INHABITED_TIME
|
||||
{
|
||||
return Err(format!("Unsupported compressed chunk version: {}", version));
|
||||
}
|
||||
|
||||
let chunk_x = read_be_i32(data, offset);
|
||||
offset += 4;
|
||||
let chunk_z = read_be_i32(data, offset);
|
||||
offset += 4;
|
||||
let last_update = read_be_i64(data, offset);
|
||||
offset += 8;
|
||||
let inhabited_time = if version >= SAVE_FILE_VERSION_CHUNK_INHABITED_TIME {
|
||||
let v = read_be_i64(data, offset);
|
||||
offset += 8;
|
||||
v
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
let lower_blocks = read_compressed_tile_storage(data, &mut offset)?;
|
||||
let upper_blocks = read_compressed_tile_storage(data, &mut offset)?;
|
||||
let lower_data = read_sparse_nibble_storage(data, &mut offset, false)?;
|
||||
let upper_data = read_sparse_nibble_storage(data, &mut offset, false)?;
|
||||
let lower_sky = read_sparse_nibble_storage(data, &mut offset, true)?;
|
||||
let upper_sky = read_sparse_nibble_storage(data, &mut offset, true)?;
|
||||
let lower_block_light = read_sparse_nibble_storage(data, &mut offset, true)?;
|
||||
let upper_block_light = read_sparse_nibble_storage(data, &mut offset, true)?;
|
||||
let height_map = read_sized_bytes(data, &mut offset, 256);
|
||||
let terrain_populated_flags = read_be_i16(data, offset);
|
||||
offset += 2;
|
||||
let biomes = read_sized_bytes(data, &mut offset, 256);
|
||||
let dynamic_root = if offset < data.len() {
|
||||
nbt::read_nbt(&data[offset..]).unwrap_or_default()
|
||||
} else {
|
||||
nbt::NbtCompound::default()
|
||||
};
|
||||
|
||||
let mut level = nbt::NbtCompound::new("Level");
|
||||
level.insert("xPos", nbt::NbtValue::Int(chunk_x));
|
||||
level.insert("zPos", nbt::NbtValue::Int(chunk_z));
|
||||
level.insert("LastUpdate", nbt::NbtValue::Long(last_update));
|
||||
level.insert("InhabitedTime", nbt::NbtValue::Long(inhabited_time));
|
||||
level.insert(
|
||||
"Blocks",
|
||||
nbt::NbtValue::ByteArray(combine_block_sections(&lower_blocks, &upper_blocks)),
|
||||
);
|
||||
level.insert(
|
||||
"Data",
|
||||
nbt::NbtValue::ByteArray(combine_nibble_sections(&lower_data, &upper_data)),
|
||||
);
|
||||
level.insert(
|
||||
"SkyLight",
|
||||
nbt::NbtValue::ByteArray(combine_nibble_sections(&lower_sky, &upper_sky)),
|
||||
);
|
||||
level.insert(
|
||||
"BlockLight",
|
||||
nbt::NbtValue::ByteArray(combine_nibble_sections(
|
||||
&lower_block_light,
|
||||
&upper_block_light,
|
||||
)),
|
||||
);
|
||||
level.insert("HeightMap", nbt::NbtValue::ByteArray(height_map));
|
||||
level.insert(
|
||||
"TerrainPopulatedFlags",
|
||||
nbt::NbtValue::Short(terrain_populated_flags),
|
||||
);
|
||||
level.insert("Biomes", nbt::NbtValue::ByteArray(biomes));
|
||||
let entities = dynamic_root
|
||||
.list("Entities")
|
||||
.map(|l| nbt::NbtValue::List(l.to_vec()))
|
||||
.unwrap_or_else(|| nbt::NbtValue::List(Vec::new()));
|
||||
level.insert("Entities", entities);
|
||||
let tile_entities = dynamic_root
|
||||
.list("TileEntities")
|
||||
.map(|l| nbt::NbtValue::List(l.to_vec()))
|
||||
.unwrap_or_else(|| nbt::NbtValue::List(Vec::new()));
|
||||
level.insert("TileEntities", tile_entities);
|
||||
if let Some(tile_ticks) = dynamic_root.get("TileTicks") {
|
||||
level.insert("TileTicks", tile_ticks.clone());
|
||||
}
|
||||
|
||||
let mut root = nbt::NbtCompound::new("");
|
||||
root.insert("Level", nbt::NbtValue::Compound(level));
|
||||
Ok(root)
|
||||
}
|
||||
|
||||
pub fn try_get_compressed_chunk_nbt_offset(data: &[u8]) -> Option<usize> {
|
||||
if !is_compressed_chunk_storage(data) {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut offset = 0;
|
||||
let version = read_be_i16(data, offset);
|
||||
offset = 2;
|
||||
if version != SAVE_FILE_VERSION_COMPRESSED_CHUNK_STORAGE
|
||||
&& version != SAVE_FILE_VERSION_CHUNK_INHABITED_TIME
|
||||
{
|
||||
return None;
|
||||
}
|
||||
|
||||
offset += 8;
|
||||
if version >= SAVE_FILE_VERSION_CHUNK_INHABITED_TIME {
|
||||
offset += 8;
|
||||
}
|
||||
|
||||
let r1 = skip_compressed_tile_storage(data, &mut offset);
|
||||
if r1.is_err() { return None; }
|
||||
let r2 = skip_compressed_tile_storage(data, &mut offset);
|
||||
if r2.is_err() { return None; }
|
||||
let r3 = skip_sparse_nibble_storage(data, &mut offset);
|
||||
if r3.is_err() { return None; }
|
||||
let r4 = skip_sparse_nibble_storage(data, &mut offset);
|
||||
if r4.is_err() { return None; }
|
||||
let r5 = skip_sparse_nibble_storage(data, &mut offset);
|
||||
if r5.is_err() { return None; }
|
||||
let r6 = skip_sparse_nibble_storage(data, &mut offset);
|
||||
if r6.is_err() { return None; }
|
||||
let r7 = skip_sparse_nibble_storage(data, &mut offset);
|
||||
if r7.is_err() { return None; }
|
||||
let r8 = skip_sparse_nibble_storage(data, &mut offset);
|
||||
if r8.is_err() { return None; }
|
||||
offset += 256;
|
||||
offset += 2;
|
||||
offset += 256;
|
||||
if offset > data.len() {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(offset)
|
||||
}
|
||||
@@ -0,0 +1,802 @@
|
||||
use std::collections::HashMap;
|
||||
use std::fs;
|
||||
use std::io::{Read, Write};
|
||||
use std::path::Path;
|
||||
use flate2::read::ZlibDecoder;
|
||||
use flate2::write::ZlibEncoder;
|
||||
use flate2::Compression;
|
||||
use super::nbt::{self, NbtCompound, NbtValue};
|
||||
const SECTOR_BYTES: usize = 4096;
|
||||
pub struct JavaWorldReader {
|
||||
world_path: String,
|
||||
region_readers: HashMap<String, JavaRegionReader>,
|
||||
}
|
||||
|
||||
pub struct JavaRegionReader {
|
||||
pub data: Vec<u8>,
|
||||
pub offsets: [u32; 1024],
|
||||
}
|
||||
|
||||
impl JavaWorldReader {
|
||||
pub fn new(world_path: &str) -> Self {
|
||||
Self {
|
||||
world_path: world_path.to_string(),
|
||||
region_readers: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn read_level_dat(&self) -> Result<NbtCompound, String> {
|
||||
let path = Path::new(&self.world_path).join("level.dat");
|
||||
let data = fs::read(&path).map_err(|e| format!("Failed to read level.dat: {}", e))?;
|
||||
nbt::read_gzip_nbt(&data)
|
||||
}
|
||||
|
||||
pub fn get_region_dir(&self, dimension: &str) -> String {
|
||||
if dimension.is_empty() {
|
||||
Path::new(&self.world_path)
|
||||
.join("region")
|
||||
.to_string_lossy()
|
||||
.to_string()
|
||||
} else {
|
||||
Path::new(&self.world_path)
|
||||
.join(dimension)
|
||||
.join("region")
|
||||
.to_string_lossy()
|
||||
.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_region_files(&self, dimension: &str) -> Vec<(i32, i32, String)> {
|
||||
let mut result = Vec::new();
|
||||
let dir = self.get_region_dir(dimension);
|
||||
let dir_path = Path::new(&dir);
|
||||
if !dir_path.is_dir() {
|
||||
return result;
|
||||
}
|
||||
|
||||
if let Ok(entries) = fs::read_dir(dir_path) {
|
||||
for entry in entries.flatten() {
|
||||
let name = entry.file_name().to_string_lossy().to_string();
|
||||
let lower = name.to_lowercase();
|
||||
if lower.ends_with(".mcr") || lower.ends_with(".mca") {
|
||||
let parts: Vec<&str> = name.split('.').collect();
|
||||
if parts.len() == 4 && parts[0] == "r" {
|
||||
if let (Ok(rx), Ok(rz)) =(parts[1].parse::<i32>(), parts[2].parse::<i32>())
|
||||
{
|
||||
result.push((rx, rz, entry.path().to_string_lossy().to_string()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
pub fn is_anvil_world(&self) -> bool {
|
||||
let region_dir = self.get_region_dir("");
|
||||
let dir_path = Path::new(®ion_dir);
|
||||
if !dir_path.is_dir() {
|
||||
return false;
|
||||
}
|
||||
if let Ok(entries) = fs::read_dir(dir_path) {
|
||||
for entry in entries.flatten() {
|
||||
let name = entry.file_name().to_string_lossy().to_string();
|
||||
if name.to_lowercase().ends_with(".mca") {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
fn get_or_create_reader(&mut self, region_path: &str) -> Result<&JavaRegionReader, String> {
|
||||
if !self.region_readers.contains_key(region_path) {
|
||||
let reader = JavaRegionReader::open(region_path)?;
|
||||
self.region_readers.insert(region_path.to_string(), reader);
|
||||
}
|
||||
Ok(self.region_readers.get(region_path).unwrap())
|
||||
}
|
||||
|
||||
pub fn has_chunk(&mut self, region_path: &str, local_x: i32, local_z: i32) -> bool {
|
||||
let reader = match self.get_or_create_reader(region_path) {
|
||||
Ok(r) => r,
|
||||
Err(_) => return false,
|
||||
};
|
||||
let index = (local_x & 31) + (local_z & 31) * 32;
|
||||
reader.offsets[index as usize] != 0
|
||||
}
|
||||
|
||||
pub fn read_chunk_nbt(
|
||||
&mut self,
|
||||
region_path: &str,
|
||||
local_x: i32,
|
||||
local_z: i32,
|
||||
) -> Result<Option<NbtCompound>, String> {
|
||||
let reader = self.get_or_create_reader(region_path)?;
|
||||
let index = (local_x & 31) + (local_z & 31) * 32;
|
||||
let offset_entry = reader.offsets[index as usize];
|
||||
if offset_entry == 0 {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let sector_offset = (offset_entry >> 8) as usize;
|
||||
let chunk_pos = sector_offset * SECTOR_BYTES;
|
||||
let data_len = reader.data.len();
|
||||
if chunk_pos + 5 > data_len {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let length = u32::from_be_bytes([
|
||||
reader.data[chunk_pos],
|
||||
reader.data[chunk_pos + 1],
|
||||
reader.data[chunk_pos + 2],
|
||||
reader.data[chunk_pos + 3],
|
||||
]) as usize;
|
||||
let compression_type = reader.data[chunk_pos + 4];
|
||||
if length <= 1 {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let compressed_length = length - 1;
|
||||
if chunk_pos + 5 + compressed_length > data_len {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let compressed_data = &reader.data[chunk_pos + 5..chunk_pos + 5 + compressed_length];
|
||||
let decompressed = match compression_type {
|
||||
1 => {
|
||||
let mut decoder = flate2::read::GzDecoder::new(compressed_data);
|
||||
let mut buf = Vec::new();
|
||||
decoder.read_to_end(&mut buf).map_err(|e| format!("gzip decompress failed: {}", e))?;
|
||||
buf
|
||||
}
|
||||
2 => {
|
||||
let mut decoder = ZlibDecoder::new(compressed_data);
|
||||
let mut buf = Vec::new();
|
||||
decoder
|
||||
.read_to_end(&mut buf)
|
||||
.map_err(|e| format!("zlib decompress failed: {}", e))?;
|
||||
buf
|
||||
}
|
||||
_ => return Ok(None),
|
||||
};
|
||||
|
||||
if decompressed.is_empty() {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let compound = nbt::read_nbt(&decompressed)?;
|
||||
Ok(Some(compound))
|
||||
}
|
||||
}
|
||||
|
||||
impl JavaRegionReader {
|
||||
fn open(path: &str) -> Result<Self, String> {
|
||||
let data = fs::read(path).map_err(|e| format!("Failed to read region file: {}", e))?;
|
||||
let mut offsets = [0u32; 1024];
|
||||
for i in 0..1024 {
|
||||
let off = i * 4;
|
||||
if off + 4 > data.len() {
|
||||
break;
|
||||
}
|
||||
offsets[i] = u32::from_be_bytes([data[off], data[off + 1], data[off + 2], data[off + 3]]);
|
||||
}
|
||||
|
||||
Ok(Self { data, offsets })
|
||||
}
|
||||
|
||||
pub fn open_from_bytes(data: &[u8]) -> Self {
|
||||
let data = data.to_vec();
|
||||
let mut offsets = [0u32; 1024];
|
||||
for i in 0..1024 {
|
||||
let off = i * 4;
|
||||
if off + 4 > data.len() {
|
||||
break;
|
||||
}
|
||||
offsets[i] = u32::from_be_bytes([data[off], data[off + 1], data[off + 2], data[off + 3]]);
|
||||
}
|
||||
|
||||
Self { data, offsets }
|
||||
}
|
||||
|
||||
pub fn read_chunk(
|
||||
&mut self,
|
||||
local_x: i32,
|
||||
local_z: i32,
|
||||
) -> Result<Option<Vec<u8>>, String> {
|
||||
let index = (local_x & 31) + (local_z & 31) * 32;
|
||||
let offset_entry = self.offsets[index as usize];
|
||||
if offset_entry == 0 {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let sector_offset = (offset_entry >> 8) as usize;
|
||||
let chunk_pos = sector_offset * SECTOR_BYTES;
|
||||
let data_len = self.data.len();
|
||||
if chunk_pos + 5 > data_len {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let length = u32::from_be_bytes([
|
||||
self.data[chunk_pos],
|
||||
self.data[chunk_pos + 1],
|
||||
self.data[chunk_pos + 2],
|
||||
self.data[chunk_pos + 3],
|
||||
]) as usize;
|
||||
let compression_type = self.data[chunk_pos + 4];
|
||||
if length <= 1 {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let compressed_length = length - 1;
|
||||
if chunk_pos + 5 + compressed_length > data_len {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let compressed_data = &self.data[chunk_pos + 5..chunk_pos + 5 + compressed_length];
|
||||
let decompressed = match compression_type {
|
||||
1 => {
|
||||
let mut decoder = flate2::read::GzDecoder::new(compressed_data);
|
||||
let mut buf = Vec::new();
|
||||
decoder
|
||||
.read_to_end(&mut buf)
|
||||
.map_err(|e| format!("gzip decompress failed: {}", e))?;
|
||||
buf
|
||||
}
|
||||
2 => {
|
||||
let mut decoder = ZlibDecoder::new(compressed_data);
|
||||
let mut buf = Vec::new();
|
||||
decoder
|
||||
.read_to_end(&mut buf)
|
||||
.map_err(|e| format!("zlib decompress failed: {}", e))?;
|
||||
buf
|
||||
}
|
||||
_ => return Ok(None),
|
||||
};
|
||||
|
||||
if decompressed.is_empty() {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
Ok(Some(decompressed))
|
||||
}
|
||||
}
|
||||
|
||||
pub struct JavaRegionFileWriter {
|
||||
path: String,
|
||||
buffer: Vec<u8>,
|
||||
offsets: [i32; 1024],
|
||||
timestamps: [i32; 1024],
|
||||
next_sector: usize,
|
||||
}
|
||||
|
||||
impl JavaRegionFileWriter {
|
||||
pub fn new(path: &str) -> Self {
|
||||
let buffer = vec![0u8; SECTOR_BYTES * 2];
|
||||
let offsets = [0i32; 1024];
|
||||
let timestamps = [0i32; 1024];
|
||||
Self {
|
||||
path: path.to_string(),
|
||||
buffer,
|
||||
offsets,
|
||||
timestamps,
|
||||
next_sector: 2,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn load_from_file(path: &str) -> Self {
|
||||
if let Ok(data) = fs::read(path) {
|
||||
let buffer = data.clone();
|
||||
let mut offsets = [0i32; 1024];
|
||||
let mut timestamps = [0i32; 1024];
|
||||
for i in 0..1024 {
|
||||
let pos = i * 4;
|
||||
if pos + 4 <= data.len() {
|
||||
offsets[i] = i32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]);
|
||||
}
|
||||
}
|
||||
|
||||
for i in 0..1024 {
|
||||
let pos = SECTOR_BYTES + i * 4;
|
||||
if pos + 4 <= data.len() {
|
||||
timestamps[i] = i32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]);
|
||||
}
|
||||
}
|
||||
|
||||
let mut max_sector = 2;
|
||||
for &off in &offsets {
|
||||
if off != 0 {
|
||||
let sector = (off >> 8) as usize;
|
||||
let count = (off & 0xFF) as usize;
|
||||
let end = sector + count;
|
||||
if end > max_sector {
|
||||
max_sector = end;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Self {
|
||||
path: path.to_string(),
|
||||
buffer,
|
||||
offsets,
|
||||
timestamps,
|
||||
next_sector: max_sector,
|
||||
}
|
||||
} else {
|
||||
Self::new(path)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn write_chunk(&mut self, local_x: i32, local_z: i32, uncompressed_nbt: &[u8]) {
|
||||
if local_x < 0 || local_x > 31 || local_z < 0 || local_z > 31 {
|
||||
return;
|
||||
}
|
||||
|
||||
let compressed = compress_zlib(uncompressed_nbt);
|
||||
let payload_length = 1 + compressed.len();
|
||||
let total_length = 4 + payload_length;
|
||||
let sectors_needed = (total_length + SECTOR_BYTES - 1) / SECTOR_BYTES;
|
||||
if sectors_needed >= 256 {
|
||||
return;
|
||||
}
|
||||
|
||||
let sector_start = self.next_sector;
|
||||
self.next_sector += sectors_needed;
|
||||
while self.buffer.len() < (sector_start + sectors_needed) * SECTOR_BYTES {
|
||||
self.buffer.extend(std::iter::repeat(0u8).take(SECTOR_BYTES));
|
||||
}
|
||||
|
||||
let write_pos = sector_start * SECTOR_BYTES;
|
||||
self.buffer[write_pos..write_pos + 4]
|
||||
.copy_from_slice(&(payload_length as u32).to_be_bytes());
|
||||
self.buffer[write_pos + 4] = 2;
|
||||
self.buffer[write_pos + 5..write_pos + 5 + compressed.len()]
|
||||
.copy_from_slice(&compressed);
|
||||
|
||||
let padding = sectors_needed * SECTOR_BYTES - total_length;
|
||||
if padding > 0 {
|
||||
for i in 0..padding {
|
||||
self.buffer[write_pos + total_length + i] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
let offset_index = (local_x & 31) + (local_z & 31) * 32;
|
||||
self.offsets[offset_index as usize] = ((sector_start as i32) << 8) | (sectors_needed as i32);
|
||||
let now = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap_or_default()
|
||||
.as_secs() as i32;
|
||||
self.timestamps[offset_index as usize] = now;
|
||||
}
|
||||
|
||||
pub fn save(&mut self) -> Result<(), String> {
|
||||
while self.buffer.len() < self.next_sector * SECTOR_BYTES {
|
||||
self.buffer.extend(std::iter::repeat(0u8).take(SECTOR_BYTES));
|
||||
}
|
||||
|
||||
for i in 0..1024 {
|
||||
let pos = i * 4;
|
||||
self.buffer[pos..pos + 4].copy_from_slice(&(self.offsets[i] as u32).to_be_bytes());
|
||||
}
|
||||
|
||||
for i in 0..1024 {
|
||||
let pos = SECTOR_BYTES + i * 4;
|
||||
self.buffer[pos..pos + 4]
|
||||
.copy_from_slice(&(self.timestamps[i] as u32).to_be_bytes());
|
||||
}
|
||||
|
||||
let total = self.next_sector * SECTOR_BYTES;
|
||||
let data = &self.buffer[..total];
|
||||
if let Some(parent) = Path::new(&self.path).parent() {
|
||||
fs::create_dir_all(parent)
|
||||
.map_err(|e| format!("Failed to create region directory: {}", e))?;
|
||||
}
|
||||
|
||||
fs::write(&self.path, data)
|
||||
.map_err(|e| format!("Failed to write region file: {}", e))?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub struct LceRegionFile {
|
||||
filename: String,
|
||||
data: Vec<u8>,
|
||||
offsets: [i32; 1024],
|
||||
timestamps: [i32; 1024],
|
||||
region_x: i32,
|
||||
region_z: i32,
|
||||
sector_count: usize,
|
||||
}
|
||||
|
||||
impl LceRegionFile {
|
||||
pub fn new(filename: &str) -> Self {
|
||||
let (region_x, region_z) = parse_region_coordinates(filename);
|
||||
let data = vec![0u8; SECTOR_BYTES * 2];
|
||||
Self {
|
||||
filename: filename.to_string(),
|
||||
data,
|
||||
offsets: [0i32; 1024],
|
||||
timestamps: [0i32; 1024],
|
||||
region_x,
|
||||
region_z,
|
||||
sector_count: 2,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn write_chunk(&mut self, x: i32, z: i32, mut uncompressed_data: Vec<u8>) {
|
||||
if x < 0 || x >= 32 || z < 0 || z >= 32 {
|
||||
return;
|
||||
}
|
||||
if uncompressed_data.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
uncompressed_data = force_chunk_coordinates(
|
||||
&uncompressed_data,
|
||||
self.region_x * 32 + x,
|
||||
self.region_z * 32 + z,
|
||||
);
|
||||
if uncompressed_data.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let compressed = compress_zlib_only(&uncompressed_data);
|
||||
let total_size = 8 + compressed.len();
|
||||
let sectors_needed = (total_size + SECTOR_BYTES - 1) / SECTOR_BYTES;
|
||||
if sectors_needed >= 256 {
|
||||
return;
|
||||
}
|
||||
|
||||
let sector_number = self.sector_count;
|
||||
self.sector_count += sectors_needed;
|
||||
while self.data.len() < (sector_number + sectors_needed) * SECTOR_BYTES {
|
||||
self.data.extend(std::iter::repeat(0u8).take(SECTOR_BYTES));
|
||||
}
|
||||
|
||||
let write_pos = sector_number * SECTOR_BYTES;
|
||||
let stored_length = compressed.len() as u32;
|
||||
self.data[write_pos..write_pos + 4]
|
||||
.copy_from_slice(&stored_length.to_le_bytes());
|
||||
self.data[write_pos + 4..write_pos + 8]
|
||||
.copy_from_slice(&(uncompressed_data.len() as u32).to_le_bytes());
|
||||
self.data[write_pos + 8..write_pos + 8 + compressed.len()]
|
||||
.copy_from_slice(&compressed);
|
||||
|
||||
let padding = sectors_needed * SECTOR_BYTES - total_size;
|
||||
if padding > 0 {
|
||||
for i in 0..padding {
|
||||
self.data[write_pos + total_size + i] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
let offset_index = (x + z * 32) as usize;
|
||||
self.offsets[offset_index] = ((sector_number as i32) << 8) | (sectors_needed as i32);
|
||||
let now = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap_or_default()
|
||||
.as_secs() as i32;
|
||||
self.timestamps[offset_index] = now;
|
||||
}
|
||||
|
||||
pub fn write_to_container(&mut self, container: &mut SaveDataContainer) {
|
||||
let _sorted_entries: Vec<(usize, i32, i32)> = (0..1024)
|
||||
.map(|i| (i, self.offsets[i], self.timestamps[i]))
|
||||
.filter(|(_, off, _)| *off != 0)
|
||||
.collect();
|
||||
|
||||
for i in 0..1024 {
|
||||
let pos = i * 4;
|
||||
self.data[pos..pos + 4].copy_from_slice(&(self.offsets[i] as u32).to_le_bytes());
|
||||
}
|
||||
|
||||
for i in 0..1024 {
|
||||
let pos = SECTOR_BYTES + i * 4;
|
||||
self.data[pos..pos + 4]
|
||||
.copy_from_slice(&(self.timestamps[i] as u32).to_le_bytes());
|
||||
}
|
||||
|
||||
let total = self.sector_count * SECTOR_BYTES;
|
||||
let region_bytes = self.data[..total].to_vec();
|
||||
let entry = container.create_file(&self.filename);
|
||||
container.write_to_file(entry, ®ion_bytes);
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_region_coordinates(filename: &str) -> (i32, i32) {
|
||||
let name = Path::new(filename)
|
||||
.file_stem()
|
||||
.unwrap_or_default()
|
||||
.to_string_lossy()
|
||||
.to_string();
|
||||
let parts: Vec<&str> = name.split('.').collect();
|
||||
if parts.len() < 3 {
|
||||
return (0, 0);
|
||||
}
|
||||
let rx = parts[parts.len() - 2].parse().unwrap_or(0);
|
||||
let rz = parts[parts.len() - 1].parse().unwrap_or(0);
|
||||
(rx, rz)
|
||||
}
|
||||
|
||||
pub fn force_chunk_coordinates(data: &[u8], expected_x: i32, expected_z: i32) -> Vec<u8> {
|
||||
if data.is_empty() {
|
||||
return Vec::new();
|
||||
}
|
||||
|
||||
if let Ok(mut compound) = nbt::read_nbt(data) {
|
||||
if compound.get("Level").is_some() {
|
||||
if let Some(nbt::NbtValue::Compound(c)) = compound.get_mut("Level") {
|
||||
c.insert("xPos", NbtValue::Int(expected_x));
|
||||
c.insert("zPos", NbtValue::Int(expected_z));
|
||||
}
|
||||
return nbt::write_nbt(&compound);
|
||||
}
|
||||
|
||||
compound.insert("xPos", NbtValue::Int(expected_x));
|
||||
compound.insert("zPos", NbtValue::Int(expected_z));
|
||||
let mut root = NbtCompound::new("");
|
||||
root.insert("Level", NbtValue::Compound(compound));
|
||||
nbt::write_nbt(&root)
|
||||
} else if data.len() >= 10 {
|
||||
let version = i16::from_be_bytes([data[0], data[1]]);
|
||||
if version == 8 || version == 9 {
|
||||
let mut patched = data.to_vec();
|
||||
patched[2..6].copy_from_slice(&expected_x.to_be_bytes());
|
||||
patched[6..10].copy_from_slice(&expected_z.to_be_bytes());
|
||||
return patched;
|
||||
}
|
||||
Vec::new()
|
||||
} else {
|
||||
Vec::new()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct SaveDataContainer {
|
||||
original_save_version: i16,
|
||||
current_save_version: i16,
|
||||
entries: Vec<SaveFileEntry>,
|
||||
blob: Vec<u8>,
|
||||
data_end: usize,
|
||||
}
|
||||
|
||||
pub struct SaveFileEntry {
|
||||
pub name: String,
|
||||
pub length: u32,
|
||||
pub start_offset: u32,
|
||||
pub last_mod: i64,
|
||||
pub current_pointer: u32,
|
||||
}
|
||||
|
||||
impl SaveDataContainer {
|
||||
pub const FILE_ENTRY_SIZE: usize = 144;
|
||||
pub fn new(original_save_version: i16, current_save_version: i16) -> Self {
|
||||
Self {
|
||||
original_save_version,
|
||||
current_save_version,
|
||||
entries: Vec::new(),
|
||||
blob: vec![0u8; 2 * 1024 * 1024],
|
||||
data_end: 12,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn create_file(&mut self, name: &str) -> usize {
|
||||
if let Some(idx) = self.entries.iter().position(|e| e.name == name) {
|
||||
return idx;
|
||||
}
|
||||
let entry = SaveFileEntry {
|
||||
name: name.to_string(),
|
||||
length: 0,
|
||||
start_offset: self.data_end as u32,
|
||||
last_mod: 0,
|
||||
current_pointer: self.data_end as u32,
|
||||
};
|
||||
self.entries.push(entry);
|
||||
self.entries.len() - 1
|
||||
}
|
||||
|
||||
pub fn write_to_file(&mut self, index: usize, data: &[u8]) {
|
||||
if data.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let needs_reinit = self.entries[index].length == 0;
|
||||
if needs_reinit {
|
||||
self.entries[index].start_offset = self.data_end as u32;
|
||||
self.entries[index].current_pointer = self.data_end as u32;
|
||||
}
|
||||
|
||||
let write_pos = self.entries[index].current_pointer as usize;
|
||||
let end_pos = write_pos + data.len();
|
||||
self.ensure_capacity(end_pos);
|
||||
self.blob[write_pos..write_pos + data.len()].copy_from_slice(data);
|
||||
self.entries[index].current_pointer += data.len() as u32;
|
||||
let written = self.entries[index].current_pointer - self.entries[index].start_offset;
|
||||
if written > self.entries[index].length {
|
||||
self.entries[index].length = written;
|
||||
}
|
||||
|
||||
let entry_end = (self.entries[index].start_offset + self.entries[index].length) as usize;
|
||||
if entry_end > self.data_end {
|
||||
self.data_end = entry_end;
|
||||
}
|
||||
|
||||
let now = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap_or_default()
|
||||
.as_millis() as i64;
|
||||
self.entries[index].last_mod = now;
|
||||
}
|
||||
|
||||
pub fn save(&mut self, output_path: &str) -> Result<(), String> {
|
||||
self.write_header();
|
||||
let total_size = self.data_end + self.entries.len() * Self::FILE_ENTRY_SIZE;
|
||||
self.ensure_capacity(total_size);
|
||||
self.write_footer();
|
||||
let raw_blob = self.blob[..total_size].to_vec();
|
||||
let mut encoder = ZlibEncoder::new(Vec::new(), Compression::best());
|
||||
encoder
|
||||
.write_all(&raw_blob)
|
||||
.map_err(|e| format!("zlib compress failed: {}", e))?;
|
||||
let compressed_data = encoder
|
||||
.finish()
|
||||
.map_err(|e| format!("zlib finish failed: {}", e))?;
|
||||
|
||||
if let Some(parent) = Path::new(output_path).parent() {
|
||||
fs::create_dir_all(parent)
|
||||
.map_err(|e| format!("Failed to create output directory: {}", e))?;
|
||||
}
|
||||
|
||||
let mut file = fs::File::create(output_path)
|
||||
.map_err(|e| format!("Failed to create output file: {}", e))?;
|
||||
file.write_all(&(0u32).to_le_bytes())
|
||||
.map_err(|e| format!("write failed: {}", e))?;
|
||||
file.write_all(&(total_size as u32).to_le_bytes())
|
||||
.map_err(|e| format!("write failed: {}", e))?;
|
||||
file.write_all(&compressed_data)
|
||||
.map_err(|e| format!("write failed: {}", e))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn write_header(&mut self) {
|
||||
self.blob[0..4]
|
||||
.copy_from_slice(&(self.data_end as u32).to_le_bytes());
|
||||
self.blob[4..8]
|
||||
.copy_from_slice(&(self.entries.len() as u32).to_le_bytes());
|
||||
self.blob[8..10]
|
||||
.copy_from_slice(&self.original_save_version.to_le_bytes());
|
||||
self.blob[10..12]
|
||||
.copy_from_slice(&self.current_save_version.to_le_bytes());
|
||||
}
|
||||
|
||||
fn write_footer(&mut self) {
|
||||
let mut sorted: Vec<usize> = (0..self.entries.len()).collect();
|
||||
sorted.sort_by_key(|&i| self.entries[i].start_offset);
|
||||
let mut pos = self.data_end;
|
||||
for &i in &sorted {
|
||||
let entry = &self.entries[i];
|
||||
let mut name_bytes = vec![0u8; 128];
|
||||
let encoded: Vec<u8> = entry
|
||||
.name
|
||||
.encode_utf16()
|
||||
.flat_map(|c| c.to_le_bytes())
|
||||
.collect();
|
||||
let copy_len = encoded.len().min(126);
|
||||
name_bytes[..copy_len].copy_from_slice(&encoded[..copy_len]);
|
||||
self.blob[pos..pos + 128].copy_from_slice(&name_bytes);
|
||||
pos += 128;
|
||||
self.blob[pos..pos + 4].copy_from_slice(&entry.length.to_le_bytes());
|
||||
pos += 4;
|
||||
self.blob[pos..pos + 4].copy_from_slice(&entry.start_offset.to_le_bytes());
|
||||
pos += 4;
|
||||
self.blob[pos..pos + 8].copy_from_slice(&entry.last_mod.to_le_bytes());
|
||||
pos += 8;
|
||||
}
|
||||
}
|
||||
|
||||
fn ensure_capacity(&mut self, required: usize) {
|
||||
if required <= self.blob.len() {
|
||||
return;
|
||||
}
|
||||
let mut new_size = self.blob.len();
|
||||
while new_size < required {
|
||||
new_size *= 2;
|
||||
}
|
||||
self.blob.resize(new_size, 0);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn compress_zlib(data: &[u8]) -> Vec<u8> {
|
||||
let mut encoder = ZlibEncoder::new(Vec::new(), Compression::best());
|
||||
encoder.write_all(data).unwrap();
|
||||
encoder.finish().unwrap()
|
||||
}
|
||||
|
||||
pub fn compress_zlib_only(data: &[u8]) -> Vec<u8> {
|
||||
compress_zlib(data)
|
||||
}
|
||||
|
||||
pub fn decompress_zlib(data: &[u8]) -> Result<Vec<u8>, String> {
|
||||
let mut decoder = ZlibDecoder::new(data);
|
||||
let mut buf = Vec::new();
|
||||
decoder.read_to_end(&mut buf).map_err(|e| format!("zlib decompress failed: {}", e))?;
|
||||
Ok(buf)
|
||||
}
|
||||
|
||||
pub fn decompress_rle_zlib(data: &[u8], decompressed_size: usize) -> Result<Vec<u8>, String> {
|
||||
let rle_data = decompress_zlib(data)?;
|
||||
Ok(rle_decode(&rle_data, decompressed_size))
|
||||
}
|
||||
|
||||
pub fn rle_encode(data: &[u8]) -> Vec<u8> {
|
||||
let mut out = Vec::new();
|
||||
let mut i = 0;
|
||||
while i < data.len() {
|
||||
let current = data[i];
|
||||
i += 1;
|
||||
let mut count = 1usize;
|
||||
while i < data.len() && data[i] == current && count < 256 {
|
||||
i += 1;
|
||||
count += 1;
|
||||
}
|
||||
|
||||
if count <= 3 {
|
||||
if current == 0xFF {
|
||||
out.push(0xFF);
|
||||
out.push((count - 1) as u8);
|
||||
} else {
|
||||
for _ in 0..count {
|
||||
out.push(current);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
out.push(0xFF);
|
||||
out.push((count - 1) as u8);
|
||||
out.push(current);
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
pub fn rle_decode(data: &[u8], expected_size: usize) -> Vec<u8> {
|
||||
let mut output = vec![0u8; expected_size];
|
||||
let mut in_pos = 0;
|
||||
let mut out_pos = 0;
|
||||
while in_pos < data.len() && out_pos < expected_size {
|
||||
let current = data[in_pos];
|
||||
in_pos += 1;
|
||||
if current == 0xFF {
|
||||
if in_pos >= data.len() {
|
||||
break;
|
||||
}
|
||||
let count = data[in_pos] as usize;
|
||||
in_pos += 1;
|
||||
if count < 3 {
|
||||
let run = count + 1;
|
||||
for _ in 0..run {
|
||||
if out_pos < expected_size {
|
||||
output[out_pos] = 0xFF;
|
||||
out_pos += 1;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let run = count + 1;
|
||||
if in_pos >= data.len() {
|
||||
break;
|
||||
}
|
||||
let value = data[in_pos];
|
||||
in_pos += 1;
|
||||
for _ in 0..run {
|
||||
if out_pos < expected_size {
|
||||
output[out_pos] = value;
|
||||
out_pos += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
output[out_pos] = current;
|
||||
out_pos += 1;
|
||||
}
|
||||
}
|
||||
|
||||
output
|
||||
}
|
||||
@@ -4,6 +4,7 @@ pub mod dlc;
|
||||
pub mod download;
|
||||
pub mod file_dialogs;
|
||||
pub mod game;
|
||||
pub mod java2lce;
|
||||
pub mod macos_setup;
|
||||
pub mod plugins;
|
||||
pub mod proxy_cmd;
|
||||
|
||||
@@ -1,9 +1,176 @@
|
||||
use std::fs;
|
||||
use std::io::Write;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
use tauri::AppHandle;
|
||||
use tauri::{AppHandle, Emitter};
|
||||
use crate::types::{WorkshopInstallRequest, InstalledWorkshopPackage, InstalledPackageEntry};
|
||||
use crate::config;
|
||||
use crate::util;
|
||||
use super::download::DownloadProgress;
|
||||
fn group_archives(zips: &std::collections::HashMap<String, String>) -> Vec<(Vec<String>, String)> {
|
||||
let mut groups: Vec<(Vec<String>, String)> = Vec::new();
|
||||
let mut consumed: std::collections::HashSet<String> = std::collections::HashSet::new();
|
||||
let mut entries: Vec<(&String, &String)> = zips.iter().collect();
|
||||
entries.sort_by(|a, b| a.0.cmp(b.0));
|
||||
for &(name, dest) in &entries {
|
||||
if consumed.contains(name) {
|
||||
continue;
|
||||
}
|
||||
if !name.to_lowercase().ends_with(".zip") {
|
||||
continue;
|
||||
}
|
||||
let base = &name[..name.len() - 4];
|
||||
let mut parts = vec![name.clone()];
|
||||
consumed.insert(name.clone());
|
||||
let mut n = 1;
|
||||
loop {
|
||||
let candidate = format!("{}.z{:02}", base, n);
|
||||
if zips.contains_key(&candidate) {
|
||||
parts.push(candidate.clone());
|
||||
consumed.insert(candidate);
|
||||
n += 1;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if parts.len() == 1 {
|
||||
let mut n = 1;
|
||||
loop {
|
||||
let candidate = format!("{}.zip.{:03}", base, n);
|
||||
if zips.contains_key(&candidate) {
|
||||
parts.push(candidate.clone());
|
||||
consumed.insert(candidate);
|
||||
n += 1;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
groups.push((parts, dest.clone()));
|
||||
}
|
||||
|
||||
for &(name, dest) in &entries {
|
||||
if !consumed.contains(name) {
|
||||
consumed.insert(name.clone());
|
||||
groups.push((vec![name.clone()], dest.clone()));
|
||||
}
|
||||
}
|
||||
groups
|
||||
}
|
||||
|
||||
async fn download_and_assemble(
|
||||
app: &AppHandle,
|
||||
tmp_dir: &Path,
|
||||
parts: &[String],
|
||||
raw_base: &str,
|
||||
package_id: &str,
|
||||
done: &mut usize,
|
||||
total: usize,
|
||||
) -> Result<PathBuf, String> {
|
||||
let mut downloaded: Vec<PathBuf> = Vec::new();
|
||||
for part in parts {
|
||||
let url = format!("{}/{}", raw_base, part);
|
||||
let response = reqwest::get(&url).await.map_err(|e| e.to_string())?;
|
||||
if !response.status().is_success() {
|
||||
return Err(format!("Failed to download {}: HTTP {}", part, response.status()));
|
||||
}
|
||||
let bytes = response.bytes().await.map_err(|e| e.to_string())?;
|
||||
let part_path = tmp_dir.join(part);
|
||||
fs::write(&part_path, &bytes).map_err(|e| e.to_string())?;
|
||||
*done += 1;
|
||||
let _ = app.emit("workshop-progress", DownloadProgress {
|
||||
instance_id: package_id.to_string(),
|
||||
percent: (*done as f64 / total.max(1) as f64) * 100.0,
|
||||
});
|
||||
downloaded.push(part_path);
|
||||
}
|
||||
|
||||
if downloaded.len() == 1 {
|
||||
return Ok(downloaded.remove(0));
|
||||
}
|
||||
|
||||
let combined_dir = tmp_dir.join("_combined");
|
||||
fs::create_dir_all(&combined_dir).map_err(|e| e.to_string())?;
|
||||
let combined = combined_dir.join(parts[0].clone());
|
||||
let mut out = std::io::BufWriter::new(fs::File::create(&combined).map_err(|e| e.to_string())?);
|
||||
for path in &downloaded {
|
||||
let mut f = std::io::BufReader::new(fs::File::open(path).map_err(|e| e.to_string())?);
|
||||
std::io::copy(&mut f, &mut out).map_err(|e| e.to_string())?;
|
||||
}
|
||||
out.flush().map_err(|e| e.to_string())?;
|
||||
Ok(combined)
|
||||
}
|
||||
|
||||
fn extract_archive(zip_tmp: &Path, dest_dir: &Path) -> Result<(Vec<String>, bool), String> {
|
||||
let zip_str = zip_tmp.to_str().unwrap_or("");
|
||||
if cfg!(target_os = "linux") {
|
||||
let mut extract_ok = false;
|
||||
let mut files: Vec<String> = Vec::new();
|
||||
if let Ok(out) = std::process::Command::new("bsdtar")
|
||||
.args(["-tf", zip_str])
|
||||
.output()
|
||||
{
|
||||
if out.status.success() {
|
||||
let listing = String::from_utf8_lossy(&out.stdout);
|
||||
files = listing.lines()
|
||||
.map(|l| l.trim())
|
||||
.filter(|l| !l.is_empty() && !l.ends_with('/'))
|
||||
.map(|l| dest_dir.join(l).to_string_lossy().to_string())
|
||||
.collect();
|
||||
let st = std::process::Command::new("bsdtar")
|
||||
.args(["-xf", zip_str, "-C", dest_dir.to_str().unwrap()])
|
||||
.status()
|
||||
.map_err(|e| e.to_string())?;
|
||||
extract_ok = st.success();
|
||||
}
|
||||
}
|
||||
if !extract_ok {
|
||||
let unzip_list = std::process::Command::new("unzip")
|
||||
.args(["-l", zip_str])
|
||||
.output()
|
||||
.map_err(|e| e.to_string())?;
|
||||
if !unzip_list.status.success() {
|
||||
return Err(format!("Failed to list contents of {}", zip_str));
|
||||
}
|
||||
let listing = String::from_utf8_lossy(&unzip_list.stdout);
|
||||
files = listing.lines()
|
||||
.filter_map(|l| {
|
||||
let mut parts = l.trim().split_whitespace();
|
||||
let size_str = parts.next()?;
|
||||
size_str.parse::<u64>().ok()?;
|
||||
parts.next()?;
|
||||
parts.next()?;
|
||||
Some(parts.collect::<Vec<&str>>().join(" "))
|
||||
})
|
||||
.filter(|l| !l.ends_with('/') && !l.contains('*'))
|
||||
.map(|l| dest_dir.join(l).to_string_lossy().to_string())
|
||||
.collect();
|
||||
let st = std::process::Command::new("unzip")
|
||||
.args(["-o", zip_str, "-d", dest_dir.to_str().unwrap()])
|
||||
.status()
|
||||
.map_err(|e| e.to_string())?;
|
||||
extract_ok = st.success();
|
||||
}
|
||||
Ok((files, extract_ok))
|
||||
} else {
|
||||
let st = std::process::Command::new("tar")
|
||||
.args(["-xf", zip_str, "-C", dest_dir.to_str().unwrap()])
|
||||
.output()
|
||||
.map_err(|e| e.to_string())?;
|
||||
let listing = std::process::Command::new("tar")
|
||||
.args(["-tf", zip_str])
|
||||
.output()
|
||||
.map_err(|e| e.to_string())?;
|
||||
let listing_str = String::from_utf8_lossy(&listing.stdout);
|
||||
let files: Vec<String> = listing_str.lines()
|
||||
.map(|l| l.trim())
|
||||
.filter(|l| !l.is_empty() && !l.ends_with('/'))
|
||||
.map(|l| dest_dir.join(l).to_string_lossy().to_string())
|
||||
.collect();
|
||||
Ok((files, st.status.success()))
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn workshop_install(app: AppHandle, request: WorkshopInstallRequest) -> Result<(), String> {
|
||||
let instance_dir = util::get_instance_working_dir(&app, &request.instance_id);
|
||||
@@ -30,111 +197,28 @@ pub async fn workshop_install(app: AppHandle, request: WorkshopInstallRequest) -
|
||||
workshop_packages.retain(|p| p.id != request.package_id);
|
||||
let raw_base = format!("https://raw.githubusercontent.com/LCE-Hub/LCE-Workshop/refs/heads/main/{}", request.package_id);
|
||||
let tmp_dir = root.join(format!("workshop_tmp_{}", request.package_id));
|
||||
let _ = fs::remove_dir_all(&tmp_dir);
|
||||
fs::create_dir_all(&tmp_dir).map_err(|e| e.to_string())?;
|
||||
let mut pkg_dirs: Vec<String> = Vec::new();
|
||||
for (zip_name, placeholder) in &request.zips {
|
||||
let zip_url = format!("{}/{}", raw_base, zip_name);
|
||||
let zip_tmp = tmp_dir.join(zip_name);
|
||||
let response = reqwest::get(&zip_url).await.map_err(|e| e.to_string())?;
|
||||
if !response.status().is_success() {
|
||||
let _ = fs::remove_dir_all(&tmp_dir);
|
||||
return Err(format!("Failed to download {}: HTTP {}", zip_name, response.status()));
|
||||
}
|
||||
let bytes = response.bytes().await.map_err(|e| e.to_string())?;
|
||||
fs::write(&zip_tmp, &bytes).map_err(|e| e.to_string())?;
|
||||
let total_parts = request.zips.len();
|
||||
let mut done_parts = 0usize;
|
||||
for (parts, placeholder) in group_archives(&request.zips) {
|
||||
let dest_dir = if placeholder.is_empty() {
|
||||
instance_dir.clone()
|
||||
} else {
|
||||
let resolved = instance_dir.clone().join(placeholder
|
||||
instance_dir.clone().join(placeholder
|
||||
.replace("{MediaDir}", media_dir.to_str().unwrap_or(""))
|
||||
.replace("{DLCDir}", dlc_dir.to_str().unwrap_or(""))
|
||||
.replace("{GameHDD}", game_hdd.to_str().unwrap_or(""))
|
||||
.replace("{MobDir}", mob_dir.to_str().unwrap_or("")));
|
||||
PathBuf::from(resolved)
|
||||
.replace("{MobDir}", mob_dir.to_str().unwrap_or("")))
|
||||
};
|
||||
|
||||
fs::create_dir_all(&dest_dir).map_err(|e| e.to_string())?;
|
||||
let (extracted_files, extract_ok) = if cfg!(target_os = "linux") {
|
||||
let bsdtar_list = std::process::Command::new("bsdtar")
|
||||
.args(["-tf", zip_tmp.to_str().unwrap()])
|
||||
.output();
|
||||
if let Ok(out) = bsdtar_list {
|
||||
if out.status.success() {
|
||||
let listing = String::from_utf8_lossy(&out.stdout);
|
||||
let files: Vec<String> = listing.lines()
|
||||
.map(|l| l.trim())
|
||||
.filter(|l| !l.is_empty() && !l.ends_with('/'))
|
||||
.map(|l| dest_dir.join(l).to_string_lossy().to_string())
|
||||
.collect();
|
||||
let st = std::process::Command::new("bsdtar")
|
||||
.args(["-xf", zip_tmp.to_str().unwrap(), "-C", dest_dir.to_str().unwrap()])
|
||||
.status()
|
||||
.map_err(|e| e.to_string())?;
|
||||
(files, st.success())
|
||||
} else {
|
||||
(Vec::new(), false)
|
||||
}
|
||||
} else {
|
||||
(Vec::new(), false)
|
||||
}
|
||||
} else {
|
||||
(Vec::new(), false)
|
||||
};
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
let (extracted_files, extract_ok) = if !extract_ok {
|
||||
let unzip_list = std::process::Command::new("unzip")
|
||||
.args(["-l", zip_tmp.to_str().unwrap()])
|
||||
.output()
|
||||
.map_err(|e| e.to_string())?;
|
||||
if !unzip_list.status.success() {
|
||||
let _ = fs::remove_dir_all(&tmp_dir);
|
||||
return Err(format!("Failed to list contents of {}", zip_name));
|
||||
}
|
||||
let listing = String::from_utf8_lossy(&unzip_list.stdout);
|
||||
let files: Vec<String> = listing.lines()
|
||||
.filter_map(|l| {
|
||||
let mut parts = l.trim().split_whitespace();
|
||||
let size_str = parts.next()?;
|
||||
size_str.parse::<u64>().ok()?;
|
||||
parts.next()?;
|
||||
parts.next()?;
|
||||
Some(parts.collect::<Vec<&str>>().join(" "))
|
||||
})
|
||||
.filter(|l| !l.ends_with('/') && !l.contains('*'))
|
||||
.map(|l| dest_dir.join(l).to_string_lossy().to_string())
|
||||
.collect();
|
||||
let st = std::process::Command::new("unzip")
|
||||
.args(["-o", zip_tmp.to_str().unwrap(), "-d", dest_dir.to_str().unwrap()])
|
||||
.status()
|
||||
.map_err(|e| e.to_string())?;
|
||||
(files, st.success())
|
||||
} else {
|
||||
(extracted_files, extract_ok)
|
||||
};
|
||||
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
let (extracted_files, extract_ok) = {
|
||||
let st = std::process::Command::new("tar")
|
||||
.args(["-xf", zip_tmp.to_str().unwrap(), "-C", dest_dir.to_str().unwrap()])
|
||||
.output()
|
||||
.map_err(|e| e.to_string())?;
|
||||
let listing = std::process::Command::new("tar")
|
||||
.args(["-tf", zip_tmp.to_str().unwrap()])
|
||||
.output()
|
||||
.map_err(|e| e.to_string())?;
|
||||
let listing_str = String::from_utf8_lossy(&listing.stdout);
|
||||
let files: Vec<String> = listing_str.lines()
|
||||
.map(|l| l.trim())
|
||||
.filter(|l| !l.is_empty() && !l.ends_with('/'))
|
||||
.map(|l| dest_dir.join(l).to_string_lossy().to_string())
|
||||
.collect();
|
||||
(files, st.status.success())
|
||||
};
|
||||
|
||||
let archive_tmp = download_and_assemble(&app, &tmp_dir, &parts, &raw_base, &request.package_id, &mut done_parts, total_parts).await?;
|
||||
let (extracted_files, extract_ok) = extract_archive(&archive_tmp, &dest_dir)?;
|
||||
if !extract_ok {
|
||||
let _ = fs::remove_dir_all(&tmp_dir);
|
||||
return Err(format!("Extraction failed for {}", zip_name));
|
||||
return Err(format!("Extraction failed for {}", parts[0]));
|
||||
}
|
||||
|
||||
for f in &extracted_files {
|
||||
|
||||
@@ -9,6 +9,7 @@ mod workshop_server;
|
||||
mod commands;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::AtomicBool;
|
||||
use tokio::sync::Mutex;
|
||||
use tauri::{Emitter, Manager};
|
||||
use commands::config as config_cmds;
|
||||
@@ -54,6 +55,7 @@ pub fn run() {
|
||||
.manage(GameState {
|
||||
child: Arc::new(Mutex::new(None)),
|
||||
workshop_cancel: Arc::new(Mutex::new(None)),
|
||||
manual_stop: Arc::new(AtomicBool::new(false)),
|
||||
})
|
||||
.manage(ProxyGuard {
|
||||
cancel_tokens: Arc::new(Mutex::new(HashMap::new())),
|
||||
@@ -108,6 +110,9 @@ pub fn run() {
|
||||
game::backup_instance,
|
||||
game::restore_instance,
|
||||
commands::console2lce::import_world,
|
||||
commands::console2lce::import_lce_save,
|
||||
commands::java2lce::java_to_lce,
|
||||
commands::java2lce::lce_to_java,
|
||||
stun::stun_discover,
|
||||
relay::start_relay_proxy,
|
||||
relay::start_host_relay,
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
use tauri::State;
|
||||
use tauri::webview::cookie::time::Duration;
|
||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||
use tokio::net::{TcpListener, TcpStream};
|
||||
use tokio::time::sleep;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
use crate::state::ProxyGuard;
|
||||
const PROXY_ADDR: &str = "proxy.mclegacyedition.xyz:2052"; //neo: yeah bro im hardcoding it
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::AtomicBool;
|
||||
use tokio::sync::Mutex;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
pub struct DownloadState {
|
||||
@@ -9,6 +10,7 @@ pub struct DownloadState {
|
||||
pub struct GameState {
|
||||
pub child: Arc<Mutex<Option<tokio::process::Child>>>,
|
||||
pub workshop_cancel: Arc<Mutex<Option<CancellationToken>>>,
|
||||
pub manual_stop: Arc<AtomicBool>,
|
||||
}
|
||||
|
||||
pub struct ProxyGuard {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://schema.tauri.app/config/2",
|
||||
"productName": "LCE Emerald Launcher",
|
||||
"version": "1.5.1",
|
||||
"version": "1.6.0",
|
||||
"identifier": "com.emerald.legacy",
|
||||
"build": {
|
||||
"beforeDevCommand": "npm run dev",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://schema.tauri.app/config/2",
|
||||
"productName": "Emerald Legacy Launcher Nightly",
|
||||
"version": "1.5.1-nightly",
|
||||
"version": "1.6.0-nightly",
|
||||
"identifier": "com.emerald.legacy.nightly",
|
||||
"build": {
|
||||
"beforeDevCommand": "npm run dev",
|
||||
|
||||
Reference in New Issue
Block a user