[Scummvm-git-logs] scummvm master -> 17bd61d4132332572859255f526a2c2c2fa40c3c

npjg noreply at scummvm.org
Sun Feb 2 16:52:15 UTC 2025


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

Summary:
73e5a7bbe3 MEDIASTATION: Move to dirty rectangles-based drawing
85e8bfa7a3 MEDIASTATION: Allow stopping sounds once started
b4b4568dec MEDIASTATION: Make movies & sprites render more properly
a2a06746ca MEDIASTATION: Store variable bools internally as ints
17bd61d413 MEDIASTATION: Properly deactivate hotspots when mouse is moved outside any hotspot


Commit: 73e5a7bbe30527f125885173a022e00557825c0b
    https://github.com/scummvm/scummvm/commit/73e5a7bbe30527f125885173a022e00557825c0b
Author: Nathanael Gentry (nathanael.gentrydb8 at gmail.com)
Date: 2025-02-02T11:52:05-05:00

Commit Message:
MEDIASTATION: Move to dirty rectangles-based drawing

Changed paths:
    engines/mediastation/asset.cpp
    engines/mediastation/asset.h
    engines/mediastation/assets/image.cpp
    engines/mediastation/assets/image.h
    engines/mediastation/assets/movie.cpp
    engines/mediastation/assets/movie.h
    engines/mediastation/assets/sprite.cpp
    engines/mediastation/assets/sprite.h
    engines/mediastation/mediastation.cpp
    engines/mediastation/mediastation.h


diff --git a/engines/mediastation/asset.cpp b/engines/mediastation/asset.cpp
index f28e39d94c2..9254eadab1e 100644
--- a/engines/mediastation/asset.cpp
+++ b/engines/mediastation/asset.cpp
@@ -45,6 +45,11 @@ int Asset::zIndex() const {
 	return _header->_zIndex;
 }
 
+Common::Rect *Asset::getBbox() {
+	return _header->_boundingBox;
+}
+
+
 void Asset::processTimeEventHandlers() {
 	if (!_isActive) {
 		warning("Asset::processTimeEventHandlers(): Attempted to process time event handlers while asset %d is not playing", _header->_id);
diff --git a/engines/mediastation/asset.h b/engines/mediastation/asset.h
index f746618d666..36a4380b360 100644
--- a/engines/mediastation/asset.h
+++ b/engines/mediastation/asset.h
@@ -44,6 +44,12 @@ public:
 	virtual void process() {
 		return;
 	}
+
+	// For spatial assets, actually redraws the dirty area.
+	virtual void redraw(Common::Rect &rect) {
+		return;
+	}
+
 	// Runs built-in bytecode methods.
 	virtual Operand callMethod(BuiltInMethod methodId, Common::Array<Operand> &args) = 0;
 	// Called to have the asset do any processing, like drawing new frames,
@@ -67,6 +73,7 @@ public:
 	AssetHeader *getHeader() const {
 		return _header;
 	}
+	Common::Rect *getBbox();
 
 protected:
 	AssetHeader *_header = nullptr;
diff --git a/engines/mediastation/assets/image.cpp b/engines/mediastation/assets/image.cpp
index b0d16c4a272..352fb35a768 100644
--- a/engines/mediastation/assets/image.cpp
+++ b/engines/mediastation/assets/image.cpp
@@ -40,15 +40,14 @@ Operand Image::callMethod(BuiltInMethod methodId, Common::Array<Operand> &args)
 	switch (methodId) {
 	case kSpatialShowMethod: {
 		assert(args.empty());
-		_isActive = true;
-		g_engine->addPlayingAsset(this);
+		spatialShow();
 		return Operand();
 		break;
 	}
 
 	case kSpatialHideMethod: {
 		assert(args.empty());
-		_isActive = false;
+		spatialHide();
 		return Operand();
 		break;
 	}
@@ -66,6 +65,38 @@ Operand Image::callMethod(BuiltInMethod methodId, Common::Array<Operand> &args)
 	}
 }
 
+void Image::redraw(Common::Rect &rect) {
+	if (!_isActive) {
+		return;
+	}
+
+	Common::Point leftTop = getLeftTop();
+	Common::Rect bbox(leftTop, _bitmap->width(), _bitmap->height());
+	Common::Rect areaToRedraw = bbox.findIntersectingRect(rect);
+	if (!areaToRedraw.isEmpty()) {
+		Common::Point originOnScreen(areaToRedraw.left, areaToRedraw.top);
+		areaToRedraw.translate(-leftTop.x, -leftTop.y);
+		g_engine->_screen->simpleBlitFrom(_bitmap->_surface, areaToRedraw, originOnScreen);
+	}
+}
+
+void Image::spatialShow() {
+	_isActive = true;
+	g_engine->addPlayingAsset(this);
+	Common::Rect bbox(getLeftTop(), _bitmap->width(), _bitmap->height());
+	g_engine->_dirtyRects.push_back(bbox);
+}
+
+void Image::spatialHide() {
+	_isActive = false;
+	Common::Rect bbox(getLeftTop(), _bitmap->width(), _bitmap->height());
+	g_engine->_dirtyRects.push_back(bbox);
+}
+
+Common::Point Image::getLeftTop() {
+	return Common::Point(_header->_x + _header->_boundingBox->left, _header->_y + _header->_boundingBox->top);
+}
+
 void Image::readChunk(Chunk &chunk) {
 	BitmapHeader *bitmapHeader = new BitmapHeader(chunk);
 	_bitmap = new Bitmap(chunk, bitmapHeader);
diff --git a/engines/mediastation/assets/image.h b/engines/mediastation/assets/image.h
index a950e207b88..ddcc1966966 100644
--- a/engines/mediastation/assets/image.h
+++ b/engines/mediastation/assets/image.h
@@ -38,10 +38,18 @@ public:
 
 	virtual void readChunk(Chunk &chunk) override;
 
+	virtual void redraw(Common::Rect &rect) override;
+
 	virtual Operand callMethod(BuiltInMethod methodId, Common::Array<Operand> &args) override;
 
 private:
 	Bitmap *_bitmap = nullptr;
+
+	// Script method implementations.
+	void spatialShow();
+	void spatialHide();
+
+	Common::Point getLeftTop();
 };
 
 } // End of namespace MediaStation
diff --git a/engines/mediastation/assets/movie.cpp b/engines/mediastation/assets/movie.cpp
index 2da51c9964c..47ca95e944b 100644
--- a/engines/mediastation/assets/movie.cpp
+++ b/engines/mediastation/assets/movie.cpp
@@ -215,23 +215,15 @@ void Movie::timePlay() {
 	// TODO: Play movies one chunk at a time, which more directly approximates
 	// the original's reading from the CD one chunk at a time.
 	if (_isActive) {
-		error("Movie::play(): Attempted to play a movie that is already playing");
+		warning("Movie::timePlay(): Attempted to play a movie that is already playing");
 		return;
 	}
 
-	// SET ANIMATION VARIABLES.
 	_isActive = true;
 	_startTime = g_system->getMillis();
 	_lastProcessedTime = 0;
 	g_engine->addPlayingAsset(this);
-
-	// GET THE DURATION OF THE MOVIE.
-	_duration = 0;
-	for (MovieFrame *frame : _frames) {
-		if (frame->endInMilliseconds() > _duration) {
-			_duration = frame->endInMilliseconds();
-		}
-	}
+	_framesNotYetShown = _frames;
 
 	// START PLAYING SOUND.
 	// TODO: This won't work when we have some chunks that don't have audio.
@@ -249,59 +241,103 @@ void Movie::timePlay() {
 }
 
 void Movie::timeStop() {
-	// RESET ANIMATION VARIABLES.
+	if (!_isActive) {
+		warning("Movie::timePlay(): Attempted to stop a movie that isn't playing");
+		return;
+	}
 	_isActive = false;
 	_startTime = 0;
 	_lastProcessedTime = 0;
-
+	_framesNotYetShown.clear();
 	runEventHandlerIfExists(kMovieStoppedEvent);
 }
 
 void Movie::process() {
 	processTimeEventHandlers();
-	drawNextFrame();
+	updateFrameState();
 }
 
-bool Movie::drawNextFrame() {
+void Movie::updateFrameState() {
 	// TODO: We'll need to support persistent frames in movies too. Do movies
 	// have the same distinction between spatialShow and timePlay that sprites
 	// do?
 
 	uint currentTime = g_system->getMillis();
 	uint movieTime = currentTime - _startTime;
-	debugC(5, kDebugGraphics, "GRAPHICS (Movie %d): Starting blitting (movie time: %d)", _header->_id, movieTime);
-	bool donePlaying = movieTime > _duration;
-	if (donePlaying) {
+	debugC(5, kDebugGraphics, "Movie::updateFrameState (%d): Starting update (movie time: %d)", _header->_id, movieTime);
+	if (_framesNotYetShown.empty()) {
 		_isActive = false;
 		_startTime = 0;
 		_lastProcessedTime = 0;
-
 		runEventHandlerIfExists(kMovieEndEvent);
-		return false;
+		return;
+	}
+	
+	// This complexity is necessary becuase movies can have more than one frame
+	// showing at the same time - for instance, a movie background and an
+	// animation on that background are a part of the saem movie and are on
+	// screen at the same time, it's just the starting and ending times of one
+	// can be different from the starting and ending times of another.
+	//
+	// We can rely on the frames being ordered in order of their start. First,
+	// see if there are any new frames to show.
+	for (auto it = _framesNotYetShown.begin(); it != _framesNotYetShown.end();) {
+		MovieFrame *frame = *it;
+		bool isAfterStart = movieTime >= frame->startInMilliseconds();
+		if (isAfterStart) {
+			_framesOnScreen.push_back(frame);
+			g_engine->_dirtyRects.push_back(getFrameBoundingBox(frame));
+
+			// We don't need ++it because we will either have another frame
+			// that needs to be drawn, or we have reached the end of the new
+			// frames.
+			it = _framesNotYetShown.erase(it);
+		} else {
+			// We've hit a frame that shouldn't yet be shown.
+			// Rely on the ordering to not bother with any further frames.
+			break;
+		}
 	}
 
-	Common::Array<MovieFrame *> framesToDraw;
-	for (MovieFrame *frame : _frames) {
-		bool isAfterStart = _startTime + frame->startInMilliseconds() <= currentTime;
-		bool isBeforeEnd = _startTime + frame->endInMilliseconds() >= currentTime;
-		if (!isAfterStart || (isAfterStart && !isBeforeEnd)) {
-			continue;
+	// Now see if there are any old frames that no longer need to be shown.
+	for (auto it = _framesOnScreen.begin(); it != _framesOnScreen.end();) {
+		MovieFrame *frame = *it;
+		bool isAfterEnd = movieTime >= frame->endInMilliseconds();
+		if (isAfterEnd) {
+			g_engine->_dirtyRects.push_back(getFrameBoundingBox(frame));
+			it = _framesOnScreen.erase(it);
+		} else {
+			++it;
 		}
-		debugC(5, kDebugGraphics, "    (time: %d ms) Must re-draw frame %d (%d x %d) @ (%d, %d); start: %d ms, end: %d ms, keyframeEnd: %d ms, zIndex = %d", movieTime, frame->index(), frame->width(), frame->height(), frame->left(), frame->top(), frame->startInMilliseconds(), frame->endInMilliseconds(), frame->keyframeEndInMilliseconds(), frame->zCoordinate());
-		framesToDraw.push_back(frame);
 	}
 
-	Common::sort(framesToDraw.begin(), framesToDraw.end(), [](MovieFrame * a, MovieFrame * b) {
+	// Show the frames that are currently active, for debugging purposes.
+	for (MovieFrame *frame : _framesOnScreen) {
+		debugC(5, kDebugGraphics, "   (time: %d ms) Frame %d (%d x %d) @ (%d, %d); start: %d ms, end: %d ms, keyframeEnd: %d ms, zIndex = %d", movieTime, frame->index(), frame->width(), frame->height(), frame->left(), frame->top(), frame->startInMilliseconds(), frame->endInMilliseconds(), frame->keyframeEndInMilliseconds(), frame->zCoordinate());
+	}
+}
+
+void Movie::redraw(Common::Rect &rect) {
+	// Make sure the frames are ordered properly before we attempt to draw them.
+	Common::sort(_framesOnScreen.begin(), _framesOnScreen.end(), [](MovieFrame * a, MovieFrame * b) {
 		return a->zCoordinate() > b->zCoordinate();
 	});
-	for (MovieFrame *frame : framesToDraw) {
-		g_engine->_screen->simpleBlitFrom(frame->_surface, Common::Point(frame->left(), frame->top()));
+
+	for (MovieFrame *frame : _framesOnScreen) {
+		Common::Rect bbox = getFrameBoundingBox(frame);
+		Common::Rect areaToRedraw = bbox.findIntersectingRect(rect);
+		if (!areaToRedraw.isEmpty()) {
+			Common::Point originOnScreen(areaToRedraw.left, areaToRedraw.top);
+			areaToRedraw.translate(-frame->left() - _header->_boundingBox->left, -frame->top() - _header->_boundingBox->top);
+			g_engine->_screen->simpleBlitFrom(frame->_surface, areaToRedraw, originOnScreen);
+		}
 	}
+}
 
-	uint blitEnd = g_system->getMillis() - _startTime;
-	uint elapsedTime = blitEnd - movieTime;
-	debugC(5, kDebugGraphics, "GRAPHICS (Movie %d): Finished blitting in %d ms (movie time: %d ms)", _header->_id, elapsedTime, blitEnd);
-	return true;
+Common::Rect Movie::getFrameBoundingBox(MovieFrame *frame) {
+	Common::Rect bbox = frame->boundingBox();
+	bbox.translate(_header->_boundingBox->left, _header->_boundingBox->top);
+	return bbox;
 }
 
 void Movie::readChunk(Chunk &chunk) {
diff --git a/engines/mediastation/assets/movie.h b/engines/mediastation/assets/movie.h
index f81f40b44c2..0e7c435f1d2 100644
--- a/engines/mediastation/assets/movie.h
+++ b/engines/mediastation/assets/movie.h
@@ -103,18 +103,24 @@ public:
 	virtual Operand callMethod(BuiltInMethod methodId, Common::Array<Operand> &args) override;
 	virtual void process() override;
 
+	virtual void redraw(Common::Rect &rect) override;
+
 private:
 	Common::Array<MovieFrame *> _frames;
 	Common::Array<MovieFrame *> _stills;
 	Common::Array<MovieFrameFooter *> _footers;
 	Common::Array<Audio::SeekableAudioStream *> _audioStreams;
 
+	Common::Array<MovieFrame *> _framesNotYetShown;
+	Common::Array<MovieFrame *> _framesOnScreen;
+
 	// Method implementations. These should be called from callMethod.
 	void timePlay();
 	void timeStop();
 
-	// Internal helper functions.
-	bool drawNextFrame();
+	void updateFrameState();
+
+	Common::Rect getFrameBoundingBox(MovieFrame *frame);
 };
 
 } // End of namespace MediaStation
diff --git a/engines/mediastation/assets/sprite.cpp b/engines/mediastation/assets/sprite.cpp
index f3aae059dae..350e96f4088 100644
--- a/engines/mediastation/assets/sprite.cpp
+++ b/engines/mediastation/assets/sprite.cpp
@@ -122,21 +122,15 @@ void Sprite::spatialShow() {
 	_isActive = true;
 	g_engine->addPlayingAsset(this);
 
-	// Persist the first frame.
-	// TODO: Is there anything that says what the persisted frame should be?
-	SpriteFrame *firstFrame = _frames[0];
-	for (SpriteFrame *frame : _frames) {
-		if (frame->index() < firstFrame->index()) {
-			firstFrame = frame;
-		}
+	_isPaused = true;
+	if (_activeFrame == nullptr) {
+		showFrame(0);
 	}
-	_persistFrame = firstFrame;
 }
 
 void Sprite::timePlay() {
-	debugC(5, kDebugScript, "Called Sprite::timePlay");
 	_isActive = true;
-	_persistFrame = nullptr;
+	_isPaused = false;
 	_startTime = g_system->getMillis();
 	_lastProcessedTime = 0;
 	_nextFrameTime = 0;
@@ -152,9 +146,9 @@ void Sprite::timePlay() {
 }
 
 void Sprite::movieReset() {
-	debugC(5, kDebugScript, "Called Sprite::movieReset");
 	_isActive = true;
 	// We do NOT reset the persisting frame, because it should keep showing!
+	_isPaused = true;
 	_startTime = 0;
 	_currentFrameIndex = 0;
 	_nextFrameTime = 0;
@@ -162,10 +156,7 @@ void Sprite::movieReset() {
 }
 
 void Sprite::process() {
-	drawNextFrame();
-
-	// TODO: I don't think sprites support time-based event handlers. Because we
-	// have a separate timer for restarting the sprite when it expires.
+	updateFrameState();
 }
 
 void Sprite::readChunk(Chunk &chunk) {
@@ -182,42 +173,31 @@ void Sprite::readChunk(Chunk &chunk) {
 	});
 }
 
-void Sprite::drawNextFrame() {
-	// TODO: With a dirty rect-based system, we would only need to redraw the frame
-	// when it NEEDS to be redrawn. But since the whole screen is currently redrawn
-	// every time, the persisting frame needs to be redrawn too.
-	bool redrawPersistentFrame = _persistFrame != nullptr;
-	if (redrawPersistentFrame) {
-		debugC(5, kDebugGraphics, "GRAPHICS (Sprite %d): Drawing persistent frame %d", _header->_id, _persistFrame->index());
-		drawFrame(_persistFrame);
+void Sprite::updateFrameState() {
+	if (_isPaused) {
 		return;
 	}
 
 	uint currentTime = g_system->getMillis() - _startTime;
-	bool redrawCurrentFrame = currentTime <= _nextFrameTime;
-	if (redrawCurrentFrame) {
-		// Just redraw the current frame in case it was covered over.
-		// See TODO above.
-		SpriteFrame *currentFrame = _frames[_currentFrameIndex];
-		debugC(5, kDebugGraphics, "GRAPHICS (Sprite %d): Re-drawing current frame %d", _header->_id, currentFrame->index());
-		drawFrame(currentFrame);
+	bool drawNextFrame = currentTime >= _nextFrameTime;
+	if (!drawNextFrame) {
 		return;
 	}
 
-	SpriteFrame *nextFrame = _frames[_currentFrameIndex];
-	debugC(5, kDebugGraphics, "GRAPHICS (Sprite %d): Drawing next frame %d (@%d)", _header->_id, nextFrame->index(), _nextFrameTime);
+	showFrame(_currentFrameIndex);
+
 	uint frameDuration = 1000 / _header->_frameRate;
-	_nextFrameTime = _currentFrameIndex * frameDuration;
-	drawFrame(nextFrame);
+	_nextFrameTime = ++_currentFrameIndex * frameDuration;
 
-	bool spriteFinishedPlaying = (++_currentFrameIndex == _frames.size());
+	bool spriteFinishedPlaying = (_currentFrameIndex == _frames.size());
 	if (spriteFinishedPlaying) {
 		// Sprites always keep their last frame showing until they are hidden
 		// with spatialHide.
-		_persistFrame = _frames[_currentFrameIndex - 1];
-		_isActive = true;
+		showFrame(_currentFrameIndex - 1);
+		_isPaused = true;
 
 		// But otherwise, the sprite's params should be reset.
+		_isActive = true;
 		_startTime = 0;
 		_lastProcessedTime = 0;
 		_currentFrameIndex = 0;
@@ -227,11 +207,37 @@ void Sprite::drawNextFrame() {
 	}
 }
 
-void Sprite::drawFrame(SpriteFrame *frame) {
-	uint frameLeft = frame->left() + _header->_boundingBox->left;
-	uint frameTop = frame->top() + _header->_boundingBox->top;
-	debugC(5, kDebugGraphics, "    Sprite frame %d (%d x %d) @ (%d, %d)", frame->index(), frame->width(), frame->height(), frameLeft, frameTop);
-	g_engine->_screen->simpleBlitFrom(frame->_surface, Common::Point(frameLeft, frameTop));
+void Sprite::redraw(Common::Rect &rect) {
+	if (_activeFrame == nullptr) {
+		return;
+	}
+
+	Common::Rect bbox = getActiveFrameBoundingBox();
+	Common::Rect areaToRedraw = bbox.findIntersectingRect(rect);
+	if (!areaToRedraw.isEmpty()) {
+		Common::Point originOnScreen(areaToRedraw.left, areaToRedraw.top);
+		areaToRedraw.translate(-_activeFrame->left() - _header->_boundingBox->left, -_activeFrame->top() - _header->_boundingBox->top);
+		g_engine->_screen->simpleBlitFrom(_activeFrame->_surface, areaToRedraw, originOnScreen);
+	}
+}
+
+void Sprite::showFrame(uint frameIndex) {
+	// Erase the previous frame.
+	if (_activeFrame != nullptr) {
+		g_engine->_dirtyRects.push_back(getActiveFrameBoundingBox());
+	}
+	
+	// Show the next frame.
+	_activeFrame = _frames[frameIndex];
+	g_engine->_dirtyRects.push_back(getActiveFrameBoundingBox());
+}
+
+Common::Rect Sprite::getActiveFrameBoundingBox() {
+	// The frame dimensions are relative to those of the sprite movie.
+	// So we must get the absolute coordinates.
+	Common::Rect bbox = _activeFrame->boundingBox();
+	bbox.translate(_header->_boundingBox->left, _header->_boundingBox->top);
+	return bbox;
 }
 
 } // End of namespace MediaStation
diff --git a/engines/mediastation/assets/sprite.h b/engines/mediastation/assets/sprite.h
index c1a3114fffb..32f9bf9e7fa 100644
--- a/engines/mediastation/assets/sprite.h
+++ b/engines/mediastation/assets/sprite.h
@@ -65,12 +65,14 @@ public:
 
 	virtual Operand callMethod(BuiltInMethod methodId, Common::Array<Operand> &args) override;
 	virtual void process() override;
+	virtual void redraw(Common::Rect &rect) override;
 
 	virtual void readChunk(Chunk &chunk) override;
 
 private:
 	Common::Array<SpriteFrame *> _frames;
-	SpriteFrame *_persistFrame = nullptr;
+	SpriteFrame *_activeFrame = nullptr;
+	bool _isPaused = false;
 	uint _currentFrameIndex = 0;
 	uint _nextFrameTime = 0;
 
@@ -79,9 +81,9 @@ private:
 	void timePlay();
 	void movieReset();
 
-	// Helper functions.
-	void drawNextFrame();
-	void drawFrame(SpriteFrame *frame);
+	void updateFrameState();
+	void showFrame(uint frameIndex);
+	Common::Rect getActiveFrameBoundingBox();
 };
 
 } // End of namespace MediaStation
diff --git a/engines/mediastation/mediastation.cpp b/engines/mediastation/mediastation.cpp
index 966cb1b1756..62ee2e2c386 100644
--- a/engines/mediastation/mediastation.cpp
+++ b/engines/mediastation/mediastation.cpp
@@ -145,17 +145,7 @@ Common::Error MediaStationEngine::run() {
 			break;
 		}
 
-		// PROCESS ANY ASSETS CURRENTLY PLAYING.
-		// TODO: Implement a dirty-rect based rendering system rather than
-		// redrawing the screen each time. This will require keeping track of
-		// all the images on screen at any given time, rather than just letting
-		// the movies handle their own drawing.
-		//
-		// First, they all need to be sorted by z-coordinate.
-		debugC(5, kDebugGraphics, "***** START RENDERING ***");
-		Common::sort(_assetsPlaying.begin(), _assetsPlaying.end(), [](Asset * a, Asset * b) {
-			return a->zIndex() > b->zIndex();
-		});
+		debugC(5, kDebugGraphics, "***** START SCREEN UPDATE ***");
 		for (auto it = _assetsPlaying.begin(); it != _assetsPlaying.end();) {
 			(*it)->process();
 			if (!(*it)->isActive()) {
@@ -164,10 +154,10 @@ Common::Error MediaStationEngine::run() {
 				++it;
 			}
 		}
-		debugC(5, kDebugGraphics, "***** END RENDERING ***");
+		redraw();
+		debugC(5, kDebugGraphics, "***** END SCREEN UPDATE ***");
 
-		// UPDATE THE SCREEN.
-		g_engine->_screen->update();
+		_screen->update();
 		g_system->delayMillis(10);
 	}
 
@@ -244,6 +234,30 @@ void MediaStationEngine::processEvents() {
 	}
 }
 
+void MediaStationEngine::redraw() {
+	if (_dirtyRects.empty()) {
+		return;
+	}
+
+	Common::sort(_assetsPlaying.begin(), _assetsPlaying.end(), [](Asset * a, Asset * b) {
+		return a->zIndex() > b->zIndex();
+	});
+
+	for (Common::Rect dirtyRect : _dirtyRects) {
+		for (Asset *asset : _assetsPlaying) {
+			Common::Rect *bbox = asset->getBbox();
+			if (bbox != nullptr) {
+				if (dirtyRect.intersects(*bbox)) {
+					asset->redraw(dirtyRect);
+				}
+			}
+		}
+	}
+
+	_screen->update();
+	_dirtyRects.clear();
+}
+
 Context *MediaStationEngine::loadContext(uint32 contextId) {
 	if (_boot == nullptr) {
 		error("Cannot load contexts before BOOT.STM is read");
@@ -360,6 +374,7 @@ void MediaStationEngine::branchToScreen(uint32 contextId) {
 
 	Context *context = loadContext(contextId);
 	_currentContext = context;
+	_dirtyRects.push_back(Common::Rect(SCREEN_WIDTH, SCREEN_HEIGHT));
 
 	if (context->_screenAsset != nullptr) {
 		// TODO: Make the screen an asset just like everything else so we can
diff --git a/engines/mediastation/mediastation.h b/engines/mediastation/mediastation.h
index c4a1ae2abc0..d06d72101a3 100644
--- a/engines/mediastation/mediastation.h
+++ b/engines/mediastation/mediastation.h
@@ -72,6 +72,7 @@ public:
 	};
 	bool isFirstGenerationEngine();
 	void processEvents();
+	void redraw();
 
 	void setPalette(Asset *palette);
 	void addPlayingAsset(Asset *assetToAdd);
@@ -88,6 +89,8 @@ public:
 	Audio::Mixer *_mixer = nullptr;
 	Context *_currentContext = nullptr;
 
+	Common::Array<Common::Rect> _dirtyRects;
+
 	// All Media Station titles run at 640x480.
 	const uint16 SCREEN_WIDTH = 640;
 	const uint16 SCREEN_HEIGHT = 480;


Commit: 85e8bfa7a3a8e6af7aa1e08962ae00aa32f25922
    https://github.com/scummvm/scummvm/commit/85e8bfa7a3a8e6af7aa1e08962ae00aa32f25922
Author: Nathanael Gentry (nathanael.gentrydb8 at gmail.com)
Date: 2025-02-02T11:52:05-05:00

Commit Message:
MEDIASTATION: Allow stopping sounds once started

Changed paths:
    engines/mediastation/assets/movie.cpp
    engines/mediastation/assets/movie.h
    engines/mediastation/assets/sound.cpp
    engines/mediastation/mediastation.cpp
    engines/mediastation/mediastation.h


diff --git a/engines/mediastation/assets/movie.cpp b/engines/mediastation/assets/movie.cpp
index 47ca95e944b..8791f749139 100644
--- a/engines/mediastation/assets/movie.cpp
+++ b/engines/mediastation/assets/movie.cpp
@@ -157,6 +157,8 @@ MovieFrame::~MovieFrame() {
 }
 
 Movie::~Movie() {
+	g_engine->_mixer->stopHandle(_soundHandle);
+
 	for (MovieFrame *frame : _frames) {
 		delete frame;
 	}
@@ -230,11 +232,12 @@ void Movie::timePlay() {
 	if (!_audioStreams.empty()) {
 		Audio::QueuingAudioStream *audio = Audio::makeQueuingAudioStream(22050, false);
 		for (Audio::SeekableAudioStream *stream : _audioStreams) {
+			stream->rewind();
 			audio->queueAudioStream(stream, DisposeAfterUse::NO);
 		}
 		// Then play the audio!
-		Audio::SoundHandle handle;
-		g_engine->_mixer->playStream(Audio::Mixer::kPlainSoundType, &handle, audio, -1, Audio::Mixer::kMaxChannelVolume);
+		g_engine->_mixer->playStream(Audio::Mixer::kPlainSoundType, &_soundHandle, audio, -1, Audio::Mixer::kMaxChannelVolume);
+		audio->finish();
 	}
 
 	runEventHandlerIfExists(kMovieBeginEvent);
@@ -249,6 +252,10 @@ void Movie::timeStop() {
 	_startTime = 0;
 	_lastProcessedTime = 0;
 	_framesNotYetShown.clear();
+	
+	g_engine->_mixer->stopHandle(_soundHandle);
+	_soundHandle = Audio::SoundHandle();
+
 	runEventHandlerIfExists(kMovieStoppedEvent);
 }
 
@@ -425,7 +432,7 @@ void Movie::readSubfile(Subfile &subfile, Chunk &chunk) {
 
 		// READ THE AUDIO.
 		debugC(5, kDebugLoading, "Movie::readSubfile(): (Frameset %d of %d) Reading audio chunk... (@0x%llx)", i, chunkCount, static_cast<long long int>(chunk.pos()));
-		bool isAudioChunk = (chunk._id = _header->_audioChunkReference);
+		bool isAudioChunk = (chunk._id == _header->_audioChunkReference);
 		if (isAudioChunk) {
 			byte *buffer = (byte *)malloc(chunk._length);
 			chunk.read((void *)buffer, chunk._length);
@@ -436,12 +443,7 @@ void Movie::readSubfile(Subfile &subfile, Chunk &chunk) {
 				break;
 
 			case SoundEncoding::IMA_ADPCM_S16LE_MONO_22050:
-				// TODO: The interface here is different. We can't pass in the
-				// buffers directly. We have to make a stream first.
-				// stream = Audio::makeADPCMStream(buffer, chunk.length,
-				// DisposeAfterUse::NO, Audio::ADPCMType::kADPCMMSIma, 22050, 1,
-				// 4);
-				error("Movie::readSubfile(): ADPCM decoding not implemented yet");
+				error("Movie::readSubfile(): ADPCM decoding not supported for movies");
 				break;
 
 			default:
diff --git a/engines/mediastation/assets/movie.h b/engines/mediastation/assets/movie.h
index 0e7c435f1d2..acb8cac62e7 100644
--- a/engines/mediastation/assets/movie.h
+++ b/engines/mediastation/assets/movie.h
@@ -110,6 +110,7 @@ private:
 	Common::Array<MovieFrame *> _stills;
 	Common::Array<MovieFrameFooter *> _footers;
 	Common::Array<Audio::SeekableAudioStream *> _audioStreams;
+	Audio::SoundHandle _soundHandle;
 
 	Common::Array<MovieFrame *> _framesNotYetShown;
 	Common::Array<MovieFrame *> _framesOnScreen;
diff --git a/engines/mediastation/assets/sound.cpp b/engines/mediastation/assets/sound.cpp
index f7170b01b82..ae42081897d 100644
--- a/engines/mediastation/assets/sound.cpp
+++ b/engines/mediastation/assets/sound.cpp
@@ -35,6 +35,8 @@ Sound::Sound(AssetHeader *header) : Asset(header) {
 }
 
 Sound::~Sound() {
+	g_engine->_mixer->stopHandle(_handle);
+
 	for (Audio::SeekableAudioStream *stream : _streams) {
 		delete stream;
 	}
diff --git a/engines/mediastation/mediastation.cpp b/engines/mediastation/mediastation.cpp
index 62ee2e2c386..0c21021fac1 100644
--- a/engines/mediastation/mediastation.cpp
+++ b/engines/mediastation/mediastation.cpp
@@ -51,8 +51,6 @@ MediaStationEngine::MediaStationEngine(OSystem *syst, const ADGameDescription *g
 }
 
 MediaStationEngine::~MediaStationEngine() {
-	_mixer = nullptr;
-
 	delete _screen;
 	_screen = nullptr;
 
diff --git a/engines/mediastation/mediastation.h b/engines/mediastation/mediastation.h
index d06d72101a3..991908c37e9 100644
--- a/engines/mediastation/mediastation.h
+++ b/engines/mediastation/mediastation.h
@@ -86,7 +86,6 @@ public:
 	Common::HashMap<uint32, Variable *> _variables;
 
 	Graphics::Screen *_screen = nullptr;
-	Audio::Mixer *_mixer = nullptr;
 	Context *_currentContext = nullptr;
 
 	Common::Array<Common::Rect> _dirtyRects;


Commit: b4b4568dec96f42d5e26973aa4d04894115df1a9
    https://github.com/scummvm/scummvm/commit/b4b4568dec96f42d5e26973aa4d04894115df1a9
Author: Nathanael Gentry (nathanael.gentrydb8 at gmail.com)
Date: 2025-02-02T11:52:05-05:00

Commit Message:
MEDIASTATION: Make movies & sprites render more properly

Changed paths:
    engines/mediastation/asset.cpp
    engines/mediastation/asset.h
    engines/mediastation/assets/movie.cpp
    engines/mediastation/assets/movie.h
    engines/mediastation/assets/sprite.cpp
    engines/mediastation/assets/sprite.h
    engines/mediastation/mediascript/scriptconstants.cpp
    engines/mediastation/mediascript/scriptconstants.h


diff --git a/engines/mediastation/asset.cpp b/engines/mediastation/asset.cpp
index 9254eadab1e..74f2670a2a4 100644
--- a/engines/mediastation/asset.cpp
+++ b/engines/mediastation/asset.cpp
@@ -49,6 +49,18 @@ Common::Rect *Asset::getBbox() {
 	return _header->_boundingBox;
 }
 
+void Asset::setActive() {
+	_isActive = true;
+	_startTime = g_system->getMillis();
+	_lastProcessedTime = 0;
+	g_engine->addPlayingAsset(this);
+}
+
+void Asset::setInactive() {
+	_isActive = false;
+	_startTime = 0;
+	_lastProcessedTime = 0;
+}
 
 void Asset::processTimeEventHandlers() {
 	if (!_isActive) {
diff --git a/engines/mediastation/asset.h b/engines/mediastation/asset.h
index 36a4380b360..db318ceeba1 100644
--- a/engines/mediastation/asset.h
+++ b/engines/mediastation/asset.h
@@ -64,6 +64,8 @@ public:
 	virtual void readChunk(Chunk &chunk);
 	virtual void readSubfile(Subfile &subfile, Chunk &chunk);
 
+	void setInactive();
+	void setActive();
 	void processTimeEventHandlers();
 	void runEventHandlerIfExists(EventType eventType);
 	void runKeyDownEventHandlerIfExists(Common::KeyState keyState);
diff --git a/engines/mediastation/assets/movie.cpp b/engines/mediastation/assets/movie.cpp
index 8791f749139..5e0443437d6 100644
--- a/engines/mediastation/assets/movie.cpp
+++ b/engines/mediastation/assets/movie.cpp
@@ -73,8 +73,7 @@ MovieFrameFooter::MovieFrameFooter(Chunk &chunk) {
 
 MovieFrame::MovieFrame(Chunk &chunk, MovieFrameHeader *header) :
 	Bitmap(chunk, header),
-	_footer(nullptr),
-	_showing(false) {
+	_footer(nullptr) {
 	_bitmapHeader = header;
 }
 
@@ -156,6 +155,13 @@ MovieFrame::~MovieFrame() {
 	_footer = nullptr;
 }
 
+Movie::Movie(AssetHeader *header) : Asset(header) {
+	if (header->_startup == kAssetStartupActive) {
+		setActive();
+		_showByDefault = true;
+	}
+}
+
 Movie::~Movie() {
 	g_engine->_mixer->stopHandle(_soundHandle);
 
@@ -190,7 +196,7 @@ Operand Movie::callMethod(BuiltInMethod methodId, Common::Array<Operand> &args)
 
 	case kSpatialShowMethod: {
 		assert(args.empty());
-		warning("Movie::callMethod(): spatialShow not implemented");
+		spatialShow();
 		return Operand();
 	}
 
@@ -202,31 +208,77 @@ Operand Movie::callMethod(BuiltInMethod methodId, Common::Array<Operand> &args)
 
 	case kSpatialHideMethod: {
 		assert(args.empty());
-		warning("Movie::callMethod(): spatialHide not implemented");
+		spatialHide();
 		return Operand();
 	}
 
+	case kIsPlayingMethod: {
+		assert(args.empty());
+		Operand returnValue(kOperandTypeLiteral1);
+		returnValue.putInteger(_isPlaying);
+		return returnValue;
+	}
+
 	default: {
-		error("Got unimplemented method ID %d", methodId);
+		error("Movie::callMethod(): Got unimplemented method ID %d", methodId);
+	}
+	}
+}
+
+void Movie::spatialShow() {
+	if (_isPlaying) {
+		warning("Movie::spatialShow(): (%d) Attempted to spatialShow movie that is already showing", _header->_id);
+		return;
+	} else if (_isShowing) {
+		warning("Movie::spatialShow(): (%d) Attempted to spatialShow movie that is already showing", _header->_id);
+		return;
+	} else if (_stills.empty()) {
+		warning("Movie::spatialShow(): (%d) No still frame to show", _header->_id);
+		return;
 	}
+
+	// TODO: For movies with keyframes, there is more than one still and they
+	// must be composited. All other movies should have just one still.)
+	_framesNotYetShown.clear();
+	_framesOnScreen.clear();
+	for (MovieFrame *still : _stills) {
+		_framesOnScreen.push_back(still);
+		g_engine->_dirtyRects.push_back(getFrameBoundingBox(still));
 	}
+
+	setActive();
+	_isShowing = true;
+	_isPlaying = false;
+}
+
+void Movie::spatialHide() {
+	if (_isPlaying) {
+		warning("Movie::spatialShow(): (%d) Attempted to spatialHide movie that is playing", _header->_id);
+		return;
+	} else if (!_isShowing) {
+		warning("Movie::spatialHide(): (%d) Attempted to spatialHide movie that is not showing", _header->_id);
+		return;
+	}
+
+	for (MovieFrame *frame : _framesOnScreen) {
+		g_engine->_dirtyRects.push_back(getFrameBoundingBox(frame));
+	}
+	_framesOnScreen.clear();
+	_framesNotYetShown.clear();
+
+	_isShowing = false;
+	_isPlaying = false;
+	setInactive();
 }
 
 void Movie::timePlay() {
-	debugC(5, kDebugScript, "Called Movie::timePlay()");
 	// TODO: Play movies one chunk at a time, which more directly approximates
 	// the original's reading from the CD one chunk at a time.
-	if (_isActive) {
-		warning("Movie::timePlay(): Attempted to play a movie that is already playing");
+	if (_isPlaying) {
+		warning("Movie::timePlay(): (%d) Attempted to play a movie that is already playing", _header->_id);
 		return;
 	}
 
-	_isActive = true;
-	_startTime = g_system->getMillis();
-	_lastProcessedTime = 0;
-	g_engine->addPlayingAsset(this);
-	_framesNotYetShown = _frames;
-
 	// START PLAYING SOUND.
 	// TODO: This won't work when we have some chunks that don't have audio.
 	if (!_audioStreams.empty()) {
@@ -240,22 +292,42 @@ void Movie::timePlay() {
 		audio->finish();
 	}
 
+	_framesNotYetShown = _frames;
+	_isShowing = true;
+	_isPlaying = true;
+	setActive();
 	runEventHandlerIfExists(kMovieBeginEvent);
 }
 
 void Movie::timeStop() {
-	if (!_isActive) {
-		warning("Movie::timePlay(): Attempted to stop a movie that isn't playing");
+	if (!_isShowing) {
+		warning("Movie::timeStop(): (%d) Attempted to stop a movie that isn't showing", _header->_id);
+		return;
+	} else if (!_isPlaying) {
+		warning("Movie::timePlay(): (%d) Attempted to stop a movie that isn't playing", _header->_id);
 		return;
 	}
-	_isActive = false;
-	_startTime = 0;
-	_lastProcessedTime = 0;
+
+	for (MovieFrame *frame : _framesOnScreen) {
+		g_engine->_dirtyRects.push_back(getFrameBoundingBox(frame));
+	}
+	_framesOnScreen.clear();
 	_framesNotYetShown.clear();
-	
+
 	g_engine->_mixer->stopHandle(_soundHandle);
 	_soundHandle = Audio::SoundHandle();
 
+	// Show the persistent frames.
+	_isPlaying = false;
+	if (!_stills.empty()) {
+		for (MovieFrame *still : _stills) {
+			_framesOnScreen.push_back(still);
+			g_engine->_dirtyRects.push_back(getFrameBoundingBox(still));
+		}
+	} else {
+		setInactive();
+	}
+
 	runEventHandlerIfExists(kMovieStoppedEvent);
 }
 
@@ -265,17 +337,27 @@ void Movie::process() {
 }
 
 void Movie::updateFrameState() {
-	// TODO: We'll need to support persistent frames in movies too. Do movies
-	// have the same distinction between spatialShow and timePlay that sprites
-	// do?
+	if (_showByDefault) {
+		spatialShow();
+		_showByDefault = false;
+	}
+
+	if (!_isPlaying) {
+		debugC(6, kDebugGraphics, "Movie::updateFrameState (%d): Not playing", _header->_id);
+		for (MovieFrame *frame : _framesOnScreen) {
+			debugC(6, kDebugGraphics, "   PERSIST: Frame %d (%d x %d) @ (%d, %d); start: %d ms, end: %d ms, keyframeEnd: %d ms, zIndex = %d", 
+				frame->index(), frame->width(), frame->height(), frame->left(), frame->top(), frame->startInMilliseconds(), frame->endInMilliseconds(), frame->keyframeEndInMilliseconds(), frame->zCoordinate());
+		}
+		return;
+	}
 
 	uint currentTime = g_system->getMillis();
 	uint movieTime = currentTime - _startTime;
 	debugC(5, kDebugGraphics, "Movie::updateFrameState (%d): Starting update (movie time: %d)", _header->_id, movieTime);
 	if (_framesNotYetShown.empty()) {
-		_isActive = false;
-		_startTime = 0;
-		_lastProcessedTime = 0;
+		_isPlaying = false;
+		setInactive();
+		_framesOnScreen.clear();
 		runEventHandlerIfExists(kMovieEndEvent);
 		return;
 	}
@@ -469,6 +551,14 @@ void Movie::readSubfile(Subfile &subfile, Chunk &chunk) {
 	}
 
 	// SET THE MOVIE FRAME FOOTERS.
+	for (MovieFrame *frame : _stills) {
+		for (MovieFrameFooter *footer : _footers) {
+			if (frame->index() == footer->_index) {
+				frame->setFooter(footer);
+			}
+		}
+	}
+
 	for (MovieFrame *frame : _frames) {
 		for (MovieFrameFooter *footer : _footers) {
 			if (frame->index() == footer->_index) {
diff --git a/engines/mediastation/assets/movie.h b/engines/mediastation/assets/movie.h
index acb8cac62e7..a0ee439c1bb 100644
--- a/engines/mediastation/assets/movie.h
+++ b/engines/mediastation/assets/movie.h
@@ -79,8 +79,6 @@ public:
 	// that could be confusing.
 	uint32 zCoordinate();
 
-	bool _showing = false;
-
 private:
 	MovieFrameHeader *_bitmapHeader = nullptr;
 	MovieFrameFooter *_footer = nullptr;
@@ -94,7 +92,7 @@ enum MovieSectionType {
 
 class Movie : public Asset {
 public:
-	Movie(AssetHeader *header) : Asset(header) {};
+	Movie(AssetHeader *header);
 	virtual ~Movie() override;
 
 	virtual void readChunk(Chunk &chunk) override;
@@ -106,6 +104,10 @@ public:
 	virtual void redraw(Common::Rect &rect) override;
 
 private:
+	bool _showByDefault = false;
+	bool _isShowing = false;
+	bool _isPlaying = false;
+
 	Common::Array<MovieFrame *> _frames;
 	Common::Array<MovieFrame *> _stills;
 	Common::Array<MovieFrameFooter *> _footers;
@@ -115,9 +117,11 @@ private:
 	Common::Array<MovieFrame *> _framesNotYetShown;
 	Common::Array<MovieFrame *> _framesOnScreen;
 
-	// Method implementations. These should be called from callMethod.
+	// Script method implementations.
 	void timePlay();
 	void timeStop();
+	void spatialShow();
+	void spatialHide();
 
 	void updateFrameState();
 
diff --git a/engines/mediastation/assets/sprite.cpp b/engines/mediastation/assets/sprite.cpp
index 350e96f4088..e29c3aa2252 100644
--- a/engines/mediastation/assets/sprite.cpp
+++ b/engines/mediastation/assets/sprite.cpp
@@ -68,7 +68,14 @@ uint32 SpriteFrame::index() {
 
 Sprite::Sprite(AssetHeader *header) : Asset(header) {
 	if (header->_startup == kAssetStartupActive) {
-		_isActive = true;
+		setActive();
+		_isShowing = true;
+	}
+
+	if (_header->_frameRate == 0) {
+		// It seems that the frame rate is 10 if it's not set in the asset
+		// header, or even if it's set to zero.
+		_header->_frameRate = 10;
 	}
 }
 
@@ -82,35 +89,47 @@ Sprite::~Sprite() {
 Operand Sprite::callMethod(BuiltInMethod methodId, Common::Array<Operand> &args) {
 	switch (methodId) {
 	case kSpatialShowMethod: {
-		assert(args.size() == 0);
+		assert(args.empty());
 		spatialShow();
 		return Operand();
 	}
 
 	case kSpatialHideMethod: {
 		assert(args.empty());
-		_isActive = false;
+		spatialHide();
 		return Operand();
 	}
 
-	case kTimeStopMethod: {
+	case kTimePlayMethod: {
 		assert(args.empty());
-		_isActive = false;
+		timePlay();
 		return Operand();
 	}
 
-	case kTimePlayMethod: {
-		assert(args.size() == 0);
-		timePlay();
+	case kTimeStopMethod: {
+		assert(args.empty());
+		timeStop();
 		return Operand();
 	}
 
 	case kMovieResetMethod: {
-		assert(args.size() == 0);
+		assert(args.empty());
 		movieReset();
 		return Operand();
 	}
 
+	case kSetCurrentClipMethod: {
+		assert(args.empty());
+		setCurrentClip();
+		return Operand();
+	}
+
+	case kIsPlayingMethod: {
+		Operand returnValue(kOperandTypeLiteral1);
+		returnValue.putInteger(static_cast<int>(_isPlaying));
+		return returnValue;
+	}
+
 	default: {
 		error("Sprite::callMethod(): Got unimplemented method ID %d", methodId);
 	}
@@ -118,45 +137,83 @@ Operand Sprite::callMethod(BuiltInMethod methodId, Common::Array<Operand> &args)
 }
 
 void Sprite::spatialShow() {
-	debugC(5, kDebugScript, "Called Sprite::spatialShow");
-	_isActive = true;
-	g_engine->addPlayingAsset(this);
+	if (_isShowing) {
+		warning("Sprite::spatialShow(): (%d) Attempted to spatialShow when already showing", _header->_id);
+		return;
+	}
+	showFrame(_frames[0]);
 
-	_isPaused = true;
-	if (_activeFrame == nullptr) {
-		showFrame(0);
+	setActive();
+	_isShowing = true;
+	_isPlaying = false;
+}
+
+void Sprite::spatialHide() {
+	if (!_isShowing) {
+		warning("Sprite::spatialHide(): (%d) Attempted to spatialHide when not showing", _header->_id);
+		return;
 	}
+	showFrame(nullptr);
+
+	setInactive();
+	_isShowing = false;
+	_isPlaying = false;
 }
 
 void Sprite::timePlay() {
-	_isActive = true;
-	_isPaused = false;
-	_startTime = g_system->getMillis();
-	_lastProcessedTime = 0;
+	if (!_isShowing) {
+		warning("Sprite::timePlay(): (%d) Attempted to timePlay when not showing", _header->_id);
+		return;
+	} else if (_isPlaying) {
+		warning("Sprite::timePlay(): (%d) Attempted to timePlay when already playing", _header->_id);
+		return;
+	}
+
+	setActive();
+	_isPlaying = true;
 	_nextFrameTime = 0;
-	g_engine->addPlayingAsset(this);
 
-	if (_header->_frameRate == 0) {
-		// It seems that the frame rate is 10 if it's not set in the asset
-		// header, or even if it's set to zero.
-		_header->_frameRate = 10;
+	runEventHandlerIfExists(kMovieBeginEvent);
+}
+
+void Sprite::timeStop() {
+	if (!_isShowing) {
+		warning("Sprite::timeStop(): (%d) Attempted to timeStop when not showing", _header->_id);
+		return;
+	} else if (!_isPlaying) {
+		warning("Sprite::timeStop(): (%d) Attempted to timeStop when not playing", _header->_id);
+		return;
 	}
 
-	runEventHandlerIfExists(kMovieBeginEvent);
+	_isPlaying = false;
+	// TODO: Find the right event handler to run here.
 }
 
 void Sprite::movieReset() {
-	_isActive = true;
-	// We do NOT reset the persisting frame, because it should keep showing!
-	_isPaused = true;
+	setActive();
+	if (_isShowing) {
+		showFrame(_frames[0]);
+	} else {
+		showFrame(nullptr);
+	}
+	_isPlaying = false;
 	_startTime = 0;
 	_currentFrameIndex = 0;
 	_nextFrameTime = 0;
 	_lastProcessedTime = 0;
 }
 
+void Sprite::setCurrentClip() {
+	if (_currentFrameIndex < _frames.size()) {
+		showFrame(_frames[_currentFrameIndex++]);
+	} else {
+		warning("Sprite::setCurrentClip(): (%d) Attempted to increment past number of frames", _header->_id);
+	}
+}
+
 void Sprite::process() {
 	updateFrameState();
+	// Sprites don't have time event handlers, separate timers do time handling.
 }
 
 void Sprite::readChunk(Chunk &chunk) {
@@ -174,17 +231,30 @@ void Sprite::readChunk(Chunk &chunk) {
 }
 
 void Sprite::updateFrameState() {
-	if (_isPaused) {
+	if (!_isActive) {
 		return;
 	}
 
+	if (!_isPlaying) {
+		if (_activeFrame != nullptr) {
+			debugC(6, kDebugGraphics, "Sprite::updateFrameState(): (%d): Not playing. Persistent frame %d (%d x %d) @ (%d, %d)", 
+				_header->_id, _activeFrame->index(), _activeFrame->width(), _activeFrame->height(), _activeFrame->left(), _activeFrame->top());
+		} else {
+			debugC(6, kDebugGraphics, "Sprite::updateFrameState(): (%d): Not playing, no persistent frame", _header->_id);
+		}
+		return;
+	}
+
+	debugC(5, kDebugGraphics, "Sprite::updateFrameState(): (%d) Frame %d (%d x %d) @ (%d, %d)",
+		_header->_id, _activeFrame->index(), _activeFrame->width(), _activeFrame->height(), _activeFrame->left(), _activeFrame->top());
+
 	uint currentTime = g_system->getMillis() - _startTime;
 	bool drawNextFrame = currentTime >= _nextFrameTime;
 	if (!drawNextFrame) {
 		return;
 	}
 
-	showFrame(_currentFrameIndex);
+	showFrame(_frames[_currentFrameIndex]);
 
 	uint frameDuration = 1000 / _header->_frameRate;
 	_nextFrameTime = ++_currentFrameIndex * frameDuration;
@@ -193,8 +263,8 @@ void Sprite::updateFrameState() {
 	if (spriteFinishedPlaying) {
 		// Sprites always keep their last frame showing until they are hidden
 		// with spatialHide.
-		showFrame(_currentFrameIndex - 1);
-		_isPaused = true;
+		showFrame(_frames[_currentFrameIndex - 1]);
+		_isPlaying = false;
 
 		// But otherwise, the sprite's params should be reset.
 		_isActive = true;
@@ -208,7 +278,7 @@ void Sprite::updateFrameState() {
 }
 
 void Sprite::redraw(Common::Rect &rect) {
-	if (_activeFrame == nullptr) {
+	if (_activeFrame == nullptr || !_isShowing) {
 		return;
 	}
 
@@ -221,15 +291,17 @@ void Sprite::redraw(Common::Rect &rect) {
 	}
 }
 
-void Sprite::showFrame(uint frameIndex) {
+void Sprite::showFrame(SpriteFrame *frame) {
 	// Erase the previous frame.
 	if (_activeFrame != nullptr) {
 		g_engine->_dirtyRects.push_back(getActiveFrameBoundingBox());
 	}
 	
 	// Show the next frame.
-	_activeFrame = _frames[frameIndex];
-	g_engine->_dirtyRects.push_back(getActiveFrameBoundingBox());
+	_activeFrame = frame;
+	if (frame != nullptr) {
+		g_engine->_dirtyRects.push_back(getActiveFrameBoundingBox());
+	}
 }
 
 Common::Rect Sprite::getActiveFrameBoundingBox() {
diff --git a/engines/mediastation/assets/sprite.h b/engines/mediastation/assets/sprite.h
index 32f9bf9e7fa..8e2119c89f0 100644
--- a/engines/mediastation/assets/sprite.h
+++ b/engines/mediastation/assets/sprite.h
@@ -58,6 +58,8 @@ private:
 	SpriteFrameHeader *_bitmapHeader = nullptr;
 };
 
+// Sprites are somewhat like movies, but they strictly show one frame at a time
+// and don't have sound. They are intended for background/recurrent animations.
 class Sprite : public Asset {
 public:
 	Sprite(AssetHeader *header);
@@ -72,17 +74,21 @@ public:
 private:
 	Common::Array<SpriteFrame *> _frames;
 	SpriteFrame *_activeFrame = nullptr;
-	bool _isPaused = false;
+	bool _isShowing = false;
+	bool _isPlaying = false;
 	uint _currentFrameIndex = 0;
 	uint _nextFrameTime = 0;
 
 	// Method implementations.
 	void spatialShow();
+	void spatialHide();
 	void timePlay();
+	void timeStop();
 	void movieReset();
+	void setCurrentClip();
 
 	void updateFrameState();
-	void showFrame(uint frameIndex);
+	void showFrame(SpriteFrame *frame);
 	Common::Rect getActiveFrameBoundingBox();
 };
 
diff --git a/engines/mediastation/mediascript/scriptconstants.cpp b/engines/mediastation/mediascript/scriptconstants.cpp
index 1aaee142b51..b4a11d78a00 100644
--- a/engines/mediastation/mediascript/scriptconstants.cpp
+++ b/engines/mediastation/mediascript/scriptconstants.cpp
@@ -161,6 +161,8 @@ const char *builtInMethodToStr(BuiltInMethod method) {
 		return "IsVisible";
 	case kMovieResetMethod:
 		return "MovieReset";
+	case kSetCurrentClipMethod:
+		return "SetCurrentClip";
 	case kSetWorldSpaceExtentMethod:
 		return "SetWorldSpaceExtent";
 	case kSetBoundsMethod:
diff --git a/engines/mediastation/mediascript/scriptconstants.h b/engines/mediastation/mediascript/scriptconstants.h
index 3e9d03547be..0228a2f047f 100644
--- a/engines/mediastation/mediascript/scriptconstants.h
+++ b/engines/mediastation/mediascript/scriptconstants.h
@@ -113,6 +113,7 @@ enum BuiltInMethod {
 
 	// SPRITE METHODS.
 	kMovieResetMethod = 219, // PARAMS: 0
+	kSetCurrentClipMethod = 221, // PARAMS: 0-1
 
 	// STAGE METHODS.
 	kSetWorldSpaceExtentMethod = 363, // PARAMS: 2


Commit: a2a06746ca5e8c43ddd477fe9108708e4e44b0e7
    https://github.com/scummvm/scummvm/commit/a2a06746ca5e8c43ddd477fe9108708e4e44b0e7
Author: Nathanael Gentry (nathanael.gentrydb8 at gmail.com)
Date: 2025-02-02T11:52:05-05:00

Commit Message:
MEDIASTATION: Store variable bools internally as ints

Since the Operand doesn't have a separate type for bools anyway.

Changed paths:
    engines/mediastation/mediascript/variable.cpp
    engines/mediastation/mediascript/variable.h


diff --git a/engines/mediastation/mediascript/variable.cpp b/engines/mediastation/mediascript/variable.cpp
index bc9be29cc47..ceeca43aeb2 100644
--- a/engines/mediastation/mediascript/variable.cpp
+++ b/engines/mediastation/mediascript/variable.cpp
@@ -96,7 +96,7 @@ Variable::Variable(Chunk &chunk, bool readId) {
 	case kVariableTypeBoolean: {
 		uint rawValue = Datum(chunk, kDatumTypeUint8).u.i;
 		debugC(7, kDebugLoading, " Variable::Variable(): %s: %d", variableTypeToStr(_type), rawValue);
-		_value.b = (rawValue == 1);
+		_value.i = static_cast<int>(rawValue == 1);
 		break;
 	}
 
@@ -169,7 +169,7 @@ Operand Variable::getValue() {
 		// TODO: Is this value type correct?
 		// Shouldn't matter too much, though, since it's still an integer type.
 		Operand returnValue(kOperandTypeLiteral1);
-		returnValue.putInteger(_value.b);
+		returnValue.putInteger(_value.i);
 		return returnValue;
 	}
 
diff --git a/engines/mediastation/mediascript/variable.h b/engines/mediastation/mediascript/variable.h
index 691f011f70b..5cc3bcbe373 100644
--- a/engines/mediastation/mediascript/variable.h
+++ b/engines/mediastation/mediascript/variable.h
@@ -45,7 +45,6 @@ public:
 	union {
 		Common::String *string;
 		Collection *collection;
-		bool b;
 		int i;
 		double d;
 		uint assetId;


Commit: 17bd61d4132332572859255f526a2c2c2fa40c3c
    https://github.com/scummvm/scummvm/commit/17bd61d4132332572859255f526a2c2c2fa40c3c
Author: Nathanael Gentry (nathanael.gentrydb8 at gmail.com)
Date: 2025-02-02T11:52:05-05:00

Commit Message:
MEDIASTATION: Properly deactivate hotspots when mouse is moved outside any hotspot

Changed paths:
    engines/mediastation/mediastation.cpp


diff --git a/engines/mediastation/mediastation.cpp b/engines/mediastation/mediastation.cpp
index 0c21021fac1..ecc1f5b25ff 100644
--- a/engines/mediastation/mediastation.cpp
+++ b/engines/mediastation/mediastation.cpp
@@ -193,7 +193,11 @@ void MediaStationEngine::processEvents() {
 				debugC(5, kDebugEvents, "EVENT_MOUSEMOVE (%d, %d): Sent to hotspot %d", e.mouse.x, e.mouse.y, hotspot->getHeader()->_id);
 				hotspot->runEventHandlerIfExists(kMouseMovedEvent);
 			} else {
-				_currentHotspot = nullptr;
+				if (_currentHotspot != nullptr) {
+					_currentHotspot->runEventHandlerIfExists(kMouseExitedEvent);
+					debugC(5, kDebugEvents, "EVENT_MOUSEMOVE (%d, %d): Exited hotspot %d", e.mouse.x, e.mouse.y, _currentHotspot->getHeader()->_id);
+					_currentHotspot = nullptr;
+				}
 			}
 			break;
 		}
@@ -373,6 +377,7 @@ void MediaStationEngine::branchToScreen(uint32 contextId) {
 	Context *context = loadContext(contextId);
 	_currentContext = context;
 	_dirtyRects.push_back(Common::Rect(SCREEN_WIDTH, SCREEN_HEIGHT));
+	_currentHotspot = nullptr;
 
 	if (context->_screenAsset != nullptr) {
 		// TODO: Make the screen an asset just like everything else so we can




More information about the Scummvm-git-logs mailing list