mirror of
https://github.com/libretro/scummvm.git
synced 2024-12-12 20:17:49 +00:00
d2910fe758
that these are stopped whenever the engine is going to do a mass killing of game resources, e.g. when restoring or restarting the game. Should fix bug #1645480. (This was a regression added during the rewrite to support DXA.) svn-id: r25204
639 lines
17 KiB
C++
639 lines
17 KiB
C++
/* Copyright (C) 1994-1998 Revolution Software Ltd.
|
|
* Copyright (C) 2003-2006 The ScummVM project
|
|
*
|
|
* 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.
|
|
*
|
|
* $URL$
|
|
* $Id$
|
|
*/
|
|
|
|
#include "common/stdafx.h"
|
|
#include "common/file.h"
|
|
#include "common/system.h"
|
|
|
|
#include "sword2/sword2.h"
|
|
#include "sword2/defs.h"
|
|
#include "sword2/header.h"
|
|
#include "sword2/console.h"
|
|
#include "sword2/logic.h"
|
|
#include "sword2/memory.h"
|
|
#include "sword2/resman.h"
|
|
#include "sword2/router.h"
|
|
#include "sword2/screen.h"
|
|
#include "sword2/sound.h"
|
|
|
|
#define Debug_Printf _vm->_debugger->DebugPrintf
|
|
|
|
namespace Sword2 {
|
|
|
|
// Welcome to the easy resource manager - written in simple code for easy
|
|
// maintenance
|
|
//
|
|
// The resource compiler will create two files
|
|
//
|
|
// resource.inf which is a list of ascii cluster file names
|
|
// resource.tab which is a table which tells us which cluster a resource
|
|
// is located in and the number within the cluster
|
|
|
|
enum {
|
|
BOTH = 0x0, // Cluster is on both CDs
|
|
CD1 = 0x1, // Cluster is on CD1 only
|
|
CD2 = 0x2, // Cluster is on CD2 only
|
|
LOCAL_CACHE = 0x4, // Cluster is cached on HDD
|
|
LOCAL_PERM = 0x8 // Cluster is on HDD.
|
|
};
|
|
|
|
struct CdInf {
|
|
uint8 clusterName[20]; // Null terminated cluster name.
|
|
uint8 cd; // Cd cluster is on and whether it is on the local drive or not.
|
|
};
|
|
|
|
ResourceManager::ResourceManager(Sword2Engine *vm) {
|
|
_vm = vm;
|
|
|
|
_totalClusters = 0;
|
|
_resList = NULL;
|
|
_resConvTable = NULL;
|
|
_cacheStart = NULL;
|
|
_cacheEnd = NULL;
|
|
_usedMem = 0;
|
|
}
|
|
|
|
ResourceManager::~ResourceManager() {
|
|
Resource *res = _cacheStart;
|
|
while (res) {
|
|
_vm->_memory->memFree(res->ptr);
|
|
res = res->next;
|
|
}
|
|
for (uint i = 0; i < _totalClusters; i++)
|
|
free(_resFiles[i].entryTab);
|
|
free(_resList);
|
|
free(_resConvTable);
|
|
}
|
|
|
|
|
|
bool ResourceManager::init() {
|
|
uint32 i, j;
|
|
|
|
// Until proven differently, assume we're on CD 1. This is so the start
|
|
// dialog will be able to play any music at all.
|
|
|
|
setCD(1);
|
|
|
|
// We read in the resource info which tells us the names of the
|
|
// resource cluster files ultimately, although there might be groups
|
|
// within the clusters at this point it makes no difference. We only
|
|
// wish to know what resource files there are and what is in each
|
|
|
|
Common::File file;
|
|
|
|
if (!file.open("resource.inf")) {
|
|
_vm->GUIErrorMessage("Broken Sword 2: Cannot open resource.inf");
|
|
return false;
|
|
}
|
|
|
|
// The resource.inf file is a simple text file containing the names of
|
|
// all the resource files.
|
|
|
|
while (file.readLine(_resFiles[_totalClusters].fileName, sizeof(_resFiles[_totalClusters].fileName))) {
|
|
_resFiles[_totalClusters].numEntries = -1;
|
|
_resFiles[_totalClusters].entryTab = NULL;
|
|
if (++_totalClusters >= MAX_res_files) {
|
|
_vm->GUIErrorMessage("Broken Sword 2: Too many entries in resource.inf");
|
|
return false;
|
|
}
|
|
}
|
|
|
|
file.close();
|
|
|
|
// Now load in the binary id to res conversion table
|
|
if (!file.open("resource.tab")) {
|
|
_vm->GUIErrorMessage("Broken Sword 2: Cannot open resource.tab");
|
|
return false;
|
|
}
|
|
|
|
// Find how many resources
|
|
uint32 size = file.size();
|
|
|
|
_totalResFiles = size / 4;
|
|
|
|
// Table seems ok so malloc some space
|
|
_resConvTable = (uint16 *)malloc(size);
|
|
|
|
for (i = 0; i < size / 2; i++)
|
|
_resConvTable[i] = file.readUint16LE();
|
|
|
|
if (file.ioFailed()) {
|
|
file.close();
|
|
_vm->GUIErrorMessage("Broken Sword 2: Cannot read resource.tab");
|
|
return false;
|
|
}
|
|
|
|
file.close();
|
|
|
|
if (!file.open("cd.inf")) {
|
|
_vm->GUIErrorMessage("Broken Sword 2: Cannot open cd.inf");
|
|
return false;
|
|
}
|
|
|
|
CdInf *cdInf = new CdInf[_totalClusters];
|
|
|
|
for (i = 0; i < _totalClusters; i++) {
|
|
file.read(cdInf[i].clusterName, sizeof(cdInf[i].clusterName));
|
|
|
|
cdInf[i].cd = file.readByte();
|
|
|
|
if (file.ioFailed()) {
|
|
delete cdInf;
|
|
file.close();
|
|
_vm->GUIErrorMessage("Broken Sword 2: Cannot read cd.inf");
|
|
return false;
|
|
}
|
|
|
|
// It has been reported that there are two different versions
|
|
// of the cd.inf file: One where all clusters on CD also have
|
|
// the LOCAL_CACHE bit set. This bit is no longer used. To
|
|
// avoid future problems, let's normalize the flag once and for
|
|
// all here.
|
|
|
|
if (cdInf[i].cd & LOCAL_PERM)
|
|
cdInf[i].cd = 0;
|
|
else if (cdInf[i].cd & CD1)
|
|
cdInf[i].cd = 1;
|
|
else if (cdInf[i].cd & CD2)
|
|
cdInf[i].cd = 2;
|
|
else
|
|
cdInf[i].cd = 0;
|
|
|
|
// Any file on "CD 0" may be needed at all times. Verify that
|
|
// it exists. Any other missing cluster will be requested with
|
|
// an "insert CD" message. Of course, the file may still vanish
|
|
// during game-play (oh, that wascally wabbit!) in which case
|
|
// the resource manager will print a fatal error.
|
|
|
|
if (cdInf[i].cd == 0 && !Common::File::exists((char *)cdInf[i].clusterName)) {
|
|
_vm->GUIErrorMessage("Broken Sword 2: Cannot find " + Common::String((char *)cdInf[i].clusterName));
|
|
delete [] cdInf;
|
|
return false;
|
|
}
|
|
}
|
|
|
|
file.close();
|
|
|
|
for (i = 0; i < _totalClusters; i++) {
|
|
for (j = 0; j < _totalClusters; j++) {
|
|
if (scumm_stricmp((char *)cdInf[j].clusterName, _resFiles[i].fileName) == 0)
|
|
break;
|
|
}
|
|
|
|
if (j == _totalClusters) {
|
|
delete [] cdInf;
|
|
_vm->GUIErrorMessage(Common::String(_resFiles[i].fileName) + " is not in cd.inf");
|
|
return false;
|
|
}
|
|
|
|
_resFiles[i].cd = cdInf[j].cd;
|
|
}
|
|
|
|
delete [] cdInf;
|
|
|
|
debug(1, "%d resources in %d cluster files", _totalResFiles, _totalClusters);
|
|
for (i = 0; i < _totalClusters; i++)
|
|
debug(2, "filename of cluster %d: -%s (%d)", i, _resFiles[i].fileName, _resFiles[i].cd);
|
|
|
|
_resList = (Resource *)malloc(_totalResFiles * sizeof(Resource));
|
|
|
|
for (i = 0; i < _totalResFiles; i++) {
|
|
_resList[i].ptr = NULL;
|
|
_resList[i].size = 0;
|
|
_resList[i].refCount = 0;
|
|
_resList[i].prev = _resList[i].next = NULL;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Returns the address of a resource. Loads if not in memory. Retains a count.
|
|
*/
|
|
|
|
byte *ResourceManager::openResource(uint32 res, bool dump) {
|
|
assert(res < _totalResFiles);
|
|
|
|
// Is the resource in memory already? If not, load it.
|
|
|
|
if (!_resList[res].ptr) {
|
|
// Fetch the correct file and read in the correct portion.
|
|
uint16 cluFileNum = _resConvTable[res * 2]; // points to the number of the ascii filename
|
|
assert(cluFileNum != 0xffff);
|
|
|
|
// Relative resource within the file
|
|
// First we have to find the file via the _resConvTable
|
|
uint16 actual_res = _resConvTable[(res * 2) + 1];
|
|
|
|
debug(5, "openResource %s res %d", _resFiles[cluFileNum].fileName, res);
|
|
|
|
// If we're loading a cluster that's only available from one
|
|
// of the CDs, remember which one so that we can play the
|
|
// correct speech and music.
|
|
|
|
setCD(_resFiles[cluFileNum].cd);
|
|
|
|
// Actually, as long as the file can be found we don't really
|
|
// care which CD it's on. But if we can't find it, keep asking
|
|
// for the CD until we do.
|
|
|
|
Common::File *file = openCluFile(cluFileNum);
|
|
|
|
if (_resFiles[cluFileNum].entryTab == NULL) {
|
|
// we didn't read from this file before, get its index table
|
|
readCluIndex(cluFileNum, file);
|
|
}
|
|
|
|
uint32 pos = _resFiles[cluFileNum].entryTab[actual_res * 2 + 0];
|
|
uint32 len = _resFiles[cluFileNum].entryTab[actual_res * 2 + 1];
|
|
|
|
file->seek(pos, SEEK_SET);
|
|
|
|
debug(6, "res len %d", len);
|
|
|
|
// Ok, we know the length so try and allocate the memory.
|
|
_resList[res].ptr = _vm->_memory->memAlloc(len, res);
|
|
_resList[res].size = len;
|
|
_resList[res].refCount = 0;
|
|
|
|
file->read(_resList[res].ptr, len);
|
|
|
|
debug(3, "Loaded resource '%s' (%d) from '%s' on CD %d (%d)", fetchName(_resList[res].ptr), res, _resFiles[cluFileNum].fileName, getCD(), _resFiles[cluFileNum].cd);
|
|
|
|
if (dump) {
|
|
char buf[256];
|
|
const char *tag;
|
|
Common::File out;
|
|
|
|
switch (fetchType(_resList[res].ptr)) {
|
|
case ANIMATION_FILE:
|
|
tag = "anim";
|
|
break;
|
|
case SCREEN_FILE:
|
|
tag = "layer";
|
|
break;
|
|
case GAME_OBJECT:
|
|
tag = "object";
|
|
break;
|
|
case WALK_GRID_FILE:
|
|
tag = "walkgrid";
|
|
break;
|
|
case GLOBAL_VAR_FILE:
|
|
tag = "globals";
|
|
break;
|
|
case PARALLAX_FILE_null:
|
|
tag = "parallax"; // Not used!
|
|
break;
|
|
case RUN_LIST:
|
|
tag = "runlist";
|
|
break;
|
|
case TEXT_FILE:
|
|
tag = "text";
|
|
break;
|
|
case SCREEN_MANAGER:
|
|
tag = "screen";
|
|
break;
|
|
case MOUSE_FILE:
|
|
tag = "mouse";
|
|
break;
|
|
case WAV_FILE:
|
|
tag = "wav";
|
|
break;
|
|
case ICON_FILE:
|
|
tag = "icon";
|
|
break;
|
|
case PALETTE_FILE:
|
|
tag = "palette";
|
|
break;
|
|
default:
|
|
tag = "unknown";
|
|
break;
|
|
}
|
|
|
|
#if defined(MACOS_CARBON)
|
|
sprintf(buf, ":dumps:%s-%d.dmp", tag, res);
|
|
#else
|
|
sprintf(buf, "dumps/%s-%d.dmp", tag, res);
|
|
#endif
|
|
|
|
if (!Common::File::exists(buf)) {
|
|
if (out.open(buf, Common::File::kFileWriteMode))
|
|
out.write(_resList[res].ptr, len);
|
|
}
|
|
}
|
|
|
|
// close the cluster
|
|
file->close();
|
|
delete file;
|
|
|
|
_usedMem += len;
|
|
checkMemUsage();
|
|
} else if (_resList[res].refCount == 0)
|
|
removeFromCacheList(_resList + res);
|
|
|
|
_resList[res].refCount++;
|
|
|
|
return _resList[res].ptr;
|
|
}
|
|
|
|
void ResourceManager::closeResource(uint32 res) {
|
|
assert(res < _totalResFiles);
|
|
|
|
// Don't try to close the resource if it has already been forcibly
|
|
// closed, e.g. by fnResetGlobals().
|
|
|
|
if (_resList[res].ptr == NULL)
|
|
return;
|
|
|
|
assert(_resList[res].refCount > 0);
|
|
|
|
_resList[res].refCount--;
|
|
if (_resList[res].refCount == 0)
|
|
addToCacheList(_resList + res);
|
|
|
|
// It's tempting to free the resource immediately when refCount
|
|
// reaches zero, but that'd be a mistake. Closing a resource does not
|
|
// mean "I'm not going to use this resource any more". It means that
|
|
// "the next time I use this resource I'm going to ask for a new
|
|
// pointer to it".
|
|
//
|
|
// Since the original memory manager had to deal with memory
|
|
// fragmentation, keeping a resource open - and thus locked down to a
|
|
// specific memory address - was considered a bad thing.
|
|
}
|
|
|
|
void ResourceManager::removeFromCacheList(Resource *res) {
|
|
if (_cacheStart == res)
|
|
_cacheStart = res->next;
|
|
|
|
if (_cacheEnd == res)
|
|
_cacheEnd = res->prev;
|
|
|
|
if (res->prev)
|
|
res->prev->next = res->next;
|
|
if (res->next)
|
|
res->next->prev = res->prev;
|
|
res->prev = res->next = NULL;
|
|
}
|
|
|
|
void ResourceManager::addToCacheList(Resource *res) {
|
|
res->prev = NULL;
|
|
res->next = _cacheStart;
|
|
if (_cacheStart)
|
|
_cacheStart->prev = res;
|
|
_cacheStart = res;
|
|
if (!_cacheEnd)
|
|
_cacheEnd = res;
|
|
}
|
|
|
|
Common::File *ResourceManager::openCluFile(uint16 fileNum) {
|
|
Common::File *file = new Common::File;
|
|
while (!file->open(_resFiles[fileNum].fileName)) {
|
|
// HACK: We have to check for this, or it'll be impossible to
|
|
// quit while the game is asking for the user to insert a CD.
|
|
// But recovering from this situation gracefully is just too
|
|
// much trouble, so quit now.
|
|
if (_vm->_quit)
|
|
g_system->quit();
|
|
|
|
// If the file is supposed to be on hard disk, or we're
|
|
// playing a demo, then we're in trouble if the file
|
|
// can't be found!
|
|
|
|
if ((_vm->_features & GF_DEMO) || _resFiles[fileNum].cd == 0)
|
|
error("Could not find '%s'", _resFiles[fileNum].fileName);
|
|
|
|
askForCD(_resFiles[fileNum].cd);
|
|
}
|
|
return file;
|
|
}
|
|
|
|
void ResourceManager::readCluIndex(uint16 fileNum, Common::File *file) {
|
|
if (_resFiles[fileNum].entryTab == NULL) {
|
|
// we didn't read from this file before, get its index table
|
|
if (file == NULL)
|
|
file = openCluFile(fileNum);
|
|
else
|
|
file->incRef();
|
|
|
|
// 1st DWORD of a cluster is an offset to the look-up table
|
|
uint32 table_offset = file->readUint32LE();
|
|
debug(6, "table offset = %d", table_offset);
|
|
uint32 tableSize = file->size() - table_offset; // the table is stored at the end of the file
|
|
file->seek(table_offset);
|
|
|
|
assert((tableSize % 8) == 0);
|
|
_resFiles[fileNum].entryTab = (uint32*)malloc(tableSize);
|
|
_resFiles[fileNum].numEntries = tableSize / 8;
|
|
file->read(_resFiles[fileNum].entryTab, tableSize);
|
|
if (file->ioFailed())
|
|
error("unable to read index table from file %s\n", _resFiles[fileNum].fileName);
|
|
#ifdef SCUMM_BIG_ENDIAN
|
|
for (int tabCnt = 0; tabCnt < _resFiles[fileNum].numEntries * 2; tabCnt++)
|
|
_resFiles[fileNum].entryTab[tabCnt] = FROM_LE_32(_resFiles[fileNum].entryTab[tabCnt]);
|
|
#endif
|
|
file->decRef();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns true if resource is valid, otherwise false.
|
|
*/
|
|
|
|
bool ResourceManager::checkValid(uint32 res) {
|
|
// Resource number out of range
|
|
if (res >= _totalResFiles)
|
|
return false;
|
|
|
|
// Points to the number of the ascii filename
|
|
uint16 parent_res_file = _resConvTable[res * 2];
|
|
|
|
// Null & void resource
|
|
if (parent_res_file == 0xffff)
|
|
return false;
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Returns the total file length of a resource - i.e. all headers are included
|
|
* too.
|
|
*/
|
|
|
|
uint32 ResourceManager::fetchLen(uint32 res) {
|
|
if (_resList[res].ptr)
|
|
return _resList[res].size;
|
|
|
|
// Does this ever happen?
|
|
warning("fetchLen: Resource %u is not loaded; reading length from file", res);
|
|
|
|
// Points to the number of the ascii filename
|
|
uint16 parent_res_file = _resConvTable[res * 2];
|
|
|
|
// relative resource within the file
|
|
uint16 actual_res = _resConvTable[(res * 2) + 1];
|
|
|
|
// first we have to find the file via the _resConvTable
|
|
// open the cluster file
|
|
|
|
if (_resFiles[parent_res_file].entryTab == NULL) {
|
|
readCluIndex(parent_res_file);
|
|
}
|
|
return _resFiles[parent_res_file].entryTab[actual_res * 2 + 1];
|
|
}
|
|
|
|
void ResourceManager::checkMemUsage() {
|
|
while (_usedMem > MAX_MEM_CACHE) {
|
|
// we're using up more memory than we wanted to. free some old stuff.
|
|
// Newly loaded objects are added to the start of the list,
|
|
// we start freeing from the end, to free the oldest items first
|
|
if (_cacheEnd) {
|
|
Resource *tmp = _cacheEnd;
|
|
assert((tmp->refCount == 0) && (tmp->ptr) && (tmp->next == NULL));
|
|
removeFromCacheList(tmp);
|
|
|
|
_vm->_memory->memFree(tmp->ptr);
|
|
tmp->ptr = NULL;
|
|
_usedMem -= tmp->size;
|
|
} else {
|
|
warning("%d bytes of memory used, but cache list is empty!\n", _usedMem);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
void ResourceManager::remove(int res) {
|
|
if (_resList[res].ptr) {
|
|
removeFromCacheList(_resList + res);
|
|
|
|
_vm->_memory->memFree(_resList[res].ptr);
|
|
_resList[res].ptr = NULL;
|
|
_resList[res].refCount = 0;
|
|
_usedMem -= _resList[res].size;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Remove all res files from memory - ready for a total restart. This includes
|
|
* the player object and global variables resource.
|
|
*/
|
|
|
|
void ResourceManager::removeAll() {
|
|
// We need to clear the FX queue, because otherwise the sound system
|
|
// will still believe that the sound resources are in memory. We also
|
|
// need to kill the movie lead-in/out.
|
|
|
|
_vm->_sound->clearFxQueue(false);
|
|
|
|
for (uint i = 0; i < _totalResFiles; i++)
|
|
remove(i);
|
|
}
|
|
|
|
/**
|
|
* Remove all resources from memory.
|
|
*/
|
|
|
|
void ResourceManager::killAll(bool wantInfo) {
|
|
int nuked = 0;
|
|
|
|
// We need to clear the FX queue, because otherwise the sound system
|
|
// will still believe that the sound resources are in memory. We also
|
|
// need to kill the movie lead-in/out.
|
|
|
|
_vm->_sound->clearFxQueue(true);
|
|
|
|
for (uint i = 0; i < _totalResFiles; i++) {
|
|
// Don't nuke the global variables or the player object!
|
|
if (i == 1 || i == CUR_PLAYER_ID)
|
|
continue;
|
|
|
|
if (_resList[i].ptr) {
|
|
if (wantInfo)
|
|
Debug_Printf("Nuked %5d: %s\n", i, fetchName(_resList[i].ptr));
|
|
|
|
remove(i);
|
|
nuked++;
|
|
}
|
|
}
|
|
|
|
if (wantInfo)
|
|
Debug_Printf("Expelled %d resources\n", nuked);
|
|
}
|
|
|
|
/**
|
|
* Like killAll but only kills objects (except George & the variable table of
|
|
* course) - ie. forcing them to reload & restart their scripts, which
|
|
* simulates the effect of a save & restore, thus checking that each object's
|
|
* re-entrant logic works correctly, and doesn't cause a statuette to
|
|
* disappear forever, or some plaster-filled holes in sand to crash the game &
|
|
* get James in trouble again.
|
|
*/
|
|
|
|
void ResourceManager::killAllObjects(bool wantInfo) {
|
|
int nuked = 0;
|
|
|
|
for (uint i = 0; i < _totalResFiles; i++) {
|
|
// Don't nuke the global variables or the player object!
|
|
if (i == 1 || i == CUR_PLAYER_ID)
|
|
continue;
|
|
|
|
if (_resList[i].ptr) {
|
|
if (fetchType(_resList[i].ptr) == GAME_OBJECT) {
|
|
if (wantInfo)
|
|
Debug_Printf("Nuked %5d: %s\n", i, fetchName(_resList[i].ptr));
|
|
|
|
remove(i);
|
|
nuked++;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (wantInfo)
|
|
Debug_Printf("Expelled %d resources\n", nuked);
|
|
}
|
|
|
|
void ResourceManager::askForCD(int cd) {
|
|
byte *textRes;
|
|
|
|
// Stop any music from playing - so the system no longer needs the
|
|
// current CD - otherwise when we take out the CD, Windows will
|
|
// complain!
|
|
|
|
_vm->_sound->stopMusic(true);
|
|
|
|
textRes = openResource(2283);
|
|
_vm->_screen->displayMsg(_vm->fetchTextLine(textRes, 5 + cd) + 2, 0);
|
|
closeResource(2283);
|
|
|
|
// The original code probably determined automagically when the correct
|
|
// CD had been inserted, but our backend doesn't support that, and
|
|
// anyway I don't know if all systems allow that sort of thing. So we
|
|
// wait for the user to press any key instead, or click the mouse.
|
|
//
|
|
// But just in case we ever try to identify the CDs by their labels,
|
|
// they should be:
|
|
//
|
|
// CD1: "RBSII1" (or "PCF76" for the PCF76 version, whatever that is)
|
|
// CD2: "RBSII2"
|
|
}
|
|
|
|
} // End of namespace Sword2
|