/*============================================================================= Name : Subtitle.c Purpose : Functions for printing subtitles in time with the speech Created 2/3/1999 by lmoloney Copyright Relic Entertainment, Inc. All rights reserved. =============================================================================*/ #define WIN32_LEAN_AND_MEAN #include #include #include #include "glinc.h" #include "debug.h" #include "memory.h" #include "font.h" #include "color.h" #include "statscript.h" #include "font.h" #include "fontReg.h" #include "main.h" #include "key.h" #include "universe.h" #include "teams.h" #include "mainrgn.h" #include "sensors.h" #include "nis.h" #include "render.h" #include "fereg.h" #include "strings.h" #include "subtitle.h" /*============================================================================= Data: =============================================================================*/ //strings for testing #if SUB_MODULE_TEST char *subTestSubtitle[NINEKEY - ONEKEY + 1] = { "A short string", "This page was originally made to tell what I saw and what had occured to me during my 1994-1995 trip to the U.S. Amundsen-Scott South Pole Station. However the process of writing the text and preparing the pictures took so much time that I had to halt for a while so that I can pass all those courses that I need in order to stay in the Graduate School. The result is that now I have come back from the second (now the third, fourth .. ) trip which I have some thing new to tell, but I still have unfinished first-year stuff to write. So here are some stories from the 1995 trip and writings based on later ones. \n\nPhotographic notes: Most photographic information are not directly put in the text. A lot of pictures are not serious shots. Here are some photographic related information. #n#nView them the right way: There are a lot of pictures in my web pages. If you find pictures on your screen too dark or too bright, probably you need to adjust gamma of your monitor. I created all images that are used here with the gamma set to 2.2.The best viewing environment is when there is no light directly shining on your screen. Adjust your monitor so that the center block in this image is indistinguishable against the grey part when you look at it from 6 to 8 feet away; and the border is as dark as possible. This will make your monitor have a closer presentation to mine. However I was told it could be hard to achieve on Macs. \n\nAnyway, enjoy the stories! ", "A meduim-length string withSomeReallyLongWordsToReallyMessWithTheWordWrappingLogic.ThisOutghToMessThemUp!", NULL, NULL, NULL, NULL, NULL, NULL }; real32 subTestSubtitleTime[NINEKEY - ONEKEY + 1] = { 1.0f, 12.0f, 2.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f }; #endif //SUB_MODULE_TEST //semaphore for communicating between stream thread and main game thread HANDLE subSemaphore; //pointer to timer to use currently. May be different depending on NIS's etc. real32 *subTimeElapsed = NULL; //set to TRUE when a fleet intel event ends for the benifit of KAS sdword subMessageEnded = FALSE; //retained copy of the player's race and all team colors so we know when to //re-load the speech icon textures. ShipRace subLastRace; trcolorinfo subLastTeamColors[TE_NumberPlayers]; /*----------------------------------------------------------------------------- This next section of data should only be accessed under the protection of the previous semaphore. -----------------------------------------------------------------------------*/ subnewsubtitle subNewSubtitles[SUB_NumberNewSubtitles]; sdword subNumberNewSubtitles; #if SUB_VERBOSE_LEVEL >= 1 sdword subMissedSubtitles; #endif /*----------------------------------------------------------------------------- Subtitle themes and printing regions. -----------------------------------------------------------------------------*/ subregion subRegion[SUB_NumberRegions]; subtheme subTheme[SUB_NumberThemes]; /*----------------------------------------------------------------------------- General subtitle stuff -----------------------------------------------------------------------------*/ real32 subScrollDwellStart = SUB_ScrollDwellStart; real32 subScrollDwellEnd = SUB_ScrollDwellEnd; real32 subScrollShortest = SUB_ScrollShortest; real32 subTitleShortest = SUB_TitleShortest; bool8 subCloseCaptionsEnabled = TRUE; /*----------------------------------------------------------------------------- Script parsing table for subtitle.script. -----------------------------------------------------------------------------*/ void subThemeColorSet(char *directory, char *field, void *dataToFillIn); void subThemeDropshadowSet(char *directory, char *field, void *dataToFillIn); void subThemeMarginSet(char *directory, char *field, void *dataToFillIn); void subThemeFontSet(char *directory, char *field, void *dataToFillIn); void subThemeFadeSet(char *directory, char *field, void *dataToFillIn); void subRegionBoundsSet(char *directory, char *field, void *dataToFillIn); void subRegionScaleRezSet(char *directory, char *field, void *dataToFillIn); void subRegionMaxLinesSet(char *directory, char *field, void *dataToFillIn); void subThemeCentredSet(char *directory, char *field, void *dataToFillIn); void subThemePictureSet(char *directory, char *field, void *dataToFillIn); void subRegionIconOffsetSet(char *directory, char *field, void *dataToFillIn); scriptEntry subScriptTable[] = { //define theme parameters { "themeColor", subThemeColorSet, NULL}, { "themeDropshadow", subThemeDropshadowSet, NULL}, { "themeMargin", subThemeMarginSet, NULL}, { "themeFont", subThemeFontSet, NULL}, { "themeFadeIn", subThemeFadeSet, (void *)FALSE}, { "themeFadeOut", subThemeFadeSet, (void *)TRUE}, { "themePicture", subThemePictureSet, (void *)TRUE}, { "themeCentred", subThemeCentredSet, (void *)TRUE}, //define printing regions { "regionBounds", subRegionBoundsSet, NULL}, { "regionScaleRez", subRegionScaleRezSet, NULL}, { "regionMaxLines", subRegionMaxLinesSet, NULL}, { "regionIconOffset", subRegionIconOffsetSet, NULL}, //general stuff { "scrollDwellStart", scriptSetReal32CB, &subScrollDwellStart}, { "scrollDwellEnd", scriptSetReal32CB, &subScrollDwellEnd}, { "scrollShortest", scriptSetReal32CB, &subScrollShortest}, { "subtitleShortest", scriptSetReal32CB, &subTitleShortest}, { NULL, NULL, 0 } }; /*============================================================================= Functions: =============================================================================*/ /*----------------------------------------------------------------------------- Name : subAnyCardsOnScreen Description : returns TRUE if any subtitle cards are on the screen Inputs : Outputs : Return : returns TRUE if any subtitle cards are on the screen ----------------------------------------------------------------------------*/ bool subAnyCardsOnScreen(void) { sdword index; for (index = 0; index < SUB_NumberRegions; index++) { if (subRegion[index].bEnabled && subRegion[index].cardIndex > 0) { return TRUE; } } return FALSE; } /*----------------------------------------------------------------------------- Name : subThemeColorSet Description : Script-oarsing callback for setting a theme's color Inputs : Outputs : Return : ----------------------------------------------------------------------------*/ void subThemeColorSet(char *directory, char *field, void *dataToFillIn) { udword nScanned, iTheme, red, green, blue; RemoveCommasFromString(field); nScanned = sscanf(field, "%d %d %d %d", &iTheme, &red, &green, &blue); #if SUB_ERROR_CHECKING if (nScanned != 4) { dbgFatalf(DBG_Loc, "Wrong number of fields in '%s': expected %d found %d", field, 4, nScanned); } if (iTheme >= SUB_NumberThemes) { dbgFatalf(DBG_Loc, "Invalid theme number %d. Must be between 0 and %d.", iTheme, SUB_NumberThemes); } if (red > UBYTE_Max || green > UBYTE_Max || blue > UBYTE_Max) { dbgFatalf(DBG_Loc, "Invalid color in string '%s'", field); } #endif subTheme[iTheme].textColor = colRGB(red, green, blue); } /*----------------------------------------------------------------------------- Name : subThemeDropshadowSet Description : Script parse callback for setting the dropshadow flag on a theme. Inputs : Outputs : Return : ----------------------------------------------------------------------------*/ void subThemeDropshadowSet(char *directory, char *field, void *dataToFillIn) { sdword nScanned, iTheme; char boolString[UBYTE_Max]; RemoveCommasFromString(field); nScanned = sscanf(field, "%d %s", &iTheme, boolString); #if SUB_ERROR_CHECKING if (nScanned != 2) { dbgFatalf(DBG_Loc, "Wrong number of fields in '%s': expected %d found %d", field, 2, nScanned); } if (iTheme < 0 || iTheme >= SUB_NumberThemes) { dbgFatalf(DBG_Loc, "Invalid theme number %d. Must be between 0 and %d.", iTheme, SUB_NumberThemes); } #endif subTheme[iTheme].bDropShadow = (bool8)scriptStringToBool(boolString); } /*----------------------------------------------------------------------------- Name : subThemeCentredSet Description : Script parse callback for setting the centred flag on a theme. Inputs : Outputs : Return : ----------------------------------------------------------------------------*/ void subThemeCentredSet(char *directory, char *field, void *dataToFillIn) { sdword nScanned, iTheme; char boolString[UBYTE_Max]; RemoveCommasFromString(field); nScanned = sscanf(field, "%d %s", &iTheme, boolString); #if SUB_ERROR_CHECKING if (nScanned != 2) { dbgFatalf(DBG_Loc, "Wrong number of fields in '%s': expected %d found %d", field, 2, nScanned); } if (iTheme < 0 || iTheme >= SUB_NumberThemes) { dbgFatalf(DBG_Loc, "Invalid theme number %d. Must be between 0 and %d.", iTheme, SUB_NumberThemes); } #endif subTheme[iTheme].bCentred = (bool8)scriptStringToBool(boolString); } /*----------------------------------------------------------------------------- Name : subThemePictureSet Description : Script parse callback for setting the picture for a theme. Inputs : Outputs : Return : ----------------------------------------------------------------------------*/ void subThemePictureSet(char *directory, char *field, void *dataToFillIn) { sdword nScanned, iTheme, iColorScheme; char pictureString[UBYTE_Max]; char fileName[UBYTE_Max]; RemoveCommasFromString(field); nScanned = sscanf(field, "%d %d %s", &iTheme, &iColorScheme, pictureString); #if SUB_ERROR_CHECKING if (nScanned != 3) { dbgFatalf(DBG_Loc, "Wrong number of fields in '%s': expected %d found %d", field, 3, nScanned); } if (iTheme < 0 || iTheme >= SUB_NumberThemes) { dbgFatalf(DBG_Loc, "Invalid theme number %d. Must be between 0 and %d.", iTheme, SUB_NumberThemes); } if (iColorScheme < 0 || iColorScheme >= TE_NumberPlayers) { dbgFatalf(DBG_Loc, "Invalid color scheme %d for '%s'", iColorScheme, pictureString); } #endif if (universe.players[0].race == R1) { strcpy(fileName, "SpeechIcons\\R1\\"); } else { strcpy(fileName, "SpeechIcons\\R2\\"); } strcat(fileName, pictureString); subTheme[iTheme].picture = trTextureRegister(fileName, &teColorSchemes[iColorScheme].textureColor, (void *)&subTheme[iTheme]); subTheme[iTheme].pictureColorScheme = iColorScheme; subTheme[iTheme].bPicture = TRUE; } /*----------------------------------------------------------------------------- Name : subThemeMarginSet Description : Script parse callback for setting the margin of a theme Inputs : Outputs : Return : ----------------------------------------------------------------------------*/ void subThemeMarginSet(char *directory, char *field, void *dataToFillIn) { sdword nScanned, iTheme, margin; RemoveCommasFromString(field); nScanned = sscanf(field, "%d %d", &iTheme, &margin); #if SUB_ERROR_CHECKING if (nScanned != 2) { dbgFatalf(DBG_Loc, "Wrong number of fields in '%s': expected %d found %d", field, 2, nScanned); } if (iTheme < 0 || iTheme >= SUB_NumberThemes) { dbgFatalf(DBG_Loc, "Invalid theme number %d. Must be between 0 and %d.", iTheme, SUB_NumberThemes); } #endif subTheme[iTheme].margin = margin; } /*----------------------------------------------------------------------------- Name : subThemeFadeSet Description : Script parse callback for setting a fade time Inputs : Outputs : Return : ----------------------------------------------------------------------------*/ void subThemeFadeSet(char *directory, char *field, void *dataToFillIn) { sdword nScanned, iTheme; real32 fadeTime; RemoveCommasFromString(field); nScanned = sscanf(field, "%d %f", &iTheme, &fadeTime); #if SUB_ERROR_CHECKING if (nScanned != 2) { dbgFatalf(DBG_Loc, "Wrong number of fields in '%s': expected %d found %d", field, 2, nScanned); } if (iTheme < 0 || iTheme >= SUB_NumberThemes) { dbgFatalf(DBG_Loc, "Invalid theme number %d. Must be between 0 and %d.", iTheme, SUB_NumberThemes); } dbgAssert(fadeTime >= 0.0f && fadeTime < 80.0f); #endif if ((bool)dataToFillIn == FALSE) { subTheme[iTheme].fadeIn = fadeTime; } else { subTheme[iTheme].fadeOut = fadeTime; } } /*----------------------------------------------------------------------------- Name : subThemeFontSet Description : Script parse callback for setting the font of a theme Inputs : Outputs : Return : ----------------------------------------------------------------------------*/ void subThemeFontSet(char *directory, char *field, void *dataToFillIn) { sdword nScanned, iTheme; char fontName[1024]; RemoveCommasFromString(field); nScanned = sscanf(field, "%d %s", &iTheme, fontName); #if SUB_ERROR_CHECKING if (nScanned != 2) { dbgFatalf(DBG_Loc, "Wrong number of fields in '%s': expected %d found %d", field, 2, nScanned); } if (iTheme < 0 || iTheme >= SUB_NumberThemes) { dbgFatalf(DBG_Loc, "Invalid theme number %d. Must be between 0 and %d.", iTheme, SUB_NumberThemes); } #endif subTheme[iTheme].font = frFontRegister(fontName); } /*----------------------------------------------------------------------------- Name : subRegionBoundsSet Description : Script parse callback to set the bounds of a subtitle region. Inputs : Outputs : Return : ----------------------------------------------------------------------------*/ void subRegionBoundsSet(char *directory, char *field, void *dataToFillIn) { sdword nScanned, iRegion; rectangle rect; RemoveCommasFromString(field); nScanned = sscanf(field, "%d %d %d %d %d", &iRegion, &rect.x0, &rect.y0, &rect.x1, &rect.y1); #if SUB_ERROR_CHECKING if (nScanned != 5) { dbgFatalf(DBG_Loc, "Wrong number of fields in '%s': expected %d found %d", field, 5, nScanned); } if (iRegion < 0 || iRegion >= SUB_NumberRegions) { dbgFatalf(DBG_Loc, "Invalid region number %d. Must be between 0 and %d.", iRegion, SUB_NumberRegions); } dbgAssert(rect.x0 < 640); dbgAssert(rect.x1 > 0); dbgAssert(rect.y0 < 480); dbgAssert(rect.y1 > 0); dbgAssert(rect.x0 < rect.x1); dbgAssert(rect.y0 < rect.y1); #endif subRegion[iRegion].defaultRect = rect; } /*----------------------------------------------------------------------------- Name : subRegionScaleRezSet Description : script parse callback to set the resolution scale flags for a subtitle region Inputs : Outputs : Return : ----------------------------------------------------------------------------*/ void subRegionScaleRezSet(char *directory, char *field, void *dataToFillIn) { sdword nScanned, iRegion; char bScaleRezX[UBYTE_Max], bScaleRezY[UBYTE_Max]; RemoveCommasFromString(field); nScanned = sscanf(field, "%d %s %s", &iRegion, bScaleRezX, bScaleRezY); #if SUB_ERROR_CHECKING if (nScanned != 3) { dbgFatalf(DBG_Loc, "Wrong number of fields in '%s': expected %d found %d", field, 3, nScanned); } if (iRegion < 0 || iRegion >= SUB_NumberRegions) { dbgFatalf(DBG_Loc, "Invalid region number %d. Must be between 0 and %d.", iRegion, SUB_NumberRegions); } #endif subRegion[iRegion].bScaleRezX = (bool8)scriptStringToBool(bScaleRezX); subRegion[iRegion].bScaleRezY = (bool8)scriptStringToBool(bScaleRezY); } /*----------------------------------------------------------------------------- Name : subRegionMaxLinesSet Description : Script parse callback for setting the max number of lines in a region. Inputs : Outputs : Return : ----------------------------------------------------------------------------*/ void subRegionMaxLinesSet(char *directory, char *field, void *dataToFillIn) { sdword nScanned, iRegion, maxLines; RemoveCommasFromString(field); nScanned = sscanf(field, "%d %d", &iRegion, &maxLines); #if SUB_ERROR_CHECKING if (nScanned != 2) { dbgFatalf(DBG_Loc, "Wrong number of fields in '%s': expected %d found %d", field, 2, nScanned); } if (iRegion < 0 || iRegion >= SUB_NumberRegions) { dbgFatalf(DBG_Loc, "Invalid region number %d. Must be between 0 and %d.", iRegion, SUB_NumberRegions); } dbgAssert(maxLines > 0 && maxLines < UDWORD_Max); #endif subRegion[iRegion].numberCards = maxLines; } /*----------------------------------------------------------------------------- Name : subRegionIconOffsetSet Description : Script parse callback for setting the speech icon offset (if any) for the region Inputs : Outputs : Return : ----------------------------------------------------------------------------*/ void subRegionIconOffsetSet(char *directory, char *field, void *dataToFillIn) { sdword nScanned, iRegion, x, y; RemoveCommasFromString(field); nScanned = sscanf(field, "%d %d %d", &iRegion, &x, &y); #if SUB_ERROR_CHECKING if (nScanned != 3) { dbgFatalf(DBG_Loc, "Wrong number of fields in '%s': expected %d found %d", field, 3, nScanned); } if (iRegion < 0 || iRegion >= SUB_NumberRegions) { dbgFatalf(DBG_Loc, "Invalid region number %d. Must be between 0 and %d.", iRegion, SUB_NumberRegions); } #endif subRegion[iRegion].iconOffsetX = x; subRegion[iRegion].iconOffsetY = y; } /*----------------------------------------------------------------------------- Name : subTextureInfoRemember Description : Remembers the player's race and team color information. Inputs : Outputs : Return : ----------------------------------------------------------------------------*/ void subTextureInfoRemember(void) { sdword index; subLastRace = universe.players[0].race; for (index = 0; index < TE_NumberPlayers; index++) { subLastTeamColors[index] = teColorSchemes[index].textureColor; } } /*----------------------------------------------------------------------------- Name : subRegionsRescale Description : Rescales all region rectangles for a new screen resolution. Inputs : Outputs : Return : ----------------------------------------------------------------------------*/ void subRegionsRescale(void) { sdword index; sdword diffX = (MAIN_WindowWidth - 640) / 2; sdword diffY = (MAIN_WindowHeight - 480) / 2; for (index = 0; index < SUB_NumberRegions; index++) { if (subRegion[index].defaultRect.x1 != 0) { //if a region was defined if (subRegion[index].bScaleRezX) { //if x-scale in resolution subRegion[index].rect.x0 = subRegion[index].defaultRect.x0 * MAIN_WindowWidth / 640; subRegion[index].rect.x1 = subRegion[index].defaultRect.x1 * MAIN_WindowWidth / 640; } else { subRegion[index].rect.x0 = subRegion[index].defaultRect.x0 + diffX; subRegion[index].rect.x1 = subRegion[index].defaultRect.x1 + diffX; } if (subRegion[index].bScaleRezY) { //if y-scale in resolution subRegion[index].rect.y0 = subRegion[index].defaultRect.y0 * MAIN_WindowHeight / 480; subRegion[index].rect.y1 = subRegion[index].defaultRect.y1 * MAIN_WindowHeight / 480; } else { subRegion[index].rect.y0 = subRegion[index].defaultRect.y0 + diffY; subRegion[index].rect.y1 = subRegion[index].defaultRect.y1 + diffY; } } } } /*----------------------------------------------------------------------------- Name : subStartup Description : Starts the subtitle module, reading in the subtitle scripts Inputs : Outputs : Return : ----------------------------------------------------------------------------*/ void subStartup(void) { sdword index; #if SUB_VERBOSE_LEVEL >= 1 dbgMessage("\nStartup subtitle module"); #endif subNumberNewSubtitles = 0; #if SUB_VERBOSE_LEVEL >= 1 subMissedSubtitles = 0; #endif subSemaphore = CreateSemaphore(NULL, 1, 1, SUB_SemaphoreName); dbgAssert(subSemaphore != NULL); memset(&subRegion, 0, sizeof(subRegion)); //clear out the themes and printing regions memset(&subTheme, 0, sizeof(subTheme)); scriptSet(NULL, SUB_ScriptFile, subScriptTable); //load in the script file //post-setup of the output regions subRegionsRescale(); for (index = 0; index < SUB_NumberRegions; index++) { if (subRegion[index].defaultRect.x1 != 0) { //if a region was defined dbgAssert(subRegion[index].numberCards > 0 && subRegion[index].numberCards < UWORD_Max); subRegion[index].card = memAlloc(sizeof(subcard) * subRegion[index].numberCards, "subTitleCards", NonVolatile); subRegion[index].bEnabled = TRUE; subRegion[index].bAborted = FALSE; } } subRegion[STR_CloseCaptioned].bEnabled = subCloseCaptionsEnabled;//special case because these are enabled with a command-line switch subTextureInfoRemember(); } /*----------------------------------------------------------------------------- Name : subReset Description : To be called between levels, clears out any leftover subtitles from all regions. Inputs : void Outputs : Return : void ----------------------------------------------------------------------------*/ void subReset(void) { sdword index; //clear all text cards out of all regions for (index = 0; index < SUB_NumberRegions; index++) { subRegion[index].cardIndex = 0; } } /*----------------------------------------------------------------------------- Name : subTexturesReset Description : Resets the subtitle textures, re-coloring and ensureing the correct race of textures is loaded. Inputs : Outputs : Return : void ----------------------------------------------------------------------------*/ void subTexturesReset(void) { sdword index; char fileName[UBYTE_Max]; texreg *reg; for (index = 0; index < SUB_NumberRegions; index++) { subRegion[index].cardIndex = 0; } //clear all text cards out of all regions for (index = 0; index < SUB_NumberThemes; index++) { //recolorize the textures #if TR_NIL_TEXTURE if (subTheme[index].bPicture && (!GLOBAL_NO_TEXTURES)) #else if (subTheme[index].bPicture) #endif { //trTextureColorsUpdate(subTheme[index].picture, &teColorSchemes[subTheme[index].pictureColorScheme].textureColor); //replace the image with the image for the correct race. reg = trStructureGet(subTheme[index].picture); strcpy(fileName, reg->fileName); if (universe.players[0].race == R1) { //Name if texture is:"SpeechIcons\RX\BlahBlah" // replace this number: ^ #define SUB_NUMBER_INDEX 13 fileName[SUB_NUMBER_INDEX] = '1'; } else { fileName[SUB_NUMBER_INDEX] = '2'; } if (universe.players[0].race != subLastRace || memcmp(&subLastTeamColors[subTheme[index].pictureColorScheme], &teColorSchemes[subTheme[index].pictureColorScheme].textureColor, sizeof(trcolorinfo))) { //trTextureUnregister(subTheme[index].picture); trRegisterRemoval(subTheme[index].picture); subTheme[index].picture = trTextureRegister(fileName, &teColorSchemes[subTheme[index].pictureColorScheme].textureColor, (void *)&subTheme[index]); } } } subTextureInfoRemember(); } /*----------------------------------------------------------------------------- Name : subShutdown Description : Shuts down subtitle module, freeing memory as needed. Inputs : Outputs : Return : ----------------------------------------------------------------------------*/ void subShutdown(void) { sdword index; #if SUB_VERBOSE_LEVEL >= 1 dbgMessage("\nShutdown subtitle module"); #endif CloseHandle(subSemaphore); //free any allocated memory for (index = 0; index < SUB_NumberRegions; index++) { if (subRegion[index].card != NULL) { memFree(subRegion[index].card); } } } /*----------------------------------------------------------------------------- Name : subStringsChop Description : Chop up a long string to make it fit in the specified region using the specified type theme. Inputs : region - screen size to print into font - what font to print in? longLength - length of string we're chopping up. longString - string we are to be chopping up Outputs : chopBuffer - buffer they'll be chopped up into (in case NULL-terminators need to be added) choppedStrings - list of pointers to chopped lines. Return : number of chopped strings ----------------------------------------------------------------------------*/ sdword subStringsChop(rectangle *rect, fonthandle font, sdword longLength, char *longString, char *chopBuffer, char **choppedStrings) { sdword nChopped = 0; sdword nBytesUsed = 0; sdword width = rect->x1 - rect->x0; char *chopStart, *chopEnd, *nextChopStart, *wrapPtr; fonthandle current = fontCurrentGet(); sdword state; fontMakeCurrent(font); chopStart = longString; // while (chopStart - longString < longLength) // { //while there is still some string to chop up for (chopEnd = chopStart; *chopStart != 0; chopEnd++) { //find the string to fit on this line //see if there is a newline or end-of-string if ((chopEnd[0] == '#' || chopEnd[0] == '\\') && chopEnd[1] == 'n') { //"#n" or "\\n" - newline for (nextChopStart = chopEnd + 2; *nextChopStart; nextChopStart++) { //find next non-whitespace, non-newline character if (!strchr(" \t\n\r", *nextChopStart)) { break; } } } else if (chopEnd[0] == '\n' || chopEnd[0] == '\r') { // '\n' or '\r' for (nextChopStart = chopEnd + 1; *nextChopStart; nextChopStart++) { //find next non-whitespace, non-newline character if (!strchr(" \t\r", *nextChopStart)) { break; } } } else if (*chopEnd == 0) { //end of string nextChopStart = chopEnd; //last time through this loop } //!!! no support for dashes yet //note: this method of fontWidth'ing is rather inefficient because //fontWidth will continuously be parsing the same piece of string. Oh well. else if (fontWidthN(chopStart, chopEnd - chopStart + 1) > width) { //if string too long for the region it's in //scan backwards to find some whitespace to cut out state = 0; //search for whitespace state for (wrapPtr = chopEnd; wrapPtr > chopStart; wrapPtr--) { if (state == 0) { //search for whitespace state if (strchr("\t ", *wrapPtr)) { //if this is whitespace state = 1; //in the whitespace state nextChopStart = wrapPtr + 1; //this will be start of next line } } else { //in the whitespace state if (!strchr("\t ", *wrapPtr)) { wrapPtr++; //point to first of the whitespace break; } } } //did we find a whitespace break to wrap on? if (wrapPtr > chopStart) { //yes, we found some whitespace to wrap on chopEnd = wrapPtr; } else { //else no whitespace was found nextChopStart = chopEnd; //next line will start right at the character that went too long } } else { //still fits, keep going continue; } //now, the string from chopStart to chopEnd will be a line. memcpy(chopBuffer + nBytesUsed, chopStart, chopEnd - chopStart);//copy the string into the buffer chopBuffer[nBytesUsed + chopEnd - chopStart] = 0;//NULL-terminate it choppedStrings[nChopped] = chopBuffer + nBytesUsed;//set pointer to the string dbgAssert(nBytesUsed < SUB_SubtitleLength + SUB_MaxLinesPerSubtitle); dbgAssert(nChopped < SUB_MaxLinesPerSubtitle); nBytesUsed += chopEnd - chopStart + 1; //update usage count nChopped++; //and number of strings //chop the next line chopStart = nextChopStart; //set to start scanning next line chopEnd = chopStart - 1; } // } fontMakeCurrent(current); //restore the old font return(nChopped); //return number of strings chopped up } /*----------------------------------------------------------------------------- Name : subCreateScrollyLines Description : Compute scroll and fade information for these strings. Inputs : region - what scrolly text region we're adding them to theme - what type theme we're using nStrings - number of strings we're adding strings - array of string pointers speechTime - length of speech event, in seconds Outputs : Return : ----------------------------------------------------------------------------*/ void subCreateScrollyLines(subregion *region, subtheme *theme, sdword nStrings, char **strings, real32 speechTime) { sdword index, linesToScroll, linesInRegion; sdword x, y; real32 scrollDistance; real32 scrollDuration; real32 delay, duration, scrollSpeed; real32 totalHeight; fonthandle fhSave = fontCurrentGet(); fontMakeCurrent(theme->font); //figure out how far we'll have to scroll linesInRegion = (region->rect.y1 - region->rect.y0) / (fontHeight(" ") + SUB_InterlineSpacing); linesToScroll = nStrings - linesInRegion; totalHeight = (real32)(nStrings * (fontHeight(" ") + SUB_InterlineSpacing)); scrollDistance = (real32)(linesToScroll * (fontHeight(" ") + SUB_InterlineSpacing)); if (scrollDistance < 0.0f) { scrollDistance = 0.0f; //no scrolling needed } //how long will the scroll take? scrollDuration = speechTime - subScrollDwellStart - subScrollDwellEnd; //prep the region to scroll and display new text region->cardIndex = 0; //kill all previous cards if (scrollDistance > 0.0f) { //if we are to scroll at all region->textScrollStart = *subTimeElapsed + subScrollDwellStart; if (scrollDuration < 0) { //does that make 0 scroll time after all the dwelling? region->textScrollEnd = region->textScrollStart + subScrollShortest;//set some minimum scroll duration (only needed if somebody speaks really fast :) //!!! this is weird... fix me please!!! } else { //else we can scroll as normal region->textScrollEnd = region->textScrollStart + scrollDuration; } region->scrollDistance = -scrollDistance; //upward scroll is negative scrollSpeed = scrollDistance / (region->textScrollEnd - region->textScrollStart); } else { //else no scrolling scrollDuration = speechTime - subScrollDwellStart - subScrollDwellEnd;//just stay around for the duration of string region->scrollDistance = 0.0f; scrollSpeed = 0.0f; //should never be divided by anyhow } //now that we've computed our scroll time, let's actually add the lines y = 0; delay = 0.0f; for (index = 0; index < nStrings; index++) { #if SUB_ERROR_CHECKING if (index >= region->numberCards) { dbgFatalf(DBG_Loc, "Exceeded %d lines with the text '%s'.", region->numberCards, strings[index]); } #endif if (index < linesInRegion) { //if first lines to appear delay = 0; //make it show up right now if (index >= nStrings - linesInRegion) { //string is starting and ending string duration = subScrollDwellStart + subScrollDwellEnd + scrollDuration; } else { //else it'll scroll off the top duration = subScrollDwellStart + (real32)y / scrollSpeed; } } else { //else it will scroll onto screen //delay = (real32)y / scrollSpeed; //this is when it scrolls into view delay = subScrollDwellStart + (real32)((index - linesInRegion) * (fontHeight(" ") + SUB_InterlineSpacing)) / scrollSpeed; if (index >= nStrings - linesInRegion) { //if it is an ending string duration = (totalHeight - (real32)y) / scrollSpeed + subScrollDwellEnd; } else { //else it will scroll on the bottom and off the top duration = (real32)(linesInRegion * (fontHeight(" ") + SUB_InterlineSpacing)) / scrollSpeed; } } if (theme->bCentred) { x = ((region->rect.x1 - region->rect.x0) - fontWidth(strings[index])) / 2; } else { x = 0; } region->card[index].x = x; region->card[index].y = y; region->card[index].creationTime = *subTimeElapsed + delay; region->card[index].duration = duration; region->card[index].fadeIn = theme->fadeIn; region->card[index].fadeOut = theme->fadeOut; region->card[index].c = theme->textColor; region->card[index].bDropShadow = theme->bDropShadow; region->card[index].font = theme->font; region->card[index].text = strings[index]; y += fontHeight(" ") + SUB_InterlineSpacing; //move the next line down } region->cardIndex = index; dbgAssert(region->cardIndex < region->numberCards); if (theme->bPicture) { region->picture = theme->picture; } else { region->picture = TR_InvalidHandle; } fontMakeCurrent(fhSave); } /*----------------------------------------------------------------------------- Name : subCreateNonScrollyLines Description : Same as above, but the lines won't scroll; intead consecutive pages will fade in and out. Inputs : Outputs : Return : ----------------------------------------------------------------------------*/ void subCreateNonScrollyLines(subregion *region, subtheme *theme, sdword nStrings, char **strings, real32 speechTime) { sdword index, linesInRegion; sdword x, y; real32 delay; sdword nPages, iPage; real32 timePerPage; fonthandle fhSave = fontCurrentGet(); dbgAssert(nStrings != 0); fontMakeCurrent(theme->font); speechTime = max(speechTime, subTitleShortest); //prep the region region->cardIndex = 0; //kill all previous cards region->scrollDistance = 0; //figure out how many pages there will be linesInRegion = (region->rect.y1 - region->rect.y0) / (fontHeight(" ") + SUB_InterlineSpacing); if (linesInRegion == 0) { linesInRegion = 1; } nPages = nStrings / linesInRegion; if (nStrings % linesInRegion) { nPages++; } timePerPage = speechTime / (real32)nPages; //now that we've computed our scroll time, let's actually add the lines for (index = 0; index < nStrings; index++) { #if SUB_ERROR_CHECKING if (index >= region->numberCards) { dbgFatalf(DBG_Loc, "Exceeded %d lines with the text '%s'.", region->numberCards, strings[index]); } #endif if (index < linesInRegion) { //if first lines to appear delay = 0; //make it show up right now } else { //else it will scroll onto screen iPage = index / linesInRegion; //what page am I on? delay = (real32)iPage * timePerPage; } if (theme->bCentred) { x = ((region->rect.x1 - region->rect.x0) - fontWidth(strings[index])) / 2; } else { x = 0; } y = (index % linesInRegion) * (fontHeight(" ") + SUB_InterlineSpacing); region->card[index].x = x; region->card[index].y = y; region->card[index].creationTime = *subTimeElapsed + delay; region->card[index].duration = timePerPage; region->card[index].fadeIn = theme->fadeIn; region->card[index].fadeOut = theme->fadeOut; region->card[index].c = theme->textColor; region->card[index].bDropShadow = theme->bDropShadow; region->card[index].font = theme->font; region->card[index].text = strings[index]; } region->cardIndex = index; dbgAssert(region->cardIndex < region->numberCards); if (theme->bPicture) { region->picture = theme->picture; } else { region->picture = TR_InvalidHandle; } fontMakeCurrent(fhSave); region->bAborted = FALSE; } /*----------------------------------------------------------------------------- Name : subControlsScan Description : Scan for control escape sequences at the start of a string. Inputs : start - start of string to parse Outputs : region - region pointer to modify on #r or #c theme - theme pointer to modify on #t or #c bContinuous - set to TRUE on \+ or #+ Return : start of actual string ----------------------------------------------------------------------------*/ char *subControlsScan(char *start, subregion **region, subtheme **theme, bool8 *bContinuous) { sdword scannedIndex; while ((*start == '#' || *start == '\\') && *start != 0) { start++; switch (*start) { case '+': *bContinuous = TRUE; break; case 'r': start++; scannedIndex = *start - '0'; dbgAssert(scannedIndex >= 0 && scannedIndex < SUB_NumberRegions); *region = &subRegion[scannedIndex]; break; case 't': start++; if (*start >= '0' && *start <= '9') { scannedIndex = *start - '0'; } else { scannedIndex = 0xa + *start - 'a'; } dbgAssert(scannedIndex >= 0 && scannedIndex < SUB_NumberThemes); *theme = &subTheme[scannedIndex]; break; case 'c': *region = &subRegion[STR_CloseCaptioned]; *theme = &subTheme[STT_CloseCaptioned]; break; #if SUB_ERROR_CHECKING default: dbgFatalf(DBG_Loc, "Invalid format character '%c' in '%s'", *start, start); #endif } start++; } return(start); } /*----------------------------------------------------------------------------- Name : subTitlesUpdate Description : Called every frame, this function updates subtitles, handling asynchronous data transfer from the stream thread. Inputs : void Outputs : Return : void ----------------------------------------------------------------------------*/ void subTitlesUpdate(void) { sdword lastActor, lastSpeechEvent, totalLength, index; real32 totalTime; char fullNewStringBuffer[SUB_SubtitleLength], *fullNewString; char *choppedStrings[SUB_MaxLinesPerSubtitle]; sdword nChoppedStrings; subregion *region; subtheme *theme; char *newString; bool8 bContinuous = FALSE, bContinuousIgnore; subTimeElapsed = &universe.totaltimeelapsed; #if SUB_MODULE_TEST { sdword key; for (key = ONEKEY; key <= NINEKEY; key++) { if (keyIsStuck(key)) { keyClearSticky(key); if (subTestSubtitle[key - ONEKEY] != NULL) { subTitleAdd(0, subTestSubtitle[key - ONEKEY], strlen(subTestSubtitle[key - ONEKEY]), subTestSubtitleTime[key - ONEKEY]); } } } } #endif //SUB_MODULE_TEST WaitForSingleObject(subSemaphore, INFINITE); //make sure nobody else is working with the subtitle transfer buffers if (subNumberNewSubtitles > 0) { //because subtitles are delivered as a series of phrases, we must "stitch" //them together here. To do this, we compare the actor index of adjacent //new subtitles. We may later want to add a more accurate method of //concatenation, such as the speech event index. for (index = 0; index < subNumberNewSubtitles; index++) { strcpy(fullNewStringBuffer, subNewSubtitles[index].text);//intialize the concatenation string lastActor = subNewSubtitles[index].actor; //!!! this is the concatenation criteria region = &subRegion[STR_LetterboxBar]; //default region to print to theme = &subTheme[lastActor]; //default theme for this actor lastSpeechEvent = subNewSubtitles[index].speechEvent; fullNewString = subControlsScan(fullNewStringBuffer, ®ion, &theme, &bContinuous); totalLength = strlen(fullNewString); totalTime = subNewSubtitles[index].time; while (index + 1 < subNumberNewSubtitles && subNewSubtitles[index + 1].actor == lastActor && subNewSubtitles[index + 1].speechEvent == lastSpeechEvent) { index++; dbgAssert(totalLength + strlen(subNewSubtitles[index].text) < SUB_SubtitleLength); strcat(fullNewString, " "); //put a space between phrases. totalLength++; newString = subNewSubtitles[index].text; newString = subControlsScan(subNewSubtitles[index].text, ®ion, &theme, &bContinuousIgnore); strcpy(fullNewString + totalLength, newString);//append the new string totalLength = strlen(fullNewString); //get new total length totalTime += subNewSubtitles[index].time; //add all the times together } } #if SUB_VERBOSE_LEVEL >= 1 dbgMessagef("\nSubtitle: actor %d says 0x%x '%s' (%.2f seconds).", lastActor, lastSpeechEvent, fullNewString, totalTime); #endif //now that we have a concatenated string; let's chop it up to wrap it around nChoppedStrings = subStringsChop(®ion->rect, theme->font, totalLength, fullNewString, region->chopBuffer, choppedStrings); #if SUB_VERBOSE_LEVEL >= 2 if (nChoppedStrings > 1) { sdword index; dbgMessage("\nChopped up like this:"); for (index = 0; index < nChoppedStrings; index++) { dbgMessagef("\n'%s'", choppedStrings[index]); } } #endif //string is all chopped up. Let's figure out how long/far to scroll and how long to stick around at the ends. if (strCurLanguage >= 1) { //use the standard timing for English; scrolly timing for forign subCreateScrollyLines(region, theme, nChoppedStrings, choppedStrings, totalTime); } else { subCreateNonScrollyLines(region, theme, nChoppedStrings, choppedStrings, totalTime); } region->bContinuousEvent = bContinuous; subNumberNewSubtitles = 0; //discard all processed new subtitles } //notify by debug message if we missed any phrases #if SUB_VERBOSE_LEVEL >= 1 if (subMissedSubtitles > 0) { dbgMessagef("\nsubtitle: %d subtitles were missed.", subMissedSubtitles); subMissedSubtitles = 0; } #endif ReleaseSemaphore(subSemaphore, 1, NULL); } /*----------------------------------------------------------------------------- Name : subTitleAdd Description : Display a subtitle. This function called from the speech streamer when a speech event is streamed in. Inputs : actor - integer for an actor. This is used to determine where to print the speech even, plus fonts and whanot. speechEvnt - what speechEvent it was, straight out of speechevent.h text - text to print. Must be in proper language when passed here. length - length of text string, same as what you would get from a strlen() call. time - length of text Outputs : Return : ? ----------------------------------------------------------------------------*/ sdword subTitleAdd(sdword actor, sdword speechEvent, char *text, sdword length, real32 time) { /* #if SUB_VERBOSE_LEVEL >= 1 dbgMessagef("\nSubtitle: actor %d says '%s'.", actor, text); #endif */ WaitForSingleObject(subSemaphore, INFINITE); //make sure nobody else is working with the subtitle transfer buffers if (subNumberNewSubtitles < SUB_NumberNewSubtitles - 1) { dbgAssert(length < SUB_SubtitleLength); subNewSubtitles[subNumberNewSubtitles].actor = actor; subNewSubtitles[subNumberNewSubtitles].speechEvent = speechEvent; subNewSubtitles[subNumberNewSubtitles].length = length; subNewSubtitles[subNumberNewSubtitles].time = time; //strcpy(subNewSubtitles[subNumberNewSubtitles].text, text); memcpy(subNewSubtitles[subNumberNewSubtitles].text, text, length); subNewSubtitles[subNumberNewSubtitles].text[length] = 0; subNumberNewSubtitles++; } else { #if SUB_VERBOSE_LEVEL >= 1 subMissedSubtitles++; #endif } ReleaseSemaphore(subSemaphore, 1, NULL); return(OKAY); } /*----------------------------------------------------------------------------- Name : subTitlesDraw Description : Render the subtitles in a given region. Inputs : region - what region to render the subtitles for. Outputs : may delete one or more of the text cards in the region because they time out. Return :void ----------------------------------------------------------------------------*/ void subTitlesDraw(subregion *region) { sdword index; fonthandle fhSave = fontCurrentGet(); char *lineStart;//, *newLine; sdword x, y; subcard *card; color c; real32 fadeValue; udword multiplier; sdword scroll; real32 scrollFloat; #if SUB_VISUALIZE_REGION primRectSolid2(®ion->rect, colBlack); primRectOutline2(®ion->rect, 1, colWhite); #endif if ((!mrRenderMainScreen && !smFleetIntel) && region == &subRegion[STR_LetterboxBar]) { //if some full screen gui is up rndTextureEnable(FALSE); glColor4f(0.0f, 0.0f, 0.0f, 0.0f); glBegin(GL_QUADS); //top scissor part glVertex2f(primScreenToGLX(-1), primScreenToGLY(0)); glVertex2f(primScreenToGLX(-1), primScreenToGLY(NIS_LetterHeight)); glVertex2f(primScreenToGLX(MAIN_WindowWidth), primScreenToGLY(NIS_LetterHeight)); glVertex2f(primScreenToGLX(MAIN_WindowWidth), primScreenToGLY(0)); glEnd(); //ferDrawBoxRegion(region->rect, ferMediumTextures, ferInternalGlow, NULL, FALSE); } //scroll the text, if applicable scroll = region->rect.y0; if (region->scrollDistance != 0.0f) { //if there is a scroll to go on if (*subTimeElapsed >= region->textScrollStart) { //if the scroll has started if (*subTimeElapsed <= region->textScrollEnd) { //if currently scrolling scrollFloat = region->scrollDistance * (*subTimeElapsed - region->textScrollStart) / (region->textScrollEnd - region->textScrollStart); } else { //else scroll has ended scrollFloat = region->scrollDistance; } scroll = (sdword)scrollFloat + region->rect.y0; } } for (index = 0, card = region->card; index < region->cardIndex; index++, card++) { if (*subTimeElapsed >= card->creationTime) { //if card has started up properly if (*subTimeElapsed - card->creationTime > card->duration) { //if card elapses // dbgMessagef("XXXX%10s", region->card[index].text); if (region->cardIndex > 1) { region->card[index] = region->card[region->cardIndex - 1]; } region->cardIndex--; index--; card = ®ion->card[index]; continue; } fontMakeCurrent(card->font); lineStart = card->text; //newLine = strstr(lineStart, NIS_NewLine); x = card->x + region->rect.x0; y = card->y + scroll; //scroll the text, if applicable /* if (card->scroll != 0.0f) { //if the text is scrolling y += (sdword)(card->scroll * //move to scrolled-to position (card->NIS->timeElapsed - card->creationTime) / card->duration); } */ c = card->c; //fade the text in, if applicable if (card->fadeIn != 0.0f) { fadeValue = (*subTimeElapsed - card->creationTime) / card->fadeIn; if (fadeValue >= 1.0f) { card->fadeIn = 0.0f; fadeValue = 1.0f; } multiplier = (udword)(fadeValue * 256.0f); c = colRGB(colRed(c) * multiplier / 256, colGreen(c) * multiplier / 256, colBlue(c) * multiplier / 256); } //fade the the text out, if applicable if (card->fadeOut != 0.0f) { //if there's a fadeout if (*subTimeElapsed >= card->creationTime + card->duration - card->fadeOut) { //if it in the fading part fadeValue = (*subTimeElapsed - (card->creationTime + card->duration - card->fadeOut)) / card->fadeOut; fadeValue = max(0.0, 1.0f - fadeValue); multiplier = (udword)(fadeValue * 256.0f); c = colRGB(colRed(c) * multiplier / 256, colGreen(c) * multiplier / 256, colBlue(c) * multiplier / 256); // dbgMessagef("<0x%2x>", multiplier); } } //draw the text /* while (newLine != NULL) { fontPrintN(x, y, card->c, lineStart, newLine - lineStart); y += fontHeight(" ") + 1; x = card->margin; newLine += NIS_NewLineLength; lineStart = newLine; newLine = strstr(newLine, NIS_NewLine); } */ if (card->bDropShadow) { fontShadowSet(FS_SE, colBlack); } fontPrint(x, y, c, lineStart); } } if (region->picture != TR_InvalidHandle) { rectangle rect; rect.x0 = region->rect.x0 + region->iconOffsetX; rect.y0 = region->rect.y0 + region->iconOffsetY; rect.x1 = rect.x0 + SUB_PictureWidth; rect.y1 = rect.y0 + SUB_PictureHeight; trMakeCurrent(region->picture); glDisable(GL_ALPHA_TEST); glEnable(GL_BLEND); if (RGLtype == SWtype) { glTexCoord2f((real32)(-rect.x0), (real32)(-rect.y1)); glDrawPixels(SUB_PictureWidth, SUB_PictureHeight, GL_RGBA, GL_UNSIGNED_BYTE, NULL); } else { primRectSolidTextured2(&rect); } glDisable(GL_BLEND); } if (region->cardIndex == 0 && region == &subRegion[STR_LetterboxBar] && (!region->bAborted) && !region->bContinuousEvent) { //if all fleet intel/command cards expired subMessageEnded = 1; } fontShadowSet(FS_NONE, colBlack); fontMakeCurrent(fhSave); } /*----------------------------------------------------------------------------- Name : subTitlesFadeOut Description : Causes all remaining subtitles in a region to fade out over a specified period. Inputs : region - what region to fade teh subtitles out of fadeTime - duration of fadeout Outputs : Return : void ----------------------------------------------------------------------------*/ void subTitlesFadeOut(subregion *region, real32 fadeTime) { sdword index; subcard *card; dbgAssert(region != NULL); for (index = 0, card = region->card; index < region->cardIndex; index++, card++) { if (card->creationTime > *subTimeElapsed) { //if card not started created yet card->creationTime = *subTimeElapsed; card->duration = -1; //make sure it never gets drawn continue; } if (*subTimeElapsed - card->creationTime + fadeTime < card->duration) { //if the card isn't going to fade out before the time specified on it's own card->fadeOut = fadeTime; //start fading it out card->duration = *subTimeElapsed - card->creationTime + fadeTime;//kill it when it's faded } } region->bAborted = TRUE; }