updated contact info in source file headers
[rocksndiamonds.git] / src / game_sp / file.c
index 40ffacb35738d5bef3479c718614c97cd74de874..438e76644e9791943153c3b6a1e04d088b4fe46f 100644 (file)
@@ -7,14 +7,22 @@
 /* functions for loading Supaplex level                                      */
 /* ------------------------------------------------------------------------- */
 
+void setTapeInfoToDefaults_SP()
+{
+  native_sp_level.demo.is_available = FALSE;
+  native_sp_level.demo.length = 0;
+}
+
 void setLevelInfoToDefaults_SP()
 {
   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++)
@@ -47,14 +55,13 @@ void setLevelInfoToDefaults_SP()
   for (i = 0; i < SP_HEADER_SIZE; i++)
     native_sp_level.header_raw_bytes[i] = 0x20;
 
-  native_sp_level.demo.is_available = FALSE;
-  native_sp_level.demo.length = 0;
+  setTapeInfoToDefaults_SP();
 }
 
 void copyInternalEngineVars_SP()
 {
-  int i, x, y;
   int count;
+  int i, x, y;
 
   LInfo = native_sp_level.header;
 
@@ -65,29 +72,53 @@ void copyInternalEngineVars_SP()
   FieldMax = (FieldWidth * FieldHeight) + HeaderSize - 1;
   LevelMax = (FieldWidth * FieldHeight) - 1;
 
-  FileMax = FieldMax + native_sp_level.demo.length;
+#if 0
+  /* (add one byte for the level number stored as first byte of demo data) */
+  FileMax = FieldMax + native_sp_level.demo.length + 1;
+#endif
 
-  PlayField8 = REDIM_1D(sizeof(byte), 0, FileMax + 1 - 1);
-  DisPlayField = REDIM_1D(sizeof(byte), 0, FieldMax + 1 - 1);
 #if 0
-  PlayField16 = REDIM_1D(sizeof(int), -FieldWidth, FieldMax);
-#else
-  PlayField16 = REDIM_1D(sizeof(int), -FieldWidth * 2, FieldMax);
+  PlayField8 = REDIM_1D(sizeof(byte), 0, FileMax);
+  DisPlayField = REDIM_1D(sizeof(byte), 0, FieldMax);
+  PlayField16 = REDIM_1D(sizeof(int), -game_sp.preceding_buffer_size, FieldMax);
 #endif
 
-#if 1
+  /* initialize preceding playfield buffer */
+  for (i = -game_sp.preceding_buffer_size; i < 0; i++)
+    PlayField16[i] = 0;
 
-#if 1
-  /* fill preceding playfield buffer zone with (indestructible) "hardware" */
-  for (i = -FieldWidth * 2; i < -FieldWidth; i++)
-    PlayField16[i] = 0x20;
-#endif
+  /* initialize preceding playfield buffer */
+  for (i = -SP_MAX_PLAYFIELD_WIDTH; i < 0; i++)
+    PlayField8[i] = 0;
 
-#if 0
-  /* fill preceding playfield buffer zone with (indestructible) "hardware" */
-  for (i = -FieldWidth; i < 0; i++)
-    PlayField16[i] = 0x20;
-#endif
+  count = 0;
+  for (i = 0; game_sp.preceding_buffer[i] != NULL; i++)
+  {
+    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')
+    {
+      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[-game_sp.preceding_buffer_size + count] |= byte;
+
+      if (hi_byte)
+       count++;
+
+      hi_byte = !hi_byte;
+
+      s += 2;
+
+      while (*s == ' ')
+       s++;
+    }
+  }
 
   count = 0;
   for (y = 0; y < native_sp_level.height; y++)
@@ -105,41 +136,25 @@ void copyInternalEngineVars_SP()
     PlayField8[i] = 0;
   }
 
-#else
-
-  for (i = 0; y = 0; y < native_sp_level.height; y++)
-  {
-    for (x = 0; x < native_sp_level.width; x++)
-    {
-      PlayField8[i] = native_sp_level.playfield[x][y];
-
-      PlayField16[i] = PlayField8[i];
-      DisPlayField[i] = PlayField8[i];
-      PlayField8[i] = 0;
-
-      i++;
-    }
-  }
-
-#endif
-
   if (native_sp_level.demo.is_available)
   {
     DemoAvailable = True;
 
+#if 0
+    /* !!! NEVER USED !!! */
     PlayField8[FieldMax + 1] = native_sp_level.demo.level_nr;
 
+    /* !!! NEVER USED !!! */
     for (i = 0; i < native_sp_level.demo.length; i++)
-      PlayField8[FieldMax + i + 2] = native_sp_level.demo.data[i];
+      PlayField8[FieldMax + 2 + i] = native_sp_level.demo.data[i];
+#endif
   }
 
+#if 0
   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);
-
-  DemoPointer = FieldMax + 1;
-  DemoOffset = DemoPointer;
-  DemoKeyRepeatCounter = 0;
+  TerminalState = REDIM_1D(sizeof(byte), 0, FieldMax);
+#endif
 
   GravityFlag = LInfo.InitialGravity;
   FreezeZonks = LInfo.InitialFreezeZonks;
@@ -147,19 +162,15 @@ void copyInternalEngineVars_SP()
 #if 1
   /* this is set by main game tape code to native random generator directly */
 #else
-
-#if 1
-  printf("::: file.c: copyInternalEngineVars_SP(): RandomSeed = LInfo.DemoRandomSeed\n");
-#endif
-
   RandomSeed = LInfo.DemoRandomSeed;
-
 #endif
 
   LevelLoaded = True;
 }
 
-static void LoadNativeLevelFromFileStream_SP(FILE *file, int width, int height,
+#if 1
+
+static void LoadNativeLevelFromFileStream_SP(File *file, int width, int height,
                                             boolean demo_available)
 {
   LevelInfoType *header = &native_sp_level.header;
@@ -168,13 +179,22 @@ static void LoadNativeLevelFromFileStream_SP(FILE *file, int width, int height,
   /* 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  = width;
-  native_sp_level.height = 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);
+  /* (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);
+
+      if (x < SP_MAX_PLAYFIELD_WIDTH &&
+         y < SP_MAX_PLAYFIELD_HEIGHT)
+       native_sp_level.playfield[x][y] = element;
+    }
+  }
 
   /* read level header (96 bytes) */
 
@@ -201,10 +221,6 @@ static void LoadNativeLevelFromFileStream_SP(FILE *file, int width, int height,
   /* 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) */
   for (i = 0; i < SP_MAX_SPECIAL_PORTS; i++)
   {
@@ -217,15 +233,137 @@ static void LoadNativeLevelFromFileStream_SP(FILE *file, int width, int height,
        which is 2 bytes per tile) */
     port->PortLocation = getFile16BitBE(file);         /* yes, big endian */
 
-#if 0
+    /* 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" */
+    port->FreezeZonks = getFile8Bit(file);
+
+    /* "freeze enemies": 1 == "turn on", anything else (0) == "turn off" */
+    port->FreezeEnemies = getFile8Bit(file);
+
+    ReadUnusedBytesFromFile(file, 1);  /* (not used by Supaplex engine) */
+  }
+
+  /* SpeedByte XOR Highbyte(RandomSeed) */
+  header->SpeedByte = getFile8Bit(file);
+
+  /* CheckSum XOR SpeedByte */
+  header->CheckSumByte = getFile8Bit(file);
+
+  /* random seed used for recorded demos */
+  header->DemoRandomSeed = getFile16BitLE(file);       /* yes, little endian */
+
+  /* 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++)
+      for (y = 0; y < native_sp_level.height; y++)
+       if (native_sp_level.playfield[x][y] == fiInfotron)
+         header->InfotronsNeeded++;
+
+    header->InfotronsNeeded &= 0xff;   /* only use low byte -- see above */
+  }
+
+  /* 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)
+  {
+    int level_nr = getFile8Bit(file);
+
+    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++)
     {
-      int port_x = (port->PortLocation / 2) % SP_PLAYFIELD_WIDTH;
-      int port_y = (port->PortLocation / 2) / SP_PLAYFIELD_WIDTH;
+      native_sp_level.demo.data[i] = getFile8Bit(file);
 
-      printf("::: %d: port_location == %d => (%d, %d)\n",
-            i, port->PortLocation, port_x, port_y);
+      if (native_sp_level.demo.data[i] == 0xff)        /* "end of demo" byte */
+      {
+       i++;
+
+       break;
+      }
     }
-#endif
+
+    native_sp_level.demo.length = i;
+    native_sp_level.demo.is_available = (native_sp_level.demo.length > 0);
+  }
+}
+
+#else
+
+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 */
+
+  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) */
+  /* (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);
+
+      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) */
+
+  /* initial gravity: 1 == "on", anything else (0) == "off" */
+  header->InitialGravity = getFile8Bit(file);
+
+  /* SpeedFixVersion XOR 0x20 */
+  header->Version = getFile8Bit(file);
+
+  /* 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" */
+  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"!) */
+  header->InfotronsNeeded = getFile8Bit(file);
+
+  /* number of special ("gravity") port entries below (maximum 10 allowed) */
+  header->SpecialPortCount = getFile8Bit(file);
+
+  /* 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];
+
+    /* high and low byte of the location of a special port; if (x, y) are the
+       coordinates of a port in the field and (0, 0) is the top-left corner,
+       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 */
 
     /* change gravity: 1 == "turn on", anything else (0) == "turn off" */
     port->Gravity = getFile8Bit(file);
@@ -247,11 +385,6 @@ static void LoadNativeLevelFromFileStream_SP(FILE *file, int width, int height,
 
   /* random seed used for recorded demos */
   header->DemoRandomSeed = getFile16BitLE(file);       /* yes, little endian */
-  // header->DemoRandomSeed = getFile16BitBE(file);    /* !!! TEST ONLY !!! */
-
-#if 0
-  printf("::: file.c: DemoRandomSeed == %d\n", header->DemoRandomSeed);
-#endif
 
   /* auto-determine number of infotrons if it was stored as "0" -- see above */
   if (header->InfotronsNeeded == 0)
@@ -299,7 +432,271 @@ static void LoadNativeLevelFromFileStream_SP(FILE *file, int width, int height,
   }
 }
 
-boolean LoadNativeLevel_SP(char *filename, int level_pos)
+#endif
+
+#if 1
+
+boolean LoadNativeLevel_SP(char *filename, int level_pos,
+                          boolean level_info_only)
+{
+  File *file;
+  int i, l, x, y;
+  char name_first, name_last;
+  struct LevelInfo_SP multipart_level;
+  int multipart_xpos, multipart_ypos;
+  boolean is_multipart_level;
+  boolean is_first_part;
+  boolean reading_multipart_level = FALSE;
+  boolean use_empty_level = FALSE;
+  LevelInfoType *header = &native_sp_level.header;
+  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 = openFile(filename, MODE_READ)))
+  {
+    if (!level_info_only)
+      Error(ERR_WARN, "cannot open file '%s' -- using empty level", filename);
+
+    return FALSE;
+  }
+
+  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);
+
+    return FALSE;
+  }
+
+  /* there exist Supaplex level package files with multi-part levels which
+     can be detected as follows: instead of leading and trailing dashes ('-')
+     to pad the level name, they have leading and trailing numbers which are
+     the x and y coordinations of the current part of the multi-part level;
+     if there are '?' characters instead of numbers on the left or right side
+     of the level name, the multi-part level consists of only horizontal or
+     vertical parts */
+
+  for (l = level_pos; l < SP_NUM_LEVELS_PER_PACKAGE; l++)
+  {
+    LoadNativeLevelFromFileStream_SP(file, level_width, level_height,
+                                    demo_available);
+
+    /* 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];
+
+    is_multipart_level =
+      ((name_first == '?' || (name_first >= '0' && name_first <= '9')) &&
+       (name_last  == '?' || (name_last  >= '0' && name_last  <= '9')));
+
+    is_first_part =
+      ((name_first == '?' || name_first == '1') &&
+       (name_last  == '?' || name_last  == '1'));
+
+    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 ---------- */
+
+    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 */
+
+      break;
+    }
+
+    /* ---------- 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 */
+
+      use_empty_level = TRUE;
+
+      break;
+    }
+
+    /* ---------- 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 */
+
+      break;
+    }
+
+    /* ---------- 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 */
+    {
+      /* copy level info structure from first part */
+      multipart_level = native_sp_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;
+    }
+
+    if (name_first == '?')
+      name_first = '1';
+    if (name_last == '?')
+      name_last = '1';
+
+    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_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");
+
+      break;
+    }
+
+    multipart_level.width  = MAX(multipart_level.width,
+                                multipart_xpos * SP_STD_PLAYFIELD_WIDTH);
+    multipart_level.height = MAX(multipart_level.height,
+                                multipart_ypos * SP_STD_PLAYFIELD_HEIGHT);
+
+    /* 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_STD_PLAYFIELD_HEIGHT; y++)
+      {
+       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];
+      }
+    }
+  }
+
+  closeFile(file);
+
+  if (use_empty_level)
+  {
+    setLevelInfoToDefaults_SP();
+
+    Error(ERR_WARN, "single part of multi-part level -- using empty level");
+  }
+
+  if (reading_multipart_level)
+    native_sp_level = multipart_level;
+
+  copyInternalEngineVars_SP();
+
+  return TRUE;
+}
+
+#else
+
+boolean LoadNativeLevel_SP(char *filename, int level_pos,
+                          boolean level_info_only)
 {
   FILE *file;
   int i, l, x, y;
@@ -315,9 +712,9 @@ boolean LoadNativeLevel_SP(char *filename, int level_pos)
                                  strSuffixLower(filename, ".mpx"));
   boolean demo_available = is_single_level_file;
   boolean is_mpx_file = strSuffixLower(filename, ".mpx");
-  int file_seek_pos = level_pos * SP_LEVEL_SIZE;
-  int level_width  = SP_PLAYFIELD_WIDTH;
-  int level_height = SP_PLAYFIELD_HEIGHT;
+  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();
@@ -325,7 +722,8 @@ boolean LoadNativeLevel_SP(char *filename, int level_pos)
 
   if (!(file = fopen(filename, MODE_READ)))
   {
-    Error(ERR_WARN, "cannot open file '%s' -- using empty level", filename);
+    if (!level_info_only)
+      Error(ERR_WARN, "cannot open file '%s' -- using empty level", filename);
 
     return FALSE;
   }
@@ -510,8 +908,8 @@ boolean LoadNativeLevel_SP(char *filename, int level_pos)
           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");
 
@@ -519,17 +917,17 @@ boolean LoadNativeLevel_SP(char *filename, int level_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++)
+    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];
@@ -553,3 +951,66 @@ boolean LoadNativeLevel_SP(char *filename, int level_pos)
 
   return TRUE;
 }
+
+#endif
+
+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]);
+  }
+
+  fclose(file);
+}