├── tests ├── __init__.py ├── test_load │ ├── __init__.py │ ├── empty.nbt │ ├── one.nbt │ ├── array.nbt │ └── test_load.py ├── test_amulet_nbt │ ├── test_tag │ │ ├── __init__.py │ │ ├── test_numeric.py │ │ └── test_abc.py │ ├── test_string_encoding │ │ ├── __init__.py │ │ └── test_utf8.py │ ├── test_read_file │ │ ├── src │ │ │ ├── snbt │ │ │ │ ├── hello_world.snbt │ │ │ │ ├── B1_11_level.snbt │ │ │ │ ├── bigtest.snbt │ │ │ │ ├── J1_12_level.snbt │ │ │ │ └── J1_13_level.snbt │ │ │ ├── big_endian_nbt │ │ │ │ ├── hello_world.nbt │ │ │ │ ├── bigtest.nbt │ │ │ │ ├── B1_11_level.nbt │ │ │ │ ├── J1_12_level.nbt │ │ │ │ └── J1_13_level.nbt │ │ │ ├── little_endian_nbt │ │ │ │ ├── hello_world.nbt │ │ │ │ ├── bigtest.nbt │ │ │ │ ├── B1_11_level.nbt │ │ │ │ ├── J1_12_level.nbt │ │ │ │ └── J1_13_level.nbt │ │ │ └── big_endian_compressed_nbt │ │ │ │ ├── bigtest.nbt │ │ │ │ ├── B1_11_level.nbt │ │ │ │ ├── J1_12_level.nbt │ │ │ │ ├── J1_13_level.nbt │ │ │ │ └── hello_world.nbt │ │ └── __init__.py │ ├── _test_amulet_nbt.pyi │ ├── test_binary_nbt_.pyi │ ├── test_metadata.py │ ├── test_binary_nbt.py │ ├── _test_amulet_nbt.py.cpp │ ├── __init__.py │ ├── test_binary_nbt_.py.cpp │ └── test_legacy.py ├── test_file_nbt │ └── gen_data.py ├── CMakeLists.txt ├── binary_data │ └── gen_bin.py ├── test_nbt.py └── test_massive_nbt.py ├── src └── amulet │ └── nbt │ ├── py.typed │ ├── __pyinstaller │ ├── __init__.py │ └── hook-amulet.nbt.py │ ├── tag │ ├── named_tag.hpp │ ├── string.hpp │ ├── eq.hpp │ ├── compound.hpp │ ├── list.hpp │ ├── abc.hpp │ ├── copy.cpp │ ├── float.hpp │ ├── copy.hpp │ ├── array.hpp │ ├── eq.cpp │ ├── int.hpp │ ├── string.py.cpp │ ├── abc.py.cpp │ └── list_methods.hpp │ ├── export.hpp │ ├── nbt_encoding │ ├── string │ │ └── snbt.py.cpp │ ├── binary.hpp │ ├── string.hpp │ └── binary │ │ └── read_binary.cpp │ ├── amulet_nbtConfig.cmake │ ├── string_encoding │ ├── encoding.py.hpp │ ├── string_encoding.hpp │ ├── notes.txt │ ├── encoding.py.cpp │ └── mutf8.cpp │ ├── common.hpp │ └── _amulet_nbt.py.cpp ├── .vscode ├── extensions.json └── settings.json ├── .gitattributes ├── .readthedocs.yml ├── MANIFEST.in ├── mypy.ini ├── get_compiler ├── CMakeLists.txt └── __init__.py ├── .github └── workflows │ ├── python-stylecheck.yml │ ├── python-unittests.yml │ └── python-build.yml ├── README.md ├── tools ├── cmake_generate.py ├── compile_tests.py └── generate_pybind_stubs.py ├── pyproject.toml ├── .gitignore ├── requirements.py ├── setup.py ├── CMakeLists.txt ├── docs_source ├── getting_started.rst └── index.rst └── LICENSE /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/amulet/nbt/py.typed: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/test_load/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/test_load/empty.nbt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/test_amulet_nbt/test_tag/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/test_load/one.nbt: -------------------------------------------------------------------------------- 1 |  -------------------------------------------------------------------------------- /tests/test_amulet_nbt/test_string_encoding/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "ms-python.python" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /tests/test_amulet_nbt/test_read_file/src/snbt/hello_world.snbt: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Bananrama" 3 | } -------------------------------------------------------------------------------- /src/amulet/nbt/__pyinstaller/__init__.py: -------------------------------------------------------------------------------- 1 | def get_hook_dirs() -> list[str]: 2 | return __path__ 3 | -------------------------------------------------------------------------------- /tests/test_amulet_nbt/test_read_file/src/big_endian_nbt/hello_world.nbt: -------------------------------------------------------------------------------- 1 | 2 | hello worldname Bananrama -------------------------------------------------------------------------------- /tests/test_amulet_nbt/test_read_file/src/little_endian_nbt/hello_world.nbt: -------------------------------------------------------------------------------- 1 | 2 | hello worldname Bananrama -------------------------------------------------------------------------------- /tests/test_amulet_nbt/_test_amulet_nbt.pyi: -------------------------------------------------------------------------------- 1 | from types import ModuleType 2 | 3 | def init(m: ModuleType) -> None: ... 4 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | src/amulet/nbt/_version.py export-subst 4 | -------------------------------------------------------------------------------- /tests/test_amulet_nbt/test_binary_nbt_.pyi: -------------------------------------------------------------------------------- 1 | from amulet.nbt import NamedTag 2 | 3 | def encode_binary_nbt(tag: NamedTag) -> bytes: ... 4 | -------------------------------------------------------------------------------- /tests/test_load/array.nbt: -------------------------------------------------------------------------------- 1 |      -------------------------------------------------------------------------------- /tests/test_amulet_nbt/test_read_file/src/big_endian_nbt/bigtest.nbt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Amulet-Team/Amulet-NBT/HEAD/tests/test_amulet_nbt/test_read_file/src/big_endian_nbt/bigtest.nbt -------------------------------------------------------------------------------- /tests/test_amulet_nbt/test_read_file/src/little_endian_nbt/bigtest.nbt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Amulet-Team/Amulet-NBT/HEAD/tests/test_amulet_nbt/test_read_file/src/little_endian_nbt/bigtest.nbt -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true, 3 | "python.defaultInterpreterPath": ".venv", 4 | "python.formatting.provider": "black", 5 | "python.linting.enabled": true 6 | } -------------------------------------------------------------------------------- /tests/test_amulet_nbt/test_read_file/src/big_endian_nbt/B1_11_level.nbt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Amulet-Team/Amulet-NBT/HEAD/tests/test_amulet_nbt/test_read_file/src/big_endian_nbt/B1_11_level.nbt -------------------------------------------------------------------------------- /tests/test_amulet_nbt/test_read_file/src/big_endian_nbt/J1_12_level.nbt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Amulet-Team/Amulet-NBT/HEAD/tests/test_amulet_nbt/test_read_file/src/big_endian_nbt/J1_12_level.nbt -------------------------------------------------------------------------------- /tests/test_amulet_nbt/test_read_file/src/big_endian_nbt/J1_13_level.nbt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Amulet-Team/Amulet-NBT/HEAD/tests/test_amulet_nbt/test_read_file/src/big_endian_nbt/J1_13_level.nbt -------------------------------------------------------------------------------- /tests/test_amulet_nbt/test_read_file/src/little_endian_nbt/B1_11_level.nbt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Amulet-Team/Amulet-NBT/HEAD/tests/test_amulet_nbt/test_read_file/src/little_endian_nbt/B1_11_level.nbt -------------------------------------------------------------------------------- /tests/test_amulet_nbt/test_read_file/src/little_endian_nbt/J1_12_level.nbt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Amulet-Team/Amulet-NBT/HEAD/tests/test_amulet_nbt/test_read_file/src/little_endian_nbt/J1_12_level.nbt -------------------------------------------------------------------------------- /tests/test_amulet_nbt/test_read_file/src/little_endian_nbt/J1_13_level.nbt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Amulet-Team/Amulet-NBT/HEAD/tests/test_amulet_nbt/test_read_file/src/little_endian_nbt/J1_13_level.nbt -------------------------------------------------------------------------------- /tests/test_amulet_nbt/test_read_file/src/big_endian_compressed_nbt/bigtest.nbt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Amulet-Team/Amulet-NBT/HEAD/tests/test_amulet_nbt/test_read_file/src/big_endian_compressed_nbt/bigtest.nbt -------------------------------------------------------------------------------- /tests/test_amulet_nbt/test_read_file/src/big_endian_compressed_nbt/B1_11_level.nbt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Amulet-Team/Amulet-NBT/HEAD/tests/test_amulet_nbt/test_read_file/src/big_endian_compressed_nbt/B1_11_level.nbt -------------------------------------------------------------------------------- /tests/test_amulet_nbt/test_read_file/src/big_endian_compressed_nbt/J1_12_level.nbt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Amulet-Team/Amulet-NBT/HEAD/tests/test_amulet_nbt/test_read_file/src/big_endian_compressed_nbt/J1_12_level.nbt -------------------------------------------------------------------------------- /tests/test_amulet_nbt/test_read_file/src/big_endian_compressed_nbt/J1_13_level.nbt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Amulet-Team/Amulet-NBT/HEAD/tests/test_amulet_nbt/test_read_file/src/big_endian_compressed_nbt/J1_13_level.nbt -------------------------------------------------------------------------------- /tests/test_amulet_nbt/test_read_file/src/big_endian_compressed_nbt/hello_world.nbt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Amulet-Team/Amulet-NBT/HEAD/tests/test_amulet_nbt/test_read_file/src/big_endian_compressed_nbt/hello_world.nbt -------------------------------------------------------------------------------- /src/amulet/nbt/__pyinstaller/hook-amulet.nbt.py: -------------------------------------------------------------------------------- 1 | from PyInstaller.utils.hooks import collect_data_files, collect_submodules 2 | 3 | hiddenimports = collect_submodules("amulet.nbt") 4 | datas = collect_data_files("amulet.nbt") 5 | -------------------------------------------------------------------------------- /.readthedocs.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | build: 4 | os: ubuntu-22.04 5 | tools: 6 | python: 3.12 7 | 8 | python: 9 | install: 10 | - method: pip 11 | path: . 12 | extra_requirements: 13 | - docs 14 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include build_requires.py 2 | include requirements.py 3 | 4 | include get_compiler/CMakeLists.txt 5 | include get_compiler/__init__.py 6 | 7 | recursive-include src/amulet *.cpp *.hpp *Config.cmake 8 | 9 | include CMakeLists.txt 10 | 11 | prune tests 12 | -------------------------------------------------------------------------------- /tests/test_amulet_nbt/test_metadata.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import amulet.nbt 3 | 4 | 5 | class LegacyNBTTests(unittest.TestCase): 6 | def test_version(self) -> None: 7 | self.assertIsInstance(amulet.nbt.__version__, str) 8 | self.assertIsInstance(amulet.nbt.__major__, int) 9 | 10 | 11 | if __name__ == "__main__": 12 | unittest.main() 13 | -------------------------------------------------------------------------------- /mypy.ini: -------------------------------------------------------------------------------- 1 | [mypy] 2 | disallow_untyped_defs = True 3 | check_untyped_defs = True 4 | warn_return_any = True 5 | python_version = 3.12 6 | explicit_package_bases = True 7 | mypy_path = $MYPY_CONFIG_FILE_DIR/src,$MYPY_CONFIG_FILE_DIR/tests 8 | files = 9 | src, 10 | tests, 11 | tools, 12 | get_compiler, 13 | build_requires.py, 14 | requirements.py, 15 | setup.py 16 | -------------------------------------------------------------------------------- /get_compiler/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.13) 2 | 3 | project(get_compiler LANGUAGES CXX) 4 | 5 | # Set C++20 6 | set(CMAKE_CXX_STANDARD 20) 7 | set(CMAKE_CXX_STANDARD_REQUIRED ON) 8 | set(CMAKE_CXX_EXTENSIONS OFF) 9 | 10 | write_file("${CMAKE_BINARY_DIR}/compiler_id.txt" "${CMAKE_CXX_COMPILER_ID}") 11 | write_file("${CMAKE_BINARY_DIR}/compiler_version.txt" "${CMAKE_CXX_COMPILER_VERSION}") 12 | -------------------------------------------------------------------------------- /tests/test_amulet_nbt/test_binary_nbt.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from unittest import TestCase 3 | 4 | from amulet.nbt import NamedTag, IntTag 5 | 6 | 7 | class TestBinaryNBT(TestCase): 8 | def test_binary_nbt(self) -> None: 9 | from test_amulet_nbt.test_binary_nbt_ import encode_binary_nbt 10 | 11 | self.assertEqual( 12 | b"\x03\x00\x02hi\x00\x00\x00\x05", 13 | encode_binary_nbt(NamedTag(IntTag(5), "hi")), 14 | ) 15 | 16 | 17 | if __name__ == "__main__": 18 | unittest.main() 19 | -------------------------------------------------------------------------------- /src/amulet/nbt/tag/named_tag.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | #include 7 | #include 8 | 9 | namespace Amulet { 10 | namespace NBT { 11 | class NamedTag { 12 | public: 13 | std::string name; 14 | TagNode tag_node; 15 | 16 | NamedTag(const std::string& name, const TagNode& tag_node) 17 | : name(name) 18 | , tag_node(tag_node) 19 | { 20 | } 21 | }; 22 | } // namespace NBT 23 | } // namespace Amulet 24 | -------------------------------------------------------------------------------- /tests/test_amulet_nbt/_test_amulet_nbt.py.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include 4 | 5 | namespace py = pybind11; 6 | namespace pyext = Amulet::pybind11_extensions; 7 | 8 | void init_binary_nbt(py::module); 9 | 10 | void init_module(py::module m){ 11 | pyext::init_compiler_config(m); 12 | pyext::check_compatibility(py::module::import("amulet.nbt"), m); 13 | 14 | init_binary_nbt(m); 15 | } 16 | 17 | PYBIND11_MODULE(_test_amulet_nbt, m) { 18 | m.def("init", &init_module, py::arg("m")); 19 | } 20 | -------------------------------------------------------------------------------- /src/amulet/nbt/export.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #ifndef AMULET_NBT_EXPORT 4 | #ifdef _WIN32 5 | #ifdef ExportAmuletNBT 6 | #define AMULET_NBT_EXPORT __declspec(dllexport) 7 | #else 8 | #define AMULET_NBT_EXPORT __declspec(dllimport) 9 | #endif 10 | #else 11 | #define AMULET_NBT_EXPORT 12 | #endif 13 | #endif 14 | 15 | #if !defined(AMULET_NBT_EXPORT_EXCEPTION) 16 | #if defined(_LIBCPP_EXCEPTION) 17 | #define AMULET_NBT_EXPORT_EXCEPTION __attribute__((visibility("default"))) 18 | #else 19 | #define AMULET_NBT_EXPORT_EXCEPTION 20 | #endif 21 | #endif 22 | -------------------------------------------------------------------------------- /tests/test_amulet_nbt/__init__.py: -------------------------------------------------------------------------------- 1 | if __name__ != "test_amulet_nbt": 2 | raise RuntimeError( 3 | f"Module name is incorrect. Expected: 'test_amulet_nbt' got '{__name__}'" 4 | ) 5 | 6 | 7 | import faulthandler as _faulthandler 8 | 9 | _faulthandler.enable() 10 | 11 | 12 | def _init() -> None: 13 | import sys 14 | 15 | # Import dependencies 16 | import amulet.nbt 17 | 18 | # This needs to be an absolute path otherwise it may get called twice 19 | # on different module objects and crash when the interpreter shuts down. 20 | from test_amulet_nbt._test_amulet_nbt import init 21 | 22 | init(sys.modules[__name__]) 23 | 24 | 25 | _init() 26 | -------------------------------------------------------------------------------- /tests/test_amulet_nbt/test_binary_nbt_.py.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include 4 | #include 5 | 6 | #include 7 | 8 | #include 9 | #include 10 | 11 | namespace py = pybind11; 12 | 13 | void init_binary_nbt(py::module m_parent) 14 | { 15 | auto m = m_parent.def_submodule("test_binary_nbt_"); 16 | 17 | m.def("encode_binary_nbt", [](const Amulet::NBT::NamedTag& named_tag) { 18 | Amulet::BinaryWriter writer(std::endian::big); 19 | Amulet::NBT::encode_nbt(writer, named_tag); 20 | return py::bytes(writer.get_buffer()); 21 | }); 22 | } 23 | -------------------------------------------------------------------------------- /tests/test_amulet_nbt/test_tag/test_numeric.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | import faulthandler 3 | 4 | faulthandler.enable() 5 | 6 | from .test_abc import AbstractBaseImmutableTagTestCase 7 | 8 | 9 | class AbstractBaseNumericTagTestCase(AbstractBaseImmutableTagTestCase, ABC): 10 | @abstractmethod 11 | def test_int(self) -> None: 12 | raise NotImplementedError 13 | 14 | @abstractmethod 15 | def test_float(self) -> None: 16 | raise NotImplementedError 17 | 18 | @abstractmethod 19 | def test_bool(self) -> None: 20 | raise NotImplementedError 21 | 22 | @abstractmethod 23 | def test_numerical_operators(self) -> None: 24 | raise NotImplementedError 25 | -------------------------------------------------------------------------------- /src/amulet/nbt/nbt_encoding/string/snbt.py.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | #include 8 | 9 | namespace py = pybind11; 10 | 11 | void init_snbt(py::module& m) 12 | { 13 | m.def( 14 | "read_snbt", 15 | [](std::string snbt) { 16 | return Amulet::NBT::decode_snbt(snbt); 17 | }, 18 | py::arg("snbt"), 19 | py::doc( 20 | "Parse Stringified NBT.\n" 21 | "\n" 22 | ":param snbt: The SNBT string to parse.\n" 23 | ":return: The tag\n" 24 | ":raises: ValueError if the SNBT format is invalid.\n" 25 | ":raises: IndexError if the data overflows the given string.\n")); 26 | } 27 | -------------------------------------------------------------------------------- /src/amulet/nbt/tag/string.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include 6 | #include 7 | 8 | namespace Amulet { 9 | namespace NBT { 10 | class StringTag : public std::string, public AbstractBaseImmutableTag { 11 | public: 12 | using std::string::string; 13 | StringTag(const std::string& value) 14 | : std::string(value.begin(), value.end()) { }; 15 | }; 16 | 17 | static_assert(std::is_copy_constructible_v, "StringTag is not copy constructible"); 18 | static_assert(std::is_copy_assignable_v, "StringTag is not copy assignable"); 19 | 20 | template <> 21 | struct tag_id { 22 | static constexpr std::uint8_t value = 8; 23 | }; 24 | } // namespace NBT 25 | } // namespace Amulet 26 | -------------------------------------------------------------------------------- /src/amulet/nbt/amulet_nbtConfig.cmake: -------------------------------------------------------------------------------- 1 | if (NOT TARGET amulet_nbt) 2 | message(STATUS "Finding amulet_nbt") 3 | 4 | find_package(amulet_io CONFIG REQUIRED) 5 | 6 | set(amulet_nbt_INCLUDE_DIR "${CMAKE_CURRENT_LIST_DIR}/../..") 7 | find_library(amulet_nbt_LIBRARY NAMES amulet_nbt PATHS "${CMAKE_CURRENT_LIST_DIR}") 8 | message(STATUS "amulet_nbt_LIBRARY: ${amulet_nbt_LIBRARY}") 9 | 10 | add_library(amulet_nbt_bin SHARED IMPORTED) 11 | set_target_properties(amulet_nbt_bin PROPERTIES 12 | IMPORTED_IMPLIB "${amulet_nbt_LIBRARY}" 13 | ) 14 | 15 | add_library(amulet_nbt INTERFACE) 16 | target_link_libraries(amulet_nbt INTERFACE amulet_io) 17 | target_link_libraries(amulet_nbt INTERFACE amulet_nbt_bin) 18 | target_include_directories(amulet_nbt INTERFACE ${amulet_nbt_INCLUDE_DIR}) 19 | endif() 20 | -------------------------------------------------------------------------------- /.github/workflows/python-stylecheck.yml: -------------------------------------------------------------------------------- 1 | # This workflow will install Python dependencies, run tests and lint with a variety of Python versions 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions 3 | 4 | name: Stylecheck 5 | 6 | on: 7 | push: 8 | branches: 9 | - master 10 | - main 11 | - '[0-9]+.[0-9]+' 12 | - '[0-9]+.[0-9]+.[0-9]+' 13 | pull_request: 14 | 15 | jobs: 16 | stylecheck: 17 | runs-on: ubuntu-24.04 18 | 19 | steps: 20 | - name: Clone 21 | uses: actions/checkout@v4 22 | 23 | - name: Set up Python 24 | uses: actions/setup-python@v5 25 | with: 26 | python-version: 3.12 27 | 28 | - name: Install dependencies 29 | run: | 30 | python -m pip install --upgrade pip 31 | pip install black 32 | 33 | - name: run stylecheck 34 | run: | 35 | python -m black --check --diff . 36 | -------------------------------------------------------------------------------- /tests/test_amulet_nbt/test_legacy.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import amulet.nbt 3 | 4 | 5 | class LegacyNBTTests(unittest.TestCase): 6 | def test_legacy(self) -> None: 7 | self.assertIs(amulet.nbt.ByteTag, amulet.nbt.TAG_Byte) 8 | self.assertIs(amulet.nbt.ShortTag, amulet.nbt.TAG_Short) 9 | self.assertIs(amulet.nbt.IntTag, amulet.nbt.TAG_Int) 10 | self.assertIs(amulet.nbt.LongTag, amulet.nbt.TAG_Long) 11 | self.assertIs(amulet.nbt.FloatTag, amulet.nbt.TAG_Float) 12 | self.assertIs(amulet.nbt.DoubleTag, amulet.nbt.TAG_Double) 13 | self.assertIs(amulet.nbt.StringTag, amulet.nbt.TAG_String) 14 | self.assertIs(amulet.nbt.ListTag, amulet.nbt.TAG_List) 15 | self.assertIs(amulet.nbt.CompoundTag, amulet.nbt.TAG_Compound) 16 | self.assertIs(amulet.nbt.ByteArrayTag, amulet.nbt.TAG_Byte_Array) 17 | self.assertIs(amulet.nbt.IntArrayTag, amulet.nbt.TAG_Int_Array) 18 | self.assertIs(amulet.nbt.LongArrayTag, amulet.nbt.TAG_Long_Array) 19 | 20 | 21 | if __name__ == "__main__": 22 | unittest.main() 23 | -------------------------------------------------------------------------------- /src/amulet/nbt/string_encoding/encoding.py.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include 6 | #include 7 | 8 | namespace py = pybind11; 9 | 10 | namespace Amulet { 11 | namespace NBT { 12 | class StringEncoding { 13 | public: 14 | Amulet::StringEncoder encode; 15 | Amulet::StringDecoder decode; 16 | StringEncoding( 17 | Amulet::StringEncoder encode, 18 | Amulet::StringDecoder decode) 19 | : encode(encode) 20 | , decode(decode) { }; 21 | }; 22 | 23 | class EncodingPreset { 24 | public: 25 | bool compressed; 26 | std::endian endianness; 27 | StringEncoding string_encoding; 28 | EncodingPreset( 29 | bool compressed, 30 | std::endian endianness, 31 | StringEncoding string_encoding) 32 | : compressed(compressed) 33 | , endianness(endianness) 34 | , string_encoding(string_encoding) { }; 35 | }; 36 | } // namespace NBT 37 | } // namespace Amulet 38 | -------------------------------------------------------------------------------- /tests/test_file_nbt/gen_data.py: -------------------------------------------------------------------------------- 1 | import glob 2 | import os 3 | import amulet.nbt 4 | 5 | datas = { 6 | "big_endian_nbt": (False), 7 | "little_endian_nbt": (), 8 | "snbt": (), 9 | } 10 | 11 | 12 | def main() -> None: 13 | data_dir = os.path.join(os.path.dirname(__file__), "src") 14 | input_dir = os.path.join(data_dir, "big_endian_compressed_nbt") 15 | 16 | for path in glob.glob(os.path.join(input_dir, "*.nbt")): 17 | fname = os.path.splitext(os.path.basename(path))[0] 18 | nbt = amulet.nbt.read_nbt(path) 19 | nbt.save_to( 20 | os.path.join(data_dir, "big_endian_nbt", fname + ".nbt"), 21 | compressed=False, 22 | little_endian=False, 23 | ) 24 | nbt.save_to( 25 | os.path.join(data_dir, "little_endian_nbt", fname + ".nbt"), 26 | compressed=False, 27 | little_endian=True, 28 | ) 29 | with open( 30 | os.path.join(data_dir, "snbt", fname + ".snbt"), "w", encoding="utf-8" 31 | ) as f: 32 | f.write(nbt.to_snbt(" ")) 33 | 34 | 35 | if __name__ == "__main__": 36 | main() 37 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Amulet-NBT 2 | 3 | ![Build](../../workflows/Build/badge.svg) 4 | ![Unittests](../../workflows/Unittests/badge.svg?event=push) 5 | ![Stylecheck](../../workflows/Stylecheck/badge.svg?event=push) 6 | 7 | Amulet-NBT is a Python 3 library, written in C++, for reading and writing both binary NBT and SNBT. 8 | 9 | SNBT (or Stringified-NBT) is the JSON like format used in Java commands. 10 | 11 | ## Installing 12 | 13 | Run this command to install from PyPi. 14 | 15 | `pip install amulet-nbt~=4.0` 16 | 17 | ## Documentation 18 | 19 | See our [readthedocs site](https://amulet-nbt.readthedocs.io) for the full documentation of this library. 20 | 21 | ## Development 22 | 23 | To develop the library you will need to download the source and run this command from the root directory. 24 | 25 | `pip install -e .[dev]` 26 | 27 | This will build the library in-place and expose it to python. 28 | Since this code is compiled you will need to run it again each time you change the C++ code. 29 | 30 | ## Links 31 | - Documentation - https://amulet-nbt.readthedocs.io 32 | - Github - https://github.com/Amulet-Team/Amulet-NBT 33 | - Website - https://www.amuletmc.com/ 34 | -------------------------------------------------------------------------------- /src/amulet/nbt/common.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | #include 10 | 11 | template 12 | constexpr size_t variant_index() { 13 | static_assert(I < std::variant_size_v, "Type T is not a member of variant V"); 14 | if constexpr (std::is_same_v, T>) { 15 | return (I); 16 | } else { 17 | return (variant_index()); 18 | } 19 | } 20 | 21 | template 22 | struct is_shared_ptr : std::false_type {}; 23 | 24 | template 25 | struct is_shared_ptr> : std::true_type {}; 26 | 27 | namespace Amulet { 28 | namespace NBT { 29 | class AMULET_NBT_EXPORT_EXCEPTION type_error : public std::runtime_error { 30 | public: 31 | using std::runtime_error::runtime_error; 32 | }; 33 | template 34 | struct tag_id; 35 | 36 | template 37 | inline constexpr std::uint8_t tag_id_v = tag_id::value; 38 | } // namespace NBT 39 | } // namespace Amulet 40 | -------------------------------------------------------------------------------- /.github/workflows/python-unittests.yml: -------------------------------------------------------------------------------- 1 | # This workflow will install Python dependencies, run tests and lint with a variety of Python versions 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions 3 | 4 | name: Unittests 5 | 6 | on: 7 | push: 8 | branches: 9 | - master 10 | - main 11 | - '[0-9]+.[0-9]+' 12 | - '[0-9]+.[0-9]+.[0-9]+' 13 | pull_request: 14 | 15 | jobs: 16 | unittests: 17 | strategy: 18 | fail-fast: false 19 | matrix: 20 | python-version: [ '3.11', '3.12' ] 21 | os: [ macos-15, windows-2025, ubuntu-24.04 ] 22 | 23 | runs-on: ${{ matrix.os }} 24 | timeout-minutes: 30 25 | defaults: 26 | run: 27 | shell: bash 28 | 29 | steps: 30 | - name: Clone 31 | uses: actions/checkout@v4 32 | 33 | - name: Set up Python 34 | uses: actions/setup-python@v5 35 | with: 36 | python-version: ${{ matrix.python-version }} 37 | 38 | - name: Build 39 | run: | 40 | pip install -v .[dev] 41 | python tools/compile_tests.py 42 | 43 | - name: Test with unittest 44 | run: python -m unittest discover -v -s tests 45 | -------------------------------------------------------------------------------- /.github/workflows/python-build.yml: -------------------------------------------------------------------------------- 1 | # This workflow will install Python dependencies, run tests and lint with a variety of Python versions 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions 3 | 4 | name: Build 5 | 6 | on: 7 | release: 8 | types: [published] 9 | 10 | jobs: 11 | deploy: 12 | strategy: 13 | fail-fast: false 14 | matrix: 15 | python-version: [ '3.11', '3.12' ] 16 | os: [ macos-15, windows-2025 ] 17 | 18 | runs-on: ${{ matrix.os }} 19 | defaults: 20 | run: 21 | shell: bash 22 | 23 | steps: 24 | - name: Clone 25 | uses: actions/checkout@v4 26 | 27 | - name: Set up Python 28 | uses: actions/setup-python@v5 29 | with: 30 | python-version: ${{ matrix.python-version }} 31 | 32 | - name: Install dependencies 33 | shell: bash 34 | run: | 35 | pip install build twine 36 | 37 | - name: Build 38 | run: | 39 | python -m build . 40 | 41 | - name: Publish 42 | env: 43 | TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }} 44 | TWINE_PASSWORD: ${{ secrets.AMULET_NBT_PYPI_PASSWORD }} 45 | run: | 46 | twine upload dist/* --skip-existing 47 | -------------------------------------------------------------------------------- /get_compiler/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | import subprocess 3 | from tempfile import TemporaryDirectory 4 | 5 | 6 | def main() -> str: 7 | if subprocess.run(["cmake", "--version"]).returncode: 8 | raise RuntimeError("Could not find cmake") 9 | 10 | with TemporaryDirectory() as build_dir: 11 | # get the compiler id and version 12 | if subprocess.run( 13 | ["cmake", "-S", os.path.dirname(__file__), "-B", build_dir] 14 | ).returncode: 15 | raise RuntimeError( 16 | "Could not find a C++ 20 compiler. Do you have a C++ 20 compiler installed?" 17 | ) 18 | 19 | # Get the compiler variables generated by the cmake file 20 | with open(os.path.join(build_dir, "compiler_id.txt")) as f: 21 | compiler_id_str = f.read().strip() 22 | with open(os.path.join(build_dir, "compiler_version.txt")) as f: 23 | compiler_version = f.read().strip().split(".", 1)[0] 24 | 25 | # convert the compiler id to an int so it can be used in a version number 26 | compiler_id_int = 0 27 | for b in compiler_id_str.encode("utf-8"): 28 | compiler_id_int <<= 8 29 | compiler_id_int += b 30 | 31 | # combine the compiler id and compiler version into a version number 32 | return f"==4.{compiler_id_int}.{compiler_version}" 33 | -------------------------------------------------------------------------------- /src/amulet/nbt/string_encoding/string_encoding.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | #include 8 | 9 | namespace Amulet { 10 | namespace NBT { 11 | typedef std::vector CodePointVector; 12 | 13 | // Functions to convert between code point vector and encoded formats 14 | AMULET_NBT_EXPORT CodePointVector read_utf8(std::string_view src); 15 | AMULET_NBT_EXPORT CodePointVector read_utf8_escape(std::string_view src); 16 | AMULET_NBT_EXPORT CodePointVector read_mutf8(std::string_view src); 17 | 18 | AMULET_NBT_EXPORT void write_utf8(std::string& dst, const CodePointVector& src); 19 | AMULET_NBT_EXPORT void write_utf8_escape(std::string& dst, const CodePointVector& src); 20 | AMULET_NBT_EXPORT void write_mutf8(std::string& dst, const CodePointVector& src); 21 | 22 | AMULET_NBT_EXPORT std::string write_utf8(const CodePointVector& src); 23 | AMULET_NBT_EXPORT std::string write_utf8_escape(const CodePointVector& src); 24 | AMULET_NBT_EXPORT std::string write_mutf8(const CodePointVector& src); 25 | 26 | // Functions to convert between the encoded formats. 27 | AMULET_NBT_EXPORT std::string utf8_to_utf8(std::string_view src); 28 | AMULET_NBT_EXPORT std::string utf8_escape_to_utf8(std::string_view src); 29 | AMULET_NBT_EXPORT std::string utf8_to_utf8_escape(std::string_view src); 30 | AMULET_NBT_EXPORT std::string mutf8_to_utf8(std::string_view src); 31 | AMULET_NBT_EXPORT std::string utf8_to_mutf8(std::string_view src); 32 | } // namespace NBT 33 | } // namespace Amulet 34 | -------------------------------------------------------------------------------- /src/amulet/nbt/tag/eq.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | // All of the NBT equal functions 4 | 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | 14 | namespace Amulet { 15 | namespace NBT { 16 | AMULET_NBT_EXPORT bool NBTTag_eq(const ByteTag& a, const ByteTag& b); 17 | AMULET_NBT_EXPORT bool NBTTag_eq(const ShortTag& a, const ShortTag& b); 18 | AMULET_NBT_EXPORT bool NBTTag_eq(const IntTag& a, const IntTag& b); 19 | AMULET_NBT_EXPORT bool NBTTag_eq(const LongTag& a, const LongTag& b); 20 | AMULET_NBT_EXPORT bool NBTTag_eq(const FloatTag& a, const FloatTag& b); 21 | AMULET_NBT_EXPORT bool NBTTag_eq(const DoubleTag& a, const DoubleTag& b); 22 | AMULET_NBT_EXPORT bool NBTTag_eq(const ByteArrayTag& a, const ByteArrayTag& b); 23 | AMULET_NBT_EXPORT bool NBTTag_eq(const StringTag& a, const StringTag& b); 24 | AMULET_NBT_EXPORT bool NBTTag_eq(const ListTag& a, const ListTag& b); 25 | AMULET_NBT_EXPORT bool NBTTag_eq(const CompoundTag& a, const CompoundTag& b); 26 | AMULET_NBT_EXPORT bool NBTTag_eq(const IntArrayTag& a, const IntArrayTag& b); 27 | AMULET_NBT_EXPORT bool NBTTag_eq(const LongArrayTag& a, const LongArrayTag& b); 28 | AMULET_NBT_EXPORT bool NBTTag_eq(const TagNode& a, const TagNode& b); 29 | AMULET_NBT_EXPORT bool NBTTag_eq(const NamedTag& a, const NamedTag& b); 30 | } // namespace NBT 31 | } // namespace Amulet 32 | -------------------------------------------------------------------------------- /src/amulet/nbt/tag/compound.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | 16 | namespace Amulet { 17 | namespace NBT { 18 | class ListTag; 19 | typedef std::shared_ptr ListTagPtr; 20 | class CompoundTag; 21 | typedef std::shared_ptr CompoundTagPtr; 22 | 23 | typedef std::variant< 24 | ByteTag, 25 | ShortTag, 26 | IntTag, 27 | LongTag, 28 | FloatTag, 29 | DoubleTag, 30 | ByteArrayTagPtr, 31 | StringTag, 32 | ListTagPtr, 33 | CompoundTagPtr, 34 | IntArrayTagPtr, 35 | LongArrayTagPtr> 36 | TagNode; 37 | 38 | typedef std::unordered_map CompoundTagNative; 39 | 40 | class CompoundTag : public CompoundTagNative, public AbstractBaseMutableTag { 41 | using unordered_map::unordered_map; 42 | }; 43 | 44 | static_assert(std::is_copy_constructible_v, "CompoundTag is not copy constructible"); 45 | static_assert(std::is_copy_assignable_v, "CompoundTag is not copy assignable"); 46 | 47 | template <> 48 | struct tag_id { 49 | static constexpr std::uint8_t value = 10; 50 | }; 51 | template <> 52 | struct tag_id { 53 | static constexpr std::uint8_t value = 10; 54 | }; 55 | } // namespace NBT 56 | } // namespace Amulet 57 | -------------------------------------------------------------------------------- /tools/cmake_generate.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import subprocess 3 | import os 4 | import shutil 5 | 6 | import pybind11 7 | import amulet.pybind11_extensions 8 | import amulet.io 9 | import amulet.zlib 10 | 11 | 12 | def fix_path(path: str) -> str: 13 | return os.path.realpath(path).replace(os.sep, "/") 14 | 15 | 16 | RootDir = fix_path(os.path.dirname(os.path.dirname(__file__))) 17 | 18 | 19 | def main() -> None: 20 | platform_args = [] 21 | if sys.platform == "win32": 22 | platform_args.extend(["-G", "Visual Studio 17 2022"]) 23 | if sys.maxsize > 2**32: 24 | platform_args.extend(["-A", "x64"]) 25 | else: 26 | platform_args.extend(["-A", "Win32"]) 27 | platform_args.extend(["-T", "v143"]) 28 | 29 | os.chdir(RootDir) 30 | shutil.rmtree(os.path.join(RootDir, "build", "CMakeFiles"), ignore_errors=True) 31 | 32 | if subprocess.run(["cmake", "--version"]).returncode: 33 | raise RuntimeError("Could not find cmake") 34 | if subprocess.run( 35 | [ 36 | "cmake", 37 | *platform_args, 38 | f"-DPYTHON_EXECUTABLE={sys.executable}", 39 | f"-Dpybind11_DIR={fix_path(pybind11.get_cmake_dir())}", 40 | f"-Damulet_pybind11_extensions_DIR={fix_path(amulet.pybind11_extensions.__path__[0])}", 41 | f"-Damulet_io_DIR={fix_path(amulet.io.__path__[0])}", 42 | f"-Damulet_zlib_DIR={fix_path(amulet.zlib.__path__[0])}", 43 | f"-Damulet_nbt_DIR={fix_path(os.path.join(RootDir, 'src', 'amulet', 'nbt'))}", 44 | f"-DCMAKE_INSTALL_PREFIX=install", 45 | f"-DBUILD_AMULET_NBT_TESTS=ON", 46 | "-B", 47 | "build", 48 | ] 49 | ).returncode: 50 | raise RuntimeError("Error configuring amulet-nbt") 51 | 52 | 53 | if __name__ == "__main__": 54 | main() 55 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = [ 3 | "setuptools>=42", 4 | "versioneer", 5 | "packaging", 6 | ] 7 | build-backend = "build_requires" 8 | backend-path = [""] 9 | 10 | [project] 11 | name = "amulet-nbt" 12 | authors = [ 13 | {name = "James Clare"}, 14 | {name = "Ben Gothard"}, 15 | ] 16 | description = "Read and write Minecraft NBT and SNBT data." 17 | dynamic = ["version", "readme", "dependencies"] 18 | requires-python = ">=3.11" 19 | classifiers = [ 20 | "Programming Language :: Python :: 3", 21 | "Operating System :: OS Independent", 22 | ] 23 | 24 | [project.optional-dependencies] 25 | docs = [ 26 | "Sphinx>=1.7.4", 27 | "sphinx-autodoc-typehints>=1.3.0", 28 | "sphinx_rtd_theme>=0.3.1", 29 | ] 30 | dev = [ 31 | "setuptools>=42", 32 | "types-setuptools", 33 | "versioneer", 34 | "types-versioneer", 35 | "packaging", 36 | "wheel", 37 | "pybind11_stubgen>=2.5.4", 38 | "black>=22.3", 39 | "isort", 40 | "autoflake", 41 | "mypy", 42 | "types-pyinstaller", 43 | ] 44 | 45 | [project.urls] 46 | Homepage = "https://www.amuletmc.com" 47 | Repository = "https://github.com/Amulet-Team/Amulet-NBT" 48 | Issues = "https://github.com/Amulet-Team/Amulet-NBT/issues" 49 | 50 | [tool.setuptools] 51 | include-package-data = false 52 | 53 | [tool.setuptools.package-data] 54 | "*" = [ 55 | "*Config.cmake", 56 | "**/*.hpp", 57 | "**/*.dll", 58 | "**/*.so", 59 | "**/*.dylib", 60 | "**/*.lib", 61 | ] 62 | 63 | [tool.setuptools.dynamic] 64 | readme = {file = ["README.md"], content-type = "text/markdown"} 65 | 66 | [project.entry-points.pyinstaller40] 67 | hook-dirs = "amulet.nbt.__pyinstaller:get_hook_dirs" 68 | 69 | [tool.versioneer] 70 | VCS = "git" 71 | style = "pep440" 72 | versionfile_source = "src/amulet/nbt/_version.py" 73 | versionfile_build = "amulet/nbt/_version.py" 74 | tag_prefix = "" 75 | parentdir_prefix = "amulet_nbt-" 76 | -------------------------------------------------------------------------------- /src/amulet/nbt/string_encoding/notes.txt: -------------------------------------------------------------------------------- 1 | UTF-8 2 | 0GFEDCBA 3 | 0 0 00000000 4 | 2**7-1 127 01111111 5 | 6 | 110KJIHG 10FEDCBA 7 | 2**7 128 11000010 10000000 8 | 2**11-1 2047 11011111 10111111 9 | 10 | 1110PONM 10LKJIHG 10FEDCBA 11 | 2**11 2048 11100000 10100000 10000000 12 | 0xD800 to 0xDFFF are invalid to not conflict with utf-16 13 | 2**16-1 65535 11101111 10111111 10111111 14 | 15 | 11110UTS 10RQPONM 10LKJIHG 10FEDCBA 16 | 2**16 65536 11110000 10010000 10000000 10000000 17 | 0x10ffff 1114111 11110100 10001111 10111111 10111111 18 | 19 | MUTF-8 20 | 0GFEDCBA 21 | 0 0 11000000 10000000 22 | 1 1 00000001 23 | 2**7-1 127 01111111 24 | 25 | 110KJIHG 10FEDCBA 26 | 2**7 128 11000010 10000000 27 | 2**11-1 2047 11011111 10111111 28 | 29 | 1110PONM 10LKJIHG 10FEDCBA 30 | 2**11 2048 11100000 10100000 10000000 31 | 55295 11101101 10011111 10111111 32 | 0xD800 55296 11101101 10100000 10000000 INVALID 33 | INVALID 34 | 0xDFFF 57343 11101101 10111111 10111111 INVALID 35 | 57344 11101110 10000000 10000000 36 | 2**16-1 65535 11101111 10111111 10111111 37 | 38 | 11101101 10000000 10000000 39 | 40 | Values >= 65536 first have 65536 subtracted from them 41 | It is then split into the high 10 bits and the low 10 bits and encoded as two 3 byte characters 42 | See the wiki page for UTF-16 for more info 43 | 11101101 1010TSRQ 10PONMLK 11101101 1011JIHG 10FEDCBA 44 | 2**16 65536 11101101 10100000 10000000 11101101 10110000 10000000 45 | 0x10ffff 1114111 11101101 10100000 10111111 11101101 10111111 10111111 46 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # Distribution / packaging 7 | .Python 8 | build/ 9 | develop-eggs/ 10 | dist/ 11 | downloads/ 12 | eggs/ 13 | .eggs/ 14 | lib/ 15 | lib64/ 16 | parts/ 17 | sdist/ 18 | var/ 19 | wheels/ 20 | *.egg-info/ 21 | .installed.cfg 22 | *.egg 23 | MANIFEST 24 | 25 | # PyInstaller 26 | # Usually these files are written by a python script from a template 27 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 28 | *.manifest 29 | *.spec 30 | !version_definitions/*/*.manifest 31 | 32 | # Installer logs 33 | pip-log.txt 34 | pip-delete-this-directory.txt 35 | 36 | # Unit test / coverage reports 37 | htmlcov/ 38 | .tox/ 39 | .coverage 40 | .coverage.* 41 | .cache 42 | nosetests.xml 43 | coverage.xml 44 | *.cover 45 | .hypothesis/ 46 | .pytest_cache/ 47 | 48 | # Translations 49 | *.mo 50 | *.pot 51 | 52 | # Django stuff: 53 | local_settings.py 54 | db.sqlite3 55 | 56 | # Flask stuff: 57 | instance/ 58 | .webassets-cache 59 | 60 | # Scrapy stuff: 61 | .scrapy 62 | 63 | # Sphinx documentation 64 | docs/_build/ 65 | docs_build/ 66 | 67 | # PyBuilder 68 | target/ 69 | 70 | # Jupyter Notebook 71 | .ipynb_checkpoints 72 | 73 | # pyenv 74 | .python-version 75 | 76 | # celery beat schedule file 77 | celerybeat-schedule 78 | 79 | # SageMath parsed files 80 | *.sage.py 81 | 82 | # Environments 83 | .env 84 | .venv 85 | env/ 86 | venv/ 87 | venv_37/ 88 | ENV/ 89 | env.bak/ 90 | venv.bak/ 91 | venv* 92 | 93 | # Spyder project settings 94 | .spyderproject 95 | .spyproject 96 | 97 | # Rope project settings 98 | .ropeproject 99 | 100 | # PyCharm settings 101 | .idea/ 102 | 103 | # mkdocs documentation 104 | /site 105 | 106 | # mypy 107 | .mypy_cache/ 108 | 109 | # Visual Studio 110 | /install/ 111 | .vs 112 | *.dll 113 | *.so 114 | *.dylib 115 | *.lib 116 | *.exp 117 | *.pdb 118 | *.ilk 119 | 120 | /amulet_nbt-* 121 | -------------------------------------------------------------------------------- /tools/compile_tests.py: -------------------------------------------------------------------------------- 1 | import subprocess 2 | import sys 3 | import shutil 4 | import os 5 | 6 | import pybind11 7 | import amulet.pybind11_extensions 8 | import amulet.io 9 | import amulet.nbt 10 | 11 | 12 | def fix_path(path: str) -> str: 13 | return os.path.realpath(path).replace(os.sep, "/") 14 | 15 | 16 | RootDir = os.path.dirname(os.path.dirname(__file__)) 17 | TestsDir = os.path.join(RootDir, "tests") 18 | 19 | 20 | def main() -> None: 21 | platform_args = [] 22 | if sys.platform == "win32": 23 | platform_args.extend(["-G", "Visual Studio 17 2022"]) 24 | if sys.maxsize > 2**32: 25 | platform_args.extend(["-A", "x64"]) 26 | else: 27 | platform_args.extend(["-A", "Win32"]) 28 | platform_args.extend(["-T", "v143"]) 29 | 30 | os.chdir(TestsDir) 31 | shutil.rmtree(os.path.join(TestsDir, "build", "CMakeFiles"), ignore_errors=True) 32 | 33 | if subprocess.run(["cmake", "--version"]).returncode: 34 | raise RuntimeError("Could not find cmake") 35 | if subprocess.run( 36 | [ 37 | "cmake", 38 | *platform_args, 39 | f"-DPYTHON_EXECUTABLE={sys.executable}", 40 | f"-Dpybind11_DIR={fix_path(pybind11.get_cmake_dir())}", 41 | f"-Damulet_pybind11_extensions_DIR={fix_path(amulet.pybind11_extensions.__path__[0])}", 42 | f"-Damulet_io_DIR={fix_path(amulet.io.__path__[0])}", 43 | f"-Damulet_nbt_DIR={fix_path(amulet.nbt.__path__[0])}", 44 | f"-DCMAKE_INSTALL_PREFIX=install", 45 | "-B", 46 | "build", 47 | ] 48 | ).returncode: 49 | raise RuntimeError("Error configuring test-amulet-nbt") 50 | if subprocess.run( 51 | ["cmake", "--build", "build", "--config", "RelWithDebInfo"] 52 | ).returncode: 53 | raise RuntimeError("Error building test-amulet-nbt") 54 | if subprocess.run( 55 | ["cmake", "--install", "build", "--config", "RelWithDebInfo"] 56 | ).returncode: 57 | raise RuntimeError("Error installing test-amulet-nbt") 58 | 59 | 60 | if __name__ == "__main__": 61 | main() 62 | -------------------------------------------------------------------------------- /tests/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.13) 2 | 3 | project(amulet_nbt_tests LANGUAGES CXX) 4 | 5 | # Set C++20 6 | set(CMAKE_CXX_STANDARD 20) 7 | set(CMAKE_CXX_STANDARD_REQUIRED ON) 8 | set(CMAKE_CXX_EXTENSIONS OFF) 9 | 10 | # Set platform variables 11 | if (WIN32) 12 | # set windows 7 as the minimum version 13 | add_definitions(-D_WIN32_WINNT=0x0601) 14 | elseif(APPLE) 15 | set(CMAKE_OSX_DEPLOYMENT_TARGET "10.15") 16 | elseif(UNIX) 17 | set(CMAKE_POSITION_INDEPENDENT_CODE ON) 18 | else() 19 | message( FATAL_ERROR "Unsupported platform. Please submit a pull request to support this platform." ) 20 | endif() 21 | 22 | # Find dependencies 23 | if (NOT TARGET pybind11::module) 24 | find_package(pybind11 CONFIG REQUIRED) 25 | endif() 26 | if (NOT TARGET amulet_pybind11_extensions) 27 | find_package(amulet_pybind11_extensions CONFIG REQUIRED) 28 | endif() 29 | if (NOT TARGET amulet_nbt) 30 | find_package(amulet_nbt CONFIG REQUIRED) 31 | endif() 32 | 33 | # Find sources 34 | file(GLOB_RECURSE SOURCES LIST_DIRECTORIES false "${CMAKE_CURRENT_LIST_DIR}/*.py.cpp") 35 | 36 | pybind11_add_module(_test_amulet_nbt) 37 | set_target_properties(_test_amulet_nbt PROPERTIES FOLDER "Tests") 38 | target_compile_definitions(_test_amulet_nbt PRIVATE PYBIND11_DETAILED_ERROR_MESSAGES) 39 | target_compile_definitions(_test_amulet_nbt PRIVATE PYBIND11_VERSION="${pybind11_VERSION}") 40 | target_compile_definitions(_test_amulet_nbt PRIVATE COMPILER_ID="${CMAKE_CXX_COMPILER_ID}") 41 | target_compile_definitions(_test_amulet_nbt PRIVATE COMPILER_VERSION="${CMAKE_CXX_COMPILER_VERSION}") 42 | target_link_libraries(_test_amulet_nbt PRIVATE amulet_pybind11_extensions) 43 | target_link_libraries(_test_amulet_nbt PRIVATE amulet_nbt) 44 | target_sources(_test_amulet_nbt PRIVATE ${SOURCES}) 45 | foreach(FILE ${SOURCES}) 46 | file(RELATIVE_PATH REL_PATH ${CMAKE_CURRENT_LIST_DIR} ${FILE}) 47 | get_filename_component(GROUP ${REL_PATH} DIRECTORY) 48 | string(REPLACE "/" "\\" GROUP "${GROUP}") 49 | source_group(${GROUP} FILES ${FILE}) 50 | endforeach() 51 | 52 | # Install 53 | install(TARGETS _test_amulet_nbt DESTINATION "${CMAKE_CURRENT_LIST_DIR}/test_amulet_nbt") 54 | -------------------------------------------------------------------------------- /src/amulet/nbt/string_encoding/encoding.py.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | #include 9 | #include 10 | 11 | namespace py = pybind11; 12 | 13 | void init_encoding(py::module& m) 14 | { 15 | py::classh StringEncoding(m, "StringEncoding"); 16 | StringEncoding.def( 17 | "encode", 18 | [](const Amulet::NBT::StringEncoding& self, py::bytes data) -> py::bytes { 19 | return self.encode(data); 20 | }); 21 | StringEncoding.def( 22 | "decode", 23 | [](const Amulet::NBT::StringEncoding& self, py::bytes data) -> py::bytes { 24 | return self.decode(data); 25 | }); 26 | 27 | Amulet::NBT::StringEncoding utf8_encoding = Amulet::NBT::StringEncoding(Amulet::NBT::utf8_to_utf8, Amulet::NBT::utf8_to_utf8); 28 | Amulet::NBT::StringEncoding utf8_escape_encoding = Amulet::NBT::StringEncoding(Amulet::NBT::utf8_to_utf8_escape, Amulet::NBT::utf8_escape_to_utf8); 29 | Amulet::NBT::StringEncoding mutf8_encoding = Amulet::NBT::StringEncoding(Amulet::NBT::utf8_to_mutf8, Amulet::NBT::mutf8_to_utf8); 30 | 31 | m.attr("utf8_encoding") = utf8_encoding; 32 | m.attr("utf8_escape_encoding") = utf8_escape_encoding; 33 | m.attr("mutf8_encoding") = mutf8_encoding; 34 | 35 | py::classh EncodingPreset(m, "EncodingPreset"); 36 | EncodingPreset.def_readonly( 37 | "compressed", 38 | &Amulet::NBT::EncodingPreset::compressed); 39 | EncodingPreset.def_property_readonly( 40 | "little_endian", 41 | [](const Amulet::NBT::EncodingPreset& self) { 42 | return self.endianness == std::endian::little; 43 | }); 44 | EncodingPreset.def_readonly( 45 | "string_encoding", 46 | &Amulet::NBT::EncodingPreset::string_encoding); 47 | 48 | Amulet::NBT::EncodingPreset java_encoding = Amulet::NBT::EncodingPreset(true, std::endian::big, mutf8_encoding); 49 | Amulet::NBT::EncodingPreset bedrock_encoding = Amulet::NBT::EncodingPreset(false, std::endian::little, utf8_escape_encoding); 50 | 51 | m.attr("java_encoding") = java_encoding; 52 | m.attr("bedrock_encoding") = bedrock_encoding; 53 | } 54 | -------------------------------------------------------------------------------- /tests/binary_data/gen_bin.py: -------------------------------------------------------------------------------- 1 | """ 2 | Creates an example of the binary format for each nbt class. 3 | The printed result is used in test_nbt_read_write.py 4 | This is here in case that data needs to be recreated in the future. 5 | """ 6 | 7 | from typing import Type, Any, Iterable 8 | from amulet.nbt import ( 9 | AbstractBaseTag, 10 | ByteTag, 11 | ShortTag, 12 | IntTag, 13 | LongTag, 14 | FloatTag, 15 | DoubleTag, 16 | ByteArrayTag, 17 | IntArrayTag, 18 | LongArrayTag, 19 | StringTag, 20 | ListTag, 21 | CompoundTag, 22 | NamedTag, 23 | AbstractBaseArrayTag, 24 | AbstractBaseNumericTag, 25 | ) 26 | 27 | names = ("", "name") 28 | tags: Iterable[ 29 | tuple[tuple[Type[AbstractBaseNumericTag], ...], tuple[int, ...]] 30 | | tuple[tuple[Type[AbstractBaseArrayTag], ...], tuple[list[int], ...]] 31 | | tuple[tuple[Type[StringTag]], tuple[str]] 32 | | tuple[tuple[Type[ListTag]], tuple[list, ...]] 33 | | tuple[tuple[Type[CompoundTag]], tuple[dict, ...]] 34 | ] = ( 35 | ((ByteTag, ShortTag, IntTag, LongTag, FloatTag, DoubleTag), (5, -5)), 36 | ((ByteArrayTag, IntArrayTag, LongArrayTag), ([], [5, 6, 7], [-5, -6, -7])), 37 | ((StringTag,), ("value",)), 38 | ((ListTag,), ([],)), 39 | ((CompoundTag,), ({},)), 40 | ) 41 | 42 | 43 | def print_line(name: str, tag: AbstractBaseTag) -> None: 44 | named_tag = NamedTag(tag, name) 45 | print( 46 | f"(" 47 | f"{repr(named_tag)}, " 48 | f"{repr(named_tag.to_nbt(compressed=False, little_endian=False))}, " 49 | f"{repr(named_tag.to_nbt(compressed=False, little_endian=True))}, " 50 | f"{repr(named_tag.to_nbt(compressed=True, little_endian=False))}, " 51 | f"{repr(named_tag.to_nbt(compressed=True, little_endian=True))}, " 52 | f"{repr(named_tag.tag.to_snbt())}" 53 | f")," 54 | ) 55 | 56 | 57 | def gen_data() -> Iterable[AbstractBaseTag]: 58 | for dtypes, values in tags: 59 | for tag in dtypes: 60 | for value in values: 61 | yield tag(value) # type: ignore 62 | 63 | 64 | def gen_data_all() -> Iterable[AbstractBaseTag]: 65 | yield from gen_data() 66 | for data in gen_data(): 67 | yield ListTag([data]) 68 | for data in gen_data(): 69 | yield CompoundTag({"key": data}) 70 | 71 | 72 | def main() -> None: 73 | for name in names: 74 | for data in gen_data_all(): 75 | print_line(name, data) 76 | print() 77 | 78 | 79 | if __name__ == "__main__": 80 | main() 81 | -------------------------------------------------------------------------------- /src/amulet/nbt/_amulet_nbt.py.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include 4 | 5 | #include 6 | 7 | #include 8 | 9 | namespace py = pybind11; 10 | namespace pyext = Amulet::pybind11_extensions; 11 | 12 | void init_encoding(py::module&); 13 | 14 | void init_abc(py::module&); 15 | void init_int(py::module&); 16 | void init_float(py::module&); 17 | void init_string(py::module&); 18 | void init_array(py::module&); 19 | void init_list(py::module&); 20 | void init_compound(py::module&); 21 | 22 | void init_named_tag(py::module&); 23 | 24 | void init_bnbt(py::module& m); 25 | void init_snbt(py::module& m); 26 | 27 | void init_module(py::module m) 28 | { 29 | pyext::init_compiler_config(m); 30 | pyext::check_compatibility(py::module::import("amulet.zlib"), m); 31 | 32 | // Convert cast_error to type_error 33 | py::register_local_exception_translator([](std::exception_ptr p) { 34 | try { 35 | if (p) { 36 | std::rethrow_exception(p); 37 | } 38 | } catch (const py::cast_error& e) { 39 | py::set_error(PyExc_TypeError, e.what()); 40 | } 41 | }); 42 | 43 | py::register_exception_translator([](std::exception_ptr p) { 44 | try { 45 | if (p) { 46 | std::rethrow_exception(p); 47 | } 48 | } catch (const Amulet::NBT::type_error& e) { 49 | py::set_error(PyExc_TypeError, e.what()); 50 | } 51 | }); 52 | 53 | init_encoding(m); 54 | init_abc(m); 55 | init_int(m); 56 | init_float(m); 57 | init_string(m); 58 | init_array(m); 59 | init_compound(m); 60 | init_list(m); 61 | 62 | // Tag Alias's 63 | m.attr("TAG_Byte") = m.attr("ByteTag"); 64 | m.attr("TAG_Short") = m.attr("ShortTag"); 65 | m.attr("TAG_Int") = m.attr("IntTag"); 66 | m.attr("TAG_Long") = m.attr("LongTag"); 67 | m.attr("TAG_Float") = m.attr("FloatTag"); 68 | m.attr("TAG_Double") = m.attr("DoubleTag"); 69 | m.attr("TAG_Byte_Array") = m.attr("ByteArrayTag"); 70 | m.attr("TAG_String") = m.attr("StringTag"); 71 | m.attr("TAG_List") = m.attr("ListTag"); 72 | m.attr("TAG_Compound") = m.attr("CompoundTag"); 73 | m.attr("TAG_Int_Array") = m.attr("IntArrayTag"); 74 | m.attr("TAG_Long_Array") = m.attr("LongArrayTag"); 75 | 76 | init_named_tag(m); 77 | 78 | init_bnbt(m); 79 | init_snbt(m); 80 | } 81 | 82 | PYBIND11_MODULE(_amulet_nbt, m) 83 | { 84 | m.def("init", &init_module, py::arg("m")); 85 | } 86 | -------------------------------------------------------------------------------- /src/amulet/nbt/tag/list.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | 15 | namespace Amulet { 16 | namespace NBT { 17 | class ListTag; 18 | typedef std::shared_ptr ListTagPtr; 19 | class CompoundTag; 20 | typedef std::shared_ptr CompoundTagPtr; 21 | 22 | // List types 23 | typedef std::vector ByteListTag; 24 | typedef std::vector ShortListTag; 25 | typedef std::vector IntListTag; 26 | typedef std::vector LongListTag; 27 | typedef std::vector FloatListTag; 28 | typedef std::vector DoubleListTag; 29 | typedef std::vector ByteArrayListTag; 30 | typedef std::vector StringListTag; 31 | typedef std::vector ListListTag; 32 | typedef std::vector CompoundListTag; 33 | typedef std::vector IntArrayListTag; 34 | typedef std::vector LongArrayListTag; 35 | 36 | typedef std::variant< 37 | std::monostate, 38 | ByteListTag, 39 | ShortListTag, 40 | IntListTag, 41 | LongListTag, 42 | FloatListTag, 43 | DoubleListTag, 44 | ByteArrayListTag, 45 | StringListTag, 46 | ListListTag, 47 | CompoundListTag, 48 | IntArrayListTag, 49 | LongArrayListTag> 50 | ListTagNative; 51 | 52 | class ListTag : public ListTagNative, public AbstractBaseImmutableTag { 53 | using variant::variant; 54 | }; 55 | 56 | static_assert(std::is_copy_constructible_v, "ListTag is not copy constructible"); 57 | static_assert(std::is_copy_assignable_v, "ListTag is not copy assignable"); 58 | 59 | template <> 60 | struct tag_id { 61 | static constexpr std::uint8_t value = 9; 62 | }; 63 | template <> 64 | struct tag_id { 65 | static constexpr std::uint8_t value = 9; 66 | }; 67 | } // namespace NBT 68 | } // namespace Amulet 69 | 70 | namespace std { 71 | template <> 72 | struct variant_size : std::variant_size { }; 73 | template 74 | struct variant_alternative : variant_alternative { }; 75 | } 76 | -------------------------------------------------------------------------------- /src/amulet/nbt/tag/abc.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | namespace Amulet { 4 | namespace NBT { 5 | 6 | class AbstractBaseTag { 7 | public: 8 | virtual ~AbstractBaseTag() { }; 9 | }; 10 | 11 | class AbstractBaseImmutableTag : public AbstractBaseTag { 12 | public: 13 | virtual ~AbstractBaseImmutableTag() { }; 14 | }; 15 | class AbstractBaseMutableTag : public AbstractBaseTag { 16 | public: 17 | virtual ~AbstractBaseMutableTag() { }; 18 | }; 19 | class AbstractBaseNumericTag : public AbstractBaseImmutableTag { 20 | public: 21 | virtual ~AbstractBaseNumericTag() { }; 22 | }; 23 | class AbstractBaseIntTag : public AbstractBaseNumericTag { 24 | public: 25 | virtual ~AbstractBaseIntTag() { }; 26 | }; 27 | class AbstractBaseFloatTag : public AbstractBaseNumericTag { 28 | public: 29 | virtual ~AbstractBaseFloatTag() { }; 30 | }; 31 | class AbstractBaseArrayTag : public AbstractBaseMutableTag { 32 | public: 33 | virtual ~AbstractBaseArrayTag() { }; 34 | }; 35 | 36 | #define FOR_EACH_LIST_TAG(MACRO) \ 37 | MACRO(1, "byte", ByteTag, Amulet::NBT::ByteTag, Amulet::NBT::ByteListTag) \ 38 | MACRO(2, "short", ShortTag, Amulet::NBT::ShortTag, Amulet::NBT::ShortListTag) \ 39 | MACRO(3, "int", IntTag, Amulet::NBT::IntTag, Amulet::NBT::IntListTag) \ 40 | MACRO(4, "long", LongTag, Amulet::NBT::LongTag, Amulet::NBT::LongListTag) \ 41 | MACRO(5, "float", FloatTag, Amulet::NBT::FloatTag, Amulet::NBT::FloatListTag) \ 42 | MACRO(6, "double", DoubleTag, Amulet::NBT::DoubleTag, Amulet::NBT::DoubleListTag) \ 43 | MACRO(7, "byte_array", ByteArrayTag, Amulet::NBT::ByteArrayTagPtr, Amulet::NBT::ByteArrayListTag)\ 44 | MACRO(8, "string", StringTag, Amulet::NBT::StringTag, Amulet::NBT::StringListTag) \ 45 | MACRO(9, "list", ListTag, Amulet::NBT::ListTagPtr, Amulet::NBT::ListListTag) \ 46 | MACRO(10, "compound", CompoundTag, Amulet::NBT::CompoundTagPtr, Amulet::NBT::CompoundListTag) \ 47 | MACRO(11, "int_array", IntArrayTag, Amulet::NBT::IntArrayTagPtr, Amulet::NBT::IntArrayListTag) \ 48 | MACRO(12, "long_array", LongArrayTag, Amulet::NBT::LongArrayTagPtr, Amulet::NBT::LongArrayListTag) 49 | 50 | #define FOR_EACH_LIST_TAG2(MACRO) \ 51 | MACRO(0, "end", std::monostate, std::monostate, std::monostate) \ 52 | FOR_EACH_LIST_TAG(MACRO) 53 | } // namespace NBT 54 | } // namespace Amulet 55 | -------------------------------------------------------------------------------- /src/amulet/nbt/tag/copy.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | 17 | #include 18 | 19 | namespace Amulet { 20 | namespace NBT { 21 | 22 | template 23 | ListTag deep_copy_list_vector(const std::vector& vec, std::set& memo) 24 | { 25 | std::vector new_vector; 26 | new_vector.reserve(vec.size()); 27 | for (const T& value : vec) { 28 | new_vector.push_back(deep_copy_2(value, memo)); 29 | } 30 | return new_vector; 31 | } 32 | 33 | ListTag deep_copy_2(const ListTag& tag, std::set& memo) 34 | { 35 | auto ptr = reinterpret_cast(&tag); 36 | if (memo.contains(ptr)) { 37 | throw std::runtime_error("ListTag cannot contain itself."); 38 | } 39 | memo.insert(ptr); 40 | auto new_tag = std::visit( 41 | [&memo](auto&& list) -> ListTag { 42 | using T = std::decay_t; 43 | if constexpr (std::is_same_v) { 44 | return ListTag(); 45 | } else if constexpr (is_shared_ptr::value) { 46 | return deep_copy_list_vector(list, memo); 47 | } else { 48 | return list; 49 | } 50 | }, 51 | tag); 52 | memo.erase(ptr); 53 | return new_tag; 54 | } 55 | 56 | CompoundTag deep_copy_2(const CompoundTag& tag, std::set& memo) 57 | { 58 | auto ptr = reinterpret_cast(&tag); 59 | if (memo.contains(ptr)) { 60 | throw std::runtime_error("CompoundTag cannot contain itself."); 61 | } 62 | memo.insert(ptr); 63 | CompoundTag new_tag; 64 | for (auto& [key, value] : tag) { 65 | new_tag.emplace(key, deep_copy_2(value, memo)); 66 | } 67 | memo.erase(ptr); 68 | return new_tag; 69 | } 70 | 71 | TagNode deep_copy_2(const TagNode& node, std::set& memo) 72 | { 73 | return std::visit( 74 | [&memo](auto&& tag) -> TagNode { 75 | return deep_copy_2(tag, memo); 76 | }, 77 | node); 78 | } 79 | 80 | NamedTag deep_copy_2(const NamedTag& named_tag, std::set& memo) 81 | { 82 | return { named_tag.name, deep_copy_2(named_tag.tag_node, memo) }; 83 | } 84 | 85 | } // namespace NBT 86 | } // namespace Amulet 87 | -------------------------------------------------------------------------------- /src/amulet/nbt/tag/float.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include 6 | #include 7 | 8 | namespace Amulet { 9 | namespace NBT { 10 | typedef float FloatTagNative; 11 | typedef double DoubleTagNative; 12 | 13 | class FloatTag : public AbstractBaseFloatTag { 14 | public: 15 | FloatTagNative value; 16 | typedef FloatTagNative native_type; 17 | FloatTag() 18 | : value() { }; 19 | FloatTag(const FloatTagNative& value) 20 | : value(value) { }; 21 | FloatTag(const FloatTag& other) 22 | : value(other.value) { }; 23 | FloatTag& operator=(const FloatTag& rhs) 24 | { 25 | value = rhs.value; 26 | return *this; 27 | }; 28 | FloatTag& operator=(const FloatTagNative& rhs) 29 | { 30 | value = rhs; 31 | return *this; 32 | }; 33 | operator const FloatTagNative&() const { return value; }; 34 | operator FloatTagNative&() { return value; }; 35 | bool operator==(const FloatTag& rhs) { return value == rhs.value; } 36 | bool operator<(const FloatTag& rhs) { return value < rhs.value; } 37 | }; 38 | 39 | class DoubleTag : public AbstractBaseFloatTag { 40 | public: 41 | DoubleTagNative value; 42 | typedef DoubleTagNative native_type; 43 | DoubleTag() 44 | : value() { }; 45 | DoubleTag(const DoubleTagNative& value) 46 | : value(value) { }; 47 | DoubleTag(const DoubleTag& other) 48 | : value(other.value) { }; 49 | DoubleTag& operator=(const DoubleTag& rhs) 50 | { 51 | value = rhs.value; 52 | return *this; 53 | }; 54 | DoubleTag& operator=(const DoubleTagNative& rhs) 55 | { 56 | value = rhs; 57 | return *this; 58 | }; 59 | operator const DoubleTagNative&() const { return value; }; 60 | operator DoubleTagNative&() { return value; }; 61 | bool operator==(const DoubleTag& rhs) { return value == rhs.value; } 62 | bool operator<(const DoubleTag& rhs) { return value < rhs.value; } 63 | }; 64 | 65 | static_assert(std::is_copy_constructible_v, "FloatTag is not copy constructible"); 66 | static_assert(std::is_copy_assignable_v, "FloatTag is not copy assignable"); 67 | static_assert(std::is_copy_constructible_v, "DoubleTag is not copy constructible"); 68 | static_assert(std::is_copy_assignable_v, "DoubleTag is not copy assignable"); 69 | 70 | template <> 71 | struct tag_id { 72 | static constexpr std::uint8_t value = 5; 73 | }; 74 | template <> 75 | struct tag_id { 76 | static constexpr std::uint8_t value = 6; 77 | }; 78 | } // namespace NBT 79 | } // namespace Amulet 80 | -------------------------------------------------------------------------------- /requirements.py: -------------------------------------------------------------------------------- 1 | import os 2 | from packaging.version import Version 3 | import get_compiler 4 | 5 | AMULET_COMPILER_TARGET_REQUIREMENT = "==2.0" 6 | 7 | PYBIND11_REQUIREMENT = "==3.0.1" 8 | AMULET_PYBIND11_EXTENSIONS_REQUIREMENT = "~=1.2.0.0a2" 9 | AMULET_IO_REQUIREMENT = "~=2.0.0.0a0" 10 | AMULET_ZLIB_REQUIREMENT = "~=1.0.8.0a0" 11 | NUMPY_REQUIREMENT = "~=2.0" 12 | 13 | if os.environ.get("AMULET_PYBIND11_EXTENSIONS_REQUIREMENT", None): 14 | AMULET_PYBIND11_EXTENSIONS_REQUIREMENT = f"{AMULET_PYBIND11_EXTENSIONS_REQUIREMENT},{os.environ['AMULET_PYBIND11_EXTENSIONS_REQUIREMENT']}" 15 | 16 | if os.environ.get("AMULET_IO_REQUIREMENT", None): 17 | AMULET_IO_REQUIREMENT = ( 18 | f"{AMULET_IO_REQUIREMENT},{os.environ['AMULET_IO_REQUIREMENT']}" 19 | ) 20 | 21 | if os.environ.get("AMULET_ZLIB_REQUIREMENT", None): 22 | AMULET_ZLIB_REQUIREMENT = ( 23 | f"{AMULET_ZLIB_REQUIREMENT},{os.environ['AMULET_ZLIB_REQUIREMENT']}" 24 | ) 25 | 26 | 27 | def get_specifier_set(version_str: str) -> str: 28 | """ 29 | version_str: The PEP 440 version number of the library. 30 | """ 31 | version = Version(version_str) 32 | if version.epoch != 0 or version.is_devrelease or version.is_postrelease: 33 | raise RuntimeError(f"Unsupported version format. {version_str}") 34 | 35 | return f"~={version.major}.{version.minor}.{version.micro}.0{''.join(map(str, version.pre or ()))}" 36 | 37 | 38 | AMULET_COMPILER_VERSION_REQUIREMENT = get_compiler.main() 39 | 40 | 41 | try: 42 | import amulet.pybind11_extensions 43 | except ImportError: 44 | pass 45 | else: 46 | AMULET_PYBIND11_EXTENSIONS_REQUIREMENT = get_specifier_set( 47 | amulet.pybind11_extensions.__version__ 48 | ) 49 | 50 | try: 51 | import amulet.io 52 | except ImportError: 53 | pass 54 | else: 55 | AMULET_IO_REQUIREMENT = get_specifier_set(amulet.io.__version__) 56 | 57 | try: 58 | import amulet.zlib 59 | except ImportError: 60 | pass 61 | else: 62 | AMULET_ZLIB_REQUIREMENT = get_specifier_set(amulet.zlib.__version__) 63 | 64 | 65 | def get_build_dependencies() -> list: 66 | return [ 67 | f"amulet-compiler-version{AMULET_COMPILER_VERSION_REQUIREMENT}", 68 | f"pybind11{PYBIND11_REQUIREMENT}", 69 | f"amulet-pybind11-extensions{AMULET_PYBIND11_EXTENSIONS_REQUIREMENT}", 70 | f"amulet-io{AMULET_IO_REQUIREMENT}", 71 | f"amulet-zlib{AMULET_ZLIB_REQUIREMENT}", 72 | ] * (not os.environ.get("AMULET_SKIP_COMPILE", None)) 73 | 74 | 75 | def get_runtime_dependencies() -> list[str]: 76 | return [ 77 | f"amulet-compiler-target{AMULET_COMPILER_TARGET_REQUIREMENT}", 78 | f"amulet-compiler-version{AMULET_COMPILER_VERSION_REQUIREMENT}", 79 | f"pybind11{PYBIND11_REQUIREMENT}", 80 | f"amulet-pybind11-extensions{AMULET_PYBIND11_EXTENSIONS_REQUIREMENT}", 81 | f"amulet-io{AMULET_IO_REQUIREMENT}", 82 | f"amulet-zlib{AMULET_ZLIB_REQUIREMENT}", 83 | f"numpy{NUMPY_REQUIREMENT}", 84 | ] 85 | -------------------------------------------------------------------------------- /src/amulet/nbt/tag/copy.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | 18 | namespace Amulet { 19 | namespace NBT { 20 | 21 | template 22 | requires std::is_same_v 23 | || std::is_same_v 24 | || std::is_same_v 25 | || std::is_same_v 26 | || std::is_same_v 27 | || std::is_same_v 28 | || std::is_same_v 29 | || std::is_same_v 30 | || std::is_same_v 31 | || std::is_same_v 32 | || std::is_same_v 33 | || std::is_same_v 34 | || std::is_same_v 35 | || std::is_same_v 36 | T shallow_copy(const T& tag) 37 | { 38 | return tag; 39 | } 40 | 41 | template 42 | std::unique_ptr shallow_copy(const std::unique_ptr& tag) 43 | { 44 | return std::make_unique(shallow_copy(*tag)); 45 | } 46 | 47 | template 48 | std::shared_ptr shallow_copy(const std::shared_ptr& tag) 49 | { 50 | return std::make_shared(shallow_copy(*tag)); 51 | } 52 | 53 | template 54 | requires std::is_same_v 55 | || std::is_same_v 56 | || std::is_same_v 57 | || std::is_same_v 58 | || std::is_same_v 59 | || std::is_same_v 60 | || std::is_same_v 61 | || std::is_same_v 62 | || std::is_same_v 63 | || std::is_same_v 64 | T deep_copy_2(const T& tag, std::set& memo) 65 | { 66 | return tag; 67 | } 68 | 69 | AMULET_NBT_EXPORT ListTag deep_copy_2(const ListTag&, std::set& memo); 70 | AMULET_NBT_EXPORT CompoundTag deep_copy_2(const CompoundTag&, std::set& memo); 71 | AMULET_NBT_EXPORT TagNode deep_copy_2(const TagNode&, std::set& memo); 72 | AMULET_NBT_EXPORT NamedTag deep_copy_2(const NamedTag&, std::set& memo); 73 | 74 | template 75 | std::unique_ptr deep_copy_2(const std::unique_ptr& tag, std::set& memo) 76 | { 77 | return std::make_unique(deep_copy_2(*tag, memo)); 78 | } 79 | 80 | template 81 | std::shared_ptr deep_copy_2(const std::shared_ptr& tag, std::set& memo) 82 | { 83 | return std::make_shared(deep_copy_2(*tag, memo)); 84 | } 85 | 86 | template 87 | auto deep_copy(const T& obj) 88 | { 89 | std::set memo; 90 | return deep_copy_2(obj, memo); 91 | } 92 | 93 | } // namespace NBT 94 | } // namespace Amulet 95 | -------------------------------------------------------------------------------- /tests/test_amulet_nbt/test_read_file/src/snbt/B1_11_level.snbt: -------------------------------------------------------------------------------- 1 | { 2 | "CenterMapsToOrigin": 0b, 3 | "ConfirmedPlatformLockedContent": 0b, 4 | "Difficulty": 2, 5 | "FlatWorldLayers": "null 6 | ", 7 | "ForceGameType": 0b, 8 | "GameType": 1, 9 | "Generator": 1, 10 | "InventoryVersion": "1.11.4", 11 | "LANBroadcast": 1b, 12 | "LANBroadcastIntent": 1b, 13 | "LastPlayed": 1562755312L, 14 | "LevelName": "DIAMONDS!!!!", 15 | "LimitedWorldOriginX": 0, 16 | "LimitedWorldOriginY": 32767, 17 | "LimitedWorldOriginZ": 4, 18 | "MinimumCompatibleClientVersion": [ 19 | 1, 20 | 11, 21 | 0, 22 | 22 23 | ], 24 | "MultiplayerGame": 1b, 25 | "MultiplayerGameIntent": 1b, 26 | "NetherScale": 8, 27 | "NetworkVersion": 354, 28 | "Platform": 2, 29 | "PlatformBroadcastIntent": 3, 30 | "RandomSeed": 3745036566L, 31 | "SpawnV1Villagers": 0b, 32 | "SpawnX": 0, 33 | "SpawnY": 32767, 34 | "SpawnZ": 4, 35 | "StorageVersion": 8, 36 | "Time": 25235L, 37 | "XBLBroadcastIntent": 3, 38 | "abilities": { 39 | "attackmobs": 1b, 40 | "attackplayers": 1b, 41 | "build": 1b, 42 | "doorsandswitches": 1b, 43 | "flySpeed": 0.05000000074505805969f, 44 | "flying": 0b, 45 | "instabuild": 0b, 46 | "invulnerable": 0b, 47 | "lightning": 0b, 48 | "mayfly": 0b, 49 | "mine": 1b, 50 | "op": 0b, 51 | "opencontainers": 1b, 52 | "permissionsLevel": 0, 53 | "playerPermissionsLevel": 1, 54 | "teleport": 0b, 55 | "walkSpeed": 0.10000000149011611938f 56 | }, 57 | "bonusChestEnabled": 0b, 58 | "bonusChestSpawned": 0b, 59 | "commandblockoutput": 1b, 60 | "commandblocksenabled": 1b, 61 | "commandsEnabled": 1b, 62 | "currentTick": 88138L, 63 | "dodaylightcycle": 0b, 64 | "doentitydrops": 1b, 65 | "dofiretick": 1b, 66 | "doimmediaterespawn": 0b, 67 | "doinsomnia": 1b, 68 | "domobloot": 1b, 69 | "domobspawning": 1b, 70 | "dotiledrops": 1b, 71 | "doweathercycle": 1b, 72 | "drowningdamage": 1b, 73 | "eduLevel": 0b, 74 | "educationFeaturesEnabled": 0b, 75 | "experimentalgameplay": 0b, 76 | "falldamage": 1b, 77 | "firedamage": 1b, 78 | "functioncommandlimit": 10000, 79 | "hasBeenLoadedInCreative": 1b, 80 | "hasLockedBehaviorPack": 0b, 81 | "hasLockedResourcePack": 0b, 82 | "immutableWorld": 0b, 83 | "isFromLockedTemplate": 0b, 84 | "isFromWorldTemplate": 0b, 85 | "isWorldTemplateOptionLocked": 0b, 86 | "keepinventory": 0b, 87 | "lastOpenedWithVersion": [ 88 | 1, 89 | 11, 90 | 4, 91 | 2 92 | ], 93 | "lightningLevel": 0.f, 94 | "lightningTime": 7878, 95 | "maxcommandchainlength": 65535, 96 | "mobgriefing": 1b, 97 | "naturalregeneration": 1b, 98 | "prid": "", 99 | "pvp": 1b, 100 | "rainLevel": 0.f, 101 | "rainTime": 138091, 102 | "randomtickspeed": 1, 103 | "requiresCopiedPackRemovalCheck": 0b, 104 | "sendcommandfeedback": 1b, 105 | "serverChunkTickRange": 10, 106 | "showcoordinates": 1b, 107 | "showdeathmessages": 1b, 108 | "spawnMobs": 1b, 109 | "startWithMapEnabled": 0b, 110 | "texturePacksRequired": 0b, 111 | "tntexplodes": 1b, 112 | "useMsaGamertagsOnly": 0b, 113 | "worldStartCount": 4294967288L 114 | } -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | import subprocess 3 | import sys 4 | from pathlib import Path 5 | import platform 6 | from tempfile import TemporaryDirectory 7 | from typing import TypeAlias, TYPE_CHECKING 8 | 9 | from setuptools import setup, Extension, Command 10 | from setuptools.command.build_ext import build_ext 11 | 12 | import versioneer 13 | 14 | import requirements 15 | 16 | 17 | def fix_path(path: str | os.PathLike[str]) -> str: 18 | return os.path.realpath(path).replace(os.sep, "/") 19 | 20 | 21 | cmdclass: dict[str, type[Command]] = versioneer.get_cmdclass() 22 | 23 | if TYPE_CHECKING: 24 | BuildExt: TypeAlias = build_ext 25 | else: 26 | BuildExt = cmdclass.get("build_ext", build_ext) 27 | 28 | 29 | class CMakeBuild(BuildExt): 30 | def build_extension(self, ext: Extension) -> None: 31 | import pybind11 32 | import amulet.pybind11_extensions 33 | import amulet.io 34 | import amulet.zlib 35 | 36 | ext_dir = ( 37 | (Path.cwd() / self.get_ext_fullpath("")).parent.resolve() / "amulet" / "nbt" 38 | ) 39 | nbt_src_dir = ( 40 | Path.cwd() / "src" / "amulet" / "nbt" if self.editable_mode else ext_dir 41 | ) 42 | 43 | platform_args = [] 44 | if sys.platform == "win32": 45 | platform_args.extend(["-G", "Visual Studio 17 2022"]) 46 | if sys.maxsize > 2**32: 47 | platform_args.extend(["-A", "x64"]) 48 | else: 49 | platform_args.extend(["-A", "Win32"]) 50 | platform_args.extend(["-T", "v143"]) 51 | elif sys.platform == "darwin": 52 | if platform.machine() == "arm64": 53 | platform_args.append("-DCMAKE_OSX_ARCHITECTURES=x86_64;arm64") 54 | 55 | if subprocess.run(["cmake", "--version"]).returncode: 56 | raise RuntimeError("Could not find cmake") 57 | with TemporaryDirectory() as tempdir: 58 | if subprocess.run( 59 | [ 60 | "cmake", 61 | *platform_args, 62 | f"-DPYTHON_EXECUTABLE={sys.executable}", 63 | f"-Dpybind11_DIR={fix_path(pybind11.get_cmake_dir())}", 64 | f"-Damulet_pybind11_extensions_DIR={fix_path(amulet.pybind11_extensions.__path__[0])}", 65 | f"-Damulet_io_DIR={fix_path(amulet.io.__path__[0])}", 66 | f"-Damulet_zlib_DIR={fix_path(amulet.zlib.__path__[0])}", 67 | f"-Damulet_nbt_DIR={fix_path(nbt_src_dir)}", 68 | f"-DAMULET_NBT_EXT_DIR={fix_path(ext_dir)}", 69 | f"-DCMAKE_INSTALL_PREFIX=install", 70 | "-B", 71 | tempdir, 72 | ] 73 | ).returncode: 74 | raise RuntimeError("Error configuring amulet-nbt") 75 | if subprocess.run( 76 | ["cmake", "--build", tempdir, "--config", "Release"] 77 | ).returncode: 78 | raise RuntimeError("Error building amulet-nbt") 79 | if subprocess.run( 80 | ["cmake", "--install", tempdir, "--config", "Release"] 81 | ).returncode: 82 | raise RuntimeError("Error installing amulet-nbt") 83 | 84 | 85 | cmdclass["build_ext"] = CMakeBuild # type: ignore 86 | 87 | 88 | setup( 89 | version=versioneer.get_version(), 90 | cmdclass=cmdclass, 91 | ext_modules=[Extension("amulet.nbt._amulet_nbt", [])] 92 | * (not os.environ.get("AMULET_SKIP_COMPILE", None)), 93 | install_requires=requirements.get_runtime_dependencies(), 94 | ) 95 | -------------------------------------------------------------------------------- /tests/test_nbt.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from copy import copy 3 | 4 | import amulet.nbt 5 | from binary_data import binary_data_tuple 6 | 7 | 8 | class NBTTests(unittest.TestCase): 9 | def _load(self, b: bytes, little_endian: bool = False) -> amulet.nbt.NamedTag: 10 | b_copy = copy(b) 11 | named_tag = amulet.nbt.read_nbt(b_copy, little_endian=little_endian) 12 | self.assertEqual(b, b_copy, msg="The buffer changed.") 13 | named_tag2 = amulet.nbt.read_nbt(b_copy, little_endian=little_endian) 14 | self.assertEqual(named_tag.name, named_tag2.name) 15 | self.assertEqual(named_tag.tag, named_tag2.tag) 16 | return named_tag 17 | 18 | def test_read_big_endian(self) -> None: 19 | for data in binary_data_tuple: 20 | self.assertEqual( 21 | data.named_tag, self._load(data.big_endian), msg=str(data.named_tag) 22 | ) 23 | 24 | def test_read_big_endian_compressed(self) -> None: 25 | for data in binary_data_tuple: 26 | self.assertEqual( 27 | data.named_tag, 28 | amulet.nbt.read_nbt(data.big_endian_compressed), 29 | msg=str(data.named_tag), 30 | ) 31 | 32 | def test_read_little_endian(self) -> None: 33 | for data in binary_data_tuple: 34 | self.assertEqual( 35 | data.named_tag, 36 | self._load(data.little_endian, little_endian=True), 37 | msg=str(data.named_tag), 38 | ) 39 | 40 | def test_read_little_endian_compressed(self) -> None: 41 | for data in binary_data_tuple: 42 | self.assertEqual( 43 | data.named_tag, 44 | amulet.nbt.read_nbt(data.little_endian_compressed, little_endian=True), 45 | msg=str(data.named_tag), 46 | ) 47 | 48 | def test_write_big_endian(self) -> None: 49 | for data in binary_data_tuple: 50 | self.assertEqual( 51 | data.named_tag.to_nbt(compressed=False), 52 | data.big_endian, 53 | msg=str(data.named_tag), 54 | ) 55 | 56 | def test_write_little_endian(self) -> None: 57 | for data in binary_data_tuple: 58 | self.assertEqual( 59 | data.named_tag.to_nbt(compressed=False, little_endian=True), 60 | data.little_endian, 61 | msg=str(data.named_tag), 62 | ) 63 | 64 | def test_unnamed(self) -> None: 65 | # Only one case is tested as the implementation of this is shared among all tag types and thus behaves the same 66 | self.assertEqual( 67 | amulet.nbt.read_nbt( 68 | b"\x01\x05", named=False, compressed=False, little_endian=False 69 | ), 70 | amulet.nbt.NamedTag(amulet.nbt.ByteTag(5), ""), 71 | "reading unnamed tag", 72 | ) 73 | self.assertEqual( 74 | amulet.nbt.read_nbt_array( 75 | b"\x01\x05\x01\x06\x01\x07", 76 | named=False, 77 | count=-1, 78 | compressed=False, 79 | little_endian=False, 80 | ), 81 | [amulet.nbt.NamedTag(amulet.nbt.ByteTag(i), "") for i in (5, 6, 7)], 82 | "reading unnamed tag array", 83 | ) 84 | self.assertEqual( 85 | amulet.nbt.ByteTag(5).to_nbt( 86 | name=None, compressed=False, little_endian=False 87 | ), 88 | b"\x01\x05", 89 | msg="writing unnamed tag", 90 | ) 91 | 92 | 93 | if __name__ == "__main__": 94 | unittest.main() 95 | -------------------------------------------------------------------------------- /tests/test_load/test_load.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import os 3 | 4 | from amulet.nbt import ( 5 | ListTag, 6 | IntTag, 7 | read_nbt, 8 | read_nbt_array, 9 | NamedTag, 10 | ) 11 | 12 | DirPath = os.path.dirname(__file__) 13 | EmptyPath = os.path.join(DirPath, "empty.nbt") 14 | OnePath = os.path.join(DirPath, "one.nbt") 15 | ArrayPath = os.path.join(DirPath, "array.nbt") 16 | 17 | 18 | class LoadTests(unittest.TestCase): 19 | def test_load(self) -> None: 20 | read_nbt_array(OnePath) 21 | 22 | with self.assertRaises(IndexError): 23 | read_nbt(EmptyPath) 24 | with self.assertRaises(IndexError): 25 | read_nbt_array(EmptyPath, count=1) 26 | self.assertEqual([], read_nbt_array(EmptyPath, count=0)) 27 | self.assertEqual([], read_nbt_array(EmptyPath, count=-1)) 28 | 29 | self.assertEqual( 30 | NamedTag(ListTag([IntTag(1), IntTag(2), IntTag(3), IntTag(4)])), 31 | read_nbt(OnePath), 32 | ) 33 | self.assertEqual( 34 | [NamedTag(ListTag([IntTag(1), IntTag(2), IntTag(3), IntTag(4)]))], 35 | read_nbt_array(OnePath), 36 | ) 37 | self.assertEqual( 38 | [NamedTag(ListTag([IntTag(1), IntTag(2), IntTag(3), IntTag(4)]))], 39 | read_nbt_array(OnePath, count=1), 40 | ) 41 | self.assertEqual( 42 | [NamedTag(ListTag([IntTag(1), IntTag(2), IntTag(3), IntTag(4)]))], 43 | read_nbt_array(OnePath, count=-1), 44 | ) 45 | with self.assertRaises(IndexError): 46 | read_nbt_array(OnePath, count=2) 47 | 48 | self.assertEqual( 49 | NamedTag(ListTag([IntTag(1), IntTag(2), IntTag(3), IntTag(4)])), 50 | read_nbt(ArrayPath), 51 | ) 52 | self.assertEqual( 53 | [NamedTag(ListTag([IntTag(1), IntTag(2), IntTag(3), IntTag(4)]))], 54 | read_nbt_array(ArrayPath), 55 | ) 56 | self.assertEqual( 57 | [NamedTag(ListTag([IntTag(1), IntTag(2), IntTag(3), IntTag(4)]))], 58 | read_nbt_array(ArrayPath, count=1), 59 | ) 60 | self.assertEqual( 61 | [ 62 | NamedTag(ListTag([IntTag(1), IntTag(2), IntTag(3), IntTag(4)])), 63 | NamedTag(ListTag([IntTag(1), IntTag(2), IntTag(3), IntTag(4)])), 64 | ], 65 | read_nbt_array(ArrayPath, count=2), 66 | ) 67 | self.assertEqual( 68 | [ 69 | NamedTag(ListTag([IntTag(1), IntTag(2), IntTag(3), IntTag(4)])), 70 | NamedTag(ListTag([IntTag(1), IntTag(2), IntTag(3), IntTag(4)])), 71 | NamedTag(ListTag([IntTag(1), IntTag(2), IntTag(3), IntTag(4)])), 72 | NamedTag(ListTag([IntTag(1), IntTag(2), IntTag(3), IntTag(4)])), 73 | NamedTag(ListTag([IntTag(1), IntTag(2), IntTag(3), IntTag(4)])), 74 | ], 75 | read_nbt_array(ArrayPath, count=5), 76 | ) 77 | self.assertEqual( 78 | [ 79 | NamedTag(ListTag([IntTag(1), IntTag(2), IntTag(3), IntTag(4)])), 80 | NamedTag(ListTag([IntTag(1), IntTag(2), IntTag(3), IntTag(4)])), 81 | NamedTag(ListTag([IntTag(1), IntTag(2), IntTag(3), IntTag(4)])), 82 | NamedTag(ListTag([IntTag(1), IntTag(2), IntTag(3), IntTag(4)])), 83 | NamedTag(ListTag([IntTag(1), IntTag(2), IntTag(3), IntTag(4)])), 84 | ], 85 | read_nbt_array(ArrayPath, count=-1), 86 | ) 87 | with self.assertRaises(IndexError): 88 | read_nbt_array(ArrayPath, count=6) 89 | 90 | 91 | if __name__ == "__main__": 92 | unittest.main() 93 | -------------------------------------------------------------------------------- /tests/test_amulet_nbt/test_read_file/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | from typing import Callable, TypeVar, Sequence, TypeAlias 3 | import unittest 4 | from amulet.nbt import CompoundTag, read_nbt, read_snbt, NamedTag, AbstractBaseTag 5 | 6 | DATA_DIR = os.path.join(os.path.dirname(__file__), "src") 7 | 8 | EncodedT = TypeVar("EncodedT") 9 | # Group name, decoder, encoder 10 | GroupType: TypeAlias = tuple[ 11 | str, Callable[[EncodedT], NamedTag], Callable[[NamedTag], EncodedT] 12 | ] 13 | 14 | 15 | class FileNBTTests(unittest.TestCase): 16 | def setUp(self) -> None: 17 | self.maxDiff = None 18 | 19 | def _open_snbt(path_: str) -> NamedTag: 20 | if os.path.isfile(path_): 21 | with open(path_, encoding="utf-8") as f_: 22 | snbt = f_.read() 23 | else: 24 | snbt = path_ 25 | return NamedTag(read_snbt(snbt)) 26 | 27 | self._groups: Sequence[GroupType] = ( 28 | ( 29 | "big_endian_compressed_nbt", 30 | lambda path_: read_nbt(path_, compressed=True, little_endian=False), 31 | lambda obj_: obj_.save_to(compressed=True, little_endian=False), 32 | ), 33 | ( 34 | "big_endian_nbt", 35 | lambda path_: read_nbt(path_, compressed=False, little_endian=False), 36 | lambda obj_: obj_.save_to(compressed=False, little_endian=False), 37 | ), 38 | ( 39 | "little_endian_nbt", 40 | lambda path_: read_nbt(path_, compressed=False, little_endian=True), 41 | lambda obj_: obj_.save_to(compressed=False, little_endian=True), 42 | ), 43 | ( 44 | "snbt", 45 | _open_snbt, 46 | lambda obj_: obj_.tag.to_snbt(" "), 47 | ), 48 | ) 49 | 50 | def test_load_nbt(self) -> None: 51 | # read in all the data 52 | group_data: dict[str, dict[str, NamedTag]] = {} 53 | for group1, load1, _ in self._groups: 54 | for path1 in os.listdir(os.path.join(DATA_DIR, group1)): 55 | name = os.path.splitext(path1)[0] 56 | try: 57 | data = load1(os.path.join(DATA_DIR, group1, path1)) 58 | except Exception as e_: 59 | print(group1, path1) 60 | raise e_ 61 | self.assertIsInstance(data, NamedTag) 62 | self.assertIsInstance(data.tag, CompoundTag) 63 | group_data.setdefault(name, {})[group1] = data 64 | 65 | # compare to all the other loaded data 66 | for name, name_data in group_data.items(): 67 | for group1, _, _ in self._groups: 68 | for group2, _, _ in self._groups: 69 | if group1 == "snbt" or group2 == "snbt": 70 | self.assertEqual( 71 | name_data[group1].tag, 72 | name_data[group2].tag, 73 | f"{group1} {group2} {name}", 74 | ) 75 | else: 76 | self.assertEqual( 77 | name_data[group1], 78 | name_data[group2], 79 | f"{group1} {group2} {name}", 80 | ) 81 | 82 | for name, name_data in group_data.items(): 83 | for group1, load, save in self._groups: 84 | data = name_data[group1] 85 | self.assertEqual(data, load(save(data)), f"{group1} {name}") 86 | 87 | 88 | if __name__ == "__main__": 89 | unittest.main() 90 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.13) 2 | 3 | project(amulet_nbt LANGUAGES CXX) 4 | 5 | set(amulet_nbt_DIR ${CMAKE_CURRENT_LIST_DIR}/src/amulet/nbt CACHE PATH "") 6 | set(BUILD_AMULET_NBT_TESTS OFF CACHE BOOL "Should tests be built?") 7 | 8 | # Set C++20 9 | set(CMAKE_CXX_STANDARD 20) 10 | set(CMAKE_CXX_STANDARD_REQUIRED ON) 11 | set(CMAKE_CXX_EXTENSIONS OFF) 12 | 13 | # Set platform variables 14 | if (WIN32) 15 | # set windows 7 as the minimum version 16 | add_definitions(-D_WIN32_WINNT=0x0601) 17 | elseif(APPLE) 18 | set(CMAKE_OSX_DEPLOYMENT_TARGET "10.15") 19 | elseif(UNIX) 20 | set(CMAKE_POSITION_INDEPENDENT_CODE ON) 21 | else() 22 | message( FATAL_ERROR "Unsupported platform. Please submit a pull request to support this platform." ) 23 | endif() 24 | 25 | if (MSVC) 26 | add_definitions("/MP") 27 | endif() 28 | 29 | # Find libraries 30 | if (NOT TARGET pybind11::module) 31 | find_package(pybind11 CONFIG REQUIRED) 32 | endif() 33 | if (NOT TARGET amulet_pybind11_extensions) 34 | find_package(amulet_pybind11_extensions CONFIG REQUIRED) 35 | endif() 36 | if (NOT TARGET amulet_io) 37 | find_package(amulet_io CONFIG REQUIRED) 38 | endif() 39 | if (NOT TARGET amulet_zlib) 40 | find_package(amulet_zlib CONFIG REQUIRED) 41 | endif() 42 | 43 | # Find C++ files 44 | file(REAL_PATH src SOURCE_PATH) 45 | file(GLOB_RECURSE EXTENSION_SOURCES LIST_DIRECTORIES false ${SOURCE_PATH}/amulet/*.py.cpp) 46 | file(GLOB_RECURSE EXTENSION_HEADERS LIST_DIRECTORIES false ${SOURCE_PATH}/amulet/*.py.hpp) 47 | file(GLOB_RECURSE SOURCES LIST_DIRECTORIES false ${SOURCE_PATH}/amulet/*.cpp) 48 | file(GLOB_RECURSE HEADERS LIST_DIRECTORIES false ${SOURCE_PATH}/amulet/*.hpp) 49 | list(REMOVE_ITEM SOURCES ${EXTENSION_SOURCES}) 50 | list(REMOVE_ITEM HEADERS ${EXTENSION_HEADERS}) 51 | 52 | # Add implementation 53 | add_library(amulet_nbt SHARED) 54 | set_target_properties(amulet_nbt PROPERTIES FOLDER "CPP") 55 | target_compile_definitions(amulet_nbt PRIVATE ExportAmuletNBT) 56 | target_link_libraries(amulet_nbt PUBLIC amulet_io) 57 | target_include_directories(amulet_nbt PUBLIC ${SOURCE_PATH}) 58 | target_sources(amulet_nbt PRIVATE ${SOURCES} ${HEADERS}) 59 | foreach(FILE ${SOURCES} ${HEADERS}) 60 | file(RELATIVE_PATH REL_PATH ${SOURCE_PATH} ${FILE}) 61 | get_filename_component(GROUP ${REL_PATH} DIRECTORY) 62 | string(REPLACE "/" "\\" GROUP ${GROUP}) 63 | source_group(${GROUP} FILES ${FILE}) 64 | endforeach() 65 | 66 | # Add python extension 67 | pybind11_add_module(_amulet_nbt) 68 | set_target_properties(_amulet_nbt PROPERTIES FOLDER "Python") 69 | target_link_libraries(_amulet_nbt PRIVATE amulet_pybind11_extensions) 70 | target_link_libraries(_amulet_nbt PRIVATE amulet_zlib) 71 | target_link_libraries(_amulet_nbt PRIVATE amulet_nbt) 72 | target_compile_definitions(_amulet_nbt PRIVATE PYBIND11_DETAILED_ERROR_MESSAGES) 73 | target_compile_definitions(_amulet_nbt PRIVATE PYBIND11_VERSION="${pybind11_VERSION}") 74 | target_compile_definitions(_amulet_nbt PRIVATE COMPILER_ID="${CMAKE_CXX_COMPILER_ID}") 75 | target_compile_definitions(_amulet_nbt PRIVATE COMPILER_VERSION="${CMAKE_CXX_COMPILER_VERSION}") 76 | target_sources(_amulet_nbt PRIVATE ${EXTENSION_SOURCES} ${EXTENSION_HEADERS}) 77 | foreach(FILE ${EXTENSION_SOURCES} ${EXTENSION_HEADERS}) 78 | file(RELATIVE_PATH REL_PATH ${SOURCE_PATH} ${FILE}) 79 | get_filename_component(GROUP ${REL_PATH} DIRECTORY) 80 | string(REPLACE "/" "\\" GROUP ${GROUP}) 81 | source_group(${GROUP} FILES ${FILE}) 82 | endforeach() 83 | 84 | if(NOT DEFINED AMULET_NBT_EXT_DIR) 85 | set(AMULET_NBT_EXT_DIR ${amulet_nbt_DIR}) 86 | endif() 87 | 88 | # Install 89 | install(TARGETS amulet_nbt DESTINATION ${amulet_nbt_DIR}) 90 | install(TARGETS _amulet_nbt DESTINATION ${AMULET_NBT_EXT_DIR}) 91 | 92 | if (BUILD_AMULET_NBT_TESTS) 93 | add_subdirectory(tests) 94 | endif() 95 | -------------------------------------------------------------------------------- /tests/test_amulet_nbt/test_tag/test_abc.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | from typing import Any 3 | import faulthandler 4 | 5 | faulthandler.enable() 6 | 7 | from amulet.nbt import ( 8 | AbstractBaseTag, 9 | AbstractBaseNumericTag, 10 | AbstractBaseIntTag, 11 | AbstractBaseFloatTag, 12 | AbstractBaseArrayTag, 13 | ByteTag, 14 | ShortTag, 15 | IntTag, 16 | LongTag, 17 | FloatTag, 18 | DoubleTag, 19 | StringTag, 20 | ByteArrayTag, 21 | ListTag, 22 | CompoundTag, 23 | IntArrayTag, 24 | LongArrayTag, 25 | ) 26 | 27 | 28 | TagNameMap = { 29 | ByteTag: "byte", 30 | ShortTag: "short", 31 | IntTag: "int", 32 | LongTag: "long", 33 | FloatTag: "float", 34 | DoubleTag: "double", 35 | StringTag: "string", 36 | ListTag: "list", 37 | CompoundTag: "compound", 38 | ByteArrayTag: "byte_array", 39 | IntArrayTag: "int_array", 40 | LongArrayTag: "long_array", 41 | } 42 | 43 | 44 | class AbstractBaseTestCase(ABC): 45 | int_types: tuple[type[AbstractBaseIntTag], ...] = ( 46 | ByteTag, 47 | ShortTag, 48 | IntTag, 49 | LongTag, 50 | ) 51 | float_types: tuple[type[AbstractBaseFloatTag], ...] = ( 52 | FloatTag, 53 | DoubleTag, 54 | ) 55 | numerical_types: tuple[type[AbstractBaseNumericTag], ...] = int_types + float_types 56 | string_types: tuple[type[StringTag]] = (StringTag,) 57 | array_types: tuple[type[AbstractBaseArrayTag], ...] = ( 58 | ByteArrayTag, 59 | IntArrayTag, 60 | LongArrayTag, 61 | ) 62 | container_types: tuple[type[ListTag | CompoundTag]] = ( 63 | ListTag, 64 | CompoundTag, 65 | ) 66 | nbt_types: tuple[type[AbstractBaseTag], ...] = ( 67 | numerical_types + string_types + array_types + container_types 68 | ) 69 | 70 | not_nbt: tuple[Any, ...] = (None, True, False, 0, 0.0, "str", [], {}, set()) 71 | 72 | @abstractmethod 73 | def test_constructor(self) -> None: 74 | raise NotImplementedError 75 | 76 | @abstractmethod 77 | def test_equal(self) -> None: 78 | raise NotImplementedError 79 | 80 | @abstractmethod 81 | def test_repr(self) -> None: 82 | raise NotImplementedError 83 | 84 | @abstractmethod 85 | def test_str(self) -> None: 86 | raise NotImplementedError 87 | 88 | @abstractmethod 89 | def test_pickle(self) -> None: 90 | raise NotImplementedError 91 | 92 | @abstractmethod 93 | def test_copy(self) -> None: 94 | raise NotImplementedError 95 | 96 | @abstractmethod 97 | def test_deepcopy(self) -> None: 98 | raise NotImplementedError 99 | 100 | @abstractmethod 101 | def test_hash(self) -> None: 102 | raise NotImplementedError 103 | 104 | @abstractmethod 105 | def test_instance(self) -> None: 106 | raise NotImplementedError 107 | 108 | @abstractmethod 109 | def test_to_nbt(self) -> None: 110 | raise NotImplementedError 111 | 112 | @abstractmethod 113 | def test_from_nbt(self) -> None: 114 | raise NotImplementedError 115 | 116 | @abstractmethod 117 | def test_to_snbt(self) -> None: 118 | raise NotImplementedError 119 | 120 | @abstractmethod 121 | def test_from_snbt(self) -> None: 122 | raise NotImplementedError 123 | 124 | 125 | class AbstractBaseTagTestCase(AbstractBaseTestCase): 126 | @abstractmethod 127 | def test_py_data(self) -> None: 128 | raise NotImplementedError 129 | 130 | 131 | class AbstractBaseImmutableTagTestCase(AbstractBaseTagTestCase): 132 | pass 133 | 134 | 135 | class AbstractBaseMutableTagTestCase(AbstractBaseTagTestCase): 136 | pass 137 | -------------------------------------------------------------------------------- /tests/test_amulet_nbt/test_string_encoding/test_utf8.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from amulet.nbt import mutf8_encoding, utf8_encoding 3 | 4 | 5 | class TestUTF(unittest.TestCase): 6 | def test_range(self) -> None: 7 | """For every valid UTF-8 character try encoding and decoding.""" 8 | for i in range(1114112): 9 | c = chr(i) 10 | try: 11 | utf8 = c.encode() 12 | except Exception: 13 | pass 14 | else: 15 | self.assertEqual(utf8_encoding.encode(utf8), utf8) 16 | self.assertEqual(utf8_encoding.decode(utf8), utf8) 17 | mutf8 = mutf8_encoding.encode(utf8) 18 | if 0 < i < 65536: 19 | self.assertEqual(mutf8, utf8) 20 | else: 21 | self.assertEqual(utf8, mutf8_encoding.decode(mutf8)) 22 | 23 | def test_powers(self) -> None: 24 | for v, mutf8_true_bin in ( 25 | (0, [0b11000000, 0b10000000]), 26 | (1, [0b00000001]), 27 | (2, [0b00000010]), 28 | (4, [0b00000100]), 29 | (8, [0b00001000]), 30 | (16, [0b00010000]), 31 | (32, [0b00100000]), 32 | (64, [0b01000000]), 33 | (128, [0b11000010, 0b10000000]), 34 | (256, [0b11000100, 0b10000000]), 35 | (512, [0b11001000, 0b10000000]), 36 | (1024, [0b11010000, 0b10000000]), 37 | (2048, [0b11100000, 0b10100000, 0b10000000]), 38 | (4096, [0b11100001, 0b10000000, 0b10000000]), 39 | (8192, [0b11100010, 0b10000000, 0b10000000]), 40 | (16384, [0b11100100, 0b10000000, 0b10000000]), 41 | (32768, [0b11101000, 0b10000000, 0b10000000]), 42 | ( 43 | 65536, 44 | [ 45 | 0b11101101, 46 | 0b10100000, 47 | 0b10000000, 48 | 0b11101101, 49 | 0b10110000, 50 | 0b10000000, 51 | ], 52 | ), 53 | ( 54 | 131072, 55 | [ 56 | 0b11101101, 57 | 0b10100001, 58 | 0b10000000, 59 | 0b11101101, 60 | 0b10110000, 61 | 0b10000000, 62 | ], 63 | ), 64 | ( 65 | 262144, 66 | [ 67 | 0b11101101, 68 | 0b10100011, 69 | 0b10000000, 70 | 0b11101101, 71 | 0b10110000, 72 | 0b10000000, 73 | ], 74 | ), 75 | ( 76 | 524288, 77 | [ 78 | 0b11101101, 79 | 0b10100111, 80 | 0b10000000, 81 | 0b11101101, 82 | 0b10110000, 83 | 0b10000000, 84 | ], 85 | ), 86 | ( 87 | 1048576, 88 | [ 89 | 0b11101101, 90 | 0b10101111, 91 | 0b10000000, 92 | 0b11101101, 93 | 0b10110000, 94 | 0b10000000, 95 | ], 96 | ), 97 | ): 98 | mutf8_true = bytes(mutf8_true_bin) 99 | with self.subTest(f"Character {v}"): 100 | utf8 = chr(v).encode() 101 | self.assertEqual(mutf8_encoding.encode(utf8), mutf8_true) 102 | self.assertEqual(mutf8_encoding.decode(mutf8_true), utf8) 103 | 104 | 105 | if __name__ == "__main__": 106 | unittest.main() 107 | -------------------------------------------------------------------------------- /src/amulet/nbt/tag/array.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | #include 9 | #include 10 | 11 | namespace Amulet { 12 | namespace NBT { 13 | template 14 | class ArrayTagTemplate : private std::vector, public AbstractBaseArrayTag { 15 | static_assert( 16 | std::is_same_v || std::is_same_v || std::is_same_v, 17 | "T must be int 8, 32 or 64"); 18 | 19 | public: 20 | // only methods that do not change the buffer size should be exposed here 21 | using std::vector::vector; 22 | 23 | // Member types 24 | using typename std::vector::value_type; 25 | using typename std::vector::size_type; 26 | using typename std::vector::difference_type; 27 | using typename std::vector::iterator; 28 | using typename std::vector::const_iterator; 29 | using typename std::vector::reverse_iterator; 30 | using typename std::vector::const_reverse_iterator; 31 | 32 | // Element access 33 | using std::vector::at; 34 | using std::vector::operator[]; 35 | using std::vector::front; 36 | using std::vector::back; 37 | using std::vector::data; 38 | 39 | // Iterators 40 | using std::vector::begin; 41 | using std::vector::cbegin; 42 | using std::vector::end; 43 | using std::vector::cend; 44 | using std::vector::rbegin; 45 | using std::vector::crbegin; 46 | using std::vector::rend; 47 | using std::vector::crend; 48 | 49 | // Capacity 50 | using std::vector::empty; 51 | using std::vector::size; 52 | 53 | bool operator==(const ArrayTagTemplate& other) const 54 | { 55 | return static_cast&>(*this) == static_cast&>(other); 56 | } 57 | 58 | // Cast 59 | explicit operator std::vector() const 60 | { 61 | return static_cast&>(*this); 62 | }; 63 | }; 64 | 65 | typedef ArrayTagTemplate ByteArrayTag; 66 | typedef ArrayTagTemplate IntArrayTag; 67 | typedef ArrayTagTemplate LongArrayTag; 68 | 69 | static_assert(std::is_copy_constructible_v, "ByteArrayTag is not copy constructible"); 70 | static_assert(std::is_copy_assignable_v, "ByteArrayTag is not copy assignable"); 71 | static_assert(std::is_copy_constructible_v, "IntArrayTag is not copy constructible"); 72 | static_assert(std::is_copy_assignable_v, "IntArrayTag is not copy assignable"); 73 | static_assert(std::is_copy_constructible_v, "LongArrayTag is not copy constructible"); 74 | static_assert(std::is_copy_assignable_v, "LongArrayTag is not copy assignable"); 75 | 76 | typedef std::shared_ptr ByteArrayTagPtr; 77 | typedef std::shared_ptr IntArrayTagPtr; 78 | typedef std::shared_ptr LongArrayTagPtr; 79 | 80 | template <> 81 | struct tag_id { 82 | static constexpr std::uint8_t value = 7; 83 | }; 84 | template <> 85 | struct tag_id { 86 | static constexpr std::uint8_t value = 7; 87 | }; 88 | template <> 89 | struct tag_id { 90 | static constexpr std::uint8_t value = 11; 91 | }; 92 | template <> 93 | struct tag_id { 94 | static constexpr std::uint8_t value = 11; 95 | }; 96 | template <> 97 | struct tag_id { 98 | static constexpr std::uint8_t value = 12; 99 | }; 100 | template <> 101 | struct tag_id { 102 | static constexpr std::uint8_t value = 12; 103 | }; 104 | } // namespace NBT 105 | } // namespace Amulet 106 | -------------------------------------------------------------------------------- /src/amulet/nbt/tag/eq.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | 18 | namespace Amulet { 19 | namespace NBT { 20 | bool NBTTag_eq(const ByteTag& a, const ByteTag& b) { return a == b; }; 21 | bool NBTTag_eq(const ShortTag& a, const ShortTag& b) { return a == b; }; 22 | bool NBTTag_eq(const IntTag& a, const IntTag& b) { return a == b; }; 23 | bool NBTTag_eq(const LongTag& a, const LongTag& b) { return a == b; }; 24 | bool NBTTag_eq(const FloatTag& a, const FloatTag& b) { return a == b; }; 25 | bool NBTTag_eq(const DoubleTag& a, const DoubleTag& b) { return a == b; }; 26 | bool NBTTag_eq(const StringTag& a, const StringTag& b) { return a == b; }; 27 | bool NBTTag_eq(const ByteArrayTag& a, const ByteArrayTag& b) { return a == b; }; 28 | bool NBTTag_eq(const IntArrayTag& a, const IntArrayTag& b) { return a == b; }; 29 | bool NBTTag_eq(const LongArrayTag& a, const LongArrayTag& b) { return a == b; }; 30 | 31 | template 32 | inline bool ListTag_eq(const std::vector& a_vec, const ListTag& b) 33 | { 34 | if (!std::holds_alternative>(b)) { 35 | return a_vec.size() == 0 && ListTag_size(b) == 0; 36 | } 37 | const std::vector& b_vec = std::get>(b); 38 | 39 | if constexpr (is_shared_ptr::value) { 40 | // Values are shared pointers 41 | if (a_vec.size() != b_vec.size()) { 42 | return false; 43 | } 44 | for (size_t i = 0; i < a_vec.size(); i++) { 45 | if (!NBTTag_eq(a_vec[i], b_vec[i])) { 46 | return false; 47 | }; 48 | } 49 | return true; 50 | } else { 51 | // Vector of non-pointers 52 | return a_vec == b_vec; 53 | } 54 | } 55 | bool NBTTag_eq(const ListTag& a, const ListTag& b) 56 | { 57 | return std::visit([&b](auto&& list) -> bool { 58 | using T = std::decay_t; 59 | if constexpr (std::is_same_v) { 60 | return ListTag_size(b) == 0; 61 | } else { 62 | return ListTag_eq(list, b); 63 | } 64 | }, 65 | a); 66 | }; 67 | bool NBTTag_eq(const CompoundTag& a, const CompoundTag& b) 68 | { 69 | if (a.size() != b.size()) { 70 | // Size does not match 71 | return false; 72 | } 73 | for (auto& [key, value] : a) { 74 | auto it = b.find(key); 75 | if (it == b.end()) { 76 | // Key not in b 77 | return false; 78 | } 79 | if (!NBTTag_eq(value, it->second)) { 80 | // Value does not match 81 | return false; 82 | } 83 | } 84 | return true; 85 | }; 86 | bool NBTTag_eq(const TagNode& a, const TagNode& b) 87 | { 88 | return std::visit([&b](auto&& tag) -> bool { 89 | using T = std::decay_t; 90 | if (!std::holds_alternative(b)) { 91 | return false; 92 | } 93 | if constexpr (is_shared_ptr::value) { 94 | return NBTTag_eq(*tag, *std::get(b)); 95 | } else { 96 | return NBTTag_eq(tag, std::get(b)); 97 | } 98 | }, 99 | a); 100 | }; 101 | bool NBTTag_eq(const NamedTag& a, const NamedTag& b) 102 | { 103 | return a.name == b.name && NBTTag_eq(a.tag_node, b.tag_node); 104 | }; 105 | } // namespace NBT 106 | } // namespace Amulet 107 | -------------------------------------------------------------------------------- /src/amulet/nbt/nbt_encoding/binary.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | #include 20 | #include 21 | 22 | namespace Amulet { 23 | namespace NBT { 24 | AMULET_NBT_EXPORT NamedTag decode_nbt(BinaryReader& reader, bool named = true); 25 | AMULET_NBT_EXPORT NamedTag decode_nbt(std::string_view, std::endian, Amulet::StringDecoder, size_t& offset, bool named = true); 26 | AMULET_NBT_EXPORT NamedTag decode_nbt(std::string_view, std::endian, Amulet::StringDecoder, bool named = true); 27 | AMULET_NBT_EXPORT std::vector decode_nbt_array(std::string_view, std::endian, Amulet::StringDecoder, size_t& offset, bool named = true); 28 | AMULET_NBT_EXPORT std::vector decode_nbt_array(std::string_view, std::endian, Amulet::StringDecoder, size_t& offset, size_t count, bool named); 29 | 30 | AMULET_NBT_EXPORT void encode_nbt(BaseBinaryWriter&, const std::optional& name, const ByteTag&); 31 | AMULET_NBT_EXPORT void encode_nbt(BaseBinaryWriter&, const std::optional& name, const ShortTag&); 32 | AMULET_NBT_EXPORT void encode_nbt(BaseBinaryWriter&, const std::optional& name, const IntTag&); 33 | AMULET_NBT_EXPORT void encode_nbt(BaseBinaryWriter&, const std::optional& name, const LongTag&); 34 | AMULET_NBT_EXPORT void encode_nbt(BaseBinaryWriter&, const std::optional& name, const FloatTag&); 35 | AMULET_NBT_EXPORT void encode_nbt(BaseBinaryWriter&, const std::optional& name, const DoubleTag&); 36 | AMULET_NBT_EXPORT void encode_nbt(BaseBinaryWriter&, const std::optional& name, const ByteArrayTag&); 37 | AMULET_NBT_EXPORT void encode_nbt(BaseBinaryWriter&, const std::optional& name, const StringTag&); 38 | AMULET_NBT_EXPORT void encode_nbt(BaseBinaryWriter&, const std::optional& name, const ListTag&); 39 | AMULET_NBT_EXPORT void encode_nbt(BaseBinaryWriter&, const std::optional& name, const CompoundTag&); 40 | AMULET_NBT_EXPORT void encode_nbt(BaseBinaryWriter&, const std::optional& name, const IntArrayTag&); 41 | AMULET_NBT_EXPORT void encode_nbt(BaseBinaryWriter&, const std::optional& name, const LongArrayTag&); 42 | AMULET_NBT_EXPORT void encode_nbt(BaseBinaryWriter&, const std::string& name, const TagNode&); 43 | AMULET_NBT_EXPORT void encode_nbt(BaseBinaryWriter&, const NamedTag& tag); 44 | 45 | AMULET_NBT_EXPORT std::string encode_nbt(const std::optional& name, const ByteTag&, std::endian, Amulet::StringEncoder); 46 | AMULET_NBT_EXPORT std::string encode_nbt(const std::optional& name, const ShortTag&, std::endian, Amulet::StringEncoder); 47 | AMULET_NBT_EXPORT std::string encode_nbt(const std::optional& name, const IntTag&, std::endian, Amulet::StringEncoder); 48 | AMULET_NBT_EXPORT std::string encode_nbt(const std::optional& name, const LongTag&, std::endian, Amulet::StringEncoder); 49 | AMULET_NBT_EXPORT std::string encode_nbt(const std::optional& name, const FloatTag&, std::endian, Amulet::StringEncoder); 50 | AMULET_NBT_EXPORT std::string encode_nbt(const std::optional& name, const DoubleTag&, std::endian, Amulet::StringEncoder); 51 | AMULET_NBT_EXPORT std::string encode_nbt(const std::optional& name, const ByteArrayTag&, std::endian, Amulet::StringEncoder); 52 | AMULET_NBT_EXPORT std::string encode_nbt(const std::optional& name, const StringTag&, std::endian, Amulet::StringEncoder); 53 | AMULET_NBT_EXPORT std::string encode_nbt(const std::optional& name, const ListTag&, std::endian, Amulet::StringEncoder); 54 | AMULET_NBT_EXPORT std::string encode_nbt(const std::optional& name, const CompoundTag&, std::endian, Amulet::StringEncoder); 55 | AMULET_NBT_EXPORT std::string encode_nbt(const std::optional& name, const IntArrayTag&, std::endian, Amulet::StringEncoder); 56 | AMULET_NBT_EXPORT std::string encode_nbt(const std::optional& name, const LongArrayTag&, std::endian, Amulet::StringEncoder); 57 | AMULET_NBT_EXPORT std::string encode_nbt(const std::string& name, const TagNode&, std::endian, Amulet::StringEncoder); 58 | AMULET_NBT_EXPORT std::string encode_nbt(const NamedTag& tag, std::endian, Amulet::StringEncoder); 59 | } // namespace NBT 60 | } // namespace Amulet 61 | -------------------------------------------------------------------------------- /docs_source/getting_started.rst: -------------------------------------------------------------------------------- 1 | ######################################## 2 | Getting Started Guide 3 | ######################################## 4 | 5 | .. code-block:: python 6 | :linenos: 7 | 8 | # Import the nbt library 9 | # Option 1 - import the package then refer to attributes on the package 10 | import amulet.nbt 11 | 12 | # Option 2 - import attributes from the package 13 | from amulet.nbt import ( 14 | ByteTag, 15 | ShortTag, 16 | IntTag, 17 | LongTag, 18 | FloatTag, 19 | DoubleTag, 20 | StringTag, 21 | ListTag, 22 | CompoundTag, 23 | ByteArrayTag, 24 | IntArrayTag, 25 | LongArrayTag, 26 | NamedTag, 27 | mutf8_encoding, 28 | utf8_encoding, 29 | utf8_escape_encoding, 30 | java_encoding, 31 | bedrock_encoding, 32 | ) 33 | 34 | # These classes are also available under their old names 35 | # from amulet.nbt import ( 36 | # TAG_Byte, 37 | # TAG_Short, 38 | # TAG_Int, 39 | # TAG_Long, 40 | # TAG_Float, 41 | # TAG_Double, 42 | # TAG_String, 43 | # TAG_List, 44 | # TAG_Compound, 45 | # TAG_Byte_Array, 46 | # TAG_Int_Array, 47 | # TAG_Long_Array 48 | # ) 49 | 50 | named_tag: NamedTag 51 | java_named_tag: NamedTag 52 | bedrock_named_tag: NamedTag 53 | 54 | # Load binary NBT 55 | named_tag = amulet.nbt.read_nbt( 56 | "the/path/to/your/binary/nbt/file", 57 | preset=java_encoding, 58 | compressed=True, # These inputs must be specified as keyword inputs like this. 59 | ) # from a file 60 | named_tag = amulet.nbt.read_nbt( 61 | "the/path/to/your/binary/nbt/file", 62 | compressed=True, # These inputs must be specified as keyword inputs like this. 63 | little_endian=False, # If you do not define them they will default to these values 64 | string_encoding=mutf8_encoding 65 | ) # from a file 66 | named_tag = amulet.nbt.read_nbt(b'') # from a bytes object 67 | 68 | # Note that Java Edition usually uses compressed modified UTF-8. 69 | java_named_tag = amulet.nbt.read_nbt( 70 | "the/path/to/your/binary/java/nbt/file", 71 | string_encoding=mutf8_encoding 72 | ) 73 | 74 | # Bedrock edition data is stored in little endian format and uses non-compressed UTF-8 but can also have arbitrary bytes. 75 | bedrock_named_tag = amulet.nbt.read_nbt( 76 | "the/path/to/your/binary/bedrock/nbt/file", 77 | preset=bedrock_encoding, 78 | compressed=False, 79 | ) 80 | bedrock_named_tag = amulet.nbt.read_nbt( 81 | "the/path/to/your/binary/bedrock/nbt/file", 82 | compressed=False, 83 | little_endian=True, 84 | string_encoding=utf8_escape_encoding # This decoder will escape all invalid bytes to the string ␛xHH 85 | ) 86 | 87 | # Save the data back to a file 88 | named_tag.save_to( 89 | "the/path/to/write/to", 90 | compressed=True, # These inputs must be specified as keyword inputs like this. 91 | preset=java_encoding 92 | ) 93 | named_tag.save_to( 94 | "the/path/to/write/to", 95 | compressed=True, # These inputs must be specified as keyword inputs like this. 96 | little_endian=False, # If you do not define them they will default to these values 97 | string_encoding=mutf8_encoding 98 | ) 99 | 100 | # save_to can also be given a file object to write to. 101 | with open('filepath', 'wb') as f: 102 | named_tag.save_to(f) 103 | 104 | # Like earlier you will need to give the correct options for the platform you are using. 105 | # Java 106 | java_named_tag.save_to( 107 | "the/path/to/write/to", 108 | preset=java_encoding 109 | ) 110 | 111 | # Bedrock 112 | bedrock_named_tag.save_to( 113 | "the/path/to/write/to", 114 | compressed=False, 115 | preset=bedrock_encoding 116 | ) 117 | 118 | 119 | # You can also parse the stringified NBT format used in Java commands. 120 | tag = amulet.nbt.read_snbt('{key1: "value", key2: 0b, key3: 0.0f}') 121 | # tag should look like this 122 | # TAG_Compound( 123 | # key1: TAG_String("value"), 124 | # key2: TAG_Byte(0) 125 | # key3: TAG_Float(0.0) 126 | # ) 127 | 128 | # Tags can be saved like the NamedTag class but they do not have a name. 129 | tag.save_to( 130 | 'filepath', 131 | # see the NamedTag save_to documentation above for other options. 132 | name="" # Tag classes do not store their name so you can define it here. 133 | ) 134 | tag.to_snbt() # convert back to SNBT 135 | 136 | # The classes can also be constructed manually like this 137 | tag = CompoundTag({ 138 | "key1": ByteTag(0), # if no input value is given it will automatically fill these defaults 139 | "key2": ShortTag(0), 140 | "key3": IntTag(0), 141 | "key4": LongTag(0), 142 | "key5": FloatTag(0.0), 143 | "key6": DoubleTag(0.0), 144 | "key7": ByteArrayTag([]), 145 | "key8": StringTag(""), 146 | "key9": ListTag([]), 147 | "key10": CompoundTag({}), 148 | "key11": IntArrayTag([]), 149 | "key12": LongArrayTag([]) 150 | }) 151 | 152 | named_tag = NamedTag( 153 | tag, 154 | name="" # Optional name input. 155 | ) 156 | -------------------------------------------------------------------------------- /src/amulet/nbt/nbt_encoding/string.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | 15 | namespace Amulet { 16 | namespace NBT { 17 | AMULET_NBT_EXPORT void encode_snbt(std::string&, const TagNode&); 18 | AMULET_NBT_EXPORT void encode_snbt(std::string&, const ByteTag&); 19 | AMULET_NBT_EXPORT void encode_snbt(std::string&, const ShortTag&); 20 | AMULET_NBT_EXPORT void encode_snbt(std::string&, const IntTag&); 21 | AMULET_NBT_EXPORT void encode_snbt(std::string&, const LongTag&); 22 | AMULET_NBT_EXPORT void encode_snbt(std::string&, const FloatTag&); 23 | AMULET_NBT_EXPORT void encode_snbt(std::string&, const DoubleTag&); 24 | AMULET_NBT_EXPORT void encode_snbt(std::string&, const ByteArrayTag&); 25 | AMULET_NBT_EXPORT void encode_snbt(std::string&, const StringTag&); 26 | AMULET_NBT_EXPORT void encode_snbt(std::string&, const ListTag&); 27 | AMULET_NBT_EXPORT void encode_snbt(std::string&, const CompoundTag&); 28 | AMULET_NBT_EXPORT void encode_snbt(std::string&, const IntArrayTag&); 29 | AMULET_NBT_EXPORT void encode_snbt(std::string&, const LongArrayTag&); 30 | 31 | AMULET_NBT_EXPORT std::string encode_snbt(const TagNode&); 32 | AMULET_NBT_EXPORT std::string encode_snbt(const ByteTag&); 33 | AMULET_NBT_EXPORT std::string encode_snbt(const ShortTag&); 34 | AMULET_NBT_EXPORT std::string encode_snbt(const IntTag&); 35 | AMULET_NBT_EXPORT std::string encode_snbt(const LongTag&); 36 | AMULET_NBT_EXPORT std::string encode_snbt(const FloatTag&); 37 | AMULET_NBT_EXPORT std::string encode_snbt(const DoubleTag&); 38 | AMULET_NBT_EXPORT std::string encode_snbt(const ByteArrayTag&); 39 | AMULET_NBT_EXPORT std::string encode_snbt(const StringTag&); 40 | AMULET_NBT_EXPORT std::string encode_snbt(const ListTag&); 41 | AMULET_NBT_EXPORT std::string encode_snbt(const CompoundTag&); 42 | AMULET_NBT_EXPORT std::string encode_snbt(const IntArrayTag&); 43 | AMULET_NBT_EXPORT std::string encode_snbt(const LongArrayTag&); 44 | 45 | // Multi-line variants 46 | AMULET_NBT_EXPORT void encode_formatted_snbt(std::string&, const TagNode&, const std::string& indent); 47 | AMULET_NBT_EXPORT void encode_formatted_snbt(std::string&, const ByteTag&, const std::string& indent); 48 | AMULET_NBT_EXPORT void encode_formatted_snbt(std::string&, const ShortTag&, const std::string& indent); 49 | AMULET_NBT_EXPORT void encode_formatted_snbt(std::string&, const IntTag&, const std::string& indent); 50 | AMULET_NBT_EXPORT void encode_formatted_snbt(std::string&, const LongTag&, const std::string& indent); 51 | AMULET_NBT_EXPORT void encode_formatted_snbt(std::string&, const FloatTag&, const std::string& indent); 52 | AMULET_NBT_EXPORT void encode_formatted_snbt(std::string&, const DoubleTag&, const std::string& indent); 53 | AMULET_NBT_EXPORT void encode_formatted_snbt(std::string&, const ByteArrayTag&, const std::string& indent); 54 | AMULET_NBT_EXPORT void encode_formatted_snbt(std::string&, const StringTag&, const std::string& indent); 55 | AMULET_NBT_EXPORT void encode_formatted_snbt(std::string&, const ListTag&, const std::string& indent); 56 | AMULET_NBT_EXPORT void encode_formatted_snbt(std::string&, const CompoundTag&, const std::string& indent); 57 | AMULET_NBT_EXPORT void encode_formatted_snbt(std::string&, const IntArrayTag&, const std::string& indent); 58 | AMULET_NBT_EXPORT void encode_formatted_snbt(std::string&, const LongArrayTag&, const std::string& indent); 59 | 60 | AMULET_NBT_EXPORT std::string encode_formatted_snbt(const TagNode&, const std::string& indent); 61 | AMULET_NBT_EXPORT std::string encode_formatted_snbt(const ByteTag&, const std::string& indent); 62 | AMULET_NBT_EXPORT std::string encode_formatted_snbt(const ShortTag&, const std::string& indent); 63 | AMULET_NBT_EXPORT std::string encode_formatted_snbt(const IntTag&, const std::string& indent); 64 | AMULET_NBT_EXPORT std::string encode_formatted_snbt(const LongTag&, const std::string& indent); 65 | AMULET_NBT_EXPORT std::string encode_formatted_snbt(const FloatTag&, const std::string& indent); 66 | AMULET_NBT_EXPORT std::string encode_formatted_snbt(const DoubleTag&, const std::string& indent); 67 | AMULET_NBT_EXPORT std::string encode_formatted_snbt(const ByteArrayTag&, const std::string& indent); 68 | AMULET_NBT_EXPORT std::string encode_formatted_snbt(const StringTag&, const std::string& indent); 69 | AMULET_NBT_EXPORT std::string encode_formatted_snbt(const ListTag&, const std::string& indent); 70 | AMULET_NBT_EXPORT std::string encode_formatted_snbt(const CompoundTag&, const std::string& indent); 71 | AMULET_NBT_EXPORT std::string encode_formatted_snbt(const IntArrayTag&, const std::string& indent); 72 | AMULET_NBT_EXPORT std::string encode_formatted_snbt(const LongArrayTag&, const std::string& indent); 73 | 74 | AMULET_NBT_EXPORT TagNode decode_snbt(const CodePointVector& snbt); 75 | AMULET_NBT_EXPORT TagNode decode_snbt(std::string_view snbt); 76 | } // namespace NBT 77 | } // namespace Amulet 78 | -------------------------------------------------------------------------------- /src/amulet/nbt/tag/int.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | #include 7 | #include 8 | 9 | namespace Amulet { 10 | namespace NBT { 11 | typedef std::int8_t ByteTagNative; 12 | typedef std::int16_t ShortTagNative; 13 | typedef std::int32_t IntTagNative; 14 | typedef std::int64_t LongTagNative; 15 | 16 | class ByteTag : public AbstractBaseIntTag { 17 | public: 18 | ByteTagNative value; 19 | typedef ByteTagNative native_type; 20 | ByteTag() 21 | : value() { }; 22 | ByteTag(const ByteTagNative& value) 23 | : value(value) { }; 24 | ByteTag(const ByteTag& other) 25 | : value(other.value) { }; 26 | ByteTag& operator=(const ByteTag& rhs) 27 | { 28 | value = rhs.value; 29 | return *this; 30 | }; 31 | ByteTag& operator=(const ByteTagNative& rhs) 32 | { 33 | value = rhs; 34 | return *this; 35 | }; 36 | operator const ByteTagNative&() const { return value; }; 37 | operator ByteTagNative&() { return value; }; 38 | bool operator==(const ByteTag& rhs) { return value == rhs.value; } 39 | bool operator<(const ByteTag& rhs) { return value < rhs.value; } 40 | }; 41 | 42 | class ShortTag : public AbstractBaseIntTag { 43 | public: 44 | ShortTagNative value; 45 | typedef ShortTagNative native_type; 46 | ShortTag() 47 | : value() { }; 48 | ShortTag(const ShortTagNative& value) 49 | : value(value) { }; 50 | ShortTag(const ShortTag& other) 51 | : value(other.value) { }; 52 | ShortTag& operator=(const ShortTag& rhs) 53 | { 54 | value = rhs.value; 55 | return *this; 56 | }; 57 | ShortTag& operator=(const ShortTagNative& rhs) 58 | { 59 | value = rhs; 60 | return *this; 61 | }; 62 | operator const ShortTagNative&() const { return value; }; 63 | operator ShortTagNative&() { return value; }; 64 | bool operator==(const ShortTag& rhs) { return value == rhs.value; } 65 | bool operator<(const ShortTag& rhs) { return value < rhs.value; } 66 | }; 67 | 68 | class IntTag : public AbstractBaseIntTag { 69 | public: 70 | IntTagNative value; 71 | typedef IntTagNative native_type; 72 | IntTag() 73 | : value() { }; 74 | IntTag(const IntTagNative& value) 75 | : value(value) { }; 76 | IntTag(const IntTag& other) 77 | : value(other.value) { }; 78 | IntTag& operator=(const IntTag& rhs) 79 | { 80 | value = rhs.value; 81 | return *this; 82 | }; 83 | IntTag& operator=(const IntTagNative& rhs) 84 | { 85 | value = rhs; 86 | return *this; 87 | }; 88 | operator const IntTagNative&() const { return value; }; 89 | operator IntTagNative&() { return value; }; 90 | bool operator==(const IntTag& rhs) { return value == rhs.value; } 91 | bool operator<(const IntTag& rhs) { return value < rhs.value; } 92 | }; 93 | 94 | class LongTag : public AbstractBaseIntTag { 95 | public: 96 | LongTagNative value; 97 | typedef LongTagNative native_type; 98 | LongTag() 99 | : value() { }; 100 | LongTag(const LongTagNative& value) 101 | : value(value) { }; 102 | LongTag(const LongTag& other) 103 | : value(other.value) { }; 104 | LongTag& operator=(const LongTag& rhs) 105 | { 106 | value = rhs.value; 107 | return *this; 108 | }; 109 | LongTag& operator=(const LongTagNative& rhs) 110 | { 111 | value = rhs; 112 | return *this; 113 | }; 114 | operator const LongTagNative&() const { return value; }; 115 | operator LongTagNative&() { return value; }; 116 | bool operator==(const LongTag& rhs) { return value == rhs.value; } 117 | bool operator<(const LongTag& rhs) { return value < rhs.value; } 118 | }; 119 | 120 | static_assert(std::is_copy_constructible_v, "ByteTag is not copy constructible"); 121 | static_assert(std::is_copy_assignable_v, "ByteTag is not copy assignable"); 122 | static_assert(std::is_copy_constructible_v, "ShortTag is not copy constructible"); 123 | static_assert(std::is_copy_assignable_v, "ShortTag is not copy assignable"); 124 | static_assert(std::is_copy_constructible_v, "IntTag is not copy constructible"); 125 | static_assert(std::is_copy_assignable_v, "IntTag is not copy assignable"); 126 | static_assert(std::is_copy_constructible_v, "LongTag is not copy constructible"); 127 | static_assert(std::is_copy_assignable_v, "LongTag is not copy assignable"); 128 | 129 | template <> 130 | struct tag_id { 131 | static constexpr std::uint8_t value = 1; 132 | }; 133 | template <> 134 | struct tag_id { 135 | static constexpr std::uint8_t value = 2; 136 | }; 137 | template <> 138 | struct tag_id { 139 | static constexpr std::uint8_t value = 3; 140 | }; 141 | template <> 142 | struct tag_id { 143 | static constexpr std::uint8_t value = 4; 144 | }; 145 | } // namespace NBT 146 | } // namespace Amulet 147 | -------------------------------------------------------------------------------- /tests/test_amulet_nbt/test_read_file/src/snbt/bigtest.snbt: -------------------------------------------------------------------------------- 1 | { 2 | "longTest": 9223372036854775807L, 3 | "shortTest": 32767s, 4 | "stringTest": "HELLO WORLD THIS IS A TEST STRING ÅÄÖ!", 5 | "floatTest": 0.49823147058486938477f, 6 | "intTest": 2147483647, 7 | "nested compound test": { 8 | "ham": { 9 | "name": "Hampus", 10 | "value": 0.75f 11 | }, 12 | "egg": { 13 | "name": "Eggbert", 14 | "value": 0.5f 15 | } 16 | }, 17 | "listTest (long)": [ 18 | 11L, 19 | 12L, 20 | 13L, 21 | 14L, 22 | 15L 23 | ], 24 | "listTest (compound)": [ 25 | { 26 | "name": "Compound tag #0", 27 | "created-on": 1264099775885L 28 | }, 29 | { 30 | "name": "Compound tag #1", 31 | "created-on": 1264099775885L 32 | } 33 | ], 34 | "byteTest": 127b, 35 | "byteArrayTest (the first 1000 values of (n*n*255+n*7)%100, starting with n=0 (0, 62, 34, 16, 8, ...))": [B;0B, 62B, 34B, 16B, 8B, 10B, 22B, 44B, 76B, 18B, 70B, 32B, 4B, 86B, 78B, 80B, 92B, 14B, 46B, 88B, 40B, 2B, 74B, 56B, 48B, 50B, 62B, 84B, 16B, 58B, 10B, 72B, 44B, 26B, 18B, 20B, 32B, 54B, 86B, 28B, 80B, 42B, 14B, 96B, 88B, 90B, 2B, 24B, 56B, 98B, 50B, 12B, 84B, 66B, 58B, 60B, 72B, 94B, 26B, 68B, 20B, 82B, 54B, 36B, 28B, 30B, 42B, 64B, 96B, 38B, 90B, 52B, 24B, 6B, 98B, 0B, 12B, 34B, 66B, 8B, 60B, 22B, 94B, 76B, 68B, 70B, 82B, 4B, 36B, 78B, 30B, 92B, 64B, 46B, 38B, 40B, 52B, 74B, 6B, 48B, 0B, 62B, 34B, 16B, 8B, 10B, 22B, 44B, 76B, 18B, 70B, 32B, 4B, 86B, 78B, 80B, 92B, 14B, 46B, 88B, 40B, 2B, 74B, 56B, 48B, 50B, 62B, 84B, 16B, 58B, 10B, 72B, 44B, 26B, 18B, 20B, 32B, 54B, 86B, 28B, 80B, 42B, 14B, 96B, 88B, 90B, 2B, 24B, 56B, 98B, 50B, 12B, 84B, 66B, 58B, 60B, 72B, 94B, 26B, 68B, 20B, 82B, 54B, 36B, 28B, 30B, 42B, 64B, 96B, 38B, 90B, 52B, 24B, 6B, 98B, 0B, 12B, 34B, 66B, 8B, 60B, 22B, 94B, 76B, 68B, 70B, 82B, 4B, 36B, 78B, 30B, 92B, 64B, 46B, 38B, 40B, 52B, 74B, 6B, 48B, 0B, 62B, 34B, 16B, 8B, 10B, 22B, 44B, 76B, 18B, 70B, 32B, 4B, 86B, 78B, 80B, 92B, 14B, 46B, 88B, 40B, 2B, 74B, 56B, 48B, 50B, 62B, 84B, 16B, 58B, 10B, 72B, 44B, 26B, 18B, 20B, 32B, 54B, 86B, 28B, 80B, 42B, 14B, 96B, 88B, 90B, 2B, 24B, 56B, 98B, 50B, 12B, 84B, 66B, 58B, 60B, 72B, 94B, 26B, 68B, 20B, 82B, 54B, 36B, 28B, 30B, 42B, 64B, 96B, 38B, 90B, 52B, 24B, 6B, 98B, 0B, 12B, 34B, 66B, 8B, 60B, 22B, 94B, 76B, 68B, 70B, 82B, 4B, 36B, 78B, 30B, 92B, 64B, 46B, 38B, 40B, 52B, 74B, 6B, 48B, 0B, 62B, 34B, 16B, 8B, 10B, 22B, 44B, 76B, 18B, 70B, 32B, 4B, 86B, 78B, 80B, 92B, 14B, 46B, 88B, 40B, 2B, 74B, 56B, 48B, 50B, 62B, 84B, 16B, 58B, 10B, 72B, 44B, 26B, 18B, 20B, 32B, 54B, 86B, 28B, 80B, 42B, 14B, 96B, 88B, 90B, 2B, 24B, 56B, 98B, 50B, 12B, 84B, 66B, 58B, 60B, 72B, 94B, 26B, 68B, 20B, 82B, 54B, 36B, 28B, 30B, 42B, 64B, 96B, 38B, 90B, 52B, 24B, 6B, 98B, 0B, 12B, 34B, 66B, 8B, 60B, 22B, 94B, 76B, 68B, 70B, 82B, 4B, 36B, 78B, 30B, 92B, 64B, 46B, 38B, 40B, 52B, 74B, 6B, 48B, 0B, 62B, 34B, 16B, 8B, 10B, 22B, 44B, 76B, 18B, 70B, 32B, 4B, 86B, 78B, 80B, 92B, 14B, 46B, 88B, 40B, 2B, 74B, 56B, 48B, 50B, 62B, 84B, 16B, 58B, 10B, 72B, 44B, 26B, 18B, 20B, 32B, 54B, 86B, 28B, 80B, 42B, 14B, 96B, 88B, 90B, 2B, 24B, 56B, 98B, 50B, 12B, 84B, 66B, 58B, 60B, 72B, 94B, 26B, 68B, 20B, 82B, 54B, 36B, 28B, 30B, 42B, 64B, 96B, 38B, 90B, 52B, 24B, 6B, 98B, 0B, 12B, 34B, 66B, 8B, 60B, 22B, 94B, 76B, 68B, 70B, 82B, 4B, 36B, 78B, 30B, 92B, 64B, 46B, 38B, 40B, 52B, 74B, 6B, 48B, 0B, 62B, 34B, 16B, 8B, 10B, 22B, 44B, 76B, 18B, 70B, 32B, 4B, 86B, 78B, 80B, 92B, 14B, 46B, 88B, 40B, 2B, 74B, 56B, 48B, 50B, 62B, 84B, 16B, 58B, 10B, 72B, 44B, 26B, 18B, 20B, 32B, 54B, 86B, 28B, 80B, 42B, 14B, 96B, 88B, 90B, 2B, 24B, 56B, 98B, 50B, 12B, 84B, 66B, 58B, 60B, 72B, 94B, 26B, 68B, 20B, 82B, 54B, 36B, 28B, 30B, 42B, 64B, 96B, 38B, 90B, 52B, 24B, 6B, 98B, 0B, 12B, 34B, 66B, 8B, 60B, 22B, 94B, 76B, 68B, 70B, 82B, 4B, 36B, 78B, 30B, 92B, 64B, 46B, 38B, 40B, 52B, 74B, 6B, 48B, 0B, 62B, 34B, 16B, 8B, 10B, 22B, 44B, 76B, 18B, 70B, 32B, 4B, 86B, 78B, 80B, 92B, 14B, 46B, 88B, 40B, 2B, 74B, 56B, 48B, 50B, 62B, 84B, 16B, 58B, 10B, 72B, 44B, 26B, 18B, 20B, 32B, 54B, 86B, 28B, 80B, 42B, 14B, 96B, 88B, 90B, 2B, 24B, 56B, 98B, 50B, 12B, 84B, 66B, 58B, 60B, 72B, 94B, 26B, 68B, 20B, 82B, 54B, 36B, 28B, 30B, 42B, 64B, 96B, 38B, 90B, 52B, 24B, 6B, 98B, 0B, 12B, 34B, 66B, 8B, 60B, 22B, 94B, 76B, 68B, 70B, 82B, 4B, 36B, 78B, 30B, 92B, 64B, 46B, 38B, 40B, 52B, 74B, 6B, 48B, 0B, 62B, 34B, 16B, 8B, 10B, 22B, 44B, 76B, 18B, 70B, 32B, 4B, 86B, 78B, 80B, 92B, 14B, 46B, 88B, 40B, 2B, 74B, 56B, 48B, 50B, 62B, 84B, 16B, 58B, 10B, 72B, 44B, 26B, 18B, 20B, 32B, 54B, 86B, 28B, 80B, 42B, 14B, 96B, 88B, 90B, 2B, 24B, 56B, 98B, 50B, 12B, 84B, 66B, 58B, 60B, 72B, 94B, 26B, 68B, 20B, 82B, 54B, 36B, 28B, 30B, 42B, 64B, 96B, 38B, 90B, 52B, 24B, 6B, 98B, 0B, 12B, 34B, 66B, 8B, 60B, 22B, 94B, 76B, 68B, 70B, 82B, 4B, 36B, 78B, 30B, 92B, 64B, 46B, 38B, 40B, 52B, 74B, 6B, 48B, 0B, 62B, 34B, 16B, 8B, 10B, 22B, 44B, 76B, 18B, 70B, 32B, 4B, 86B, 78B, 80B, 92B, 14B, 46B, 88B, 40B, 2B, 74B, 56B, 48B, 50B, 62B, 84B, 16B, 58B, 10B, 72B, 44B, 26B, 18B, 20B, 32B, 54B, 86B, 28B, 80B, 42B, 14B, 96B, 88B, 90B, 2B, 24B, 56B, 98B, 50B, 12B, 84B, 66B, 58B, 60B, 72B, 94B, 26B, 68B, 20B, 82B, 54B, 36B, 28B, 30B, 42B, 64B, 96B, 38B, 90B, 52B, 24B, 6B, 98B, 0B, 12B, 34B, 66B, 8B, 60B, 22B, 94B, 76B, 68B, 70B, 82B, 4B, 36B, 78B, 30B, 92B, 64B, 46B, 38B, 40B, 52B, 74B, 6B, 48B, 0B, 62B, 34B, 16B, 8B, 10B, 22B, 44B, 76B, 18B, 70B, 32B, 4B, 86B, 78B, 80B, 92B, 14B, 46B, 88B, 40B, 2B, 74B, 56B, 48B, 50B, 62B, 84B, 16B, 58B, 10B, 72B, 44B, 26B, 18B, 20B, 32B, 54B, 86B, 28B, 80B, 42B, 14B, 96B, 88B, 90B, 2B, 24B, 56B, 98B, 50B, 12B, 84B, 66B, 58B, 60B, 72B, 94B, 26B, 68B, 20B, 82B, 54B, 36B, 28B, 30B, 42B, 64B, 96B, 38B, 90B, 52B, 24B, 6B, 98B, 0B, 12B, 34B, 66B, 8B, 60B, 22B, 94B, 76B, 68B, 70B, 82B, 4B, 36B, 78B, 30B, 92B, 64B, 46B, 38B, 40B, 52B, 74B, 6B, 48B], 36 | "doubleTest": 0.49312871321823148474d 37 | } -------------------------------------------------------------------------------- /src/amulet/nbt/tag/string.py.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | #include 6 | #include 7 | #include 8 | 9 | #include 10 | 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | 20 | namespace py = pybind11; 21 | 22 | void init_string(py::module& m) 23 | { 24 | py::object mutf8_encoding = m.attr("mutf8_encoding"); 25 | py::object java_encoding = m.attr("java_encoding"); 26 | 27 | py::classh StringTag(m, "StringTag", 28 | "A class that behaves like a string."); 29 | StringTag.def_property_readonly_static("tag_id", [](py::object) { return 8; }); 30 | StringTag.def( 31 | py::init([](py::object value) { 32 | if (py::isinstance(value)) { 33 | return value.cast(); 34 | } else if (py::isinstance(value) || py::isinstance(value)) { 35 | return Amulet::NBT::StringTag(value.cast()); 36 | } else { 37 | return Amulet::NBT::StringTag(py::str(value).cast()); 38 | } 39 | }), 40 | py::arg("value") = "", 41 | py::doc("__init__(self: amulet.nbt.StringTag, value: str | bytes) -> None")); 42 | StringTag.def_property_readonly( 43 | "py_str", 44 | [](const Amulet::NBT::StringTag& self) -> std::string { 45 | return self; 46 | }, 47 | py::doc( 48 | "The data stored in the class as a python string.\n" 49 | "\n" 50 | "In some rare cases the data cannot be decoded to a string and this will raise a UnicodeDecodeError.")); 51 | StringTag.def_property_readonly( 52 | "py_bytes", 53 | [](const Amulet::NBT::StringTag& self) { 54 | return py::bytes(self); 55 | }, 56 | py::doc( 57 | "The bytes stored in the class.")); 58 | StringTag.def_property_readonly( 59 | "py_str_or_bytes", 60 | [](const Amulet::NBT::StringTag& self) -> std::variant { 61 | try { 62 | return py::str(self); 63 | } catch (py::error_already_set&) { 64 | return py::bytes(self); 65 | } 66 | }, 67 | py::doc("If the payload is UTF-8 returns a string else returns bytes.")); 68 | StringTag.def_property_readonly( 69 | "py_data", 70 | [](const Amulet::NBT::StringTag& self) { 71 | return py::bytes(self); 72 | }, 73 | py::doc( 74 | "A python representation of the class. Note that the return type is undefined and may change in the future.\n" 75 | "\n" 76 | "You would be better off using the py_{type} or np_array properties if you require a fixed type.\n" 77 | "This is here for convenience to get a python representation under the same property name.\n")); 78 | SerialiseTag(StringTag) 79 | StringTag.def( 80 | "__repr__", 81 | [](const Amulet::NBT::StringTag& self) { 82 | try { 83 | return "StringTag(" + py::repr(py::str(self)).cast() + ")"; 84 | } catch (py::error_already_set&) { 85 | return "StringTag(" + py::repr(py::bytes(self)).cast() + ")"; 86 | } 87 | }); 88 | StringTag.def( 89 | "__str__", 90 | [](const Amulet::NBT::StringTag& self) -> std::string { 91 | return self; 92 | }); 93 | StringTag.def( 94 | "__bytes__", 95 | [](const Amulet::NBT::StringTag& self) { 96 | return py::bytes(self); 97 | }); 98 | StringTag.def( 99 | py::pickle( 100 | [](const Amulet::NBT::StringTag& self) { 101 | return py::bytes(self); 102 | }, 103 | [](py::bytes state) { 104 | return Amulet::NBT::StringTag(state); 105 | })); 106 | StringTag.def( 107 | "__copy__", 108 | [](const Amulet::NBT::StringTag& self) { 109 | return shallow_copy(self); 110 | }); 111 | StringTag.def( 112 | "__deepcopy__", 113 | [](const Amulet::NBT::StringTag& self, py::dict) { 114 | return deep_copy(self); 115 | }, 116 | py::arg("memo")); 117 | StringTag.def( 118 | "__hash__", 119 | [](const Amulet::NBT::StringTag& self) { 120 | return py::hash(py::make_tuple(8, py::bytes(self))); 121 | }); 122 | StringTag.def( 123 | "__bool__", 124 | [](const Amulet::NBT::StringTag& self) { 125 | return !self.empty(); 126 | }); 127 | StringTag.def( 128 | "__eq__", 129 | [](const Amulet::NBT::StringTag& self, const Amulet::NBT::StringTag& other) { 130 | return self == other; 131 | }, 132 | py::is_operator()); 133 | StringTag.def( 134 | "__ge__", 135 | [](const Amulet::NBT::StringTag& self, const Amulet::NBT::StringTag& other) { 136 | return self >= other; 137 | }, 138 | py::is_operator()); 139 | StringTag.def( 140 | "__gt__", 141 | [](const Amulet::NBT::StringTag& self, const Amulet::NBT::StringTag& other) { 142 | return self > other; 143 | }, 144 | py::is_operator()); 145 | StringTag.def( 146 | "__le__", 147 | [](const Amulet::NBT::StringTag& self, const Amulet::NBT::StringTag& other) { 148 | return self <= other; 149 | }, 150 | py::is_operator()); 151 | StringTag.def( 152 | "__lt__", 153 | [](const Amulet::NBT::StringTag& self, const Amulet::NBT::StringTag& other) { 154 | return self < other; 155 | }, 156 | py::is_operator()); 157 | } 158 | -------------------------------------------------------------------------------- /docs_source/index.rst: -------------------------------------------------------------------------------- 1 | ######################################## 2 | Welcome to Amulet NBT's documentation! 3 | ######################################## 4 | 5 | .. toctree:: 6 | :maxdepth: 4 7 | :caption: Contents: 8 | 9 | getting_started 10 | 11 | 12 | .. inheritance-diagram:: amulet.nbt.ByteTag 13 | amulet.nbt.ShortTag 14 | amulet.nbt.IntTag 15 | amulet.nbt.LongTag 16 | amulet.nbt.FloatTag 17 | amulet.nbt.DoubleTag 18 | amulet.nbt.StringTag 19 | amulet.nbt.ListTag 20 | amulet.nbt.CompoundTag 21 | amulet.nbt.ByteArrayTag 22 | amulet.nbt.IntArrayTag 23 | amulet.nbt.LongArrayTag 24 | amulet.nbt.NamedTag 25 | :top-classes: collections.abc.MutableSequence, collections.abc.MutableMapping 26 | :parts: 1 27 | 28 | 29 | ############# 30 | Tag Classes 31 | ############# 32 | 33 | .. autoclass:: amulet.nbt.ByteTag 34 | :members: 35 | :inherited-members: 36 | :undoc-members: 37 | :special-members: 38 | :show-inheritance: 39 | :member-order: bysource 40 | 41 | .. autoclass:: amulet.nbt.ShortTag 42 | :members: 43 | :inherited-members: 44 | :undoc-members: 45 | :special-members: 46 | :show-inheritance: 47 | 48 | .. autoclass:: amulet.nbt.IntTag 49 | :members: 50 | :inherited-members: 51 | :undoc-members: 52 | :special-members: 53 | :show-inheritance: 54 | 55 | .. autoclass:: amulet.nbt.LongTag 56 | :members: 57 | :inherited-members: 58 | :undoc-members: 59 | :special-members: 60 | :show-inheritance: 61 | 62 | .. autoclass:: amulet.nbt.FloatTag 63 | :members: 64 | :inherited-members: 65 | :undoc-members: 66 | :special-members: 67 | :show-inheritance: 68 | 69 | .. autoclass:: amulet.nbt.DoubleTag 70 | :members: 71 | :inherited-members: 72 | :undoc-members: 73 | :special-members: 74 | :show-inheritance: 75 | 76 | .. autoclass:: amulet.nbt.StringTag 77 | :members: 78 | :inherited-members: 79 | :undoc-members: 80 | :special-members: 81 | :show-inheritance: 82 | 83 | .. autoclass:: amulet.nbt.ListTag 84 | :members: 85 | :inherited-members: 86 | :undoc-members: 87 | :special-members: 88 | :show-inheritance: 89 | 90 | .. autoclass:: amulet.nbt.CompoundTag 91 | :members: 92 | :inherited-members: 93 | :undoc-members: 94 | :special-members: 95 | :show-inheritance: 96 | 97 | .. autoclass:: amulet.nbt.ByteArrayTag 98 | :members: 99 | :inherited-members: 100 | :undoc-members: 101 | :special-members: 102 | :show-inheritance: 103 | 104 | .. autoclass:: amulet.nbt.IntArrayTag 105 | :members: 106 | :inherited-members: 107 | :undoc-members: 108 | :special-members: 109 | :show-inheritance: 110 | 111 | .. autoclass:: amulet.nbt.LongArrayTag 112 | :members: 113 | :inherited-members: 114 | :undoc-members: 115 | :special-members: 116 | :show-inheritance: 117 | 118 | 119 | ########################################## 120 | :class:`amulet.nbt.NamedTag` class 121 | ########################################## 122 | 123 | .. autoclass:: amulet.nbt.NamedTag 124 | :members: 125 | :inherited-members: 126 | :undoc-members: 127 | :special-members: 128 | 129 | 130 | ####################### 131 | Abstract Base Classes 132 | ####################### 133 | 134 | .. autoclass:: amulet.nbt.AbstractBaseTag 135 | :members: 136 | :inherited-members: 137 | :undoc-members: 138 | :special-members: 139 | 140 | .. autoclass:: amulet.nbt.AbstractBaseImmutableTag 141 | :members: 142 | :inherited-members: 143 | :undoc-members: 144 | :special-members: 145 | :show-inheritance: 146 | 147 | .. autoclass:: amulet.nbt.AbstractBaseMutableTag 148 | :members: 149 | :inherited-members: 150 | :undoc-members: 151 | :special-members: 152 | :show-inheritance: 153 | 154 | .. autoclass:: amulet.nbt.AbstractBaseNumericTag 155 | :members: 156 | :inherited-members: 157 | :undoc-members: 158 | :special-members: 159 | :show-inheritance: 160 | 161 | .. autoclass:: amulet.nbt.AbstractBaseIntTag 162 | :members: 163 | :inherited-members: 164 | :undoc-members: 165 | :special-members: 166 | :show-inheritance: 167 | 168 | .. autoclass:: amulet.nbt.AbstractBaseFloatTag 169 | :members: 170 | :inherited-members: 171 | :undoc-members: 172 | :special-members: 173 | :show-inheritance: 174 | 175 | .. autoclass:: amulet.nbt.AbstractBaseArrayTag 176 | :members: 177 | :inherited-members: 178 | :undoc-members: 179 | :special-members: 180 | :show-inheritance: 181 | 182 | 183 | ########## 184 | load nbt 185 | ########## 186 | 187 | These are functions to load the binary and stringified NBT formats. 188 | 189 | .. autofunction:: amulet.nbt.read_nbt 190 | .. autofunction:: amulet.nbt.read_nbt_array 191 | .. autoclass:: amulet.nbt.ReadOffset 192 | :members: 193 | .. autofunction:: amulet.nbt.read_snbt 194 | 195 | 196 | ################# 197 | String Encoding 198 | ################# 199 | 200 | These are instances of a class storing C++ functions to encode and decode strings. 201 | 202 | .. autoclass:: amulet.nbt.StringEncoding 203 | :members: 204 | :inherited-members: 205 | :undoc-members: 206 | 207 | They can be passed to the string_encoding argument in to_nbt, save_to, read_nbt and read_nbt_array to control the string encoding behaviour. 208 | 209 | The usual string encoding scheme is called UTF-8. 210 | 211 | .. autodata:: amulet.nbt.utf8_encoding 212 | 213 | Bedrock Edition uses UTF-8 to encode strings but has been known to store non-UTF-8 byte sequences in TAG_String fields. 214 | amulet.nbt.utf8_escape_encoding will escape invalid UTF-8 bytes as ␛xHH 215 | 216 | .. autodata:: amulet.nbt.utf8_escape_encoding 217 | 218 | Java Edition uses a modified version of UTF-8 implemented by the Java programming language. 219 | 220 | .. autodata:: amulet.nbt.mutf8_encoding 221 | 222 | 223 | ################## 224 | Encoding Presets 225 | ################## 226 | 227 | The string encoding and endianness can be defined separately but for simplicity the following presets have been defined. 228 | 229 | .. autodata:: amulet.nbt.java_encoding 230 | .. autodata:: amulet.nbt.bedrock_encoding 231 | 232 | 233 | #################### 234 | Indices and tables 235 | #################### 236 | 237 | - :ref:`genindex` 238 | -------------------------------------------------------------------------------- /tests/test_amulet_nbt/test_read_file/src/snbt/J1_12_level.snbt: -------------------------------------------------------------------------------- 1 | { 2 | "Data": { 3 | "RandomSeed": -3251779305773917566L, 4 | "generatorName": "debug_all_block_states", 5 | "BorderCenterZ": 0.d, 6 | "Difficulty": 0b, 7 | "BorderSizeLerpTime": 0L, 8 | "raining": 0b, 9 | "DimensionData": { 10 | "1": { 11 | "DragonFight": { 12 | "Gateways": [ 13 | 3, 14 | 9, 15 | 0, 16 | 10, 17 | 17, 18 | 19, 19 | 14, 20 | 8, 21 | 7, 22 | 4, 23 | 11, 24 | 5, 25 | 2, 26 | 6, 27 | 12, 28 | 16, 29 | 1, 30 | 18, 31 | 13, 32 | 15 33 | ], 34 | "DragonKilled": 1b, 35 | "PreviouslyKilled": 1b 36 | } 37 | } 38 | }, 39 | "Time": 86L, 40 | "GameType": 3, 41 | "MapFeatures": 0b, 42 | "BorderCenterX": 0.d, 43 | "BorderDamagePerBlock": 0.2000000000000000111d, 44 | "BorderWarningBlocks": 5.d, 45 | "BorderSizeLerpTarget": 60000000.d, 46 | "Version": { 47 | "Snapshot": 0b, 48 | "Id": 1343, 49 | "Name": "1.12.2" 50 | }, 51 | "DayTime": 6000L, 52 | "initialized": 1b, 53 | "allowCommands": 1b, 54 | "SizeOnDisk": 0L, 55 | "GameRules": { 56 | "doTileDrops": "true", 57 | "doFireTick": "true", 58 | "gameLoopFunction": "-", 59 | "maxCommandChainLength": "65536", 60 | "reducedDebugInfo": "false", 61 | "naturalRegeneration": "true", 62 | "disableElytraMovementCheck": "false", 63 | "doMobLoot": "true", 64 | "announceAdvancements": "true", 65 | "keepInventory": "false", 66 | "doEntityDrops": "true", 67 | "doLimitedCrafting": "false", 68 | "mobGriefing": "true", 69 | "randomTickSpeed": "3", 70 | "commandBlockOutput": "true", 71 | "spawnRadius": "10", 72 | "doMobSpawning": "true", 73 | "maxEntityCramming": "24", 74 | "logAdminCommands": "true", 75 | "spectatorsGenerateChunks": "true", 76 | "doWeatherCycle": "true", 77 | "sendCommandFeedback": "true", 78 | "doDaylightCycle": "false", 79 | "showDeathMessages": "true" 80 | }, 81 | "Player": { 82 | "HurtByTimestamp": 0, 83 | "SleepTimer": 0s, 84 | "Attributes": [ 85 | { 86 | "Base": 20.d, 87 | "Name": "generic.maxHealth" 88 | }, 89 | { 90 | "Base": 0.d, 91 | "Name": "generic.knockbackResistance" 92 | }, 93 | { 94 | "Base": 0.10000000149011611938d, 95 | "Name": "generic.movementSpeed" 96 | }, 97 | { 98 | "Base": 0.d, 99 | "Name": "generic.armor" 100 | }, 101 | { 102 | "Base": 0.d, 103 | "Name": "generic.armorToughness" 104 | }, 105 | { 106 | "Base": 1.d, 107 | "Name": "generic.attackDamage" 108 | }, 109 | { 110 | "Base": 4.d, 111 | "Name": "generic.attackSpeed" 112 | }, 113 | { 114 | "Base": 0.d, 115 | "Name": "generic.luck" 116 | } 117 | ], 118 | "Invulnerable": 0b, 119 | "FallFlying": 0b, 120 | "PortalCooldown": 0, 121 | "AbsorptionAmount": 0.f, 122 | "abilities": { 123 | "invulnerable": 1b, 124 | "mayfly": 1b, 125 | "instabuild": 0b, 126 | "walkSpeed": 0.10000000149011611938f, 127 | "mayBuild": 0b, 128 | "flying": 1b, 129 | "flySpeed": 0.05000000074505805969f 130 | }, 131 | "FallDistance": 0.f, 132 | "recipeBook": { 133 | "recipes": [], 134 | "isFilteringCraftable": 0b, 135 | "toBeDisplayed": [], 136 | "isGuiOpen": 0b 137 | }, 138 | "DeathTime": 0s, 139 | "XpSeed": 0, 140 | "XpTotal": 0, 141 | "playerGameType": 3, 142 | "seenCredits": 0b, 143 | "Motion": [ 144 | 0.d, 145 | 0.d, 146 | 0.d 147 | ], 148 | "UUIDLeast": -7965449663550299548L, 149 | "Health": 20.f, 150 | "foodSaturationLevel": 5.f, 151 | "Air": 300s, 152 | "OnGround": 0b, 153 | "Dimension": 0, 154 | "Rotation": [ 155 | -30.149997711181640625f, 156 | -3.90000104904174804688f 157 | ], 158 | "XpLevel": 0, 159 | "Score": 0, 160 | "UUIDMost": 1283543676063598931L, 161 | "Sleeping": 0b, 162 | "Pos": [ 163 | 3.5d, 164 | 64.3490206121319374688d, 165 | -2.5d 166 | ], 167 | "Fire": -20s, 168 | "XpP": 0.f, 169 | "EnderItems": [], 170 | "DataVersion": 1343, 171 | "foodLevel": 20, 172 | "foodExhaustionLevel": 0.f, 173 | "HurtTime": 0s, 174 | "SelectedItemSlot": 0, 175 | "Inventory": [], 176 | "foodTickTimer": 0 177 | }, 178 | "SpawnY": 1, 179 | "rainTime": 1, 180 | "thunderTime": 1, 181 | "SpawnZ": 0, 182 | "hardcore": 0b, 183 | "DifficultyLocked": 1b, 184 | "SpawnX": 0, 185 | "clearWeatherTime": 999999914, 186 | "thundering": 0b, 187 | "generatorVersion": 0, 188 | "version": 19133, 189 | "BorderSafeZone": 5.d, 190 | "generatorOptions": "", 191 | "LastPlayed": 1527359472107L, 192 | "BorderWarningTime": 15.d, 193 | "LevelName": "Debug World 2", 194 | "BorderSize": 60000000.d, 195 | "DataVersion": 1343 196 | } 197 | } -------------------------------------------------------------------------------- /tests/test_amulet_nbt/test_read_file/src/snbt/J1_13_level.snbt: -------------------------------------------------------------------------------- 1 | { 2 | "Data": { 3 | "RandomSeed": -199552259844446636L, 4 | "generatorName": "debug_all_block_states", 5 | "BorderCenterZ": 0.d, 6 | "Difficulty": 0b, 7 | "BorderSizeLerpTime": 0L, 8 | "raining": 0b, 9 | "DimensionData": { 10 | "1": { 11 | "DragonFight": { 12 | "Gateways": [ 13 | 15, 14 | 13, 15 | 10, 16 | 3, 17 | 11, 18 | 9, 19 | 16, 20 | 19, 21 | 1, 22 | 2, 23 | 14, 24 | 5, 25 | 6, 26 | 4, 27 | 12, 28 | 17, 29 | 0, 30 | 7, 31 | 18, 32 | 8 33 | ], 34 | "DragonKilled": 1b, 35 | "PreviouslyKilled": 1b 36 | } 37 | } 38 | }, 39 | "Time": 89L, 40 | "GameType": 3, 41 | "MapFeatures": 0b, 42 | "BorderCenterX": 0.d, 43 | "BorderDamagePerBlock": 0.2000000000000000111d, 44 | "BorderWarningBlocks": 5.d, 45 | "BorderSizeLerpTarget": 60000000.d, 46 | "Version": { 47 | "Snapshot": 1b, 48 | "Id": 1497, 49 | "Name": "18w22a" 50 | }, 51 | "DayTime": 6000L, 52 | "initialized": 1b, 53 | "allowCommands": 1b, 54 | "SizeOnDisk": 0L, 55 | "CustomBossEvents": {}, 56 | "GameRules": { 57 | "doTileDrops": "true", 58 | "doFireTick": "true", 59 | "maxCommandChainLength": "65536", 60 | "reducedDebugInfo": "false", 61 | "naturalRegeneration": "true", 62 | "disableElytraMovementCheck": "false", 63 | "doMobLoot": "true", 64 | "announceAdvancements": "true", 65 | "keepInventory": "false", 66 | "doEntityDrops": "true", 67 | "doLimitedCrafting": "false", 68 | "mobGriefing": "true", 69 | "randomTickSpeed": "3", 70 | "commandBlockOutput": "true", 71 | "spawnRadius": "10", 72 | "doMobSpawning": "true", 73 | "maxEntityCramming": "24", 74 | "logAdminCommands": "true", 75 | "spectatorsGenerateChunks": "true", 76 | "doWeatherCycle": "true", 77 | "sendCommandFeedback": "true", 78 | "doDaylightCycle": "false", 79 | "showDeathMessages": "true" 80 | }, 81 | "Player": { 82 | "HurtByTimestamp": 0, 83 | "SleepTimer": 0s, 84 | "Attributes": [ 85 | { 86 | "Base": 20.d, 87 | "Name": "generic.maxHealth" 88 | }, 89 | { 90 | "Base": 0.d, 91 | "Name": "generic.knockbackResistance" 92 | }, 93 | { 94 | "Base": 0.10000000149011611938d, 95 | "Name": "generic.movementSpeed" 96 | }, 97 | { 98 | "Base": 0.d, 99 | "Name": "generic.armor" 100 | }, 101 | { 102 | "Base": 0.d, 103 | "Name": "generic.armorToughness" 104 | }, 105 | { 106 | "Base": 1.d, 107 | "Name": "generic.attackDamage" 108 | }, 109 | { 110 | "Base": 4.d, 111 | "Name": "generic.attackSpeed" 112 | }, 113 | { 114 | "Base": 0.d, 115 | "Name": "generic.luck" 116 | } 117 | ], 118 | "Invulnerable": 0b, 119 | "FallFlying": 0b, 120 | "PortalCooldown": 0, 121 | "AbsorptionAmount": 0.f, 122 | "abilities": { 123 | "invulnerable": 1b, 124 | "mayfly": 1b, 125 | "instabuild": 0b, 126 | "walkSpeed": 0.10000000149011611938f, 127 | "mayBuild": 0b, 128 | "flying": 1b, 129 | "flySpeed": 0.05000000074505805969f 130 | }, 131 | "FallDistance": 0.f, 132 | "recipeBook": { 133 | "recipes": [], 134 | "isFilteringCraftable": 0b, 135 | "toBeDisplayed": [], 136 | "isFurnaceGuiOpen": 0b, 137 | "isGuiOpen": 0b, 138 | "isFurnaceFilteringCraftable": 0b 139 | }, 140 | "DeathTime": 0s, 141 | "XpSeed": 0, 142 | "XpTotal": 0, 143 | "playerGameType": 3, 144 | "seenCredits": 0b, 145 | "Motion": [ 146 | 0.d, 147 | 0.d, 148 | 0.d 149 | ], 150 | "UUIDLeast": -7965449663550299548L, 151 | "Health": 20.f, 152 | "foodSaturationLevel": 5.f, 153 | "Air": 300s, 154 | "OnGround": 0b, 155 | "Dimension": 0, 156 | "Rotation": [ 157 | 0.f, 158 | 0.f 159 | ], 160 | "XpLevel": 0, 161 | "Score": 0, 162 | "UUIDMost": 1283543676063598931L, 163 | "Sleeping": 0b, 164 | "Pos": [ 165 | 0.5d, 166 | 2.d, 167 | 0.5d 168 | ], 169 | "Fire": -20s, 170 | "XpP": 0.f, 171 | "EnderItems": [], 172 | "DataVersion": 1497, 173 | "foodLevel": 20, 174 | "foodExhaustionLevel": 0.f, 175 | "HurtTime": 0s, 176 | "SelectedItemSlot": 0, 177 | "Inventory": [], 178 | "foodTickTimer": 0 179 | }, 180 | "SpawnY": 1, 181 | "rainTime": 1, 182 | "thunderTime": 1, 183 | "SpawnZ": 0, 184 | "hardcore": 0b, 185 | "DifficultyLocked": 1b, 186 | "SpawnX": 0, 187 | "clearWeatherTime": 999999911, 188 | "thundering": 0b, 189 | "generatorVersion": 0, 190 | "version": 19133, 191 | "BorderSafeZone": 5.d, 192 | "generatorOptions": "{}", 193 | "LastPlayed": 1527647775463L, 194 | "BorderWarningTime": 15.d, 195 | "LevelName": "1.13 World", 196 | "BorderSize": 60000000.d, 197 | "DataVersion": 1497, 198 | "DataPacks": { 199 | "Enabled": [ 200 | "vanilla" 201 | ], 202 | "Disabled": [] 203 | } 204 | } 205 | } -------------------------------------------------------------------------------- /tests/test_massive_nbt.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import numpy 3 | import os 4 | from tempfile import TemporaryDirectory 5 | from amulet.nbt import ( 6 | ByteTag, 7 | ShortTag, 8 | IntTag, 9 | LongTag, 10 | FloatTag, 11 | DoubleTag, 12 | ByteArrayTag, 13 | IntArrayTag, 14 | LongArrayTag, 15 | StringTag, 16 | ListTag, 17 | CompoundTag, 18 | NamedTag, 19 | read_nbt, 20 | ) 21 | 22 | 23 | class MassiveNBTTests(unittest.TestCase): 24 | def test_api(self) -> None: 25 | compound = CompoundTag() 26 | named_compound = NamedTag(compound, name="hello") 27 | 28 | # the nbt objects with no inputs 29 | compound["emptyByte"] = ByteTag() 30 | compound["emptyShort"] = ShortTag() 31 | compound["emptyInt"] = IntTag() 32 | compound["emptyLong"] = LongTag() 33 | compound["emptyFloat"] = FloatTag() 34 | compound["emptyDouble"] = DoubleTag() 35 | compound["emptyByteArray"] = ByteArrayTag() 36 | compound["emptyString"] = StringTag() 37 | compound["emptyList"] = ListTag() 38 | compound["emptyCompound"] = CompoundTag() 39 | compound["emptyIntArray"] = IntArrayTag() 40 | compound["emptyLongArray"] = LongArrayTag() 41 | 42 | # the nbt objects with zero or empty inputs (pure python only) 43 | compound["zeroByte"] = ByteTag(0) 44 | compound["zeroShort"] = ShortTag(0) 45 | compound["zeroInt"] = IntTag(0) 46 | compound["zeroLong"] = LongTag(0) 47 | compound["zeroFloat"] = FloatTag(0) 48 | compound["zeroDouble"] = DoubleTag(0) 49 | compound["zeroByteArray"] = ByteArrayTag([]) 50 | compound["zeroString"] = StringTag("") 51 | compound["zeroList"] = ListTag([]) 52 | compound["zeroCompound"] = CompoundTag({}) 53 | compound["zeroIntArray"] = IntArrayTag([]) 54 | compound["zeroLongArray"] = LongArrayTag([]) 55 | 56 | # empty inputs but numpy arrays for the array types 57 | compound["zeroNumpyByteArray"] = ByteArrayTag(numpy.array([])) 58 | compound["zeroNumpyIntArray"] = IntArrayTag(numpy.array([])) 59 | compound["zeroNumpyLongArray"] = LongArrayTag(numpy.array([])) 60 | 61 | # test the array types with some python data 62 | compound["listByteArray"] = ByteArrayTag([i for i in range(-128, 127)]) 63 | compound["listIntArray"] = IntArrayTag([i for i in range(-400, 400)]) 64 | compound["listLongArray"] = LongArrayTag([i for i in range(-400, 400)]) 65 | 66 | # test the array types with numpy data of varying dtypes 67 | compound["numpyDtypeTestByteArray"] = ByteArrayTag( 68 | numpy.array([i for i in range(-128, 127)], dtype=int) 69 | ) 70 | compound["numpyDtypeuTestByteArray"] = ByteArrayTag( 71 | numpy.array([i for i in range(0, 255)], dtype=numpy.uint) 72 | ) 73 | compound["numpyDtypeTestIntArray"] = IntArrayTag( 74 | numpy.array([i for i in range(-400, 400)], dtype=int) 75 | ) 76 | compound["numpyDtypeuTestIntArray"] = IntArrayTag( 77 | numpy.array([i for i in range(0, 800)], dtype=numpy.uint) 78 | ) 79 | compound["numpyDtypeTestLongArray"] = LongArrayTag( 80 | numpy.array([i for i in range(-400, 400)], dtype=int) 81 | ) 82 | compound["numpyDtypeuTestLongArray"] = LongArrayTag( 83 | numpy.array([i for i in range(0, 800)], dtype=numpy.uint) 84 | ) 85 | 86 | compound["numpyDtypedTestByteArray"] = ByteArrayTag( 87 | numpy.array([i for i in range(-128, 127)]) 88 | ) 89 | compound["numpyDtypedTestIntArray"] = IntArrayTag( 90 | numpy.array([i for i in range(-400, 400)]) 91 | ) 92 | compound["numpyDtypedTestLongArray"] = LongArrayTag( 93 | numpy.array([i for i in range(-400, 400)]) 94 | ) 95 | 96 | # test the extremes of the array types 97 | # byte array tested above 98 | compound["numpyExtremeTestIntArray"] = IntArrayTag( 99 | numpy.array([-(2**31), (2**31) - 1], dtype=int) 100 | ) 101 | compound["numpyExtremeTestLongArray"] = LongArrayTag( 102 | numpy.array([-(2**63), (2**63) - 1], dtype="q") 103 | ) 104 | 105 | compound["minByte"] = ByteTag(-128) 106 | compound["minShort"] = ShortTag(-(2**15)) 107 | compound["minInt"] = IntTag(-(2**31)) 108 | compound["minLong"] = LongTag(-(2**63)) 109 | 110 | compound["maxByte"] = ByteTag(127) 111 | compound["maxShort"] = ShortTag(2**15 - 1) 112 | compound["maxInt"] = IntTag(2**31 - 1) 113 | compound["maxLong"] = LongTag(2**63 - 1) 114 | 115 | # these should either overflow when setting or error when saving. Test each and if it errors just comment it out 116 | compound["overflowByte"] = ByteTag(300) 117 | compound["underflowByte"] = ByteTag(-300) 118 | compound["overflowShort"] = ShortTag(2**16) 119 | compound["underflowShort"] = ShortTag(-(2**16)) 120 | compound["overflowInt"] = IntTag(2**32) 121 | compound["underflowInt"] = IntTag(-(2**32)) 122 | compound["overflowLong"] = LongTag(2**64) 123 | compound["underflowLong"] = LongTag(-(2**64)) 124 | 125 | # compound['overflowByteArray'] = ByteArrayTag([-129, 128]) 126 | # compound['overflowIntArray'] = IntArrayTag([-2**31-1, 2**31]) 127 | # compound['overflowLongArray'] = LongArrayTag([-2**63-1, 2**63]) 128 | 129 | # compound['overflowNumpyByteArray'] = ByteArrayTag(numpy.array([-129, 128])) 130 | # compound['overflowNumpyIntArray'] = IntArrayTag(numpy.array([-2**31-1, 2**31])) 131 | # compound['overflowNumpyLongArray'] = LongArrayTag(numpy.array([-2**63-1, 2**63])) 132 | 133 | # save then load back up and check the data matches 134 | 135 | with TemporaryDirectory() as temp_dir: 136 | named_compound.save_to( 137 | os.path.join(temp_dir, "massive_nbt_test_big_endian.nbt"), 138 | compressed=False, 139 | ) 140 | named_compound.save_to( 141 | os.path.join(temp_dir, "massive_nbt_test_big_endian_compressed.nbt"), 142 | compressed=True, 143 | ) 144 | named_compound.save_to( 145 | os.path.join(temp_dir, "massive_nbt_test_little_endian.nbt"), 146 | compressed=False, 147 | little_endian=True, 148 | ) 149 | 150 | test_be = read_nbt( 151 | os.path.join(temp_dir, "massive_nbt_test_big_endian.nbt") 152 | ) 153 | test_be_compressed = read_nbt( 154 | os.path.join(temp_dir, "massive_nbt_test_big_endian_compressed.nbt") 155 | ) 156 | test_le = read_nbt( 157 | os.path.join(temp_dir, "massive_nbt_test_little_endian.nbt"), 158 | little_endian=True, 159 | ) 160 | 161 | self.assertEqual(test_be, named_compound) 162 | self.assertEqual(test_be_compressed, named_compound) 163 | self.assertEqual(test_le, named_compound) 164 | 165 | 166 | if __name__ == "__main__": 167 | unittest.main() 168 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Amulet Team License 1.0.0 2 | # This is a combination of PolyForm Sheild and Noncommercial 3 | # Added a clause to allow commercial educational usage 4 | 5 | ## Acceptance 6 | 7 | In order to get any license under these terms, you must agree 8 | to them as both strict obligations and conditions to all 9 | your licenses. 10 | 11 | ## Copyright License 12 | 13 | The licensor grants you a copyright license for the 14 | software to do everything you might do with the software 15 | that would otherwise infringe the licensor's copyright 16 | in it for any permitted purpose. However, you may 17 | only distribute the software according to [Distribution 18 | License](#distribution-license) and make changes or new works 19 | based on the software according to [Changes and New Works 20 | License](#changes-and-new-works-license). 21 | 22 | ## Distribution License 23 | 24 | The licensor grants you an additional copyright license 25 | to distribute copies of the software. Your license 26 | to distribute covers distributing the software with 27 | changes and new works permitted by [Changes and New Works 28 | License](#changes-and-new-works-license). 29 | 30 | ## Notices 31 | 32 | You must ensure that anyone who gets a copy of any part of 33 | the software from you also gets a copy of these terms or the 34 | URL for them above, as well as copies of any plain-text lines 35 | beginning with `Required Notice:` that the licensor provided 36 | with the software. 37 | 38 | > Required Notice: Copyright Amulet Team. (https://www.amuletmc.com/) 39 | 40 | ## Changes and New Works License 41 | 42 | The licensor grants you an additional copyright license to 43 | make changes and new works based on the software for any 44 | permitted purpose. 45 | 46 | ## Third Party Agreements 47 | 48 | Use or modification of the software in a way that violates a third 49 | party's licence, terms or copyright is not a permitted purpose. 50 | 51 | ## Patent License 52 | 53 | The licensor grants you a patent license for the software that 54 | covers patent claims the licensor can license, or becomes able 55 | to license, that you would infringe by using the software. 56 | 57 | ## Noncommercial Purposes 58 | 59 | Any noncommercial purpose is a permitted purpose. 60 | 61 | ## Commercial Educational Purposes 62 | 63 | Commercial use purely for educational purpose is a permitted purpose. 64 | 65 | ## Personal Uses 66 | 67 | Personal use for research, experiment, and testing for 68 | the benefit of public knowledge, personal study, private 69 | entertainment, hobby projects, amateur pursuits, or religious 70 | observance, without any anticipated commercial application, 71 | is use for a permitted purpose. 72 | 73 | ## Noncommercial Organizations 74 | 75 | Use by any charitable organization, educational institution, 76 | public research organization, public safety or health 77 | organization, environmental protection organization, 78 | or government institution is use for a permitted purpose 79 | regardless of the source of funding or obligations resulting 80 | from the funding. 81 | 82 | ## Noncompete 83 | 84 | Any purpose is a permitted purpose, except for providing any 85 | product that competes with the software or any product the 86 | licensor or any of its affiliates provides using the software. 87 | 88 | ## Competition 89 | 90 | Goods and services compete even when they provide functionality 91 | through different kinds of interfaces or for different technical 92 | platforms. Applications can compete with services, libraries 93 | with plugins, frameworks with development tools, and so on, 94 | even if they're written in different programming languages 95 | or for different computer architectures. Goods and services 96 | compete even when provided free of charge. If you market a 97 | product as a practical substitute for the software or another 98 | product, it definitely competes. 99 | 100 | ## New Products 101 | 102 | If you are using the software to provide a product that does 103 | not compete, but the licensor or any of its affiliates brings 104 | your product into competition by providing a new version of 105 | the software or another product using the software, you may 106 | continue using versions of the software available under these 107 | terms beforehand to provide your competing product, but not 108 | any later versions. 109 | 110 | ## Discontinued Products 111 | 112 | You may begin using the software to compete with a product 113 | or service that the licensor or any of its affiliates has 114 | stopped providing, unless the licensor includes a plain-text 115 | line beginning with `Licensor Line of Business:` with the 116 | software that mentions that line of business. For example: 117 | 118 | > Licensor Line of Business: YoyodyneCMS Content Management 119 | System (http://example.com/cms) 120 | 121 | ## Sales of Business 122 | 123 | If the licensor or any of its affiliates sells a line of 124 | business developing the software or using the software 125 | to provide a product, the buyer can also enforce 126 | [Noncompete](#noncompete) for that product. 127 | 128 | ## Fair Use 129 | 130 | You may have "fair use" rights for the software under the 131 | law. These terms do not limit them. 132 | 133 | ## No Other Rights 134 | 135 | These terms do not allow you to sublicense or transfer any of 136 | your licenses to anyone else, or prevent the licensor from 137 | granting licenses to anyone else. These terms do not imply 138 | any other licenses. 139 | 140 | ## Patent Defense 141 | 142 | If you make any written claim that the software infringes or 143 | contributes to infringement of any patent, your patent license 144 | for the software granted under these terms ends immediately. If 145 | your company makes such a claim, your patent license ends 146 | immediately for work on behalf of your company. 147 | 148 | ## Violations 149 | 150 | The first time you are notified in writing that you have 151 | violated any of these terms, or done anything with the software 152 | not covered by your licenses, your licenses can nonetheless 153 | continue if you come into full compliance with these terms, 154 | and take practical steps to correct past violations, within 155 | 32 days of receiving notice. Otherwise, all your licenses 156 | end immediately. 157 | 158 | ## No Liability 159 | 160 | ***As far as the law allows, the software comes as is, without 161 | any warranty or condition, and the licensor will not be liable 162 | to you for any damages arising out of these terms or the use 163 | or nature of the software, under any kind of legal claim.*** 164 | 165 | ## Definitions 166 | 167 | The **licensor** is the individual or entity offering these 168 | terms, and the **software** is the software the licensor makes 169 | available under these terms. 170 | 171 | A **product** can be a good or service, or a combination 172 | of them. 173 | 174 | **You** refers to the individual or entity agreeing to these 175 | terms. 176 | 177 | **Your company** is any legal entity, sole proprietorship, 178 | or other kind of organization that you work for, plus all 179 | its affiliates. 180 | 181 | **Affiliates** means the other organizations than an 182 | organization has control over, is under the control of, or is 183 | under common control with. 184 | 185 | **Control** means ownership of substantially all the assets of 186 | an entity, or the power to direct its management and policies 187 | by vote, contract, or otherwise. Control can be direct or 188 | indirect. 189 | 190 | **Your licenses** are all the licenses granted to you for the 191 | software under these terms. 192 | 193 | **Use** means anything you do with the software requiring one 194 | of your licenses. -------------------------------------------------------------------------------- /src/amulet/nbt/tag/abc.py.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | 7 | #include 8 | #include 9 | #include 10 | 11 | #include 12 | #include 13 | 14 | namespace py = pybind11; 15 | 16 | template 17 | void abstract_method(T self, py::args, const py::kwargs&) 18 | { 19 | PyErr_SetString(PyExc_NotImplementedError, ""); 20 | throw py::error_already_set(); 21 | } 22 | 23 | void init_abc(py::module& m) 24 | { 25 | py::object mutf8_encoding = m.attr("mutf8_encoding"); 26 | py::object java_encoding = m.attr("java_encoding"); 27 | 28 | py::classh AbstractBaseTag(m, "AbstractBaseTag", 29 | "Abstract Base Class for all tag classes"); 30 | AbstractBaseTag.def_property_readonly_static( 31 | "tag_id", 32 | abstract_method); 33 | AbstractBaseTag.def_property_readonly( 34 | "py_data", 35 | abstract_method, 36 | "A python representation of the class. Note that the return type is undefined and may change in the future.\n" 37 | "\n" 38 | "You would be better off using the py_{type} or np_array properties if you require a fixed type.\n" 39 | "This is here for convenience to get a python representation under the same property name."); 40 | AbstractBaseTag.def( 41 | "to_nbt", 42 | []( 43 | const Amulet::NBT::AbstractBaseTag& self, 44 | Amulet::NBT::EncodingPreset preset, 45 | std::optional name) { 46 | PyErr_SetString(PyExc_NotImplementedError, ""); 47 | throw py::error_already_set(); 48 | }, 49 | py::kw_only(), 50 | py::arg("preset") = java_encoding, 51 | py::arg("name") = ""); 52 | AbstractBaseTag.def( 53 | "to_nbt", 54 | []( 55 | const Amulet::NBT::AbstractBaseTag& self, 56 | bool compressed, 57 | bool little_endian, 58 | Amulet::NBT::StringEncoding string_encoding, 59 | std::optional name) { 60 | PyErr_SetString(PyExc_NotImplementedError, ""); 61 | throw py::error_already_set(); 62 | }, 63 | py::kw_only(), 64 | py::arg("compressed") = true, 65 | py::arg("little_endian") = false, 66 | py::arg("string_encoding") = mutf8_encoding, 67 | py::arg("name") = ""); 68 | AbstractBaseTag.def( 69 | "save_to", 70 | []( 71 | const Amulet::NBT::AbstractBaseTag& self, 72 | py::object filepath_or_writable, 73 | Amulet::NBT::EncodingPreset preset, 74 | std::optional name) { 75 | PyErr_SetString(PyExc_NotImplementedError, ""); 76 | throw py::error_already_set(); 77 | }, 78 | py::arg("filepath_or_writable") = py::none(), 79 | py::pos_only(), 80 | py::kw_only(), 81 | py::arg("preset") = java_encoding, 82 | py::arg("name") = ""); 83 | AbstractBaseTag.def( 84 | "save_to", 85 | []( 86 | const Amulet::NBT::AbstractBaseTag& self, 87 | py::object filepath_or_writable, 88 | bool compressed, 89 | bool little_endian, 90 | Amulet::NBT::StringEncoding string_encoding, 91 | std::optional name) { 92 | PyErr_SetString(PyExc_NotImplementedError, ""); 93 | throw py::error_already_set(); 94 | }, 95 | py::arg("filepath_or_writable") = py::none(), 96 | py::pos_only(), 97 | py::kw_only(), 98 | py::arg("compressed") = true, 99 | py::arg("little_endian") = false, 100 | py::arg("string_encoding") = mutf8_encoding, 101 | py::arg("name") = ""); 102 | AbstractBaseTag.def( 103 | "to_snbt", 104 | []( 105 | const Amulet::NBT::AbstractBaseTag& self, 106 | py::object indent) { 107 | PyErr_SetString(PyExc_NotImplementedError, ""); 108 | throw py::error_already_set(); 109 | }, 110 | py::arg("indent") = py::none()); 111 | AbstractBaseTag.def( 112 | "__eq__", 113 | abstract_method, 114 | "Check if the instance is equal to another instance.\n" 115 | "\n" 116 | "This will only return True if the tag type is the same and the data contained is the same."); 117 | AbstractBaseTag.def( 118 | "__repr__", 119 | abstract_method, 120 | "A string representation of the object to show how it can be constructed."); 121 | AbstractBaseTag.def( 122 | "__str__", 123 | abstract_method, 124 | "A string representation of the object."); 125 | AbstractBaseTag.def( 126 | "__copy__", 127 | abstract_method, 128 | "A string representation of the object."); 129 | AbstractBaseTag.def( 130 | "__deepcopy__", 131 | abstract_method, 132 | "A string representation of the object."); 133 | 134 | py::classh AbstractBaseImmutableTag(m, "AbstractBaseImmutableTag", 135 | "Abstract Base Class for all tag classes"); 136 | AbstractBaseImmutableTag.def( 137 | "__hash__", 138 | abstract_method, 139 | "A hash of the data in the class."); 140 | 141 | py::classh AbstractBaseNumericTag(m, "AbstractBaseNumericTag", 142 | "Abstract Base Class for all numeric tag classes"); 143 | AbstractBaseNumericTag.def( 144 | "__int__", 145 | abstract_method, 146 | "Get a python int representation of the class."); 147 | AbstractBaseNumericTag.def( 148 | "__float__", 149 | abstract_method, 150 | "Get a python float representation of the class."); 151 | AbstractBaseNumericTag.def( 152 | "__bool__", 153 | abstract_method, 154 | "Get a python bool representation of the class."); 155 | 156 | py::classh AbstractBaseIntTag(m, "AbstractBaseIntTag", 157 | "Abstract Base Class for all int tag classes"); 158 | AbstractBaseIntTag.def_property_readonly( 159 | "py_int", 160 | abstract_method, 161 | "A python int representation of the class.\n" 162 | "\n" 163 | "The returned data is immutable so changes will not mirror the instance."); 164 | 165 | py::classh AbstractBaseFloatTag(m, "AbstractBaseFloatTag", 166 | "Abstract Base Class for all float tag classes."); 167 | AbstractBaseFloatTag.def_property_readonly( 168 | "py_float", 169 | abstract_method, 170 | "A python float representation of the class.\n" 171 | "\n" 172 | "The returned data is immutable so changes will not mirror the instance."); 173 | 174 | py::classh AbstractBaseMutableTag(m, "AbstractBaseMutableTag", 175 | "Abstract Base Class for all mutable tags."); 176 | AbstractBaseMutableTag.attr("__hash__") = py::none(); 177 | 178 | py::classh AbstractBaseArrayTag(m, "AbstractBaseArrayTag", 179 | "Abstract Base Class for all array tag classes."); 180 | } 181 | -------------------------------------------------------------------------------- /src/amulet/nbt/string_encoding/mutf8.cpp: -------------------------------------------------------------------------------- 1 | // Partially based on the mutf8 python library by Tyler Kennedy MIT 2 | // Rewritten to stay within C++ 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | #include 11 | #include 12 | 13 | namespace Amulet { 14 | namespace NBT { 15 | CodePointVector read_mutf8(std::string_view src) 16 | { 17 | CodePointVector dst; 18 | dst.reserve(src.size()); 19 | 20 | for (size_t index = 0; index < src.size(); index++) { 21 | uint8_t b1 = src[index]; 22 | 23 | if (b1 == 0) { 24 | throw std::invalid_argument("Embedded NULL byte at index " + std::to_string(index)); 25 | } else if (b1 < 0b10000000) { 26 | // ASCII/1 byte codepoint. 27 | dst.push_back(b1 & 0b01111111); 28 | } else if ((b1 & 0b11100000) == 0b11000000) { 29 | // 2 byte codepoint. 30 | if (index + 1 >= src.size()) { 31 | throw std::invalid_argument("2-byte codepoint started at index " + std::to_string(index) + ", but input too short to finish."); 32 | } 33 | uint8_t b2 = src[index + 1]; 34 | if ((b2 & 0b11000000) != 0b10000000) { 35 | throw std::invalid_argument("2-byte codepoint started at index " + std::to_string(index) + ", but format bits of byte 2 are incorrect."); 36 | } 37 | size_t value = (0b00011111 & b1) << 6 | (0b00111111 & b2); 38 | if (0 < value && value < 0x80) { 39 | throw std::invalid_argument("2-byte codepoint at index " + std::to_string(index) + " has invalid value."); 40 | } 41 | dst.push_back(value); 42 | index++; 43 | } else if ((b1 & 0b11110000) == 0b11100000) { 44 | // 3 or 6 byte codepoint. 45 | if (index + 2 >= src.size()) { 46 | throw std::invalid_argument("3-byte codepoint started at index " + std::to_string(index) + ", but input too short to finish."); 47 | } 48 | uint8_t b2 = src[index + 1]; 49 | uint8_t b3 = src[index + 2]; 50 | 51 | if (b1 == 0b11101101 && (b2 & 0b11100000) == 0b10100000) { 52 | if (index + 5 >= src.size()) { 53 | throw std::invalid_argument("6-byte codepoint started at index " + std::to_string(index) + ", but input too short to finish."); 54 | } 55 | 56 | uint8_t b4 = src[index + 3]; 57 | uint8_t b5 = src[index + 4]; 58 | uint8_t b6 = src[index + 5]; 59 | if ((b2 & 0b11110000) != 0b10100000) { 60 | throw std::invalid_argument("6-byte codepoint started at index " + std::to_string(index) + ", but format bits of byte 2 are incorrect."); 61 | } 62 | if ((b3 & 0b11000000) != 0b10000000) { 63 | throw std::invalid_argument("6-byte codepoint started at index " + std::to_string(index) + ", but format bits of byte 3 are incorrect."); 64 | } 65 | if (b4 != 0b11101101) { 66 | throw std::invalid_argument("6-byte codepoint started at index " + std::to_string(index) + ", but format bits of byte 4 are incorrect."); 67 | } 68 | if ((b5 & 0b11110000) != 0b10110000) { 69 | throw std::invalid_argument("6-byte codepoint started at index " + std::to_string(index) + ", but format bits of byte 5 are incorrect."); 70 | } 71 | if ((b6 & 0b11000000) != 0b10000000) { 72 | throw std::invalid_argument("6-byte codepoint started at index " + std::to_string(index) + ", but format bits of byte 6 are incorrect."); 73 | } 74 | size_t value = 0x10000 + ((0b00001111 & b2) << 16 | (0b00111111 & b3) << 10 | (0b00001111 & b5) << 6 | (0b00111111 & b6)); 75 | if (value < 0x10000) { 76 | throw std::invalid_argument("6-byte codepoint at index " + std::to_string(index) + " has invalid value."); 77 | } 78 | dst.push_back(value); 79 | index += 5; 80 | } else { 81 | if ((b2 & 0b11000000) != 0b10000000) { 82 | throw std::invalid_argument("3-byte codepoint started at index " + std::to_string(index) + ", but format bits of byte 2 are incorrect."); 83 | } 84 | if ((b3 & 0b11000000) != 0b10000000) { 85 | throw std::invalid_argument("3-byte codepoint started at index " + std::to_string(index) + ", but format bits of byte 3 are incorrect."); 86 | } 87 | size_t value = ((0b00001111 & b1) << 12) | ((0b00111111 & b2) << 6) | (0b00111111 & b3); 88 | if (value < 0x800) { 89 | throw std::invalid_argument("3-byte codepoint at index " + std::to_string(index) + " has invalid value."); 90 | } 91 | dst.push_back(value); 92 | index += 2; 93 | } 94 | } else { 95 | throw std::invalid_argument("Invalid byte at index " + std::to_string(index)); 96 | } 97 | } 98 | return dst; 99 | } 100 | 101 | void write_mutf8(std::string& dst, const CodePointVector& src) 102 | { 103 | dst.reserve(dst.size() + src.size()); 104 | for (size_t index = 0; index < src.size(); index++) { 105 | const size_t& c = src[index]; 106 | if (c == 0) { 107 | dst.push_back(0xC0); 108 | dst.push_back(0x80); 109 | } else if (c <= 127) { 110 | dst.push_back(c & 0b01111111); 111 | } else if (c <= 2047) { 112 | dst.push_back(0b11000000 | (0b00011111 & (c >> 6))); 113 | dst.push_back(0b10000000 | (0b00111111 & c)); 114 | } else if (c <= 65535) { 115 | if ((c >= 0xD800) && (c <= 0xDFFF)) { 116 | throw std::invalid_argument("code point at index " + std::to_string(index) + " cannot be encoded."); 117 | } 118 | dst.push_back(0b11100000 | (0b00001111 & (c >> 12))); 119 | dst.push_back(0b10000000 | (0b00111111 & (c >> 6))); 120 | dst.push_back(0b10000000 | (0b00111111 & c)); 121 | } else if (c <= 1114111) { 122 | dst.push_back(0b11101101); 123 | dst.push_back(0b10100000 | (0b00001111 & ((c >> 16) - 1))); 124 | dst.push_back(0b10000000 | (0b00111111 & (c >> 10))); 125 | dst.push_back(0b11101101); 126 | dst.push_back(0b10110000 | (0b00001111 & (c >> 6))); 127 | dst.push_back(0b10000000 | (0b00111111 & c)); 128 | } else { 129 | throw std::invalid_argument("Invalid code point at index " + std::to_string(index)); 130 | } 131 | } 132 | } 133 | 134 | std::string write_mutf8(const CodePointVector& src) 135 | { 136 | std::string dst; 137 | write_mutf8(dst, src); 138 | return dst; 139 | } 140 | 141 | // Decode a modified utf-8 byte sequence to a regular utf-8 byte sequence 142 | std::string mutf8_to_utf8(std::string_view src) 143 | { 144 | std::string dst; 145 | write_utf8(dst, read_mutf8(src)); 146 | return dst; 147 | } 148 | 149 | // Encode a regular utf-8 byte sequence to a modified utf-8 byte sequence 150 | std::string utf8_to_mutf8(std::string_view src) 151 | { 152 | std::string dst; 153 | write_mutf8(dst, read_utf8(src)); 154 | return dst; 155 | } 156 | } // namespace NBT 157 | } // namespace Amulet 158 | -------------------------------------------------------------------------------- /src/amulet/nbt/tag/list_methods.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | namespace Amulet { 7 | namespace NBT { 8 | inline size_t ListTag_size(const ListTag& self) 9 | { 10 | return std::visit([](auto&& list) -> size_t { 11 | using T = std::decay_t; 12 | if constexpr (std::is_same_v) { 13 | return 0; 14 | } else { 15 | return list.size(); 16 | } 17 | }, 18 | self); 19 | }; 20 | 21 | template < 22 | typename tagT, 23 | std::enable_if_t< 24 | std::is_same_v || std::is_same_v || std::is_same_v || std::is_same_v || std::is_same_v || std::is_same_v || std::is_same_v || std::is_same_v || std::is_same_v || std::is_same_v || std::is_same_v || std::is_same_v, 25 | bool> 26 | = true> 27 | inline void ListTag_append(ListTag& self, const tagT& tag) 28 | { 29 | if (std::holds_alternative>(self)) { 30 | std::get>(self).push_back(tag); 31 | } else if (ListTag_size(self) == 0) { 32 | self.emplace>().push_back(tag); 33 | } else { 34 | throw type_error( 35 | "ListTag has element type " + std::to_string(self.index()) + " but the tag has type " + std::to_string(variant_index>())); 36 | } 37 | }; 38 | 39 | template < 40 | typename tagT, 41 | std::enable_if_t< 42 | std::is_same_v, 43 | bool> 44 | = true> 45 | inline void ListTag_append(ListTag& self, const TagNode& node) 46 | { 47 | std::visit([&self](auto&& tag) { 48 | using T = std::decay_t; 49 | ListTag_append(self, tag); 50 | }, 51 | node); 52 | } 53 | 54 | template 55 | size_t ListTag_bounds_check(size_t size, indexT index) 56 | { 57 | if constexpr (std::is_signed_v) { 58 | if (index < 0) { 59 | index += size; 60 | } 61 | if constexpr (clamp) { 62 | if (index < 0) { 63 | index = 0; 64 | } 65 | } else { 66 | if (index < 0) { 67 | throw std::out_of_range("ListTag index is out of range."); 68 | } 69 | } 70 | } 71 | size_t abs_index = index; 72 | if constexpr (clamp) { 73 | if (size < abs_index) { 74 | abs_index = size; 75 | } 76 | } else { 77 | if (size <= abs_index) { 78 | throw std::out_of_range("ListTag index is out of range."); 79 | } 80 | } 81 | return abs_index; 82 | }; 83 | 84 | template < 85 | typename tagT, 86 | typename indexT, 87 | std::enable_if_t< 88 | std::is_same_v || std::is_same_v || std::is_same_v || std::is_same_v || std::is_same_v || std::is_same_v || std::is_same_v || std::is_same_v || std::is_same_v || std::is_same_v || std::is_same_v || std::is_same_v, 89 | bool> 90 | = true> 91 | tagT ListTag_get(const ListTag& self, indexT index) 92 | { 93 | auto& list_tag = std::get>(self); 94 | return list_tag[ListTag_bounds_check(list_tag.size(), index)]; 95 | } 96 | 97 | template 98 | TagNode ListTag_get_node(const ListTag& self, indexT index) 99 | { 100 | return std::visit([&self, &index](auto&& list) -> TagNode { 101 | using T = std::decay_t; 102 | if constexpr (std::is_same_v) { 103 | throw type_error("Cannot get from null ListTag."); 104 | } else { 105 | return list[ListTag_bounds_check(list.size(), index)]; 106 | } 107 | }, 108 | self); 109 | } 110 | 111 | template 112 | void ListTag_set(ListTag& self, indexT index, tagT tag) 113 | { 114 | // Get the unsigned index. Also do bounds checking. 115 | size_t abs_index = ListTag_bounds_check(ListTag_size(self), index); 116 | if (std::holds_alternative>(self)) { 117 | // If the list type is the same as the tag 118 | auto& list_tag = std::get>(self); 119 | list_tag[abs_index] = tag; 120 | } else if (ListTag_size(self) == 1 && abs_index == 0) { 121 | // Overwriting the only value 122 | self.emplace>({ tag }); 123 | } else { 124 | throw type_error("NBT ListTag item mismatch."); 125 | } 126 | } 127 | 128 | template 129 | void ListTag_del(ListTag& self, indexT index) 130 | { 131 | std::visit([&index](auto&& list) { 132 | using T = std::decay_t; 133 | if constexpr (std::is_same_v) { 134 | // do nothing 135 | } else { 136 | size_t abs_index = ListTag_bounds_check(list.size(), index); 137 | list.erase(list.begin() + abs_index); 138 | } 139 | }, 140 | self); 141 | } 142 | 143 | template 144 | inline TagNode ListTag_pop(ListTag& self, const indexT& index) 145 | { 146 | return std::visit([&index](auto&& list) -> TagNode { 147 | using T = std::decay_t; 148 | if constexpr (std::is_same_v) { 149 | throw std::out_of_range("ListTag index is out of range."); 150 | } else { 151 | size_t abs_index = ListTag_bounds_check(list.size(), index); 152 | typename T::value_type tag = list[abs_index]; 153 | list.erase(list.begin() + abs_index); 154 | return tag; 155 | } 156 | }, 157 | self); 158 | } 159 | 160 | template 161 | void ListTag_insert(ListTag& self, indexT index, const tagT& tag) 162 | { 163 | if (!std::holds_alternative>(self)) { 164 | if (ListTag_size(self) == 0) { 165 | self.emplace>(); 166 | } else { 167 | throw type_error( 168 | "ListTag has element type " + std::to_string(self.index()) + " but the tag has type " + std::to_string(variant_index>())); 169 | } 170 | } 171 | auto& list_tag = std::get>(self); 172 | size_t abs_index = ListTag_bounds_check(list_tag.size(), index); 173 | list_tag.insert(list_tag.begin() + abs_index, tag); 174 | } 175 | 176 | template 177 | void ListTag_insert(ListTag& self, indexT index, const TagNode& node) 178 | { 179 | std::visit([&self, &index](auto&& tag) { 180 | using T = std::decay_t; 181 | ListTag_insert(self, index, tag); 182 | }, 183 | node); 184 | } 185 | 186 | template 187 | size_t ListTag_index(const ListTag& self, tagT tag, indexT start = 0, indexT stop = std::numeric_limits::max()) 188 | { 189 | if (!std::holds_alternative>(self)) { 190 | throw std::invalid_argument("item is not in the ListTag"); 191 | } 192 | auto& list_tag = std::get>(self); 193 | size_t abs_start = ListTag_bounds_check(list_tag.size(), start); 194 | size_t abs_stop = ListTag_bounds_check(list_tag.size(), stop); 195 | for (size_t i = abs_start; i < abs_stop; i++) { 196 | tagT tag_i = list_tag[i]; 197 | if (NBTTag_eq(tag, tag_i)) { 198 | return i; 199 | } 200 | } 201 | throw std::invalid_argument("item is not in the ListTag"); 202 | } 203 | 204 | template 205 | size_t ListTag_count(const ListTag& self, tagT tag) 206 | { 207 | if (!std::holds_alternative>(self)) { 208 | return 0; 209 | } 210 | auto& list_tag = std::get>(self); 211 | size_t count = 0; 212 | for (tagT tag_i : list_tag) { 213 | if (NBTTag_eq(tag, tag_i)) { 214 | count++; 215 | } 216 | } 217 | return count; 218 | } 219 | } // namespace NBT 220 | } // namespace Amulet 221 | -------------------------------------------------------------------------------- /src/amulet/nbt/nbt_encoding/binary/read_binary.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | 20 | namespace Amulet { 21 | namespace NBT { 22 | 23 | template 24 | inline T read_numeric_tag(BinaryReader& reader) 25 | { 26 | return T(reader.read_numeric()); 27 | } 28 | 29 | inline std::string read_string(BinaryReader& reader) 30 | { 31 | std::uint16_t length = reader.read_numeric(); 32 | return reader.read_string(length); 33 | }; 34 | 35 | inline StringTag read_string_tag(BinaryReader& reader) 36 | { 37 | return StringTag(read_string(reader)); 38 | }; 39 | 40 | inline TagNode read_node(BinaryReader& reader, std::uint8_t tag_id); 41 | 42 | inline CompoundTagPtr read_compound_tag(BinaryReader& reader) 43 | { 44 | CompoundTagPtr tag_ptr = std::make_shared(); 45 | CompoundTag& tag = *tag_ptr; 46 | while (true) { 47 | std::uint8_t tag_id = reader.read_numeric(); 48 | if (tag_id == 0) { 49 | break; 50 | } 51 | std::string name = read_string(reader); 52 | TagNode node = read_node(reader, tag_id); 53 | tag[name] = node; 54 | } 55 | return tag_ptr; 56 | }; 57 | 58 | template 59 | inline std::shared_ptr read_array_tag(BinaryReader& reader) 60 | { 61 | std::int32_t length = reader.read_numeric(); 62 | if (length < 0) { 63 | length = 0; 64 | } 65 | std::shared_ptr tag = std::make_shared(length); 66 | for (std::int32_t i = 0; i < length; i++) { 67 | reader.read_numeric_into((*tag)[i]); 68 | } 69 | return tag; 70 | } 71 | 72 | template 73 | inline ListTagPtr read_numeric_list_tag(BinaryReader& reader) 74 | { 75 | std::int32_t length = reader.read_numeric(); 76 | if (length < 0) { 77 | length = 0; 78 | } 79 | ListTagPtr tag = std::make_shared(std::vector(length)); 80 | std::vector& list = std::get>(*tag); 81 | for (std::int32_t i = 0; i < length; i++) { 82 | list[i] = T(reader.read_numeric()); 83 | } 84 | return tag; 85 | } 86 | 87 | template 88 | inline ListTagPtr read_template_list_tag(BinaryReader& reader) 89 | { 90 | std::int32_t length = reader.read_numeric(); 91 | if (length < 0) { 92 | length = 0; 93 | } 94 | ListTagPtr tag = std::make_shared(std::vector(length)); 95 | std::vector& list = std::get>(*tag); 96 | for (std::int32_t i = 0; i < length; i++) { 97 | list[i] = readTag(reader); 98 | } 99 | return tag; 100 | } 101 | 102 | inline ListTagPtr read_void_list_tag(BinaryReader& reader) 103 | { 104 | std::int32_t length = reader.read_numeric(); 105 | if (length < 0) { 106 | length = 0; 107 | } 108 | if (length != 0) { 109 | throw std::runtime_error("Void list tag must have a length of 0"); 110 | } 111 | return std::make_shared(); 112 | } 113 | 114 | inline ListTagPtr read_list_tag(BinaryReader& reader) 115 | { 116 | std::uint8_t tag_type = reader.read_numeric(); 117 | switch (tag_type) { 118 | case 0: 119 | return read_void_list_tag(reader); 120 | case tag_id_v: 121 | return read_numeric_list_tag(reader); 122 | case tag_id_v: 123 | return read_numeric_list_tag(reader); 124 | case tag_id_v: 125 | return read_numeric_list_tag(reader); 126 | case tag_id_v: 127 | return read_numeric_list_tag(reader); 128 | case tag_id_v: 129 | return read_numeric_list_tag(reader); 130 | case tag_id_v: 131 | return read_numeric_list_tag(reader); 132 | case tag_id_v: 133 | return read_template_list_tag>(reader); 134 | case tag_id_v: 135 | return read_template_list_tag(reader); 136 | case tag_id_v: 137 | return read_template_list_tag(reader); 138 | case tag_id_v: 139 | return read_template_list_tag(reader); 140 | case tag_id_v: 141 | return read_template_list_tag>(reader); 142 | case tag_id_v: 143 | return read_template_list_tag>(reader); 144 | default: 145 | throw std::runtime_error("This shouldn't happen"); 146 | } 147 | }; 148 | 149 | inline TagNode read_node(BinaryReader& reader, std::uint8_t tag_id) 150 | { 151 | switch (tag_id) { 152 | case tag_id_v: 153 | return read_numeric_tag(reader); 154 | case tag_id_v: 155 | return read_numeric_tag(reader); 156 | case tag_id_v: 157 | return read_numeric_tag(reader); 158 | case tag_id_v: 159 | return read_numeric_tag(reader); 160 | case tag_id_v: 161 | return read_numeric_tag(reader); 162 | case tag_id_v: 163 | return read_numeric_tag(reader); 164 | case tag_id_v: 165 | return read_array_tag(reader); 166 | case tag_id_v: 167 | return read_string_tag(reader); 168 | case tag_id_v: 169 | return read_list_tag(reader); 170 | case tag_id_v: 171 | return read_compound_tag(reader); 172 | case tag_id_v: 173 | return read_array_tag(reader); 174 | case tag_id_v: 175 | return read_array_tag(reader); 176 | default: 177 | throw std::runtime_error("Unsupported tag type " + std::to_string(tag_id)); 178 | } 179 | }; 180 | 181 | NamedTag decode_nbt(BinaryReader& reader, bool named) 182 | { 183 | std::uint8_t tag_id = reader.read_numeric(); 184 | std::string name = named ? read_string_tag(reader) : ""; 185 | TagNode node = read_node(reader, tag_id); 186 | return NamedTag(name, node); 187 | } 188 | 189 | // Read one (un)named tag from the string at position offset. 190 | NamedTag decode_nbt(std::string_view raw, std::endian endianness, Amulet::StringDecoder string_decode, size_t& offset, bool named) 191 | { 192 | BinaryReader reader(raw, offset, endianness, string_decode); 193 | auto tag = decode_nbt(reader, named); 194 | offset = reader.get_position(); 195 | return tag; 196 | } 197 | 198 | // Read one (un)named tag from the string. 199 | NamedTag decode_nbt(std::string_view raw, std::endian endianness, Amulet::StringDecoder string_decode, bool named) 200 | { 201 | size_t offset = 0; 202 | return decode_nbt(raw, endianness, string_decode, offset, named); 203 | } 204 | 205 | // Read count (un)named tags from the string at position offset. 206 | std::vector decode_nbt_array(std::string_view raw, std::endian endianness, Amulet::StringDecoder string_decode, size_t& offset, size_t count, bool named) 207 | { 208 | BinaryReader reader(raw, offset, endianness, string_decode); 209 | std::vector out; 210 | for (size_t i = 0; i < count; i++) { 211 | out.push_back(decode_nbt(reader, named)); 212 | } 213 | offset = reader.get_position(); 214 | return out; 215 | } 216 | 217 | // Read all (un)named tags from the string at position offset. 218 | std::vector decode_nbt_array(std::string_view raw, std::endian endianness, Amulet::StringDecoder string_decode, size_t& offset, bool named) 219 | { 220 | BinaryReader reader(raw, offset, endianness, string_decode); 221 | std::vector out; 222 | while (reader.has_more_data()) { 223 | out.push_back(decode_nbt(reader, named)); 224 | } 225 | offset = reader.get_position(); 226 | return out; 227 | } 228 | } // namespace NBT 229 | } // namespace Amulet 230 | -------------------------------------------------------------------------------- /tools/generate_pybind_stubs.py: -------------------------------------------------------------------------------- 1 | import os 2 | import glob 3 | import importlib.util 4 | import sys 5 | import subprocess 6 | import re 7 | import pybind11_stubgen 8 | from pybind11_stubgen.structs import Identifier 9 | from pybind11_stubgen.parser.mixins.filter import FilterClassMembers 10 | 11 | 12 | ForwardRefPattern = re.compile(r"ForwardRef\('(?P[a-zA-Z_][a-zA-Z0-9_]*)'\)") 13 | 14 | QuotePattern = re.compile(r"'(?P[a-zA-Z_][a-zA-Z0-9_]*)'") 15 | 16 | 17 | def fix_value(value: str) -> str: 18 | value = value.replace("NoneType", "None") 19 | value = ForwardRefPattern.sub(lambda match: match.group("variable"), value) 20 | value = QuotePattern.sub(lambda match: match.group("variable"), value) 21 | return value 22 | 23 | 24 | UnionPattern = re.compile( 25 | r"^(?P[a-zA-Z_][a-zA-Z0-9_]*): (types\.UnionType|typing\._UnionGenericAlias)\s*#\s*value = (?P.*)$", 26 | flags=re.MULTILINE, 27 | ) 28 | 29 | 30 | def union_sub_func(match: re.Match[str]) -> str: 31 | return f'{match.group("variable")}: typing.TypeAlias = {fix_value(match.group("value"))}' 32 | 33 | 34 | ClassVarUnionPattern = re.compile( 35 | r"(?P[a-zA-Z_][a-zA-Z0-9_]*): typing\.ClassVar\[types\.UnionType]\s*#\s*value = (?P.*)$", 36 | flags=re.MULTILINE, 37 | ) 38 | 39 | 40 | def class_var_union_sub_func(match: re.Match) -> str: 41 | return f'{match.group("variable")}: typing.TypeAlias = {fix_value(match.group("value"))}' 42 | 43 | 44 | VersionPattern = re.compile(r"(?P[a-zA-Z0-9_].*): str = '.*?'") 45 | 46 | 47 | def str_sub_func(match: re.Match) -> str: 48 | return f"{match.group('var')}: str" 49 | 50 | 51 | CompilerConfigPattern = re.compile(r"compiler_config: dict.*") 52 | 53 | 54 | def compiler_config_sub_func(match: re.Match) -> str: 55 | return "compiler_config: dict" 56 | 57 | 58 | EqPattern = re.compile( 59 | r"(?P[ \t]+)def __eq__\(self, arg0: (?P[a-zA-Z1-9.]+)\) -> (?P[a-zA-Z1-9.]+):" 60 | r"(?P\s*((\.\.\.)|(\"\"\"(.|\n)*?\"\"\")))" 61 | ) 62 | 63 | 64 | def eq_sub_func(match: re.Match[str]) -> str: 65 | """ 66 | if one - add @overload and overloaded signature 67 | 68 | """ 69 | if match.string[: match.start()].endswith("@typing.overload\n"): 70 | # is overload 71 | if re.match( 72 | f"\\n{match.group('indent')}@typing.overload\\n{match.group('indent')}def __eq__\\(self, ", 73 | match.string[match.end() :], 74 | ): 75 | # is not last overload 76 | return match.group() 77 | else: 78 | return "\n".join( 79 | [ 80 | f"{match.group('indent')}def __eq__(self, other: {match.group('other')}) -> {match.group('return')}:{match.group('ellipsis_docstring')}", 81 | f"{match.group('indent')}@typing.overload", 82 | f"{match.group('indent')}def __eq__(self, other: typing.Any) -> bool | types.NotImplementedType: ...", 83 | ] 84 | ) 85 | else: 86 | return "\n".join( 87 | [ 88 | f"{match.group('indent')}@typing.overload", 89 | f"{match.group('indent')}def __eq__(self, other: {match.group('other')}) -> {match.group('return')}:{match.group('ellipsis_docstring')}", 90 | f"{match.group('indent')}@typing.overload", 91 | f"{match.group('indent')}def __eq__(self, other: typing.Any) -> bool | types.NotImplementedType: ...", 92 | ] 93 | ) 94 | 95 | 96 | GenericAliasPattern = re.compile( 97 | r"(?P[a-zA-Z0-9]+): types.GenericAlias\s*# value = (?P.*)" 98 | ) 99 | 100 | 101 | def generic_alias_sub_func(match: re.Match) -> str: 102 | return f'{match.group("variable")}: typing.TypeAlias = {fix_value(match.group("value"))}' 103 | 104 | 105 | def get_module_path(name: str) -> str: 106 | spec = importlib.util.find_spec(name) 107 | assert spec is not None 108 | module_path = spec.origin 109 | assert module_path is not None 110 | return module_path 111 | 112 | 113 | def get_package_dir(name: str) -> str: 114 | return os.path.realpath(os.path.dirname(get_module_path(name))) 115 | 116 | 117 | def patch_stubgen() -> None: 118 | class_member_blacklist: set[Identifier] = FilterClassMembers._FilterClassMembers__class_member_blacklist # type: ignore 119 | attribute_blacklist: set[Identifier] = FilterClassMembers._FilterClassMembers__attribute_blacklist # type: ignore 120 | 121 | # Is there a better way to add items to the blacklist? 122 | # Pybind11 123 | class_member_blacklist.add(Identifier("_pybind11_conduit_v1_")) 124 | # Python 125 | class_member_blacklist.add(Identifier("__new__")) 126 | class_member_blacklist.add(Identifier("__subclasshook__")) 127 | # Pickle 128 | class_member_blacklist.add(Identifier("__getnewargs__")) 129 | class_member_blacklist.add(Identifier("__getstate__")) 130 | class_member_blacklist.add(Identifier("__setstate__")) 131 | # ABC 132 | attribute_blacklist.add(Identifier("__abstractmethods__")) 133 | attribute_blacklist.add(Identifier("__orig_bases__")) 134 | attribute_blacklist.add(Identifier("__parameters__")) 135 | attribute_blacklist.add(Identifier("_abc_impl")) 136 | # Protocol 137 | attribute_blacklist.add(Identifier("__protocol_attrs__")) 138 | attribute_blacklist.add(Identifier("__non_callable_proto_members__")) 139 | attribute_blacklist.add(Identifier("_is_protocol")) 140 | attribute_blacklist.add(Identifier("_is_runtime_protocol")) 141 | # dataclass 142 | attribute_blacklist.add(Identifier("__dataclass_fields__")) 143 | attribute_blacklist.add(Identifier("__dataclass_params__")) 144 | attribute_blacklist.add(Identifier("__match_args__")) 145 | # Buffer protocol 146 | class_member_blacklist.add(Identifier("__buffer__")) 147 | class_member_blacklist.add(Identifier("__release_buffer__")) 148 | 149 | 150 | def main() -> None: 151 | root_path = os.path.dirname(os.path.dirname(__file__)) 152 | src_path = os.path.join(root_path, "src") 153 | amulet_nbt_path = get_package_dir("amulet.nbt") 154 | tests_path = os.path.join(root_path, "tests") 155 | test_amulet_nbt_path = os.path.join(tests_path, "test_amulet_nbt") 156 | 157 | # make tests importable 158 | sys.path.append(tests_path) 159 | 160 | # out_dir, module_dir, module_name 161 | modules: list[tuple[str, str, str]] = [ 162 | (src_path, amulet_nbt_path, "amulet.nbt"), 163 | (tests_path, test_amulet_nbt_path, "test_amulet_nbt"), 164 | ] 165 | 166 | # Remove all existing stub files 167 | print("Removing stub files...") 168 | for _, module_dir, _ in modules: 169 | for stub_path in glob.iglob( 170 | os.path.join(glob.escape(module_dir), "**", "*.pyi"), recursive=True 171 | ): 172 | os.remove(stub_path) 173 | 174 | # Extend pybind11-stubgen 175 | patch_stubgen() 176 | 177 | # Call pybind11-stubgen 178 | print("Running pybind11-stubgen...") 179 | for out_dir, _, module_name in modules: 180 | pybind11_stubgen.main( 181 | [ 182 | f"--output-dir={out_dir}", 183 | module_name, 184 | ] 185 | ) 186 | 187 | # Run normal stubgen on the python files 188 | # print("Running stubgen...") 189 | # stubgen.main([ 190 | # *glob.glob( 191 | # os.path.join(glob.escape(package_path), "**", "*.py"), recursive=True 192 | # ), 193 | # "-o", 194 | # package_path, 195 | # "--include-docstrings", 196 | # ]) 197 | 198 | # Remove stub files generated for python modules 199 | for _, module_dir, _ in modules: 200 | for stub_path in glob.iglob( 201 | os.path.join(glob.escape(module_dir), "**", "*.pyi"), recursive=True 202 | ): 203 | if os.path.isfile(stub_path[:-1]) and not stub_path.endswith( 204 | "__init__.pyi" 205 | ): 206 | os.remove(stub_path) 207 | 208 | print("Patching stub files...") 209 | # Fix some issues and reformat the stub files. 210 | stub_paths = [] 211 | for _, module_dir, _ in modules: 212 | stub_paths.extend( 213 | glob.glob( 214 | os.path.join(glob.escape(module_dir), "**", "*.pyi"), recursive=True 215 | ) 216 | ) 217 | for stub_path in stub_paths: 218 | with open(stub_path, encoding="utf-8") as f: 219 | pyi = f.read() 220 | pyi = UnionPattern.sub(union_sub_func, pyi) 221 | pyi = ClassVarUnionPattern.sub(class_var_union_sub_func, pyi) 222 | pyi = VersionPattern.sub(str_sub_func, pyi) 223 | pyi = CompilerConfigPattern.sub(compiler_config_sub_func, pyi) 224 | pyi = GenericAliasPattern.sub(generic_alias_sub_func, pyi) 225 | pyi = pyi.replace( 226 | "__hash__: typing.ClassVar[None] = None", 227 | "__hash__: typing.ClassVar[None] = None # type: ignore", 228 | ) 229 | pyi = EqPattern.sub(eq_sub_func, pyi) 230 | pyi = pyi.replace("**kwargs)", "**kwargs: typing.Any)") 231 | pyi_split = [l.rstrip("\r") for l in pyi.split("\n")] 232 | for hidden_import in ["typing", "types"]: 233 | if hidden_import in pyi and f"import {hidden_import}" not in pyi_split: 234 | pyi_split.insert( 235 | pyi_split.index("from __future__ import annotations") + 1, 236 | f"import {hidden_import}", 237 | ) 238 | pyi = "\n".join(pyi_split) 239 | with open(stub_path, "w", encoding="utf-8") as f: 240 | f.write(pyi) 241 | 242 | subprocess.run( 243 | [ 244 | "isort", 245 | *stub_paths, 246 | ] 247 | ) 248 | 249 | subprocess.run( 250 | [ 251 | "autoflake", 252 | "--in-place", 253 | "--remove-unused-variables", 254 | *stub_paths, 255 | ] 256 | ) 257 | 258 | subprocess.run( 259 | [sys.executable, "-m", "black", *[module_dir for _, module_dir, _ in modules]] 260 | ) 261 | 262 | 263 | if __name__ == "__main__": 264 | main() 265 | --------------------------------------------------------------------------------