2b8a1755fbd66d1ad559fe76ad9c86dbf4132b19
[rocksndiamonds.git] / src / game_sp / file.c
1
2 #include "main_sp.h"
3 #include "global.h"
4
5
6 /* ------------------------------------------------------------------------- */
7 /* functions for loading Supaplex level                                      */
8 /* ------------------------------------------------------------------------- */
9
10 void setTapeInfoToDefaults_SP()
11 {
12   native_sp_level.demo.is_available = FALSE;
13   native_sp_level.demo.length = 0;
14 }
15
16 void setLevelInfoToDefaults_SP()
17 {
18   LevelInfoType *header = &native_sp_level.header;
19   char *empty_title = "-------- EMPTY --------";
20   int i, x, y;
21
22   native_sp_level.game_sp = &game_sp;
23
24   native_sp_level.width  = SP_STD_PLAYFIELD_WIDTH;
25   native_sp_level.height = SP_STD_PLAYFIELD_HEIGHT;
26
27   for (x = 0; x < native_sp_level.width; x++)
28     for (y = 0; y < native_sp_level.height; y++)
29       native_sp_level.playfield[x][y] = fiSpace;
30
31   /* copy string (without terminating '\0' character!) */
32   for (i = 0; i < SP_LEVEL_NAME_LEN; i++)
33     header->LevelTitle[i] = empty_title[i];
34
35   header->InitialGravity = 0;
36   header->Version = 0;
37   header->InitialFreezeZonks = 0;
38   header->InfotronsNeeded = 0;
39   header->SpecialPortCount = 0;
40   header->SpeedByte = 0;
41   header->CheckSumByte = 0;
42   header->DemoRandomSeed = 0;
43
44   for (i = 0; i < SP_MAX_SPECIAL_PORTS; i++)
45   {
46     SpecialPortType *port = &header->SpecialPort[i];
47
48     port->PortLocation = 0;
49     port->Gravity = 0;
50     port->FreezeZonks = 0;
51     port->FreezeEnemies = 0;
52   }
53
54   /* set raw header bytes (used for subsequent buffer zone) to "hardware" */
55   for (i = 0; i < SP_HEADER_SIZE; i++)
56     native_sp_level.header_raw_bytes[i] = 0x20;
57
58   setTapeInfoToDefaults_SP();
59 }
60
61 void copyInternalEngineVars_SP()
62 {
63   char *preceding_playfield_memory[] =
64   {
65     "95 89 95 89 95 89 3b 8a  3b 8a 3b 8a 3b 8a 3b 8a", // |......;.;.;.;.;.|
66     "3b 8a 3b 8a 3b 8a e8 8a  e8 8a e8 8a e8 8a e8 8a", // |;.;.;.è.è.è.è.è.|
67     "e8 8a e8 8a e8 8a b1 8b  b1 8b b1 8b b1 8b b1 8b", // |è.è.è.±.±.±.±.±.|
68     "b1 8b b1 8b b1 8b 85 8c  85 8c 85 8c 85 8c 85 8c", // |±.±.±...........|
69     "85 8c 85 8c 85 8c 5b 8d  5b 8d 5b 8d 5b 8d 5b 8d", // |......[.[.[.[.[.|
70     "5b 8d 5b 8d 5b 8d 06 8e  06 8e 06 8e 06 8e 06 8e", // |[.[.[...........|
71     "06 8e 06 8e 06 8e ac 8e  ac 8e ac 8e ac 8e ac 8e", // |......¬.¬.¬.¬.¬.|
72     "ac 8e ac 8e ac 8e 59 8f  59 8f 59 8f 59 8f 59 8f", // |¬.¬.¬.Y.Y.Y.Y.Y.|
73     "59 8f 59 8f 59 8f 00 00  70 13 00 00 00 00 e8 17", // |Y.Y.Y...p.....è.|
74     "00 00 00 00 00 00 69 38  00 00 00 00 00 00 00 00", // |......i8........|
75     "00 00 00 00 00 00 00 00  d0 86 00 00 b2 34 00 00", // |........Ð...²4..|
76     "00 00 00 00 00 00 8f 8b  1d 34 00 00 00 00 00 00", // |.........4......|
77     "00 00 00 00 23 39 09 09  00 0c 00 08 00 58 00 00", // |....#9.......X..|
78     "00 00 00 25 77 06 7f 00  00 00 01 00 00 00 00 00", // |...%w...........|
79     "00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00", // |................|
80     "00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00", // |................|
81     "00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00", // |................|
82     "00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00", // |................|
83     "00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00", // |................|
84     "00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00", // |................|
85     "00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00", // |................|
86     "00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00", // |................|
87     "00 00 00 00 00 00 00 00  00 ec 06 26 05 00 00 00", // |.........ì.&....|
88     "00 00 00 01 00 00 00 00  31 32 33 34 35 36 37 38", // |........12345678|
89     "39 30 2d 00 08 00 51 57  45 52 54 59 55 49 4f 50", // |90-...QWERTYUIOP|
90     "00 00 0a 00 41 53 44 46  47 48 4a 4b 4c 00 00 00", // |....ASDFGHJKL...|
91     "00 00 5a 58 43 56 42 4e  4d 00 00 00 00 00 00 20", // |..ZXCVBNM...... |
92     "00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00", // |................|
93     "00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00", // |................|
94     "00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00", // |................|
95     "00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00", // |................|
96     "00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00", // |................|
97     "00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00", // |................|
98     "00 00 00 00 00 00 2e 00  1e 00 31 00 14 00 39 00", // |..........1...9.|
99     "1f 00 14 00 18 00 ff ff  01 00 01 4c 45 56 45 4c", // |......ÿÿ...LEVEL|
100     "53 2e 44 41 54 00 00 00  00 00 00 00 00 00 00 00", // |S.DAT...........|
101     "00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00", // |................|
102     "00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00", // |................|
103     "00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00", // |................|
104     "00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00", // |................|
105     "00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00", // |................|
106     "00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00", // |................|
107     "00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00", // |................|
108
109     NULL
110   };
111   int preceding_buffer_size = 0;
112   int count;
113   int i, x, y;
114
115   for (i = 0; preceding_playfield_memory[i] != NULL; i++)
116     preceding_buffer_size += 8;         /* eight 16-bit integer values */
117
118   /* needed for engine snapshots */
119   game_sp.preceding_buffer_size = preceding_buffer_size;
120
121   LInfo = native_sp_level.header;
122
123   FieldWidth  = native_sp_level.width;
124   FieldHeight = native_sp_level.height;
125   HeaderSize = 96;
126
127   FieldMax = (FieldWidth * FieldHeight) + HeaderSize - 1;
128   LevelMax = (FieldWidth * FieldHeight) - 1;
129
130   /* (add one byte for the level number stored as first byte of demo data) */
131   FileMax = FieldMax + native_sp_level.demo.length + 1;
132
133   PlayField8 = REDIM_1D(sizeof(byte), 0, FileMax);
134   DisPlayField = REDIM_1D(sizeof(byte), 0, FieldMax);
135   PlayField16 = REDIM_1D(sizeof(int), -preceding_buffer_size, FieldMax);
136
137   count = 0;
138   for (i = 0; preceding_playfield_memory[i] != NULL; i++)
139   {
140     char *s = preceding_playfield_memory[i];
141     boolean hi_byte = FALSE;    /* little endian data => start with low byte */
142
143     while (s[0] != '\0' && s[1] != '\0')
144     {
145       int hi_nibble = s[0] - (s[0] > '9' ? 'a' - 10 : '0');
146       int lo_nibble = s[1] - (s[1] > '9' ? 'a' - 10 : '0');
147       int byte = (hi_nibble << 4) | lo_nibble;
148
149       if (hi_byte)
150         byte <<= 8;
151
152       PlayField16[-preceding_buffer_size + count] |= byte;
153
154       if (hi_byte)
155         count++;
156
157       hi_byte = !hi_byte;
158
159       s += 2;
160
161       while (*s == ' ')
162         s++;
163     }
164   }
165
166   count = 0;
167   for (y = 0; y < native_sp_level.height; y++)
168     for (x = 0; x < native_sp_level.width; x++)
169       PlayField8[count++] = native_sp_level.playfield[x][y];
170
171   /* add raw header bytes to subsequent playfield buffer zone */
172   for (i = 0; i < SP_HEADER_SIZE; i++)
173     PlayField8[count++] = native_sp_level.header_raw_bytes[i];
174
175   for (i = 0; i < count; i++)
176   {
177     PlayField16[i] = PlayField8[i];
178     DisPlayField[i] = PlayField8[i];
179     PlayField8[i] = 0;
180   }
181
182   if (native_sp_level.demo.is_available)
183   {
184     DemoAvailable = True;
185
186     PlayField8[FieldMax + 1] = native_sp_level.demo.level_nr;
187
188     for (i = 0; i < native_sp_level.demo.length; i++)
189       PlayField8[FieldMax + 2 + i] = native_sp_level.demo.data[i];
190   }
191
192   AnimationPosTable = REDIM_1D(sizeof(int), 0, LevelMax - 2 * FieldWidth);
193   AnimationSubTable = REDIM_1D(sizeof(byte), 0, LevelMax - 2 * FieldWidth);
194   TerminalState = REDIM_1D(sizeof(byte), 0, FieldMax + 1 - 1);
195
196   GravityFlag = LInfo.InitialGravity;
197   FreezeZonks = LInfo.InitialFreezeZonks;
198
199 #if 1
200   /* this is set by main game tape code to native random generator directly */
201 #else
202   RandomSeed = LInfo.DemoRandomSeed;
203 #endif
204
205   LevelLoaded = True;
206 }
207
208 static void LoadNativeLevelFromFileStream_SP(FILE *file, int width, int height,
209                                              boolean demo_available)
210 {
211   LevelInfoType *header = &native_sp_level.header;
212   int i, x, y;
213
214   /* for details of the Supaplex level format, see Herman Perk's Supaplex
215      documentation file "SPFIX63.DOC" from his Supaplex "SpeedFix" package */
216
217   native_sp_level.width  = width;
218   native_sp_level.height = height;
219
220   /* read level playfield (width * height == 60 * 24 tiles == 1440 bytes) */
221   for (y = 0; y < native_sp_level.height; y++)
222     for (x = 0; x < native_sp_level.width; x++)
223       native_sp_level.playfield[x][y] = getFile8Bit(file);
224
225   /* read level header (96 bytes) */
226
227   ReadUnusedBytesFromFile(file, 4);     /* (not used by Supaplex engine) */
228
229   /* initial gravity: 1 == "on", anything else (0) == "off" */
230   header->InitialGravity = getFile8Bit(file);
231
232   /* SpeedFixVersion XOR 0x20 */
233   header->Version = getFile8Bit(file);
234
235   /* level title in uppercase letters, padded with dashes ("-") (23 bytes) */
236   for (i = 0; i < SP_LEVEL_NAME_LEN; i++)
237     header->LevelTitle[i] = getFile8Bit(file);
238
239   /* initial "freeze zonks": 2 == "on", anything else (0, 1) == "off" */
240   header->InitialFreezeZonks = getFile8Bit(file);
241
242   /* number of infotrons needed; 0 means that Supaplex will count the total
243      amount of infotrons in the level and use the low byte of that number
244      (a multiple of 256 infotrons will result in "0 infotrons needed"!) */
245   header->InfotronsNeeded = getFile8Bit(file);
246
247   /* number of special ("gravity") port entries below (maximum 10 allowed) */
248   header->SpecialPortCount = getFile8Bit(file);
249
250   /* database of properties of up to 10 special ports (6 bytes per port) */
251   for (i = 0; i < SP_MAX_SPECIAL_PORTS; i++)
252   {
253     SpecialPortType *port = &header->SpecialPort[i];
254
255     /* high and low byte of the location of a special port; if (x, y) are the
256        coordinates of a port in the field and (0, 0) is the top-left corner,
257        the 16 bit value here calculates as 2 * (x + (y * 60)) (this is twice
258        of what may be expected: Supaplex works with a game field in memory
259        which is 2 bytes per tile) */
260     port->PortLocation = getFile16BitBE(file);          /* yes, big endian */
261
262     /* change gravity: 1 == "turn on", anything else (0) == "turn off" */
263     port->Gravity = getFile8Bit(file);
264
265     /* "freeze zonks": 2 == "turn on", anything else (0, 1) == "turn off" */
266     port->FreezeZonks = getFile8Bit(file);
267
268     /* "freeze enemies": 1 == "turn on", anything else (0) == "turn off" */
269     port->FreezeEnemies = getFile8Bit(file);
270
271     ReadUnusedBytesFromFile(file, 1);   /* (not used by Supaplex engine) */
272   }
273
274   /* SpeedByte XOR Highbyte(RandomSeed) */
275   header->SpeedByte = getFile8Bit(file);
276
277   /* CheckSum XOR SpeedByte */
278   header->CheckSumByte = getFile8Bit(file);
279
280   /* random seed used for recorded demos */
281   header->DemoRandomSeed = getFile16BitLE(file);        /* yes, little endian */
282
283   /* auto-determine number of infotrons if it was stored as "0" -- see above */
284   if (header->InfotronsNeeded == 0)
285   {
286     for (x = 0; x < native_sp_level.width; x++)
287       for (y = 0; y < native_sp_level.height; y++)
288         if (native_sp_level.playfield[x][y] == fiInfotron)
289           header->InfotronsNeeded++;
290
291     header->InfotronsNeeded &= 0xff;    /* only use low byte -- see above */
292   }
293
294   /* read raw level header bytes (96 bytes) */
295
296   fseek(file, -(SP_HEADER_SIZE), SEEK_CUR);     /* rewind file */
297   for (i = 0; i < SP_HEADER_SIZE; i++)
298     native_sp_level.header_raw_bytes[i] = fgetc(file);
299
300   /* also load demo tape, if available (only in single level files) */
301
302   if (demo_available)
303   {
304     int level_nr = getFile8Bit(file);
305
306     level_nr &= 0x7f;                   /* clear highest bit */
307     level_nr = (level_nr < 1   ? 1   :
308                 level_nr > 111 ? 111 : level_nr);
309
310     native_sp_level.demo.level_nr = level_nr;
311
312     for (i = 0; i < SP_MAX_TAPE_LEN && !feof(file); i++)
313     {
314       native_sp_level.demo.data[i] = getFile8Bit(file);
315
316       if (native_sp_level.demo.data[i] == 0xff) /* "end of demo" byte */
317       {
318         i++;
319
320         break;
321       }
322     }
323
324     native_sp_level.demo.length = i;
325     native_sp_level.demo.is_available = (native_sp_level.demo.length > 0);
326   }
327 }
328
329 boolean LoadNativeLevel_SP(char *filename, int level_pos)
330 {
331   FILE *file;
332   int i, l, x, y;
333   char name_first, name_last;
334   struct LevelInfo_SP multipart_level;
335   int multipart_xpos, multipart_ypos;
336   boolean is_multipart_level;
337   boolean is_first_part;
338   boolean reading_multipart_level = FALSE;
339   boolean use_empty_level = FALSE;
340   LevelInfoType *header = &native_sp_level.header;
341   boolean is_single_level_file = (strSuffixLower(filename, ".sp") ||
342                                   strSuffixLower(filename, ".mpx"));
343   boolean demo_available = is_single_level_file;
344   boolean is_mpx_file = strSuffixLower(filename, ".mpx");
345   int file_seek_pos = level_pos * SP_STD_LEVEL_SIZE;
346   int level_width  = SP_STD_PLAYFIELD_WIDTH;
347   int level_height = SP_STD_PLAYFIELD_HEIGHT;
348
349   /* always start with reliable default values */
350   setLevelInfoToDefaults_SP();
351   copyInternalEngineVars_SP();
352
353   if (!(file = fopen(filename, MODE_READ)))
354   {
355     Error(ERR_WARN, "cannot open file '%s' -- using empty level", filename);
356
357     return FALSE;
358   }
359
360   if (is_mpx_file)
361   {
362     char mpx_chunk_name[4 + 1];
363     int mpx_version;
364     int mpx_level_count;
365     LevelDescriptor *mpx_level_desc;
366
367     getFileChunkBE(file, mpx_chunk_name, NULL);
368
369     if (!strEqual(mpx_chunk_name, "MPX "))
370     {
371       Error(ERR_WARN, "cannot find MPX ID in file '%s' -- using empty level",
372             filename);
373
374       return FALSE;
375     }
376
377     mpx_version = getFile16BitLE(file);
378
379     if (mpx_version != 1)
380     {
381       Error(ERR_WARN, "unknown MPX version in file '%s' -- using empty level",
382             filename);
383
384       return FALSE;
385     }
386
387     mpx_level_count = getFile16BitLE(file);
388
389     if (mpx_level_count < 1)
390     {
391       Error(ERR_WARN, "no MPX levels found in file '%s' -- using empty level",
392             filename);
393
394       return FALSE;
395     }
396
397     if (level_pos >= mpx_level_count)
398     {
399       Error(ERR_WARN, "MPX level not found in file '%s' -- using empty level",
400             filename);
401
402       return FALSE;
403     }
404
405     mpx_level_desc = checked_calloc(mpx_level_count * sizeof(LevelDescriptor));
406
407     for (i = 0; i < mpx_level_count; i++)
408     {
409       LevelDescriptor *ldesc = &mpx_level_desc[i];
410
411       ldesc->Width  = getFile16BitLE(file);
412       ldesc->Height = getFile16BitLE(file);
413       ldesc->OffSet = getFile32BitLE(file);     /* starts with 1, not with 0 */
414       ldesc->Size   = getFile32BitLE(file);
415     }
416
417     level_width  = mpx_level_desc[level_pos].Width;
418     level_height = mpx_level_desc[level_pos].Height;
419
420     file_seek_pos = mpx_level_desc[level_pos].OffSet - 1;
421   }
422
423   /* position file stream to the requested level (in case of level package) */
424   if (fseek(file, file_seek_pos, SEEK_SET) != 0)
425   {
426     Error(ERR_WARN, "cannot fseek in file '%s' -- using empty level", filename);
427
428     return FALSE;
429   }
430
431   /* there exist Supaplex level package files with multi-part levels which
432      can be detected as follows: instead of leading and trailing dashes ('-')
433      to pad the level name, they have leading and trailing numbers which are
434      the x and y coordinations of the current part of the multi-part level;
435      if there are '?' characters instead of numbers on the left or right side
436      of the level name, the multi-part level consists of only horizontal or
437      vertical parts */
438
439   for (l = level_pos; l < SP_NUM_LEVELS_PER_PACKAGE; l++)
440   {
441     LoadNativeLevelFromFileStream_SP(file, level_width, level_height,
442                                      demo_available);
443
444     /* check if this level is a part of a bigger multi-part level */
445
446     if (is_single_level_file)
447       break;
448
449     name_first = header->LevelTitle[0];
450     name_last  = header->LevelTitle[SP_LEVEL_NAME_LEN - 1];
451
452     is_multipart_level =
453       ((name_first == '?' || (name_first >= '0' && name_first <= '9')) &&
454        (name_last  == '?' || (name_last  >= '0' && name_last  <= '9')));
455
456     is_first_part =
457       ((name_first == '?' || name_first == '1') &&
458        (name_last  == '?' || name_last  == '1'));
459
460     if (is_multipart_level)
461     {
462       /* correct leading multipart level meta information in level name */
463       for (i = 0;
464            i < SP_LEVEL_NAME_LEN && header->LevelTitle[i] == name_first;
465            i++)
466         header->LevelTitle[i] = '-';
467
468       /* correct trailing multipart level meta information in level name */
469       for (i = SP_LEVEL_NAME_LEN - 1;
470            i >= 0 && header->LevelTitle[i] == name_last;
471            i--)
472         header->LevelTitle[i] = '-';
473     }
474
475     /* ---------- check for normal single level ---------- */
476
477     if (!reading_multipart_level && !is_multipart_level)
478     {
479       /* the current level is simply a normal single-part level, and we are
480          not reading a multi-part level yet, so return the level as it is */
481
482       break;
483     }
484
485     /* ---------- check for empty level (unused multi-part) ---------- */
486
487     if (!reading_multipart_level && is_multipart_level && !is_first_part)
488     {
489       /* this is a part of a multi-part level, but not the first part
490          (and we are not already reading parts of a multi-part level);
491          in this case, use an empty level instead of the single part */
492
493       use_empty_level = TRUE;
494
495       break;
496     }
497
498     /* ---------- check for finished multi-part level ---------- */
499
500     if (reading_multipart_level &&
501         (!is_multipart_level ||
502          !strEqualN(header->LevelTitle, multipart_level.header.LevelTitle,
503                     SP_LEVEL_NAME_LEN)))
504     {
505       /* we are already reading parts of a multi-part level, but this level is
506          either not a multi-part level, or a part of a different multi-part
507          level; in both cases, the multi-part level seems to be complete */
508
509       break;
510     }
511
512     /* ---------- here we have one part of a multi-part level ---------- */
513
514     reading_multipart_level = TRUE;
515
516     if (is_first_part)  /* start with first part of new multi-part level */
517     {
518       /* copy level info structure from first part */
519       multipart_level = native_sp_level;
520
521       /* clear playfield of new multi-part level */
522       for (x = 0; x < SP_MAX_PLAYFIELD_WIDTH; x++)
523         for (y = 0; y < SP_MAX_PLAYFIELD_HEIGHT; y++)
524           multipart_level.playfield[x][y] = fiSpace;
525     }
526
527     if (name_first == '?')
528       name_first = '1';
529     if (name_last == '?')
530       name_last = '1';
531
532     multipart_xpos = (int)(name_first - '0');
533     multipart_ypos = (int)(name_last  - '0');
534
535 #if 0
536     printf("----------> part (%d/%d) of multi-part level '%s'\n",
537            multipart_xpos, multipart_ypos, multipart_level.header.LevelTitle);
538 #endif
539
540     if (multipart_xpos * SP_STD_PLAYFIELD_WIDTH  > SP_MAX_PLAYFIELD_WIDTH ||
541         multipart_ypos * SP_STD_PLAYFIELD_HEIGHT > SP_MAX_PLAYFIELD_HEIGHT)
542     {
543       Error(ERR_WARN, "multi-part level is too big -- ignoring part of it");
544
545       break;
546     }
547
548     multipart_level.width  = MAX(multipart_level.width,
549                                  multipart_xpos * SP_STD_PLAYFIELD_WIDTH);
550     multipart_level.height = MAX(multipart_level.height,
551                                  multipart_ypos * SP_STD_PLAYFIELD_HEIGHT);
552
553     /* copy level part at the right position of multi-part level */
554     for (x = 0; x < SP_STD_PLAYFIELD_WIDTH; x++)
555     {
556       for (y = 0; y < SP_STD_PLAYFIELD_HEIGHT; y++)
557       {
558         int start_x = (multipart_xpos - 1) * SP_STD_PLAYFIELD_WIDTH;
559         int start_y = (multipart_ypos - 1) * SP_STD_PLAYFIELD_HEIGHT;
560
561         multipart_level.playfield[start_x + x][start_y + y] =
562           native_sp_level.playfield[x][y];
563       }
564     }
565   }
566
567   fclose(file);
568
569   if (use_empty_level)
570   {
571     setLevelInfoToDefaults_SP();
572
573     Error(ERR_WARN, "single part of multi-part level -- using empty level");
574   }
575
576   if (reading_multipart_level)
577     native_sp_level = multipart_level;
578
579   copyInternalEngineVars_SP();
580
581   return TRUE;
582 }
583
584 void SaveNativeLevel_SP(char *filename)
585 {
586   LevelInfoType *header = &native_sp_level.header;
587   FILE *file;
588   int i, x, y;
589
590   if (!(file = fopen(filename, MODE_WRITE)))
591   {
592     Error(ERR_WARN, "cannot save native level file '%s'", filename);
593
594     return;
595   }
596
597   /* write level playfield (width * height == 60 * 24 tiles == 1440 bytes) */
598   for (y = 0; y < native_sp_level.height; y++)
599     for (x = 0; x < native_sp_level.width; x++)
600       putFile8Bit(file, native_sp_level.playfield[x][y]);
601
602   /* write level header (96 bytes) */
603
604   WriteUnusedBytesToFile(file, 4);
605
606   putFile8Bit(file, header->InitialGravity);
607   putFile8Bit(file, header->Version);
608
609   for (i = 0; i < SP_LEVEL_NAME_LEN; i++)
610     putFile8Bit(file, header->LevelTitle[i]);
611
612   putFile8Bit(file, header->InitialFreezeZonks);
613   putFile8Bit(file, header->InfotronsNeeded);
614   putFile8Bit(file, header->SpecialPortCount);
615
616   for (i = 0; i < SP_MAX_SPECIAL_PORTS; i++)
617   {
618     SpecialPortType *port = &header->SpecialPort[i];
619
620     putFile16BitBE(file, port->PortLocation);
621     putFile8Bit(file, port->Gravity);
622     putFile8Bit(file, port->FreezeZonks);
623     putFile8Bit(file, port->FreezeEnemies);
624
625     WriteUnusedBytesToFile(file, 1);
626   }
627
628   putFile8Bit(file, header->SpeedByte);
629   putFile8Bit(file, header->CheckSumByte);
630   putFile16BitLE(file, header->DemoRandomSeed);
631
632   /* also save demo tape, if available */
633
634   if (native_sp_level.demo.is_available)
635   {
636     putFile8Bit(file, native_sp_level.demo.level_nr);
637
638     for (i = 0; i < native_sp_level.demo.length; i++)
639       putFile8Bit(file, native_sp_level.demo.data[i]);
640   }
641
642   fclose(file);
643 }