scummvm/sword2/resman.cpp
2005-05-08 13:05:31 +00:00

806 lines
21 KiB
C++

/* Copyright (C) 1994-1998 Revolution Software Ltd.
* Copyright (C) 2003-2005 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
*
* $Header$
*/
#include "common/stdafx.h"
#include "common/file.h"
#include "common/system.h"
#include "sword2/sword2.h"
#include "sword2/defs.h"
#include "sword2/console.h"
#include "sword2/logic.h"
#include "sword2/memory.h"
#include "sword2/resman.h"
#include "sword2/router.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
#if !defined(__GNUC__)
#pragma START_PACK_STRUCTS
#endif
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.
} GCC_PACK;
#if !defined(__GNUC__)
#pragma END_PACK_STRUCTS
#endif
ResourceManager::ResourceManager(Sword2Engine *vm) {
_vm = vm;
// Until proven differently, assume we're on CD 1. This is so the start
// dialog will be able to play any music at all.
_curCd = 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
File file;
uint32 size;
byte *temp;
_totalClusters = 0;
_resConvTable = NULL;
if (!file.open("resource.inf"))
error("Cannot open resource.inf");
size = file.size();
// Get some space for the incoming resource file - soon to be trashed
temp = (byte *) malloc(size);
if (file.read(temp, size) != size) {
file.close();
error("init cannot *READ* resource.inf");
}
file.close();
// Ok, we've loaded in the resource.inf file which contains a list of
// all the files now extract the filenames.
// Using this method the Gode generated resource.inf must have #0d0a on
// the last entry
uint32 i = 0;
uint32 j = 0;
do {
// item must have an #0d0a
while (temp[i] != 13) {
_resFiles[_totalClusters].fileName[j] = temp[i];
i++;
j++;
}
// NULL terminate our extracted string
_resFiles[_totalClusters].fileName[j] = '\0';
_resFiles[_totalClusters].numEntries = -1;
_resFiles[_totalClusters].entryTab = NULL;
// Reset position in current slot between entries, skip the
// 0x0a in the source and increase the number of clusters.
j = 0;
i += 2;
_totalClusters++;
// TODO: put overload check here
} while (i != size);
free(temp);
// Now load in the binary id to res conversion table
if (!file.open("resource.tab"))
error("Cannot open resource.tab");
// Find how many resources
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();
error("Cannot read resource.tab");
}
file.close();
if (!file.open("cd.inf"))
error("Cannot open cd.inf");
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())
error("Cannot read cd.inf");
}
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)
error("%s is not in cd.inf", _resFiles[i].fileName);
_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", i, _resFiles[i].fileName);
_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;
}
_cacheStart = _cacheEnd = NULL;
_usedMem = 0;
}
ResourceManager::~ResourceManager() {
Resource *res = _cacheStart;
while (res) {
_vm->_memory->memFree(res->ptr);
res = res->next;
}
free(_resList);
free(_resConvTable);
}
// Quick macro to make swapping in-place easier to write
#define SWAP16(x) x = SWAP_BYTES_16(x)
#define SWAP32(x) x = SWAP_BYTES_32(x)
void convertEndian(byte *file, uint32 len) {
int i;
StandardHeader *hdr = (StandardHeader *) file;
file += sizeof(StandardHeader);
SWAP32(hdr->compSize);
SWAP32(hdr->decompSize);
switch (hdr->fileType) {
case ANIMATION_FILE: {
AnimHeader *animHead = (AnimHeader *) file;
SWAP16(animHead->noAnimFrames);
SWAP16(animHead->feetStartX);
SWAP16(animHead->feetStartY);
SWAP16(animHead->feetEndX);
SWAP16(animHead->feetEndY);
SWAP16(animHead->blend);
CdtEntry *cdtEntry = (CdtEntry *) (file + sizeof(AnimHeader));
for (i = 0; i < animHead->noAnimFrames; i++, cdtEntry++) {
SWAP16(cdtEntry->x);
SWAP16(cdtEntry->y);
SWAP32(cdtEntry->frameOffset);
FrameHeader *frameHeader = (FrameHeader *) (file + cdtEntry->frameOffset);
// Quick trick to prevent us from incorrectly applying the endian
// fixes multiple times. This assumes that frames are less than 1 MB
// and have height/width less than 4096.
if ((frameHeader->compSize & 0xFFF00000) ||
(frameHeader->width & 0xF000) ||
(frameHeader->height & 0xF000)) {
SWAP32(frameHeader->compSize);
SWAP16(frameHeader->width);
SWAP16(frameHeader->height);
}
}
break;
}
case SCREEN_FILE: {
MultiScreenHeader *mscreenHeader = (MultiScreenHeader *) file;
SWAP32(mscreenHeader->palette);
SWAP32(mscreenHeader->bg_parallax[0]);
SWAP32(mscreenHeader->bg_parallax[1]);
SWAP32(mscreenHeader->screen);
SWAP32(mscreenHeader->fg_parallax[0]);
SWAP32(mscreenHeader->fg_parallax[1]);
SWAP32(mscreenHeader->layers);
SWAP32(mscreenHeader->paletteTable);
SWAP32(mscreenHeader->maskOffset);
// screenHeader
ScreenHeader *screenHeader = (ScreenHeader *) (file + mscreenHeader->screen);
SWAP16(screenHeader->width);
SWAP16(screenHeader->height);
SWAP16(screenHeader->noLayers);
// layerHeader
LayerHeader *layerHeader = (LayerHeader *) (file + mscreenHeader->layers);
for (i = 0; i < screenHeader->noLayers; i++, layerHeader++) {
SWAP16(layerHeader->x);
SWAP16(layerHeader->y);
SWAP16(layerHeader->width);
SWAP16(layerHeader->height);
SWAP32(layerHeader->maskSize);
SWAP32(layerHeader->offset);
}
// backgroundParallaxLayer
Parallax *parallax;
int offset;
offset = mscreenHeader->bg_parallax[0];
if (offset > 0) {
parallax = (Parallax *) (file + offset);
SWAP16(parallax->w);
SWAP16(parallax->h);
}
offset = mscreenHeader->bg_parallax[1];
if (offset > 0) {
parallax = (Parallax *) (file + offset);
SWAP16(parallax->w);
SWAP16(parallax->h);
}
// backgroundLayer
offset = mscreenHeader->screen + sizeof(ScreenHeader);
if (offset > 0) {
parallax = (Parallax *) (file + offset);
SWAP16(parallax->w);
SWAP16(parallax->h);
}
// foregroundParallaxLayer
offset = mscreenHeader->fg_parallax[0];
if (offset > 0) {
parallax = (Parallax *) (file + offset);
SWAP16(parallax->w);
SWAP16(parallax->h);
}
offset = mscreenHeader->fg_parallax[1];
if (offset > 0) {
parallax = (Parallax *) (file + offset);
SWAP16(parallax->w);
SWAP16(parallax->h);
}
break;
}
case GAME_OBJECT: {
ObjectHub *objectHub = (ObjectHub *) file;
objectHub->type = (int) SWAP_BYTES_32(objectHub->type);
SWAP32(objectHub->logic_level);
for (i = 0; i < TREE_SIZE; i++) {
SWAP32(objectHub->logic[i]);
SWAP32(objectHub->script_id[i]);
SWAP32(objectHub->script_pc[i]);
}
break;
}
case WALK_GRID_FILE: {
WalkGridHeader *walkGridHeader = (WalkGridHeader *) file;
SWAP32(walkGridHeader->numBars);
SWAP32(walkGridHeader->numNodes);
BarData *barData = (BarData *) (file + sizeof(WalkGridHeader));
for (i = 0; i < walkGridHeader->numBars; i++) {
SWAP16(barData->x1);
SWAP16(barData->y1);
SWAP16(barData->x2);
SWAP16(barData->y2);
SWAP16(barData->xmin);
SWAP16(barData->ymin);
SWAP16(barData->xmax);
SWAP16(barData->ymax);
SWAP16(barData->dx);
SWAP16(barData->dy);
SWAP32(barData->co);
barData++;
}
uint16 *node = (uint16 *) (file + sizeof(WalkGridHeader) + walkGridHeader->numBars * sizeof(BarData));
for (i = 0; i < walkGridHeader->numNodes * 2; i++) {
SWAP16(*node);
node++;
}
break;
}
case GLOBAL_VAR_FILE:
break;
case PARALLAX_FILE_null:
break;
case RUN_LIST: {
uint32 *list = (uint32 *) file;
while (*list) {
SWAP32(*list);
list++;
}
break;
}
case TEXT_FILE: {
TextHeader *textHeader = (TextHeader *) file;
SWAP32(textHeader->noOfLines);
break;
}
case SCREEN_MANAGER:
break;
case MOUSE_FILE:
break;
case ICON_FILE:
break;
}
}
/**
* 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 music.
if ((_resFiles[cluFileNum].cd == CD1) || (_resFiles[cluFileNum].cd == CD2))
_curCd = _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.
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);
if (dump) {
StandardHeader *header = (StandardHeader *) _resList[res].ptr;
char buf[256];
const char *tag;
File out;
switch (header->fileType) {
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 (!out.exists(buf, "")) {
if (out.open(buf, File::kFileWriteMode, ""))
out.write(_resList[res].ptr, len);
}
}
// close the cluster
file->close();
delete file;
_usedMem += len;
checkMemUsage();
#ifdef SCUMM_BIG_ENDIAN
convertEndian(_resList[res].ptr, len);
#endif
} else if (_resList[res].refCount == 0)
removeFromCacheList(_resList + res);
_resList[res].refCount++;
return _resList[res].ptr;
}
void ResourceManager::closeResource(uint32 res) {
assert(res < _totalResFiles);
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;
}
File *ResourceManager::openCluFile(uint16 fileNum) {
File *file = new 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 & LOCAL_PERM))
error("Could not find '%s'", _resFiles[fileNum].fileName);
getCd(_resFiles[fileNum].cd & 3);
}
return file;
}
void ResourceManager::readCluIndex(uint16 fileNum, 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");
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, and that
// it's ok to close them.
_vm->_sound->clearFxQueue();
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, and that
// it's ok to close them.
_vm->_sound->clearFxQueue();
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) {
StandardHeader *header = (StandardHeader *) _resList[i].ptr;
if (wantInfo)
Debug_Printf("Nuked %5d: %s\n", i, header->name);
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) {
StandardHeader *header = (StandardHeader *) _resList[i].ptr;
if (header->fileType == GAME_OBJECT) {
if (wantInfo)
Debug_Printf("Nuked %5d: %s\n", i, header->name);
remove(i);
nuked++;
}
}
}
if (wantInfo)
Debug_Printf("Expelled %d resources\n", nuked);
}
int ResourceManager::whichCd() {
return _curCd;
}
void ResourceManager::getCd(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