From cc79f9caeefc89263c4c67729f3ec41ceea714d7 Mon Sep 17 00:00:00 2001 From: kichikuou Date: Sun, 12 Mar 2023 22:38:53 +0900 Subject: [PATCH] Use global/group save file format compatible with System4 After this, xsystem4 uses the global/group save format used by the original System4, instead of the JSON format. Existing JSON save files can still be loaded. This makes save files interoperable between System4 and xsystem4 in some games, including Sengoku Rance and Toushin Toshi III. --- include/savedata.h | 3 +- src/savedata.c | 361 ++++++++++++++++++++++++++++++++++++-------- src/vm.c | 4 +- subprojects/libsys4 | 2 +- 4 files changed, 300 insertions(+), 70 deletions(-) diff --git a/include/savedata.h b/include/savedata.h index 616c124..11f2a72 100644 --- a/include/savedata.h +++ b/include/savedata.h @@ -26,8 +26,7 @@ void json_load_page(struct page *page, cJSON *vars, bool call_dtors); union vm_value json_to_vm_value(enum ain_data_type type, enum ain_data_type struct_type, int array_rank, cJSON *json); int save_json(const char *filename, cJSON *json); -int save_globals(const char *keyname, const char *filename); -int save_group(const char *keyname, const char *filename, const char *group_name, int *n); +int save_globals(const char *keyname, const char *filename, const char *group_name, int *n); int load_globals(const char *keyname, const char *filename, const char *group_name, int *n); int delete_save_file(const char *filename); diff --git a/src/savedata.c b/src/savedata.c index 45c6ffa..41cd939 100644 --- a/src/savedata.c +++ b/src/savedata.c @@ -14,6 +14,7 @@ * along with this program; if not, see . */ +#include #include #include #include @@ -23,6 +24,7 @@ #include "system4.h" #include "system4/ain.h" #include "system4/file.h" +#include "system4/savefile.h" #include "system4/string.h" #include "savedata.h" @@ -77,27 +79,6 @@ cJSON *vm_value_to_json(enum ain_data_type type, union vm_value val) } } -struct global_save_data { - cJSON *save; - cJSON *globals; -}; - -static void init_global_save_data(struct global_save_data *data, const char *keyname) -{ - data->save = cJSON_CreateObject(); - data->globals = cJSON_CreateArray(); - cJSON_AddStringToObject(data->save, "key", keyname); - cJSON_AddItemToObject(data->save, "globals", data->globals); -} - -static void add_global(struct global_save_data *data, int global, union vm_value val) -{ - cJSON *obj = cJSON_CreateObject(); - cJSON_AddNumberToObject(obj, "index", global); - cJSON_AddItemToObject(obj, "value", vm_value_to_json(ain->globals[global].type.data, val)); - cJSON_AddItemToArray(data->globals, obj); -} - int save_json(const char *filename, cJSON *json) { char *path = savedir_path(filename); @@ -126,25 +107,6 @@ int save_json(const char *filename, cJSON *json) } -static int write_save_data(struct global_save_data *data, const char *filename) -{ - int r = save_json(filename, data->save); - cJSON_Delete(data->save); - return r; -} - -int save_globals(const char *keyname, const char *filename) -{ - struct global_save_data data; - init_global_save_data(&data, keyname); - - for (int i = 0; i < ain->nr_globals; i++) { - add_global(&data, i, global_get(i)); - } - - return write_save_data(&data, filename); -} - static int get_group_index(const char *name) { for (int i = 0; i < ain->nr_global_groups; i++) { @@ -154,27 +116,145 @@ static int get_group_index(const char *name) return -1; } -int save_group(const char *keyname, const char *filename, const char *group_name, int *n) -{ - int group; +static int32_t add_value_to_gsave(enum ain_data_type type, union vm_value val, struct gsave *save); - if ((group = get_group_index(group_name)) < 0) { - WARNING("Unregistered global group: %s", display_sjis0(group_name)); +static struct gsave_flat_array *collect_flat_arrays(struct page *page, struct gsave_flat_array *fa, struct gsave *save) +{ + if (page->array.rank > 1) { + for (int i = 0; i < page->nr_vars; i++) + fa = collect_flat_arrays(heap_get_page(page->values[i].i), fa, save); + return fa; + } + fa->nr_values = page->nr_vars; + fa->values = xcalloc(fa->nr_values, sizeof(struct gsave_array_value)); + for (int i = 0; i < page->nr_vars; i++) { + enum ain_data_type type = variable_type(page, i, NULL, NULL); + fa->values[i].type = type; + fa->values[i].value = add_value_to_gsave(type, page->values[i], save); + } + return ++fa; +} + +static int32_t add_value_to_gsave(enum ain_data_type type, union vm_value val, struct gsave *save) +{ + switch (type) { + case AIN_VOID: + case AIN_INT: + case AIN_BOOL: + case AIN_FUNC_TYPE: + case AIN_DELEGATE: + case AIN_LONG_INT: + case AIN_FLOAT: + return val.i; + case AIN_STRING: + return gsave_add_string(save, heap[val.i].s); + case AIN_STRUCT: + { + struct page *page = heap_get_page(val.i); + assert(page->type == STRUCT_PAGE); + struct ain_struct *st = &ain->structures[page->index]; + assert(st->nr_members == page->nr_vars); + struct gsave_record rec = { + .type = GSAVE_RECORD_STRUCT, + .struct_name = strdup(st->name), + .nr_indices = st->nr_members, + .indices = xcalloc(st->nr_members, sizeof(int32_t)), + }; + for (int i = 0; i < page->nr_vars; i++) { + enum ain_data_type type = st->members[i].type.data; + int32_t value = add_value_to_gsave(type, page->values[i], save); + struct gsave_keyval kv = { + .name = strdup(st->members[i].name), + .type = type, + .value = value, + }; + rec.indices[i] = gsave_add_keyval(save, &kv); + } + return gsave_add_record(save, &rec); + } + case AIN_ARRAY_TYPE: + { + struct page *page = heap_get_page(val.i); + if (!page) { + struct gsave_array array = { .rank = -1 }; + return gsave_add_array(save, &array); + } + assert(page->type == ARRAY_PAGE); + struct gsave_array array = { + .rank = page->array.rank, + .dimensions = xcalloc(page->array.rank, sizeof(int32_t)), + .nr_flat_arrays = 1 + }; + for (struct page *p = page;; p = heap_get_page(page->values[0].i)) { + array.dimensions[p->array.rank - 1] = p->nr_vars; + if (p->array.rank == 1) + break; + array.nr_flat_arrays *= p->nr_vars; + } + array.flat_arrays = xcalloc(array.nr_flat_arrays, sizeof(struct gsave_flat_array)); + collect_flat_arrays(page, array.flat_arrays, save); + return gsave_add_array(save, &array); + } + case AIN_REF_TYPE: + return -1; + default: + WARNING("Unhandled type: %s", ain_strtype(ain, type, -1)); + return -1; + } +} + +int save_globals(const char *keyname, const char *filename, const char *group_name, int *n_out) +{ + int group = -1; + int nr_vars; + if (group_name) { + if ((group = get_group_index(group_name)) < 0) { + WARNING("Unregistered global group: %s", display_sjis0(group_name)); + return 0; + } + nr_vars = 0; + for (int i = 0; i < ain->nr_globals; i++) { + if (ain->globals[i].group_index == group) + nr_vars++; + } + } else { + nr_vars = ain->nr_globals; + } + + int gsave_version = AIN_VERSION_GTE(ain, 5, 0) ? 5 : 4; + struct gsave *save = gsave_create(gsave_version, keyname, ain->nr_globals, group_name); + gsave_add_globals_record(save, nr_vars); + + struct gsave_global *global = save->globals; + for (int i = 0; i < ain->nr_globals; i++) { + if (group >= 0 && ain->globals[i].group_index != group) + continue; + global->name = strdup(ain->globals[i].name); + global->type = ain->globals[i].type.data; + global->value = add_value_to_gsave(global->type, global_get(i), save); + global++; + } + + char *path = savedir_path(filename); + FILE *fp = file_open_utf8(path, "wb"); + if (!fp) { + WARNING("Failed to open save file %s: %s", display_utf0(path), strerror(errno)); + free(path); + gsave_free(save); return 0; } + free(path); - struct global_save_data data; - init_global_save_data(&data, keyname); - - *n = 0; - for (int i = 0; i < ain->nr_globals; i++) { - if (ain->globals[i].group_index != group) - continue; - add_global(&data, i, global_get(i)); - (*n)++; - } - - return write_save_data(&data, filename); + bool encrypt = !AIN_VERSION_GTE(ain, 6, 0); + int compression_level = AIN_VERSION_GTE(ain, 6, 0) ? 1 : 9; + enum savefile_error error = gsave_write(save, fp, encrypt, compression_level); + if (error != SAVEFILE_SUCCESS) + WARNING("Failed to write save file: %s", savefile_strerror(error)); + fclose(fp); + gsave_free(save); + if (n_out) + *n_out = nr_vars; + return error == SAVEFILE_SUCCESS; } union vm_value json_to_vm_value(enum ain_data_type type, enum ain_data_type struct_type, int array_rank, cJSON *json); @@ -273,19 +353,16 @@ union vm_value json_to_vm_value(enum ain_data_type type, enum ain_data_type stru } } -static cJSON *read_save_file(const char *filename) +static cJSON *read_save_file(const char *path) { FILE *f; long len; char *buf; - char *path = savedir_path(filename); if (!(f = file_open_utf8(path, "rb"))) { - WARNING("Failed to open save file: %s: %s", display_utf0(filename), strerror(errno)); - free(path); + WARNING("Failed to open save file: %s: %s", display_utf0(path), strerror(errno)); return NULL; } - free(path); fseek(f, 0, SEEK_END); len = ftell(f); @@ -294,7 +371,7 @@ static cJSON *read_save_file(const char *filename) buf = xmalloc(len+1); buf[len] = '\0'; if (fread(buf, len, 1, f) != 1) { - WARNING("Failed to read save file: %s", display_utf0(filename)); + WARNING("Failed to read save file: %s", display_utf0(path)); free(buf); return 0; } @@ -305,10 +382,10 @@ static cJSON *read_save_file(const char *filename) return r; } -int load_globals(const char *keyname, const char *filename, const char *group_name, int *n) +static int load_globals_from_json(const char *path, const char *keyname, const char *group_name, int *n) { int retval = 0; - cJSON *save = read_save_file(filename); + cJSON *save = read_save_file(path); if (!save) return 0; if (!cJSON_IsObject(save)) { @@ -365,6 +442,160 @@ cleanup: return retval; } +static int struct_member_index(struct ain_struct *st, const char *name) +{ + for (int i = 0; i < st->nr_members; i++) { + if (!strcmp(st->members[i].name, name)) + return i; + } + return -1; +} + +static union vm_value gsave_to_vm_value(struct gsave *save, enum ain_data_type type, int struct_type, int array_rank, int32_t value); + +static struct gsave_flat_array *gsave_load_array(struct gsave *save, struct page *page, struct gsave_flat_array *flat_array) +{ + assert(page->type == ARRAY_PAGE); + if (page->array.rank > 1) { + for (int i = 0; i < page->nr_vars; i++) + flat_array = gsave_load_array(save, heap_get_page(page->values[i].i), flat_array); + return flat_array; + } + + if (page->nr_vars != flat_array->nr_values) + VM_ERROR("Bad save file: unexpected number of array elements"); + for (int i = 0; i < page->nr_vars; i++) { + int struct_type, array_rank; + enum ain_data_type data_type = variable_type(page, i, &struct_type, &array_rank); + if (data_type != flat_array->values[i].type) + VM_ERROR("Bad save file: unexpected array element type"); + union vm_value val = gsave_to_vm_value(save, data_type, struct_type, array_rank, flat_array->values[i].value); + page->values[i] = val; + } + return flat_array + 1; +} + +static union vm_value gsave_to_vm_value(struct gsave *save, enum ain_data_type type, int struct_type, int array_rank, int32_t value) +{ + switch (type) { + case AIN_VOID: + case AIN_INT: + case AIN_BOOL: + case AIN_LONG_INT: + case AIN_FLOAT: + return vm_int(value); + case AIN_STRING: + { + int slot = heap_alloc_slot(VM_STRING); + heap[slot].s = string_ref(save->strings[value]); + return vm_int(slot); + } + case AIN_STRUCT: + { + struct gsave_record *rec = &save->records[value]; + struct ain_struct *st = &ain->structures[struct_type]; + if (strcmp(rec->struct_name, st->name)) + VM_ERROR("Bad save file: structure name mismatch"); + int slot = alloc_struct(struct_type); + struct page *page = heap[slot].page; + for (int i = 0; i < rec->nr_indices; i++) { + struct gsave_keyval *kv = &save->keyvals[rec->indices[i]]; + int index = struct_member_index(st, kv->name); + if (index < 0) + VM_ERROR("Bad save file: %s has no member named %s", display_sjis0(rec->struct_name), display_sjis1(kv->name)); + int struct_type, array_rank; + enum ain_data_type data_type = variable_type(page, index, &struct_type, &array_rank); + if (data_type != kv->type) + VM_ERROR("Bad save file: structure member type mismatch"); + union vm_value val = gsave_to_vm_value(save, data_type, struct_type, array_rank, kv->value); + page->values[index] = val; + } + return vm_int(slot); + } + case AIN_ARRAY_TYPE: + { + struct gsave_array *array = &save->arrays[value]; + int slot = heap_alloc_slot(VM_PAGE); + if (array->rank == -1) { + heap[slot].page = NULL; + return vm_int(slot); + } + if (array_rank != array->rank) + VM_ERROR("Bad save file: array rank mismatch"); + union vm_value *dims = xmalloc(sizeof(union vm_value) * array_rank); + for (int i = 0; i < array_rank; i++) + dims[i].i = array->dimensions[array_rank - 1 - i]; + struct page *page = alloc_array(array_rank, dims, type, struct_type, false); + heap[slot].page = page; + free(dims); + gsave_load_array(save, page, array->flat_arrays); + return vm_int(slot); + } + case AIN_REF_TYPE: + return vm_int(-1); + default: + WARNING("Unhandled data type: %s", ain_strtype(ain, type, -1)); + return vm_int(-1); + } +} + +int load_globals_from_gsave(struct gsave *save, const char *keyname, const char *group_name, int *n) +{ + if (strcmp(keyname, save->key)) + VM_ERROR("Attempted to load save data with wrong key: %s", display_sjis0(keyname)); + if (!group_name) + group_name = ""; + if (!save->group) + save->group = strdup(""); + if (strcmp(group_name, save->group)) + VM_ERROR("Attempted to load save data with wrong group name: '%s'", display_sjis0(group_name)); + + for (struct gsave_global *g = save->globals; g < save->globals + save->nr_globals; g++) { + int global_index = ain_get_global(ain, g->name); + if (global_index < 0) { + WARNING("invalid global name %s", display_sjis0(g->name)); + return 0; + } + struct ain_type *type = &ain->globals[global_index].type; + if (type->data != g->type) { + WARNING("%s: type mismatch", display_sjis0(g->name)); + return 0; + } + bool call_dtors = false; // Destructors for old objects should not be called. + global_set(global_index, gsave_to_vm_value(save, type->data, type->struc, type->rank, g->value), call_dtors); + if (n) + (*n)++; + } + + return 1; +} + +int load_globals(const char *keyname, const char *filename, const char *group_name, int *n) +{ + char *path = savedir_path(filename); + int retval; + + // First, try reading as a gsave. + enum savefile_error error; + struct gsave *save = gsave_read(path, &error); + switch (error) { + case SAVEFILE_SUCCESS: + free(path); + retval = load_globals_from_gsave(save, keyname, group_name, n); + gsave_free(save); + return retval; + case SAVEFILE_INVALID_SIGNATURE: + // If not a System4 save file, try reading as a json. + retval = load_globals_from_json(path, keyname, group_name, n); + free(path); + return retval; + default: + WARNING("%s: %s", display_utf0(path), savefile_strerror(error)); + free(path); + return 0; + } +} + int delete_save_file(const char *filename) { char *path = savedir_path(filename); diff --git a/src/vm.c b/src/vm.c index 042a6e0..547ff8c 100644 --- a/src/vm.c +++ b/src/vm.c @@ -429,7 +429,7 @@ static void system_call(enum syscall_code code) case SYS_GLOBAL_SAVE: { // system.GlobalSave(string szKeyName, string szFileName) int filename = stack_pop().i; int keyname = stack_pop().i; - stack_push(save_globals(heap_get_string(keyname)->text, heap_get_string(filename)->text)); + stack_push(save_globals(heap_get_string(keyname)->text, heap_get_string(filename)->text, NULL, NULL)); heap_unref(filename); heap_unref(keyname); break; @@ -603,7 +603,7 @@ static void system_call(enum syscall_code code) int groupname = stack_pop().i; int filename = stack_pop().i; int keyname = stack_pop().i; - stack_push(save_group(heap_get_string(keyname)->text, + stack_push(save_globals(heap_get_string(keyname)->text, heap_get_string(filename)->text, heap_get_string(groupname)->text, &n->i)); diff --git a/subprojects/libsys4 b/subprojects/libsys4 index 187a463..b869f6c 160000 --- a/subprojects/libsys4 +++ b/subprojects/libsys4 @@ -1 +1 @@ -Subproject commit 187a463044a0b9b207f322796aa20c42322fed98 +Subproject commit b869f6cc8d6f417272de6a2f01ef66473cb06e22