[Scummvm-git-logs] scummvm master -> 40b9e1d018777509bf05d47aaa37243fa00168cf

dreammaster noreply at scummvm.org
Thu Jul 9 10:44:57 UTC 2026


This automated email contains information about 4 new commits which have been
pushed to the 'scummvm' repo located at https://api.github.com/repos/scummvm/scummvm .

Summary:
f2ccd58789 MM: XEEN: Fix French accented characters
7d81c8e0e7 MM: XEEN: Fix subtitle reveal off-by-one
e1654f1bd0 MM: XEEN: Keep subtitles inside their box
40b9e1d018 MM: XEEN: Adapt subtitle pacing to line length


Commit: f2ccd587892724787ccd36eb3ab5cd5e9be9ff95
    https://github.com/scummvm/scummvm/commit/f2ccd587892724787ccd36eb3ab5cd5e9be9ff95
Author: NK (nicolas.devillers at airbus.com)
Date: 2026-07-09T20:44:52+10:00

Commit Message:
MM: XEEN: Fix French accented characters

The French DOS version stores its accented characters as code page 437
codes (0x82 for e-acute, 0x8A for e-grave, ...), but getNextChar()
masks every byte with 0x7f, which turned them into font control codes:
0x82 became "enable reduced font", 0x8A became a newline that pushed
the rest of the line below the subtitle box, 0x87 became "set
background color" and swallowed the next three characters, etc. In
cutscene subtitles this garbled the line and could leave overlapping
text on screen.

The French font stores its accented glyphs in repurposed ASCII slots
(0x24 holds e-acute in place of '$', 0x5D holds a-grave in place of
']', ...), which were identified by rendering every glyph of the
French fnt resource, so translate the CP437 codes to those slots the
way the original interpreter did. Unknown high bytes now degrade to a
printable character instead of a control code.

Verified on the French DOS World of Xeen Dark Side intro. Other
localizations (German, Spanish) may need their own tables if their
data also uses high-bit characters; their behavior is unchanged.

Changed paths:
    engines/mm/xeen/font.cpp


diff --git a/engines/mm/xeen/font.cpp b/engines/mm/xeen/font.cpp
index d4f0fd91fa2..9c6a970b860 100644
--- a/engines/mm/xeen/font.cpp
+++ b/engines/mm/xeen/font.cpp
@@ -363,6 +363,32 @@ void FontSurface::writeCharacter(uint16_t c, const Common::Rect &clipRect) {
 	_fontJustify = justify;
 }
 
+// The French version stores accented characters as their code page 437
+// codes, while its font keeps the corresponding glyphs in repurposed ASCII
+// slots, so translate the former into the latter
+static uint16_t frenchChar(byte c) {
+	switch (c) {
+	case 0x81: return 0x5E;   // u with diaeresis
+	case 0x82: return 0x24;   // e with acute accent
+	case 0x83: return 0x26;   // a with circumflex
+	case 0x85: return 0x5D;   // a with grave accent
+	case 0x87: return 0x7D;   // c with cedilla
+	case 0x88: return 0x23;   // e with circumflex
+	case 0x8A: return 0x25;   // e with grave accent
+	case 0x8B: return 0x5F;   // i with diaeresis
+	case 0x8C: return 0x7B;   // i with circumflex
+	case 0x93: return 0x5B;   // o with circumflex
+	case 0x96: return 0x3D;   // u with circumflex
+	case 0x97: return 0x5C;   // u with grave accent
+	case 0x80: return 'C';    // capital C with cedilla, which has no glyph
+	case 0x90: return 'E';    // capital E with acute accent, which has no glyph
+	default:
+		// Never let an unknown high byte become a control code
+		c &= 0x7f;
+		return (c < ' ') ? ' ' : c;
+	}
+}
+
 uint16_t FontSurface::getNextChar() {
 	if (_isBig5) {
 		uint8_t lead = *_displayString++;
@@ -371,6 +397,8 @@ uint16_t FontSurface::getNextChar() {
 		return (lead << 8) | (*_displayString++ & 0xff);
 	} else if (Common::RU_RUS == lang)
 		return *_displayString++ & 0xff;
+	else if (Common::FR_FRA == lang && (*_displayString & 0x80))
+		return frenchChar(*_displayString++ & 0xff);
 	else
 		return *_displayString++ & 0x7f;
 }


Commit: 7d81c8e0e7e8e3167da01e2b01ab39f4f6644e4c
    https://github.com/scummvm/scummvm/commit/7d81c8e0e7e8e3167da01e2b01ab39f4f6644e4c
Author: NK (nicolas.devillers at airbus.com)
Date: 2026-07-09T20:44:52+10:00

Commit Message:
MM: XEEN: Fix subtitle reveal off-by-one

The character-by-character subtitle reveal capped _lineEnd at
_lineSize - 1, so the last character of a line was never displayed:
the line blanked and reset right before completing, and the hold state
added for TTS kept the incomplete text frozen on screen for the whole
duration of the speech.

Let _lineEnd reach _lineSize so the full line is shown before the
display wraps and resets. This also removes a division by zero when a
subtitle line is empty.

Changed paths:
    engines/mm/xeen/subtitles.cpp


diff --git a/engines/mm/xeen/subtitles.cpp b/engines/mm/xeen/subtitles.cpp
index c704a436e51..c81f4f10750 100644
--- a/engines/mm/xeen/subtitles.cpp
+++ b/engines/mm/xeen/subtitles.cpp
@@ -143,8 +143,8 @@ void Subtitles::show() {
 		reset();
 	} else {
 		Common::TextToSpeechManager *ttsMan = g_system->getTextToSpeechManager();
-		if (timeElapsed() && (_lineEnd != _lineSize - 1 || !ttsMan || !ttsMan->isSpeaking())) {
-			_lineEnd = (_lineEnd + 1) % _lineSize;
+		if (timeElapsed() && (_lineEnd != _lineSize || !ttsMan || !ttsMan->isSpeaking())) {
+			_lineEnd = (_lineEnd + 1) % (_lineSize + 1);
 			int count;
 			if (Common::RU_RUS == g_vm->getLanguage())
 				count = MAX(_lineEnd - 36, 0);


Commit: e1654f1bd07ab58573e7914a703529c201d132ed
    https://github.com/scummvm/scummvm/commit/e1654f1bd07ab58573e7914a703529c201d132ed
Author: NK (nicolas.devillers at airbus.com)
Date: 2026-07-09T20:44:52+10:00

Commit Message:
MM: XEEN: Keep subtitles inside their box

The subtitle reveal capped the visible portion of a line at 40
characters (36 for Russian), but with the variable-width font that
doesn't bound its rendered width: a window of wide glyphs could exceed
the screen width, making writeString word-wrap it below the subtitle
box or truncate it, and any text wider than the opaque box graphic
wasn't erased by the next redraw.

Add FontSurface::fitToWidth(), which drops leading characters until
the rendered text stays narrower than a given pixel width, and use it
to fit the visible portion within the subtitle box. This supersedes
the Russian-specific 36-character cap; note that Russian lines, which
previously spilled beyond the box (36 wide glyphs can reach ~288
pixels), are now kept inside it like all other languages.

Also mark the whole box dirty on each draw, since the cutscene wait
loops redraw the subtitle more often than the scene repaints the
screen, so that a partially revealed line never lingers over a more
recent one.

Changed paths:
    engines/mm/xeen/font.cpp
    engines/mm/xeen/font.h
    engines/mm/xeen/subtitles.cpp


diff --git a/engines/mm/xeen/font.cpp b/engines/mm/xeen/font.cpp
index 9c6a970b860..adc437c0738 100644
--- a/engines/mm/xeen/font.cpp
+++ b/engines/mm/xeen/font.cpp
@@ -353,6 +353,31 @@ const char *FontSurface::writeString(const Common::String &s, const Common::Rect
 	return _displayString;
 }
 
+const char *FontSurface::fitToWidth(const char *s, int maxWidth) {
+	const char *strSave = _displayString;
+
+	for (;;) {
+		// Measure the rendered width of the string
+		_displayString = s;
+		int total = 0;
+		while (*_displayString && !getNextCharWidth(total)) {
+		}
+
+		// writeString wraps once its running position reaches the right
+		// edge, so the text only fits if it stays strictly narrower
+		if (total < maxWidth || !*s)
+			break;
+
+		// Too wide, so drop the leading character and remeasure
+		_displayString = s;
+		getNextChar();
+		s = _displayString;
+	}
+
+	_displayString = strSave;
+	return s;
+}
+
 void FontSurface::writeCharacter(uint16_t c, const Common::Rect &clipRect) {
 	Justify justify = _fontJustify;
 	_fontJustify = JUSTIFY_NONE;
diff --git a/engines/mm/xeen/font.h b/engines/mm/xeen/font.h
index 850185148fa..fb36b55284f 100644
--- a/engines/mm/xeen/font.h
+++ b/engines/mm/xeen/font.h
@@ -117,6 +117,16 @@ public:
 	 *		justification is set, the message will be written at _writePos
 	 */
 	const char *writeString(const Common::String &s, const Common::Rect &clipRect, bool ttsVoiceText = true, Common::String *ttsMessage = nullptr);
+
+	/**
+	 * Returns the tail of a plain text string, dropping as many leading
+	 * characters as necessary for the rendered text to be narrower than
+	 * the given pixel width, and so display without wrapping
+	 * @param s			String to fit; measuring stops at any control code
+	 * @param maxWidth	Pixel width the rendered string must stay below
+	 * @returns			Pointer within s to the portion that fits
+	 */
+	const char *fitToWidth(const char *s, int maxWidth);
 	bool isSpace(char c);
 	/**
 	 * Write a charcter to the window
diff --git a/engines/mm/xeen/subtitles.cpp b/engines/mm/xeen/subtitles.cpp
index c81f4f10750..9d4d4df0ebf 100644
--- a/engines/mm/xeen/subtitles.cpp
+++ b/engines/mm/xeen/subtitles.cpp
@@ -24,6 +24,7 @@
 #include "mm/xeen/subtitles.h"
 #include "mm/xeen/events.h"
 #include "mm/xeen/files.h"
+#include "mm/xeen/screen.h"
 #include "mm/xeen/xeen.h"
 
 namespace MM {
@@ -31,6 +32,9 @@ namespace Xeen {
 
 static const char *SUBTITLE_LINE = "\f35\x3""c\v190\t000%s";
 
+// Bounds of the subtitle box (box.vga frame 0, an opaque 255x11 panel)
+static constexpr Common::Rect SUBTITLE_BOX(Common::Point(36, 189), 255, 11);
+
 Subtitles::Subtitles() : _lineNum(-1), _boxSprites(nullptr), _lineEnd(0), _lineSize(0) {
 }
 
@@ -145,19 +149,24 @@ void Subtitles::show() {
 		Common::TextToSpeechManager *ttsMan = g_system->getTextToSpeechManager();
 		if (timeElapsed() && (_lineEnd != _lineSize || !ttsMan || !ttsMan->isSpeaking())) {
 			_lineEnd = (_lineEnd + 1) % (_lineSize + 1);
-			int count;
-			if (Common::RU_RUS == g_vm->getLanguage())
-				count = MAX(_lineEnd - 36, 0);
-			else
-				count = MAX(_lineEnd - 40, 0);
+			int count = MAX(_lineEnd - 40, 0);
 
 			// Get the portion of the line to display
 			char buffer[1000];
 			strncpy(buffer, _lines[_lineNum].c_str() + count, _lineEnd - count);
 			buffer[_lineEnd - count] = '\0';
 
+			// The character-based cap above doesn't bound the pixel width of
+			// the variable-width font, so drop further leading characters
+			// until the text fits within the subtitle box, whose opaque
+			// interior then erases the previous text on each redraw. The
+			// text is centered on the screen, so it stays inside the box as
+			// long as it doesn't reach the box's left edge
+			const char *text = windows[0].fitToWidth(buffer,
+				2 * (SCREEN_WIDTH / 2 - SUBTITLE_BOX.left));
+
 			// Form the display line
-			_displayLine = Common::String::format(SUBTITLE_LINE, buffer);
+			_displayLine = Common::String::format(SUBTITLE_LINE, text);
 			markTime();
 		}
 
@@ -165,11 +174,16 @@ void Subtitles::show() {
 		if (!_boxSprites)
 			// Not already loaded, so load it
 			_boxSprites = new SpriteResource("box.vga");
-		_boxSprites->draw(0, 0, Common::Point(36, 189));
+		_boxSprites->draw(0, 0, Common::Point(SUBTITLE_BOX.left, SUBTITLE_BOX.top));
 
 		// Write the subtitle line
 		windows[0].writeString(_displayLine, false);
 
+		// The subtitles are redrawn more often than the scenes showing them
+		// redraw the screen, so always push the whole box out, so that a
+		// partially drawn line never lingers over a more recent one
+		windows[0].addDirtyRect(SUBTITLE_BOX);
+
 		if (_lineEnd == 0 && (!ttsMan || !ttsMan->isSpeaking()))
 			reset();
 	}


Commit: 40b9e1d018777509bf05d47aaa37243fa00168cf
    https://github.com/scummvm/scummvm/commit/40b9e1d018777509bf05d47aaa37243fa00168cf
Author: NK (nicolas.devillers at airbus.com)
Date: 2026-07-09T20:44:52+10:00

Commit Message:
MM: XEEN: Adapt subtitle pacing to line length

The reveal always advanced one character every two ticks (10 chars per
second), a speed tuned for the English lines. Localized lines can be
much longer (the French Dark Side intro has a 181-character line,
taking 18 seconds to reveal), which left the text lagging well behind
the voice and the animation. Scale the step with the line length, one
extra character per 100 characters of line, so lines up to 100
characters keep the original pacing while longer ones catch up.

Also keep the fully revealed line on screen while a voice line is
still playing, as was already done for TTS speech, instead of blanking
it in the middle of the speech. Note that in text-only mode long lines
now finish sooner, so scenes that wait on the subtitle end earlier
than before (still about 20 characters per second of reading time).

The original game only ever showed subtitles when voice was disabled,
so the text pacing drove the scene; the lag only exists in ScummVM's
combined voice plus subtitles mode.

Changed paths:
    engines/mm/xeen/subtitles.cpp


diff --git a/engines/mm/xeen/subtitles.cpp b/engines/mm/xeen/subtitles.cpp
index 9d4d4df0ebf..b5b6c87350c 100644
--- a/engines/mm/xeen/subtitles.cpp
+++ b/engines/mm/xeen/subtitles.cpp
@@ -147,8 +147,15 @@ void Subtitles::show() {
 		reset();
 	} else {
 		Common::TextToSpeechManager *ttsMan = g_system->getTextToSpeechManager();
-		if (timeElapsed() && (_lineEnd != _lineSize || !ttsMan || !ttsMan->isSpeaking())) {
-			_lineEnd = (_lineEnd + 1) % (_lineSize + 1);
+		bool speaking = sound.isSoundPlaying() || (ttsMan && ttsMan->isSpeaking());
+
+		if (timeElapsed() && (_lineEnd != _lineSize || !speaking)) {
+			// Advance the reveal. The original reveals one character per
+			// interval, which was tuned for the English lines; localized
+			// lines can be much longer, so scale the step with the length
+			// to keep long lines from lagging behind the voice
+			int step = 1 + _lineSize / 100;
+			_lineEnd = (_lineEnd == _lineSize) ? 0 : MIN(_lineEnd + step, _lineSize);
 			int count = MAX(_lineEnd - 40, 0);
 
 			// Get the portion of the line to display
@@ -184,7 +191,7 @@ void Subtitles::show() {
 		// partially drawn line never lingers over a more recent one
 		windows[0].addDirtyRect(SUBTITLE_BOX);
 
-		if (_lineEnd == 0 && (!ttsMan || !ttsMan->isSpeaking()))
+		if (_lineEnd == 0 && !speaking)
 			reset();
 	}
 }




More information about the Scummvm-git-logs mailing list