Swords of Glass graphic format
Format type | Image |
---|---|
Hardware | CGA |
Colour depth | 2-bit (CGA) |
Minimum size (pixels) | 1×1 |
Maximum size (pixels) | 30×50 |
Palette | Default CGA (0) |
Plane count | 2 |
Transparent pixels? | Yes |
Hitmap pixels? | No |
Games |
Swords of Glass's graphics are stored in SHAPES.DAT, CSHAPES.DAT, SHAPES1.DAT, CSHAPES1.DAT, and DOORS.DAT. Shapes contains the player graphics and all monster graphics, CShapes contains environment objects like chests and stairs, Doors contains wall graphics like doorknobs, gargoyles, and keyholes. There are 48 objects in Shapes, 9 in CShapes, and 71 in Doors. Shapes1 and CShapes1 contain the smaller versions of these graphics when they're seen in the distance and have the same number of shapes in each. Doors contains three sizes of each object. Small and medium exists for facing, left, and right, but there is only a large version of the facing graphic. The number of graphics stored in each file is stored in the COM executable, so adding more graphics would be more extensive hack. The Swords of Glass monster format specifies which graphic each monster will use. Both players use the same graphics in the dungeon, but player 2 appears to be XORed.
The format of Shapes1, CShapes1, and Doors is pretty straight-forward. Each record is a single Shape Data (header and CGA data) padded to the record size (468 bytes for shapes, 78 for Doors). These graphics do not support transparency.
The format in Shapes and CShapes is more complex because they support transparency. Each record in the begins with a 60-byte Shape Map for transparency, then has 60 Shape Data records corresponding to each byte in the Shape Map. Each Shape Data record is a 5x5 pixel block used to build up the larger 6x10 blocks image, resulting in a 30x50 pixel graphic. Each Shape Data record is padded out to 26 bytes.
Each Shape Data record has a header, a CGA bit stream where two bits represent a pixel, and padding to fill out the size of the record. The data is not even remotely optimized. Pixel data is stored even for transparent blocks, and, some of the pixel data is redundant because it's based on 4-pixel increments even when they aren't needed. For example, a 5x5 image (25 pixels, and thus 50 bits (7 bytes in 2-bit color) ends up requiring 10 bytes each time, in addition to all the wasted padding used.
The pixel blocks have a lot of what appears to be unused data. The padding following each record appears to be uncleared memory when the files were saved including snippets of source code (the same garbage can be seen in the non-graphics data files). Each Shape Data record begins with a 16-bit integer that is always 2. My guess is that this is the number of bits per pixel, but changing it doesn't appear to affect how the graphic is drawn by the game, so it's probably ignored. While the height and width are used to draw the Shape1 and CShape1 images, the 5x5 blocks of the Shape and CShape graphics must be hard-coded because changing them doesn't affect how they're drawn in the game.
Although the format's header technically allows images up to 65,536×65,536, the record size and game engine constrains them images to much smaller areas, and the largest monster ever drawn in the game is 30×50 pixels, and the largest wall graphic is 12×12.
Shape Map
Each record in Shape and CShape files begin with a shape map which it uses for transparency. The shape map is 60 bytes representing a 6x10 rectangle of boxes stored top-to-bottom, left-to-right. A value of 0x01 indicates the pixel area should be drawn, a 0x00 indicates it should not be drawn (i.e., transparent).
A Shape Map is always followed by 60 Shape Data records of size 5x5 pixels and padded out to 26 bytes.
Data type | Description |
---|---|
BYTE[60] Transparent | Transparency flag (00 - Yes, 01 - No) |
Shape Data
This is the actual graphic data. In Shape1, CShape1, and Doors, this is a single graphic image. In Shape and CShape, there will be 60 blocks that make up a single graphic.
Data type | Description |
---|---|
UINT16LE Unknown | Always 0x02 0x00. Doesn't appear to be used. Perhaps it was meant to be the number of bits-per-pixel the format uses? |
UINT16LE Width | Width of the image. |
UINT16LE Height | Height of the image. |
BYTE[ceiling(width / 4) × height] CGA bit stream | CGA graphic data. When converted to pixels, it is drawn left-to-right, bottom-to-top. |
BYTE[] Padding | Extra data to pad out the record. Shape/CShape use 26 byte blocks, Shape1/CShape1 use 468 bytes, Door uses 78 bytes. |
Source Code
Shape Viewer
This FreeBASIC program will cycle through all of the shapes in the specified file and draw them with the transparent sections noted. Press any key to view the next shape.
ScreenRes 320, 200, 32
Dim As UByte Map(0 To 47, 0 To 59)
Dim As UByte BlockData(0 To 9)
Dim As Integer Pixels(0 To 39)
Dim As UByte Buffer
Dim As Integer X, Y, I, MapNo, Pixel, BX, BY, ShapeNo, PixelColor
Dim As String BinText
Open "Shapes.dat" For Binary As #1
For ShapeNo = 0 To 47
Locate 1, 20: Print "Shape #: " + Str(ShapeNo)
' Load the map of the shape blocks.
' The map is a rectangle 6 units wide and 10 units tall.
' It is stored from top to bottom, right to left.
' Those map blocks that are 1 will be drawn with pixels in them,
' those that are 0 are transparent.
For MapNo = 0 To 59
Get #1, , Buffer
If Eof(1) Then
End
End If
Map(ShapeNo, MapNo) = Buffer
Next MapNo
' There are 60 blocks of data, corresponding to the 60 blocks in the map.
' Those blocks that are transparent still have space reserved in the file,
' So the file is a lot larger than it needs to be.
BX = 0: BY = 0
For MapNo = 0 To 59
' Each block's header is 0x02 0x00, 0x05 0x00, 0x05 0x00.
' This is an unknown value, then the width and height.
' But the game engine hard-codes these values, so we can skip them.
For I = 0 To 5
Get #1, , Buffer
Next I
' The next 10 bytes are a bitstream of CGA graphic data for a 5x5 pixel block.
For I = 0 To 9
Get #1, , Buffer
BlockData(I) = Buffer
Next I
' The next 10 bytes are different for each block, but they're identical for each shape.
' They appear to be padding and aren't needed to draw the graphic, so we throw them away.
For I = 0 To 9
Get #1, , Buffer
Next I
If Map(ShapeNo, MapNo) = 0 Then
' This section will be transparent. Draw a box to show it.
Line(BX, BY)-(BX + 4, BY + 4), RGB(31, 31, 31), B
Else
' Convert the bitstream into CGA pixels.
Pixel = 0
For I = 0 To 9
BinText = Bin(BlockData(I), 8)
For X = 1 To 8 Step 2
If Mid(BinText, X, 2) = "00" Then PixelColor = RGB(0, 0, 0)
If Mid(BinText, X, 2) = "01" Then PixelColor = RGB(0, 170, 0)
If Mid(BinText, X, 2) = "10" Then PixelColor = RGB(170, 0, 0)
If Mid(BinText, X, 2) = "11" Then PixelColor = RGB(170, 85, 0)
Pixels(Pixel) = PixelColor
Pixel = Pixel + 1
Next X
Next I
' Draw the CGA pixels in the 5x5 block.
' Pixels are drawn from left to right, bottom to top.
' The bitsteam stores redundant data pulled from the next block.
X = 0
Y = 4
For I = 0 To 39
' Skip redundant data.
If X < 5 Then
PSet(BX + X, BY + Y), Pixels(I)
End If
X = X + 1
If X = 8 Then
X = 0
Y = Y - 1
End If
Next I
End If
' Increase to the next block.
BY = BY + 5
If BY = 50 Then
BY = 0
BX = BX + 5
End If
Next MapNo
Sleep
Cls
Next ShapeNo
Shape1 Viewer
This FreeBASIC program will draw all of the Shape1 graphics.
ScreenRes 1000, 600, 32
Open "SHAPES1.DAT" For Binary As #1
Dim As UByte BufferByte
Dim As UShort Unknown, ShapeWidth, ShapeHeight
Dim As Integer BytesWide, X, Y, I, Pixels, PixelColor, ShapeSize, ShapeNo
Dim As UByte ShapeData(0 To 999)
Dim As Integer PixelData(0 To 9999)
Dim As String BinText
For ShapeNo = 0 To 47
Cls
Get #1, , Unknown ' Possibly bits-per-pixel?
Get #1, , ShapeWidth
Get #1, , ShapeHeight
If Eof(1) Then End
Locate 1, 20: Print "Shape: " + Str(ShapeNo)
Locate 3, 20: Print "Width: " + Str(ShapeWidth)
Locate 4, 20: Print "Height: " + Str(ShapeHeight)
' Determine how many bytes it will take to hold a single row in 2-bit color graphics.
' We use this odd way of rounding up because FreeBASIC doesn't have a Ceiling function.
If Frac(ShapeWidth / 4) > 0 Then
BytesWide = Int(ShapeWidth / 4) + 1
Else
BytesWide = Int(ShapeWidth / 4)
End If
' Determine how many bytes of CGA data we have to read for this image.
ShapeSize = BytesWide * ShapeHeight
Locate 5, 20: Print "Bytes Per Row: " + Str(BytesWide)
Locate 6, 20: Print "Total Size: " + Str(ShapeSize)
' Load the bitstream data.
I = 0
For I = 0 To ShapeSize - 1
Get #1, , BufferByte
ShapeData(I) = BufferByte
Next I
' Convert the bitstream into CGA pixels.
Pixels = 0
For I = 0 To ShapeSize - 1
BinText = Bin(ShapeData(I), 8)
For X = 1 To 8 Step 2
If Mid(BinText, X, 2) = "00" Then PixelColor = RGB(0, 0, 0)
If Mid(BinText, X, 2) = "01" Then PixelColor = RGB(0, 170, 0)
If Mid(BinText, X, 2) = "10" Then PixelColor = RGB(170, 0, 0)
If Mid(BinText, X, 2) = "11" Then PixelColor = RGB(170, 85, 0)
PixelData(Pixels) = PixelColor
Pixels = Pixels + 1
Next X
Next I
' Draw the pixels from left-to-right, bottom-to-top.
Y = ShapeHeight - 1
X = 0
For I = 0 To Pixels - 1
PSet (X, Y), PixelData(I)
X = X + 1
If X = BytesWide * 4 Then
X = 0
Y = Y - 1
End If
Next I
' Records are exactly 468 bytes, regardless of whether the shape uses them all.
' So, jump ahead to the next record.
Get #1, (ShapeNo + 1) * 468, BufferByte
Sleep
Next ShapeNo
Lookups
This is a quick lookup of all the shapes in the game files by default.
Shapes | CShapes | Doors |
---|---|---|
00 - Player Towards 01 - Player Right 02 - Player Away 03 - Player Left 04 - Axe Man 05 - Archer 06 - Swordsman 07 - Wizard 08 - Smoke 09 - Three Flies 10 - Dog 11 - Snake 12 - Ghost 13 - Shark 14 - Large Cat 15 - Rhino 16 - Ooze 17 - Slug 18 - Skeleton 19 - Dragon 20 - Tree 21 - Weed 22 - Tyrannosaurus 23 - Lion 24 - Pterodactyl 25 - Cockroach 26 - Two-Headed Mutant 27 - Bat 28 - Rabbit 29 - Elephant 30 - Alligator 31 - Rubble Monster 32 - Jellyfish 33 - Scorpion 34 - Ant 35 - Bear 36 - Lizard 37 - Octopus 38 - Mosquito 39 - Golem 40 - Demon 41 - Spider 42 - Gnats 43 - Giraffe 44 - Jack-o-lantern 45 - Tomato 46 - Kangaroo 47 - Storm |
00 - Chest 01 - Stairs Up 02 - Stairs Down 03 - Fire 04 - Rubble 05 - Stalactites 06 - Statue 07 - Rope 08 - Tombstone |
00 - Door Knob 01 - Green Keyhole 02 - Torch 03 - Chalk Mark 04 - Spider 05 - Gargoyle 06 - Leech 07 - Jewel 08 - Red Keyhole 09 - Gold Keyhole |
Credits
This format was reverse engineered by TheAlmightyGuru. If you find this information helpful in a project you're working on, please give credit where credit is due. (A link back to this wiki would be nice too!)