diff --git a/CMakeLists.txt b/CMakeLists.txt index 368917ca..c52efed8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -155,7 +155,9 @@ set(RENDER_HEADERS src/render/image/tgafile.h src/render/image/tpcfile.h src/render/image/txifile.h - src/render/lipfile.h + src/render/lip/lipanimation.h + src/render/lip/lipfile.h + src/render/lip/lips.h src/render/mesh/aabb.h src/render/mesh/billboard.h src/render/mesh/cube.h @@ -191,7 +193,9 @@ set(RENDER_SOURCES src/render/image/tgafile.cpp src/render/image/tpcfile.cpp src/render/image/txifile.cpp - src/render/lipfile.cpp + src/render/lip/lipanimation.cpp + src/render/lip/lipfile.cpp + src/render/lip/lips.cpp src/render/mesh/aabb.cpp src/render/mesh/billboard.cpp src/render/mesh/cube.cpp diff --git a/src/game/game.cpp b/src/game/game.cpp index aa02bd6d..e2a044ce 100644 --- a/src/game/game.cpp +++ b/src/game/game.cpp @@ -26,6 +26,7 @@ #include "../common/log.h" #include "../common/pathutil.h" #include "../experimental/tor/gr2file.h" +#include "../render/lip/lips.h" #include "../render/model/mdlfile.h" #include "../render/model/models.h" #include "../render/textures.h" @@ -229,6 +230,8 @@ void Game::loadModule(const string &name, string entry) { Scripts::instance().invalidateCache(); Blueprints::instance().invalidateCache(); SoundSets::instance().invalidateCache(); + Lips::instance().invalidateCache(); + Resources::instance().loadModule(name); if (_module) { diff --git a/src/game/gui/conversation.cpp b/src/game/gui/conversation.cpp index 9b7728ef..a5c21996 100644 --- a/src/game/gui/conversation.cpp +++ b/src/game/gui/conversation.cpp @@ -21,6 +21,7 @@ #include "../../audio/player.h" #include "../../common/log.h" #include "../../gui/control/listbox.h" +#include "../../render/lip/lips.h" #include "../../render/model/models.h" #include "../../resource/resources.h" @@ -161,15 +162,16 @@ void Conversation::loadVoiceOver() { } // Play current voice over either from Sound or from VO_ResRef - shared_ptr voice; + string voiceResRef; if (!_currentEntry->sound.empty()) { - voice = AudioFiles::instance().get(_currentEntry->sound); + voiceResRef = _currentEntry->sound; } - if (!voice && !_currentEntry->voResRef.empty()) { - voice = AudioFiles::instance().get(_currentEntry->voResRef); + if (voiceResRef.empty() && !_currentEntry->voResRef.empty()) { + voiceResRef = _currentEntry->voResRef; } - if (voice) { - _currentVoice = AudioPlayer::instance().play(voice, AudioType::Voice); + if (!voiceResRef.empty()) { + _currentVoice = AudioPlayer::instance().play(voiceResRef, AudioType::Voice); + _lipAnimation = Lips::instance().get(voiceResRef); } } diff --git a/src/game/gui/conversation.h b/src/game/gui/conversation.h index 0581a745..87c7e69b 100644 --- a/src/game/gui/conversation.h +++ b/src/game/gui/conversation.h @@ -19,6 +19,7 @@ #include "../../audio/soundhandle.h" #include "../../common/timer.h" +#include "../../render/lip/lipanimation.h" #include "../../render/model/model.h" #include "../camera/types.h" @@ -83,6 +84,7 @@ protected: private: std::shared_ptr _currentVoice; + std::shared_ptr _lipAnimation; Timer _endEntryTimer; float _entryDuration { 0.0f }; std::vector _replies; diff --git a/src/render/lip/lipanimation.cpp b/src/render/lip/lipanimation.cpp new file mode 100644 index 00000000..7eda62da --- /dev/null +++ b/src/render/lip/lipanimation.cpp @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2020-2021 The reone project contributors + * + * 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 3 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, see . + */ + +#include "lipanimation.h" + +#include + +using namespace std; + +namespace reone { + +namespace render { + +LipAnimation::LipAnimation(vector keyframes) : _keyframes(move(keyframes)) { +} + +const LipAnimation::Keyframe &LipAnimation::getKeyframe(float time) const { + if (_keyframes.empty()) { + throw logic_error("keyframes is empty"); + } + for (auto &frame : _keyframes) { + if (time <= frame.time) return frame; + } + return _keyframes.back(); +} + +} // namespace render + +} // namespace reone diff --git a/src/render/lip/lipanimation.h b/src/render/lip/lipanimation.h new file mode 100644 index 00000000..4bab2899 --- /dev/null +++ b/src/render/lip/lipanimation.h @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2020-2021 The reone project contributors + * + * 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 3 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, see . + */ + +#pragma once + +#include +#include + +namespace reone { + +namespace render { + +class LipAnimation { +public: + struct Keyframe { + float time { 0.0f }; + uint8_t shape { 0 }; /**< an index into the keyframes of the "talk" animation */ + }; + + LipAnimation(std::vector keyframes); + + const Keyframe &getKeyframe(float time) const; + +private: + std::vector _keyframes; +}; + +} // namespace render + +} // namespace reone diff --git a/src/render/lipfile.cpp b/src/render/lip/lipfile.cpp similarity index 86% rename from src/render/lipfile.cpp rename to src/render/lip/lipfile.cpp index 9907482d..3cae1911 100644 --- a/src/render/lipfile.cpp +++ b/src/render/lip/lipfile.cpp @@ -32,12 +32,15 @@ void LipFile::doLoad() { float length = readFloat(); uint32_t entryCount = readUint32(); + vector keyframes; for (uint32_t i = 0; i < entryCount; ++i) { - Keyframe keyframe; + LipAnimation::Keyframe keyframe; keyframe.time = readFloat(); keyframe.shape = readByte(); - _keyframes.push_back(move(keyframe)); + keyframes.push_back(move(keyframe)); } + + _animation = make_shared(move(keyframes)); } } // namespace render diff --git a/src/render/lipfile.h b/src/render/lip/lipfile.h similarity index 79% rename from src/render/lipfile.h rename to src/render/lip/lipfile.h index f9624ee8..6a8beb42 100644 --- a/src/render/lipfile.h +++ b/src/render/lip/lipfile.h @@ -17,7 +17,9 @@ #pragma once -#include "../resource/format/binfile.h" +#include "../../resource/format/binfile.h" + +#include "lipanimation.h" namespace reone { @@ -25,17 +27,12 @@ namespace render { class LipFile : public resource::BinaryFile { public: - struct Keyframe { - float time { 0.0f }; - uint8_t shape { 0 }; - }; - LipFile(); - const std::vector &keyframes() const { return _keyframes; } + std::shared_ptr animation() const { return _animation; } private: - std::vector _keyframes; + std::shared_ptr _animation; void doLoad() override; }; diff --git a/src/render/lip/lips.cpp b/src/render/lip/lips.cpp new file mode 100644 index 00000000..026a34f4 --- /dev/null +++ b/src/render/lip/lips.cpp @@ -0,0 +1,60 @@ + +/* + * Copyright (c) 2020-2021 The reone project contributors + * + * 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 3 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, see . + */ + +#include "lips.h" + +#include "../../common/streamutil.h" +#include "../../resource/resources.h" + +#include "lipfile.h" + +using namespace std; + +using namespace reone::resource; + +namespace reone { + +namespace render { + +Lips &Lips::instance() { + static Lips instance; + return instance; +} + +void Lips::invalidateCache() { + _cache.clear(); +} + +shared_ptr Lips::get(const string &resRef) { + auto maybeAnimation = _cache.find(resRef); + if (maybeAnimation != _cache.end()) return maybeAnimation->second; + + shared_ptr animation; + shared_ptr lipData(Resources::instance().get(resRef, ResourceType::Lip)); + if (lipData) { + LipFile lip; + lip.load(wrap(lipData)); + animation = lip.animation(); + } + + return _cache.insert(make_pair(resRef, move(animation))).first->second; +} + +} // namespace render + +} // namespace reone diff --git a/src/render/lip/lips.h b/src/render/lip/lips.h new file mode 100644 index 00000000..bee46367 --- /dev/null +++ b/src/render/lip/lips.h @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2020-2021 The reone project contributors + * + * 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 3 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, see . + */ + +#pragma once + +#include +#include +#include + +#include "lipanimation.h" + +namespace reone { + +namespace render { + +class Lips { +public: + static Lips &instance(); + + void invalidateCache(); + + std::shared_ptr get(const std::string &resRef); + +private: + std::unordered_map> _cache; + + Lips() = default; +}; + +} // namespace render + +} // namespace reone diff --git a/src/resource/format/binfile.cpp b/src/resource/format/binfile.cpp index ebf66ace..94a56d89 100644 --- a/src/resource/format/binfile.cpp +++ b/src/resource/format/binfile.cpp @@ -17,6 +17,8 @@ #include "binfile.h" +#include + using namespace std; namespace fs = boost::filesystem; @@ -61,7 +63,7 @@ void BinaryFile::checkSignature() { char buf[16]; _in->read(buf, _signSize); if (!equal(_sign.begin(), _sign.end(), buf)) { - throw runtime_error("Invalid binary file signature"); + throw runtime_error(str(boost::format("Invalid binary file signature: %s") % string(buf, _signSize))); } } diff --git a/src/resource/resources.cpp b/src/resource/resources.cpp index 58d4bef6..72c8188a 100644 --- a/src/resource/resources.cpp +++ b/src/resource/resources.cpp @@ -49,6 +49,7 @@ static const char kSoundsDirectoryName[] = "streamsounds"; static const char kVoiceDirectoryName[] = "streamvoice"; static const char kWavesDirectoryName[] = "streamwaves"; static const char kTexturePackDirectoryName[] = "texturepacks"; +static const char kLipsDirectoryName[] = "lips"; static const char kGUITexturePackFilename[] = "swpc_tex_gui.erf"; static const char kTexturePackFilename[] = "swpc_tex_tpa.erf"; @@ -64,10 +65,12 @@ void Resources::init(GameID gameId, const fs::path &gamePath) { indexKeyBifFiles(); indexTexturePacks(); - indexAudioFiles(); - indexOverrideDirectory(); indexTalkTable(); + indexAudioFiles(); + indexLipModFiles(); indexExeFile(); + indexOverrideDirectory(); + loadModuleNames(); } @@ -121,6 +124,28 @@ void Resources::indexAudioFiles() { } } +void Resources::indexLipModFiles() { + static vector kotorMods { "global", "localization" }; + static vector tslMods { "localization" }; + + const vector &mods = _gameId == GameID::KotOR ? kotorMods : tslMods; + fs::path lipsPath(getPathIgnoreCase(_gamePath, "lips")); + + for (auto &mod : mods) { + fs::path modPath(getPathIgnoreCase(lipsPath, mod + ".mod")); + indexErfFile(modPath); + } +} + +void Resources::indexRimFile(const fs::path &path) { + auto rim = make_unique(); + rim->load(path); + + _providers.push_back(move(rim)); + + debug(boost::format("Resources: indexed: %s") % path); +} + void Resources::indexDirectory(const fs::path &path) { auto folder = make_unique(); folder->load(path); @@ -192,11 +217,15 @@ void Resources::loadModule(const string &name) { _transientProviders.clear(); fs::path modulesPath(getPathIgnoreCase(_gamePath, kModulesDirectoryName)); - fs::path rimPath(getPathIgnoreCase(modulesPath, name + ".rim")); - fs::path rimsPath(getPathIgnoreCase(modulesPath, name + "_s.rim")); + fs::path moduleRimPath(getPathIgnoreCase(modulesPath, name + ".rim")); + fs::path moduleRimSPath(getPathIgnoreCase(modulesPath, name + "_s.rim")); - indexTransientRimFile(rimPath); - indexTransientRimFile(rimsPath); + fs::path lipsPath(getPathIgnoreCase(_gamePath, kLipsDirectoryName)); + fs::path lipModPath(getPathIgnoreCase(lipsPath, name + "_loc.mod")); + + indexTransientRimFile(moduleRimPath); + indexTransientRimFile(moduleRimSPath); + indexTransientErfFile(lipModPath); if (_gameId == GameID::TSL) { fs::path dlgPath(getPathIgnoreCase(modulesPath, name + "_dlg.erf")); diff --git a/src/resource/resources.h b/src/resource/resources.h index 21ba6ac0..064d95da 100644 --- a/src/resource/resources.h +++ b/src/resource/resources.h @@ -112,16 +112,19 @@ private: Resources &operator=(const Resources &) = delete; - void indexAudioFiles(); - void indexDirectory(const boost::filesystem::path &path); - void indexErfFile(const boost::filesystem::path &path); - void indexExeFile(); void indexKeyBifFiles(); - void indexOverrideDirectory(); - void indexTalkTable(); void indexTexturePacks(); + void indexTalkTable(); + void indexAudioFiles(); + void indexLipModFiles(); + void indexExeFile(); + void indexOverrideDirectory(); + + void indexErfFile(const boost::filesystem::path &path); void indexTransientErfFile(const boost::filesystem::path &path); + void indexRimFile(const boost::filesystem::path &path); void indexTransientRimFile(const boost::filesystem::path &path); + void indexDirectory(const boost::filesystem::path &path); void invalidateCache(); void loadModuleNames(); diff --git a/tools/gfftool.cpp b/tools/gfftool.cpp index 74e8d10f..2f2db50e 100644 --- a/tools/gfftool.cpp +++ b/tools/gfftool.cpp @@ -104,9 +104,7 @@ void GffTool::toJSON(const fs::path &path, const fs::path &destPath) { } bool GffTool::supports(Operation operation, const fs::path &target) const { - return - !fs::is_directory(target) && - operation == Operation::ToJSON; + return !fs::is_directory(target) && operation == Operation::ToJSON; } } // namespace tools