Ultima V Internal Formats

This text is from Nodling's information. It is only a copy paste, as I go, I'll be reformating it and then adding information.

= LZW compressed =

Some files from the PC version of Ultima 5 have been compressed with the LZW algorithm: (and possibly others)
 * .4
 * .16

The compressed files are stored as (uint32 uncompressedLength, uint8[] compressedData).

Graphics files (*.4 and *.16 extensions)
These are all LZW-compressed image files. Those with a .4 extension have four-colour, two-bit data; those with a .16 extension have 16-colour, four-bit data.

Pixels are stored in descending bit order. So for a 16-color byte the first pixel is mask 0xF0, and the second is mask 0x0F. For a 4-color byte, the first pixel is mask 0xC0, the second pixel is mask 0x30, the third pixel is mask 0x0C, and the fourth pixel is mask 0x03.

Rows of pixels are padded to a four-pixel (byte) boundary for 4-color data, and an eight-pixel (four byte) boundary for 16-color data. So for a given width in pixels, a 4-color row is ((width + 3) / 4) bytes long, while a 16-color row is ((width + 7) / 8 * 4) bytes long.

"tiles.4" and "tiles.16"
These files, when uncompressed, contain 512 (0x200) tiles that are each 16x16 pixels stored in order without any headers. Using the above rules, the 4-color rows are stored as 4 bytes per row, and the 16-color rows are 8 bytes per row.

The 16-color tile sets are :

PC:



Amiga:



Here is the 4-color tileset using a gray-scale scheme with this file:



All other *.4 and *.16 files
These LZW-compressed files are composed of a number of images of varying sizes. The header is (uint16 count, uint32[count] offsets), where count is the number of images, and offsets is the start of an image from the beginning of the file, or zero if there is no image in that slot (this only occurs in "dng*.16" files).

Each image is composed of (uint16 width, uint16 height, ImageRow[height] data). Each row has the number of bytes discussed above.

"items.*" and "mon*.*" contain dungeon ladders, chests, and monster graphics, and have an additional twist:
 * The offsets are actually in uint16 and not uint32


 * Immediately following the image data is an image mask, composed of (uint16 width, uint16 height, uint8[(width + height + 7) / 8] data); width and height must be identical to those in the image. Pixels are stored in descending bit order with no padding, so the first pixel is mask 0x80, the second pixel is mask 0x40, and so on. If a value is set, then that pixel is masked and should not be drawn.

= *.CBT =

The combat files define a series of combat maps that are 11 x 11 in size.

File Format
struct CBT_File { Combat_Map c_maps[n]; };

struct Combat_Map { Map_Row             row0[1];  //Information contains the new tiles once a trigger happens // row 1 = east // row 2 = west // row 3 = south // row 4 = north Map_PlayerPos_Row   row1_4[4]; Map_MonsterTile_Row row5[1]; Map_MonsterX_Row    row6[1]; Map_MonsterY_Row    row7[1];

// row 8: positions of triggers, one position hits to new tiles // row 9: position X of the new tiles // row 10: position Y of the new tiles Map_Row             row8_10[3]; };

struct Map_Row { uint8 tiles[11]; uint8 newTiles[8]; uint8 padding[13]; };

struct Map_PlayerPos_Row { uint8 tiles[11]; uint8 initial_X[6]; // initial x position of each party member uint8 initial_Y[6]; // initial y position of each party member uint8 zeroes[9] = {0,0,0,0,0,0,0,0,0}; };

struct Map_MonsterTile_Row { uint8 tiles[11]; uint8 monster_tiles[16]; // tile for each monster uint8 zeroes[5] = {0,0,0,0,0}; };

struct Map_MonsterX_Row { uint8 tiles[11]; uint8 initial_X[16]; // initial x position of each monster uint8 zeroes[5] = {0,0,0,0,0}; };

struct Map_MonsterY_Row { uint8 tiles[11]; uint8 initial_Y[16]; // initial y position of each monster uint8 zeroes[5] = {0,0,0,0,0}; };

Notes:
 * Certain fields are explained in the next couple of sections
 * The initial party member positions depend on the direction from which the party entered the map.

Information field in the Map_Row
The information field has different meanings depending on the considered row. The information relates to map changes that happen when a character moves onto a tile.


 * The position of the tiles are provided in row 8 where the 16 bytes of information are divided into two 8-byte sections (X position and Y position)
 * The type of tiles are obtained in row 0
 * The position of the triggers is provided in rows 9 and 10. Row 9 provides the X position, row 10 provides the Y position.

It seems that one trigger automatically changes two tiles.

Example of a dungeon room
Here is an example of a dungeon room entirely decoded.

FF 4F 44 44 44 44 44 4F FF FF FF 4F 4F 4F 44 44 44 44 44 00 00 00 00 00 00 00 00 00 00 00 00 00
 * 1) Level 1
 * 1) New tiles for the changes
 * 1) Unknown
 * 1) Unkown

FF 4F 44 44 44 44 44 4F 4F 4F 4F 08 09 09 0A 0A 0A 04 03 05 04 02 06 00 00 00 00 00 00 00 00 00  FF 4F 44 44 44 44 44 44 44 44 44 00 00 00 00 00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00 00  FF 4F 4F 44 44 44 44 44 44 44 44 04 05 03 04 06 02 08 09 09 0A 0A 0A 00 00 00 00 00 00 00 00 00 FF FF 4F 4F 44 44 44 44 44 44 44 04 03 05 04 02 06 02 01 01 00 00 00  00 00 00 00 00 00 00 00 00  FF FF FF 4F 44 44 44 44 44 44 44 DC D0 D0 D0 94 94 94 94 DC 00 00 00 00 00 00 00 00 00 00 00 00
 * 1) Level 2
 * 1) Position X (east)
 * 1) Position Y (east)
 * 1) Unkown
 * 1) Level 3
 * 1) Position X (west)
 * 1) Position Y (west)
 * 1) Unkown
 * 1) Level 4
 * 1) Position X (south)
 * 1) Position Y (south)
 * 1) Unkown
 * 1) Level 5
 * 1) Position X (north)
 * 1) Position Y (north)
 * 1) Unkown
 * 1) Level 6
 * 1) Monster tiles (what monsters are in the room)
 * 1) Unkown

FF FF 4F 4F 44 44 44 44 44 44 44 09 02 01 01 01 01 01 01 09 00 00 00 00 00 00 00 00 00 00 00 00  FF 4F 4F 44 44 44 44 44 44 46 46 09 05 04 06 05 05 05 05 08 00 00 00 00 00 00 00 00 00 00 00 00
 * 1) Level 7
 * 1) Initial X position for monsters
 * 1) Unkown
 * 1) Level 8
 * 1) Initial Y position for monsters
 * 1) Unkown

FF 4F 44 44 44 44 44 44 46 44 46 06 06 06 06 06 06 06 06 06 06 06 06 06 06 06 06  00 00 00 00 00
 * 1) Level 9
 * 1) Position X of trigger for 2 elements of level 1, positions are in level 10 and level 11
 * 1) Position Y of trigger for 2 elements of level 1, positions are in level 10 and level 11
 * 1) Unknown

FF 4F 44 44 44 44 44 46 44 44 46 00 00 00 01 01 02 02 03 03 05 07 04 06 04 06 05  00 00 00 00 00
 * 1) Level 10
 * 1) Position X of changes in map from trigger in Level 9
 * 1) Position Y of changes in map from trigger in Level 9
 * 1) Unkown

FF 4F 44 44 44 44 44 46 46 46 46 00 00 00 01 02 03 03 03 04 06 06 05 05 04 06 05  00 00 00 00 00
 * 1) Level 11
 * 1) Position X of changes in map from trigger in Level 9
 * 1) Position Y of changes in map from trigger in Level 9
 * 1) Unkown

Tiles for each monster
In the case of dungeon maps, the tile for each monster gives the engine which monster to load where. With the initial_X and initial_Y fields of the Map_MonsterX_Row or Map_MonsterY_Row, the system can load up any dungeon map with a maximum of 16 monsters in a given room.

The values of the the monster_tile array in the Map_MonsterTile_Row element coincide with the elements of the tile set. However, since we are considering monsters the first 256 tiles are ignored.

Thus 1 is a chest, 2 is gold, 3 is a potion, etc.

Notes:
 * For some reason, certain values provide unexpected results
 * 5C would normally be the bard but is the jocker instead
 * EC, ED, EE, and EF seem to be a random monster value
 * Though the system seems to use the first frame of each element, any frame seems to work indifferently:
 * 0x78 is Blackthorne but so is: 0x79, 0x7A, and 0x7B

DUNGEON.CBT
= *.CH =

Font files with 128 characters each: - ibm.ch contains the Latin characters and a number of symbols. - runes.ch contains the Britannian runes and the remaining symbols.

File format:

Each character is 8x8 pixels, 1 bit per pixel (0 = black, 1 = white). I don't know which palette index is associated with a 0 or 1. Each byte represents 8 pixels. The most significant bit represents the leftmost pixel, and the least significant bit represents the rightmost pixel.

= *.DAT =

BRIT.DAT
The Britannian map. Its size is 256x256 tiles. It is divided into 256 chunks. Each chunk has a size of 16x16 tiles, and each tile is stored as a uint8 (in Ultima 4, the chunk size was 32x32 tiles).

To save space, chunks that are all water (tile 0x1) were left out. The location of the all-water chunks is stored in DATA.OVL. The chunks are stored from west to east, north to south, i.e. the first chunk in the uncompressed map is the one in the northwest corner.

The tiles in a chunk are also stored from west to east, north to south.

The complete map of Britannia is: 

The complete map of Britannia with the original tileset is: 

CASTLE.DAT
The castle file contains all the maps concerning the castles.


 * Lord British's Castle




 * Blackthorn's Castle




 * Britannies




 * Small towns



DUNGEON.DAT
The DUNGEON.DAT file contains the dungeon maps for the game. It is made of 8 * 8 * 8 * 8 tiles where :


 * The first 8 is a line of Deceit
 * The first 8 * 8 is the first level of Deceit
 * The first 8 * 8 * 8 is the dungeon deceit, each level being 8 * 8

The code for each tile is actually divided in two groups of 4-bits:

Higher 4 bits:


 * 0: Nothing
 * 1: Ladder Up
 * Lower bit: 8 provides an upper trap
 * 2: Ladder Down
 * Lower bit: 8 provides an upper trap
 * 3: Ladder Up and Down
 * Lower bit: 8 provides an upper trap
 * 4: Chest
 * Lower bits:
 * 0: normal chest
 * 1, 2: trapped
 * 4: poisonned
 * 5: Fountain
 * Lower bits:
 * 0: cure poison
 * 1: heal
 * 2: poison
 * anything else: bad taste, damage
 * 6: Trap
 * Lower bits:
 * 0: lower trap visible
 * 1: bomb trap
 * 2: invisible trap
 * 8: upper trap visible
 * 7: Open chest
 * 8: Energy fields
 * Lower 3 bits:
 * 0: Poison
 * 1: Sleep
 * 2: Fire
 * 3: Energy
 * A: Rooms but doesn't seem to work when loading them
 * B: Wall
 * Lower values:
 * 1: Text
 * C: Secondary wall
 * D: Secret door
 * E: Normal door
 * F: Room

Note :
 * 4th lower bit says if there is an upper trap or not

TOWNE.DAT
This data file contains the cities of Ultima 5:


 * Moonglow




 * Britain




 * Jhelom




 * Yew




 * Minoc




 * Trinsic




 * Skara Brae




 * New Magincia



DWELLING.DAT
The dwelling data contains the various dwellings of the game.


 * Fogsbane




 * Stormcrom




 * Greyhaven




 * Waveguide




 * Iolo's hut




 * Spektran




 * Sin Vraal's hut




 * Grendel's hut



KEEP.DAT
The keep data contains the various keeps of the game.


 * Ararat




 * Bordermarch




 * Farthing




 * Windemere




 * Stonegate




 * The Lycaeum




 * Empath Abbey




 * The Serpent's Hold



LOOK2.DAT
This file contains the "look" descriptions for the 0x200 tiles. Each description is a zero-terminated ASCII string. Some descriptions consist only of an asterisk, which the game displays as a diamond. The offsets are relative to the beginning of the file.

look2.dat = set(0x200) of offset16, set(0x200) of ascii_string

offset16 = uint16 ascii_string = set of ascii_char, terminator ascii_char = uint8 terminator = (uint8) 0

MISCMAPS.DAT
This file contains:


 * 1) 4x Cutscene Screens (11x11 tiles)
 * 2) 4x Intro Screens (19x4 tiles)
 * 3) Intro Script Data

Intro Maps
Notes:
 * The intro maps are stored in the order in which they appear during the introduction:
 * 1) "The Summoning"
 * 2) "The Journey"
 * 3) "The Arrival"
 * 4) "The Welcoming"


 * the script data controls the movement of NPC's in the introduction
 * TODO: battle screen descriptions, script format

SIGNS.DAT
This file contains the set of 33 (0x21) sign groups in this game. First the 16-bit offsets to each sign are stored; after that comes the data for each sign. Each offset represents 0 or more sign groups for a single location, with differing XY coordinates and floors.

signs.dat = set(0x21) of offset16, set(0x21) of sign_group

offset16 = uint16 sign_group = set of sign_data

sign_data = header4, set of sign_char, terminator

header4 = location, coord_Z, coord_X, coord_Y location, coord_Z, coord_X, coord_Y = uint8 sign_char = uint8 terminator = (uint8)

Notes:


 * offset16[i] points to the signs in location i. For locations without signs, the corresponding offset16 is 0. If you remove the zero values, you'll find that the remaining offset16's are sorted in ascending order.


 * each sign starts with a 4-byte header, specifying location (shown below), z, x and y coordinates.


 * the data between your current offset and proceeding offset will contain 1..N signs (sign group) separated by a single NULL (0) byte


 * in some of the eight virtue towns, there are signs that refer to the laws - if there are two then one is defined by only a "\n", the second definition contains the strings.


 * The game may not support signs in dungeons.

0 <= sign_char <= 0x7F --> runes.ch[sign_char] 0x80 <= sign_char <= 0xFF --> ibm.ch[sign_char - 0x80]
 * Signs can contain characters from both ibm.ch and runes.ch:


 * If you want to review the plaintext string of a sign, simply subtract 128 (0x80) from any character > 127 (0x7F)


 * The signs each have the lines denoted as lower case alphabet characters.

UNDER.DAT
The Underworld map

Its size is 256x256 tiles.

It is divided into 256 chunks. Each chunk has a size of 16x16 tiles, and each tile is stored as a uint8 (in Ultima 4, the chunk size was 32x32 tiles). Unlike BRIT.DAT, it is not compressed (no chunks were left out).

The chunks are stored from west to east, north to south, i.e. the first chunk in the map is the one in the northwest corner. The tiles in a chunk are also stored from west to east, north to south. 

= *.HCS =

Font files with 128 characters each.

ibm.hcs contains the Latin characters and a number of symbols.

runes.hcs contains the Britannian runes and the remaining symbols.

File format:

Each character is 16x12 pixels, 1 bit per pixel (0 = black, 1 = white). I don't know which palette index is associated with a 0 or 1. Each byte represents 8 pixels. The most significant bit represents the leftmost pixel, and the least significant bit represents the rightmost pixel.

= *.NPC =

Each file contains 4608 bytes.

There are of course : - CASTLE.NPC - DWELLING.NPC - KEEP.NPC - TOWNE.NPC

CASTLE.NPC
Todo.

DWELLING.NPC
Todo.

KEEP.NPC
Todo.

TOWNE.NPC
It is divided into 8 parts.

These files contain information about NPC's.

The general structure of the whole file is : struct NPC_File { NPC_Info info[8]; // each NPC file has information for 8 maps }; For each city, we have an information entry for the Npcs of the map:

struct NPC_Info { NPC_Schedule schedule[32]; uint8 type[32]; // merchant, guard, etc.  uint8 dialog_number[32]; };

The dialog number gives the entry index to the *.TLK file.

Finally, the schedule says how the Npc moves around in the city and especially when: struct NPC_Schedule { uint8 AI_types[3]; uint8 x_coordinates[3]; uint8 y_coordinates[3]; sint8 z_coordinates[3]; uint8 times[4]; };

Notes:

1) All maps can hold a maximum of 31 (not 32) NPC's. In every map, schedule[0], type[0] and dialog_number[0] are not used. However, type[0] is sometimes 0 and sometimes 0x1C, so perhaps it has some unknown purpose.

2) Each NPC_Schedule contains information about 3 locations that the NPC will go to at different times of day. The x and y coordinates are between 0 and 31, because each map has a size of 32x32 tiles. The z coordinates represent the level, relative to level 0. 0xFF would make the NPC go to the level below level 0, while 0x1 would make the NPC go to the level above level 0.

The times are given in hours, so they range from 0 to 23.

times[0] --> NPC goes to location 0

times[1] --> NPC goes to location 1

times[2] --> NPC goes to location 2

times[3] --> NPC goes to location 1

Values for the dialog_number
= *.OVL =

Code or data overlays.

DATA.OVL
Here is the data layout for this file:

 Mapping of Data.ovl

The old list is here for the moment:

offset     length      purpose // monster flags 0x154C     0x30*2      flags that define the special abilities of                           monsters during combat; 32 bits per monster 0x0020 = undead (affected by An Xen Corp) todo: - passes through walls (ghost, shadowlord) - can become invisible (wisp, ghost, shadowlord) - can teleport (wisp, shadowlord) - can't move (reaper, mimic) - able to camouflage itself - may divide when hit (slime, gargoyle) // moon phases 0x1EEA     28*2        moon phases (28 byte pairs, one for each day of the month) // shrines and mantras 0x1F7E     8           x coordinates of shrines 0x1F86     8           y coordinates of shrines // this section contains information about hidden, non-regenerating objects (e.g. the magic axe in the dead tree in Jhelom); there are // only 0x71 such objects; the last entry in each table is 0 0x3E88     0x72        object type (tile - 0x100) 0x3EFA     0x72        object quality (e.g. potion type, number of gems) 0x3F6C     0x72        location number (see "Party Location") 0x3FDE     0x72        level 0x4050     0x72        x coordinate 0x40C2     0x72        y coordinate // dock coordinates (where puchased ships/skiffs are placed) // 0 = Jhelom // 1 = Minoc // 2 = East Brittany // 3 = Buccaneer's Den 0x4D86     0x4         x coordinate 0x4D8A     0x4         y coordinate // scan code translation table: // when the player presses a key that produces one of the scan codes in  // the first table, the game translates it to the corresponding code in   // the second table 0x541E     8           scancodes 0x5426     8           internal codes // wells 0x7252     0x32        wishing for one of these keywords at a wishing well gets you a horse

The other OVL
The other OVL files, as far as we can tell are actually binary files containing code. The base executable actually loads these up on a need to execute basis.

= *.TLK =

These files contain conversation scripts. Here is the transcript for Ultima V. There are 4 files for each group of Maps :

The format for each of these files is:

Notes:

1) The first NPC number is 1.

2) The script_indexes are sorted by NPC number, in ascending order.

3) The script_data blocks are sorted by NPC number, in ascending order.

4) The conversations appear to be scripted, like the ones in Ultima 6.

The way this is linked together is via the Npc information since it gives us the dialogue index which links itself to the script index.

The text encoding
For each Npc, there are a certain number of '\0' terminated strings which are encodings of what is the name, job, etc. of the Npc. To decode these texts, I've taken Nodling's decoder and tweaked it a bit.

First part: fixed entries
For each Npc, it starts with a certain number of fixed entries:

- Name, Description, Greeting, Job, Bye

Second part: key words
We specify it by:

 (Therefore text only) [  [ ]...]  (Potentially anything

Third part : labels
Then it's question/answers with labels.

Todo

Generalities
For the moment, for each entry, I do :

if((c >= 160) && (c < 255)) {           c -= 128; }       else {           special = true; }

If it's a special case, then we have two cases : it's a code based symbol, it's an entry to an array of texts from DATA.OVL (the offset table starts at 0x24f8).

For the moment, I've confirmed these bindings :

If it's not a special case, then I just copy the character in the string.

= General Notes =

1) Wishing Wells

There are two wishing wells in the game: - Paws (location 0x16) - Empath Abbey (location 0x1F)

These locations are hard-coded into the game. There is no difference between horses from wishing wells and horses from vendors. It also doesn't make any difference if you wish for "horse" or a car brand.

= EGA.DRV =

See u5_ega_drv.txt

= INIT.GAM =

Initial "SAVED.GAM".

= SAVED.GAM =

Ultima 5 (PC version) SAVED.GAM

=
==================

Last updated on 3-Jan-2020

Please send additions, corrections and feedback to this e-Mail address:

Remove space + vowels from "marc winterrowd" and append "at yahoo dot com"

SAVED.GAM -

This file contains the current saved game.

When you start a new game, "SAVED.GAM" is initalized from "INIT.GAM".

Notes:

--- Movement Lists --- When it's time for an NPC to move to a new location (as dictated by his schedule), the game calculates a path to the new location and stores this path in the NPC's movement list. The movement lists are stored at 0xBB8. There are 0x20 lists (one for eachNPC). Each list can store up to 0x10 movement commands. The movement commands are made up of two bytes: the number of repetitions and the direction. struct Movement_List_Table { Movement_List mov_lists[0x20]; }  struct Movement_List { Movement_Command mov_commands[0x10]; }  struct Movement_Command { uint8 repeats; // 1 = east // 2 = north // 3 = west // 4 = south uint8 direction; } The table at 0xFB8 contains 0x20 word offsets (one for each NPC). Entries can have the following values:
 * 0xFFFF = movement list for the NPC is empty
 * else = offset into the NPC's movement list (must be a multiple of 2)

If the NPC hasn't reached his destination after going through the movement commands, the game calculates a new path and stores it in the NPC'smovement list. Notes:
 * There are no up/down movement commands. When an NPC wants to move to a location on another level, he goes to the nearest stair/ladder and then teleports to his destination.

--- Monster Format ---

Notes:
 * 1) The monster table contains not only monsters, but also inanimate objects (e.g. empty ships) and the party.
 * 2) The party is always in slot 0.
 * 3) Non-empty entries don't have to be contiguous.

Todo: empty entries, object-specific fields

--- Character record format ---

Notes:


 * If the character is staying at an inn, the byte at offset 0x1F contains the settlement number (see "Party Location") of the inn.
 * If the character is in the party, it contains 0.
 * If the character hasn't joined the party yet, it contains 0xFF.
 * If the character has been permanently killed, it contains 0x7F.

--- Equipment ---

Ordered list of Armour, Weapons and Scrolls

--- Party Location ---

= Sources =

Nytegard 

http://nodling.nullneuron.net/nytegard/nytegard.html

http://nodling.nullneuron.net/ultima/ultima.html

http://martin.brenner.de/ultima/u5save.html

http://www.cosy.sbg.ac.at/~lendl/ultima/ultima5/ on the Internet Archive

http://www.wi.leidenuniv.nl/~psimoons/ultima5t.htm on the Internet Archive

Sheng Long Gradilla 

Brad Hannah 