1 /*
2 ===========================================================================
3 Copyright (C) 2000 - 2013, Raven Software, Inc.
4 Copyright (C) 2001 - 2013, Activision, Inc.
5 Copyright (C) 2013 - 2015, OpenJK contributors
6 
7 This file is part of the OpenJK source code.
8 
9 OpenJK is free software; you can redistribute it and/or modify it
10 under the terms of the GNU General Public License version 2 as
11 published by the Free Software Foundation.
12 
13 This program is distributed in the hope that it will be useful,
14 but WITHOUT ANY WARRANTY; without even the implied warranty of
15 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16 GNU General Public License for more details.
17 
18 You should have received a copy of the GNU General Public License
19 along with this program; if not, see <http://www.gnu.org/licenses/>.
20 ===========================================================================
21 */
22 
23 // Filename:-	cg_credits.cpp
24 //
25 // module for end credits code
26 
27 #include "cg_local.h"
28 #include "cg_media.h"
29 
30 
31 #define fCARD_FADESECONDS		1.0f	// fade up time, also fade down time
32 #define fCARD_SUSTAINSECONDS	2.0f	// hold time before fade down
33 #define fLINE_SECONDTOSCROLLUP	15.0f	// how long one line takes to scroll up the screen
34 
35 
36 #define MAX_LINE_BYTES 2048
37 
38 qhandle_t ghFontHandle = 0;
39 float gfFontScale = 1.0f;
40 vec4_t gv4Color = {0};
41 
42 struct StringAndSize_t
43 {
44 	int iStrLenPixels;
45 	std::string str;
46 
StringAndSize_tStringAndSize_t47 	StringAndSize_t()
48 	{
49 		iStrLenPixels = -1;
50 		str = "";
51 	}
StringAndSize_tStringAndSize_t52 	StringAndSize_t(const char *psString)
53 	{
54 		iStrLenPixels = -1;
55 		str = psString;
56 	}
operator =StringAndSize_t57 	StringAndSize_t & operator = (const char *psString)
58 	{
59 		iStrLenPixels = -1;
60 		str = psString;
61 		return *this;
62 	}
63 
c_strStringAndSize_t64 	const char *c_str(void)
65 	{
66 		return str.c_str();
67 	}
68 
GetPixelLengthStringAndSize_t69 	int GetPixelLength(void)
70 	{
71 		if (iStrLenPixels == -1)
72 		{
73 			iStrLenPixels = cgi_R_Font_StrLenPixels(str.c_str(), ghFontHandle, gfFontScale);
74 		}
75 
76 		return iStrLenPixels;
77 	}
78 
IsEmptyStringAndSize_t79 	bool IsEmpty(void)
80 	{
81 		return str.empty();
82 	}
83 };
84 
85 struct CreditCard_t
86 {
87 	int						iTime;
88 	StringAndSize_t			strTitle;
89 	std::vector<StringAndSize_t> vstrText;
90 
CreditCard_tCreditCard_t91 	CreditCard_t()
92 	{
93 		iTime = -1;	// flag "not set yet"
94 	}
95 };
96 
97 struct CreditLine_t
98 {
99 	int						iLine;
100 	StringAndSize_t			strText;
101 	std::vector<StringAndSize_t> vstrText;
102 	bool					bDotted;
103 };
104 
105 typedef std::list <CreditLine_t>		CreditLines_t;
106 typedef std::list <CreditCard_t>		CreditCards_t;
107 
108 struct CreditData_t
109 {
110 	int			iStartTime;
111 
112 	CreditCards_t CreditCards;
113 	CreditLines_t CreditLines;
114 
RunningCreditData_t115 	qboolean Running(void)
116 	{
117 		return (qboolean)(CreditCards.size() || CreditLines.size());
118 	}
119 };
120 
121 CreditData_t CreditData;
122 
123 
Capitalize(const char * psTest)124 static const char *Capitalize( const char *psTest )
125 {
126 	static char sTemp[MAX_LINE_BYTES];
127 
128 	Q_strncpyz(sTemp, psTest, sizeof(sTemp));
129 
130 	if (!cgi_Language_IsAsian())
131 	{
132 		Q_strupr(sTemp);	// capitalise titles (if not asian!!!!)
133 	}
134 
135 	return sTemp;
136 }
137 
CountsAsWhiteSpaceForCaps(char c)138 static bool CountsAsWhiteSpaceForCaps( char c )
139 {
140 	return !!(isspace(c) || c == '-' || c == '.' || c == '(' || c == ')');
141 }
UpperCaseFirstLettersOnly(const char * psTest)142 static const char *UpperCaseFirstLettersOnly( const char *psTest )
143 {
144 	static char sTemp[MAX_LINE_BYTES];
145 
146 	Q_strncpyz(sTemp, psTest, sizeof(sTemp));
147 
148 	if (!cgi_Language_IsAsian())
149 	{
150 		Q_strlwr(sTemp);
151 
152 		char *p = sTemp;
153 		while (*p)
154 		{
155 			while (*p && CountsAsWhiteSpaceForCaps(*p)) p++;//(isspace(*p) || *p == '-' || *p == '.')) p++;	// also copes with hyphenated names (awkward gits)
156 			if (*p)
157 			{
158 				*p = toupper(*p);
159 				while (*p && !CountsAsWhiteSpaceForCaps(*p)) p++;	// cope with hyphenated names and initials (awkward gits)
160 			}
161 		}
162 	}
163 
164 	// now restore any weird stuff...
165 	//
166 	char *p = strstr(sTemp," Mc");	// eg "Mcfarrell" should be "McFarrell"
167 	if (p && isalpha(p[3]))
168 	{
169 		p[3] = toupper(p[3]);
170 	}
171 	p = strstr(sTemp," O'");	// eg "O'flaherty" should be "O'Flaherty"
172 	if (p && isalpha(p[3]))
173 	{
174 		p[3] = toupper(p[3]);
175 	}
176 	p = strstr(sTemp,"Lucasarts");
177 	if (p)
178 	{
179 		p[5] = 'A';	// capitalise the 'A' in LucasArts (jeez...)
180 	}
181 
182 	return sTemp;
183 }
184 
GetSubString(std::string & strResult)185 static const char *GetSubString(std::string &strResult)
186 {
187 	static char sTemp[MAX_LINE_BYTES];
188 
189 	if (!strlen(strResult.c_str()))
190 		return NULL;
191 
192 	Q_strncpyz(sTemp,strResult.c_str(),sizeof(sTemp));
193 
194 	char *psSemiColon = strchr(sTemp,';');
195 	if (  psSemiColon)
196 	{
197 		 *psSemiColon = '\0';
198 
199 		 strResult.erase(0,(psSemiColon-sTemp)+1);
200 	}
201 	else
202 	{
203 		// no semicolon found, probably last entry? (though i think even those have them on, oh well)
204 		//
205 		strResult.erase();
206 	}
207 
208 	return sTemp;
209 }
210 
211 // sort entries by their last name (starts at back of string and moves forward until start or just before whitespace)
212 // ...
SortBySurname(const StringAndSize_t & str1,const StringAndSize_t & str2)213 static bool SortBySurname(const StringAndSize_t &str1, const StringAndSize_t &str2)
214 {
215 	std::string::const_reverse_iterator rstart1 = std::find_if(str1.str.rbegin(), str1.str.rend(), isspace);
216 	std::string::const_reverse_iterator rstart2 = std::find_if(str2.str.rbegin(), str2.str.rend(), isspace);
217 
218 
219 	return Q_stricmp(&*rstart1.base(), &*rstart2.base()) < 0;
220 }
221 
222 
223 
CG_Credits_Init(const char * psStripReference,vec4_t * pv4Color)224 void CG_Credits_Init( const char *psStripReference, vec4_t *pv4Color )
225 {
226 	// could make these into parameters later, but for now...
227 	//
228 	ghFontHandle = cgs.media.qhFontMedium;
229 	gfFontScale = 1.0f;
230 
231 	memcpy(gv4Color,pv4Color,sizeof(gv4Color));	// memcpy so we can poke into alpha channel
232 
233 	// first, ask the strlen of the final string...
234 	//
235 	int iStrLen = cgi_SP_GetStringTextString( psStripReference, NULL, 0 );
236 	if (!iStrLen)
237 	{
238 #ifndef FINAL_BUILD
239 		Com_Printf("WARNING: CG_Credits_Init(): invalid text key :'%s'\n", psStripReference);
240 #endif
241 		return;
242 	}
243 	//
244 	// malloc space to hold it...
245 	//
246 	char *psMallocText = (char *) cgi_Z_Malloc( iStrLen+1, TAG_TEMP_WORKSPACE );
247 	//
248 	// now get the string...
249 	//
250 	iStrLen = cgi_SP_GetStringTextString( psStripReference, psMallocText, iStrLen+1 );
251 	//ensure we found a match
252 	if (!iStrLen)
253 	{
254 		assert(0);	// should never get here now, but wtf?
255 		cgi_Z_Free(psMallocText);
256 #ifndef FINAL_BUILD
257 		Com_Printf("WARNING: CG_Credits_Init(): invalid text key :'%s'\n", psStripReference);
258 #endif
259 		return;
260 	}
261 
262 	// read whole string in and process as cards, lines etc...
263 	//
264 	typedef enum
265 	{
266 		eNothing = 0,
267 		eLine,
268 		eDotEntry,
269 		eTitle,
270 		eCard,
271 		eFinished,
272 	} Mode_e;
273 	Mode_e eMode = eNothing;
274 
275 	qboolean bCardsFinished = qfalse;
276 	int iLineNumber = 0;
277 	const char *psTextParse = psMallocText;
278 	while (*psTextParse != '\0')
279 	{
280 		// read a line...
281 		//
282 		char sLine[MAX_LINE_BYTES];
283 			 sLine[0]='\0';
284 		qboolean bWasCommand = qtrue;
285 		while (1)
286 		{
287 			qboolean bIsTrailingPunctuation;
288 			unsigned int uiLetter = cgi_AnyLanguage_ReadCharFromString(&psTextParse, &bIsTrailingPunctuation);
289 
290 			// concat onto string so far...
291 			//
292 			if (uiLetter == 32 && sLine[0] == '\0')
293 			{
294 				continue;	// unless it's a space at the start of a line, in which case ignore it.
295 			}
296 
297 			if (uiLetter == '\n' || uiLetter == '\0' )
298 			{
299 				// have we got a command word?...
300 				//
301 				if (!Q_stricmpn(sLine,"(#",2))
302 				{
303 					// yep...
304 					//
305 					if (!Q_stricmp(sLine, "(#CARD)"))
306 					{
307 						if (!bCardsFinished)
308 						{
309 							eMode = eCard;
310 						}
311 						else
312 						{
313 							#ifndef FINAL_BUILD
314 							Com_Printf( S_COLOR_YELLOW "CG_Credits_Init(): No current support for cards after scroll!\n" );
315 							#endif
316 							eMode = eNothing;
317 						}
318 						break;
319 					}
320 					else
321 					if (!Q_stricmp(sLine, "(#TITLE)"))
322 					{
323 						eMode = eTitle;
324 						bCardsFinished = qtrue;
325 						break;
326 					}
327 					else
328 					if (!Q_stricmp(sLine, "(#LINE)"))
329 					{
330 						eMode = eLine;
331 						bCardsFinished = qtrue;
332 						break;
333 					}
334 					else
335 					if (!Q_stricmp(sLine, "(#DOTENTRY)"))
336 					{
337 						eMode = eDotEntry;
338 						bCardsFinished = qtrue;
339 						break;
340 					}
341 					else
342 					{
343 						#ifndef FINAL_BUILD
344 						Com_Printf( S_COLOR_YELLOW "CG_Credits_Init(): bad keyword \"%s\"!\n", sLine );
345 						#endif
346 						eMode = eNothing;
347 					}
348 				}
349 				else
350 				{
351 					// I guess not...
352 					//
353 					bWasCommand = qfalse;
354 					break;
355 				}
356 			}
357 			else
358 			{
359 				// must be a letter...
360 				//
361 				if (uiLetter > 255)
362 				{
363 					Q_strcat(sLine, sizeof(sLine), va("%c%c",uiLetter >> 8, uiLetter & 0xFF));
364 				}
365 				else
366 				{
367 					Q_strcat(sLine, sizeof(sLine), va("%c",uiLetter & 0xFF));
368 				}
369 			}
370 		}
371 
372 		// command?...
373 		//
374 		if (bWasCommand)
375 		{
376 			// this'll just be a mode change, so ignore...
377 			//
378 		}
379 		else
380 		{
381 			// else we've got some text to display...
382 			//
383 			switch (eMode)
384 			{
385 				case eNothing:	break;
386 				case eLine:
387 				{
388 					CreditLine_t	CreditLine;
389 									CreditLine.iLine	= iLineNumber++;
390 									CreditLine.strText	= sLine;
391 
392 					CreditData.CreditLines.push_back( CreditLine );
393 				}
394 				break;
395 
396 				case eDotEntry:
397 				{
398 					CreditLine_t	CreditLine;
399 									CreditLine.iLine	= iLineNumber;
400 									CreditLine.bDotted	= true;
401 
402 					std::string strResult(sLine);
403 					const char *p;
404 					while ((p=GetSubString(strResult)) != NULL)
405 					{
406 						if (CreditLine.strText.IsEmpty())
407 						{
408 							CreditLine.strText = p;
409 						}
410 						else
411 						{
412 							CreditLine.vstrText.push_back( UpperCaseFirstLettersOnly(p) );
413 						}
414 					}
415 
416 					if (!CreditLine.strText.IsEmpty() && CreditLine.vstrText.size())
417 					{
418 						// sort entries RHS dotted entries by alpha...
419 						//
420 						std::sort( CreditLine.vstrText.begin(), CreditLine.vstrText.end(), SortBySurname );
421 
422 						CreditData.CreditLines.push_back( CreditLine );
423 						iLineNumber += CreditLine.vstrText.size();
424 					}
425 				}
426 				break;
427 
428 				case eTitle:
429 				{
430 					iLineNumber++;	// leading blank line
431 
432 					CreditLine_t	CreditLine;
433 									CreditLine.iLine	= iLineNumber++;
434 									CreditLine.strText	= Capitalize(sLine);
435 
436 					CreditData.CreditLines.push_back( CreditLine );
437 
438 					iLineNumber++;	// trailing blank line
439 					break;
440 				}
441 				case eCard:
442 				{
443 					CreditCard_t CreditCard;
444 
445 					std::string strResult(sLine);
446 					const char *p;
447 					while ((p=GetSubString(strResult)) != NULL)
448 					{
449 						if (CreditCard.strTitle.IsEmpty())
450 						{
451 							CreditCard.strTitle = Capitalize( p );
452 						}
453 						else
454 						{
455 							CreditCard.vstrText.push_back( UpperCaseFirstLettersOnly( p ) );
456 						}
457 					}
458 
459 					if (!CreditCard.strTitle.IsEmpty())
460 					{
461 						// sort entries by alpha...
462 						//
463 						std::sort( CreditCard.vstrText.begin(), CreditCard.vstrText.end(), SortBySurname );
464 
465 						CreditData.CreditCards.push_back(CreditCard);
466 					}
467 				}
468 				break;
469 				default:
470 				break;
471 			}
472 		}
473 	}
474 
475 	cgi_Z_Free(psMallocText);
476 	CreditData.iStartTime = cg.time;
477 }
478 
CG_Credits_Running(void)479 qboolean CG_Credits_Running( void )
480 {
481 	return CreditData.Running();
482 }
483 
484 // returns qtrue if still drawing...
485 //
CG_Credits_Draw(void)486 qboolean CG_Credits_Draw( void )
487 {
488 	if ( CG_Credits_Running() )
489 	{
490 		const int iFontHeight = (int) (1.5f * (float) cgi_R_Font_HeightPixels(ghFontHandle, gfFontScale));	// taiwanese & japanese need 1.5 fontheight spacing
491 
492 //		cgi_R_SetColor( *gpv4Color );
493 
494 		// display cards first...
495 		//
496 		if (CreditData.CreditCards.size())
497 		{
498 			// grab first card off the list (we know there's at least one here, so...)
499 			//
500 			CreditCard_t &CreditCard = (*CreditData.CreditCards.begin());
501 
502 			if (CreditCard.iTime == -1)
503 			{
504 				// onceonly time init...
505 				//
506 				CreditCard.iTime = cg.time;
507 			}
508 
509 			// play with the alpha channel for fade up/down...
510 			//
511 			const float fMilliSecondsElapsed = cg.time - CreditCard.iTime;
512 			const float fSecondsElapsed		 = fMilliSecondsElapsed / 1000.0f;
513 			if (fSecondsElapsed < fCARD_FADESECONDS)
514 			{
515 				// fading up...
516 				//
517 				gv4Color[3] = fSecondsElapsed / fCARD_FADESECONDS;
518 //				OutputDebugString(va("fade up: %f\n",gv4Color[3]));
519 			}
520 			else
521 			if (fSecondsElapsed > fCARD_FADESECONDS + fCARD_SUSTAINSECONDS)
522 			{
523 				// fading down...
524 				//
525 				const float fFadeDownSeconds = fSecondsElapsed - (fCARD_FADESECONDS + fCARD_SUSTAINSECONDS);
526 				gv4Color[3] = 1.0f - (fFadeDownSeconds / fCARD_FADESECONDS);
527 //				OutputDebugString(va("fade dw: %f\n",gv4Color[3]));
528 			}
529 			else
530 			{
531 				gv4Color[3] = 1.0f;
532 //				OutputDebugString(va("normal: %f\n",gv4Color[3]));
533 			}
534 			if (gv4Color[3] < 0.0f)
535 				gv4Color[3] = 0.0f;	// ... otherwise numbers that have dipped slightly -ve flash up fullbright after fade down
536 
537 			//
538 			// how many lines is it?
539 			//
540 			int iLines = CreditCard.vstrText.size() + 2;	// +2 for title itself & one seperator line
541 			//
542 			int iYpos = (SCREEN_HEIGHT - (iLines * iFontHeight))/2;
543 			//
544 			// draw it, title first...
545 			//
546 			int iWidth = CreditCard.strTitle.GetPixelLength();
547 			int iXpos  = (SCREEN_WIDTH - iWidth)/2;
548 			cgi_R_Font_DrawString(iXpos, iYpos, CreditCard.strTitle.c_str(), gv4Color, ghFontHandle, -1, gfFontScale);
549 			//
550 			iYpos += iFontHeight*2;	// skip blank line then move to main pos
551 			//
552 			for (size_t i=0; i<CreditCard.vstrText.size(); i++)
553 			{
554 				StringAndSize_t &StringAndSize = CreditCard.vstrText[i];
555 				iWidth = StringAndSize.GetPixelLength();
556 				iXpos  = (SCREEN_WIDTH - iWidth)/2;
557 				cgi_R_Font_DrawString(iXpos, iYpos, StringAndSize.c_str(), gv4Color, ghFontHandle, -1, gfFontScale);
558 				iYpos += iFontHeight;
559 			}
560 
561 			// next card?...
562 			//
563 			if (fSecondsElapsed > fCARD_FADESECONDS + fCARD_SUSTAINSECONDS + fCARD_FADESECONDS)
564 			{
565 				// yep, so erase the first entry (which will trigger the next one to be initialised on re-entry)...
566 				//
567 				CreditData.CreditCards.erase( CreditData.CreditCards.begin() );
568 
569 				if (!CreditData.CreditCards.size())
570 				{
571 					// all cards gone, so re-init timer for lines...
572 					//
573 					CreditData.iStartTime = cg.time;
574 				}
575 			}
576 			//
577 			return qtrue;
578 		}
579 		else
580 		{
581 			// doing scroll text...
582 			//
583 			if (CreditData.CreditLines.size())
584 			{
585 				// process all lines...
586 				//
587 				const float fMilliSecondsElapsed = cg.time - CreditData.iStartTime;
588 				const float fSecondsElapsed		 = fMilliSecondsElapsed / 1000.0f;
589 
590 				bool bEraseOccured = false;
591 				for (CreditLines_t::iterator it = CreditData.CreditLines.begin(); it != CreditData.CreditLines.end(); bEraseOccured ? it : ++it)
592 				{
593 					CreditLine_t &CreditLine = (*it);
594 					bEraseOccured = false;
595 
596 					static const float fPixelsPerSecond = ((float)SCREEN_HEIGHT / fLINE_SECONDTOSCROLLUP);
597 
598 					int iYpos = SCREEN_HEIGHT + (CreditLine.iLine * iFontHeight);
599 						iYpos-= (int) (fPixelsPerSecond * fSecondsElapsed);
600 
601 					int iTextLinesThisItem = Q_max( (int)CreditLine.vstrText.size(), 1);
602 					if (iYpos + (iTextLinesThisItem * iFontHeight) < 0)
603 					{
604 						// scrolled off top of screen, so erase it...
605 						//
606 						it = CreditData.CreditLines.erase( it );
607 						bEraseOccured = true;
608 					}
609 					else
610 					if (iYpos < SCREEN_HEIGHT)
611 					{
612 						// onscreen, so print it...
613 						//
614 						bool bIsDotted = !!CreditLine.vstrText.size();	// eg "STUNTS ...................... MR ED"
615 
616 						int iWidth = CreditLine.strText.GetPixelLength();
617 						int iXpos  = bIsDotted ? 4 : (SCREEN_WIDTH - iWidth)/2;
618 
619 						gv4Color[3] = 1.0f;
620 
621 						cgi_R_Font_DrawString(iXpos, iYpos, CreditLine.strText.c_str(), gv4Color, ghFontHandle, -1, gfFontScale);
622 
623 						// now print any dotted members...
624 						//
625 						for (size_t i=0; i<CreditLine.vstrText.size(); i++)
626 						{
627 							StringAndSize_t &StringAndSize = CreditLine.vstrText[i];
628 							iWidth = StringAndSize.GetPixelLength();
629 							iXpos  = (SCREEN_WIDTH-4 - iWidth);
630 							cgi_R_Font_DrawString(iXpos, iYpos, StringAndSize.c_str(), gv4Color, ghFontHandle, -1, gfFontScale);
631 							iYpos += iFontHeight;
632 						}
633 					}
634 				}
635 
636 				return qtrue;
637 			}
638 		}
639 	}
640 
641 	return qfalse;
642 }
643 
644 
645 
646 ////////////////////// eof /////////////////////
647 
648