[Scummvm-git-logs] scummvm master -> f882fd670ad0397cdc3609725b1bccf56f6a810a
alexbevi
noreply at scummvm.org
Sat Aug 22 12:37:27 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:
b9f6ea7020 HARVESTER: Remove dead combat script checks
e91d795c23 HARVESTER: Move refreshed monster combat state
2bf152eb63 HARVESTER: Move loaded dialogue portrait
b2c0959076 HARVESTER: Fix narrow region exit activation
da2161c375 HARVESTER: Enforce keyboard walk bounds
29f101a95c HARVESTER: Correct monster attack animation rate
f882fd670a HARVESTER: Correct player death animation rate
Commit: b9f6ea702015349446f36511763771b97f3ce2bd
https://github.com/scummvm/scummvm/commit/b9f6ea702015349446f36511763771b97f3ce2bd
Author: Alex Bevilacqua (alex at alexbevi.com)
Date: 2026-08-22T08:36:02-04:00
Commit Message:
HARVESTER: Remove dead combat script checks
The room loop resolves its setup through the loaded startup script before monster combat can run. Treat that script as a required reference in the combat updater and remove unreachable null-only logging and guards.
This preserves monster hit, death action, and runtime-record synchronization behavior while resolving Coverity CID 1660902.
Assisted-by: Codex:gpt-5.6-sol
Changed paths:
engines/harvester/room.cpp
diff --git a/engines/harvester/room.cpp b/engines/harvester/room.cpp
index fd8952d8189..2a9063a107b 100644
--- a/engines/harvester/room.cpp
+++ b/engines/harvester/room.cpp
@@ -2882,13 +2882,13 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
if (!entityManager)
return Common::kNoError;
- Script *script = _engine.getScript();
+ Script &script = *_engine.getScript();
const uint32 now = Player::getRuntimeClockTicks();
const uint32 moveInterval = MAX<uint32>(1, 100U / (uint32)kRoomMonsterAnimationRate);
const int horizontalStepBase = 8;
const int depthStep = MAX<int>(1, roundRoomCombatFloat(
scene.state.roomZVelocityStep > 0.0f ? scene.state.roomZVelocityStep : 1.0f));
- bool playerAlive = script && script->getPlayerCurrentHitPoints() > 0;
+ bool playerAlive = script.getPlayerCurrentHitPoints() > 0;
if (npcCombatStates.size() != scene.state.roomNpcs.size())
npcCombatStates.resize(scene.state.roomNpcs.size());
@@ -2945,7 +2945,7 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
entity->setAnimationFrameRange(monster.runtimeState, monster.runtimeState, false);
entity->setCurrentFrame(monster.runtimeState);
entity->setAnimationEnabled(false);
- (void)script->syncRuntimeMonsterRecord(monster);
+ (void)script.syncRuntimeMonsterRecord(monster);
debugC(1, kDebugCombat,
"Harvester: combat monster death complete target='%s' damage_type=%d corpse_frame=%d on_death='%s'",
monster.monsterName.c_str(), combatState.deathDamageType,
@@ -2957,9 +2957,9 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
InteractionResult interaction;
interaction.mutatedRuntimeState = true;
interaction.visualRuntimeStateChanged = true;
- if (script && !monster.onDeathActionTag.empty()) {
+ if (!monster.onDeathActionTag.empty()) {
InteractionResult deathInteraction;
- if (script->executeActionTag(
+ if (script.executeActionTag(
monster.onDeathActionTag, deathInteraction, true, monster.roomName)) {
interaction = Common::move(deathInteraction);
interaction.mutatedRuntimeState = true;
@@ -3006,7 +3006,7 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
if (monster.posX != previousX) {
(void)applyRoomActorPlacement(
scene.state, *entity, monster.posX, monster.posY, (float)monster.posZ);
- (void)script->syncRuntimeMonsterRecord(monster);
+ (void)script.syncRuntimeMonsterRecord(monster);
debugC(1, kDebugCombat,
"Harvester: combat monster hit knockback target='%s' from_x=%d to_x=%d remaining=%d",
monster.monsterName.c_str(), previousX, monster.posX,
@@ -3042,18 +3042,14 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
playerState.entity &&
!combatState.attackTargetName.empty() &&
playerState.entity->getName().equalsIgnoreCase(combatState.attackTargetName);
- if (!script) {
- debugC(1, kDebugCombat,
- "Harvester: combat monster attack contact monster='%s' frame=%d skipped reason='no startup script'",
- monster.monsterName.c_str(), currentFrame);
- } else if (!playerAlive || !hasTrackedPlayerTarget) {
+ if (!playerAlive || !hasTrackedPlayerTarget) {
debugC(1, kDebugCombat,
"Harvester: combat monster attack miss monster='%s' frame=%d player_hp=%d reason='no live target'",
- monster.monsterName.c_str(), currentFrame, script->getPlayerCurrentHitPoints());
+ monster.monsterName.c_str(), currentFrame, script.getPlayerCurrentHitPoints());
} else {
- const int playerHitPointsBefore = script->getPlayerCurrentHitPoints();
- const bool changed = script->adjustPlayerCurrentHitPoints(-monster.damageAmount);
- const int playerHitPointsAfter = script->getPlayerCurrentHitPoints();
+ const int playerHitPointsBefore = script.getPlayerCurrentHitPoints();
+ const bool changed = script.adjustPlayerCurrentHitPoints(-monster.damageAmount);
+ const int playerHitPointsAfter = script.getPlayerCurrentHitPoints();
const int damageLanded = playerHitPointsBefore - playerHitPointsAfter;
debugC(1, kDebugCombat,
"Harvester: combat monster attack hit monster='%s' frame=%d damage=%d damage_type='%s' player_hp=%d->%d changed=%d",
@@ -3160,8 +3156,7 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
(void)applyRoomActorPlacement(scene.state, *entity, monster.posX, monster.posY, (float)monster.posZ);
if (monster.posZ != previousZ)
entityManager->reinsertSceneEntity(entity);
- if (script)
- (void)script->syncRuntimeMonsterRecord(monster);
+ (void)script.syncRuntimeMonsterRecord(monster);
debugC(1, kDebugCombat,
"Harvester: combat monster chase move monster='%s' live_center_dx=%d z_delta=%.2f engage=%d waypoint_tol=%d from=(%d,%d,z=%d) to=(%d,%d,z=%d) facing=%d",
monster.monsterName.c_str(), liveCenterDx, (double)zDelta, engageDistance, liveWaypointTolerance,
Commit: e91d795c237d6ced102e01dd0c69c6f804e11d1c
https://github.com/scummvm/scummvm/commit/e91d795c237d6ced102e01dd0c69c6f804e11d1c
Author: Alex Bevilacqua (alex at alexbevi.com)
Date: 2026-08-22T08:36:02-04:00
Commit Message:
HARVESTER: Move refreshed monster combat state
Room refresh rebuilds monster combat state in a temporary array before replacing the live state. Move the temporary at the final handoff because it is not used afterward.
This avoids the copy reported by Coverity CID 1660884 without changing the refreshed combat state.
Assisted-by: Codex:gpt-5.6-sol
Changed paths:
engines/harvester/room.cpp
diff --git a/engines/harvester/room.cpp b/engines/harvester/room.cpp
index 2a9063a107b..3cfbd1e9fe2 100644
--- a/engines/harvester/room.cpp
+++ b/engines/harvester/room.cpp
@@ -1348,7 +1348,7 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
}
scene.state = updatedState;
- monsterCombatStates = updatedMonsterCombatStates;
+ monsterCombatStates = Common::move(updatedMonsterCombatStates);
scene.sceneObjects = updatedScene.sceneObjects;
scene.sceneAnimations = updatedScene.sceneAnimations;
scene.sceneRegions = updatedScene.sceneRegions;
Commit: 2bf152eb639ce58fb589d5682eb957ac872645a4
https://github.com/scummvm/scummvm/commit/2bf152eb639ce58fb589d5682eb957ac872645a4
Author: Alex Bevilacqua (alex at alexbevi.com)
Date: 2026-08-22T08:36:02-04:00
Commit Message:
HARVESTER: Move loaded dialogue portrait
Dialogue portrait loading fills a temporary bitmap before replacing the cached left or right portrait. Move the temporary at that final handoff because it is not used afterward.
This avoids the copy reported by Coverity CID 1660908 without changing portrait selection or load-failure behavior.
Assisted-by: Codex:gpt-5.6-sol
Changed paths:
engines/harvester/dialogue.cpp
diff --git a/engines/harvester/dialogue.cpp b/engines/harvester/dialogue.cpp
index 6117ff04e5c..2ce94627fbd 100644
--- a/engines/harvester/dialogue.cpp
+++ b/engines/harvester/dialogue.cpp
@@ -1105,7 +1105,7 @@ private:
IndexedBitmap updatedBitmap;
if (loadDialogueHeadBitmap(_engine, speakerId, headVariant, updatedBitmap)) {
- *targetBitmap = updatedBitmap;
+ *targetBitmap = Common::move(updatedBitmap);
*targetSpeakerId = headId;
} else {
warning("Harvester: unable to load dialogue head for '%s'", headId.c_str());
Commit: b2c0959076a0285a3c7caf9590f0e6ba2b024410
https://github.com/scummvm/scummvm/commit/b2c0959076a0285a3c7caf9590f0e6ba2b024410
Author: Alex Bevilacqua (alex at alexbevi.com)
Date: 2026-08-22T08:36:02-04:00
Commit Message:
HARVESTER: Fix narrow region exit activation
Check enabled player/region overlaps before advancing the actor state. This preserves the active walk-frame bounds at narrow exits and lets mouse targets and keyboard movement dispatch the same region action.
Retail run_harvester_main_loop calls check_player_region_interaction at 0x6ff71 before update_actor_runtime_state at 0x7012d. Match do_entity_screen_bounds_overlap at 0x4b700 by accepting edge contact, and keep region movement component-based: region clicks set X and Z without an exact screen-Y requirement.
Assisted-by: Codex:gpt-5.6-sol
Changed paths:
engines/harvester/flow.cpp
engines/harvester/player.cpp
engines/harvester/room.cpp
engines/harvester/room_support.h
diff --git a/engines/harvester/flow.cpp b/engines/harvester/flow.cpp
index be36dd27d8e..5f5c5543ce0 100644
--- a/engines/harvester/flow.cpp
+++ b/engines/harvester/flow.cpp
@@ -1151,8 +1151,18 @@ bool doesPlayerOverlapRegion(const Entity &playerEntity, const RegionRecord ®
const Common::Rect regionBounds = getRegionBounds(region);
if (regionBounds.isEmpty())
return false;
- if (!playerEntity.getScreenRect().intersects(regionBounds))
+
+ // Native do_entity_screen_bounds_overlap (0x4b700) compares each entity's
+ // origin plus width/height and rejects only when one edge is strictly before
+ // the other. Common::Rect uses exclusive right/bottom edges, so intersects()
+ // would incorrectly reject the edge contact that activates narrow exits.
+ const Common::Rect playerBounds = playerEntity.getScreenRect();
+ if (playerBounds.right < regionBounds.left ||
+ regionBounds.right < playerBounds.left ||
+ playerBounds.bottom < regionBounds.top ||
+ regionBounds.bottom < playerBounds.top) {
return false;
+ }
const float playerMaxZ = playerEntity.getZ() + playerEntity.getZExtent();
return playerMaxZ >= (float)region.minZ && (float)region.maxZ >= playerEntity.getZ();
diff --git a/engines/harvester/player.cpp b/engines/harvester/player.cpp
index b2bd3f1dc25..c86bed42a7c 100644
--- a/engines/harvester/player.cpp
+++ b/engines/harvester/player.cpp
@@ -626,10 +626,6 @@ static int resolveMoveTargetDepthDirection(const RoomPlayerState &playerState) {
return 1;
if (playerState.z + kRoomPlayerDepthTargetSlack < playerState.targetZ)
return -1;
- if (playerState.moveTargetRequiresScreenY && playerState.bottomY < playerState.targetBottomY)
- return 1;
- if (playerState.moveTargetRequiresScreenY && playerState.bottomY > playerState.targetBottomY)
- return -1;
return 0;
}
@@ -1007,13 +1003,12 @@ static bool recordNativePrimaryWalkBlockerHistory(const RoomSetupState &state,
}
static void setMoveTargetInternal(const RoomSetupState &state, RoomPlayerState &playerState,
- int targetX, float targetZ, int targetBottomY, bool requireScreenY) {
+ int targetX, float targetZ, int targetBottomY) {
playerState.hasMoveTarget = true;
playerState.nextMovementTick = 0;
playerState.targetX = CLIP<int>(targetX, 0, 639);
playerState.targetBottomY = clampRoomMovementY(state, targetBottomY);
playerState.targetZ = clampRoomDepth(state, targetZ);
- playerState.moveTargetRequiresScreenY = requireScreenY;
resetPlayerMoveTargetProgress(playerState);
debugC(1, kDebugPlayer,
"Harvester: player move target room='%s' current=(%d,%d,z=%.2f) target=(%d,%d,z=%.2f)",
@@ -1224,20 +1219,20 @@ void Player::updateIdleTrigger(RoomIdleAnimationState &idleState) {
void Player::setMoveTarget(const RoomSetupState &state, RoomPlayerState &playerState,
int targetX, float targetZ) {
setMoveTargetInternal(state, playerState, targetX, targetZ,
- mapRoomDepthToScreenY(state, targetZ), false);
+ mapRoomDepthToScreenY(state, targetZ));
}
void Player::setRegionMoveTarget(const RoomSetupState &state, RoomPlayerState &playerState,
int targetX, float targetZ) {
setMoveTargetInternal(state, playerState, targetX, targetZ,
- mapRoomDepthToScreenY(state, targetZ), true);
+ mapRoomDepthToScreenY(state, targetZ));
}
void Player::setMoveTargetFromScreenPoint(const RoomSetupState &state,
RoomPlayerState &playerState, int targetX, int targetBottomY) {
const int clampedTargetBottomY = clampRoomMovementY(state, targetBottomY);
setMoveTargetInternal(state, playerState, targetX,
- mapRoomScreenYToDepth(state, clampedTargetBottomY), clampedTargetBottomY, false);
+ mapRoomScreenYToDepth(state, clampedTargetBottomY), clampedTargetBottomY);
}
bool Player::resolveBlockedStartupSpawn(HarvesterEngine &engine, const RoomSetupState &state,
diff --git a/engines/harvester/room.cpp b/engines/harvester/room.cpp
index 3cfbd1e9fe2..043e0d27622 100644
--- a/engines/harvester/room.cpp
+++ b/engines/harvester/room.cpp
@@ -3438,6 +3438,11 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
InteractionResult interaction;
if (!_engine.getScript()->resolveRegionInteraction(region, interaction, scene.state.roomName))
return Common::kNoError;
+ debugC(2, kDebugRoom,
+ "Harvester: region interaction room='%s' region='%s' action='%s' player=(%d,%d,z=%.2f) next_room='%s'",
+ scene.state.roomName.c_str(), region.regionName.c_str(), region.actionTag.c_str(),
+ playerState.centerX, playerState.bottomY, (double)playerState.z,
+ interaction.nextRoomName.c_str());
bool didTransition = false;
Common::Error interactionError =
@@ -3473,29 +3478,12 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
stopPlayerRegionInteraction();
return runRegionInteraction(*region);
};
- auto tryActivateHoveredRegion = [&]() -> Common::Error {
- if (!playerState.entity)
- return Common::kNoError;
-
- const RoomHoverState hoverState = resolveRoomHoverState(
- _engine, scene.state, scene.sceneObjects, scene.state.roomNpcs,
- scene.sceneRegions, _mousePos, &flow._dialogue);
- if (!hoverState.region || !hoverState.region->startEnabled)
- return Common::kNoError;
- if (!doesPlayerOverlapRegion(*playerState.entity, *hoverState.region))
- return Common::kNoError;
- if (!doesPlayerFacingMatchRegion(playerState.facing, *hoverState.region))
- return Common::kNoError;
-
- stopPlayerRegionInteraction();
- return runRegionInteraction(*hoverState.region);
- };
auto tryActivatePassiveRegion = [&]() -> Common::Error {
if (!playerState.entity)
return Common::kNoError;
for (const RegionRecord ®ion : scene.sceneRegions) {
- if (!region.startEnabled || region.cursorEnabled)
+ if (!region.startEnabled)
continue;
if (!doesPlayerOverlapRegion(*playerState.entity, region))
continue;
@@ -4316,7 +4304,36 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
stopPlayerRegionInteraction();
}
- bool playerAdvancedThisFrame = false;
+ // Native run_harvester_main_loop checks player/region overlap before
+ // update_actor_runtime_state. This is significant when a wide walk frame
+ // reaches a narrow exit: the actor update may otherwise satisfy the X
+ // target and replace that frame with a narrower idle frame first.
+ Common::Error pendingRegionError =
+ isPlayerCombatLocked()
+ ? Common::kNoError
+ : tryActivatePendingRegion();
+ if (pendingRegionError.getCode() != Common::kNoError)
+ return pendingRegionError;
+ if (flow.hasPendingMainMenuReturn())
+ return Common::kNoError;
+ if (!pendingRoomChange.empty()) {
+ if (!stowCarriedRoomItemToInventory())
+ return Common::kReadingFailed;
+ break;
+ }
+ if (!isPlayerCombatLocked()) {
+ pendingRegionError = tryActivatePassiveRegion();
+ if (pendingRegionError.getCode() != Common::kNoError)
+ return pendingRegionError;
+ if (flow.hasPendingMainMenuReturn())
+ return Common::kNoError;
+ if (!pendingRoomChange.empty()) {
+ if (!stowCarriedRoomItemToInventory())
+ return Common::kReadingFailed;
+ break;
+ }
+ }
+
Common::Error combatError = Common::kNoError;
if (!playerControlPaused) {
combatError = resolvePlayerAttackContact();
@@ -4333,7 +4350,6 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
needsRedraw = true;
if (Player::updateHitAnimationState(
_engine, scene.state, scene.sceneObjects, scene.sceneAnimations, playerState)) {
- playerAdvancedThisFrame = true;
needsRedraw = true;
}
}
@@ -4367,7 +4383,6 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
}
if (playerCanAct && !playerState.attackActive && !playerState.hitActive &&
Player::updateTurnAnimationState(playerState)) {
- playerAdvancedThisFrame = true;
needsRedraw = true;
}
@@ -4376,13 +4391,11 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
!keyboardAttackRequested && !idleState.active && !idleState.exiting) {
if (Player::stepKeyboardMovement(_engine, scene.state, scene.sceneObjects, scene.sceneAnimations,
playerState, moveLeft, moveRight, moveUp, moveDown)) {
- playerAdvancedThisFrame = true;
notePlayerActivity();
needsRedraw = true;
} else if (Player::stepMoveTarget(
_engine, scene.state, scene.sceneObjects, scene.sceneAnimations,
playerState)) {
- playerAdvancedThisFrame = true;
notePlayerActivity();
needsRedraw = true;
} else if (!moveLeft && !moveRight && !moveUp && !moveDown && !playerState.hasMoveTarget &&
@@ -4414,43 +4427,6 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
return Common::kReadingFailed;
break;
}
- Common::Error pendingRegionError =
- isPlayerCombatLocked()
- ? Common::kNoError
- : tryActivatePendingRegion();
- if (pendingRegionError.getCode() != Common::kNoError)
- return pendingRegionError;
- if (flow.hasPendingMainMenuReturn())
- return Common::kNoError;
- if (!pendingRoomChange.empty()) {
- if (!stowCarriedRoomItemToInventory())
- return Common::kReadingFailed;
- break;
- }
- if (playerAdvancedThisFrame && !isPlayerCombatLocked()) {
- pendingRegionError = tryActivatePassiveRegion();
- if (pendingRegionError.getCode() != Common::kNoError)
- return pendingRegionError;
- if (flow.hasPendingMainMenuReturn())
- return Common::kNoError;
- if (!pendingRoomChange.empty()) {
- if (!stowCarriedRoomItemToInventory())
- return Common::kReadingFailed;
- break;
- }
-
- pendingRegionError = tryActivateHoveredRegion();
- if (pendingRegionError.getCode() != Common::kNoError)
- return pendingRegionError;
- if (flow.hasPendingMainMenuReturn())
- return Common::kNoError;
- if (!pendingRoomChange.empty()) {
- if (!stowCarriedRoomItemToInventory())
- return Common::kReadingFailed;
- break;
- }
- }
-
if (flow.tickRuntimeEntities())
needsRedraw = true;
syncAnimatedRoomActorPlacement();
diff --git a/engines/harvester/room_support.h b/engines/harvester/room_support.h
index 4d9733580b1..7c52d9df7f7 100644
--- a/engines/harvester/room_support.h
+++ b/engines/harvester/room_support.h
@@ -64,7 +64,6 @@ struct RoomPlayerState {
int targetX = 0;
int targetBottomY = 0;
float targetZ = 0.0f;
- bool moveTargetRequiresScreenY = false;
bool moveTargetXReached = false;
bool moveTargetZReached = false;
Common::String currentBlockerName;
Commit: da2161c3759f4288eceede68ec8c8de7a099c007
https://github.com/scummvm/scummvm/commit/da2161c3759f4288eceede68ec8c8de7a099c007
Author: Alex Bevilacqua (alex at alexbevi.com)
Date: 2026-08-22T08:36:02-04:00
Commit Message:
HARVESTER: Enforce keyboard walk bounds
Keyboard Up and Down continued changing the actor screen position after room depth reached its scripted limit, allowing the player to cross the retail walk boundary.
update_player_combat_avatar_state at 0x553a0 maps those keys to walk states 0x0b and 0x04. update_actor_runtime_state at 0x4d750 emits the screen-Y and Z steps together only while z is below max_z or above min_z, then zeros both steps and restores the facing idle frame.
Apply the same coupled depth gate to keyboard movement. PCROOM now stops at z=27 when walking up and z=11 when walking down without drifting farther on screen.
Assisted-by: Codex:gpt-5.6-sol
Changed paths:
engines/harvester/player.cpp
diff --git a/engines/harvester/player.cpp b/engines/harvester/player.cpp
index c86bed42a7c..97fc1d5cde3 100644
--- a/engines/harvester/player.cpp
+++ b/engines/harvester/player.cpp
@@ -1817,16 +1817,36 @@ bool Player::stepKeyboardMovement(HarvesterEngine &engine, const RoomSetupState
playerState, playerState.centerX + horizontalInput * horizontalStep);
int candidateBottomY = playerState.bottomY;
float candidateZ = playerState.z;
+ bool depthBoundaryReached = false;
if (verticalInput != 0) {
- candidateBottomY = clampRoomMovementY(state,
- playerState.bottomY + verticalInput * verticalStep);
- // Native room-combat movement advances screen Y and room depth independently:
- // down-walk states apply -z_velocity_step while up-walk states apply +z_velocity_step.
- candidateZ = clampRoomDepth(state, playerState.z - verticalInput * depthStep);
+ // update_actor_runtime_state (0x4d750) only emits the paired screen-Y and Z
+ // steps for state 0x04 while min_z < z and state 0x0b while z < max_z.
+ // At either depth limit it zeros both steps and returns to the facing idle state.
+ depthBoundaryReached = verticalInput < 0
+ ? playerState.z >= (float)state.roomMaxZ
+ : playerState.z <= (float)state.roomMinZ;
+ if (!depthBoundaryReached) {
+ candidateBottomY = clampRoomMovementY(state,
+ playerState.bottomY + verticalInput * verticalStep);
+ candidateZ = clampRoomDepth(state, playerState.z - verticalInput * depthStep);
+ }
}
if (candidateCenterX == playerState.centerX &&
candidateBottomY == playerState.bottomY &&
fabsf(candidateZ - playerState.z) <= kRoomDepthCompareEpsilon) {
+ playerState.hasMoveTarget = false;
+ if (depthBoundaryReached) {
+ const bool stopped = setIdleAnimation(
+ playerState, playerState.facing >= 0 ? playerState.facing : 0);
+ if (stopped) {
+ debugC(3, kDebugPathfinding,
+ "Harvester: player keyboard depth boundary room='%s' input_y=%d pos=(%d,%d,z=%.2f) bounds=(%d,%d)",
+ state.roomName.c_str(), verticalInput,
+ playerState.centerX, playerState.bottomY, (double)playerState.z,
+ state.roomMinZ, state.roomMaxZ);
+ }
+ return stopped;
+ }
return false;
}
Commit: 29f101a95cd7ee1ecbd47a0fc8c613da60579826
https://github.com/scummvm/scummvm/commit/29f101a95cd7ee1ecbd47a0fc8c613da60579826
Author: Alex Bevilacqua (alex at alexbevi.com)
Date: 2026-08-22T08:36:02-04:00
Commit Message:
HARVESTER: Correct monster attack animation rate
Retail update_actor_runtime_state at 0x4d750 forces attack states 0x16 through 0x1b to a three-centisecond frame interval by storing animation rate 0x1e at 0x53525. Use that rate for room monster attack banks instead of rate 4, which made attacks more than eight times slower.
Keep the attack transition at debug level 2 and emit the detailed frame and timing payload at level 3 for retail comparisons.
Assisted-by: Codex:gpt-5.6-sol
Changed paths:
engines/harvester/room.cpp
engines/harvester/room_combat.cpp
diff --git a/engines/harvester/room.cpp b/engines/harvester/room.cpp
index 043e0d27622..cadde58a66e 100644
--- a/engines/harvester/room.cpp
+++ b/engines/harvester/room.cpp
@@ -3212,8 +3212,11 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
entity->setAnimationRate(kNativeMonsterAttackAnimationRate);
entity->setAnimationEnabled(true);
entity->setCurrentFrame(range.firstFrame);
- debugC(1, kDebugCombat,
- "Harvester: combat monster attack start monster='%s' target='%s' frames=%d..%d contact=%d resume_facing=%d live_center_dx=%d z_delta=%.2f engage=%d rate=%d attack_seed_tick=%u close_bypass=%d",
+ debugC(2, kDebugCombat,
+ "Harvester: combat monster attack start monster='%s' target='%s'",
+ monster.monsterName.c_str(), combatState.attackTargetName.c_str());
+ debugC(3, kDebugCombat,
+ "Harvester: combat monster attack timing monster='%s' target='%s' frames=%d..%d contact=%d resume_facing=%d live_center_dx=%d z_delta=%.2f engage=%d rate=%d attack_seed_tick=%u close_bypass=%d",
monster.monsterName.c_str(), combatState.attackTargetName.c_str(), range.firstFrame, range.lastFrame,
combatState.attackContactFrame, range.resumeFacing, liveCenterDx, (double)zDelta,
engageDistance, kNativeMonsterAttackAnimationRate, combatState.attackCooldownSeedTick,
diff --git a/engines/harvester/room_combat.cpp b/engines/harvester/room_combat.cpp
index 87adaa2ac47..f336c2ac3cb 100644
--- a/engines/harvester/room_combat.cpp
+++ b/engines/harvester/room_combat.cpp
@@ -29,7 +29,9 @@
namespace Harvester {
const int kRoomMonsterAnimationRate = 17;
-const int kNativeMonsterAttackAnimationRate = 4;
+// update_actor_runtime_state (0x4d750), states 0x16..0x1b: the block at
+// 0x53525 forces a three-centisecond interval by assigning animation rate 0x1e.
+const int kNativeMonsterAttackAnimationRate = 30;
const int kNativeMonsterHitAnimationRate = 5;
const uint32 kNativeMonsterAttackCooldownBaseTicks = 50;
const float kNativeNpcMonsterZExtent = 5.0f;
Commit: f882fd670ad0397cdc3609725b1bccf56f6a810a
https://github.com/scummvm/scummvm/commit/f882fd670ad0397cdc3609725b1bccf56f6a810a
Author: Alex Bevilacqua (alex at alexbevi.com)
Date: 2026-08-22T08:36:03-04:00
Commit Message:
HARVESTER: Correct player death animation rate
Retail update_actor_runtime_state writes rate 14 at 0x4e576 when hit points reach zero, then death states 0x28 through 0x33 consume the normal rate 17 seeded at 0x4d889 after the first death frame. The reimplementation instead held every frame at rate 4, making the game-over transition visibly slow.
Apply the native entry and running rates and log both intervals at debug level 3 for runtime comparison.
Assisted-by: Codex:gpt-5.6-sol
Changed paths:
engines/harvester/player.cpp
diff --git a/engines/harvester/player.cpp b/engines/harvester/player.cpp
index 97fc1d5cde3..0c435957189 100644
--- a/engines/harvester/player.cpp
+++ b/engines/harvester/player.cpp
@@ -44,7 +44,11 @@ static const int kRoomPlayerWalkAnimationRate = 17;
static const int kRoomPlayerVerticalScreenStep = 1;
static const int kRoomPlayerAttackAnimationRate = kRoomPlayerWalkAnimationRate;
static const int kRoomPlayerHitAnimationRate = 4;
-static const int kRoomPlayerDeathAnimationRate = 4;
+// update_actor_runtime_state (0x4d750) seeds a zero-HP transition at 0x4e576
+// with rate 14, then death states 0x28..0x33 consume the normal rate 17 seeded
+// at 0x4d889 after the first death frame advances.
+static const int kNativePlayerDeathEntryAnimationRate = 14;
+static const int kNativePlayerDeathAnimationRate = kRoomPlayerWalkAnimationRate;
static const int kRoomPlayerMinOpaqueLeftX = 4;
static const int kRoomPlayerMaxOpaqueRightX = 0x27c;
static const float kRoomPlayerAttackUpperYOffset = 144.44f;
@@ -1585,25 +1589,42 @@ bool Player::startDeathAnimation(RoomPlayerState &playerState, int damageType, b
playerState.deathLastFrame = range.lastFrame;
playerState.deathDamageType = damageType;
playerState.entity->setAnimationFrameRange(range.firstFrame, range.lastFrame, false);
- playerState.entity->setAnimationRate(kRoomPlayerDeathAnimationRate);
+ playerState.entity->setAnimationRate(kNativePlayerDeathEntryAnimationRate);
playerState.entity->setCurrentFrame(range.firstFrame);
playerState.entity->setVisible(true);
- debugC(1, kDebugCombat,
+ debugC(2, kDebugCombat,
"Harvester: player death animation start damage_type=%d gore=%d facing=%d frames=%d..%d",
damageType, goreEnabled, playerState.facing, range.firstFrame, range.lastFrame);
+ debugC(3, kDebugCombat,
+ "Harvester: player death animation timing frames=%d..%d entry_rate=%d entry_interval_ticks=%d running_rate=%d running_interval_ticks=%d",
+ range.firstFrame, range.lastFrame,
+ kNativePlayerDeathEntryAnimationRate, 100 / kNativePlayerDeathEntryAnimationRate,
+ kNativePlayerDeathAnimationRate, 100 / kNativePlayerDeathAnimationRate);
return true;
}
bool Player::updateDeathAnimationState(RoomPlayerState &playerState) {
if (!playerState.deathActive || !playerState.entity)
return false;
- if (playerState.entity->getCurrentFrame() < playerState.deathLastFrame)
+
+ const int currentFrame = playerState.entity->getCurrentFrame();
+ if (currentFrame > playerState.deathFirstFrame &&
+ currentFrame < playerState.deathLastFrame &&
+ playerState.entity->getAnimationRate() != kNativePlayerDeathAnimationRate) {
+ playerState.entity->setAnimationRate(kNativePlayerDeathAnimationRate);
+ debugC(3, kDebugCombat,
+ "Harvester: player death animation running frame=%d rate=%d interval_ticks=%d",
+ currentFrame, kNativePlayerDeathAnimationRate,
+ 100 / kNativePlayerDeathAnimationRate);
+ }
+
+ if (currentFrame < playerState.deathLastFrame)
return false;
playerState.deathActive = false;
- debugC(1, kDebugCombat,
+ debugC(2, kDebugCombat,
"Harvester: player death animation complete damage_type=%d frame=%d",
- playerState.deathDamageType, playerState.entity->getCurrentFrame());
+ playerState.deathDamageType, currentFrame);
return true;
}
More information about the Scummvm-git-logs
mailing list