[Scummvm-git-logs] scummvm master -> 6ac56c5cf8bb76166b57087644b68b4781bc0876

sev- noreply at scummvm.org
Sun Jun 14 19:13:50 UTC 2026


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

Summary:
d1c4b02b6a CHAMBER: Fix infinite failed-ordeals death loop and ordeal timer bleed
cfdf32e5df CHAMBER: Seed RNG and fix stale rand_value in prepareAspirant
565af35a80 CHAMBER: Fix EGA lutin scratch overflow corrupting sprites_list
48ab13e0da CHAMBER: Drop stale actor/command on door room swap
ed6b4260f2 CHAMBER: Fix EGA opcode 0x68 PlaySfx operand width
b528e1ca20 CHAMBER: Fix endgame confrontation menu re-prompt after winning flask
6ac56c5cf8 CHAMBER: Make RNG seed overridable via random_seed config key


Commit: d1c4b02b6a0ccfa14277bfe80b309236c3fcc1fb
    https://github.com/scummvm/scummvm/commit/d1c4b02b6a0ccfa14277bfe80b309236c3fcc1fb
Author: Ion Andrei Cristian (lecturatul2017 at gmail.com)
Date: 2026-06-14T21:13:43+02:00

Commit Message:
CHAMBER: Fix infinite failed-ordeals death loop and ordeal timer bleed

The Master's Orbit endlessly replayed the failed-ordeals death scene.
Two distinct issues were behind it:

- checkGameTimeLimit() queues next_protozorqs_cmd=0xC012 (the death
  dispatcher) when the one-hour ordeal timer expires, but the command
  was never cleared: updateProtozorqs() early-returns at bvar_26 >= 63
  before its own clear, so the scene re-fired every frame. Consume the
  command when dispatched so it fires once.

- timer_ticks2 (the ordeal clock) is real wall-clock time gated only by
  game_paused, which was never set during the intro or the slow zone
  load/room transitions. Those bled real seconds into the one-hour
  budget. Freeze the timer across the intro and SCR_42_LoadZone.

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


diff --git a/engines/chamber/kult.cpp b/engines/chamber/kult.cpp
index 32b9445be78..b8d4d42061a 100644
--- a/engines/chamber/kult.cpp
+++ b/engines/chamber/kult.cpp
@@ -175,8 +175,15 @@ void gameLoop(byte *target) {
 				continue;
 
 			the_command = Swap16(script_word_vars.next_protozorqs_cmd);
-			if (the_command)
+			if (the_command) {
+				// Consume the queued command so a terminal command (e.g. the
+				// "failed the ordeals" death scene queued by checkGameTimeLimit)
+				// fires once instead of every frame. updateProtozorqs() re-queues
+				// live protozorq AI each frame, but it early-returns once
+				// bvar_26 >= 63 and would otherwise leave this set forever.
+				script_word_vars.next_protozorqs_cmd = 0;
 				goto process;
+			}
 
 			if (Swap16(next_vorts_ticks) < script_word_vars.timer_ticks2) { /*TODO: is this ok? ticks2 is BE, ticks3 is LE*/
 				the_command = next_vorts_cmd;
@@ -467,8 +474,13 @@ Common::Error ChamberEngine::execute() {
 	//ResetInput();
 
 	/* Play introduction sequence and initialize game */
+	// Freeze the ordeal timer during the intro/setup: it is installed before this
+	// point (initTimer), so the non-interactive intro would otherwise start the
+	// player's one-hour ordeal budget early.
+	script_byte_vars.game_paused = 1;
 	the_command = 0xC001;
 	runCommand();
+	script_byte_vars.game_paused = 0;
 
 	if (_shouldQuit)
 		return Common::kNoError;
diff --git a/engines/chamber/script.cpp b/engines/chamber/script.cpp
index 1468a83e52a..11b0fc6a966 100644
--- a/engines/chamber/script.cpp
+++ b/engines/chamber/script.cpp
@@ -1517,6 +1517,13 @@ If go through a door, play door's opening animation
 uint16 SCR_42_LoadZone(void) {
 	byte index;
 	bool door_animated = false;
+	// Freeze the ordeal timer (timer_ticks2) while we load the zone and play the
+	// room transition. These are far slower under ScummVM than on DOS and would
+	// otherwise bleed real seconds into the one-hour ordeal budget. timer_ticks
+	// (animation pacing) keeps running. Save/restore so we don't unpause an
+	// already-paused state (e.g. a zone load during the game-over sequence).
+	byte saved_paused = script_byte_vars.game_paused;
+	script_byte_vars.game_paused = 1;
 
 	script_ptr++;
 	index = *script_ptr++;
@@ -1558,6 +1565,7 @@ uint16 SCR_42_LoadZone(void) {
 	if (door_animated)
 		g_vm->_renderer->backBufferToRealFull();
 
+	script_byte_vars.game_paused = saved_paused;
 	return 0;
 }
 


Commit: cfdf32e5dfa6caac37568dd42610d8ca8dc9b592
    https://github.com/scummvm/scummvm/commit/cfdf32e5dfa6caac37568dd42610d8ca8dc9b592
Author: Ion Andrei Cristian (lecturatul2017 at gmail.com)
Date: 2026-06-14T21:13:43+02:00

Commit Message:
CHAMBER: Seed RNG and fix stale rand_value in prepareAspirant

randomize() was a stub leaving rand_seed=0, making every game roll the
same deterministic sequence (guaranteed-hostile Aspirant, fixed starting
item). Seed rand_seed from the host millisecond timer and prime the
stream. Also pull a fresh getRand() for Aspirant hostility instead of
reusing the stale rand_value.

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


diff --git a/engines/chamber/kult.cpp b/engines/chamber/kult.cpp
index b8d4d42061a..fc3b1c2ab77 100644
--- a/engines/chamber/kult.cpp
+++ b/engines/chamber/kult.cpp
@@ -111,15 +111,10 @@ uint16 benchmarkCpu(void) {
 }
 
 void randomize(void) {
-	warning("STUB: Randomize()");
-#if 0
-	union REGS reg;
-
-	reg.h.ah = 0;
-	int86(0x1A, &reg, &reg);
-	rand_seed = reg.h.dl;
-	Rand();
-#endif
+	// 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.
+	rand_seed = (byte)(g_system->getMillis());
+	getRand();
 }
 
 void TRAP() {
diff --git a/engines/chamber/room.cpp b/engines/chamber/room.cpp
index a4d0081ec50..974540e3fe4 100644
--- a/engines/chamber/room.cpp
+++ b/engines/chamber/room.cpp
@@ -1309,7 +1309,7 @@ void prepareAspirant(void) {
 	if (aspirant_ptr->flags & PERSFLG_40)
 		return;
 
-	hostility = script_byte_vars.rand_value;
+	hostility = getRand();
 	appearance = getRand();
 	flags = 0;
 	/*


Commit: 565af35a8062c45d2eb7c36a7210e6acd5bd7d5f
    https://github.com/scummvm/scummvm/commit/565af35a8062c45d2eb7c36a7210e6acd5bd7d5f
Author: Ion Andrei Cristian (lecturatul2017 at gmail.com)
Date: 2026-06-14T21:13:43+02:00

Commit Message:
CHAMBER: Fix EGA lutin scratch overflow corrupting sprites_list

The EGA path decodes each lutin to CLUT8 (4 bytes per CGA byte), twice the
CGA footprint, but getScratchBuffer kept the CGA slot strides and scratch_mem1
stayed at 8010 bytes. A large lutin (e.g. the Vort animation in The Return)
overran its slot past the end of scratch_mem1 into the adjacent sprites_list[],
corrupting it and later crashing in blitSpritesToBackBuffer/restoreImage.
Double the partition strides in EGA and size scratch_mem1 for the EGA worst
case.

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


diff --git a/engines/chamber/anim.cpp b/engines/chamber/anim.cpp
index 43119446cba..8ba0b2d9e9f 100644
--- a/engines/chamber/anim.cpp
+++ b/engines/chamber/anim.cpp
@@ -55,10 +55,16 @@ extern void loadLutinSprite(uint16 lutidx);
 void getScratchBuffer(byte mode) {
 	byte *buffer = scratch_mem2;
 	uint16 offs = 0;
+	// EGA decodes each lutin to CLUT8 (1 byte per pixel = 4 bytes per CGA byte),
+	// so a slot is twice the CGA footprint. Double the partition strides in EGA,
+	// otherwise a large lutin overruns its slot into the next one - and the top
+	// slot overruns the end of scratch_mem1 into the adjacent sprites_list[],
+	// corrupting it (later crashing in blitSpritesToBackBuffer/restoreImage).
+	uint16 slot = (g_vm->_videoMode == Common::kRenderEGA) ? 3200 : 1600;
 	if (mode & 0x80)
-		offs += 3200;
+		offs += slot * 2;
 	if (mode & 0x40)
-		offs += 1600;
+		offs += slot;
 	lutin_mem = buffer + offs;
 }
 
diff --git a/engines/chamber/room.cpp b/engines/chamber/room.cpp
index 974540e3fe4..c72977ba08e 100644
--- a/engines/chamber/room.cpp
+++ b/engines/chamber/room.cpp
@@ -40,7 +40,12 @@
 
 namespace Chamber {
 
-byte scratch_mem1[8010];
+// 1500-byte spot-backup region, then the lutin/anim scratch (scratch_mem2).
+// The scratch holds up to four simultaneous lutin slots (see getScratchBuffer).
+// CGA needs 4*1600 = 6400 there; EGA decodes lutins to CLUT8 (4 bytes per CGA
+// byte) so it needs 4*3200 = 12800. Sized for the EGA worst case so a big lutin
+// can't overrun into the adjacent sprites_list[].
+byte scratch_mem1[14400];
 byte *scratch_mem2 = scratch_mem1 + 1500;
 
 rect_t room_bounds_rect = {0, 0, 0, 0};
diff --git a/engines/chamber/room.h b/engines/chamber/room.h
index 8211089e028..263e1ea4667 100644
--- a/engines/chamber/room.h
+++ b/engines/chamber/room.h
@@ -90,7 +90,7 @@ typedef struct turkeyanims_t {
 	animdesc_t field_4;
 } turkeyanims_t;
 
-extern byte scratch_mem1[8010];
+extern byte scratch_mem1[14400];
 extern byte *scratch_mem2;
 
 extern rect_t room_bounds_rect;


Commit: 48ab13e0da3d1fd3624a8c253a8f2ebdf8c605de
    https://github.com/scummvm/scummvm/commit/48ab13e0da3d1fd3624a8c253a8f2ebdf8c605de
Author: Ion Andrei Cristian (lecturatul2017 at gmail.com)
Date: 2026-06-14T21:13:43+02:00

Commit Message:
CHAMBER: Drop stale actor/command on door room swap

Going through a door must end any interaction in progress: abort the command
chain and reset CurrentPers in SCR_42_LoadZone so a queued command (e.g. an
Aspirant trade) can't run in a zone with no matching spawn spot. Also correct
the DEBUG_QUEST script offset (0x4F->0x5B) to the quest-selection branch.

Changed paths:
    engines/chamber/script.cpp


diff --git a/engines/chamber/script.cpp b/engines/chamber/script.cpp
index 11b0fc6a966..0affa6388c0 100644
--- a/engines/chamber/script.cpp
+++ b/engines/chamber/script.cpp
@@ -1544,6 +1544,15 @@ uint16 SCR_42_LoadZone(void) {
 	}
 	beforeChangeZone(index);
 	changeZone(index);
+
+	// End any in-flight interaction when going through a door: an Aspirant trade
+	// can still be mid-flight (runCommand ScriptRerun loop, CurrentPers still on
+	// the old room's actor), so abort the chain and drop the stale actor here.
+	// prepareVorts/Turkey/Aspirant below repopulate CurrentPers. Done here, not in
+	// changeZone(), since SCR_25_ChangeZoneOnly's in-place swaps must keep both.
+	the_command = 0;
+	script_vars[kScrPool8_CurrentPers] = pers_list;
+
 	script_byte_vars.zone_area_copy = script_byte_vars.zone_area;
 	script_byte_vars.cur_spot_idx = findInitialSpot();
 	skip_zone_transition |= script_byte_vars.cur_spot_idx;
@@ -4441,8 +4450,12 @@ uint16 RunScript(byte *code) {
 #endif
 
 #ifdef DEBUG_QUEST
-		if (script_ptr - templ_data == 0x4F) {
+		if (script_ptr - templ_data == 0x5B) {
 			/*manipulate rand_value to get a quest item we need*/
+			// 0x5B is the EGA (kultega.bin) offset of the quest-selection branch
+			// 'if rand_value < 0x40'; the old 0x4F was mid-instruction so it never
+			// fired. Forcing rand_value here picks the quest: 0x00=Rope/De Profundis,
+			// 0x40=Knife/The Wall, 0x80=Goblet/Twins, 0xC0=Fly/Scorpion's.
 			script_byte_vars.rand_value = DEBUG_QUEST;
 		}
 #endif


Commit: ed6b4260f2318df0f67524f9fe6f899d38c02542
    https://github.com/scummvm/scummvm/commit/ed6b4260f2318df0f67524f9fe6f899d38c02542
Author: Ion Andrei Cristian (lecturatul2017 at gmail.com)
Date: 2026-06-14T21:13:43+02:00

Commit Message:
CHAMBER: Fix EGA opcode 0x68 PlaySfx operand width

0x68 has a single sfx-index operand in kultega.bin (unlike 0x69 which
has a pad byte). The stray pad read desynced script_ptr - e.g. in the
Scorpion ordeal it ate the setVar that unlocks the door.

Changed paths:
    engines/chamber/script.cpp


diff --git a/engines/chamber/script.cpp b/engines/chamber/script.cpp
index 0affa6388c0..7abd9a61999 100644
--- a/engines/chamber/script.cpp
+++ b/engines/chamber/script.cpp
@@ -3272,12 +3272,15 @@ uint16 SCR_67_Unused(void) {
 /*
 Play Sfx
 NB! Do nothing in EU PC/CGA version
+EGA (kultega.bin) encodes this as a single operand byte (the sfx index) - unlike
+SCR_69 below which has a trailing pad byte. Reading a pad here too would consume
+the following opcode and desync script_ptr (e.g. eats the setVar that unlocks the
+Scorpion ordeal door, leaving its speech bubble stuck on screen).
 */
 uint16 SCR_68_PlaySfx(void) {
 	byte index;
 	script_ptr++;
 	index = *script_ptr++;
-	script_ptr++;
 	IFGM_PlaySfx(index);
 	return 0;
 }


Commit: b528e1ca20780f2feb2f59f82f6d1e01cf17526c
    https://github.com/scummvm/scummvm/commit/b528e1ca20780f2feb2f59f82f6d1e01cf17526c
Author: Ion Andrei Cristian (lecturatul2017 at gmail.com)
Date: 2026-06-14T21:13:43+02:00

Commit Message:
CHAMBER: Fix endgame confrontation menu re-prompt after winning flask

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


diff --git a/engines/chamber/kult.cpp b/engines/chamber/kult.cpp
index fc3b1c2ab77..084c28b39c8 100644
--- a/engines/chamber/kult.cpp
+++ b/engines/chamber/kult.cpp
@@ -198,7 +198,11 @@ process:
 			;
 			updateUndrawCursor(target);
 			refreshSpritesData();
-			runCommand();
+			// Drain priority commands at this main-loop baseline too: a queued
+			// AI command (e.g. the timed "failed the ordeals" death scene) may
+			// fire a priority command, which runCommand now propagates up to a
+			// runCommandKeepSp anchor instead of running it nested.
+			runCommandKeepSp();
 			if (g_vm->_shouldRestart)
 				return;
 			blitSpritesToBackBuffer();
diff --git a/engines/chamber/script.cpp b/engines/chamber/script.cpp
index 7abd9a61999..9359788b66b 100644
--- a/engines/chamber/script.cpp
+++ b/engines/chamber/script.cpp
@@ -3332,6 +3332,7 @@ uint16 CMD_2_PsiPowers(void) {
 	/*Psi powers bar*/
 	g_vm->_renderer->backupAndShowSprite(3, 280 / 4, 40);
 	processInput();
+	clearButtons();
 	do {
 		pollInput();
 		g_vm->_renderer->selectCursor(CURSOR_FINGER);
@@ -4535,9 +4536,17 @@ again:;
 		break;
 	case 0xF000:
 		/*restore sp from keep_sp then run script*/
-		/*currently only supposed to work correctly from the SCR_4D_PriorityCommand handler*/
+		// A priority command (SCR_4D_PriorityCommand) discards the current
+		// callchain. The original restored the script stack pointer to the
+		// main-loop baseline (keep_sp); here that baseline is an empty stack.
+		// Without this, a priority command fired from inside a call'd
+		// subroutine breaks out via ScriptRerun before the matching ret runs,
+		// leaking a script_stack frame each time. In the endgame confrontation
+		// repeated PSI POWERS use overflows the 5-frame script_stack array,
+		// corrupting adjacent globals (sprite/rendering glitches) and script
+		// state (looping menu + forced game-over).
 		debug("Restore: $%X 0x%X", the_command, cmd);
-	/*TODO("SCR_RESTORE\n");*/
+		script_stack_ptr = script_stack;
 	/*fall through*/
 	default:
 		res = RunScript(getScriptSubroutine(cmd - 1));
@@ -4559,12 +4568,18 @@ again:;
 	if (g_vm->_shouldRestart)
 		return runCommandKeepSp();
 
-	if (g_vm->_prioritycommand_1 && !(g_vm->_prioritycommand_2))
+	// A priority command (SCR_4D_PriorityCommand) discards the entire current
+	// callchain and re-runs from the main-loop baseline. Propagate the pending
+	// flag straight up to the runCommandKeepSp anchor instead of re-entering
+	// here: re-entering at this (possibly deeply nested) level ran the priority
+	// script but left the outer ActionsMenu frames alive on the C stack. In the
+	// endgame confrontation each PSI POWERS use fired a priority command and
+	// nested one menu level deeper; after the winning flask the stack unwound
+	// only one level into a stale confrontation menu, which re-prompted and (the
+	// win state already consumed) forced a game-over.
+	if (g_vm->_prioritycommand_1)
 		return res;
 
-	if (g_vm->_prioritycommand_1 && g_vm->_prioritycommand_2)
-		return runCommandKeepSp();
-
 	/*TODO: this is pretty hacky, original code manipulates the stack to discard old script invocation*/
 	if (res == ScriptRerun)
 		goto again;
@@ -4574,11 +4589,21 @@ again:;
 
 uint16 runCommandKeepSp(void) {
 	/*keep_sp = sp;*/
-	g_vm->_prioritycommand_1 = false;
-	g_vm->_prioritycommand_2 = false;
-	if (g_vm->_shouldRestart)
-		return RUNCOMMAND_RESTART;
-	return runCommand();
+	// Anchor for priority commands. The original engine restored the stack
+	// pointer to this baseline (keep_sp) on every priority command, collapsing
+	// any nested script/menu callchain. Emulate that by draining every pending
+	// priority command here in a loop: nested frames propagate the flag up to
+	// this point (see runCommand) and we re-run from the baseline until none
+	// remain, so menu frames never accumulate on the call stack.
+	uint16 res = 0;
+	do {
+		g_vm->_prioritycommand_1 = false;
+		g_vm->_prioritycommand_2 = false;
+		if (g_vm->_shouldRestart)
+			return RUNCOMMAND_RESTART;
+		res = runCommand();
+	} while (g_vm->_prioritycommand_1);
+	return res;
 }
 
 } // End of namespace Chamber


Commit: 6ac56c5cf8bb76166b57087644b68b4781bc0876
    https://github.com/scummvm/scummvm/commit/6ac56c5cf8bb76166b57087644b68b4781bc0876
Author: Ion Andrei Cristian (lecturatul2017 at gmail.com)
Date: 2026-06-14T21:13:43+02:00

Commit Message:
CHAMBER: Make RNG seed overridable via random_seed config key

Changed paths:
    engines/chamber/kult.cpp


diff --git a/engines/chamber/kult.cpp b/engines/chamber/kult.cpp
index 084c28b39c8..58907294db0 100644
--- a/engines/chamber/kult.cpp
+++ b/engines/chamber/kult.cpp
@@ -19,6 +19,7 @@
  *
  */
 
+#include "common/config-manager.h"
 #include "common/error.h"
 #include "common/system.h"
 #include "engines/advancedDetector.h"
@@ -113,7 +114,10 @@ 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.
-	rand_seed = (byte)(g_system->getMillis());
+	if (ConfMan.hasKey("random_seed"))
+		rand_seed = (byte)ConfMan.getInt("random_seed");
+	else
+		rand_seed = (byte)(g_system->getMillis());
 	getRand();
 }
 




More information about the Scummvm-git-logs mailing list