/* 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 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$ * */ // RSC Resource file management module #include "saga/saga.h" #include "saga/actor.h" #include "saga/animation.h" #include "saga/interface.h" #include "saga/music.h" #include "saga/resource.h" #include "saga/scene.h" #include "saga/sndres.h" #include "common/advancedDetector.h" namespace Saga { Resource::Resource(SagaEngine *vm): _vm(vm) { _contexts = NULL; _contextsCount = 0; } Resource::~Resource() { clearContexts(); } bool Resource::loadResContext_v1(ResourceContext *context, uint32 contextOffset, uint32 contextSize) { size_t i; bool result; byte tableInfo[RSC_TABLEINFO_SIZE]; byte *tableBuffer; size_t tableSize; uint32 resourceTableOffset; ResourceData *resourceData; if (contextSize < RSC_MIN_FILESIZE) { return false; } context->file->seek(contextOffset + contextSize - RSC_TABLEINFO_SIZE); if (context->file->read(tableInfo, RSC_TABLEINFO_SIZE) != RSC_TABLEINFO_SIZE) { return false; } MemoryReadStreamEndian readS(tableInfo, RSC_TABLEINFO_SIZE, context->isBigEndian); resourceTableOffset = readS.readUint32(); context->count = readS.readUint32(); // Check for sane table offset if (resourceTableOffset != contextSize - RSC_TABLEINFO_SIZE - RSC_TABLEENTRY_SIZE * context->count) { return false; } // Load resource table tableSize = RSC_TABLEENTRY_SIZE * context->count; tableBuffer = (byte *)malloc(tableSize); context->file->seek(resourceTableOffset + contextOffset, SEEK_SET); result = (context->file->read(tableBuffer, tableSize) == tableSize); if (result) { context->table = (ResourceData *)calloc(context->count, sizeof(*context->table)); MemoryReadStreamEndian readS1(tableBuffer, tableSize, context->isBigEndian); for (i = 0; i < context->count; i++) { resourceData = &context->table[i]; resourceData->offset = contextOffset + readS1.readUint32(); resourceData->size = readS1.readUint32(); //sanity check if ((resourceData->offset > (uint)context->file->size()) || (resourceData->size > contextSize)) { result = false; break; } } } free(tableBuffer); return result; } bool Resource::loadContext(ResourceContext *context) { size_t i; const GamePatchDescription *patchDescription; ResourceData *resourceData; uint16 subjectResourceType; ResourceContext *subjectContext; uint32 subjectResourceId; uint32 patchResourceId; ResourceData *subjectResourceData; byte *tableBuffer; size_t tableSize; bool isMacBinary; if (!context->file->open(context->fileName)) { return false; } context->isBigEndian = _vm->isBigEndian(); if (context->fileType & GAME_SWAPENDIAN) context->isBigEndian = !context->isBigEndian; isMacBinary = (context->fileType & GAME_MACBINARY) > 0; context->fileType &= ~GAME_MACBINARY; if (!isMacBinary) { if (!loadResContext(context, 0, context->file->size())) { return false; } } else { if (!loadMacContext(context)) { return false; } } //process internal patch files if (context->fileType & GAME_PATCHFILE) { subjectResourceType = ~GAME_PATCHFILE & context->fileType; subjectContext = getContext((GameFileTypes)subjectResourceType); if (subjectContext == NULL) { error("Resource::loadContext() Subject context not found"); } loadResource(context, context->count - 1, tableBuffer, tableSize); MemoryReadStreamEndian readS2(tableBuffer, tableSize, context->isBigEndian); for (i = 0; i < tableSize / 8; i++) { subjectResourceId = readS2.readUint32(); patchResourceId = readS2.readUint32(); subjectResourceData = subjectContext->getResourceData(subjectResourceId); resourceData = context->getResourceData(patchResourceId); subjectResourceData->patchData = new PatchData(context->file); subjectResourceData->offset = resourceData->offset; subjectResourceData->size = resourceData->size; } free(tableBuffer); } //process external patch files for (patchDescription = _vm->getPatchDescriptions(); patchDescription && patchDescription->fileName; ++patchDescription) { if ((patchDescription->fileType & context->fileType) != 0) { if (patchDescription->resourceId < context->count) { resourceData = &context->table[patchDescription->resourceId]; resourceData->patchData = new PatchData(patchDescription); if (resourceData->patchData->_patchFile->open(patchDescription->fileName)) { resourceData->offset = 0; resourceData->size = resourceData->patchData->_patchFile->size(); // ITE uses several patch files which are loaded and then not needed // anymore (as they're in memory), so close them here. IHNM uses only // 1 patch file, which is reused, so don't close it if (_vm->getGameId() == GID_ITE) resourceData->patchData->_patchFile->close(); } else { delete resourceData->patchData; resourceData->patchData = NULL; } } } } // Close the file if it's part of a series of files // This prevents having all voice files open in IHNM for no reason, as each chapter uses // a different voice file if (context->serial > 0) context->file->close(); return true; } bool Resource::createContexts() { int i; ResourceContext *context; char musicFileName[256]; char soundFileName[256]; int soundFileIndex = 0; int voicesFileIndex = 0; bool digitalMusic = false; bool soundFileInArray = false; bool multipleVoices = false; bool censoredVersion = false; uint16 voiceFileType = GAME_VOICEFILE; bool fileFound = false; int maxFile = 0; _vm->_voiceFilesExist = true; struct SoundFileInfo { char fileName[40]; bool isCompressed; }; SoundFileInfo *curSoundfiles = 0; // If the Wyrmkeep credits file is found, set the Wyrmkeep version flag to true if (Common::File::exists("graphics/credit3n.dlt")) { _vm->_gf_wyrmkeep = true; } _contextsCount = 0; for (i = 0; _vm->getFilesDescriptions()[i].fileName; i++) { _contextsCount++; if (_vm->getFilesDescriptions()[i].fileType == GAME_SOUNDFILE) soundFileInArray = true; } //// Detect and add SFX files //////////////////////////////////////////////// SoundFileInfo sfxFilesITE[] = { { "sounds.rsc", false }, { "sounds.cmp", true }, { "soundsd.rsc", false }, { "soundsd.cmp", true } }; #ifdef ENABLE_IHNM SoundFileInfo sfxFilesIHNM[] = { { "sfx.res", false }, { "sfx.cmp", true } }; #endif #ifdef ENABLE_SAGA2 SoundFileInfo sfxFilesFTA2[] = { { "ftasound.hrs", false } }; #endif if (!soundFileInArray) { // If the sound file is not specified in the detector table, add it here fileFound = false; switch (_vm->getGameId()) { case GID_ITE: curSoundfiles = sfxFilesITE; maxFile = 4; break; #ifdef ENABLE_IHNM case GID_IHNM: curSoundfiles = sfxFilesIHNM; maxFile = 2; break; #endif #ifdef ENABLE_SAGA2 case GID_DINO: // TODO curSoundfiles = NULL; maxFile = 0; break; case GID_FTA2: curSoundfiles = sfxFilesFTA2; maxFile = 1; break; #endif } for (i = 0; i < maxFile; i++) { if (Common::File::exists(curSoundfiles[i].fileName)) { _contextsCount++; soundFileIndex = _contextsCount - 1; strcpy(soundFileName, curSoundfiles[i].fileName); _vm->_gf_compressed_sounds = curSoundfiles[i].isCompressed; fileFound = true; break; } } if (!fileFound) { // No sound file found, don't add any file to the array soundFileInArray = true; if (_vm->getGameId() == GID_ITE) { // ITE floppy versions have both voices and sounds in voices.rsc voiceFileType = GAME_SOUNDFILE | GAME_VOICEFILE; } } } //// Detect and add voice files ///////////////////////////////////////////// SoundFileInfo voiceFilesITE[] = { { "voices.rsc", false }, { "voices.cmp", true }, { "voicesd.rsc", false }, { "voicesd.cmp", true }, { "inherit the earth voices", false }, { "inherit the earth voices.cmp", true }, { "ite voices.bin", false } }; #ifdef ENABLE_IHNM SoundFileInfo voiceFilesIHNM[] = { { "voicess.res", false }, { "voicess.cmp", true }, { "voicesd.res", false }, { "voicesd.cmp", true }, }; #endif #ifdef ENABLE_SAGA2 SoundFileInfo voiceFilesFTA2[] = { { "ftavoice.hrs", false }, }; #endif // Detect and add voice files fileFound = false; switch (_vm->getGameId()) { case GID_ITE: curSoundfiles = voiceFilesITE; maxFile = 7; break; #ifdef ENABLE_IHNM case GID_IHNM: curSoundfiles = voiceFilesIHNM; maxFile = 4; break; #endif #ifdef ENABLE_SAGA2 case GID_DINO: // TODO curSoundfiles = NULL; maxFile = 0; break; case GID_FTA2: curSoundfiles = voiceFilesFTA2; maxFile = 1; break; #endif } for (i = 0; i < maxFile; i++) { if (Common::File::exists(curSoundfiles[i].fileName)) { _contextsCount++; voicesFileIndex = _contextsCount - 1; strcpy(_voicesFileName[0], curSoundfiles[i].fileName); _vm->_gf_compressed_sounds = curSoundfiles[i].isCompressed; fileFound = true; // Special cases if (!scumm_stricmp(curSoundfiles[i].fileName, "inherit the earth voices") || !scumm_stricmp(curSoundfiles[i].fileName, "inherit the earth voices.cmp")) { // The resources in the Wyrmkeep combined Windows/Mac/Linux CD version are little endian, but // the voice file is big endian. If we got such a version with mixed files, mark this voice file // as big endian voiceFileType = GAME_VOICEFILE | GAME_SWAPENDIAN; // This file is big endian } if (!scumm_stricmp(curSoundfiles[i].fileName, "ite voices.bin")) { voiceFileType = GAME_VOICEFILE | GAME_MACBINARY; } if (!scumm_stricmp(curSoundfiles[i].fileName, "voicess.res") || !scumm_stricmp(curSoundfiles[i].fileName, "voicess.cmp")) { // IHNM has multiple voice files multipleVoices = true; // Note: it is assumed that the voice files are always last in the list if (Common::File::exists("voices4.res") || Common::File::exists("voices4.cmp")) { _contextsCount += 6; // voices1-voices6 } else { // The German and French versions of IHNM don't have Nimdok's chapter, // therefore the voices file for that chapter is missing _contextsCount += 5; // voices1-voices3, voices4-voices5 censoredVersion = true; } } break; } } if (!fileFound) { if (_vm->getGameId() == GID_IHNM && _vm->isMacResources()) { // The Macintosh version of IHNM has no voices.res, and it has all // its voice files in subdirectories, so don't do anything here } else { warning("No voice file found, voices will be disabled"); _vm->_voicesEnabled = false; _vm->_subtitlesEnabled = true; _vm->_voiceFilesExist = false; } } //// Detect and add ITE music files ///////////////////////////////////////// // We don't set the compressed flag here SoundFileInfo musicFilesITE[] = { { "music.rsc", true }, { "music.cmp", true }, { "musicd.rsc", true }, { "musicd.cmp", true }, }; // Check for digital music in ITE if (_vm->getGameId() == GID_ITE) { fileFound = false; for (i = 0; i < 4; i++) { if (Common::File::exists(musicFilesITE[i].fileName)) { _contextsCount++; digitalMusic = true; fileFound = true; strcpy(musicFileName, musicFilesITE[i].fileName); break; } } if (!fileFound) { // No sound file found, don't add any file to the array digitalMusic = false; } } _contexts = (ResourceContext*)calloc(_contextsCount, sizeof(*_contexts)); for (i = 0; i < _contextsCount; i++) { context = &_contexts[i]; context->file = new Common::File(); context->serial = 0; // For ITE, add the digital music file and sfx file information here if (_vm->getGameId() == GID_ITE && digitalMusic && i == _contextsCount - 1) { context->fileName = musicFileName; context->fileType = GAME_MUSICFILE; } else if (!soundFileInArray && i == soundFileIndex) { context->fileName = soundFileName; context->fileType = GAME_SOUNDFILE; } else if (_vm->_voiceFilesExist && i == voicesFileIndex && !(_vm->getGameId() == GID_IHNM && _vm->isMacResources())) { context->fileName = _voicesFileName[0]; // can be GAME_VOICEFILE or GAME_SOUNDFILE | GAME_VOICEFILE or GAME_VOICEFILE | GAME_SWAPENDIAN context->fileType = voiceFileType; } else { if (!(_vm->_voiceFilesExist && multipleVoices && (i > voicesFileIndex))) { context->fileName = _vm->getFilesDescriptions()[i].fileName; context->fileType = _vm->getFilesDescriptions()[i].fileType; } else { int token = (censoredVersion && (i - voicesFileIndex >= 4)) ? 1 : 0; // censored versions don't have voice4 if (_vm->getFeatures() & GF_COMPRESSED_SOUNDS) sprintf(_voicesFileName[i - voicesFileIndex + token], "voices%i.cmp", i - voicesFileIndex + token); else sprintf(_voicesFileName[i - voicesFileIndex + token], "voices%i.res", i - voicesFileIndex + token); context->fileName = _voicesFileName[i - voicesFileIndex + token]; context->fileType = GAME_VOICEFILE; // IHNM has several different voice files, so we need to allow // multiple resource contexts of the same type. We tell them // apart by assigning each of the duplicates a unique serial // number. The default behaviour when requesting a context will // be to look for serial number 0. context->serial = i - voicesFileIndex + token; } } if (!loadContext(context)) { return false; } } return true; } void Resource::clearContexts() { int i; size_t j; ResourceContext *context; if (_contexts == NULL) { return; } for (i = 0; i < _contextsCount; i++) { context = &_contexts[i]; delete context->file; if (context->table != NULL) { for (j = 0; j < context->count; j++) { delete context->table[j].patchData; } } if (_vm->isSaga2()) { free(context->categories); } free(context->table); } free(_contexts); _contexts = NULL; } void Resource::loadResource(ResourceContext *context, uint32 resourceId, byte*&resourceBuffer, size_t &resourceSize) { Common::File *file; uint32 resourceOffset; ResourceData *resourceData; debug(8, "loadResource %d", resourceId); resourceData = context->getResourceData(resourceId); file = context->getFile(resourceData); resourceOffset = resourceData->offset; resourceSize = resourceData->size; resourceBuffer = (byte*)malloc(resourceSize); file->seek((long)resourceOffset, SEEK_SET); if (file->read(resourceBuffer, resourceSize) != resourceSize) { error("Resource::loadResource() failed to read"); } // ITE uses several patch files which are loaded and then not needed // anymore (as they're in memory), so close them here. IHNM uses only // 1 patch file, which is reused, so don't close it if (resourceData->patchData != NULL && _vm->getGameId() == GID_ITE) file->close(); } } // End of namespace Saga