571 lines
16 KiB
C++
571 lines
16 KiB
C++
/* 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 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 <http://www.gnu.org/licenses/>.
|
|
*
|
|
*/
|
|
|
|
#include "glk/adrift/scare.h"
|
|
#include "glk/adrift/scprotos.h"
|
|
#include "glk/adrift/serialization.h"
|
|
|
|
namespace Glk {
|
|
namespace Adrift {
|
|
|
|
/* Assorted definitions and constants. */
|
|
static const sc_uint MEMENTO_MAGIC = 0x9fd33d1d;
|
|
enum { MEMO_ALLOCATION_BLOCK = 32 };
|
|
|
|
/*
|
|
* Game memo structure, saves a serialized game. Allocation is preserved so
|
|
* that structures can be reused without requiring reallocation.
|
|
*/
|
|
struct sc_memo_s {
|
|
sc_byte *serialized_game;
|
|
sc_int allocation;
|
|
sc_int length;
|
|
};
|
|
typedef sc_memo_s sc_memo_t;
|
|
typedef sc_memo_t *sc_memoref_t;
|
|
|
|
/*
|
|
* Game command history structure, similar to a memo. Saves a player input
|
|
* command to create a history, reusing allocation where possible.
|
|
*/
|
|
struct sc_history_s {
|
|
sc_char *command;
|
|
sc_int sequence;
|
|
sc_int timestamp;
|
|
sc_int turns;
|
|
sc_int allocation;
|
|
sc_int length;
|
|
};
|
|
typedef sc_history_s sc_history_t;
|
|
typedef sc_history_t *sc_historyref_t;
|
|
|
|
/*
|
|
* Memo set structure. This reserves space for a predetermined number of
|
|
* serialized games, and an indicator cursor showing where additions are
|
|
* placed. The structure is a ring, with old elements being overwritten by
|
|
* newer arrivals. Also tacked onto this structure is a set of strings
|
|
* used to hold a command history that operates in a somewhat csh-like way,
|
|
* also a ring with limited capacity.
|
|
*/
|
|
enum { MEMO_UNDO_TABLE_SIZE = 16, MEMO_HISTORY_TABLE_SIZE = 64 };
|
|
struct sc_memo_set_s {
|
|
sc_uint magic;
|
|
sc_memo_t memo[MEMO_UNDO_TABLE_SIZE];
|
|
sc_int memo_cursor;
|
|
|
|
sc_history_t history[MEMO_HISTORY_TABLE_SIZE];
|
|
sc_int history_count;
|
|
sc_int current_history;
|
|
sc_bool is_at_start;
|
|
};
|
|
typedef sc_memo_set_s sc_memo_set_t;
|
|
|
|
|
|
/*
|
|
* memo_is_valid()
|
|
*
|
|
* Return TRUE if pointer is a valid memo set, FALSE otherwise.
|
|
*/
|
|
static sc_bool memo_is_valid(sc_memo_setref_t memento) {
|
|
return memento && memento->magic == MEMENTO_MAGIC;
|
|
}
|
|
|
|
|
|
/*
|
|
* memo_round_up()
|
|
*
|
|
* Round up an allocation in bytes to the next allocation block.
|
|
*/
|
|
static sc_int memo_round_up(sc_int allocation) {
|
|
sc_int extended;
|
|
|
|
extended = allocation + MEMO_ALLOCATION_BLOCK - 1;
|
|
return (extended / MEMO_ALLOCATION_BLOCK) * MEMO_ALLOCATION_BLOCK;
|
|
}
|
|
|
|
|
|
/*
|
|
* memo_create()
|
|
*
|
|
* Create and return a new set of memos.
|
|
*/
|
|
sc_memo_setref_t memo_create(void) {
|
|
sc_memo_setref_t memento;
|
|
|
|
/* Create and initialize a clean set of memos. */
|
|
memento = (sc_memo_setref_t)sc_malloc(sizeof(*memento));
|
|
memento->magic = MEMENTO_MAGIC;
|
|
|
|
memset(memento->memo, 0, sizeof(memento->memo));
|
|
memento->memo_cursor = 0;
|
|
|
|
memset(memento->history, 0, sizeof(memento->history));
|
|
memento->history_count = 0;
|
|
memento->current_history = 0;
|
|
memento->is_at_start = FALSE;
|
|
|
|
return memento;
|
|
}
|
|
|
|
|
|
/*
|
|
* memo_destroy()
|
|
*
|
|
* Destroy a memo set, and free its heap memory.
|
|
*/
|
|
void memo_destroy(sc_memo_setref_t memento) {
|
|
sc_int index_;
|
|
assert(memo_is_valid(memento));
|
|
|
|
/* Free the content of any used memo and any used history. */
|
|
for (index_ = 0; index_ < MEMO_UNDO_TABLE_SIZE; index_++) {
|
|
sc_memoref_t memo;
|
|
|
|
memo = memento->memo + index_;
|
|
sc_free(memo->serialized_game);
|
|
}
|
|
for (index_ = 0; index_ < MEMO_HISTORY_TABLE_SIZE; index_++) {
|
|
sc_historyref_t history;
|
|
|
|
history = memento->history + index_;
|
|
sc_free(history->command);
|
|
}
|
|
|
|
/* Poison and free the memo set itself. */
|
|
memset(memento, 0xaa, sizeof(*memento));
|
|
sc_free(memento);
|
|
}
|
|
|
|
|
|
/*
|
|
* memo_save_game_callback()
|
|
*
|
|
* Callback function for game serialization. Appends the data passed in to
|
|
* that already stored in the memo.
|
|
*/
|
|
static void memo_save_game_callback(void *opaque, const sc_byte *buffer, sc_int length) {
|
|
sc_memoref_t memo = (sc_memoref_t)opaque;
|
|
sc_int required;
|
|
assert(opaque && buffer && length > 0);
|
|
|
|
/*
|
|
* If necessary, increase the allocation for this memo. Serialized games
|
|
* tend to grow slightly as the game progresses, so we add a bit of extra
|
|
* to the actual allocation.
|
|
*/
|
|
required = memo->length + length;
|
|
if (required > memo->allocation) {
|
|
required = memo_round_up(required + 2 * MEMO_ALLOCATION_BLOCK);
|
|
memo->serialized_game = (sc_byte *)sc_realloc(memo->serialized_game, required);
|
|
memo->allocation = required;
|
|
}
|
|
|
|
/* Add this block of data to the buffer. */
|
|
memcpy(memo->serialized_game + memo->length, buffer, length);
|
|
memo->length += length;
|
|
}
|
|
|
|
|
|
/*
|
|
* memo_save_game()
|
|
*
|
|
* Store a game in the next memo slot.
|
|
*/
|
|
void memo_save_game(sc_memo_setref_t memento, sc_gameref_t game) {
|
|
sc_memoref_t memo;
|
|
assert(memo_is_valid(memento));
|
|
|
|
/*
|
|
* If the current slot is in use, we can re-use its allocation. Saved
|
|
* games will tend to be of roughly equal sizes, so it's worth doing.
|
|
*/
|
|
memo = memento->memo + memento->memo_cursor;
|
|
memo->length = 0;
|
|
|
|
/* Serialize the given game into this memo. */
|
|
SaveSerializer ser(game, memo_save_game_callback, memo);
|
|
ser.save();
|
|
|
|
/*
|
|
* If serialization worked (failure would be a surprise), advance the
|
|
* current memo cursor.
|
|
*/
|
|
if (memo->length > 0) {
|
|
memento->memo_cursor++;
|
|
memento->memo_cursor %= MEMO_UNDO_TABLE_SIZE;
|
|
} else
|
|
sc_error("memo_save_game: warning: game save failed\n");
|
|
}
|
|
|
|
|
|
/*
|
|
* memo_load_game_callback()
|
|
*
|
|
* Callback function for game deserialization. Returns data from the memo
|
|
* until it's drained.
|
|
*/
|
|
static sc_int memo_load_game_callback(void *opaque, sc_byte *buffer, sc_int length) {
|
|
sc_memoref_t memo = (sc_memoref_t)opaque;
|
|
sc_int bytes;
|
|
assert(opaque && buffer && length > 0);
|
|
|
|
/* Send back either all the bytes, or as many as the buffer allows. */
|
|
bytes = (memo->length < length) ? memo->length : length;
|
|
|
|
/* Read and remove the first block of data (or all if less than length). */
|
|
memcpy(buffer, memo->serialized_game, bytes);
|
|
memmove(memo->serialized_game,
|
|
memo->serialized_game + bytes, memo->length - bytes);
|
|
memo->length -= bytes;
|
|
|
|
/* Return the count of bytes placed in the buffer. */
|
|
return bytes;
|
|
}
|
|
|
|
|
|
/*
|
|
* memo_load_game()
|
|
*
|
|
* Restore a game from the last memo slot used, if possible.
|
|
*/
|
|
sc_bool memo_load_game(sc_memo_setref_t memento, sc_gameref_t game) {
|
|
sc_int cursor;
|
|
sc_memoref_t memo;
|
|
assert(memo_is_valid(memento));
|
|
|
|
/* Look back one from the current memo cursor. */
|
|
cursor = (memento->memo_cursor == 0)
|
|
? MEMO_UNDO_TABLE_SIZE - 1 : memento->memo_cursor - 1;
|
|
memo = memento->memo + cursor;
|
|
|
|
/* If this slot is not empty, restore the serialized game held in it. */
|
|
if (memo->length > 0) {
|
|
sc_bool status;
|
|
|
|
/*
|
|
* Deserialize the given game from this memo; failure would be somewhat
|
|
* of a surprise here.
|
|
*/
|
|
LoadSerializer ser(game, memo_load_game_callback, memo);
|
|
status = ser.load();
|
|
if (!status)
|
|
sc_error("memo_load_game: warning: game load failed\n");
|
|
|
|
/*
|
|
* This should have drained the memo of all data, but to be sure that
|
|
* there's no chance of trying to restore from this slot again, we'll
|
|
* force it anyway.
|
|
*/
|
|
if (memo->length > 0) {
|
|
sc_error("memo_load_game: warning: data remains after loading\n");
|
|
memo->length = 0;
|
|
}
|
|
|
|
/* Regress current memo, and return TRUE if we restored a memo. */
|
|
memento->memo_cursor = cursor;
|
|
return status;
|
|
}
|
|
|
|
/* There are no more memos to restore. */
|
|
return FALSE;
|
|
}
|
|
|
|
|
|
/*
|
|
* memo_is_load_available()
|
|
*
|
|
* Returns TRUE if a memo restore is likely to succeed if called, FALSE
|
|
* otherwise.
|
|
*/
|
|
sc_bool memo_is_load_available(sc_memo_setref_t memento) {
|
|
sc_int cursor;
|
|
sc_memoref_t memo;
|
|
assert(memo_is_valid(memento));
|
|
|
|
/*
|
|
* Look back one from the current memo cursor. Return TRUE if this slot
|
|
* contains a serialized game.
|
|
*/
|
|
cursor = (memento->memo_cursor == 0)
|
|
? MEMO_UNDO_TABLE_SIZE - 1 : memento->memo_cursor - 1;
|
|
memo = memento->memo + cursor;
|
|
return memo->length > 0;
|
|
}
|
|
|
|
|
|
/*
|
|
* memo_clear_games()
|
|
*
|
|
* Forget the memos of saved games.
|
|
*/
|
|
void memo_clear_games(sc_memo_setref_t memento) {
|
|
sc_int index_;
|
|
assert(memo_is_valid(memento));
|
|
|
|
/* Deallocate every entry. */
|
|
for (index_ = 0; index_ < MEMO_UNDO_TABLE_SIZE; index_++) {
|
|
sc_memoref_t memo;
|
|
|
|
memo = memento->memo + index_;
|
|
sc_free(memo->serialized_game);
|
|
}
|
|
|
|
/* Reset all entries and the cursor. */
|
|
memset(memento->memo, 0, sizeof(memento->memo));
|
|
memento->memo_cursor = 0;
|
|
}
|
|
|
|
|
|
/*
|
|
* memo_save_command()
|
|
*
|
|
* Store a player command in the command history, evicting any least recently
|
|
* used item if necessary.
|
|
*/
|
|
void memo_save_command(sc_memo_setref_t memento, const sc_char *command, sc_int timestamp, sc_int turns) {
|
|
sc_historyref_t history;
|
|
sc_int length;
|
|
assert(memo_is_valid(memento));
|
|
|
|
/* As with memos, reuse the allocation of the next slot if it has one. */
|
|
history = memento->history
|
|
+ memento->history_count % MEMO_HISTORY_TABLE_SIZE;
|
|
|
|
/*
|
|
* Resize the allocation for this slot if required. Strings tend to be
|
|
* short, so round up to a block to avoid too many reallocs.
|
|
*/
|
|
length = strlen(command) + 1;
|
|
if (history->allocation < length) {
|
|
sc_int required;
|
|
|
|
required = memo_round_up(length);
|
|
history->command = (sc_char *)sc_realloc(history->command, required);
|
|
history->allocation = required;
|
|
}
|
|
|
|
/* Save the string into this slot, and normalize it for neatness. */
|
|
Common::strcpy_s(history->command, history->allocation, command);
|
|
sc_normalize_string(history->command);
|
|
history->sequence = memento->history_count + 1;
|
|
history->timestamp = timestamp;
|
|
history->turns = turns;
|
|
history->length = length;
|
|
|
|
/* Increment the count of histories handled. */
|
|
memento->history_count++;
|
|
}
|
|
|
|
|
|
/*
|
|
* memo_unsave_command()
|
|
*
|
|
* Remove the last saved command. This is special functionality for the
|
|
* history lister. To keep synchronized with the runner main loop, it needs
|
|
* to "invent" a history item at the end of the list before listing, then
|
|
* remove it again as the main runner loop will add the real thing.
|
|
*/
|
|
void memo_unsave_command(sc_memo_setref_t memento) {
|
|
assert(memo_is_valid(memento));
|
|
|
|
/* Do nothing if for some reason there's no history to unsave. */
|
|
if (memento->history_count > 0) {
|
|
sc_historyref_t history;
|
|
|
|
/* Decrement the count of histories handled, erase the prior entry. */
|
|
memento->history_count--;
|
|
history = memento->history
|
|
+ memento->history_count % MEMO_HISTORY_TABLE_SIZE;
|
|
history->sequence = 0;
|
|
history->timestamp = 0;
|
|
history->turns = 0;
|
|
history->length = 0;
|
|
}
|
|
}
|
|
|
|
|
|
/*
|
|
* memo_get_command_count()
|
|
*
|
|
* Return a count of available saved commands.
|
|
*/
|
|
sc_int memo_get_command_count(sc_memo_setref_t memento) {
|
|
assert(memo_is_valid(memento));
|
|
|
|
/* Return the lesser of the history count and the history table size. */
|
|
if (memento->history_count < MEMO_HISTORY_TABLE_SIZE)
|
|
return memento->history_count;
|
|
else
|
|
return MEMO_HISTORY_TABLE_SIZE;
|
|
}
|
|
|
|
|
|
/*
|
|
* memo_first_command()
|
|
*
|
|
* Iterator rewind function, reset current location to the first command.
|
|
*/
|
|
void memo_first_command(sc_memo_setref_t memento) {
|
|
sc_int cursor;
|
|
sc_historyref_t history;
|
|
assert(memo_is_valid(memento));
|
|
|
|
/*
|
|
* If the buffer has cycled, we have the full complement of saved commands,
|
|
* so start iterating at the current cursor. Otherwise, start from index 0.
|
|
* Detect cycling by looking at the current slot; if it's filled, we've
|
|
* been here before. Set at_start flag to indicate the special case for
|
|
* circular buffers.
|
|
*/
|
|
cursor = memento->history_count % MEMO_HISTORY_TABLE_SIZE;
|
|
history = memento->history + cursor;
|
|
memento->current_history = (history->length > 0) ? cursor : 0;
|
|
memento->is_at_start = TRUE;
|
|
}
|
|
|
|
|
|
/*
|
|
* memo_next_command()
|
|
*
|
|
* Iterator function, return the next saved command and its sequence id
|
|
* starting at 1, and the timestamp and turns when the command was saved.
|
|
*/
|
|
void memo_next_command(sc_memo_setref_t memento, const sc_char **command,
|
|
sc_int *sequence, sc_int *timestamp, sc_int *turns) {
|
|
assert(memo_is_valid(memento));
|
|
|
|
/* If valid, return the current command and advance. */
|
|
if (memo_more_commands(memento)) {
|
|
sc_historyref_t history;
|
|
|
|
/* Note the current history, and advance its index. */
|
|
history = memento->history + memento->current_history;
|
|
memento->current_history++;
|
|
memento->current_history %= MEMO_HISTORY_TABLE_SIZE;
|
|
memento->is_at_start = FALSE;
|
|
|
|
/* Return details from the history noted above. */
|
|
*command = history->command;
|
|
*sequence = history->sequence;
|
|
*timestamp = history->timestamp;
|
|
*turns = history->turns;
|
|
} else {
|
|
/* Return NULL and zeroes if no more commands available. */
|
|
*command = nullptr;
|
|
*sequence = 0;
|
|
*timestamp = 0;
|
|
*turns = 0;
|
|
}
|
|
}
|
|
|
|
|
|
/*
|
|
* memo_more_commands()
|
|
*
|
|
* Iterator end function, returns TRUE if more commands are readable.
|
|
*/
|
|
sc_bool memo_more_commands(sc_memo_setref_t memento) {
|
|
sc_int cursor;
|
|
sc_historyref_t history;
|
|
assert(memo_is_valid(memento));
|
|
|
|
/* Get the current effective write position, and the current history. */
|
|
cursor = memento->history_count % MEMO_HISTORY_TABLE_SIZE;
|
|
history = memento->history + memento->current_history;
|
|
|
|
/*
|
|
* More data if the current history is behind the write position and is
|
|
* occupied, or if it matches and is occupied and we're at the start of
|
|
* iteration (circular buffer special case).
|
|
*/
|
|
if (memento->current_history == cursor)
|
|
return (memento->is_at_start) ? history->length > 0 : FALSE;
|
|
else
|
|
return history->length > 0;
|
|
}
|
|
|
|
|
|
/*
|
|
* memo_find_command()
|
|
*
|
|
* Find and return the command string for the given sequence number (-ve
|
|
* indicates an offset from the last defined), or NULL if not found.
|
|
*/
|
|
const sc_char *memo_find_command(sc_memo_setref_t memento, sc_int sequence) {
|
|
sc_int target, index_;
|
|
sc_historyref_t matched;
|
|
assert(memo_is_valid(memento));
|
|
|
|
/* Decide on a search target, depending on the sign of sequence. */
|
|
target = (sequence < 0) ? memento->history_count + sequence + 1 : sequence;
|
|
|
|
/*
|
|
* A backwards search starting at the write position would probably be more
|
|
* efficient here, but this is a rarely called function so we'll do it the
|
|
* simpler way.
|
|
*/
|
|
matched = nullptr;
|
|
for (index_ = 0; index_ < MEMO_HISTORY_TABLE_SIZE; index_++) {
|
|
sc_historyref_t history;
|
|
|
|
history = memento->history + index_;
|
|
if (history->sequence == target) {
|
|
matched = history;
|
|
break;
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Return the command or NULL. If sequence passed in was zero, and the
|
|
* history was not full, this will still return NULL as it should, since
|
|
* this unused history's command found by the search above will be NULL.
|
|
*/
|
|
return matched ? matched->command : nullptr;
|
|
}
|
|
|
|
|
|
/*
|
|
* memo_clear_commands()
|
|
*
|
|
* Forget all saved commands.
|
|
*/
|
|
void memo_clear_commands(sc_memo_setref_t memento) {
|
|
sc_int index_;
|
|
assert(memo_is_valid(memento));
|
|
|
|
/* Deallocate every entry. */
|
|
for (index_ = 0; index_ < MEMO_HISTORY_TABLE_SIZE; index_++) {
|
|
sc_historyref_t history;
|
|
|
|
history = memento->history + index_;
|
|
sc_free(history->command);
|
|
}
|
|
|
|
/* Reset all entries, the count, and the iteration variables. */
|
|
memset(memento->history, 0, sizeof(memento->history));
|
|
memento->history_count = 0;
|
|
memento->current_history = 0;
|
|
memento->is_at_start = FALSE;
|
|
}
|
|
|
|
} // End of namespace Adrift
|
|
} // End of namespace Glk
|