From 38e5277b6904200fb994038253981536979e2049 Mon Sep 17 00:00:00 2001 From: RTTV Date: Thu, 11 Apr 2024 14:24:53 -0400 Subject: [PATCH] - fixed issue with tab asking for Save As when file is valid but doesn't exist - fixed issue where cli wasn't printing output - changed hex extension to `bin` - allowed edits to list of byte/short/int/long in hex - added readme --- Cargo.toml | 12 ++++-- README.md | 59 ++++++++++++++++++++++++++++++ icons/features.png | Bin 0 -> 359 bytes icons/nbtworkbench.png | Bin 0 -> 1146 bytes src/element_action.rs | 38 ++++++++++++++++++- src/elements/element.rs | 24 ++++++++---- src/main.rs | 41 +++++++++++++-------- src/tab.rs | 2 +- src/workbench.rs | 79 ++++++++++++++++++++++------------------ 9 files changed, 191 insertions(+), 64 deletions(-) create mode 100644 README.md create mode 100644 icons/features.png create mode 100644 icons/nbtworkbench.png diff --git a/Cargo.toml b/Cargo.toml index 5375c76..ee244b2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,11 +8,12 @@ repository = "https://github.com/RealRTTV/nbtworkbench" keywords = ["nbt", "window", "unsafe", "editor", "tree"] categories = ["graphics", "rendering", "text-editors", "parser-implementations"] -# Required for Wasm target, but breaks `winres` -[lib] -crate-type = ["cdylib", "rlib"] -path = "src/main.rs" +# Wasm Only +#[lib] +#crate-type = ["cdylib", "rlib"] +#path = "src/main.rs" +# Windows Only [package.metadata.winres] # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html @@ -66,6 +67,9 @@ lz4_flex = { version = "0.11.2", default-features = false, features = ["std", "n regex = "1.10.3" glob = "0.3.1" +[target.'cfg(target_os = "windows")'.dependencies] +winapi = { version = "0.3.9", features = [] } + [target.'cfg(not(target_arch = "wasm32"))'.dependencies] cli-clipboard = "0.4.0" pollster = "0.3.0" diff --git a/README.md b/README.md new file mode 100644 index 0000000..efb6ca3 --- /dev/null +++ b/README.md @@ -0,0 +1,59 @@ +# NBT Workbench + +### [Downloads & Releases here!](https://github.com/RealRTTV/nbtworkbench/releases) + +NBT Workbench is an [NBT](https://wiki.vg/NBT) editing application, +the successor to [NBT Studio](https://github.com/tryashtar/nbt-studio), +which is in turn the successor to [NBTExplorer](https://github.com/jaquadro/NBTExplorer). +NBT Workbench is written completely from scratch in [Rust](https://www.rust-lang.org/) and designed to be as performant and efficient as possible. + +## Features +(Features marked with a star are new and not available in NBT Studio or Explorer): + +* Java NBT files (`level.dat` / `hotbar.nbt`) +* Java region files (`.mca`) + * ☆ Now supporting the new 1.21 compression format +* SNBT files (`.snbt`) +* ☆ [Web Version](https://rttv.ca/main) +* Save as dialog +* Create new nbt file / new region file +* ☆ Action wheel + * By holding right-click over an NBT tag: A circular action wheel will appear, which will let you make specific changes to NBT tags, this includes: + * Copying the condensed/raw or formatted/pretty SNBT version of a tag. + * ☆ Opening an array in a preferred hex editor. + * ☆ Opening nbt as SNBT in a preferred text editor. + * ☆ Sorting Compounds alphabetically or by type. +* ☆ Hotkeys to quickly create new elements by their type +* ☆ Editing tag key/values in one click by simply being overtop the text. +* Tags can be selected, dragged and dropped to move them around. +* Undo and redo with Ctrl + Z and Ctrl + Y / Ctrl + Shift + Z respectively. +* ☆ Ctrl + D to duplicate the hovered tag below itself +* ☆ Searching with substrings, regex and snbt matching. +* ☆ Bookmarks +* ☆ Line Numbers +* ☆ Dark Mode +* ☆ Freehand mode to easily dive into NBT files without needing to click precisely +* Alt + Left / Right Arrow to retract and expand respectively + * ☆ Alt + Shift + Right Arrow to expand fully +* ☆ Remastered NBT Explorer Art +* ☆ CLI Mode `nbtworkbench -?` + * ☆ `nbtworkbench find` to search across multiple files + * ☆ `nbtworkbench reformat` to reformat the extensions of multiple files +* Reload button +* ☆ Tabs +* ☆ The fastest NBT read / write around + +# Credits +NBT Workbench was made by myself, Katie; +however, it would not come to be without the lovely projects below inspiring it. + +### Design +* [NBT Studio by tryashtar](https://github.com/tryashtar/nbt-studio) +* [NBTExplorer by jaquadro](https://github.com/jaquadro/NBTExplorer) + +### Technologies +* [WGPU](https://github.com/gfx-rs/wgpu) +* [Rust](https://rust-lang.org) + +### Icons +* Remastered/Inspired by [jaquado](https://github.com/jaquadro)'s [NBTExplorer](https://github.com/jaquadro/NBTExplorer) icons. diff --git a/icons/features.png b/icons/features.png new file mode 100644 index 0000000000000000000000000000000000000000..a766693900180956ce8ef6d9aaabb798a3b01073 GIT binary patch literal 359 zcmV-t0hs=YP)Px#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){sAK>D0Q^ZrK~y+T#gjb> z!axv(*Pp^7QBcInLhuwK2nv$c)+=}e58w^Ff~~Dsi`TFg{6QlzlC04gcZQuHA~t^T zvctZ4uiB`;lQE~-%tnbBk@<3MrWb>L%S`%mRWS;4|$J zSg77<__RIPg$x$}Xjtm>dg8PNy95`I@r_`Yz=C_|zg_$!$Z!FGms)=% z*d_2iuO?pN`)9lWN;z#N$w}wTq@ItfsM_NN|4Toj)C1fyKaI2aUT1mw^P4iDouq&bw$+ah%n6@oS6P4O$8)>iAD7*K&?P6qzQCD5 z!G}1M^L6u$XnX(vZ%(Qo>*TzD|4-^1v((q$B`cU43>b+;8;+*WoIdaQ`uWT2i~gUm z@csGo!95G#$X`Fbb^rd$j;@-2@YZ)u(oEdDcK-AD-!{M32A}V}JSBPkervzkpI%qz z_gtRx`}#h)<(l5}|KIv9xTbb~gE<3-FwrRbuU!Ms=(;zLwI7>2JhyS^!Sn6^pR@4Y zSy6Yf@Wb7w`C_*J?w$_6C$n5rJ+H>xuRZJK=lD~#3FQ4b%4eo#6e|d`k`ny2O-!U`%GbP^TuIZiSyYId`)A_jj zop8n5rI&$rOjPFYyF6vFyS&tL&G7a2CDqmDSl8CfS$1dd@8`f+{(CPTsA#eNdZ3;8 z`FA7#$j2O#W@zCg8m+AotNbp1M}5P#?RxX1muvp+x3lqUfAwIy{jrw;duxBbvdBGk z>-o>=pV$}@nTSTS?N0xx-|+P}&$Vq|KYxjS&P#OYvXGMbKx!L&e@ZKKHJMKdkDmeJ Nd%F6$taD0e0sx~Ai~#@u literal 0 HcmV?d00001 diff --git a/src/element_action.rs b/src/element_action.rs index 825894e..36c506e 100644 --- a/src/element_action.rs +++ b/src/element_action.rs @@ -146,7 +146,7 @@ impl ElementAction { let hash = (since_epoch().as_millis() as usize).wrapping_mul(element as *mut NbtElement as usize); let path = std::env::temp_dir().join(format!( - "nbtworkbench-{hash:0width$x}.hex", + "nbtworkbench-{hash:0width$x}.bin", width = usize::BITS as usize / 8 )); let (tx, rx) = std::sync::mpsc::channel(); @@ -196,6 +196,42 @@ impl ElementAction { vec.extend(child.as_long_unchecked().value.to_be_bytes()); } vec + } else if let Some(list) = element.as_list() { + match list.element { + NbtByte::ID => { + subscription_type = FileUpdateSubscriptionType::ByteList; + let mut vec = Vec::with_capacity(list.len()); + for child in list.children() { + vec.push(child.as_byte_unchecked().value as u8); + } + vec + }, + NbtShort::ID => { + subscription_type = FileUpdateSubscriptionType::ShortList; + let mut vec = Vec::with_capacity(list.len() * 2); + for child in list.children() { + vec.extend(child.as_short_unchecked().value.to_be_bytes()); + } + vec + }, + NbtInt::ID => { + subscription_type = FileUpdateSubscriptionType::IntList; + let mut vec = Vec::with_capacity(list.len() * 4); + for child in list.children() { + vec.extend(child.as_int_unchecked().value.to_be_bytes()); + } + vec + }, + NbtLong::ID => { + subscription_type = FileUpdateSubscriptionType::LongList; + let mut vec = Vec::with_capacity(list.len() * 8); + for child in list.children() { + vec.extend(child.as_long_unchecked().value.to_be_bytes()); + } + vec + }, + _ => panic_unchecked("list was let through even thought it didn't have valid type"), + } } else { break 'm; } diff --git a/src/elements/element.rs b/src/elements/element.rs index ac40541..91fb3e9 100644 --- a/src/elements/element.rs +++ b/src/elements/element.rs @@ -1026,7 +1026,7 @@ impl NbtElement { #[inline] #[must_use] #[allow(clippy::match_same_arms)] - pub const fn actions(&self) -> &[ElementAction] { + pub fn actions(&self) -> &[ElementAction] { unsafe { match self.id() { NbtByte::ID => &[ @@ -1079,12 +1079,22 @@ impl NbtElement { #[cfg(not(target_arch = "wasm32"))] ElementAction::OpenInTxt, ], - NbtList::ID => &[ - ElementAction::CopyRaw, - ElementAction::CopyFormatted, - #[cfg(not(target_arch = "wasm32"))] - ElementAction::OpenInTxt, - ], + NbtList::ID => { + const FULL: [ElementAction; 4] = [ + ElementAction::CopyRaw, + ElementAction::CopyFormatted, + #[cfg(not(target_arch = "wasm32"))] + ElementAction::OpenInTxt, + #[cfg(not(target_arch = "wasm32"))] + ElementAction::OpenArrayInHex + ]; + let id = self.as_list_unchecked().element; + if let NbtByte::ID | NbtShort::ID | NbtInt::ID | NbtLong::ID = id { + &FULL + } else { + &FULL[..FULL.len() - 1] + } + }, NbtCompound::ID => &[ ElementAction::CopyRaw, ElementAction::CopyFormatted, diff --git a/src/main.rs b/src/main.rs index 81f7d18..e069fe1 100644 --- a/src/main.rs +++ b/src/main.rs @@ -19,11 +19,11 @@ maybe_uninit_uninit_array, new_uninit, optimize_attribute, + panic_update_hook, stmt_expr_attributes, unchecked_math )] -#![feature(panic_update_hook)] -#![cfg_attr(all(windows, not(debug_assertions)), windows_subsystem = "windows")] +#![windows_subsystem = "windows"] use std::cell::UnsafeCell; use std::cmp::Ordering; @@ -206,6 +206,9 @@ pub fn wasm_main() { #[cfg(not(target_arch = "wasm32"))] pub fn main() -> ! { + #[cfg(target_os = "windows")] unsafe { + winapi::um::wincon::AttachConsole(winapi::um::wincon::ATTACH_PARENT_PROCESS); + } let first_arg = std::env::args().nth(1); if let Some("find") = first_arg.as_deref() { @@ -217,20 +220,21 @@ pub fn main() -> ! { std::process::exit(0); } else if let Some("-?" | "/?" | "--help" | "-h") = first_arg.as_deref() { println!( - r#"Usage: - nbtworkbench --version|-v - nbtworkbench -?|-h|--help|/? - nbtworkbench find [(--mode|-m)=normal|regex|snbt] [(--search|-s)=key|value|all] - nbtworkbench reformat (--format|-f)= [(--out-dir|-d)=] [(--out-ext|-e)=] - - Options: - --version, -v Displays the version of nbtworkbench you're running. - -?, -h, --help, /? Displays this dialog. - --mode, -m Changes the `find` mode to take the field as either, a containing substring, a regex (match whole), or snbt. [default: normal] - --search, -s Searches for results matching the in either, the key, the value, or both (note that substrings and regex search the same pattern in both key and value, while the regex uses it's key field to match equal strings). [default: all] - --format, -f Specifies the format to be reformatted to; either `nbt`, `snbt`, `dat/dat_old/gzip` or `zlib`. - --out-dir, -d Specifies the output directory. [default: ./] - --out-ext, -e Specifies the output file extension (if not specified, it will infer from --format)"# + r#" +Usage: + nbtworkbench --version|-v + nbtworkbench -?|-h|--help|/? + nbtworkbench find [(--mode|-m)=normal|regex|snbt] [(--search|-s)=key|value|all] + nbtworkbench reformat (--format|-f)= [(--out-dir|-d)=] [(--out-ext|-e)=] + +Options: + --version, -v Displays the version of nbtworkbench you're running. + -?, -h, --help, /? Displays this dialog. + --mode, -m Changes the `find` mode to take the field as either, a containing substring, a regex (match whole), or snbt. [default: normal] + --search, -s Searches for results matching the in either, the key, the value, or both (note that substrings and regex search the same pattern in both key and value, while the regex uses it's key field to match equal strings). [default: all] + --format, -f Specifies the format to be reformatted to; either `nbt`, `snbt`, `dat/dat_old/gzip` or `zlib`. + --out-dir, -d Specifies the output directory. [default: ./] + --out-ext, -e Specifies the output file extension (if not specified, it will infer from --format)"# ); std::process::exit(0); } else { @@ -523,11 +527,16 @@ pub struct FileUpdateSubscription { tab_uuid: uuid::Uuid, } +#[derive(Copy, Clone)] pub enum FileUpdateSubscriptionType { Snbt, ByteArray, IntArray, LongArray, + ByteList, + ShortList, + IntList, + LongList, } #[derive(Copy, Clone)] diff --git a/src/tab.rs b/src/tab.rs index bfb3506..6e6eacc 100644 --- a/src/tab.rs +++ b/src/tab.rs @@ -71,7 +71,7 @@ impl Tab { #[cfg(any(target_os = "windows", target_os = "macos", target_os = "linux"))] pub fn save(&mut self, force_dialog: bool) -> Result<()> { let path = self.path.as_deref().unwrap_or(self.name.as_ref().as_ref()); - if !path.exists() || force_dialog { + if path.try_exists().is_err() || force_dialog { let mut builder = native_dialog::FileDialog::new(); if self.value.id() == NbtRegion::ID { builder = builder.add_filter("Region File", &["mca", "mcr"]); diff --git a/src/workbench.rs b/src/workbench.rs index e2e872d..5359250 100644 --- a/src/workbench.rs +++ b/src/workbench.rs @@ -4,7 +4,7 @@ use std::str::FromStr; use std::sync::mpsc::TryRecvError; use std::time::Duration; -use anyhow::{anyhow, Result}; +use anyhow::{anyhow, Context, Result}; use compact_str::{CompactString, format_compact, ToCompactString}; use fxhash::{FxBuildHasher, FxHashSet}; use uuid::Uuid; @@ -17,12 +17,14 @@ use crate::alert::Alert; use crate::assets::{ACTION_WHEEL_Z, BACKDROP_UV, BASE_TEXT_Z, BASE_Z, BOOKMARK_UV, CLOSED_WIDGET_UV, DARK_STRIPE_UV, EDITED_UV, HEADER_SIZE, HELD_ENTRY_Z, HIDDEN_BOOKMARK_UV, HORIZONTAL_SEPARATOR_UV, HOVERED_STRIPE_UV, HOVERED_WIDGET_UV, JUST_OVERLAPPING_BASE_TEXT_Z, LIGHT_STRIPE_UV, LINE_NUMBER_SEPARATOR_UV, NEW_FILE_UV, OPEN_FOLDER_UV, SELECTED_ACTION_WHEEL, SELECTED_WIDGET_UV, SELECTION_UV, TRAY_UV, UNEDITED_UV, UNSELECTED_ACTION_WHEEL, UNSELECTED_WIDGET_UV}; use crate::bookmark::Bookmarks; use crate::color::TextColor; +use crate::decoder::Decoder; use crate::elements::chunk::{NbtChunk, NbtRegion}; use crate::elements::compound::NbtCompound; use crate::elements::element::{NbtByte, NbtByteArray, NbtDouble, NbtFloat, NbtInt, NbtIntArray, NbtLong, NbtLongArray, NbtShort}; use crate::elements::element::NbtElement; use crate::elements::list::{NbtList, ValueIterator}; use crate::elements::string::NbtString; +use crate::encoder::UncheckedBufWriter; use crate::search_box::SearchBox; use crate::selected_text::{SelectedText, SelectedTextAdditional}; use crate::tab::{FileFormat, Tab}; @@ -612,40 +614,47 @@ impl Workbench { match subscription.rx.try_recv() { Ok(data) => match subscription.subscription_type { FileUpdateSubscriptionType::Snbt => write_snbt(subscription, &data, tab)?, - FileUpdateSubscriptionType::ByteArray => write_array(subscription, tab, { - let mut array = NbtByteArray::new(); - for (idx, byte) in data.into_iter().enumerate() { - let _ = array.insert(idx, NbtElement::Byte(NbtByte { value: byte as i8 })); - } - NbtElement::ByteArray(array) - })?, - FileUpdateSubscriptionType::IntArray => write_array(subscription, tab, { - let mut array = NbtIntArray::new(); - let iter = data.array_chunks::<4>(); - if !iter.remainder().is_empty() { return Err(anyhow!("Expected a multiple of 4 bytes for int array")) } - for (idx, &chunk) in iter.enumerate() { - let _ = array.insert( - idx, - NbtElement::Int(NbtInt { - value: i32::from_be_bytes(chunk), - }), - ); - } - NbtElement::IntArray(array) - })?, - FileUpdateSubscriptionType::LongArray => write_array(subscription, tab, { - let mut array = NbtLongArray::new(); - let iter = data.array_chunks::<8>(); - if !iter.remainder().is_empty() { return Err(anyhow!("Expected a multiple of 8 bytes for long array")) } - for (idx, &chunk) in iter.enumerate() { - let _ = array.insert( - idx, - NbtElement::Long(NbtLong { - value: i64::from_be_bytes(chunk), - }), - ); - } - NbtElement::LongArray(array) + kind @ (FileUpdateSubscriptionType::ByteArray | FileUpdateSubscriptionType::IntArray | FileUpdateSubscriptionType::LongArray | FileUpdateSubscriptionType::ByteList | FileUpdateSubscriptionType::ShortList | FileUpdateSubscriptionType::IntList | FileUpdateSubscriptionType::LongList) => write_array(subscription, tab, { + let mut buf = UncheckedBufWriter::new(); + let id = match kind { + FileUpdateSubscriptionType::ByteArray if data.len() % 1 == 0 => { + buf.write(&(data.len() as u32).to_be_bytes()); + NbtByteArray::ID + }, + FileUpdateSubscriptionType::IntArray if data.len() % 4 == 0 => { + buf.write(&(data.len() as u32 / 4).to_be_bytes()); + NbtIntArray::ID + }, + FileUpdateSubscriptionType::LongArray if data.len() % 8 == 0 => { + buf.write(&(data.len() as u32 / 8).to_be_bytes()); + NbtLongArray::ID + }, + FileUpdateSubscriptionType::ByteList if data.len() % 1 == 0 => { + buf.write(&[NbtByte::ID]); + buf.write(&(data.len() as u32).to_be_bytes()); + NbtList::ID + }, + FileUpdateSubscriptionType::ShortList if data.len() % 2 == 0 => { + buf.write(&[NbtShort::ID]); + buf.write(&(data.len() as u32 / 2).to_be_bytes()); + NbtList::ID + }, + FileUpdateSubscriptionType::IntList if data.len() % 4 == 0 => { + buf.write(&[NbtInt::ID]); + buf.write(&(data.len() as u32 / 4).to_be_bytes()); + NbtList::ID + }, + FileUpdateSubscriptionType::LongList if data.len() % 8 == 0 => { + buf.write(&[NbtLong::ID]); + buf.write(&(data.len() as u32 / 8).to_be_bytes()); + NbtList::ID + }, + _ => return Err(anyhow!("Invalid width for designated type of array")), + }; + buf.write(&data); + let buf = buf.finish(); + let mut decoder = Decoder::new(&buf, SortAlgorithm::None); + NbtElement::from_bytes(id, &mut decoder).context("Could not read bytes for array")? })?, }, Err(TryRecvError::Disconnected) => {