Ultima II Dungeon Format
Format type | Map/level |
---|---|
Map type | 2D cell-based |
Layer count | 1 |
Cell dimensions | 16×16 (on each level) |
Games |
Ultima II: Revenge of the Enchantress has several dungeon/tower maps. Dungeon maps are those map files whose names end in “5” while the tower maps’ end in “4”. This pattern applies to the unpatched version of the game as well as those patched by the Ultima 2 Upgrade patch. All other maps are planets or towns.
File Format
The map for each floor is a 256-byte chunk representing a 16×16 cell floor, storing each cell as a single byte. The full file goes from level 0 to the last level, with "Level 0" being the closest level to the surface, meaning the bottom level for towers, and the top level for dungeons. The level numbers go up for those that move farther away from the surface. This makes the map files at least 4096 bytes, although they are all larger than that.
Note that some map files are corrupted. Look to the Ultima 2 Upgrade patch for fixes.
Data type | Name | Description |
---|---|---|
UINT8[NrOfLevels][256] | Levels | The 16×16 cell maps for each level, with one byte per cell. |
Tile Data
Tile Id | Tile Description |
---|---|
0x00 | Floor |
0x10 | Ladder up |
0x20 | Ladder down |
0x30 | Ladder up/down |
0x40 | Chest or tri-lithium |
0x80 | Wall |
0xC0 | Door |
0xE0 | Secret door |
Source Code
FreeBASIC
Map Viewer
This FreeBASIC program displays all the dungeon/tower levels of the specified map.
' Draws the maps of ''Ultima II'' dungeons/towers.
' Change the file path to the dungeon/tower you want.
' File names ending with a “5” are dungeon maps and towers end in “4”.
Dim As String FilePath = "H:\DOS\Ultima2\mapx24"
Screen 13
Dim As UByte Map
Dim As UByte X
Dim As UByte Y
Dim As UByte TileNumber
' Open the map file.
Open FilePath For Binary As #1
Color 8
Locate 3, 23: Print Chr(177)
Color 6
Locate 7, 23: Print Chr(219)
Color 9
Locate 9, 23: Print Chr(219)
Color 10
Locate 11, 23: Print "$"
Color 14
Locate 13, 23: Print Chr(24)
Locate 15, 23: Print Chr(25)
Locate 17, 23: Print Chr(18)
Color 15
Locate 3, 25: Print "- Wall"
Locate 5, 25: Print "- Floor"
Locate 7, 25: Print "- Door"
Locate 9, 25: Print "- Secret Door"
Locate 11, 25: Print "- Chest"
Locate 13, 25: Print "- Ladder Up"
Locate 15, 25: Print "- Ladder Down"
Locate 17, 25: Print "- Up/Down"
Line (15, 15)-(144, 144), 7, B
' Load the entire map, and draw the appropriate tiles.
For Map = 0 To 15
For Y = 0 To 15
For X = 0 To 15
Get #1, , TileNumber
Locate Y + 3, X + 3
Select Case TileNumber
Case &h00 ' Floor
Color 0
Print " "
Case &h10 ' Ladder Up
Color 14
Print Chr(24)
Case &h20 ' Ladder Down
Color 14
Print Chr(25)
Case &h30 ' Ladder Up/Down
Color 14
Print Chr(18)
Case &h40 ' Chest
Color 10
Print "$"
Case &h80 ' Wall
Color 8
Print Chr(177)
Case &hC0 ' Door
Color 6
Print Chr(219)
Case &hE0 ' Secret Door
Color 9
Print Chr(219)
End Select
Next X
Next Y
Color 15
Locate 23, 3: Print "Floor: " + Str(Map)
Sleep
Next Map
Close #1
Credits
This map 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!)
C
The above FreeBASIC algorithm does not take into account the implied elements of dungeons/towers, such as the tri-lithium on the final level or half the outer walls. This C function will supply that.
#define MAP_CODE_FLOOR 0x00
#define MAP_CODE_UP 0x01
#define MAP_CODE_DOWN 0x02
#define MAP_CODE_UPDOWN 0x03
#define MAP_CODE_CHEST 0x04
#define MAP_CODE_WALL 0x05
#define MAP_CODE_DOOR 0x06
#define MAP_CODE_SECRET 0x07
#define MAP_CODE_TRILI 0x08
#define MAP_CODE_UNKNOWN 0x09
/**
Takes a 256-byte chunk from the map file, representing a level and returns a 17x17 map for the entire level,
including parts that are not explicitly encoded (such as missing outer walls and tri-lithium).\nWhile the function
does do sanity checking, it will sometimes process entire map files that are not for dungeons or towers, without
returning an error.\nThe output can be rotated 90° (several times if you like).
@param nRotation 0 = no rotation, 1 = 90° rotation, 2 = 180° rotation, 3 = -90° rotation.
@param nMap The 0-based index for the level being mapped.
@param code The level as taken raw from a map file.
@param tiles The output, including all features implied by the engine (i.e. outer walls and tri-li)
@return True if everything in \c code had dungeon/tower codes, false if characters not conforming to dungeon/tower
codes.
*/
bool getMap(int nRotation, int nMap, unsigned char code[16][16], unsigned char tiles[17][17]) {
bool bGood = true;
const int inWidth = 16, inHeight = 16;
const int outWidth = 17, outHeight = 17;
memset(tiles, MAP_CODE_WALL, outWidth*outHeight); //so we get the uncoded outer wall
for (int x = 0; x < inWidth && bGood; x++) {
for (int y = 0; y < inHeight && bGood; y++) {
unsigned char c = code[y][x];
int nOutY = y, nOutX = x;
for (int i = 0; i < nRotation; i++) {
int nOriginalX = nOutX;
nOutX = outHeight - 1 - nOutY;
nOutY = nOriginalX;
}
unsigned char &o = tiles[nOutY][nOutX];
switch (c) {
case 0x00: o = MAP_CODE_FLOOR; break;
case 0x10: o = MAP_CODE_UP; break;
case 0x20: o = MAP_CODE_DOWN; break;
case 0x30: o = MAP_CODE_UPDOWN; break;
case 0x40: o = nMap < 15 ? MAP_CODE_CHEST : MAP_CODE_TRILI; break;
case 0x80: o = MAP_CODE_WALL; break;
case 0xC0: o = MAP_CODE_DOOR; break;
case 0xE0: o = MAP_CODE_SECRET; break;
default:
o = MAP_CODE_UNKNOWN;
bGood = false;
}
}
}
return bGood;
}