584 lines
13 KiB
C++
584 lines
13 KiB
C++
|
/*
|
||
|
** Command & Conquer Renegade(tm)
|
||
|
** Copyright 2025 Electronic Arts Inc.
|
||
|
**
|
||
|
** This program is free software: you can redistribute it and/or modify
|
||
|
** it under the terms of the GNU General Public License as published by
|
||
|
** the Free Software Foundation, either version 3 of the License, or
|
||
|
** (at your option) any later version.
|
||
|
**
|
||
|
** This program is distributed in the hope that it will be useful,
|
||
|
** but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||
|
** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||
|
** GNU General Public License for more details.
|
||
|
**
|
||
|
** You should have received a copy of the GNU General Public License
|
||
|
** along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||
|
*/
|
||
|
|
||
|
/****************************************************************************
|
||
|
*
|
||
|
* FILE
|
||
|
* $Archive: /Commando/Code/BinkMovie/subtitleparser.cpp $
|
||
|
*
|
||
|
* DESCRIPTION
|
||
|
* Subtitling control file parser
|
||
|
*
|
||
|
* PROGRAMMER
|
||
|
* Denzil E. Long, Jr.
|
||
|
*
|
||
|
* VERSION INFO
|
||
|
* $Author: Denzil_l $
|
||
|
* $Modtime: 1/12/02 9:27p $
|
||
|
* $Revision: 2 $
|
||
|
*
|
||
|
****************************************************************************/
|
||
|
|
||
|
#include "subtitleparser.h"
|
||
|
#include "subtitle.h"
|
||
|
#include "straw.h"
|
||
|
#include "readline.h"
|
||
|
#include "trim.h"
|
||
|
#include <wchar.h>
|
||
|
#include <stdlib.h>
|
||
|
|
||
|
// Subtitle control file parsing tokens
|
||
|
#define BEGINMOVIE_TOKEN L"BeginMovie"
|
||
|
#define ENDMOVIE_TOKEN L"EndMovie"
|
||
|
#define TIMEBIAS_TOKEN L"TimeBias"
|
||
|
#define TIME_TOKEN L"Time"
|
||
|
#define DURATION_TOKEN L"Duration"
|
||
|
#define POSITION_TOKEN L"Position"
|
||
|
#define COLOR_TOKEN L"Color"
|
||
|
#define TEXT_TOKEN L"Text"
|
||
|
|
||
|
unsigned long DecodeTimeString(wchar_t* string);
|
||
|
void Parse_Time(wchar_t* string, SubTitleClass* subTitle);
|
||
|
void Parse_Duration(wchar_t* string, SubTitleClass* subTitle);
|
||
|
void Parse_Position(wchar_t* string, SubTitleClass* subTitle);
|
||
|
void Parse_Color(wchar_t* string, SubTitleClass* subTitle);
|
||
|
void Parse_Text(wchar_t* string, SubTitleClass* subTitle);
|
||
|
|
||
|
SubTitleParserClass::TokenHook SubTitleParserClass::mTokenHooks[] =
|
||
|
{
|
||
|
{TIME_TOKEN, Parse_Time},
|
||
|
{DURATION_TOKEN, Parse_Duration},
|
||
|
{POSITION_TOKEN, Parse_Position},
|
||
|
{COLOR_TOKEN, Parse_Color},
|
||
|
{TEXT_TOKEN, Parse_Text},
|
||
|
{NULL, NULL}
|
||
|
};
|
||
|
|
||
|
/******************************************************************************
|
||
|
*
|
||
|
* NAME
|
||
|
* SubTitleParserClass::SubTitleParserClass
|
||
|
*
|
||
|
* DESCRIPTION
|
||
|
*
|
||
|
* INPUTS
|
||
|
* Input - Control file input stream.
|
||
|
*
|
||
|
* RESULTS
|
||
|
* NONE
|
||
|
*
|
||
|
******************************************************************************/
|
||
|
|
||
|
SubTitleParserClass::SubTitleParserClass(Straw& input)
|
||
|
:
|
||
|
mInput(input),
|
||
|
mLineNumber(0)
|
||
|
{
|
||
|
// Check for Unicode byte-order mark.
|
||
|
// All Unicode plaintext files are prefixed with the byte-order mark U+FEFF
|
||
|
// or its mirror U+FFFE. This mark is used to indicate the byte order of a
|
||
|
// text stream.
|
||
|
wchar_t byteOrderMark = 0;
|
||
|
mInput.Get(&byteOrderMark, sizeof(wchar_t));
|
||
|
WWASSERT(byteOrderMark == 0xFEFF);
|
||
|
|
||
|
if (byteOrderMark != 0xFEFF) {
|
||
|
WWDEBUG_SAY(("Error: Subtitle control file is not unicode!\n"));
|
||
|
}
|
||
|
}
|
||
|
|
||
|
|
||
|
/******************************************************************************
|
||
|
*
|
||
|
* NAME
|
||
|
* SubTitleParserClass::~SubTitleParserClass
|
||
|
*
|
||
|
* DESCRIPTION
|
||
|
*
|
||
|
* INPUTS
|
||
|
* NONE
|
||
|
*
|
||
|
* RESULTS
|
||
|
* NONE
|
||
|
*
|
||
|
******************************************************************************/
|
||
|
|
||
|
SubTitleParserClass::~SubTitleParserClass()
|
||
|
{
|
||
|
}
|
||
|
|
||
|
|
||
|
/******************************************************************************
|
||
|
*
|
||
|
* NAME
|
||
|
* SubTitleParserClass::GetSubTitles
|
||
|
*
|
||
|
* DESCRIPTION
|
||
|
*
|
||
|
* INPUTS
|
||
|
* NONE
|
||
|
*
|
||
|
* RESULTS
|
||
|
*
|
||
|
******************************************************************************/
|
||
|
|
||
|
DynamicVectorClass<SubTitleClass*>* SubTitleParserClass::Get_Sub_Titles(const char* moviename)
|
||
|
{
|
||
|
DynamicVectorClass<SubTitleClass*>* subTitleCollection = NULL;
|
||
|
|
||
|
// Find the movie marker
|
||
|
if (Find_Movie_Entry(moviename) == true) {
|
||
|
// Allocate container to hold subtitles
|
||
|
subTitleCollection = new DynamicVectorClass<SubTitleClass*>;
|
||
|
WWASSERT(subTitleCollection != NULL);
|
||
|
|
||
|
if (subTitleCollection != NULL) {
|
||
|
for (;;) {
|
||
|
// Retrieve a line from the control file
|
||
|
wchar_t* string = Get_Next_Line();
|
||
|
|
||
|
if ((string != NULL) && (wcslen(string) > 0)) {
|
||
|
// Check for subtitle entry markers
|
||
|
if ((string[0] == L'<') && (string[wcslen(string) - 1] == L'>')) {
|
||
|
// Trim off markers
|
||
|
string++;
|
||
|
string[wcslen(string) - 1] = 0;
|
||
|
wcstrim(string);
|
||
|
|
||
|
// Ignore empty caption
|
||
|
if (wcslen(string) == 0) {
|
||
|
continue;
|
||
|
}
|
||
|
|
||
|
// Create a new SubTitleClass
|
||
|
SubTitleClass* subTitle = new SubTitleClass();
|
||
|
WWASSERT(subTitle != NULL);
|
||
|
|
||
|
if (subTitle == NULL) {
|
||
|
WWDEBUG_SAY(("***** Failed to create SubTitleClass!\n"));
|
||
|
break;
|
||
|
}
|
||
|
|
||
|
if (Parse_Sub_Title(string, subTitle) == true) {
|
||
|
subTitleCollection->Add(subTitle);
|
||
|
}
|
||
|
else {
|
||
|
delete subTitle;
|
||
|
}
|
||
|
|
||
|
continue;
|
||
|
}
|
||
|
|
||
|
// Terminate if end movie token encountered.
|
||
|
if (wcsnicmp(string, ENDMOVIE_TOKEN, wcslen(ENDMOVIE_TOKEN)) == 0) {
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (subTitleCollection->Count() == 0) {
|
||
|
delete subTitleCollection;
|
||
|
subTitleCollection = NULL;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return subTitleCollection;
|
||
|
}
|
||
|
|
||
|
|
||
|
/******************************************************************************
|
||
|
*
|
||
|
* NAME
|
||
|
* SubTitleParserClass::FindMovieEntry
|
||
|
*
|
||
|
* DESCRIPTION
|
||
|
* No description provided,
|
||
|
*
|
||
|
* INPUTS
|
||
|
* Moviename - Pointer to name of movie to find subtitles for.
|
||
|
*
|
||
|
* RESULTS
|
||
|
* Success - True if movie entry found; False if unable to find movie entry.
|
||
|
*
|
||
|
******************************************************************************/
|
||
|
|
||
|
bool SubTitleParserClass::Find_Movie_Entry(const char* moviename)
|
||
|
{
|
||
|
// Convert the moviename into Unicode
|
||
|
WWASSERT(moviename != NULL);
|
||
|
wchar_t wideName[32];
|
||
|
mbstowcs(wideName, moviename, 32);
|
||
|
|
||
|
do {
|
||
|
// Retrieve line of text
|
||
|
wchar_t* string = Get_Next_Line();
|
||
|
|
||
|
// Terminate if no string read.
|
||
|
if (string == NULL) {
|
||
|
break;
|
||
|
}
|
||
|
|
||
|
// Look for begin movie token
|
||
|
if (wcsnicmp(string, BEGINMOVIE_TOKEN, wcslen(BEGINMOVIE_TOKEN)) == 0) {
|
||
|
// Get moviename following the token
|
||
|
wchar_t* ptr = wcschr(string, L' ');
|
||
|
|
||
|
// Check for matching moviename
|
||
|
if (ptr != NULL) {
|
||
|
wcstrim(ptr);
|
||
|
|
||
|
if (wcsicmp(ptr, wideName) == 0) {
|
||
|
WWDEBUG_SAY(("Found movie entry %s\n", moviename));
|
||
|
return true;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
} while (true);
|
||
|
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
|
||
|
/******************************************************************************
|
||
|
*
|
||
|
* NAME
|
||
|
* SubTitleParserClass::ParseSubTitle
|
||
|
*
|
||
|
* DESCRIPTION
|
||
|
*
|
||
|
* INPUTS
|
||
|
* wchar_t* string
|
||
|
* SubTitleClass* subTitle
|
||
|
*
|
||
|
* RESULTS
|
||
|
* bool
|
||
|
*
|
||
|
******************************************************************************/
|
||
|
|
||
|
bool SubTitleParserClass::Parse_Sub_Title(wchar_t* string, SubTitleClass* subTitle)
|
||
|
{
|
||
|
// Parameter check
|
||
|
WWASSERT(string != NULL);
|
||
|
WWASSERT(subTitle != NULL);
|
||
|
|
||
|
for (;;) {
|
||
|
// Find token separator
|
||
|
wchar_t* separator = wcschr(string, L'=');
|
||
|
|
||
|
if (separator == NULL) {
|
||
|
WWDEBUG_SAY(("Error on line %d: syntax error\n", Get_Line_Number()));
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
// NULL terminate token part
|
||
|
*separator++ = 0;
|
||
|
|
||
|
// Tokens are to the left of the separator
|
||
|
wchar_t* token = string;
|
||
|
wcstrim(token);
|
||
|
|
||
|
// Parameters are to the right of the separator
|
||
|
wchar_t* param = separator;
|
||
|
wcstrim(param);
|
||
|
|
||
|
// Quoted parameters are treated as literals (ignore contents)
|
||
|
if (param[0] == L'"') {
|
||
|
// Skip leading quote
|
||
|
param++;
|
||
|
|
||
|
// Use next quote to mark end of parameter
|
||
|
separator = wcschr(param, L'"');
|
||
|
|
||
|
if (separator == NULL) {
|
||
|
WWDEBUG_SAY(("Error on line %d: mismatched quotes\n", Get_Line_Number()));
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
// NULL terminate parameter
|
||
|
*separator++ = 0;
|
||
|
|
||
|
// Skip any comma following a literal string since we used the trailing
|
||
|
// quote to terminate the tokens parameters
|
||
|
wcstrim(separator);
|
||
|
|
||
|
if (*separator == L',') {
|
||
|
separator++;
|
||
|
}
|
||
|
|
||
|
// Advance string past quoted parameter
|
||
|
string = separator;
|
||
|
}
|
||
|
else {
|
||
|
// Look for separator to next token
|
||
|
separator = wcspbrk(param, L", ");
|
||
|
|
||
|
if (separator != NULL) {
|
||
|
*separator++ = 0;
|
||
|
string = separator;
|
||
|
}
|
||
|
else {
|
||
|
string = L"";
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Error on empty tokens
|
||
|
if (wcslen(token) == 0) {
|
||
|
WWDEBUG_SAY(("Error on line %d: missing token\n", Get_Line_Number()));
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
// Parse current token
|
||
|
Parse_Token(token, param, subTitle);
|
||
|
|
||
|
// Prepare for next token
|
||
|
wcstrim(string);
|
||
|
|
||
|
if (wcslen(string) == 0) {
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
|
||
|
/******************************************************************************
|
||
|
*
|
||
|
* NAME
|
||
|
* SubTitleParserClass::ParseToken
|
||
|
*
|
||
|
* DESCRIPTION
|
||
|
*
|
||
|
* INPUTS
|
||
|
* wchar_t* token
|
||
|
* wchar_t* param
|
||
|
* SubTitleClass* subTitle
|
||
|
*
|
||
|
* RESULTS
|
||
|
* NONE
|
||
|
*
|
||
|
******************************************************************************/
|
||
|
|
||
|
void SubTitleParserClass::Parse_Token(wchar_t* token, wchar_t* param, SubTitleClass* subTitle)
|
||
|
{
|
||
|
// Parameter check
|
||
|
WWASSERT(token != NULL);
|
||
|
WWASSERT(subTitle != NULL);
|
||
|
|
||
|
if (token != NULL) {
|
||
|
int index = 0;
|
||
|
|
||
|
while (mTokenHooks[index].Token != NULL) {
|
||
|
TokenHook& hook = mTokenHooks[index];
|
||
|
|
||
|
if (wcsicmp(hook.Token, token) == 0) {
|
||
|
WWASSERT(subTitle != NULL);
|
||
|
hook.Handler(param, subTitle);
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
index++;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
|
||
|
/******************************************************************************
|
||
|
*
|
||
|
* NAME
|
||
|
* SubTitleParserClass::GetNextLine
|
||
|
*
|
||
|
* DESCRIPTION
|
||
|
* Retrieve the next line of text from the control file.
|
||
|
*
|
||
|
* INPUTS
|
||
|
* NONE
|
||
|
*
|
||
|
* RESULTS
|
||
|
* String - Pointer to next line of text. NULL if error or EOF.
|
||
|
*
|
||
|
******************************************************************************/
|
||
|
|
||
|
wchar_t* SubTitleParserClass::Get_Next_Line(void)
|
||
|
{
|
||
|
bool eof = false;
|
||
|
|
||
|
while (eof == false) {
|
||
|
// Read in a line of text
|
||
|
Read_Line(mInput, mBuffer, LINE_MAX, eof);
|
||
|
mLineNumber++;
|
||
|
|
||
|
// Remove whitespace
|
||
|
wchar_t* string = wcstrim(mBuffer);
|
||
|
|
||
|
// Skip comments and blank lines
|
||
|
if ((wcslen(string) > 0) && (string[0] != L';')) {
|
||
|
return string;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return NULL;
|
||
|
}
|
||
|
|
||
|
|
||
|
// Convert a time string in the format hh:mm:ss:tt into 1/60 second ticks.
|
||
|
unsigned long Decode_Time_String(wchar_t* string)
|
||
|
{
|
||
|
#define TICKS_PER_SECOND 60
|
||
|
#define TICKS_PER_MINUTE (60 * TICKS_PER_SECOND)
|
||
|
#define TICKS_PER_HOUR (60 * TICKS_PER_MINUTE)
|
||
|
|
||
|
WWASSERT(string != NULL);
|
||
|
|
||
|
wchar_t buffer[12];
|
||
|
wcsncpy(buffer, string, 12);
|
||
|
buffer[11] = 0;
|
||
|
|
||
|
wchar_t* ptr = &buffer[0];
|
||
|
|
||
|
// Isolate hours part
|
||
|
wchar_t* separator = wcschr(ptr, L':');
|
||
|
WWASSERT(separator != NULL);
|
||
|
*separator++ = 0;
|
||
|
unsigned long hours = wcstoul(ptr, NULL, 10);
|
||
|
|
||
|
// Isolate minutes part
|
||
|
ptr = separator;
|
||
|
separator = wcschr(ptr, L':');
|
||
|
WWASSERT(separator != NULL);
|
||
|
*separator++ = 0;
|
||
|
unsigned long minutes = wcstoul(ptr, NULL, 10);
|
||
|
|
||
|
// Isolate seconds part
|
||
|
ptr = separator;
|
||
|
separator = wcschr(ptr, L':');
|
||
|
WWASSERT(separator != NULL);
|
||
|
*separator++ = 0;
|
||
|
unsigned long seconds = wcstoul(ptr, NULL, 10);
|
||
|
|
||
|
// Isolate hundredth part (1/100th of a second)
|
||
|
ptr = separator;
|
||
|
unsigned long hundredth = wcstoul(ptr, NULL, 10);
|
||
|
|
||
|
unsigned long time = (hours * TICKS_PER_HOUR);
|
||
|
time += (minutes * TICKS_PER_MINUTE);
|
||
|
time += (seconds * TICKS_PER_SECOND);
|
||
|
time += ((hundredth * TICKS_PER_SECOND) / 100);
|
||
|
|
||
|
return time;
|
||
|
}
|
||
|
|
||
|
|
||
|
void Parse_Time(wchar_t* param, SubTitleClass* subTitle)
|
||
|
{
|
||
|
WWASSERT(param != NULL);
|
||
|
WWASSERT(subTitle != NULL);
|
||
|
unsigned long time = Decode_Time_String(param);
|
||
|
subTitle->Set_Display_Time(time);
|
||
|
}
|
||
|
|
||
|
|
||
|
void Parse_Duration(wchar_t* param, SubTitleClass* subTitle)
|
||
|
{
|
||
|
WWASSERT(param != NULL);
|
||
|
WWASSERT(subTitle != NULL);
|
||
|
unsigned long time = Decode_Time_String(param);
|
||
|
|
||
|
if (time > 0) {
|
||
|
subTitle->Set_Display_Duration(time);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
|
||
|
void Parse_Position(wchar_t* param, SubTitleClass* subTitle)
|
||
|
{
|
||
|
static struct
|
||
|
{
|
||
|
const wchar_t* Name;
|
||
|
SubTitleClass::Alignment Align;
|
||
|
} _alignLookup[] = {
|
||
|
{L"Left", SubTitleClass::Left},
|
||
|
{L"Right", SubTitleClass::Right},
|
||
|
{L"Center", SubTitleClass::Center},
|
||
|
{NULL, SubTitleClass::Center}
|
||
|
};
|
||
|
|
||
|
WWASSERT(subTitle != NULL);
|
||
|
WWASSERT(param != NULL);
|
||
|
|
||
|
wchar_t* ptr = param;
|
||
|
|
||
|
// Line position
|
||
|
wchar_t* separator = wcschr(ptr, L':');
|
||
|
|
||
|
if (separator != NULL) {
|
||
|
*separator++ = 0;
|
||
|
int linePos = wcstol(ptr, NULL, 0);
|
||
|
subTitle->Set_Line_Position(linePos);
|
||
|
ptr = separator;
|
||
|
}
|
||
|
|
||
|
// Justification
|
||
|
SubTitleClass::Alignment align = SubTitleClass::Center;
|
||
|
int index = 0;
|
||
|
|
||
|
while (_alignLookup[index].Name != NULL) {
|
||
|
if (wcsicmp(ptr, _alignLookup[index].Name) == 0) {
|
||
|
align = _alignLookup[index].Align;
|
||
|
break;
|
||
|
}
|
||
|
|
||
|
index++;
|
||
|
}
|
||
|
|
||
|
subTitle->Set_Alignment(align);
|
||
|
}
|
||
|
|
||
|
|
||
|
void Parse_Color(wchar_t* param, SubTitleClass* subTitle)
|
||
|
{
|
||
|
WWASSERT(param != NULL);
|
||
|
WWASSERT(subTitle != NULL);
|
||
|
|
||
|
wchar_t* ptr = param;
|
||
|
|
||
|
wchar_t* separator = wcschr(ptr, L':');
|
||
|
*separator++ = 0;
|
||
|
unsigned char red = (unsigned char)wcstoul(ptr, NULL, 10);
|
||
|
|
||
|
ptr = separator;
|
||
|
separator = wcschr(ptr, L':');
|
||
|
*separator++ = 0;
|
||
|
unsigned char green = (unsigned char)wcstoul(ptr, NULL, 10);
|
||
|
|
||
|
ptr = separator;
|
||
|
unsigned char blue = (unsigned char)wcstoul(ptr, NULL, 10);
|
||
|
|
||
|
subTitle->Set_RGB_Color(red, green, blue);
|
||
|
}
|
||
|
|
||
|
|
||
|
void Parse_Text(wchar_t* param, SubTitleClass* subTitle)
|
||
|
{
|
||
|
WWASSERT(param != NULL);
|
||
|
WWASSERT(subTitle != NULL);
|
||
|
|
||
|
subTitle->Set_Caption(param);
|
||
|
}
|