X-Git-Url: https://git.artsoft.org/?p=rocksndiamonds.git;a=blobdiff_plain;f=src%2Fgame_sp%2Ffile.c;h=4e8c9a8feb4b4d7949b5f01f980288f48546a427;hp=2aa0ddf5e34e39adff14a286f8874db1dadf28ad;hb=12a8fd3a64d6bee5ca5f5b89e4a00b49d78bbd2c;hpb=98edd2c02783d6cf8ffe4d7aec340fe80cc8bcff diff --git a/src/game_sp/file.c b/src/game_sp/file.c index 2aa0ddf5..4e8c9a8f 100644 --- a/src/game_sp/file.c +++ b/src/game_sp/file.c @@ -3,24 +3,32 @@ #include "global.h" -/* ------------------------------------------------------------------------- */ -/* functions for loading Supaplex level */ -/* ------------------------------------------------------------------------- */ +// ---------------------------------------------------------------------------- +// functions for loading Supaplex level +// ---------------------------------------------------------------------------- -void setLevelInfoToDefaults_SP() +static void setTapeInfoToDefaults_SP(void) +{ + native_sp_level.demo.is_available = FALSE; + native_sp_level.demo.length = 0; +} + +void setLevelInfoToDefaults_SP(void) { LevelInfoType *header = &native_sp_level.header; char *empty_title = "-------- EMPTY --------"; int i, x, y; - native_sp_level.width = SP_PLAYFIELD_WIDTH; - native_sp_level.height = SP_PLAYFIELD_HEIGHT; + native_sp_level.game_sp = &game_sp; + + native_sp_level.width = SP_STD_PLAYFIELD_WIDTH; + native_sp_level.height = SP_STD_PLAYFIELD_HEIGHT; for (x = 0; x < native_sp_level.width; x++) for (y = 0; y < native_sp_level.height; y++) native_sp_level.playfield[x][y] = fiSpace; - /* copy string (without terminating '\0' character!) */ + // copy string (without terminating '\0' character!) for (i = 0; i < SP_LEVEL_NAME_LEN; i++) header->LevelTitle[i] = empty_title[i]; @@ -43,12 +51,16 @@ void setLevelInfoToDefaults_SP() port->FreezeEnemies = 0; } - native_sp_level.demo_available = FALSE; - native_sp_level.demo_length = 0; + // set raw header bytes (used for subsequent buffer zone) to "hardware" + for (i = 0; i < SP_HEADER_SIZE; i++) + native_sp_level.header_raw_bytes[i] = 0x20; + + setTapeInfoToDefaults_SP(); } -void copyInternalEngineVars_SP() +void copyInternalEngineVars_SP(void) { + int count; int i, x, y; LInfo = native_sp_level.header; @@ -60,96 +72,122 @@ void copyInternalEngineVars_SP() FieldMax = (FieldWidth * FieldHeight) + HeaderSize - 1; LevelMax = (FieldWidth * FieldHeight) - 1; - FileMax = FieldMax + native_sp_level.demo_length; + // initialize preceding playfield buffer + for (i = -game_sp.preceding_buffer_size; i < 0; i++) + PlayField16[i] = 0; - PlayField8 = REDIM_1D(sizeof(byte), 0, FileMax + 1 - 1); - DisPlayField = REDIM_1D(sizeof(byte), 0, FieldMax + 1 - 1); - PlayField16 = REDIM_1D(sizeof(int), -FieldWidth, FieldMax); + // initialize preceding playfield buffer + for (i = -SP_MAX_PLAYFIELD_WIDTH; i < 0; i++) + PlayField8[i] = 0; - for (i = 0, y = 0; y < native_sp_level.height; y++) + count = 0; + for (i = 0; game_sp.preceding_buffer[i] != NULL; i++) { - for (x = 0; x < native_sp_level.width; x++) + char *s = game_sp.preceding_buffer[i]; + boolean hi_byte = FALSE; // little endian data => start with low byte + + while (s[0] != '\0' && s[1] != '\0') { - PlayField8[i] = native_sp_level.playfield[x][y]; + int hi_nibble = s[0] - (s[0] > '9' ? 'a' - 10 : '0'); + int lo_nibble = s[1] - (s[1] > '9' ? 'a' - 10 : '0'); + int byte = (hi_nibble << 4) | lo_nibble; + + if (hi_byte) + byte <<= 8; - PlayField16[i] = PlayField8[i]; - DisPlayField[i] = PlayField8[i]; - PlayField8[i] = 0; + PlayField16[-game_sp.preceding_buffer_size + count] |= byte; - i++; + if (hi_byte) + count++; + + hi_byte = !hi_byte; + + s += 2; + + while (*s == ' ') + s++; } } - if (native_sp_level.demo_available) - { - DemoAvailable = True; + count = 0; + for (y = 0; y < native_sp_level.height; y++) + for (x = 0; x < native_sp_level.width; x++) + PlayField8[count++] = native_sp_level.playfield[x][y]; - for (i = 0; i < native_sp_level.demo_length; i++) - PlayField8[FieldMax + i + 1] = native_sp_level.demo[i]; - } + // add raw header bytes to subsequent playfield buffer zone + for (i = 0; i < SP_HEADER_SIZE; i++) + PlayField8[count++] = native_sp_level.header_raw_bytes[i]; - AnimationPosTable = REDIM_1D(sizeof(int), 0, LevelMax - 2 * FieldWidth); - AnimationSubTable = REDIM_1D(sizeof(byte), 0, LevelMax - 2 * FieldWidth); - TerminalState = REDIM_1D(sizeof(byte), 0, FieldMax + 1 - 1); + for (i = 0; i < count; i++) + { + PlayField16[i] = PlayField8[i]; + DisPlayField[i] = PlayField8[i]; + PlayField8[i] = 0; + } - DemoPointer = FieldMax + 1; - DemoOffset = DemoPointer; - DemoKeyRepeatCounter = 0; + if (native_sp_level.demo.is_available) + DemoAvailable = True; GravityFlag = LInfo.InitialGravity; FreezeZonks = LInfo.InitialFreezeZonks; - RandomSeed = LInfo.DemoRandomSeed; - LevelLoaded = True; + + // random seed set by main game tape code to native random generator seed } -static void LoadNativeLevelFromFileStream_SP(FILE *file, boolean demo_available) +static void LoadNativeLevelFromFileStream_SP(File *file, int width, int height, + boolean demo_available) { LevelInfoType *header = &native_sp_level.header; int i, x, y; - /* for details of the Supaplex level format, see Herman Perk's Supaplex - documentation file "SPFIX63.DOC" from his Supaplex "SpeedFix" package */ + // for details of the Supaplex level format, see Herman Perk's Supaplex + // documentation file "SPFIX63.DOC" from his Supaplex "SpeedFix" package - native_sp_level.width = SP_PLAYFIELD_WIDTH; - native_sp_level.height = SP_PLAYFIELD_HEIGHT; + native_sp_level.width = MIN(width, SP_MAX_PLAYFIELD_WIDTH); + native_sp_level.height = MIN(height, SP_MAX_PLAYFIELD_HEIGHT); - /* read level playfield (width * height == 60 * 24 tiles == 1440 bytes) */ - for (y = 0; y < native_sp_level.height; y++) - for (x = 0; x < native_sp_level.width; x++) - native_sp_level.playfield[x][y] = getFile8Bit(file); + // read level playfield (width * height == 60 * 24 tiles == 1440 bytes) + // (MPX levels may have non-standard playfield size -- check max. size) + for (y = 0; y < height; y++) + { + for (x = 0; x < width; x++) + { + byte element = getFile8Bit(file); - /* read level header (96 bytes) */ + if (x < SP_MAX_PLAYFIELD_WIDTH && + y < SP_MAX_PLAYFIELD_HEIGHT) + native_sp_level.playfield[x][y] = element; + } + } + + // read level header (96 bytes) - ReadUnusedBytesFromFile(file, 4); /* (not used by Supaplex engine) */ + ReadUnusedBytesFromFile(file, 4); // (not used by Supaplex engine) - /* initial gravity: 1 == "on", anything else (0) == "off" */ + // initial gravity: 1 == "on", anything else (0) == "off" header->InitialGravity = getFile8Bit(file); - /* SpeedFixVersion XOR 0x20 */ + // SpeedFixVersion XOR 0x20 header->Version = getFile8Bit(file); - /* level title in uppercase letters, padded with dashes ("-") (23 bytes) */ + // level title in uppercase letters, padded with dashes ("-") (23 bytes) for (i = 0; i < SP_LEVEL_NAME_LEN; i++) header->LevelTitle[i] = getFile8Bit(file); - /* initial "freeze zonks": 2 == "on", anything else (0, 1) == "off" */ + // initial "freeze zonks": 2 == "on", anything else (0, 1) == "off" header->InitialFreezeZonks = getFile8Bit(file); - /* number of infotrons needed; 0 means that Supaplex will count the total - amount of infotrons in the level and use the low byte of that number - (a multiple of 256 infotrons will result in "0 infotrons needed"!) */ + // number of infotrons needed; 0 means that Supaplex will count the total + // amount of infotrons in the level and use the low byte of that number + // (a multiple of 256 infotrons will result in "0 infotrons needed"!) header->InfotronsNeeded = getFile8Bit(file); - /* number of special ("gravity") port entries below (maximum 10 allowed) */ + // number of special ("gravity") port entries below (maximum 10 allowed) header->SpecialPortCount = getFile8Bit(file); -#if 0 - printf("::: num_special_ports == %d\n", header->SpecialPortCount); -#endif - - /* database of properties of up to 10 special ports (6 bytes per port) */ + // database of properties of up to 10 special ports (6 bytes per port) for (i = 0; i < SP_MAX_SPECIAL_PORTS; i++) { SpecialPortType *port = &header->SpecialPort[i]; @@ -159,44 +197,30 @@ static void LoadNativeLevelFromFileStream_SP(FILE *file, boolean demo_available) the 16 bit value here calculates as 2 * (x + (y * 60)) (this is twice of what may be expected: Supaplex works with a game field in memory which is 2 bytes per tile) */ - port->PortLocation = getFile16BitBE(file); /* yes, big endian */ + port->PortLocation = getFile16BitBE(file); // yes, big endian -#if 0 - { - int port_x = (port->PortLocation / 2) % SP_PLAYFIELD_WIDTH; - int port_y = (port->PortLocation / 2) / SP_PLAYFIELD_WIDTH; - - printf("::: %d: port_location == %d => (%d, %d)\n", - i, port->PortLocation, port_x, port_y); - } -#endif - - /* change gravity: 1 == "turn on", anything else (0) == "turn off" */ + // change gravity: 1 == "turn on", anything else (0) == "turn off" port->Gravity = getFile8Bit(file); - /* "freeze zonks": 2 == "turn on", anything else (0, 1) == "turn off" */ + // "freeze zonks": 2 == "turn on", anything else (0, 1) == "turn off" port->FreezeZonks = getFile8Bit(file); - /* "freeze enemies": 1 == "turn on", anything else (0) == "turn off" */ + // "freeze enemies": 1 == "turn on", anything else (0) == "turn off" port->FreezeEnemies = getFile8Bit(file); - ReadUnusedBytesFromFile(file, 1); /* (not used by Supaplex engine) */ + ReadUnusedBytesFromFile(file, 1); // (not used by Supaplex engine) } - /* SpeedByte XOR Highbyte(RandomSeed) */ + // SpeedByte XOR Highbyte(RandomSeed) header->SpeedByte = getFile8Bit(file); - /* CheckSum XOR SpeedByte */ + // CheckSum XOR SpeedByte header->CheckSumByte = getFile8Bit(file); - /* random seed used for recorded demos */ - header->DemoRandomSeed = getFile16BitLE(file); /* yes, little endian */ + // random seed used for recorded demos + header->DemoRandomSeed = getFile16BitLE(file); // yes, little endian -#if 0 - printf("::: file.c: DemoRandomSeed == %d\n", header->DemoRandomSeed); -#endif - - /* auto-determine number of infotrons if it was stored as "0" -- see above */ + // auto-determine number of infotrons if it was stored as "0" -- see above if (header->InfotronsNeeded == 0) { for (x = 0; x < native_sp_level.width; x++) @@ -204,33 +228,48 @@ static void LoadNativeLevelFromFileStream_SP(FILE *file, boolean demo_available) if (native_sp_level.playfield[x][y] == fiInfotron) header->InfotronsNeeded++; - header->InfotronsNeeded &= 0xff; /* only use low byte -- see above */ + header->InfotronsNeeded &= 0xff; // only use low byte -- see above } - /* also load demo tape, if available */ + // read raw level header bytes (96 bytes) + + seekFile(file, -(SP_HEADER_SIZE), SEEK_CUR); // rewind file + for (i = 0; i < SP_HEADER_SIZE; i++) + native_sp_level.header_raw_bytes[i] = getByteFromFile(file); + + // also load demo tape, if available (only in single level files) if (demo_available) { - for (i = 0; i < SP_MAX_TAPE_LEN && !feof(file); i++) - { - native_sp_level.demo[i] = getFile8Bit(file); + int level_nr = getFile8Bit(file); - if (native_sp_level.demo[i] == 0xff) /* "end of demo" byte */ - { - i++; + level_nr &= 0x7f; // clear highest bit + level_nr = (level_nr < 1 ? 1 : + level_nr > 111 ? 111 : level_nr); + + native_sp_level.demo.level_nr = level_nr; + + for (i = 0; i < SP_MAX_TAPE_LEN && !checkEndOfFile(file); i++) + { + native_sp_level.demo.data[i] = getFile8Bit(file); + if (native_sp_level.demo.data[i] == 0xff) // "end of demo" byte break; - } } - native_sp_level.demo_length = i; - native_sp_level.demo_available = (native_sp_level.demo_length > 0); + if (i >= SP_MAX_TAPE_LEN) + Error(ERR_WARN, "SP demo truncated: size exceeds maximum SP demo size %d", + SP_MAX_TAPE_LEN); + + native_sp_level.demo.length = i; + native_sp_level.demo.is_available = (native_sp_level.demo.length > 0); } } -boolean LoadNativeLevel_SP(char *filename, int pos) +boolean LoadNativeLevel_SP(char *filename, int level_pos, + boolean level_info_only) { - FILE *file; + File *file; int i, l, x, y; char name_first, name_last; struct LevelInfo_SP multipart_level; @@ -240,22 +279,91 @@ boolean LoadNativeLevel_SP(char *filename, int pos) boolean reading_multipart_level = FALSE; boolean use_empty_level = FALSE; LevelInfoType *header = &native_sp_level.header; - boolean demo_available = (strSuffix(filename, ".sp") || - strSuffix(filename, ".SP")); - - /* always start with reliable default values */ + boolean is_single_level_file = (strSuffixLower(filename, ".sp") || + strSuffixLower(filename, ".mpx")); + boolean demo_available = is_single_level_file; + boolean is_mpx_file = strSuffixLower(filename, ".mpx"); + int file_seek_pos = level_pos * SP_STD_LEVEL_SIZE; + int level_width = SP_STD_PLAYFIELD_WIDTH; + int level_height = SP_STD_PLAYFIELD_HEIGHT; + + // always start with reliable default values setLevelInfoToDefaults_SP(); copyInternalEngineVars_SP(); - if (!(file = fopen(filename, MODE_READ))) + if (!(file = openFile(filename, MODE_READ))) { - Error(ERR_WARN, "cannot open level '%s' -- using empty level", filename); + if (!level_info_only) + Error(ERR_WARN, "cannot open file '%s' -- using empty level", filename); return FALSE; } - /* position file stream to the requested level (in case of level package) */ - if (fseek(file, pos * SP_LEVEL_SIZE, SEEK_SET) != 0) + if (is_mpx_file) + { + char mpx_chunk_name[4 + 1]; + int mpx_version; + int mpx_level_count; + LevelDescriptor *mpx_level_desc; + + getFileChunkBE(file, mpx_chunk_name, NULL); + + if (!strEqual(mpx_chunk_name, "MPX ")) + { + Error(ERR_WARN, "cannot find MPX ID in file '%s' -- using empty level", + filename); + + return FALSE; + } + + mpx_version = getFile16BitLE(file); + + if (mpx_version != 1) + { + Error(ERR_WARN, "unknown MPX version in file '%s' -- using empty level", + filename); + + return FALSE; + } + + mpx_level_count = getFile16BitLE(file); + + if (mpx_level_count < 1) + { + Error(ERR_WARN, "no MPX levels found in file '%s' -- using empty level", + filename); + + return FALSE; + } + + if (level_pos >= mpx_level_count) + { + Error(ERR_WARN, "MPX level not found in file '%s' -- using empty level", + filename); + + return FALSE; + } + + mpx_level_desc = checked_calloc(mpx_level_count * sizeof(LevelDescriptor)); + + for (i = 0; i < mpx_level_count; i++) + { + LevelDescriptor *ldesc = &mpx_level_desc[i]; + + ldesc->Width = getFile16BitLE(file); + ldesc->Height = getFile16BitLE(file); + ldesc->OffSet = getFile32BitLE(file); // starts with 1, not with 0 + ldesc->Size = getFile32BitLE(file); + } + + level_width = mpx_level_desc[level_pos].Width; + level_height = mpx_level_desc[level_pos].Height; + + file_seek_pos = mpx_level_desc[level_pos].OffSet - 1; + } + + // position file stream to the requested level (in case of level package) + if (seekFile(file, file_seek_pos, SEEK_SET) != 0) { Error(ERR_WARN, "cannot fseek in file '%s' -- using empty level", filename); @@ -270,11 +378,15 @@ boolean LoadNativeLevel_SP(char *filename, int pos) of the level name, the multi-part level consists of only horizontal or vertical parts */ - for (l = pos; l < SP_NUM_LEVELS_PER_PACKAGE; l++) + for (l = level_pos; l < SP_NUM_LEVELS_PER_PACKAGE; l++) { - LoadNativeLevelFromFileStream_SP(file, demo_available); + LoadNativeLevelFromFileStream_SP(file, level_width, level_height, + demo_available); + + // check if this level is a part of a bigger multi-part level - /* check if this level is a part of a bigger multi-part level */ + if (is_single_level_file) + break; name_first = header->LevelTitle[0]; name_last = header->LevelTitle[SP_LEVEL_NAME_LEN - 1]; @@ -287,61 +399,68 @@ boolean LoadNativeLevel_SP(char *filename, int pos) ((name_first == '?' || name_first == '1') && (name_last == '?' || name_last == '1')); - /* correct leading multipart level meta information in level name */ - for (i = 0; i < SP_LEVEL_NAME_LEN && header->LevelTitle[i] == name_first; i++) - header->LevelTitle[i] = '-'; - - /* correct trailing multipart level meta information in level name */ - for (i = SP_LEVEL_NAME_LEN - 1; i >= 0 && header->LevelTitle[i] == name_last; i--) - header->LevelTitle[i] = '-'; + if (is_multipart_level) + { + // correct leading multipart level meta information in level name + for (i = 0; + i < SP_LEVEL_NAME_LEN && header->LevelTitle[i] == name_first; + i++) + header->LevelTitle[i] = '-'; + + // correct trailing multipart level meta information in level name + for (i = SP_LEVEL_NAME_LEN - 1; + i >= 0 && header->LevelTitle[i] == name_last; + i--) + header->LevelTitle[i] = '-'; + } - /* ---------- check for normal single level ---------- */ + // ---------- check for normal single level ---------- if (!reading_multipart_level && !is_multipart_level) { - /* the current level is simply a normal single-part level, and we are - not reading a multi-part level yet, so return the level as it is */ + // the current level is simply a normal single-part level, and we are + // not reading a multi-part level yet, so return the level as it is break; } - /* ---------- check for empty level (unused multi-part) ---------- */ + // ---------- check for empty level (unused multi-part) ---------- if (!reading_multipart_level && is_multipart_level && !is_first_part) { - /* this is a part of a multi-part level, but not the first part - (and we are not already reading parts of a multi-part level); - in this case, use an empty level instead of the single part */ + // this is a part of a multi-part level, but not the first part + // (and we are not already reading parts of a multi-part level); + // in this case, use an empty level instead of the single part use_empty_level = TRUE; break; } - /* ---------- check for finished multi-part level ---------- */ + // ---------- check for finished multi-part level ---------- if (reading_multipart_level && (!is_multipart_level || !strEqualN(header->LevelTitle, multipart_level.header.LevelTitle, SP_LEVEL_NAME_LEN))) { - /* we are already reading parts of a multi-part level, but this level is - either not a multi-part level, or a part of a different multi-part - level; in both cases, the multi-part level seems to be complete */ + // we are already reading parts of a multi-part level, but this level is + // either not a multi-part level, or a part of a different multi-part + // level; in both cases, the multi-part level seems to be complete break; } - /* ---------- here we have one part of a multi-part level ---------- */ + // ---------- here we have one part of a multi-part level ---------- reading_multipart_level = TRUE; - if (is_first_part) /* start with first part of new multi-part level */ + if (is_first_part) // start with first part of new multi-part level { - /* copy level info structure from first part */ + // copy level info structure from first part multipart_level = native_sp_level; - /* clear playfield of new multi-part level */ + // clear playfield of new multi-part level for (x = 0; x < SP_MAX_PLAYFIELD_WIDTH; x++) for (y = 0; y < SP_MAX_PLAYFIELD_HEIGHT; y++) multipart_level.playfield[x][y] = fiSpace; @@ -355,13 +474,8 @@ boolean LoadNativeLevel_SP(char *filename, int pos) multipart_xpos = (int)(name_first - '0'); multipart_ypos = (int)(name_last - '0'); -#if 0 - printf("----------> part (%d/%d) of multi-part level '%s'\n", - multipart_xpos, multipart_ypos, multipart_level.header.LevelTitle); -#endif - - if (multipart_xpos * SP_PLAYFIELD_WIDTH > SP_MAX_PLAYFIELD_WIDTH || - multipart_ypos * SP_PLAYFIELD_HEIGHT > SP_MAX_PLAYFIELD_HEIGHT) + if (multipart_xpos * SP_STD_PLAYFIELD_WIDTH > SP_MAX_PLAYFIELD_WIDTH || + multipart_ypos * SP_STD_PLAYFIELD_HEIGHT > SP_MAX_PLAYFIELD_HEIGHT) { Error(ERR_WARN, "multi-part level is too big -- ignoring part of it"); @@ -369,17 +483,17 @@ boolean LoadNativeLevel_SP(char *filename, int pos) } multipart_level.width = MAX(multipart_level.width, - multipart_xpos * SP_PLAYFIELD_WIDTH); + multipart_xpos * SP_STD_PLAYFIELD_WIDTH); multipart_level.height = MAX(multipart_level.height, - multipart_ypos * SP_PLAYFIELD_HEIGHT); + multipart_ypos * SP_STD_PLAYFIELD_HEIGHT); - /* copy level part at the right position of multi-part level */ - for (x = 0; x < SP_PLAYFIELD_WIDTH; x++) + // copy level part at the right position of multi-part level + for (x = 0; x < SP_STD_PLAYFIELD_WIDTH; x++) { - for (y = 0; y < SP_PLAYFIELD_HEIGHT; y++) + for (y = 0; y < SP_STD_PLAYFIELD_HEIGHT; y++) { - int start_x = (multipart_xpos - 1) * SP_PLAYFIELD_WIDTH; - int start_y = (multipart_ypos - 1) * SP_PLAYFIELD_HEIGHT; + int start_x = (multipart_xpos - 1) * SP_STD_PLAYFIELD_WIDTH; + int start_y = (multipart_ypos - 1) * SP_STD_PLAYFIELD_HEIGHT; multipart_level.playfield[start_x + x][start_y + y] = native_sp_level.playfield[x][y]; @@ -387,7 +501,7 @@ boolean LoadNativeLevel_SP(char *filename, int pos) } } - fclose(file); + closeFile(file); if (use_empty_level) { @@ -403,3 +517,66 @@ boolean LoadNativeLevel_SP(char *filename, int pos) return TRUE; } + +void SaveNativeLevel_SP(char *filename) +{ + LevelInfoType *header = &native_sp_level.header; + FILE *file; + int i, x, y; + + if (!(file = fopen(filename, MODE_WRITE))) + { + Error(ERR_WARN, "cannot save native level file '%s'", filename); + + return; + } + + // write level playfield (width * height == 60 * 24 tiles == 1440 bytes) + for (y = 0; y < native_sp_level.height; y++) + for (x = 0; x < native_sp_level.width; x++) + putFile8Bit(file, native_sp_level.playfield[x][y]); + + // write level header (96 bytes) + + WriteUnusedBytesToFile(file, 4); + + putFile8Bit(file, header->InitialGravity); + putFile8Bit(file, header->Version); + + for (i = 0; i < SP_LEVEL_NAME_LEN; i++) + putFile8Bit(file, header->LevelTitle[i]); + + putFile8Bit(file, header->InitialFreezeZonks); + putFile8Bit(file, header->InfotronsNeeded); + putFile8Bit(file, header->SpecialPortCount); + + for (i = 0; i < SP_MAX_SPECIAL_PORTS; i++) + { + SpecialPortType *port = &header->SpecialPort[i]; + + putFile16BitBE(file, port->PortLocation); + putFile8Bit(file, port->Gravity); + putFile8Bit(file, port->FreezeZonks); + putFile8Bit(file, port->FreezeEnemies); + + WriteUnusedBytesToFile(file, 1); + } + + putFile8Bit(file, header->SpeedByte); + putFile8Bit(file, header->CheckSumByte); + putFile16BitLE(file, header->DemoRandomSeed); + + // also save demo tape, if available + + if (native_sp_level.demo.is_available) + { + putFile8Bit(file, native_sp_level.demo.level_nr); + + for (i = 0; i < native_sp_level.demo.length; i++) + putFile8Bit(file, native_sp_level.demo.data[i]); + + putFile8Bit(file, 0xff); // "end of demo" byte + } + + fclose(file); +}