//Copyright Paul Reiche, Fred Ford. 1992-2002

/*
 *  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 2 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, write to the Free Software
 *  Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
 */

#define COMM_INTERNAL
#include "comm.h"

#include "build.h"
#include "commanim.h"
#include "commglue.h"
#include "controls.h"
#include "colors.h"
#include "sis.h"
#include "units.h"
#include "encount.h"
#include "starmap.h"
#include "endian_uqm.h"
#include "gamestr.h"
#include "options.h"
#include "oscill.h"
#include "save.h"
#include "settings.h"
#include "setup.h"
#include "sounds.h"
#include "nameref.h"
#include "uqmdebug.h"
#include "libs/graphics/gfx_common.h"
#include "libs/inplib.h"
#include "libs/sound/sound.h"
#include "libs/sound/trackplayer.h"
#include "libs/log.h"

#include <ctype.h>

#define MAX_RESPONSES 8
#define BACKGROUND_VOL (usingSpeech ? (NORMAL_VOLUME / 2) : NORMAL_VOLUME)
#define FOREGROUND_VOL NORMAL_VOLUME

// Oscilloscope frame rate
// Should be <= COMM_ANIM_RATE
// XXX: was 32 picked experimentally?
#define OSCILLOSCOPE_RATE   (ONE_SECOND / 32)

// Maximum comm animation frame rate (actual execution rate)
// A gfx frame is not always produced during an execution frame,
// and several animations are combined into one gfx frame.
// The rate was originally 120fps which allowed for more animation
// precision which is ultimately wasted on the human eye anyway.
// The highest known stable animation rate is 40fps, so that's what we use.
#define COMM_ANIM_RATE   (ONE_SECOND / 40)

static CONTEXT AnimContext;

LOCDATA CommData;
UNICODE shared_phrase_buf[2048];

static BOOLEAN TalkingFinished;
static CommIntroMode curIntroMode = CIM_DEFAULT;
static TimeCount fadeTime;

typedef struct response_entry
{
	RESPONSE_REF response_ref;
	TEXT response_text;
	RESPONSE_FUNC response_func;
} RESPONSE_ENTRY;

typedef struct encounter_state
{
	BOOLEAN (*InputFunc) (struct encounter_state *pES);

	COUNT Initialized;
	TimeCount NextTime; // framerate control
	BYTE num_responses;
	BYTE cur_response;
	BYTE top_response;
	RESPONSE_ENTRY response_list[MAX_RESPONSES];

	UNICODE phrase_buf[1024];
} ENCOUNTER_STATE;

static ENCOUNTER_STATE *pCurInputState;

static BOOLEAN clear_subtitles;
static TEXT SubtitleText;
static const UNICODE *last_subtitle;

static CONTEXT TextCacheContext;
static FRAME TextCacheFrame;


RECT CommWndRect = {
	// default values; actually inited by HailAlien()
	{SIS_ORG_X, SIS_ORG_Y},
	{0, 0}
};

static void ClearSubtitles (void);
static void CheckSubtitles (void);
static void RedrawSubtitles (void);


/* _count_lines - sees how many lines a given input string would take to
 * display given the line wrapping information
 */
static int
_count_lines (TEXT *pText)
{
	SIZE text_width;
	const char *pStr;
	int numLines = 0;
	BOOLEAN eol;

	text_width = CommData.AlienTextWidth;
	SetContextFont (CommData.AlienFont);

	pStr = pText->pStr;
	do
	{
		++numLines;
		pText->pStr = pStr;
		eol = getLineWithinWidth (pText, &pStr, text_width, (COUNT)~0);
	} while (!eol);
	pText->pStr = pStr;

	return numLines;
}

// status == -1: draw highlighted player dialog option
// status == -2: draw non-highlighted player dialog option
// status == -4: use current context, and baseline from pTextIn
// status ==  1:  draw alien speech; subtitle cache is used
static COORD
add_text (int status, TEXT *pTextIn)
{
	COUNT maxchars, numchars;
	TEXT locText;
	TEXT *pText;
	SIZE leading;
	const char *pStr;
	SIZE text_width;
	int num_lines = 0;
	static COORD last_baseline;
	BOOLEAN eol;
	CONTEXT OldContext = NULL;
	
	BatchGraphics ();

	maxchars = (COUNT)~0;
	if (status == 1)
	{
		if (last_subtitle == pTextIn->pStr)
		{
			// draws cached subtitle
			STAMP s;

			s.origin.x = 0;
			s.origin.y = 0;
			s.frame = TextCacheFrame;
			DrawStamp (&s);
			UnbatchGraphics ();
			return last_baseline;
		}
		else
		{
			// draw to subtitle cache; prepare first
			OldContext = SetContext (TextCacheContext);
			ClearDrawable ();

			last_subtitle = pTextIn->pStr;
		}

		text_width = CommData.AlienTextWidth;
		SetContextFont (CommData.AlienFont);
		GetContextFontLeading (&leading);

		pText = pTextIn;
	}
	else if (GetContextFontLeading (&leading), status <= -4)
	{
		text_width = (SIZE) (SIS_SCREEN_WIDTH - 8 - (TEXT_X_OFFS << 2));

		pText = pTextIn;
	}
	else
	{
		text_width = (SIZE) (SIS_SCREEN_WIDTH - 8 - (TEXT_X_OFFS << 2));

		switch (status)
		{
			case -3:
				// Unknown. Never reached; color matches the background color.
				SetContextForeGroundColor (
						BUILD_COLOR (MAKE_RGB15 (0x00, 0x00, 0x14), 0x01));
				break;
			case -2:
				// Not highlighted dialog options.
				SetContextForeGroundColor (COMM_PLAYER_TEXT_NORMAL_COLOR);
				break;
			case -1:
				// Currently highlighted dialog option.
				SetContextForeGroundColor (COMM_PLAYER_TEXT_HIGHLIGHT_COLOR);
				break;
		}

		maxchars = pTextIn->CharCount;
		locText = *pTextIn;
		locText.baseline.x -= 8;
		locText.CharCount = (COUNT)~0;
		locText.pStr = STR_BULLET;
		font_DrawText (&locText);

		locText = *pTextIn;
		pText = &locText;
		pText->baseline.y -= leading;
	}

	numchars = 0;
	pStr = pText->pStr;

	if (status > 0 && (CommData.AlienTextValign &
			(VALIGN_MIDDLE | VALIGN_BOTTOM)))
	{
		num_lines = _count_lines(pText);
		if (CommData.AlienTextValign == VALIGN_BOTTOM)
			pText->baseline.y -= (leading * num_lines);
		else if (CommData.AlienTextValign == VALIGN_MIDDLE)
			pText->baseline.y -= ((leading * num_lines) / 2);
		if (pText->baseline.y < 0)
			pText->baseline.y = 0;
	}

	do
	{
		pText->pStr = pStr;
		pText->baseline.y += leading;

		eol = getLineWithinWidth (pText, &pStr, text_width, maxchars);

		maxchars -= pText->CharCount;
		if (maxchars != 0)
			--maxchars;
		numchars += pText->CharCount;
		
		if (status <= 0)
		{
			// Player dialog option or (status == -4) other non-alien
			// text.
			if (pText->baseline.y < SIS_SCREEN_HEIGHT)
				font_DrawText (pText);

			if (status < -4 && pText->baseline.y >= -status - 10)
			{
				// Never actually reached. Status is never <-4.
				++pStr;
				break;
			}
		}
		else
		{
			// Alien speech
			font_DrawTracedText (pText,
					CommData.AlienTextFColor, CommData.AlienTextBColor);
		}
	} while (!eol && maxchars);
	pText->pStr = pStr;

	if (status == 1)
	{
		STAMP s;
		
		// We were drawing to cache -- flush to screen
		SetContext (OldContext);
		s.origin.x = s.origin.y = 0;
		s.frame = TextCacheFrame;
		DrawStamp (&s);
		
		last_baseline = pText->baseline.y;
	}

	UnbatchGraphics ();
	return (pText->baseline.y);
}

// This function calculates how much of a string can be fitted within
// a specific width, up to a newline or terminating \0.
// pText is the text to be fitted. pText->CharCount will be set to the
// number of characters that fitted.
// startNext will be filled with the start of the first word that
// doesn't fit in one line, or if an entire line fits, to the character
// past the newline, or if the entire string fits, to the end of the
// string.
// maxWidth is the maximum number of pixels that a line may be wide
//   ASSUMPTION: there are no words in the text wider than maxWidth
// maxChars is the maximum number of characters (not bytes) that are to
// be fitted.
// TRUE is returned if a complete line fitted
// FALSE otherwise
BOOLEAN
getLineWithinWidth(TEXT *pText, const char **startNext,
		SIZE maxWidth, COUNT maxChars)
{
	BOOLEAN eol;
			// The end of the line of text has been reached.
	BOOLEAN done;
			// We cannot add any more words.
	RECT rect;
	COUNT oldCount;
	const char *ptr;
	const char *wordStart;
	UniChar ch;
	COUNT charCount;

	//GetContextClipRect (&rect);

	eol = FALSE;	
	done = FALSE;
	oldCount = 1;
	charCount = 0;
	ch = '\0';
	ptr = pText->pStr;
	for (;;)
	{
		wordStart = ptr;

		// Scan one word.
		for (;;)
		{
			if (*ptr == '\0')
			{
				eol = TRUE;
				done = TRUE;
				break;
			}
			ch = getCharFromString (&ptr);
			eol = ch == '\0' || ch == '\n' || ch == '\r';
			done = eol || charCount >= maxChars;
			if (done || ch == ' ')
				break;
			charCount++;
		}

		oldCount = pText->CharCount;
		pText->CharCount = charCount;
		TextRect (pText, &rect, NULL);
		
		if (rect.extent.width >= maxWidth)
		{
			pText->CharCount = oldCount;
			*startNext = wordStart;
			return FALSE;
		}

		if (done)
		{
			*startNext = ptr;
			return eol;
		}
		charCount++;
				// For the space in between words.
	}
}

static void
DrawSISComWindow (void)
{
	CONTEXT OldContext;

	if (LOBYTE (GLOBAL (CurrentActivity)) != WON_LAST_BATTLE)
	{
		RECT r;

		OldContext = SetContext (SpaceContext);

		r.corner.x = 0;
		r.corner.y = SLIDER_Y + SLIDER_HEIGHT;
		r.extent.width = SIS_SCREEN_WIDTH;
		r.extent.height = SIS_SCREEN_HEIGHT - r.corner.y;
		SetContextForeGroundColor (COMM_PLAYER_BACKGROUND_COLOR);
		DrawFilledRectangle (&r);

		SetContext (OldContext);
	}
}

void
init_communication (void)
{
	// now a no-op
}

void
uninit_communication (void)
{
	// now a no-op
}

static void
RefreshResponses (ENCOUNTER_STATE *pES)
{
	COORD y;
	BYTE response;
	SIZE leading;
	STAMP s;

	SetContext (SpaceContext);
	GetContextFontLeading (&leading);
	BatchGraphics ();

	DrawSISComWindow ();
	y = SLIDER_Y + SLIDER_HEIGHT + 1;
	for (response = pES->top_response; response < pES->num_responses;
			++response)
	{
		pES->response_list[response].response_text.baseline.x = TEXT_X_OFFS + 8;
		pES->response_list[response].response_text.baseline.y = y + leading;
		pES->response_list[response].response_text.align = ALIGN_LEFT;
		if (response == pES->cur_response)
			y = add_text (-1, &pES->response_list[response].response_text);
		else
			y = add_text (-2, &pES->response_list[response].response_text);
	}

	if (pES->top_response)
	{
		s.origin.y = SLIDER_Y + SLIDER_HEIGHT + 1;
		s.frame = SetAbsFrameIndex (ActivityFrame, 6);
	}
	else if (y > SIS_SCREEN_HEIGHT)
	{
		s.origin.y = SIS_SCREEN_HEIGHT - 2;
		s.frame = SetAbsFrameIndex (ActivityFrame, 7);
	}
	else
		s.frame = 0;
	if (s.frame)
	{
		RECT r;

		GetFrameRect (s.frame, &r);
		s.origin.x = SIS_SCREEN_WIDTH - r.extent.width - 1;
		DrawStamp (&s);
	}

	UnbatchGraphics ();
}

static void
FeedbackPlayerPhrase (UNICODE *pStr)
{
	SetContext (SpaceContext);
	
	BatchGraphics ();
	DrawSISComWindow ();
	if (pStr[0])
	{
		TEXT ct;

		ct.baseline.x = SIS_SCREEN_WIDTH >> 1;
		ct.baseline.y = SLIDER_Y + SLIDER_HEIGHT + 13;
		ct.align = ALIGN_CENTER;
		ct.CharCount = (COUNT)~0;

		ct.pStr = GAME_STRING (FEEDBACK_STRING_BASE);
				// "(In response to your statement)"
		SetContextForeGroundColor (COMM_RESPONSE_INTRO_TEXT_COLOR);
		font_DrawText (&ct);

		ct.baseline.y += 16;
		SetContextForeGroundColor (COMM_FEEDBACK_TEXT_COLOR);
		ct.pStr = pStr;
		add_text (-4, &ct);
	}
	UnbatchGraphics ();
}

static void
InitSpeechGraphics (void)
{
	InitOscilloscope (SetAbsFrameIndex (ActivityFrame, 9));

	InitSlider (0, SLIDER_Y, SIS_SCREEN_WIDTH,
			SetAbsFrameIndex (ActivityFrame, 5),
			SetAbsFrameIndex (ActivityFrame, 2));
}

static void
UpdateSpeechGraphics (void)
{
	static TimeCount NextTime;
	CONTEXT OldContext;

	if (GetTimeCounter () < NextTime)
		return; // too early

	NextTime = GetTimeCounter () + OSCILLOSCOPE_RATE;

	OldContext = SetContext (RadarContext);
	DrawOscilloscope ();
	SetContext (SpaceContext);
	DrawSlider ();
	SetContext (OldContext);
}

static void
UpdateAnimations (bool paused)
{
	static TimeCount NextTime;
	CONTEXT OldContext;
	BOOLEAN change;

	if (GetTimeCounter () < NextTime)
		return; // too early

	NextTime = GetTimeCounter () + COMM_ANIM_RATE;

	OldContext = SetContext (AnimContext);
	BatchGraphics ();
	// Advance and draw ambient, transit and talk animations
	change = ProcessCommAnimations (clear_subtitles, paused);
	if (change || clear_subtitles)
		RedrawSubtitles ();
	UnbatchGraphics ();
	clear_subtitles = FALSE;
	SetContext (OldContext);
}

static void
UpdateCommGraphics (void)
{
	UpdateAnimations (false);
	UpdateSpeechGraphics ();
}

// Derived from INPUT_STATE_DESC
typedef struct talking_state
{
	// Fields required by DoInput()
	BOOLEAN (*InputFunc) (struct talking_state *);

	TimeCount NextTime;  // framerate control
	COUNT waitTrack;
	bool rewind;
	bool seeking;
	bool ended;

} TALKING_STATE;

static BOOLEAN
DoTalkSegue (TALKING_STATE *pTS)
{
	bool left = false;
	bool right = false;
	COUNT curTrack;

	if (GLOBAL (CurrentActivity) & CHECK_ABORT)
	{
		pTS->ended = true;
		return FALSE;
	}
	
	if (PulsedInputState.menu[KEY_MENU_CANCEL])
	{
		JumpTrack ();
		pTS->ended = true;
		return FALSE;
	}

	if (optSmoothScroll == OPT_PC)
	{
		left = PulsedInputState.menu[KEY_MENU_LEFT] != 0;
		right = PulsedInputState.menu[KEY_MENU_RIGHT] != 0;
	}
	else if (optSmoothScroll == OPT_3DO)
	{
		left = CurrentInputState.menu[KEY_MENU_LEFT] != 0;
		right = CurrentInputState.menu[KEY_MENU_RIGHT] != 0;
	}

#if DEMO_MODE || CREATE_JOURNAL
	left = false;
	right = false;
#endif

	if (right)
	{
		SetSliderImage (SetAbsFrameIndex (ActivityFrame, 3));
		if (optSmoothScroll == OPT_PC)
			FastForward_Page ();
		else if (optSmoothScroll == OPT_3DO)
			FastForward_Smooth ();
		pTS->seeking = true;
	}
	else if (left || pTS->rewind)
	{
		pTS->rewind = false;
		SetSliderImage (SetAbsFrameIndex (ActivityFrame, 4));
		if (optSmoothScroll == OPT_PC)
			FastReverse_Page ();
		else if (optSmoothScroll == OPT_3DO)
			FastReverse_Smooth ();
		pTS->seeking = true;
	}
	else if (pTS->seeking)
	{
		// This is only done once the seeking is over (in the smooth
		// scroll case, once the user releases the seek button)
		pTS->seeking = false;
		SetSliderImage (SetAbsFrameIndex (ActivityFrame, 2));
	}
	else
	{
		// This used to have a buggy guard condition, which
		// would cause the animations to remain paused in a couple cases
		// after seeking back to the beginning.
		// Broken cases were: Syreen "several hours later" and Starbase
		// VUX Beast analysis by the scientist.
		CheckSubtitles ();
	}

	// XXX: When seeking, all animations (talking and ambient) stop
	// progressing. This is an original 3DO behavior, and I see no
	// reason why the animations cannot continue while seeking.
	UpdateAnimations (pTS->seeking);
	UpdateSpeechGraphics ();

	curTrack = PlayingTrack ();
	pTS->ended = !pTS->seeking && !curTrack;

	SleepThreadUntil (pTS->NextTime);
	// Need a high enough framerate for 3DO smooth seeking
	pTS->NextTime = GetTimeCounter () + ONE_SECOND / 60;

	return pTS->seeking || (curTrack && curTrack <= pTS->waitTrack);
}

static void
runCommAnimFrame (void)
{
	UpdateCommGraphics ();
	SleepThread (COMM_ANIM_RATE);
}

static BOOLEAN
TalkSegue (COUNT wait_track)
{
	TALKING_STATE talkingState;

	// Transition animation to talking state, if necessary
	if (wantTalkingAnim () && haveTalkingAnim ())
	{	
		if (haveTransitionAnim ())
			setRunIntroAnim ();
					
		setRunTalkingAnim ();

		// wait until the transition finishes
		while (runningIntroAnim ())
			runCommAnimFrame ();
	}

	memset (&talkingState, 0, sizeof talkingState);

	if (wait_track == 0)
	{	// Restarting with a rewind
		wait_track = WAIT_TRACK_ALL;
		talkingState.rewind = true;
	}
	else if (!PlayingTrack ())
	{	// initial start of player
		PlayTrack ();
		assert (PlayingTrack ());
	}

	// Run the talking controls
	SetMenuSounds (MENU_SOUND_NONE, MENU_SOUND_NONE);
	talkingState.InputFunc = DoTalkSegue;
	talkingState.waitTrack = wait_track;
	DoInput (&talkingState, FALSE);

	ClearSubtitles ();

	if (talkingState.ended)
	{	// reached the end; set STOP icon
		SetSliderImage (SetAbsFrameIndex (ActivityFrame, 8));
	}

	// transition back to silent, if necessary
	if (runningTalkingAnim ())
		setStopTalkingAnim ();

	// Wait until the animation task stops "talking"
	while (runningTalkingAnim ())
		runCommAnimFrame ();

	return talkingState.ended;
}

static void
CommIntroTransition (void)
{
	if (curIntroMode == CIM_CROSSFADE_SCREEN)
	{
		ScreenTransition (3, NULL);
		UnbatchGraphics ();
	}
	else if (curIntroMode == CIM_CROSSFADE_SPACE)
	{
		RECT r;
		r.corner.x = SIS_ORG_X;
		r.corner.y = SIS_ORG_Y;
		r.extent.width = SIS_SCREEN_WIDTH;
		r.extent.height = SIS_SCREEN_HEIGHT;
		ScreenTransition (3, &r);
		UnbatchGraphics ();
	}
	else if (curIntroMode == CIM_CROSSFADE_WINDOW)
	{
		ScreenTransition (3, &CommWndRect);
		UnbatchGraphics ();
	}
	else if (curIntroMode == CIM_FADE_IN_SCREEN)
	{
		UnbatchGraphics ();
		FadeScreen (FadeAllToColor, fadeTime);
	}
	else
	{	// Uknown transition
		// Have to unbatch anyway or no more graphics, ever
		UnbatchGraphics ();
		assert (0 && "Unknown comm intro transition");
	}

	// Reset the mode for next time. Everything that needs a
	// different one will let us know.
	curIntroMode = CIM_DEFAULT;
}

void
AlienTalkSegue (COUNT wait_track)
{
	// this skips any talk segues that follow an aborted one
	if ((GLOBAL (CurrentActivity) & CHECK_ABORT) || TalkingFinished)
		return;

	if (!pCurInputState->Initialized)
	{
		InitSpeechGraphics ();
		SetColorMap (GetColorMapAddress (CommData.AlienColorMap));
		SetContext (AnimContext);
		DrawAlienFrame (NULL, 0, TRUE);
		UpdateSpeechGraphics ();
		CommIntroTransition ();
		
		pCurInputState->Initialized = TRUE;

		PlayMusic (CommData.AlienSong, TRUE, 1);
		SetMusicVolume (BACKGROUND_VOL);

		InitCommAnimations ();

		LastActivity &= ~CHECK_LOAD;
	}
	
	TalkingFinished = TalkSegue (wait_track);
	if (TalkingFinished)
		FadeMusic (FOREGROUND_VOL, ONE_SECOND);
}


typedef struct summary_state
{
	// standard state required by DoInput
	BOOLEAN (*InputFunc) (struct summary_state *pSS);

	// extended state
	BOOLEAN Initialized;
	BOOLEAN PrintNext;
	SUBTITLE_REF NextSub;
	const UNICODE *LeftOver;

} SUMMARY_STATE;

static BOOLEAN
DoConvSummary (SUMMARY_STATE *pSS)
{
#define DELTA_Y_SUMMARY 8
#define MAX_SUMM_ROWS ((SIS_SCREEN_HEIGHT - SLIDER_Y - SLIDER_HEIGHT) \
			/ DELTA_Y_SUMMARY) - 1

	if (!pSS->Initialized)
	{
		pSS->PrintNext = TRUE;
		pSS->NextSub = GetFirstTrackSubtitle ();
		pSS->LeftOver = NULL;
		pSS->InputFunc = DoConvSummary;
		pSS->Initialized = TRUE;
		DoInput (pSS, FALSE);
	}
	else if (GLOBAL (CurrentActivity) & CHECK_ABORT)
	{
		return FALSE; // bail out
	}
	else if (PulsedInputState.menu[KEY_MENU_SELECT]
			|| PulsedInputState.menu[KEY_MENU_CANCEL]
			|| PulsedInputState.menu[KEY_MENU_RIGHT])
	{
		if (pSS->NextSub)
		{	// we want the next page
			pSS->PrintNext = TRUE;
		}
		else
		{	// no more, we are done
			return FALSE;
		}
	}
	else if (pSS->PrintNext)
	{	// print the next page
		RECT r;
		TEXT t;
		int row;

		r.corner.x = 0;
		r.corner.y = 0;
		r.extent.width = SIS_SCREEN_WIDTH;
		r.extent.height = SIS_SCREEN_HEIGHT - SLIDER_Y - SLIDER_HEIGHT + 2;

		SetContext (AnimContext);
		SetContextForeGroundColor (COMM_HISTORY_BACKGROUND_COLOR);
		DrawFilledRectangle (&r);

		SetContextForeGroundColor (COMM_HISTORY_TEXT_COLOR);

		r.extent.width -= 2 + 2;
		t.baseline.x = 2;
		t.align = ALIGN_LEFT;
		t.baseline.y = DELTA_Y_SUMMARY;
		SetContextFont (TinyFont);

		for (row = 0; row < MAX_SUMM_ROWS && pSS->NextSub;
				++row, pSS->NextSub = GetNextTrackSubtitle (pSS->NextSub))
		{
			const char *next = NULL;

			if (pSS->LeftOver)
			{	// some text left from last subtitle
				t.pStr = pSS->LeftOver;
				pSS->LeftOver = NULL;
			}
			else
			{
				t.pStr = GetTrackSubtitleText (pSS->NextSub);
				if (!t.pStr)
					continue;
			}

			t.CharCount = (COUNT)~0;
			for ( ; row < MAX_SUMM_ROWS &&
					!getLineWithinWidth (&t, &next, r.extent.width, (COUNT)~0);
					++row)
			{
				font_DrawText (&t);
				t.baseline.y += DELTA_Y_SUMMARY;
				t.pStr = next;
				t.CharCount = (COUNT)~0;
			}

			if (row >= MAX_SUMM_ROWS)
			{	// no more space on screen, but some text left over
				// from the current subtitle
				pSS->LeftOver = next;
				break;
			}
		
			// this subtitle fit completely
			font_DrawText (&t);
			t.baseline.y += DELTA_Y_SUMMARY;
		}

		if (row >= MAX_SUMM_ROWS && (pSS->NextSub || pSS->LeftOver))
		{	// draw *MORE*
			TEXT mt;
			UNICODE buffer[80];

			mt.baseline.x = SIS_SCREEN_WIDTH >> 1;
			mt.baseline.y = t.baseline.y;
			mt.align = ALIGN_CENTER;
			snprintf (buffer, sizeof (buffer), "%s%s%s", // "MORE"
					STR_MIDDLE_DOT, GAME_STRING (FEEDBACK_STRING_BASE + 1),
					STR_MIDDLE_DOT);
			mt.pStr = buffer;
			SetContextForeGroundColor (COMM_MORE_TEXT_COLOR);
			font_DrawText (&mt);
		}


		pSS->PrintNext = FALSE;
	}
	else
	{
		SleepThread (ONE_SECOND / 20);
	}

	return TRUE; // keep going
}

// Called when the player presses the select button on a response.
static void
SelectResponse (ENCOUNTER_STATE *pES)
{
	TEXT *response_text =
			&pES->response_list[pES->cur_response].response_text;
	utf8StringCopy (pES->phrase_buf, sizeof pES->phrase_buf,
			response_text->pStr);
	FeedbackPlayerPhrase (pES->phrase_buf);
	StopTrack ();
	ClearSubtitles ();
	SetSliderImage (SetAbsFrameIndex (ActivityFrame, 2));

	FadeMusic (BACKGROUND_VOL, ONE_SECOND);

	TalkingFinished = FALSE;
	pES->num_responses = 0;
	(*pES->response_list[pES->cur_response].response_func)
			(pES->response_list[pES->cur_response].response_ref);
}

// Called when the player presses the cancel button in comm.
static void
SelectConversationSummary (ENCOUNTER_STATE *pES)
{
	SUMMARY_STATE SummaryState;
	
	if (pES)
		FeedbackPlayerPhrase (pES->phrase_buf);

	SummaryState.Initialized = FALSE;
	DoConvSummary (&SummaryState);

	if (pES)
		RefreshResponses (pES);
	clear_subtitles = TRUE;
}

static void
SelectReplay (ENCOUNTER_STATE *pES)
{
	FadeMusic (BACKGROUND_VOL, ONE_SECOND);
	if (pES)
		FeedbackPlayerPhrase (pES->phrase_buf);

	TalkSegue (0);
}

static void
PlayerResponseInput (ENCOUNTER_STATE *pES)
{
	BYTE response;

	if (pES->top_response == (BYTE)~0)
	{
		pES->top_response = 0;
		RefreshResponses (pES);
	}

	if (PulsedInputState.menu[KEY_MENU_SELECT])
	{
		SelectResponse (pES);
	}
	else if (PulsedInputState.menu[KEY_MENU_CANCEL] &&
			LOBYTE (GLOBAL (CurrentActivity)) != WON_LAST_BATTLE)
	{
		SelectConversationSummary (pES);
	}
	else
	{
		response = pES->cur_response;
		if (PulsedInputState.menu[KEY_MENU_LEFT])
		{
			SelectReplay (pES);

			if (!(GLOBAL (CurrentActivity) & CHECK_ABORT))
			{
				RefreshResponses (pES);
				FadeMusic (FOREGROUND_VOL, ONE_SECOND);
			}
		}
		else if (PulsedInputState.menu[KEY_MENU_UP])
			response = (BYTE)((response + (BYTE)(pES->num_responses - 1))
					% pES->num_responses);
		else if (PulsedInputState.menu[KEY_MENU_DOWN])
			response = (BYTE)((BYTE)(response + 1) % pES->num_responses);

		if (response != pES->cur_response)
		{
			COORD y;

			BatchGraphics ();
			add_text (-2,
					&pES->response_list[pES->cur_response].response_text);

			pES->cur_response = response;

			y = add_text (-1,
					&pES->response_list[pES->cur_response].response_text);
			if (response < pES->top_response)
			{
				pES->top_response = 0;
				RefreshResponses (pES);
			}
			else if (y > SIS_SCREEN_HEIGHT)
			{
				pES->top_response = response;
				RefreshResponses (pES);
			}
			UnbatchGraphics ();
		}

		UpdateCommGraphics ();

		SleepThreadUntil (pES->NextTime);
		pES->NextTime = GetTimeCounter () + COMM_ANIM_RATE;
	}
}

// Derived from INPUT_STATE_DESC
typedef struct last_replay_state
{
	// Fields required by DoInput()
	BOOLEAN (*InputFunc) (struct last_replay_state *);

	TimeCount NextTime; // framerate control
	TimeCount TimeOut;

} LAST_REPLAY_STATE;

static BOOLEAN
DoLastReplay (LAST_REPLAY_STATE *pLRS)
{
	if (GLOBAL (CurrentActivity) & CHECK_ABORT)
		return FALSE;

	if (GetTimeCounter () > pLRS->TimeOut)
		return FALSE; // timed out and done

	if (PulsedInputState.menu[KEY_MENU_CANCEL] &&
			LOBYTE (GLOBAL (CurrentActivity)) != WON_LAST_BATTLE)
	{
		FadeMusic (BACKGROUND_VOL, ONE_SECOND);
		SelectConversationSummary (NULL);
		pLRS->TimeOut = FadeMusic (0, ONE_SECOND * 2) + ONE_SECOND / 60;
	}
	else if (PulsedInputState.menu[KEY_MENU_LEFT])
	{
		SelectReplay (NULL);
		pLRS->TimeOut = FadeMusic (0, ONE_SECOND * 2) + ONE_SECOND / 60;
	}

	UpdateCommGraphics ();
	
	SleepThreadUntil (pLRS->NextTime);
	pLRS->NextTime = GetTimeCounter () + COMM_ANIM_RATE;

	return TRUE;
}

static BOOLEAN
DoCommunication (ENCOUNTER_STATE *pES)
{
	SetMenuSounds (MENU_SOUND_UP | MENU_SOUND_DOWN, MENU_SOUND_SELECT);

	// First, finish playing all queued tracks if not done yet
	if (!TalkingFinished)
	{
		AlienTalkSegue (WAIT_TRACK_ALL);
		return TRUE;
	}

	if (GLOBAL (CurrentActivity) & CHECK_ABORT)
		;
	else if (pES->num_responses == 0)
	{
		// The player doesn't get a chance to say anything,
		// but can still review alien's last phrases.
		LAST_REPLAY_STATE replayState;

		memset (&replayState, 0, sizeof replayState);
		replayState.TimeOut = FadeMusic (0, ONE_SECOND * 3) + ONE_SECOND / 60;
		replayState.InputFunc = DoLastReplay;
		DoInput (&replayState, FALSE);
	}
	else
	{
		PlayerResponseInput (pES);
		return TRUE;
	}

	SetContext (SpaceContext);
	DestroyContext (AnimContext);
	AnimContext = NULL;

	FlushColorXForms ();
	ClearSubtitles ();

	StopMusic ();
	StopSound ();
	StopTrack ();
	SleepThreadUntil (FadeMusic (NORMAL_VOLUME, 0) + ONE_SECOND / 60);

	return FALSE;
}

void
DoResponsePhrase (RESPONSE_REF R, RESPONSE_FUNC response_func,
		UNICODE *ConstructStr)
{
	ENCOUNTER_STATE *pES = pCurInputState;
	RESPONSE_ENTRY *pEntry;

	if (GLOBAL (CurrentActivity) & CHECK_ABORT)
		return;
			
	if (pES->num_responses == 0)
	{
		pES->cur_response = 0;
		pES->top_response = (BYTE)~0;
	}

	pEntry = &pES->response_list[pES->num_responses];
	pEntry->response_ref = R;
	pEntry->response_text.pStr = ConstructStr;
	if (pEntry->response_text.pStr)
		pEntry->response_text.CharCount = (COUNT)~0;
	else
	{
		STRING locString;
		
		locString = SetAbsStringTableIndex (CommData.ConversationPhrases,
				(COUNT) (R - 1));
		pEntry->response_text.pStr =
				(UNICODE *) GetStringAddress (locString);
		pEntry->response_text.CharCount = GetStringLength (locString);
//#define BVT_PROBLEM
#ifdef BVT_PROBLEM
		if (pEntry->response_text.pStr[pEntry->response_text.CharCount - 1]
				== '\0')
			--pEntry->response_text.CharCount;
#endif /* BVT_PROBLEM */
	}
	pEntry->response_func = response_func;
	++pES->num_responses;
}

static void
HailAlien (void)
{
	ENCOUNTER_STATE ES;
	FONT PlayerFont, OldFont;
	MUSIC_REF SongRef = 0;
	Color TextBack;

	pCurInputState = &ES;
	memset (pCurInputState, 0, sizeof (*pCurInputState));

	TalkingFinished = FALSE;

	ES.InputFunc = DoCommunication;
	PlayerFont = LoadFont (PLAYER_FONT);

	CommData.AlienFrame = CaptureDrawable (
			LoadGraphic (CommData.AlienFrameRes));
	CommData.AlienFont = LoadFont (CommData.AlienFontRes);
	CommData.AlienColorMap = CaptureColorMap (
			LoadColorMap (CommData.AlienColorMapRes));
	if ((CommData.AlienSongFlags & LDASF_USE_ALTERNATE)
			&& CommData.AlienAltSongRes)
		SongRef = LoadMusic (CommData.AlienAltSongRes);
	if (SongRef)
		CommData.AlienSong = SongRef;
	else
		CommData.AlienSong = LoadMusic (CommData.AlienSongRes);

	CommData.ConversationPhrases = CaptureStringTable (
			LoadStringTable (CommData.ConversationPhrasesRes));

	SubtitleText.baseline = CommData.AlienTextBaseline;
	SubtitleText.align = CommData.AlienTextAlign;


	// init subtitle cache context
	TextCacheContext = CreateContext ("TextCacheContext");
	TextCacheFrame = CaptureDrawable (
			CreateDrawable (WANT_PIXMAP, SIS_SCREEN_WIDTH,
			SIS_SCREEN_HEIGHT - SLIDER_Y - SLIDER_HEIGHT + 2, 1));
	SetContext (TextCacheContext);
	SetContextFGFrame (TextCacheFrame);
	TextBack = BUILD_COLOR (MAKE_RGB15 (0x00, 0x00, 0x10), 0x00);
			// Color key for the background.
	SetContextBackGroundColor (TextBack);
	ClearDrawable ();
	SetFrameTransparentColor (TextCacheFrame, TextBack);

	ES.phrase_buf[0] = '\0';

	SetContext (SpaceContext);
	OldFont = SetContextFont (PlayerFont);

	{
		RECT r;

		AnimContext = CreateContext ("AnimContext");
		SetContext (AnimContext);
		SetContextFGFrame (Screen);
		GetFrameRect (CommData.AlienFrame, &r);
		r.extent.width = SIS_SCREEN_WIDTH;
		CommWndRect.extent = r.extent;
		
		SetTransitionSource (NULL);
		BatchGraphics ();
		if (LOBYTE (GLOBAL (CurrentActivity)) == WON_LAST_BATTLE)
		{
			r.corner = CommWndRect.corner;
			SetContextClipRect (&r);
		}
		else
		{
			r.corner.x = SIS_ORG_X;
			r.corner.y = SIS_ORG_Y;
			SetContextClipRect (&r);
			CommWndRect.corner = r.corner;

			DrawSISFrame ();
			// TODO: find a better way to do this, perhaps set the titles
			// forward from callers.
			if (GET_GAME_STATE (GLOBAL_FLAGS_AND_DATA) == (BYTE)~0
					&& GET_GAME_STATE (STARBASE_AVAILABLE))
			{	// Talking to allied Starbase
				DrawSISMessage (GAME_STRING (STARBASE_STRING_BASE + 1));
						// "Starbase Commander"
				DrawSISTitle (GAME_STRING (STARBASE_STRING_BASE + 0));
						// "Starbase"
			}
			else
			{	// Default titles: star name + planet name
				DrawSISMessage (NULL);
				DrawSISTitle (GLOBAL_SIS (PlanetName));
			}
		}

		DrawSISComWindow ();
	}


	LastActivity |= CHECK_LOAD; /* prevent spurious input */
	(*CommData.init_encounter_func) ();
	DoInput (&ES, FALSE);
	if (!(GLOBAL (CurrentActivity) & (CHECK_ABORT | CHECK_LOAD)))
		(*CommData.post_encounter_func) ();
	(*CommData.uninit_encounter_func) ();

	SetContext (SpaceContext);
	SetContextFont (OldFont);

	DestroyStringTable (ReleaseStringTable (CommData.ConversationPhrases));
	DestroyMusic (CommData.AlienSong);
	DestroyColorMap (ReleaseColorMap (CommData.AlienColorMap));
	DestroyFont (CommData.AlienFont);
	DestroyDrawable (ReleaseDrawable (CommData.AlienFrame));

	DestroyContext (TextCacheContext);
	DestroyDrawable (ReleaseDrawable (TextCacheFrame));

	DestroyFont (PlayerFont);

	// Some support code tests either of these to see if the
	// game is currently in comm or encounter
	CommData.ConversationPhrasesRes = 0;
	CommData.ConversationPhrases = 0;
	pCurInputState = 0;
}

void
SetCommIntroMode (CommIntroMode newMode, TimeCount howLong)
{
	curIntroMode = newMode;
	fadeTime = howLong;
}

COUNT
InitCommunication (CONVERSATION which_comm)
{
	COUNT status;
	LOCDATA *LocDataPtr;

#ifdef DEBUG
	if (disableInteractivity)
		return 0;
#endif
	

	if (LastActivity & CHECK_LOAD)
	{
		LastActivity &= ~CHECK_LOAD;
		if (which_comm != COMMANDER_CONVERSATION)
		{
			if (LOBYTE (LastActivity) == 0)
			{
				DrawSISFrame ();
			}
			else
			{
				ClearSISRect (DRAW_SIS_DISPLAY);
				RepairSISBorder ();
			}
			DrawSISMessage (NULL);
			if (inHQSpace ())
				DrawHyperCoords (GLOBAL (ShipStamp.origin));
			else if (GLOBAL (ip_planet) == 0)
				DrawHyperCoords (CurStarDescPtr->star_pt);
			else
				DrawSISTitle (GLOBAL_SIS (PlanetName));
		}
	}


	if (which_comm == URQUAN_DRONE_CONVERSATION)
	{
		status = URQUAN_DRONE_SHIP;
		which_comm = URQUAN_CONVERSATION;
	}
	else
	{
		if (which_comm == YEHAT_REBEL_CONVERSATION)
		{
			status = YEHAT_REBEL_SHIP;
			which_comm = YEHAT_CONVERSATION;
		}
		else
		{
			COUNT commToShip[] = {
				RACE_SHIP_FOR_COMM
			};
			status = commToShip[which_comm];
			if (status >= YEHAT_REBEL_SHIP) {
				/* conversation exception, set to self */
				status = HUMAN_SHIP;
			}
		}
		StartSphereTracking (status);

		if (which_comm == ORZ_CONVERSATION
				|| (which_comm == TALKING_PET_CONVERSATION
				&& (!GET_GAME_STATE (TALKING_PET_ON_SHIP)
				|| LOBYTE (GLOBAL (CurrentActivity)) == IN_LAST_BATTLE))
				|| (which_comm != CHMMR_CONVERSATION
				&& which_comm != SYREEN_CONVERSATION
				))//&& CheckAlliance (status) == BAD_GUY))
			BuildBattle (NPC_PLAYER_NUM);
	}

	LocDataPtr = init_race (
			status != YEHAT_REBEL_SHIP ? which_comm :
			YEHAT_REBEL_CONVERSATION);
	if (LocDataPtr)
	{	// We make a copy here
		CommData = *LocDataPtr;
	}

	if (GET_GAME_STATE (BATTLE_SEGUE) == 0)
	{
		// Not offered the chance to attack.
		status = HAIL;
	}
	else if ((status = InitEncounter ()) == HAIL && LocDataPtr)
	{
		// The player chose to talk.
		SET_GAME_STATE (BATTLE_SEGUE, 0);
	}
	else
	{
		// The player chose to attack.
		status = ATTACK;
		SET_GAME_STATE (BATTLE_SEGUE, 1);
	}

	if (status == HAIL)
	{
		HailAlien ();
	}
	else if (LocDataPtr)
	{	// only when comm initied successfully
		if (!(GLOBAL (CurrentActivity) & (CHECK_ABORT | CHECK_LOAD)))
			(*CommData.post_encounter_func) (); // process states

		(*CommData.uninit_encounter_func) (); // cleanup
	}

	status = 0;
	if (!(GLOBAL (CurrentActivity) & (CHECK_ABORT | CHECK_LOAD)))
	{
		// The Sa-Matra battle is skipped when Cyborg is enabled.
		// Most likely because the Cyborg is too dumb to know what
		// to do in this battle.
		if (LOBYTE (GLOBAL (CurrentActivity)) == IN_LAST_BATTLE
				&& (GLOBAL (glob_flags) & CYBORG_ENABLED))
			ReinitQueue (&GLOBAL (npc_built_ship_q));

		// Clear the location flags
		SET_GAME_STATE (GLOBAL_FLAGS_AND_DATA, 0);
		status = (GET_GAME_STATE (BATTLE_SEGUE)
				&& GetHeadLink (&GLOBAL (npc_built_ship_q)));
		if (status)
		{
			// Start combat
			BuildBattle (RPG_PLAYER_NUM);
			EncounterBattle ();
		}
		else
		{
			SET_GAME_STATE (BATTLE_SEGUE, 0);
		}
	}

	UninitEncounter ();

	return (status);
}

void
RaceCommunication (void)
{
	COUNT i, status;
	HSHIPFRAG hStarShip;
	SHIP_FRAGMENT *FragPtr;
	HENCOUNTER hEncounter = 0;
	CONVERSATION RaceComm[] =
	{
		RACE_COMMUNICATION
	};

	if (LOBYTE (GLOBAL (CurrentActivity)) == IN_LAST_BATTLE)
	{
		/* Going into talking pet conversation */
		ReinitQueue (&GLOBAL (npc_built_ship_q));
		CloneShipFragment (SAMATRA_SHIP, &GLOBAL (npc_built_ship_q), 0);
		InitCommunication (TALKING_PET_CONVERSATION);
		if (!(GLOBAL (CurrentActivity) & (CHECK_ABORT | CHECK_LOAD))
				&& GLOBAL_SIS (CrewEnlisted) != (COUNT)~0)
		{
			GLOBAL (CurrentActivity) = WON_LAST_BATTLE;
		}
		return;
	}
	else if (NextActivity & CHECK_LOAD)
	{
		BYTE ec;

		ec = GET_GAME_STATE (ESCAPE_COUNTER);

		if (GET_GAME_STATE (FOUND_PLUTO_SPATHI) == 1)
			InitCommunication (SPATHI_CONVERSATION);
		else if (GET_GAME_STATE (GLOBAL_FLAGS_AND_DATA) == 0)
			InitCommunication (TALKING_PET_CONVERSATION);
		else if (GET_GAME_STATE (GLOBAL_FLAGS_AND_DATA) &
				((1 << 4) | (1 << 5)))
			// Communicate with the Ilwrath using a Hyperwave Broadcaster.
			InitCommunication (ILWRATH_CONVERSATION);
		else
			InitCommunication (CHMMR_CONVERSATION);
		if (GLOBAL_SIS (CrewEnlisted) != (COUNT)~0)
		{
			NextActivity = GLOBAL (CurrentActivity) & ~START_ENCOUNTER;
			if (LOBYTE (NextActivity) == IN_INTERPLANETARY)
				NextActivity |= START_INTERPLANETARY;
			GLOBAL (CurrentActivity) |= CHECK_LOAD; /* fake a load game */
		}

		SET_GAME_STATE (ESCAPE_COUNTER, ec);
		return;
	}
	else if (inHQSpace ())
	{
		ReinitQueue (&GLOBAL (npc_built_ship_q));
		if (GET_GAME_STATE (ARILOU_SPACE_SIDE) >= 2)
		{
			InitCommunication (ARILOU_CONVERSATION);
			return;
		}
		else
		{
			/* Encounter with a black globe in HS, prepare enemy ship list */
			ENCOUNTER *EncounterPtr;

			// The encounter globe that the flagship collided with is moved
			// to the head of the queue in hyper.c:cleanup_hyperspace()
			hEncounter = GetHeadEncounter ();
			LockEncounter (hEncounter, &EncounterPtr);

			for (i = 0; i < EncounterPtr->num_ships; ++i)
			{
				CloneShipFragment (EncounterPtr->race_id,
						&GLOBAL (npc_built_ship_q),
						EncounterPtr->ShipList[i].crew_level);
			}

			// XXX: Bug: CurStarDescPtr was abused to point within
			//    an ENCOUNTER struct, which is immediately unlocked
			//CurStarDescPtr = (STAR_DESC*)&EncounterPtr->SD;
			UnlockEncounter (hEncounter);
		}
	}

	// First ship in the npc queue defines which alien race
	// the player will be talking to
	hStarShip = GetHeadLink (&GLOBAL (npc_built_ship_q));
	FragPtr = LockShipFrag (&GLOBAL (npc_built_ship_q), hStarShip);
	i = FragPtr->race_id;
	UnlockShipFrag (&GLOBAL (npc_built_ship_q), hStarShip);

	status = InitCommunication (RaceComm[i]);

	if (GLOBAL (CurrentActivity) & (CHECK_ABORT | CHECK_LOAD))
		return;

	if (i == CHMMR_SHIP)
		ReinitQueue (&GLOBAL (npc_built_ship_q));

	if (LOBYTE (GLOBAL (CurrentActivity)) == IN_INTERPLANETARY)
	{
		/* if used destruct code in interplanetary */
		if (i == SLYLANDRO_SHIP && status == 0)
			ReinitQueue (&GLOBAL (npc_built_ship_q));
	}
	else if (hEncounter)
	{
		/* Update HSpace encounter info, ships lefts, etc. */
		BYTE i, NumShips;
		ENCOUNTER *EncounterPtr;

		LockEncounter (hEncounter, &EncounterPtr);

		NumShips = CountLinks (&GLOBAL (npc_built_ship_q));
		EncounterPtr->num_ships = NumShips;
		EncounterPtr->flags |= ENCOUNTER_REFORMING;
		if (status == 0)
			EncounterPtr->flags |= ONE_SHOT_ENCOUNTER;

		for (i = 0; i < NumShips; ++i)
		{
			HSHIPFRAG hStarShip;
			SHIP_FRAGMENT *FragPtr;
			BRIEF_SHIP_INFO *BSIPtr;

			hStarShip = GetStarShipFromIndex (&GLOBAL (npc_built_ship_q), i);
			FragPtr = LockShipFrag (&GLOBAL (npc_built_ship_q), hStarShip);
			BSIPtr = &EncounterPtr->ShipList[i];
			BSIPtr->race_id = FragPtr->race_id;
			BSIPtr->crew_level = FragPtr->crew_level;
			BSIPtr->max_crew = FragPtr->max_crew;
			BSIPtr->max_energy = FragPtr->max_energy;
			UnlockShipFrag (&GLOBAL (npc_built_ship_q), hStarShip);
		}
		
		UnlockEncounter (hEncounter);
		ReinitQueue (&GLOBAL (npc_built_ship_q));
	}
}

static void
RedrawSubtitles (void)
{
	TEXT t;

	if (!optSubtitles)
		return;

	if (SubtitleText.pStr)
	{
		t = SubtitleText;
		add_text (1, &t);
	}
}

static void
ClearSubtitles (void)
{
	clear_subtitles = TRUE;
	last_subtitle = NULL;
	SubtitleText.pStr = NULL;
	SubtitleText.CharCount = 0;
}

static void
CheckSubtitles (void)
{
	const UNICODE *pStr;
	POINT baseline;
	TEXT_ALIGN align;

	pStr = GetTrackSubtitle ();
	baseline = CommData.AlienTextBaseline;
	align = CommData.AlienTextAlign;

	if (pStr != SubtitleText.pStr ||
			SubtitleText.baseline.x != baseline.x ||
			SubtitleText.baseline.y != baseline.y ||
			SubtitleText.align != align)
	{	// Subtitles changed
		clear_subtitles = TRUE;
		// Baseline may be updated by the ZFP
		SubtitleText.baseline = baseline;
		SubtitleText.align = align;
		// Make a note in the logs if the update was multiframe
		if (SubtitleText.pStr == pStr)
		{
			log_add (log_Warning, "Dialog text and location changed out of sync");
		}

		SubtitleText.pStr = pStr;
		// may have been cleared too
		if (pStr)
			SubtitleText.CharCount = (COUNT)~0;
		else
			SubtitleText.CharCount = 0;
	}
}

void
EnableTalkingAnim (BOOLEAN enable)
{
	if (enable)
		CommData.AlienTalkDesc.AnimFlags &= ~PAUSE_TALKING;
	else
		CommData.AlienTalkDesc.AnimFlags |= PAUSE_TALKING;
}
