From ed7060de23a6fd0c296d97717ae3b703efa410fc Mon Sep 17 00:00:00 2001 From: Holger Schemel Date: Tue, 27 Feb 2024 21:26:13 +0100 Subject: [PATCH] added saving BDCFF files for BD engine --- src/game_bd/bd_bdcff.c | 502 ++++++++++++++++++++++++++++++++++++ src/game_bd/bd_bdcff.h | 16 ++ src/game_bd/bd_caveobject.c | 85 ++++++ src/game_bd/bd_caveobject.h | 1 + src/game_bd/bd_caveset.c | 35 +++ src/game_bd/bd_caveset.h | 2 +- 6 files changed, 640 insertions(+), 1 deletion(-) diff --git a/src/game_bd/bd_bdcff.c b/src/game_bd/bd_bdcff.c index 07c6cd61..31fab6e8 100644 --- a/src/game_bd/bd_bdcff.c +++ b/src/game_bd/bd_bdcff.c @@ -1265,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; +} diff --git a/src/game_bd/bd_bdcff.h b/src/game_bd/bd_bdcff.h index d5b73c42..c12c875e 100644 --- a/src/game_bd/bd_bdcff.h +++ b/src/game_bd/bd_bdcff.h @@ -17,6 +17,22 @@ #ifndef BD_BDCFF_H #define BD_BDCFF_H +typedef struct _gd_ptr_array +{ + void **data; + unsigned int size; + unsigned int size_initial; + unsigned int size_allocated; +} GdPtrArray; + +GdPtrArray *gd_ptr_array_sized_new(unsigned int size); +GdPtrArray *gd_ptr_array_new(void); +void gd_ptr_array_add(GdPtrArray *array, void *data); +boolean gd_ptr_array_remove(GdPtrArray *array, void *data); +void gd_ptr_array_free(GdPtrArray *array, boolean free_data); +#define gd_ptr_array_index(array, index) ((array)->data)[index] + boolean gd_caveset_load_from_bdcff(const char *contents); +GdPtrArray *gd_caveset_save_to_bdcff(void); #endif // BD_BDCFF_H diff --git a/src/game_bd/bd_caveobject.c b/src/game_bd/bd_caveobject.c index 57bc963c..b9c09425 100644 --- a/src/game_bd/bd_caveobject.c +++ b/src/game_bd/bd_caveobject.c @@ -26,6 +26,91 @@ GdObjectLevels gd_levels_mask[] = GD_OBJECT_LEVEL5 }; +/* bdcff text description of object. caller should free string. */ +char *gd_object_get_bdcff(const GdObject *object) +{ + char *str = NULL; + int j; + const char *type; + + switch (object->type) + { + case GD_POINT: + return getStringPrint("Point=%d %d %s", object->x1, object->y1, gd_elements[object->element].filename); + + case GD_LINE: + return getStringPrint("Line=%d %d %d %d %s", object->x1, object->y1, object->x2, object->y2, gd_elements[object->element].filename); + + case GD_RECTANGLE: + return getStringPrint("Rectangle=%d %d %d %d %s", object->x1, object->y1, object->x2, object->y2, gd_elements[object->element].filename); + + case GD_FILLED_RECTANGLE: + /* if elements are not the same */ + if (object->fill_element!=object->element) + return getStringPrint("FillRect=%d %d %d %d %s %s", object->x1, object->y1, object->x2, object->y2, gd_elements[object->element].filename, gd_elements[object->fill_element].filename); + /* they are the same */ + return getStringPrint("FillRect=%d %d %d %d %s", object->x1, object->y1, object->x2, object->y2, gd_elements[object->element].filename); + + case GD_RASTER: + return getStringPrint("Raster=%d %d %d %d %d %d %s", object->x1, object->y1, (object->x2-object->x1)/object->dx+1, (object->y2-object->y1)/object->dy+1, object->dx, object->dy, gd_elements[object->element].filename); + + case GD_JOIN: + return getStringPrint("Add=%d %d %s %s", object->dx, object->dy, gd_elements[object->element].filename, gd_elements[object->fill_element].filename); + + case GD_FLOODFILL_BORDER: + return getStringPrint("BoundaryFill=%d %d %s %s", object->x1, object->y1, gd_elements[object->fill_element].filename, gd_elements[object->element].filename); + + case GD_FLOODFILL_REPLACE: + return getStringPrint("FloodFill=%d %d %s %s", object->x1, object->y1, gd_elements[object->fill_element].filename, gd_elements[object->element].filename); + + case GD_MAZE: + case GD_MAZE_UNICURSAL: + case GD_MAZE_BRAID: + switch(object->type) + { + case GD_MAZE: + type = "perfect"; + break; + + case GD_MAZE_UNICURSAL: + type = "unicursal"; + break; + + case GD_MAZE_BRAID: + type = "braid"; + break; + + default: + /* never reached */ + break; + } + + return getStringPrint("Maze=%d %d %d %d %d %d %d %d %d %d %d %d %s %s %s", object->x1, object->y1, object->x2, object->y2, object->dx, object->dy, object->horiz, object->seed[0], object->seed[1], object->seed[2], object->seed[3], object->seed[4], gd_elements[object->element].filename, gd_elements[object->fill_element].filename, type); + + case GD_RANDOM_FILL: + appendStringPrint(&str, "%s=%d %d %d %d %d %d %d %d %d %s", object->c64_random?"RandomFillC64":"RandomFill", object->x1, object->y1, object->x2, object->y2, object->seed[0], object->seed[1], object->seed[2], object->seed[3], object->seed[4], gd_elements[object->fill_element].filename); + + /* seed and initial fill */ + for (j = 0; j < 4; j++) + if (object->random_fill_probability[j] != 0) + appendStringPrint(&str, " %s %d", gd_elements[object->random_fill[j]].filename, object->random_fill_probability[j]); + + if (object->element != O_NONE) + appendStringPrint(&str, " %s", gd_elements[object->element].filename); + + return str; + + case GD_COPY_PASTE: + return getStringPrint("CopyPaste=%d %d %d %d %d %d %s %s", object->x1, object->y1, object->x2, object->y2, object->dx, object->dy, object->mirror?"mirror":"nomirror", object->flip?"flip":"noflip"); + + case NONE: + /* never reached */ + break; + } + + return NULL; +} + /* create an INDIVIDUAL POINT CAVE OBJECT */ GdObject *gd_object_new_point(GdObjectLevels levels, int x, int y, GdElement elem) { diff --git a/src/game_bd/bd_caveobject.h b/src/game_bd/bd_caveobject.h index f52503ce..8adc02cb 100644 --- a/src/game_bd/bd_caveobject.h +++ b/src/game_bd/bd_caveobject.h @@ -91,6 +91,7 @@ GdObject *gd_object_new_random_fill(GdObjectLevels levels, int x1, int y1, int x GdObject *gd_object_new_copy_paste(GdObjectLevels levels, int x1, int y1, int x2, int y2, int dx, int dy, boolean mirror, boolean flip); void gd_cave_draw_object(GdCave *cave, const GdObject *object, int level); +char *gd_object_get_bdcff(const GdObject *object); GdObject *gd_object_new_from_string(char *str); GdCave *gd_cave_new_rendered(const GdCave *data, const int level, unsigned int seed); diff --git a/src/game_bd/bd_caveset.c b/src/game_bd/bd_caveset.c index 0e04806b..cce3692d 100644 --- a/src/game_bd/bd_caveset.c +++ b/src/game_bd/bd_caveset.c @@ -611,6 +611,41 @@ boolean gd_caveset_load_from_file(char *filename) return TRUE; } +boolean gd_caveset_save_to_file(const char *filename) +{ + GdPtrArray *saved = gd_caveset_save_to_bdcff(); + boolean success; + File *file; + int i; + + if ((file = openFile(filename, MODE_WRITE)) != NULL) + { + for (i = 0; i < saved->size; i++) + { + writeFile(file, saved->data[i], 1, strlen(saved->data[i])); + writeFile(file, "\n", 1, 1); + } + + closeFile(file); + + /* remember that it is saved */ + gd_caveset_edited = FALSE; + + success = TRUE; + } + else + { + Warn("cannot open file '%s'", filename); + + success = FALSE; + } + + gd_ptr_array_free(saved, TRUE); + + return success; +} + + int gd_cave_check_replays(GdCave *cave, boolean report, boolean remove, boolean repair) { List *riter; diff --git a/src/game_bd/bd_caveset.h b/src/game_bd/bd_caveset.h index 95bb7891..79dc545f 100644 --- a/src/game_bd/bd_caveset.h +++ b/src/game_bd/bd_caveset.h @@ -64,7 +64,7 @@ const char **gd_caveset_get_internal_game_names(void); /* caveset load from file */ boolean gd_caveset_load_from_file(char *filename); /* caveset save to bdcff file */ -boolean gd_caveset_save(const char *filename); +boolean gd_caveset_save_to_file(const char *filename); /* misc caveset functions */ int gd_caveset_count(void); -- 2.34.1