mirror of
https://github.com/nunuhara/xsystem4.git
synced 2026-09-30 19:08:12 +03:00
Pastel Chime 3 executes system.GroupSave() with a group name of "共有データ", but that group name does not exist in the OBJG section of AIN. Pascha3.exe does not treat such system.GroupSave() calls as failures, instead behaving as if saving an empty variable group.
609 lines
17 KiB
C
609 lines
17 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;
|
|
}
|
|
|
|
cJSON *load_json(const char *filename)
|
|
{
|
|
char *path = savedir_path(filename);
|
|
char *json = file_read(path, NULL);
|
|
free(path);
|
|
if (!json)
|
|
return NULL;
|
|
cJSON *root = cJSON_Parse(json);
|
|
free(json);
|
|
return root;
|
|
}
|
|
|
|
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;
|
|
}
|
|
enum ain_data_type type = variable_type(page, 0, NULL, NULL);
|
|
fa->nr_values = page->nr_vars;
|
|
fa->type = type;
|
|
fa->values = xcalloc(fa->nr_values, sizeof(struct gsave_array_value));
|
|
for (int i = 0; i < page->nr_vars; i++) {
|
|
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)),
|
|
};
|
|
if (save->version >= 7) {
|
|
rec.struct_index = gsave_get_struct_def(save, st->name);
|
|
if (rec.struct_index < 0)
|
|
rec.struct_index = gsave_add_struct_def(save, st);
|
|
}
|
|
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;
|
|
}
|
|
}
|
|
|
|
static int get_gsave_version(void)
|
|
{
|
|
if (AIN_VERSION_GTE(ain, 6, 0)) {
|
|
return config.vm_name ? 5 : 7;
|
|
}
|
|
return AIN_VERSION_GTE(ain, 5, 0) ? 5 : 4;
|
|
}
|
|
|
|
int save_globals(const char *keyname, const char *filename, const char *group_name, int *n_out)
|
|
{
|
|
int group = -1;
|
|
int nr_vars = 0;
|
|
if (group_name) {
|
|
if ((group = get_group_index(group_name)) < 0) {
|
|
WARNING("Unregistered global group: %s", display_sjis0(group_name));
|
|
} else {
|
|
for (int i = 0; i < ain->nr_globals; i++) {
|
|
if (ain->globals[i].group_index == group)
|
|
nr_vars++;
|
|
}
|
|
}
|
|
} else {
|
|
nr_vars = ain->nr_globals;
|
|
}
|
|
|
|
struct gsave *save = gsave_create(get_gsave_version(), keyname, ain->nr_globals, group_name);
|
|
gsave_add_globals_record(save, nr_vars);
|
|
|
|
if (nr_vars > 0) {
|
|
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(value == GSAVE7_EMPTY_STRING ? &EMPTY_STRING : 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];
|
|
struct gsave_struct_def *sd =
|
|
save->version >= 7 ? &save->struct_defs[rec->struct_index] : NULL;
|
|
const char *struct_name = sd ? sd->name : rec->struct_name;
|
|
if (strcmp(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]];
|
|
const char *field_name = sd ? sd->fields[i].name : kv->name;
|
|
enum ain_data_type field_type = sd ? sd->fields[i].type : kv->type;
|
|
int index = struct_member_index(st, field_name);
|
|
if (index < 0) {
|
|
WARNING("structure %s has no member named %s", display_sjis0(struct_name), display_sjis1(field_name));
|
|
continue;
|
|
}
|
|
int struct_type, array_rank;
|
|
enum ain_data_type data_type = variable_type(page, index, &struct_type, &array_rank);
|
|
if (data_type != field_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;
|
|
}
|