diff --git a/dependencies/union-api b/dependencies/union-api index 3468530..406e3fb 160000 --- a/dependencies/union-api +++ b/dependencies/union-api @@ -1 +1 @@ -Subproject commit 3468530e94b41834bc343af8e83ddf7fe4ba7dc7 +Subproject commit 406e3fb32232300bae2ff2ee0018685c15c7f1ef diff --git a/src/Gothic/BassLoader.hpp b/src/Gothic/BassLoader.hpp new file mode 100644 index 0000000..125250d --- /dev/null +++ b/src/Gothic/BassLoader.hpp @@ -0,0 +1,163 @@ +#include + +namespace GOTHIC_NAMESPACE +{ + struct GothicMusicTheme + { + zSTRING fileName; + float vol; + bool loop; + float reverbMix; + float reverbTime; + zTMus_TransType trType; + zTMus_TransSubType trSubType; + }; + + struct BassMusicTheme + { + zSTRING Name; + zSTRING Zones; + }; + + struct BassMusicThemeAudio + { + zSTRING Type; + zSTRING Filename; + }; + + class BassLoader + { + zCParser* m_Parser; + std::vector m_GothicThemeInstances; + std::vector m_BassThemeInstances; + std::vector m_BassThemeAudioInstances; + + public: + explicit BassLoader(zCParser* p) : m_Parser(p) {} + + ~BassLoader() + { + for (auto theme: m_GothicThemeInstances) { delete theme; } + for (auto theme: m_BassThemeInstances) { delete theme; } + for (auto theme: m_BassThemeAudioInstances) { delete theme; } + } + + void Load() + { + LoadGothic(); + LoadBass(); + } + + private: + void LoadGothic() + { + ForEachClass( + "C_MUSICTHEME", + [&]() { return m_GothicThemeInstances.emplace_back(new GothicMusicTheme{}); }, + [&](GothicMusicTheme* input, zCPar_Symbol* symbol) + { + std::shared_ptr theme = std::make_shared(symbol->name.ToChar()); + theme->SetAudioFile(NH::Bass::AudioFile::DEFAULT, input->fileName.ToChar()); + theme->SetAudioEffects(NH::Bass::AudioFile::DEFAULT, [&](NH::Bass::AudioEffects& effects) + { + effects.Loop.Active = input->loop; + effects.Volume.Active = true; + effects.Volume.Volume = input->vol; + if (!NH::Bass::Options->ForceDisableReverb) + { + effects.ReverbDX8.Active = true; + effects.ReverbDX8.Mix = input->reverbMix; + effects.ReverbDX8.Time = input->reverbTime; + } + bool forceFade = NH::Bass::Options->ForceFadeTransition; + if (forceFade || input->trType == zMUS_TR_INTRO || input->trType == zMUS_TR_ENDANDINTRO) + { + effects.FadeIn.Active = true; + effects.FadeIn.Duration = NH::Bass::Options->TransitionTime; + } + if (forceFade || input->trType == zMUS_TR_END || input->trType == zMUS_TR_ENDANDINTRO) + { + effects.FadeOut.Active = true; + effects.FadeOut.Duration = NH::Bass::Options->TransitionTime; + } + }); + theme->AddZone(symbol->name.ToChar()); + NH::Bass::Engine::GetInstance()->GetMusicManager().AddTheme(symbol->name.ToChar(), theme); + }); + } + + void LoadBass() + { + ForEachClass( + "C_BassMusic_Theme", + [&]() { return m_BassThemeInstances.emplace_back(new BassMusicTheme{}); }, + [&](BassMusicTheme* theme, zCPar_Symbol* symbol) + { + // @todo: + }); + + ForEachClass( + "C_BassMusic_ThemeAudio", + [&]() { return m_BassThemeAudioInstances.emplace_back(new BassMusicThemeAudio{}); }, + [&](BassMusicThemeAudio* theme, zCPar_Symbol* symbol) + { + // @todo: + }); + } + + template + void ForEachClass(const zSTRING& className, const std::function& classFactory, const std::function& instanceFunc) + { + ForEachPrototype(className, [&](int index) + { + T* theme = classFactory(); + if (theme) + { + m_Parser->CreatePrototype(index, theme); + } + }); + ForEachInstance(className, [&](int index, zCPar_Symbol* symbol) + { + T* theme = classFactory(); + if (theme) + { + m_Parser->CreateInstance(index, theme); + } + instanceFunc(theme, symbol); + }); + } + + void ForEachPrototype(const zSTRING& className, const std::function& func) + { + int classIndex = m_Parser->GetIndex(className); + if (classIndex < 0) + { + return; + } + + int prototypeIndex = m_Parser->GetPrototype(classIndex, 0); + while (prototypeIndex > 0) + { + func(prototypeIndex); + prototypeIndex = m_Parser->GetPrototype(classIndex, prototypeIndex + 1); + } + } + + void ForEachInstance(const zSTRING& className, const std::function& func) + { + int classIndex = m_Parser->GetIndex(className); + if (classIndex < 0) + { + return; + } + + int symbolIndex = m_Parser->GetInstance(classIndex, 0); + while (symbolIndex > 0) + { + zCPar_Symbol* symbol = m_Parser->GetSymbol(symbolIndex); + func(symbolIndex, symbol); + symbolIndex = m_Parser->GetInstance(classIndex, symbolIndex + 1); + } + } + }; +} \ No newline at end of file diff --git a/src/Gothic/CMusicSys_Bass.hpp b/src/Gothic/CMusicSys_Bass.hpp index f1a594c..af837f7 100644 --- a/src/Gothic/CMusicSys_Bass.hpp +++ b/src/Gothic/CMusicSys_Bass.hpp @@ -2,13 +2,14 @@ namespace GOTHIC_NAMESPACE { namespace BassEvent { - void Event_OnEnd(const NH::Bass::MusicDef& musicDef, int data, void* userData) + void Event_OnEnd(const NH::Bass::Event& event, void* userData) { static NH::Logger* log = NH::CreateLogger("zBassMusic::Event_OnEnd"); - log->Trace("{0}", musicDef.Filename); - zSTRING filename{ musicDef.Filename.ToChar() }; - zSTRING name{ musicDef.Name.ToChar() }; + NH::Bass::Event::MusicEnd data = std::get(event.Data); + zSTRING filename{ data.Theme->GetAudioFile(data.AudioId).Filename.ToChar() }; + zSTRING name{ data.Theme->GetName() }; + log->Trace("{0}, {1}", name.ToChar(), filename.ToChar()); for (int i = 0; i < Globals->Event_OnEnd_Functions.GetNumInList(); i++) { @@ -19,30 +20,33 @@ namespace GOTHIC_NAMESPACE } } - void Event_OnTransition(const NH::Bass::MusicDef& musicDef, int data, void* userData) + void Event_OnTransition(const NH::Bass::Event& event, void* userData) { static NH::Logger* log = NH::CreateLogger("zBassMusic::Event_OnTransition"); - log->Trace("{0}, {1} ms", musicDef.Filename, data); - zSTRING filename{ musicDef.Filename.ToChar() }; - zSTRING name{ musicDef.Name.ToChar() }; + NH::Bass::Event::MusicTransition data = std::get(event.Data); + zSTRING filename{ data.Theme->GetAudioFile(data.AudioId).Filename.ToChar() }; + zSTRING name{ data.Theme->GetName() }; + float timeLeft = data.TimeLeft; + log->Trace("{0}, {1}", name.ToChar(), filename.ToChar()); for (int i = 0; i < Globals->Event_OnTransition_Functions.GetNumInList(); i++) { const int funcId = Globals->Event_OnTransition_Functions[i]; Globals->BassMusic_EventThemeFilename = filename; Globals->BassMusic_EventThemeID = name; - parser->CallFunc(funcId, data); + parser->CallFunc(funcId, timeLeft); } } - void Event_OnChange(const NH::Bass::MusicDef& musicDef, int data, void* userData) + void Event_OnChange(const NH::Bass::Event& event, void* userData) { static NH::Logger* log = NH::CreateLogger("zBassMusic::Event_OnChange"); - log->Trace("{0}", musicDef.Filename); - zSTRING filename{ musicDef.Filename.ToChar() }; - zSTRING name{ musicDef.Name.ToChar() }; + NH::Bass::Event::MusicChange data = std::get(event.Data); + zSTRING filename{ data.Theme->GetAudioFile(data.AudioId).Filename.ToChar() }; + zSTRING name{ data.Theme->GetName() }; + log->Trace("{0}, {1}", name.ToChar(), filename.ToChar()); Globals->BassMusic_ActiveThemeFilename = filename; Globals->BassMusic_ActiveThemeID = name; @@ -114,6 +118,7 @@ namespace GOTHIC_NAMESPACE } zCMusicTheme* theme = new zCMusicTheme; + theme->name = identifier; if (!(NH::Bass::Options->CreateMainParserCMusicTheme && parser->CreateInstance(identifier, &theme->fileName))) { @@ -125,60 +130,6 @@ namespace GOTHIC_NAMESPACE delete theme; theme = m_DirectMusic->LoadThemeByScript(id); } - else - { - theme->name = identifier; - - zoptions->ChangeDir(DIR_MUSIC); - std::unique_ptr file{ zfactory->CreateZFile(theme->fileName) }; - - if (file->Exists()) - { - NH::Bass::MusicFile& musicFileRef = m_BassEngine->CreateMusicBuffer(theme->fileName.ToChar()); - if (!musicFileRef.Ready && !musicFileRef.Loading) - { - log->Trace("Loading music: {0}", file->GetFullPath().ToChar()); - - const auto error = file->Open(false); - - if (error == 0) - { - musicFileRef.Loading = true; - - std::thread([loadingStart = std::chrono::system_clock::now(), this](std::unique_ptr myFile, NH::Bass::MusicFile* myMusicPtr) - { - - zSTRING path = myFile->GetFullPath(); - const long size = myFile->Size(); - - myMusicPtr->Buffer.resize(static_cast(size)); - const long read = myFile->Read(myMusicPtr->Buffer.data(), size); - - if (read == size) - { - myMusicPtr->Ready = true; - log->Trace("Music ready: {0}, size = {1}", path.ToChar(), read); - } - - myMusicPtr->Loading = false; - myFile->Close(); - - auto loadingTime = std::chrono::duration_cast(std::chrono::system_clock::now() - loadingStart).count(); - - log->Trace("Loading done: {0}, time = {1}", path.ToChar(), loadingTime); - }, std::move(file), &musicFileRef).detach(); - } - else - { - log->Error("Could not open file {0}\n at {1}{2}", theme->fileName.ToChar(), __FILE__, __LINE__); - } - } - } - else - { - log->Error("Could not find file {0}\n at {1}{2}", theme->fileName.ToChar(), __FILE__, __LINE__); - } - } return theme; } @@ -205,38 +156,19 @@ namespace GOTHIC_NAMESPACE } zCMusicTheme* theme = LoadThemeByScript(id); - if (theme) + if (theme && IsDirectMusicFormat(theme->fileName)) { - if (IsDirectMusicFormat(theme->fileName)) - { - m_ActiveTheme = theme; - m_BassEngine->StopMusic(); - return m_DirectMusic->PlayThemeByScript(id, manipulate, done); - } - - if (done) - { - *done = true; - } - - switch (manipulate) - { - case 1: - PlayTheme(theme, zMUS_THEME_VOL_DEFAULT, zMUS_TR_END, zMUS_TRSUB_DEFAULT); - break; - case 2: - PlayTheme(theme, zMUS_THEME_VOL_DEFAULT, zMUS_TR_NONE, zMUS_TRSUB_DEFAULT); - break; - default: - PlayTheme(theme); - } - - return; + m_ActiveTheme = theme; + m_BassEngine->StopMusic(); + return m_DirectMusic->PlayThemeByScript(id, manipulate, done); } + identifier.Upper(); + m_BassEngine->GetCommandQueue().AddCommand(std::make_shared(identifier.ToChar())); + if (done) { - *done = false; + *done = true; } } @@ -256,50 +188,9 @@ namespace GOTHIC_NAMESPACE } m_DirectMusic->Stop(); - - if (transition != zMUS_TR_DEFAULT) - { - theme->trType = transition; - } - - if (subTransition != zMUS_TRSUB_DEFAULT) - { - theme->trSubType = subTransition; - } - - NH::Bass::MusicDef musicDef{}; - musicDef.Filename = theme->fileName.ToChar(); - musicDef.Name = theme->name.ToChar(); - musicDef.Loop = theme->loop; - musicDef.Volume = theme->vol; - - if (theme->trType == zMUS_TR_INTRO || theme->trType == zMUS_TR_ENDANDINTRO) - { - musicDef.StartTransition.Type = NH::Bass::TransitionType::FADE; - musicDef.StartTransition.Duration = NH::Bass::Options->TransitionTime; - } - - if (theme->trType == zMUS_TR_END || theme->trType == zMUS_TR_ENDANDINTRO) - { - musicDef.EndTransition.Type = NH::Bass::TransitionType::FADE; - musicDef.EndTransition.Duration = NH::Bass::Options->TransitionTime; - } - - if (m_DirectMusic->prefs.globalReverbEnabled) - { - musicDef.Effects.Reverb = true; - musicDef.Effects.ReverbMix = theme->reverbMix; - musicDef.Effects.ReverbTime = theme->reverbTime; - } - m_ActiveTheme = theme; - - // Engine::PlayMusic() uses a mutex, so let's submit it in a deteached thread to avoid blocking - std::thread submitThread([this, musicDef]() - { - m_BassEngine->PlayMusic(musicDef); - }); - submitThread.detach(); + log->Warning("This path in CMusicSys_Bass::PlayTheme() shouldn't be possible"); + PlayThemeByScript(theme->name, 0, nullptr); } zCMusicTheme* GetActiveTheme() override diff --git a/src/Gothic/Hooks.hpp b/src/Gothic/Hooks.hpp index 211578f..0aca9b2 100644 --- a/src/Gothic/Hooks.hpp +++ b/src/Gothic/Hooks.hpp @@ -43,6 +43,14 @@ namespace GOTHIC_NAMESPACE float volume = zoptions->ReadReal("SOUND", "musicVolume", 1.0f); zmusic->SetVolume(volume); log->Info("Set music system to CMusicSys_Bass"); + + if (NH::Bass::Options->CreateMainParserCMusicTheme) + { + BassLoader bassLoader(parser); + bassLoader.Load(); + } + BassLoader bassLoader(parserMusic); + bassLoader.Load(); } else { diff --git a/src/NH/Bass/Channel.cpp b/src/NH/Bass/Channel.cpp index ddee24c..1e7d7a9 100644 --- a/src/NH/Bass/Channel.cpp +++ b/src/NH/Bass/Channel.cpp @@ -4,77 +4,72 @@ namespace NH::Bass { - void Channel::Play(const MusicDef& music, const MusicFile* file) + void Channel::Play(const std::shared_ptr& theme, HashString id) { if (m_Stream > 0) { BASS_ChannelFree(m_Stream); } - m_Music = music; - m_Stream = BASS_StreamCreateFile(true, file->Buffer.data(), 0, file->Buffer.size(), 0); + m_Theme = theme; + m_AudioId = id; + const auto& file = theme->GetAudioFile(id); + const auto& effects = theme->GetAudioEffects(id); + + m_Stream = BASS_StreamCreateFile(true, file.Buffer.data(), 0, file.Buffer.size(), 0); if (!m_Stream) { log->Error("Could not create stream: {0}\n error: {1}\n at {2}:{3}", - m_Music.Filename, Engine::ErrorCodeToString(BASS_ErrorGetCode()), + m_Theme->GetName(), Engine::ErrorCodeToString(BASS_ErrorGetCode()), __FILE__, __LINE__); return; } BASS_ChannelStart(m_Stream); + log->Info("Channel started: {0}", m_Theme->GetName()); - log->Info("Channel started: {0}", m_Music.Filename); + float targetVolume = effects.Volume.Active ? effects.Volume.Volume : 1.0f; + BASS_ChannelSetAttribute(m_Stream, BASS_ATTRIB_VOL, targetVolume); - if (Options->ForceFadeTransition) - { - log->Debug("BASSENGINE.ForceFadeTransition is set, forcing TransitionType::FADE"); - m_Music.StartTransition.Type = TransitionType::FADE; - m_Music.EndTransition.Type = TransitionType::FADE; - } - - if (m_Music.StartTransition.Type == TransitionType::FADE) + if (effects.FadeIn.Active) { BASS_ChannelSetAttribute(m_Stream, BASS_ATTRIB_VOL, 0.0f); - BASS_ChannelSlideAttribute(m_Stream, BASS_ATTRIB_VOL, m_Music.Volume, m_Music.StartTransition.Duration); - } - else - { - BASS_ChannelSetAttribute(m_Stream, BASS_ATTRIB_VOL, m_Music.Volume); + BASS_ChannelSlideAttribute(m_Stream, BASS_ATTRIB_VOL, targetVolume, effects.FadeIn.Duration); } - if (m_Music.Loop) + if (effects.Loop.Active) { BASS_ChannelFlags(m_Stream, BASS_SAMPLE_LOOP, BASS_SAMPLE_LOOP); - log->Trace("Loop set {0}", music.Filename); + log->Trace("Loop set {0}", m_Theme->GetName()); } - if (!Options->ForceDisableReverb && m_Music.Effects.Reverb) + if (!Options->ForceDisableReverb && effects.ReverbDX8.Active) { HFX fx = BASS_ChannelSetFX(m_Stream, BASS_FX_DX8_REVERB, 1); - BASS_DX8_REVERB params{ 0, m_Music.Effects.ReverbMix, m_Music.Effects.ReverbTime, 0.001f }; + BASS_DX8_REVERB params{ 0, effects.ReverbDX8.Mix, effects.ReverbDX8.Time, 0.001f }; if (!BASS_FXSetParameters(fx, (void*)¶ms)) { log->Error("Could not set reverb FX: {0}\n error: {1}\n at {2}:{3}", - m_Music.Filename, Engine::ErrorCodeToString(BASS_ErrorGetCode()), + m_Theme->GetName(), Engine::ErrorCodeToString(BASS_ErrorGetCode()), __FILE__, __LINE__); } - log->Trace("Reverb set: {0}", m_Music.Filename); + log->Trace("Reverb set: {0}", m_Theme->GetName()); } - if (m_Music.EndTransition.Type != TransitionType::NONE) + if (effects.FadeOut.Active) { const QWORD length = BASS_ChannelGetLength(m_Stream, BASS_POS_BYTE); - const QWORD transitionBytes = BASS_ChannelSeconds2Bytes(m_Stream, m_Music.EndTransition.Duration / 1000.0f); + const QWORD transitionBytes = BASS_ChannelSeconds2Bytes(m_Stream, effects.FadeOut.Duration / 1000.0f); const QWORD offset = length - transitionBytes; - BASS_ChannelSetSync(m_Stream, BASS_SYNC_POS, offset, OnTransitionSync, this); - log->Trace("SyncTransitionPos set: {0}", m_Music.Filename); + BASS_ChannelSetSync(m_Stream, BASS_SYNC_END, offset, OnVolumeSlideSync, this); + log->Trace("SyncEnd set: {0}", m_Theme->GetName()); } BASS_ChannelSetSync(m_Stream, BASS_SYNC_END, 0, OnEndSync, this); - log->Trace("SyncEnd set: {0}", m_Music.Filename); + log->Trace("SyncEnd set: {0}", m_Theme->GetName()); m_Status = ChannelStatus::PLAYING; - m_EventManager.DispatchEvent(EventType::MUSIC_CHANGE, m_Music); + m_EventManager.DispatchEvent(MusicChangeEvent(m_Theme, m_AudioId)); } double Channel::CurrentPosition() const @@ -95,18 +90,14 @@ namespace NH::Bass return -1; } - const MusicDef& Channel::CurrentMusic() const - { - return m_Music; - } - void Channel::Stop() { if (m_Stream > 0) { - if (m_Music.EndTransition.Type == TransitionType::FADE) + const auto& effects = m_Theme->GetAudioEffects(m_AudioId); + if (effects.FadeOut.Active) { - BASS_ChannelSlideAttribute(m_Stream, BASS_ATTRIB_VOL, 0.0f, m_Music.EndTransition.Duration); + BASS_ChannelSlideAttribute(m_Stream, BASS_ATTRIB_VOL, 0.0f, effects.FadeOut.Duration); BASS_ChannelSetSync(m_Stream, BASS_SYNC_SLIDE, 0, OnVolumeSlideSync, this); m_Status = ChannelStatus::FADING_OUT; } @@ -119,32 +110,48 @@ namespace NH::Bass } } + std::shared_ptr Channel::CurrentTheme() const + { + return m_Theme; + } + + const AudioFile& Channel::CurrentAudioFile() const + { + return m_Theme->GetAudioFile(m_AudioId); + } + + const AudioEffects& Channel::CurrentAudioEffects() const + { + return m_Theme->GetAudioEffects(m_AudioId); + } + void Channel::OnTransitionSync(HSYNC, DWORD channel, DWORD data, void* userData) { - const auto _this = static_cast(userData); + auto* _this = static_cast(userData); if (_this->m_Stream == channel) { - _this->m_EventManager.DispatchEvent(EventType::MUSIC_TRANSITION, _this->m_Music, - _this->m_Music.EndTransition.Duration); + _this->m_EventManager.DispatchEvent(MusicTransitionEvent(_this->m_Theme, _this->m_AudioId, _this->CurrentAudioEffects().FadeOut.Duration)); } } void Channel::OnEndSync(HSYNC, DWORD channel, DWORD data, void* userData) { - const auto _this = static_cast(userData); + auto* _this = static_cast(userData); if (_this->m_Stream == channel) { - _this->m_EventManager.DispatchEvent(EventType::MUSIC_END, _this->m_Music); - if (!_this->m_Music.Loop) + const auto& effects = _this->m_Theme->GetAudioEffects(_this->m_AudioId); + if (!effects.Loop.Active) { _this->m_Status = ChannelStatus::AVAILABLE; } + + _this->m_EventManager.DispatchEvent(MusicEndEvent(_this->m_Theme, _this->m_AudioId)); } } void Channel::OnVolumeSlideSync(HSYNC, DWORD channel, DWORD data, void* userData) { - const auto _this = static_cast(userData); + auto* _this = static_cast(userData); if (_this->m_Stream == channel && _this->m_Status == ChannelStatus::FADING_OUT) { float volume; diff --git a/src/NH/Bass/Channel.h b/src/NH/Bass/Channel.h index 0dadd77..dc559e9 100644 --- a/src/NH/Bass/Channel.h +++ b/src/NH/Bass/Channel.h @@ -3,9 +3,11 @@ #include "NH/Bass/CommonTypes.h" #include "EventManager.h" #include "NH/Logger.h" +#include #include + namespace NH::Bass { enum class ChannelStatus @@ -22,7 +24,8 @@ namespace NH::Bass EventManager& m_EventManager; ChannelStatus m_Status = ChannelStatus::AVAILABLE; HSTREAM m_Stream = 0; - MusicDef m_Music{}; + std::shared_ptr m_Theme = std::shared_ptr(); + HashString m_AudioId{""}; public: explicit Channel(size_t index, EventManager& em) : m_Index(index), m_EventManager(em) @@ -30,7 +33,7 @@ namespace NH::Bass log = NH::CreateLogger(Union::String::Format("zBassMusic::Channel({0})", index)); }; - void Play(const MusicDef& music, const MusicFile* file); + void Play(const std::shared_ptr&, HashString id = AudioFile::DEFAULT); void Stop(); @@ -39,11 +42,15 @@ namespace NH::Bass return m_Status == ChannelStatus::AVAILABLE; }; - double CurrentPosition() const; + [[nodiscard]] double CurrentPosition() const; + + [[nodiscard]] double CurrentLength() const; + + [[nodiscard]] std::shared_ptr CurrentTheme() const; - double CurrentLength() const; + [[nodiscard]] const AudioFile& CurrentAudioFile() const; - const MusicDef& CurrentMusic() const; + [[nodiscard]] const AudioEffects& CurrentAudioEffects() const; static void CALLBACK OnTransitionSync(HSYNC, DWORD channel, DWORD data, void* userData); diff --git a/src/NH/Bass/Command.cpp b/src/NH/Bass/Command.cpp new file mode 100644 index 0000000..0979526 --- /dev/null +++ b/src/NH/Bass/Command.cpp @@ -0,0 +1,68 @@ +#include "Command.h" + +namespace NH::Bass +{ + CommandResult OnTimeCommand::Execute(Engine& engine) + { + if (std::chrono::high_resolution_clock::now() >= m_TimePoint) + { + return m_Command->Execute(engine); + } + return m_ForceOrder ? CommandResult::RETRY : CommandResult::DEFER; + } + + void CommandQueue::AddCommand(std::shared_ptr command) + { + std::lock_guard lock(m_Mutex); + log->Info("Adding command to queue"); + m_Commands.push_back(std::move(command)); + } + + void CommandQueue::AddCommandDeferred(std::shared_ptr command) + { + std::lock_guard lock(m_DeferredMutex); + m_DeferredCommands.push_back(std::move(command)); + } + + void CommandQueue::AddCommandOnFront(std::shared_ptr command) + { + std::lock_guard lock(m_Mutex); + log->Info("Adding command to queue"); + m_Commands.push_front(std::move(command)); + } + + void CommandQueue::Update(Engine& engine) + { + { + std::lock_guard lock(m_DeferredMutex); + m_Commands.insert(m_Commands.end(), m_DeferredCommands.begin(), m_DeferredCommands.end()); + m_DeferredCommands.clear(); + } + + std::lock_guard lock(m_Mutex); + std::shared_ptr commandToDefer; + bool exitEarly = false; + while (!exitEarly && !m_Commands.empty()) + { + auto result = m_Commands.front()->Execute(engine); + switch (result) + { + case CommandResult::DONE: + m_Commands.pop_front(); + break; + case CommandResult::RETRY: + // Return, so we don't execute the next command + exitEarly = true; + break; + case CommandResult::DEFER: + commandToDefer = m_Commands.front(); + m_Commands.pop_front(); + break; + } + } + if (commandToDefer) + { + m_Commands.push_back(commandToDefer); + } + } +} \ No newline at end of file diff --git a/src/NH/Bass/Command.h b/src/NH/Bass/Command.h new file mode 100644 index 0000000..cac117e --- /dev/null +++ b/src/NH/Bass/Command.h @@ -0,0 +1,64 @@ +#pragma once + +#include + +#include +#include +#include +#include +#include + +namespace NH::Bass +{ + enum class CommandResult + { + DONE, // Remove command from the queue + RETRY, // Keep command in the front of a queue + DEFER // Keep command in the back of a queue + }; + + class Engine; + + struct Command + { + virtual CommandResult Execute(Engine& engine) = 0; + }; + + class OnTimeCommand : public Command + { + using TimePointType = std::chrono::high_resolution_clock::time_point; + + TimePointType m_TimePoint; + std::shared_ptr m_Command; + bool m_ForceOrder = false; + + public: + OnTimeCommand(TimePointType timePoint, std::shared_ptr command, bool forceOrder = false) + : m_TimePoint(timePoint), m_Command(std::move(command)), m_ForceOrder(forceOrder) {} + + CommandResult Execute(Engine& engine) override; + }; + + class CommandQueue + { + Logger* log = CreateLogger("zBassMusic::CommandQueue"); + std::deque> m_Commands; + std::deque> m_DeferredCommands; + std::mutex m_Mutex; + std::mutex m_DeferredMutex; + + public: + void AddCommand(std::shared_ptr command); + + /** + * Method to add commands within an executing command. + * We can't use AddCommand() directly, because it would cause a deadlock. + * @param command + */ + void AddCommandDeferred(std::shared_ptr command); + + void AddCommandOnFront(std::shared_ptr command); + + void Update(Engine& engine); + }; +} diff --git a/src/NH/Bass/CommonTypes.h b/src/NH/Bass/CommonTypes.h index b819ad3..4c00cae 100644 --- a/src/NH/Bass/CommonTypes.h +++ b/src/NH/Bass/CommonTypes.h @@ -5,63 +5,5 @@ namespace NH::Bass { - enum class TransitionType - { - NONE, - FADE - }; - struct Transition - { - TransitionType Type = TransitionType::NONE; - unsigned int Duration = 0; - }; - - struct MusicFile - { - Union::StringUTF8 Filename; - std::vector Buffer; - bool Ready = false; - bool Loading = false; - }; - - struct MusicDef - { - Union::StringUTF8 Filename; - Union::StringUTF8 Name = Filename; - float Volume = 1.0f; - bool Loop = false; - Transition StartTransition{}; - Transition EndTransition{}; - struct AudioEffects - { - bool Reverb = false; - float ReverbMix = 0.0f; - float ReverbTime = 1000.0f; - } Effects{}; - - void CopyFrom(const MusicDef& other) - { - Filename = other.Filename; - Name = other.Name; - Volume = other.Volume; - Loop = other.Loop; - StartTransition = other.StartTransition; - EndTransition = other.EndTransition; - Effects = other.Effects; - } - }; - - struct MusicDefRetry - { - MusicDef musicDef{}; - int32_t delayMs = 0; - }; - - struct MusicChannel - { - HSTREAM Stream = 0; - bool Playing = false; - MusicDef Music{}; - }; } diff --git a/src/NH/Bass/Engine.cpp b/src/NH/Bass/Engine.cpp index 42072aa..a2749b5 100644 --- a/src/NH/Bass/Engine.cpp +++ b/src/NH/Bass/Engine.cpp @@ -17,99 +17,6 @@ namespace NH::Bass return s_Instance; } - MusicFile& Engine::CreateMusicBuffer(const Union::StringUTF8& filename) - { - for (auto& [key, m]: m_MusicFiles) - { - if (key == filename) - { - log->Debug("Buffer already exists for {0}", filename); - return m; - } - } - - m_MusicFiles.emplace(std::make_pair(filename, MusicFile{ filename, std::vector() })); - log->Debug("New buffer for {0}", filename); - - return m_MusicFiles.at(filename); - } - - void Engine::PlayMusic(MusicDef inMusicDef) - { - std::lock_guard guard(m_PlayMusicMutex); - if (!m_Initialized) - { - return; - } - - // We are taking ownership of the MusicDef from here to gurarantee it's lifetime. - // MusicDef lives as long as the Engine instance and other classes expect it to stay - // at the same place in memory, so if we receive a second MusicDef with the same filename, - // we just copy its' data to our instance. - auto musicDefKey = HashString(inMusicDef.Filename); - if (m_MusicDefs.find(musicDefKey) == m_MusicDefs.end()) - { - m_MusicDefs[musicDefKey] = std::move(inMusicDef); - } - else - { - m_MusicDefs[musicDefKey].CopyFrom(inMusicDef); - } - - const MusicFile* file = GetMusicFile(m_MusicDefs[musicDefKey].Filename); - - if (!file) - { - log->Error("Could not play {0}. Music file is not loaded.\n at {1}:{2}", - m_MusicDefs[musicDefKey].Filename, __FILE__, __LINE__); - return; - } - - if (!file->Ready && file->Loading) - { - static int32_t delay = 10; - log->Debug("{0} is loading, will retry after {1} ms", m_MusicDefs[musicDefKey].Filename, delay); - MusicDefRetry retry{ MusicDef(m_MusicDefs[musicDefKey]), delay }; - m_PlayMusicRetryList.emplace_back(retry); - return; - } - - if (!file->Ready) - { - log->Error("Invalid state. MusicDef is not ready but not loading {0}\n at {1}:{2}", - m_MusicDefs[musicDefKey].Filename, __FILE__, __LINE__); - return; - } - - if (!m_ActiveChannel) - { - FinalizeScheduledMusic(m_MusicDefs[musicDefKey]); - return; - } - - m_TransitionScheduler.Schedule(*m_ActiveChannel, m_MusicDefs[musicDefKey]); - } - - void Engine::FinalizeScheduledMusic(const NH::Bass::MusicDef& musicDef) - { - Channel* channel = FindAvailableChannel(); - if (channel == nullptr) - { - log->Error("Could not play {0}. No channel is available.\n at {1}:{2}", - musicDef.Filename, __FILE__, __LINE__); - return; - } - - log->Info("Starting playback: {0}", musicDef.Filename); - if (m_ActiveChannel) - { - m_ActiveChannel->Stop(); - } - - m_ActiveChannel = channel; - m_ActiveChannel->Play(musicDef, GetMusicFile(musicDef.Filename)); - } - void Engine::Update() { if (!m_Initialized) @@ -122,25 +29,10 @@ namespace NH::Bass uint64_t delta = std::chrono::duration_cast(now - lastTimestamp).count(); lastTimestamp = now; - for (auto& retry: m_PlayMusicRetryList) - { - retry.delayMs -= delta; - if (retry.delayMs < 0) - { - log->Trace("PlayMusic({0})", retry.musicDef.Filename); - PlayMusic(retry.musicDef); - } - } - std::erase_if(m_PlayMusicRetryList, [](const MusicDefRetry& retry) { return retry.delayMs < 0; }); - - m_TransitionScheduler.Update([this](const MusicDef& musicDef) - { - log->Trace("onReady from scheduler {0}", musicDef.Filename); - FinalizeScheduledMusic(musicDef); - }); + m_TransitionScheduler.Update(*this); + m_CommandQueue.Update(*this); BASS_Update(delta); - GetEM().Update(); } @@ -181,6 +73,16 @@ namespace NH::Bass return m_TransitionScheduler; } + MusicManager& Engine::GetMusicManager() + { + return m_MusicManager; + } + + CommandQueue& Engine::GetCommandQueue() + { + return m_CommandQueue; + } + void Engine::StopMusic() { if (!m_Initialized) @@ -188,9 +90,9 @@ namespace NH::Bass return; } - for (auto& channel: m_Channels) + for (const auto& channel: m_Channels) { - channel.Stop(); + channel->Stop(); } m_ActiveChannel = nullptr; @@ -228,126 +130,87 @@ namespace NH::Bass m_Channels.clear(); for (size_t i = 0; i < Channels_Max; i++) { - m_Channels.emplace_back(i, m_EventManager); + m_Channels.emplace_back(std::make_shared(i, m_EventManager)); } log->Info("Initialized with device: {0}", deviceIndex); } - MusicFile* Engine::GetMusicFile(const Union::StringUTF8& filename) + std::shared_ptr Engine::FindAvailableChannel() { - for (auto& [key, m]: m_MusicFiles) + for (auto channel: m_Channels) { - if (key == filename) + if (channel->IsAvailable()) { - return &m; + return channel; } } return nullptr; } - Channel* Engine::FindAvailableChannel() + Union::StringUTF8 Engine::ErrorCodeToString(const int code) + { + // @formatter:off + static String map[] = { "BASS_OK", "BASS_ERROR_MEM", "BASS_ERROR_FILEOPEN", "BASS_ERROR_DRIVER", "BASS_ERROR_BUFLOST", "BASS_ERROR_HANDLE", "BASS_ERROR_FORMAT", "BASS_ERROR_POSITION", "BASS_ERROR_INIT", "BASS_ERROR_START", "BASS_ERROR_SSL", "BASS_ERROR_REINIT", "BASS_ERROR_ALREADY", "BASS_ERROR_NOTAUDIO", "BASS_ERROR_NOCHAN", "BASS_ERROR_ILLTYPE", "BASS_ERROR_ILLPARAM", "BASS_ERROR_NO3D", "BASS_ERROR_NOEAX", "BASS_ERROR_DEVICE", "BASS_ERROR_NOPLAY", "BASS_ERROR_FREQ", "BASS_ERROR_NOTFILE", "BASS_ERROR_NOHW", "BASS_ERROR_EMPTY", "BASS_ERROR_NONET", "BASS_ERROR_CREATE", "BASS_ERROR_NOFX", "BASS_ERROR_NOTAVAIL", "BASS_ERROR_DECODE", "BASS_ERROR_DX", "BASS_ERROR_TIMEOUT", "BASS_ERROR_FILEFORM", "BASS_ERROR_SPEAKER", "BASS_ERROR_VERSION", "BASS_ERROR_CODEC", "BASS_ERROR_ENDED", "BASS_ERROR_BUSY", "BASS_ERROR_UNSTREAMABLE", "BASS_ERROR_PROTOCOL", "BASS_ERROR_DENIED", "BASS_ERROR_UNKNOWN" }; + // @formatter:on + return map[code]; + } + + Logger* ChangeZoneCommand::log = CreateLogger("zBassMusic::ChangeZoneCommand"); + Logger* PlayThemeCommand::log = CreateLogger("zBassMusic::PlayThemeCommand"); + Logger* ScheduleThemeChangeCommand::log = CreateLogger("zBassMusic::ScheduleThemeChangeCommand"); + + CommandResult ChangeZoneCommand::Execute(Engine& engine) + { + log->Trace("Executing ChangeZoneCommand for zone {0}", m_Zone); + + const auto themes = engine.GetMusicManager().GetThemesForZone(m_Zone); + if (themes.empty()) + { + log->Warning("No themes found for zone {0}", m_Zone); + return CommandResult::DONE; + } + engine.GetCommandQueue().AddCommandDeferred(std::make_shared(themes[0].first)); + return CommandResult::DONE; + } + + CommandResult PlayThemeCommand::Execute(Engine& engine) { - for (auto& channel: m_Channels) + log->Trace("Executing PlayThemeCommand for theme {0}", m_ThemeId); + + auto theme = engine.GetMusicManager().GetTheme(m_ThemeId); + if (!theme->IsAudioFileReady(AudioFile::DEFAULT)) { - if (channel.IsAvailable()) + log->Trace("Theme {0} is not ready", m_ThemeId); + m_RetryCount++; + if (m_RetryCount > 10000) { - return &channel; + log->Error("Theme {0} was not ready after 10000 retries (roughly 600ms @ 60 FPS), removing it from queue", m_ThemeId); + return CommandResult::DONE; } + return CommandResult::RETRY; } - return nullptr; + auto channel = engine.FindAvailableChannel(); + if (!channel) + { + log->Error("No available channels"); + return CommandResult::DEFER; + } + + if (engine.m_ActiveChannel) { engine.m_ActiveChannel->Stop(); } + channel->Play(theme, m_AudioId); + engine.m_ActiveChannel = channel; + + return CommandResult::DONE; } - Union::StringUTF8 Engine::ErrorCodeToString(const int code) + CommandResult ScheduleThemeChangeCommand::Execute(Engine& engine) { - switch (code) - { - case 0: - return Union::StringUTF8("BASS_OK"); - case 1: - return Union::StringUTF8("BASS_ERROR_MEM"); - case 2: - return Union::StringUTF8("BASS_ERROR_FILEOPEN"); - case 3: - return Union::StringUTF8("BASS_ERROR_DRIVER"); - case 4: - return Union::StringUTF8("BASS_ERROR_BUFLOST"); - case 5: - return Union::StringUTF8("BASS_ERROR_HANDLE"); - case 6: - return Union::StringUTF8("BASS_ERROR_FORMAT"); - case 7: - return Union::StringUTF8("BASS_ERROR_POSITION"); - case 8: - return Union::StringUTF8("BASS_ERROR_INIT"); - case 9: - return Union::StringUTF8("BASS_ERROR_START"); - case 10: - return Union::StringUTF8("BASS_ERROR_SSL"); - case 11: - return Union::StringUTF8("BASS_ERROR_REINIT"); - case 14: - return Union::StringUTF8("BASS_ERROR_ALREADY"); - case 17: - return Union::StringUTF8("BASS_ERROR_NOTAUDIO"); - case 18: - return Union::StringUTF8("BASS_ERROR_NOCHAN"); - case 19: - return Union::StringUTF8("BASS_ERROR_ILLTYPE"); - case 20: - return Union::StringUTF8("BASS_ERROR_ILLPARAM"); - case 21: - return Union::StringUTF8("BASS_ERROR_NO3D"); - case 22: - return Union::StringUTF8("BASS_ERROR_NOEAX"); - case 23: - return Union::StringUTF8("BASS_ERROR_DEVICE"); - case 24: - return Union::StringUTF8("BASS_ERROR_NOPLAY"); - case 25: - return Union::StringUTF8("BASS_ERROR_FREQ"); - case 27: - return Union::StringUTF8("BASS_ERROR_NOTFILE"); - case 29: - return Union::StringUTF8("BASS_ERROR_NOHW"); - case 31: - return Union::StringUTF8("BASS_ERROR_EMPTY"); - case 32: - return Union::StringUTF8("BASS_ERROR_NONET"); - case 33: - return Union::StringUTF8("BASS_ERROR_CREATE"); - case 34: - return Union::StringUTF8("BASS_ERROR_NOFX"); - case 37: - return Union::StringUTF8("BASS_ERROR_NOTAVAIL"); - case 38: - return Union::StringUTF8("BASS_ERROR_DECODE"); - case 39: - return Union::StringUTF8("BASS_ERROR_DX"); - case 40: - return Union::StringUTF8("BASS_ERROR_TIMEOUT"); - case 41: - return Union::StringUTF8("BASS_ERROR_FILEFORM"); - case 42: - return Union::StringUTF8("BASS_ERROR_SPEAKER"); - case 43: - return Union::StringUTF8("BASS_ERROR_VERSION"); - case 44: - return Union::StringUTF8("BASS_ERROR_CODEC"); - case 45: - return Union::StringUTF8("BASS_ERROR_ENDED"); - case 46: - return Union::StringUTF8("BASS_ERROR_BUSY"); - case 47: - return Union::StringUTF8("BASS_ERROR_UNSTREAMABLE"); - case 48: - return Union::StringUTF8("BASS_ERROR_PROTOCOL"); - case 49: - return Union::StringUTF8("BASS_ERROR_DENIED"); - default: - return Union::StringUTF8("BASS_ERROR_UNKNOWN"); - } + log->Trace("Executing ScheduleThemeChangeCommand for theme {0}", m_ThemeId); + auto theme = engine.GetMusicManager().GetTheme(m_ThemeId); + engine.GetTransitionScheduler().Schedule(engine.m_ActiveChannel, theme); + return CommandResult::DONE; } } \ No newline at end of file diff --git a/src/NH/Bass/Engine.h b/src/NH/Bass/Engine.h index ecdc78c..fbfcec0 100644 --- a/src/NH/Bass/Engine.h +++ b/src/NH/Bass/Engine.h @@ -6,6 +6,8 @@ #include "TransitionScheduler.h" #include "NH/Logger.h" #include "NH/HashString.h" +#include "NH/Bass/MusicManager.h" +#include "NH/Bass/Command.h" #include #include #include @@ -13,51 +15,81 @@ namespace NH::Bass { + class ChangeZoneCommand; + class PlayThemeCommand; + class ScheduleThemeChangeCommand; + class Engine { + friend class ChangeZoneCommand; + friend class PlayThemeCommand; + friend class ScheduleThemeChangeCommand; + static NH::Logger* log; static Engine* s_Instance; bool m_Initialized = false; float m_MasterVolume = 1.0f; - std::vector m_Channels; - Channel* m_ActiveChannel = nullptr; + std::vector> m_Channels; + std::shared_ptr m_ActiveChannel = nullptr; + CommandQueue m_CommandQueue{}; EventManager m_EventManager{}; + MusicManager m_MusicManager{}; TransitionScheduler m_TransitionScheduler{}; - std::unordered_map m_MusicDefs; - std::unordered_map m_MusicFiles; - - std::mutex m_PlayMusicMutex; - std::vector m_PlayMusicRetryList; public: static Engine* GetInstance(); - MusicFile& CreateMusicBuffer(const Union::StringUTF8& filename); - - void PlayMusic(MusicDef musicDef); - void Update(); void SetVolume(float volume); - float GetVolume() const; + [[nodiscard]] float GetVolume() const; EventManager& GetEM(); TransitionScheduler& GetTransitionScheduler(); + MusicManager& GetMusicManager(); + + CommandQueue& GetCommandQueue(); + void StopMusic(); private: Engine(); - MusicFile* GetMusicFile(const Union::StringUTF8& filename); + std::shared_ptr FindAvailableChannel(); - Channel* FindAvailableChannel(); + public: + static Union::StringUTF8 ErrorCodeToString(int code); + }; - void FinalizeScheduledMusic(const MusicDef& musicDef); + class ChangeZoneCommand : public Command + { + static Logger* log; + HashString m_Zone; + public: + explicit ChangeZoneCommand(HashString zone) : m_Zone(zone) {}; + CommandResult Execute(Engine& engine) override; + }; + class PlayThemeCommand : public Command + { + static Logger* log; + HashString m_ThemeId; + HashString m_AudioId; + size_t m_RetryCount = 0; public: - static Union::StringUTF8 ErrorCodeToString(int code); + explicit PlayThemeCommand(HashString themeId, HashString audioId) : m_ThemeId(themeId), m_AudioId(audioId) {}; + CommandResult Execute(Engine& engine) override; + }; + + class ScheduleThemeChangeCommand : public Command + { + static Logger* log; + HashString m_ThemeId; + public: + explicit ScheduleThemeChangeCommand(HashString themeId) : m_ThemeId(themeId) {}; + CommandResult Execute(Engine& engine) override; }; } diff --git a/src/NH/Bass/EventManager.cpp b/src/NH/Bass/EventManager.cpp index 0896e81..a07eb6f 100644 --- a/src/NH/Bass/EventManager.cpp +++ b/src/NH/Bass/EventManager.cpp @@ -5,7 +5,7 @@ namespace NH::Bass NH::Logger* EventManager::log = NH::CreateLogger("zBassMusic::EventManager"); EventSubscriber* - EventManager::AddSubscriber(const EventType type, const EventSubscriberFunction function, void* userData) + EventManager::AddSubscriber(const EventType type, const EventSubscriberFn function, void* userData) { return &m_Subscribers.emplace_back(EventSubscriber{ type, function, userData }); } @@ -18,9 +18,14 @@ namespace NH::Bass }); } - void EventManager::DispatchEvent(const EventType type, const MusicDef& musicDef, const int data) + void EventManager::DispatchEvent(Event&& event) { - m_EventQueue.emplace_front(Event{ type, musicDef, data }); + m_EventQueue.emplace_front(event); + } + + void EventManager::DispatchEvent(const Event& event) + { + m_EventQueue.emplace_front(event); } void EventManager::Update() @@ -32,9 +37,9 @@ namespace NH::Bass m_EventQueue.pop_back(); for (const auto& s: m_Subscribers) { - if (s.Type == event.type) + if (s.Type == event.Type) { - s.Function(event.music, event.data, s.UserData); + s.Function(event, s.UserData); } } } diff --git a/src/NH/Bass/EventManager.h b/src/NH/Bass/EventManager.h index b41af3c..8579ed0 100644 --- a/src/NH/Bass/EventManager.h +++ b/src/NH/Bass/EventManager.h @@ -2,12 +2,15 @@ #include "NH/Bass/CommonTypes.h" #include "NH/Logger.h" +#include "MusicTheme.h" +#include +#include #include #include +#include namespace NH::Bass { - using EventSubscriberFunction = void (*)(const MusicDef&, int data, void*); enum class EventType { @@ -17,10 +20,26 @@ namespace NH::Bass MUSIC_CHANGE }; + struct Event + { + struct MusicEnd { std::shared_ptr Theme; HashString AudioId; }; + struct MusicTransition { std::shared_ptr Theme; HashString AudioId; float TimeLeft; }; + struct MusicChange { std::shared_ptr Theme; HashString AudioId; }; + using DataType = std::variant; + + EventType Type = EventType::UNKNOWN; + DataType Data; + + Event() = delete; + Event(EventType type, DataType data) : Type(type), Data(std::move(data)) {} + }; + + using EventSubscriberFn = void (*)(const Event&, void*); + struct EventSubscriber { EventType Type = EventType::UNKNOWN; - EventSubscriberFunction Function{}; + EventSubscriberFn Function{}; void* UserData = nullptr; bool operator==(const EventSubscriber& other) const @@ -35,28 +54,42 @@ namespace NH::Bass { static NH::Logger* log; - struct Event - { - EventType type = EventType::UNKNOWN; - MusicDef music; - int data{}; - }; - friend Engine; std::vector m_Subscribers{}; std::deque m_EventQueue; public: - EventSubscriber* AddSubscriber(EventType type, EventSubscriberFunction function, void* userData = nullptr); + EventSubscriber* AddSubscriber(EventType type, EventSubscriberFn function, void* userData = nullptr); void RemoveSubscriber(const EventSubscriber* subscriber); - void DispatchEvent(EventType type, const MusicDef& musicDef, int data = 0); + void DispatchEvent(Event&& event); + + void DispatchEvent(const Event& event); void Update(); private: EventManager() = default; }; + + struct MusicEndEvent : public Event + { + MusicEnd Data; + MusicEndEvent(std::shared_ptr theme, HashString audioId) : Event(EventType::MUSIC_END, MusicEnd{std::move(theme), audioId}) {} + }; + + struct MusicTransitionEvent : public Event + { + MusicTransition Data; + MusicTransitionEvent(std::shared_ptr theme, HashString audioId, float timeLeft) + : Event(EventType::MUSIC_TRANSITION, MusicTransition{std::move(theme), audioId, timeLeft}) {} + }; + + struct MusicChangeEvent : public Event + { + MusicChange Data; + MusicChangeEvent(std::shared_ptr theme, HashString audioId) : Event(EventType::MUSIC_CHANGE, MusicChange{std::move(theme), audioId}) {} + }; } \ No newline at end of file diff --git a/src/NH/Bass/MusicManager.cpp b/src/NH/Bass/MusicManager.cpp new file mode 100644 index 0000000..e03874d --- /dev/null +++ b/src/NH/Bass/MusicManager.cpp @@ -0,0 +1,34 @@ +#include "MusicManager.h" + +namespace NH::Bass +{ + void MusicManager::AddTheme(HashString id, std::shared_ptr theme) + { + m_Themes.emplace(id, theme); + m_Themes.at(id)->LoadAudioFiles(Executors.IO); + log->Info("New theme {0}", id); + log->PrintRaw(LoggerLevel::Debug, m_Themes.at(id)->ToString()); + } + + std::vector>> MusicManager::GetThemesForZone(HashString zone) + { + std::vector>> themes; + for (auto& [id, theme] : m_Themes) + { + if (theme->HasZone(zone)) + { + themes.emplace_back(id, theme); + } + } + return std::move(themes); + } + + std::shared_ptr MusicManager::GetTheme(HashString id) + { + if (!m_Themes.contains(id)) + { + return {}; + } + return m_Themes.at(id); + } +} \ No newline at end of file diff --git a/src/NH/Bass/MusicManager.h b/src/NH/Bass/MusicManager.h new file mode 100644 index 0000000..eba0bb3 --- /dev/null +++ b/src/NH/Bass/MusicManager.h @@ -0,0 +1,21 @@ +#pragma once + +#include +#include + +namespace NH::Bass +{ + + class MusicManager + { + Logger* log = CreateLogger("zBassMusic::MusicManager"); + std::unordered_map> m_Themes; + + public: + void AddTheme(HashString id, std::shared_ptr theme); + + [[nodiscard]] std::shared_ptr GetTheme(HashString id); + + std::vector>> GetThemesForZone(HashString zone); + }; +} diff --git a/src/NH/Bass/MusicTheme.cpp b/src/NH/Bass/MusicTheme.cpp new file mode 100644 index 0000000..48ae819 --- /dev/null +++ b/src/NH/Bass/MusicTheme.cpp @@ -0,0 +1,75 @@ +#include "MusicTheme.h" + +#include + +namespace NH::Bass +{ + HashString AudioFile::DEFAULT = "DEFAULT"; + AudioEffects AudioEffects::None = {}; + MusicTheme MusicTheme::None = MusicTheme(""); + Logger* MusicTheme::log = CreateLogger("zBassMusic::MusicTheme"); + + MusicTheme::MusicTheme(const String& name) + : m_Name(name) + { + + } + + void MusicTheme::SetAudioFile(HashString type, const String& filename) + { + m_AudioFiles.emplace(std::make_pair(type, AudioFile{})); + m_AudioFiles[type].Filename = filename; + m_AudioFiles[type].Status = AudioFile::StatusType::NOT_LOADED; + } + + void MusicTheme::SetAudioEffects(HashString type, const std::function& effectsSetter) + { + m_AudioEffects.emplace(std::make_pair(type, AudioEffects{})); + effectsSetter(m_AudioEffects[type]); + } + + void MusicTheme::AddZone(HashString zone) + { + m_Zones.emplace_back(zone); + } + + void MusicTheme::LoadAudioFiles(Executor& executor) + { + for (auto& [type, audioFile]: m_AudioFiles) + { + if (audioFile.Status == AudioFile::StatusType::NOT_LOADED) + { + audioFile.Status = AudioFile::StatusType::LOADING; + executor.AddTask([type, this]() + { + int systems = VDF_VIRTUAL | VDF_PHYSICAL; + const Union::VDFS::File* file = Union::VDFS::GetDefaultInstance().GetFile(m_AudioFiles[type].Filename, systems); + if (!file) + { + m_AudioFiles[type].Status = AudioFile::StatusType::FAILED; + m_AudioFiles[type].Error = "File not found"; + return; + } + + Union::Stream* stream = file->Open(); + m_AudioFiles[type].Buffer.resize(stream->GetSize()); + stream->Read(m_AudioFiles[type].Buffer.data(), m_AudioFiles[type].Buffer.size()); + stream->Close(); + + m_AudioFiles[type].Status = AudioFile::StatusType::READY; + }); + } + } + } + + const AudioEffects& MusicTheme::GetAudioEffects(HashString type) const + { + if (m_AudioFiles.contains(type)) { return m_AudioEffects.at(type); } + return AudioEffects::None; + } + + bool MusicTheme::HasZone(HashString zone) const + { + return std::find(m_Zones.begin(), m_Zones.end(), zone) != m_Zones.end(); + } +} \ No newline at end of file diff --git a/src/NH/Bass/MusicTheme.h b/src/NH/Bass/MusicTheme.h new file mode 100644 index 0000000..b5dcaa0 --- /dev/null +++ b/src/NH/Bass/MusicTheme.h @@ -0,0 +1,125 @@ +#pragma once + +#include "CommonTypes.h" +#include +#include +#include +#include + +#include +#include +#include + +namespace NH::Bass +{ + struct AudioFile : public HasToString + { + static HashString DEFAULT; + + enum class StatusType : size_t { NOT_LOADED = 0, LOADING, READY, FAILED }; + + String Filename; + std::vector Buffer = {}; + StatusType Status = StatusType::NOT_LOADED; + String Error; + + [[nodiscard]] String ToString() const override + { + static String types[] = { "NOT_LOADED", "LOADING", "READY", "FAILED" }; + return String("AudioFile{ Filename: ") + Filename + ", Status: " + types[(size_t) Status] + ", Error: " + Error + " }"; + } + }; + + struct AudioEffects : public HasToString + { + static AudioEffects None; + + struct { bool Active = false; float Mix = 0; float Time = 0; } ReverbDX8{}; + struct { bool Active = false; double Duration = 0; } FadeIn{}; + struct { bool Active = false; double Duration = 0; } FadeOut{}; + struct { bool Active = false; float Volume = 1.0f; } Volume{}; + struct { bool Active = false; } Loop{}; + + [[nodiscard]] String ToString() const override + { + return String("AudioEffects{ \n" + "\tReverbDX8: { Active: ") + String(ReverbDX8.Active) + ", Mix: " + String(ReverbDX8.Mix) + ", Time: " + String(ReverbDX8.Time) + " }, \n" + + "\tFadeIn: { Active: " + String(FadeIn.Active) + ", Duration: " + String(FadeIn.Duration) + " }, \n" + + "\tFadeOut: { Active: " + String(FadeOut.Active) + ", Duration: " + String(FadeOut.Duration) + " }, \n" + + "\tVolume: { Active: " + String(Volume.Active) + ", Volume: " + String(Volume.Volume) + " } \n}"; + } + }; + + class MusicTheme : public HasToString + { + static Logger* log; + + String m_Name; + std::unordered_map m_AudioFiles; + std::unordered_map m_AudioEffects; + std::vector m_Zones; + + public: + static MusicTheme None; + + explicit MusicTheme(const String& name); + + void SetAudioFile(HashString type, const String& filename); + + void SetAudioEffects(HashString type, const std::function& effectsSetter); + + void AddZone(HashString zone); + + void LoadAudioFiles(Executor& executor); + + [[nodiscard]] const String& GetName() const { return m_Name; } + + [[nodiscard]] bool HasAudioFile(HashString type) const { return m_AudioFiles.find(type) != m_AudioFiles.end(); } + + [[nodiscard]] bool IsAudioFileReady(HashString type) const { return HasAudioFile(type) && m_AudioFiles.at(type).Status == AudioFile::StatusType::READY; } + + [[nodiscard]] const AudioFile& GetAudioFile(HashString type) const { return m_AudioFiles.at(type); } + + [[nodiscard]] const AudioEffects& GetAudioEffects(HashString type) const; + + [[nodiscard]] bool HasZone(HashString zone) const; + + [[nodiscard]] String ToString() const override + { + String result = String("MusicTheme{ \n\tName: ") + m_Name + ", \n\tAudioFiles: {\n"; + int i = 0; + for (auto& [type, audioFile]: m_AudioFiles) + { + result += String("\t\t") + String(type) + ": " + audioFile.ToString().Replace("\n", "\n\tt"); + if (++i < m_AudioFiles.size()) + { + result += ",\n"; + } + } + i = 0; + result += "\n\t},\n\tAudioEffects: { \n"; + for (auto& [type, audioEffects]: m_AudioEffects) + { + result += String("\t\t") + String(type) + ": " + audioEffects.ToString().Replace("\n", "\n\t\t"); + if (++i < m_AudioEffects.size()) + { + result += ",\n"; + } + } + i = 0; + result += "\n\t},\n\tZones: { "; + for (auto& zone: m_Zones) + { + result += String(zone); + if (++i < m_Zones.size()) + { + result += ", "; + } + } + result += " }\n }"; + return result; + } + + private: + }; +} diff --git a/src/NH/Bass/TransitionScheduler.cpp b/src/NH/Bass/TransitionScheduler.cpp index 8102b5f..64dd44b 100644 --- a/src/NH/Bass/TransitionScheduler.cpp +++ b/src/NH/Bass/TransitionScheduler.cpp @@ -1,122 +1,138 @@ #include "TransitionScheduler.h" +#include +#include #include namespace NH::Bass { - void TransitionScheduler::Schedule(Channel& activeChannel, const MusicDef& nextMusic) + void TransitionScheduler::Schedule(const std::shared_ptr& activeChannel, const std::shared_ptr& nextMusic) { - const MusicDef& music = activeChannel.CurrentMusic(); - log->Trace("Schedule for {0} -> {1}", music.Filename, nextMusic.Filename); - TransitionScheduleRule rule = GetScheduleRule(music); + if (!activeChannel) + { + m_ScheduledActions.emplace_back(ScheduledAction{ activeChannel, 0, [id = HashString(nextMusic->GetName())](Engine& engine) + { + engine.GetCommandQueue().AddCommand(std::make_shared(id, AudioFile::DEFAULT)); + }}); + return; + } + + const auto& currentTheme = activeChannel->CurrentTheme(); + const TransitionScheduleRule& rule = GetScheduleRule(currentTheme->GetName()); if (rule.Type == TransitionScheduleType::INSTANT) { - log->Trace("Schedule rule type = INSTANT"); - m_Monitors.emplace_back(ScheduleMonitor{ &activeChannel, 0, - [&nextMusic](const std::function& onReady) - { - onReady(nextMusic); - }}); + ScheduleInstant(activeChannel, nextMusic); return; } if (rule.Type == TransitionScheduleType::ON_BEAT) { - log->Trace("Schedule rule type = ON_BEAT"); - TransitionScheduleRule::DataOnBeat data = std::get(rule.Data); - - log->Trace("OnBeat.Interval = {0}", data.Interval); - log->Trace("OnBeat.TimePoints.Count = {0}", data.TimePoints.size()); + ScheduleOnBeat(activeChannel, nextMusic, rule); + return; + } - double overhead = music.EndTransition.Type != TransitionType::NONE ? music.EndTransition.Duration : 0; - log->Trace("overhead = {0}", overhead); - std::sort(data.TimePoints.begin(), data.TimePoints.end()); + log->Error("Unknown Schedule type = {0}", (size_t)rule.Type); + } - double length = activeChannel.CurrentLength(); - double currentPosition = activeChannel.CurrentPosition(); - log->Trace("length = {0}", length); - log->Trace("currentPosition = {0}", currentPosition); - double timePointCandidate = -1; - double intervalCandidate = -1; + void TransitionScheduler::ScheduleInstant(const std::shared_ptr& activeChannel, const std::shared_ptr& nextMusic) + { + m_ScheduledActions.emplace_back(ScheduledAction{ activeChannel, 0, [id = HashString(nextMusic->GetName())](Engine& engine) + { + engine.GetCommandQueue().AddCommand(std::make_shared(id, AudioFile::DEFAULT)); + }}); + } - log->Trace("Rule checking TimePoints"); - for (const auto& item: data.TimePoints) + void TransitionScheduler::ScheduleOnBeat(const std::shared_ptr& activeChannel, const std::shared_ptr& nextMusic, const TransitionScheduleRule& rule) + { + TransitionScheduleRule::DataOnBeat data = std::get(rule.Data); + const auto& currentTheme = activeChannel->CurrentTheme(); + const auto& effects = currentTheme->GetAudioEffects(AudioFile::DEFAULT); + + double overhead = effects.FadeOut.Active ? effects.FadeOut.Duration : 0; + std::sort(data.TimePoints.begin(), data.TimePoints.end()); + + double length = activeChannel->CurrentLength(); + double currentPosition = activeChannel->CurrentPosition(); + log->Trace("length = {0}", length); + log->Trace("currentPosition = {0}", currentPosition); + double timePointCandidate = -1; + double intervalCandidate = -1; + + log->Trace("Rule checking TimePoints"); + for (const auto& item: data.TimePoints) + { + double minValue = currentPosition - overhead; + if (item > minValue) { - double minValue = currentPosition - overhead; - if (item > minValue) - { - log->Trace("found TimePoint = {0}", item); - timePointCandidate = item; - break; - } + log->Trace("found TimePoint = {0}", item); + timePointCandidate = item; + break; } + } - if (data.Interval > 0) - { - log->Trace("Rule checking Interval"); - - // Please don't abort me. Quick hack for preview. Fast enough. - for (double time = 0; time < length; time += data.Interval) - { - double minValue = currentPosition - overhead; - if (time > minValue) - { - log->Trace("found TimePoint = {0}", time); - intervalCandidate = time; - break; - } - } - } + if (data.Interval > 0) + { + log->Trace("Rule checking Interval"); - if (timePointCandidate <= 0 && intervalCandidate <= 0) + // Please don't abort me. Quick hack for preview. Fast enough. + for (double time = 0; time < length; time += data.Interval) { - log->Trace("Not found any candidates, rollback to INSTANT"); - m_Monitors.emplace_back(ScheduleMonitor{ &activeChannel, 0, [&nextMusic]( - const std::function& onReady) + double minValue = currentPosition - overhead; + if (time > minValue) { - onReady(nextMusic); - }}); - return; + log->Trace("found TimePoint = {0}", time); + intervalCandidate = time; + break; + } } + } - double target = timePointCandidate > intervalCandidate ? timePointCandidate : intervalCandidate; - log->Trace("target = {0}", target); - - log->Debug("Starting Monitor {0} -> {1}, position = {2}", music.Filename, nextMusic.Filename, target); - m_Monitors.emplace_back(ScheduleMonitor{ &activeChannel, target, - [&nextMusic](const std::function& onReady) - { - onReady(nextMusic); - }}); - + if (timePointCandidate <= 0 && intervalCandidate <= 0) + { + log->Trace("Not found any candidates, rollback to INSTANT"); + ScheduleInstant(activeChannel, nextMusic); return; } - log->Error("Unknown Schedule type = {0}", (size_t)rule.Type); + double target = timePointCandidate > intervalCandidate ? timePointCandidate : intervalCandidate; + log->Trace("target = {0}", target); + + log->Debug("Starting Monitor {0} -> {1}, position = {2}", currentTheme->GetName(), nextMusic->GetName(), target); + m_ScheduledActions.emplace_back(ScheduledAction{ activeChannel, target, [id = HashString(nextMusic->GetName())](Engine& engine) + { + engine.GetCommandQueue().AddCommand(std::make_shared(id, AudioFile::DEFAULT)); + }}); } - void TransitionScheduler::Update(const std::function& onReady) + void TransitionScheduler::Update(Engine& engine) { - for (auto& monitor: m_Monitors) + for (auto& scheduledAction : m_ScheduledActions) { - if (monitor.Position <= monitor.Channel->CurrentPosition()) + if (!scheduledAction.Channel) + { + scheduledAction.Done = true; + scheduledAction.Action(engine); + continue; + } + + if (scheduledAction.Position <= scheduledAction.Channel->CurrentPosition()) { - if (monitor.Position > 0) + if (scheduledAction.Position > 0) { - double target = monitor.Position; - double position = monitor.Channel->CurrentPosition(); + double target = scheduledAction.Position; + double position = scheduledAction.Channel->CurrentPosition(); double delay = position - target; log->Debug( "Monitor for {0} completed, calling onReady\n target = {1}\n current = {2}\n delay = {3}", - monitor.Channel->CurrentMusic().Filename, target, position, delay); + scheduledAction.Channel->CurrentTheme()->GetName(), target, position, delay); } - monitor.Done = true; - monitor.Action(onReady); + scheduledAction.Done = true; + scheduledAction.Action(engine); } } - std::erase_if(m_Monitors, [](ScheduleMonitor& monitor) { return monitor.Done; }); + std::erase_if(m_ScheduledActions, [](const ScheduledAction& monitor) { return monitor.Done; }); } void TransitionScheduler::AddRuleOnBeat(const char* name, double interval, std::vector timePoints) @@ -125,12 +141,11 @@ namespace NH::Bass m_ScheduleRules.insert_or_assign(name, TransitionScheduleRule::OnBeat(interval, std::move(timePoints))); } - const TransitionScheduleRule& TransitionScheduler::GetScheduleRule(const MusicDef& music) + const TransitionScheduleRule& TransitionScheduler::GetScheduleRule(HashString id) { - const char* name = music.Filename.ToChar(); - if (m_ScheduleRules.contains(name)) + if (m_ScheduleRules.contains(id)) { - return m_ScheduleRules.at(name); + return m_ScheduleRules.at(id); } static TransitionScheduleRule defaultRule = TransitionScheduleRule::Instant(); diff --git a/src/NH/Bass/TransitionScheduler.h b/src/NH/Bass/TransitionScheduler.h index 4eda6e0..3dc5b16 100644 --- a/src/NH/Bass/TransitionScheduler.h +++ b/src/NH/Bass/TransitionScheduler.h @@ -57,27 +57,32 @@ namespace NH::Bass } }; - struct ScheduleMonitor + struct ScheduledAction { - Channel* Channel; + std::shared_ptr Channel; double Position; - std::function onReady)> Action; + std::function Action; bool Done = false; }; class TransitionScheduler { NH::Logger* log = NH::CreateLogger("zBassMusic::TransitionScheduler"); - std::unordered_map m_ScheduleRules; - std::vector m_Monitors; + std::unordered_map m_ScheduleRules; + std::vector m_ScheduledActions; public: - void Schedule(Channel& activeChannel, const MusicDef& nextMusic); + void Schedule(const std::shared_ptr& activeChannel, const std::shared_ptr& nextMusic); - void Update(const std::function& onReady); + void Update(Engine& engine); void AddRuleOnBeat(const char* name, double interval = 0, std::vector timePoints = {}); - const TransitionScheduleRule& GetScheduleRule(const MusicDef& music); + private: + const TransitionScheduleRule& GetScheduleRule(HashString id); + + void ScheduleInstant(const std::shared_ptr& activeChannel, const std::shared_ptr& nextMusic); + + void ScheduleOnBeat(const std::shared_ptr& activeChannel, const std::shared_ptr& nextMusic, const TransitionScheduleRule& rule); }; } \ No newline at end of file diff --git a/src/NH/Executor.cpp b/src/NH/Executor.cpp new file mode 100644 index 0000000..32574f3 --- /dev/null +++ b/src/NH/Executor.cpp @@ -0,0 +1,74 @@ +#include "Executor.h" + +namespace NH +{ + ThreadPool::ThreadPool(const String& name, size_t threads) + : Executor(), m_Name(name), log(NH::CreateLogger(String::Format("zBassMusic::ThreadPool({0})", name))) + { + for (size_t i = 0; i < threads; i++) + { + m_Threads.emplace_back([this]() + { + log->Info("[thread-{0}] started", std::format("{}", std::this_thread::get_id()).c_str()); + EventLoop(); + log->Info("[thread-{0}] exiting", std::format("{}", std::this_thread::get_id()).c_str()); + }); + for (auto& thread: m_Threads) + { + thread.detach(); + } + } + } + + ThreadPool::~ThreadPool() + { + m_Stop = true; + for (auto& thread: m_Threads) + { + if (thread.joinable()) + { + thread.join(); + } + } + } + + void ThreadPool::AddTask(TaskFN&& task) + { + std::lock_guard lock(m_TasksMutex); + m_Tasks.push_back(std::move(task)); + } + + void ThreadPool::AddTask(const TaskFN& task) + { + std::lock_guard lock(m_TasksMutex); + m_Tasks.push_back(task); + } + + void ThreadPool::EventLoop() + { + while (!m_Stop) + { + TaskFN task; + { + std::lock_guard lock(m_TasksMutex); + if (!m_Tasks.empty()) + { + task = std::move(m_Tasks.front()); + m_Tasks.pop_front(); + } + } + + if (task) + { + log->Trace("[thread-{0}] processing task", std::format("{}", std::this_thread::get_id()).c_str()); + task(); + log->Trace("[thread-{0}] task done", std::format("{}", std::this_thread::get_id()).c_str()); + } + else + { + std::this_thread::yield(); + } + } + } + +} \ No newline at end of file diff --git a/src/NH/Executor.h b/src/NH/Executor.h new file mode 100644 index 0000000..88a057f --- /dev/null +++ b/src/NH/Executor.h @@ -0,0 +1,57 @@ +#pragma once + +#include + +#include +#include +#include +#include +#include +#include + +namespace NH +{ + using TaskFN = std::function; + + struct Executor + { + virtual void AddTask(TaskFN&& task) = 0; + virtual void AddTask(const TaskFN& task) = 0; + }; + + class InstantExecutor : public Executor + { + public: + void AddTask(TaskFN&& task) override { task(); } + void AddTask(const TaskFN& task) override { task(); } + }; + + class ThreadPool : public Executor + { + NH::Logger* log; + String m_Name; + std::vector m_Threads; + std::deque m_Tasks; + std::mutex m_TasksMutex; + bool m_Stop = false; + + public: + explicit ThreadPool(const String& name, size_t threads = std::thread::hardware_concurrency()); + + ~ThreadPool(); + + void AddTask(TaskFN&& task) override; + + void AddTask(const TaskFN& task) override; + + private: + ThreadPool(ThreadPool const&); + ThreadPool& operator=(ThreadPool const&); + + void EventLoop(); + }; + + struct { + ThreadPool IO = ThreadPool("IO", std::thread::hardware_concurrency()); + } Executors; +} \ No newline at end of file diff --git a/src/NH/HashString.h b/src/NH/HashString.h index 134cb8e..774a5c9 100644 --- a/src/NH/HashString.h +++ b/src/NH/HashString.h @@ -33,11 +33,21 @@ namespace NH class HashString { uint64_t Id; + const char* Value; public: - constexpr explicit(false) HashString(const char* str) noexcept: Id(FNV1a::hash_64_fnv1a_const(str)) {} // NOLINT(google-explicit-constructor) - constexpr explicit(false) HashString(const String& str) noexcept: Id(FNV1a::hash_64_fnv1a_const(str)) {} // NOLINT(google-explicit-constructor) + constexpr HashString() noexcept : Id(0), Value(nullptr) {} + constexpr explicit(false) HashString(const char* str) noexcept: Id(FNV1a::hash_64_fnv1a_const(str)), Value(str) {} // NOLINT(google-explicit-constructor) + explicit(false) HashString(const String& str) noexcept: Id(FNV1a::hash_64_fnv1a_const(str)), Value(str.ToChar()) {} // NOLINT(google-explicit-constructor) + + [[nodiscard]] constexpr uint64_t GetHash() const { return Id; } + + [[nodiscard]] constexpr const char* GetValue() const { return Value; } + constexpr explicit(false) operator uint64_t() const noexcept { return Id; } // NOLINT(google-explicit-constructor) + constexpr explicit(false) operator const char*() const noexcept { return Value; } // NOLINT(google-explicit-constructor) + explicit(false) operator String() const noexcept { return { Value }; } // NOLINT(google-explicit-constructor) + constexpr bool operator==(const HashString& other) const noexcept { return Id == other.Id; } }; diff --git a/src/NH/Logger.cpp b/src/NH/Logger.cpp index de47842..653c7ec 100644 --- a/src/NH/Logger.cpp +++ b/src/NH/Logger.cpp @@ -38,12 +38,21 @@ namespace NH } } + void Logger::PrintRaw(LoggerLevel level, const String& message) const + { + auto* adapter = GetAdapter(); + if ((adapter && adapter->CanLog(level)) || !adapter) + { + String::Format("\x1B[0m\x1B[36;3m{0}\x1B[0m", message).StdPrintLine(); + } + } + UnionConsoleLoggerAdapter::UnionConsoleLoggerAdapter(LoggerLevel level) : ILoggerAdapter(level) { SetLevelColor(LoggerLevel::Fatal, Color{ "\x1B[31m", "\x1B[41;1m\x1B[37;1m", "\x1B[41m\x1B[37;1m" }); SetLevelColor(LoggerLevel::Error, Color{ "\x1B[31m", "\x1B[41;1m\x1B[37;1m", "\x1B[31;1m" }); SetLevelColor(LoggerLevel::Warn, Color{ "\x1B[33m", "\x1B[43;1m\x1B[37m", "\x1B[0m\x1B[33;1m" }); - SetLevelColor(LoggerLevel::Info, Color{ "\x1B[30;1m", "\x1B[47;2m\x1B[39m", "\x1B[0m" }); + SetLevelColor(LoggerLevel::Info, Color{ "\x1B[36;1m", "\x1B[44m\x1B[37;1m", "\x1B[36m" }); SetLevelColor(LoggerLevel::Debug, Color{ "\x1B[32m", "\x1B[42;1m\x1B[37;1m", "\x1B[0m\x1B[32;1m" }); SetLevelColor(LoggerLevel::Trace, Color{ "\x1B[35m", "\x1B[35;1m\x1B[30m", "\x1B[35;1m" }); } diff --git a/src/NH/Logger.h b/src/NH/Logger.h index f5d9d7b..1027a0e 100644 --- a/src/NH/Logger.h +++ b/src/NH/Logger.h @@ -66,6 +66,8 @@ namespace NH void Message(LoggerLevel level, const String& message); + void PrintRaw(LoggerLevel level, const String& message) const; + template void Message(LoggerLevel level, const char* format, Args... args) { Message(level, String::Format(format, args...)); } diff --git a/src/NH/ToString.h b/src/NH/ToString.h new file mode 100644 index 0000000..5fdf1d4 --- /dev/null +++ b/src/NH/ToString.h @@ -0,0 +1,13 @@ +#pragma once + +#include "Logger.h" + +namespace NH +{ + struct HasToString + { + [[nodiscard]] virtual String ToString() const = 0; + + explicit(false) operator String() const noexcept { return ToString(); } // NOLINT(google-explicit-constructor) + }; +} diff --git a/src/Plugin.hpp b/src/Plugin.hpp index 1ef64b1..57cc2e5 100644 --- a/src/Plugin.hpp +++ b/src/Plugin.hpp @@ -1,5 +1,6 @@ #include #include #include +#include #include #include