diff --git a/include/wdl/load_wdz_internal.h b/include/wdl/load_wdz_internal.h index 7d8a9b98cc..0789528a0d 100644 --- a/include/wdl/load_wdz_internal.h +++ b/include/wdl/load_wdz_internal.h @@ -8,6 +8,8 @@ #include "load_wdz.h" // Re-export the public header for internal use as well +#define DEFAULT_PLAYER_OBJ_ID "player" + /* * filename_extension_is: Checks if a filename string has a certain extension * diff --git a/src/wdl/src/attributes.c b/src/wdl/src/attributes.c index 9c5dca210b..60c4fe933a 100644 --- a/src/wdl/src/attributes.c +++ b/src/wdl/src/attributes.c @@ -98,6 +98,7 @@ int free_attr(obj_attr_t *head, obj_attr_t *a) return FAILURE; } DL_DELETE(head, a); + free(a->data); free(a); return SUCCESS; } diff --git a/src/wdl/src/load_wdz_common.c b/src/wdl/src/load_wdz_common.c index d6b917ae19..ab8f0df031 100644 --- a/src/wdl/src/load_wdz_common.c +++ b/src/wdl/src/load_wdz_common.c @@ -5,14 +5,259 @@ #include "wdl/load_wdz_internal.h" +// Helper macros for the json name -> game object type conversion -object_t *convert_j_obj_to_game_obj(json_object *j_game_obj, char *j_name) +#define SAME_STRING(s1, s2) (strcmp(s1, s2) == (0)) +#define TYPE_PLAYER_STR "player" +#define TYPE_ROOM_STR "rooms" +#define TYPE_ITEM_STR "items" +#define TYPE_ACTION_STR "actions" +#define TYPE_GCONDITION_STR "globalconditions" +#define TYPE_DIALOG_STR "dialog" +#define TYPE_NPC_STR "npcs" +#define TYPE_CUSTOM_SCRIPT_STR "customscripts" + +/* + * match_j_name_to_game_obj_type: + * Converts a raw json filename/top-level key name into + * the corresponding objtype_t enum value. + * + * Parameters: + * - j_name: A string that is the raw name + * + * Returns: + * - An objtype_t value corresponding to that name + */ +objtype_t match_j_name_to_game_obj_type(char *j_name) { - // Not implemented yet. This is in progress inside branch wdl/parsing-wdz-namanh - // and will hopefully get resolved. + objtype_t game_obj_type; + if (SAME_STRING(j_name, TYPE_PLAYER_STR)) + { + game_obj_type = TYPE_PLAYER; + } + else if (SAME_STRING(j_name, TYPE_ROOM_STR)) + { + game_obj_type = TYPE_ROOM; + } + else if (SAME_STRING(j_name, TYPE_ITEM_STR)) + { + game_obj_type = TYPE_ITEM; + } + else if (SAME_STRING(j_name, TYPE_ACTION_STR)) + { + game_obj_type = TYPE_ACTION; + } + else if (SAME_STRING(j_name, TYPE_GCONDITION_STR)) + { + game_obj_type = TYPE_GCONDITION; + } + else if (SAME_STRING(j_name, TYPE_NPC_STR)) + { + game_obj_type = TYPE_NPC; + } + else if (SAME_STRING(j_name, TYPE_DIALOG_STR)) + { + game_obj_type = TYPE_DIALOG; + } + else if (SAME_STRING(j_name, TYPE_CUSTOM_SCRIPT_STR)) + { + game_obj_type = TYPE_CUSTOM_SCRIPT; + } + else if (strlen(j_name) == 0) + { + game_obj_type = TYPE_NONE; + } + else + { + game_obj_type = TYPE_ERR; + } + + return game_obj_type; +} + +/* + * make_data_from_j_value + * Recursively converts every JSON object that isn't formatted like a proper + * game object. This works on nested JSON objects and arrays, too. + * Serves to make the void ptr data field for attributes. + * WARNING: This mallocs the data, so they must later be freed. + * + * Parameters: + * - j_value: the json_object to be converted + * + * Returns: + * - a void pointer pointing to allocated memory containing the desired data + */ +void *make_data_from_j_value(json_object *j_value) +{ + json_type j_type = json_object_get_type(j_value); + switch (j_type) + { + case json_type_boolean: + { + json_bool *val = malloc(sizeof(*val)); + *val = json_object_get_boolean(j_value); + return (void*)val; + } + case json_type_double: + { + double *val = malloc(sizeof(*val)); + *val = json_object_get_double(j_value); + return (void*)val; + } + case json_type_int: + { + int *val = malloc(sizeof(*val)); + *val = json_object_get_int(j_value); + return (void*)val; + } + case json_type_string: + { + int str_len = json_object_get_string_len(j_value); + char *val = calloc(str_len + 1, sizeof(*val)); + + strcpy(val, json_object_get_string(j_value)); + return (void*)val; + } + case json_type_null: + { + void *val = NULL; + return (void*)val; + } + /* the recursive converters */ + case json_type_object: + { + object_t *val = new_object("", TYPE_NONE); + json_object_iter j_iterator; + json_object_object_foreachC(j_value, j_iterator) + { + char* attr_name = j_iterator.key; + json_object *attr_val = j_iterator.val; + + add_attribute(&(val->attrs), attr_name, + make_data_from_j_value(attr_val)); + } + return (void*)val; + + } + case json_type_array: + { + int arr_len = json_object_array_length(j_value); + if (arr_len == 0) + { + void *val = NULL; + } + object_t *val = new_object("", TYPE_NONE); + for (int i = 0; i < arr_len; i++) + { + /* var->attrs is the head element */ + json_object *arr_elt = json_object_array_get_idx(j_value, i); + void *raw_data = make_data_from_j_value(arr_elt); + assert(raw_data != NULL); + append_attr(val->attrs, raw_data); + } + return (void*)val; + } + default: + { + void *val = NULL; + return (void*)val; + } + } return NULL; } +/* convert_j_obj_to_game_obj + * Converts a JSON object of the form + * { + * "key_name":[ INDIVIDUAL NESTED OBJECTS HERE ] + * } + * + * or + * { + * "key_name":{ ATTRIBS HERE } + * } + * + * into the right internal object_t representation. + * + * Parameters: + * - j_game_obj: The JSON object to convert + * - j_name: The name of the top-level container of j_game_obj. E.g. "rooms" + * (NOT the id of j_game_obj itself!) + * This is also the filename of the JSON file that contains this JSON. + * Returns: + * - A pointer to the resulting object_t + */ +object_t *convert_j_obj_to_game_obj(json_object *j_game_obj, char *j_name) +{ + /* First find game object type (e.g. a room, or an item) */ + objtype_t game_obj_type = match_j_name_to_game_obj_type(j_name); + + /* Then find the game object's ID */ + char game_obj_id[MAXLEN_ID]; + strncpy(game_obj_id, "", MAXLEN_ID); + json_object *game_obj_id_j_obj = NULL; + json_object_object_get_ex(j_game_obj, "id", &game_obj_id_j_obj); + if (!game_obj_id_j_obj) + { + /* Player object is a one-off obj that doesn't need an id field + * the Other object types also should not require an id field + */ + if (game_obj_type == TYPE_PLAYER) + { + strncpy(game_obj_id, DEFAULT_PLAYER_OBJ_ID, MAXLEN_ID); + } + else if ((game_obj_type != TYPE_NONE) + && (game_obj_type != TYPE_ERR)) + { + fprintf(stderr, + "No id key found in the JSON of this game object!\n"); + return NULL; + } + } + if (json_object_is_type(game_obj_id_j_obj, json_type_string)) + { + strncpy(game_obj_id, json_object_get_string(game_obj_id_j_obj), MAXLEN_ID); + } + else + { + if ((game_obj_type != TYPE_PLAYER) + && (game_obj_type != TYPE_NONE) + && (game_obj_type != TYPE_ERR)) + { + fprintf(stderr, + "id key in the JSON of this game object has wrong value type (must be string!)\n"); + return NULL; + } + } + + /* Once type and id are obtained, create the game object */ + object_t *game_obj = new_object(game_obj_id, game_obj_type); + + if (!game_obj) + { + fprintf(stderr, "Unable to allocate memory for game object\n"); + return NULL; + } + + /* Loops through all attributes in the JSON object */ + json_object_iter j_iterator; + json_object_object_foreachC(j_game_obj, j_iterator) + { + char *attr_name = j_iterator.key; + json_object *j_value = j_iterator.val; + if (SAME_STRING(attr_name, "id")) + { + continue; // we already added the ID above + } + else + { + add_attribute(&(game_obj->attrs), attr_name, make_data_from_j_value(j_value)); + } + } + return game_obj; +} + /* See load_wdz_internal.h */ bool filename_extension_is(const char *ext, const char *str) @@ -37,32 +282,61 @@ int load_game_objects_from_json_object return FAILURE; } - json_object_object_foreach(j_obj, j_name, j_value) + json_object_iter j_iterator; + json_object_object_foreachC(j_obj, j_iterator) { + char *j_name = j_iterator.key; + json_object *j_value = j_iterator.val; if (json_object_is_type(j_value, json_type_array)) { int n_objects = json_object_array_length(j_value); + int omitted = 0; + int added = 0; + array_list *j_array = json_object_get_array(j_value); + for (int i = 0; i < n_objects; i++) // for each json_object in the array { - json_object *j_game_obj = json_object_array_get_idx(j_value, i); + /* Using internal array_list because the public interface + * functions cause weird segfault on _get_idx + */ + json_object *j_game_obj = array_list_get_idx(j_array, i); object_t *game_obj = convert_j_obj_to_game_obj(j_game_obj, j_name); if (!game_obj) { - return FAILURE; + fprintf(stderr, "Couldn't convert json object %s[%d] into game object\n", j_name, i); + omitted++; + continue; + } + if (add_objstore(obj_store, game_obj) == FAILURE) + { + omitted++; + } + else + { + added++; } - add_objstore(obj_store, game_obj); } - return SUCCESS; + + if (omitted > 0) + { + fprintf(stderr, "Omitted import of %d game objects\n", omitted); + } + if (added > 0) + { + return SUCCESS; + } + else + { + return FAILURE; + } } else if (json_object_is_type(j_value, json_type_object)) { /* The only file with an object-type value as top-level * is players.json. Other special cases can go here, but unlikely. */ - printf("Found player object.\n"); - json_object *j_player_obj; - json_object_object_get_ex(j_value, j_name, &j_player_obj); - object_t *player = convert_j_obj_to_game_obj(j_player_obj, j_name); + object_t *player = convert_j_obj_to_game_obj(j_value, j_name); + if (!player) { return FAILURE; @@ -75,7 +349,7 @@ int load_game_objects_from_json_object return FAILURE; } } - // If no return yet by now then it failed. + /* If no return yet by now then it failed. */ return FAILURE; } diff --git a/src/wdl/src/load_wdz_lzip.c b/src/wdl/src/load_wdz_lzip.c index c25ce8f024..aa5f8128f8 100644 --- a/src/wdl/src/load_wdz_lzip.c +++ b/src/wdl/src/load_wdz_lzip.c @@ -16,6 +16,8 @@ #include "wdl/load_wdz_internal.h" +#define GAME_DIR "game/" + /* Maximum buffer size for json file, in bytes. * This is currently set to 2 MiB. */ @@ -105,6 +107,13 @@ int populate_objstore_from_wdz zip_file_t *curr_file = zip_fopen_index(wdz, i, 0); { // Within the context of this opened entry... const char *j_path_and_name = zip_get_name(wdz, i, 0); + + // check for actual correct path game_name/game/ before reading + if (strncmp(strchr(j_path_and_name, '/') + 1, GAME_DIR, strlen(GAME_DIR))) + { + continue; + } + struct json_object *j_obj = get_json_obj_from_zip_file_entry(curr_file, j_path_and_name); diff --git a/tests/wdl/test_attributes.c b/tests/wdl/test_attributes.c index 69ab9e56a8..1562f43b3f 100644 --- a/tests/wdl/test_attributes.c +++ b/tests/wdl/test_attributes.c @@ -158,13 +158,20 @@ Test(attributes, count) cr_assert_eq(res, 2, "count_attr_list() failed"); } +#define MAX_TEST_DATA_LENGTH 20 + Test(attributes, free) { - obj_attr_t *item1 = new_attr("class", "adventurer"); + char *data1, *data2; + data1 = calloc(MAX_TEST_DATA_LENGTH, sizeof(*data1)); + data2 = calloc(MAX_TEST_DATA_LENGTH, sizeof(*data2)); + strncpy(data1, "adventurer", MAX_TEST_DATA_LENGTH); + strncpy(data2, "stick", MAX_TEST_DATA_LENGTH); + obj_attr_t *item1 = new_attr("class", data1); cr_assert_not_null(item1, "new_attr() failed to init & alloc attr"); obj_attr_t *head = init_attr_list(item1); - obj_attr_t *item2 = new_attr("weapon", "stick"); + obj_attr_t *item2 = new_attr("weapon", data2); append_attr(head, item2); int res = free_attr(head, item2); diff --git a/tests/wdl/test_wdz.c b/tests/wdl/test_wdz.c index 4f6d59de89..b3c736c4fc 100644 --- a/tests/wdl/test_wdz.c +++ b/tests/wdl/test_wdz.c @@ -31,7 +31,95 @@ Test(load_wdz, file_extension_withpath) "File extension was NOT wdz but filename_extension_is returned true"); } +/* Test helper function */ +void check_objstore_from_json +( + const char *json_buf, + objtype_t obj_type, + char *expected_id +) +{ + json_object *j_obj = json_tokener_parse(json_buf); + + objstore_t *obj_store = NULL; + + int res = load_game_objects_from_json_object(&obj_store, j_obj); + + cr_assert_eq(res, SUCCESS, "Load function returned FAILURE"); + cr_assert_not_null(obj_store, "Object store still empty after load"); + + objstore_t *imported = + find_objstore(&obj_store, expected_id, obj_type); + + + object_t *target; + + cr_assert_not_null(imported, "Could not find the right object store node"); + target = imported->o; + cr_assert_not_null(target, + "Could not get the loaded game object from the object store" + ); + cr_assert_str_eq(target->id, expected_id, "Loaded game object did not have correct ID"); + free_all_objstore(&obj_store); +} + +/* This test should cover everything from load_game_objs_from_json to + * the conversion function convert_j_obj_to_game_obj, which also invokes + * the make_data_from_j_value function. If this works, all of them work. + */ +Test(load_wdz, load_room_objs_from_json) +{ + const char *test_json = + "{\"rooms\": \ + [ \ + {\"id\":\"best_room\", \"number\":123}, \ + {\"id\":\"test_room\", \"can_enter\":false} \ + ] \ + }"; + check_objstore_from_json(test_json, TYPE_ROOM, "best_room"); + check_objstore_from_json(test_json, TYPE_ROOM, "test_room"); +} -/* The other loading functions do not have tests as their full - * implementations are still pending, in branch wdl/parsing-wdz-namanh. - */ \ No newline at end of file +/* Same as above, but the special player object needs some more attention */ +Test(load_wdz, load_player_from_json) +{ + const char *test_json = + "{\"player\": \ + { \ + \"intro_text\":\"my intro text\", \ + \"start_room\":\"whiteroom\" \ + } \ + }"; + check_objstore_from_json(test_json, TYPE_PLAYER, DEFAULT_PLAYER_OBJ_ID); +} + + +/* A test that simulates what would actually happen on chiventure startup */ +#define RELATIVE_FILE_PATH "../../../src/wdl/examples/wdz/test_game.wdz" +Test(load_wdz, load_from_wdz_file) +{ + objstore_t *obj_store = NULL; + int n_jsons; + populate_objstore_from_wdz(&obj_store, &n_jsons, RELATIVE_FILE_PATH); + cr_assert_not_null(obj_store, "Object store is empty after load"); + cr_log_warn("There are %d\n items in objstore", HASH_COUNT(obj_store)); + + objstore_t *el, *tmp; + HASH_ITER(hh, obj_store, el, tmp) + { + cr_log_warn("object has ID %s and type %d\n",el->key.id, el->key.type); + } + + cr_assert_not_null(find_objstore(&obj_store, DEFAULT_PLAYER_OBJ_ID, TYPE_PLAYER), + "Player object not loaded"); + // Find a representative object from each type + cr_assert_not_null(find_objstore(&obj_store, "greenlever", TYPE_ITEM), + "Item greenlever not loaded"); + cr_assert_not_null(find_objstore(&obj_store, "action_normal_pull ", TYPE_ACTION), + "Action action_normal_pull not loaded"); + cr_assert_not_null(find_objstore(&obj_store, "globcond_unlock", TYPE_GCONDITION), + "Global condition globcond_unlock not loaded"); + cr_assert_not_null(find_objstore(&obj_store, "purpleroom", TYPE_ROOM), + "Room purpleroom not loaded"); + free_all_objstore(&obj_store); +}