Commits

zzzeek  committed 06be850

initial rev

  • Participants

Comments (0)

Files changed (11)

+syntax:regexp
+^build/
+^doc/build/output
+.pyc$
+.orig$
+.egg-info
+.*,cover
+.un~
+\.coverage
+\.DS_Store
+test.cfg
+========
+nbt2yaml
+========
+
+Synopsis
+========
+
+nbt2yaml presents a Python parser/writer for Minecraft NBT files.   It then includes a system that can marshal
+this format to/from Yaml files.   Finally, it provides simple shell commands to provide these transformations,
+including ``editnbt``, which will shell out the Yaml version of the file to your editor of choice, allowing
+easy command-line editing of NBT files.
+
+NBT format:  http://www.minecraft.net/docs/NBT.txt
+Yaml: http://www.yaml.org/
+
+Installation
+============
+
+Install via pip is easiest::
+
+    pip install http://bitbucket.org/zzzeek/nbt2yaml
+
+Usage
+=====
+
+Once installed, the ``nbtedit`` command should be available::
+
+    nbtedit --help
+
+    nbtedit <file>
+
+The script uses the standard ``EDITOR`` environment variable to determine which
+text editor should be invoked.
+
+

File nbt2yaml/__init__.py

+from parse import parse_nbt
+from yamlgen import to_yaml

File nbt2yaml/parse.py

+import struct
+import gzip
+from collections import namedtuple
+
+class Tag(object):
+    _tags = {}
+    _tuple = namedtuple("Tag", ["type", "name", "data"])
+
+    def __init__(self, name, id):
+        self.name = name
+        Tag._tags[id] = self
+
+    @classmethod
+    def from_stream(cls, stream):
+        return cls._tags[struct.unpack('>b', stream.read(1))[0]]
+
+    def parse(self, stream):
+        name = TAG_String._parse_impl(stream)
+        data = self._parse_impl(stream)
+        return Tag._tuple(self, name, data)
+
+    def _parse_impl(self):
+        raise NotImplementedError()
+
+    def __repr__(self):
+        return "TAG_%s" % self.name
+
+class FixedTag(Tag):
+    def __init__(self, name, id, size, format):
+        Tag.__init__(self, name, id)
+        self.size = size
+        self.format = format
+
+    def _parse_impl(self, stream):
+        return struct.unpack(">" + self.format, stream.read(self.size))[0]
+
+class EndTag(Tag):
+    ""
+
+class VariableTag(Tag):
+    def __init__(self, name, id, length_tag, encoding=None):
+        Tag.__init__(self, name, id)
+        self.length_tag = length_tag
+        self.encoding = encoding
+
+    def _parse_impl(self, stream):
+        length = _data_length(self.length_tag, stream)
+        data = stream.read(length)
+        if self.encoding:
+            data = data.decode(self.encoding)
+        return data
+
+class ListTag(Tag):
+    def _parse_impl(self, stream):
+        element_type = Tag.from_stream(stream)
+        length = _data_length(TAG_Int, stream)
+
+        return [
+            element_type._parse_impl(stream) for i in xrange(length)
+        ]
+
+class CompoundTag(Tag):
+    def _parse_impl(self, stream):
+        data = []
+        while True:
+            c_type_ = Tag.from_stream(stream)
+            if c_type_ is TAG_End:
+                break
+            data.append(c_type_.parse(stream))
+        return data
+
+TAG_End = EndTag('end', 0)
+TAG_Byte = FixedTag('byte', 1, 1, 'b')
+TAG_Short = FixedTag('short', 2, 2, 'h')
+TAG_Int = FixedTag('int', 3, 4, 'i')
+TAG_Long = FixedTag('long', 4, 8, 'q')
+TAG_Float = FixedTag('float', 5, 4, 'f')
+TAG_Double = FixedTag('double', 6, 8, 'd')
+TAG_Byte_Array = VariableTag('byte_array', 7, TAG_Int)
+TAG_String = VariableTag('string', 8, TAG_Short, encoding='utf-8')
+TAG_List = ListTag('list', 9)
+TAG_Compound = CompoundTag('compound', 10)
+
+def _data_length(length_type, stream):
+    return struct.unpack(">" + length_type.format, stream.read(length_type.size))[0]
+
+def parse_nbt(stream, gzipped=True):
+    if gzipped:
+        stream = gzip.GzipFile(fileobj=stream)
+    type_ = Tag.from_stream(stream)
+    return type_.parse(stream)
+

File nbt2yaml/yamlgen.py

+import yaml
+from nbt2yaml import parse
+
+class YamlSerialize(object):
+    def __init__(self, struct):
+        self.tag, self.name, self.data = struct
+
+
+def _tag_representer(dumper, struct):
+    tag, name, data = struct.tag, struct.name, struct.data
+    if tag is parse.TAG_Compound:
+        data = [YamlSerialize(elem) for elem in data]
+    elif tag is parse.TAG_String:
+        data = data.encode('utf-8')
+
+    return dumper.represent_mapping(
+        u'!%s' % repr(tag), {
+            name.encode('utf-8'):data
+        },
+    )
+
+
+yaml.add_representer(YamlSerialize, _tag_representer)
+
+def to_yaml(struct):
+    print yaml.dump(YamlSerialize(struct))
+import os
+import sys
+
+from setuptools import setup, find_packages
+
+extra = {}
+if sys.version_info >= (3, 0):
+    extra.update(
+        use_2to3=True,
+    )
+
+readme = os.path.join(os.path.dirname(__file__), 'README.rst')
+
+setup(name='nbt2yaml',
+      version="0.1.0",
+      description="Read and write Minecraft NBT files using Yaml.",
+      long_description=file(readme).read(),
+      classifiers=[
+      'Development Status :: 3 - Alpha',
+      'License :: OSI Approved :: BSD License',
+      'Programming Language :: Python',
+      'Programming Language :: Python :: 3',
+      ],
+      keywords='minecraft',
+      author='Mike Bayer',
+      author_email='mike_mp@zzzcomputing.com',
+      url='http://bitbucket.org/zzzeek/nbt2yaml',
+      license='BSD',
+      packages=find_packages(exclude=['ez_setup', 'tests']),
+      zip_safe=False,
+      install_requires=['PyYAML'],
+      test_suite='nose.collector',
+      tests_require=['nose'],
+      **extra
+)

File tests/__init__.py

+import os
+
+def datafile(name):
+    return open(os.path.join(os.path.dirname(__file__), 'files', name))

File tests/files/bigtest.nbt

Binary file added.

File tests/files/test.nbt

Binary file added.

File tests/test_parse.py

+import unittest
+from nbt2yaml import parse_nbt
+from tests import datafile
+
+class ParseNBTTest(unittest.TestCase):
+    def test_basic(self):
+        print parse_nbt(datafile("test.nbt"))
+
+    def test_large(self):
+        print parse_nbt(datafile("bigtest.nbt"))

File tests/test_yaml.py

+import unittest
+from nbt2yaml import parse_nbt, to_yaml
+from tests import datafile
+
+class ToYamlTest(unittest.TestCase):
+    def test_basic(self):
+        data = parse_nbt(datafile("test.nbt"))
+        print "\n", to_yaml(data)
+
+    def test_large(self):
+        data = parse_nbt(datafile("bigtest.nbt"))
+        print "\n", to_yaml(data)