Browse Source

Merge 1b9e01f414 into 5a08031caf

pull/8394/merge
Yuri Pourre 4 days ago committed by GitHub
parent
commit
bca6912ce8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 1
      CMake/Assets.cmake
  2. 12
      Source/CMakeLists.txt
  3. 167
      Source/engine/render/subtitle_renderer.cpp
  4. 62
      Source/engine/render/subtitle_renderer.hpp
  5. 2
      Source/options.cpp
  6. 2
      Source/options.h
  7. 24
      Source/storm/storm_svid.cpp
  8. 17
      Source/translation_dummy.cpp
  9. 7
      Source/utils/sdl_compat.h
  10. 100
      Source/utils/srt_parser.cpp
  11. 38
      Source/utils/srt_parser.hpp
  12. 67
      assets/gendata/diabend.srt
  13. 72
      tools/extract_translation_data.py

1
CMake/Assets.cmake

@ -150,6 +150,7 @@ set(devilutionx_assets
gendata/cutportrw.clx
gendata/cutstartw.clx
gendata/cutttw.clx
gendata/diabend.srt
gendata/pause.trn
levels/l1data/sklkngt.dun
levels/l2data/bonechat.dun

12
Source/CMakeLists.txt

@ -165,6 +165,7 @@ set(libdevilutionx_SRCS
utils/language.cpp
utils/sdl_bilinear_scale.cpp
utils/sdl_thread.cpp
utils/srt_parser.cpp
utils/surface_to_clx.cpp
utils/timer.cpp)
@ -711,6 +712,16 @@ target_link_dependencies(libdevilutionx_text_render
libdevilutionx_utf8
)
add_devilutionx_object_library(libdevilutionx_subtitle_renderer
engine/render/subtitle_renderer.cpp
)
target_link_dependencies(libdevilutionx_subtitle_renderer
PRIVATE
libdevilutionx_log
libdevilutionx_surface
libdevilutionx_text_render
)
add_devilutionx_object_library(libdevilutionx_ticks
engine/ticks.cpp
)
@ -952,6 +963,7 @@ target_link_dependencies(libdevilutionx PUBLIC
libdevilutionx_spells
libdevilutionx_stores
libdevilutionx_strings
libdevilutionx_subtitle_renderer
libdevilutionx_text_render
libdevilutionx_txtdata
libdevilutionx_ticks

167
Source/engine/render/subtitle_renderer.cpp

@ -0,0 +1,167 @@
#include "engine/render/subtitle_renderer.hpp"
#include <algorithm>
#include <string>
#ifdef USE_SDL3
#include <SDL3/SDL_error.h>
#include <SDL3/SDL_pixels.h>
#include <SDL3/SDL_rect.h>
#include <SDL3/SDL_surface.h>
#else
#include <SDL.h>
#endif
#include "DiabloUI/ui_flags.hpp"
#include "engine/rectangle.hpp"
#include "engine/render/text_render.hpp"
#include "engine/surface.hpp"
#include "utils/log.hpp"
#include "utils/sdl_compat.h"
#include "utils/sdl_wrap.h"
#include "utils/srt_parser.hpp"
namespace devilution {
void SubtitleRenderer::LoadSubtitles(const char *videoFilename)
{
Clear();
// Generate subtitle filename by replacing .smk with .srt
std::string subtitlePath(videoFilename);
std::replace(subtitlePath.begin(), subtitlePath.end(), '\\', '/');
const size_t extPos = subtitlePath.rfind('.');
subtitlePath = (extPos != std::string::npos ? subtitlePath.substr(0, extPos) : subtitlePath) + ".srt";
Log("Loading subtitles from: {}", subtitlePath);
subtitles_ = LoadSrtFile(subtitlePath);
Log("Loaded {} subtitle entries", subtitles_.size());
if (!subtitles_.empty()) {
Log("First subtitle: {}ms-{}ms: \"{}\"",
subtitles_[0].startTimeMs,
subtitles_[0].endTimeMs,
subtitles_[0].text);
}
}
void SubtitleRenderer::RenderSubtitles(SDL_Surface *videoSurface, uint32_t videoWidth, uint32_t videoHeight, uint64_t currentTimeMs)
{
if (subtitles_.empty() || videoSurface == nullptr)
return;
const std::string subtitleText = GetSubtitleAtTime(subtitles_, currentTimeMs);
if (subtitleText.empty())
return;
LogVerbose(LogCategory::Video, "Rendering subtitle at {}ms: \"{}\"", currentTimeMs, subtitleText);
if (SDLC_SURFACE_BITSPERPIXEL(videoSurface) != 8)
return;
const int videoWidthInt = static_cast<int>(videoWidth);
const int videoHeightInt = static_cast<int>(videoHeight);
// Create subtitle overlay surface if not already created
if (subtitleSurface_ == nullptr) {
constexpr int SubtitleMaxHeight = 100;
subtitleSurface_ = SDLWrap::CreateRGBSurface(
0, videoWidthInt, SubtitleMaxHeight, 8, 0, 0, 0, 0);
// Create and set up palette for subtitle surface - copy from video surface
subtitlePalette_ = 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 = subtitlePalette_->colors;
constexpr int MaxColors = 256;
for (int i = 0; i < MaxColors && 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 = subtitlePalette_->colors;
colors[0].r = 0;
colors[0].g = 0;
colors[0].b = 0;
constexpr int MaxColors = 256;
for (int i = 1; i < MaxColors; i++) {
colors[i].r = 255;
colors[i].g = 255;
colors[i].b = 255;
}
}
#ifndef USE_SDL1
constexpr int MaxColors = 256;
for (int i = 0; i < MaxColors; i++) {
subtitlePalette_->colors[i].a = SDL_ALPHA_OPAQUE;
}
#endif
if (!SDLC_SetSurfacePalette(subtitleSurface_.get(), subtitlePalette_.get())) {
Log("Failed to set subtitle overlay palette");
}
// Set color key for transparency (index 0 = transparent)
#ifdef USE_SDL1
SDL_SetColorKey(subtitleSurface_.get(), SDL_SRCCOLORKEY, 0);
#else
if (!SDL_SetSurfaceColorKey(subtitleSurface_.get(), true, 0)) {
Log("Failed to set color key: {}", SDL_GetError());
}
#endif
}
// Clear the overlay surface (fill with transparent color)
SDL_FillSurfaceRect(subtitleSurface_.get(), nullptr, 0);
// Render text to the overlay surface
Surface overlaySurface(subtitleSurface_.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;
constexpr int TextHorizontalPadding = 10;
Rectangle subtitleRect { { TextHorizontalPadding, textY }, { videoWidthInt - TextHorizontalPadding * 2, 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 = videoHeightInt - SubtitleMaxHeight - SubtitleBottomPadding;
dstRect.w = videoWidthInt;
dstRect.h = SubtitleMaxHeight;
#ifdef USE_SDL3
if (!SDL_BlitSurface(subtitleSurface_.get(), nullptr, videoSurface, &dstRect)) {
Log("Failed to blit subtitle overlay: {}", SDL_GetError());
}
#else
if (SDL_BlitSurface(subtitleSurface_.get(), nullptr, videoSurface, &dstRect) < 0) {
Log("Failed to blit subtitle overlay: {}", SDL_GetError());
}
#endif
}
void SubtitleRenderer::Clear()
{
subtitles_.clear();
subtitleSurface_ = nullptr;
subtitlePalette_ = nullptr;
}
} // namespace devilution

62
Source/engine/render/subtitle_renderer.hpp

@ -0,0 +1,62 @@
/**
* @file subtitle_renderer.hpp
*
* Subtitle rendering for video playback.
*/
#pragma once
#include <cstdint>
#include <string>
#include <vector>
#include "utils/sdl_wrap.h"
#include "utils/srt_parser.hpp"
namespace devilution {
/**
* @brief Manages subtitle rendering state for video playback
*/
class SubtitleRenderer {
public:
SubtitleRenderer() = default;
~SubtitleRenderer() = default;
SubtitleRenderer(const SubtitleRenderer &) = delete;
SubtitleRenderer &operator=(const SubtitleRenderer &) = delete;
SubtitleRenderer(SubtitleRenderer &&) = default;
SubtitleRenderer &operator=(SubtitleRenderer &&) = default;
/**
* @brief Load subtitles for the given video
* @param videoFilename Path to the video file (will look for matching .srt file)
*/
void LoadSubtitles(const char *videoFilename);
/**
* @brief Render subtitles onto a video surface
* @param videoSurface The video surface to render subtitles onto
* @param videoWidth Width of the video
* @param videoHeight Height of the video
* @param currentTimeMs Current playback time in milliseconds
*/
void RenderSubtitles(SDL_Surface *videoSurface, uint32_t videoWidth, uint32_t videoHeight, uint64_t currentTimeMs);
/**
* @brief Clear subtitle data and free resources
*/
void Clear();
/**
* @brief Check if subtitles are loaded
* @return True if subtitles are available
*/
bool HasSubtitles() const { return !subtitles_.empty(); }
private:
std::vector<SubtitleEntry> subtitles_;
SDLSurfaceUniquePtr subtitleSurface_;
SDLPaletteUniquePtr subtitlePalette_;
};
} // namespace devilution

2
Source/options.cpp

@ -516,6 +516,7 @@ AudioOptions::AudioOptions()
, walkingSound("Walking Sound", OptionEntryFlags::None, N_("Walking Sound"), N_("Player emits sound when walking."), true)
, autoEquipSound("Auto Equip Sound", OptionEntryFlags::None, N_("Auto Equip Sound"), N_("Automatically equipping items on pickup emits the equipment sound."), false)
, itemPickupSound("Item Pickup Sound", OptionEntryFlags::None, N_("Item Pickup Sound"), N_("Picking up items emits the items pickup sound."), false)
, showSubtitles("Show Subtitles", OptionEntryFlags::None, N_("Show Subtitles"), N_("Display subtitles during video cutscenes."), true)
, sampleRate("Sample Rate", OptionEntryFlags::CantChangeInGame, N_("Sample Rate"), N_("Output sample rate (Hz)."), DEFAULT_AUDIO_SAMPLE_RATE, { 22050, 44100, 48000 })
, channels("Channels", OptionEntryFlags::CantChangeInGame, N_("Channels"), N_("Number of output channels."), DEFAULT_AUDIO_CHANNELS, { 1, 2 })
, bufferSize("Buffer Size", OptionEntryFlags::CantChangeInGame, N_("Buffer Size"), N_("Buffer size (number of frames per channel)."), DEFAULT_AUDIO_BUFFER_SIZE, { 1024, 2048, 5120 })
@ -532,6 +533,7 @@ std::vector<OptionEntryBase *> AudioOptions::GetEntries()
&walkingSound,
&autoEquipSound,
&itemPickupSound,
&showSubtitles,
&sampleRate,
&channels,
&bufferSize,

2
Source/options.h

@ -497,6 +497,8 @@ struct AudioOptions : OptionCategoryBase {
OptionEntryBoolean autoEquipSound;
/** @brief Picking up items emits the items pickup sound. */
OptionEntryBoolean itemPickupSound;
/** @brief Display subtitles during video cutscenes. */
OptionEntryBoolean showSubtitles;
/** @brief Output sample rate (Hz). */
OptionEntryInt<std::uint32_t> sampleRate;

24
Source/storm/storm_svid.cpp

@ -1,9 +1,12 @@
#include "storm/storm_svid.h"
#include <algorithm>
#include <cstddef>
#include <cstdint>
#include <cstring>
#include <optional>
#include <string>
#include <vector>
#ifdef USE_SDL3
#include <SDL3/SDL_error.h>
@ -30,6 +33,7 @@
#include "engine/assets.hpp"
#include "engine/dx.h"
#include "engine/palette.h"
#include "engine/render/subtitle_renderer.hpp"
#include "options.h"
#include "utils/display.h"
#include "utils/log.hpp"
@ -37,7 +41,6 @@
#include "utils/sdl_wrap.h"
namespace devilution {
namespace {
#ifndef NOSOUND
#ifdef USE_SDL3
@ -77,6 +80,11 @@ SDLSurfaceUniquePtr SVidSurface;
uint64_t SVidFrameEnd;
// The length of a frame in SMK time units.
uint32_t SVidFrameLength;
// Video start time in SMK time units (when playback began).
uint64_t SVidStartTime;
// Subtitle renderer for current video
SubtitleRenderer SVidSubtitleRenderer;
bool IsLandscapeFit(unsigned long srcW, unsigned long srcH, unsigned long dstW, unsigned long dstH)
{
@ -219,6 +227,13 @@ void UpdatePalette()
bool BlitFrame()
{
// Render subtitles if available and enabled - do this BEFORE blitting to output
if (*GetOptions().Audio.showSubtitles && SVidSubtitleRenderer.HasSubtitles()) {
const uint64_t currentTimeSmk = GetTicksSmk();
const uint64_t videoTimeMs = TimeSmkToMs(currentTimeSmk - SVidStartTime);
SVidSubtitleRenderer.RenderSubtitles(SVidSurface.get(), SVidWidth, SVidHeight, videoTimeMs);
}
#ifndef USE_SDL1
if (renderer != nullptr) {
if (
@ -327,8 +342,6 @@ void SVidInitAudioStream(const SmackerAudioInfo &audioInfo)
}
#endif
} // namespace
bool SVidPlayBegin(const char *filename, int flags)
{
if ((flags & 0x10000) != 0 || (flags & 0x20000000) != 0) {
@ -345,6 +358,9 @@ bool SVidPlayBegin(const char *filename, int flags)
// 0x800000 // Edge detection
// 0x200800 // Clear FB
// Load subtitles if available
SVidSubtitleRenderer.LoadSubtitles(filename);
auto *videoStream = OpenAssetAsSdlRwOps(filename);
SVidHandle = Smacker_Open(videoStream);
if (!SVidHandle.isValid) {
@ -449,6 +465,7 @@ bool SVidPlayBegin(const char *filename, int flags)
UpdatePalette();
SVidFrameEnd = GetTicksSmk() + SVidFrameLength;
SVidStartTime = GetTicksSmk();
return true;
}
@ -523,6 +540,7 @@ void SVidPlayEnd()
SVidPalette = nullptr;
SVidSurface = nullptr;
SVidFrameBuffer = nullptr;
SVidSubtitleRenderer.Clear();
#ifndef USE_SDL1
if (renderer != nullptr) {

17
Source/translation_dummy.cpp

@ -1041,5 +1041,22 @@ const char *SPELL_RUNE_OF_LIGHT_NAME = P_("spell", "Rune of Light");
const char *SPELL_RUNE_OF_NOVA_NAME = P_("spell", "Rune of Nova");
const char *SPELL_RUNE_OF_IMMOLATION_NAME = P_("spell", "Rune of Immolation");
const char *SPELL_RUNE_OF_STONE_NAME = P_("spell", "Rune of Stone");
const char *SUBTITLE_DIABEND_0 = N_("The Soulstone burns with hellfire as an eerie");
const char *SUBTITLE_DIABEND_1 = N_("red glow blurs your vision.");
const char *SUBTITLE_DIABEND_2 = N_("Fresh blood flows into your eyes and you");
const char *SUBTITLE_DIABEND_3 = N_("begin to hear the tormented whispers of the damned.");
const char *SUBTITLE_DIABEND_4 = N_("You have done what you knew must be done.");
const char *SUBTITLE_DIABEND_5 = N_("The essence of Diablo is contained.");
const char *SUBTITLE_DIABEND_6 = N_("For now.");
const char *SUBTITLE_DIABEND_7 = N_("You pray that you have become strong enough");
const char *SUBTITLE_DIABEND_8 = N_("to contain the demon and keep him at bay.");
const char *SUBTITLE_DIABEND_9 = N_("Although you have been fortified by your quest,");
const char *SUBTITLE_DIABEND_10 = N_("you can still feel him, clawing his way");
const char *SUBTITLE_DIABEND_11 = N_("up from the dark recesses of your soul.");
const char *SUBTITLE_DIABEND_12 = N_("Fighting to retain control, your thoughts turn toward");
const char *SUBTITLE_DIABEND_13 = N_("the ancient, mystic lands of the Far East.");
const char *SUBTITLE_DIABEND_14 = N_("Perhaps there, beyond the desolate wastes of Aranak,");
const char *SUBTITLE_DIABEND_15 = N_("you will find an answer.");
const char *SUBTITLE_DIABEND_16 = N_("Or perhaps, salvation.");
} // namespace

7
Source/utils/sdl_compat.h

@ -325,6 +325,13 @@ inline bool SDL_CursorVisible() { return SDL_ShowCursor(SDL_QUERY) == SDL_ENABLE
inline bool SDLC_PointInRect(const SDL_Point *p, const SDL_Rect *r) { return SDL_PointInRect(p, r) == SDL_TRUE; }
#endif
#ifdef USE_SDL1
inline bool SDLC_SetSurfacePalette(SDL_Surface *surface, SDL_Palette *palette)
{
return SDL_SetPalette(surface, SDL_LOGPAL, palette->colors, 0, palette->ncolors) != 0;
}
#endif
inline bool SDLC_ShowCursor() { return SDL_ShowCursor(SDL_ENABLE) >= 0; }
inline bool SDLC_HideCursor() { return SDL_ShowCursor(SDL_DISABLE) >= 0; }

100
Source/utils/srt_parser.cpp

@ -0,0 +1,100 @@
#include "utils/srt_parser.hpp"
#include <algorithm>
#include <cstdio>
#include <istream>
#include <sstream>
#include <string>
#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<uint64_t>(h * 3600000ULL + m * 60000ULL + s * 1000ULL + ms);
}
return 0;
}
std::vector<SubtitleEntry> LoadSrtFile(std::string_view subtitlePath)
{
std::vector<SubtitleEntry> 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<unsigned char>(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<SubtitleEntry> &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

38
Source/utils/srt_parser.hpp

@ -0,0 +1,38 @@
#pragma once
#include <cstdint>
#include <string>
#include <string_view>
#include <vector>
namespace devilution {
struct SubtitleEntry {
uint64_t startTimeMs; // Start time in milliseconds
uint64_t endTimeMs; // End time in milliseconds
std::string text; // Subtitle text (may contain multiple lines)
};
/**
* @brief Parse SRT timestamp (HH:MM:SS,mmm or HH:MM:SS.mmm) to milliseconds
* @param timestamp Timestamp string in SRT format
* @return Time in milliseconds, or 0 if parsing fails
*/
uint64_t ParseSrtTimestamp(std::string_view timestamp);
/**
* @brief Load and parse SRT subtitle file
* @param subtitlePath Path to the SRT file
* @return Vector of subtitle entries, empty if file not found or parsing fails
*/
std::vector<SubtitleEntry> LoadSrtFile(std::string_view subtitlePath);
/**
* @brief Get subtitle text for a given time from a list of subtitle entries
* @param subtitles Vector of subtitle entries
* @param videoTimeMs Current video time in milliseconds
* @return Subtitle text if a subtitle is active at this time, empty string otherwise
*/
std::string GetSubtitleAtTime(const std::vector<SubtitleEntry> &subtitles, uint64_t videoTimeMs);
} // namespace devilution

67
assets/gendata/diabend.srt

@ -0,0 +1,67 @@
1
00:00:00,600 --> 00:00:04,520
The Soulstone burns with hellfire as an eerie
2
00:00:04,520 --> 00:00:06,240
red glow blurs your vision
3
00:00:06,980 --> 00:00:09,560
Fresh blood flows into your eyes and you
4
00:00:09,560 --> 00:00:11,840
begin to hear the tormented whispers of the damned
5
00:00:12,860 --> 00:00:14,440
You have done what you knew must be done
6
00:00:15,360 --> 00:00:17,460
The essence of Diablo is contained
7
00:00:18,200 --> 00:00:18,860
For now...
8
00:00:19,980 --> 00:00:22,240
You pray that you have become strong enough
9
00:00:22,240 --> 00:00:24,820
to contain the demon and keep him at bay
10
00:00:25,200 --> 00:00:28,260
Although you have been fortified by your quest,
11
00:00:28,580 --> 00:00:31,439
you can still feel him, clawing his way
12
00:00:31,439 --> 00:00:33,980
up from the dark recesses of your soul
13
00:00:34,880 --> 00:00:38,040
Fighting to retain control, your thoughts turn toward
14
00:00:38,040 --> 00:00:40,760
the ancient, mystic lands of the Far East
15
00:00:41,300 --> 00:00:45,100
Perhaps there, beyond the desolate wastes of Aranak,
16
00:00:45,340 --> 00:00:46,560
you will find an answer
17
00:00:47,140 --> 00:00:49,340
Or perhaps, salvation

72
tools/extract_translation_data.py

@ -49,6 +49,68 @@ replacement_table = str.maketrans(
def create_identifier(value, prefix = '', suffix = ''):
return prefix + value.upper().translate(replacement_table) + suffix
def escape_cpp_string(s):
"""Escape a string for use in a C++ string literal."""
return s.replace('\\', '\\\\').replace('"', '\\"').replace('\n', '\\n')
def process_srt_file(srt_path, temp_source, prefix="SUBTITLE"):
"""Parse an SRT file and extract subtitle text for translation."""
if not srt_path.exists():
return
try:
with open(srt_path, 'r', encoding='utf-8') as f:
content = f.read()
except Exception:
return
lines = content.split('\n')
text = ""
subtitle_index = 0
i = 0
while i < len(lines):
line = lines[i]
# Remove \r if present (matching C++ parser behavior)
if line and line.endswith('\r'):
line = line[:-1]
# Skip empty lines (end of subtitle block)
if not line:
if text:
# Remove trailing newline from text
text = text.rstrip('\n')
if text:
var_name = f'{prefix}_{subtitle_index}'
escaped_text = escape_cpp_string(text)
write_entry(temp_source, var_name, "subtitle", escaped_text, False)
subtitle_index += 1
text = ""
i += 1
continue
# Check if line is a number (subtitle index) - skip it
if line.strip().isdigit():
i += 1
continue
# Check if line contains --> (timestamp line) - skip it
if '-->' in line:
i += 1
continue
# Otherwise it's subtitle text
if text:
text += "\n"
text += line
i += 1
# Handle last subtitle if file doesn't end with blank line
if text:
text = text.rstrip('\n')
if text:
var_name = f'{prefix}_{subtitle_index}'
escaped_text = escape_cpp_string(text)
write_entry(temp_source, var_name, "subtitle", escaped_text, False)
def process_files(paths, temp_source):
# Classes
if "classdat" in paths:
@ -140,4 +202,14 @@ with open(translation_dummy_path, 'w') as temp_source:
process_files(base_paths, temp_source)
process_files(hf_paths, temp_source)
# Process SRT subtitle files
srt_files = [
root.joinpath("assets/gendata/diabend.srt"),
]
for srt_file in srt_files:
# Extract filename without extension and convert to uppercase for prefix
filename = srt_file.stem.upper()
prefix = f"SUBTITLE_{filename}"
process_srt_file(srt_file, temp_source, prefix)
temp_source.write(f'\n}} // namespace\n')

Loading…
Cancel
Save