If you have any information that you feel should be added to this document, please contact me or one of the fine people below.
ANIM.MUL, ANIM.IDX, ART.MUL, ARTIDX.MUL, PALETTE.MUL, TEXMAPS.MUL, TEXIDX.MUL, TILEDATA.MUL, GUMPIDX.MUL, GUMPART.MUL, HUES.MUL and FAQ coalation by Alazane
MAP0, RADARCOL, STAIDX0, STATICS0 and FAQ touch-up by Tim Walters
SKILLS.IDX, SKILLS.MUL by Sir Valgriz
SOUNDIDX, SOUND by Steve Dang
VERDATA by Cironian
CHAR | 8 bit | unsigned character (ex. 'A', 'a', etc.) |
BYTE | 8 bit | signed value (-128..127) |
UBYTE | 8 bit | unsigned value (0..255) |
WORD | 16 bit | signed value (-32768..32767) |
UWORD | 16 bit | unsigned value (0..65535) |
DWORD | 32 bit | signed value (-2147483648..2147483647) |
Ultima is completely 16 bit based--meaning that each pixel is UWORD value that can be broken down as follows:
1 | 1 | 1 | 1 | 1 | 1 | ||||||||||
5 | 4 | 3 | 2 | 1 | 0 | 9 | 8 | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
U | R | R | R | R | R | G | G | G | G | G | B | B | B | B | B |
Where U=Unused, R=Red, G=Green and B=Blue
So, to convert to 32 bit:
Color32 = ( (((Color16 >> 10) & 0x1F) * 0xFF /
0x1F) |
((((Color16 >> 5) & 0x1F) * 0xFF / 0x1F) << 8)
|
((( Color16 & 0x1F) * 0xFF / 0x1F) << 16));
ANIM | Animations such as monsters, people, and armor. |
ANIMDATA | |
ANIMINFO | |
ART | Artwork such as ground, objects, etc. |
ARTIDX | Index to ART |
CHARDATA | |
FONTS | Fixed size bitmaps style fonts. |
GUMPART | Gumps. Stationary controller bitmaps such as windows, buttons, paperdoll pieces, etc. |
GUMPIDX | Index to GUMPART |
HUES | |
LIGHT | |
LIGHTIDX | |
MAP0 | Terrain data |
MULTI | Groups of art (houses, castles, etc) |
PALETTE | Contains an 8 bit palette (use unknown) |
RADARCOL | Colors for converting the terrain data to radar view |
REGIONS | |
SKILLGRP | |
SKILLS | |
SOUND | Sampled sounds |
SOUNDIDX | Index into SOUND |
STAIDX0 | Index into STATICS0 |
STATICS0 | Static objects on the map |
TEXIDX | Index into TEXMAPS |
TEXMAPS | Texture map data (the ground). |
TILEDATA | Data about tiles in ART |
VERDATA | Patched overrides of data in any of the above files. |
This file contains an index into ANIM.MUL. To load a specific group of frames from ANIM.MUL, simply seek BNum * 12, read in the index record and use Lookup to find the group within ANIM.MUL.
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | A | B |
Lookup | Size | Unknown |
DWORD Lookup - Is either undefined ($FFFFFFFF -1) or the file
offset in ANIM.MUL
DWORD Size - Size of the art tile
DWORD Unknown -
Unknown
This file contains the raw animation data. It can be accessed using ANIM.IDX.
AnimationGroup
WORD[256] Palette
DWORD
FrameCount
DWORD[FrameCount] FrameOffset
Seek from the end of Palette plus FrameOffset[FrameNum] bytes
to find the start of Frame
WORD ImageCenterX
WORD ImageCenterY
WORD
Width
WORD Height
...Data Stream..
// Note: Set CenterY and CenterY to the vertical and horizontal position in // the bitmap at which you want the anim to be drawn. PrevLineNum = $FF Y = CenterY - ImageCenterY - Height while not EOF { Read UWORD RowHeader Read UWORD RowOfs if (RowHeader = 0x7FFF) or (RowOfs = 0x7FFF) then Exit RunLength = RowHeader and $FFF LineNum = RowHeader shr 12 Unknown = RowOfs and $3F RowOfs = RowOfs sar 6 X = CenterX + RowOfs if (PrevLineNum <> $FF) and (LineNum <> PrevLineNum) then Y++ PrevLineNum = LineNum if Y >= 0 then { if Y >= MaxHeight then Exit; For I := 0 to RunLength-1 do { Read(B, 1) Row[X+I,Y] = Palette[B] } } Seek(RunLength, FromCurrent) }
This file contains an index into ART.MUL. To load a specific tile number in ART.MUL, simply seek BNum * 12, read in the index tile data and use Lookup to find the tile within ART.MUL.
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | A | B |
Lookup | Size | Unknown |
DWORD Lookup - Is either undefined ($FFFFFFFF -1) or the file
offset in ART.MUL
DWORD Size - Size of the art tile
DWORD Unknown -
Unknown
This file has no header and contains tiles that can only be read via ARTIDX.MUL. There are two types of tiles, which I will call Raw and Run tiles.
DWORD Flag
..DATA..
if (Flag > $FFFF or Flag ==
0)
... tile is Raw ...
else
... tile is Run ...
end if
Raw tiles are always 44x44 pixels square. However, the data stored for a tile resembles a square block, rotated 45 degrees. Therefore, the tile is loaded from the tip, down to the widest portion, then down to the bottom tip as opposed to a straight matrix.
WORD Pixels, in the series:
2, 4, 6, 8, 10 ... 40, 42, 44,
44, 42, 40 ... 10, 8, 6, 4, 2 (See 1.2 Colors for pixel info)
The resulting top 9 lines look like this:
UWORD Width
UWORD Height
Read : UWORD Width if (Width = 0 || Width >= 1024) return Read : UWORD Height if (Height = 0 || Height >= 1024) return Read : LStart = A table of Height number of UWORD values DStart = Stream position X = 0; Y = 0; Seek : DStart + LStart[Y] * 2 while (Y < Height) { Read : UWORD XOffs Read : UWORD Run if (XOffs + Run >= 2048) return else if (XOffs + Run <> 0) { X += XOffs; Load Run number of pixels at X (See 1.2 Colors for pixel info) X += Run; } else { X = 0; Y++; Seek : DStart + LStart[Y] * 2 } }
Gumps are stored in runs of pixels (badly inefficient, but is better than no compression). An array of pairs of Value & Word are loaded for a particular row (shown below) and these can then be easily shoved into a bitmap. For example, if the final bitmap was going to display one black pixel, then three white pixels, the stream would contain 0000 0001 FFFF 0003. It is imperative that GUMPIDX.MUL be loaded since it contains Width, Height and Size which are needed to decode the Gump.
DataStart := CurrentPosition DWORD RowLookup[GumpIdx.Height] for Y = 0 to GumpIdx.Height-1 { cf Y < Height-1 then RunBlockCount := RowLookup[Y+1]-RowLookup[Y] else RunBlockCount := GumpIdx.Size div 4 - RowLookup[Y]; Seek : DataStart + RowLookup[Y] * 4 (WORD Value, WORD Run)[RunBlockCount] }
This file contains an index into GUMPART.MUL.
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | A | B |
Lookup | Size | Height | Width |
DWORD Lookup - Is either undefined ($FFFFFFFF -1) or the file
offset in GUMPART.MUL
DWORD Size - Size of the block
UWORD Height - Height
(in pixels) of the block
UWORD Width - Width (in pixels) of the block
Just read in HueGroups until you hit the end of the file. Note that large chunks of this file consist of garbage--OSI admits to this (something about a bug in their old code).
If you want to look at the hues, check out this.
Hues are applied to an image in one of two ways. Either all gray pixels are mapped to the specified color range (resulting in a part of an image changed to that color) or all pixels are first mapped to grayscale and then the specified color range is mapped over the entire image.
WORD ColorTable[32];
WORD TableStart;
WORD
TableEnd;
CHAR Name[20];
DWORD Header;
HueEntry Entries[8];
This file holds all the base-level terrain, and doesn't look too pretty without the static data.
The map is stored as a 768x512 matrix of blocks. A block is basically a 8x8 matrix of cells. Each individual cell contains data about the tile for that cell, and the cell's altitude. Therefore, the entire map is 6144x4096 individual cells in size.
Blocks are loaded top-to-bottom then left-to-right. Cells are loaded from blocks left-to-right then top-to-bottom.
The formula used to locate an individual CELL in the file is a little complex, since you have to work out what block it is in...
If you refer to the map in blocks, then there's 512 blocks down, by 768 blocks across.
XBlock = Int(XPos/8)
YBlock = Int(YPos/8)
Block Number
= (XBlock * 512) + YBlock
393,216 [Block]s sequentially, Block = 196 bytes
DWORD
header, unknown content
64 Cells
0 | 1 | 2 |
Color | Alt |
UWORD cell graphic (which can be looked up in
RADARCOL).
BYTE Altitude (-128..127 units above/below sea level).
This file contains an index into MULTI.MUL
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | A | B |
Start | Length | Unknown |
DWORD Lookup - Is either undefined ($FFFFFFFF -1) or the file
offset in MULTI.MUL
DWORD Size - Size of the multi block
DWORD Unknown -
Unknown
The Size should be loaded from MULTI.IDX and as many Multi Blocks should be read as fit into this size.
WORD BlockNum
WORD X
WORD Y
WORD Alt
UDWORD Flags
Once 16384+BlockNum has been looked up in ART, the block can
be drawn using the following positioning:
DrawX = LeftMostX +
(MultiBlock.X - MultiBlock.Y) * 22 - (Block.Width shr 1)
DrawY = TopMostY +
(MultiBlock.X + MultiBlock.Y) * 22 - Block.Height - MultiBlock.Alt * 4
Contains an 8 bit palette. Use Unknown.
256 [Palette Entries]
0 | 1 | 2 |
R | G | B |
UBYTE Red
UBYTE Green
UBYTE Blue
Use to lookup colors for tile numbers from MAP0 and STAIDX0/STATICS0.
65536 sequential UWORD color values (see 1.2 Colors for color info)
Contains index entries into the SKILLS.MUL file.
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | A | B | C | D | E | F |
00 | Start | Length | Unknown |
DWORD is the Start position for this sound
DWORD is the
Length of the entire associated data segment
DWORD Unknown
Contains skill names.
BYTE is 1 if there is a button next to the name, 0 if there
is not
CHAR Name[] is the name of the skill
Contains sampled sounds from the game. All sounds are 16 bit mono sampled at 22050khz.
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | A | B | C | D | E | F | |
00 | Original filename | |||||||||||||||
10 | Unknown | Unknown | Unknown | Unknown | ||||||||||||
20 | Data... |
CHAR[16] Original filename
DWORD Unknown
DWORD
Unknown
DWORD Unknown
DWORD Unknown
DATA
Contains index entries into the SOUND.MUL file.
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | A | B | C | D | E | F |
00 | Start | Length | Index | Reserved |
DWORD is the Start position for this sound
DWORD is the
Length of the entire associated data segment
UWORD index
UWORD reserved
Contains index to entries into the STATICS0.MUL file. Special thanks to Mental4 from ultima.scorched.com for the hint to cracking this one The 12 byte blocks in this file match up one to one with the blocks in the map file, hence the file size of 768 * 512 * 12 = 4,718,592 bytes. Pretty simple, easiest to read this at the same time as MAP0.MUL, if there's static objects, read them per block, doing height comparisons to see which ones to draw for radar type view.
393,216 [Index Block]s sequentially
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | A | B |
Start | Length | Unknown |
DWORD is the Start position (0xFFFFFFFF if no static objects
exist for this block)
DWORD is the Length of the data segment
DWORD is of
unknown use
Notes: This is where all the fun stuff is, as a side-note, I tried playing with NO static objects for the purpose of item-recovery (stuff lost behind walls), the Britain area is duplicated server-side (could explain the extra lag there), same with Moonglow area, places like Buccaneer's Den, Trinsic and other towns worked fine... no walls, no bridges, no shallow water (note, server still stops you from walking through walls, even if you can't see them).
Static Data (7 bytes)
0 | 1 | 2 | 3 | 4 | 5 | 6 |
Color | X | Y | Alt | Unknown |
UWORD for the Color/Object ID (add 16384 to offset it for
RADARCOL.MUL)
UBYTE X Offset in block (0..7)
UBYTE Y Offset in block
(0..7)
BYTE Altitude (-128..127 units above/below sea level, like
MAP0)
UWORD Unknown
Contains data about tiles in ART.MUL.
512 Blocks of [Land Tile Groups]
X Blocks of [Static Tile
Groups]
DWORD Unknown
32 Blocks of [Land Tile Data]
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | A | B | C | D | E | F | |
00 | Flags | Texture ID | Tile Name... | |||||||||||||
10 | ... |
DWORD Flags (see below)
UWORD Texture ID (If 0, the land
tile has not texture)
CHAR[20] Tile Name
DWORD Unknown
32 Blocks of [Static Tile Data]
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | A | B | C | D | E | F | |
00 | Flags | Weight | Quality | Unknown | Quantity | Anim ID | Unknown1 | Hue | Unknown2 | |||||||
10 | Height | Tile Name... | ||||||||||||||
20 | ... |
DWORD Flags (see below)
BYTE Weight (weight of the item,
255 means not movable)
BYTE Quality (If Wearable, this is a Layer. If Light
Source, this is Light ID)
UWORD Unknown
BYTE Unknown1
BYTE Quantity (if
Weapon, this is Weapon Class. If Armor, Armor Class)
UWORD Anim ID (The Body
ID the animatation. Add 50,000 and 60,000 respectivefully to get the two gump
indicies assocaited with this tile)
BYTE Unknown2
BYTE Hue (perhaps
colored light?)
UWORD Unknown3
BYTE Height (If Conatainer, this is how
much the container can hold)
CHAR[20] Tile Name
0x00000001 Background
0x00000002 Weapon
0x00000004
Transparent
0x00000008 Translucent
0x00000010 Wall
0x00000020
Damaging
0x00000040 Impassable
0x00000080 Wet
0x00000100
Unknown
0x00000200 Surface
0x00000400 Bridge
0x00000800
Generic/Stackable
0x00001000 Window
0x00002000 No Shoot
0x00004000
Prefix A
0x00008000 Prefix An
0x00010000 Internal (things like hair,
beards, etc)
0x00020000 Foliage
0x00040000 Partial Hue
0x00080000
Unknown 1
0x00100000 Map
0x00200000 Container
0x00400000
Wearable
0x00800000 LightSource
0x01000000 Animated
0x02000000 No
Diagonal
0x04000000 Unknown 2
0x08000000 Armor
0x10000000
Roof
0x20000000 Door
0x40000000 StairBack
0x80000000 StairRight
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | A | B |
Start | Length | Unknown |
DWORD is the Start position
DWORD is the Length of the
data segment
DWORD is of unknown use
The texture maps in general were about an afternoon hack. Since the index is the same as all the other indices and the data is quite straightforward, it was almost an afterthought to include it.
Raw 16-bit data, sized as follows:
If Length = 0x2000 then
data = 64*64
If Length = 0x8000 then data = 128*128
VERDATA.MUL contains patch entries that override already existing entries in the above data files. The use of VERDATA.MUL was a recent addition to InsideUO, thanks almost completely to Cironian for dropping the format for it on me out of the blue.
First DWORD: Number of entries in index
Index entry:
DWORD: File ID (see index
below)
DWORD: Block (Item number, Gump number or whatever; like in the
file)
DWORD: Position (Where to find this block in verdata.mul)
DWORD:
Size (Size in Byte)
DWORD: Various (depends on the file format)
File IDs: (* means used in current verdata)
00 -
map0.mul
01 - staidx0.mul
02 - statics0.mul
03 - artidx.mul
04 -
art.mul*
05 - anim.idx
06 - anim.mul
07 - soundidx.mul
08 -
sound.mul
09 - texidx.mul
0A - texmaps.mul
0B - gumpidx.mul
0C -
gumpart.mul* - Various is WORD Height + WORD Width
0D - multi.idx
0E -
multi.mul*
0F - skills.idx
10 - skills.mul
1E - tiledata.mul*
1F -
animdata.mul*
The block of data that is located and read in from VERDATA.MUL is of exactly the same format as the data in the source file.
Stadifl#.mul,Stadifi#.mul,Stadif#.mul,Mapdifl#.mul, and Mapdif#.mul contains patch entries that override already existing entries in the above data files. The use of diff files is activiated by a packet sent to the client from the server. The 3D client, starting with LBR , no longer uses Verdata.mul. It is uknown how other data patches are done. .
Until end of file, DWORDs:Block Number
The format of this file maps exactly to the format of StaIDX#.mul. Each entry corresponds to the blockid in the Stadifl#.mul. The offset and length of the entires are the offset into Stadif#.mul
The format of this file maps exactly to Map#.mul. Each entry corresponds to the block id in Mapdifl#.mul.
The format of this file maps exactly to Statics#.mul. This file is indexed by Stadifi#.mul
Last reviewed by Alazane 03/23/2002