I've already written a thread on the sprite file structure, but I thought I'd post a new, updated, official one.
Note: A big thanks to Sketchy for the help with greatly increasing the speed of reading the sprite, specifically creating a bitmap of the sprite, by replacing Bitmap.SetPixel() with BitMap.LockBits()/Bitmap.UnlockBits().
- Like the Dat file structure, the sprite file begins with 4 bytes, which I call the SpriteVersion, and is unique for each client version so that the client knows that the correct sprite file is being used.
- This is followed by 2 bytes that contains the number of sprites in the sprite file.
- Immediately following this we have a 4-byte value for each sprite containing it's byte offset in the sprite file.
[x] = Number of bytes.
- Now we start getting in to the actual sprite data. Each sprite begins with the transparent-pixels' color. In this case, the Tibia client uses magenta so the three bytes returned are 255, 0, 255. You don't need this pixel when creating the bitmap.Code:[4] Sprite Version [2] Number of sprites [4] Offset of the first sprite [4] Offset of the second sprite [4] Offset of the third sprite ----Continue this for each sprite.
- Following this is 2 bytes indicating the size of the sprite. (All sprites are 32x32 pixels, 1024 pixels, so this should never change.)
- Next we start reading the actual pixel data of the sprite. This begins with 2 bytes holding the number of transparent pixels before colored pixel.
- Then 2 bytes holding the number of colored pixels before a transparent pixel. Immediately following this is three 1-byte values holding the RGB value of the pixel. For example, if the number of colored pixels returned is 3 then you would read the next 9 bytes (3 bytes for each pixel) to get the pixel information of each.
[x] = Number of bytes.
- In the way we used to make bitmap's from the sprite data in TibiaAPI (which you can view here: http://code.google.com/p/tibiaapi/so...priteReader.cs ) we used Bitmap.SetPixel() to set the pixel color in the bitmap. While this is an easy way to achieve what we want, it's also a bit slow. But, thanks to Sketchy, you can achieve a 10x speed increase by using Bitmap.LockBits() and Bitmap.UnlockBits(). (In my tests, I could load the whole 9.44 sprite file in 35 seconds using Bitmap.SetPixels(), but I could load it in just 3.5 seconds! using Bitmap.LockBits()/Bitmap.UnlockBits().)Code:[1] - red value of transparent pixels [1] - green value of transparent pixels [1] - blue value of transparent pixels [2] - number of transparent pixels [2] - number of colored pixels (for this example we'll say it's 3) [1] - red value of the first colored pixel [1] - green value of the first colored pixel [1] - blue value of the first colored pixel [1] - red value of the second colored pixel [1] - green value of the second colored pixel [1] - blue value of the second colored pixel [1] - red value of the third colored pixel [1] - green value of the third colored pixel [1] - blue value of the third colored pixel [2] - number of transparent pixels [2] - number of colored pixels ----This will repeat for all pixels in the sprite.----
Now, here's code used to read the sprite file that you can throw in to VB.Net and run. This uses the LockBits/UnlockBits method, and I have commented the code to help you understand what's going on. The sprite structure has been the same as far back as I can remember, so you shouldn't have a problem using this code for any client version. Also, this code reads and stores each sprite in an array, but you can modify it like the TibiaAPI code to only retrieve a specific sprite. If you have any questions feel free to ask:
[code=vb.net]
Public Class Form1
Dim SpriteArray As Image()
Private Sub Form1_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
Dim FileName As String = vbNullString
MessageBox.Show("Please locate and open your Tibia.spr file", ".spr Reader")
With OpenFileDialog1
.Filter = "Spr file (*.spr)|*.spr|" & "All files|*.*"
If .ShowDialog = Windows.Forms.DialogResult.OK Then
FileName = .FileName
Else
Application.Exit()
End If
End With
Using reader As New IO.BinaryReader(IO.File.OpenRead(FileName))
Dim SpriteVersion As Integer = reader.ReadUInt32()
Dim NumberOfSprites As Integer = reader.ReadUInt16()
SpriteArray = Array.CreateInstance(GetType(Image), NumberOfSprites)
For i As Integer = 0 To NumberOfSprites - 1 'even though sprite IDs start at 2, I started the loop at 0 for the array
Dim Sprite As Bitmap = New Bitmap(32, 32) 'this is the bitmap we'll be storing the sprite in, notice it's 32x32 pixels
Dim SpriteData As Drawing.Imaging.BitmapData = Sprite.LockBits(New Rectangle(0, 0, 32, 32), Imaging.ImageLockMode.ReadWrite, Imaging.PixelFormat.Format32bppArgb) 'here is where we lock the bits of the bitmap, since sprites are ARGB-based we have to make sure the format is the same
Dim ByteArray As Byte() = New Byte(4096) {} 'this is the array we'll store the pixel data in, even though a 32x32 pixel sprite has 1024 pixels we have to store 4 bits for each pixel (alpha, red, green blue)
Dim currentPixel As UShort = 0
Dim targetOffset As Long
'to begin, we have to seek to the sprite to get it's offset
'since I started the loop at 0 I have to add 1 because the sprites aren't 0-based (however, keep in mind sprite IDs begin at 2, not 1)
'each sprite is offset by 4 because the offset is stored as an integer (4 bytes)
'and the +6 is to skip past the SpriteVersion and NumberOfSprites
reader.BaseStream.Seek(6 + (i + 1) * 4, IO.SeekOrigin.Begin)
'now that we're at our desired sprite we have to read it's offset address (reader.ReadUInt32()), then we seek the returned value +3
'the +3 is to skip the transparent pixels' color, since we don't need it
reader.BaseStream.Seek(reader.ReadUInt32() + 3, IO.SeekOrigin.Begin)
'it seems as though sprite file for clients 7.40 and lower return a out-of-bounds position on the last sprite
'I didn't take the time to figure out why, but you can easily counter this problem with this line of code making sure the position
'of the stream is not beyond the file's size
If reader.BaseStream.Position >= reader.BaseStream.Length Then Continue For
targetOffset = reader.BaseStream.Position + reader.ReadUInt16() 'the ReadUInt16() returns the size of the sprite
If reader.BaseStream.Position < 9 Then Continue For 'if we encounter a blank sprite it actually sets the position of the stream at byte-5 and that's actually the beginning of the NumberOfSprites value, so we use Continue For to skip this sprite
While reader.BaseStream.Position < targetOffset
Dim transparentPixels As UShort = reader.ReadUInt16()
Dim coloredPixels As UShort = reader.ReadUInt16()
If (transparentPixels > 1024) OrElse (coloredPixels > 1024) Then Exit While 'as an extra padding of protection, I make sure the number of transparent and/or colored pixels doesn't exceed the size of a sprite
currentPixel += transparentPixels
For x As Integer = 0 To coloredPixels - 1
'here's where we get different from TibiaAPI's SetPixel()
'remember what I said about us storing all four pixel colors? here's where we use it
Dim CurrentOffset As Integer = (currentPixel * 4) 'this goes to the offset in ByteArray where the current pixel we're reading is going to be located
'pixel data is actually stored in reverse in bitmaps, so we write the Alpha value first (255) at the end of the pixel data, followed by Red before it, then Green before Red, and finally Blue at the beginning of the pixel data.
ByteArray(CurrentOffset + 3) = 255
ByteArray(CurrentOffset + 2) = reader.ReadByte()
ByteArray(CurrentOffset + 1) = reader.ReadByte()
ByteArray(CurrentOffset) = reader.ReadByte()
currentPixel += 1
Next
End While
Runtime.InteropServices.Marshal.Copy(ByteArray, 0, SpriteData.Scan0, 4096) 'here is where we copy the data in the byte array (where we stored the pixel data for the sprite) into our BitmapData
Sprite.UnlockBits(SpriteData) 'then we unlock the sprite so that we can use it
SpriteArray(i) = Sprite 'here I store the sprite in an Image array
Next
End Using
End Sub
End Class
[/code]
You can easily check each image by adding a PictureBox, TextBox, and Button to your form and using this code:
[code=vb.net]
'put this code in the button's click event
If IsNumeric(TextBox1.Text) AndAlso Int(TextBox1.Text) >= 2 Then 'this makes sure the text in the text box is a number and that's not lower than 2 because sprite IDs start at 2 and our array is 0-based
PictureBox1.Image = SpriteArray((Int(TextBox1.Text)) - 2) 'we use -2 here because of our array being 0-based and sprite IDs starting at 2
End If
[/code]