[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, ®, ®);
- 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