Difference between revisions of "PC-98 format"
(Added hacks for easier playtesting) |
(Added how to make a disk image) |
||
Line 140: | Line 140: | ||
*** Exit the loop. | *** Exit the loop. | ||
* Done. | * Done. | ||
+ | |||
+ | === How to make a disk image from files === | ||
+ | |||
+ | * First make an empty disk image: | ||
+ | ** Copy the Boot sector from the original disk image. | ||
+ | ** Fill the File allocation table with 0x00. | ||
+ | ** Fill the first 16 bytes of the File allocation table with 0xFF. | ||
+ | ** Fill the Directory with 0xFF. | ||
+ | ** Fill the Data area with 0xE5. | ||
+ | * Set ''current_sector'' = 8. | ||
+ | * For each file to be added: | ||
+ | ** Load the file into ''file_data''. | ||
+ | ** Set ''start_sector'' = ''current_sector''. | ||
+ | ** Set ''file_pos'' = 0. | ||
+ | ** Set ''file_len'' = the length of the file. | ||
+ | ** Repeat while ''file_len'' - ''file_pos'' > 0x400: | ||
+ | *** Let ''sector_data'' be 0x400 bytes of ''file_data'' from offset ''file_pos''. | ||
+ | *** Write ''sector_data'' into the sector numbered ''current_sector''. (offset = ''current_sector'' * 0x400, length = 0x400) | ||
+ | *** Write ''current_sector'' + 1 into the [[#File allocation table]] entry corresponding to ''current_sector''. (offset = 0x400 + (''current_sector'' * 2), length = 2) | ||
+ | *** Increment ''file_pos'' by 0x400. | ||
+ | *** Increment ''current_sector'' by 1. | ||
+ | ** Write the last sector of the file: | ||
+ | *** Let ''sector_data'' be the remaining data in ''file_data'' from offset ''file_pos''. | ||
+ | *** Let ''padded_sector_data'' be ''sector_data'' padded to 0x400 bytes with 0x00. | ||
+ | *** Write ''padded_sector_data'' into the sector numbered ''current_sector''. (offset = ''current_sector'' * 0x400, length = 0x400) | ||
+ | *** Set ''last'' = the length of ''sector_data'' (= ''file_len'' - ''file_pos''), bitwise or'ed with 0xFC00. | ||
+ | *** Write ''last'' into the [[#File allocation table]] entry corresponding to ''current_sector''. (offset = 0x400 + (''current_sector'' * 2), length = 2) | ||
+ | *** Increment ''current_sector'' by 1. | ||
+ | ** Add a new [[#Directory]] entry with the file name and ''start_sector''. | ||
== List of files == | == List of files == | ||
Line 1,229: | Line 1,258: | ||
* FM Towns version (PRI1.DAT) | * FM Towns version (PRI1.DAT) | ||
* VCLIP.DAT | * VCLIP.DAT | ||
− | |||
* TODO: Document the offsets of all texts? | * TODO: Document the offsets of all texts? | ||
Latest revision as of 17:36, 12 April 2022
This document describes the format of data in Prince of Persia 1 for the PC-98.
Contents
- 1 General
- 2 Disk images
- 3 File system
- 4 List of files
- 5 Compression
- 6 Levels
- 7 Palette
- 8 Tiles
- 9 Backgrounds
- 10 Sprites
- 11 Hit point images
- 12 Broderbund logo
- 13 Fonts
- 14 Music
- 15 Recorded moves
- 16 User disk
- 17 Hacks for easier playtesting
- 17.1 Don't play the Broderbund logo animation
- 17.2 Don't load the Broderbund logo animation
- 17.3 Skip the intro
- 17.4 Skip the main menu
- 17.5 Set the starting level and HP if the main menu is skipped
- 17.6 Set the starting time if the main menu is skipped
- 17.7 Set the starting level, HP, and time if the main menu is not skipped
- 17.8 Set the demo level and HP
- 18 TODO
- 19 External links
General
Byte order: Numbers larger than a byte use little-endian (Intel) byte order, unless noted otherwise.
Number systems: Hexadecimal numbers are prefixed with 0x. This is omitted in sequences of bytes, for readability.
Numbering of bits: The least significant bit of a byte is bit 0, the most significant bit of a byte is bit 7.
Tools: A Japanese programmer, Zarazala, has made some tools for working with the files described in this document. You can get them from #External links. In each section below, I mention the relevant tool.
Disk images
Disk images can be either headerless (*.TFD), or have a header (*.FDI). Emulators can use both.
Online, this game is most commonly found in *.FDI files, these have a header of 0x1000 bytes.
- Tools: datacut.exe to extract files from a (headerless!) disk image, makeimg.exe to make a disk image from files.
How recognize and convert headered images
A headerless game disk starts with the bytes EB 0A. If they appear at offset 0x1000 then you have a headered disk image.
To convert a headered disk image to headerless, go to offset 0x1000 and delete everything before it. You might want to save the new file under a different name. On the Linux CLI, you can use: $ dd if="DISK_A.FDI" of="DISK_A_HL.FDI" skip=4096 bs=1
FDI header
The FDI header contains the following information:
Offset in bytes | Size | Value in PoP disk images | Meaning |
---|---|---|---|
0x00 | 4 bytes | 0 | (unknown) |
0x04 | 4 bytes | 0x90 = 144 | (unknown) |
0x08 | 4 bytes | 0x1000 = 4096 | Start offset of the raw disk image (i.e. the size of the FDI header). |
0x0C | 4 bytes | 0x134000 = 1261568 | Size of the raw disk image in bytes. |
0x10 | 4 bytes | 0x400 = 1024 | Size of a sector (or cluster?) in bytes. |
0x14 | 4 bytes | 8 | Number of sectors per track? |
0x18 | 4 bytes | 2 | Number of sides? |
0x1C | 4 bytes | 0x4D = 77 | Number of tracks per side? |
If you multiply the last four numbers, you get the size of the raw disk image: 0x400 * 8 * 2 * 0x4D = 0x134000.
The rest of the header is filled with zeroes.
File system
A disk consists of 0x4D0 sectors, numbered from 0 to 0x4CF. Each sector is 0x400 = 1024 bytes.
The game disk (disk A) contains the following parts:
Offset in bytes | Occupied sectors | Contents |
---|---|---|
0x0000 - 0x03FF | 0 | #Boot sector |
0x0400 - 0x0FFF | 1 - 3 | #File allocation table |
0x1000 - 0x1FFF | 4 - 7 | #Directory |
0x2000 - 0x133FFF | 8 - 0x4CF | Data area |
The offsets in the table assume you have a headerless disk image.
Boot sector
The boot sector contains the code for starting the game.
From offset 2 is the disk label "GAME DISK" on disk A, and "USER DISK" on disk B.
Directory
This area contains 0x100 = 256 entries, of 0x10 = 16 bytes each.
Each entry describes a file in the following format:
Offset in bytes | Size | Meaning |
---|---|---|
0 | 8 bytes | Base name. If it's shorter than 8 characters then it's padded with spaces (0x20). |
8 | 3 bytes | Extension. (The same padding is used here, although it does not occur in the original PoP disk image.) |
0x0B = 11 | 3 bytes | Unknown. Might be file attributes and/or modification date/time? |
0x0E = 14 | 2 bytes | The number of the first sector of this file. |
To get the full file name, concatenate the base name (with padding removed), a dot, and the extension.
Unused entries are filled with 0xFF bytes.
File allocation table
This area contains 2-byte entries for each sector on the disk.
There is space for 0x600 entries, but only the first 0x4D0 entries are used, since the disk has only that many sectors. The unused entries are filled with zeroes.
The possible values of each entry are:
- 0xFFFF : If the sector is before the data area (sectors 0-7).
- 0x0008 - 0x04CF : If this sector is part of a file but not the last sector.
- This whole sector is part of the file.
- The value is the number of the next sector of the file.
- 0xFC00 - 0xFFFF : If this is the last sector of a file.
- Subtract 0xFC00 from the value to get a size. The first size bytes of this sector are part of the file.
- If you get 0 from the subtraction, then the whole sector is part of the file. (This does not occur in the original PoP disk image, though.)
- The unused area of last sectors is filled with 0x00. (makeimg.exe fills it with 0xE5.)
- 0x0000 : If the sector is not used or does not exist (unused entries).
- Unused sectors are filled with 0xE5.
How to read a file
- Find the file in the #Directory.
- Read the number of the first sector into current_sector.
- Repeat until exit:
- current_sector should be a valid sector number pointing into the data area (between 0x0008 and 0x04CF). If it's not then it's an error.
- Read the #File allocation table entry corresponding to current_sector into next_sector. (offset = 0x400 + (current_sector * 2), length = 2)
- Read the sector numbered current_sector into sector_data. (offset = current_sector * 0x400, length = 0x400)
- If next_sector < 0xFC00:
- Append sector_data to the output.
- Set current_sector to next_sector.
- Continue the loop.
- else:
- Set size to next_sector - 0xFC00.
- If size = 0 then set size to 0x400.
- Append the first size bytes of sector_data to the output.
- Exit the loop.
- Done.
How to make a disk image from files
- First make an empty disk image:
- Copy the Boot sector from the original disk image.
- Fill the File allocation table with 0x00.
- Fill the first 16 bytes of the File allocation table with 0xFF.
- Fill the Directory with 0xFF.
- Fill the Data area with 0xE5.
- Set current_sector = 8.
- For each file to be added:
- Load the file into file_data.
- Set start_sector = current_sector.
- Set file_pos = 0.
- Set file_len = the length of the file.
- Repeat while file_len - file_pos > 0x400:
- Let sector_data be 0x400 bytes of file_data from offset file_pos.
- Write sector_data into the sector numbered current_sector. (offset = current_sector * 0x400, length = 0x400)
- Write current_sector + 1 into the #File allocation table entry corresponding to current_sector. (offset = 0x400 + (current_sector * 2), length = 2)
- Increment file_pos by 0x400.
- Increment current_sector by 1.
- Write the last sector of the file:
- Let sector_data be the remaining data in file_data from offset file_pos.
- Let padded_sector_data be sector_data padded to 0x400 bytes with 0x00.
- Write padded_sector_data into the sector numbered current_sector. (offset = current_sector * 0x400, length = 0x400)
- Set last = the length of sector_data (= file_len - file_pos), bitwise or'ed with 0xFC00.
- Write last into the #File allocation table entry corresponding to current_sector. (offset = 0x400 + (current_sector * 2), length = 2)
- Increment current_sector by 1.
- Add a new #Directory entry with the file name and start_sector.
List of files
In the order they appear in the #Directory:
file | format | contents |
---|---|---|
MUSIC.SYS | program | music player |
MAIN.PRG | program | gameplay, cutscenes, #Hit point images |
SUBPROG.PRG | program | menus, intro, ending |
BJLOGO.PRG | program | #Broderbund logo animation (BJ = Broderbund Japan?) |
PALET.DAT | palette | the palette used by the game |
FONT.DAT | font | font used in the menu, and for messages like the level number |
FIRE.DAT | sprites | torch flame used on the levels |
MUSIC1.MUS | music | level 0, 4-6, 10-11 |
MUSIC2.MUS | music | level 1-3, 7-9 |
MUSIC3.MUS | music | level 14 |
MUSIC4.MUS | music | level 12-13 |
FIGHT.MUS | music | guard fight |
FIGHTF.MUS | music | final fight (level 15) |
FIGHTS.MUS | music | skeleton fight (level 3) |
FIGHTK.MUS | music | shadow fight (level 12) (K might stand for kage, Japanese for shadow.) |
VICT1.MUS | music | guard died |
APPEAR.MUS | music | meet Jaffar on level 14 |
APPEAR2.MUS | music | meet shadow on level 6, meet level 13 boss |
PICKUP.MUS | music | get sword |
POWUP.MUS | music | big potion, merged shadow (level 12) |
KUSURI.MUS | music | heal potion (kusuri is Japanese for medicine.) |
CLEAR.MUS | music | level complete |
EXIT.MUS | music | exit door open |
DEAD.MUS | music | died outside battle |
DEAD2.MUS | music | died in battle |
OPEN.MUS | music | title screen, intro |
ROOM1.MUS | music | intro cutscene |
ROOM2.MUS | music | cutscenes |
END1.MUS | music | ending: meet the princess |
END2.MUS | music | ending: memories |
END3.MUS | music | ending: garden |
TIMEOUT.MUS | music | time expired |
MENU.MUS | music | main menu |
SOUND.EFC | sound effects | sound effects used when synthesizer hardware is available (in Neko Project 2: Device → Sounds → PC-9801-86) |
BEEP.EFC | sound effects | sound effects used when only a beeper is available (in Neko Project 2: Device → Sounds → Disable boards) |
JAFFER1.DAT | background | Intro: Jaffar's face |
JAFFER2.DAT | background | Intro: Jaffar's hands and top half of crystal ball |
JAFFER3.DAT | background | Intro: bottom half of crystal ball |
OPEN1.DAT | background | Intro: In the crystal ball: the prince and the princess |
OPEN2.DAT | background | Intro: In the crystal ball: the prince and the princess caught |
OPEN3.DAT | background | Intro: In the crystal ball: the prince in dungeon |
OPEN11.DAT | background | Intro: Waving water for OPEN1.DAT |
OPEN12.DAT | background | Intro: Waving water for OPEN1.DAT |
OPEN32.DAT | background | Intro: Torch flame for OPEN3.DAT |
OPEN33.DAT | background | Intro: Torch flame for OPEN3.DAT |
JAF_S1.DAT | background | Intro: Jaffar grinning |
JAF_S2.DAT | background | Intro: Jaffar grinning |
JAF_E1.DAT | background | Intro: Jaffar's eyes glowing |
JAF_E2.DAT | background | Intro: Jaffar's eyes glowing |
JAF_E3.DAT | background | Intro: Jaffar's eyes glowing |
JAF_E4.DAT | background | Intro: Jaffar's eyes glowing |
TITLE.DAT | background | Title screen: Persia at night |
TITLE2.DAT | background | Title screen: game title |
TITLE3.DAT | background | Title screen: copyrights |
TITLE4.DAT | background | Title screen: "a game by Jordan Mechner" |
PROOM.DAT | background | Princess's room |
VCLIP.DAT | raw 1bpp image? | clipping masks? |
MOYOU.DAT | background | background of the main menu (moyō is Japanese for pattern, design.) |
TRAP.DAT | (compressed) sprites | traps, potions, doors, everything not part of the background (level objects except buttons and loose floors) |
PLATE1.DAT | (compressed) sprites | buttons, loose floors for LEV01.CHR |
PLATE4.DAT | (compressed) sprites | buttons, loose floors for LEV04.CHR |
PLATE7.DAT | (compressed) sprites | buttons, loose floors for LEV07.CHR |
PLATEA.DAT | (compressed) sprites | buttons, loose floors for LEV10.CHR |
PLATEC.DAT | (compressed) sprites | buttons, loose floors for LEV12.CHR |
PLATEE.DAT | (compressed) sprites | button for LEV14.CHR |
CHTAB1.DAT | (compressed) sprites | prince |
CHTAB2.DAT | (compressed) sprites | prince and mouse |
CHTAB3.DAT | (compressed) sprites | prince and sword |
CHTAB4F.DAT | (compressed) sprites | fat guard on level 6 |
CHTAB4G.DAT | (compressed) sprites | regular guard |
CHTAB4K.DAT | (compressed) sprites | shadow on level 12 (K might stand for kage, Japanese for shadow.) |
CHTAB4S.DAT | (compressed) sprites | skeleton on level 3 |
CHTAB4V.DAT | (compressed) sprites | the boss on level 13 (V might stand for Vizier?) |
CHTAB4X.DAT | (compressed) sprites | Jaffar on level 14, 15 |
CHTAB5.DAT | (compressed) sprites | prince |
CHTAB6.DAT | (compressed) sprites | princess, Jaffar in the intro, hourglass, torch flame used in the princess's room |
CHTAB7.DAT | (compressed) sprites | Jaffar in the intro |
LEV01.CHR | (compressed) tiles | graphics for levels 0, 1, 2, 3 (dungeon) |
LEV04.CHR | (compressed) tiles | graphics for levels 4, 5, 6 (palace) |
LEV07.CHR | (compressed) tiles | graphics for levels 7, 8, 9 (dungeon) |
LEV10.CHR | (compressed) tiles | graphics for levels 10, 11 (palace) |
LEV12.CHR | (compressed) tiles | graphics for levels 12, 13 (dungeon) |
LEV14.CHR | (compressed) tiles | graphics for level 14 (halls) |
LEV15.CHR | (compressed) tiles | graphics for level 15 |
LEV00.MAP | (compressed) level | level 0 (demo) |
LEV01.MAP | (compressed) level | level 1 |
LEV02.MAP | (compressed) level | level 2 |
LEV03.MAP | (compressed) level | level 3 |
LEV04.MAP | (compressed) level | level 4 |
LEV05.MAP | (compressed) level | level 5 |
LEV06.MAP | (compressed) level | level 6 |
LEV07.MAP | (compressed) level | level 7 |
LEV08.MAP | (compressed) level | level 8 |
LEV09.MAP | (compressed) level | level 9 |
LEV10.MAP | (compressed) level | level 10 |
LEV11.MAP | (compressed) level | level 11 |
LEV12.MAP | (compressed) level | level 12 (12a) (up to the Shadow fight) |
LEV13.MAP | (compressed) level | level 13 (12b) (a boss, but not Jaffar yet) |
LEV14.MAP | (compressed) level | level 14 (12c) (the halls leading to the princess) |
LEV15.MAP | (compressed) level | level 15 (12d) (the fight with Jaffar) |
DEMOPLAY.KEY | recorded moves | Automatic moves of the prince for the demo level. |
EFONT.DAT | font | ending credits font |
ENDING1.DAT | background | Ending: the prince bows before the sultan |
ENDING2.DAT | background | Ending: the prince and the princess wave to the people |
ENDING3.DAT | background | Ending: the prince and the princess together in the sunset |
ENDING4.DAT | background | Ending: palace garden |
ENDING5.DAT | background | Ending: "The End" text |
ENDING6.DAT | background (multiple images) | Ending: waving flags |
Compression
- Tools: compress.exe
The following files are compressed: TRAP.DAT, PLATE*.DAT, CHTAB*.DAT, LEV*.CHR, LEV*.MAP
Compressed data is stored as a series of blocks. Each block starts with a head byte specifying what to do, followed by 0 to 4 argument bytes.
Each block adds 4 bytes to the output, except those with head bytes 0x11, 0x21, and 0x91. Those add multiples of 4 bytes.
There is no end marker, the decompression ends when the compressed file ends.
In the arguments and the output column, each group of two letters represents a byte.
In the following table, the head bytes are ordered first by their second half, then by their first half. The system in the codes is more visible this way.
head byte | arguments | output |
---|---|---|
0x00 | xx yy zz ww | xx yy zz ww (i.e. copy the next 4 bytes to the output) |
0x01 | - | Repeat the last 4 bytes of the output. |
0x11 | nn | Repeat the last 4 bytes of the output, 0xnn + 1 times. |
0x21 | nn mm | Repeat the last 4 bytes of the output, 0xmmnn + 1 times. |
0x81 | - | Repeat the penultimate 4 bytes of the output. |
0x91 | nn | Repeat the penultimate 4 bytes of the output, 0xnn + 1 times.
Phrased differently, start from the 8th last byte of the output, and copy (0xnn + 1) * 4 bytes. |
0x02 | xx | xx xx xx xx (i.e. write the next byte 4 times to the output) |
0x03 | xx yy | xx yy yy yy |
0x13 | xx yy | xx yy xx xx |
0x23 | xx yy | xx xx yy xx |
0x33 | xx yy | xx xx xx yy |
0x04 | xx yy | xx xx yy yy |
0x14 | xx yy | xx yy xx yy |
0x24 | xx yy | xx yy yy xx |
0x44 | xx yy zz | xx xx yy zz |
0x54 | xx yy zz | xx yy xx zz |
0x64 | xx yy zz | xx yy zz xx |
0x74 | xx yy zz | xx yy yy zz |
0x84 | xx yy zz | xx yy zz yy |
0x94 | xx yy zz | xx yy zz zz |
0xN5 (N=0..F) | ba dc | Na Nb Nc Nd (i.e. write bytes whose upper half comes from the head and their lower half comes from the arguments) |
0xN6 (N=0..F) | ba dc | aN bN cN dN (i.e. write bytes whose lower half comes from the head and their upper half comes from the arguments) |
0x07 | xx | 00 00 00 xx |
0x17 | xx | 00 00 xx 00 |
0x27 | xx | 00 xx 00 00 |
0x37 | xx | xx 00 00 00 |
0x47 | xx yy | 00 00 xx yy |
0x57 | xx yy | 00 xx 00 yy |
0x67 | xx yy | 00 xx yy 00 |
0x77 | xx yy | xx 00 00 yy |
0x87 | xx yy | xx 00 yy 00 |
0x97 | xx yy | xx yy 00 00 |
0xA7 | xx yy zz | 00 xx yy zz |
0xB7 | xx yy zz | xx 00 yy zz |
0xC7 | xx yy zz | xx yy 00 zz |
0xD7 | xx yy zz | xx yy zz 00 |
0xF7 | - | 00 00 00 00 |
0xN8 (N=0..D or F) | see above | Like 0xN7, but instead of 00, use FF. |
0xN9 (N=0..D) | see above | Like 0xN7, but instead of 00, use the corresponding byte(s) from the last 4 bytes of the output (the values from 4 bytes before). |
0xNA (N=0..D) | see above | Like 0xN7, but instead of 00, use the corresponding byte(s) from the penultimate 4 bytes of the output (the values from 8 bytes before). |
Levels
- Tools: mapedit.exe
The levels are stored in the (compressed) LEV*.MAP files.
After decompression, levels contain the following data:
Offset | Size | Contents |
---|---|---|
0x0000..0x07FF | 2048 (128 halfblocks * 4*4 CHR tiles) | CHR tiles of each halfblock |
0x0800..0x08FF | 256 (128 blocks * 2 halfblocks) | back layer halfblocks of each block |
0x0900..0x09FF | 256 (128 blocks * 2 halfblocks) | front layer halfblocks of each block (The most significant bit is sometimes set, what does it mean?) |
0x0A00..0x0AFF | 256 (128 blocks * 2 bytes) | #Block flags and objects of each block |
0x0B00..0x0DCF | 720 (24 rooms * 30 tiles) | blocks in each room (The most significant bit is sometimes set, what does it mean?) |
The remaining parts correspond to the DOS / Apple II level format from offset 0x2D0. | ||
0x0DD0..0x109F | 720 (24 rooms * 30 tiles) | modifiers in each room |
0x10A0..0x119F | 256 | #Door events 1 |
0x11A0..0x129F | 256 | #Door events 2 |
0x12A0..0x12FF | 96 (24 rooms * 4 directions) | room links (left,right,up,down) (1 to 24, or 0 if there is no adjacent room) |
0x1300 | 1 | number of used rooms |
0x1301..0x1318 | 24 | the column (x) of each room within the level map (unused) |
0x1319..0x1330 | 24 | the row (y) of each room within the level map (unused) |
0x1331..0x133F | 15 | unused |
0x1340 | 1 | starting room (1 to 24) |
0x1341 | 1 | starting tile position (0 to 29, row*10+column) |
0x1342 | 1 | starting direction (0x00=right, 0xFF=left) Inverted on level 1 and level 13, because the prince does not turn around on those levels. |
0x1343..0x1346 | 4 | unused |
0x1347..0x135E | 24 | guard tile position for each room (0 to 29 like the starting tile position, or 30=0x1E if no guard) |
0x135F..0x1376 | 24 | guard direction for each room (0x00=right, 0xFF=left) |
0x1377..0x138E | 24 | unused? |
0x138F..0x13A6 | 24 | unused? |
0x13A7..0x13BE | 24 | guard skill for each room |
0x13BF..0x13D6 | 24 | unused? |
0x13D7..0x13EE | 24 | unused? (DOS PoP stores the guard color here) |
0x13EF..0x13FF | 17 | unused |
For level 15 (the final battle), the starting position is not used. The prince stays in the same room (room 1), tile, and direction where he was on level 14, before Jaffar teleported him to level 15.
Tiles and blocks
Tiles from LEV*.CHR files are 16×16 pixels. 4×4 tiles are combined into a halfblock (64×64 pixels). Then two halfblocks (top and bottom halves) are combined in each layer (back, front) of each block (64×128 pixels). These blocks are combined into rooms.
Blocks changed by the game
(Based on the documentation by Zarazala)
When you pick up an item or a loose floor starts falling, the block number of that tile is incremented by 1. Therefore, for each block with a potion, a sword, or a loose floor, the next block must have the same graphics and flags but no object.
Special events which change tiles similarly increase the block number by 1. The exception is the appearing of the mirror, which decreases the block number by 1.
Landing loose floors, however, don't change block numbers. The broken floors are drawn from PLATE*.DAT.
Block 0 is used for the edges of the level. It is always a wall on the original levels.
Block flags and objects
Only the first byte of each item is used, the second bytes are zero.
byte | meaning | notes |
---|---|---|
flags -- these can be combined with objects | ||
0x01 | floor | |
0x02 | wall | |
0x04 | torch | Unlike in DOS / Apple II PoP, torches appear in the tile where they are placed, not one tile to the right. |
objects | ||
0x00 | none | |
0x08 | loose floor | |
0x10 | open button | The modifier selects the (first) door event to be triggered. |
0x18 | close button | The modifier selects the (first) door event to be triggered. |
0x20 | gate | If the modifier is 1 then the door starts open, else it starts closed. |
0x28 | spike | |
0x30 | heal potion | |
0x38 | hurt potion | |
0x40 | life potion | |
0x48 | upside down potion | |
0x50 | slow fall potion | |
0x58 | chomper | |
0x60 | sword | |
0x68 | mirror | Not used in level data: Placed on level 4 when the exit door opens. |
0x70 | level door | |
0x78 | skeleton | |
0x80 | door top | |
0x88 | balcony stars | Used in the top left corner of balconies. |
Door events
(same format as in the DOS / Apple II version) ------------------------- 7 6 5 4 3 2 1 0 <-bits byte from door events 1 | NX R1 R0 L4 L3 L2 L1 L0 byte from door events 2 | R4 R3 R2 0 0 0 0 0
R4 R3 R2 R1 R0: The number of the room. (1..24)
L4 L3 L2 L1 L0: The location in the room. (0..29)
NX: If zero then trigger next door event, if one then don't.
Palette
The palette is stored in PALET.DAT.
It is used for the whole game, except the following:
- It does not affect the Broderbund logo animation.
- Nor does it affect the flashes of the screen when someone is hurt, etc. (those colors are in MAIN.PRG)
It contains 7 RGB color values, for palette slots 1-7. Slot 0 is not in the file, it is always black.
Each color is encoded in two bytes, with the bits arranged as: rrrrbbbb 0000gggg
That is, in the first byte, bits 0-3 store the blue intensity, bits 4-7 store the red intensity. In the second byte, bits 0-3 store the green intensity, and bits 4-7 are unused.
The default colors are:
index | bytes | HTML | color | |
---|---|---|---|---|
0 | - | #000 | black | |
1 | 09 00 | #009 | blue | |
2 | A0 00 | #A00 | red | |
3 | BB 00 | #B0B | magenta | |
4 | 00 08 | #080 | green | |
5 | 0D 0B | #0BD | cyan | |
6 | D0 0D | #DD0 | yellow | |
7 | EE 0E | #EEE | white |
Tiles
- Tools: chrconv.exe
Tiles are stored in the (compressed) LEV*.CHR files.
After decompression, they contain data for 256 tiles, each is 0x80 bytes long. The data of each tile is further split into four sections (bitplanes) of 0x20 bytes each.
To decode the image of a tile, arrange the bits of each plane in a 16×16 array. The bits should be read from bit 0 to bit 7, and the array should be filled left to right, top to bottom.
The first three planes contain bits 0, 1, and 2 of the color indices for each pixel. These are then combined into a three bits-per-pixel image.
With the default palette, these bitplanes roughly correspond to blue, red, and green components, respectively. I say "roughly" because the intensity of the enabled components is different for different colors.
The fourth plane contains transparency masks, but they are used only for tiles in the foreground layer? Each bit of the transparency mask is 0 for transparent parts and for some backgrounds, 1 otherwise.
chrconv.exe treats the transparency mask as bit 3 of the color index.
The 256 tiles should be displayed in a 16×16 arrangement, this keeps related tiles together.
Backgrounds
- Tools: picconv.exe
Backgrounds are images without transparent parts. They don't necessarily fill the screen.
Backgrounds have this header:
- 2 bytes: Width in bytes. Multiply by 8 to get the width in pixels.
- 2 bytes: Height in pixels.
After the header comes the image data. The image data is compressed, but with a different method than described in #Compression.
The basic idea is the same, though: Compressed data is stored as a series of blocks. Each block starts with a head byte specifying what to do, followed by 0 to 3 argument bytes.
Backgrounds decode into three bitplanes, similarly to #Tiles, except the bytes of the planes are interleaved.
Each block adds 3 bytes to the output, or rather, one byte to each of the 3 bitplanes. The exceptions are the blocks which repeat bytes. These add multiples of 3 bytes, or, the same number of bytes to each bitplane.
There is no end marker, the decompression ends when the compressed file ends (or when the output has enough bytes?).
In the arguments and the output column, each group of two letters represents a byte.
In the following table, the head bytes are ordered first by their second half, then by their first half. The system in the codes is more visible this way.
head byte | arguments | output |
---|---|---|
0x00 | nn | Repeat the next 3×1 bytes of the output 0xnn times. |
0xN0 (N=1..F) | - | Repeat the next 3×1 bytes of the output N times. |
Note: The above two codes are a bit tricky. When you see a head byte ending in 0, don't write anything into the output yet.
Just set a flag which reminds you that you need to repeat, and also store the value of nn or N as repeat_count. Every time after you processed a block which is not a "repeat next" block, check if the repeat flag is set. If it is, then repeat the last 3×1 bytes repeat_count times. | ||
0x01 | xx | xx xx xx (i.e. write the next byte 3 times to the output) |
0x02 | xx yy | xx xx yy |
0x12 | xx yy | xx yy xx |
0x22 | xx yy | xx yy yy |
0x03 | xx yy zz | xx yy zz (i.e. copy the next 3 bytes to the output) |
0x04 | xx | 00 00 xx |
0x14 | xx | 00 xx 00 |
0x24 | xx | xx 00 00 |
0x34 | xx yy | 00 xx yy |
0x44 | xx yy | xx 00 yy |
0x54 | xx yy | xx yy 00 |
0x05 | xx | FF FF xx |
0x15 | xx | FF xx FF |
0x25 | xx | xx FF FF |
0x35 | xx yy | FF xx yy |
0x45 | xx yy | xx FF yy |
0x55 | xx yy | xx yy FF |
Note: 0xN5 head bytes are like 0xN4 head bytes, except they use 0xFF instead of 0x00. | ||
0x06 | - | 00 00 00 |
0x16 | - | FF 00 00 |
0x26 | - | 00 FF 00 |
0x36 | - | FF FF 00 |
0x46 | - | 00 00 FF |
0x56 | - | FF 00 FF |
0x66 | - | 00 FF FF |
0x76 | - | FF FF FF |
Note: A 0xN6 head byte basically appends 8 pixels with color N.
The appended bytes are filled with bits 4, 5, and 6 of the head byte, respectively. | ||
0xR7 | nn | Go up R+1 scanlines from where you are (i.e. go back (R + 1) × width in bytes × 3 bytes), read 0xnn × 3 bytes from there, and append them to the output. |
ENDING6.DAT
ENDING6.DAT has a slightly different format, because it contains multiple images.
The file begins with 12 offsets, 2 bytes each. These offsets tell where each image starts in the file.
The images themselves are in the same format as the other #Backgrounds.
Sprites
- Tools: patview.exe
Sprites are in the files FIRE.DAT, CHTAB*.DAT, PLATE*.DAT, TRAP.DAT. They are compressed, except for FIRE.DAT.
The files begin with the following header:
- 2 bytes: The number of images.
- For each image:
- 2 bytes: The offset part of the address. (Always in 0x0..0xF?)
- 2 bytes: The segment part of the address.
- To get the actual start address of the image, calculate segment * 16 + offset.
- This strange form of addressing comes from the x86-compatible CPU in the PC-98.
The images are in the following format:
- 1 byte: Width in bytes. Multiply by 8 to get the width in pixels.
- 1 byte: Height in pixels.
- Data.
The data of the sprite images can decompressed in the following way:
- Repeat until you have width in bytes × height × 4 bytes in the output:
- Read a head byte.
- If head byte >= 0x80:
- Append (head byte - 0x80 + 1) × 4 zero bytes to the output.
- else:
- Copy the next (head byte + 1) × 4 bytes from the input to the output.
The resulting output consists of four interleaved bitplanes.
- To decode, go through the output four bytes at a time.
- Within each group, the first byte belongs to the first bitplane, the second byte to the second bitplane, and so on.
- The first three planes contain bits of the color indices, while the fourth contains a transparency mask.
- Each bit of the transparency mask is 0 for transparent pixels, 1 otherwise.
- patview.exe treats the transparency mask as bit 3 of the color index.
- The bits in the bitplanes are arranged in the same way as in tiles and backgrounds: the bits from bit 0 to bit 7 go into 8 pixels from left to right.
- But the order of bytes is different: Each byte corresponds to a block of 8 pixels, below the previous block.
- This will first fill a vertical strip of 8 pixels on the left side of the image.
- When you reach the bottom of the image, continue at the next vertical strip of 8 pixels.
Hit point images
The hit point images are in MAIN.PRG at the following offsets:
offset | content |
---|---|
0xAE92..0xAFB1 | prince's full hit point (red) |
0xAFB2..0xB0D1 | prince's empty hit point (black) |
0xB0D2..0xB1F1 | guard's full hit point (green) |
All of them are 24×24 pixels, and are stored as four bitplanes, like the tiles.
Broderbund logo
BJLOGO.PRG contains the images of the Broderbund logo animation shown at startup.
(BJ probably stands for Broderbund Japan. [1])
Black and white images
Offsets 0xF68..0xF6D contain the offsets of the three b/w images.
The offsets are as follows:
offset | image |
---|---|
0x0F6E | triple crown |
0x117E | "Broderbund" |
0x161E | "Presents" |
These images are stored in the following format:
- 2 bytes: width (in pixels)
- 2 bytes: height
- For each scanline (1..height):
- 2 bytes: offset of the next scanline (relative to the start address of the image)
- Repeat until you have the whole scanline:
- Read a head byte.
- If head byte == 0x00 then this scanline is the same as the previous. Exit the loop.
- If 0x01 <= head byte <= 0x7E then append head byte black pixels.
- If head byte == 0x7F then
- Read extra byte.
- Append extra byte + 0x7E black pixels.
- If head byte >= 0x80 then append head byte - 0x80 white pixels.
Color image
The color image starts at offset 0x17CE and it's in the same format as #Backgrounds.
Fonts
- Tools: fontconv.exe
Fonts are stored as a sequence of 1-bits-per-pixel images. The bits within a byte correspond to pixels in the same way as for other images, but now there is only one bitplane.
The size of the images and the included characters depend on the font.
File | number and size of glyphs | contained characters | notes |
---|---|---|---|
FONT.DAT | 48 glyphs of 16×16 pixels (32 bytes) each | 0-9 A-Z . , / # (followed by 8 empty images) | "#" appears as the symbol of the Enter key: ↵⃣ |
EFONT.DAT | 45 glyphs of 24×24 pixels (72 bytes) each | A-M O P S T Y a-i k-p r-w y z ( ) - . | missing letters: N Q R U-X Z j q x |
For FONT.DAT, the advance width of all glyphs is 16 pixels.
SUBPROG.PRG contains the following information about EFONT.DAT:
- Offsets 0x556E..0x55CD contain a mapping table from the ASCII codes 0x20..0x7F to the (1-based) glyph indices in the font.
- Zero means the font has no glyph for the character.
- Offsets 0x55CE..0x55FB contain the advance width for each glyph, in pixels.
- The table has 46 bytes, one more than glyphs in the font. Perhaps the last item is for the space?
The font used on the images in TITLE2.DAT and TITLE3.DAT is similar to the font in EFONT.DAT, but it has slightly shorter capital letters.
Japanese font
The game does not contain font for Japanese text. Instead, it uses the 16×16 font contained in the ROM of PC-98. (FONT.ROM in Neko Project 2.)
The game transforms that font slightly:
- In the menu, the font is made thicker.
- This is done by drawing each glyph twice, the second time offset by one pixel to the side.
- In the intro, the font is made thicker as above.
- Additionally, a black outline is added to the glyphs, and they are filled with a gradient which goes from yellow at the top to white at the bottom.
Texts
The texts of the game are in MAIN.PRG and SUBPROG.PRG.
Texts use the Shift-JIS encoding. For English text, this matches ASCII.
- The end of the text is marked with 0x00.
- Newlines are marked with 0x0D (ASCII carriage return).
- 0x1E denotes a narrow space (8 pixels wide) used for centering menu items and table captions.
- 0x1F does the same, but it's not used in the original texts.
- All other characters display as wide (16 pixels wide).
- This includes the ASCII space (0x20), that's why a separate code exists for narrow space.
- The existing English translation would look less awkward if ASCII characters could appear as narrow.
Music
- Compare with SNES format#Music.
Music is stored in *.MUS files. Sound effects are stored in *.EFC files.
Both start with the following header:
- 2 bytes: Offset to the instruments data.
- 1 byte: 0
.MUS files continue with the following:
- 1 byte: 0
- 1 byte: Flags to enable channels. (Bits 0..5 correspond to channels 1..6.) Should be 0x3F to enable all channels.
- For each of the 6 channels:
- 2 bytes: Offset to the sequence of this channel.
Sequences
The structure of each channel's sequence:
- For each item:
- 1 byte: Repeat count (0x01..0xFD).
- 2 byte: Offset of the command string.
- (The items are played in sequence.)
- 1 byte: end marker
- 0xFF = stop (for "event" music, for example potions) (0x00 seems to mean the same?)
- 0xFE = loop (for background music)
- 0xFE is followed by a 2 byte offset (into the sequence) that tells where to restart when looping.
.EFC files have a single sequence starting from offset 3. Each item of this sequence represents a separate sound effect. There is no end marker.
Command strings
Command strings are ASCII strings terminated with a 0x00 byte.
The possible commands are:
command | parameter | scope | meaning | |||
---|---|---|---|---|---|---|
type | range | initial (on startup) |
default (if omitted) | |||
T | n | 0-255 | 0 | 0 | global | set tempo, i.e. set the unit of note length to n * 0.0003 seconds |
V | n | 0-15 | 13 | 0 | channel | set volume |
^ | channel | increase volume (wraps around from 15 to 0) | ||||
_ | channel | decrease volume (stops at 0) | ||||
@ | n | 0-255 | 255 | 0 | channel | set instrument |
O | n | 0-7 | 4 | 0 | channel | set octave |
+ | channel | up one octave (wraps around from 7 to 0) | ||||
- | channel | down one octave (wraps around from 0 to 7) | ||||
C,D,E,F,G,A,B | n | 0-255 | 0 | previous | channel | play note (n = set length of this note and next ones, relative to Tn) |
# | channel | make the next note sharp | ||||
R | n | 0-255 | 0 | previous | channel | play rest (n = same as for CDEFGAB) |
[ | channel | start merge? (disable Decay?) | ||||
& | channel | separate merge? (disable Attack?) | ||||
] | channel | end merge? (enable Attack and Decay?) | ||||
) | n | 0-255 | 0 | channel | set detune? (signed) | |
! | global | Notify the game that it can continue. Used in APPEAR.MUS and ROOM1.MUS. |
Instruments
The structure of instruments data:
- For each instrument:
- 2 byte: Offset to instrument data of this instrument.
Each instrument data is 31 bytes. I haven't looked into their contents. The documentation by Zarazala in the #External links contains some information about them.
Recorded moves
DEMOPLAY.KEY contains the automatic moves of the prince for the demo level.
- For each item:
- 1 byte: state of the keys (bitflags)
- 1 byte: number of ticks for which the specified state applies
Each state byte is the sum of the values corresponding to the pressed keys:
- 0x01 = up
- 0x02 = down
- 0x04 = left
- 0x08 = right
- 0x10 = Space (has the same effect as Shift, not used in the original DEMOPLAY.KEY)
- 0x20 = Shift
- 0x40 = (no effect)
- 0x80 = (no effect)
The demoplay ends when both bytes of an item are 0xFF.
User disk
The user disk (Disk B) contains the following data.
All offsets are for headerless disk images.
At 0x00..0x0F: Header
- 2 bytes: 01 00
- 10 bytes: "USER DISK" 00 -- The game checks these bytes to see if the disk is a user disk.
- 4 bytes: date and time when the user disk was created/formatted
The date is stored in the following format:
offset | 0x0C | 0x0D | 0x0E | 0x0F | ||||||||||||||||||||||||||||
bits | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
meaning | year | month | day | hour | minute | second |
That is, if you consider the value to be a single 32-bit big-endian number, then you have the following fields from MSB to LSB:
- 6 bits: year: The lower 6 bits of the last two digits of the year. In C code: (year % 100) % 64.
- 4 bits: month (1-12)
- 5 bits: day (1-31)
- 5 bits: hour (0-23)
- 6 bits: minute (0-59)
- 6 bits: second (0-59)
At 0x10..0x5F: Saved games
- For each save slot 1..10:
- 1 byte: level number (1-14), or 0 if the slot is empty
- Level 13 and 14 are shown as 12 in the save/load game menus.
- If you save on level 15 (final fight), the save will be for level 14.
- If you try to load a save with level 15, you get an error message.
- 1 byte: hit points
- 1 byte: remaining minutes
- 1 byte: unused
- 2 bytes: remaining ticks (1/8 seconds)
- 1 byte: movement speed setting (1-9)
- 1 byte: battle speed setting (1-9)
- 1 byte: level number (1-14), or 0 if the slot is empty
At 0x60..0x7D: Starting time for saved games
- For each save slot 1..10: Remaining time when the level was loaded (before the save was made)
- 1 byte: remaining minutes
- 2 bytes: remaining ticks (1/8 seconds)
I'm not sure what is this for.
At 0x80..0x13F: Best times
- For each level 1..12:
- 1 byte: best time minutes
- 1 byte: unused
- 2 bytes: best time ticks (1/8 seconds)
- 8 bytes: player name
- 2 bytes: 0xFFFF if there is a replay for this level, 0x0000 if not.
- 1 byte: starting hit points for the replay
- 1 byte: 0 for most levels, 2 for level 12
- This is the number of additional replays (level 13 and 14), but it's ignored on read.
An entry with zero time is considered empty.
The starting remaining time is not stored for replays.
At 0x210..0x25F: Player names for saved games
- For each save slot 1..10:
- 8 bytes: player name
Player names are padded with spaces; except if the name is empty, then it's all zero bytes.
Replays for best times
- At 0x02000..0x03FFF: replay for level 1
- At 0x04000..0x05FFF: replay for level 2
- At 0x06000..0x07FFF: replay for level 3
- At 0x08000..0x09FFF: replay for level 4
- At 0x0A000..0x0BFFF: replay for level 5
- At 0x0C000..0x0DFFF: replay for level 6
- At 0x0E000..0x0FFFF: replay for level 7
- At 0x10000..0x11FFF: replay for level 8
- At 0x12000..0x13FFF: replay for level 9
- At 0x14000..0x15FFF: replay for level 10
- At 0x16000..0x17FFF: replay for level 11
- At 0x18000..0x19FFF: replay for level 12
- At 0x1A000..0x1BFFF: unused (!)
- At 0x1C000..0x1DFFF: replay for level 13
- At 0x1E000..0x1FFFF: replay for level 14 and 15
Replays are in the same format as DEMOPLAY.KEY, except the end is not marked. Worse, there is junk data after the replay data. The replay (and the recording) ends when the player completes the level.
Replays of level 12 and 13 for saved games
- At 0x20000..0x23FFF: replays for save slot 1
- At 0x24000..0x27FFF: replays for save slot 2
- At 0x28000..0x2BFFF: replays for save slot 3
- At 0x2C000..0x2FFFF: replays for save slot 4
- At 0x30000..0x33FFF: replays for save slot 5
- At 0x34000..0x37FFF: replays for save slot 6
- At 0x38000..0x3BFFF: replays for save slot 7
- At 0x3C000..0x3FFFF: replays for save slot 8
- At 0x40000..0x43FFF: replays for save slot 9
- At 0x44000..0x47FFF: replays for save slot 10
Each area contains the following:
- At 0x0000..0x1FFF: replay of level 12, if you saved on level 13 or 14
- At 0x2000..0x3FFF: replay of level 13, if you saved on level 14
If you saved on any other level, the area of the slot contains only junk data.
This is needed because replays for levels 12-14 are saved to the main replay area together, when you finish level 14.
Hacks for easier playtesting
Don't play the Broderbund logo animation
- Search: 8C C8 8E D8 8E C0 8B
- Change: 8C to CB
- Offset in FDI: 0x17400
Don't load the Broderbund logo animation
- Search: E8 15 00 C7 06 1E CD 00 00
- Change: E8 15 00 to 90 90 90
- Offset in FDI: 0x4C5F
If you use this hack then you don't need the previous one.
Skip the intro
- Search: C7 06 1E CD 00 00 E8 40 00
- Change: E8 40 00 to 90 90 90
- Offset in FDI: 0x4C68
- Search: E8 B6 89 E8 B2 93
- Change: E8 B2 93 to 90 90 90
- Offset in FDI: 0x4DCD
Skip loading the background image as well:
- Change: E8 B6 89 to 90 90 90
- Offset in FDI: 0x4DCA
- Search: C6 06 90 CD 01 C6 06 EA E6 03
- Change: 01 to the level number, 03 to the number of HPs.
- Offsets in FDI: level: 0x4D7C, HP: 0x4D81
The valid level numbers are 0-14 (0x00-0x0E). If you set the level to 0, you can play the demo level.
- Search: C7 06 9C CD 3C 00 C7 06 9E CD 00 00
- Change: 3C to minutes
- Offset in FDI: 0x4D92
- Search: C6 06 90 CD 01 C6 06 EA E6 03 C7 06 9C CD 3C 00
- Change: 01 to level, 03 to HP, 3C to minutes
- Offsets in FDI: level: 0x12C20, HP: 0x12C25, minutes: 0x12C2A
The valid level numbers are 0-14 (0x00-0x0E). If you set the level to 0, you can play the demo level.
Set the demo level and HP
- Search: C6 06 90 CD 00 C6 06 EA E6 04
- Change: 00 to level, 04 to HP
- Offsets in FDI: level: 0x4D88, HP: 0x4D8D
TODO
- FM Towns version (PRI1.DAT)
- VCLIP.DAT
- TODO: Document the offsets of all texts?