If you have any information that you feel should be added to this
document, please contact me
or one of the fine people below.
Credits
ANIM.MUL, ANIM.IDX, ART.MUL,
ARTIDX.MUL, PALETTE.MUL, TEXMAPS.MUL,
TEXIDX.MUL, TILEDATA.MUL, GUMPIDX.MUL,
GUMPART.MUL, HUES.MUL and File Formats
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.
ANIM.IDX
Index Record (12 bytes)
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.
ANIM.IDX
AnimationGroup WORD[256] Palette DWORD
FrameCount DWORD[FrameCount] FrameOffset
Frame
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..
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.
ARTIDX.MUL
Index Record (12 bytes)
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.
ART.MUL
DWORD Flag ..DATA..
if (Flag > $FFFF or Flag ==
0) ... tile is Raw ... else ... tile is Run ... end if
Raw Tile 
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:
Run Tile 
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
}
}
Loading a Gump
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.
GUMPIDX.MUL
Index Record (12 bytes)
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.
HueEntry
WORD ColorTable[32]; WORD TableStart; WORD
TableEnd; CHAR Name[20];
HueGroup
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
MAP0 (37,748,736 bytes)
393,216 [Block]s sequentially, Block = 196 bytes DWORD header,
unknown content 64 Cells
Cell (3 bytes)
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
MULTI.IDX
Index Record (12 bytes)
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
MULTI.MUL
The Size should be loaded from MULTI.IDX and as many Multi Blocks
should be read as fit into this size.
Multi Block (12 bytes)
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.
PALETTE.MUL (768 bytes)
256 [Palette Entries]
Palette Entries (3 bytes)
UBYTE Red UBYTE Green UBYTE Blue
Use to lookup colors for tile numbers from MAP0 and
STAIDX0/STATICS0.
RADARCOL.MUL (131,072 bytes)
65536 sequential UWORD color values (see 1.2 Colors for color
info)
Contains index entries into the SKILLS.MUL file.
SKILLS.IDX
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.
SOUND.MUL
|
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.
SOUNDIDX.MUL
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.
STAIDX0.MUL (4,718,592 bytes)
393,216 [Index Block]s sequentially
Index Record (12 bytes)
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).
STATICS0.MUL
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.
TILEDATA.MUL
512 Blocks of [Land Tile Groups] X Blocks of [Static Tile
Groups]
Land Tile Groups (836 bytes)
DWORD Unknown 32 Blocks of [Land Tile Data]
Land Tile Data (26 bytes)
| 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
Static Tile Groups (1188 bytes)
DWORD Unknown 32 Blocks of [Static Tile Data]
Static Tile Data (37 bytes)
| 0 |
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
A |
B |
C |
D |
E |
F |
00 |
Flags |
Weight |
Quality |
Unknown |
Unknown1 |
Quantity |
Anim ID |
Unknown2 |
Hue |
Unknown3 |
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
Flags
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
TEXIDX.MUL
Index Record (12 bytes)
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.
TEXMAPS.MUL
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.
VERDATA.MUL
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. .
Stadifl#.mul, Mapdifl#.mul
Until end of file, DWORDs:Block Number
Stadifi#.mul
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
Mapdif#.mul
The format of this file maps exactly to Map#.mul. Each entry
corresponds to the block id in Mapdifl#.mul.
Stadifi#.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
|