/* ScummVM - Graphic Adventure Engine * * ScummVM is the legal property of its developers, whose names * are too numerous to list here. Please refer to the COPYRIGHT * file distributed with this source distribution. * * 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, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * */ #include "glk/adrift/serialization.h" #include "glk/adrift/scprotos.h" #include "glk/adrift/scgamest.h" namespace Glk { namespace Adrift { /* Assorted definitions and constants. */ static const sc_char NEWLINE = '\n'; static const sc_char CARRIAGE_RETURN = '\r'; static const sc_char NUL = '\0'; enum { BUFFER_SIZE = 4096 }; void SaveSerializer::save() { const sc_var_setref_t vars = gs_get_vars(_game); const sc_prop_setref_t bundle = gs_get_bundle(_game); sc_vartype_t vt_key[3]; sc_int index_, var_count; // Write the _game name vt_key[0].string = "Globals"; vt_key[1].string = "GameName"; writeString(prop_get_string(bundle, "S<-ss", vt_key)); /* Write the counts of rooms, objects, etc. */ writeInt(gs_room_count(_game)); writeInt(gs_object_count(_game)); writeInt(gs_task_count(_game)); writeInt(gs_event_count(_game)); writeInt(gs_npc_count(_game)); /* Write the score and player information. */ writeInt(_game->score); writeInt(gs_playerroom(_game) + 1); writeInt(gs_playerparent(_game)); writeInt(gs_playerposition(_game)); /* Write player gender. */ vt_key[0].string = "Globals"; vt_key[1].string = "PlayerGender"; writeInt(prop_get_integer(bundle, "I<-ss", vt_key)); /* * Write encumbrance details. The player limits are constant for a given * _game, and can be extracted from properties. The current sizes and * weights can also be recalculated from held objects, so we don't maintain * them in the _game. We can write constants here, then, and ignore * the values on restoring. Note however that if the Adrift Runner is * relying on these values, this may give it problems with one of our saved * games. */ writeInt(90); writeInt(0); writeInt(90); writeInt(0); /* Save rooms information. */ for (index_ = 0; index_ < gs_room_count(_game); index_++) writeBool(gs_room_seen(_game, index_)); /* Save objects information. */ for (index_ = 0; index_ < gs_object_count(_game); index_++) { writeInt(gs_object_position(_game, index_)); writeBool(gs_object_seen(_game, index_)); writeInt(gs_object_parent(_game, index_)); if (gs_object_openness(_game, index_) != 0) writeInt(gs_object_openness(_game, index_)); if (gs_object_state(_game, index_) != 0) writeInt(gs_object_state(_game, index_)); writeBool(gs_object_unmoved(_game, index_)); } /* Save tasks information. */ for (index_ = 0; index_ < gs_task_count(_game); index_++) { writeBool(gs_task_done(_game, index_)); writeBool(gs_task_scored(_game, index_)); } /* Save events information. */ for (index_ = 0; index_ < gs_event_count(_game); index_++) { sc_int startertype, task; /* Get starter task, if any. */ vt_key[0].string = "Events"; vt_key[1].integer = index_; vt_key[2].string = "StarterType"; startertype = prop_get_integer(bundle, "I<-sis", vt_key); if (startertype == 3) { vt_key[2].string = "TaskNum"; task = prop_get_integer(bundle, "I<-sis", vt_key); } else task = 0; /* Save event details. */ writeInt(gs_event_time(_game, index_)); writeInt(task); writeInt(gs_event_state(_game, index_) - 1); if (task > 0) writeBool(gs_task_done(_game, task - 1)); else writeBool(false); } /* Save NPCs information. */ for (index_ = 0; index_ < gs_npc_count(_game); index_++) { sc_int walk; writeInt(gs_npc_location(_game, index_)); writeBool(gs_npc_seen(_game, index_)); for (walk = 0; walk < gs_npc_walkstep_count(_game, index_); walk++) writeIntSpecial(gs_npc_walkstep(_game, index_, walk)); } /* Save each variable. */ vt_key[0].string = "Variables"; var_count = prop_get_child_count(bundle, "I<-s", vt_key); for (index_ = 0; index_ < var_count; index_++) { const sc_char *name; sc_int var_type; vt_key[1].integer = index_; vt_key[2].string = "Name"; name = prop_get_string(bundle, "S<-sis", vt_key); vt_key[2].string = "Type"; var_type = prop_get_integer(bundle, "I<-sis", vt_key); switch (var_type) { case TAFVAR_NUMERIC: writeInt(var_get_integer(vars, name)); break; case TAFVAR_STRING: writeString(var_get_string(vars, name)); break; default: sc_fatal("ser_save_game: unknown variable type, %ld\n", var_type); } } /* Save timing information. */ writeUint(var_get_elapsed_seconds(vars)); /* Save turns count. */ writeUint((sc_uint)_game->turns); /* * Flush the last buffer contents, and drop the callback and opaque * references. */ flush(TRUE); _callback = nullptr; _opaque = nullptr; } void SaveSerializer::flush(sc_bool is_final) { if (is_final) { _callback(_opaque, _buffer.getData(), _buffer.size()); } } void SaveSerializer::writeChar(sc_char character) { // Validate the buffer hasn't exceeded the maximum allowable size if (_buffer.size() == BUFFER_SIZE) sc_error("Ran out of serialization buffer"); // Add to the buffer _buffer.writeByte(character); } void SaveSerializer::write(const sc_char *buffer, sc_int length) { // Add each character to the buffer for (int idx = 0; idx < length; ++idx) writeChar(buffer[idx]); } void SaveSerializer::writeString(const sc_char *string) { // Write string, followed by DOS style end-of-line write(string, strlen(string)); writeChar(CARRIAGE_RETURN); writeChar(NEWLINE); } void SaveSerializer::writeInt(sc_int value) { Common::String s = Common::String::format("%ld", value); writeString(s.c_str()); } void SaveSerializer::writeIntSpecial(sc_int value) { Common::String s = Common::String::format("% ld ", value); writeString(s.c_str()); } void SaveSerializer::writeUint(sc_uint value) { Common::String s = Common::String::format("%lu", value); writeString(s.c_str()); } void SaveSerializer::writeBool(sc_bool boolean) { // Write a 1 for TRUE, 0 for false writeString(boolean ? "1" : "0"); } /*--------------------------------------------------------------------------*/ #define CHECK if (context._break) goto ser_tas_error bool LoadSerializer::load() { const sc_filterref_t filter = gs_get_filter(_game); const sc_prop_setref_t bundle = gs_get_bundle(_game); sc_vartype_t vt_key[3]; sc_int index_, var_count; const sc_char *gamename; sc_var_setref_t new_vars = nullptr; sc_gameref_t new_game = nullptr; Context context; sc_int count = 0; // Create a TAF (TAS) reference from callbacks, for reader functions ser_tas = taf_create_tas(_callback, _opaque); if (!ser_tas) return false; // Reset line counter for error messages. ser_tasline = 1; // Read the _game name, and compare with the one in the _game. Fail if they don't match exactly. // A tighter check than this would perhaps be preferable, say, something based on the TAF file // header, but this isn't in the save file format. vt_key[0].string = "Globals"; vt_key[1].string = "GameName"; gamename = prop_get_string(bundle, "S<-ss", vt_key); if (strcmp(readString(context), gamename) != 0 || context._break) goto ser_tas_error; // Read and verify the counts in the saved _game. if ((readInt(context) != gs_room_count(_game) || context._break) || (readInt(context) != gs_object_count(_game) || context._break) || (readInt(context) != gs_task_count(_game) || context._break) || (readInt(context) != gs_event_count(_game) || context._break) || (readInt(context) != gs_npc_count(_game) || context._break)) goto ser_tas_error; // Create a variables set and _game to restore into. new_vars = var_create(bundle); new_game = gs_create(new_vars, bundle, filter); var_register_game(new_vars, new_game); // All set to load TAF (TAS) data into the new _game. // Restore the score and player information. new_game->score = readInt(context); CHECK; gs_set_playerroom(new_game, readInt(context) - 1); CHECK; gs_set_playerparent(new_game, readInt(context)); CHECK; gs_set_playerposition(new_game, readInt(context)); CHECK; // Skip player gender. (void)readInt(context); CHECK; // Skip encumbrance details, not currently maintained by the _game. (void)readInt(context); CHECK; (void)readInt(context); CHECK; (void)readInt(context); CHECK; (void)readInt(context); CHECK; // Restore rooms information count = gs_room_count(new_game); for (index_ = 0; index_ < count; ++index_) { gs_set_room_seen(new_game, index_, readBool(context)); CHECK; } // Restore objects information count = gs_object_count(new_game); for (index_ = 0; index_ < count; ++index_) { sc_int openable, currentstate; // Bypass mutators for position and parent. Fix later? new_game->objects[index_].position = readInt(context); CHECK; gs_set_object_seen(new_game, index_, readBool(context)); CHECK; new_game->objects[index_].parent = readInt(context); CHECK; vt_key[0].string = "Objects"; vt_key[1].integer = index_; vt_key[2].string = "Openable"; openable = prop_get_integer(bundle, "I<-sis", vt_key); gs_set_object_openness(new_game, index_, openable != 0 ? readInt(context) : 0); CHECK; vt_key[2].string = "CurrentState"; currentstate = prop_get_integer(bundle, "I<-sis", vt_key); gs_set_object_state(new_game, index_, currentstate != 0 ? readInt(context) : 0); CHECK; gs_set_object_unmoved(new_game, index_, readBool(context)); CHECK; } // Restore tasks information. for (index_ = 0; index_ < gs_task_count(new_game); index_++) { gs_set_task_done(new_game, index_, readBool(context)); CHECK; gs_set_task_scored(new_game, index_, readBool(context)); CHECK; } // Restore events information count = gs_event_count(new_game); for (index_ = 0; index_ < count; index_++) { sc_int startertype, task; // Restore first event details. gs_set_event_time(new_game, index_, readInt(context)); CHECK; task = readInt(context); CHECK; gs_set_event_state(new_game, index_, readInt(context) + 1); CHECK; // Verify and restore the starter task, if any. if (task > 0) { vt_key[0].string = "Events"; vt_key[1].integer = index_; vt_key[2].string = "StarterType"; startertype = prop_get_integer(bundle, "I<-sis", vt_key); if (startertype != 3) goto ser_tas_error; // Restore task state. gs_set_task_done(new_game, task - 1, readBool(context)); CHECK; } else { (void)readBool(context); CHECK; } } // Restore NPCs information count = gs_npc_count(new_game); for (index_ = 0; index_ < count; index_++) { sc_int walk; gs_set_npc_location(new_game, index_, readInt(context)); CHECK; gs_set_npc_seen(new_game, index_, readBool(context)); CHECK; for (walk = 0; walk < gs_npc_walkstep_count(new_game, index_); walk++) gs_set_npc_walkstep(new_game, index_, walk, readInt(context)); CHECK; } // Restore each variable. vt_key[0].string = "Variables"; var_count = prop_get_child_count(bundle, "I<-s", vt_key); for (index_ = 0; index_ < var_count; index_++) { const sc_char *name; sc_int var_type; vt_key[1].integer = index_; vt_key[2].string = "Name"; name = prop_get_string(bundle, "S<-sis", vt_key); vt_key[2].string = "Type"; var_type = prop_get_integer(bundle, "I<-sis", vt_key); switch (var_type) { case TAFVAR_NUMERIC: var_put_integer(new_vars, name, readInt(context)); CHECK; break; case TAFVAR_STRING: var_put_string(new_vars, name, readString(context)); CHECK; break; default: sc_fatal("ser_load_game: unknown variable type, %ld\n", var_type); } } // Restore timing information. var_set_elapsed_seconds(new_vars, readUint(context)); CHECK; // Restore turns count. new_game->turns = (sc_int)readUint(context); CHECK; /* Resources tweak -- set requested to match those in the current _game so that they remain * unchanged by the gs_copy() of new_game onto game. This way, both the requested and the * active resources in the game are unchanged by restore. */ new_game->requested_sound = _game->requested_sound; new_game->requested_graphic = _game->requested_graphic; /* If we got this far, we successfully restored the _game from the file. * As our final act, copy the new _game onto the old one. */ new_game->temporary = _game->temporary; new_game->undo = _game->undo; gs_copy(_game, new_game); // Done with the temporary _game and variables. gs_destroy(new_game); var_destroy(new_vars); // Done with TAF (TAS) file; destroy it and return successfully taf_destroy(ser_tas); return true; ser_tas_error: // Destroy any temporary _game and variables if (new_game) gs_destroy(new_game); if (new_vars) var_destroy(new_vars); // Destroy the TAF (TAS) file and return fail status taf_destroy(ser_tas); return false; } const sc_char *LoadSerializer::readString(CONTEXT) { const sc_char *string; /* Get the next line, and complain if absent. */ string = taf_next_line(ser_tas); if (!string) { sc_error("readString: out of TAS data at line %ld\n", ser_tasline); LONG_JUMP0 } ser_tasline++; return string; } sc_int LoadSerializer::readInt(CONTEXT) { const sc_char *string; sc_int value; // Get line, and scan for a single integer; return it R0FUNC0(readString, string) if (sscanf(string, "%ld", &value) != 1) { sc_error("readInt: invalid integer at line %ld\n", ser_tasline - 1); LONG_JUMP0 } return value; } sc_uint LoadSerializer::readUint(CONTEXT) { const sc_char *string; sc_uint value; // Get line, and scan for a single integer; return it R0FUNC0(readString, string) if (sscanf(string, "%lu", &value) != 1) { sc_error("readUint: invalid integer at line %ld\n", ser_tasline - 1); LONG_JUMP0 } return value; } sc_bool LoadSerializer::readBool(CONTEXT) { const sc_char *string; sc_uint value; // Get line, and scan for a single integer; check it's a valid-looking flag, and return it. R0FUNC0(readString, string) if (sscanf(string, "%lu", &value) != 1) { sc_error("readBool: invalid boolean at line %ld\n", ser_tasline - 1); LONG_JUMP0 } if (value != 0 && value != 1) { sc_error("readBool: warning: suspect boolean at line %ld\n", ser_tasline - 1); LONG_JUMP0 } return value != 0; } } // End of namespace Adrift } // End of namespace Glk