added saving BD style levels in R'n'D format as native BDCFF files
[rocksndiamonds.git] / src / game_bd / bd_bdcff.c
index 9d2a509c136782516d39cee96eaa709b051f2839..31fab6e814144d2b5a88cf19e200dbcaea7fd50a 100644 (file)
@@ -14,9 +14,6 @@
  * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
  */
 
-#include <glib.h>
-#include <glib/gi18n.h>
-
 #include <errno.h>
 
 #include "main_bd.h"
@@ -140,7 +137,7 @@ static boolean attrib_is_valid_for_caveset(const char *attrib)
   return FALSE;
 }
 
-static boolean struct_set_property(gpointer str, const GdStructDescriptor *prop_desc,
+static boolean struct_set_property(void *str, const GdStructDescriptor *prop_desc,
                                   const char *attrib, const char *param, int ratio)
 {
   char **params;
@@ -163,7 +160,7 @@ static boolean struct_set_property(gpointer str, const GdStructDescriptor *prop_
     if (strcasecmp(prop_desc[i].identifier, attrib) == 0)
     {
       /* found the identifier */
-      gpointer value = G_STRUCT_MEMBER_P(str, prop_desc[i].offset);
+      void *value = STRUCT_MEMBER_P(str, prop_desc[i].offset);
 
       /* these point to the same, but to avoid the awkward cast syntax */
       int *ivalue = value;
@@ -204,7 +201,7 @@ static boolean struct_set_property(gpointer str, const GdStructDescriptor *prop_
       for (j = 0; j < prop_desc[i].count && params[paramindex] != NULL; j++)
       {
        boolean success = FALSE;
-       gdouble res;
+       double res;
 
        switch (prop_desc[i].type)
        {
@@ -254,7 +251,8 @@ static boolean struct_set_property(gpointer str, const GdStructDescriptor *prop_
            break;
 
          case GD_TYPE_PROBABILITY:
-           res = g_ascii_strtod(params[paramindex], NULL);
+           errno = 0;  /* must be reset before calling strtod() to detect overflow/underflow */
+           res = strtod(params[paramindex], NULL);
            if (errno == 0 && res >= 0 && res <= 1)
            {
              /* fill all remaining items in array - may be only one */
@@ -268,7 +266,8 @@ static boolean struct_set_property(gpointer str, const GdStructDescriptor *prop_
            break;
 
          case GD_TYPE_RATIO:
-           res = g_ascii_strtod (params[paramindex], NULL);
+           errno = 0;  /* must be reset before calling strtod() to detect overflow/underflow */
+           res = strtod (params[paramindex], NULL);
            if (errno == 0 && res >= 0 && res <= 1)
            {
              for (k = j; k < prop_desc[i].count; k++)
@@ -356,7 +355,7 @@ static boolean replay_store_more_from_bdcff(GdReplay *replay, const char *param)
 }
 
 /* report all remaining tags; called after the above function. */
-static void replay_report_unknown_tags_func(const char *attrib, const char *param, gpointer data)
+static void replay_report_unknown_tags_func(const char *attrib, const char *param, void *data)
 {
   Warn("unknown replay tag '%s'", attrib);
 }
@@ -399,9 +398,11 @@ static void replay_process_tags(GdReplay *replay, HashTable *tags)
 static boolean cave_process_tags_func(const char *attrib, const char *param, GdCave *cave)
 {
   char **params;
+  int paramcount;
   boolean identifier_found;
 
   params = getSplitStringArray(param, " ", -1);
+  paramcount = getStringArrayLength(params);
   identifier_found = FALSE;
 
   if (strcasecmp(attrib, "SnapExplosions") == 0)
@@ -463,17 +464,65 @@ static boolean cave_process_tags_func(const char *attrib, const char *param, GdC
   else if (strcasecmp(attrib, "Colors") == 0)
   {
     /* colors attribute is a mess, have to process explicitly */
+    boolean ok = TRUE;
 
     /* Colors = [border background] foreground1 foreground2 foreground3 [amoeba slime] */
     identifier_found = TRUE;
 
-    cave->colorb = GD_GDASH_BLACK;    /* border - black */
-    cave->color0 = GD_GDASH_BLACK;    /* background - black */
-    cave->color1 = GD_GDASH_RED;
-    cave->color2 = GD_GDASH_PURPLE;
-    cave->color3 = GD_GDASH_YELLOW;
-    cave->color4 = cave->color3;    /* amoeba */
-    cave->color5 = cave->color1;    /* slime */
+    if (paramcount == 3)
+    {
+      /* only color1,2,3 */
+      cave->colorb = gd_c64_color(0);    /* border - black */
+      cave->color0 = gd_c64_color(0);    /* background - black */
+      cave->color1 = gd_color_get_from_string(params[0]);
+      cave->color2 = gd_color_get_from_string(params[1]);
+      cave->color3 = gd_color_get_from_string(params[2]);
+      cave->color4 = cave->color3;    /* amoeba */
+      cave->color5 = cave->color1;    /* slime */
+    }
+    else if (paramcount == 5)
+    {
+      /* bg,color0,1,2,3 */
+      cave->colorb = gd_color_get_from_string(params[0]);
+      cave->color0 = gd_color_get_from_string(params[1]);
+      cave->color1 = gd_color_get_from_string(params[2]);
+      cave->color2 = gd_color_get_from_string(params[3]);
+      cave->color3 = gd_color_get_from_string(params[4]);
+      cave->color4 = cave->color3;    /* amoeba */
+      cave->color5 = cave->color1;    /* slime */
+    }
+    else if (paramcount == 7)
+    {
+      /* bg,color0,1,2,3,amoeba,slime */
+      cave->colorb = gd_color_get_from_string(params[0]);
+      cave->color0 = gd_color_get_from_string(params[1]);
+      cave->color1 = gd_color_get_from_string(params[2]);
+      cave->color2 = gd_color_get_from_string(params[3]);
+      cave->color3 = gd_color_get_from_string(params[4]);
+      cave->color4 = gd_color_get_from_string(params[5]);    /* amoeba */
+      cave->color5 = gd_color_get_from_string(params[6]);    /* slime */
+    }
+    else
+    {
+      Warn("invalid number of color strings: %s", param);
+
+      ok = FALSE;
+    }
+
+    /* now check and maybe make up some new. */
+    if (!ok ||
+       gd_color_is_unknown(cave->colorb) ||
+       gd_color_is_unknown(cave->color0) ||
+       gd_color_is_unknown(cave->color1) ||
+       gd_color_is_unknown(cave->color2) ||
+       gd_color_is_unknown(cave->color3) ||
+       gd_color_is_unknown(cave->color4) ||
+       gd_color_is_unknown(cave->color5))
+    {
+      Warn("created a new C64 color scheme.");
+
+      gd_cave_set_random_c64_colors(cave);    /* just create some random */
+    }
   }
   else
   {
@@ -487,7 +536,7 @@ static boolean cave_process_tags_func(const char *attrib, const char *param, GdC
 }
 
 /* report all remaining tags; called after the above function. */
-static void cave_report_and_copy_unknown_tags_func(char *attrib, char *param, gpointer data)
+static void cave_report_and_copy_unknown_tags_func(char *attrib, char *param, void *data)
 {
   GdCave *cave = (GdCave *)data;
 
@@ -497,7 +546,7 @@ static void cave_report_and_copy_unknown_tags_func(char *attrib, char *param, gp
 }
 
 /* having read all strings belonging to the cave, process it. */
-static void cave_process_tags(GdCave *cave, HashTable *tags, GList *maplines)
+static void cave_process_tags(GdCave *cave, HashTable *tags, List *maplines)
 {
   char *value;
 
@@ -584,8 +633,8 @@ static void cave_process_tags(GdCave *cave, HashTable *tags, GList *maplines)
   /* some old bdcff files use smaller intermissions than the one specified. */
   if (maplines)
   {
-    int x, y, length = g_list_length(maplines);
-    GList *iter;
+    int x, y, length = list_length(maplines);
+    List *iter;
 
     /* create map and fill with initial border, in case that map strings are shorter or somewhat */
     cave->map = gd_cave_map_new(cave, GdElement);
@@ -642,7 +691,7 @@ boolean gd_caveset_load_from_bdcff(const char *contents)
   char **lines;
   int lineno;
   GdCave *cave;
-  GList *iter;
+  List *iter;
   boolean reading_replay = FALSE;
   boolean reading_map = FALSE;
   boolean reading_mapcodes = FALSE;
@@ -651,7 +700,7 @@ boolean gd_caveset_load_from_bdcff(const char *contents)
   boolean reading_bdcff_demo = FALSE;
   /* assume version to be 0.32, also when the file does not specify it explicitly */
   GdString version_read = "0.32";
-  GList *mapstrings = NULL;
+  List *mapstrings = NULL;
   int linenum;
   HashTable *tags, *replay_tags;
   GdObjectLevels levels = GD_OBJECT_LEVEL_ALL;
@@ -701,7 +750,7 @@ boolean gd_caveset_load_from_bdcff(const char *contents)
        if (mapstrings)
        {
          Warn("incorrect file format: new [cave] section, but already read some map lines");
-         g_list_free(mapstrings);
+         list_free(mapstrings);
          mapstrings = NULL;
        }
 
@@ -710,12 +759,12 @@ boolean gd_caveset_load_from_bdcff(const char *contents)
 
        /* ... to be able to create a copy for a new cave. */
        cave = gd_cave_new_from_cave(default_cave);
-       gd_caveset = g_list_append (gd_caveset, cave);
+       gd_caveset = list_append (gd_caveset, cave);
       }
       else if (strcasecmp(line, "[/cave]") == 0)
       {
        cave_process_tags(cave, tags, mapstrings);
-       g_list_free(mapstrings);
+       list_free(mapstrings);
        mapstrings = NULL;
 
        hashtable_foreach(tags, (hashtable_fn)cave_report_and_copy_unknown_tags_func, cave);
@@ -730,7 +779,7 @@ boolean gd_caveset_load_from_bdcff(const char *contents)
        if (mapstrings != NULL)
        {
          Warn("incorrect file format: new [map] section, but already read some map lines");
-         g_list_free(mapstrings);
+         list_free(mapstrings);
          mapstrings = NULL;
        }
       }
@@ -773,7 +822,7 @@ boolean gd_caveset_load_from_bdcff(const char *contents)
          replay = gd_replay_new();
          replay->saved = TRUE;
          replay->success = TRUE;   /* we think that it is a successful demo */
-         cave->replays = g_list_append(cave->replays, replay);
+         cave->replays = list_append(cave->replays, replay);
          gd_strcpy(replay->player_name, "???");    /* name not saved */
        }
        else
@@ -812,7 +861,7 @@ boolean gd_caveset_load_from_bdcff(const char *contents)
 
        if (replay->movements->len != 0)
        {
-         cave->replays = g_list_append(cave->replays, replay);
+         cave->replays = list_append(cave->replays, replay);
        }
        else
        {
@@ -877,14 +926,14 @@ boolean gd_caveset_load_from_bdcff(const char *contents)
     if (reading_map)
     {
       /* just append to the mapstrings list. we will process it later */
-      mapstrings = g_list_append(mapstrings, line);
+      mapstrings = list_append(mapstrings, line);
 
       continue;
     }
 
     /* strip leading and trailing spaces AFTER checking if we are reading a map.
        map lines might begin or end with spaces */
-    g_strstrip(line);
+    stripString(line);
 
     if (reading_highscore)
     {
@@ -912,13 +961,13 @@ boolean gd_caveset_load_from_bdcff(const char *contents)
     if (reading_bdcff_demo)
     {
       GdReplay *replay;
-      GList *iter;
+      List *iter;
 
       /* demo must be in [cave] section. we already showed an error message for this. */
       if (cave == default_cave)
        continue;
 
-      iter = g_list_last(cave->replays);
+      iter = list_last(cave->replays);
 
       replay = (GdReplay *)iter->data;
       replay_store_more_from_bdcff(replay, line);
@@ -934,7 +983,7 @@ boolean gd_caveset_load_from_bdcff(const char *contents)
       if (new_object)
       {
        new_object->levels = levels;    /* apply levels to new object */
-       cave->objects = g_list_append(cave->objects, new_object);
+       cave->objects = list_append(cave->objects, new_object);
       }
       else
       {
@@ -1060,7 +1109,7 @@ boolean gd_caveset_load_from_bdcff(const char *contents)
                strcasecmp(params[0], gd_cave_properties[i].identifier) == 0)
            {
              /* found identifier */
-             gpointer value = G_STRUCT_MEMBER_P (cave, gd_cave_properties[i].offset);
+             void *value = STRUCT_MEMBER_P (cave, gd_cave_properties[i].offset);
 
              *((GdElement *) value) = gd_get_element_from_string (params[1]);
              break;
@@ -1114,8 +1163,10 @@ boolean gd_caveset_load_from_bdcff(const char *contents)
            hashtable_insert(tags, getStringCopy(attrib), getStringCopy(param));
          }
          else
+         {
            /* unknown setting - report. */
            Warn("invalid attribute for [game] '%s'", attrib);
+         }
        }
        else
        {
@@ -1135,7 +1186,7 @@ boolean gd_caveset_load_from_bdcff(const char *contents)
   if (mapstrings)
   {
     Warn("incorrect file format: end of file, but still have some map lines read");
-    g_list_free(mapstrings);
+    list_free(mapstrings);
     mapstrings = NULL;
   }
 
@@ -1163,7 +1214,7 @@ boolean gd_caveset_load_from_bdcff(const char *contents)
 
   if (strEqual(version_read, "0.32"))
   {
-    GList *iter;
+    List *iter;
 
     Warn("No BDCFF version, or 0.32. Using unspecified-intermission-size hack.");
 
@@ -1194,12 +1245,12 @@ boolean gd_caveset_load_from_bdcff(const char *contents)
        object.element = cave->initial_border;
        object.fill_element = cave->initial_border;
 
-       cave->objects = g_list_prepend(cave->objects, g_memdup(&object, sizeof(object)));
+       cave->objects = list_prepend(cave->objects, get_memcpy(&object, sizeof(object)));
 
        object.x1 = 19;
        object.y1 = 0;    /* 19, as it is also the border */
 
-       cave->objects = g_list_prepend(cave->objects, g_memdup(&object, sizeof(object)));    /* another */
+       cave->objects = list_prepend(cave->objects, get_memcpy(&object, sizeof(object)));    /* another */
       }
     }
   }
@@ -1214,3 +1265,505 @@ boolean gd_caveset_load_from_bdcff(const char *contents)
   /* if there was some error message - return fail XXX */
   return TRUE;
 }
+
+/********************************************************************************
+ *
+ * BDCFF saving
+ *
+ */
+
+#define GD_PTR_ARRAY_MINIMUM_INITIAL_SIZE      64
+
+GdPtrArray *gd_ptr_array_sized_new(unsigned int size)
+{
+  GdPtrArray *array = checked_calloc(sizeof(GdPtrArray));
+
+  array->data = checked_calloc(size * sizeof(void *));
+  array->size_allocated = size;
+  array->size_initial = size;
+  array->size = 0;
+
+  return array;
+}
+
+GdPtrArray *gd_ptr_array_new(void)
+{
+  return gd_ptr_array_sized_new(GD_PTR_ARRAY_MINIMUM_INITIAL_SIZE);
+}
+
+void gd_ptr_array_add(GdPtrArray *array, void *data)
+{
+  if (array->size == array->size_allocated)
+  {
+    array->size_allocated += array->size_initial;
+    array->data = checked_realloc(array->data, array->size_allocated * sizeof(void *));
+  }
+
+  array->data[array->size++] = data;
+}
+
+boolean gd_ptr_array_remove(GdPtrArray *array, void *data)
+{
+  int i, j;
+
+  for (i = 0; i < array->size; i++)
+  {
+    if (array->data[i] == data)
+    {
+      checked_free(array->data[i]);
+
+      for (j = i; j < array->size - 1; j++)
+       array->data[j] = array->data[j + 1];
+
+      array->size--;
+
+      return TRUE;
+    }
+  }
+
+  return FALSE;
+}
+
+void gd_ptr_array_free(GdPtrArray *array, boolean free_data)
+{
+  int i;
+
+  if (free_data)
+  {
+    for (i = 0; i < array->size; i++)
+      checked_free(array->data[i]);
+  }
+
+  checked_free(array);
+}
+
+/* ratio: max cave size for GD_TYPE_RATIO. should be set to cave->w*cave->h when calling */
+static void save_properties(GdPtrArray *out, void *str, void *str_def,
+                           const GdStructDescriptor *prop_desc, int ratio)
+{
+  int i, j;
+  boolean parameter_written = FALSE, should_write = FALSE;
+  char *line = NULL;
+  const char *identifier = NULL;
+
+  for (i = 0; prop_desc[i].identifier != NULL; i++)
+  {
+    void *value, *default_value;
+
+    if (prop_desc[i].type == GD_TAB || prop_desc[i].type == GD_LABEL)
+      /* used only by the gui */
+      continue;
+
+    /* these are handled explicitly */
+    if (prop_desc[i].flags & GD_DONT_SAVE)
+      continue;
+
+    /* string data */
+    /* write together with identifier, as one string per line. */
+    if (prop_desc[i].type == GD_TYPE_STRING)
+    {
+      /* treat strings as special - do not even write identifier if no string. */
+      char *text = STRUCT_MEMBER_P(str, prop_desc[i].offset);
+
+      if (strlen(text) > 0)
+       gd_ptr_array_add(out, getStringPrint("%s=%s", prop_desc[i].identifier, text));
+
+      continue;
+    }
+
+    /* dynamic string: need to escape newlines */
+    if (prop_desc[i].type == GD_TYPE_LONGSTRING)
+    {
+      char *string = STRUCT_MEMBER(char *, str, prop_desc[i].offset);
+
+      if (string != NULL && strlen(string) > 0)
+      {
+       char *escaped = getEscapedString(string);
+
+       gd_ptr_array_add(out, getStringPrint("%s=%s", prop_desc[i].identifier, escaped));
+
+       checked_free(escaped);
+      }
+
+      continue;
+    }
+
+    /* if identifier differs from the previous, write out the line collected, and start a new one */
+    if (identifier == NULL || !strEqual(prop_desc[i].identifier, identifier))
+    {
+      if (should_write)
+      {
+       /* write lines only which carry information other than the default settings */
+       gd_ptr_array_add(out, getStringCopy(line));
+      }
+
+      if (prop_desc[i].type == GD_TYPE_EFFECT)
+       setStringPrint(&line, "Effect=");
+      else
+       setStringPrint(&line, "%s=", prop_desc[i].identifier);
+
+      parameter_written = FALSE;    /* no value written yet */
+      should_write = FALSE;
+
+      /* remember identifier */
+      identifier = prop_desc[i].identifier;
+    }
+
+    /* if we always save this identifier, remember now */
+    if (prop_desc[i].flags & GD_ALWAYS_SAVE)
+      should_write = TRUE;
+
+    value         = STRUCT_MEMBER_P(str,     prop_desc[i].offset);
+    default_value = STRUCT_MEMBER_P(str_def, prop_desc[i].offset);
+
+    for (j = 0; j < prop_desc[i].count; j++)
+    {
+      /* separate values by spaces. of course no space required for the first one */
+      if (parameter_written)
+       appendStringPrint(&line, " ");
+
+      parameter_written = TRUE;    /* at least one value written, so write space the next time */
+
+      switch (prop_desc[i].type)
+      {
+       case GD_TYPE_BOOLEAN:
+         appendStringPrint(&line, "%s", ((boolean *) value)[j] ? "true" : "false");
+         if (((boolean *) value)[j] != ((boolean *) default_value)[j])
+           should_write = TRUE;
+         break;
+
+       case GD_TYPE_INT:
+         appendStringPrint(&line, "%d", ((int *) value)[j]);
+         if (((int *) value)[j] != ((int *) default_value)[j])
+           should_write = TRUE;
+         break;
+
+       case GD_TYPE_RATIO:
+         appendStringPrint(&line, "%6.5f", ((int *) value)[j] / (double)ratio);
+         if (((int *) value)[j] != ((int *) default_value)[j])
+           should_write = TRUE;
+         break;
+
+       case GD_TYPE_PROBABILITY:
+         appendStringPrint(&line, "%6.5f", ((int *) value)[j] / 1E6);   /* probabilities are stored as *1E6 */
+
+         if (((double *) value)[j] != ((double *) default_value)[j])
+           should_write = TRUE;
+         break;
+
+       case GD_TYPE_ELEMENT:
+         appendStringPrint(&line, "%s", gd_elements[((GdElement *) value)[j]].filename);
+         if (((GdElement *) value)[j] != ((GdElement *) default_value)[j])
+           should_write = TRUE;
+         break;
+
+       case GD_TYPE_EFFECT:
+         /* for effects, the property identifier is the effect name. "Effect=" is hardcoded; see above. */
+         appendStringPrint(&line, "%s %s", prop_desc[i].identifier, gd_elements[((GdElement *) value)[j]].filename);
+         if (((GdElement *) value)[j] != ((GdElement *) default_value)[j])
+           should_write = TRUE;
+         break;
+
+       case GD_TYPE_COLOR:
+         appendStringPrint(&line, "%s", gd_color_get_string(((GdColor *) value)[j]));
+         should_write = TRUE;
+         break;
+
+       case GD_TYPE_DIRECTION:
+         appendStringPrint(&line, "%s", gd_direction_get_filename(((GdDirection *) value)[j]));
+         if (((GdDirection *) value)[j] != ((GdDirection *) default_value)[j])
+           should_write = TRUE;
+         break;
+
+       case GD_TYPE_SCHEDULING:
+         appendStringPrint(&line, "%s", gd_scheduling_get_filename(((GdScheduling *) value)[j]));
+         if (((GdScheduling *) value)[j] != ((GdScheduling *) default_value)[j])
+           should_write = TRUE;
+         break;
+
+       case GD_TAB:
+       case GD_LABEL:
+         /* used by the editor ui */
+         break;
+
+       case GD_TYPE_STRING:
+       case GD_TYPE_LONGSTRING:
+         /* never reached */
+         break;
+      }
+    }
+  }
+
+  /* write remaining data */
+  if (should_write)
+    gd_ptr_array_add(out, getStringCopy(line));
+
+  checked_free(line);
+}
+
+/* remove a line from the list of strings. */
+/* the prefix should be a property; add an equal sign! so properties which have names like
+   "slime" and "slimeproperties" won't match each other. */
+static void cave_properties_remove(GdPtrArray *out, const char *prefix)
+{
+  int i;
+
+  if (!strSuffix(prefix, "="))
+    Warn("string '%s' should have suffix '='", prefix);
+
+  /* search for strings which match, and set them to NULL. */
+  /* also free them. */
+  for (i = 0; i < out->size; i++)
+  {
+    if (strPrefix(gd_ptr_array_index(out, i), prefix))
+    {
+      checked_free(gd_ptr_array_index(out, i));
+      gd_ptr_array_index(out, i) = NULL;
+    }
+  }
+
+  /* remove all "null" occurrences */
+  while (gd_ptr_array_remove(out, NULL))
+    ;
+}
+
+/* output properties of a structure to a file. */
+/* list_foreach func, so "out" is the last parameter! */
+static void caveset_save_cave_func(GdCave *cave, GdPtrArray *out)
+{
+  GdCave *default_cave;
+  GdPtrArray *this_out;
+  char *line = NULL;    /* used for various purposes */
+  int i;
+
+  gd_ptr_array_add(out, getStringCopy(""));
+  gd_ptr_array_add(out, getStringCopy("[cave]"));
+
+  /* first add the properties to a local ptr array. */
+  /* later, some are deleted (slime permeability, for example) - this is needed because of the inconsistencies of the bdcff. */
+  /* finally, remaining will be added to the normal "out" array. */
+  this_out = gd_ptr_array_new();
+
+  default_cave = gd_cave_new();
+  save_properties(this_out, cave, default_cave, gd_cave_properties, cave->w * cave->h);
+  gd_cave_free(default_cave);
+
+  /* properties which are handled explicitly. these cannot be handled easily above,
+     as they have some special meaning. for example, slime_permeability=x sets permeability to
+     x, and sets predictable to false. bdcff format is simply inconsistent in these aspects. */
+
+  /* slime permeability is always set explicitly, as it also sets predictability. */
+  if (cave->slime_predictable)
+    /* if slime is predictable, remove permeab. flag, as that would imply unpredictable slime. */
+    cave_properties_remove(this_out, "SlimePermeability=");
+  else
+    /* if slime is UNpredictable, remove permeabc64 flag, as that would imply predictable slime. */
+    cave_properties_remove(this_out, "SlimePermeabilityC64=");
+
+  /* add tags to output, and free local array */
+  for (i = 0; i < this_out->size; i++)
+    gd_ptr_array_add(out, gd_ptr_array_index(this_out, i));
+
+  /* do not free data pointers, which were just added to array "out" */
+  gd_ptr_array_free(this_out, FALSE);
+
+#if 0
+  /* save unknown tags as they are */
+  if (cave->tags)
+  {
+    List *hashkeys;
+    List *iter;
+
+    hashkeys = g_hash_table_get_keys(cave->tags);
+    for (iter = hashkeys; iter != NULL; iter = iter->next)
+    {
+      gchar *key = (gchar *)iter->data;
+
+      gd_ptr_array_add(out, getStringPrint("%s=%s", key, (const char *) g_hash_table_lookup(cave->tags, key)));
+    }
+
+    list_free(hashkeys);
+  }
+#endif
+
+  /* map */
+  if (cave->map)
+  {
+    int x, y;
+
+    gd_ptr_array_add(out, getStringCopy(""));
+    gd_ptr_array_add(out, getStringCopy("[map]"));
+
+    line = checked_calloc(cave->w + 1);
+
+    /* save map */
+    for (y = 0; y < cave->h; y++)
+    {
+      for (x = 0; x < cave->w; x++)
+      {
+       /* check if character is non-zero; the ...save() should have assigned a character to every element */
+       if (gd_elements[cave->map[y][x]].character_new == 0)
+         Warn("gd_elements[cave->map[y][x]].character_new should be non-zero");
+
+       line[x] = gd_elements[cave->map[y][x]].character_new;
+      }
+
+      gd_ptr_array_add(out, getStringCopy(line));
+    }
+
+    gd_ptr_array_add(out, getStringCopy("[/map]"));
+  }
+
+  /* save drawing objects */
+  if (cave->objects)
+  {
+    List *listiter;
+
+    gd_ptr_array_add(out, getStringCopy(""));
+    gd_ptr_array_add(out, getStringCopy("[objects]"));
+
+    for (listiter = cave->objects; listiter; listiter = list_next(listiter))
+    {
+      GdObject *object = listiter->data;
+      char *text;
+
+      /* not for all levels? */
+      if (object->levels != GD_OBJECT_LEVEL_ALL)
+      {
+       int i;
+       boolean once;    /* true if already written one number */
+
+       setStringPrint(&line, "[Level=");
+       once = FALSE;
+
+       for (i = 0; i < 5; i++)
+       {
+         if (object->levels & gd_levels_mask[i])
+         {
+           if (once)    /* if written at least one number so far, we need a comma */
+             appendStringPrint(&line, ",");
+
+           appendStringPrint(&line, "%d", i+1);
+           once = TRUE;
+         }
+       }
+
+       appendStringPrint(&line, "]");
+       gd_ptr_array_add(out, getStringCopy(line));
+      }
+
+      text = gd_object_get_bdcff(object);
+      gd_ptr_array_add(out, getStringCopy(text));
+      checked_free(text);
+
+      if (object->levels != GD_OBJECT_LEVEL_ALL)
+       gd_ptr_array_add(out, getStringCopy("[/Level]"));
+    }
+
+    gd_ptr_array_add(out, getStringCopy("[/objects]"));
+  }
+
+  gd_ptr_array_add(out, getStringCopy("[/cave]"));
+
+  checked_free(line);
+}
+
+/* save cave in bdcff format. */
+/* "out" will be added lines of bdcff description. */
+GdPtrArray *gd_caveset_save_to_bdcff(void)
+{
+  GdPtrArray *out = gd_ptr_array_sized_new(512);
+  GdCavesetData *default_caveset;
+  boolean write_mapcodes = FALSE;
+  List *iter;
+  int i;
+
+  /* check if we need an own mapcode table ------ */
+  /* copy original characters to character_new fields; new elements will be added to that one */
+  for (i = 0; i < O_MAX; i++)
+    gd_elements[i].character_new = gd_elements[i].character;
+
+  /* also regenerate this table as we use it */
+  gd_create_char_to_element_table();
+
+  /* check all caves */
+  for (iter = gd_caveset; iter != NULL; iter = iter->next)
+  {
+    GdCave *cave = (GdCave *)iter->data;
+
+    /* if they have a map (random elements+object based maps do not need characters) */
+    if (cave->map)
+    {
+      int x, y;
+
+      /* check every element of map */
+      for(y = 0; y < cave->h; y++)
+       for (x = 0; x < cave->w; x++)
+       {
+         GdElement e = cave->map[y][x];
+
+         /* if no character assigned */
+         if (gd_elements[e].character_new == 0)
+         {
+           int j;
+
+           /* we found at least one, so later we have to write the mapcodes */
+           write_mapcodes = TRUE;
+
+           /* find a character which is not yet used for any element */
+           for (j = 32; j < 128; j++)
+           {
+             /* the string contains the characters which should not be used. */
+             if (strchr("<>&[]/=\\", j) == NULL && gd_char_to_element[j] == O_UNKNOWN)
+               break;
+           }
+
+           /* if no more space... XXX we should rather report to the user */
+           if (j == 128)
+             Warn("variable j should be != 128");
+
+           gd_elements[e].character_new = j;
+
+           /* we also record to this table, as we use it ^^ a few lines above */
+           gd_char_to_element[j] = e;
+         }
+       }
+    }
+  }
+
+  gd_ptr_array_add(out, getStringCopy("[BDCFF]"));
+  gd_ptr_array_add(out, getStringPrint("Version=%s", BDCFF_VERSION));
+
+  /* this flag was set above if we need to write mapcodes */
+  if (write_mapcodes)
+  {
+    int i;
+
+    gd_ptr_array_add(out, getStringCopy("[mapcodes]"));
+    gd_ptr_array_add(out, getStringCopy("Length=1"));
+
+    for (i = 0; i < O_MAX; i++)
+    {
+      /* if no character assigned by specification BUT (AND) we assigned one */
+      if (gd_elements[i].character == 0 && gd_elements[i].character_new != 0)
+       gd_ptr_array_add(out, getStringPrint("%c=%s", gd_elements[i].character_new, gd_elements[i].filename));
+    }
+
+    gd_ptr_array_add(out, getStringCopy("[/mapcodes]"));
+  }
+
+  gd_ptr_array_add(out, getStringCopy("[game]"));
+
+  default_caveset = gd_caveset_data_new();
+  save_properties(out, gd_caveset_data, default_caveset, gd_caveset_properties, 0);
+  gd_caveset_data_free(default_caveset);
+  gd_ptr_array_add(out, getStringCopy("Levels=5"));
+
+  list_foreach(gd_caveset, (list_fn)caveset_save_cave_func, out);
+
+  gd_ptr_array_add(out, getStringCopy("[/game]"));
+  gd_ptr_array_add(out, getStringCopy("[/BDCFF]"));
+
+  /* saved to ptrarray */
+  return out;
+}