diff options
34 files changed, 1451 insertions, 358 deletions
@@ -4,6 +4,8 @@ For a more comprehensive changelog of the latest experimental code, see: 1.8.0 (????-??-??) New Games: - Added support for Sfinx. + - Added support for Zork Nemesis: The Forbidden Lands. + - Added support for Zork: Grand Inquisitor. General: - Updated Munt MT-32 emulation code to version 1.5.0. @@ -373,6 +373,10 @@ SCI Games by Sierra Entertainment: Wintermute Games: Chivalry is Not Dead [chivalry] +ZVISION Games by Activision: + Zork Nemesis: The Forbidden Lands [znemesis] + Zork: Grand Inquisitor [zgi] + SCUMM Games by Humongous Entertainment: Backyard Baseball [baseball] Backyard Baseball 2001 [baseball2001] @@ -1344,6 +1348,7 @@ Engines which currently support returning to the Launcher are: TOUCHE TSAGE TUCKER + ZVISION 5.5) Hotkeys: @@ -1508,6 +1513,26 @@ other games. t - Switch between 'Voice only', 'Voice and Text' and 'Text only' + Zork: Grand Inquisitor + Ctrl-s - Save + Ctrl-r - Restore + Ctrl-q - Quit + Ctrl-p - Preferences + F1 - Help + F5 - Inventory + F6 - Spellbook + F7 - Score + F8 - Put away current object/forget spell + F9 - Extract coin (must have the coin bag) + Space - Skips movies + + Zork Nemesis: The Forbidden Lands + Ctrl-s - Save + Ctrl-r - Restore + Ctrl-q - Quit + Ctrl-p - Preferences + Space - Skips movies + Note that using Ctrl-f or Ctrl-g is not recommended: games can crash when being run faster than their normal speed, as scripts will lose synchronisation. @@ -1627,6 +1652,7 @@ Where 'xxx' is exact the saved game slot (ie 001) under ScummVM TOUCHE TSAGE TUCKER + ZVISION --save-slot/-x: @@ -1659,6 +1685,7 @@ Where 'xxx' is exact the saved game slot (ie 001) under ScummVM TOUCHE TSAGE TUCKER + ZVISION 7.0) Music and Sound: @@ -2340,6 +2367,27 @@ The 7th Guest adds the following non-standard keyword: normal speed, to avoid music synchronization issues +Zork Nemesis: The Forbidden Lands adds the following non-standard keywords: + + originalsaveload bool If true, the original save/load screens are + used instead of the enhanced ScummVM ones + doublefps bool If true, game FPS are increased from 30 to 60 + venusenabled bool If true, the in-game Venus help system is + enabled + noanimwhileturning bool If true, animations are disabled while turning + in panoramic mode + +Zork: Grand Inquisitor adds the following non-standard keywords: + + originalsaveload bool If true, the original save/load screens are + used instead of the enhanced ScummVM ones + doublefps bool If true, game FPS are increased from 30 to 60 + noanimwhileturning bool If true, animations are disabled while turning + in panoramic mode + mpegmovies bool If true, the hires MPEG movies are used in the + DVD version of the game, instead of the lowres + AVI ones + 8.2) Custom game options that can be toggled via the GUI ---- --------------------------------------------------- diff --git a/backends/graphics/opengl/opengl-graphics.cpp b/backends/graphics/opengl/opengl-graphics.cpp index c455c4ce2e..5821856c30 100644 --- a/backends/graphics/opengl/opengl-graphics.cpp +++ b/backends/graphics/opengl/opengl-graphics.cpp @@ -49,9 +49,9 @@ OpenGLGraphicsManager::OpenGLGraphicsManager() _displayWidth(0), _displayHeight(0), _defaultFormat(), _defaultFormatAlpha(), _gameScreen(nullptr), _gameScreenShakeOffset(0), _overlay(nullptr), _overlayVisible(false), _cursor(nullptr), - _cursorX(0), _cursorY(0), _cursorHotspotX(0), _cursorHotspotY(0), _cursorHotspotXScaled(0), - _cursorHotspotYScaled(0), _cursorWidthScaled(0), _cursorHeightScaled(0), _cursorKeyColor(0), - _cursorVisible(false), _cursorDontScale(false), _cursorPaletteEnabled(false) + _cursorX(0), _cursorY(0), _cursorDisplayX(0),_cursorDisplayY(0), _cursorHotspotX(0), _cursorHotspotY(0), + _cursorHotspotXScaled(0), _cursorHotspotYScaled(0), _cursorWidthScaled(0), _cursorHeightScaled(0), + _cursorKeyColor(0), _cursorVisible(false), _cursorDontScale(false), _cursorPaletteEnabled(false) #ifdef USE_OSD , _osdAlpha(0), _osdFadeStartTime(0), _osd(nullptr) #endif @@ -351,7 +351,7 @@ void OpenGLGraphicsManager::updateScreen() { return; } - // Clear the screen buffer + // Clear the screen buffer. GLCALL(glClear(GL_COLOR_BUFFER_BIT)); const GLfloat shakeOffset = _gameScreenShakeOffset * (GLfloat)_displayHeight / _gameScreen->getHeight(); @@ -370,12 +370,42 @@ void OpenGLGraphicsManager::updateScreen() { // visible. const GLfloat cursorOffset = _overlayVisible ? 0 : shakeOffset; - _cursor->draw(_cursorX - _cursorHotspotXScaled, _cursorY - _cursorHotspotYScaled + cursorOffset, + _cursor->draw(_cursorDisplayX - _cursorHotspotXScaled, + _cursorDisplayY - _cursorHotspotYScaled + cursorOffset, _cursorWidthScaled, _cursorHeightScaled); } + // Fourth step: Draw black borders around the game screen when no overlay + // is visible. This makes sure that the mouse cursor etc. is only drawn + // in the actual game screen area in this case. + if (!_overlayVisible) { + GLCALL(glColor4f(0.0f, 0.0f, 0.0f, 1.0f)); + + GLCALL(glDisable(GL_TEXTURE_2D)); + GLCALL(glDisableClientState(GL_TEXTURE_COORD_ARRAY)); + + // Top border. + drawRect(0, 0, _outputScreenWidth, _displayY); + + // Left border. + drawRect(0, 0, _displayX, _outputScreenHeight); + + // Bottom border. + const int y = _displayY + _displayHeight; + drawRect(0, y, _outputScreenWidth, _outputScreenHeight - y); + + // Right border. + const int x = _displayX + _displayWidth; + drawRect(x, 0, _outputScreenWidth - x, _outputScreenHeight); + + GLCALL(glEnableClientState(GL_TEXTURE_COORD_ARRAY)); + GLCALL(glEnable(GL_TEXTURE_2D)); + + GLCALL(glColor4f(1.0f, 1.0f, 1.0f, 1.0f)); + } + #ifdef USE_OSD - // Fourth step: Draw the OSD. + // Fifth step: Draw the OSD. if (_osdAlpha > 0) { Common::StackLock lock(_osdMutex); @@ -435,10 +465,16 @@ int16 OpenGLGraphicsManager::getOverlayHeight() { void OpenGLGraphicsManager::showOverlay() { _overlayVisible = true; + + // Update cursor position. + setMousePosition(_cursorX, _cursorY); } void OpenGLGraphicsManager::hideOverlay() { _overlayVisible = false; + + // Update cursor position. + setMousePosition(_cursorX, _cursorY); } Graphics::PixelFormat OpenGLGraphicsManager::getOverlayFormat() const { @@ -892,8 +928,8 @@ void OpenGLGraphicsManager::adjustMousePosition(int16 &x, int16 &y) { const int16 width = _gameScreen->getWidth(); const int16 height = _gameScreen->getHeight(); - x = (x * width) / _displayWidth; - y = (y * height) / _displayHeight; + x = (x * width) / (int)_displayWidth; + y = (y * height) / (int)_displayHeight; // Make sure we only supply valid coordinates. x = CLIP<int16>(x, 0, width - 1); @@ -901,6 +937,19 @@ void OpenGLGraphicsManager::adjustMousePosition(int16 &x, int16 &y) { } } +void OpenGLGraphicsManager::setMousePosition(int x, int y) { + _cursorX = x; + _cursorY = y; + + if (_overlayVisible) { + _cursorDisplayX = x; + _cursorDisplayY = y; + } else { + _cursorDisplayX = CLIP<int>(x, _displayX, _displayX + _displayWidth - 1); + _cursorDisplayY = CLIP<int>(y, _displayY, _displayY + _displayHeight - 1); + } +} + Texture *OpenGLGraphicsManager::createTexture(const Graphics::PixelFormat &format, bool wantAlpha) { GLenum glIntFormat, glFormat, glType; if (format.bytesPerPixel == 1) { @@ -1046,6 +1095,9 @@ void OpenGLGraphicsManager::recalculateDisplayArea() { // We center the screen in the middle for now. _displayX = (_outputScreenWidth - _displayWidth ) / 2; _displayY = (_outputScreenHeight - _displayHeight) / 2; + + // Update the cursor position to adjust for new display area. + setMousePosition(_cursorX, _cursorY); } void OpenGLGraphicsManager::updateCursorPalette() { @@ -1163,4 +1215,20 @@ void OpenGLGraphicsManager::saveScreenshot(const Common::String &filename) const delete[] pixels; } +void OpenGLGraphicsManager::drawRect(GLfloat x, GLfloat y, GLfloat w, GLfloat h) { + if (w < 0 || h < 0) { + return; + } + + const GLfloat vertices[4*2] = { + x, y, + x + w, y, + x, y + h, + x + w, y + h + }; + GLCALL(glVertexPointer(2, GL_FLOAT, 0, vertices)); + + GLCALL(glDrawArrays(GL_TRIANGLE_STRIP, 0, 4)); +} + } // End of namespace OpenGL diff --git a/backends/graphics/opengl/opengl-graphics.h b/backends/graphics/opengl/opengl-graphics.h index dde21533b0..cec970e0cc 100644 --- a/backends/graphics/opengl/opengl-graphics.h +++ b/backends/graphics/opengl/opengl-graphics.h @@ -155,7 +155,7 @@ protected: * @param x X coordinate in physical coordinates. * @param y Y coordinate in physical coordinates. */ - void setMousePosition(int x, int y) { _cursorX = x; _cursorY = y; } + void setMousePosition(int x, int y); /** * Query the mouse position in physical coordinates. @@ -394,6 +394,16 @@ private: int _cursorY; /** + * X coordinate used for drawing the cursor. + */ + int _cursorDisplayX; + + /** + * Y coordinate used for drawing the cursor. + */ + int _cursorDisplayY; + + /** * The X offset for the cursor hotspot in unscaled coordinates. */ int _cursorHotspotX; @@ -454,6 +464,11 @@ private: */ byte _cursorPalette[3 * 256]; + /** + * Draws a rectangle + */ + void drawRect(GLfloat x, GLfloat y, GLfloat w, GLfloat h); + #ifdef USE_OSD // // OSD diff --git a/engines/sci/graphics/screen.cpp b/engines/sci/graphics/screen.cpp index 2f95bf7751..5a3b30f7ef 100644 --- a/engines/sci/graphics/screen.cpp +++ b/engines/sci/graphics/screen.cpp @@ -81,7 +81,8 @@ GfxScreen::GfxScreen(ResourceManager *resMan) : _resMan(resMan) { case GID_LSL1: case GID_LSL5: case GID_SQ1: - _width = 190; + _scriptHeight = 190; + break; default: break; } diff --git a/engines/sword25/util/double_serialization.cpp b/engines/sword25/util/double_serialization.cpp index a34eb0fbff..73da296e40 100644 --- a/engines/sword25/util/double_serialization.cpp +++ b/engines/sword25/util/double_serialization.cpp @@ -65,80 +65,4 @@ double decodeDouble(SerializedDouble value) { return ((value.signAndSignificandTwo & 0x80000000) == 0x80000000) ? -returnValue : returnValue; } -#if 0 - -// Why these are needed? - -uint64 encodeDouble_64(double value) { - // Split the value into its significand and exponent - int exponent; - double significand = frexp(value, &exponent); - - // Shift the significand into the integer range - double shiftedsignificand = ldexp(abs(significand), 53); - - // Combine everything using the IEEE standard - uint64 uintsignificand = (uint64)shiftedsignificand; - return ((uint64)(value < 0 ? 1 : 0) << 63) | // Sign - ((uint64)(exponent + 1023) << 52) | // Exponent stored as an offset to 1023 - (uintsignificand & 0x000FFFFFFFFFFFFFLL); // significand with MSB inferred -} - -double decodeDouble_64(uint64 value) { - // Expand the exponent and significand - int exponent = (int)((value >> 52) & 0x7FF) - 1023; - double expandedsignificand = (double)(0x10000000000000LL /* Inferred MSB */ | (value & 0x000FFFFFFFFFFFFFLL)); - - // Deflate the significand - int temp; - double significand = frexp(expandedsignificand, &temp); - - // Re-calculate the actual double - double returnValue = ldexp(significand, exponent); - - // Check the sign bit and return - return ((value & 0x8000000000000000LL) == 0x8000000000000000LL) ? -returnValue : returnValue; -} - -CompactSerializedDouble encodeDouble_Compact(double value) { - // Split the value into its significand and exponent - int exponent; - double significand = frexp(value, &exponent); - - // Shift the the first part of the significand into the integer range - double shiftedsignificandPart = ldexp(abs(significand), 32); - uint32 significandOne = uint32(floor(shiftedsignificandPart)); - - // Shift the remainder of the significand into the integer range - shiftedsignificandPart -= significandOne; - uint32 significandTwo = (uint32)(ldexp(shiftedsignificandPart, 21)); - - CompactSerializedDouble returnValue; - returnValue.signAndSignificandOne = ((uint32)(value < 0 ? 1 : 0) << 31) | // Sign - (significandOne & 0x7FFFFFFF); // significandOne with MSB inferred - // Exponent stored as an offset to 1023 - returnValue.exponentAndSignificandTwo = ((uint32)(exponent + 1023) << 21) | significandTwo; - - return returnValue; -} - -double decodeDouble_Compact(CompactSerializedDouble value) { - // Expand the exponent and the parts of the significand - int exponent = (int)(value.exponentAndSignificandTwo >> 21) - 1023; - double expandedsignificandOne = (double)(0x80000000 /* Inferred MSB */ | (value.signAndSignificandOne & 0x7FFFFFFF)); - double expandedsignificandTwo = (double)(value.exponentAndSignificandTwo & 0x1FFFFF); - - // Deflate the significand - double shiftedsignificand = ldexp(expandedsignificandTwo, -21); - double significand = ldexp(expandedsignificandOne + shiftedsignificand, -32); - - // Re-calculate the actual double - double returnValue = ldexp(significand, exponent); - - // Check the sign bit and return - return ((value.signAndSignificandOne & 0x80000000) == 0x80000000) ? -returnValue : returnValue; -} - -#endif - } // End of namespace Sword25 diff --git a/engines/sword25/util/double_serialization.h b/engines/sword25/util/double_serialization.h index a910a66f20..af58d03c17 100644 --- a/engines/sword25/util/double_serialization.h +++ b/engines/sword25/util/double_serialization.h @@ -56,43 +56,6 @@ SerializedDouble encodeDouble(double value); */ double decodeDouble(SerializedDouble value); -#if 0 -/** - * Encodes a double as a uint64 - * - * Does NOT support denormalized numbers. Does NOT support NaN, or Inf - * - * @param value The value to encode - * @return The encoded value - */ -uint64 encodeDouble_64(double value); -/** - * Decodes a previously encoded double - * - * @param value The value to decode - * @return The decoded value - */ -double decodeDouble_64(uint64 value); - -/** - * Encodes a double as two uint32 - * - * Does NOT support denormalized numbers. Does NOT support NaN, or Inf - * - * @param value The value to encode - * @return The encoded value - */ -CompactSerializedDouble encodeDouble_Compact(double value); -/** - * Decodes a previously encoded double - * - * @param value The value to decode - * @return The decoded value - */ -double decodeDouble_Compact(CompactSerializedDouble value); - -#endif - } // End of namespace Sword25 #endif diff --git a/engines/zvision/configure.engine b/engines/zvision/configure.engine index 02e31943af..38a5959995 100644 --- a/engines/zvision/configure.engine +++ b/engines/zvision/configure.engine @@ -1,3 +1,3 @@ # This file is included from the main "configure" script # add_engine [name] [desc] [build-by-default] [subengines] [base games] [deps] -add_engine zvision "ZVision" no "" "" "freetype2 16bit" +add_engine zvision "ZVision" yes "" "" "freetype2 16bit" diff --git a/engines/zvision/core/console.cpp b/engines/zvision/core/console.cpp index b5e542d777..f39c06b57e 100644 --- a/engines/zvision/core/console.cpp +++ b/engines/zvision/core/console.cpp @@ -53,6 +53,7 @@ Console::Console(ZVision *engine) : GUI::Debugger(), _engine(engine) { registerCmd("location", WRAP_METHOD(Console, cmdLocation)); registerCmd("dumpfile", WRAP_METHOD(Console, cmdDumpFile)); registerCmd("dumpfiles", WRAP_METHOD(Console, cmdDumpFiles)); + registerCmd("dumpimage", WRAP_METHOD(Console, cmdDumpImage)); } bool Console::cmdLoadVideo(int argc, const char **argv) { @@ -262,11 +263,70 @@ bool Console::cmdDumpFiles(int argc, const char **argv) { debugPrintf("Dumping %s\n", fileName.c_str()); in = iter->_value.arch->createReadStreamForMember(iter->_value.name); - dumpFile(in, fileName.c_str()); + if (in) + dumpFile(in, fileName.c_str()); delete in; } return true; } +bool Console::cmdDumpImage(int argc, const char **argv) { + if (argc != 2) { + debugPrintf("Use %s <TGA/TGZ name> to dump a ZVision TGA/TGZ image into a regular BMP image\n", argv[0]); + return true; + } + + Common::String fileName = argv[1]; + if (!fileName.hasSuffix(".tga")) { + debugPrintf("%s is not an image file", argv[1]); + } + + Common::File f; + if (!_engine->getSearchManager()->openFile(f, argv[1])) { + warning("File not found: %s", argv[1]); + return true; + } + + Graphics::Surface surface; + _engine->getRenderManager()->readImageToSurface(argv[1], surface, false); + + // Open file + Common::DumpFile out; + + fileName.setChar('b', fileName.size() - 3); + fileName.setChar('m', fileName.size() - 2); + fileName.setChar('p', fileName.size() - 1); + + out.open(fileName); + + // Write BMP header + out.writeByte('B'); + out.writeByte('M'); + out.writeUint32LE(surface.h * surface.pitch + 54); + out.writeUint32LE(0); + out.writeUint32LE(54); + out.writeUint32LE(40); + out.writeUint32LE(surface.w); + out.writeUint32LE(surface.h); + out.writeUint16LE(1); + out.writeUint16LE(16); + out.writeUint32LE(0); + out.writeUint32LE(0); + out.writeUint32LE(0); + out.writeUint32LE(0); + out.writeUint32LE(0); + out.writeUint32LE(0); + + // Write pixel data to BMP + out.write(surface.getPixels(), surface.pitch * surface.h); + + out.flush(); + out.close(); + + surface.free(); + + return true; +} + } // End of namespace ZVision diff --git a/engines/zvision/core/console.h b/engines/zvision/core/console.h index a7bd88ebc3..ffce87869f 100644 --- a/engines/zvision/core/console.h +++ b/engines/zvision/core/console.h @@ -47,6 +47,7 @@ private: bool cmdLocation(int argc, const char **argv); bool cmdDumpFile(int argc, const char **argv); bool cmdDumpFiles(int argc, const char **argv); + bool cmdDumpImage(int argc, const char **argv); }; } // End of namespace ZVision diff --git a/engines/zvision/detection.cpp b/engines/zvision/detection.cpp index 1eaff83413..545ebe35d4 100644 --- a/engines/zvision/detection.cpp +++ b/engines/zvision/detection.cpp @@ -59,6 +59,7 @@ namespace ZVision { #define GAMEOPTION_DOUBLE_FPS GUIO_GAMEOPTIONS2 #define GAMEOPTION_ENABLE_VENUS GUIO_GAMEOPTIONS3 #define GAMEOPTION_DISABLE_ANIM_WHILE_TURNING GUIO_GAMEOPTIONS4 +#define GAMEOPTION_USE_HIRES_MPEG_MOVIES GUIO_GAMEOPTIONS5 static const ZVisionGameDescription gameDescriptions[] = { @@ -77,6 +78,40 @@ static const ZVisionGameDescription gameDescriptions[] = { }, { + // Zork Nemesis French version + { + "znemesis", + 0, + {{"CSCR.ZFS", 0, "f04113357b4748c13efcb58b4629887c", 2577873}, + {"NEMESIS.STR", 0, "333bcb17bbb7f57cae742fbbe44f56f3", 9219}, + AD_LISTEND + }, + Common::FR_FRA, + Common::kPlatformDOS, + ADGF_NO_FLAGS, + GUIO4(GAMEOPTION_ORIGINAL_SAVELOAD, GAMEOPTION_DOUBLE_FPS, GAMEOPTION_ENABLE_VENUS, GAMEOPTION_DISABLE_ANIM_WHILE_TURNING) + }, + GID_NEMESIS + }, + + { + // Zork Nemesis German version + { + "znemesis", + 0, + {{"CSCR.ZFS", 0, "f04113357b4748c13efcb58b4629887c", 2577873}, + {"NEMESIS.STR", 0, "3d1a12b907751653866cffc6d4dfb331", 9505}, + AD_LISTEND + }, + Common::DE_DEU, + Common::kPlatformDOS, + ADGF_NO_FLAGS, + GUIO4(GAMEOPTION_ORIGINAL_SAVELOAD, GAMEOPTION_DOUBLE_FPS, GAMEOPTION_ENABLE_VENUS, GAMEOPTION_DISABLE_ANIM_WHILE_TURNING) + }, + GID_NEMESIS + }, + + { // Zork Nemesis English demo version { "znemesis", @@ -113,7 +148,7 @@ static const ZVisionGameDescription gameDescriptions[] = { Common::EN_ANY, Common::kPlatformWindows, ADGF_NO_FLAGS, - GUIO3(GAMEOPTION_ORIGINAL_SAVELOAD, GAMEOPTION_DOUBLE_FPS, GAMEOPTION_DISABLE_ANIM_WHILE_TURNING) + GUIO4(GAMEOPTION_ORIGINAL_SAVELOAD, GAMEOPTION_DOUBLE_FPS, GAMEOPTION_DISABLE_ANIM_WHILE_TURNING, GAMEOPTION_USE_HIRES_MPEG_MOVIES) }, GID_GRANDINQUISITOR }, @@ -160,7 +195,7 @@ static const ADExtraGuiOptionsMap optionsList[] = { GAMEOPTION_DOUBLE_FPS, { _s("Double FPS"), - _s("Halve the update delay"), + _s("Increase game FPS from 30 to 60"), "doublefps", false } @@ -186,6 +221,16 @@ static const ADExtraGuiOptionsMap optionsList[] = { } }, + { + GAMEOPTION_USE_HIRES_MPEG_MOVIES, + { + _s("Use the hires MPEG movies"), + _s("Use the hires MPEG movies of the DVD version, instead of the lowres AVI ones"), + "mpegmovies", + true + } + }, + AD_EXTRA_GUI_OPTIONS_TERMINATOR }; @@ -236,7 +281,7 @@ Common::Error ZVision::ZVision::loadGameState(int slot) { } Common::Error ZVision::ZVision::saveGameState(int slot, const Common::String &desc) { - _saveManager->saveGame(slot, desc); + _saveManager->saveGame(slot, desc, false); return Common::kNoError; } diff --git a/engines/zvision/file/save_manager.cpp b/engines/zvision/file/save_manager.cpp index 042fafd38e..5e96e4ab5e 100644 --- a/engines/zvision/file/save_manager.cpp +++ b/engines/zvision/file/save_manager.cpp @@ -69,7 +69,7 @@ bool SaveManager::scummVMSaveLoadDialog(bool isSave) { return false; if (isSave) { - saveGame(slot, desc); + saveGame(slot, desc, false); return true; } else { Common::ErrorCode result = loadGame(slot).getCode(); @@ -77,46 +77,34 @@ bool SaveManager::scummVMSaveLoadDialog(bool isSave) { } } -void SaveManager::saveGame(uint slot, const Common::String &saveName) { - Common::SaveFileManager *saveFileManager = g_system->getSavefileManager(); - Common::OutSaveFile *file = saveFileManager->openForSaving(_engine->generateSaveFileName(slot)); - - writeSaveGameHeader(file, saveName); +void SaveManager::saveGame(uint slot, const Common::String &saveName, bool useSaveBuffer) { + if (!_tempSave && useSaveBuffer) + return; - _engine->getScriptManager()->serialize(file); - - file->finalize(); - delete file; - - _lastSaveTime = g_system->getMillis(); -} - -void SaveManager::saveGame(uint slot, const Common::String &saveName, Common::MemoryWriteStreamDynamic *stream) { Common::SaveFileManager *saveFileManager = g_system->getSavefileManager(); Common::OutSaveFile *file = saveFileManager->openForSaving(_engine->generateSaveFileName(slot)); - writeSaveGameHeader(file, saveName); + writeSaveGameHeader(file, saveName, useSaveBuffer); - file->write(stream->getData(), stream->size()); + if (useSaveBuffer) + file->write(_tempSave->getData(), _tempSave->size()); + else + _engine->getScriptManager()->serialize(file); file->finalize(); delete file; - _lastSaveTime = g_system->getMillis(); -} - -void SaveManager::saveGameBuffered(uint slot, const Common::String &saveName) { - if (_tempSave) { - saveGame(slot, saveName, _tempSave); + if (useSaveBuffer) flushSaveBuffer(); - } + + _lastSaveTime = g_system->getMillis(); } void SaveManager::autoSave() { - saveGame(0, "Auto save"); + saveGame(0, "Auto save", false); } -void SaveManager::writeSaveGameHeader(Common::OutSaveFile *file, const Common::String &saveName) { +void SaveManager::writeSaveGameHeader(Common::OutSaveFile *file, const Common::String &saveName, bool useSaveBuffer) { file->writeUint32BE(SAVEGAME_ID); // Write version @@ -126,8 +114,11 @@ void SaveManager::writeSaveGameHeader(Common::OutSaveFile *file, const Common::S file->writeString(saveName); file->writeByte(0); - // Create a thumbnail and save it - Graphics::saveThumbnail(*file); + // Save the game thumbnail + if (useSaveBuffer) + file->write(_tempThumbnail->getData(), _tempThumbnail->size()); + else + Graphics::saveThumbnail(*file); // Write out the save date/time TimeDate td; @@ -139,39 +130,25 @@ void SaveManager::writeSaveGameHeader(Common::OutSaveFile *file, const Common::S file->writeSint16LE(td.tm_min); } -Common::Error SaveManager::loadGame(uint slot) { - Common::SeekableReadStream *saveFile = getSlotFile(slot); - if (saveFile == 0) { - return Common::kPathDoesNotExist; - } - - // Read the header - SaveGameHeader header; - if (!readSaveGameHeader(saveFile, header)) { - return Common::kUnknownError; - } - - ScriptManager *scriptManager = _engine->getScriptManager(); - // Update the state table values - scriptManager->deserialize(saveFile); - - delete saveFile; - if (header.thumbnail) - delete header.thumbnail; - - return Common::kNoError; -} - -Common::Error SaveManager::loadGame(const Common::String &saveName) { - Common::File *saveFile = _engine->getSearchManager()->openFile(saveName); - if (saveFile == NULL) { - saveFile = new Common::File; - if (!saveFile->open(saveName)) { - delete saveFile; - return Common::kPathDoesNotExist; +Common::Error SaveManager::loadGame(int slot) { + Common::SeekableReadStream *saveFile = NULL; + + if (slot >= 0) { + saveFile = getSlotFile(slot); + } else { + Common::File *saveFile = _engine->getSearchManager()->openFile("r.svr"); + if (!saveFile) { + saveFile = new Common::File; + if (!saveFile->open("r.svr")) { + delete saveFile; + return Common::kPathDoesNotExist; + } } } + if (!saveFile) + return Common::kPathDoesNotExist; + // Read the header SaveGameHeader header; if (!readSaveGameHeader(saveFile, header)) { @@ -272,18 +249,20 @@ Common::SeekableReadStream *SaveManager::getSlotFile(uint slot) { } void SaveManager::prepareSaveBuffer() { - if (_tempSave) - delete _tempSave; + delete _tempThumbnail; + _tempThumbnail = new Common::MemoryWriteStreamDynamic; + Graphics::saveThumbnail(*_tempThumbnail); + delete _tempSave; _tempSave = new Common::MemoryWriteStreamDynamic; - _engine->getScriptManager()->serialize(_tempSave); } void SaveManager::flushSaveBuffer() { - if (_tempSave) - delete _tempSave; + delete _tempThumbnail; + _tempThumbnail = NULL; + delete _tempSave; _tempSave = NULL; } diff --git a/engines/zvision/file/save_manager.h b/engines/zvision/file/save_manager.h index fc8db67566..9e816373ea 100644 --- a/engines/zvision/file/save_manager.h +++ b/engines/zvision/file/save_manager.h @@ -48,7 +48,7 @@ struct SaveGameHeader { class SaveManager { public: - SaveManager(ZVision *engine) : _engine(engine), _tempSave(NULL), _lastSaveTime(0) {} + SaveManager(ZVision *engine) : _engine(engine), _tempSave(NULL), _tempThumbnail(NULL), _lastSaveTime(0) {} ~SaveManager() { flushSaveBuffer(); } @@ -67,6 +67,7 @@ private: SAVE_VERSION = 1 }; + Common::MemoryWriteStreamDynamic *_tempThumbnail; Common::MemoryWriteStreamDynamic *_tempSave; public: @@ -83,17 +84,14 @@ public: * @param slot The save slot this save pertains to. Must be [1, 20] * @param saveName The internal name for this save. This is NOT the name of the actual save file. */ - void saveGame(uint slot, const Common::String &saveName); - void saveGame(uint slot, const Common::String &saveName, Common::MemoryWriteStreamDynamic *stream); - void saveGameBuffered(uint slot, const Common::String &saveName); + void saveGame(uint slot, const Common::String &saveName, bool useSaveBuffer); /** * Loads the state data from the save file that slot references. Uses * ZVision::generateSaveFileName(slot) to get the save file name. * * @param slot The save slot to load. Must be [1, 20] */ - Common::Error loadGame(uint slot); - Common::Error loadGame(const Common::String &saveName); + Common::Error loadGame(int slot); Common::SeekableReadStream *getSlotFile(uint slot); bool readSaveGameHeader(Common::SeekableReadStream *in, SaveGameHeader &header); @@ -102,7 +100,7 @@ public: void flushSaveBuffer(); bool scummVMSaveLoadDialog(bool isSave); private: - void writeSaveGameHeader(Common::OutSaveFile *file, const Common::String &saveName); + void writeSaveGameHeader(Common::OutSaveFile *file, const Common::String &saveName, bool useSaveBuffer); }; } // End of namespace ZVision diff --git a/engines/zvision/file/search_manager.cpp b/engines/zvision/file/search_manager.cpp index ec250ff648..9f709dd0a1 100644 --- a/engines/zvision/file/search_manager.cpp +++ b/engines/zvision/file/search_manager.cpp @@ -62,19 +62,6 @@ SearchManager::~SearchManager() { _archList.clear(); } -void SearchManager::addPatch(const Common::String &src, const Common::String &dst) { - Common::String lowerCaseName = dst; - lowerCaseName.toLowercase(); - - SearchManager::MatchList::iterator it = _files.find(lowerCaseName); - - if (it != _files.end()) { - lowerCaseName = src; - lowerCaseName.toLowercase(); - _files[lowerCaseName] = it->_value; - } -} - void SearchManager::addFile(const Common::String &name, Common::Archive *arch) { bool addArch = true; Common::List<Common::Archive *>::iterator it = _archList.begin(); @@ -147,9 +134,10 @@ bool SearchManager::hasFile(const Common::String &name) { return false; } -void SearchManager::loadZix(const Common::String &name) { +bool SearchManager::loadZix(const Common::String &name) { Common::File file; - file.open(name); + if (!file.open(name)) + return false; Common::String line; @@ -160,7 +148,7 @@ void SearchManager::loadZix(const Common::String &name) { } if (file.eos()) - return; + error("Corrupt ZIX file: %s", name.c_str()); Common::Array<Common::Archive *> archives; @@ -169,42 +157,37 @@ void SearchManager::loadZix(const Common::String &name) { line.trim(); if (line.matchString("----------*", true)) break; - else if (line.matchString("DIR:*", true) || line.matchString("CD0:*", true) || line.matchString("CD1:*", true)) { + else if (line.matchString("DIR:*", true) || line.matchString("CD0:*", true) || line.matchString("CD1:*", true) || line.matchString("CD2:*", true)) { + Common::Archive *arc; + Common::String path(line.c_str() + 5); + for (uint i = 0; i < path.size(); i++) + if (path[i] == '\\') + path.setChar('/', i); + + // Check if NEMESIS.ZIX/MEDIUM.ZIX refers to the znemesis folder, and + // check the game root folder instead + if (path.hasPrefix("znemesis/")) + path = Common::String(path.c_str() + 9); + // Check if INQUIS.ZIX refers to the ZGI folder, and check the game // root folder instead - if (path.hasPrefix("zgi\\")) + if (path.hasPrefix("zgi/")) path = Common::String(path.c_str() + 4); + if (path.hasPrefix("zgi_e/")) + path = Common::String(path.c_str() + 6); - Common::Archive *arc; - char tempPath[128]; - strcpy(tempPath, path.c_str()); - for (uint i = 0; i < path.size(); i++) - if (tempPath[i] == '\\') - tempPath[i] = '/'; - - path = Common::String(tempPath); if (path.size() && path[0] == '.') path.deleteChar(0); if (path.size() && path[0] == '/') path.deleteChar(0); + if (path.size() && path.hasSuffix("/")) + path.deleteLastChar(); - if (path.matchString("*.zfs", true)) + if (path.matchString("*.zfs", true)) { arc = new ZfsArchive(path); - else { - if (path.size()) { - if (path[path.size() - 1] == '\\' || path[path.size() - 1] == '/') - path.deleteLastChar(); - if (path.size()) - for (Common::List<Common::String>::iterator it = _dirList.begin(); it != _dirList.end(); ++it) - if (path.equalsIgnoreCase(*it)) { - path = *it; - break; - } - } - + } else { path = Common::String::format("%s/%s", _root.c_str(), path.c_str()); - arc = new Common::FSDirectory(path); } archives.push_back(arc); @@ -212,7 +195,7 @@ void SearchManager::loadZix(const Common::String &name) { } if (file.eos()) - return; + error("Corrupt ZIX file: %s", name.c_str()); while (!file.eos()) { line = file.readLine(); @@ -225,6 +208,8 @@ void SearchManager::loadZix(const Common::String &name) { } } } + + return true; } void SearchManager::addDir(const Common::String &name) { diff --git a/engines/zvision/file/search_manager.h b/engines/zvision/file/search_manager.h index b9ed02ec13..0d0ab14d31 100644 --- a/engines/zvision/file/search_manager.h +++ b/engines/zvision/file/search_manager.h @@ -39,13 +39,12 @@ public: void addFile(const Common::String &name, Common::Archive *arch); void addDir(const Common::String &name); - void addPatch(const Common::String &src, const Common::String &dst); Common::File *openFile(const Common::String &name); bool openFile(Common::File &file, const Common::String &name); bool hasFile(const Common::String &name); - void loadZix(const Common::String &name); + bool loadZix(const Common::String &name); struct Node { Common::String name; diff --git a/engines/zvision/graphics/cursors/cursor.cpp b/engines/zvision/graphics/cursors/cursor.cpp index f32c68645d..2c011668ac 100644 --- a/engines/zvision/graphics/cursors/cursor.cpp +++ b/engines/zvision/graphics/cursors/cursor.cpp @@ -43,7 +43,7 @@ ZorkCursor::ZorkCursor(ZVision *engine, const Common::String &fileName) _hotspotY(0) { Common::File file; if (!engine->getSearchManager()->openFile(file, fileName)) - return; + error("Cursor file %s does not exist", fileName.c_str()); uint32 magic = file.readUint32BE(); if (magic != MKTAG('Z', 'C', 'R', '1')) { diff --git a/engines/zvision/graphics/cursors/cursor_manager.cpp b/engines/zvision/graphics/cursors/cursor_manager.cpp index c364426bad..1e048efedf 100644 --- a/engines/zvision/graphics/cursors/cursor_manager.cpp +++ b/engines/zvision/graphics/cursors/cursor_manager.cpp @@ -37,7 +37,7 @@ const char *CursorManager::_cursorNames[NUM_CURSORS] = { "active", "arrow", "bac "hright", "hup", "idle", "leftarrow", "rightarrow", "suggest_surround", "suggest_tilt", "turnaround", "zuparrow" }; -const char *CursorManager::_zgiCursorFileNames[NUM_CURSORS] = { "g0gbc011.zcr", "g0gac001.zcr", "g0gac021.zcr", "g0gac031.zcr", "g0gac041.zcr", "g0gac051.zcr", "g0gac061.zcr", "g0gac071.zcr", "g0gac081.zcr", +const char *CursorManager::_zgiCursorFileNames[NUM_CURSORS] = { "g0gbc011.zcr", "g0gac011.zcr", "g0gac021.zcr", "g0gac031.zcr", "g0gac041.zcr", "g0gac051.zcr", "g0gac061.zcr", "g0gac071.zcr", "g0gac081.zcr", "g0gac091.zcr", "g0gac101.zcr", "g0gac011.zcr", "g0gac111.zcr", "g0gac121.zcr", "g0gac131.zcr", "g0gac141.zcr", "g0gac151.zcr", "g0gac161.zcr" }; @@ -55,6 +55,11 @@ CursorManager::CursorManager(ZVision *engine, const Graphics::PixelFormat pixelF for (int i = 0; i < NUM_CURSORS; i++) { if (_engine->getGameId() == GID_NEMESIS) { Common::String name; + if (i == 1) { + // Cursors "arrowa.zcr" and "arrowb.zcr" are missing + _cursors[i][0] = _cursors[i][1] = ZorkCursor(); + continue; + } name = Common::String::format("%sa.zcr", _zNemCursorFileNames[i]); _cursors[i][0] = ZorkCursor(_engine, name); // Up cursor name = Common::String::format("%sb.zcr", _zNemCursorFileNames[i]); diff --git a/engines/zvision/graphics/render_manager.cpp b/engines/zvision/graphics/render_manager.cpp index a1cc8ac53c..4a43e09b07 100644 --- a/engines/zvision/graphics/render_manager.cpp +++ b/engines/zvision/graphics/render_manager.cpp @@ -42,28 +42,25 @@ namespace ZVision { RenderManager::RenderManager(ZVision *engine, uint32 windowWidth, uint32 windowHeight, const Common::Rect workingWindow, const Graphics::PixelFormat pixelFormat, bool doubleFPS) : _engine(engine), _system(engine->_system), - _workingWidth(workingWindow.width()), - _workingHeight(workingWindow.height()), - _screenCenterX(_workingWidth / 2), - _screenCenterY(_workingHeight / 2), + _screenCenterX(_workingWindow.width() / 2), + _screenCenterY(_workingWindow.height() / 2), _workingWindow(workingWindow), _pixelFormat(pixelFormat), _backgroundWidth(0), _backgroundHeight(0), _backgroundOffset(0), - _renderTable(_workingWidth, _workingHeight), - _doubleFPS(doubleFPS) { + _renderTable(_workingWindow.width(), _workingWindow.height()), + _doubleFPS(doubleFPS), + _subid(0) { - _backgroundSurface.create(_workingWidth, _workingHeight, _pixelFormat); - _effectSurface.create(_workingWidth, _workingHeight, _pixelFormat); - _warpedSceneSurface.create(_workingWidth, _workingHeight, _pixelFormat); + _backgroundSurface.create(_workingWindow.width(), _workingWindow.height(), _pixelFormat); + _effectSurface.create(_workingWindow.width(), _workingWindow.height(), _pixelFormat); + _warpedSceneSurface.create(_workingWindow.width(), _workingWindow.height(), _pixelFormat); _menuSurface.create(windowWidth, workingWindow.top, _pixelFormat); - _subtitleSurface.create(windowWidth, windowHeight - workingWindow.bottom, _pixelFormat); - + _menuArea = Common::Rect(0, 0, windowWidth, workingWindow.top); - _subtitleArea = Common::Rect(0, workingWindow.bottom, windowWidth, windowHeight); - _subid = 0; + initSubArea(windowWidth, windowHeight, workingWindow); } RenderManager::~RenderManager() { @@ -83,7 +80,7 @@ void RenderManager::renderSceneToScreen() { // If we have graphical effects, we apply them using a temporary buffer if (!_effects.empty()) { bool copied = false; - Common::Rect windowRect(_workingWidth, _workingHeight); + Common::Rect windowRect(_workingWindow.width(), _workingWindow.height()); for (EffectsList::iterator it = _effects.begin(); it != _effects.end(); it++) { Common::Rect rect = (*it)->getRegion(); @@ -121,7 +118,7 @@ void RenderManager::renderSceneToScreen() { if (!_backgroundSurfaceDirtyRect.isEmpty()) { _renderTable.mutateImage(&_warpedSceneSurface, in); out = &_warpedSceneSurface; - outWndDirtyRect = Common::Rect(_workingWidth, _workingHeight); + outWndDirtyRect = Common::Rect(_workingWindow.width(), _workingWindow.height()); } } else { out = in; @@ -590,7 +587,7 @@ void RenderManager::prepareBackground() { if (state == RenderTable::PANORAMA) { // Calculate the visible portion of the background - Common::Rect viewPort(_workingWidth, _workingHeight); + Common::Rect viewPort(_workingWindow.width(), _workingWindow.height()); viewPort.translate(-(_screenCenterX - _backgroundOffset), 0); Common::Rect drawRect = _backgroundDirtyRect; drawRect.clip(viewPort); @@ -635,7 +632,7 @@ void RenderManager::prepareBackground() { } } else if (state == RenderTable::TILT) { // Tilt doesn't allow wrapping, so we just do a simple clip - Common::Rect viewPort(_workingWidth, _workingHeight); + Common::Rect viewPort(_workingWindow.width(), _workingWindow.height()); viewPort.translate(0, -(_screenCenterY - _backgroundOffset)); Common::Rect drawRect = _backgroundDirtyRect; drawRect.clip(viewPort); @@ -655,7 +652,7 @@ void RenderManager::prepareBackground() { // Clear the dirty rect since everything is clean now _backgroundDirtyRect = Common::Rect(); - _backgroundSurfaceDirtyRect.clip(_workingWidth, _workingHeight); + _backgroundSurfaceDirtyRect.clip(_workingWindow.width(), _workingWindow.height()); } void RenderManager::clearMenuSurface() { @@ -687,6 +684,15 @@ void RenderManager::renderMenuToScreen() { } } +void RenderManager::initSubArea(uint32 windowWidth, uint32 windowHeight, const Common::Rect workingWindow) { + _workingWindow = workingWindow; + + _subtitleSurface.free(); + + _subtitleSurface.create(windowWidth, windowHeight - workingWindow.bottom, _pixelFormat); + _subtitleArea = Common::Rect(0, workingWindow.bottom, windowWidth, windowHeight); +} + uint16 RenderManager::createSubArea(const Common::Rect &area) { _subid++; @@ -791,8 +797,8 @@ Common::Rect RenderManager::transformBackgroundSpaceRectToScreenSpace(const Comm if (state == RenderTable::PANORAMA) { if (_backgroundOffset < _screenCenterX) { - Common::Rect rScreen(_screenCenterX + _backgroundOffset, _workingHeight); - Common::Rect lScreen(_workingWidth - rScreen.width(), _workingHeight); + Common::Rect rScreen(_screenCenterX + _backgroundOffset, _workingWindow.height()); + Common::Rect lScreen(_workingWindow.width() - rScreen.width(), _workingWindow.height()); lScreen.translate(_backgroundWidth - lScreen.width(), 0); lScreen.clip(src); rScreen.clip(src); @@ -802,8 +808,8 @@ Common::Rect RenderManager::transformBackgroundSpaceRectToScreenSpace(const Comm tmp.translate(_screenCenterX - _backgroundOffset, 0); } } else if (_backgroundWidth - _backgroundOffset < _screenCenterX) { - Common::Rect rScreen(_screenCenterX - (_backgroundWidth - _backgroundOffset), _workingHeight); - Common::Rect lScreen(_workingWidth - rScreen.width(), _workingHeight); + Common::Rect rScreen(_screenCenterX - (_backgroundWidth - _backgroundOffset), _workingWindow.height()); + Common::Rect lScreen(_workingWindow.width() - rScreen.width(), _workingWindow.height()); lScreen.translate(_backgroundWidth - lScreen.width(), 0); lScreen.clip(src); rScreen.clip(src); @@ -1172,4 +1178,11 @@ void RenderManager::rotateTo(int16 _toPos, int16 _time) { _engine->startClock(); } +void RenderManager::upscaleRect(Common::Rect &rect) { + rect.top = rect.top * HIRES_WINDOW_HEIGHT / WINDOW_HEIGHT; + rect.left = rect.left * HIRES_WINDOW_WIDTH / WINDOW_WIDTH; + rect.bottom = rect.bottom * HIRES_WINDOW_HEIGHT / WINDOW_HEIGHT; + rect.right = rect.right * HIRES_WINDOW_WIDTH / WINDOW_WIDTH; +} + } // End of namespace ZVision diff --git a/engines/zvision/graphics/render_manager.h b/engines/zvision/graphics/render_manager.h index c22f9a78c9..e3cbbc34ce 100644 --- a/engines/zvision/graphics/render_manager.h +++ b/engines/zvision/graphics/render_manager.h @@ -73,12 +73,8 @@ private: * are given in this coordinate space. Also, all images are clipped to the * edges of this Rectangle */ - const Common::Rect _workingWindow; + Common::Rect _workingWindow; - // Width of the working window. Saved to prevent extraneous calls to _workingWindow.width() - const int _workingWidth; - // Height of the working window. Saved to prevent extraneous calls to _workingWindow.height() - const int _workingHeight; // Center of the screen in the x direction const int _screenCenterX; // Center of the screen in the y direction @@ -106,7 +102,6 @@ private: // A buffer for subtitles Graphics::Surface _subtitleSurface; - Common::Rect _subtitleSurfaceDirtyRect; // Rectangle for subtitles area Common::Rect _subtitleArea; @@ -242,6 +237,8 @@ public: // Subtitles methods + void initSubArea(uint32 windowWidth, uint32 windowHeight, const Common::Rect workingWindow); + // Create subtitle area and return ID uint16 createSubArea(const Common::Rect &area); uint16 createSubArea(); @@ -335,6 +332,8 @@ public: void checkBorders(); void rotateTo(int16 to, int16 time); void updateRotation(); + + void upscaleRect(Common::Rect &rect); }; } // End of namespace ZVision diff --git a/engines/zvision/scripting/actions.cpp b/engines/zvision/scripting/actions.cpp index a61fa26223..e989478dd1 100644 --- a/engines/zvision/scripting/actions.cpp +++ b/engines/zvision/scripting/actions.cpp @@ -21,14 +21,13 @@ */ #include "common/scummsys.h" +#include "video/video_decoder.h" #include "zvision/scripting/actions.h" #include "zvision/zvision.h" #include "zvision/scripting/script_manager.h" #include "zvision/graphics/render_manager.h" -#include "zvision/sound/zork_raw.h" -#include "zvision/video/zork_avi_decoder.h" #include "zvision/file/save_manager.h" #include "zvision/scripting/menu.h" #include "zvision/scripting/effects/timer_effect.h" @@ -46,10 +45,6 @@ #include "zvision/graphics/effects/wave.h" #include "zvision/graphics/cursors/cursor_manager.h" -#include "common/file.h" - -#include "audio/decoders/wave.h" - namespace ZVision { ////////////////////////////////////////////////////////////////////////////// @@ -815,7 +810,7 @@ ActionRestoreGame::ActionRestoreGame(ZVision *engine, int32 slotkey, const Commo } bool ActionRestoreGame::execute() { - _engine->getSaveManager()->loadGame(_fileName); + _engine->getSaveManager()->loadGame(-1); return false; } @@ -932,35 +927,62 @@ ActionStreamVideo::ActionStreamVideo(ZVision *engine, int32 slotkey, const Commo } bool ActionStreamVideo::execute() { - ZorkAVIDecoder decoder; - Common::File *_file = _engine->getSearchManager()->openFile(_fileName); - - if (_file) { - if (!decoder.loadStream(_file)) { + Video::VideoDecoder *decoder; + Common::Rect destRect = Common::Rect(_x1, _y1, _x2 + 1, _y2 + 1); + Common::String subname = _fileName; + subname.setChar('s', subname.size() - 3); + subname.setChar('u', subname.size() - 2); + subname.setChar('b', subname.size() - 1); + bool subtitleExists = _engine->getSearchManager()->hasFile(subname); + bool switchToHires = false; + +// NOTE: We only show the hires MPEG2 videos when libmpeg2 is compiled in, +// otherwise we fall back to the lowres ones +#ifdef USE_MPEG2 + Common::String hiresFileName = _fileName; + hiresFileName.setChar('d', hiresFileName.size() - 8); + hiresFileName.setChar('v', hiresFileName.size() - 3); + hiresFileName.setChar('o', hiresFileName.size() - 2); + hiresFileName.setChar('b', hiresFileName.size() - 1); + + if (_engine->getScriptManager()->getStateValue(StateKey_MPEGMovies) == 1 &&_engine->getSearchManager()->hasFile(hiresFileName)) { + // TODO: Enable once AC3 support is implemented + if (!_engine->getSearchManager()->hasFile(_fileName)) // Check for the regular video return true; - } - - _engine->getCursorManager()->showMouse(false); + warning("The hires videos of the DVD version of ZGI aren't supported yet, using lowres"); + //_fileName = hiresFileName; + //switchToHires = true; + } else if (!_engine->getSearchManager()->hasFile(_fileName)) + return true; +#else + if (!_engine->getSearchManager()->hasFile(_fileName)) + return true; +#endif - Common::Rect destRect = Common::Rect(_x1, _y1, _x2 + 1, _y2 + 1); + decoder = _engine->loadAnimation(_fileName); + Subtitle *sub = (subtitleExists) ? new Subtitle(_engine, subname, switchToHires) : NULL; - Common::String subname = _fileName; - subname.setChar('s', subname.size() - 3); - subname.setChar('u', subname.size() - 2); - subname.setChar('b', subname.size() - 1); + _engine->getCursorManager()->showMouse(false); - Subtitle *sub = NULL; + if (switchToHires) { + _engine->initHiresScreen(); + destRect = Common::Rect(40, -40, 760, 440); + Common::Rect workingWindow = _engine->_workingWindow; + workingWindow.translate(0, -40); + _engine->getRenderManager()->initSubArea(HIRES_WINDOW_WIDTH, HIRES_WINDOW_HEIGHT, workingWindow); + } - if (_engine->getSearchManager()->hasFile(subname)) - sub = new Subtitle(_engine, subname); + _engine->playVideo(*decoder, destRect, _skippable, sub); - _engine->playVideo(decoder, destRect, _skippable, sub); + if (switchToHires) { + _engine->initScreen(); + _engine->getRenderManager()->initSubArea(WINDOW_WIDTH, WINDOW_HEIGHT, _engine->_workingWindow); + } - _engine->getCursorManager()->showMouse(true); + _engine->getCursorManager()->showMouse(true); - if (sub) - delete sub; - } + delete decoder; + delete sub; return true; } diff --git a/engines/zvision/scripting/controls/save_control.cpp b/engines/zvision/scripting/controls/save_control.cpp index 6cedddffeb..2ac77c4776 100644 --- a/engines/zvision/scripting/controls/save_control.cpp +++ b/engines/zvision/scripting/controls/save_control.cpp @@ -102,9 +102,7 @@ bool SaveControl::process(uint32 deltaTimeInMillis) { toSave = false; if (toSave) { - // FIXME: At this point, the screen shows the save control, so the save game thumbnails will always - // show the save control - _engine->getSaveManager()->saveGameBuffered(iter->saveId, inp->getText()); + _engine->getSaveManager()->saveGame(iter->saveId, inp->getText(), true); _engine->getRenderManager()->delayedMessage(_engine->getStringManager()->getTextLine(StringManager::ZVISION_STR_SAVED), 2000); _engine->getScriptManager()->changeLocation(_engine->getScriptManager()->getLastMenuLocation()); } diff --git a/engines/zvision/scripting/scr_file_handling.cpp b/engines/zvision/scripting/scr_file_handling.cpp index 227c43557c..b4da61a119 100644 --- a/engines/zvision/scripting/scr_file_handling.cpp +++ b/engines/zvision/scripting/scr_file_handling.cpp @@ -270,6 +270,8 @@ void ScriptManager::parseResults(Common::SeekableReadStream &stream, Common::Lis // Only used by Zork: Nemesis actionList.push_back(new ActionRegion(_engine, slot, args)); } else if (act.matchString("restore_game", true)) { + // Only used by ZGI to load the restart game slot, r.svr. + // Used by the credits screen. actionList.push_back(new ActionRestoreGame(_engine, slot, args)); } else if (act.matchString("rotate_to", true)) { actionList.push_back(new ActionRotateTo(_engine, slot, args)); diff --git a/engines/zvision/scripting/script_manager.cpp b/engines/zvision/scripting/script_manager.cpp index 464e8bfe4d..ad049434c3 100644 --- a/engines/zvision/scripting/script_manager.cpp +++ b/engines/zvision/scripting/script_manager.cpp @@ -500,7 +500,7 @@ void ScriptManager::changeLocation(char _world, char _room, char _node, char _vi _nextLocation.node = _node; _nextLocation.view = _view; _nextLocation.offset = offset; - // If next location 0000 - it's indicate to go to previous location. + // If next location is 0000, return to the previous location. if (_nextLocation.world == '0' && _nextLocation.room == '0' && _nextLocation.node == '0' && _nextLocation.view == '0') { if (getStateValue(StateKey_World) != 'g' || getStateValue(StateKey_Room) != 'j') { _nextLocation.world = getStateValue(StateKey_LastWorld); diff --git a/engines/zvision/scripting/script_manager.h b/engines/zvision/scripting/script_manager.h index 78c1b77dea..f6201c3572 100644 --- a/engines/zvision/scripting/script_manager.h +++ b/engines/zvision/scripting/script_manager.h @@ -87,6 +87,7 @@ enum StateKey { StateKey_JapanFonts = 75, StateKey_ExecScopeStyle = 76, StateKey_Brightness = 77, + StateKey_MPEGMovies = 78, StateKey_EF9_R = 91, StateKey_EF9_G = 92, StateKey_EF9_B = 93, diff --git a/engines/zvision/sound/zork_raw.cpp b/engines/zvision/sound/zork_raw.cpp index 6d1980b1af..0ef5de2f55 100644 --- a/engines/zvision/sound/zork_raw.cpp +++ b/engines/zvision/sound/zork_raw.cpp @@ -242,11 +242,27 @@ Audio::RewindableAudioStream *makeRawZorkStream(Common::SeekableReadStream *stre Audio::RewindableAudioStream *makeRawZorkStream(const Common::String &filePath, ZVision *engine) { Common::File *file = new Common::File(); - if (!engine->getSearchManager()->openFile(*file, filePath)) - error("File not found: %s", filePath.c_str()); + Common::String actualName = filePath; + bool found = engine->getSearchManager()->openFile(*file, actualName); + bool isRaw = actualName.hasSuffix(".raw"); + + if ((!found && isRaw) || (found && isRaw && file->size() < 10)) { + if (found) + file->close(); + + // Check for an audio patch (.src) + actualName.setChar('s', actualName.size() - 3); + actualName.setChar('r', actualName.size() - 2); + actualName.setChar('c', actualName.size() - 1); + + if (!engine->getSearchManager()->openFile(*file, actualName)) + error("File not found: %s", actualName.c_str()); + } else if (!found && !isRaw) { + error("File not found: %s", actualName.c_str()); + } // Get the file name - Common::StringTokenizer tokenizer(filePath, "/\\"); + Common::StringTokenizer tokenizer(actualName, "/\\"); Common::String fileName; while (!tokenizer.empty()) { fileName = tokenizer.nextToken(); diff --git a/engines/zvision/text/string_manager.cpp b/engines/zvision/text/string_manager.cpp index 6b870d0b8d..c62e18f4b0 100644 --- a/engines/zvision/text/string_manager.cpp +++ b/engines/zvision/text/string_manager.cpp @@ -51,9 +51,8 @@ void StringManager::initialize(ZVisionGameId gameId) { void StringManager::loadStrFile(const Common::String &fileName) { Common::File file; - if (!_engine->getSearchManager()->openFile(file, fileName)) { + if (!_engine->getSearchManager()->openFile(file, fileName)) error("%s does not exist. String parsing failed", fileName.c_str()); - } uint lineNumber = 0; while (!file.eos()) { diff --git a/engines/zvision/text/subtitles.cpp b/engines/zvision/text/subtitles.cpp index acf4c37c2f..ffc9e2b808 100644 --- a/engines/zvision/text/subtitles.cpp +++ b/engines/zvision/text/subtitles.cpp @@ -27,7 +27,7 @@ namespace ZVision { -Subtitle::Subtitle(ZVision *engine, const Common::String &subname) : +Subtitle::Subtitle(ZVision *engine, const Common::String &subname, bool upscaleToHires) : _engine(engine), _areaId(-1), _subId(-1) { @@ -44,6 +44,8 @@ Subtitle::Subtitle(ZVision *engine, const Common::String &subname) : int32 x1, y1, x2, y2; sscanf(str.c_str(), "%*[^:]:%d %d %d %d", &x1, &y1, &x2, &y2); Common::Rect rct = Common::Rect(x1, y1, x2, y2); + if (upscaleToHires) + _engine->getRenderManager()->upscaleRect(rct); _areaId = _engine->getRenderManager()->createSubArea(rct); } else if (str.matchString("*TextFile*", true)) { char filename[64]; @@ -67,6 +69,11 @@ Subtitle::Subtitle(ZVision *engine, const Common::String &subname) : int32 sb; if (sscanf(str.c_str(), "%*[^:]:(%d,%d)=%d", &st, &en, &sb) == 3) { if (sb <= (int32)_subs.size()) { + if (upscaleToHires) { + // Convert from 15FPS (AVI) to 29.97FPS (VOB) + st = st * 2997 / 1500; + en = en * 2997 / 1500; + } _subs[sb].start = st; _subs[sb].stop = en; } diff --git a/engines/zvision/text/subtitles.h b/engines/zvision/text/subtitles.h index c3da6583a4..329339be55 100644 --- a/engines/zvision/text/subtitles.h +++ b/engines/zvision/text/subtitles.h @@ -31,7 +31,7 @@ class ZVision; class Subtitle { public: - Subtitle(ZVision *engine, const Common::String &subname); + Subtitle(ZVision *engine, const Common::String &subname, bool upscaleToHires = false); ~Subtitle(); void process(int32 time); diff --git a/engines/zvision/video/video.cpp b/engines/zvision/video/video.cpp index 0913b28818..d5ffbeb536 100644 --- a/engines/zvision/video/video.cpp +++ b/engines/zvision/video/video.cpp @@ -23,13 +23,16 @@ #include "common/scummsys.h" #include "common/system.h" #include "video/video_decoder.h" +#ifdef USE_MPEG2 +#include "video/mpegps_decoder.h" +#endif #include "engines/util.h" #include "graphics/surface.h" #include "zvision/zvision.h" #include "zvision/core/clock.h" #include "zvision/graphics/render_manager.h" -#include "zvision/scripting//script_manager.h" +#include "zvision/scripting/script_manager.h" #include "zvision/text/subtitles.h" #include "zvision/video/rlf_decoder.h" #include "zvision/video/zork_avi_decoder.h" @@ -45,6 +48,10 @@ Video::VideoDecoder *ZVision::loadAnimation(const Common::String &fileName) { animation = new RLFDecoder(); else if (tmpFileName.hasSuffix(".avi")) animation = new ZorkAVIDecoder(); +#ifdef USE_MPEG2 + else if (tmpFileName.hasSuffix(".vob")) + animation = new Video::MPEGPSDecoder(); +#endif else error("Unknown suffix for animation %s", fileName.c_str()); diff --git a/engines/zvision/zvision.cpp b/engines/zvision/zvision.cpp index 54991aced3..b42906fef3 100644 --- a/engines/zvision/zvision.cpp +++ b/engines/zvision/zvision.cpp @@ -52,7 +52,7 @@ namespace ZVision { -#define ZVISION_SETTINGS_KEYS_COUNT 11 +#define ZVISION_SETTINGS_KEYS_COUNT 12 struct zvisionIniSettings { const char *name; @@ -73,7 +73,8 @@ struct zvisionIniSettings { {"panarotatespeed", StateKey_RotateSpeed, 540, false, true}, // checked by universe.scr {"noanimwhileturning", StateKey_NoTurnAnim, -1, false, true}, // toggle playing animations during pana rotation {"venusenabled", StateKey_VenusEnable, -1, true, true}, - {"subtitles", StateKey_Subtitles, -1, true, true} + {"subtitles", StateKey_Subtitles, -1, true, true}, + {"mpegmovies", StateKey_MPEGMovies, -1, true, true} // Zork: Grand Inquisitor DVD hi-res MPEG movies (0 = normal, 1 = hires, 2 = disable option) }; ZVision::ZVision(OSystem *syst, const ZVisionGameDescription *gameDesc) @@ -105,15 +106,6 @@ ZVision::ZVision(OSystem *syst, const ZVisionGameDescription *gameDesc) debug(1, "ZVision::ZVision"); - uint16 workingWindowWidth = (gameDesc->gameId == GID_NEMESIS) ? ZNM_WORKING_WINDOW_WIDTH : ZGI_WORKING_WINDOW_WIDTH; - uint16 workingWindowHeight = (gameDesc->gameId == GID_NEMESIS) ? ZNM_WORKING_WINDOW_HEIGHT : ZGI_WORKING_WINDOW_HEIGHT; - _workingWindow = Common::Rect( - (WINDOW_WIDTH - workingWindowWidth) / 2, - (WINDOW_HEIGHT - workingWindowHeight) / 2, - ((WINDOW_WIDTH - workingWindowWidth) / 2) + workingWindowWidth, - ((WINDOW_HEIGHT - workingWindowHeight) / 2) + workingWindowHeight - ); - memset(_cheatBuffer, 0, sizeof(_cheatBuffer)); } @@ -193,25 +185,17 @@ void ZVision::initialize() { _searchManager->addDir("addon"); if (_gameDescription->gameId == GID_GRANDINQUISITOR) { - _searchManager->loadZix("INQUIS.ZIX"); - _searchManager->addPatch("C000H01Q.RAW", "C000H01Q.SRC"); - _searchManager->addPatch("CM00H01Q.RAW", "CM00H01Q.SRC"); - _searchManager->addPatch("DM00H01Q.RAW", "DM00H01Q.SRC"); - _searchManager->addPatch("E000H01Q.RAW", "E000H01Q.SRC"); - _searchManager->addPatch("EM00H50Q.RAW", "EM00H50Q.SRC"); - _searchManager->addPatch("GJNPH65P.RAW", "GJNPH65P.SRC"); - _searchManager->addPatch("GJNPH72P.RAW", "GJNPH72P.SRC"); - _searchManager->addPatch("H000H01Q.RAW", "H000H01Q.SRC"); - _searchManager->addPatch("M000H01Q.RAW", "M000H01Q.SRC"); - _searchManager->addPatch("P000H01Q.RAW", "P000H01Q.SRC"); - _searchManager->addPatch("Q000H01Q.RAW", "Q000H01Q.SRC"); - _searchManager->addPatch("SW00H01Q.RAW", "SW00H01Q.SRC"); - _searchManager->addPatch("T000H01Q.RAW", "T000H01Q.SRC"); - _searchManager->addPatch("U000H01Q.RAW", "U000H01Q.SRC"); - } else if (_gameDescription->gameId == GID_NEMESIS) - _searchManager->loadZix("NEMESIS.ZIX"); + if (!_searchManager->loadZix("INQUIS.ZIX")) + error("Unable to load the game ZIX file"); + } else if (_gameDescription->gameId == GID_NEMESIS) { + if (!_searchManager->loadZix("NEMESIS.ZIX")) { + // The game might not be installed, try MEDIUM.ZIX instead + if (!_searchManager->loadZix("ZNEMSCR/MEDIUM.ZIX")) + error("Unable to load the game ZIX file"); + } + } - initGraphics(WINDOW_WIDTH, WINDOW_HEIGHT, true, &_screenPixelFormat); + initScreen(); // Register random source _rnd = new Common::RandomSource("zvision"); @@ -239,6 +223,11 @@ void ZVision::initialize() { loadSettings(); +#ifndef USE_MPEG2 + // libmpeg2 not loaded, disable the MPEG2 movies option + _scriptManager->setStateValue(StateKey_MPEGMovies, 2); +#endif + // Create debugger console. It requires GFX to be initialized _console = new Console(this); _doubleFPS = ConfMan.getBool("doublefps"); @@ -358,4 +347,23 @@ void ZVision::fpsTimer() { _renderedFrameCount = 0; } +void ZVision::initScreen() { + uint16 workingWindowWidth = (_gameDescription->gameId == GID_NEMESIS) ? ZNM_WORKING_WINDOW_WIDTH : ZGI_WORKING_WINDOW_WIDTH; + uint16 workingWindowHeight = (_gameDescription->gameId == GID_NEMESIS) ? ZNM_WORKING_WINDOW_HEIGHT : ZGI_WORKING_WINDOW_HEIGHT; + _workingWindow = Common::Rect( + (WINDOW_WIDTH - workingWindowWidth) / 2, + (WINDOW_HEIGHT - workingWindowHeight) / 2, + ((WINDOW_WIDTH - workingWindowWidth) / 2) + workingWindowWidth, + ((WINDOW_HEIGHT - workingWindowHeight) / 2) + workingWindowHeight + ); + + initGraphics(WINDOW_WIDTH, WINDOW_HEIGHT, true, &_screenPixelFormat); +} + +void ZVision::initHiresScreen() { + _renderManager->upscaleRect(_workingWindow); + + initGraphics(HIRES_WINDOW_WIDTH, HIRES_WINDOW_HEIGHT, true, &_screenPixelFormat); +} + } // End of namespace ZVision diff --git a/engines/zvision/zvision.h b/engines/zvision/zvision.h index ad22ddaaa2..854cd77bb8 100644 --- a/engines/zvision/zvision.h +++ b/engines/zvision/zvision.h @@ -67,6 +67,27 @@ class TextRenderer; class Subtitle; class MidiManager; +enum { + WINDOW_WIDTH = 640, + WINDOW_HEIGHT = 480, + + HIRES_WINDOW_WIDTH = 800, + HIRES_WINDOW_HEIGHT = 600, + + // Zork nemesis working window sizes + ZNM_WORKING_WINDOW_WIDTH = 512, + ZNM_WORKING_WINDOW_HEIGHT = 320, + + // ZGI working window sizes + ZGI_WORKING_WINDOW_WIDTH = 640, + ZGI_WORKING_WINDOW_HEIGHT = 344, + + ROTATION_SCREEN_EDGE_OFFSET = 60, + MAX_ROTATION_SPEED = 400, // Pixels per second + + KEYBUF_SIZE = 20 +}; + class ZVision : public Engine { public: ZVision(OSystem *syst, const ZVisionGameDescription *gameDesc); @@ -83,24 +104,6 @@ public: const Graphics::PixelFormat _screenPixelFormat; private: - enum { - WINDOW_WIDTH = 640, - WINDOW_HEIGHT = 480, - - // Zork nemesis working window sizes - ZNM_WORKING_WINDOW_WIDTH = 512, - ZNM_WORKING_WINDOW_HEIGHT = 320, - - // ZGI working window sizes - ZGI_WORKING_WINDOW_WIDTH = 640, - ZGI_WORKING_WINDOW_HEIGHT = 344, - - ROTATION_SCREEN_EDGE_OFFSET = 60, - MAX_ROTATION_SPEED = 400, // Pixels per second - - KEYBUF_SIZE = 20 - }; - Console *_console; const ZVisionGameDescription *_gameDescription; @@ -194,6 +197,9 @@ public: _clock.stop(); } + void initScreen(); + void initHiresScreen(); + /** * Play a video until it is finished. This is a blocking call. It will call * _clock.stop() when the video starts and _clock.start() when the video finishes. diff --git a/video/module.mk b/video/module.mk index 5754350e42..be014598f2 100644 --- a/video/module.mk +++ b/video/module.mk @@ -5,6 +5,7 @@ MODULE_OBJS := \ coktel_decoder.o \ dxa_decoder.o \ flic_decoder.o \ + mpegps_decoder.o \ psx_decoder.o \ qt_decoder.o \ smk_decoder.o \ diff --git a/video/mpegps_decoder.cpp b/video/mpegps_decoder.cpp new file mode 100644 index 0000000000..d8f7f5a68c --- /dev/null +++ b/video/mpegps_decoder.cpp @@ -0,0 +1,732 @@ +/* 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. + * + */ + +#include "audio/audiostream.h" +#include "audio/decoders/raw.h" +#include "common/debug.h" +#include "common/endian.h" +#include "common/stream.h" +#include "common/system.h" +#include "common/textconsole.h" + +#include "video/mpegps_decoder.h" +#include "image/codecs/mpeg.h" + +// The demuxing code is based on libav's demuxing code + +namespace Video { + +enum { + kStartCodePack = 0x1BA, + kStartCodeSystemHeader = 0x1BB, + kStartCodeProgramStreamMap = 0x1BC, + kStartCodePrivateStream1 = 0x1BD, + kStartCodePaddingStream = 0x1BE, + kStartCodePrivateStream2 = 0x1BF +}; + +MPEGPSDecoder::MPEGPSDecoder() { + _stream = 0; + memset(_psmESType, 0, 256); +} + +MPEGPSDecoder::~MPEGPSDecoder() { + close(); +} + +bool MPEGPSDecoder::loadStream(Common::SeekableReadStream *stream) { + close(); + + _stream = stream; + + if (!addFirstVideoTrack()) { + close(); + return false; + } + + _stream->seek(0); + return true; +} + +void MPEGPSDecoder::close() { + VideoDecoder::close(); + + delete _stream; + _stream = 0; + + _streamMap.clear(); + + memset(_psmESType, 0, 256); +} + +void MPEGPSDecoder::readNextPacket() { + if (_stream->eos()) + return; + + for (;;) { + int32 startCode; + uint32 pts, dts; + int size = readNextPacketHeader(startCode, pts, dts); + + if (size < 0) { + // End of stream + for (TrackListIterator it = getTrackListBegin(); it != getTrackListEnd(); it++) + if ((*it)->getTrackType() == Track::kTrackTypeVideo) + ((MPEGVideoTrack *)*it)->setEndOfTrack(); + return; + } + + MPEGStream *stream = 0; + Common::SeekableReadStream *packet = _stream->readStream(size); + + if (_streamMap.contains(startCode)) { + // We already found the stream + stream = _streamMap[startCode]; + } else { + // We haven't seen this before + + if (startCode == kStartCodePrivateStream1) { + PrivateStreamType streamType = detectPrivateStreamType(packet); + packet->seek(0); + + // TODO: Handling of these types (as needed) + + const char *typeName; + + switch (streamType) { + case kPrivateStreamAC3: + typeName = "AC-3"; + break; + case kPrivateStreamDTS: + typeName = "DTS"; + break; + case kPrivateStreamDVDPCM: + typeName = "DVD PCM"; + break; + case kPrivateStreamPS2Audio: + typeName = "PS2 Audio"; + break; + default: + typeName = "Unknown"; + break; + } + + warning("Unhandled DVD private stream: %s", typeName); + + // Make it 0 so we don't get the warning twice + _streamMap[startCode] = 0; + } else if (startCode >= 0x1E0 && startCode <= 0x1EF) { + // Video stream + // TODO: Multiple video streams + warning("Found extra video stream 0x%04X", startCode); + _streamMap[startCode] = 0; + } else if (startCode >= 0x1C0 && startCode <= 0x1DF) { +#ifdef USE_MAD + // MPEG Audio stream + MPEGAudioTrack *audioTrack = new MPEGAudioTrack(packet); + stream = audioTrack; + _streamMap[startCode] = audioTrack; + addTrack(audioTrack); +#else + warning("Found audio stream 0x%04X, but no MAD support compiled in", startCode); + _streamMap[startCode] = 0; +#endif + } else { + // Probably not relevant + debug(0, "Found unhandled MPEG-PS stream type 0x%04x", startCode); + _streamMap[startCode] = 0; + } + } + + if (stream) { + bool done = stream->sendPacket(packet, pts, dts); + + if (done && stream->getStreamType() == MPEGStream::kStreamTypeVideo) + return; + } else { + delete packet; + } + } +} + +#define MAX_SYNC_SIZE 100000 + +int MPEGPSDecoder::findNextStartCode(uint32 &size) { + size = MAX_SYNC_SIZE; + int32 state = 0xFF; + + while (size > 0) { + byte v = _stream->readByte(); + + if (_stream->eos()) + return -1; + + size--; + + if (state == 0x1) + return ((state << 8) | v) & 0xFFFFFF; + + state = ((state << 8) | v) & 0xFFFFFF; + } + + return -1; +} + +int MPEGPSDecoder::readNextPacketHeader(int32 &startCode, uint32 &pts, uint32 &dts) { + for (;;) { + uint32 size; + startCode = findNextStartCode(size); + + if (_stream->eos()) + return -1; + + if (startCode < 0) + continue; + + uint32 lastSync = _stream->pos(); + + if (startCode == kStartCodePack || startCode == kStartCodeSystemHeader) + continue; + + int length = _stream->readUint16BE(); + + if (startCode == kStartCodePaddingStream || startCode == kStartCodePrivateStream2) { + _stream->skip(length); + continue; + } + + if (startCode == kStartCodeProgramStreamMap) { + parseProgramStreamMap(length); + continue; + } + + // Find matching stream + if (!((startCode >= 0x1C0 && startCode <= 0x1DF) || + (startCode >= 0x1E0 && startCode <= 0x1EF) || + startCode == kStartCodePrivateStream1 || startCode == 0x1FD)) + continue; + + // Stuffing + byte c; + for (;;) { + if (length < 1) { + _stream->seek(lastSync); + continue; + } + + c = _stream->readByte(); + length--; + + // XXX: for mpeg1, should test only bit 7 + if (c != 0xFF) + break; + } + + if ((c & 0xC0) == 0x40) { + // Buffer scale and size + _stream->readByte(); + c = _stream->readByte(); + length -= 2; + } + + pts = 0xFFFFFFFF; + dts = 0xFFFFFFFF; + + if ((c & 0xE0) == 0x20) { + dts = pts = readPTS(c); + length -= 4; + + if (c & 0x10) { + dts = readPTS(-1); + length -= 5; + } + } else if ((c & 0xC0) == 0x80) { + // MPEG-2 PES + byte flags = _stream->readByte(); + int headerLength = _stream->readByte(); + length -= 2; + + if (headerLength > length) { + _stream->seek(lastSync); + continue; + } + + length -= headerLength; + + if (flags & 0x80) { + dts = pts = readPTS(-1); + headerLength -= 5; + + if (flags & 0x40) { + dts = readPTS(-1); + headerLength -= 5; + } + } + + if (flags & 0x3F && headerLength == 0) { + flags &= 0xC0; + warning("Further flags set but no bytes left"); + } + + if (flags & 0x01) { // PES extension + byte pesExt =_stream->readByte(); + headerLength--; + + // Skip PES private data, program packet sequence + int skip = (pesExt >> 4) & 0xB; + skip += skip & 0x9; + + if (pesExt & 0x40 || skip > headerLength) { + warning("pesExt %x is invalid", pesExt); + pesExt = skip = 0; + } else { + _stream->skip(skip); + headerLength -= skip; + } + + if (pesExt & 0x01) { // PES extension 2 + byte ext2Length = _stream->readByte(); + headerLength--; + + if ((ext2Length & 0x7F) != 0) { + byte idExt = _stream->readByte(); + + if ((idExt & 0x80) == 0) + startCode = (startCode & 0xFF) << 8; + + headerLength--; + } + } + } + + if (headerLength < 0) { + _stream->seek(lastSync); + continue; + } + + _stream->skip(headerLength); + } else if (c != 0xF) { + continue; + } + + if (length < 0) { + _stream->seek(lastSync); + continue; + } + + return length; + } +} + +uint32 MPEGPSDecoder::readPTS(int c) { + byte buf[5]; + + buf[0] = (c < 0) ? _stream->readByte() : c; + _stream->read(buf + 1, 4); + + return ((buf[0] & 0x0E) << 29) | ((READ_BE_UINT16(buf + 1) >> 1) << 15) | (READ_BE_UINT16(buf + 3) >> 1); +} + +void MPEGPSDecoder::parseProgramStreamMap(int length) { + _stream->readByte(); + _stream->readByte(); + + // skip program stream info + _stream->skip(_stream->readUint16BE()); + + int esMapLength = _stream->readUint16BE(); + + while (esMapLength >= 4) { + byte type = _stream->readByte(); + byte esID = _stream->readByte(); + uint16 esInfoLength = _stream->readUint16BE(); + + // Remember mapping from stream id to stream type + _psmESType[esID] = type; + + // Skip program stream info + _stream->skip(esInfoLength); + + esMapLength -= 4 + esInfoLength; + } + + _stream->readUint32BE(); // CRC32 +} + +bool MPEGPSDecoder::addFirstVideoTrack() { + for (;;) { + int32 startCode; + uint32 pts, dts; + int size = readNextPacketHeader(startCode, pts, dts); + + // End of stream? We failed + if (size < 0) + return false; + + if (startCode >= 0x1E0 && startCode <= 0x1EF) { + // Video stream + // Can be MPEG-1/2 or MPEG-4/h.264. We'll assume the former and + // I hope we never need the latter. + Common::SeekableReadStream *firstPacket = _stream->readStream(size); + MPEGVideoTrack *track = new MPEGVideoTrack(firstPacket, getDefaultHighColorFormat()); + addTrack(track); + _streamMap[startCode] = track; + delete firstPacket; + break; + } + + _stream->skip(size); + } + + return true; +} + +MPEGPSDecoder::PrivateStreamType MPEGPSDecoder::detectPrivateStreamType(Common::SeekableReadStream *packet) { + uint32 dvdCode = packet->readUint32LE(); + if (packet->eos()) + return kPrivateStreamUnknown; + + uint32 ps2Header = packet->readUint32BE(); + if (!packet->eos() && ps2Header == MKTAG('S', 'S', 'h', 'd')) + return kPrivateStreamPS2Audio; + + switch (dvdCode & 0xE0) { + case 0x80: + if ((dvdCode & 0xF8) == 0x88) + return kPrivateStreamDTS; + + return kPrivateStreamAC3; + case 0xA0: + return kPrivateStreamDVDPCM; + } + + return kPrivateStreamUnknown; +} + +MPEGPSDecoder::MPEGVideoTrack::MPEGVideoTrack(Common::SeekableReadStream *firstPacket, const Graphics::PixelFormat &format) { + _surface = 0; + _endOfTrack = false; + _curFrame = -1; + _nextFrameStartTime = Audio::Timestamp(0, 27000000); // 27 MHz timer + + findDimensions(firstPacket, format); + +#ifdef USE_MPEG2 + _mpegDecoder = new Image::MPEGDecoder(); +#endif +} + +MPEGPSDecoder::MPEGVideoTrack::~MPEGVideoTrack() { +#ifdef USE_MPEG2 + delete _mpegDecoder; +#endif + + if (_surface) { + _surface->free(); + delete _surface; + } +} + +uint16 MPEGPSDecoder::MPEGVideoTrack::getWidth() const { + return _surface ? _surface->w : 0; +} + +uint16 MPEGPSDecoder::MPEGVideoTrack::getHeight() const { + return _surface ? _surface->h : 0; +} + +Graphics::PixelFormat MPEGPSDecoder::MPEGVideoTrack::getPixelFormat() const { + if (!_surface) + return Graphics::PixelFormat(); + + return _surface->format; +} + +const Graphics::Surface *MPEGPSDecoder::MPEGVideoTrack::decodeNextFrame() { + return _surface; +} + +bool MPEGPSDecoder::MPEGVideoTrack::sendPacket(Common::SeekableReadStream *packet, uint32 pts, uint32 dts) { +#ifdef USE_MPEG2 + uint32 framePeriod; + bool foundFrame = _mpegDecoder->decodePacket(*packet, framePeriod, _surface); + + if (foundFrame) { + _curFrame++; + _nextFrameStartTime = _nextFrameStartTime.addFrames(framePeriod); + } +#endif + + delete packet; + +#ifdef USE_MPEG2 + return foundFrame; +#else + return true; +#endif +} + +void MPEGPSDecoder::MPEGVideoTrack::findDimensions(Common::SeekableReadStream *firstPacket, const Graphics::PixelFormat &format) { + // First, check for the picture start code + if (firstPacket->readUint32BE() != 0x1B3) + error("Failed to detect MPEG sequence start"); + + // This is part of the bitstream, but there's really no purpose + // to use Common::BitStream just for this: 12 bits width, 12 bits + // height + uint16 width = firstPacket->readByte() << 4; + uint16 height = firstPacket->readByte(); + width |= (height & 0xF0) >> 4; + height = ((height & 0x0F) << 8) | firstPacket->readByte(); + + debug(0, "MPEG dimensions: %dx%d", width, height); + + _surface = new Graphics::Surface(); + _surface->create(width, height, format); + + firstPacket->seek(0); +} + +#ifdef USE_MAD + +// The audio code here is almost entirely based on what we do in mp3.cpp + +MPEGPSDecoder::MPEGAudioTrack::MPEGAudioTrack(Common::SeekableReadStream *firstPacket) { + // The MAD_BUFFER_GUARD must always contain zeros (the reason + // for this is that the Layer III Huffman decoder of libMAD + // may read a few bytes beyond the end of the input buffer). + memset(_buf + BUFFER_SIZE, 0, MAD_BUFFER_GUARD); + + _state = MP3_STATE_INIT; + _audStream = 0; + + // Find out our audio parameters + initStream(firstPacket); + + while (_state != MP3_STATE_EOS) + readHeader(firstPacket); + + _audStream = Audio::makeQueuingAudioStream(_frame.header.samplerate, MAD_NCHANNELS(&_frame.header) == 2); + + deinitStream(); + + firstPacket->seek(0); + _state = MP3_STATE_INIT; +} + +MPEGPSDecoder::MPEGAudioTrack::~MPEGAudioTrack() { + deinitStream(); + delete _audStream; +} + +static inline int scaleSample(mad_fixed_t sample) { + // round + sample += (1L << (MAD_F_FRACBITS - 16)); + + // clip + if (sample > MAD_F_ONE - 1) + sample = MAD_F_ONE - 1; + else if (sample < -MAD_F_ONE) + sample = -MAD_F_ONE; + + // quantize and scale to not saturate when mixing a lot of channels + return sample >> (MAD_F_FRACBITS + 1 - 16); +} + +bool MPEGPSDecoder::MPEGAudioTrack::sendPacket(Common::SeekableReadStream *packet, uint32 pts, uint32 dts) { + while (_state != MP3_STATE_EOS) + decodeMP3Data(packet); + + _state = MP3_STATE_READY; + delete packet; + return true; +} + +Audio::AudioStream *MPEGPSDecoder::MPEGAudioTrack::getAudioStream() const { + return _audStream; +} + +void MPEGPSDecoder::MPEGAudioTrack::initStream(Common::SeekableReadStream *packet) { + if (_state != MP3_STATE_INIT) + deinitStream(); + + // Init MAD + mad_stream_init(&_stream); + mad_frame_init(&_frame); + mad_synth_init(&_synth); + + // Reset the stream data + packet->seek(0, SEEK_SET); + + // Update state + _state = MP3_STATE_READY; + + // Read the first few sample bytes + readMP3Data(packet); +} + +void MPEGPSDecoder::MPEGAudioTrack::deinitStream() { + if (_state == MP3_STATE_INIT) + return; + + // Deinit MAD + mad_synth_finish(&_synth); + mad_frame_finish(&_frame); + mad_stream_finish(&_stream); + + _state = MP3_STATE_EOS; +} + +void MPEGPSDecoder::MPEGAudioTrack::readMP3Data(Common::SeekableReadStream *packet) { + uint32 remaining = 0; + + // Give up immediately if we already used up all data in the stream + if (packet->eos()) { + _state = MP3_STATE_EOS; + return; + } + + if (_stream.next_frame) { + // If there is still data in the MAD stream, we need to preserve it. + // Note that we use memmove, as we are reusing the same buffer, + // and hence the data regions we copy from and to may overlap. + remaining = _stream.bufend - _stream.next_frame; + assert(remaining < BUFFER_SIZE); // Paranoia check + memmove(_buf, _stream.next_frame, remaining); + } + + memset(_buf + remaining, 0, BUFFER_SIZE - remaining); + + // Try to read the next block + uint32 size = packet->read(_buf + remaining, BUFFER_SIZE - remaining); + if (size == 0) { + _state = MP3_STATE_EOS; + return; + } + + // Feed the data we just read into the stream decoder + _stream.error = MAD_ERROR_NONE; + mad_stream_buffer(&_stream, _buf, size + remaining); +} + +void MPEGPSDecoder::MPEGAudioTrack::readHeader(Common::SeekableReadStream *packet) { + if (_state != MP3_STATE_READY) + return; + + // If necessary, load more data into the stream decoder + if (_stream.error == MAD_ERROR_BUFLEN) + readMP3Data(packet); + + while (_state != MP3_STATE_EOS) { + _stream.error = MAD_ERROR_NONE; + + // Decode the next header. Note: mad_frame_decode would do this for us, too. + // However, for seeking we don't want to decode the full frame (else it would + // be far too slow). Hence we perform this explicitly in a separate step. + if (mad_header_decode(&_frame.header, &_stream) == -1) { + if (_stream.error == MAD_ERROR_BUFLEN) { + readMP3Data(packet); // Read more data + continue; + } else if (MAD_RECOVERABLE(_stream.error)) { + debug(6, "MPEGAudioTrack::readHeader(): Recoverable error in mad_header_decode (%s)", mad_stream_errorstr(&_stream)); + continue; + } else { + warning("MPEGAudioTrack::readHeader(): Unrecoverable error in mad_header_decode (%s)", mad_stream_errorstr(&_stream)); + break; + } + } + + break; + } + + if (_stream.error != MAD_ERROR_NONE) + _state = MP3_STATE_EOS; +} + +void MPEGPSDecoder::MPEGAudioTrack::decodeMP3Data(Common::SeekableReadStream *packet) { + if (_state == MP3_STATE_INIT) + initStream(packet); + + if (_state == MP3_STATE_EOS) + return; + + do { + // If necessary, load more data into the stream decoder + if (_stream.error == MAD_ERROR_BUFLEN) + readMP3Data(packet); + + while (_state == MP3_STATE_READY) { + _stream.error = MAD_ERROR_NONE; + + // Decode the next frame + if (mad_frame_decode(&_frame, &_stream) == -1) { + if (_stream.error == MAD_ERROR_BUFLEN) { + break; // Read more data + } else if (MAD_RECOVERABLE(_stream.error)) { + // Note: we will occasionally see MAD_ERROR_BADDATAPTR errors here. + // These are normal and expected (caused by our frame skipping (i.e. "seeking") + // code above). + debug(6, "MPEGAudioTrack::decodeMP3Data(): Recoverable error in mad_frame_decode (%s)", mad_stream_errorstr(&_stream)); + continue; + } else { + warning("MPEGAudioTrack::decodeMP3Data(): Unrecoverable error in mad_frame_decode (%s)", mad_stream_errorstr(&_stream)); + break; + } + } + + // Synthesize PCM data + mad_synth_frame(&_synth, &_frame); + + // Output it to our queue + if (_synth.pcm.length != 0) { + byte *buffer = (byte *)malloc(_synth.pcm.length * 2 * MAD_NCHANNELS(&_frame.header)); + int16 *ptr = (int16 *)buffer; + + for (int i = 0; i < _synth.pcm.length; i++) { + *ptr++ = (int16)scaleSample(_synth.pcm.samples[0][i]); + + if (MAD_NCHANNELS(&_frame.header) == 2) + *ptr++ = (int16)scaleSample(_synth.pcm.samples[1][i]); + } + + int flags = Audio::FLAG_16BITS; + + if (_audStream->isStereo()) + flags |= Audio::FLAG_STEREO; + +#ifdef SCUMM_LITTLE_ENDIAN + flags |= Audio::FLAG_LITTLE_ENDIAN; +#endif + + _audStream->queueBuffer(buffer, _synth.pcm.length * 2 * MAD_NCHANNELS(&_frame.header), DisposeAfterUse::YES, flags); + } + break; + } + } while (_state != MP3_STATE_EOS && _stream.error == MAD_ERROR_BUFLEN); + + if (_stream.error != MAD_ERROR_NONE) + _state = MP3_STATE_EOS; +} + +#endif + +} // End of namespace Video diff --git a/video/mpegps_decoder.h b/video/mpegps_decoder.h new file mode 100644 index 0000000000..0184d6f9ba --- /dev/null +++ b/video/mpegps_decoder.h @@ -0,0 +1,189 @@ +/* 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. + * + */ + +#ifndef VIDEO_MPEGPS_DECODER_H +#define VIDEO_MPEGPS_DECODER_H + +#include "common/hashmap.h" +#include "graphics/surface.h" +#include "video/video_decoder.h" + +#ifdef USE_MAD +#include <mad.h> +#endif + +namespace Audio { +class QueuingAudioStream; +} + +namespace Common { +class SeekableReadStream; +} + +namespace Graphics { +struct PixelFormat; +} + +namespace Image { +class MPEGDecoder; +} + +namespace Video { + +/** + * Decoder for MPEG Program Stream videos. + * Video decoder used in engines: + * - zvision + */ +class MPEGPSDecoder : public VideoDecoder { +public: + MPEGPSDecoder(); + virtual ~MPEGPSDecoder(); + + bool loadStream(Common::SeekableReadStream *stream); + void close(); + +protected: + void readNextPacket(); + bool useAudioSync() const { return false; } + +private: + // Base class for handling MPEG streams + class MPEGStream { + public: + virtual ~MPEGStream() {} + + enum StreamType { + kStreamTypeVideo, + kStreamTypeAudio + }; + + virtual bool sendPacket(Common::SeekableReadStream *firstPacket, uint32 pts, uint32 dts) = 0; + virtual StreamType getStreamType() const = 0; + }; + + // An MPEG 1/2 video track + class MPEGVideoTrack : public VideoTrack, public MPEGStream { + public: + MPEGVideoTrack(Common::SeekableReadStream *firstPacket, const Graphics::PixelFormat &format); + ~MPEGVideoTrack(); + + bool endOfTrack() const { return _endOfTrack; } + uint16 getWidth() const; + uint16 getHeight() const; + Graphics::PixelFormat getPixelFormat() const; + int getCurFrame() const { return _curFrame; } + uint32 getNextFrameStartTime() const { return _nextFrameStartTime.msecs(); } + const Graphics::Surface *decodeNextFrame(); + + bool sendPacket(Common::SeekableReadStream *packet, uint32 pts, uint32 dts); + StreamType getStreamType() const { return kStreamTypeVideo; } + + void setEndOfTrack() { _endOfTrack = true; } + + private: + bool _endOfTrack; + int _curFrame; + Audio::Timestamp _nextFrameStartTime; + Graphics::Surface *_surface; + + void findDimensions(Common::SeekableReadStream *firstPacket, const Graphics::PixelFormat &format); + +#ifdef USE_MPEG2 + Image::MPEGDecoder *_mpegDecoder; +#endif + }; + +#ifdef USE_MAD + // An MPEG audio track + // TODO: Merge this with the normal MP3Stream somehow + class MPEGAudioTrack : public AudioTrack, public MPEGStream { + public: + MPEGAudioTrack(Common::SeekableReadStream *firstPacket); + ~MPEGAudioTrack(); + + bool sendPacket(Common::SeekableReadStream *packet, uint32 pts, uint32 dts); + StreamType getStreamType() const { return kStreamTypeAudio; } + + protected: + Audio::AudioStream *getAudioStream() const; + + private: + Audio::QueuingAudioStream *_audStream; + + enum State { + MP3_STATE_INIT, // Need to init the decoder + MP3_STATE_READY, // ready for processing data + MP3_STATE_EOS // end of data reached (may need to loop) + }; + + State _state; + + mad_stream _stream; + mad_frame _frame; + mad_synth _synth; + + enum { + BUFFER_SIZE = 5 * 8192 + }; + + // This buffer contains a slab of input data + byte _buf[BUFFER_SIZE + MAD_BUFFER_GUARD]; + + void initStream(Common::SeekableReadStream *packet); + void deinitStream(); + void readMP3Data(Common::SeekableReadStream *packet); + void readHeader(Common::SeekableReadStream *packet); + void decodeMP3Data(Common::SeekableReadStream *packet); + }; +#endif + + // The different types of private streams we can detect at the moment + enum PrivateStreamType { + kPrivateStreamUnknown, + kPrivateStreamAC3, + kPrivateStreamDTS, + kPrivateStreamDVDPCM, + kPrivateStreamPS2Audio + }; + + PrivateStreamType detectPrivateStreamType(Common::SeekableReadStream *packet); + + bool addFirstVideoTrack(); + + int readNextPacketHeader(int32 &startCode, uint32 &pts, uint32 &dts); + int findNextStartCode(uint32 &size); + uint32 readPTS(int c); + + void parseProgramStreamMap(int length); + byte _psmESType[256]; + + // A map from stream types to stream handlers + typedef Common::HashMap<int, MPEGStream *> StreamMap; + StreamMap _streamMap; + + Common::SeekableReadStream *_stream; +}; + +} // End of namespace Video + +#endif |