1
0
Fork 0
trader/src/fileio.c

611 lines
18 KiB
C

/************************************************************************
* *
* Star Traders: A Game of Interstellar Trading *
* Copyright (C) 1990-2024, John Zaitseff *
* *
************************************************************************/
/*
Author: John Zaitseff <J.Zaitseff@zap.org.au>
$Id$
This file, fileio.c, contains the implementation of the game load and
save functions used in Star Traders.
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 3 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 https://www.gnu.org/licenses/.
*/
#include "trader.h"
/************************************************************************
* Module-specific macros *
************************************************************************/
// Macros used in load_game()
#define load_game_scanf(_fmt, _var, _cond) \
do { \
if (fgets(inbuf, BIGBUFSIZE, file) == NULL) { \
err_exit(_("%s: missing field on line %d"), \
filename, lineno); \
} \
if (unscramble(buf, inbuf, BUFSIZE, crypt_key_p) == NULL) { \
err_exit(_("%s: illegal field on line %d"), \
filename, lineno); \
} \
if (sscanf(buf, _fmt "\n", &(_var)) != 1) { \
err_exit(_("%s: illegal field on line %d: '%s'"), \
filename, lineno, buf); \
} \
if (! (_cond)) { \
err_exit(_("%s: illegal value on line %d: '%s'"), \
filename, lineno, buf); \
} \
lineno++; \
} while (0)
#define load_game_read_int(_var, _cond) \
load_game_scanf("%d", _var, _cond)
#define load_game_read_long(_var, _cond) \
load_game_scanf("%ld", _var, _cond)
#define load_game_read_double(_var, _cond) \
load_game_scanf("%lf", _var, _cond)
#define load_game_read_bool(_var) \
do { \
int b; \
\
load_game_scanf("%d", b, b == false || b == true); \
(_var) = b; \
} while (0)
#ifdef USE_UTF8_GAME_FILE
# define load_game_read_string(_var, _var_utf8) \
do { \
char *s; \
int len; \
\
if (fgets(inbuf, BIGBUFSIZE, file) == NULL) { \
err_exit(_("%s: missing field on line %d"), \
filename, lineno); \
} \
if (unscramble(buf, inbuf, BUFSIZE, crypt_key_p) == NULL) { \
err_exit(_("%s: illegal field on line %d"), \
filename, lineno); \
} \
if (strlen(buf) == 0) { \
err_exit(_("%s: illegal value on line %d"), \
filename, lineno); \
} \
if (need_icd) { \
s = str_cd_iconv(buf, icd); \
if (s == NULL) { \
if (errno == EILSEQ) { \
err_exit(_("%s: illegal characters on line %d"), \
filename, lineno); \
} else { \
errno_exit("str_cd_iconv"); \
} \
} \
} else { \
s = xstrdup(buf); \
} \
\
len = strlen(s); \
if (len > 0 && s[len - 1] == '\n') { \
s[len - 1] = '\0'; \
} \
\
xmbstowcs(wcbuf, s, BUFSIZE); \
(_var) = xwcsdup(wcbuf); \
(_var_utf8) = s; \
\
lineno++; \
} while (0)
#else // ! USE_UTF8_GAME_FILE
# define load_game_read_string(_var, _var_utf8) \
do { \
char *s; \
int len; \
\
if (fgets(inbuf, BIGBUFSIZE, file) == NULL) { \
err_exit(_("%s: missing field on line %d"), \
filename, lineno); \
} \
if (unscramble(buf, inbuf, BUFSIZE, crypt_key_p) == NULL) { \
err_exit(_("%s: illegal field on line %d"), \
filename, lineno); \
} \
if (strlen(buf) == 0) { \
err_exit(_("%s: illegal value on line %d"), \
filename, lineno); \
} \
\
s = xstrdup(buf); \
\
len = strlen(s); \
if (len > 0 && s[len - 1] == '\n') { \
s[len - 1] = '\0'; \
} \
\
xmbstowcs(wcbuf, s, BUFSIZE); \
(_var) = xwcsdup(wcbuf); \
(_var_utf8) = s; \
\
lineno++; \
} while (0)
#endif // ! USE_UTF8_GAME_FILE
// Macros used in save_game()
#define save_game_printf(_fmt, _var) \
do { \
snprintf(buf, BUFSIZE, _fmt "\n", _var); \
scramble(encbuf, buf, BIGBUFSIZE, crypt_key_p); \
fprintf(file, "%s", encbuf); \
} while (0)
#define save_game_write_int(_var) \
save_game_printf("%d", _var)
#define save_game_write_long(_var) \
save_game_printf("%ld", _var)
#define save_game_write_double(_var) \
save_game_printf("%2.20e", _var)
#define save_game_write_bool(_var) \
save_game_printf("%d", (int) _var)
#ifdef USE_UTF8_GAME_FILE
# define save_game_write_string(_var, _var_utf8) \
do { \
if ((_var_utf8) != NULL) { \
save_game_printf("%s", _var_utf8); \
} else { \
if (need_icd) { \
snprintf(buf, BUFSIZE, "%ls", _var); \
char *s = str_cd_iconv(buf, icd); \
if (s == NULL) { \
if (errno == EILSEQ) { \
err_exit(_("%s: could not convert string"), \
filename); \
} else { \
errno_exit("str_cd_iconv"); \
} \
} \
save_game_printf("%s", s); \
free(s); \
} else { \
save_game_printf("%ls", _var); \
} \
} \
} while (0)
#else // ! USE_UTF8_GAME_FILE
# define save_game_write_string(_var, _var_utf8) \
do { \
if ((_var_utf8) != NULL) { \
save_game_printf("%s", _var_utf8); \
} else { \
save_game_printf("%ls", _var); \
} \
} while (0)
#endif // ! USE_UTF8_GAME_FILE
/************************************************************************
* Game load and save function definitions *
************************************************************************/
// These functions are documented in the file "fileio.h"
/***********************************************************************/
// load_game: Load a previously-saved game from disk
bool load_game (int num)
{
char *filename;
FILE *file;
char *codeset, *codeset_nl;
int saved_errno, lineno;
char *prev_locale;
char *buf, *inbuf;
wchar_t *wcbuf;
unsigned int crypt_key;
unsigned int *crypt_key_p;
int is_encrypted_input;
int n, i, j;
#ifdef USE_UTF8_GAME_FILE
iconv_t icd;
bool need_icd;
#endif
assert(num >= 1 && num <= 9);
buf = xmalloc(BUFSIZE);
inbuf = xmalloc(BIGBUFSIZE);
wcbuf = xmalloc(BUFSIZE * sizeof(wchar_t));
filename = game_filename(num);
assert(filename != NULL);
file = game_fopen(num, "r");
if (file == NULL) {
// File could not be opened
if (errno == ENOENT) {
// File not found
txdlgbox(MAX_DLG_LINES, 50, 9, WCENTER, attr_error_window,
attr_error_title, attr_error_highlight, 0, 0,
attr_error_waitforkey, _(" Game Not Found "),
_("Game %d has not been saved to disk."), num);
} else {
// Some other file error
saved_errno = errno;
txdlgbox(MAX_DLG_LINES, 60, 9, WCENTER, attr_error_window,
attr_error_title, attr_error_highlight,
attr_error_normal, 0, attr_error_waitforkey,
_(" Game Not Loaded "),
_("Game %d could not be loaded from disk.\n\n"
"^{File %s: %s^}"), num, filename,
strerror(saved_errno));
}
free(buf);
free(inbuf);
free(wcbuf);
free(filename);
return false;
}
#ifdef USE_UTF8_GAME_FILE
// Make sure all strings are read in UTF-8 format for consistency
codeset = nl_langinfo(CODESET);
if (codeset == NULL) {
errno_exit("nl_langinfo(CODESET)");
}
need_icd = (strcmp(codeset, GAME_FILE_CHARSET) != 0);
if (need_icd) {
// Try using the GNU libiconv "//TRANSLIT" option
strcpy(buf, codeset);
strcat(buf, GAME_FILE_TRANSLIT);
icd = iconv_open(buf, GAME_FILE_CHARSET);
if (icd == (iconv_t) -1) {
// Try iconv_open() without "//TRANSLIT"
icd = iconv_open(codeset, GAME_FILE_CHARSET);
if (icd == (iconv_t) -1) {
errno_exit("iconv_open");
}
}
} else {
icd = (iconv_t) -1;
}
codeset_nl = xstrdup(GAME_FILE_CHARSET "\n");
#else // ! USE_UTF8_GAME_FILE
// Make sure all strings are read in the correct codeset
codeset = nl_langinfo(CODESET);
if (codeset == NULL) {
errno_exit("nl_langinfo(CODESET)");
}
codeset_nl = xmalloc(strlen(codeset) + 2);
strcpy(codeset_nl, codeset);
strcat(codeset_nl, "\n");
#endif // ! USE_UTF8_GAME_FILE
// Change the formatting of numbers to the POSIX locale for consistency
prev_locale = xstrdup(setlocale(LC_NUMERIC, NULL));
setlocale(LC_NUMERIC, "C");
// Read the game file header
if (fgets(buf, BUFSIZE, file) == NULL) {
err_exit(_("%s: missing header in game file"), filename);
}
if (strcmp(buf, GAME_FILE_HEADER "\n") != 0) {
err_exit(_("%s: not a valid game file"), filename);
}
if (fgets(buf, BUFSIZE, file) == NULL) {
err_exit(_("%s: missing subheader in game file"), filename);
}
if (strcmp(buf, GAME_FILE_API_VERSION "\n") != 0) {
err_exit(_("%s: saved under a different version of Star Traders"),
filename);
}
if (fgets(buf, BUFSIZE, file) == NULL) {
err_exit(_("%s: missing subheader in game file"), filename);
}
if (strcmp(buf, codeset_nl) != 0) {
err_exit(_("%s: saved under an incompatible character encoding"),
filename);
}
lineno = 4;
// Read in the game file encryption status
if (fscanf(file, "%i\n", &is_encrypted_input) != 1) {
err_exit(_("%s: illegal or missing field on line %d"), filename, lineno);
}
lineno++;
crypt_key = 0;
crypt_key_p = is_encrypted_input ? &crypt_key : NULL;
// Read in various game variables
load_game_read_int(n, n == MAX_X);
load_game_read_int(n, n == MAX_Y);
load_game_read_int(max_turn, max_turn >= 1);
load_game_read_int(turn_number, turn_number >= 1 && turn_number <= max_turn);
load_game_read_int(number_players, number_players >= 1 && number_players <= MAX_PLAYERS);
load_game_read_int(current_player, current_player >= 0 && current_player < number_players);
load_game_read_int(first_player, first_player >= 0 && first_player < number_players);
load_game_read_int(n, n == MAX_COMPANIES);
load_game_read_double(interest_rate, interest_rate > 0.0);
// Read in player data
for (i = 0; i < number_players; i++) {
load_game_read_string(player[i].name, player[i].name_utf8);
load_game_read_double(player[i].cash, player[i].cash >= 0.0);
load_game_read_double(player[i].debt, player[i].debt >= 0.0);
load_game_read_bool(player[i].in_game);
for (j = 0; j < MAX_COMPANIES; j++) {
load_game_read_long(player[i].stock_owned[j], player[i].stock_owned[j] >= 0);
}
}
// Read in company data
for (i = 0; i < MAX_COMPANIES; i++) {
xmbstowcs(wcbuf, gettext(company_name[i]), BUFSIZE);
company[i].name = xwcsdup(wcbuf);
load_game_read_double(company[i].share_price, company[i].share_price >= 0.0);
load_game_read_double(company[i].share_return, true);
load_game_read_long(company[i].stock_issued, company[i].stock_issued >= 0);
load_game_read_long(company[i].max_stock, company[i].max_stock >= 0);
load_game_read_bool(company[i].on_map);
}
// Read in galaxy map
for (int x = 0; x < MAX_X; x++) {
if (fgets(inbuf, BIGBUFSIZE, file) == NULL) {
err_exit(_("%s: missing field on line %d"), filename, lineno);
}
if (unscramble(buf, inbuf, BUFSIZE, crypt_key_p) == NULL) {
err_exit(_("%s: illegal field on line %d"), filename, lineno);
}
if (strlen(buf) != MAX_Y + 1) {
err_exit(_("%s: illegal field on line %d"), filename, lineno);
}
for (int y = 0; y < MAX_Y; y++) {
char c = buf[y];
if (c == MAP_EMPTY || c == MAP_OUTPOST || c == MAP_STAR
|| (c >= MAP_A && c <= MAP_LAST)) {
galaxy_map[x][y] = (map_val_t) c;
} else {
err_exit(_("%s: illegal value on line %d"), filename, lineno);
}
}
lineno++;
}
// Read in a dummy sentinel value
load_game_read_int(n, n == GAME_FILE_SENTINEL);
if (fclose(file) == EOF) {
errno_exit("%s", filename);
}
// Change the formatting of numbers back to the user-supplied locale
setlocale(LC_NUMERIC, prev_locale);
#ifdef USE_UTF8_GAME_FILE
if (need_icd) {
iconv_close(icd);
}
#endif
free(buf);
free(inbuf);
free(wcbuf);
free(filename);
free(prev_locale);
free(codeset_nl);
return true;
}
/***********************************************************************/
// save_game: Save the current game to disk
bool save_game (int num)
{
const char *data_dir;
int data_dir_fd;
char *buf, *encbuf;
char *filename;
FILE *file;
char *codeset;
int saved_errno;
char *prev_locale;
int i, j, x, y;
unsigned int crypt_key;
unsigned int *crypt_key_p;
#ifdef USE_UTF8_GAME_FILE
iconv_t icd;
bool need_icd;
#endif
assert(num >= 1 && num <= 9);
buf = xmalloc(BUFSIZE);
encbuf = xmalloc(BIGBUFSIZE);
crypt_key = 0;
crypt_key_p = option_dont_encrypt ? NULL : &crypt_key;
// Create the data directory, if needed
data_dir = data_directory();
data_dir_fd = data_directory_fileno();
if (data_dir != NULL && data_dir_fd == -1) {
if (xmkdir(data_dir, S_IRWXU | S_IRWXG | S_IRWXO) != 0) {
// Data directory could not be created
saved_errno = errno;
txdlgbox(MAX_DLG_LINES, 60, 7, WCENTER, attr_error_window,
attr_error_title, attr_error_highlight, attr_error_normal,
0, attr_error_waitforkey, _(" Game Not Saved "),
_("Game %d could not be saved to disk.\n\n"
"^{Directory %s: %s^}"), num, data_dir,
strerror(saved_errno));
free(buf);
free(encbuf);
return false;
}
}
filename = game_filename(num);
assert(filename != NULL);
file = game_fopen(num, "w");
if (file == NULL) {
// File could not be opened for writing
saved_errno = errno;
txdlgbox(MAX_DLG_LINES, 60, 7, WCENTER, attr_error_window,
attr_error_title, attr_error_highlight,
attr_error_normal, 0, attr_error_waitforkey,
_(" Game Not Saved "),
_("Game %d could not be saved to disk.\n\n"
"^{File %s: %s^}"), num, filename, strerror(saved_errno));
free(buf);
free(encbuf);
free(filename);
return false;
}
#ifdef USE_UTF8_GAME_FILE
// Make sure all strings are output in UTF-8 format for consistency
codeset = nl_langinfo(CODESET);
if (codeset == NULL) {
errno_exit("nl_langinfo(CODESET)");
}
need_icd = (strcmp(codeset, GAME_FILE_CHARSET) != 0);
if (need_icd) {
icd = iconv_open(GAME_FILE_CHARSET, codeset);
if (icd == (iconv_t) -1) {
errno_exit("iconv_open");
}
} else {
icd = (iconv_t) -1;
}
codeset = GAME_FILE_CHARSET; // Now contains output codeset
#else // ! USE_UTF8_GAME_FILE
// Make sure all strings are output in the correct codeset
codeset = nl_langinfo(CODESET);
if (codeset == NULL) {
errno_exit("nl_langinfo(CODESET)");
}
#endif // ! USE_UTF8_GAME_FILE
// Change the formatting of numbers to the POSIX locale for consistency
prev_locale = xstrdup(setlocale(LC_NUMERIC, NULL));
setlocale(LC_NUMERIC, "C");
// Write out the game file header and encryption status
fprintf(file, "%s\n" "%s\n", GAME_FILE_HEADER, GAME_FILE_API_VERSION);
fprintf(file, "%s\n" "%d\n", codeset, ! option_dont_encrypt);
// Write out various game variables
save_game_write_int(MAX_X);
save_game_write_int(MAX_Y);
save_game_write_int(max_turn);
save_game_write_int(turn_number);
save_game_write_int(number_players);
save_game_write_int(current_player);
save_game_write_int(first_player);
save_game_write_int(MAX_COMPANIES);
save_game_write_double(interest_rate);
// Write out player data
for (i = 0; i < number_players; i++) {
save_game_write_string(player[i].name, player[i].name_utf8);
save_game_write_double(player[i].cash);
save_game_write_double(player[i].debt);
save_game_write_bool(player[i].in_game);
for (j = 0; j < MAX_COMPANIES; j++) {
save_game_write_long(player[i].stock_owned[j]);
}
}
// Write out company data
for (i = 0; i < MAX_COMPANIES; i++) {
save_game_write_double(company[i].share_price);
save_game_write_double(company[i].share_return);
save_game_write_long(company[i].stock_issued);
save_game_write_long(company[i].max_stock);
save_game_write_bool(company[i].on_map);
}
// Write out galaxy map
for (x = 0; x < MAX_X; x++) {
char *p;
memset(buf, 0, MAX_Y + 2);
for (p = buf, y = 0; y < MAX_Y; p++, y++) {
*p = (char) galaxy_map[x][y];
}
*p++ = '\n';
*p = '\0';
scramble(encbuf, buf, BIGBUFSIZE, crypt_key_p);
fprintf(file, "%s", encbuf);
}
// Write out a dummy sentinel value
save_game_write_int(GAME_FILE_SENTINEL);
if (fclose(file) == EOF) {
errno_exit("%s", filename);
}
// Change the formatting of numbers back to the user-supplied locale
setlocale(LC_NUMERIC, prev_locale);
#ifdef USE_UTF8_GAME_FILE
if (need_icd) {
iconv_close(icd);
}
#endif
free(buf);
free(encbuf);
free(filename);
free(prev_locale);
return true;
}
/***********************************************************************/
// End of file