mca-json
v2.0.0
Published
Convert Minecraft MCA (Anvil files) and NBT into JSON and back, or modify directly in memory. Works on region files and player data.
Maintainers
Readme
MCA JSON
Command-line tools to work with Minecraft Java Edition Anvil files (*.mca) and NBT files (*.dat). Region files from any release can be listed, trimmed, searched, and dumped as JSON. See Supported versions.
This is also a library to let you parse and modify MCA files, NBT files, and SNBT strings in your own JavaScript programs. The library is written in TypeScript and has type definitions for all of the classes and methods.
Installation
First you need to install Node.js, then use npm to install the package. Deno, Bun, pnpm, and other users can use this too - adjust the next command as you see fit.
# For the command-line tools
npm install -g mca-json
# To use the library in your project
npm install mca-jsonThe library itself doesn't require any Node-specific tooling, allowing you to bundle and build it into a browser environment. It is published as ES modules only.
Make a backup of your world before modifying it. Also make sure Minecraft isn't running, because the game keeps region files open and will overwrite your changes.
Command-Line Tools
mca-json - Convert MCA to JSON
Converts an MCA file to JSON. Both input and output files are optional and it will read from stdin and write to stdout if not provided.
# See all of the options available
mca-json --help
# Convert a MCA file to JSON
mca-json ~/.minecraft/saves/Test_World/region/r.2.1.mca ~/r.2.1.jsonThe output JSON is expected to be quite large. For example, one mostly untouched region used for testing is about 1 gigabyte of JSON. Because of this, there are options to make the output smaller while still being valid JSON. To not pretty-print the result, using --compact will reduce the size to roughly 1/10 of the expanded JSON but it's now all one long line. To preserve the majority of the spacing but collapse lists of numbers into a single line while still pretty printing the rest, use --condense and that will consume about 3/10 of the expanded size but still be understandable in a text editor.
The best way to keep the output small is to only export the chunks you care about. Use mca-chunks to see what is in the file.
mca-json --condense --chunk-coords=64,-32 --chunk-coords=64,-31 r.2.-1.mcaEvery tag is written as an object with its numeric NBT type and a property holding its value, which keeps the exact tag types. Longs are written as strings because they don't fit in a JSON number.
{
"type": 10,
"compound": {
"DataVersion": { "type": 3, "int": 4189 },
"InhabitedTime": { "type": 4, "long": "1210" },
"Status": { "type": 8, "string": "minecraft:full" }
}
}mca-chunks - List the chunk coordinates within a MCA file
Lists the chunk coordinates (not world coordinates) of each chunk contained with a single MCA file. Each chunk's coordinates are listed to the output, one per line in the format "X,Z".
# Show all chunk coordinates in a file
mca-chunks ~/.minecraft/saves/Test_World/region/r.2.1.mcaThis can also read the MCA file from stdin or write the output to a file. See mca-chunks --help for usage instructions.
mca-find-signs - Lists all signs in MCA files
This utility will list all signs in the given MCA files, along with their coordinates and text. The output is a JSON array of objects, each with the following properties:
{
"chunkX": number,
"chunkZ": number,
"x": number,
"y": number,
"z": number,
"front": string[],
"back": string[] | null
}The text is plain text for every version of Minecraft, without the formatting or the JSON that older versions stored. back is null for versions before 1.20, where signs had one side.
Signs can be filtered to look for blank signs (--blank), signs with any letter or number (--user), or signs that could be naturally generated (--generated). Use --first to stop looking in a chunk after its first match, which is faster when you only need to know which chunks have signs.
# Find the signs that were probably written by players
mca-find-signs --user ~/.minecraft/saves/Test_World/region/*.mcaFor reference, the naturally generating signs will have message like the following.
- Chairs in taiga villages: ["", "", "", ""] and ["", "", "", ""]
- Igloo basements: ["", "<----", "---->", ""] and ["", "", "", ""]
mca-remove-chunks - Erase chunks
Removes chunks from MCA files, which will cause Minecraft to forget about those chunks and generate them again the next time they are loaded. You either list the chunks to remove or the chunks to preserve. The files are modified in place.
# Remove everything except one chunk
mca-remove-chunks --preserve=-90,118 ~/.minecraft/saves/Test_World/region/r.-3.3.mca
# Remove two chunks
mca-remove-chunks --remove=-90,118 --remove=-90,119 ~/.minecraft/saves/Test_World/region/r.-3.3.mca
# See what would happen without changing the file
mca-remove-chunks --dry-run --verbose --remove-list=chunks.txt r.-3.3.mca--remove-list and --preserve-list read the coordinates from a text file with one "X,Z" per line, which is the same format that mca-chunks writes.
After the chunks are removed, the file is compacted so it no longer contains the removed chunks' data. The remaining chunks are moved to fill the gaps, but their bytes are not altered in any way. Use --no-compact to only erase the chunks from the file's index. That keeps the file the same size and leaves the old data in place until Minecraft reuses the space.
Keep the files' original names, such as r.-3.3.mca. The name says where the region is in the world, which lets the tool find chunks using only the file's index. That is fast and also works for chunks that this library is unable to read. Files with other names still work, but every chunk has to be decompressed and parsed to find its coordinates.
A world stores data about a chunk in more than one place. For a complete removal, run this against the matching files in the region, entities, and poi folders.
nbt-get-player-location - Get player location from a *.dat file
This utility will read the player's *.dat file and get the player's current location. The output is a JSON object with the following properties:
{
"chunkX": number,
"chunkZ": number,
"x": number,
"y": number,
"z": number,
"dimension": string
}nbt-json - Convert NBT data to JSON
This is useful for seeing what's in a player's *.dat file. Simply loads and decompresses the NBT, then writes it as a JSON object.
# See all of the options available
nbt-json --help
# Convert an NBT file to JSON
nbt-json ~/.minecraft/saves/Test_World/playerdata/b248e729-09c2-40dd-9168-12d191b4f0b8.datAPI
Are you here to do some coding? This library is also available for use in your own JavaScript programs. Below is the documentation for the API. All methods and classes are fully typed with TypeScript, so you can get autocomplete and type checking in your editor.
class Anvil
import { Anvil } from 'mca-json';
import { readFile, writeFile } from 'node:fs/promises';
const data = await readFile('r.0.0.mca');
const anvil = Anvil.fromBuffer(data);
// Remove a chunk
anvil.deleteChunk([3, 7]);
// Change a chunk
const chunk = anvil.getChunk([4, 7]);
chunk.nbt().deleteChild('Heightmaps');
anvil.setChunk(chunk);
// Write a file without any unused space
await writeFile('r.0.0.mca', Buffer.from(anvil.compact()));static fromBuffer(buffer: ArrayBufferLike | ArrayBufferView): Anvil
Loads data into an Anvil class. This accepts an ArrayBuffer, a typed array such as Uint8Array, or a Node Buffer. Nothing gets parsed right away and is all loaded and parsed on demand.
When given an ArrayBuffer or a view that covers a whole ArrayBuffer, the data is used directly and not copied, so deleteChunk() and setChunk() could modify it. Always use anvil.buffer() or anvil.compact() to get the result.
static regionCoordinates(filename: string): Coords2d | undefined
Gets the region coordinates from a file name. Anvil.regionCoordinates('region/r.-3.3.mca') returns [-3, 3]. Returns undefined when the name isn't in that format.
anvil.buffer(): ArrayBufferLike
Returns the region file's data, ready to be written to a file. Loading a file and reading its chunks never changes the data, so this returns exactly the same bytes that were loaded unless deleteChunk(), setChunk(), or compact() were used.
anvil.compact(): ArrayBufferLike
Removes all of the space in the file that isn't used by a chunk, such as the data left behind by deleteChunk() and setChunk(), and the gaps that Minecraft leaves when chunks grow. The chunks are packed together in the order they were already stored and the file's index is updated to point at their new locations.
Chunks are moved without being decompressed, so the bytes of each chunk in the result are identical to what they were before compacting. The locations are calculated from the space each chunk currently uses, so this is correct after chunks were changed to be larger or smaller. Timestamps are not changed. Compacting a file that has no unused space returns the same bytes.
The compacted data is returned and is also what the instance uses from then on. The buffer that was originally loaded is not modified by this method.
anvil.deleteChunk(chunkLocation: Coords2d | Chunk): void
Removes a chunk, given either its chunk coordinates or a Chunk instance. Only the four bytes in the file's index are erased. The file stays the same size and the chunk's data is still in it until you call compact().
anvil.getAllChunks(): Chunk[]
Parses the NBT data and returns an array of Chunk instances. Chunks compressed with zlib, gzip, and LZ4 are read, as well as uncompressed chunks. This throws when a chunk can't be read, which includes chunks using a custom compression and oversized chunks that are stored in a separate .mcc file.
anvil.getChunkCoordinates(regionCoords?: Coords2d): Coords2d[]
Lists the chunk coordinates of every chunk in the file, in the same order that getAllChunks() returns them. This only looks at the file's index, so it is far faster than reading the chunks and it includes chunks that can't be read.
A region file doesn't record where it is in the world. Pass the region's coordinates, which are in the file's name, to get real chunk coordinates. Without them, the coordinates are relative to the region and are from 0 to 31.
const region = Anvil.regionCoordinates('r.-3.3.mca');
anvil.getChunkCoordinates(region); // [[-96, 96], [-95, 96], ...]anvil.getChunk(chunkCoords: Coords2d): Chunk | undefined
Parses and returns a single chunk using its chunk coordinates. Returns undefined when the chunk is not in the file. Coordinates are wrapped to fit in the region, so it is up to you to use the right region file.
anvil.setChunk(chunk: Chunk, options?: { timestamp?: number }): void
Adds a chunk or replaces the chunk at the same coordinates. Changes made to a chunk's NBT are not saved until the chunk is passed to this method.
The chunk is converted to NBT and compressed with zlib, which Minecraft is able to read no matter how the server is configured to compress chunks. Compressing never gives the same bytes that Minecraft made, even when nothing was changed, so only use this on chunks that you changed. All other chunks in the file are left alone.
Like Minecraft does, the chunk is written over its previous data when it fits and is added to the end of the file when it does not. Either can leave unused space, which compact() removes. The chunk's timestamp is set to the current time, or to options.timestamp in seconds.
This throws when the compressed chunk is larger than 1 MiB, which is the most that fits in a region file.
class Chunk
// Continuing example from Anvil class
const chunks = anvil.getAllChunks();
const firstChunk = chunks[0];
console.log(firstChunk.chunkCoordinates()); // [0, 0]chunk.blockEntityData(coords: Coords3d): NbtCompound | undefined
Gets associated block entity data from the chunk. For instance, this can get a sign's text or the items in a chest. If the information is not found, this will return undefined.
chunk.blockEntities(): NbtList<NbtCompound> | undefined
Returns the list of all block entities in the chunk.
chunk.chunkCoordinates(): Coords2d | undefined
Returns an array with the X and Z coordinates. The chunk's coordinates are the xPos and zPos tags, which are absolute chunk coordinates from the world origin, not from the region. Chunks from the entities folder use their Position tag.
chunk.chunkKey(): string | undefined
The chunk coordinates as a string, such as "-23,14". This is the format used by the command-line tools.
chunk.findBlocksByName(name: string): Coords3d[]
Returns an array. Each element in the array is another array of the X, Y, and Z coordinates of blocks. The coordinates are real-world coordinates. This method will need to unpack the block list, so it will first use a palette to see if the decompression and the lengthy scan can be avoided. The name is the full block name, such as minecraft:oak_sign, and all variations of the block (rotation, waterlogged, etc.) are found.
chunk.getBlock(coords: Coords3d): GenericBlock
Returns a block class that represents the block at the given world coordinates. For known blocks, there are additional methods. See "Block Classes" for further information. This throws if the coordinates are not within the chunk.
chunk.inhabitedTime(): bigint | undefined
Shows the number of ticks processed in the chunk. When the player is nearby, this increases at 20 ticks per second. When the player is farther away, the tick count could advance slower. Does not increment when the chunk isn't loaded.
chunk.nbt(): NbtCompound
Returns the chunk's root NBT tag. Modify the tags and then call anvil.setChunk(chunk) to save the changes. Don't change the block data and then look up blocks using the same Chunk instance, because parsed sections are cached.
chunk.toBuffer(): Uint8Array
Converts the chunk to uncompressed, binary NBT.
chunk.toObject(): object
Converts the internal NBT structure to an object literal, which can be serialized properly with JSON.stringify().
chunk.uniqueBlockNames(): Set<string>
Returns a list of all unique block names within a given chunk.
chunk.worldCoordinates(): Coords2d | undefined
Returns an array with the X and Z real-world coordinates. The chunk will span from X to X+15 (going East) and from Z to Z+15 (going South). The chunk is the entire vertical column of the world.
Block Classes
All blocks will inherit from Generic. Other types of blocks are detected by their name. Below are the patterns and the type of block that will be produced if that filter matches. There's a built-in function that will be used to determine if a block is of a specific type. These are static methods on the Block class, which all take the name as the only parameter and return a boolean.
Block.isBanner(name: string): boolean- Matches all bannersBlock.isBarrel(name: string): booleanBlock.isBeacon(name: string): booleanBlock.isBed(name: string): booleanBlock.isBeehive(name: string): boolean- Matches beehives and bee nestsBlock.isBell(name: string): booleanBlock.isSign(name: string): boolean- Matches all signs
To add to this complexity, there are many blocks that support generic methods, such as .customName() and .lock(). Those are listed under generic in this documentation and called out special because they are not on all block instances.
generic.components(): NbtCompound | undefined
Associated data components for this block.
generic.coords: Coords3d
The world coordinates of the block.
generic.customName(): string | undefined
Returns the custom name, used in the GUI when interacting with the block.
Method only exists if block supports a custom name.
generic.dataVersion: number
The data version of the chunk that the block is in, which identifies the version of Minecraft that saved it. This is 0 when it isn't known.
generic.entityData: NbtCompound | undefined
The NBT entity data associated with this block. If there is none, this is left undefined.
generic.lock(): string | undefined
Returns the item name needed to interact with the block.
Method only exists if block supports locking.
generic.name: string
The name of the block.
banner.patterns(): BannerPattern[]
Returns a list of banner pattern information applied to the banner, in order. The list is empty for a plain banner. Before 1.20.5, colors are numbers and patterns are short codes, such as { color: 14, pattern: "ts" }.
interface BannerPattern {
color: string;
pattern: string | {
asset_id: string;
translation_key: string;
}
}barrel.items(): NbtList<NbtCompound>
The list of items in each slot of the barrel.
barrel.lootTable(): string | undefined
The loot table name for generating random contents.
barrel.lootTableSeed(): bigint | undefined
The seed to use when generating random contents. 0 means to use a random seed.
beacon.primaryEffect(): string | undefined
The primary effect name of the beacon, such as minecraft:speed. This uses the tags from 1.20.2 and newer. Older versions stored a number, which is not converted, so this returns undefined for them.
beacon.secondaryEffect(): string | undefined
The secondary effect name of the beacon.
beehive.bees(): NbtList<NbtCompound> | undefined
Information about the bees within the beehive or bee nest.
beehive.flowerPos(): Coords3d | undefined
Location of the associated flower.
sign.backColor(): string | undefined
Returns the color of the text on the back of the sign for 1.20+. For older versions, this returns undefined.
sign.backGlowingText(): boolean | undefined
Returns true if the sign has glowing text on the back for 1.20+. For older versions, this returns undefined.
sign.backPlainText(): string[] | undefined
Returns the four lines of text on the back of the sign for 1.20+, without formatting. For older versions, this returns undefined. See sign.frontPlainText().
sign.backText(): string[] | undefined
Returns the four lines on the back of the sign for 1.20+, as they are stored. For older versions, this returns undefined. See sign.frontText().
sign.frontColor(): string | undefined
Returns the color of the text on the front of the sign.
sign.frontGlowingText(): boolean | undefined
Returns true if the sign has glowing text on the front.
sign.frontPlainText(): string[]
Returns the four lines of text on the front of the sign as plain text, without colors or other formatting. This gives the same result for all versions of Minecraft and is what you should use to read a sign.
sign.frontText(): string[]
Returns the four lines on the front of the sign as they are stored, which depends on the version of Minecraft that saved the chunk.
| Version | A line that says Hello |
|---------|------------------------|
| 1.21.5 and newer | Hello, or SNBT such as {text:"Hello",bold:1b} when it has formatting |
| 1.8 to 1.21.4 | JSON, either {"text":"Hello"} or "Hello" with the quotes |
| 1.7 and older | Hello |
sign.isWaxed(): boolean | undefined
Returns true if the sign is waxed and false if not for 1.20+. For older versions, this returns undefined.
NBT Classes
There is one class for each type of NBT tag: NbtByte, NbtShort, NbtInt, NbtLong, NbtFloat, NbtDouble, NbtString, NbtByteArray, NbtIntArray, NbtLongArray, NbtList, and NbtCompound. They all extend NbtBase and have these properties:
name- The tag's name. This is an empty string for items in a list.type- The numeric tag type, which matches theNbtTagTypeenum.data- The tag's value. Longs are abigint. Arrays, lists, and compounds are a JavaScript array. Bytes are signed, from -128 to 127.
import { Nbt } from 'mca-json';
import { inflate } from 'pako';
import { readFile } from 'node:fs/promises';
// Player data is compressed
const nbt = Nbt.fromBuffer(inflate(await readFile('player.dat')));
console.log(nbt.findChild('Pos/0')?.data);
console.log(nbt.findChild('Inventory')?.toSnbt());Tags are created with new NbtInt(5, 'name'), where the name is optional, or with new NbtList(items, NbtTagType.INT, 'name') for lists. The name and data properties are able to be changed directly.
Nbt.fromBuffer(data: ArrayBufferLike | ArrayBufferView): NbtBase<any>
Parses NBT data. Returns one of the NBT classes, depending on the tag's type. This does not deal with compressed data. It's the responsibility of the container (Anvil) to decompress the data stream before passing it to this parser.
Nbt.fromSnbt(snbt: string): NbtBase<any>
Parses SNBT data. Returns the hydrated NBT classes from the string form.
Nbt.toBuffer(tag: NbtBase<any>): Uint8Array
Converts a tag and all of its children to uncompressed, binary NBT. Reading a file with Nbt.fromBuffer() and writing it with this produces the same bytes.
import { gzip } from 'pako';
nbt.findChild('Pos/1').data = 100;
await writeFile('player.dat', gzip(Nbt.toBuffer(nbt)));Nbt.getTag(binaryData: BinaryData): NbtBase<any>
Reads binary data from the BinaryData instance to determine the type, then calls the appropriate reader to provide the NBT tag.
NbtBase.readName(binaryData: BinaryData): string
Reads the name portion from a tag. Included in most tags.
nbtBase.findChild(path: string): NbtBase<any> | undefined
Finds a child by looking for exact string matches on names. Children can also be matched by using "/" as a separator, such as "Level/sections".
Only works on NbtList and NbtCompound tags. On all others, this will return undefined.
nbtCompound.setChild(tag: NbtBase<any>): void
Adds a tag to a compound. If the compound already has a child with the same name, it is replaced.
nbtCompound.deleteChild(name: string): boolean
Removes the child with the given name from a compound. Returns true when there was one.
nbtBase.isCompound(name: string): boolean
Checks if this is a compound tag. If name is also given, this checks to see if the compound tag's name also exactly matches. If either condition is not met, this returns false.
nbtBase.isList(subtype: NbtTagType): boolean
Checks if the tag is a list. If subtype is also given, this checks to see if the list tag's subtype also exactly matches. If either condition is not met, this returns false.
nbtBase.toObject(): any
Returns a plain object form of the tag. If this is a compound or list, then all children are included as well.
nbtBase.toSnbt(): string
Converts the tag to SNBT. The conversion will write out into a standard form, not necessarily matching what was parsed. For instance, the byte value "true" would be changed to "1b". Strings will all use double quotes. Other minor changes, but the output will be valid SNBT that Nbt.fromSnbt() can parse again.
LZ4
lz4Decompress(data: Uint8Array): Uint8Array
Decompresses LZ4 data in the format that Minecraft uses for chunks, which is the block stream of the lz4-java library. This is not the same as the LZ4 frame format that the lz4 command-line tool writes. Anvil uses this automatically.
Additional Notes
Coordinate Systems
A player in Minecraft can easily determine their world coordinates. To convert from world coordinates to chunk coordinates, divide the x and z world coordinates by 16, rounded down. World coordinates and chunk coordinates also relate similarly to regions, which are 32 x 32 chunks. The region coordinates are the chunk coordinates divided by 32 and rounded down.
| Type | X | Y | Z | |:------------------:|:-----:|:-----:|:-----:| | World Coordinates | 1234 | 65 | -5678 | | Chunk Coordinates | 77 | * | -355 | | Region Coordinates | 2 | * | -12 |
Special notes about the above example:
- Rounding always rounds down. This may be tricky for negative numbers. -5678 / 16 = -354.875, so that rounds down to -355.
- Chunks span the entire Y column, from bedrock to the build limit.
- Region coordinates are used in the naming of the anvil file. The coordinates used in the example above would be in the file named
r.2.-12.mca.
Region file format
A region file starts with two 4096 byte tables. The first has a location for each of the 32 x 32 chunks, which is the sector where the chunk starts and the number of 4096 byte sectors that it uses. The second has a timestamp for each chunk. The rest of the file is the chunks, each one compressed separately and padded to fill its last sector.
When a chunk grows, Minecraft moves it to another spot and the old sectors become unused. Deleting a chunk only needs its location to be erased. Both leave data in the file that nothing points to, which is what anvil.compact() and mca-remove-chunks remove.
Supported versions
Only Java Edition is supported. Bedrock Edition uses a different format.
Reading the region file structure and the NBT works for every version, so mca-json, mca-chunks, mca-remove-chunks, nbt-json, and the related API all work with any world. This has been tried with files from 1.2.1 through 1.21.
The parts that understand the contents of a chunk are more limited.
- Chunk coordinates are found in all versions.
- Looking up blocks (
chunk.getBlock(),chunk.findBlocksByName(),mca-find-signs) works in all versions. Worlds from 1.12 and older store a number for each block instead of a name. Those are converted to the names that 1.12 used, which are often not the same as the current names. For example, signs areminecraft:standing_signandminecraft:wall_sign, and all colors of wool areminecraft:wool. Numbers without a name, which come from mods, areminecraft:unknown_followed by the number. - Block classes mostly follow the tag names used in 1.20.5 and newer. Signs also handle older formats.
- 26.3 changed how the list of blocks in a section is stored. That is handled, and version 1.2.2 and older of this library are not able to find any blocks or signs in worlds saved by 26.3 or newer.
This has been loosely tested with the following editions. If you have problems with other versions, please supply a test MCA file and a description of the problem, or submit a pull request.
- Java, 26.3, data version 5023.
- Java, 1.21.11, data version 4671.
- Java, 1.21.10, data version 4556, using LZ4 compression.
- Java, 1.21.4, data version 4189.
- Java, 1.20, data version 3463.
- Java, 1.18, data version 2860.
- Java, 1.17.1, data version 2730.
- Java, 1.16.5, data version 2586.
- Java, 1.15.2, data version 2230.
- Java, 1.12.2, data version 1343.
- Java, 1.8.9 and 1.7.10, which don't have a data version.
Chunks compressed with LZ4 (an option in server.properties since 1.20.5) are able to be read. When a changed chunk is saved, it is written with zlib.
Chunks larger than 1 MiB that are stored in .mcc files and chunks using a custom compression are not able to be read. They can still be listed and removed.
SNBT follows the syntax of 1.21.5 and newer, which includes hexadecimal (0xFF) and binary (0b101) numbers, unsigned and signed suffixes (255ub), underscores in numbers (1_000), lists with mixed types, bool(), and uuid(). Numbers that don't fit in their type are an error. The \N{name} escape for named Unicode characters is not supported and is reported as an error.
Development
git clone https://github.com/fidian/mca-json.git
cd mca-json
npm install
# Compile TypeScript into dist/
npm run build
# Build, then run the tests
npm test
# Check the code with ESLint, Prettier, and the TypeScript compiler
npm run lint
# Fix the formatting
npm run formatThe library works with Node 20 and newer. Development needs Node 22.18 or newer because the tests are TypeScript files that Node runs directly, using its built-in test runner. Most of them build their own region files in memory for every chunk format. There are also a few small region files in test/fixtures/ that were cut from real worlds. See the README.md in that folder.
Both npm run lint and npm test are run by GitHub Actions for every push and pull request.
src/index.ts is generated during the build. Any file in src/ that exports something is automatically part of the library's API.
