mirror of
https://github.com/nunuhara/xsystem4.git
synced 2026-10-02 03:48:00 +03:00
579 lines
16 KiB
C
579 lines
16 KiB
C
/* Copyright (C) 2019 Nunuhara Cabbage <nunuhara@haniwa.technology>
|
|
*
|
|
* This program is free software; you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License as published by
|
|
* the Free Software Foundation; either version 2 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with this program; if not, see <http://gnu.org/licenses/>.
|
|
*/
|
|
|
|
#include <assert.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <errno.h>
|
|
#include "cJSON.h"
|
|
|
|
#include "system4.h"
|
|
#include "system4/ain.h"
|
|
#include "system4/file.h"
|
|
#include "system4/savefile.h"
|
|
#include "system4/string.h"
|
|
|
|
#include "savedata.h"
|
|
#include "vm.h"
|
|
#include "vm/heap.h"
|
|
#include "vm/page.h"
|
|
#include "xsystem4.h"
|
|
|
|
static int current_global;
|
|
|
|
#define invalid_save_data(msg, data) { \
|
|
char *str = data ? cJSON_Print(data) : strdup("NULL"); \
|
|
if (!str) str = strdup("PRINTING FAILED"); \
|
|
WARNING("Invalid save data (%d): " msg ": %s", current_global, data); \
|
|
free(str); \
|
|
}
|
|
|
|
int save_json(const char *filename, cJSON *json)
|
|
{
|
|
char *path = savedir_path(filename);
|
|
FILE *f = file_open_utf8(path, "w");
|
|
if (!f) {
|
|
WARNING("Failed to open save file: %s: %s", display_utf0(filename), strerror(errno));
|
|
free(path);
|
|
return 0;
|
|
}
|
|
free(path);
|
|
|
|
char *str = cJSON_Print(json);
|
|
|
|
if (fwrite(str, strlen(str), 1, f) != 1) {
|
|
WARNING("Failed to write save file: %s", strerror(errno));
|
|
free(str);
|
|
return 0;
|
|
}
|
|
if (fclose(f)) {
|
|
WARNING("Error writing save to file: %s: %s", display_utf0(filename), strerror(errno));
|
|
free(str);
|
|
return 0;
|
|
}
|
|
free(str);
|
|
return 1;
|
|
|
|
}
|
|
|
|
static int get_group_index(const char *name)
|
|
{
|
|
for (int i = 0; i < ain->nr_global_groups; i++) {
|
|
if (!strcmp(ain->global_group_names[i], name))
|
|
return i;
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
static int32_t add_value_to_gsave(enum ain_data_type type, union vm_value val, struct gsave *save);
|
|
|
|
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);
|
|
|
|
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;
|
|
}
|
|
|
|
static union vm_value json_to_vm_value(enum ain_data_type type, enum ain_data_type struct_type, int array_rank, cJSON *json);
|
|
|
|
void get_array_dims(cJSON *json, int rank, union vm_value *dims)
|
|
{
|
|
cJSON *array = json;
|
|
for (int i = 0; i < rank; i++) {
|
|
dims[i].i = cJSON_GetArraySize(array);
|
|
array = cJSON_GetArrayItem(array, 0);
|
|
}
|
|
}
|
|
|
|
void json_load_page(struct page *page, cJSON *vars, bool call_dtors)
|
|
{
|
|
int i = 0;
|
|
cJSON *v;
|
|
cJSON_ArrayForEach(v, vars) {
|
|
int struct_type, array_rank;
|
|
enum ain_data_type data_type = variable_type(page, i, &struct_type, &array_rank);
|
|
union vm_value val = json_to_vm_value(data_type, struct_type, array_rank, v);
|
|
if (call_dtors)
|
|
variable_set(page, i, data_type, val);
|
|
else
|
|
page->values[i] = val;
|
|
i++;
|
|
}
|
|
}
|
|
|
|
static union vm_value json_to_vm_value(enum ain_data_type type, enum ain_data_type struct_type, int array_rank, cJSON *json)
|
|
{
|
|
char *str;
|
|
int slot;
|
|
union vm_value *dims;
|
|
struct page *page;
|
|
switch (type) {
|
|
case AIN_INT:
|
|
case AIN_BOOL:
|
|
case AIN_LONG_INT:
|
|
if (!cJSON_IsNumber(json)) {
|
|
invalid_save_data("Not a number", json);
|
|
return vm_int(0);
|
|
}
|
|
return vm_int(json->valueint);
|
|
case AIN_FLOAT:
|
|
if (!cJSON_IsNumber(json)) {
|
|
invalid_save_data("Not a number", json);
|
|
return vm_float(0);
|
|
}
|
|
return vm_float(json->valuedouble);
|
|
case AIN_STRING:
|
|
slot = heap_alloc_slot(VM_STRING);
|
|
if (!cJSON_IsString(json)) {
|
|
invalid_save_data("Not a string", json);
|
|
heap[slot].s = string_ref(&EMPTY_STRING);
|
|
return vm_int(slot);
|
|
}
|
|
str = cJSON_GetStringValue(json);
|
|
if (!str[0]) {
|
|
heap[slot].s = string_ref(&EMPTY_STRING);
|
|
} else {
|
|
heap[slot].s = make_string(str, strlen(str));
|
|
}
|
|
return vm_int(slot);
|
|
case AIN_STRUCT:
|
|
slot = alloc_struct(struct_type);
|
|
if (!cJSON_IsArray(json) || cJSON_GetArraySize(json) != ain->structures[struct_type].nr_members) {
|
|
invalid_save_data("Not an array", json);
|
|
} else {
|
|
json_load_page(heap[slot].page, json, false);
|
|
}
|
|
return vm_int(slot);
|
|
case AIN_ARRAY_TYPE:
|
|
slot = heap_alloc_slot(VM_PAGE);
|
|
if (cJSON_IsNull(json)) {
|
|
heap[slot].page = NULL;
|
|
return vm_int(slot);
|
|
}
|
|
if (!cJSON_IsArray(json)) {
|
|
invalid_save_data("Not an array", json);
|
|
heap[slot].page = NULL;
|
|
return vm_int(slot);
|
|
}
|
|
dims = xmalloc(sizeof(union vm_value) * array_rank);
|
|
get_array_dims(json, array_rank, dims);
|
|
page = alloc_array(array_rank, dims, type, struct_type, false);
|
|
heap[slot].page = page;
|
|
free(dims);
|
|
json_load_page(heap[slot].page, json, false);
|
|
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);
|
|
}
|
|
}
|
|
|
|
static cJSON *read_save_file(const char *path)
|
|
{
|
|
FILE *f;
|
|
long len;
|
|
char *buf;
|
|
|
|
if (!(f = file_open_utf8(path, "rb"))) {
|
|
WARNING("Failed to open save file: %s: %s", display_utf0(path), strerror(errno));
|
|
return NULL;
|
|
}
|
|
|
|
fseek(f, 0, SEEK_END);
|
|
len = ftell(f);
|
|
fseek(f, 0, SEEK_SET);
|
|
|
|
buf = xmalloc(len+1);
|
|
buf[len] = '\0';
|
|
if (fread(buf, len, 1, f) != 1) {
|
|
WARNING("Failed to read save file: %s", display_utf0(path));
|
|
free(buf);
|
|
return 0;
|
|
}
|
|
fclose(f);
|
|
|
|
cJSON *r = cJSON_Parse(buf);
|
|
free(buf);
|
|
return r;
|
|
}
|
|
|
|
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(path);
|
|
if (!save)
|
|
return 0;
|
|
if (!cJSON_IsObject(save)) {
|
|
invalid_save_data("Not an object", save);
|
|
goto cleanup;
|
|
}
|
|
|
|
cJSON *key = cJSON_GetObjectItem(save, "key");
|
|
if (!key || strcmp(keyname, cJSON_GetStringValue(key)))
|
|
VM_ERROR("Attempted to load save data with wrong key: %s", display_sjis0(keyname));
|
|
|
|
if (group_name) {
|
|
// TODO?
|
|
}
|
|
|
|
cJSON *globals = cJSON_GetObjectItem(save, "globals");
|
|
if (!globals || !cJSON_IsArray(globals)) {
|
|
invalid_save_data("Not an array", globals);
|
|
goto cleanup;
|
|
}
|
|
|
|
cJSON *g;
|
|
cJSON_ArrayForEach(g, globals) {
|
|
cJSON *index, *value;
|
|
if (!(index = cJSON_GetObjectItem(g, "index"))) {
|
|
invalid_save_data("Missing index", g);
|
|
goto cleanup;
|
|
}
|
|
if (!(value = cJSON_GetObjectItem(g, "value"))) {
|
|
invalid_save_data("Missing value", g);
|
|
goto cleanup;
|
|
}
|
|
if (!cJSON_IsNumber(index)) {
|
|
invalid_save_data("Not a number", index);
|
|
goto cleanup;
|
|
}
|
|
|
|
int i = index->valueint;
|
|
if (i < 0 || i > ain->nr_globals) {
|
|
invalid_save_data("Invalid global index", index);
|
|
goto cleanup;
|
|
}
|
|
current_global = i;
|
|
|
|
bool call_dtors = false; // Destructors for old objects are not called.
|
|
global_set(i, json_to_vm_value(ain->globals[i].type.data, ain->globals[i].type.struc, ain->globals[i].type.rank, value), call_dtors);
|
|
if (n)
|
|
(*n)++;
|
|
}
|
|
|
|
retval = 1;
|
|
cleanup:
|
|
cJSON_Delete(save);
|
|
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) {
|
|
WARNING("structure %s has no member named %s", display_sjis0(rec->struct_name), display_sjis1(kv->name));
|
|
continue;
|
|
}
|
|
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);
|
|
if (!file_exists(path)) {
|
|
free(path);
|
|
return 0;
|
|
}
|
|
if (remove_utf8(path)) {
|
|
WARNING("remove(\"%s\"): %s", display_utf0(path), strerror(errno));
|
|
free(path);
|
|
return 0;
|
|
}
|
|
free(path);
|
|
return 1;
|
|
}
|