Imports System.IO
Imports System.IO.Compression
Imports System.Text
'''
''' This class can be used to deal with NBT-Files, specified by Notch.
''' At now only reading is supported..
'''
Public Class NBTFile
#Region "Class-Variables & Enum"
'''
''' Root-tag
'''
Protected _Tags As NBTTag
'''
''' The bytes of the current NBTFile
'''
Protected _aBytes As Byte()
'''
''' Current Position where to read in _aBytes
'''
Protected _iByteIndex As Integer = 0
'''
''' Name of the current file
'''
Protected _sFileName As String = ""
'''
''' The Magic Numbers from a GZip-File as Hex-String
'''
Protected Const GZIP_MAGIC_NUMBERS As String = "1F-8B-08"
'''
''' Enumeration for how much bytes need to be read for a numtype
'''
Public Enum ByteCount
Count_Byte = 1
Count_Short = 2
Count_Int = 4
Count_Long = 8
Count_Single = 4
Count_Double = 8
End Enum
'''
''' Enumeration needed to convert bytes to a number with _bytetoNum()
'''
Public Enum NumTypes
NUM_Short = 0
NUM_Integer = 1
NUM_Long = 2
NUM_Single = 3
NUM_Double = 4
End Enum
#End Region
#Region "Constructor & File-Operations"
'''
''' Skips reading a file and simply initiates via the given bytes
'''
''' Bytes from the NBT-File
Public Sub New(ByVal aFileBytes() As Byte)
Me.init(aFileBytes)
End Sub
'''
''' Reads the file from the given filename and initiates a new NBT-File
'''
''' The Path to the file to read
Public Sub New(ByVal sFileName As String)
Me._sFileName = sFileName
Dim aFileBytes() As Byte = Me._readFile()
Me.init(aFileBytes)
End Sub
'''
''' Reads all bytes from the file which path is storaged in _sFileName.
''' Additionally decompresses the file if it is compressed
'''
''' The raw/decompressed bytes from the file
Protected Function _readFile() As Byte()
'read the file out
Dim aBytes() As Byte = File.ReadAllBytes(Me._sFileName)
'check for compression
If Me._checkFileCompression(aBytes) Then
'if compressed first decompress
aBytes = Me._decompressBytes(aBytes)
End If
'now return the bytes raw/decompressed
Return aBytes
End Function
'''
''' Decompresses the given bytes via GZipStream and returns the result as Byte-Array
'''
''' The compressed bytes
''' The decompressed bytes as Byte-Array
Protected Function _decompressBytes(ByVal aBytes() As Byte) As Byte()
'create a bytestream
Dim oByteStream As New MemoryStream()
oByteStream.Write(aBytes, 0, aBytes.Length)
oByteStream.Position = 0
'create a gzip-stream to decompress the bytestream
Dim oGZipStream As New GZipStream(oByteStream, CompressionMode.Decompress)
'new byte list for individual length in result
Dim bResult = New List(Of Byte)
'count of bytes read in current step
Dim iBytesRead As Integer = 0
While True
'declaring buffer
Dim aBuffer = New Byte(1024) {}
'read from gzip-stream till buffer is full or end of stream
iBytesRead = oGZipStream.Read(aBuffer, 0, aBuffer.Length - 1)
'when bytes were read, write the buffer into the result
If iBytesRead > 0 Then
For iByteIndex As Integer = 0 To iBytesRead - 1
bResult.Add(aBuffer(iByteIndex))
Next
'otherwise stop reading
Else
Exit While
End If
End While
'close the streams
oByteStream.Close()
oGZipStream.Close()
'convert the result-list into an array and return it
Return bResult.ToArray()
End Function
'''
''' Compares the first 3 Bytes of the given ones with the GZIP_MAGIC_NUMBERS to
''' check if the bytes are compressed with GZip
'''
''' Bytes from File
''' True, if compressed, otherwise false
Protected Function _checkFileCompression(ByVal aFileBytes() As Byte) As Boolean
'Read the magic numbers of the filebytes
Dim sFileNumbers As String = BitConverter.ToString(aFileBytes, 0, 3)
'compare it with them from gzip to check if the file is compressed
If sFileNumbers = NBTFile.GZIP_MAGIC_NUMBERS Then
Return True
Else
Return False
End If
End Function
'''
''' Initiates a NBTFile by the given bytes, sets the bytes into the class-variable _aBytes
''' and reads till the root-compound-tag starts, then initiates reading it and saves it into
''' _Tags
'''
''' The bytes of the NBTFile
Public Sub init(ByVal aBytes() As Byte)
'setting bytes in class-variable
Me._aBytes = aBytes
'Read the Root-Tag
Me._Tags = Me._readTag(NBTTag.TagType.TAG_Compound, True, False)
End Sub
#End Region
#Region "Reading Tags from the Byte-Array"
'''
''' Reads the Tag on the current Position in _aBytes and returns it as NBTTag
'''
''' The Type of the Tag to read, if equals NBTTag.TagType.Empty, which is default, reads the fallowing tag-type directly from the bytes
''' If true reads the name of the current Tag, otherwise skips this step
''' If true and a TagType is forced, skips the next byte
''' Returns the read NBTTag from _aBytes as NBTTag
Protected Function _readTag(Optional ByVal iTagType As NBTTag.TagType = NBTTag.TagType.Empty, Optional ByVal blNamed As Boolean = True, Optional ByVal blSkipIfForced As Boolean = True) As NBTTag
Dim sTagName As String = ""
'No tag-type was given, so read the next type out
If iTagType = NBTTag.TagType.Empty Then
Dim bTagByte As Byte = Me._getBytes(1)
iTagType = bTagByte
Dim iReloaded As Integer = 0
ElseIf blSkipIfForced = True Then
'otherwise if a tag type should be forced, skip the next byte
Me._incrementIndex()
End If
If iTagType = NBTTag.TagType.TAG_End Then
'TAG_End has no name so dont read it
blNamed = False
End If
'Create a new tag which will get filled with informations
Dim oCurrentTag As NBTTag = New NBTTag(iTagType)
'If the tag is named, read the name out and set it to the tag
If blNamed Then
sTagName = Me._getCurrentTagName()
oCurrentTag.setName(sTagName)
End If
'Switch by tag-type
Select Case (iTagType)
Case NBTTag.TagType.TAG_String
'first get the length of the coming string
Dim oLengthTag As NBTTag = Me._readTag(NBTTag.TagType.TAG_Short, False, False)
Dim stLength As Short = oLengthTag.getValue()
If stLength > 0 Then
'read the value-bytes
Dim aValueBytes() As Byte
If stLength = 1 Then
aValueBytes = New Byte() {Me._getBytes(stLength)}
Else
aValueBytes = Me._getBytes(stLength)
End If
Dim oEncoding As Encoding = System.Text.Encoding.UTF8
'convert & save it
Dim sValue As String = oEncoding.GetString(aValueBytes)
oCurrentTag.setValue(sValue)
Else
'We've nothing to read because the length is 0
oCurrentTag.setValue("")
End If
Case NBTTag.TagType.TAG_Byte
Dim aValueByte = Me._getBytes(NBTFile.ByteCount.Count_Byte)
oCurrentTag.setValue(aValueByte)
Case NBTTag.TagType.TAG_Short
Dim aValueBytes = Me._getBytes(NBTFile.ByteCount.Count_Short)
Dim stValue As Short = Me._byteToNum(aValueBytes, NumTypes.NUM_Short)
oCurrentTag.setValue(stValue)
Case NBTTag.TagType.TAG_Int
Dim aValueBytes = Me._getBytes(NBTFile.ByteCount.Count_Int)
Dim iValue As Integer = Me._byteToNum(aValueBytes, NumTypes.NUM_Integer)
oCurrentTag.setValue(iValue)
Case NBTTag.TagType.TAG_Long
Dim aValueBytes = Me._getBytes(NBTFile.ByteCount.Count_Long)
Dim iValue As Long = Me._byteToNum(aValueBytes, NumTypes.NUM_Long)
oCurrentTag.setValue(iValue)
Case NBTTag.TagType.TAG_Float
Dim aValueBytes = Me._getBytes(NBTFile.ByteCount.Count_Single)
Dim sglValue As Single = Me._byteToNum(aValueBytes, NumTypes.NUM_Single)
oCurrentTag.setValue(sglValue)
Case NBTTag.TagType.TAG_Double
Dim aValueBytes = Me._getBytes(NBTFile.ByteCount.Count_Double)
Dim dblValue As Double = Me._byteToNum(aValueBytes, NumTypes.NUM_Double)
oCurrentTag.setValue(dblValue)
Case NBTTag.TagType.TAG_Compound
oCurrentTag = Me._readCompound(sTagName)
Case NBTTag.TagType.TAG_Int_Array
oCurrentTag = Me._readIntArray(sTagName)
Case NBTTag.TagType.TAG_Byte_Array
oCurrentTag = Me._readByteArray(sTagName)
Case NBTTag.TagType.TAG_List
oCurrentTag = Me._readList(sTagName)
End Select
Return oCurrentTag
End Function
'''
''' Reads a TAG_List from the current position in _aBytes
'''
''' The name of the list as string
''' The finished TAG_List as NBTTag
Protected Function _readList(ByVal sTagName As String) As NBTTag
Dim oTagList As NBTTag = New NBTTag(NBTTag.TagType.TAG_List)
oTagList.setName(sTagName)
'read the tag-id of the list-items and their count
Dim iTagId As NBTTag.TagType = Me._readTag(NBTTag.TagType.TAG_Byte, False, False).getValue()
Dim iItemCount As Integer = Me._readTag(NBTTag.TagType.TAG_Int, False, False).getValue()
'go through them all and add as children
For i = 0 To iItemCount - 1
Dim oCurrentTag As NBTTag = Me._readTag(iTagId, False, False)
oTagList.addChild(oCurrentTag)
Next
Return oTagList
End Function
'''
''' Reads a TAG_Byte_Array from the current position in _aBytes
'''
''' The name of the array as string
''' The finished TAG_Byte_Array as NBTTag
Protected Function _readByteArray(ByVal sTagName As String) As NBTTag
Dim oByteArray As NBTTag = New NBTTag(NBTTag.TagType.TAG_Byte_Array)
oByteArray.setName(sTagName)
'read the count of items in array
Dim iItemCount As Integer = Me._readTag(NBTTag.TagType.TAG_Int, False, False).getValue()
'read them all and add as children
For i = 0 To iItemCount - 1
Dim oCurrentTag As NBTTag = Me._readTag(NBTTag.TagType.TAG_Byte, False, False)
oByteArray.addChild(oCurrentTag)
Next
Return oByteArray
End Function
'''
''' Reads a TAG_Int_Array from the current position in _aBytes
'''
''' The name of the array as string
''' The finished TAG_Int_Array as NBTTag
Protected Function _readIntArray(ByVal sTagName As String) As NBTTag
Dim oIntArray As NBTTag = New NBTTag(NBTTag.TagType.TAG_Int_Array)
oIntArray.setName(sTagName)
'read the count of items in array
Dim iItemCount As Integer = Me._readTag(NBTTag.TagType.TAG_Int, False, False).getValue()
'read them all and add as children
For i = 0 To iItemCount - 1
Dim oCurrentTag As NBTTag = Me._readTag(NBTTag.TagType.TAG_Int, False, False)
oIntArray.addChild(oCurrentTag)
Next
Return oIntArray
End Function
'''
''' Reads a TAG_Compound from the current position in _aBytes
'''
''' The name of the compound as string
''' The finished TAG_Compound as NBTTag
Protected Function _readCompound(ByVal sTagName As String) As NBTTag
Dim oCompound As NBTTag = New NBTTag(NBTTag.TagType.TAG_Compound)
oCompound.setName(sTagName)
Dim oChildTag As NBTTag = Me._readTag()
'While the read tag's type not TAG_End read all tags and add them as child to the compound
While oChildTag.getTagType() <> NBTTag.TagType.TAG_End
oCompound.addChild(oChildTag)
oChildTag = Me._readTag()
End While
Return oCompound
End Function
'''
''' Reads the Name of the current Tag
'''
''' String
Protected Function _getCurrentTagName() As String
Dim oNameTag As NBTTag = Me._readTag(NBTTag.TagType.TAG_String, False, False)
Return oNameTag.getValue().ToString()
End Function
#End Region
#Region "Byte-Reading- & Converting Operations"
'''
''' Increments the index in the bytes by the given count or 1 as default
'''
Protected Sub _incrementIndex(Optional ByVal iCount As Integer = 1)
Me._iByteIndex += iCount
End Sub
'''
''' Converts the given bytes into the given NumType
'''
''' Bytes to convert
''' The type in which the bytes should be converted to
''' The converted and casted number
'''
Protected Function _byteToNum(ByVal bBytes() As Byte, ByVal iType As NBTFile.NumTypes) As Object
Dim ReturnValue As Object
If BitConverter.IsLittleEndian Then
'reverse the bytes to get a big endian
bBytes = bBytes.Reverse().ToArray()
End If
Select Case iType
Case NumTypes.NUM_Short
ReturnValue = BitConverter.ToInt16(bBytes, 0)
Case NumTypes.NUM_Integer
ReturnValue = BitConverter.ToInt32(bBytes, 0)
Case NumTypes.NUM_Long
ReturnValue = BitConverter.ToInt64(bBytes, 0)
Case NumTypes.NUM_Single
ReturnValue = BitConverter.ToSingle(bBytes, 0)
Case NumTypes.NUM_Double
ReturnValue = BitConverter.ToDouble(bBytes, 0)
Case Else
'Default just ReturnValue = normal int
ReturnValue = BitConverter.ToInt32(bBytes, 0)
End Select
Return ReturnValue
End Function
'''
''' Get an amound of bytes from the current index in the file-bytes by the given count
''' and increments the index for each byte read
'''
''' The count as integer how much bytes to read
''' If count equals 1 just returns a single byte, otherwise array of bytes
Protected Function _getBytes(ByVal iCount As Integer) As Object
Dim aBytes() As Byte = New Byte(iCount - 1) {}
For iByteIndex As Integer = 0 To iCount - 1
Me._incrementIndex()
aBytes(iByteIndex) = Me._aBytes(Me._iByteIndex)
Next iByteIndex
If aBytes.Count = 1 Then
Return aBytes(0)
End If
Return aBytes
End Function
#End Region
#Region "Reading the finished Data"
'''
''' Returns the Root-NBTTag
'''
''' The Root-NBTTag
Public Function getData() As NBTTag
Return Me._Tags
End Function
'''
''' Tries to read a tag by the given "Path" as string, starting 1 level deeper root.
''' It should look like "Player/abilities/flying".
''' This doesn't work with lists, as their items aren't named.
''' This method goes through all tags at the current level and enters the next deeper if a matching
''' name was found, so if a tag-name in 1 level isn't unique the first in file will be entered.
''' Therefore if the whole level was scanned and no matching tag was found, returns the current level's parent-tag
'''
''' The Path to the Tag want to read, format: "Level1/Level2/TagToRead". Simply "TagToRead" is also be supported
''' Returns the found tag(or root-tag resp. current's level parent-tag) as NBTTag
Public Function readTag(ByVal sReadString As String) As NBTTag
'at first split the path if more then 1 level was found
Dim aPath(1) As String
If sReadString.Contains("/") Then
aPath = sReadString.Split("/")
Else
aPath(0) = sReadString
End If
'the current tag is root
Dim oCurTag As NBTTag = Me.getData()
'at now we didn't found any children from path
Dim blChildFound As Boolean = False
For Each sChildName As String In aPath
'reset found
blChildFound = False
If sChildName <> "" Then
For Each oChildTag As NBTTag In oCurTag.getChildren()
'if current child's tagname equals the current level in path set it as current child
'and set found to true
If oChildTag.getName() = sChildName Then
blChildFound = True
oCurTag = oChildTag
End If
Next
If blChildFound = False Then
'didn't found any child so the path couldn't be correct
Return oCurTag
End If
End If
Next
Return oCurTag
End Function
'''
''' Same as readTag just returns the value of the read tag
'''
''' Path to the Tag
''' The value from the found tag as Object
''' readTag
Public Function read(ByVal sPath As String) As Object
Return Me.readTag(sPath).getValue()
End Function
#End Region
End Class
'''
''' Class to Handle NBT-Tags
'''
Public Class NBTTag
'''
''' The Name of the Tag
'''
Protected _sName As String = ""
'''
''' Value of the Tag as Object
'''
Protected _oValue As Object = 0
'''
''' Type of the Tag as NBTTag.TagType
'''
Protected _iTagType As NBTTag.TagType = NBTTag.TagType.Empty
'''
''' All Children 1 level deeper of the current Tag as List(Of NBTTag)
'''
Protected _aChildTags As List(Of NBTTag) = New List(Of NBTTag)
'''
''' Enumeration to compare and define tag-types
'''
Enum TagType
Empty = -1
TAG_End = 0
TAG_Byte = 1
TAG_Short = 2
TAG_Int = 3
TAG_Long = 4
TAG_Float = 5
TAG_Double = 6
TAG_Byte_Array = 7
TAG_String = 8
TAG_List = 9
TAG_Compound = 10
TAG_Int_Array = 11
End Enum
'''
''' Constructor with setting the tag-type as NBTTag.TagType
'''
''' The Type of the current Tag as NBTTag.TagType
Public Sub New(ByVal iTagType As TagType)
Me._iTagType = iTagType
End Sub
'''
''' Sets the name of the tag
'''
''' The name of the tag as string
Public Sub setName(ByVal sName As String)
Me._sName = sName
End Sub
'''
''' Sets the value to the tag as object
'''
''' The value from the tag as object
Public Sub setValue(ByVal oValue As Object)
Me._oValue = oValue
End Sub
'''
''' Casts the value from the current tag by the tagtype and returns it
'''
''' Casted tag's value
Public Function getValue() As Object
Select Case Me._iTagType
Case NBTTag.TagType.TAG_Byte
Return DirectCast(Me._oValue, Byte)
Case NBTTag.TagType.TAG_Double
Return DirectCast(Me._oValue, Double)
Case NBTTag.TagType.TAG_Float
Return DirectCast(Me._oValue, Single)
Case NBTTag.TagType.TAG_Int
Return DirectCast(Me._oValue, Integer)
Case NBTTag.TagType.TAG_Long
Return DirectCast(Me._oValue, Long)
Case NBTTag.TagType.TAG_Short
Return DirectCast(Me._oValue, Short)
Case NBTTag.TagType.TAG_String
Return DirectCast(Me._oValue, String)
Case Else
Return Me._oValue
End Select
End Function
'''
''' Adds the given NBTTag to the children of the tag
'''
''' The child to add as NBTTag
Public Sub addChild(ByVal oChild As NBTTag)
Me._aChildTags.Add(oChild)
End Sub
'''
''' Getter for the TagType of the tag
'''
''' The TagType of the tag as NBTTag.TagType
Public Function getTagType() As TagType
Return Me._iTagType
End Function
'''
''' Getter for the Children from the tag
'''
''' The Children from the tag as List(Of NBTTag)
Public Function getChildren() As List(Of NBTTag)
Return Me._aChildTags
End Function
'''
''' Getter for the Tag's name
'''
''' The name of the tag as string
Public Function getName() As String
Return Me._sName
End Function
End Class