[Scummvm-git-logs] scummvm master -> 08e53baf6090e3d794da607b2512ba529e88fd39

alexbevi noreply at scummvm.org
Wed Sep 2 11:18:21 UTC 2026


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

Summary:
d1c4ec6f49 HARVESTER: Preserve NPC animation anchors
08e53baf60 ASYLUM: Implement cinematic matte palette transitions


Commit: d1c4ec6f49e9ae35081c7943a6ea417c96b33c7b
    https://github.com/scummvm/scummvm/commit/d1c4ec6f49e9ae35081c7943a6ea417c96b33c7b
Author: Alex Bevilacqua (alex at alexbevi.com)
Date: 2026-09-02T07:02:45-04:00

Commit Message:
HARVESTER: Preserve NPC animation anchors

NPCs would incorrectly appear to stutter when their idle animation looped

Assisted-by: Codex:gpt-5.6-sol

Changed paths:
    engines/harvester/detection.cpp
    engines/harvester/flow.cpp
    engines/harvester/room.cpp
    engines/harvester/runtime_entity.cpp


diff --git a/engines/harvester/detection.cpp b/engines/harvester/detection.cpp
index b4bd28a326b..3d6e4b28c11 100644
--- a/engines/harvester/detection.cpp
+++ b/engines/harvester/detection.cpp
@@ -28,7 +28,7 @@ const DebugChannelDef HarvesterMetaEngineDetection::debugFlagList[] = {
 	{ Harvester::kDebugGeneral, "general", "General debug level" },
 	{ Harvester::kDebugCursor, "cursor", "Cursor animation, hover, and placement debug" },
 	{ Harvester::kDebugRoom, "room", "Room setup, scene population, and interaction debug" },
-	{ Harvester::kDebugPlayer, "player", "Player animation, movement, and spawn debug" },
+	{ Harvester::kDebugPlayer, "player", "Player and NPC animation, movement, and spawn debug" },
 	{ Harvester::kDebugPathfinding, "pathfinding", "Player pathfinding target, blocker, and movement-band debug" },
 	{ Harvester::kDebugCombat, "combat", "Room combat pursuit, attack, and damage debug" },
 	{ Harvester::kDebugResources, "resources", "Resource loading" },
diff --git a/engines/harvester/flow.cpp b/engines/harvester/flow.cpp
index ee4bc9c7d54..4e00c58e22b 100644
--- a/engines/harvester/flow.cpp
+++ b/engines/harvester/flow.cpp
@@ -2271,6 +2271,12 @@ bool Flow::populateRoomSceneEntities(RoomSetupState &state,
 			npc.deathDamageType != 0 &&
 			npc.runtimeState >= 0;
 		entity->setHitTestMode(isCorpse ? kRuntimeEntityHitTestNone : kRuntimeEntityHitTestOpaquePixels);
+		// Native spawn positions frame 0 before selecting ambient or corpse frame banks.
+		if (!applyRoomActorPlacementInternal(state, *entity,
+				npc.posX, npc.posY, (float)npc.posZ, nullptr, false)) {
+			debug(1, "Harvester: unable to apply room npc placement for '%s'",
+				npc.npcName.c_str());
+		}
 		if (isCorpse) {
 			const int corpseFrame = MIN(entity->getLastFrame(), npc.runtimeState);
 			entity->setAnimationFrameRange(corpseFrame, corpseFrame, false);
@@ -2283,12 +2289,6 @@ bool Flow::populateRoomSceneEntities(RoomSetupState &state,
 			if (npc.frameDelay > 0)
 				entity->setAnimationRate(npc.frameDelay);
 		}
-		// Native room NPCs come from spawn_abm_entity_base, which leaves the depth-scale flag cleared.
-		if (!applyRoomActorPlacementInternal(state, *entity,
-				npc.posX, npc.posY, (float)npc.posZ, nullptr, false)) {
-			debug(1, "Harvester: unable to apply room npc placement for '%s'",
-				npc.npcName.c_str());
-		}
 		entityManager->reinsertSceneEntity(entity);
 		debugC(1, kDebugRoom,
 			"Harvester: scene npc spawned room='%s' npc='%s' class=0x%x pos=(%d,%d,z=%.2f) frame_delay=%d model='%s' active=%d visible=%d",
diff --git a/engines/harvester/room.cpp b/engines/harvester/room.cpp
index 65814b5bcaf..7852a62a8aa 100644
--- a/engines/harvester/room.cpp
+++ b/engines/harvester/room.cpp
@@ -855,15 +855,19 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
 			if (!entityManager || !shouldSpawnNpc)
 				return nullptr;
 
-			const int initialFrame = preservedCorpse ? npc.runtimeState : 0;
 			Entity *entity = entityManager->spawnSceneActorEntity(
-				npc.npcName, npc.modelPath, Common::Point(npc.posX, npc.posY), (float)npc.posZ, initialFrame);
+				npc.npcName, npc.modelPath, Common::Point(npc.posX, npc.posY), (float)npc.posZ, 0);
 			if (!entity)
 				return nullptr;
 
 			entity->setClassId(kRuntimeEntityClassNpc);
 			entity->setZExtent(kNativeNpcMonsterZExtent);
 			entity->setHitTestMode(preservedCorpse ? kRuntimeEntityHitTestNone : kRuntimeEntityHitTestOpaquePixels);
+			entity->setVisible(true);
+			if (!applyRoomNpcPlacement(*entity, npc)) {
+				removeSceneEntityByName(npc.npcName);
+				return nullptr;
+			}
 			if (preservedCorpse) {
 				const int corpseFrame = MIN(entity->getLastFrame(), npc.runtimeState);
 				entity->setAnimationFrameRange(corpseFrame, corpseFrame, false);
@@ -874,11 +878,6 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
 				entity->setAnimationFrameRange(0, MIN(entity->getLastFrame(), kRoomNpcAmbientLastFrame), true);
 				entity->setAnimationRate(npc.frameDelay > 0 ? npc.frameDelay : 0);
 			}
-			entity->setVisible(true);
-			if (!applyRoomNpcPlacement(*entity, npc)) {
-				removeSceneEntityByName(npc.npcName);
-				return nullptr;
-			}
 			entityManager->reinsertSceneEntity(entity);
 			return entity;
 		};
@@ -1458,12 +1457,7 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
 			if (!entityManager)
 				return;
 
-			for (const NpcRecord &npc : scene.state.roomNpcs) {
-				Entity *entity = entityManager->findSceneEntityByName(npc.npcName);
-				if (!entity)
-					continue;
-				(void)applyRoomNpcPlacement(*entity, npc);
-			}
+			// Class-4 NPCs retain their spawn base through ambient and death animation banks.
 			for (const MonsterRecord &monster : scene.state.roomMonsters) {
 				Entity *entity = entityManager->findSceneEntityByName(monster.monsterName);
 				if (!entity)
diff --git a/engines/harvester/runtime_entity.cpp b/engines/harvester/runtime_entity.cpp
index 52ea2c13d77..5aec9b654e0 100644
--- a/engines/harvester/runtime_entity.cpp
+++ b/engines/harvester/runtime_entity.cpp
@@ -245,12 +245,31 @@ bool Entity::loadAbmResource(ResourceManager &resources, const Common::String &p
 }
 
 void Entity::setPosition(int x, int y, float z) {
+	const int previousX = _x;
+	const int previousY = _y;
+	const float previousZ = _z;
+	const Common::Point previousDrawOrigin = getDrawOrigin();
 	const bool anchorChanged = _x != x || _y != y;
 	_x = x;
 	_y = y;
 	_z = z;
 	if (anchorChanged)
 		updateScreenBaseFromCurrentFrame();
+
+	if ((anchorChanged || previousZ != z) && _classId == kRuntimeEntityClassNpc) {
+		int width = 0;
+		int height = 0;
+		int xOffset = 0;
+		int yOffset = 0;
+		(void)getCurrentFrameMetrics(width, height, xOffset, yOffset);
+		const Common::Point drawOrigin = getDrawOrigin();
+		debugC(3, kDebugPlayer,
+			"Harvester: npc position npc='%s' frame=%d entity=(%d,%d,z=%.2f)->(%d,%d,z=%.2f) frame_size=%dx%d frame_offset=(%d,%d) draw=(%d,%d)->(%d,%d)",
+			_name.c_str(), _currentFrame,
+			previousX, previousY, (double)previousZ, _x, _y, (double)_z,
+			width, height, xOffset, yOffset,
+			previousDrawOrigin.x, previousDrawOrigin.y, drawOrigin.x, drawOrigin.y);
+	}
 }
 
 void Entity::setAnchorMode(RuntimeEntityAnchorMode anchorMode) {
@@ -259,6 +278,7 @@ void Entity::setAnchorMode(RuntimeEntityAnchorMode anchorMode) {
 }
 
 void Entity::setAnimationRate(int rate) {
+	const int previousRate = _animationRate;
 	if (rate == 0)
 		_animationTickInterval = 0;
 	else
@@ -267,11 +287,24 @@ void Entity::setAnimationRate(int rate) {
 	if (rate != _animationRate) {
 		_nextAnimationTick = 0;
 		_animationRate = rate;
+		if (_classId == kRuntimeEntityClassNpc) {
+			debugC(2, kDebugPlayer,
+				"Harvester: npc animation rate npc='%s' rate=%d->%d interval_ticks=%u next_tick=%u",
+				_name.c_str(), previousRate, _animationRate,
+				_animationTickInterval, _nextAnimationTick);
+		}
 	}
 }
 
 void Entity::setAnimationEnabled(bool enabled) {
+	const bool wasEnabled = _animationEnabled;
 	_animationEnabled = enabled && !_frames.empty() && _currentFrame >= 0;
+	if (wasEnabled != _animationEnabled && _classId == kRuntimeEntityClassNpc) {
+		debugC(2, kDebugPlayer,
+			"Harvester: npc animation enabled npc='%s' enabled=%d->%d frame=%d range=%d..%d",
+			_name.c_str(), wasEnabled, _animationEnabled,
+			_currentFrame, _firstFrame, _lastFrame);
+	}
 }
 
 void Entity::setCurrentFrame(int frame) {
@@ -286,6 +319,9 @@ void Entity::setAnimationFrameRange(int firstFrame, int lastFrame, bool looping)
 	if (_frames.empty())
 		return;
 
+	const int previousFirstFrame = _firstFrame;
+	const int previousLastFrame = _lastFrame;
+	const bool wasLooping = _looping;
 	firstFrame = CLIP<int>(firstFrame, 0, (int)_frames.size() - 1);
 	lastFrame = CLIP<int>(lastFrame, 0, (int)_frames.size() - 1);
 	if (lastFrame < firstFrame)
@@ -301,6 +337,14 @@ void Entity::setAnimationFrameRange(int firstFrame, int lastFrame, bool looping)
 		advanceAnimationFrame(_firstFrame);
 	else
 		updateBoundsFromCurrentFrame();
+
+	if (_classId == kRuntimeEntityClassNpc) {
+		debugC(2, kDebugPlayer,
+			"Harvester: npc animation range npc='%s' frames=%d..%d->%d..%d current=%d looping=%d->%d enabled=%d",
+			_name.c_str(), previousFirstFrame, previousLastFrame,
+			_firstFrame, _lastFrame, _currentFrame,
+			wasLooping, _looping, _animationEnabled);
+	}
 }
 
 void Entity::setAnimationSequence(int sequence) {
@@ -394,9 +438,31 @@ bool Entity::tickVisualState(uint32 now) {
 	if (now < _nextAnimationTick)
 		return false;
 
+	const int previousFrameIndex = _currentFrame;
+	const bool wasPlayingBackwards = _playBackwards;
+	const AbmFrame &previousFrame = _frames[(uint)previousFrameIndex];
+	const Common::Point previousDrawOrigin = getDrawOrigin();
 	advanceAnimationFrame(_playBackwards ? -1 : -2);
 	_nextAnimationTick = now + _animationTickInterval;
 	_animationAdvancedLastTick = true;
+
+	if (_classId == kRuntimeEntityClassNpc) {
+		const AbmFrame &currentFrame = _frames[(uint)_currentFrame];
+		const Common::Point drawOrigin = getDrawOrigin();
+		const bool loopReset = _looping && !_pingPong &&
+			((!wasPlayingBackwards && previousFrameIndex == _lastFrame && _currentFrame == _firstFrame) ||
+			 (wasPlayingBackwards && previousFrameIndex == _firstFrame && _currentFrame == _lastFrame));
+		debugC(3, kDebugPlayer,
+			"Harvester: npc animation advance npc='%s' frame=%d->%d range=%d..%d loop_reset=%d backwards=%d->%d rate=%d interval=%u entity=(%d,%d,z=%.2f) previous=(size=%ux%u offset=%d,%d draw=%d,%d) current=(size=%ux%u offset=%d,%d draw=%d,%d)",
+			_name.c_str(),
+			previousFrameIndex, _currentFrame, _firstFrame, _lastFrame,
+			loopReset, wasPlayingBackwards, _playBackwards,
+			_animationRate, _animationTickInterval, _x, _y, (double)_z,
+			previousFrame.width, previousFrame.height, previousFrame.xOffset, previousFrame.yOffset,
+			previousDrawOrigin.x, previousDrawOrigin.y,
+			currentFrame.width, currentFrame.height, currentFrame.xOffset, currentFrame.yOffset,
+			drawOrigin.x, drawOrigin.y);
+	}
 	return true;
 }
 


Commit: 08e53baf6090e3d794da607b2512ba529e88fd39
    https://github.com/scummvm/scummvm/commit/08e53baf6090e3d794da607b2512ba529e88fd39
Author: Alex Bevilacqua (alex at alexbevi.com)
Date: 2026-09-02T07:18:00-04:00

Commit Message:
ASYLUM: Implement cinematic matte palette transitions

The original game builds a grayscale target when movie playback starts. Its
shared scene transition blends toward that palette while the matte bars close
and back to the selected scene palette while they open.

Assisted-by: Codex:gpt-5.6-sol

Changed paths:
    engines/asylum/resources/encounters.cpp
    engines/asylum/resources/encounters.h
    engines/asylum/resources/script.cpp
    engines/asylum/system/screen.cpp
    engines/asylum/system/screen.h


diff --git a/engines/asylum/resources/encounters.cpp b/engines/asylum/resources/encounters.cpp
index 40b01f94cad..110dc0d8125 100644
--- a/engines/asylum/resources/encounters.cpp
+++ b/engines/asylum/resources/encounters.cpp
@@ -1219,7 +1219,7 @@ void Encounter::drawScreen() {
 			if (getSharedData()->getMatteInitialized()) {
 				getScreen()->drawWideScreenBars(82);
 
-				getScreen()->updatePalette();
+				getScreen()->copyGrayPaletteToWorkingPalette();
 				getScreen()->setupPalette(NULL, 0, 0);
 				getScreen()->paletteFade(0, 25, 10);
 			} else {
@@ -1243,7 +1243,7 @@ void Encounter::drawScreen() {
 					getScene()->updateScreen();
 					getScreen()->drawWideScreenBars(82);
 
-					getScreen()->updatePalette(0);
+					getScreen()->copyGrayPaletteToWorkingPalette();
 					getScreen()->setupPalette(NULL, 0, 0);
 
 					if (getSharedData()->getMattePlaySound() /* Scene::updateScreen() does script processing, so the value might have changed */
@@ -1268,7 +1268,7 @@ void Encounter::drawScreen() {
 			ResourceId paletteId = getWorld()->actions[getScene()->getActor()->getActionIndex3()]->paletteResourceId;
 			getScreen()->setPaletteGamma(paletteId ? paletteId : getWorld()->currentPaletteId);
 
-			updatePalette1();
+			blendPaletteFromGray();
 			getScreen()->setupPalette(NULL, 0, 0);
 		}
 	} else {
@@ -1277,7 +1277,7 @@ void Encounter::drawScreen() {
 
 		getScreen()->setPaletteGamma(getWorld()->currentPaletteId);
 
-		updatePalette2();
+		blendPaletteToGray();
 		getScreen()->setupPalette(NULL, 0, 0);
 	}
 }
@@ -1492,12 +1492,16 @@ bool Encounter::updateScreen() {
 	return false;
 }
 
-void Encounter::updatePalette1() {
-	debugC(kDebugLevelEncounter, "[Encounter::updatePalette1] Not implemented!");
+void Encounter::blendPaletteFromGray() {
+	const int32 step = getSharedData()->getMatteBarHeight() - 90;
+	debugC(kDebugLevelEncounter, "[Encounter] Blending palette from grayscale: step %d/80", step);
+	getScreen()->blendPaletteFromGray(step, 80);
 }
 
-void Encounter::updatePalette2() {
-	debugC(kDebugLevelEncounter, "[Encounter::updatePalette2] Not implemented!");
+void Encounter::blendPaletteToGray() {
+	const int32 step = getSharedData()->getMatteBarHeight();
+	debugC(kDebugLevelEncounter, "[Encounter] Blending palette to grayscale: step %d/85", step);
+	getScreen()->blendPaletteToGray(step, 85);
 }
 
 //////////////////////////////////////////////////////////////////////////
diff --git a/engines/asylum/resources/encounters.h b/engines/asylum/resources/encounters.h
index 070d6a5a425..fe69324b392 100644
--- a/engines/asylum/resources/encounters.h
+++ b/engines/asylum/resources/encounters.h
@@ -257,8 +257,8 @@ private:
 	void updateDrawingStatus1(int32 rectIndex);
 	void updateDrawingStatus2(int32 rectIndex);
 	bool updateScreen();
-	void updatePalette1();
-	void updatePalette2();
+	void blendPaletteFromGray();
+	void blendPaletteToGray();
 
 	bool isKeywordVisible(int16 keyword) const  { return (bool)(BYTE1(keyword) & kKeywordOptionsVisible); }
 	bool isKeywordDisabled(int16 keyword) const { return (bool)(BYTE1(keyword) & kKeywordOptionsDisabled); }
diff --git a/engines/asylum/resources/script.cpp b/engines/asylum/resources/script.cpp
index 95bdf2e13bf..d14dd4187cc 100644
--- a/engines/asylum/resources/script.cpp
+++ b/engines/asylum/resources/script.cpp
@@ -1259,7 +1259,7 @@ IMPLEMENT_OPCODE(CreatePalette)
 		return;
 	}
 
-	getScreen()->updatePalette(cmd->param1);
+	getScreen()->blendScenePaletteForFadeStep(cmd->param1);
 
 	_processNextEntry = true;
 	++cmd->param1;
diff --git a/engines/asylum/system/screen.cpp b/engines/asylum/system/screen.cpp
index 71ac4c4bbb7..177e058ff2e 100644
--- a/engines/asylum/system/screen.cpp
+++ b/engines/asylum/system/screen.cpp
@@ -312,14 +312,13 @@ void Screen::setupPalette(byte *buffer, int start, int count) {
 	_vm->_system->getPaletteManager()->setPalette(_mainPalette, 0, 256);
 }
 
-void Screen::updatePalette() {
-	// FIXME: This is used to replace all the inline code to setup the palette before calls to setupPalette/paletteFade
-	// See if all that code can really be factorized into a single function or not
-	debugC(kDebugLevelScene, "[Screen::updatePalette] Not implemented!");
+void Screen::copyGrayPaletteToWorkingPalette() {
+	memcpy(_mainPalette + 3, _currentPalette + 3, sizeof(_mainPalette) - 6);
+	debugC(kDebugLevelScene, "[Screen] Copied grayscale scene palette to the working palette");
 }
 
-void Screen::updatePalette(int32 param) {
-	if (param >= 21) {
+void Screen::blendScenePaletteForFadeStep(int32 step) {
+	if (step >= 21) {
 		for (uint32 j = 3; j < ARRAYSIZE(_mainPalette) - 3; j += 3) {
 			_mainPalette[j]     = _currentPalette[j];
 			_mainPalette[j + 1] = _currentPalette[j + 1];
@@ -338,17 +337,30 @@ void Screen::updatePalette(int32 param) {
 		byte *paletteData = getPaletteData(paletteId);
 		paletteData += 4;
 
-		float fParam = param / 20.0;
+		float fraction = step / 20.0;
 		for (uint32 j = 3; j < ARRAYSIZE(_mainPalette) - 3; j += 3) {
-			_mainPalette[j]     = (byte)((1.0 - fParam) * 4 * paletteData[j]     + fParam * _currentPalette[j]);
-			_mainPalette[j + 1] = (byte)((1.0 - fParam) * 4 * paletteData[j + 1] + fParam * _currentPalette[j + 1]);
-			_mainPalette[j + 2] = (byte)((1.0 - fParam) * 4 * paletteData[j + 2] + fParam * _currentPalette[j + 2]);
+			_mainPalette[j]     = (byte)((1.0 - fraction) * 4 * paletteData[j]     + fraction * _currentPalette[j]);
+			_mainPalette[j + 1] = (byte)((1.0 - fraction) * 4 * paletteData[j + 1] + fraction * _currentPalette[j + 1]);
+			_mainPalette[j + 2] = (byte)((1.0 - fraction) * 4 * paletteData[j + 2] + fraction * _currentPalette[j + 2]);
 		}
 
 		setupPalette(nullptr, 0, 0);
 	}
 }
 
+void Screen::blendPaletteFromGray(int32 step, int32 stepCount) {
+	blendPalette(_currentPalette, _mainPalette, step, stepCount);
+}
+
+void Screen::blendPaletteToGray(int32 step, int32 stepCount) {
+	blendPalette(_mainPalette, _currentPalette, step, stepCount);
+}
+
+void Screen::blendPalette(const byte *from, const byte *to, int32 step, int32 stepCount) {
+	for (uint32 i = 3; i < ARRAYSIZE(_mainPalette) - 3; ++i)
+		_mainPalette[i] = (byte)(from[i] + ((int32)to[i] - from[i]) * step / stepCount);
+}
+
 //////////////////////////////////////////////////////////////////////////
 // Palette fading
 //////////////////////////////////////////////////////////////////////////
diff --git a/engines/asylum/system/screen.h b/engines/asylum/system/screen.h
index 0165cd313a5..824c6b4e6a4 100644
--- a/engines/asylum/system/screen.h
+++ b/engines/asylum/system/screen.h
@@ -106,8 +106,10 @@ public:
 	const byte *getPalette() { return _mainPalette; }
 	void setMainPalette(const byte *data);
 	void loadGrayPalette();
-	void updatePalette();
-	void updatePalette(int32 param);
+	void copyGrayPaletteToWorkingPalette();
+	void blendScenePaletteForFadeStep(int32 step);
+	void blendPaletteFromGray(int32 step, int32 stepCount);
+	void blendPaletteToGray(int32 step, int32 stepCount);
 	void setupPalette(byte *buffer, int start, int count);
 
 	bool isFading() { return _isFading; }
@@ -175,6 +177,7 @@ private:
 	Common::Queue<FadeParameters> _fadeQueue;
 
 	byte *getPaletteData(ResourceId id);
+	void blendPalette(const byte *from, const byte *to, int32 step, int32 stepCount);
 	void setPaletteGamma(byte *data, byte *target = NULL);
 
 	void stopQueuedPaletteFade();




More information about the Scummvm-git-logs mailing list