Skip to content

Commit

Permalink
- fixed issue with tab asking for Save As when file is valid but does…
Browse files Browse the repository at this point in the history
…n'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
  • Loading branch information
RealRTTV committed Apr 11, 2024
1 parent 2161b6d commit 38e5277
Show file tree
Hide file tree
Showing 9 changed files with 191 additions and 64 deletions.
12 changes: 8 additions & 4 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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"
Expand Down
59 changes: 59 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# <img src="icons/nbtworkbench.png" width=48> 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.

## <img src="icons/features.png" width=16> 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.
Binary file added icons/features.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added icons/nbtworkbench.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
38 changes: 37 additions & 1 deletion src/element_action.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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;
}
Expand Down
24 changes: 17 additions & 7 deletions src/elements/element.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 => &[
Expand Down Expand Up @@ -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,
Expand Down
41 changes: 25 additions & 16 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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() {
Expand All @@ -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 <path> [(--mode|-m)=normal|regex|snbt] [(--search|-s)=key|value|all] <query>
nbtworkbench reformat (--format|-f)=<format> [(--out-dir|-d)=<out-dir>] [(--out-ext|-e)=<out-ext>] <path>
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 <query> field as either, a containing substring, a regex (match whole), or snbt. [default: normal]
--search, -s Searches for results matching the <query> 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 <path> [(--mode|-m)=normal|regex|snbt] [(--search|-s)=key|value|all] <query>
nbtworkbench reformat (--format|-f)=<format> [(--out-dir|-d)=<out-dir>] [(--out-ext|-e)=<out-ext>] <path>
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 <query> field as either, a containing substring, a regex (match whole), or snbt. [default: normal]
--search, -s Searches for results matching the <query> 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 {
Expand Down Expand Up @@ -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)]
Expand Down
2 changes: 1 addition & 1 deletion src/tab.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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"]);
Expand Down
79 changes: 44 additions & 35 deletions src/workbench.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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};
Expand Down Expand Up @@ -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) => {
Expand Down

0 comments on commit 38e5277

Please sign in to comment.