[Scummvm-git-logs] scummvm master -> f43ebd3a88d0b9b6b91f0ca080f0e914cef522bb

sev- noreply at scummvm.org
Sun Jun 21 22:39:01 UTC 2026


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

Summary:
2bbb9d16a7 CHAMBER: Seed table RNG offset via Common::RandomSource
302bc60901 CHAMBER: Fix EGA MURSM sprite assembly squashing The Wall doors
f160896826 CHAMBER: Fix EGA endgame saucer animation and THE END screen
7e61d411d3 CHAMBER: Remove debug zone number from banner and hide cursor during text/room transitions
f43ebd3a88 CHAMBER: Add EGA zone transitions (Ring/passage spiral, lift, dot, zoom)


Commit: 2bbb9d16a7e9a4f494abf22c2f6628fa0c2cf5fb
    https://github.com/scummvm/scummvm/commit/2bbb9d16a7e9a4f494abf22c2f6628fa0c2cf5fb
Author: Ion Andrei Cristian (lecturatul2017 at gmail.com)
Date: 2026-06-22T00:38:55+02:00

Commit Message:
CHAMBER: Seed table RNG offset via Common::RandomSource

Derive rand_seed from the engine's Common::RandomSource instead of
reading ConfMan/getMillis directly. RandomSource already honors the
"random_seed" config key for reproducible runs and otherwise seeds
from the time/date, so this drops the hand-rolled seeding while keeping
the original aleat_data[] table (and its exact distribution) intact.

Expose _rnd as public to match the other engine members reached through
g_vm, and drop the now-unused config-manager.h include in kult.cpp.

Changed paths:
    engines/chamber/chamber.h
    engines/chamber/kult.cpp


diff --git a/engines/chamber/chamber.h b/engines/chamber/chamber.h
index 2559d76b7d4..b23bf427327 100644
--- a/engines/chamber/chamber.h
+++ b/engines/chamber/chamber.h
@@ -47,11 +47,10 @@ enum CHAMBERActions {
 };
 
 class ChamberEngine : public Engine {
-private:
+public:
 	// We need random numbers
 	Common::RandomSource *_rnd;
 
-public:
 	ChamberEngine(OSystem *syst, const ADGameDescription *desc);
 	~ChamberEngine();
 
diff --git a/engines/chamber/kult.cpp b/engines/chamber/kult.cpp
index 58907294db0..ef110b3c611 100644
--- a/engines/chamber/kult.cpp
+++ b/engines/chamber/kult.cpp
@@ -19,7 +19,6 @@
  *
  */
 
-#include "common/config-manager.h"
 #include "common/error.h"
 #include "common/system.h"
 #include "engines/advancedDetector.h"
@@ -113,11 +112,12 @@ uint16 benchmarkCpu(void) {
 
 void randomize(void) {
 	// Original read the low byte of the BIOS timer-tick count (int 0x1A) into
-	// rand_seed. Use the host millisecond timer as an equivalent entropy source.
-	if (ConfMan.hasKey("random_seed"))
-		rand_seed = (byte)ConfMan.getInt("random_seed");
-	else
-		rand_seed = (byte)(g_system->getMillis());
+	// rand_seed, the starting offset into the fixed aleat_data[] table that
+	// getRand() walks. Keep that table (it reproduces the DOS game's exact
+	// random distribution) but derive the offset from ScummVM's RandomSource:
+	// its constructor already honors the "random_seed" config key for
+	// reproducible runs and otherwise seeds from the time/date.
+	rand_seed = (byte)g_vm->_rnd->getRandomNumber(255);
 	getRand();
 }
 


Commit: 302bc609013480a937a3e156124700f01f6dd574
    https://github.com/scummvm/scummvm/commit/302bc609013480a937a3e156124700f01f6dd574
Author: Ion Andrei Cristian (lecturatul2017 at gmail.com)
Date: 2026-06-22T00:38:55+02:00

Commit Message:
CHAMBER: Fix EGA MURSM sprite assembly squashing The Wall doors

loadMursmSprite placed each door sub-sprite at half spacing on both
axes (row = cgaOfs/40, col = (cgaOfs%40)/2), collapsing the 4x2 grid
into the top-left quarter and leaving the rest of the gate black.
Mirror the CGA layout (byte offset in a 20-byte-pitch buffer):
row = cgaOfs/20, col = cgaOfs%20. Fixes the black centre on the
closed/half-closed wall and the open/close animation frames.

Changed paths:
    engines/chamber/room.cpp


diff --git a/engines/chamber/room.cpp b/engines/chamber/room.cpp
index c72977ba08e..3f79018456c 100644
--- a/engines/chamber/room.cpp
+++ b/engines/chamber/room.cpp
@@ -1573,11 +1573,13 @@ byte *loadMursmSprite(byte index) {
 			byte sprIdx = *pinfo++;
 			uint16 flags = *pinfo++;
 			flags |= (*pinfo++) << 8;
-			uint16 cgaOfs = flags & 0x3FFF; /* offset in CGA flat buffer (pitch=20, 2 bytes/unit) */
+			uint16 cgaOfs = flags & 0x3FFF; /* byte offset into the CGA flat buffer (pitch = 20 bytes = 80 px) */
 
-			/* Convert CGA flat offset → EGA flat offset */
-			uint16 row   = cgaOfs / (20 * 2);       /* 20 CGA bytes × 2 bytes/unit = 40 bytes/row */
-			uint16 colCga = (cgaOfs % (20 * 2)) / 2; /* column in CGA bytes */
+			/* Convert CGA flat offset → EGA flat offset. Mirror the CGA path,
+			   which places the sub-sprite at byte cgaOfs in a 20-byte-pitch
+			   buffer: row = cgaOfs / 20, column = cgaOfs % 20 CGA bytes (× 4 px). */
+			uint16 row    = cgaOfs / 20;
+			uint16 colCga = cgaOfs % 20;
 			uint16 egaBufOfs = row * egaPitch + colCga * 4;
 
 			Graphics::Surface *surf = ega_puzzl_res->getSprite(sprIdx & 0x7F);


Commit: f16089682650d894ba7657c414d5cdd348416042
    https://github.com/scummvm/scummvm/commit/f16089682650d894ba7657c414d5cdd348416042
Author: Ion Andrei Cristian (lecturatul2017 at gmail.com)
Date: 2026-06-22T00:38:55+02:00

Commit Message:
CHAMBER: Fix EGA endgame saucer animation and THE END screen

The EGA build crashed with a divide-by-zero (SIGFPE) at the winning end
sequence, the flying-saucer cutscene flew off-screen instead of receding,
and THE END logo was clipped or never drawn. Fix the EGA-specific paths:

- Add a real nearest-neighbour scaler (ega_scaleImage) and route zoomImage /
  zoomInplaceXY through it. The EGA versions were non-scaling stubs that
  ignored the target dimensions, so AnimSaucer redrew the saucer at native
  size every frame instead of shrinking it.
- Treat the saucer path x coordinate from SOUCO.BIN as a raw pixel column,
  not a 4-pixel-byte column (drop the *4). Every frame after the first was
  landing far off-screen to the right, so the saucer appeared to shoot right
  and vanish with no ascent.
- Sample the source with srcW/dstW rather than (srcW-1)/dstW so a 1:1 draw is
  the identity map. The old form dropped the final source column, clipping the
  right stroke of the 'D' in THE END.
- Add the missing pers_frames[9] = {9,11,0,0,0,0} end-logo frame. Its absence
  made the EGA portrait builder read a zero frame width and divide by it
  (SIGFPE), and left the logo unbuilt. Also guard the divide defensively.
- Clear the whole back buffer (and, in EGA, the front buffer) at the start of
  AnimSaucer. The original's 'sizeof - 2' left the bottom-right two pixels
  uncleared; in EGA the scroll-reveal lifted them up the screen as a red dot
  rising with the saucer.

Changed paths:
    engines/chamber/ega.cpp
    engines/chamber/portrait.cpp
    engines/chamber/script.cpp


diff --git a/engines/chamber/ega.cpp b/engines/chamber/ega.cpp
index 98a093a0004..7c61d61a2cd 100644
--- a/engines/chamber/ega.cpp
+++ b/engines/chamber/ega.cpp
@@ -492,16 +492,74 @@ void EGARenderer::traceLine(uint16 sx, uint16 ex, uint16 sy, uint16 ey, byte *so
 	  flushing the whole screen here turned each line into an updateScreen.*/
 }
 
-void EGARenderer::zoomImage(byte *pixels, byte w, byte h, byte /*nw*/, byte /*nh*/, byte *target, uint16 ofs) {
-	blitAndWait(pixels, w, w, h, target, ofs);
+// Nearest-neighbour scale an 8-bpp source (srcW x srcH pixels) into a
+// dstW x dstH pixel rect at (dstX, dstY). This is the 8-bpp equivalent of the
+// CGA cga_ZoomInplace math; the CGA partial-pixel/xbase packing logic collapses
+// to a plain sample-and-copy here since every EGA pixel is one byte. Every write
+// is clipped to the 320x200 screen so the saucer animation can never bleed past
+// the framebuffer bounds.
+static void ega_scaleImage(const byte *pixels, uint16 srcW, uint16 srcH,
+                           int16 dstX, int16 dstY, uint16 dstW, uint16 dstH, byte *target) {
+	if (dstW == 0 || dstH == 0 || srcW == 0 || srcH == 0)
+		return;
+
+	// 8.8 fixed-point source advance per target pixel. Use srcW/dstW (not
+	// (srcW-1)/dstW): at a 1:1 ratio that yields the identity map so the final
+	// source column/row is preserved -- the -1 form dropped it, which clipped the
+	// last pixel column (visible as the missing stroke of the "D" in THE END).
+	uint32 xstep = ((uint32)srcW << 8) / dstW;
+	uint32 ystep = ((uint32)srcH << 8) / dstH;
+
+	uint32 yval = 0;
+	for (uint16 ty = 0; ty < dstH; ty++, yval += ystep) {
+		int16 py = dstY + ty;
+		if (py < 0 || py >= EGA_HEIGHT)
+			continue;
+		uint16 sy = yval >> 8;
+		if (sy >= srcH)
+			sy = srcH - 1;
+		const byte *srcRow = pixels + sy * srcW;
+		byte *dstRow = target + py * EGA_BYTES_PER_LINE;
+		uint32 xval = 0;
+		for (uint16 tx = 0; tx < dstW; tx++, xval += xstep) {
+			int16 px = dstX + tx;
+			if (px < 0 || px >= EGA_WIDTH)
+				continue;
+			uint16 sx = xval >> 8;
+			if (sx >= srcW)
+				sx = srcW - 1;
+			dstRow[px] = srcRow[sx];
+		}
+	}
+
+	if (target == SCREENBUFFER) {
+		// flush the clipped destination rect
+		int16 x0 = dstX < 0 ? 0 : dstX;
+		int16 y0 = dstY < 0 ? 0 : dstY;
+		int16 x1 = dstX + (int16)dstW;
+		int16 y1 = dstY + (int16)dstH;
+		if (x1 > EGA_WIDTH)  x1 = EGA_WIDTH;
+		if (y1 > EGA_HEIGHT) y1 = EGA_HEIGHT;
+		if (x1 > x0 && y1 > y0)
+			g_vm->_renderer->blitToScreen(x0, y0, x1 - x0, y1 - y0);
+	}
+}
+
+void EGARenderer::zoomImage(byte *pixels, byte w, byte h, byte nw, byte nh, byte *target, uint16 ofs) {
+	// nw is the target width in 4-pixel bytes, nh the height in rows (see CGA zoomImage).
+	ega_scaleImage(pixels, w * 4, h, ofs % EGA_BYTES_PER_LINE, ofs / EGA_BYTES_PER_LINE, nw * 4, nh, target);
 }
 
 void EGARenderer::animZoomIn(byte *pixels, byte w, byte h, byte *target, uint16 ofs) {
 	blitAndWait(pixels, w, w, h, target, ofs);
 }
 
-void EGARenderer::zoomInplaceXY(byte *pixels, byte w, byte h, byte /*nw*/, byte /*nh*/, uint16 x, uint16 y, byte *target) {
-	blit(pixels, w, w, h, target, calcXY_p(x, y));
+void EGARenderer::zoomInplaceXY(byte *pixels, byte w, byte h, byte nw, byte nh, uint16 x, uint16 y, byte *target) {
+	// AnimSaucer's only caller passes the saucer path data from SOUCO.BIN, whose
+	// x/y/nw/nh are raw pixel coordinates (0..319), not 4-pixel-byte columns. So x
+	// is already a pixel column here -- pass it straight through (no *4), otherwise
+	// every frame after the first lands far off-screen to the right.
+	ega_scaleImage(pixels, w * 4, h, x, y, nw, nh, target);
 }
 
 void EGARenderer::drawSprite(byte *sprite, byte *screen, uint16 ofs) {
diff --git a/engines/chamber/portrait.cpp b/engines/chamber/portrait.cpp
index 8e56711ffc1..1c956b95811 100644
--- a/engines/chamber/portrait.cpp
+++ b/engines/chamber/portrait.cpp
@@ -70,7 +70,8 @@ persframe_t pers_frames[] = {
 	{47, 13, 0xFF,    0, 0xC0,    3},
 	{65, 18, 0xFF, 0xAA, 0xEA, 0xAB},
 	{38, 11, 0xFF,    0, 0xC0,    3},
-	{27, 34,    0,    0,    0,    0}
+	{27, 34,    0,    0,    0,    0},
+	{ 9, 11,    0,    0,    0,    0}   /*end-screen logo frame (icon 167); missing entry caused a divide-by-zero in EGA*/
 };
 
 void makePortraitFrame(byte index, byte *target) {
@@ -205,6 +206,8 @@ static byte *ega_loadPortrait(byte **pinfo, byte *end) {
 		int16 spitch = surf->pitch;
 
 		uint16 cga_ofs = flags & 0x3FFF;
+		if (cur_frame_width == 0) /*degenerate frame: avoid the divide below*/
+			continue;
 		uint16 row = cga_ofs / cur_frame_width;
 		uint16 col_cga = cga_ofs % cur_frame_width;
 		byte *dst = sprit_load_buffer + 2 + 2 + row * frame_pw + col_cga * 4;
diff --git a/engines/chamber/script.cpp b/engines/chamber/script.cpp
index 9359788b66b..5ec0ba077ad 100644
--- a/engines/chamber/script.cpp
+++ b/engines/chamber/script.cpp
@@ -2875,7 +2875,17 @@ static void AnimSaucer(void) {
 	uint16 delay;
 	byte scroll_done = 0;
 
-	memset(backbuffer, 0, sizeof(backbuffer) - 2);  /*TODO: original bug?*/
+	/*Clear the whole buffer: the original's "- 2" left the bottom-right two
+	  bytes (row 199, x=318..319) holding stale pixels. In EGA those are visible
+	  and the scroll-reveal lifts them up the screen as a red dot rising with the
+	  saucer, so clear all of it.*/
+	memset(backbuffer, 0, sizeof(backbuffer));
+	/*EGA: also blank the front buffer. The saucer animation only paints the
+	  saucer region, so leftover sprites from the final game frame (player,
+	  HUD pixels) would otherwise survive on the black backdrop right through to
+	  THE END screen.*/
+	if (g_vm->_videoMode == Common::kRenderEGA)
+		memset(frontbuffer, 0, sizeof(SCREENBUFFER));
 	g_vm->_renderer->backBufferToRealFull();
 	g_vm->_renderer->colorSelect(0x30);
 


Commit: 7e61d411d343178250e273576755c851e8b1b2cf
    https://github.com/scummvm/scummvm/commit/7e61d411d343178250e273576755c851e8b1b2cf
Author: Ion Andrei Cristian (lecturatul2017 at gmail.com)
Date: 2026-06-22T00:38:55+02:00

Commit Message:
CHAMBER: Remove debug zone number from banner and hide cursor during text/room transitions

Disable DEBUG_ZONE so the 3-digit zone index no longer prints on the
location banner. Hide the mouse pointer before text boxes/bubbles are
drawn and while a room's entry animation plays; the game loop re-shows
it once it waits for player input.

Changed paths:
    engines/chamber/common.h
    engines/chamber/cursor.cpp
    engines/chamber/cursor.h
    engines/chamber/dialog.cpp
    engines/chamber/print.cpp
    engines/chamber/room.cpp


diff --git a/engines/chamber/common.h b/engines/chamber/common.h
index a2314c736b9..eb9dbe66480 100644
--- a/engines/chamber/common.h
+++ b/engines/chamber/common.h
@@ -58,7 +58,7 @@ typedef struct rect_t {
 #define DEBUG_SKIP_INTRO 0xFF
 #endif
 
-#if 1
+#if 0
 #define DEBUG_ZONE
 #endif
 
diff --git a/engines/chamber/cursor.cpp b/engines/chamber/cursor.cpp
index 7474ed31785..38d0e6cd84b 100644
--- a/engines/chamber/cursor.cpp
+++ b/engines/chamber/cursor.cpp
@@ -153,6 +153,13 @@ Restore background pixels under cursor
 void undrawCursor(byte *target) {
 }
 
+/*
+Hide the system mouse pointer (used during non-interactive room transitions)
+*/
+void hideMouseCursor(void) {
+	CursorMan.showMouse(false);
+}
+
 /*
 Restore pixels under cursor and update cursor sprite
 */
diff --git a/engines/chamber/cursor.h b/engines/chamber/cursor.h
index 0a22c84a352..bf898096b74 100644
--- a/engines/chamber/cursor.h
+++ b/engines/chamber/cursor.h
@@ -52,6 +52,7 @@ void updateCursor(void);
 void drawCursor(byte *target);
 void undrawCursor(byte *target);
 void updateUndrawCursor(byte *target);
+void hideMouseCursor(void);
 
 } // End of namespace Chamber
 
diff --git a/engines/chamber/dialog.cpp b/engines/chamber/dialog.cpp
index 7235a716442..7b92359c6b8 100644
--- a/engines/chamber/dialog.cpp
+++ b/engines/chamber/dialog.cpp
@@ -20,6 +20,7 @@
  */
 
 #include "common/system.h"
+#include "graphics/cursorman.h"
 
 #include "chamber/chamber.h"
 #include "chamber/common.h"
@@ -114,6 +115,10 @@ void drawPersonBubble(byte x, byte y, byte flags, byte *msg) {
 	byte w, h;
 	uint16 ww, nw;
 
+	// Hide the mouse pointer before the bubble is drawn, so it is gone from the
+	// moment the text appears. selectCursor() re-shows it when play resumes.
+	CursorMan.showMouse(false);
+
 	char_draw_max_width = flags & 0x1F;
 	char_xlat_table = chars_color_bonw;
 
diff --git a/engines/chamber/print.cpp b/engines/chamber/print.cpp
index e16ce3484e5..42fc5f9d632 100644
--- a/engines/chamber/print.cpp
+++ b/engines/chamber/print.cpp
@@ -19,6 +19,8 @@
  *
  */
 
+#include "graphics/cursorman.h"
+
 #include "chamber/chamber.h"
 #include "chamber/common.h"
 #include "chamber/cga.h"
@@ -198,6 +200,11 @@ void cga_DrawTextBox(byte *msg, byte *target) {
 	uint16 x, y, w, i;
 	uint16 ww, nw;
 
+	// Hide the mouse pointer before the text box is drawn, so it is gone from
+	// the moment the box appears (not only once promptWait() begins). The game
+	// loop's selectCursor() re-shows it when interactive play resumes.
+	CursorMan.showMouse(false);
+
 	char_xlat_table = chars_color_bonc;
 
 	if (g_vm->getLanguage() == Common::EN_USA) {
diff --git a/engines/chamber/room.cpp b/engines/chamber/room.cpp
index 3f79018456c..c0b39c7281b 100644
--- a/engines/chamber/room.cpp
+++ b/engines/chamber/room.cpp
@@ -1040,6 +1040,11 @@ void drawZoneSpots(void) {
 }
 
 void refreshZone(void) {
+	// Hide the mouse pointer while the new room is drawn and its entry
+	// animation plays. The game loop's selectCursor() re-shows it once it is
+	// waiting for player input again.
+	hideMouseCursor();
+
 	popDirtyRects(DirtyRectSprite);
 	popDirtyRects(DirtyRectBubble);
 	popDirtyRects(DirtyRectText);


Commit: f43ebd3a88d0b9b6b91f0ca080f0e914cef522bb
    https://github.com/scummvm/scummvm/commit/f43ebd3a88d0b9b6b91f0ca080f0e914cef522bb
Author: Ion Andrei Cristian (lecturatul2017 at gmail.com)
Date: 2026-06-22T00:38:55+02:00

Commit Message:
CHAMBER: Add EGA zone transitions (Ring/passage spiral, lift, dot, zoom)

Implement the EGA renderer's stubbed transition effects so room changes
animate like CGA: the spiral background reveal when walking The Ring and
passages (gated by the previously-unused skip_zone_transition), plus the
lift wipes, dot dissolve and zoom-in reveals.

Changed paths:
    engines/chamber/anim.cpp
    engines/chamber/ega.cpp
    engines/chamber/ega.h
    engines/chamber/room.cpp
    engines/chamber/script.cpp


diff --git a/engines/chamber/anim.cpp b/engines/chamber/anim.cpp
index 8ba0b2d9e9f..18a8108a2e3 100644
--- a/engines/chamber/anim.cpp
+++ b/engines/chamber/anim.cpp
@@ -116,6 +116,36 @@ void clipSprite(byte *x, byte *y, byte *sprw, byte *sprh, byte **sprite, int8 dx
 
 void copyScreenBlockWithDotEffect(byte *source, byte x, byte y, byte width, byte height, byte *target) {
 	if (g_vm->_videoMode == Common::kRenderEGA) {
+		/* EGA: linear 1 byte/pixel. Reveal the block in the same scattered
+		   ("dot dissolve") order as the CGA path, blitting periodically so the
+		   transition is animated instead of an instant copy. */
+		uint16 xx = x * 4;
+		uint16 ww = width * 4;
+		uint32 end = (uint32)ww * height;
+		uint16 step = dot_effect_step ? dot_effect_step : 17;
+		uint32 offs = 0;
+		uint32 guard = 0;
+
+		if (target == SCREENBUFFER) {
+			do {
+				uint16 px = xx + offs % ww;
+				uint16 py = y + offs / ww;
+				uint16 ofs = py * EGA_BYTES_PER_LINE + px;
+				target[ofs] = source[ofs];
+
+				offs += step;
+				if (offs > end)
+					offs -= end;
+
+				/* blit roughly once per block-row's worth of dots */
+				if ((++guard % ww) == 0) {
+					g_vm->_renderer->blitToScreen(xx, y, ww, height);
+					waitVBlank();
+				}
+			} while (offs != 0 && guard <= end);
+		}
+
+		/* ensure the block is fully copied (the scatter may skip pixels) */
 		g_vm->_renderer->copyScreenBlock(source, width, height, target, g_vm->_renderer->calcXY_p(x, y));
 		return;
 	}
diff --git a/engines/chamber/ega.cpp b/engines/chamber/ega.cpp
index 7c61d61a2cd..d9a346e025a 100644
--- a/engines/chamber/ega.cpp
+++ b/engines/chamber/ega.cpp
@@ -65,6 +65,65 @@ void ega_drawBackground(byte *target) {
 		g_vm->_renderer->blitToScreen(0, 0, EGA_WIDTH, EGA_HEIGHT);
 }
 
+#define EGA_WIPE_BLOCK 20
+
+static int s_wipeBlocks;
+
+/*Reveal one clean-background block onto the screen; flush+throttle every few blocks*/
+static void egaRevealBlock(int bx, int by) {
+	int w = (bx + EGA_WIPE_BLOCK > EGA_WIDTH) ? (EGA_WIDTH - bx) : EGA_WIPE_BLOCK;
+	int h = (by + EGA_WIPE_BLOCK > EGA_HEIGHT) ? (EGA_HEIGHT - by) : EGA_WIPE_BLOCK;
+	if (w <= 0 || h <= 0)
+		return;
+	for (int i = 0; i < h; i++)
+		memcpy(SCREENBUFFER + (by + i) * EGA_BYTES_PER_LINE + bx,
+		       ega_fond_clean + (by + i) * EGA_BYTES_PER_LINE + bx, w);
+	if (++s_wipeBlocks % 3 == 0) {
+		g_vm->_renderer->blitToScreen(0, 0, EGA_WIDTH, EGA_HEIGHT);
+		waitVBlank();
+	}
+}
+
+void ega_zoneRevealWipe(void) {
+	// Original zone-change transition for The Ring / passages: first the empty
+	// background pattern spirals in over the old room (no room underneath), then
+	// the new room is shown. Here we spiral in the clean background, then blit the
+	// new room (already composited in the backbuffer) on top.
+	const int B = EGA_WIPE_BLOCK;
+	int left = 0, top = 0, right = EGA_WIDTH, bottom = EGA_HEIGHT;
+	s_wipeBlocks = 0;
+
+	while (left < right && top < bottom) {
+		for (int x = left; x < right; x += B)        /*top row, L->R*/
+			egaRevealBlock(x, top);
+		top += B;
+		for (int y = top; y < bottom; y += B)        /*right col, T->B*/
+			egaRevealBlock(right - B, y);
+		right -= B;
+		if (top < bottom) {
+			for (int x = right - B; x >= left; x -= B)   /*bottom row, R->L*/
+				egaRevealBlock(x, bottom - B);
+			bottom -= B;
+		}
+		if (left < right) {
+			for (int y = bottom - B; y >= top; y -= B)   /*left col, B->T*/
+				egaRevealBlock(left, y);
+			left += B;
+		}
+	}
+
+	// make sure the whole play area is the clean background (no room), then hold
+	// briefly so the empty background is visible before the room appears
+	memcpy(SCREENBUFFER, ega_fond_clean, EGA_SCREEN_SIZE);
+	g_vm->_renderer->blitToScreen(0, 0, EGA_WIDTH, EGA_HEIGHT);
+	for (int i = 0; i < 8; i++)
+		waitVBlank();
+
+	// now show the new room (already composited in the backbuffer)
+	memcpy(SCREENBUFFER, ega_backbuffer, EGA_SCREEN_SIZE);
+	g_vm->_renderer->blitToScreen(0, 0, EGA_WIDTH, EGA_HEIGHT);
+}
+
 Graphics::Surface *ega_loadFond(const char *filename) {
 	Common::File fd;
 	if (!fd.open(filename)) {
@@ -452,20 +511,83 @@ void EGARenderer::animLiftToRight(uint16 n, byte *pixels, uint16 pw, uint16 w, u
 	}
 }
 
+/* EGA port of the CGA lift transitions: shift the block one line/column per
+   step, fill the freed edge from source (the new image), then blit+vblank so
+   the move is animated. ofs is a linear EGA offset; w is in CGA bytes, so a
+   block column is 4 EGA pixels wide. */
+
+/*Shift block n lines down, filling the freed top line from source*/
 void EGARenderer::hideScreenBlockLiftToDown(uint16 n, byte *screen, byte *source, uint16 w, uint16 h, byte *target, uint16 ofs) {
-	ega_CopyScreenBlock(source, w * 4, h + n, target, ofs);
+	uint16 ew = w * 4;
+	while (n--) {
+		uint16 sofs = ofs;
+		uint16 tofs = ofs + EGA_BYTES_PER_LINE;
+		for (uint16 i = 0; i < h; i++) {
+			memcpy(target + tofs, screen + sofs, ew);
+			tofs = sofs;
+			sofs -= EGA_BYTES_PER_LINE;
+		}
+		memcpy(target + tofs, source + tofs, ew);
+		if (screen == SCREENBUFFER)
+			blitToScreen(0, 0, EGA_WIDTH, EGA_HEIGHT);
+		waitVBlank();
+		ofs += EGA_BYTES_PER_LINE;
+	}
 }
 
+/*Shift block n lines up, filling the freed bottom line from source*/
 void EGARenderer::hideScreenBlockLiftToUp(uint16 n, byte *screen, byte *source, uint16 w, uint16 h, byte *target, uint16 ofs) {
-	ega_CopyScreenBlock(source, w * 4, h + n, target, ofs);
+	uint16 ew = w * 4;
+	while (n--) {
+		uint16 sofs = ofs;
+		uint16 tofs = ofs - EGA_BYTES_PER_LINE;
+		for (uint16 i = 0; i < h; i++) {
+			memcpy(target + tofs, screen + sofs, ew);
+			tofs = sofs;
+			sofs += EGA_BYTES_PER_LINE;
+		}
+		memcpy(target + tofs, source + tofs, ew);
+		if (screen == SCREENBUFFER)
+			blitToScreen(0, 0, EGA_WIDTH, EGA_HEIGHT);
+		waitVBlank();
+		ofs -= EGA_BYTES_PER_LINE;
+	}
 }
 
+/*Shift block n columns left, filling the freed right column from source*/
 void EGARenderer::hideScreenBlockLiftToLeft(uint16 n, byte *screen, byte *source, uint16 w, uint16 h, byte *target, uint16 ofs) {
-	ega_CopyScreenBlock(source, (w + n) * 4, h, target, ofs - n * 4);
+	uint16 ew = w * 4;
+	while (n--) {
+		uint16 sofs = ofs;
+		for (uint16 i = 0; i < h; i++) {
+			uint16 tofs = sofs - 4;
+			memmove(target + tofs, screen + sofs, ew);
+			memcpy(target + tofs + ew, source + tofs + ew, 4);
+			sofs += EGA_BYTES_PER_LINE;
+		}
+		if (screen == SCREENBUFFER)
+			blitToScreen(0, 0, EGA_WIDTH, EGA_HEIGHT);
+		waitVBlank();
+		ofs -= 4;
+	}
 }
 
+/*Shift block n columns right, filling the freed left column from source*/
 void EGARenderer::hideScreenBlockLiftToRight(uint16 n, byte *screen, byte *source, uint16 w, uint16 h, byte *target, uint16 ofs) {
-	ega_CopyScreenBlock(source, (w + n) * 4, h, target, ofs - w * 4);
+	uint16 ew = w * 4;
+	while (n--) {
+		uint16 sofs = ofs;
+		for (uint16 i = 0; i < h; i++) {
+			uint16 tofs = sofs + 4;
+			memmove(target + tofs - ew, screen + sofs - ew, ew);
+			memcpy(target + tofs - ew, source + tofs - ew, 4);
+			sofs += EGA_BYTES_PER_LINE;
+		}
+		if (screen == SCREENBUFFER)
+			blitToScreen(0, 0, EGA_WIDTH, EGA_HEIGHT);
+		waitVBlank();
+		ofs += 4;
+	}
 }
 
 void EGARenderer::hideShatterFall(byte *screen, byte *source, uint16 w, uint16 h, byte *target, uint16 ofs) {
@@ -551,6 +673,38 @@ void EGARenderer::zoomImage(byte *pixels, byte w, byte h, byte nw, byte nh, byte
 }
 
 void EGARenderer::animZoomIn(byte *pixels, byte w, byte h, byte *target, uint16 ofs) {
+	/* Progressive zoom-in reveal (CGA animZoomIn equivalent for EGA): draw the
+	   image scaled from a small size up to full, growing out of its centre.
+	   Each larger frame is centred and covers the previous one, so no per-step
+	   restore is needed. zoomImage() draws and flushes the scaled rect. */
+	uint16 fw = (uint16)w * 4;          /* final width in pixels */
+	uint16 fh = h;                      /* final height in rows */
+	uint16 ox = ofs % EGA_BYTES_PER_LINE;
+	uint16 oy = ofs / EGA_BYTES_PER_LINE;
+	uint16 cx = ox + fw / 2;            /* zoom centre */
+	uint16 cy = oy + fh / 2;
+
+	uint16 maxside = (fw > fh) ? fw : fh;
+	uint16 nsteps = maxside / 2;
+	if (nsteps < 8)
+		nsteps = 8;
+	if (nsteps > 64)
+		nsteps = 64;
+
+	for (uint16 s = 1; s < nsteps; s++) {
+		byte nw = (byte)((uint16)w * s / nsteps);
+		byte nh = (byte)((uint16)h * s / nsteps);
+		if (nw < 1)
+			nw = 1;
+		if (nh < 1)
+			nh = 1;
+		uint16 tx = cx - (uint16)nw * 4 / 2;
+		uint16 ty = cy - (uint16)nh / 2;
+		zoomImage(pixels, w, h, nw, nh, target, ty * EGA_BYTES_PER_LINE + tx);
+		waitVBlank();
+	}
+
+	/* final full-size image */
 	blitAndWait(pixels, w, w, h, target, ofs);
 }
 
diff --git a/engines/chamber/ega.h b/engines/chamber/ega.h
index 9d3d7f5050a..9516de42274 100644
--- a/engines/chamber/ega.h
+++ b/engines/chamber/ega.h
@@ -50,6 +50,10 @@ Graphics::Surface *ega_loadFond(const char *filename);
 extern byte ega_fond_clean[EGA_SCREEN_SIZE];
 void ega_drawBackground(byte *target);
 
+// --- zone-change transition ---
+// Reveal the new room (backbuffer) over the screen with a serpentine line wipe.
+void ega_zoneRevealWipe(void);
+
 } // End of namespace Chamber
 
 #endif // CHAMBER_EGA_H
diff --git a/engines/chamber/room.cpp b/engines/chamber/room.cpp
index c0b39c7281b..63cdfb33c2d 100644
--- a/engines/chamber/room.cpp
+++ b/engines/chamber/room.cpp
@@ -1039,6 +1039,23 @@ void drawZoneSpots(void) {
 	drawPersons();
 }
 
+/*
+The line-wipe zone transition only plays while moving through the corridor-like
+areas: The Ring and the Passages. Normal room entries stay instant (or use their
+own scripted drawIcone effect).
+*/
+static bool zoneHasWalkTransition(byte area) {
+	switch (area) {
+	case kAreaTheRing1: case kAreaTheRing2: case kAreaTheRing3:
+	case kAreaTheRing4: case kAreaTheRing5: case kAreaTheRing6:
+	case kAreaPassage1: case kAreaPassage2: case kAreaPassage3:
+	case kAreaPassage4: case kAreaPassage5: case kAreaPassage6: case kAreaPassage7:
+		return true;
+	default:
+		return false;
+	}
+}
+
 void refreshZone(void) {
 	// Hide the mouse pointer while the new room is drawn and its entry
 	// animation plays. The game loop's selectCursor() re-shows it once it is
@@ -1049,7 +1066,16 @@ void refreshZone(void) {
 	popDirtyRects(DirtyRectBubble);
 	popDirtyRects(DirtyRectText);
 
-	g_vm->_renderer->backBufferToRealFull();
+	// Reveal the new room with an animated transition unless this change asked to
+	// skip it (e.g. entering through an already-animated door). skip_zone_transition
+	// is set by SCR_42_LoadZone; plain zone changes (walking around The Ring / through
+	// passages via SCR_36_ChangeZone) leave it 0 and so get the dot-dissolve reveal.
+	if (!skip_zone_transition && g_vm->_videoMode == Common::kRenderEGA
+	        && zoneHasWalkTransition(script_byte_vars.zone_area)) {
+		ega_zoneRevealWipe();
+	} else {
+		g_vm->_renderer->backBufferToRealFull();
+	}
 
 	in_de_profundis = 0;
 	IFGM_StopSample();
diff --git a/engines/chamber/script.cpp b/engines/chamber/script.cpp
index 5ec0ba077ad..754faf0fc36 100644
--- a/engines/chamber/script.cpp
+++ b/engines/chamber/script.cpp
@@ -1109,7 +1109,13 @@ uint16 SCR_1B_HidePortraitLiftUp(void) {
 	}
 
 	if (g_vm->_videoMode == Common::kRenderEGA) {
-		g_vm->_renderer->copyScreenBlock(backbuffer, width, height, SCREENBUFFER, offs);
+		offs = g_vm->_renderer->calcXY_p(x, y + 1);
+		while (--height)
+			g_vm->_renderer->hideScreenBlockLiftToUp(1, SCREENBUFFER, backbuffer, width, height, SCREENBUFFER, offs);
+		/*hide topmost line*/
+		offs -= EGA_BYTES_PER_LINE;
+		memcpy(SCREENBUFFER + offs, backbuffer + offs, width * 4);
+		g_vm->_renderer->blitToScreen(offs % EGA_BYTES_PER_LINE, offs / EGA_BYTES_PER_LINE, width * 4, 1);
 		return 0;
 	}
 
@@ -1150,7 +1156,13 @@ uint16 SCR_1C_HidePortraitLiftDown(void) {
 	}
 
 	if (g_vm->_videoMode == Common::kRenderEGA) {
-		g_vm->_renderer->copyScreenBlock(backbuffer, width, height, SCREENBUFFER, offs);
+		offs = g_vm->_renderer->calcXY_p(x, y + height - 2);
+		while (--height)
+			g_vm->_renderer->hideScreenBlockLiftToDown(1, SCREENBUFFER, backbuffer, width, height, SCREENBUFFER, offs);
+		/*hide bottommost line*/
+		offs += EGA_BYTES_PER_LINE;
+		memcpy(SCREENBUFFER + offs, backbuffer + offs, width * 4);
+		g_vm->_renderer->blitToScreen(offs % EGA_BYTES_PER_LINE, offs / EGA_BYTES_PER_LINE, width * 4, 1);
 		return 0;
 	}
 
@@ -1612,6 +1624,8 @@ Draw new zone
 */
 uint16 SCR_43_RefreshZone(void) {
 	script_ptr++;
+	// A standalone refresh (not a zone change) should just redraw instantly.
+	skip_zone_transition = 1;
 	refreshZone();
 	return 0;
 }




More information about the Scummvm-git-logs mailing list