Skip to content

Latest commit

 

History

History
592 lines (488 loc) · 31.1 KB

PC save format.md

File metadata and controls

592 lines (488 loc) · 31.1 KB

Sonic CD for PC save file format

This is a specification for the save file format used by the Sega PC release of Sonic CD, released in 1996 for Windows 95. The format of this file differs significantly from the save files of the original Sega CD game.

This document uses the following terms to represent data sizes, consistent with their use in Win32 API:

Term Length Range
BYTE 1 byte (8 bits) 0–255
WORD 2 bytes (16 bits) 0–65,535
DWORD 4 bytes (32 bits) 0–4,294,967,295

All numbers are little endian integers, meaning that the word $FF00 has the decimal value of 255 and not 65,280. All text is 7-bit ASCII.

Overview

The file is named "s_score.dat" by default and saved in the program directory. The game can save up to 6 different save slots in a single save file. The file is always 4,324 bytes in length.

The first four bytes of the file are an integer indicating the currently selected slot. This value is DWORD-sized and contains a value between $00 and $05.

Offset Size Format Description
$0000$0003 DWORD Number Selected slot (between $00 and $05)
$0004$02D3 720 bytes Section Slot 1
$02D4$05A3 720 bytes Section Slot 2
$05A4$0873 720 bytes Section Slot 3
$0874$0B43 720 bytes Section Slot 4
$0B44$0E13 720 bytes Section Slot 5
$0E14$10E3 720 bytes Section Slot 6

Decoding and encoding save slots

Each save slot is encoded using a seeded pseudorandom number sequence for obfuscation. This sequence is generated by the srand function of C with the seed $0551. The output of this function is implementation-specific, so to generate the same numbers used by the game, we need to use the msvc compiler. Since each save slot is 720 bytes long, we need to generate 720 numbers.

The following C code can be compiled in msvc to generate the correct sequence:

#include <stdio.h>

int main(void) {
	size_t i;

	srand(0x0551);

	for (i = 0; i < 720; i++) {
		// rand gives us a word but we only need the low byte
		printf("%02x\n", rand() & 0xff);
	}

	return 0;
}

This gives us a list of bytes that can be used with any other language or compiler to decode save files. Each save slot is encoded individually. To decode a save slot, we need to iterate through each byte and xor it with its corresponding byte in the srand sequence.

A simple JavaScript function for decoding a slot (given an array SRAND_SEQUENCE containing the generated numbers):

function encodeFile(bytes) {
	return bytes.map(function(value, i) {
		return value ^ SRAND_SEQUENCE[i];
	});
}

Because xor is its own inverse function, the same algorithm is used to re-encode slots. Note that modified slots also need to have their checksums recalculated to be considered valid by the game.

See Appendix A for a sample standalone C program that reproduces the entire srand sequence literally (so it works in any C compiler and not only msvc) and decodes save files by iterating through each of the 6 sections separately.

Slot contents

Each slot is 720 bytes long. Offsets are given relative to the beginning of the slot and not the beginning of the file. The first slot is offset by 4 bytes; the nth slot is offset by 4 + (n − 1) × 720 bytes.

Offset Size Format Description
$0000$0003 DWORD Number New game
$0004$000F 12 bytes Text Slot name
$0010$0013 DWORD Number Current Round
$0014$001F 12 bytes Section Date/time (see subsection)
$0020$02BF 672 bytes Section Time Attack (see subsection)
$02C0$02C3 DWORD Number Total completion time
$02C4 BYTE Bit field Time Stones
$02C5 BYTE Bit field Good Futures
$02C6$02C7 WORD Number Next Special Zone
$02C8$02CB DWORD Number Unknown, usually 0 but not always
$02CC$02CF DWORD Number Checksum

New game

$00 for new game, $01 otherwise.

Save slots with a value of $00 here are ignored by the game and treated as empty regardless of the contents of the rest of the data.

Slot name

ASCII text, used by the in-game Saved Games screen. The game limits this field to 10 characters, but it can be up to 12 characters long (though the last two characters will visually overlap the date/time in the interface). If the provided name is less than 12 bytes long, the game pads the last bytes as $20.

Underscore characters (_) and spaces are allowed and displayed as blank characters. Only uppercase letters and numerals are allowed. Lowercase letters and all other characters are shown as blank characters.

Current Round

The current Round. This is used by the Continue option on the title screen as well as for determining which levels are available in Time Attack. Possible values:

Value Continue Round Latest Round in Time Attack
$00 Palmtree Panic Not available
$01 Collision Chaos Palmtree Panic
$02 Tidal Tempest Collision Chaos
$03 Quartz Quadrant Tidal Tempest
$04 Wacky Workbench Quartz Quadrant
$05 Stardust Speedway Wacky Workbench
$06 Metallic Madness Stardust Speedway
$07 Metallic Madness Metallic Madness

Completed games have a value of $07.

Date/time

Saves the last modified time of each save slot.

Offset Size Format Description
$0014$0015 WORD Number Year
$0016$0017 WORD Number Month, between 1 and 12
$0018$0019 WORD Number Day of month, between 1 and 31
$001A$001B WORD Number Hour, between 0 and 23
$001C$001D WORD Number Minute, between 0 and 59
$001E$001F WORD Number Seconds, between 0 and 59

Note that the month and day start at 1 and not 0 (as in some programming languages). The year is stored as a full number (for example, 1996 would be stored as $CC07) and not as an offset from 1900 or similar.

These values are all set to $00 for new games.

Time Attack

The game stores three different time positions for each of the 21 zones and 7 Special Zones, in the order of:

  • Palmtree Panic I, 1st place
  • Palmtree Panic I, 2nd place
  • Palmtree Panic I, 3rd place
  • Palmtree Panic II, 1st place
  • Palmtree Panic II, 2nd place
  • Palmtree Panic II, 3rd place
  • ...
  • Special Zone 6, 1st place
  • Special Zone 6, 2nd place
  • Special Zone 6, 3rd place
  • Special Zone 7, 1st place
  • Special Zone 7, 2nd place
  • Special Zone 7, 3rd place

See Appendix B for a complete list of Time Attack position offsets.

Each position consists of 8 bytes:

  • A DWORD number for the time in ticks. Default value is $5046, representing 5 minutes in ticks (5 × 60 × 60).
  • A DWORD of text containing three ASCII characters representing the player's initials and one null byte ($00). Default value is $414141, representing "AAA" in ASCII.

Time in Sonic CD is kept in minutes, seconds, and ticks. Ticks are 1/60 of a second but displayed in-game as 1/100 of a second. The formula to convert from the stored form (1/60) to the displayed form (1/100) is ⌊ 100 × tick / 60 ⌋.

Uppercase letters, numerals, and spaces are allowed for initials. All other characters are shown as blank characters.

Total completion time

The sum of all of the first-place times in the Time Attack mode, in ticks. This is used to determine if the Play Music or Visual Mode options are available on the title screen without having to recalculate it every time the game is started.

The total time is computed by adding all of the first-place times (excluding Special Zones) in their original, 1/60 of a second form. Adding times in their converted, 1/100 of a second form results in a slightly different time from the one presented in-game.

Default value is $90C405, representing 105 minutes in ticks.

Time Stones

Tracks the Time Stones collected by the player. Each Time Stone is associated with a particular Special Zone, so they can be collected out of order if the player fails a Special Zone.

This is stored as a bit field of seven bits, each representing one Time Stone, starting with the least significant bit:

0 1 1 1 1 1 1 1
  | | | | | | |
  | | | | | | Green
  | | | | | Orange
  | | | | Yellow
  | | | Blue
  | | Cyan
  | Purple
  Red

This can also be expressed as a sum of the powers of 2:

Time Stone Value
Green 1
Orange 2
Yellow 4
Blue 8
Cyan 16
Purple 32
Red 64

A value of $7F means that all of the Time Stones have been collected.

Good Futures

Tracks the Good Futures created by the player. A Good Future can be created in a particular Round by traveling to the Past in each of the first two Zones and finding and destroying the robot transporter machine. A Good Future is also created in each Round played after the player collects all of the Time Stones.

This is stored as a bit field of seven bits, each representing one of the game's seven Rounds, starting with the least significant bit:

0 1 1 1 1 1 1 1
  | | | | | | |
  | | | | | | Palmtree Panic
  | | | | | Collision Chaos
  | | | | Tidal Tempest
  | | | Quartz Quadrant
  | | Wacky Workbench
  | Stardust Speedway
  Metallic Madness

This can also be expressed as a sum of the powers of 2:

Round Value
Palmtree Panic 1
Collision Chaos 2
Tidal Tempest 4
Quartz Quadrant 8
Wacky Workbench 16
Stardust Speedway 32
Metallic Madness 64

A value of $7F means that all of the Good Futures have been obtained.

Next Special Zone

The next Special Zone to be played, between $00 and $06. The game goes through all of the Special Zones in rotation, skipping the ones that have already been successfully completed. This value is set to $00 once the player has collected all of the Time Stones.

It is possible to set this value to that of a Special Zone that has already been completed. In this case, the selected Special Zone is played next but does not award another Time Stone if completed successfully (since each Special Zone is associated with a particular Time Stone).

Checksum

The checksum is computed by iterating over the first 716 bytes of the slot (that is, the entire slot except the checksum itself) and adding each byte as a signed integer. In C, it can be calculated with the following function:

int calculate_checksum(signed char bytes[]) {
	int checksum = 0;

	for (size_t i = 0; i < 716; i++) {
		checksum += bytes[i];
	}

	return checksum;
}

It is significant to note that the algorithm treats each byte as a signed integer, meaning that any value greater than $7F is treated as a two's complement and reduces the value of the checksum rather than increasing it. If we were to implement the function in a language with no concept of a signed byte type, such as JavaScript, we would need to take this into account:

function calculateChecksum(bytes) {
	let checksum = 0;

	// adds all bytes of file as signed integers
	for (let i = 0; i < bytes.length - 4; i++) {
		if (bytes[i] < 0x80) {
			checksum += bytes[i];
		} else { // converts two's complement
			checksum -= 0xff - bytes[i] + 1;
		}
	}

	return checksum;
}

This means that a file can theoretically produce a negative checksum, but this can never happen with a real save file because there are not enough possible bytes in the format with valid values above $7F.

Slots with invalid checksums are treated as empty and ignored by the game.

Appendices

A. Decoder/encoder program

#include <stdio.h>

#define EXIT_SUCCESS 0
#define EXIT_FAILURE 1

#define HEADER_SIZE 4
#define SAVE_SLOTS  6
#define SLOT_SIZE   720
#define FILE_SIZE   4132

int main(int argc, char *argv[]) {
	char *input_file, *output_file;

	if (argc != 3) {
		puts("Usage: pcdecode <input file> <output file>");
		return EXIT_FAILURE;
	} else {
		input_file = argv[1];
		output_file = argv[2];
	}

	// pseudorandom number sequence is reproduced here so that the program works
	// independently of compiler
	unsigned char sequence[] = {
		0x83, 0x0c, 0x27, 0xdf, 0x0f, 0x2c, 0xbb, 0x88, 0x24, 0xf4, 0x89, 0xf0,
		0xb8, 0x17, 0xc6, 0x86, 0x49, 0xa6, 0x8e, 0x0c, 0x66, 0x87, 0x83, 0xa5,
		0x71, 0x24, 0xb7, 0x2f, 0xa3, 0x60, 0xcc, 0xd2, 0x50, 0xcf, 0x59, 0xd9,
		0x6d, 0x26, 0x90, 0xc1, 0x4f, 0xf0, 0x1e, 0x90, 0xb5, 0x83, 0x53, 0x69,
		0x09, 0x33, 0x83, 0x5c, 0xe3, 0xc5, 0xac, 0x82, 0xcf, 0x27, 0x58, 0x4a,
		0x50, 0x5e, 0xc5, 0x0e, 0x25, 0xbf, 0x44, 0xea, 0xca, 0x62, 0xdf, 0xcd,
		0x42, 0xd4, 0x3e, 0xd0, 0x14, 0x0c, 0xcc, 0xc7, 0x94, 0xa0, 0x15, 0x16,
		0x63, 0x39, 0x72, 0xc7, 0x39, 0x45, 0xe9, 0xd9, 0xe2, 0xeb, 0x4f, 0xda,
		0x87, 0x44, 0xaf, 0xb8, 0x2f, 0xc8, 0xf0, 0xd4, 0x85, 0x06, 0xb1, 0x58,
		0xdc, 0x99, 0x78, 0xcb, 0x70, 0x57, 0x0c, 0xe2, 0xef, 0xcb, 0x1f, 0x99,
		0x38, 0xe6, 0x30, 0x83, 0x62, 0xf1, 0xb0, 0x5e, 0x00, 0xc6, 0x64, 0xeb,
		0xa3, 0x40, 0x0a, 0xfd, 0xa1, 0xf0, 0x3f, 0xcf, 0x15, 0x11, 0xa0, 0x9a,
		0x27, 0xbe, 0x30, 0x67, 0x8e, 0x63, 0xfa, 0x22, 0x53, 0x72, 0xf7, 0xf1,
		0xd7, 0x57, 0x31, 0xc4, 0x17, 0xad, 0x29, 0x2c, 0x2f, 0xb2, 0x76, 0x70,
		0x1d, 0xf8, 0xb0, 0xde, 0xc8, 0x5e, 0x8c, 0x5f, 0x41, 0x3f, 0x48, 0x4e,
		0x48, 0xe9, 0x4a, 0x8a, 0x12, 0x51, 0x04, 0xca, 0x49, 0x05, 0x1a, 0x31,
		0x55, 0x61, 0xc7, 0x23, 0xda, 0x06, 0x7d, 0x56, 0x81, 0x88, 0xcb, 0x2c,
		0xfc, 0x67, 0x84, 0x40, 0x44, 0x41, 0x1e, 0x3e, 0x25, 0x45, 0x58, 0xf9,
		0xfd, 0xeb, 0x1f, 0xb7, 0xc1, 0xe2, 0xb3, 0xcf, 0x40, 0x4a, 0x06, 0x77,
		0xac, 0x24, 0x65, 0xd7, 0x56, 0x07, 0x59, 0x62, 0xb9, 0x14, 0xd0, 0x65,
		0xba, 0x2c, 0x78, 0xe0, 0x2e, 0x60, 0x6c, 0x96, 0x9d, 0xa8, 0x12, 0x60,
		0x45, 0xd8, 0x43, 0xc6, 0x62, 0xd1, 0xad, 0xd0, 0xad, 0xf4, 0x76, 0x1a,
		0x23, 0xdd, 0x22, 0x27, 0x07, 0x4d, 0xb7, 0xf7, 0x2a, 0x67, 0x1f, 0xdc,
		0x6a, 0x20, 0xcd, 0x89, 0x79, 0xf1, 0xa4, 0x6c, 0xe0, 0xce, 0x14, 0x3e,
		0x43, 0x5c, 0x86, 0xd8, 0xe6, 0x5f, 0xfa, 0x48, 0x70, 0x71, 0xee, 0x24,
		0xef, 0xfa, 0x82, 0x1e, 0x1d, 0x5c, 0xdb, 0xd9, 0xe2, 0x6e, 0xc5, 0xfb,
		0x19, 0x2c, 0x9c, 0x84, 0x97, 0xad, 0x6c, 0x5e, 0x69, 0x55, 0x57, 0x34,
		0x5e, 0x4b, 0x37, 0x88, 0xc6, 0x2d, 0x85, 0x00, 0x78, 0x03, 0x7a, 0x01,
		0x1a, 0x72, 0x73, 0x7f, 0x9c, 0x33, 0x9a, 0x14, 0x06, 0xc3, 0xc3, 0x4f,
		0x74, 0x5b, 0x94, 0x4e, 0x5e, 0x22, 0xe9, 0x8f, 0x1d, 0xa2, 0x76, 0x03,
		0xab, 0x78, 0xaf, 0x64, 0xab, 0x50, 0xe4, 0xc9, 0xa9, 0x11, 0xb1, 0x78,
		0xa2, 0x55, 0x95, 0xfb, 0xc7, 0x1c, 0xe1, 0x76, 0x7e, 0xc1, 0xd4, 0x37,
		0xaa, 0x2d, 0x04, 0x8f, 0x2c, 0x4a, 0xff, 0xe0, 0xaa, 0xba, 0x33, 0xf8,
		0x8e, 0xca, 0x0b, 0x9d, 0x52, 0xa1, 0x5b, 0x69, 0xfc, 0xbe, 0xfe, 0xd9,
		0xe4, 0xa1, 0xbe, 0xa0, 0xbd, 0xc7, 0x73, 0x41, 0xd3, 0xdc, 0x6f, 0xdc,
		0x92, 0x2d, 0x1a, 0x48, 0x48, 0x5c, 0xda, 0x63, 0x2c, 0x57, 0x36, 0xa6,
		0x9e, 0x8a, 0x3a, 0xfd, 0xb0, 0x54, 0x1d, 0xd5, 0xe6, 0xb8, 0x21, 0x76,
		0x3a, 0x56, 0xbb, 0x93, 0x63, 0x99, 0xf4, 0x1f, 0x57, 0x31, 0x0f, 0x63,
		0x0f, 0xc4, 0x6c, 0x4e, 0x8a, 0xe0, 0xac, 0x0c, 0x14, 0x35, 0x16, 0xda,
		0xc8, 0x01, 0x39, 0x18, 0x54, 0xcb, 0xd3, 0x9f, 0xfc, 0x54, 0xf2, 0x56,
		0xe2, 0xcb, 0x59, 0x00, 0x84, 0x40, 0x25, 0x58, 0x86, 0x5b, 0xb1, 0x60,
		0xb2, 0x4e, 0xb6, 0xf6, 0x3d, 0x08, 0xb7, 0xa8, 0x4b, 0xab, 0x9c, 0xc9,
		0xb6, 0x41, 0x9d, 0xc4, 0x0b, 0xab, 0x61, 0xb1, 0xd6, 0xd9, 0x67, 0x26,
		0x20, 0x41, 0xa6, 0x50, 0x35, 0x89, 0x70, 0x42, 0xab, 0x87, 0x9c, 0x8c,
		0x9f, 0x6c, 0xe4, 0x10, 0x42, 0x3c, 0x8c, 0x11, 0x95, 0x82, 0x45, 0x8d,
		0x70, 0x41, 0x50, 0xcd, 0xc9, 0x2d, 0xe6, 0x3a, 0x33, 0x1b, 0xd8, 0x72,
		0xa5, 0xb7, 0x73, 0x9b, 0x7d, 0x75, 0xa6, 0xf8, 0xc4, 0xca, 0x67, 0xb9,
		0xb4, 0x9b, 0x52, 0x18, 0x77, 0xf6, 0x93, 0xa1, 0x32, 0x00, 0x07, 0xd1,
		0x42, 0x2e, 0x9e, 0xe3, 0xc6, 0xb8, 0x04, 0xe7, 0x5d, 0x4b, 0x80, 0x14,
		0x31, 0xff, 0x1c, 0x5f, 0x34, 0x88, 0x06, 0x4d, 0xa9, 0xb0, 0x36, 0x05,
		0xe7, 0x05, 0x51, 0xa9, 0x59, 0xce, 0xcd, 0xe6, 0xca, 0x44, 0x57, 0xca,
		0xdf, 0xff, 0x71, 0xd6, 0xe5, 0xaf, 0x60, 0x55, 0xcd, 0x0c, 0x45, 0xec,
		0x75, 0x10, 0x8a, 0x71, 0x2a, 0x63, 0x7f, 0x00, 0x6a, 0x17, 0x45, 0x4c,
		0xee, 0x95, 0xec, 0x34, 0xe8, 0xd7, 0xd8, 0x97, 0x89, 0xd8, 0x69, 0x68,
		0xc6, 0x49, 0xdb, 0x05, 0x5c, 0x86, 0x6a, 0xc4, 0x11, 0xc8, 0xba, 0xcf,
		0x3d, 0x9c, 0x77, 0x33, 0x8a, 0x94, 0x38, 0x33, 0xf8, 0x3c, 0xaa, 0xe2,
		0x24, 0x4f, 0xea, 0xf1, 0xc9, 0x2b, 0x32, 0xc5, 0x87, 0x83, 0xb8, 0xcc,
		0xe3, 0x54, 0xd3, 0x12, 0x90, 0x1a, 0x60, 0x0f, 0xed, 0x43, 0x64, 0xc4,
		0xce, 0xe4, 0xf3, 0x06, 0x7f, 0xab, 0x50, 0x19, 0x06, 0x15, 0x55, 0x80,
		0xa9, 0xe0, 0x15, 0x14, 0xb0, 0xc4, 0xc1, 0x54, 0x6c, 0x5e, 0xc8, 0xfc
	};

	FILE *input_handle, *output_handle;

	if ((input_handle = fopen(input_file, "rb")) == NULL) {
		puts("Could not open input file.");
		return EXIT_FAILURE;
	}

	if ((output_handle = fopen(output_file, "wb")) == NULL) {
		puts("Could not open output file.");
		return EXIT_FAILURE;
	}

	// first four bytes of file are copied over exactly
	for (size_t i = 0; i < HEADER_SIZE; i++) {
		unsigned char buffer[HEADER_SIZE];

		fseek(input_handle, 0, SEEK_SET);
		fread(&buffer, sizeof(unsigned char), sizeof(buffer), input_handle);

		fseek(output_handle, 0, SEEK_SET);
		fwrite(buffer, sizeof(unsigned char), sizeof(buffer), output_handle);
	}

	for (size_t i = 0; i < SAVE_SLOTS; i++) {
		unsigned char buffer[SLOT_SIZE];

		fseek(input_handle, HEADER_SIZE + i * SLOT_SIZE, SEEK_SET);
		fread(&buffer, sizeof(unsigned char), sizeof(buffer), input_handle);

		for (size_t j = 0; j < SLOT_SIZE; j++) {
			buffer[j] ^= sequence[j];
		}

		fseek(output_handle, HEADER_SIZE + i * SLOT_SIZE, SEEK_SET);
		fwrite(buffer, sizeof(unsigned char), sizeof(buffer), output_handle);
	}

	fclose(input_handle);
	fclose(output_handle);

	return EXIT_SUCCESS;
}

B. Time Attack offsets

Offset Size Format Description
$0020$0023 DWORD Number Palmtree Panic I, 1st place time
$0024$0027 DWORD Text Palmtree Panic I, 1st place initials
$0028$002B DWORD Number Palmtree Panic I, 2nd place time
$002C$002F DWORD Text Palmtree Panic I, 2nd place initials
$0030$0033 DWORD Number Palmtree Panic I, 3rd place time
$0034$0037 DWORD Text Palmtree Panic I, 3rd place initials
$0038$003B DWORD Number Palmtree Panic II, 1st place time
$003C$003F DWORD Text Palmtree Panic II, 1st place initials
$0040$0043 DWORD Number Palmtree Panic II, 2nd place time
$0044$0047 DWORD Text Palmtree Panic II, 2nd place initials
$0048$004B DWORD Number Palmtree Panic II, 3rd place time
$004C$004F DWORD Text Palmtree Panic II, 3rd place initials
$0050$0053 DWORD Number Palmtree Panic III, 1st place time
$0054$0057 DWORD Text Palmtree Panic III, 1st place initials
$0058$005B DWORD Number Palmtree Panic III, 2nd place time
$005C$005F DWORD Text Palmtree Panic III, 2nd place initials
$0060$0063 DWORD Number Palmtree Panic III, 3rd place time
$0064$0067 DWORD Text Palmtree Panic III, 3rd place initials
$0068$006B DWORD Number Collision Chaos I, 1st place time
$006C$006F DWORD Text Collision Chaos I, 1st place initials
$0070$0073 DWORD Number Collision Chaos I, 2nd place time
$0074$0077 DWORD Text Collision Chaos I, 2nd place initials
$0078$007B DWORD Number Collision Chaos I, 3rd place time
$007C$007F DWORD Text Collision Chaos I, 3rd place initials
$0080$0083 DWORD Number Collision Chaos II, 1st place time
$0084$0087 DWORD Text Collision Chaos II, 1st place initials
$0088$008B DWORD Number Collision Chaos II, 2nd place time
$008C$008F DWORD Text Collision Chaos II, 2nd place initials
$0090$0093 DWORD Number Collision Chaos II, 3rd place time
$0094$0097 DWORD Text Collision Chaos II, 3rd place initials
$0098$009B DWORD Number Collision Chaos III, 1st place time
$009C$009F DWORD Text Collision Chaos III, 1st place initials
$00A0$00A3 DWORD Number Collision Chaos III, 2nd place time
$00A4$00A7 DWORD Text Collision Chaos III, 2nd place initials
$00A8$00AB DWORD Number Collision Chaos III, 3rd place time
$00AC$00AF DWORD Text Collision Chaos III, 3rd place initials
$00B0$00B3 DWORD Number Tidal Tempest I, 1st place time
$00B4$00B7 DWORD Text Tidal Tempest I, 1st place initials
$00B8$00BB DWORD Number Tidal Tempest I, 2nd place time
$00BC$00BF DWORD Text Tidal Tempest I, 2nd place initials
$00C0$00C3 DWORD Number Tidal Tempest I, 3rd place time
$00C4$00C7 DWORD Text Tidal Tempest I, 3rd place initials
$00C8$00CB DWORD Number Tidal Tempest II, 1st place time
$00CC$00CF DWORD Text Tidal Tempest II, 1st place initials
$00D0$00D3 DWORD Number Tidal Tempest II, 2nd place time
$00D4$00D7 DWORD Text Tidal Tempest II, 2nd place initials
$00D8$00DB DWORD Number Tidal Tempest II, 3rd place time
$00DC$00DF DWORD Text Tidal Tempest II, 3rd place initials
$00E0$00E3 DWORD Number Tidal Tempest III, 1st place time
$00E4$00E7 DWORD Text Tidal Tempest III, 1st place initials
$00E8$00EB DWORD Number Tidal Tempest III, 2nd place time
$00EC$00EF DWORD Text Tidal Tempest III, 2nd place initials
$00F0$00F3 DWORD Number Tidal Tempest III, 3rd place time
$00F4$00F7 DWORD Text Tidal Tempest III, 3rd place initials
$00F8$00FB DWORD Number Quartz Quadrant I, 1st place time
$00FC$00FF DWORD Text Quartz Quadrant I, 1st place initials
$0100$0103 DWORD Number Quartz Quadrant I, 2nd place time
$0104$0107 DWORD Text Quartz Quadrant I, 2nd place initials
$0108$010B DWORD Number Quartz Quadrant I, 3rd place time
$010C$010F DWORD Text Quartz Quadrant I, 3rd place initials
$0110$0113 DWORD Number Quartz Quadrant II, 1st place time
$0114$0117 DWORD Text Quartz Quadrant II, 1st place initials
$0118$011B DWORD Number Quartz Quadrant II, 2nd place time
$011C$011F DWORD Text Quartz Quadrant II, 2nd place initials
$0120$0123 DWORD Number Quartz Quadrant II, 3rd place time
$0124$0127 DWORD Text Quartz Quadrant II, 3rd place initials
$0128$012B DWORD Number Quartz Quadrant III, 1st place time
$012C$012F DWORD Text Quartz Quadrant III, 1st place initials
$0130$0133 DWORD Number Quartz Quadrant III, 2nd place time
$0134$0137 DWORD Text Quartz Quadrant III, 2nd place initials
$0138$013B DWORD Number Quartz Quadrant III, 3rd place time
$013C$013F DWORD Text Quartz Quadrant III, 3rd place initials
$0140$0143 DWORD Number Wacky Workbench I, 1st place time
$0144$0147 DWORD Text Wacky Workbench I, 1st place initials
$0148$014B DWORD Number Wacky Workbench I, 2nd place time
$014C$014F DWORD Text Wacky Workbench I, 2nd place initials
$0150$0153 DWORD Number Wacky Workbench I, 3rd place time
$0154$0157 DWORD Text Wacky Workbench I, 3rd place initials
$0158$015B DWORD Number Wacky Workbench II, 1st place time
$015C$015F DWORD Text Wacky Workbench II, 1st place initials
$0160$0163 DWORD Number Wacky Workbench II, 2nd place time
$0164$0167 DWORD Text Wacky Workbench II, 2nd place initials
$0168$016B DWORD Number Wacky Workbench II, 3rd place time
$016C$016F DWORD Text Wacky Workbench II, 3rd place initials
$0170$0173 DWORD Number Wacky Workbench III, 1st place time
$0174$0177 DWORD Text Wacky Workbench III, 1st place initials
$0178$017B DWORD Number Wacky Workbench III, 2nd place time
$017C$017F DWORD Text Wacky Workbench III, 2nd place initials
$0180$0183 DWORD Number Wacky Workbench III, 3rd place time
$0184$0187 DWORD Text Wacky Workbench III, 3rd place initials
$0188$018B DWORD Number Stardust Speedway I, 1st place time
$018C$018F DWORD Text Stardust Speedway I, 1st place initials
$0190$0193 DWORD Number Stardust Speedway I, 2nd place time
$0194$0197 DWORD Text Stardust Speedway I, 2nd place initials
$0198$019B DWORD Number Stardust Speedway I, 3rd place time
$019C$019F DWORD Text Stardust Speedway I, 3rd place initials
$01A0$01A3 DWORD Number Stardust Speedway II, 1st place time
$01A4$01A7 DWORD Text Stardust Speedway II, 1st place initials
$01A8$01AB DWORD Number Stardust Speedway II, 2nd place time
$01AC$01AF DWORD Text Stardust Speedway II, 2nd place initials
$01B0$01B3 DWORD Number Stardust Speedway II, 3rd place time
$01B4$01B7 DWORD Text Stardust Speedway II, 3rd place initials
$01B8$01BB DWORD Number Stardust Speedway III, 1st place time
$01BC$01BF DWORD Text Stardust Speedway III, 1st place initials
$01C0$01C3 DWORD Number Stardust Speedway III, 2nd place time
$01C4$01C7 DWORD Text Stardust Speedway III, 2nd place initials
$01C8$01CB DWORD Number Stardust Speedway III, 3rd place time
$01CC$01CF DWORD Text Stardust Speedway III, 3rd place initials
$01D0$01D3 DWORD Number Metallic Madness I, 1st place time
$01D4$01D7 DWORD Text Metallic Madness I, 1st place initials
$01D8$01DB DWORD Number Metallic Madness I, 2nd place time
$01DC$01DF DWORD Text Metallic Madness I, 2nd place initials
$01E0$01E3 DWORD Number Metallic Madness I, 3rd place time
$01E4$01E7 DWORD Text Metallic Madness I, 3rd place initials
$01E8$01EB DWORD Number Metallic Madness II, 1st place time
$01EC$01EF DWORD Text Metallic Madness II, 1st place initials
$01F0$01F3 DWORD Number Metallic Madness II, 2nd place time
$01F4$01F7 DWORD Text Metallic Madness II, 2nd place initials
$01F8$01FB DWORD Number Metallic Madness II, 3rd place time
$01FC$01FF DWORD Text Metallic Madness II, 3rd place initials
$0200$0203 DWORD Number Metallic Madness III, 1st place time
$0204$0207 DWORD Text Metallic Madness III, 1st place initials
$0208$020B DWORD Number Metallic Madness III, 2nd place time
$020C$020F DWORD Text Metallic Madness III, 2nd place initials
$0210$0213 DWORD Number Metallic Madness III, 3rd place time
$0214$0217 DWORD Text Metallic Madness III, 3rd place initials
$0218$021B DWORD Number Special Zone 1, 1st place time
$021C$021F DWORD Text Special Zone 1, 1st place initials
$0220$0223 DWORD Number Special Zone 1, 2nd place time
$0224$0227 DWORD Text Special Zone 1, 2nd place initials
$0228$022B DWORD Number Special Zone 1, 3rd place time
$022C$022F DWORD Text Special Zone 1, 3rd place initials
$0230$0233 DWORD Number Special Zone 2, 1st place time
$0234$0237 DWORD Text Special Zone 2, 1st place initials
$0238$023B DWORD Number Special Zone 2, 2nd place time
$023C$023F DWORD Text Special Zone 2, 2nd place initials
$0240$0243 DWORD Number Special Zone 2, 3rd place time
$0244$0247 DWORD Text Special Zone 2, 3rd place initials
$0248$024B DWORD Number Special Zone 3, 1st place time
$024C$024F DWORD Text Special Zone 3, 1st place initials
$0250$0253 DWORD Number Special Zone 3, 2nd place time
$0254$0257 DWORD Text Special Zone 3, 2nd place initials
$0258$025B DWORD Number Special Zone 3, 3rd place time
$025C$025F DWORD Text Special Zone 3, 3rd place initials
$0260$0263 DWORD Number Special Zone 4, 1st place time
$0264$0267 DWORD Text Special Zone 4, 1st place initials
$0268$026B DWORD Number Special Zone 4, 2nd place time
$026C$026F DWORD Text Special Zone 4, 2nd place initials
$0270$0273 DWORD Number Special Zone 4, 3rd place time
$0274$0277 DWORD Text Special Zone 4, 3rd place initials
$0278$027B DWORD Number Special Zone 5, 1st place time
$027C$027F DWORD Text Special Zone 5, 1st place initials
$0280$0283 DWORD Number Special Zone 5, 2nd place time
$0284$0287 DWORD Text Special Zone 5, 2nd place initials
$0288$028B DWORD Number Special Zone 5, 3rd place time
$028C$028F DWORD Text Special Zone 5, 3rd place initials
$0290$0293 DWORD Number Special Zone 6, 1st place time
$0294$0297 DWORD Text Special Zone 6, 1st place initials
$0298$029B DWORD Number Special Zone 6, 2nd place time
$029C$029F DWORD Text Special Zone 6, 2nd place initials
$02A0$02A3 DWORD Number Special Zone 6, 3rd place time
$02A4$02A7 DWORD Text Special Zone 6, 3rd place initials
$02A8$02AB DWORD Number Special Zone 7, 1st place time
$02AC$02AF DWORD Text Special Zone 7, 1st place initials
$02B0$02B3 DWORD Number Special Zone 7, 2nd place time
$02B4$02B7 DWORD Text Special Zone 7, 2nd place initials
$02B8$02BB DWORD Number Special Zone 7, 3rd place time
$02BC$02BF DWORD Text Special Zone 7, 3rd place initials

Authors

Credit to MainMemory for providing a C decompilation, from which the encoding and checksum algorithms were obtained.