diff --git a/Source/DiabloUI/credits.cpp b/Source/DiabloUI/credits.cpp index 7ac898cb5..75483f647 100644 --- a/Source/DiabloUI/credits.cpp +++ b/Source/DiabloUI/credits.cpp @@ -27,6 +27,7 @@ #include "engine/render/text_render.hpp" #include "engine/surface.hpp" #include "hwcursor.hpp" +#include "movie.h" #include "utils/display.h" #include "utils/is_of.hpp" #include "utils/language.h" @@ -186,10 +187,8 @@ bool TextDialog(const char *const *text, std::size_t textLines) bool UiCreditsDialog() { - ArtBackgroundWidescreen = LoadOptionalClx("ui_art\\creditsw.clx"); - LoadBackgroundArt("ui_art\\credits"); - - return TextDialog(CreditLines, CreditLinesSize); + play_movie("gendata\\diabend.smk", true); + return true; } bool UiSupportDialog() diff --git a/Source/storm/storm_svid.cpp b/Source/storm/storm_svid.cpp index 119fe1bd4..fa5efb1bb 100644 --- a/Source/storm/storm_svid.cpp +++ b/Source/storm/storm_svid.cpp @@ -42,7 +42,6 @@ #include "utils/srt_parser.hpp" namespace devilution { -namespace { #ifndef NOSOUND #ifdef USE_SDL3 @@ -88,6 +87,10 @@ uint64_t SVidStartTime; // Subtitle entries for current video std::vector SVidSubtitles; +// Subtitle overlay surface and palette for rendering text +SDLSurfaceUniquePtr SVidSubtitleSurface; +SDLPaletteUniquePtr SVidSubtitlePalette; + bool IsLandscapeFit(unsigned long srcW, unsigned long srcH, unsigned long dstW, unsigned long dstH) { return srcW * dstH > dstW * srcH; @@ -229,6 +232,115 @@ void UpdatePalette() bool BlitFrame() { + // Render subtitles if available - do this BEFORE blitting to output + if (!SVidSubtitles.empty()) { + const uint64_t currentTimeSmk = GetTicksSmk(); + const uint64_t videoTimeMs = TimeSmkToMs(currentTimeSmk - SVidStartTime); + const std::string subtitleText = GetSubtitleAtTime(SVidSubtitles, videoTimeMs); + + if (!subtitleText.empty()) { + LogVerbose(LogCategory::Video, "Rendering subtitle at {}ms: \"{}\"", videoTimeMs, subtitleText); + + SDL_Surface *videoSurface = SVidSurface.get(); + + if (videoSurface != nullptr && SDLC_SURFACE_BITSPERPIXEL(videoSurface) == 8) { + const int videoWidth = static_cast(SVidWidth); + const int videoHeight = static_cast(SVidHeight); + + // Create subtitle overlay surface if not already created + if (SVidSubtitleSurface == nullptr) { + constexpr int SubtitleMaxHeight = 100; + SVidSubtitleSurface = SDLWrap::CreateRGBSurface( + 0, videoWidth, SubtitleMaxHeight, 8, 0, 0, 0, 0); + + // Create and set up palette for subtitle surface - copy from video surface + SVidSubtitlePalette = SDLWrap::AllocPalette(); +#ifdef USE_SDL3 + const SDL_Palette *videoPalette = SDL_GetSurfacePalette(videoSurface); +#else + const SDL_Palette *videoPalette = videoSurface->format->palette; +#endif + if (videoPalette != nullptr) { + // Copy the video surface's palette so text colors map correctly + SDL_Color *colors = SVidSubtitlePalette->colors; + for (int i = 0; i < 256 && i < videoPalette->ncolors; i++) { + colors[i] = videoPalette->colors[i]; + } + // Ensure index 0 is black/transparent for color key + colors[0].r = 0; + colors[0].g = 0; + colors[0].b = 0; + } else { + // Fallback: initialize palette manually + SDL_Color *colors = SVidSubtitlePalette->colors; + colors[0].r = 0; + colors[0].g = 0; + colors[0].b = 0; + for (int i = 1; i < 256; i++) { + colors[i].r = 255; + colors[i].g = 255; + colors[i].b = 255; + } + } +#ifndef USE_SDL1 + for (int i = 0; i < 256; i++) { + SVidSubtitlePalette->colors[i].a = SDL_ALPHA_OPAQUE; + } +#endif + + if (!SDLC_SetSurfacePalette(SVidSubtitleSurface.get(), SVidSubtitlePalette.get())) { + Log("Failed to set subtitle overlay palette"); + } + + // Set color key for transparency (index 0 = transparent) +#ifdef USE_SDL1 + SDL_SetColorKey(SVidSubtitleSurface.get(), SDL_SRCCOLORKEY, 0); +#else + if (!SDL_SetSurfaceColorKey(SVidSubtitleSurface.get(), true, 0)) { + Log("Failed to set color key: {}", SDL_GetError()); + } +#endif + } + + // Clear the overlay surface (fill with transparent color) + SDL_FillSurfaceRect(SVidSubtitleSurface.get(), nullptr, 0); + + // Render text to the overlay surface + Surface overlaySurface(SVidSubtitleSurface.get()); + constexpr int SubtitleMaxHeight = 100; + constexpr int SubtitleBottomPadding = 12; + // Position text rectangle at bottom of overlay (FontSize12 has line height ~12) + // Position y so text appears near bottom of overlay + constexpr int TextLineHeight = 12; + const int textY = SubtitleMaxHeight - TextLineHeight - SubtitleBottomPadding; + Rectangle subtitleRect { { 10, textY }, { videoWidth - 20, TextLineHeight + SubtitleBottomPadding } }; + + TextRenderOptions opts; + opts.flags = UiFlags::AlignCenter | UiFlags::ColorWhite | UiFlags::FontSize12; + opts.spacing = 1; + DrawString(overlaySurface, subtitleText, subtitleRect, opts); + + // Blit the overlay onto the video surface at the bottom + SDL_Rect dstRect; + dstRect.x = 0; + // Position overlay at the very bottom of the video, with small padding + dstRect.y = videoHeight - SubtitleMaxHeight - SubtitleBottomPadding; + dstRect.w = videoWidth; + dstRect.h = 100; + +#ifdef USE_SDL3 + if (!SDL_BlitSurface(SVidSubtitleSurface.get(), nullptr, videoSurface, &dstRect)) { + Log("Failed to blit subtitle overlay: {}", SDL_GetError()); + } +#else + if (SDL_BlitSurface(SVidSubtitleSurface.get(), nullptr, videoSurface, &dstRect) < 0) { + Log("Failed to blit subtitle overlay: {}", SDL_GetError()); + } +#endif + } + } + } + #ifndef USE_SDL1 if (renderer != nullptr) { if ( @@ -304,71 +416,6 @@ bool BlitFrame() } } - // Render subtitles if available - if (!SVidSubtitles.empty()) { - const uint64_t currentTimeSmk = GetTicksSmk(); - const uint64_t videoTimeMs = TimeSmkToMs(currentTimeSmk - SVidStartTime); - const std::string subtitleText = GetSubtitleAtTime(SVidSubtitles, videoTimeMs); - - if (!subtitleText.empty()) { - SDL_Surface *outputSurface = GetOutputSurface(); - SDL_Rect outputRect; -#ifndef USE_SDL1 - if (renderer != nullptr) { - // When using renderer, video fills the entire output surface - outputRect.w = outputSurface->w; - outputRect.h = outputSurface->h; - outputRect.x = 0; - outputRect.y = 0; - } else -#endif - { - // Calculate video rect (same logic as above) -#ifdef USE_SDL1 - const bool isIndexedOutputFormat = SDLBackport_IsPixelFormatIndexed(outputSurface->format); -#else -#ifdef USE_SDL3 - const SDL_PixelFormat wndFormat = SDL_GetWindowPixelFormat(ghMainWnd); -#else - const Uint32 wndFormat = SDL_GetWindowPixelFormat(ghMainWnd); -#endif - const bool isIndexedOutputFormat = SDL_ISPIXELFORMAT_INDEXED(wndFormat); -#endif - if (isIndexedOutputFormat) { - outputRect.w = static_cast(SVidWidth); - outputRect.h = static_cast(SVidHeight); - } else if (IsLandscapeFit(SVidWidth, SVidHeight, outputSurface->w, outputSurface->h)) { - outputRect.w = outputSurface->w; - outputRect.h = SVidHeight * outputSurface->w / SVidWidth; - } else { - outputRect.w = SVidWidth * outputSurface->h / SVidHeight; - outputRect.h = outputSurface->h; - } - outputRect.x = (outputSurface->w - outputRect.w) / 2; - outputRect.y = (outputSurface->h - outputRect.h) / 2; - } - - // Calculate subtitle position (bottom center, with some padding) - constexpr int SubtitlePadding = 20; - const int subtitleY = outputRect.y + outputRect.h - SubtitlePadding; - const int subtitleX = outputRect.x; - const int subtitleWidth = outputRect.w; - - // Create a surface for rendering text - Surface outSurface(outputSurface); - // Allow enough height for multiple lines of text - constexpr int SubtitleMaxHeight = 120; - const int subtitleRectY = std::max(0, subtitleY - SubtitleMaxHeight); - Rectangle subtitleRect { { subtitleX, subtitleRectY }, { subtitleWidth, SubtitleMaxHeight } }; - - // Render subtitle with white text, centered, and outlined for visibility - TextRenderOptions opts; - opts.flags = UiFlags::AlignCenter | UiFlags::ColorWhite | UiFlags::Outlined; - opts.spacing = 1; - DrawString(outSurface, subtitleText, subtitleRect, opts); - } - } - RenderPresent(); return true; } @@ -413,11 +460,17 @@ void LoadSubtitles(const char *videoFilename) const size_t extPos = subtitlePath.rfind('.'); subtitlePath = (extPos != std::string::npos ? subtitlePath.substr(0, extPos) : subtitlePath) + ".srt"; + Log("Loading subtitles from: {}", subtitlePath); SVidSubtitles = LoadSrtFile(subtitlePath); + Log("Loaded {} subtitle entries", SVidSubtitles.size()); + if (!SVidSubtitles.empty()) { + Log("First subtitle: {}ms-{}ms: \"{}\"", + SVidSubtitles[0].startTimeMs, + SVidSubtitles[0].endTimeMs, + SVidSubtitles[0].text); + } } -} // namespace - bool SVidPlayBegin(const char *filename, int flags) { if ((flags & 0x10000) != 0 || (flags & 0x20000000) != 0) { @@ -617,6 +670,8 @@ void SVidPlayEnd() SVidSurface = nullptr; SVidFrameBuffer = nullptr; SVidSubtitles.clear(); + SVidSubtitleSurface = nullptr; + SVidSubtitlePalette = nullptr; #ifndef USE_SDL1 if (renderer != nullptr) { diff --git a/Source/utils/srt_parser.cpp b/Source/utils/srt_parser.cpp index 148cde835..c4a4ae998 100644 --- a/Source/utils/srt_parser.cpp +++ b/Source/utils/srt_parser.cpp @@ -1,98 +1,101 @@ -#include "utils/srt_parser.hpp" - -#include -#include -#include -#include -#include - -#include "engine/assets.hpp" -#include "utils/language.h" - -namespace devilution { - -uint64_t ParseSrtTimestamp(std::string_view timestamp) -{ - unsigned long long h = 0, m = 0, s = 0, ms = 0; - if (sscanf(timestamp.data(), "%llu:%llu:%llu,%llu", &h, &m, &s, &ms) == 4 - || sscanf(timestamp.data(), "%llu:%llu:%llu.%llu", &h, &m, &s, &ms) == 4) { - return static_cast(h * 3600000ULL + m * 60000ULL + s * 1000ULL + ms); - } - return 0; -} - -std::vector LoadSrtFile(std::string_view subtitlePath) -{ - std::vector subtitles; - - std::string pathStr(subtitlePath); - auto assetData = LoadAsset(pathStr); - if (!assetData.has_value()) - return subtitles; - - std::string content(assetData->data.get(), assetData->size); - std::istringstream stream(content); - std::string line, text; - uint64_t startTime = 0, endTime = 0; - - while (std::getline(stream, line)) { - // Remove \r if present - if (!line.empty() && line.back() == '\r') - line.pop_back(); - - // Skip empty lines (end of subtitle block) - if (line.empty()) { - if (!text.empty() && startTime < endTime) { - // Remove trailing newline from text - if (!text.empty() && text.back() == '\n') - text.pop_back(); - subtitles.push_back({ startTime, endTime, text }); - text.clear(); - } - continue; - } - - // Check if line is a number (subtitle index) - skip it - if (std::all_of(line.begin(), line.end(), [](char c) { return std::isdigit(static_cast(c)); })) - continue; - - // Check if line contains --> (timestamp line) - const size_t arrowPos = line.find("-->"); - if (arrowPos != std::string::npos) { - const std::string startStr = line.substr(0, arrowPos); - const std::string endStr = line.substr(arrowPos + 3); - startTime = ParseSrtTimestamp(startStr); - endTime = ParseSrtTimestamp(endStr); - continue; - } - - // Otherwise it's subtitle text - if (!text.empty()) - text += "\n"; - text += line; - } - - // Handle last subtitle if file doesn't end with blank line - if (!text.empty() && startTime < endTime) { - if (!text.empty() && text.back() == '\n') - text.pop_back(); - subtitles.push_back({ startTime, endTime, text }); - } - - return subtitles; -} - -std::string GetSubtitleAtTime(const std::vector &subtitles, uint64_t videoTimeMs) -{ - for (const auto &entry : subtitles) { - if (videoTimeMs >= entry.startTimeMs && videoTimeMs < entry.endTimeMs) { - // Translate the subtitle text - std::string_view translated = LanguageTranslate(entry.text); - return std::string(translated); - } - } - return ""; -} - -} // namespace devilution - +#include "utils/srt_parser.hpp" + +#include +#include +#include +#include +#include + +#include "engine/assets.hpp" +#include "utils/language.h" +#include "utils/log.hpp" + +namespace devilution { + +uint64_t ParseSrtTimestamp(std::string_view timestamp) +{ + unsigned long long h = 0, m = 0, s = 0, ms = 0; + if (sscanf(timestamp.data(), "%llu:%llu:%llu,%llu", &h, &m, &s, &ms) == 4 + || sscanf(timestamp.data(), "%llu:%llu:%llu.%llu", &h, &m, &s, &ms) == 4) { + return static_cast(h * 3600000ULL + m * 60000ULL + s * 1000ULL + ms); + } + return 0; +} + +std::vector LoadSrtFile(std::string_view subtitlePath) +{ + std::vector subtitles; + + std::string pathStr(subtitlePath); + auto assetData = LoadAsset(pathStr); + if (!assetData.has_value()) { + LogError("Subtitle file not found: {} ({})", subtitlePath, assetData.error()); + return subtitles; + } + + std::string content(assetData->data.get(), assetData->size); + std::istringstream stream(content); + std::string line, text; + uint64_t startTime = 0, endTime = 0; + + while (std::getline(stream, line)) { + // Remove \r if present + if (!line.empty() && line.back() == '\r') + line.pop_back(); + + // Skip empty lines (end of subtitle block) + if (line.empty()) { + if (!text.empty() && startTime < endTime) { + // Remove trailing newline from text + if (!text.empty() && text.back() == '\n') + text.pop_back(); + subtitles.push_back({ startTime, endTime, text }); + text.clear(); + } + continue; + } + + // Check if line is a number (subtitle index) - skip it + if (std::all_of(line.begin(), line.end(), [](char c) { return std::isdigit(static_cast(c)); })) + continue; + + // Check if line contains --> (timestamp line) + const size_t arrowPos = line.find("-->"); + if (arrowPos != std::string::npos) { + const std::string startStr = line.substr(0, arrowPos); + const std::string endStr = line.substr(arrowPos + 3); + startTime = ParseSrtTimestamp(startStr); + endTime = ParseSrtTimestamp(endStr); + continue; + } + + // Otherwise it's subtitle text + if (!text.empty()) + text += "\n"; + text += line; + } + + // Handle last subtitle if file doesn't end with blank line + if (!text.empty() && startTime < endTime) { + if (!text.empty() && text.back() == '\n') + text.pop_back(); + subtitles.push_back({ startTime, endTime, text }); + } + + return subtitles; +} + +std::string GetSubtitleAtTime(const std::vector &subtitles, uint64_t videoTimeMs) +{ + for (const auto &entry : subtitles) { + if (videoTimeMs >= entry.startTimeMs && videoTimeMs < entry.endTimeMs) { + // Translate the subtitle text + std::string_view translated = LanguageTranslate(entry.text); + return std::string(translated); + } + } + return ""; +} + +} // namespace devilution +