Swords of Glass graphic format
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're 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.
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.
|BYTE Transparent||Transparency flag (00 - Yes, 01 - No)|
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.
|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.|
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
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
This is a quick lookup of all the shapes in the game files by default.
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
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!)