[Scummvm-git-logs] scummvm master -> 084f0d6376b59d2d8aaf55b1f6390d88cbcc30a2

neuromancer noreply at scummvm.org
Sat Jun 6 19:01:46 UTC 2026


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

Summary:
80a7a31357 SCUMM: RA2: small corruption fixed in some levels
4865d17161 SCUMM: RA2: fixed corruption from L2 to L3
084f0d6376 SCUMM: RA2: reduce oscilation when using the mouse as input


Commit: 80a7a313577987c3c5bd1b91522ecaf61abd9f70
    https://github.com/scummvm/scummvm/commit/80a7a313577987c3c5bd1b91522ecaf61abd9f70
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-06T20:45:51+02:00

Commit Message:
SCUMM: RA2: small corruption fixed in some levels

Changed paths:
    engines/scumm/insane/rebel2/rebel.cpp
    engines/scumm/insane/rebel2/rebel.h
    engines/scumm/insane/rebel2/render.cpp
    engines/scumm/smush/rebel/codec_ra2.cpp
    engines/scumm/smush/rebel/codec_ra2.h
    engines/scumm/smush/rebel/smush_player_ra2.cpp
    engines/scumm/smush/rebel/smush_player_ra2.h


diff --git a/engines/scumm/insane/rebel2/rebel.cpp b/engines/scumm/insane/rebel2/rebel.cpp
index 7b4781a1ffa..7b8cb21dee8 100644
--- a/engines/scumm/insane/rebel2/rebel.cpp
+++ b/engines/scumm/insane/rebel2/rebel.cpp
@@ -174,6 +174,8 @@ InsaneRebel2::InsaneRebel2(ScummEngine_v7 *scumm) {
 
 	_pauseOverlayActive = false;
 	memset(_savedPausePalette, 0, sizeof(_savedPausePalette));
+	memset(_rebelEmbeddedCodec45Palette, 0, sizeof(_rebelEmbeddedCodec45Palette));
+	memset(_rebelEmbeddedCodec45Lookup, 0, sizeof(_rebelEmbeddedCodec45Lookup));
 
 	_enemies.clear();
 	_rebelHandler = 0;  // Not set yet - will be set by IACT opcode 6
@@ -1102,6 +1104,20 @@ bool InsaneRebel2::notifyEvent(const Common::Event &event) {
 			}
 			break;
 
+		case Common::KEYCODE_d:
+			// Debug shortcut: Shift+D forces the current gameplay section to end in death.
+			if (splayer &&
+			    _gameState == kStateGameplay &&
+			    _rebelHandler != 0 &&
+			    event.kbd.hasFlags(Common::KBD_SHIFT)) {
+				_playerDamage = 255;
+				_playerShield = 0;
+				debug("Rebel2: Shift+D pressed - forcing player death");
+				_vm->_smushVideoShouldFinish = true;
+				return true;  // Consume the event
+			}
+			break;
+
 		default:
 			break;
 		}
diff --git a/engines/scumm/insane/rebel2/rebel.h b/engines/scumm/insane/rebel2/rebel.h
index 099add0d6cd..e8fe499b865 100644
--- a/engines/scumm/insane/rebel2/rebel.h
+++ b/engines/scumm/insane/rebel2/rebel.h
@@ -772,6 +772,8 @@ public:
 	};
 
 	EmbeddedSanFrame _rebelEmbeddedHud[16];  // HUD overlay slots (userId 0-15)
+	byte _rebelEmbeddedCodec45Palette[0x300];
+	byte _rebelEmbeddedCodec45Lookup[0x8000];
 
 	// Load and decode an embedded SAN animation from IACT chunk data
 	// userId: HUD slot (1-4), animData: raw ANIM data, size: data size, renderBitmap: current frame buffer
diff --git a/engines/scumm/insane/rebel2/render.cpp b/engines/scumm/insane/rebel2/render.cpp
index c0a7e3ab4d7..1c986483361 100644
--- a/engines/scumm/insane/rebel2/render.cpp
+++ b/engines/scumm/insane/rebel2/render.cpp
@@ -403,8 +403,9 @@ void InsaneRebel2::loadEmbeddedSan(int userId, byte *animData, int32 size, byte
 								frame.valid = true;
 								debug("Rebel2: Decoded embedded HUD (codec %d/line update): %dx%d", codec, width, height);
 							} else if (codec == 45) {
-								// Codec 45: RA2-specific BOMP RLE (FUN_0042B5F0)
-								smushDecodeRA2Bomp(frame.pixels, fobjData, 0, 0, width, height, width, dataSize);
+								// Codec 45: blur/wipe mask (FUN_0042B460 -> FUN_0042B530 -> FUN_0042DDF0)
+								smushDecodeRA2Blur(frame.pixels, fobjData, 0, 0, width, height, width, dataSize,
+									_rebelEmbeddedCodec45Palette, _rebelEmbeddedCodec45Lookup);
 								frame.valid = true;
 							} else if (codec == 23) {
 								// Codec 23: Skip/copy with embedded RLE (FUN_0042BBF0)
diff --git a/engines/scumm/smush/rebel/codec_ra2.cpp b/engines/scumm/smush/rebel/codec_ra2.cpp
index 44f454f88b4..e528d51f8a9 100644
--- a/engines/scumm/smush/rebel/codec_ra2.cpp
+++ b/engines/scumm/smush/rebel/codec_ra2.cpp
@@ -225,134 +225,99 @@ void smushDecodeSkipRLE(byte *dst, const byte *src, int left, int top, int width
 	}
 }
 
-/**
- * Codec 45: RA2-specific BOMP RLE with variable header
- * Used for embedded ANIM frames, particularly small animation elements.
- * Has a variable-length header (commonly 6 bytes starting with 01 FE).
- * Note: For overlay sprites, color 0 is treated as transparent.
- */
-void smushDecodeRA2Bomp(byte *dst, const byte *src, int left, int top, int width, int height, int pitch, int dataSize) {
-	dst += top * pitch + left;
-
-	// Detect header pattern and find RLE data start
-	int headerSkip = 0;
-	bool foundValidOffset = false;
-
-	// Check for common 6-byte header pattern: 01 FE XX XX XX XX
-	if (dataSize > 6 && src[0] == 0x01 && src[1] == 0xFE) {
-		headerSkip = 6;
-		foundValidOffset = true;
-	} else {
-		// Probe offsets to find valid RLE start
-		// Valid start should have reasonable line size values
-		for (int testOffset = 0; testOffset <= 6 && testOffset + 2 <= dataSize; testOffset += 2) {
-			int testLineSize = READ_LE_INT16(src + testOffset);
-			// A valid line size should be positive and reasonable for the width
-			if (testLineSize > 0 && testLineSize <= width * 2 && testLineSize < dataSize - testOffset) {
-				// Further validation: try to count valid line sizes
-				int linesTest = 0;
-				const byte *testPtr = src + testOffset;
-				bool validSum = true;
-
-				while (linesTest < height && testPtr + 2 <= src + dataSize) {
-					int ls = READ_LE_INT16(testPtr);
-					if (ls <= 0 || ls > width * 2) {
-						validSum = false;
-						break;
-					}
-					testPtr += ls + 2;
-					linesTest++;
-				}
-
-				if (validSum && linesTest >= height - 1) {
-					headerSkip = testOffset;
-					foundValidOffset = true;
-					break;
-				}
-			}
+bool smushPrepareRA2BlurData(const byte *src, int dataSize, byte *palette, byte *lookup, const byte *&maskData, int &maskSize) {
+	maskData = nullptr;
+	maskSize = 0;
+
+	if (src == nullptr || dataSize < 6 || palette == nullptr || lookup == nullptr)
+		return false;
+
+	// FUN_0042B530 only processes this codec body when byte +4 is 1.
+	if (src[4] != 1)
+		return false;
+
+	const int tableMode = READ_LE_INT16(src + 2);
+	maskData = src + 6;
+	maskSize = dataSize - 6;
+
+	if (tableMode == 0) {
+		if (dataSize < 0x306)
+			return false;
+
+		memcpy(palette, src + 6, 0x300);
+
+		const byte *p = src + 0x306;
+		int remaining = dataSize - 0x306;
+		int lookupIndex = 0;
+		while (lookupIndex < 0x8000 && remaining >= 2) {
+			int count = p[0];
+			const byte color = p[1];
+			p += 2;
+			remaining -= 2;
+
+			while (count-- > 0 && lookupIndex < 0x8000)
+				lookup[lookupIndex++] = color;
 		}
-	}
 
-	if (!foundValidOffset) {
-		warning("smushDecodeRA2Bomp: Codec 45 couldn't find valid RLE offset, using offset 0");
-	}
+		if (lookupIndex < 0x8000)
+			return false;
 
-	src += headerSkip;
-	const byte *dataEnd = src + (dataSize - headerSkip);
+		maskData = p;
+		maskSize = remaining;
+	}
 
-	// Check first value to determine per-line vs continuous mode
-	int firstVal = (src + 2 <= dataEnd) ? READ_LE_INT16(src) : 0;
-	bool perLineMode = (firstVal > 0 && firstVal <= width * 2);
+	return maskSize > 3;
+}
 
-	if (perLineMode) {
-		// Per-line RLE with 2-byte size headers
-		for (int row = 0; row < height && src < dataEnd; row++) {
-			int lineSize = READ_LE_INT16(src);
-			src += 2;
-			if (lineSize <= 0 || lineSize > (int)(dataEnd - src))
-				break;
+/**
+ * Codec 45: RA2 blur/wipe mask.
+ * Original path: FUN_0042B460 -> FUN_0042B530 -> FUN_0042DDF0.
+ */
+void smushDecodeRA2Blur(byte *dst, const byte *src, int left, int top, int dstWidth, int dstHeight, int pitch, int dataSize, byte *palette, byte *lookup) {
+	const byte *maskData = nullptr;
+	int maskSize = 0;
+	if (dst == nullptr || dstWidth <= 2 || dstHeight <= 2 || pitch <= 0 ||
+			!smushPrepareRA2BlurData(src, dataSize, palette, lookup, maskData, maskSize))
+		return;
 
-			const byte *lineEnd = src + lineSize;
-			byte *rowDst = dst + row * pitch;
-			int x = 0;
-
-			while (src < lineEnd && x < width) {
-				byte ctrl = *src++;
-				int count = (ctrl >> 1) + 1;
-
-				if (ctrl & 1) {
-					// RLE fill - color 0 is transparent for overlay sprites
-					byte color = (src < lineEnd) ? *src++ : 0;
-					if (color != 0) {
-						int num = (count > width - x) ? width - x : count;
-						memset(rowDst + x, color, num);
-					}
-					x += count;
-					if (x > width)
-						x = width;
-				} else {
-					// Literal copy - color 0 is transparent for overlay sprites
-					for (int i = 0; i < count && x < width && src < lineEnd; i++) {
-						byte color = *src++;
-						if (color != 0)
-							rowDst[x] = color;
-						x++;
-					}
-				}
-			}
-			src = lineEnd;
-		}
-	} else {
-		// Continuous BOMP RLE (no per-line headers)
-		for (int row = 0; row < height && src < dataEnd; row++) {
-			byte *rowDst = dst + row * pitch;
-			int x = 0;
-
-			while (x < width && src < dataEnd) {
-				byte ctrl = *src++;
-				int count = (ctrl >> 1) + 1;
-
-				if (ctrl & 1) {
-					// RLE fill - color 0 is transparent for overlay sprites
-					byte color = (src < dataEnd) ? *src++ : 0;
-					if (color != 0) {
-						int num = (count > width - x) ? width - x : count;
-						memset(rowDst + x, color, num);
-					}
-					x += count;
-					if (x > width)
-						x = width;
-				} else {
-					// Literal copy - color 0 is transparent for overlay sprites
-					for (int i = 0; i < count && x < width && src < dataEnd; i++) {
-						byte color = *src++;
-						if (color != 0)
-							rowDst[x] = color;
-						x++;
-					}
-				}
+	int x = left;
+	int y = top;
+
+	while (maskSize > 3) {
+		const int dx = READ_LE_INT16(maskData);
+		const int dy = maskData[2];
+		const int count = maskData[3];
+		maskData += 4;
+		maskSize -= 4;
+
+		x += dx;
+		y += dy;
+
+		for (int i = 0; i <= count; ++i) {
+			if (x > 0 && y > 0 && x < dstWidth - 1) {
+				if (y >= dstHeight - 1)
+					return;
+
+				byte *pixel = dst + y * pitch + x;
+				const byte leftColor = pixel[-1];
+				const byte rightColor = pixel[1];
+				const byte topColor = pixel[-pitch];
+				const byte bottomColor = pixel[pitch];
+
+				const int red = palette[leftColor * 3] + palette[rightColor * 3] +
+					palette[topColor * 3] + palette[bottomColor * 3];
+				const int green = palette[leftColor * 3 + 1] + palette[rightColor * 3 + 1] +
+					palette[topColor * 3 + 1] + palette[bottomColor * 3 + 1];
+				const int blue = palette[leftColor * 3 + 2] + palette[rightColor * 3 + 2] +
+					palette[topColor * 3 + 2] + palette[bottomColor * 3 + 2];
+
+				const int lookupIndex = ((red << 5) & 0x7c00) + (green & 0x3e0) + (blue >> 5);
+				*pixel = lookup[lookupIndex & 0x7fff];
 			}
+
+			++x;
 		}
+		--x;
 	}
 }
 
diff --git a/engines/scumm/smush/rebel/codec_ra2.h b/engines/scumm/smush/rebel/codec_ra2.h
index 1aa8d2e86d0..b89797c1a0a 100644
--- a/engines/scumm/smush/rebel/codec_ra2.h
+++ b/engines/scumm/smush/rebel/codec_ra2.h
@@ -29,7 +29,7 @@ namespace Scumm {
 void smushDecodeRLEOpaque(byte *dst, const byte *src, int left, int top, int width, int height, int pitch, int dataSize);
 void smushDecodeLineUpdate(byte *dst, const byte *src, int left, int top, int width, int height, int pitch, int dataSize);
 void smushDecodeSkipRLE(byte *dst, const byte *src, int left, int top, int width, int height, int pitch, int dataSize);
-void smushDecodeRA2Bomp(byte *dst, const byte *src, int left, int top, int width, int height, int pitch, int dataSize);
+void smushDecodeRA2Blur(byte *dst, const byte *src, int left, int top, int dstWidth, int dstHeight, int pitch, int dataSize, byte *palette, byte *lookup);
 const byte *smushSkipRLELines(const byte *src, int &dataSize, int lines);
 
 } // End of namespace Scumm
diff --git a/engines/scumm/smush/rebel/smush_player_ra2.cpp b/engines/scumm/smush/rebel/smush_player_ra2.cpp
index 2c43508df0d..b24ea74678c 100644
--- a/engines/scumm/smush/rebel/smush_player_ra2.cpp
+++ b/engines/scumm/smush/rebel/smush_player_ra2.cpp
@@ -140,6 +140,8 @@ void SmushPlayerRebel2::initGamePlayerFields() {
 	_ra2FrameObjectSurfaceWidth = 0;
 	_ra2FrameObjectSurfaceHeight = 0;
 	_ra2PendingAnimHeaderPalette = false;
+	memset(_ra2Codec45Palette, 0, sizeof(_ra2Codec45Palette));
+	memset(_ra2Codec45Lookup, 0, sizeof(_ra2Codec45Lookup));
 	_scrollX = 0;
 	_scrollY = 0;
 }
@@ -726,6 +728,18 @@ void SmushPlayerRebel2::ra2PrepareFrameObjectSurface(int left, int top, int widt
 }
 
 bool SmushPlayerRebel2::ra2SelectFrameBuffer(int codec, int width, int height) {
+	if (codec == SMUSH_CODEC_RA2_BOMP) {
+		if (_specialBuffer != nullptr) {
+			_dst = _specialBuffer;
+			debugC(DEBUG_SMUSH, "SmushPlayerRebel2::ra2SelectFrameBuffer: Using _specialBuffer for codec 45 mask");
+		} else {
+			VirtScreen *vs = &_vm->_virtscr[kMainVirtScreen];
+			_dst = vs->getPixels(0, 0);
+			debugC(DEBUG_SMUSH, "SmushPlayerRebel2::ra2SelectFrameBuffer: Using virtual screen for codec 45 mask");
+		}
+		return true;
+	}
+
 	// Rebel2 allocates the low-res gameplay target as 424x260 (FUN_00424730).
 	// Use that target once an oversized gameplay FOBJ appears, then keep small
 	// overlay FOBJ chunks compositing into it. Pure 320x200 gameplay videos keep
@@ -802,8 +816,20 @@ bool SmushPlayerRebel2::ra2SelectFrameBuffer(int codec, int width, int height) {
  * Returns true if the codec was handled, false for standard codecs.
  */
 bool SmushPlayerRebel2::ra2DecodeCodec(int codec, const uint8 *src, int left, int top,
-								 int width, int height, int pitch, int dataSize) {
+									 int width, int height, int pitch, int dataSize) {
 	switch (codec) {
+	case SMUSH_CODEC_RLE_ALT:
+		// RA2 codec 3 dispatches directly to FUN_0042CAA0, the opaque RLE path.
+		smushDecodeRLEOpaque(_dst, src, left, top, width, height, pitch, dataSize);
+		return true;
+	case SMUSH_CODEC_RLE:
+		if ((_curVideoFlags & 0x100) != 0) {
+			// Original RA2 RLE dispatch selects the opaque renderer only when
+			// render flag 0x100 is set. Cinematic wipe masks use transparent 0.
+			smushDecodeRLEOpaque(_dst, src, left, top, width, height, pitch, dataSize);
+			return true;
+		}
+		return false;
 	case SMUSH_CODEC_UNCOMPRESSED: {
 		const int sourcePitch = (_ra2FrameObjectOriginalWidth > 0) ? _ra2FrameObjectOriginalWidth : width;
 		smushDecodeRA2Uncompressed(_dst, src, left, top, width, height, pitch, sourcePitch,
@@ -822,8 +848,8 @@ bool SmushPlayerRebel2::ra2DecodeCodec(int codec, const uint8 *src, int left, in
 		return true;
 	}
 	case SMUSH_CODEC_RA2_BOMP: {
-		const uint8 *adjustedSrc = smushSkipRLELines(src, dataSize, _ra2FrameSourceSkipY);
-		smushDecodeRA2Bomp(_dst, adjustedSrc, left, top, width, height, pitch, dataSize);
+		smushDecodeRA2Blur(_dst, src, left, top, pitch, _height, pitch, dataSize,
+			_ra2Codec45Palette, _ra2Codec45Lookup);
 		return true;
 	}
 	default:
@@ -943,10 +969,18 @@ bool SmushPlayerRebel2::handleGameAdjustCoords(int codec, int &left, int &top, i
 	const int adjustedLeft = left + _fobjOffsetX;
 	_ra2FrameSourceSkipX = (adjustedLeft < 0) ? -adjustedLeft : 0;
 	_ra2FrameSourceSkipY = 0;
+
+	if (codec == SMUSH_CODEC_RA2_BOMP) {
+		left = adjustedLeft;
+		top += _fobjOffsetY;
+		if (srcSkipY)
+			*srcSkipY = 0;
+		return true;
+	}
+
 	adjustFrameCoords(left, top, width, height, pitch, &sourceSkipY);
 	if (codec == SMUSH_CODEC_LINE_UPDATE || codec == SMUSH_CODEC_LINE_UPDATE2 ||
-			codec == SMUSH_CODEC_SKIP_RLE || codec == SMUSH_CODEC_RA2_BOMP ||
-			codec == SMUSH_CODEC_UNCOMPRESSED) {
+			codec == SMUSH_CODEC_SKIP_RLE || codec == SMUSH_CODEC_UNCOMPRESSED) {
 		_ra2FrameSourceSkipY = sourceSkipY;
 		if (srcSkipY)
 			*srcSkipY = 0;
diff --git a/engines/scumm/smush/rebel/smush_player_ra2.h b/engines/scumm/smush/rebel/smush_player_ra2.h
index 87c64a12431..ac4700919e8 100644
--- a/engines/scumm/smush/rebel/smush_player_ra2.h
+++ b/engines/scumm/smush/rebel/smush_player_ra2.h
@@ -89,6 +89,8 @@ private:
 	int _ra2FrameObjectSurfaceWidth;
 	int _ra2FrameObjectSurfaceHeight;
 	bool _ra2PendingAnimHeaderPalette;
+	byte _ra2Codec45Palette[0x300];
+	byte _ra2Codec45Lookup[0x8000];
 };
 
 } // End of namespace Scumm


Commit: 4865d1716190ff623e2da2fba5f14881a66df031
    https://github.com/scummvm/scummvm/commit/4865d1716190ff623e2da2fba5f14881a66df031
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-06T20:45:51+02:00

Commit Message:
SCUMM: RA2: fixed corruption from L2 to L3

Changed paths:
    engines/scumm/insane/rebel2/iact.cpp
    engines/scumm/insane/rebel2/rebel.h
    engines/scumm/insane/rebel2/runlevels.cpp
    engines/scumm/smush/rebel/font_rebel2.cpp
    engines/scumm/smush/rebel/font_rebel2.h


diff --git a/engines/scumm/insane/rebel2/iact.cpp b/engines/scumm/insane/rebel2/iact.cpp
index 8122855a018..bbde2d14589 100644
--- a/engines/scumm/insane/rebel2/iact.cpp
+++ b/engines/scumm/insane/rebel2/iact.cpp
@@ -2079,10 +2079,9 @@ bool InsaneRebel2::loadHandler7FlySprites(Common::SeekableReadStream &b, int64 r
 		}
 	}
 
-	// Load as NUT
-	NutRenderer *newNut = new NutRenderer(_vm);
-	newNut->loadFontFromData(nutData, bytesRead);
-	if (newNut->getNumChars() <= 0) {
+	// Load as a Rebel2 embedded sprite ANIM.
+	NutRenderer *newNut = makeRebel2SpriteFromData(_vm, nutData, bytesRead);
+	if (!newNut || newNut->getNumChars() <= 0) {
 		debug("Rebel2 loadHandler7FlySprites: NUT load failed for par4=%d", par4);
 		delete newNut;
 		free(nutData);
@@ -2145,8 +2144,7 @@ bool InsaneRebel2::loadTurretHudOverlay(byte *animData, int32 size, int16 par3)
 		return false;  // Not a turret HUD slot
 	}
 
-	NutRenderer *newNut = new NutRenderer(_vm);
-	newNut->loadFontFromData(animData, size);
+	NutRenderer *newNut = makeRebel2SpriteFromData(_vm, animData, size);
 	if (!newNut || newNut->getNumChars() <= 0) {
 		debug("Rebel2 loadTurretHudOverlay: NUT load failed for par3=%d", par3);
 		delete newNut;
@@ -2187,8 +2185,7 @@ bool InsaneRebel2::loadHandler8ShipSprites(byte *animData, int32 size, int16 par
 		return false;
 	}
 
-	NutRenderer *newNut = new NutRenderer(_vm);
-	newNut->loadFontFromData(animData, size);
+	NutRenderer *newNut = makeRebel2SpriteFromData(_vm, animData, size);
 	if (!newNut || newNut->getNumChars() <= 0) {
 		debug("Rebel2 loadHandler8ShipSprites: NUT load failed for par4=%d", par4);
 		delete newNut;
@@ -2239,8 +2236,7 @@ bool InsaneRebel2::loadHandler25GrdSprites(byte *animData, int32 size, int16 par
 		return false;
 	}
 
-	NutRenderer *newNut = new NutRenderer(_vm);
-	newNut->loadFontFromData(animData, size);
+	NutRenderer *newNut = makeRebel2SpriteFromData(_vm, animData, size);
 	if (!newNut || newNut->getNumChars() <= 0) {
 		debug("Rebel2 loadHandler25GrdSprites: NUT load failed for par4=%d", par4);
 		delete newNut;
diff --git a/engines/scumm/insane/rebel2/rebel.h b/engines/scumm/insane/rebel2/rebel.h
index e8fe499b865..765ab1f4b22 100644
--- a/engines/scumm/insane/rebel2/rebel.h
+++ b/engines/scumm/insane/rebel2/rebel.h
@@ -434,6 +434,7 @@ public:
 	bool handleLevelDeath(int levelId, int phase, const char *deathVideo, const char *retryVideo, int &levelResult);
 	void resetLevelAttemptState(int initialPhase);
 	void resetLevelPhaseState(bool clearEnemies);
+	void clearEmbeddedHudFrames();
 	void resetLevelWaveState();
 	void resetHandler7FlightState();
 
diff --git a/engines/scumm/insane/rebel2/runlevels.cpp b/engines/scumm/insane/rebel2/runlevels.cpp
index 55a0e046ee1..ede78f398b2 100644
--- a/engines/scumm/insane/rebel2/runlevels.cpp
+++ b/engines/scumm/insane/rebel2/runlevels.cpp
@@ -278,6 +278,17 @@ void InsaneRebel2::resetLevelPhaseState(bool clearEnemies) {
 	_grd002Sprite = nullptr;
 	_grdShotOriginTableLoaded = false;
 
+	clearEmbeddedHudFrames();
+
+	free(_level2Background);
+	_level2Background = nullptr;
+	_level2BackgroundLoaded = false;
+
+	if (clearEnemies)
+		_enemies.clear();
+}
+
+void InsaneRebel2::clearEmbeddedHudFrames() {
 	for (uint i = 0; i < ARRAYSIZE(_rebelEmbeddedHud); ++i) {
 		EmbeddedSanFrame &frame = _rebelEmbeddedHud[i];
 		free(frame.pixels);
@@ -288,13 +299,6 @@ void InsaneRebel2::resetLevelPhaseState(bool clearEnemies) {
 		frame.renderY = 0;
 		frame.valid = false;
 	}
-
-	free(_level2Background);
-	_level2Background = nullptr;
-	_level2BackgroundLoaded = false;
-
-	if (clearEnemies)
-		_enemies.clear();
 }
 
 void InsaneRebel2::resetLevelWaveState() {
@@ -647,6 +651,8 @@ int InsaneRebel2::runLevel3() {
 		_playerDamage = 0;
 		_currentPhase = 1;
 
+		clearEmbeddedHudFrames();
+
 		// Reset bit table before gameplay starts - FUN_00423880 calls FUN_00423a00(0)
 		clearBit(0);
 		resetHandler7FlightState();
@@ -699,6 +705,8 @@ int InsaneRebel2::runLevel3() {
 		_playerDamage = 0;
 		_playerScore = phase1Score;
 
+		clearEmbeddedHudFrames();
+
 		// Reset bit table before gameplay starts
 		clearBit(0);
 		resetHandler7FlightState();
diff --git a/engines/scumm/smush/rebel/font_rebel2.cpp b/engines/scumm/smush/rebel/font_rebel2.cpp
index d83fbef6585..a9f69d0cbdb 100644
--- a/engines/scumm/smush/rebel/font_rebel2.cpp
+++ b/engines/scumm/smush/rebel/font_rebel2.cpp
@@ -27,22 +27,50 @@
 #include "scumm/file.h"
 #include "scumm/nut_renderer.h"
 #include "scumm/scumm.h"
+#include "scumm/smush/rebel/codec_ra2.h"
 
 namespace Scumm {
 
 enum {
-	kRebel2MaxStrings = 80
+	kRebel2MaxStrings = 80,
+	kRebel2MaxSpritePixels = 16 * 1024 * 1024,
+	kRebel2MaxDecodedSpriteBytes = 32 * 1024 * 1024
 };
 
+struct Rebel2NutFrameInfo {
+	Rebel2NutFrameInfo() : codec(0), xoffs(0), yoffs(0), width(0), height(0), data(nullptr), dataSize(0) {}
+
+	int codec;
+	int16 xoffs;
+	int16 yoffs;
+	uint16 width;
+	uint16 height;
+	const byte *data;
+	int32 dataSize;
+};
+
+static void decodeRebel2RawSprite(byte *dst, const byte *src, int width, int height, int dataSize) {
+	const int32 totalSize = width * height;
+	const int32 copySize = MIN<int32>(totalSize, dataSize);
+	if (copySize > 0)
+		memcpy(dst, src, copySize);
+}
+
 class Rebel2NutRenderer : public NutRenderer {
 public:
 	Rebel2NutRenderer(ScummEngine *vm, const char *filename) : NutRenderer(vm, nullptr) {
 		loadRebel2Font(filename);
 	}
 
+	Rebel2NutRenderer(ScummEngine *vm, const byte *data, int32 dataSize) : NutRenderer(vm, nullptr) {
+		loadRebel2SpriteFromData(data, dataSize);
+	}
+
 private:
 	void codec44(byte *dst, const byte *src, int width, int height, int pitch);
 	void loadRebel2Font(const char *filename);
+	void loadRebel2SpriteFromData(const byte *data, int32 dataSize);
+	void decodeRebel2Frame(byte *dst, const Rebel2NutFrameInfo &frame, byte *codec45Palette, byte *codec45Lookup);
 };
 
 void Rebel2NutRenderer::codec44(byte *dst, const byte *src, int width, int height, int pitch) {
@@ -189,10 +217,176 @@ void Rebel2NutRenderer::loadRebel2Font(const char *filename) {
 	delete[] dataSrc;
 }
 
+void Rebel2NutRenderer::decodeRebel2Frame(byte *dst, const Rebel2NutFrameInfo &frame, byte *codec45Palette, byte *codec45Lookup) {
+	switch (frame.codec) {
+	case 1:
+	case 3:
+		codec1(dst, frame.data, frame.width, frame.height, frame.width);
+		break;
+	case 20:
+		decodeRebel2RawSprite(dst, frame.data, frame.width, frame.height, frame.dataSize);
+		break;
+	case 21:
+		smushDecodeLineUpdate(dst, frame.data, 0, 0, frame.width, frame.height, frame.width, frame.dataSize);
+		break;
+	case 23:
+		smushDecodeSkipRLE(dst, frame.data, 0, 0, frame.width, frame.height, frame.width, frame.dataSize);
+		break;
+	case 44:
+		codec44(dst, frame.data, frame.width, frame.height, frame.width);
+		break;
+	case 45:
+		smushDecodeRA2Blur(dst, frame.data, 0, 0, frame.width, frame.height, frame.width, frame.dataSize,
+			codec45Palette, codec45Lookup);
+		break;
+	default:
+		warning("Rebel2NutRenderer::loadRebel2SpriteFromData: unknown codec: %d", frame.codec);
+		break;
+	}
+}
+
+void Rebel2NutRenderer::loadRebel2SpriteFromData(const byte *data, int32 dataSize) {
+	if (!data || dataSize < 16) {
+		warning("Rebel2NutRenderer::loadRebel2SpriteFromData: data too small (%d bytes)", dataSize);
+		return;
+	}
+
+	if (READ_BE_UINT32(data) != MKTAG('A','N','I','M')) {
+		warning("Rebel2NutRenderer::loadRebel2SpriteFromData: no ANIM chunk");
+		return;
+	}
+
+	uint32 length = READ_BE_UINT32(data + 4);
+	if (length > (uint32)(dataSize - 8)) {
+		warning("Rebel2NutRenderer::loadRebel2SpriteFromData: ANIM size (%u) exceeds data size (%d)", length, dataSize);
+		length = dataSize - 8;
+	}
+
+	const int64 animEnd = 8 + (int64)length;
+	if (animEnd < 16 || READ_BE_UINT32(data + 8) != MKTAG('A','H','D','R')) {
+		warning("Rebel2NutRenderer::loadRebel2SpriteFromData: no AHDR chunk in font data");
+		return;
+	}
+
+	int declaredChars = READ_LE_UINT16(data + 18);
+	if (declaredChars > (int)ARRAYSIZE(_chars)) {
+		warning("Rebel2NutRenderer::loadRebel2SpriteFromData: numChars (%d) exceeds max, clamping", declaredChars);
+		declaredChars = ARRAYSIZE(_chars);
+	}
+
+	Rebel2NutFrameInfo frames[ARRAYSIZE(_chars)];
+	uint64 decodedLength = 0;
+	int frameCount = 0;
+
+	for (int64 offset = 8; offset + 8 <= animEnd && frameCount < declaredChars;) {
+		const uint32 tag = READ_BE_UINT32(data + offset);
+		const uint32 chunkSize = READ_BE_UINT32(data + offset + 4);
+		const int64 chunkDataStart = offset + 8;
+		if ((int64)chunkSize > animEnd - chunkDataStart) {
+			warning("Rebel2NutRenderer::loadRebel2SpriteFromData: truncated chunk 0x%08x at offset %llx", tag, (long long)offset);
+			break;
+		}
+
+		const int64 chunkDataEnd = chunkDataStart + chunkSize;
+		int64 nextChunk = chunkDataEnd + (chunkSize & 1);
+		if (nextChunk > animEnd)
+			nextChunk = chunkDataEnd;
+
+		if (tag == MKTAG('F','R','M','E')) {
+			Rebel2NutFrameInfo &frame = frames[frameCount];
+			if (chunkSize >= 8 && chunkDataStart + 8 <= chunkDataEnd &&
+					READ_BE_UINT32(data + chunkDataStart) == MKTAG('F','O','B','J')) {
+				const uint32 fobjSize = READ_BE_UINT32(data + chunkDataStart + 4);
+				const int64 fobjDataStart = chunkDataStart + 8;
+				const int64 fobjDataEnd = fobjDataStart + fobjSize;
+
+				if (fobjSize >= 14 && fobjDataEnd <= chunkDataEnd) {
+					frame.codec = READ_LE_UINT16(data + fobjDataStart);
+					frame.xoffs = READ_LE_INT16(data + fobjDataStart + 2);
+					frame.yoffs = READ_LE_INT16(data + fobjDataStart + 4);
+					frame.width = READ_LE_UINT16(data + fobjDataStart + 6);
+					frame.height = READ_LE_UINT16(data + fobjDataStart + 8);
+					frame.data = data + fobjDataStart + 14;
+					frame.dataSize = fobjSize - 14;
+
+					const uint64 pixels = (uint64)frame.width * frame.height;
+					if (pixels == 0) {
+						frame.width = 0;
+						frame.height = 0;
+						frame.data = nullptr;
+						frame.dataSize = 0;
+					} else if (pixels > kRebel2MaxSpritePixels || decodedLength + pixels > kRebel2MaxDecodedSpriteBytes) {
+						warning("Rebel2NutRenderer::loadRebel2SpriteFromData: invalid sprite dimensions %ux%u at frame %d",
+							frame.width, frame.height, frameCount);
+						frame.width = 0;
+						frame.height = 0;
+						frame.data = nullptr;
+						frame.dataSize = 0;
+					} else {
+						decodedLength += pixels;
+					}
+				}
+			}
+			frameCount++;
+		}
+
+		offset = nextChunk;
+	}
+
+	_numChars = frameCount;
+	if (_numChars <= 0) {
+		warning("Rebel2NutRenderer::loadRebel2SpriteFromData: no decodable frames");
+		return;
+	}
+
+	delete[] _decodedData;
+	_decodedData = decodedLength ? new byte[(uint32)decodedLength] : nullptr;
+	memset(_chars, 0, sizeof(_chars));
+	_fontHeight = 0;
+
+	byte *decodedPtr = _decodedData;
+	byte codec45Palette[0x300];
+	byte codec45Lookup[0x8000];
+	memset(codec45Palette, 0, sizeof(codec45Palette));
+	memset(codec45Lookup, 0, sizeof(codec45Lookup));
+
+	for (int i = 0; i < _numChars; i++) {
+		const Rebel2NutFrameInfo &frame = frames[i];
+		_chars[i].xoffs = frame.xoffs;
+		_chars[i].yoffs = frame.yoffs;
+		_chars[i].width = frame.width;
+		_chars[i].height = frame.height;
+		_chars[i].transparency = kDefaultTransparentColor;
+
+		if (frame.width == 0 || frame.height == 0 || frame.data == nullptr)
+			continue;
+
+		const uint32 pixels = frame.width * frame.height;
+		_chars[i].src = decodedPtr;
+		decodedPtr += pixels;
+		_fontHeight = MAX<int>(_fontHeight, frame.height);
+
+		memset(_chars[i].src, kDefaultTransparentColor, pixels);
+		decodeRebel2Frame(_chars[i].src, frame, codec45Palette, codec45Lookup);
+	}
+
+	debug(1, "Rebel2NutRenderer::loadRebel2SpriteFromData() - numChars=%d decodedLength=%u", _numChars, (uint32)decodedLength);
+}
+
 NutRenderer *makeRebel2Font(ScummEngine *vm, const char *filename) {
 	return new Rebel2NutRenderer(vm, filename);
 }
 
+NutRenderer *makeRebel2SpriteFromData(ScummEngine *vm, const byte *data, int32 dataSize) {
+	Rebel2NutRenderer *renderer = new Rebel2NutRenderer(vm, data, dataSize);
+	if (renderer->getNumChars() <= 0) {
+		delete renderer;
+		return nullptr;
+	}
+
+	return renderer;
+}
+
 Rebel2FontSet::Rebel2FontSet() : numFonts(0), defaultFont(0) {
 	memset(fonts, 0, sizeof(fonts));
 }
diff --git a/engines/scumm/smush/rebel/font_rebel2.h b/engines/scumm/smush/rebel/font_rebel2.h
index 2a5ae2f7ae7..1276e1c13a3 100644
--- a/engines/scumm/smush/rebel/font_rebel2.h
+++ b/engines/scumm/smush/rebel/font_rebel2.h
@@ -46,6 +46,7 @@ struct Rebel2FontSet {
 };
 
 NutRenderer *makeRebel2Font(ScummEngine *vm, const char *filename);
+NutRenderer *makeRebel2SpriteFromData(ScummEngine *vm, const byte *data, int32 dataSize);
 int drawRebel2Char(NutRenderer *font, byte *buffer, Common::Rect &clipRect, int x, int y,
 		int pitch, int16 col, byte chr);
 int getRebel2StringWidth(const Rebel2FontSet &fontSet, const char *str, uint len);


Commit: 084f0d6376b59d2d8aaf55b1f6390d88cbcc30a2
    https://github.com/scummvm/scummvm/commit/084f0d6376b59d2d8aaf55b1f6390d88cbcc30a2
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-06T20:56:53+02:00

Commit Message:
SCUMM: RA2: reduce oscilation when using the mouse as input

Changed paths:
    engines/scumm/insane/rebel2/iact.cpp


diff --git a/engines/scumm/insane/rebel2/iact.cpp b/engines/scumm/insane/rebel2/iact.cpp
index bbde2d14589..8572dd26a67 100644
--- a/engines/scumm/insane/rebel2/iact.cpp
+++ b/engines/scumm/insane/rebel2/iact.cpp
@@ -36,6 +36,7 @@ namespace Scumm {
 
 const int kRA2Handler7DirectInputNumerator = 4;
 const int kRA2Handler7DirectInputDenominator = 5;
+const int kRA2Handler7MouseTargetRangeX = 0xc0;
 
 static bool readLevel2BackgroundChunkHeader(Common::SeekableReadStream &stream, int64 containerEnd, const char *context,
 		uint32 &tag, uint32 &chunkSize, int64 &dataEnd, int64 &nextChunkPos) {
@@ -860,6 +861,12 @@ void InsaneRebel2::handleOpcode6Handler7(Common::SeekableReadStream &b, int16 pa
 		kRA2Handler7DirectInputDenominator);
 	scaledInputY = (int16)((scaledInputY * kRA2Handler7DirectInputNumerator) /
 		kRA2Handler7DirectInputDenominator);
+	const bool useMouseFlightTarget = !_gamepadAimActive;
+	int16 mouseFlightTargetX = _flyShipScreenX;
+	if (useMouseFlightTarget) {
+		mouseFlightTargetX = (int16)(0xd4 + (scaledInputX * kRA2Handler7MouseTargetRangeX) / 127);
+		mouseFlightTargetX = CLIP<int16>(mouseFlightTargetX, 0x14, 0x194);
+	}
 
 	// Step 3: Velocity history + smoothed average (lines 141-157).
 	for (int i = 24; i > 0; i--) {
@@ -920,6 +927,16 @@ void InsaneRebel2::handleOpcode6Handler7(Common::SeekableReadStream &b, int16 pa
 	if (positionDeltaX > 11)
 		positionDeltaX = 12;
 
+	// Retail integrates relative flight axes. ScummVM's real mouse is an
+	// absolute position, so steer toward a bounded position target instead of
+	// letting a held off-center cursor keep pushing the ship until it bounces.
+	if (useMouseFlightTarget) {
+		int targetDeltaX = mouseFlightTargetX - _flyShipScreenX;
+		positionDeltaX = (int16)CLIP<int>(targetDeltaX / 4, -12, 12);
+		if (positionDeltaX == 0 && targetDeltaX != 0)
+			positionDeltaX = (targetDeltaX < 0) ? -1 : 1;
+	}
+
 	// Apply X delta (line 193 / 237).
 	_flyShipScreenX += positionDeltaX;
 
@@ -1076,11 +1093,12 @@ void InsaneRebel2::handleOpcode6Handler7(Common::SeekableReadStream &b, int16 pa
 		((_vm->VAR(_vm->VAR_LEFTBTN_HOLD) != 0) ||
 		 _vm->getActionState(kScummActionInsaneAttack));
 
-	debugC(DEBUG_INSANE, "Rebel2 H7: mouse=(%d,%d) raw=(%d,%d) scaled=(%d,%d) pos=(%d,%d) "
-		"vel=%d vIn=%d dx=%d dir=%d mode=%d",
+	debugC(DEBUG_INSANE, "Rebel2 H7: mouse=(%d,%d) raw=(%d,%d) scaled=(%d,%d) targetX=%d pos=(%d,%d) "
+		"vel=%d vIn=%d dx=%d dir=%d mode=%d mouseTarget=%d",
 		mouseX, mouseY, inputX, inputY, scaledInputX, scaledInputY,
-		_flyShipScreenX, _flyShipScreenY, _smoothedVelocity,
-		_verticalInput, positionDeltaX, _shipDirectionIndex, _flyControlMode);
+		mouseFlightTargetX, _flyShipScreenX, _flyShipScreenY, _smoothedVelocity,
+		_verticalInput, positionDeltaX, _shipDirectionIndex, _flyControlMode,
+		useMouseFlightTarget ? 1 : 0);
 }
 
 // ScummVM refactor helper for opcode 6 Handler 25, not a separate retail function.




More information about the Scummvm-git-logs mailing list