feat: Implement dialog loading

This commit is contained in:
Vsevolod Kremianskii 2020-08-07 15:08:08 +07:00
parent 7718bf149d
commit c4fb806af3
20 changed files with 356 additions and 59 deletions

View file

@ -25,6 +25,8 @@
#include "../core/log.h"
using namespace std;
namespace reone {
namespace audio {
@ -43,29 +45,31 @@ void AudioPlayer::init(const AudioOptions &opts) {
_device = alcOpenDevice(nullptr);
if (!_device) {
throw std::runtime_error("Failed to open audio device");
throw runtime_error("Failed to open audio device");
}
_context = alcCreateContext(_device, nullptr);
if (!_context) {
throw std::runtime_error("Failed to create audio context");
throw runtime_error("Failed to create audio context");
}
alcMakeContextCurrent(_context);
alListenerf(AL_GAIN, _opts.volume / 100.0f);
_thread = std::thread(std::bind(threadStart, this));
_thread = thread(bind(&AudioPlayer::threadStart, this));
}
void AudioPlayer::threadStart(AudioPlayer *player) {
while (player->_run) {
std::lock_guard<std::recursive_mutex> lock(player->_soundsMutex);
std::remove_if(
player->_sounds.begin(),
player->_sounds.end(),
[](const SoundInstance &sound) { return sound.stopped(); });
void AudioPlayer::threadStart() {
while (_run) {
lock_guard<recursive_mutex> lock(_soundsMutex);
auto it = remove_if(
_sounds.begin(),
_sounds.end(),
[](const shared_ptr<SoundInstance> &sound) { return sound->stopped(); });
for (auto &sound : player->_sounds) {
sound.update();
_sounds.erase(it, _sounds.end());
for (auto &sound : _sounds) {
sound->update();
}
}
}
@ -93,18 +97,21 @@ void AudioPlayer::deinit() {
}
}
void AudioPlayer::play(const std::shared_ptr<AudioStream> &stream, bool loop) {
if (!stream) {
throw std::invalid_argument("Audio stream is empty");
}
SoundInstance sound(stream, loop);
std::lock_guard<std::recursive_mutex> lock(_soundsMutex);
_sounds.push_back(std::move(sound));
void AudioPlayer::reset() {
lock_guard<recursive_mutex> lock(_soundsMutex);
_sounds.clear();
}
void AudioPlayer::reset() {
std::lock_guard<std::recursive_mutex> lock(_soundsMutex);
_sounds.clear();
shared_ptr<SoundInstance> AudioPlayer::play(const shared_ptr<AudioStream> &stream, bool loop) {
if (!stream) {
throw invalid_argument("Audio stream is empty");
}
shared_ptr<SoundInstance> sound(new SoundInstance(stream, loop));
lock_guard<recursive_mutex> lock(_soundsMutex);
_sounds.push_back(sound);
return move(sound);
}
} // namespace audio

View file

@ -36,8 +36,9 @@ public:
void init(const AudioOptions &opts);
void deinit();
void play(const std::shared_ptr<AudioStream> &stream, bool loop = false);
void reset();
std::shared_ptr<SoundInstance> play(const std::shared_ptr<AudioStream> &stream, bool loop = false);
private:
AudioOptions _opts;
@ -45,7 +46,7 @@ private:
ALCcontext *_context { nullptr };
std::thread _thread;
std::atomic_bool _run { true };
std::list<SoundInstance> _sounds;
std::list<std::shared_ptr<SoundInstance>> _sounds;
std::recursive_mutex _soundsMutex;
AudioPlayer() = default;
@ -54,7 +55,7 @@ private:
AudioPlayer &operator=(const AudioPlayer &) = delete;
static void threadStart(AudioPlayer *);
void threadStart();
};
#define TheAudioPlayer audio::AudioPlayer::instance()

View file

@ -67,7 +67,14 @@ void SoundInstance::update() {
if (_state == State::NotInited) {
init();
}
if (!_multiframe) return;
if (!_multiframe) {
ALint state = 0;
alGetSourcei(_source, AL_SOURCE_STATE, &state);
if (state == AL_STOPPED) {
_state = State::Stopped;
}
return;
}
ALint processed = 0;
alGetSourcei(_source, AL_BUFFERS_PROCESSED, &processed);
@ -88,6 +95,10 @@ void SoundInstance::update() {
}
}
void SoundInstance::stop() {
alSourceStop(_source);
}
bool SoundInstance::stopped() const {
return _state == State::Stopped;
}

View file

@ -34,6 +34,8 @@ public:
SoundInstance &operator=(SoundInstance &&) = default;
void update();
void stop();
bool stopped() const;
private:
@ -49,8 +51,8 @@ private:
State _state { State::NotInited };
int _nextFrame { 0 };
int _nextBuffer { 0 };
unsigned int _source { 0 };
std::vector<unsigned int> _buffers;
uint32_t _source { 0 };
std::vector<uint32_t> _buffers;
SoundInstance(SoundInstance &) = delete;
SoundInstance &operator=(SoundInstance &) = delete;

View file

@ -17,6 +17,8 @@
#include "dialog.h"
#include "../resources/manager.h"
using namespace std;
using namespace reone::resources;
@ -25,7 +27,72 @@ namespace reone {
namespace game {
void Dialog::reset() {
_entries.clear();
_replies.clear();
_startEntries.clear();
}
void Dialog::load(const string &resRef, const GffStruct &dlg) {
for (auto &entry : dlg.getList("EntryList")) {
_entries.push_back(getEntryReply(entry));
}
for (auto &reply : dlg.getList("ReplyList")) {
_replies.push_back(getEntryReply(reply));
}
for (auto &entry : dlg.getList("StartingList")) {
_startEntries.push_back(getEntryReplyLink(entry));
}
}
Dialog::EntryReplyLink Dialog::getEntryReplyLink(const GffStruct &gffs) const {
EntryReplyLink link;
link.index = gffs.getInt("Index");
link.active = gffs.getString("Active");
return move(link);
}
Dialog::EntryReply Dialog::getEntryReply(const GffStruct &gffs) const {
int strRef = gffs.getInt("Text");
EntryReply entry;
entry.speaker = gffs.getString("Speaker");
entry.text = strRef == -1 ? "" : ResMan.getString(strRef).text;
entry.voResRef = gffs.getString("VO_ResRef");
entry.script = gffs.getString("Script");
entry.sound = gffs.getString("Sound");
entry.listener = gffs.getString("Listener");
entry.cameraAngle = gffs.getInt("CameraAngle");
const GffField *repliesList = gffs.find("RepliesList");
if (repliesList) {
for (auto &link : repliesList->children()) {
entry.replies.push_back(getEntryReplyLink(link));
}
}
const GffField *entriesList = gffs.find("EntriesList");
if (entriesList) {
for (auto &link : entriesList->children()) {
entry.entries.push_back(getEntryReplyLink(link));
}
}
return move(entry);
}
const vector<Dialog::EntryReplyLink> &Dialog::startEntries() const {
return _startEntries;
}
const Dialog::EntryReply &Dialog::getEntry(int index) const {
assert(index >= 0 && index < _entries.size());
return _entries[index];
}
const Dialog::EntryReply &Dialog::getReply(int index) const {
assert(index >= 0 && index < _replies.size());
return _replies[index];
}
} // namespace game

View file

@ -18,6 +18,7 @@
#pragma once
#include <string>
#include <vector>
#include "../resources/gfffile.h"
@ -27,13 +28,43 @@ namespace game {
class Dialog {
public:
struct EntryReplyLink {
int index { 0 };
std::string active;
};
struct EntryReply {
std::string speaker;
std::string text;
std::string voResRef;
std::string script;
std::string sound;
std::string listener;
int cameraAngle { 0 };
std::vector<EntryReplyLink> replies;
std::vector<EntryReplyLink> entries;
};
Dialog() = default;
void reset();
void load(const std::string &resRef, const resources::GffStruct &dlg);
const std::vector<EntryReplyLink> &startEntries() const;
const EntryReply &getEntry(int index) const;
const EntryReply &getReply(int index) const;
private:
std::vector<EntryReplyLink> _startEntries;
std::vector<EntryReply> _entries;
std::vector<EntryReply> _replies;
int _entryIndex { -1 };
Dialog(const Dialog &) = delete;
Dialog &operator=(const Dialog &) = delete;
EntryReplyLink getEntryReplyLink(const resources::GffStruct &gffs) const;
EntryReply getEntryReply(const resources::GffStruct &gffs) const;
};
} // namespace game

View file

@ -143,6 +143,9 @@ void Game::loadModule(const string &name, string entry) {
unique_ptr<DialogGui> dialog(new DialogGui(_opts.graphics));
dialog->load(_version);
dialog->initGL();
dialog->setOnDialogFinished([this]() {
_screen = Screen::InGame;
});
_dialog = move(dialog);
}
@ -164,6 +167,7 @@ void Game::configureModule() {
});
_module->setStartConversation([this](const string &name) {
_screen = Screen::Dialog;
_dialog->startDialog(name);
});
}

View file

@ -17,15 +17,22 @@
#include "dialog.h"
#include <boost/format.hpp>
#include "../../audio/player.h"
#include "../../gui/control/listbox.h"
#include "../../gui/control/panel.h"
#include "../../resources/manager.h"
#include "../../script/execution.h"
#include "../../script/manager.h"
using namespace std;
using namespace reone::audio;
using namespace reone::gui;
using namespace reone::render;
using namespace reone::resources;
using namespace reone::script;
namespace reone {
@ -94,7 +101,6 @@ void DialogGui::configureMessage() {
extent.top = -_rootControl->extent().top;
Control::Text text(message.text());
text.text = "Hello, world!";
text.color = _version == GameVersion::KotOR ? g_kotorBaseColor : g_tslBaseColor;
message.setExtent(move(extent));
@ -114,16 +120,110 @@ void DialogGui::configureReplies() {
protoItem.setHilight(move(hilight));
protoItem.setText(move(text));
replies.add(ListBox::Item { "", "1. Reply 1" });
replies.add(ListBox::Item { "", "2. Reply 2" });
replies.add(ListBox::Item { "", "3. Reply 3" });
replies.add(ListBox::Item { "", "4. Reply 4" });
replies.add(ListBox::Item { "", "5. Reply 5" });
replies.add(ListBox::Item { "", "6. Reply 6" });
replies.add(ListBox::Item { "", "7. Reply 7" });
replies.add(ListBox::Item { "", "8. Reply 8" });
replies.add(ListBox::Item { "", "9. Reply 9" });
replies.add(ListBox::Item { "", "10. Reply 10" });
replies.setOnItemClicked([this](const string &ctrl, const string &item) {
int replyIdx = stoi(item);
onReplyClicked(replyIdx);
});
}
void DialogGui::onReplyClicked(int index) {
const Dialog::EntryReply &reply = _dialog->getReply(index);
if (reply.entries.empty()) {
if (_onDialogFinished) _onDialogFinished();
return;
}
int entryIdx = -1;
for (auto &link : reply.entries) {
if (link.active.empty()) {
entryIdx = link.index;
continue;
}
if (checkCondition(link.active)) {
entryIdx = link.index;
break;
}
}
if (entryIdx != -1) {
_currentEntry.reset(new Dialog::EntryReply(_dialog->getEntry(entryIdx)));
loadCurrentEntry();
}
}
void DialogGui::startDialog(const string &resRef) {
shared_ptr<GffStruct> dlg(ResMan.findGFF(resRef, ResourceType::Conversation));
if (!dlg) {
if (_onDialogFinished) _onDialogFinished();
return;
}
_dialog.reset(new Dialog());
_dialog->load(resRef, *dlg);
loadStartEntry();
}
void DialogGui::loadStartEntry() {
int entryIdx = -1;
for (auto &link : _dialog->startEntries()) {
if (link.active.empty()) {
entryIdx = link.index;
continue;
}
if (checkCondition(link.active)) {
entryIdx = link.index;
break;
}
}
if (entryIdx != -1) {
_currentEntry.reset(new Dialog::EntryReply(_dialog->getEntry(entryIdx)));
loadCurrentEntry();
}
}
bool DialogGui::checkCondition(const string &script) {
shared_ptr<ScriptProgram> program(ScriptMan.find(script));
return ScriptExecution(program, ExecutionContext()).run() != 0;
}
void DialogGui::loadCurrentEntry() {
if (_currentVoice) _currentVoice->stop();
assert(_currentEntry);
if (!_currentEntry->voResRef.empty()) {
shared_ptr<AudioStream> voice(ResMan.findAudio(_currentEntry->voResRef));
if (voice) {
_currentVoice = TheAudioPlayer.play(voice);
}
}
Control &message = getControl("LBL_MESSAGE");
message.setTextMessage(_currentEntry->text);
ListBox &replies = static_cast<ListBox &>(getControl("LB_REPLIES"));
replies.clearItems();
int replyCount = 0;
for (auto &link : _currentEntry->replies) {
if (!link.active.empty() && !checkCondition(link.active)) continue;
string text(_dialog->getReply(link.index).text);
if (text.empty()) text = "[continue]";
replies.add({ to_string(link.index), str(boost::format("%d. %s") % ++replyCount % text) });
}
if (!_currentEntry->script.empty()) {
ScriptExecution(ScriptMan.find(_currentEntry->script), ExecutionContext()).run();
}
if (replyCount == 0 && _onDialogFinished) {
_onDialogFinished();
}
}
void DialogGui::setOnDialogFinished(const std::function<void()> &fn) {
_onDialogFinished = fn;
}
} // namespace game

View file

@ -17,9 +17,12 @@
#pragma once
#include "../../audio/soundinstance.h"
#include "../../gui/gui.h"
#include "../../resources/types.h"
#include "../dialog.h"
namespace reone {
namespace game {
@ -29,15 +32,26 @@ public:
DialogGui(const render::GraphicsOptions &opts);
void load(resources::GameVersion version);
void startDialog(const std::string &resRef);
void setOnDialogFinished(const std::function<void()> &fn);
private:
resources::GameVersion _version { resources::GameVersion::KotOR };
std::shared_ptr<Dialog> _dialog;
std::shared_ptr<Dialog::EntryReply> _currentEntry;
std::shared_ptr<audio::SoundInstance> _currentVoice;
std::function<void()> _onDialogFinished;
void addTopFrame();
void addBottomFrame();
void addFrame(int top, int height);
void configureMessage();
void configureReplies();
void onReplyClicked(int index);
void loadStartEntry();
bool checkCondition(const std::string &script);
void loadCurrentEntry();
};
} // namespace game

View file

@ -394,6 +394,10 @@ void Control::setText(const Text &text) {
_text = text;
}
void Control::setTextMessage(const string &text) {
_text.text = text;
}
const string &Control::tag() const {
return _tag;
}

View file

@ -85,6 +85,7 @@ public:
void setBorder(const Border &border);
void setHilight(const Border &hilight);
void setText(const Text &text);
void setTextMessage(const std::string &text);
const std::string &tag() const;
const Extent &extent() const;

View file

@ -73,6 +73,12 @@ void ListBox::updateItems() {
}
}
void ListBox::clearItems() {
_items.clear();
_itemOffset = 0;
updateItems();
}
void ListBox::add(const Item &item) {
_items.push_back(item);
updateItems();
@ -106,7 +112,7 @@ int ListBox::getItemIndex(int y) const {
bool ListBox::handleMouseWheel(int x, int y) {
if (y < 0) {
if (_itemOffset < _items.size() - _slotCount) _itemOffset++;
if (_items.size() - _itemOffset > _slotCount) _itemOffset++;
return true;
} else if (y > 0) {
if (_itemOffset > 0) _itemOffset--;
@ -145,8 +151,10 @@ void ListBox::render(const glm::mat4 &transform, const std::string &textOverride
const Control::Extent &protoExtent = _protoItem->extent();
glm::mat4 itemTransform(glm::translate(transform, glm::vec3(_extent.left, _extent.top - protoExtent.top, 0.0f)));
for (int i = 0; i < _items.size() && i < _slotCount; ++i) {
for (int i = 0; i < _slotCount; ++i) {
int itemIdx = i + _itemOffset;
if (itemIdx >= _items.size()) break;
_protoItem->setFocus(_hilightedIndex == itemIdx);
_protoItem->render(itemTransform, _items[itemIdx].text);
itemTransform = glm::translate(itemTransform, glm::vec3(0.0f, protoExtent.height + _padding, 0.0f));
@ -155,7 +163,7 @@ void ListBox::render(const glm::mat4 &transform, const std::string &textOverride
if (_scrollBar) {
ScrollBar &scrollBar = static_cast<ScrollBar &>(*_scrollBar);
scrollBar.setCanScrollUp(_itemOffset > 0);
scrollBar.setCanScrollDown(_itemOffset + _slotCount < _items.size());
scrollBar.setCanScrollDown(_items.size() - _itemOffset > _slotCount);
scrollBar.render(transform, "");
}
}

View file

@ -38,6 +38,7 @@ public:
ListBox(const std::string &tag);
void loadCustom();
void clearItems();
void add(const Item &item);
void load(const resources::GffStruct &gffs) override;

View file

@ -21,6 +21,8 @@
#include "util.h"
using namespace std;
namespace fs = boost::filesystem;
namespace reone {
@ -29,24 +31,30 @@ namespace resources {
void Folder::load(const fs::path &path) {
if (!fs::is_directory(path)) {
throw std::runtime_error("Folder not found: " + path.string());
throw runtime_error("Folder not found: " + path.string());
}
loadDirectory(path);
_path = path;
}
void Folder::loadDirectory(const fs::path &path) {
for (auto &entry : fs::directory_iterator(path)) {
const fs::path &path2 = entry.path();
if (fs::is_directory(path2)) continue;
const fs::path &childPath = entry.path();
if (fs::is_directory(childPath)) {
loadDirectory(childPath);
continue;
}
std::string resRef(path2.filename().replace_extension("").string());
string resRef(childPath.filename().replace_extension("").string());
boost::to_lower(resRef);
std::string ext(path2.extension().string().substr(1));
string ext(childPath.extension().string().substr(1));
Resource res;
res.path = path2;
res.path = childPath;
res.type = getResTypeByExt(ext);
_resources.insert(std::make_pair(resRef, res));
_resources.insert(make_pair(resRef, res));
}
}
@ -54,7 +62,7 @@ bool Folder::supports(ResourceType type) const {
return true;
}
std::shared_ptr<ByteArray> Folder::find(const std::string &resRef, ResourceType type) {
shared_ptr<ByteArray> Folder::find(const string &resRef, ResourceType type) {
fs::path path;
for (auto &res : _resources) {
if (res.first == resRef && res.second.type == type) {
@ -63,18 +71,18 @@ std::shared_ptr<ByteArray> Folder::find(const std::string &resRef, ResourceType
}
}
if (path.empty()) {
return std::shared_ptr<ByteArray>();
return shared_ptr<ByteArray>();
}
fs::ifstream in(path, std::ios::binary);
fs::ifstream in(path, ios::binary);
in.seekg(0, std::ios::end);
in.seekg(0, ios::end);
size_t size = in.tellg();
in.seekg(std::ios::beg);
in.seekg(ios::beg);
ByteArray data(size);
in.read(&data[0], size);
return std::make_shared<ByteArray>(std::move(data));
return make_shared<ByteArray>(move(data));
}
} // namespace resources

View file

@ -48,6 +48,8 @@ private:
Folder(const Folder &) = delete;
Folder &operator=(const Folder &) = delete;
void loadDirectory(const boost::filesystem::path &path);
};
} // namespace resources

View file

@ -55,6 +55,9 @@ static const char kTexturePackDirectoryName[] = "texturepacks";
static const char kTexturePackFilename[] = "swpc_tex_tpa.erf";
static const char kGUITexturePackFilename[] = "swpc_tex_gui.erf";
static const char kMusicDirectoryName[] = "streammusic";
static const char kSoundsDirectoryName[] = "streamsounds";
static const char kWavesDirectoryName[] = "streamwaves";
static const char kVoiceDirectoryName[] = "streamvoice";
static map<string, shared_ptr<ByteArray>> g_resCache;
static map<string, shared_ptr<TwoDaTable>> g_2daCache;
@ -94,10 +97,25 @@ void ResourceManager::init(GameVersion version, const boost::filesystem::path &g
fs::path texPackPath(getPathIgnoreCase(texPacksPath, kTexturePackFilename));
fs::path guiTexPackPath(getPathIgnoreCase(texPacksPath, kGUITexturePackFilename));
fs::path musicPath(getPathIgnoreCase(gamePath, kMusicDirectoryName));
fs::path soundsPath(getPathIgnoreCase(gamePath, kSoundsDirectoryName));
addErfProvider(texPackPath);
addErfProvider(guiTexPackPath);
addFolderProvider(musicPath);
addFolderProvider(soundsPath);
switch (version) {
case GameVersion::KotOR: {
fs::path wavesPath(getPathIgnoreCase(gamePath, kWavesDirectoryName));
addFolderProvider(wavesPath);
break;
}
case GameVersion::TheSithLords: {
fs::path voicePath(getPathIgnoreCase(gamePath, kVoiceDirectoryName));
addFolderProvider(voicePath);
break;
}
}
_version = version;
_gamePath = gamePath;
@ -143,6 +161,11 @@ void ResourceManager::loadModule(const string &name) {
addTransientRimProvider(rimPath);
addTransientRimProvider(rimsPath);
if (_version == GameVersion::TheSithLords) {
fs::path dlgPath(getPathIgnoreCase(modulesPath, name + "_dlg.erf"));
addTransientErfProvider(dlgPath);
}
}
void ResourceManager::addTransientRimProvider(const fs::path &path) {
@ -151,6 +174,12 @@ void ResourceManager::addTransientRimProvider(const fs::path &path) {
_transientProviders.push_back(move(rim));
}
void ResourceManager::addTransientErfProvider(const fs::path &path) {
unique_ptr<ErfFile> erf(new ErfFile());
erf->load(path);
_transientProviders.push_back(move(erf));
}
void ResourceManager::addFolderProvider(const fs::path &path) {
unique_ptr<Folder> folder(new Folder());
folder->load(path);

View file

@ -78,6 +78,7 @@ private:
void addErfProvider(const boost::filesystem::path &path);
void addTransientRimProvider(const boost::filesystem::path &path);
void addTransientErfProvider(const boost::filesystem::path &path);
void addFolderProvider(const boost::filesystem::path &path);
void initModuleNames();
inline std::string getCacheKey(const std::string &resRef, ResourceType type) const;

View file

@ -85,11 +85,12 @@ ScriptExecution::ScriptExecution(const std::shared_ptr<ScriptProgram> &program,
int ScriptExecution::run() {
uint32_t insOff = kStartInstructionOffset;
_stack.push_back(Variable(0));
if (_context.savedState) {
vector<Variable> globals(_context.savedState->globals);
copy(globals.begin(), globals.end(), back_inserter(_stack));
_globalCount = globals.size();
_globalCount = _stack.size();
vector<Variable> locals(_context.savedState->locals);
copy(locals.begin(), locals.end(), back_inserter(_stack));
@ -112,7 +113,9 @@ int ScriptExecution::run() {
insOff = _nextInstruction;
}
return 0;
assert(_stack.front().type == VariableType::Int);
return _stack.front().intValue;
}
void ScriptExecution::executeCopyDownSP(const Instruction &ins) {

View file

@ -37,7 +37,7 @@ namespace script {
#define Action VariableType::Action
void RoutineManager::addKotorRoutines() {
_routines.emplace_back("Random", Int, vector<VariableType>());
_routines.emplace_back("Random", Int, vector<VariableType> { Int });
_routines.emplace_back("PrintString", Void, vector<VariableType> { String });
_routines.emplace_back("PrintFloat", Void, vector<VariableType> { Float, Int, Int });
_routines.emplace_back("FloatToString", String, vector<VariableType> { Float, Int, Int });

View file

@ -43,8 +43,11 @@ Variable Variable::operator+(const Variable &other) const {
if (type == VariableType::Float && other.type == VariableType::Float) {
return floatValue + other.floatValue;
}
if (type == VariableType::String && other.type == VariableType::String) {
return strValue + other.strValue;
}
throw logic_error(str(boost::format("Unsupported variable types: %02x %02x") % static_cast<int>(type) % static_cast<int>(other.type)));
throw logic_error(str(boost::format("Unsupported variable types: %02x %02x") % static_cast<int>(type) % static_cast<int>(other.type)));
}
Variable Variable::operator-(const Variable &other) const {