Ultima II Dungeon Format

From ModdingWiki
Jump to navigation Jump to search
Ultima II Dungeon Format
Ultima II Dungeon Format.png
Format typeMap/level
Map type2D cell-based
Layer count1
Cell dimensions16×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;
}