/* 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