Files
2026-02-02 04:50:13 +01:00

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