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