diff --git a/Externals/lz4/lz4 b/Externals/lz4/lz4 deleted file mode 160000 index 5fc0630a0ed5..000000000000 --- a/Externals/lz4/lz4 +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 5fc0630a0ed55e9755b0fe3990cd021ae1c3edc1 diff --git a/Source/Core/Common/CommonPaths.h b/Source/Core/Common/CommonPaths.h index 44ada6dd192b..6783c91a29f9 100644 --- a/Source/Core/Common/CommonPaths.h +++ b/Source/Core/Common/CommonPaths.h @@ -94,6 +94,7 @@ #define DYNAMICINPUT_DIR "DynamicInputTextures" #define GRAPHICSMOD_DIR "GraphicMods" #define WIISDSYNC_DIR "WiiSDSync" +#define SUBTITLE_DIR "Subtitles" // This one is only used to remove it if it was present #define SHADERCACHE_LEGACY_DIR "ShaderCache" diff --git a/Source/Core/Common/FileUtil.cpp b/Source/Core/Common/FileUtil.cpp index eb880c51ad01..c29c2bf1bef5 100644 --- a/Source/Core/Common/FileUtil.cpp +++ b/Source/Core/Common/FileUtil.cpp @@ -864,6 +864,7 @@ static void RebuildUserDirectories(unsigned int dir_index) s_user_paths[D_WFSROOT_IDX] = s_user_paths[D_USER_IDX] + WFSROOT_DIR DIR_SEP; s_user_paths[D_BACKUP_IDX] = s_user_paths[D_USER_IDX] + BACKUP_DIR DIR_SEP; s_user_paths[D_RESOURCEPACK_IDX] = s_user_paths[D_USER_IDX] + RESOURCEPACK_DIR DIR_SEP; + s_user_paths[D_SUBTITLES_IDX] = s_user_paths[D_LOAD_IDX] + SUBTITLE_DIR DIR_SEP; s_user_paths[D_DYNAMICINPUT_IDX] = s_user_paths[D_LOAD_IDX] + DYNAMICINPUT_DIR DIR_SEP; s_user_paths[D_GRAPHICSMOD_IDX] = s_user_paths[D_LOAD_IDX] + GRAPHICSMOD_DIR DIR_SEP; s_user_paths[D_WIISDCARDSYNCFOLDER_IDX] = s_user_paths[D_LOAD_IDX] + WIISDSYNC_DIR DIR_SEP; diff --git a/Source/Core/Common/FileUtil.h b/Source/Core/Common/FileUtil.h index 8d5f312d65ab..15302ed95723 100644 --- a/Source/Core/Common/FileUtil.h +++ b/Source/Core/Common/FileUtil.h @@ -62,6 +62,7 @@ enum D_WFSROOT_IDX, D_BACKUP_IDX, D_RESOURCEPACK_IDX, + D_SUBTITLES_IDX, D_DYNAMICINPUT_IDX, D_GRAPHICSMOD_IDX, D_GBAUSER_IDX, diff --git a/Source/Core/Common/Logging/Log.h b/Source/Core/Common/Logging/Log.h index a13e9fee9445..23fb7aef54fd 100644 --- a/Source/Core/Common/Logging/Log.h +++ b/Source/Core/Common/Logging/Log.h @@ -31,6 +31,7 @@ enum class LogType : int DYNA_REC, EXPANSIONINTERFACE, FILEMON, + SUBTITLES, FRAMEDUMP, GDB_STUB, GPFIFO, diff --git a/Source/Core/Common/Logging/LogManager.cpp b/Source/Core/Common/Logging/LogManager.cpp index 749117bc74e3..7d1e5f09f032 100644 --- a/Source/Core/Common/Logging/LogManager.cpp +++ b/Source/Core/Common/Logging/LogManager.cpp @@ -115,6 +115,7 @@ LogManager::LogManager() m_log[LogType::DYNA_REC] = {"JIT", "JIT Dynamic Recompiler"}; m_log[LogType::EXPANSIONINTERFACE] = {"EXI", "Expansion Interface"}; m_log[LogType::FILEMON] = {"FileMon", "File Monitor"}; + m_log[LogType::SUBTITLES] = {"Subtitles", "Subtitles"}; m_log[LogType::FRAMEDUMP] = {"FRAMEDUMP", "FrameDump"}; m_log[LogType::GDB_STUB] = {"GDB_STUB", "GDB Stub"}; m_log[LogType::GPFIFO] = {"GP", "GatherPipe FIFO"}; diff --git a/Source/Core/Core/Boot/Boot.cpp b/Source/Core/Core/Boot/Boot.cpp index 86501bd910e8..8c94fbe5a74d 100644 --- a/Source/Core/Core/Boot/Boot.cpp +++ b/Source/Core/Core/Boot/Boot.cpp @@ -510,6 +510,7 @@ bool CBoot::BootUp(Core::System& system, const Core::CPUThreadGuard& guard, bool operator()(BootParameters::Disc& disc) const { NOTICE_LOG_FMT(BOOT, "Booting from disc: {}", disc.path); + const DiscIO::VolumeDisc* volume = SetDisc(std::move(disc.volume), disc.auto_disc_change_paths); @@ -520,6 +521,7 @@ bool CBoot::BootUp(Core::System& system, const Core::CPUThreadGuard& guard, return false; SConfig::OnNewTitleLoad(guard); + return true; } diff --git a/Source/Core/Core/ConfigManager.cpp b/Source/Core/Core/ConfigManager.cpp index 27b7b2a51bd3..0c49befc179f 100644 --- a/Source/Core/Core/ConfigManager.cpp +++ b/Source/Core/Core/ConfigManager.cpp @@ -60,6 +60,8 @@ #include "DiscIO/Volume.h" #include "DiscIO/VolumeWad.h" +#include + SConfig* SConfig::m_Instance; SConfig::SConfig() @@ -209,6 +211,7 @@ void SConfig::OnNewTitleLoad(const Core::CPUThreadGuard& guard) PatchEngine::Reload(); HiresTexture::Update(); WC24PatchEngine::Reload(); + Subtitles::Reload(); } void SConfig::LoadDefaults() diff --git a/Source/Core/Core/HW/DVD/DVDThread.cpp b/Source/Core/Core/HW/DVD/DVDThread.cpp index d861bba2eb42..c3a07617cc83 100644 --- a/Source/Core/Core/HW/DVD/DVDThread.cpp +++ b/Source/Core/Core/HW/DVD/DVDThread.cpp @@ -34,6 +34,8 @@ #include "DiscIO/Enums.h" #include "DiscIO/Volume.h" +#include "Subtitles/Subtitles.h" + namespace DVD { DVDThread::DVDThread(Core::System& system) : m_system(system) @@ -343,6 +345,7 @@ void DVDThread::DVDThreadMain() while (m_request_queue.Pop(request)) { m_file_logger.Log(*m_disc, request.partition, request.dvd_offset); + Subtitles::OnFileAccess(*m_disc, request.partition, request.dvd_offset); std::vector buffer(request.length); if (!m_disc->Read(request.dvd_offset, request.length, buffer.data(), request.partition)) diff --git a/Source/Core/Core/HW/DVD/FileMonitor.cpp b/Source/Core/Core/HW/DVD/FileMonitor.cpp index f4bc53a09717..93036874ac5b 100644 --- a/Source/Core/Core/HW/DVD/FileMonitor.cpp +++ b/Source/Core/Core/HW/DVD/FileMonitor.cpp @@ -21,7 +21,7 @@ namespace FileMonitor { // Filtered files -static bool IsSoundFile(const std::string& filename) +static bool IsSoundOrVideoFile(const std::string& filename) { std::string extension; SplitPath(filename, nullptr, nullptr, &extension); @@ -41,6 +41,8 @@ static bool IsSoundFile(const std::string& filename) ".song", // Tales of Symphonia ".ssm", // Custom Robo, Kirby Air Ride, etc. ".str", // Harry Potter & the Sorcerer's Stone + + ".thp", // Wii/Game Cube Video File }; return extensions.find(extension) != extensions.end(); @@ -72,15 +74,18 @@ void FileLogger::Log(const DiscIO::Volume& volume, const DiscIO::Partition& part return; const u64 file_offset = file_info->GetOffset(); + const u64 relativeOffset = offset - file_info->GetOffset(); + // TODO add last_log time to keep logging streamed asset offsets without spamming logs? Or another LogType so user can enable nonstop logging? // Do nothing if we found the same file again if (m_previous_partition == partition && m_previous_file_offset == file_offset) return; const std::string size_string = Common::ThousandSeparate(file_info->GetSize() / 1000, 7); const std::string path = file_info->GetPath(); - const std::string log_string = fmt::format("{} kB {}", size_string, path); - if (IsSoundFile(path)) + const std::string log_string = fmt::format("{} kB {} offset {} fileOffset {} relativeOffset {}", size_string, path, offset, file_offset, relativeOffset); + + if (IsSoundOrVideoFile(path)) INFO_LOG_FMT(FILEMON, "{}", log_string); else WARN_LOG_FMT(FILEMON, "{}", log_string); diff --git a/Source/Core/DolphinLib.vcxproj b/Source/Core/DolphinLib.vcxproj index d2071d6618fa..6deef8ace7c7 100644 --- a/Source/Core/DolphinLib.vcxproj +++ b/Source/Core/DolphinLib.vcxproj @@ -1,4 +1,4 @@ - + diff --git a/Source/Core/Subtitles/Helpers.cpp b/Source/Core/Subtitles/Helpers.cpp new file mode 100644 index 000000000000..08bc52defedc --- /dev/null +++ b/Source/Core/Subtitles/Helpers.cpp @@ -0,0 +1,69 @@ +// Copyright 2023 Dolphin Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include "Subtitles/Helpers.h" + +#include + +#include + +#include "Common/CommonTypes.h" +#include "Common/Logging/LogManager.h" +#include "Common/StringUtil.h" +#include "Subtitles/WebColors.h" +#include "VideoCommon/OnScreenDisplay.h" + +namespace Subtitles +{ + +void OSDInfo(std::string msg) +{ + if (Common::Log::LogManager::GetInstance()->IsEnabled(Common::Log::LogType::SUBTITLES, + Common::Log::LogLevel::LWARNING)) + { + OSD::AddMessage(msg, 5000, OSD::Color::GREEN); + } +} + +void Info(std::string msg) +{ + OSD::AddMessage(msg, 5000, OSD::Color::GREEN); + INFO_LOG_FMT(SUBTITLES, "{}", msg); +} + +void Error(std::string err) +{ + OSD::AddMessage(err, 5000, OSD::Color::RED); + ERROR_LOG_FMT(SUBTITLES, "{}", err); +} + +u32 TryParsecolor(const picojson::value& raw, u32 defaultColor) +{ + if (raw.is()) + { + return raw.get(); + } + else + { + auto str = raw.to_str(); + Common::ToLower(&str); + + if (str.starts_with("0x")) + { + u32 parsedHex = 0; + if (TryParse(str, &parsedHex)) + { + return parsedHex; + } + } + else if (WebColors.count(str) == 1) + { + // html color name + return WebColors[str]; + } + } + return defaultColor; +} +} // namespace Subtitles diff --git a/Source/Core/Subtitles/Helpers.h b/Source/Core/Subtitles/Helpers.h new file mode 100644 index 000000000000..c48983001871 --- /dev/null +++ b/Source/Core/Subtitles/Helpers.h @@ -0,0 +1,18 @@ +// Copyright 2023 Dolphin Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include + +#include + +#include "Common/CommonTypes.h" + +namespace Subtitles +{ +void OSDInfo(std::string msg); +void Info(std::string msg); +void Error(std::string err); +u32 TryParsecolor(const picojson::value& raw, u32 defaultColor); +} // namespace Subtitles diff --git a/Source/Core/Subtitles/SubtitleEntry.cpp b/Source/Core/Subtitles/SubtitleEntry.cpp new file mode 100644 index 000000000000..eae6c00459e5 --- /dev/null +++ b/Source/Core/Subtitles/SubtitleEntry.cpp @@ -0,0 +1,131 @@ +// Copyright 2023 Dolphin Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include "Subtitles/SubtitleEntry.h" + +#include +#include +#include + +#include "Common/CommonTypes.h" +#include "Subtitles/Helpers.h" + +namespace Subtitles +{ +// Ensure lines are sorted in reverse to simplify querying +void SubtitleEntryGroup::Preprocess() +{ + for (auto i = 0; i < subtitleLines.size(); i++) + { + hasOffsets |= subtitleLines[i].Offset > 0; + hasTimestamps |= subtitleLines[i].Timestamp > 0; + } + + if (hasOffsets) + { + std::sort(subtitleLines.begin(), subtitleLines.end(), + [](const auto& lhs, const auto& rhs) { return lhs.Offset > rhs.Offset; }); + } + // Offsets override Timestamps + else if (hasTimestamps) + { + std::sort(subtitleLines.begin(), subtitleLines.end(), + [](const auto& lhs, const auto& rhs) { return lhs.Timestamp > rhs.Timestamp; }); + } +} +SubtitleEntry* SubtitleEntryGroup::GetSubtitle(u32 offset) +{ + if (subtitleLines.empty()) + return nullptr; + + if (hasOffsets) + { + return GetSubtitleForRelativeOffset(offset); + } + if (hasTimestamps) + { + if (offset == 0) + { + // restart timer if file is being read from start + timer.Start(); + } + // TODO do sync emulated time with real time, or just ingore this issue? + auto timestamp = timer.ElapsedMs(); + return GetSubtitleForRelativeTimestamp(timestamp); + } + + return &subtitleLines[0]; +} +SubtitleEntry* SubtitleEntryGroup::GetSubtitleForRelativeOffset(u32 offset) +{ + // from latest to earliest + for (auto i = 0; i < subtitleLines.size(); i++) + { + // find first translation that covers current offset + if (offset >= subtitleLines[i].Offset) + { + // if range is open, or offset is in range + if (subtitleLines[i].OffsetEnd == 0 || subtitleLines[i].OffsetEnd >= offset) + { + return &subtitleLines[i]; + } + else + { + return nullptr; + } + } + } + + return nullptr; +} +SubtitleEntry* SubtitleEntryGroup::GetSubtitleForRelativeTimestamp(u64 timestamp) +{ + //if Subttile log is enabled, display timestamp for easier subtitle time aligning + OSDInfo(fmt::format("Timestamp: {}", timestamp)); + + // from latest to earliest + for (auto i = 0; i < subtitleLines.size(); i++) + { + // find first translation that covers current offset + if (timestamp >= subtitleLines[i].Timestamp) + { + // use display time as treshold + auto endstamp = subtitleLines[i].Timestamp + subtitleLines[i].Miliseconds; + if (endstamp >= timestamp) + { + return &subtitleLines[i]; + } + else + { + return nullptr; + } + } + } + + return nullptr; +} +void SubtitleEntryGroup::Add(SubtitleEntry& tl) +{ + subtitleLines.push_back(tl); +} + +SubtitleEntry::SubtitleEntry() + : Filename(""), Text(""), Miliseconds(0), Color(0), Enabled(false), AllowDuplicate(false), + Scale(1), Offset(0), OffsetEnd(0), DisplayOnTop(false), Timestamp(0) +{ +} +SubtitleEntry::SubtitleEntry(std::string& filename, std::string& text, u32 miliseconds, u32 color, + bool enabled, bool allowDuplicates, float scale, u32 offset, + u32 offsetEnd, bool displayOnTop, u64 timestamp) + : Filename(filename), Text(text), Miliseconds(miliseconds), Color(color), Enabled(enabled), + AllowDuplicate(allowDuplicates), Scale(scale), Offset(offset), OffsetEnd(offsetEnd), + DisplayOnTop(displayOnTop), Timestamp(timestamp) +{ +} +bool SubtitleEntry::IsOffset() +{ + return Offset > 0; +} +} // namespace Subtitles diff --git a/Source/Core/Subtitles/SubtitleEntry.h b/Source/Core/Subtitles/SubtitleEntry.h new file mode 100644 index 000000000000..4ec25b8b7c6f --- /dev/null +++ b/Source/Core/Subtitles/SubtitleEntry.h @@ -0,0 +1,56 @@ +// Copyright 2023 Dolphin Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include + +#include "Common/CommonTypes.h" +#include "Common/Timer.h" + +namespace Subtitles +{ + +struct SubtitleEntry +{ + std::string Filename; + std::string Text; + u32 Miliseconds; + u32 Color; + bool Enabled; + bool AllowDuplicate; + float Scale; + u32 Offset; + u32 OffsetEnd; + bool DisplayOnTop; + u64 Timestamp; + +public: + SubtitleEntry(); + SubtitleEntry(std::string& filename, std::string& text, u32 miliseconds, u32 color, bool enabled, + bool allowDuplicates, float scale, u32 offset, u32 offsetEnd, bool displayOnTop, + u64 timestamp); + bool IsOffset(); +}; + +/// +/// Helper struct to deal with multiple subtitle lines per file +/// +struct SubtitleEntryGroup +{ + Common::Timer timer; + + std::vector subtitleLines; + bool hasOffsets = false; + bool hasTimestamps = false; + + // Ensure lines are sorted in reverse to simplify querying + void Preprocess(); + void Add(SubtitleEntry& tl); + SubtitleEntry* GetSubtitle(u32 offset); + +private: + SubtitleEntry* GetSubtitleForRelativeOffset(u32 offset); + SubtitleEntry* GetSubtitleForRelativeTimestamp(u64 timestamp); +}; +} // namespace Subtitles diff --git a/Source/Core/Subtitles/Subtitles.cpp b/Source/Core/Subtitles/Subtitles.cpp new file mode 100644 index 000000000000..66e1b1cf72a5 --- /dev/null +++ b/Source/Core/Subtitles/Subtitles.cpp @@ -0,0 +1,192 @@ +// Copyright 2023 Dolphin Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include "Subtitles/Subtitles.h" + +#include +#include +#include +#include +#include + +#include + +#include "Common/FileUtil.h" +#include "Common/Logging/LogManager.h" +#include "Core/ConfigManager.h" +#include "DiscIO/Filesystem.h" +#include "Subtitles/Helpers.h" +#include "Subtitles/SubtitleEntry.h" +#include "VideoCommon/OnScreenDisplay.h" + +namespace Subtitles +{ +bool g_messageStacksInitialized = false; +bool g_subtitlesInitialized = false; +std::map Translations; + +void DeserializeSubtitlesJson(std::string& filepath) +{ + OSDInfo(fmt::format("Reading translations from: {}", filepath)); + + std::string json; + File::ReadFileToString(filepath, json); + + if (json == "") + return; + + picojson::value v; + std::string err = picojson::parse(v, json); + if (!err.empty()) + { + Error(fmt::format("Subtitle JSON Error: {} in {}", err, filepath)); + return; + } + + if (!v.is()) + { + Error(fmt::format("Subtitle JSON Error: Not an array in {}", filepath)); + return; + } + + auto arr = v.get(); + for (auto item : arr) + { + const auto FileName = item.get("FileName"); + const auto Translation = item.get("Translation"); + const auto Miliseconds = item.get("Miliseconds"); + const auto Color = item.get("Color"); + const auto Enabled = item.get("Enabled"); + const auto AllowDuplicate = item.get("AllowDuplicate"); + const auto Scale = item.get("Scale"); + const auto Offset = item.get("Offset"); + const auto OffsetEnd = item.get("OffsetEnd"); + const auto DisplayOnTop = item.get("DisplayOnTop"); + const auto Timestamp = item.get("Timestamp"); + + // fitler out disabled entries, to lighten lookup load + bool enabled = Enabled.is() ? Enabled.get() : true; + if (!enabled) + continue; + + // FileName and Translation are required fields + if (!FileName.is() || !Translation.is()) + continue; + + const u32 color = TryParsecolor(Color, OSD::Color::CYAN); + + std::string filename = FileName.to_str(); + std::string translation = Translation.to_str(); + + auto tl = SubtitleEntry( + filename, translation, + Miliseconds.is() ? Miliseconds.get() : OSD::Duration::SHORT, color, enabled, + AllowDuplicate.is() ? AllowDuplicate.get() : false, + Scale.is() ? Scale.get() : 1, + Offset.is() ? Offset.get() : 0, + OffsetEnd.is() ? OffsetEnd.get() : 0, + DisplayOnTop.is() ? DisplayOnTop.get() : false, + Timestamp.is() ? Timestamp.get() : 0); + + Translations[tl.Filename].Add(tl); + } +} + +void RecursivelyReadTranslationJsons(const File::FSTEntry& folder, const std::string& filter) +{ + for (const auto& child : folder.children) + { + if (child.isDirectory) + { + RecursivelyReadTranslationJsons(child, filter); + } + else + { + auto filepath = child.physicalName; + std::string extension; + SplitPath(filepath, nullptr, nullptr, &extension); + Common::ToLower(&extension); + + if (extension == filter) + { + DeserializeSubtitlesJson(filepath); + } + } + } +} + +void IniitalizeOSDMessageStacks() +{ + if (g_messageStacksInitialized) + return; + + OSD::AddMessageStack(0, 0, OSD::MessageStackDirection::Upward, true, true, BottomOSDStackName); + + OSD::AddMessageStack(0, 0, OSD::MessageStackDirection::Downward, true, false, TopOSDStackName); + + g_messageStacksInitialized = true; +} + +void LoadSubtitlesForGame(const std::string& gameId) +{ + g_subtitlesInitialized = false; + Translations.clear(); + + auto subtitleDir = File::GetUserPath(D_SUBTITLES_IDX) + gameId; + + OSDInfo(fmt::format("Loading subtitles for {} from {}", gameId, subtitleDir)); + + auto fileEnumerator = File::ScanDirectoryTree(subtitleDir, true); + RecursivelyReadTranslationJsons(fileEnumerator, SubtitleFileExtension); + + if (Translations.empty()) + return; + + // ensure stuff is sorted, you never know what mess people will make in text files :) + std::for_each(Translations.begin(), Translations.end(), + [](std::pair& t) { t.second.Preprocess(); }); + + IniitalizeOSDMessageStacks(); + + g_subtitlesInitialized = true; + Info(fmt::format("Subtitles loaded for {}", gameId)); +} + +void Reload() +{ + LoadSubtitlesForGame(SConfig::GetInstance().GetGameID()); +} + +void OnFileAccess(const DiscIO::Volume& volume, const DiscIO::Partition& partition, u64 offset) +{ + if (!g_subtitlesInitialized) + return; + + const DiscIO::FileSystem* file_system = volume.GetFileSystem(partition); + if (!file_system) + return; + + const std::unique_ptr file_info = file_system->FindFileInfo(offset); + + if (!file_info) + return; + + std::string path = file_info->GetPath(); + + auto relativeOffset = offset - file_info->GetOffset(); + + if (Translations.count(path) == 0) + return; + + auto tl = Translations[path].GetSubtitle((u32)relativeOffset); + + if (!tl) + return; + + OSD::AddMessage(tl->Text, tl->Miliseconds, tl->Color, nullptr, + tl->DisplayOnTop ? TopOSDStackName : BottomOSDStackName, !tl->AllowDuplicate, + tl->Scale); +} +} // namespace Subtitles diff --git a/Source/Core/Subtitles/Subtitles.h b/Source/Core/Subtitles/Subtitles.h new file mode 100644 index 000000000000..2e5e4fd5d49c --- /dev/null +++ b/Source/Core/Subtitles/Subtitles.h @@ -0,0 +1,17 @@ +// Copyright 2023 Dolphin Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include + +#include "DiscIO/Filesystem.h" + +namespace Subtitles +{ +const std::string SubtitleFileExtension = ".json"; +const std::string BottomOSDStackName = "subtitles-bottom"; +const std::string TopOSDStackName = "subtitles-top"; +void Reload(); +void OnFileAccess(const DiscIO::Volume& volume, const DiscIO::Partition& partition, u64 offset); +} // namespace Subtitles diff --git a/Source/Core/Subtitles/WebColors.h b/Source/Core/Subtitles/WebColors.h new file mode 100644 index 000000000000..26bd67cceb96 --- /dev/null +++ b/Source/Core/Subtitles/WebColors.h @@ -0,0 +1,156 @@ +// Copyright 2023 Dolphin Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include + +#include "Common/CommonTypes.h" + +namespace Subtitles +{ +// web colors in hex form with Alpha = 255 +std::map WebColors = { + {"mediumvioletred", 0xFFC71585}, + {"deeppink", 0xFFFF1493}, + {"palevioletred", 0xFFDB7093}, + {"hotpink", 0xFFFF69B4}, + {"lightpink", 0xFFFFB6C1}, + {"pink", 0xFFFFC0CB}, + {"darkred", 0xFF8B0000}, + {"red", 0xFFFF0000}, + {"firebrick", 0xFFB22222}, + {"crimson", 0xFFDC143C}, + {"indianred", 0xFFCD5C5C}, + {"lightcoral", 0xFFF08080}, + {"salmon", 0xFFFA8072}, + {"darksalmon", 0xFFE9967A}, + {"lightsalmon", 0xFFFFA07A}, + {"orangered", 0xFFFF4500}, + {"tomato", 0xFFFF6347}, + {"darkorange", 0xFFFF8C00}, + {"coral", 0xFFFF7F50}, + {"orange", 0xFFFFA500}, + {"darkkhaki", 0xFFBDB76B}, + {"gold", 0xFFFFD700}, + {"khaki", 0xFFF0E68C}, + {"peachpuff", 0xFFFFDAB9}, + {"yellow", 0xFFFFFF00}, + {"palegoldenrod", 0xFFEEE8AA}, + {"moccasin", 0xFFFFE4B5}, + {"papayawhip", 0xFFFFEFD5}, + {"lightgoldenrodyellow", 0xFFFAFAD2}, + {"lemonchiffon", 0xFFFFFACD}, + {"lightyellow", 0xFFFFFFE0}, + {"maroon", 0xFF800000}, + {"brown", 0xFFA52A2A}, + {"saddlebrown", 0xFF8B4513}, + {"sienna", 0xFFA0522D}, + {"chocolate", 0xFFD2691E}, + {"darkgoldenrod", 0xFFB8860B}, + {"peru", 0xFFCD853F}, + {"rosybrown", 0xFFBC8F8F}, + {"goldenrod", 0xFFDAA520}, + {"sandybrown", 0xFFF4A460}, + {"tan", 0xFFD2B48C}, + {"burlywood", 0xFFDEB887}, + {"wheat", 0xFFF5DEB3}, + {"navajowhite", 0xFFFFDEAD}, + {"bisque", 0xFFFFE4C4}, + {"blanchedalmond", 0xFFFFEBCD}, + {"cornsilk", 0xFFFFF8DC}, + {"indigo", 0xFF4B0082}, + {"purple", 0xFF800080}, + {"darkmagenta", 0xFF8B008B}, + {"darkviolet", 0xFF9400D3}, + {"darkslateblue", 0xFF483D8B}, + {"blueviolet", 0xFF8A2BE2}, + {"darkorchid", 0xFF9932CC}, + {"fuchsia", 0xFFFF00FF}, + {"magenta", 0xFFFF00FF}, + {"slateblue", 0xFF6A5ACD}, + {"mediumslateblue", 0xFF7B68EE}, + {"mediumorchid", 0xFFBA55D3}, + {"mediumpurple", 0xFF9370DB}, + {"orchid", 0xFFDA70D6}, + {"violet", 0xFFEE82EE}, + {"plum", 0xFFDDA0DD}, + {"thistle", 0xFFD8BFD8}, + {"lavender", 0xFFE6E6FA}, + {"midnightblue", 0xFF191970}, + {"navy", 0xFF000080}, + {"darkblue", 0xFF00008B}, + {"mediumblue", 0xFF0000CD}, + {"blue", 0xFF0000FF}, + {"royalblue", 0xFF041690}, + {"steelblue", 0xFF4682B4}, + {"dodgerblue", 0xFF1E90FF}, + {"deepskyblue", 0xFF00BFFF}, + {"cornflowerblue", 0xFF6495ED}, + {"skyblue", 0xFF87CEEB}, + {"lightskyblue", 0xFF87CEFA}, + {"lightsteelblue", 0xFFB0C4DE}, + {"lightblue", 0xFFADD8E6}, + {"powderblue", 0xFFB0E0E6}, + {"teal", 0xFF008080}, + {"darkcyan", 0xFF008B8B}, + {"lightseagreen", 0xFF20B2AA}, + {"cadetblue", 0xFF5F9EA0}, + {"darkturquoise", 0xFF00CED1}, + {"mediumturquoise", 0xFF48D1CC}, + {"turquoise", 0xFF40E0D0}, + {"aqua", 0xFF00FFFF}, + {"cyan", 0xFF00FFFF}, + {"aquamarine", 0xFF7FFFD4}, + {"paleturquoise", 0xFFAFEEEE}, + {"lightcyan", 0xFFE0FFFF}, + {"darkgreen", 0xFF006400}, + {"green", 0xFF008000}, + {"darkolivegreen", 0xFF556B2F}, + {"forestgreen", 0xFF228B22}, + {"seagreen", 0xFF2E8B57}, + {"olive", 0xFF808000}, + {"olivedrab", 0xFF6B8E23}, + {"mediumseagreen", 0xFF3CB371}, + {"limegreen", 0xFF32CD32}, + {"lime", 0xFF00FF00}, + {"springgreen", 0xFF00FF7F}, + {"mediumspringgreen", 0xFF00FA9A}, + {"darkseagreen", 0xFF8FBC8F}, + {"mediumaquamarine", 0xFF66CDAA}, + {"yellowgreen", 0xFF9ACD32}, + {"lawngreen", 0xFF7CFC00}, + {"chartreuse", 0xFF7FFF00}, + {"lightgreen", 0xFF90EE90}, + {"greenyellow", 0xFFADFF2F}, + {"palegreen", 0xFF98FB98}, + {"mistyrose", 0xFFFFE4E1}, + {"antiquewhite", 0xFFFAEBD7}, + {"linen", 0xFFFAF0E6}, + {"beige", 0xFFF5F5DC}, + {"whitesmoke", 0xFFF5F5F5}, + {"lavenderblush", 0xFFFFF0F5}, + {"oldlace", 0xFFFDF5E6}, + {"aliceblue", 0xFFF0F8FF}, + {"seashell", 0xFFFFF5EE}, + {"ghostwhite", 0xFFF8F8FF}, + {"honeydew", 0xFFF0FFF0}, + {"floralwhite", 0xFFFFFAF0}, + {"azure", 0xFFF0FFFF}, + {"mintcream", 0xFFF5FFFA}, + {"snow", 0xFFFFFAFA}, + {"ivory", 0xFFFFFFF0}, + {"white", 0xFFFFFFFF}, + {"black", 0xFF000000}, + {"darkslategray", 0xFF2F4F4F}, + {"dimgray", 0xFF696969}, + {"slategray", 0xFF708090}, + {"gray", 0xFF808080}, + {"lightslategray", 0xFF778899}, + {"darkgray", 0xFFA9A9A9}, + {"silver", 0xFFC0C0C0}, + {"lightgray", 0xFFD3D3D3}, + {"gainsboro", 0xFFDCDCDC}, +}; +} // namespace Subtitles diff --git a/Source/Core/UICommon/UICommon.cpp b/Source/Core/UICommon/UICommon.cpp index 9b31ad5bf9cc..7c4f5142d088 100644 --- a/Source/Core/UICommon/UICommon.cpp +++ b/Source/Core/UICommon/UICommon.cpp @@ -84,6 +84,7 @@ static void CreateLoadPath(std::string path) File::CreateFullPath(File::GetUserPath(D_RIIVOLUTION_IDX)); File::CreateFullPath(File::GetUserPath(D_GRAPHICSMOD_IDX)); File::CreateFullPath(File::GetUserPath(D_DYNAMICINPUT_IDX)); + File::CreateFullPath(File::GetUserPath(D_SUBTITLES_IDX)); } static void CreateResourcePackPath(std::string path) diff --git a/Source/Core/VideoCommon/OnScreenDisplay.cpp b/Source/Core/VideoCommon/OnScreenDisplay.cpp index 0e07dbc3974c..d0ed4377148b 100644 --- a/Source/Core/VideoCommon/OnScreenDisplay.cpp +++ b/Source/Core/VideoCommon/OnScreenDisplay.cpp @@ -36,8 +36,10 @@ static std::atomic s_obscured_pixels_top = 0; struct Message { Message() = default; - Message(std::string text_, u32 duration_, u32 color_, std::unique_ptr icon_ = nullptr) - : text(std::move(text_)), duration(duration_), color(color_), icon(std::move(icon_)) + Message(std::string text_, u32 duration_, u32 color_, std::unique_ptr icon_ = nullptr, + float scale_ = 1) + : text(std::move(text_)), duration(duration_), color(color_), icon(std::move(icon_)), + scale(scale_) { timer.Start(); } @@ -50,8 +52,48 @@ struct Message u32 color = 0; std::unique_ptr icon; std::unique_ptr texture; + float scale = 1; }; -static std::multimap s_messages; + +struct OSDMessageStack +{ + ImVec2 initialPosOffset; + MessageStackDirection dir; + bool centered; + bool reversed; + std::string name; + std::multimap messages; + + OSDMessageStack() : OSDMessageStack(0, 0, MessageStackDirection::Downward, false, false, "") {} + OSDMessageStack(float x_offset, float y_offset, MessageStackDirection dir, bool centered, + bool reversed, std::string name) + : dir(dir), centered(centered), reversed(reversed), name(name) + { + initialPosOffset = ImVec2(x_offset, y_offset); + } + + bool IsVertical() + { + return dir == MessageStackDirection::Downward || dir == MessageStackDirection::Upward; + } + + bool HasMessage(std::string message, MessageType type = OSD::MessageType::Typeless) + { + for (auto it = messages.begin(); it != messages.end(); ++it) + { + if (type == it->first && message == it->second.text) + { + return true; + } + } + return false; + } +}; + +// default message stack +static OSDMessageStack s_defaultMessageStack = OSDMessageStack(); +static std::map messageStacks; + static std::mutex s_messages_mutex; static ImVec4 ARGBToImVec4(const u32 argb) @@ -62,14 +104,14 @@ static ImVec4 ARGBToImVec4(const u32 argb) static_cast((argb >> 24) & 0xFF) / 255.0f); } -static float DrawMessage(int index, Message& msg, const ImVec2& position, int time_left) +static ImVec2 DrawMessage(int index, Message& msg, const ImVec2& position, int time_left, + OSDMessageStack& message_Stack) { // We have to provide a window name, and these shouldn't be duplicated. // So instead, we generate a name based on the number of messages drawn. - const std::string window_name = fmt::format("osd_{}", index); + const std::string window_name = fmt::format("osd_{}_{}", message_Stack.name, index); // The size must be reset, otherwise the length of old messages could influence new ones. - ImGui::SetNextWindowPos(position); ImGui::SetNextWindowSize(ImVec2(0.0f, 0.0f)); // Gradually fade old messages away (except in their first frame) @@ -78,6 +120,7 @@ static float DrawMessage(int index, Message& msg, const ImVec2& position, int ti ImGui::PushStyleVar(ImGuiStyleVar_Alpha, msg.ever_drawn ? alpha : 1.0); float window_height = 0.0f; + float window_width = 0.0f; if (ImGui::Begin(window_name.c_str(), nullptr, ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoInputs | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoSavedSettings | @@ -111,10 +154,43 @@ static float DrawMessage(int index, Message& msg, const ImVec2& position, int ti } } + //TODO fractional scaling based on viewport size instead of screen pixels? + ImGui::SetWindowFontScale(msg.scale); // Use %s in case message contains %. ImGui::TextColored(ARGBToImVec4(msg.color), "%s", msg.text.c_str()); + window_width = + ImGui::GetWindowSize().x + (WINDOW_PADDING * ImGui::GetIO().DisplayFramebufferScale.x); window_height = ImGui::GetWindowSize().y + (WINDOW_PADDING * ImGui::GetIO().DisplayFramebufferScale.y); + + float x_pos = position.x; + float y_pos = position.y; + + if (message_Stack.centered) + { + if (message_Stack.IsVertical()) + { + const float x_center = ImGui::GetIO().DisplaySize.x / 2.0; + x_pos = x_center - window_width / 2; + } + else + { + const float y_center = ImGui::GetIO().DisplaySize.y / 2.0; + y_pos = y_center - window_height / 2; + } + } + + if (message_Stack.dir == MessageStackDirection::Leftward) + { + x_pos -= window_width; + } + if (message_Stack.dir == MessageStackDirection::Upward) + { + y_pos -= window_height; + } + + const auto windowPos = ImVec2(x_pos, y_pos); + ImGui::SetWindowPos(window_name.c_str(), windowPos); } ImGui::End(); @@ -122,46 +198,81 @@ static float DrawMessage(int index, Message& msg, const ImVec2& position, int ti msg.ever_drawn = true; - return window_height; + return ImVec2(window_width, window_height); } -void AddTypedMessage(MessageType type, std::string message, u32 ms, u32 argb, - std::unique_ptr icon) +void AddTypedMessage(MessageType type, std::string message, u32 ms, u32 argb, std::unique_ptr icon, + std::string message_stack, bool prevent_duplicate, float scale) { std::lock_guard lock{s_messages_mutex}; - // A message may hold a reference to a texture that can only be destroyed on the video thread, so - // only mark the old typed message (if any) for removal. It will be discarded on the next call to - // DrawMessages(). - auto range = s_messages.equal_range(type); - for (auto it = range.first; it != range.second; ++it) - it->second.should_discard = true; - - s_messages.emplace(type, Message(std::move(message), ms, argb, std::move(icon))); + OSDMessageStack* stack = &s_defaultMessageStack; + if (messageStacks.contains(message_stack)) + { + stack = &messageStacks[message_stack]; + } + + if (prevent_duplicate && stack->HasMessage(message, type)) + { + return; + } + if (type != MessageType::Typeless) + { + // A message may hold a reference to a texture that can only be destroyed on the video thread, so + // only mark the old typed message (if any) for removal. It will be discarded on the next call to + // DrawMessages(). + auto range = stack->messages.equal_range(type); + for (auto it = range.first; it != range.second; ++it) + it->second.should_discard = true; + } + stack->messages.emplace(type, Message(std::move(message), ms, argb, std::move(icon), scale)); } -void AddMessage(std::string message, u32 ms, u32 argb, std::unique_ptr icon) +void AddMessage(std::string message, u32 ms, u32 argb, std::unique_ptr icon, std::string message_stack, bool prevent_duplicate, float scale) { - std::lock_guard lock{s_messages_mutex}; - s_messages.emplace(MessageType::Typeless, Message(std::move(message), ms, argb, std::move(icon))); + AddTypedMessage(MessageType::Typeless, message, ms, argb, std::move(icon), message_stack, + prevent_duplicate, + scale); } -void DrawMessages() +void AddMessageStack(float x_offset, float y_offset, MessageStackDirection dir, bool centered, + bool reversed, std::string name) +{ + messageStacks.emplace(name, OSDMessageStack(x_offset, y_offset, dir, centered, reversed, name)); +} +void DrawMessages(OSDMessageStack& messageStack) { const bool draw_messages = Config::Get(Config::MAIN_OSD_MESSAGES); - const float current_x = - LEFT_MARGIN * ImGui::GetIO().DisplayFramebufferScale.x + s_obscured_pixels_left; - float current_y = TOP_MARGIN * ImGui::GetIO().DisplayFramebufferScale.y + s_obscured_pixels_top; + float current_x = LEFT_MARGIN * ImGui::GetIO().DisplayFramebufferScale.x + + s_obscured_pixels_left + messageStack.initialPosOffset.x; + float current_y = TOP_MARGIN * ImGui::GetIO().DisplayFramebufferScale.y + s_obscured_pixels_top + + messageStack.initialPosOffset.y; int index = 0; - std::lock_guard lock{s_messages_mutex}; + if (messageStack.dir == MessageStackDirection::Leftward) + { + current_x = ImGui::GetIO().DisplaySize.x - current_x; + } + if (messageStack.dir == MessageStackDirection::Upward) + { + current_y = ImGui::GetIO().DisplaySize.y - current_y; + } - for (auto it = s_messages.begin(); it != s_messages.end();) + std::lock_guard lock{s_messages_mutex}; + for (auto it = (messageStack.reversed ? messageStack.messages.end() : + messageStack.messages.begin()); + it != + (messageStack.reversed ? messageStack.messages.begin() : messageStack.messages.end());) { + if (messageStack.reversed) + { + --it; + } + Message& msg = it->second; if (msg.should_discard) { - it = s_messages.erase(it); + it = messageStack.messages.erase(it); continue; } @@ -171,23 +282,49 @@ void DrawMessages() // unless enough time has expired, in that case, we drop them if (time_left <= 0 && (msg.ever_drawn || -time_left >= MESSAGE_DROP_TIME)) { - it = s_messages.erase(it); + it = messageStack.messages.erase(it); continue; } - else + else if (!messageStack.reversed) { ++it; } if (draw_messages) - current_y += DrawMessage(index++, msg, ImVec2(current_x, current_y), time_left); + { + const auto messageSize = + DrawMessage(index++, msg, ImVec2(current_x, current_y), time_left, messageStack); + + if (messageStack.IsVertical()) + { + current_y += + messageStack.dir == OSD::MessageStackDirection::Upward ? -messageSize.y : messageSize.y; + } + else + { + current_x += messageStack.dir == OSD::MessageStackDirection::Leftward ? -messageSize.x : + messageSize.x; + } + } + } +} +void DrawMessages() +{ + DrawMessages(s_defaultMessageStack); + for (auto& [name, stack] : messageStacks) + { + DrawMessages(stack); } } void ClearMessages() { std::lock_guard lock{s_messages_mutex}; - s_messages.clear(); + s_defaultMessageStack.messages.clear(); + for (auto& [name, stack] : messageStacks) + { + stack.messages.clear(); + } } void SetObscuredPixelsLeft(int width) diff --git a/Source/Core/VideoCommon/OnScreenDisplay.h b/Source/Core/VideoCommon/OnScreenDisplay.h index f566eb0bf129..a442162f4f05 100644 --- a/Source/Core/VideoCommon/OnScreenDisplay.h +++ b/Source/Core/VideoCommon/OnScreenDisplay.h @@ -4,14 +4,28 @@ #pragma once #include +#include #include #include #include +#include + #include "Common/CommonTypes.h" +#include "Common/Timer.h" + +#include "VideoCommon/AbstractTexture.h" namespace OSD { +enum class MessageStackDirection +{ + Downward = 1, + Upward = 2, + Rightward = 4, + Leftward = 8, +}; + enum class MessageType { NetPlayPing, @@ -44,11 +58,17 @@ struct Icon u32 height = 0; }; // struct Icon +void AddMessageStack(float x_offset, float y_offset, MessageStackDirection dir, bool centered, + bool reversed, std::string name); + // On-screen message display (colored yellow by default) void AddMessage(std::string message, u32 ms = Duration::SHORT, u32 argb = Color::YELLOW, - std::unique_ptr icon = nullptr); + std::unique_ptr icon = nullptr, std::string message_stack = "", + bool prevent_duplicate = false, float scale = 1); void AddTypedMessage(MessageType type, std::string message, u32 ms = Duration::SHORT, - u32 argb = Color::YELLOW, std::unique_ptr icon = nullptr); + u32 argb = Color::YELLOW, std::unique_ptr icon = nullptr, + std::string message_stack = "", bool prevent_duplicate = false, + float scale = 1); // Draw the current messages on the screen. Only call once per frame. void DrawMessages();