silverfish 0.3.0

Easily edit Minecraft worlds with a simple, fast and powerful API
Documentation
# silverfish


![Silverfish Logo](https://bimply.lifelike.dev/d/qjlzVoyc9H)

Easily edit Minecraft worlds with a simple, fast and powerful API.  
Works with worlds from version **1.18+** (including modded worlds).  

With `silverfish` you can set blocks, retrive blocks.  
Even set and get biomes, all in an easy to use crate.  

You can either continue reading the documentation.  
Or look at some examples in `examples/*`.  

## Usage


```sh
cargo add silverfish
```

### Set block


When calling [`Region::set_block`], it won't actually write the changes to the chunks.  
But instead write it to an internal buffer that also prevents duplicate blocks.  
*If a block is already present on some coordinates in the buffer, set_block returns a `None`*  
To actually flush the block changes to the chunks, call [`Region::write_blocks`]


```rust
use silverfish::Region;

let mut region = Region::full_empty((0, 0));
// look at `silverfish::Block` for info on blocks  
// and at `silverfish::Coords` for all coordinate arguments.  
region.set_block((42, 65, 84), "stone")?;
region.write_blocks()?;
let mut buf = vec![];
region.write(&mut buf)?;

Ok::<(), silverfish::Error>(())
```

### Get block


You can retrieve blocks in batches or single call.  
Use [`Region::get_blocks`] with a list of coordinates to batch them together.  


```rust ,no_run
use silverfish::Region;

let mut region_buf = std::io::Cursor::new(vec![]);
let region = Region::from_region(&mut region_buf, (0, 0))?;
let block = region.get_block((42, 65, 84))?;

Ok::<(), silverfish::Error>(())
```

### Biomes


You can also set and or get biomes within your worlds.  
Each biome in your world, at the lowest level is divided into 4x4x4 cells.  
So to set a cell to a specific biome you have to specify:  
- The chunk coordinates  
- Section Y level  
- Cell within the section  

Or you can just call it with tuple: `(52, 12, 62)` with region local coordinates.  
As it implements `Into<`[`BiomeCell`]`>` and will convert it for you.  

```rust
// Set a biome cell
use silverfish::{Region, BiomeCell};

let mut region = Region::full_empty((0, 0));
region.set_biome(BiomeCell::new((4, 1), -1, (1, 1, 3)), "minecraft:plains")?;
region.write_biomes()?;

Ok::<(), silverfish::Error>(())
```

```rust
// Get a biome cell
use silverfish::Region;

let mut region = Region::full_empty((0, 0));
let biome = region.get_biome((61, 12, 284))?;

Ok::<(), silverfish::Error>(())
```

### Block properties


Blocks can have any property attached to them.  
A [`Block`] can be created via [`Block::new`] or [`Block::try_new_with_props`]/[`Block::new_with_props`].  

```rust
use silverfish::Block;

let block = Block::try_new_with_props(
    "minecraft:sea_pickle", 
    &[("waterlogged", "true"), ("pickles", "3")]
)?;

Ok::<(), silverfish::Error>(())
```

Look futher down under `performance` for more information on block names and their namespaces.  

### Region


A [`Region`] is the main object you will work with to apply changes and read data.  
And can be constructed via 4 different methods.  
Note that the last argument for any [`Region`] constructor is the region coordinates.  

```rust ,ignore
use silverfish::Region;

// A new empty region with no chunk data
let region = Region::empty(...);

// A new full region with empty pre-filled chunks
let region = Region::full_empty(...);

// Creates a region from a HashMap<(u8, u8), NbtCompound>
// Where each key is the chunk coordinate and the value is the entire chunk nbt compound
let region = Region::from_nbt(...);

// Creates a region based off a writer from a `.mca` region file format.  
let region = Region::from_region(...)?;
```

### Config


A config can be specified in the [`Region`] to dictate how it should write blocks.  
The most notable one is [`Config::update_lighting`] which structures the chunks in so that  
Minecraft will automatically update the lighting in the chunks on first reload.  
*(which is set to true by default)*

```rust
use silverfish::{Config, Region};

let mut region = Region::full_empty((0, 0));

let mut config = Config::default();
config.update_lighting = false;
region.set_config(config)?;

Ok::<(), silverfish::Error>(())
```

----

> [!NOTE]  
> Do note that all of these coordinates used above is local to the **region** *(x=0..512, z=0..512)*.  
> To transform normal *global* world coordinates to local region coordinates.  
> You can pass them through [`silverfish::to_region_local`].  

## Performance


There is already lot of optimizations put into this crate to make it quite fast, if you ask me.  
But there is also some pitfalls and optimizations you,  
as the user can do to modify your world even faster than before.  

### Namespaced Blocks


When you construct a [`Block`], you may notice that the id argument you give is `B: Into<`[`Name`]`>`.  
[`Name`] is an enum which can either be [`Name::Namespaced`] or [`Name::Id`], this tells the block if it begins with  
a namespace or not. A namespace is the `minecraft:` part of an id (`minecraft:furnace`, for example).  
And if you know that your block has a namespace in it you can safetely construct a `Name` with a namespace.  

```rust
use silverfish::{Block, Name};

let name = Name::new_namespace("minecraft:bell");
let block = Block::new(name);

Ok::<(), silverfish::Error>(())
```

Namespaces are not strictly *required* but heavily recommended incase  
blocks would collide in let's say modded enviroments.  
If you create a new [`Block`] with just a `&str` it will default to an [`Name::Id`] .  
And automatically convert it to a namespaced variant if it doesn't contain a namespace on NBT write.  

### Pre-allocating internal buffers


If you already know which chunks and sections within your region  
that you will modify, it helps to preallocate those internal buffers.  
Since calling [`Region::set_block`] doesn't actually write the changes.  
We store the blocks in internal buffers until [`Region::write_blocks`] is called.  

```rust
use silverfish::Region;

let mut region = Region::full_empty((0, 0));
// preallocates the first 16 chunks within the region
region.allocate_block_buffer(0..4, 0..4, -4..20, 4096);
region.set_block((6, 1, 7), "birch_planks");
// ...
```

Note that calling [`Region::allocate_block_buffer`] or [`Region::allocate_biome_buffer`]  
resets all internal buffers related to blocks.  
So if you've called [`Region::set_block`] before preallocating, all of that is gone.  

If you need sparsed preallocation that isn't within a specific chunk range.  
Look at [`region.set_block_buffer`] & [`region.set_biome_buffer`] to manage it yourself.  

### Batching


Almost all operations you do can be made in batches internally.  

Let's say you're writing a ton of blocks.

#### set_block: Single calls, slow

```rust
use silverfish::{Region, Error};

let mut region = Region::full_empty((0, 0));
region.set_block((5, 1, 28), "air");
region.write_blocks()?;
region.set_block((71, -5, 14), "air");
region.write_blocks()?;
region.set_block((451, 51, 162), "air");
region.write_blocks()?;

Ok::<(), silverfish::Error>(())
```
#### set_block: Batched, fast

```rust
use silverfish::Region;

let mut region = Region::full_empty((0, 0));
region.set_block((5, 1, 28), "air");
region.set_block((71, -5, 14), "air");
region.set_block((451, 51, 162), "air");
region.write_blocks()?;

Ok::<(), silverfish::Error>(())
```

This can also be applied to getting a ton of blocks at the same time.  

#### get_block: Single calls, slow

```rust
use silverfish::Region;

let region = Region::full_empty((0, 0));
let block_1 = region.get_block((6, 1, 4))?;
let block_2 = region.get_block((1, -61, 52))?;
let block_3 = region.get_block((78, 13, 152))?;
let block_4 = region.get_block((4, 62, 84))?;

Ok::<(), silverfish::Error>(())
```
#### get_blocks: Batched, fast

```rust
use silverfish::Region;

let region = Region::full_empty((0, 0));
let blocks = region.get_blocks(&vec![
    (6, 1, 4), (1, -61, 52),
    (78, 13, 152), (4, 62, 84)
])?;

Ok::<(), silverfish::Error>(())
```

### Parallel set block


Due to how the internal strructure it set up, it's quite easy to  
set blocks within the same region in parallel.  
*(chunks are limited to one "thread" at a time)*

Below is an example using `rayon` to iterate over 32 chunks,  
and placing a furnace at `0, 0, 0` in each chunk.  

[`Region::write_blocks`] is already parallel internally,  
So no need to try and call it within the `par_iter`.  
If wish to skip the internal parallel of [`Region::write_blocks`]:  
You can call [`ChunkData::write_blocks`] on each chunk and manage it yourself.  

```rust
use rayon::iter::{IntoParallelRefIterator, ParallelIterator};
use silverfish::Region;

let mut region = Region::full_empty((0, 0));

(0..32).collect::<Vec<u8>>().par_iter().try_for_each(|z| {
    let mut chunk = region.get_chunk_mut(0, *z)?;
    chunk.set_block((0, 0, 0), "furnace").unwrap();

    Ok::<(), silverfish::Error>(())
})?;

region.write_blocks()?;

Ok::<(), silverfish::Error>(())
```

### Set Sections


If you know that you will fill, let's say an entire region with a single block.  
It is over 10x times faster to do it via [`Region::set_sections`].  
This function and it's brother ([`Region::set_section`]), allows you to set an entire  
section *(4096 blocks, 16\*16\*16)* to a single block at once.  

```rust
use silverfish::Region;

let mut region = Region::full_empty((0, 0));
let mut sections = Vec::with_capacity(24_576);
for x in 0..32 {
    for y in -4..20 {
        for z in 0..32 {
            sections.push(((x, z), y, "minecraft:air"));
        }
    }
}
region.set_sections(sections)?;

Ok::<(), silverfish::Error>(())
```

Look at the [Minecraft Wiki](https://minecraft.wiki/w/Chunk_format) for more information on how sections are structured.  

----

All of this batching also applies to biomes and their get / set.  

### Numbers


While these are pointless in real world examples.  
They are *fun*. 

On my machine (Ryzen 7 5800X) and in release mode.  
Have gotten a throughput of **162,360,154** blocks per second when writing to the chunks NBT.  
*(1,290,555,076 b/s if doing one block per section via [`Region::set_sections`])*  

The scenario was writing *100,663,296* blocks (an entire region) that only contained the same block.  
So this got the maximum amount of palette caches hit and least clean up internally.  
This was also with the entire region preallocated within the internal buffers.  
And didn't use [`Region::set_section`] or [`Region::set_sections`] which is faster in real world use.  
Those 100 million or so blocks took *650ms~* or so to flush from the buffers to NBT. 

Even if real world examples are slower, it is quite fast enough for 99% of people.