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

npjg noreply at scummvm.org
Sat Jun 14 15:07:27 UTC 2025


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

Summary:
fdefc58d3e MEDIASTATION: Don't implicitly cast point members to unsigned
8a5282dc23 MEDIASTATION: Introduce SpatialEntity class for on-screen assets
06c02db208 MEDIASTATION: Store asset-specific fields in the assets themselves
a7aae9b0b1 MEDIASTATION: Factor audio sequences into their own class
e56caa42fe MEDIASTATION: Don't keep separate "active" assets list
cb66b27c9f MEDIASTATION: Improve Sprite rendering accuracy
65578a4510 MEDIASTATION: Improve Movie rendering accuracy
6cb99deffe MEDIASTATION: JANITORIAL: FIx whitespace & namespace comments


Commit: fdefc58d3e400237e9fba35e9e950c79b777ac24
    https://github.com/scummvm/scummvm/commit/fdefc58d3e400237e9fba35e9e950c79b777ac24
Author: Nathanael Gentry (nathanael.gentrydb8 at gmail.com)
Date: 2025-06-14T11:06:58-04:00

Commit Message:
MEDIASTATION: Don't implicitly cast point members to unsigned

Changed paths:
    engines/mediastation/bitmap.cpp
    engines/mediastation/bitmap.h


diff --git a/engines/mediastation/bitmap.cpp b/engines/mediastation/bitmap.cpp
index 3d0f1f889ee..389c03555b4 100644
--- a/engines/mediastation/bitmap.cpp
+++ b/engines/mediastation/bitmap.cpp
@@ -44,8 +44,8 @@ bool BitmapHeader::isCompressed() {
 Bitmap::Bitmap(Chunk &chunk, BitmapHeader *bitmapHeader) :
 	_bitmapHeader(bitmapHeader) {
 	// The header must be constructed beforehand.
-	uint16 width = _bitmapHeader->_dimensions.x;
-	uint16 height = _bitmapHeader->_dimensions.y;
+	int16 width = _bitmapHeader->_dimensions.x;
+	int16 height = _bitmapHeader->_dimensions.y;
 	_surface.create(width, height, Graphics::PixelFormat::createFormatCLUT8());
 	_surface.setTransparentColor(0);
 	uint8 *pixels = (uint8 *)_surface.getPixels();
@@ -70,11 +70,11 @@ Bitmap::~Bitmap() {
 	_bitmapHeader = nullptr;
 }
 
-uint16 Bitmap::width() {
+int16 Bitmap::width() {
 	return _bitmapHeader->_dimensions.x;
 }
 
-uint16 Bitmap::height() {
+int16 Bitmap::height() {
 	return _bitmapHeader->_dimensions.y;
 }
 
@@ -104,9 +104,9 @@ void Bitmap::decompress(Chunk &chunk) {
 	// size_t transparencyRunTopYCoordinate = 0;
 	// size_t transparencyRunLeftXCoordinate = 0;
 	bool imageFullyRead = false;
-	size_t currentYCoordinate = 0;
+	int16 currentYCoordinate = 0;
 	while (currentYCoordinate < height()) {
-		size_t currentXCoordinate = 0;
+		int16 currentXCoordinate = 0;
 		bool readingTransparencyRun = false;
 		while (true) {
 			byte operation = chunk.readByte();
diff --git a/engines/mediastation/bitmap.h b/engines/mediastation/bitmap.h
index a1cb1add861..8ef973a6c2a 100644
--- a/engines/mediastation/bitmap.h
+++ b/engines/mediastation/bitmap.h
@@ -55,8 +55,8 @@ public:
 	Bitmap(Chunk &chunk, BitmapHeader *bitmapHeader);
 	virtual ~Bitmap();
 
-	uint16 width();
-	uint16 height();
+	int16 width();
+	int16 height();
 	Graphics::ManagedSurface _surface;
 
 private:


Commit: 8a5282dc233cfd27ed07a6e3c653126fd51ed3ff
    https://github.com/scummvm/scummvm/commit/8a5282dc233cfd27ed07a6e3c653126fd51ed3ff
Author: Nathanael Gentry (nathanael.gentrydb8 at gmail.com)
Date: 2025-06-14T11:07:14-04:00

Commit Message:
MEDIASTATION: Introduce SpatialEntity class for on-screen assets

Following the pattern and name of the original.

Changed paths:
    engines/mediastation/asset.cpp
    engines/mediastation/asset.h
    engines/mediastation/assets/canvas.cpp
    engines/mediastation/assets/canvas.h
    engines/mediastation/assets/font.cpp
    engines/mediastation/assets/font.h
    engines/mediastation/assets/hotspot.cpp
    engines/mediastation/assets/hotspot.h
    engines/mediastation/assets/image.cpp
    engines/mediastation/assets/image.h
    engines/mediastation/assets/movie.cpp
    engines/mediastation/assets/movie.h
    engines/mediastation/assets/palette.cpp
    engines/mediastation/assets/palette.h
    engines/mediastation/assets/path.cpp
    engines/mediastation/assets/screen.cpp
    engines/mediastation/assets/screen.h
    engines/mediastation/assets/sound.cpp
    engines/mediastation/assets/sprite.cpp
    engines/mediastation/assets/sprite.h
    engines/mediastation/assets/text.cpp
    engines/mediastation/assets/text.h
    engines/mediastation/assets/timer.cpp
    engines/mediastation/context.cpp
    engines/mediastation/mediascript/scriptconstants.cpp
    engines/mediastation/mediascript/scriptconstants.h
    engines/mediastation/mediastation.cpp


diff --git a/engines/mediastation/asset.cpp b/engines/mediastation/asset.cpp
index 24cb7a0e2e4..e8f26f8f285 100644
--- a/engines/mediastation/asset.cpp
+++ b/engines/mediastation/asset.cpp
@@ -30,6 +30,10 @@ Asset::~Asset() {
 	_header = nullptr;
 }
 
+ScriptValue Asset::callMethod(BuiltInMethod methodId, Common::Array<ScriptValue> &args) {
+	error("Got unimplemented method call %d (%s)", static_cast<uint>(methodId), builtInMethodToStr(methodId));
+}
+
 void Asset::readChunk(Chunk &chunk) {
 	error("Asset::readChunk(): Chunk reading for asset type 0x%x is not implemented", static_cast<uint>(_header->_type));
 }
@@ -38,18 +42,6 @@ void Asset::readSubfile(Subfile &subfile, Chunk &chunk) {
 	error("Asset::readSubfile(): Subfile reading for asset type 0x%x is not implemented", static_cast<uint>(_header->_type));
 }
 
-AssetType Asset::type() const {
-	return _header->_type;
-}
-
-int Asset::zIndex() const {
-	return _header->_zIndex;
-}
-
-Common::Rect Asset::getBbox() const {
-	return _header->_boundingBox;
-}
-
 void Asset::setActive() {
 	_isActive = true;
 	_startTime = g_system->getMillis();
@@ -73,7 +65,7 @@ void Asset::processTimeEventHandlers() {
 	uint currentTime = g_system->getMillis();
 	const Common::Array<EventHandler *> &_timeHandlers = _header->_eventHandlers.getValOrDefault(kTimerEvent);
 	for (EventHandler *timeEvent : _timeHandlers) {
- 		// Indeed float, not time.
+		// Indeed float, not time.
 		double timeEventInFractionalSeconds = timeEvent->_argumentValue.asFloat();
 		uint timeEventInMilliseconds = timeEventInFractionalSeconds * 1000;
 		bool timeEventAlreadyProcessed = timeEventInMilliseconds < _lastProcessedTime;
@@ -111,4 +103,145 @@ void Asset::runEventHandlerIfExists(EventType eventType) {
 	runEventHandlerIfExists(eventType, scriptValue);
 }
 
+ScriptValue SpatialEntity::callMethod(BuiltInMethod methodId, Common::Array<ScriptValue> &args) {
+	ScriptValue returnValue;
+	switch (methodId) {
+	case kSpatialMoveToMethod: {
+		assert(args.size() == 2);
+		int16 x = static_cast<int16>(args[0].asFloat());
+		int16 y = static_cast<int16>(args[1].asFloat());
+		moveTo(x, y);
+		break;
+	}
+
+	case kSpatialMoveToByOffsetMethod: {
+		assert(args.size() == 2);
+		int16 dx = static_cast<int16>(args[0].asFloat());
+		int16 dy = static_cast<int16>(args[1].asFloat());
+
+		Common::Point currentPos = getTopLeft();
+		int16 newX = currentPos.x + dx;
+		int16 newY = currentPos.y + dy;
+		moveTo(newX, newY);
+		break;
+	}
+
+	case kSpatialZMoveToMethod: {
+		assert(args.size() == 1);
+		int zIndex = static_cast<int>(args[0].asFloat());
+		setZIndex(zIndex);
+		break;
+	}
+
+	case kSpatialCenterMoveToMethod: {
+		assert(args.size() == 2);
+		int16 x = static_cast<int16>(args[0].asFloat());
+		int16 y = static_cast<int16>(args[1].asFloat());
+		moveToCentered(x, y);
+		break;
+	}
+
+	case kGetLeftXMethod:
+		assert(args.empty());
+		returnValue.setToFloat(_header->_boundingBox.left);
+		break;
+
+	case kGetTopYMethod:
+		assert(args.empty());
+		returnValue.setToFloat(_header->_boundingBox.top);
+		break;
+
+	case kGetWidthMethod:
+		assert(args.empty());
+		returnValue.setToFloat(_header->_boundingBox.width());
+		break;
+
+	case kGetHeightMethod:
+		assert(args.empty());
+		returnValue.setToFloat(_header->_boundingBox.height());
+		break;
+
+	case kGetCenterXMethod: {
+		int centerX = _header->_boundingBox.left + (_header->_boundingBox.width() / 2);
+		returnValue.setToFloat(centerX);
+		break;
+	}
+
+	case kGetCenterYMethod: {
+		int centerY = _header->_boundingBox.top + (_header->_boundingBox.height() / 2);
+		returnValue.setToFloat(centerY);
+		break;
+	}
+
+	case kGetZCoordinateMethod:
+		assert(args.empty());
+		returnValue.setToFloat(_header->_zIndex);
+		break;
+
+	case kIsVisibleMethod:
+		assert(args.empty());
+		returnValue.setToBool(isVisible());
+		break;
+
+	default:
+		Asset::callMethod(methodId, args);
+	}
+	return returnValue;
+}
+
+void SpatialEntity::moveTo(int16 x, int16 y) {
+	Common::Point dest(x, y);
+	if (dest == getTopLeft()) {
+		// We aren't actually moving anywhere.
+		return;
+	}
+
+	if (isVisible()) {
+		invalidateLocalBounds();
+	}
+	_header->_boundingBox.moveTo(dest);
+	if (isVisible()) {
+		invalidateLocalBounds();
+	}
+}
+
+void SpatialEntity::moveToCentered(int16 x, int16 y) {
+	int16 targetX = x - (_header->_boundingBox.width() / 2);
+	int16 targetY = y - (_header->_boundingBox.height() / 2);
+	moveTo(targetX, targetY);
+}
+
+void SpatialEntity::setBounds(const Common::Rect &bounds) {
+	if (_header->_boundingBox == bounds) {
+		// We aren't actually moving anywhere.
+		return;
+	}
+
+	if (isVisible()) {
+		invalidateLocalBounds();
+	}
+	_header->_boundingBox = bounds;
+	if (isVisible()) {
+		invalidateLocalBounds();
+	}
+}
+
+void SpatialEntity::setZIndex(int zIndex) {
+	if (_header->_zIndex == zIndex) {
+		// We aren't actually moving anywhere.
+		return;
+	}
+
+	_header->_zIndex = zIndex;
+	invalidateLocalZIndex();
+}
+
+void SpatialEntity::invalidateLocalBounds() {
+	g_engine->_dirtyRects.push_back(_header->_boundingBox);
+}
+
+void SpatialEntity::invalidateLocalZIndex() {
+	warning("STUB: Asset::invalidateLocalZIndex()");
+}
+
 } // End of namespace MediaStation
diff --git a/engines/mediastation/asset.h b/engines/mediastation/asset.h
index 98e63689450..390c7a23de5 100644
--- a/engines/mediastation/asset.h
+++ b/engines/mediastation/asset.h
@@ -40,23 +40,13 @@ public:
 	virtual ~Asset();
 
 	// Does any needed frame drawing, audio playing, event handlers, etc.
-	virtual void process() {
-		return;
-	}
-
-	// For spatial assets, actually redraws the dirty area.
-	virtual void redraw(Common::Rect &rect) {
-		return;
-	}
+	virtual void process() { return; }
 
 	// Runs built-in bytecode methods.
-	virtual ScriptValue callMethod(BuiltInMethod methodId, Common::Array<ScriptValue> &args) = 0;
-	// Called to have the asset do any processing, like drawing new frames,
-	// handling time-based event handlers, and such. Some assets don't have any
-	// processing to do.
-	virtual bool isActive() const {
-		return _isActive;
-	}
+	virtual ScriptValue callMethod(BuiltInMethod methodId, Common::Array<ScriptValue> &args);
+
+	virtual bool isSpatialActor() const { return false; }
+	virtual bool isActive() const { return _isActive; }
 
 	// These are not pure virtual so if an asset doesnʻt read any chunks or
 	// subfiles it doesnʻt need to just implement these with an error message.
@@ -69,22 +59,40 @@ public:
 	void runEventHandlerIfExists(EventType eventType, const ScriptValue &arg);
 	void runEventHandlerIfExists(EventType eventType);
 
-	AssetType type() const;
-	int zIndex() const;
-	Common::Rect getBbox() const;
-	AssetHeader *getHeader() const {
-		return _header;
-	}
+	AssetType type() const { return _header->_type; }
+	uint id() const { return _header->_id; }
+	AssetHeader *getHeader() const { return _header; }
 
 protected:
 	AssetHeader *_header = nullptr;
 	bool _isActive = false;
 	uint _startTime = 0;
 	uint _lastProcessedTime = 0;
-	// TODO: Rename this to indicate the time is in milliseconds.
 	uint _duration = 0;
 };
 
+class SpatialEntity : public Asset {
+public:
+	SpatialEntity(AssetHeader *header) : Asset(header) {};
+
+	virtual void redraw(Common::Rect &rect) { return; }
+	virtual ScriptValue callMethod(BuiltInMethod methodId, Common::Array<ScriptValue> &args) override;
+
+	virtual bool isSpatialActor() const override { return true; }
+	virtual bool isVisible() const { return false; }
+	Common::Point getTopLeft() { return Common::Point(_header->_boundingBox.left, _header->_boundingBox.top); }
+	Common::Rect getBbox() const { return _header->_boundingBox; }
+	int zIndex() const { return _header->_zIndex; }
+
+	void moveTo(int16 x, int16 y);
+	void moveToCentered(int16 x, int16 y);
+	void setBounds(const Common::Rect &bounds);
+	void setZIndex(int zIndex);
+
+	virtual void invalidateLocalBounds();
+	virtual void invalidateLocalZIndex();
+};
+
 } // End of namespace MediaStation
 
 #endif
\ No newline at end of file
diff --git a/engines/mediastation/assets/canvas.cpp b/engines/mediastation/assets/canvas.cpp
index 6579842dbac..6bd43408244 100644
--- a/engines/mediastation/assets/canvas.cpp
+++ b/engines/mediastation/assets/canvas.cpp
@@ -30,7 +30,7 @@ ScriptValue Canvas::callMethod(BuiltInMethod methodId, Common::Array<ScriptValue
 	}
 
 	default:
-		error("Canvas::callMethod(): Got unimplemented method ID %s (%d)", builtInMethodToStr(methodId), static_cast<uint>(methodId));
+		return SpatialEntity::callMethod(methodId, args);
 	}
 }
 
diff --git a/engines/mediastation/assets/canvas.h b/engines/mediastation/assets/canvas.h
index ca2214ddf82..b7dcd0431e7 100644
--- a/engines/mediastation/assets/canvas.h
+++ b/engines/mediastation/assets/canvas.h
@@ -29,11 +29,15 @@
 
 namespace MediaStation {
 
-class Canvas : public Asset {
+class Canvas : public SpatialEntity {
 public:
-	Canvas(AssetHeader *header) : Asset(header) {};
+	Canvas(AssetHeader *header) : SpatialEntity(header) {};
 
+	virtual bool isVisible() const override { return _isVisible; }
 	virtual ScriptValue callMethod(BuiltInMethod methodId, Common::Array<ScriptValue> &args) override;
+
+private:
+	bool _isVisible = false;
 };
 
 } // End of namespace MediaStation
diff --git a/engines/mediastation/assets/font.cpp b/engines/mediastation/assets/font.cpp
index a185a375f11..8f07342e600 100644
--- a/engines/mediastation/assets/font.cpp
+++ b/engines/mediastation/assets/font.cpp
@@ -37,10 +37,6 @@ Font::~Font() {
 	_glyphs.clear();
 }
 
-ScriptValue Font::callMethod(BuiltInMethod methodId, Common::Array<ScriptValue> &args) {
-	error("Font::callMethod(): Font does not have any callable methods");
-}
-
 void Font::readChunk(Chunk &chunk) {
 	debugC(5, kDebugLoading, "Font::readChunk(): Reading font glyph (@0x%llx)", static_cast<long long int>(chunk.pos()));
 	uint asciiCode = chunk.readTypedUint16();
diff --git a/engines/mediastation/assets/font.h b/engines/mediastation/assets/font.h
index 3ecab34e8b6..5a599af6e61 100644
--- a/engines/mediastation/assets/font.h
+++ b/engines/mediastation/assets/font.h
@@ -46,15 +46,10 @@ public:
 	Font(AssetHeader *header) : Asset(header) {};
 	~Font();
 
-	virtual ScriptValue callMethod(BuiltInMethod methodId, Common::Array<ScriptValue> &args) override;
-
 	virtual void readChunk(Chunk &chunk) override;
 
 private:
 	Common::HashMap<uint, FontGlyph *> _glyphs;
-
-	// Method implementations.
-	// No methods are implemented as of now.
 };
 
 } // End of namespace MediaStation
diff --git a/engines/mediastation/assets/hotspot.cpp b/engines/mediastation/assets/hotspot.cpp
index 7f7a50dbfa7..989ed995be4 100644
--- a/engines/mediastation/assets/hotspot.cpp
+++ b/engines/mediastation/assets/hotspot.cpp
@@ -24,7 +24,7 @@
 
 namespace MediaStation {
 
-Hotspot::Hotspot(AssetHeader *header) : Asset(header) {
+Hotspot::Hotspot(AssetHeader *header) : SpatialEntity(header) {
 	if (header->_startup == kAssetStartupActive) {
 		_isActive = true;
 	}
@@ -106,7 +106,7 @@ ScriptValue Hotspot::callMethod(BuiltInMethod methodId, Common::Array<ScriptValu
 	}
 
 	default:
-		error("Hotspot::callMethod(): Got unimplemented method ID %s (%d)", builtInMethodToStr(methodId), static_cast<uint>(methodId));
+		return SpatialEntity::callMethod(methodId, args);
 	}
 }
 
diff --git a/engines/mediastation/assets/hotspot.h b/engines/mediastation/assets/hotspot.h
index fb1aa73e2f3..dfde0dcad94 100644
--- a/engines/mediastation/assets/hotspot.h
+++ b/engines/mediastation/assets/hotspot.h
@@ -29,11 +29,12 @@
 
 namespace MediaStation {
 
-class Hotspot : public Asset {
+class Hotspot : public SpatialEntity {
 public:
 	Hotspot(AssetHeader *header);
 
 	bool isInside(const Common::Point &pointToCheck);
+	virtual bool isVisible() const override { return false; }
 
 	virtual ScriptValue callMethod(BuiltInMethod methodId, Common::Array<ScriptValue> &args) override;
 };
diff --git a/engines/mediastation/assets/image.cpp b/engines/mediastation/assets/image.cpp
index 54771709580..e2e92d9d28b 100644
--- a/engines/mediastation/assets/image.cpp
+++ b/engines/mediastation/assets/image.cpp
@@ -25,7 +25,7 @@
 
 namespace MediaStation {
 
-Image::Image(AssetHeader *header) : Asset(header) {
+Image::Image(AssetHeader *header) : SpatialEntity(header) {
 	if (header->_startup == kAssetStartupActive) {
 		_isActive = true;
 	}
@@ -42,7 +42,6 @@ Image::~Image() {
 
 ScriptValue Image::callMethod(BuiltInMethod methodId, Common::Array<ScriptValue> &args) {
 	ScriptValue returnValue;
-
 	switch (methodId) {
 	case kSpatialShowMethod: {
 		assert(args.empty());
@@ -62,29 +61,8 @@ ScriptValue Image::callMethod(BuiltInMethod methodId, Common::Array<ScriptValue>
 		return returnValue;
 	}
 
-	case kSpatialMoveToMethod: {
-		assert(args.size() == 2);
-
-		// Mark the previous location dirty.
-		Common::Rect bbox(getLeftTop(), _bitmap->width(), _bitmap->height());
-		g_engine->_dirtyRects.push_back(bbox);
-
-		// Update location and mark new location dirty.
-		int newXAdjust = static_cast<int>(args[0].asFloat());
-		int newYAdjust = static_cast<int>(args[1].asFloat());
-		if (_xAdjust != newXAdjust || _yAdjust != newYAdjust) {
-			_xAdjust = newXAdjust;
-			_yAdjust = newYAdjust;
-
-			bbox = Common::Rect(getLeftTop(), _bitmap->width(), _bitmap->height());
-			g_engine->_dirtyRects.push_back(bbox);
-		}
-
-		return returnValue;
-	}
-
 	default:
-		error("Image::callMethod(): Got unimplemented method ID %s (%d)", builtInMethodToStr(methodId), static_cast<uint>(methodId));
+		return SpatialEntity::callMethod(methodId, args);
 	}
 }
 
@@ -106,6 +84,7 @@ void Image::redraw(Common::Rect &rect) {
 
 void Image::spatialShow() {
 	_isActive = true;
+	_isVisible = true;
 	g_engine->addPlayingAsset(this);
 	Common::Rect bbox(getLeftTop(), _bitmap->width(), _bitmap->height());
 	g_engine->_dirtyRects.push_back(bbox);
@@ -113,12 +92,13 @@ void Image::spatialShow() {
 
 void Image::spatialHide() {
 	_isActive = false;
+	_isVisible = 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 + _xAdjust, _header->_y + _header->_boundingBox.top + _yAdjust);
+	return Common::Point(_header->_x + _header->_boundingBox.left, _header->_y + _header->_boundingBox.top);
 }
 
 void Image::readChunk(Chunk &chunk) {
diff --git a/engines/mediastation/assets/image.h b/engines/mediastation/assets/image.h
index 14850c6e930..a0953058e25 100644
--- a/engines/mediastation/assets/image.h
+++ b/engines/mediastation/assets/image.h
@@ -31,7 +31,7 @@
 
 namespace MediaStation {
 
-class Image : public Asset {
+class Image : public SpatialEntity {
 friend class Context;
 
 public:
@@ -39,15 +39,13 @@ public:
 	virtual ~Image() override;
 
 	virtual void readChunk(Chunk &chunk) override;
-
-	virtual void redraw(Common::Rect &rect) override;
-
 	virtual ScriptValue callMethod(BuiltInMethod methodId, Common::Array<ScriptValue> &args) override;
+	virtual void redraw(Common::Rect &rect) override;
+	virtual bool isVisible() const override { return _isVisible; }
 
 private:
 	Bitmap *_bitmap = nullptr;
-	int _xAdjust = 0;
-	int _yAdjust = 0;
+	bool _isVisible = false;
 
 	// Script method implementations.
 	void spatialShow();
diff --git a/engines/mediastation/assets/movie.cpp b/engines/mediastation/assets/movie.cpp
index 65c2daf8f4a..59588f039d0 100644
--- a/engines/mediastation/assets/movie.cpp
+++ b/engines/mediastation/assets/movie.cpp
@@ -154,7 +154,7 @@ MovieFrame::~MovieFrame() {
 	_footer = nullptr;
 }
 
-Movie::Movie(AssetHeader *header) : Asset(header) {
+Movie::Movie(AssetHeader *header) : SpatialEntity(header) {
 	if (header->_startup == kAssetStartupActive) {
 		setActive();
 		_showByDefault = true;
@@ -213,35 +213,13 @@ ScriptValue Movie::callMethod(BuiltInMethod methodId, Common::Array<ScriptValue>
 		return returnValue;
 	}
 
-	case kIsVisibleMethod: {
-		assert(args.empty());
-		returnValue.setToBool(_isShowing);
-		return returnValue;
-	}
-
-	case kSpatialCenterMoveToMethod: {
-		assert(args.size() == 2);
-		int x = static_cast<int>(args[0].asFloat());
-		int y = static_cast<int>(args[1].asFloat());
-		spatialCenterMoveTo(x, y);
-		return returnValue;
-	}
-
-	case kSpatialMoveToMethod: {
-		assert(args.size() == 2);
-		int x = static_cast<int>(args[0].asFloat());
-		int y = static_cast<int>(args[1].asFloat());
-		spatialMoveTo(x, y);
-		return returnValue;
-	}
-
 	case kIsPlayingMethod: {
 		assert(args.empty());
 		returnValue.setToBool(_isPlaying);
 		return returnValue;
 	}
 
-	case kXPositionMethod: {
+	case kGetLeftXMethod: {
 		assert(args.empty());
 		double left = static_cast<double>(_header->_boundingBox.left);
 		returnValue.setToFloat(left);
@@ -249,7 +227,7 @@ ScriptValue Movie::callMethod(BuiltInMethod methodId, Common::Array<ScriptValue>
 
 	}
 
-	case kYPositionMethod: {
+	case kGetTopYMethod: {
 		assert(args.empty());
 		double top = static_cast<double>(_header->_boundingBox.top);
 		returnValue.setToFloat(top);
@@ -263,7 +241,7 @@ ScriptValue Movie::callMethod(BuiltInMethod methodId, Common::Array<ScriptValue>
 	}
 
 	default:
-		error("Movie::callMethod(): Got unimplemented method ID %s (%d)", builtInMethodToStr(methodId), static_cast<uint>(methodId));
+		return SpatialEntity::callMethod(methodId, args);
 	}
 }
 
@@ -373,48 +351,6 @@ void Movie::timeStop() {
 	runEventHandlerIfExists(kMovieStoppedEvent);
 }
 
-void Movie::spatialCenterMoveTo(int x, int y) {
-	// Mark the previous location dirty.
-	for (MovieFrame *frame : _framesOnScreen) {
-		Common::Rect bbox = getFrameBoundingBox(frame);
-		g_engine->_dirtyRects.push_back(bbox);
-	}
-
-	// Calculate the center of the movie, and update location.
-	int frameWidth = _header->_boundingBox.width();
-	int frameHeight = _header->_boundingBox.height();
-	int centerX = x - frameWidth / 2;
-	int centerY = y - frameHeight / 2;
-
-	debugC(5, kDebugScript, "Movie::callMethod(): (%d) Moving movie center to (%d, %d)", _header->_id, x, y);
-	// Unlike the sprites, movie bounding boxes must be moved too.
-	_header->_boundingBox.moveTo(centerX, centerY);
-
-	// Mark the new location dirty.
-	for (MovieFrame *frame : _framesOnScreen) {
-		Common::Rect bbox = getFrameBoundingBox(frame);
-		g_engine->_dirtyRects.push_back(bbox);
-	}
-}
-
-void Movie::spatialMoveTo(int x, int y) {
-	// Mark the previous location dirty.
-	for (MovieFrame *frame : _framesOnScreen) {
-		Common::Rect bbox = getFrameBoundingBox(frame);
-		g_engine->_dirtyRects.push_back(bbox);
-	}
-
-	// Update the location and mark the new location dirty.
-	debugC(5, kDebugGraphics, "Movie::callMethod(): (%d) Moving movie to (%d, %d)", _header->_id, x, y);
-	// Unlike the sprites, movie bounding boxes must be moved too.
-	_header->_boundingBox.moveTo(x, y);
-
-	for (MovieFrame *frame : _framesOnScreen) {
-		Common::Rect bbox = getFrameBoundingBox(frame);
-		g_engine->_dirtyRects.push_back(bbox);
-	}
-}
-
 void Movie::process() {
 	if (_isPlaying) {
 		processTimeEventHandlers();
diff --git a/engines/mediastation/assets/movie.h b/engines/mediastation/assets/movie.h
index 514f4f57f74..2dbdc1a9977 100644
--- a/engines/mediastation/assets/movie.h
+++ b/engines/mediastation/assets/movie.h
@@ -89,7 +89,7 @@ enum MovieSectionType {
 	kMovieFooterSection = 0x06aa
 };
 
-class Movie : public Asset {
+class Movie : public SpatialEntity {
 public:
 	Movie(AssetHeader *header);
 	virtual ~Movie() override;
@@ -102,6 +102,8 @@ public:
 
 	virtual void redraw(Common::Rect &rect) override;
 
+	virtual bool isVisible() const override { return _isShowing; }
+
 private:
 	bool _showByDefault = false;
 	bool _isShowing = false;
@@ -121,8 +123,6 @@ private:
 	void timeStop();
 	void spatialShow();
 	void spatialHide();
-	void spatialCenterMoveTo(int x, int y);
-	void spatialMoveTo(int x, int y);
 
 	void updateFrameState();
 	void showPersistentFrame();
diff --git a/engines/mediastation/assets/palette.cpp b/engines/mediastation/assets/palette.cpp
index d8b00ce04e8..c55e78ee6ee 100644
--- a/engines/mediastation/assets/palette.cpp
+++ b/engines/mediastation/assets/palette.cpp
@@ -25,9 +25,5 @@
 
 namespace MediaStation {
 
-ScriptValue Palette::callMethod(BuiltInMethod methodId, Common::Array<ScriptValue> &args) {
-	error("Palette::callMethod(): Got unimplemented method ID %s (%d)", builtInMethodToStr(methodId), static_cast<uint>(methodId));
-}
-
 } // End of namespace MediaStation
 
diff --git a/engines/mediastation/assets/palette.h b/engines/mediastation/assets/palette.h
index d960792aaa1..b3dd3114c70 100644
--- a/engines/mediastation/assets/palette.h
+++ b/engines/mediastation/assets/palette.h
@@ -32,8 +32,6 @@ namespace MediaStation {
 class Palette : public Asset {
 public:
 	Palette(AssetHeader *header) : Asset(header) {};
-
-	virtual ScriptValue callMethod(BuiltInMethod methodId, Common::Array<ScriptValue> &args) override;
 };
 
 } // End of namespace MediaStation
diff --git a/engines/mediastation/assets/path.cpp b/engines/mediastation/assets/path.cpp
index 6f3f6496672..10e637cf38f 100644
--- a/engines/mediastation/assets/path.cpp
+++ b/engines/mediastation/assets/path.cpp
@@ -64,7 +64,7 @@ ScriptValue Path::callMethod(BuiltInMethod methodId, Common::Array<ScriptValue>
 	}
 
 	default:
-		error("Path::callMethod(): Got unimplemented method ID %s (%d)", builtInMethodToStr(methodId), static_cast<uint>(methodId));
+		return Asset::callMethod(methodId, args);
 	}
 }
 
diff --git a/engines/mediastation/assets/screen.cpp b/engines/mediastation/assets/screen.cpp
index 13238178b09..c9b6d131e52 100644
--- a/engines/mediastation/assets/screen.cpp
+++ b/engines/mediastation/assets/screen.cpp
@@ -24,8 +24,4 @@
 
 namespace MediaStation {
 
-ScriptValue Screen::callMethod(BuiltInMethod methodId, Common::Array<ScriptValue> &args) {
-	error("Screen::callMethod(): Got unimplemented method ID %s (%d)", builtInMethodToStr(methodId), static_cast<uint>(methodId));
-}
-
 } // End of namespace MediaStation
diff --git a/engines/mediastation/assets/screen.h b/engines/mediastation/assets/screen.h
index 7a8c16f1732..c5846240361 100644
--- a/engines/mediastation/assets/screen.h
+++ b/engines/mediastation/assets/screen.h
@@ -35,8 +35,6 @@ namespace MediaStation {
 class Screen : public Asset {
 public:
 	Screen(AssetHeader *header) : Asset(header) {};
-
-	virtual ScriptValue callMethod(BuiltInMethod methodId, Common::Array<ScriptValue> &args) override;
 };
 
 } // End of namespace MediaStation
diff --git a/engines/mediastation/assets/sound.cpp b/engines/mediastation/assets/sound.cpp
index b3467d51237..100fb9a2f31 100644
--- a/engines/mediastation/assets/sound.cpp
+++ b/engines/mediastation/assets/sound.cpp
@@ -72,7 +72,7 @@ ScriptValue Sound::callMethod(BuiltInMethod methodId, Common::Array<ScriptValue>
 	}
 
 	default:
-		error("Sound::callMethod(): Got unimplemented method %s (%d)", builtInMethodToStr(methodId), static_cast<uint>(methodId));
+		return Asset::callMethod(methodId, args);
 	}
 }
 
diff --git a/engines/mediastation/assets/sprite.cpp b/engines/mediastation/assets/sprite.cpp
index ec2b9dd652e..575ee5d82f4 100644
--- a/engines/mediastation/assets/sprite.cpp
+++ b/engines/mediastation/assets/sprite.cpp
@@ -60,7 +60,7 @@ uint32 SpriteFrame::index() {
 	return _bitmapHeader->_index;
 }
 
-Sprite::Sprite(AssetHeader *header) : Asset(header) {
+Sprite::Sprite(AssetHeader *header) : SpatialEntity(header) {
 	if (header->_startup == kAssetStartupActive) {
 		setActive();
 		_isShowing = true;
@@ -135,31 +135,8 @@ ScriptValue Sprite::callMethod(BuiltInMethod methodId, Common::Array<ScriptValue
 		return returnValue;
 	}
 
-	case kSpatialMoveToMethod: {
-		assert(args.size() == 2);
-
-		// Mark the previous location dirty.
-		if (_activeFrame != nullptr) {
-			g_engine->_dirtyRects.push_back(getActiveFrameBoundingBox());
-		}
-
-		// Update the location and mark the new location dirty.
-		int newXAdjust = static_cast<int>(args[0].asFloat());
-		int newYAdjust = static_cast<int>(args[1].asFloat());
-		if (_xAdjust != newXAdjust || _yAdjust != newYAdjust) {
-			debugC(5, kDebugGraphics, "Sprite::callMethod(): (%d) Moving sprite to (%d, %d)", _header->_id, newXAdjust, newYAdjust);
-			_xAdjust = newXAdjust;
-			_yAdjust = newYAdjust;
-			if (_activeFrame != nullptr) {
-				g_engine->_dirtyRects.push_back(getActiveFrameBoundingBox());
-			}
-		}
-
-		return returnValue;
-	}
-
 	default:
-		error("Sprite::callMethod(): Got unimplemented method ID %s (%d)", builtInMethodToStr(methodId), static_cast<uint>(methodId));
+		return SpatialEntity::callMethod(methodId, args);
 	}
 }
 
@@ -322,7 +299,7 @@ void Sprite::redraw(Common::Rect &rect) {
 	Common::Rect areaToRedraw = bbox.findIntersectingRect(rect);
 	if (!areaToRedraw.isEmpty()) {
 		Common::Point originOnScreen(areaToRedraw.left, areaToRedraw.top);
-		areaToRedraw.translate(-_activeFrame->left() - _header->_boundingBox.left - _xAdjust, -_activeFrame->top() - _header->_boundingBox.top - _yAdjust);
+		areaToRedraw.translate(-_activeFrame->left() - _header->_boundingBox.left, -_activeFrame->top() - _header->_boundingBox.top);
 		areaToRedraw.clip(Common::Rect(0, 0, _activeFrame->width(), _activeFrame->height()));
 		g_engine->_screen->simpleBlitFrom(_activeFrame->_surface, areaToRedraw, originOnScreen);
 	}
@@ -345,7 +322,7 @@ 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 + _xAdjust, _header->_boundingBox.top + _yAdjust);
+	bbox.translate(_header->_boundingBox.left, _header->_boundingBox.top);
 	return bbox;
 }
 
diff --git a/engines/mediastation/assets/sprite.h b/engines/mediastation/assets/sprite.h
index d90b8506349..8994b412250 100644
--- a/engines/mediastation/assets/sprite.h
+++ b/engines/mediastation/assets/sprite.h
@@ -59,17 +59,20 @@ private:
 
 // 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 {
+class Sprite : public SpatialEntity {
 friend class Context;
 
 public:
 	Sprite(AssetHeader *header);
 	~Sprite();
 
-	virtual ScriptValue callMethod(BuiltInMethod methodId, Common::Array<ScriptValue> &args) override;
 	virtual void process() override;
 	virtual void redraw(Common::Rect &rect) override;
 
+	virtual ScriptValue callMethod(BuiltInMethod methodId, Common::Array<ScriptValue> &args) override;
+
+	virtual bool isVisible() const override { return _isShowing; }
+
 	virtual void readChunk(Chunk &chunk) override;
 
 private:
@@ -81,9 +84,6 @@ private:
 	uint _currentFrameIndex = 0;
 	uint _nextFrameTime = 0;
 
-	int _xAdjust = 0;
-	int _yAdjust = 0;
-
 	// Method implementations.
 	void spatialShow();
 	void spatialHide();
diff --git a/engines/mediastation/assets/text.cpp b/engines/mediastation/assets/text.cpp
index 2b7ef861b01..59dead1d9c2 100644
--- a/engines/mediastation/assets/text.cpp
+++ b/engines/mediastation/assets/text.cpp
@@ -52,7 +52,7 @@ ScriptValue Text::callMethod(BuiltInMethod methodId, Common::Array<ScriptValue>
 	}
 
 	default:
-		error("Text::callMethod(): Got unimplemented method ID %s (%d)", builtInMethodToStr(methodId), static_cast<uint>(methodId));
+		return SpatialEntity::callMethod(methodId, args);
 	}
 }
 
diff --git a/engines/mediastation/assets/text.h b/engines/mediastation/assets/text.h
index 5ba2f9be7a8..e7dd97bb690 100644
--- a/engines/mediastation/assets/text.h
+++ b/engines/mediastation/assets/text.h
@@ -31,13 +31,16 @@
 
 namespace MediaStation {
 
-class Text : public Asset {
+class Text : public SpatialEntity {
 public:
-	Text(AssetHeader *header) : Asset(header) {};
+	Text(AssetHeader *header) : SpatialEntity(header) {};
 
+	virtual bool isVisible() const override { return _isVisible; }
 	virtual ScriptValue callMethod(BuiltInMethod methodId, Common::Array<ScriptValue> &args) override;
 
 private:
+	bool _isVisible = false;
+
 	// Method implementations.
 	Common::String text() const;
 	void setText(Common::String text);
diff --git a/engines/mediastation/assets/timer.cpp b/engines/mediastation/assets/timer.cpp
index 38a9c0debc2..284f971510e 100644
--- a/engines/mediastation/assets/timer.cpp
+++ b/engines/mediastation/assets/timer.cpp
@@ -49,7 +49,7 @@ ScriptValue Timer::callMethod(BuiltInMethod methodId, Common::Array<ScriptValue>
 	}
 
 	default:
-		error("Timer::callMethod(): Got unimplemented method ID %s (%d)", builtInMethodToStr(methodId), static_cast<uint>(methodId));
+		return Asset::callMethod(methodId, args);
 	}
 }
 
diff --git a/engines/mediastation/context.cpp b/engines/mediastation/context.cpp
index 4857e95593d..5fbd406c925 100644
--- a/engines/mediastation/context.cpp
+++ b/engines/mediastation/context.cpp
@@ -81,12 +81,12 @@ Context::Context(const Common::Path &path) : Datafile(path) {
 		Asset *asset = it->_value;
 		uint referencedAssetId = asset->getHeader()->_assetReference;
 		if (referencedAssetId != 0) {
-			switch (asset->getHeader()->_type) {
+			switch (asset->type()) {
 			case kAssetTypeImage: {
 				Image *image = static_cast<Image *>(asset);
 				Image *referencedImage = static_cast<Image *>(getAssetById(referencedAssetId));
 				if (referencedImage == nullptr) {
-					error("Context::Context(): Asset %d references non-existent asset %d", asset->getHeader()->_id, referencedAssetId);
+					error("Context::Context(): Asset %d references non-existent asset %d", asset->id(), referencedAssetId);
 				}
 				image->_bitmap = referencedImage->_bitmap;
 				break;
@@ -96,14 +96,14 @@ Context::Context(const Common::Path &path) : Datafile(path) {
 				Sprite *sprite = static_cast<Sprite *>(asset);
 				Sprite *referencedSprite = static_cast<Sprite *>(getAssetById(referencedAssetId));
 				if (referencedSprite == nullptr) {
-					error("Context::Context(): Asset %d references non-existent asset %d", asset->getHeader()->_id, referencedAssetId);
+					error("Context::Context(): Asset %d references non-existent asset %d", asset->id(), referencedAssetId);
 				}
 				sprite->_frames = referencedSprite->_frames;
 				break;
 			}
 
 			default:
-				error("Context::Context(): Asset type %d referenced, but reference not implemented yet", asset->getHeader()->_type);
+				error("Context::Context(): Asset type %d referenced, but reference not implemented yet", asset->type());
 			}
 		}
 	}
diff --git a/engines/mediastation/mediascript/scriptconstants.cpp b/engines/mediastation/mediascript/scriptconstants.cpp
index 62616d2f6cd..966e1e33e49 100644
--- a/engines/mediastation/mediascript/scriptconstants.cpp
+++ b/engines/mediastation/mediascript/scriptconstants.cpp
@@ -153,20 +153,20 @@ const char *builtInMethodToStr(BuiltInMethod method) {
 		return "MouseActivate";
 	case kMouseDeactivateMethod:
 		return "MouseDeactivate";
-	case kXPositionMethod:
-		return "XPosition";
-	case kYPositionMethod:
-		return "YPosition";
+	case kGetLeftXMethod:
+		return "GetLeftX";
+	case kGetTopYMethod:
+		return "GetTopY";
 	case kTriggerAbsXPositionMethod:
 		return "TriggerAbsXPosition";
 	case kTriggerAbsYPositionMethod:
 		return "TriggerAbsYPosition";
 	case kIsActiveMethod:
 		return "IsActive";
-	case kWidthMethod:
-		return "Width";
-	case kHeightMethod:
-		return "Height";
+	case kGetWidthMethod:
+		return "GetWidth";
+	case kGetHeightMethod:
+		return "GetHeight";
 	case kIsVisibleMethod:
 		return "IsVisible";
 	case kMovieResetMethod:
diff --git a/engines/mediastation/mediascript/scriptconstants.h b/engines/mediastation/mediascript/scriptconstants.h
index edd8d86ce63..ae2f70a3f9c 100644
--- a/engines/mediastation/mediascript/scriptconstants.h
+++ b/engines/mediastation/mediascript/scriptconstants.h
@@ -89,8 +89,11 @@ enum BuiltInMethod {
 	// Currently it's only in var_7be1_cursor_currentTool in
 	// IBM/Crayola.
 	kCursorSetMethod = 200, // PARAMS: 0
+
+	// SPATIAL ENTITY METHODS.
 	kSpatialHideMethod = 203, // PARAMS: 1
 	kSpatialMoveToMethod = 204, // PARAMS: 2
+	kSpatialMoveToByOffsetMethod = 205, // PARAMS: 2
 	kSpatialZMoveToMethod = 216, // PARAMS: 1
 	kSpatialShowMethod = 202, // PARAMS: 1
 	kTimePlayMethod = 206, // PARAMS: 1
@@ -98,21 +101,25 @@ enum BuiltInMethod {
 	kIsPlayingMethod = 372, // PARAMS: 0
 	kSetDissolveFactorMethod = 241, // PARAMS: 1
 	kSpatialCenterMoveToMethod = 230,
+	kGetLeftXMethod = 233,
+	kGetTopYMethod = 234,
+	kGetWidthMethod = 235, // PARAMS: 0
+	kGetHeightMethod = 236, // PARAMS: 0
+	kGetCenterXMethod = 237,
+	kGetCenterYMethod = 238,
+	kGetZCoordinateMethod = 239,
+	kIsPointInsideMethod = 246,
+	kGetMouseXOffsetMethod = 264,
+	kGetMouseYOffsetMethod = 265,
+	kIsVisibleMethod = 269,
 
 	// HOTSPOT METHODS.
 	kMouseActivateMethod = 210, // PARAMS: 1
 	kMouseDeactivateMethod = 211, // PARAMS: 0
-	kXPositionMethod = 233, // PARAMS: 0
-	kYPositionMethod = 234, // PARAMS: 0
 	kTriggerAbsXPositionMethod = 321, // PARAMS: 0
 	kTriggerAbsYPositionMethod = 322, // PARAMS: 0
 	kIsActiveMethod = 371, // PARAMS: 0
 
-	// IMAGE METHODS.
-	kWidthMethod = 235, // PARAMS: 0
-	kHeightMethod = 236, // PARAMS: 0
-	kIsVisibleMethod = 269,
-
 	// SPRITE METHODS.
 	kMovieResetMethod = 219, // PARAMS: 0
 	kSetSpriteFrameByIdMethod = 220, // PARAMS: 1
diff --git a/engines/mediastation/mediastation.cpp b/engines/mediastation/mediastation.cpp
index 5bfe0511b8f..7001e7edf8e 100644
--- a/engines/mediastation/mediastation.cpp
+++ b/engines/mediastation/mediastation.cpp
@@ -226,7 +226,7 @@ void MediaStationEngine::processEvents() {
 			// Even though this is a keydown event, we need to look at the mouse position.
 			Asset *hotspot = findAssetToAcceptMouseEvents();
 			if (hotspot != nullptr) {
-				debugC(1, kDebugEvents, "EVENT_KEYDOWN (%d): Sent to hotspot %d", _event.kbd.ascii, hotspot->getHeader()->_id);
+				debugC(1, kDebugEvents, "EVENT_KEYDOWN (%d): Sent to hotspot %d", _event.kbd.ascii, hotspot->id());
 				ScriptValue keyCode;
 				keyCode.setToFloat(_event.kbd.ascii);
 				hotspot->runEventHandlerIfExists(kKeyDownEvent, keyCode);
@@ -237,7 +237,7 @@ void MediaStationEngine::processEvents() {
 		case Common::EVENT_LBUTTONDOWN: {
 			Asset *hotspot = findAssetToAcceptMouseEvents();
 			if (hotspot != nullptr) {
-				debugC(1, kDebugEvents, "EVENT_LBUTTONDOWN (%d, %d): Sent to hotspot %d", _mousePos.x, _mousePos.y, hotspot->getHeader()->_id);
+				debugC(1, kDebugEvents, "EVENT_LBUTTONDOWN (%d, %d): Sent to hotspot %d", _mousePos.x, _mousePos.y, hotspot->id());
 				hotspot->runEventHandlerIfExists(kMouseDownEvent);
 			}
 			break;
@@ -272,11 +272,11 @@ void MediaStationEngine::refreshActiveHotspot() {
 	if (hotspot != _currentHotspot) {
 		if (_currentHotspot != nullptr) {
 			_currentHotspot->runEventHandlerIfExists(kMouseExitedEvent);
-			debugC(5, kDebugEvents, "refreshActiveHotspot(): (%d, %d): Exited hotspot %d", _mousePos.x, _mousePos.y, _currentHotspot->getHeader()->_id);
+			debugC(5, kDebugEvents, "refreshActiveHotspot(): (%d, %d): Exited hotspot %d", _mousePos.x, _mousePos.y, _currentHotspot->id());
 		}
 		_currentHotspot = hotspot;
 		if (hotspot != nullptr) {
-			debugC(5, kDebugEvents, "refreshActiveHotspot(): (%d, %d): Entered hotspot %d", _mousePos.x, _mousePos.y, hotspot->getHeader()->_id);
+			debugC(5, kDebugEvents, "refreshActiveHotspot(): (%d, %d): Entered hotspot %d", _mousePos.x, _mousePos.y, hotspot->id());
 			setCursor(hotspot->getHeader()->_cursorResourceId);
 			hotspot->runEventHandlerIfExists(kMouseEnteredEvent);
 		} else {
@@ -286,7 +286,7 @@ void MediaStationEngine::refreshActiveHotspot() {
 	}
 
 	if (hotspot != nullptr) {
-		debugC(5, kDebugEvents, "refreshActiveHotspot(): (%d, %d): Sent to hotspot %d", _mousePos.x, _mousePos.y, hotspot->getHeader()->_id);
+		debugC(5, kDebugEvents, "refreshActiveHotspot(): (%d, %d): Sent to hotspot %d", _mousePos.x, _mousePos.y, hotspot->id());
 		hotspot->runEventHandlerIfExists(kMouseMovedEvent);
 	}
 }
@@ -297,15 +297,23 @@ void MediaStationEngine::redraw() {
 	}
 
 	Common::sort(_assetsPlaying.begin(), _assetsPlaying.end(), [](Asset * a, Asset * b) {
-		return a->zIndex() > b->zIndex();
+		if (!a->isSpatialActor() || !b->isSpatialActor()) {
+			return false;
+		}
+		return static_cast<SpatialEntity *>(a)->zIndex() > static_cast<SpatialEntity *>(b)->zIndex();
 	});
 
 	for (Common::Rect dirtyRect : _dirtyRects) {
 		for (Asset *asset : _assetsPlaying) {
-			Common::Rect bbox = asset->getBbox();
+			if (!asset->isSpatialActor()) {
+				continue;
+			}
+
+			SpatialEntity *entity = static_cast<SpatialEntity *>(asset);
+			Common::Rect bbox = entity->getBbox();
 			if (!bbox.isEmpty()) {
 				if (dirtyRect.intersects(bbox)) {
-					asset->redraw(dirtyRect);
+					entity->redraw(dirtyRect);
 				}
 			}
 		}
@@ -420,7 +428,7 @@ ScriptValue MediaStationEngine::callMethod(BuiltInMethod methodId, Common::Array
 void MediaStationEngine::doBranchToScreen() {
 	if (_currentContext != nullptr) {
 		_currentContext->_screenAsset->runEventHandlerIfExists(kExitEvent);
-		releaseContext(_currentContext->_screenAsset->getHeader()->_id);
+		releaseContext(_currentContext->_screenAsset->id());
 	}
 
 	Context *context = loadContext(_requestedScreenBranchId);
@@ -456,7 +464,7 @@ void MediaStationEngine::releaseContext(uint32 contextId) {
 	// Unload any assets currently playing from this context. They should have
 	// already been stopped by scripts, but this is a last check.
 	for (auto it = _assetsPlaying.begin(); it != _assetsPlaying.end();) {
-		uint assetId = (*it)->getHeader()->_id;
+		uint assetId = (*it)->id();
 		Asset *asset = context->getAssetById(assetId);
 		if (asset != nullptr) {
 			it = _assetsPlaying.erase(it);
@@ -477,10 +485,11 @@ Asset *MediaStationEngine::findAssetToAcceptMouseEvents() {
 
 	for (Asset *asset : _assetsPlaying) {
 		if (asset->type() == kAssetTypeHotspot) {
-			debugC(5, kDebugGraphics, "findAssetToAcceptMouseEvents(): Hotspot %d (z-index %d)", asset->getHeader()->_id, asset->zIndex());
-			if (asset->isActive() && static_cast<Hotspot *>(asset)->isInside(_mousePos)) {
-				if (asset->zIndex() < lowestZIndex) {
-					lowestZIndex = asset->zIndex();
+			Hotspot *hotspot = static_cast<Hotspot *>(asset);
+			debugC(5, kDebugGraphics, "findAssetToAcceptMouseEvents(): Hotspot %d (z-index %d)", hotspot->id(), hotspot->zIndex());
+			if (hotspot->isActive() && hotspot->isInside(_mousePos)) {
+				if (hotspot->zIndex() < lowestZIndex) {
+					lowestZIndex = hotspot->zIndex();
 					intersectingAsset = asset;
 				}
 			}


Commit: 06c02db20893622b21c83a2b13dfa2cc520ef7cd
    https://github.com/scummvm/scummvm/commit/06c02db20893622b21c83a2b13dfa2cc520ef7cd
Author: Nathanael Gentry (nathanael.gentrydb8 at gmail.com)
Date: 2025-06-14T11:07:14-04:00

Commit Message:
MEDIASTATION: Store asset-specific fields in the assets themselves

Rather than putting all these fields in a generic asset header type.

Changed paths:
  R engines/mediastation/assetheader.cpp
  R engines/mediastation/assetheader.h
  R engines/mediastation/assets/stage.h
    engines/mediastation/asset.cpp
    engines/mediastation/asset.h
    engines/mediastation/assets/canvas.cpp
    engines/mediastation/assets/canvas.h
    engines/mediastation/assets/font.cpp
    engines/mediastation/assets/font.h
    engines/mediastation/assets/hotspot.cpp
    engines/mediastation/assets/hotspot.h
    engines/mediastation/assets/image.cpp
    engines/mediastation/assets/image.h
    engines/mediastation/assets/movie.cpp
    engines/mediastation/assets/movie.h
    engines/mediastation/assets/palette.cpp
    engines/mediastation/assets/palette.h
    engines/mediastation/assets/path.cpp
    engines/mediastation/assets/path.h
    engines/mediastation/assets/screen.cpp
    engines/mediastation/assets/screen.h
    engines/mediastation/assets/sound.cpp
    engines/mediastation/assets/sound.h
    engines/mediastation/assets/sprite.cpp
    engines/mediastation/assets/sprite.h
    engines/mediastation/assets/text.cpp
    engines/mediastation/assets/text.h
    engines/mediastation/assets/timer.cpp
    engines/mediastation/assets/timer.h
    engines/mediastation/bitmap.h
    engines/mediastation/context.cpp
    engines/mediastation/context.h
    engines/mediastation/mediastation.cpp
    engines/mediastation/mediastation.h
    engines/mediastation/module.mk


diff --git a/engines/mediastation/asset.cpp b/engines/mediastation/asset.cpp
index e8f26f8f285..028241aa347 100644
--- a/engines/mediastation/asset.cpp
+++ b/engines/mediastation/asset.cpp
@@ -19,15 +19,56 @@
  *
  */
 
-#include "mediastation/debugchannels.h"
 #include "mediastation/asset.h"
+#include "mediastation/debugchannels.h"
 #include "mediastation/mediascript/scriptconstants.h"
+#include "mediastation/mediastation.h"
 
 namespace MediaStation {
 
 Asset::~Asset() {
-	delete _header;
-	_header = nullptr;
+	for (auto it = _eventHandlers.begin(); it != _eventHandlers.end(); ++it) {
+		Common::Array<EventHandler *> &handlersForType = it->_value;
+		for (EventHandler *handler : handlersForType) {
+			delete handler;
+		}
+		handlersForType.clear();
+	}
+	_eventHandlers.clear();
+}
+
+void Asset::initFromParameterStream(Chunk &chunk) {
+	AssetHeaderSectionType paramType = kAssetHeaderEmptySection;
+	while (true) {
+		paramType = static_cast<AssetHeaderSectionType>(chunk.readTypedUint16());
+		if (paramType == 0) {
+			break;
+		} else {
+			readParameter(chunk, paramType);
+		}
+	}
+}
+
+void Asset::readParameter(Chunk &chunk, AssetHeaderSectionType paramType) {
+	switch (paramType) {
+	case kAssetHeaderEventHandler: {
+		EventHandler *eventHandler = new EventHandler(chunk);
+		Common::Array<EventHandler *> &eventHandlersForType = _eventHandlers.getOrCreateVal(eventHandler->_type);
+
+		// This is not a hashmap because we don't want to have to hash ScriptValues.
+		for (EventHandler *existingEventHandler : eventHandlersForType) {
+			if (existingEventHandler->_argumentValue == eventHandler->_argumentValue) {
+				error("AssetHeader::readSection(): Event handler for %s (%s) already exists",
+					  eventTypeToStr(eventHandler->_type), eventHandler->getDebugHeader().c_str());
+			}
+		}
+		eventHandlersForType.push_back(eventHandler);
+		break;
+	}
+
+	default:
+		error("Got unimplemented asset parameter 0x%x", static_cast<uint>(paramType));
+	}
 }
 
 ScriptValue Asset::callMethod(BuiltInMethod methodId, Common::Array<ScriptValue> &args) {
@@ -35,11 +76,11 @@ ScriptValue Asset::callMethod(BuiltInMethod methodId, Common::Array<ScriptValue>
 }
 
 void Asset::readChunk(Chunk &chunk) {
-	error("Asset::readChunk(): Chunk reading for asset type 0x%x is not implemented", static_cast<uint>(_header->_type));
+	error("Asset::readChunk(): Chunk reading for asset type 0x%x is not implemented", static_cast<uint>(_type));
 }
 
 void Asset::readSubfile(Subfile &subfile, Chunk &chunk) {
-	error("Asset::readSubfile(): Subfile reading for asset type 0x%x is not implemented", static_cast<uint>(_header->_type));
+	error("Asset::readSubfile(): Subfile reading for asset type 0x%x is not implemented", static_cast<uint>(_type));
 }
 
 void Asset::setActive() {
@@ -57,13 +98,13 @@ void Asset::setInactive() {
 
 void Asset::processTimeEventHandlers() {
 	if (!_isActive) {
-		warning("Asset::processTimeEventHandlers(): Attempted to process time event handlers while asset %d is not playing", _header->_id);
+		warning("Asset::processTimeEventHandlers(): Attempted to process time event handlers while asset %d is not playing", _id);
 		return;
 	}
 
 	// TODO: Replace with a queue.
 	uint currentTime = g_system->getMillis();
-	const Common::Array<EventHandler *> &_timeHandlers = _header->_eventHandlers.getValOrDefault(kTimerEvent);
+	const Common::Array<EventHandler *> &_timeHandlers = _eventHandlers.getValOrDefault(kTimerEvent);
 	for (EventHandler *timeEvent : _timeHandlers) {
 		// Indeed float, not time.
 		double timeEventInFractionalSeconds = timeEvent->_argumentValue.asFloat();
@@ -72,30 +113,30 @@ void Asset::processTimeEventHandlers() {
 		bool timeEventNeedsToBeProcessed = timeEventInMilliseconds <= currentTime - _startTime;
 		if (!timeEventAlreadyProcessed && timeEventNeedsToBeProcessed) {
 			debugC(5, kDebugScript, "Asset::processTimeEventHandlers(): Running On Time handler for time %d ms", timeEventInMilliseconds);
-			timeEvent->execute(_header->_id);
+			timeEvent->execute(_id);
 		}
 	}
 	_lastProcessedTime = currentTime - _startTime;
 }
 
 void Asset::runEventHandlerIfExists(EventType eventType, const ScriptValue &arg) {
-	const Common::Array<EventHandler *> &_eventHandlers = _header->_eventHandlers.getValOrDefault(eventType);
-	for (EventHandler *eventHandler : _eventHandlers) {
+	const Common::Array<EventHandler *> &eventHandlers = _eventHandlers.getValOrDefault(eventType);
+	for (EventHandler *eventHandler : eventHandlers) {
 		const ScriptValue &argToCheck = eventHandler->_argumentValue;
 
 		if (arg.getType() != argToCheck.getType()) {
 			warning("Got event handler arg type %s, expected %s",
-				scriptValueTypeToStr(arg.getType()), scriptValueTypeToStr(argToCheck.getType()));
+					scriptValueTypeToStr(arg.getType()), scriptValueTypeToStr(argToCheck.getType()));
 			continue;
 		}
 
 		if (arg == argToCheck) {
-			debugC(5, kDebugScript, "Executing handler for event type %s on asset %d", eventTypeToStr(eventType), _header->_id);
-			eventHandler->execute(_header->_id);
+			debugC(5, kDebugScript, "Executing handler for event type %s on asset %d", eventTypeToStr(eventType), _id);
+			eventHandler->execute(_id);
 			return;
 		}
 	}
-	debugC(5, kDebugScript, "No event handler for event type %s on asset %d", eventTypeToStr(eventType), _header->_id);
+	debugC(5, kDebugScript, "No event handler for event type %s on asset %d", eventTypeToStr(eventType), _id);
 }
 
 void Asset::runEventHandlerIfExists(EventType eventType) {
@@ -118,10 +159,8 @@ ScriptValue SpatialEntity::callMethod(BuiltInMethod methodId, Common::Array<Scri
 		assert(args.size() == 2);
 		int16 dx = static_cast<int16>(args[0].asFloat());
 		int16 dy = static_cast<int16>(args[1].asFloat());
-
-		Common::Point currentPos = getTopLeft();
-		int16 newX = currentPos.x + dx;
-		int16 newY = currentPos.y + dy;
+		int16 newX = _boundingBox.left + dx;
+		int16 newY = _boundingBox.top + dy;
 		moveTo(newX, newY);
 		break;
 	}
@@ -143,39 +182,39 @@ ScriptValue SpatialEntity::callMethod(BuiltInMethod methodId, Common::Array<Scri
 
 	case kGetLeftXMethod:
 		assert(args.empty());
-		returnValue.setToFloat(_header->_boundingBox.left);
+		returnValue.setToFloat(_boundingBox.left);
 		break;
 
 	case kGetTopYMethod:
 		assert(args.empty());
-		returnValue.setToFloat(_header->_boundingBox.top);
+		returnValue.setToFloat(_boundingBox.top);
 		break;
 
 	case kGetWidthMethod:
 		assert(args.empty());
-		returnValue.setToFloat(_header->_boundingBox.width());
+		returnValue.setToFloat(_boundingBox.width());
 		break;
 
 	case kGetHeightMethod:
 		assert(args.empty());
-		returnValue.setToFloat(_header->_boundingBox.height());
+		returnValue.setToFloat(_boundingBox.height());
 		break;
 
 	case kGetCenterXMethod: {
-		int centerX = _header->_boundingBox.left + (_header->_boundingBox.width() / 2);
+		int centerX = _boundingBox.left + (_boundingBox.width() / 2);
 		returnValue.setToFloat(centerX);
 		break;
 	}
 
 	case kGetCenterYMethod: {
-		int centerY = _header->_boundingBox.top + (_header->_boundingBox.height() / 2);
+		int centerY = _boundingBox.top + (_boundingBox.height() / 2);
 		returnValue.setToFloat(centerY);
 		break;
 	}
 
 	case kGetZCoordinateMethod:
 		assert(args.empty());
-		returnValue.setToFloat(_header->_zIndex);
+		returnValue.setToFloat(_zIndex);
 		break;
 
 	case kIsVisibleMethod:
@@ -189,9 +228,43 @@ ScriptValue SpatialEntity::callMethod(BuiltInMethod methodId, Common::Array<Scri
 	return returnValue;
 }
 
+void SpatialEntity::readParameter(Chunk &chunk, AssetHeaderSectionType paramType) {
+	switch (paramType) {
+	case kAssetHeaderBoundingBox:
+		_boundingBox = chunk.readTypedRect();
+		break;
+
+	case kAssetHeaderZIndex:
+		_zIndex = chunk.readTypedGraphicUnit();
+		break;
+
+	case kAssetHeaderStartup:
+		_isVisible = static_cast<bool>(chunk.readTypedByte());
+		if (_isVisible) {
+			setActive();
+		}
+		break;
+
+	case kAssetHeaderTransparency:
+		_hasTransparency = static_cast<bool>(chunk.readTypedByte());
+		break;
+
+	case kAssetHeaderStageId:
+		_stageId = chunk.readTypedUint16();
+		break;
+
+	case kAssetHeaderAssetReference:
+		_assetReference = chunk.readTypedUint16();
+		break;
+
+	default:
+		Asset::readParameter(chunk, paramType);
+	}
+}
+
 void SpatialEntity::moveTo(int16 x, int16 y) {
 	Common::Point dest(x, y);
-	if (dest == getTopLeft()) {
+	if (dest == _boundingBox.origin()) {
 		// We aren't actually moving anywhere.
 		return;
 	}
@@ -199,20 +272,20 @@ void SpatialEntity::moveTo(int16 x, int16 y) {
 	if (isVisible()) {
 		invalidateLocalBounds();
 	}
-	_header->_boundingBox.moveTo(dest);
+	_boundingBox.moveTo(dest);
 	if (isVisible()) {
 		invalidateLocalBounds();
 	}
 }
 
 void SpatialEntity::moveToCentered(int16 x, int16 y) {
-	int16 targetX = x - (_header->_boundingBox.width() / 2);
-	int16 targetY = y - (_header->_boundingBox.height() / 2);
+	int16 targetX = x - (_boundingBox.width() / 2);
+	int16 targetY = y - (_boundingBox.height() / 2);
 	moveTo(targetX, targetY);
 }
 
 void SpatialEntity::setBounds(const Common::Rect &bounds) {
-	if (_header->_boundingBox == bounds) {
+	if (_boundingBox == bounds) {
 		// We aren't actually moving anywhere.
 		return;
 	}
@@ -220,24 +293,24 @@ void SpatialEntity::setBounds(const Common::Rect &bounds) {
 	if (isVisible()) {
 		invalidateLocalBounds();
 	}
-	_header->_boundingBox = bounds;
+	_boundingBox = bounds;
 	if (isVisible()) {
 		invalidateLocalBounds();
 	}
 }
 
 void SpatialEntity::setZIndex(int zIndex) {
-	if (_header->_zIndex == zIndex) {
+	if (_zIndex == zIndex) {
 		// We aren't actually moving anywhere.
 		return;
 	}
 
-	_header->_zIndex = zIndex;
+	_zIndex = zIndex;
 	invalidateLocalZIndex();
 }
 
 void SpatialEntity::invalidateLocalBounds() {
-	g_engine->_dirtyRects.push_back(_header->_boundingBox);
+	g_engine->_dirtyRects.push_back(_boundingBox);
 }
 
 void SpatialEntity::invalidateLocalZIndex() {
diff --git a/engines/mediastation/asset.h b/engines/mediastation/asset.h
index 390c7a23de5..f4f27c09445 100644
--- a/engines/mediastation/asset.h
+++ b/engines/mediastation/asset.h
@@ -24,19 +24,108 @@
 
 #include "common/keyboard.h"
 
-#include "mediastation/mediastation.h"
 #include "mediastation/datafile.h"
+#include "mediastation/mediascript/eventhandler.h"
 #include "mediastation/mediascript/scriptconstants.h"
 #include "mediastation/mediascript/scriptvalue.h"
-#include "mediastation/assetheader.h"
 
 namespace MediaStation {
 
-class AssetHeader;
+enum AssetType {
+	kAssetTypeEmpty = 0x0000,
+	kAssetTypeScreen = 0x0001, // SCR
+	kAssetTypeStage = 0x0002, // STG
+	kAssetTypePath = 0x0004, // PTH
+	kAssetTypeSound = 0x0005, // SND
+	kAssetTypeTimer = 0x0006, // TMR
+	kAssetTypeImage = 0x0007, // IMG
+	kAssetTypeHotspot = 0x000b, // HSP
+	kAssetTypeSprite = 0x000e, // SPR
+	kAssetTypeLKZazu = 0x000f,
+	kAssetTypeLKConstellations = 0x0010,
+	kAssetTypeImageSet = 0x001d,
+	kAssetTypeCursor = 0x000c, // CSR
+	kAssetTypePrinter = 0x0019, // PRT
+	kAssetTypeMovie = 0x0016, // MOV
+	kAssetTypePalette = 0x0017,
+	kAssetTypeText = 0x001a, // TXT
+	kAssetTypeFont = 0x001b, // FON
+	kAssetTypeCamera = 0x001c, // CAM
+	kAssetTypeCanvas = 0x001e, // CVS
+	kAssetTypeXsnd = 0x001f,
+	kAssetTypeXsndMidi = 0x0020,
+	kAssetTypeRecorder = 0x0021,
+	kAssetTypeFunction = 0x0069 // FUN
+};
+
+enum AssetHeaderSectionType {
+	kAssetHeaderEmptySection = 0x0000,
+	kAssetHeaderSoundEncoding1 = 0x0001,
+	kAssetHeaderSoundEncoding2 = 0x0002,
+	kAssetHeaderEventHandler = 0x0017,
+	kAssetHeaderStageId = 0x0019,
+	kAssetHeaderAssetId = 0x001a,
+	kAssetHeaderChunkReference = 0x001b,
+	kAssetHeaderMovieAnimationChunkReference = 0x06a4,
+	kAssetHeaderMovieAudioChunkReference = 0x06a5,
+	kAssetHeaderAssetReference = 0x077b,
+	kAssetHeaderBoundingBox = 0x001c,
+	kAssetHeaderMouseActiveArea = 0x001d,
+	kAssetHeaderZIndex = 0x001e,
+	kAssetHeaderStartup = 0x001f,
+	kAssetHeaderTransparency = 0x0020,
+	kAssetHeaderHasOwnSubfile = 0x0021,
+	kAssetHeaderCursorResourceId = 0x0022,
+	kAssetHeaderFrameRate = 0x0024,
+	kAssetHeaderLoadType = 0x0032,
+	kAssetHeaderSoundInfo = 0x0033,
+	kAssetHeaderMovieLoadType = 0x0037,
+	kAssetHeaderSpriteChunkCount = 0x03e8,
+	kAssetHeaderPalette = 0x05aa,
+	kAssetHeaderDissolveFactor = 0x05dc,
+	kAssetHeaderGetOffstageEvents = 0x05dd,
+	kAssetHeaderX = 0x05de,
+	kAssetHeaderY = 0x05df,
+
+	// PATH FIELDS.
+	kAssetHeaderStartPoint = 0x060e,
+	kAssetHeaderEndPoint = 0x060f,
+	kAssetHeaderPathTotalSteps = 0x0610,
+	kAssetHeaderStepRate = 0x0611,
+	kAssetHeaderDuration = 0x0612,
+
+	// CAMERA FIELDS.
+	kAssetHeaderViewportOrigin = 0x076f,
+	kAssetHeaderLensOpen = 0x0770,
+
+	// STAGE FIELDS.
+	kAssetHeaderStageUnk1 = 0x0771,
+	kAssetHeaderCylindricalX = 0x0772,
+	kAssetHeaderCylindricalY = 0x0773,
+	kAssetHeaderAssetName = 0x0bb8,
+
+	// TEXT FIELDS.
+	kAssetHeaderEditable = 0x03eb,
+	kAssetHeaderFontId = 0x0258,
+	kAssetHeaderInitialText = 0x0259,
+	kAssetHeaderTextMaxLength = 0x25a,
+	kAssetHeaderTextJustification = 0x025b,
+	kAssetHeaderTextPosition = 0x25f,
+	kAssetHeaderTextUnk1 = 0x262,
+	kAssetHeaderTextUnk2 = 0x263,
+	kAssetHeaderTextCharacterClass = 0x0266,
+
+	// SPRITE FIELDS.
+	kAssetHeaderSpriteFrameMapping = 0x03e9
+};
 
+enum SoundEncoding {
+	PCM_S16LE_MONO_22050 = 0x0010, // Uncompressed linear PCM
+	IMA_ADPCM_S16LE_MONO_22050 = 0x0004 // IMA ADPCM encoding, must be decoded
+};
 class Asset {
 public:
-	Asset(AssetHeader *header) : _header(header) {};
+	Asset(AssetType type) : _type(type) {};
 	virtual ~Asset();
 
 	// Does any needed frame drawing, audio playing, event handlers, etc.
@@ -48,6 +137,9 @@ public:
 	virtual bool isSpatialActor() const { return false; }
 	virtual bool isActive() const { return _isActive; }
 
+	virtual void initFromParameterStream(Chunk &chunk);
+	virtual void readParameter(Chunk &chunk, AssetHeaderSectionType paramType);
+
 	// These are not pure virtual so if an asset doesnʻt read any chunks or
 	// subfiles it doesnʻt need to just implement these with an error message.
 	virtual void readChunk(Chunk &chunk);
@@ -59,30 +151,49 @@ public:
 	void runEventHandlerIfExists(EventType eventType, const ScriptValue &arg);
 	void runEventHandlerIfExists(EventType eventType);
 
-	AssetType type() const { return _header->_type; }
-	uint id() const { return _header->_id; }
-	AssetHeader *getHeader() const { return _header; }
+	AssetType type() const { return _type; }
+	uint id() const { return _id; }
+	uint contextId() const { return _contextId; }
+	void setId(uint id) { _id = id; }
+	void setContextId(uint id) { _contextId = id; }
+
+	uint32 _chunkReference = 0;
+	uint _assetReference = 0;
 
 protected:
-	AssetHeader *_header = nullptr;
+	AssetType _type = kAssetTypeEmpty;
+	uint _id = 0;
+	uint _contextId = 0;
+
 	bool _isActive = false;
 	uint _startTime = 0;
 	uint _lastProcessedTime = 0;
 	uint _duration = 0;
+	Common::HashMap<uint, Common::Array<EventHandler *> > _eventHandlers;
 };
 
 class SpatialEntity : public Asset {
 public:
-	SpatialEntity(AssetHeader *header) : Asset(header) {};
+	SpatialEntity(AssetType type) : Asset(type) {};
 
 	virtual void redraw(Common::Rect &rect) { return; }
 	virtual ScriptValue callMethod(BuiltInMethod methodId, Common::Array<ScriptValue> &args) override;
 
 	virtual bool isSpatialActor() const override { return true; }
-	virtual bool isVisible() const { return false; }
-	Common::Point getTopLeft() { return Common::Point(_header->_boundingBox.left, _header->_boundingBox.top); }
-	Common::Rect getBbox() const { return _header->_boundingBox; }
-	int zIndex() const { return _header->_zIndex; }
+	virtual bool isVisible() const { return _isVisible; }
+
+	virtual void readParameter(Chunk &chunk, AssetHeaderSectionType paramType) override;
+
+	Common::Rect getBbox() const { return _boundingBox; }
+	int zIndex() const { return _zIndex; }
+
+protected:
+	uint _stageId = 0;
+	int _zIndex = 0;
+	Common::Rect _boundingBox;
+	bool _isVisible = false;
+	bool _hasTransparency = false;
+	bool _getOffstageEvents = false;
 
 	void moveTo(int16 x, int16 y);
 	void moveToCentered(int16 x, int16 y);
diff --git a/engines/mediastation/assetheader.cpp b/engines/mediastation/assetheader.cpp
deleted file mode 100644
index 8e741d44d41..00000000000
--- a/engines/mediastation/assetheader.cpp
+++ /dev/null
@@ -1,303 +0,0 @@
-/* ScummVM - Graphic Adventure Engine
- *
- * ScummVM is the legal property of its developers, whose names
- * are too numerous to list here. Please refer to the COPYRIGHT
- * file distributed with this source distribution.
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- *
- */
-
-#include "mediastation/assetheader.h"
-#include "mediastation/debugchannels.h"
-
-namespace MediaStation {
-
-AssetHeader::AssetHeader(Chunk &chunk) {
-	_fileNumber = chunk.readTypedUint16();
-	_type = static_cast<AssetType>(chunk.readTypedUint16());
-	_id = chunk.readTypedUint16();
-	debugC(4, kDebugLoading, "AssetHeader::AssetHeader(): _type = 0x%x, _id = 0x%x (@0x%llx)", static_cast<uint>(_type), _id, static_cast<long long int>(chunk.pos()));
-
-	AssetHeaderSectionType sectionType = static_cast<AssetHeaderSectionType>(chunk.readTypedUint16());
-	bool moreSectionsToRead = (kAssetHeaderEmptySection != sectionType);
-	while (moreSectionsToRead) {
-		readSection(sectionType, chunk);
-		sectionType = static_cast<AssetHeaderSectionType>(chunk.readTypedUint16());
-		moreSectionsToRead = (kAssetHeaderEmptySection != sectionType);
-	}
-}
-
-AssetHeader::~AssetHeader() {
-	for (auto it = _eventHandlers.begin(); it != _eventHandlers.end(); ++it) {
-		for (EventHandler *eventHandler : it->_value) {
-			delete eventHandler;
-		}
-	}
-	_eventHandlers.clear();
-
-	delete _palette;
-	_palette = nullptr;
-}
-
-void AssetHeader::readSection(AssetHeaderSectionType sectionType, Chunk& chunk) {
-	debugC(5, kDebugLoading, "AssetHeader::AssetHeader(): sectionType = 0x%x (@0x%llx)", static_cast<uint>(sectionType), static_cast<long long int>(chunk.pos()));
-	switch (sectionType) {
-	case kAssetHeaderEmptySection: {
-		break;
-	}
-
-	case kAssetHeaderEventHandler: {
-		EventHandler *eventHandler = new EventHandler(chunk);
-		Common::Array<EventHandler *> &eventHandlersForType = _eventHandlers.getOrCreateVal(eventHandler->_type);
-
-		// This is not a hashmap because we don't want to have to hash ScriptValues.
-		for (EventHandler *existingEventHandler : eventHandlersForType) {
-			if (existingEventHandler->_argumentValue == eventHandler->_argumentValue) {
-				error("AssetHeader::readSection(): Event handler for %s (%s) already exists",
-					eventTypeToStr(eventHandler->_type), eventHandler->getDebugHeader().c_str());
-			}
-		}
-		eventHandlersForType.push_back(eventHandler);
-		break;
-	}
-
-	case kAssetHeaderStageId: {
-		_stageId = chunk.readTypedUint16();
-		break;
-	}
-
-	case kAssetHeaderAssetId: {
-		// We already have this asset's ID, so we will just verify it is the same
-		// as the ID we have already read.
-		uint32 duplicateAssetId = chunk.readTypedUint16();
-		if (duplicateAssetId != _id) {
-			warning("AssetHeader::readSection(): AssetHeader ID %d does not match original asset ID %d", duplicateAssetId, _id);
-		}
-		break;
-	}
-
-	case kAssetHeaderChunkReference: {
-		// These are references to the chunk(s) that hold the data for this asset.
-		// The references and the chunks have the following format "a501".
-		// There is no guarantee where these chunk(s) might actually be located:
-		//  - They might be in the same RIFF subfile as this header,
-		//  - They might be in a different RIFF subfile in the same CXT file,
-		//  - They might be in a different CXT file entirely.
-		_chunkReference = chunk.readTypedChunkReference();
-		break;
-	}
-
-	case kAssetHeaderMovieAudioChunkReference: {
-		_audioChunkReference = chunk.readTypedChunkReference();
-		break;
-	}
-
-	case kAssetHeaderMovieAnimationChunkReference: {
-		_animationChunkReference = chunk.readTypedChunkReference();
-		break;
-	}
-
-	case kAssetHeaderBoundingBox: {
-		_boundingBox = chunk.readTypedRect();
-		break;
-	}
-
-	case kAssetHeaderMouseActiveArea: {
-		uint16 total_points = chunk.readTypedUint16();
-		for (int i = 0; i < total_points; i++) {
-			Common::Point point = chunk.readTypedPoint();
-			_mouseActiveArea.push_back(point);
-		}
-		break;
-	}
-
-	case kAssetHeaderZIndex: {
-		_zIndex = chunk.readTypedGraphicUnit();
-		break;
-	}
-
-	case kAssetHeaderAssetReference: {
-		_assetReference = chunk.readTypedUint16();
-		break;
-	}
-
-	case kAssetHeaderStartup: {
-		_startup = chunk.readTypedByte();
-		break;
-	}
-
-	case kAssetHeaderTransparency: {
-		_transparency = chunk.readTypedByte();
-		break;
-	}
-
-	case kAssetHeaderHasOwnSubfile: {
-		_hasOwnSubfile = chunk.readTypedByte();
-		break;
-	}
-
-	case kAssetHeaderCursorResourceId: {
-		_cursorResourceId = chunk.readTypedUint16();
-		break;
-	}
-
-	case kAssetHeaderFrameRate: {
-		_frameRate = static_cast<uint32>(chunk.readTypedDouble());
-		break;
-	}
-
-	case kAssetHeaderLoadType: {
-		_loadType = chunk.readTypedByte();
-		break;
-	}
-
-	case kAssetHeaderSoundInfo: {
-		_chunkCount = chunk.readTypedUint16();
-		_rate = chunk.readTypedUint32();
-		break;
-	}
-
-	case kAssetHeaderMovieLoadType: {
-		_loadType = chunk.readTypedByte();
-		break;
-	}
-
-	case kAssetHeaderGetOffstageEvents: {
-		_getOffstageEvents = chunk.readTypedByte();
-		break;
-	}
-
-	case kAssetHeaderPalette: {
-		// TODO: Avoid the copying here!
-		const uint PALETTE_ENTRIES = 256;
-		const uint PALETTE_BYTES = PALETTE_ENTRIES * 3;
-		byte* buffer = new byte[PALETTE_BYTES];
-		chunk.read(buffer, PALETTE_BYTES);
-		_palette = new Graphics::Palette(buffer, PALETTE_ENTRIES);
-		delete[] buffer;
-		break;
-	}
-
-	case kAssetHeaderDissolveFactor: {
-		_dissolveFactor = chunk.readTypedDouble();
-		break;
-	}
-
-	case kAssetHeaderSoundEncoding1:
-	case kAssetHeaderSoundEncoding2: {
-		_soundEncoding = static_cast<SoundEncoding>(chunk.readTypedUint16());
-		break;
-	}
-
-	case kAssetHeaderSpriteChunkCount: {
-		_chunkCount = chunk.readTypedUint16();
-		break;
-	}
-
-	case kAssetHeaderStartPoint: {
-		_startPoint = chunk.readTypedPoint();
-		break;
-	}
-
-	case kAssetHeaderEndPoint: {
-		_endPoint = chunk.readTypedPoint();
-		break;
-	}
-
-	case kAssetHeaderStepRate: {
-		double _stepRateFloat = chunk.readTypedDouble();
-		// This should always be an integer anyway,
-		// so we'll cast away any fractional part.
-		_stepRate = static_cast<uint32>(_stepRateFloat);
-		break;
-	}
-
-	case kAssetHeaderDuration: {
-		// These are stored in the file as fractional seconds,
-		// but we want milliseconds.
-		_duration = (uint32)(chunk.readTypedTime() * 1000);
-		break;
-	}
-
-	case kAssetHeaderX: {
-		_x = chunk.readTypedUint16();
-		break;
-	}
-
-	case kAssetHeaderY: {
-		_y = chunk.readTypedUint16();
-		break;
-	}
-
-	case kAssetHeaderEditable: {
-		_editable = chunk.readTypedByte();
-		break;
-	}
-
-	case kAssetHeaderFontId: {
-		_fontAssetId = chunk.readTypedUint16();
-		break;
-	}
-
-	case kAssetHeaderTextMaxLength: {
-		_maxTextLength = chunk.readTypedUint16();
-		break;
-	}
-
-	case kAssetHeaderInitialText: {
-		_text = chunk.readTypedString();
-		break;
-	}
-
-	case kAssetHeaderTextJustification: {
-		_justification = static_cast<TextJustification>(chunk.readTypedUint16());
-		break;
-	}
-
-	case kAssetHeaderTextPosition: {
-		_position = static_cast<TextPosition>(chunk.readTypedUint16());
-		break;
-	}
-
-	case kAssetHeaderTextCharacterClass: {
-		CharacterClass characterClass;
-		characterClass.firstAsciiCode = chunk.readTypedUint16();
-		characterClass.lastAsciiCode = chunk.readTypedUint16();
-		_acceptedInput.push_back(characterClass);
-		break;
-	}
-
-	case kAssetHeaderSpriteFrameMapping: {
-		uint32 externalFrameId = chunk.readTypedUint16();
-		uint32 internalFrameId = chunk.readTypedUint16();
-		uint32 unk1 = chunk.readTypedUint16();
-		if (unk1 != internalFrameId) {
-			warning("AssetHeader::readSection(): Repeated internalFrameId doesn't match");
-		}
-		_spriteFrameMapping.setVal(externalFrameId, internalFrameId);
-		break;
-	}
-
-    case kAssetHeaderPathTotalSteps: {
-		_totalSteps = chunk.readTypedUint16();
-		break;
-	}
-
-	default:
-		error("AssetHeader::readSection(): Unknown section type 0x%x (@0x%llx)", static_cast<uint>(sectionType), static_cast<long long int>(chunk.pos()));
-	}
-}
-
-} // end of namespace MediaStation
diff --git a/engines/mediastation/assetheader.h b/engines/mediastation/assetheader.h
deleted file mode 100644
index af077b20a30..00000000000
--- a/engines/mediastation/assetheader.h
+++ /dev/null
@@ -1,222 +0,0 @@
-/* ScummVM - Graphic Adventure Engine
- *
- * ScummVM is the legal property of its developers, whose names
- * are too numerous to list here. Please refer to the COPYRIGHT
- * file distributed with this source distribution.
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- *
- */
-
-#ifndef MEDIASTATION_ASSET_HEADER_H
-#define MEDIASTATION_ASSET_HEADER_H
-
-#include "common/str.h"
-#include "common/rect.h"
-#include "common/hashmap.h"
-#include "graphics/palette.h"
-
-#include "mediastation/datafile.h"
-#include "mediastation/mediascript/eventhandler.h"
-
-namespace MediaStation {
-
-struct MovieChunkReference {
-	uint32 headerChunkId;
-	uint32 audioChunkId;
-	uint32 videoChunkId;
-};
-
-typedef uint32 ChunkReference;
-
-typedef uint32 AssetId;
-
-enum AssetType {
-	kAssetTypeScreen = 0x0001, // SCR
-	kAssetTypeStage = 0x0002, // STG
-	kAssetTypePath = 0x0004, // PTH
-	kAssetTypeSound = 0x0005, // SND
-	kAssetTypeTimer = 0x0006, // TMR
-	kAssetTypeImage = 0x0007, // IMG
-	kAssetTypeHotspot = 0x000b, // HSP
-	kAssetTypeSprite = 0x000e, // SPR
-	kAssetTypeLKZazu = 0x000f,
-	kAssetTypeLKConstellations = 0x0010,
-	kAssetTypeImageSet = 0x001d,
-	kAssetTypeCursor = 0x000c, // CSR
-	kAssetTypePrinter = 0x0019, // PRT
-	kAssetTypeMovie = 0x0016, // MOV
-	kAssetTypePalette = 0x0017,
-	kAssetTypeText = 0x001a, // TXT
-	kAssetTypeFont = 0x001b, // FON
-	kAssetTypeCamera = 0x001c, // CAM
-	kAssetTypeCanvas = 0x001e, // CVS
-	// TODO: Discover how the XSND differs from regular sounds.
-	// Only appears in Ariel.
-	kAssetTypeXsnd = 0x001f,
-	kAssetTypeXsndMidi = 0x0020,
-	// TODO: Figure out what this is. Only appears in Ariel.
-	kAssetTypeRecorder = 0x0021,
-	kAssetTypeFunction = 0x0069 // FUN
-};
-
-enum AssetStartupType {
-	kAssetStartupInactive = 0,
-	kAssetStartupActive = 1
-};
-
-enum AssetHeaderSectionType {
-	kAssetHeaderEmptySection = 0x0000,
-	kAssetHeaderSoundEncoding1 = 0x0001,
-	kAssetHeaderSoundEncoding2 = 0x0002,
-	kAssetHeaderEventHandler = 0x0017,
-	kAssetHeaderStageId = 0x0019,
-	kAssetHeaderAssetId = 0x001a,
-	kAssetHeaderChunkReference = 0x001b,
-	kAssetHeaderMovieAnimationChunkReference = 0x06a4,
-	kAssetHeaderMovieAudioChunkReference = 0x06a5,
-	kAssetHeaderAssetReference = 0x077b,
-	kAssetHeaderBoundingBox = 0x001c,
-	kAssetHeaderMouseActiveArea = 0x001d,
-	kAssetHeaderZIndex = 0x001e,
-	kAssetHeaderStartup = 0x001f,
-	kAssetHeaderTransparency = 0x0020,
-	kAssetHeaderHasOwnSubfile = 0x0021,
-	kAssetHeaderCursorResourceId = 0x0022,
-	kAssetHeaderFrameRate = 0x0024,
-	kAssetHeaderLoadType = 0x0032,
-	kAssetHeaderSoundInfo = 0x0033,
-	kAssetHeaderMovieLoadType = 0x0037,
-	kAssetHeaderSpriteChunkCount = 0x03e8,
-	kAssetHeaderPalette = 0x05aa,
-	kAssetHeaderDissolveFactor = 0x05dc,
-	kAssetHeaderGetOffstageEvents = 0x05dd,
-	kAssetHeaderX = 0x05de,
-	kAssetHeaderY = 0x05df,
-
-	// PATH FIELDS.
-	kAssetHeaderStartPoint = 0x060e,
-	kAssetHeaderEndPoint = 0x060f,
-	kAssetHeaderPathTotalSteps = 0x0610,
-	kAssetHeaderStepRate = 0x0611,
-	kAssetHeaderDuration = 0x0612,
-
-	// CAMERA FIELDS.
-	kAssetHeaderViewportOrigin = 0x076f,
-	kAssetHeaderLensOpen = 0x0770,
-
-	// STAGE FIELDS.
-	kAssetHeaderStageUnk1 = 0x0771,
-	kAssetHeaderCylindricalX = 0x0772,
-	kAssetHeaderCylindricalY = 0x0773,
-	kAssetHeaderAssetName = 0x0bb8,
-
-	// TEXT FIELDS.
-	kAssetHeaderEditable = 0x03eb,
-	kAssetHeaderFontId = 0x0258,
-	kAssetHeaderInitialText = 0x0259,
-	kAssetHeaderTextMaxLength = 0x25a,
-	kAssetHeaderTextJustification = 0x025b,
-	kAssetHeaderTextPosition = 0x25f,
-	kAssetHeaderTextUnk1 = 0x262,
-	kAssetHeaderTextUnk2 = 0x263,
-	kAssetHeaderTextCharacterClass = 0x0266,
-
-	// SPRITE FIELDS.
-	kAssetHeaderSpriteFrameMapping = 0x03e9
-};
-
-enum TextJustification {
-	kTextJustificationLeft = 0x25c,
-	kTextJustificationRight = 0x25d,
-	kTextJustificationCenter = 0x25e
-};
-
-enum TextPosition {
-	kTextPositionMiddle = 0x25e,
-	kTextPositionTop = 0x260,
-	kTextPositionBotom = 0x261
-};
-
-struct CharacterClass {
-	uint firstAsciiCode = 0;
-	uint lastAsciiCode = 0;
-};
-
-enum SoundEncoding {
-	PCM_S16LE_MONO_22050 = 0x0010, // Uncompressed linear PCM
-	IMA_ADPCM_S16LE_MONO_22050 = 0x0004 // IMA ADPCM encoding, must be decoded
-};
-
-class AssetHeader {
-public:
-	AssetHeader(Chunk &chunk);
-	~AssetHeader();
-
-	uint32 _fileNumber = 0;
-	AssetType _type;
-	AssetId _id;
-
-	ChunkReference _chunkReference = 0;
-	// These two are only used in movies.
-	ChunkReference _audioChunkReference = 0;
-	ChunkReference _animationChunkReference = 0;
-	Common::Rect _boundingBox;
-	Common::Array<Common::Point> _mouseActiveArea;
-	int _zIndex = 0;
-	uint32 _assetReference = 0;
-	uint32 _startup = 0;
-	bool _transparency = false;
-	bool _hasOwnSubfile = false;
-	uint32 _cursorResourceId = 0;
-	uint32 _frameRate = 0;
-	uint32 _loadType = 0;
-	uint32 _rate = 0;
-	bool _editable = 0;
-	Graphics::Palette *_palette = nullptr;
-	bool _getOffstageEvents = 0;
-	uint32 _x = 0; // Image only.
-	uint32 _y = 0; // Image only.
-	Common::String _name;
-	uint32 _stageId = 0;
-	SoundEncoding _soundEncoding;
-	uint32 _chunkCount = 0;
-	Common::HashMap<uint32, uint32> _spriteFrameMapping;
-
-	// PATH FIELDS.
-	uint32 _dissolveFactor = 0;
-	Common::Point _startPoint;
-	Common::Point _endPoint;
-	uint32 _stepRate = 0;
-	uint32 _duration = 0;
-	uint _totalSteps = 0;
-
-	// EVENT HANDLER FIELDS.
-	Common::HashMap<uint, Common::Array<EventHandler *>> _eventHandlers;
-
-	// TEXT FIELDS.
-	Common::String _text;
-	uint _maxTextLength = 0;
-	uint _fontAssetId = 0;
-	TextJustification _justification;
-	TextPosition _position;
-	Common::Array<CharacterClass> _acceptedInput;
-
-private:
-	void readSection(AssetHeaderSectionType sectionType, Chunk &chunk);
-};
-
-} // End of namespace MediaStation
-
-#endif
\ No newline at end of file
diff --git a/engines/mediastation/assets/canvas.cpp b/engines/mediastation/assets/canvas.cpp
index 6bd43408244..cb9ead43def 100644
--- a/engines/mediastation/assets/canvas.cpp
+++ b/engines/mediastation/assets/canvas.cpp
@@ -23,6 +23,21 @@
 
 namespace MediaStation {
 
+void Canvas::readParameter(Chunk &chunk, AssetHeaderSectionType paramType) {
+	switch (paramType) {
+	case kAssetHeaderStartup:
+		_isVisible = static_cast<bool>(chunk.readTypedByte());
+		break;
+
+	case kAssetHeaderDissolveFactor:
+		_dissolveFactor = chunk.readTypedDouble();
+		break;
+
+	default:
+		SpatialEntity::readParameter(chunk, paramType);
+	}
+}
+
 ScriptValue Canvas::callMethod(BuiltInMethod methodId, Common::Array<ScriptValue> &args) {
 	switch (methodId) {
 	case kClearToPaletteMethod: {
diff --git a/engines/mediastation/assets/canvas.h b/engines/mediastation/assets/canvas.h
index b7dcd0431e7..76e121d092a 100644
--- a/engines/mediastation/assets/canvas.h
+++ b/engines/mediastation/assets/canvas.h
@@ -23,7 +23,6 @@
 #define MEDIASTATION_CANVAS_H
 
 #include "mediastation/asset.h"
-#include "mediastation/assetheader.h"
 #include "mediastation/mediascript/scriptvalue.h"
 #include "mediastation/mediascript/scriptconstants.h"
 
@@ -31,13 +30,13 @@ namespace MediaStation {
 
 class Canvas : public SpatialEntity {
 public:
-	Canvas(AssetHeader *header) : SpatialEntity(header) {};
+	Canvas() : SpatialEntity(kAssetTypeCanvas) {};
 
-	virtual bool isVisible() const override { return _isVisible; }
+	virtual void readParameter(Chunk &chunk, AssetHeaderSectionType paramType) override;
 	virtual ScriptValue callMethod(BuiltInMethod methodId, Common::Array<ScriptValue> &args) override;
 
 private:
-	bool _isVisible = false;
+	double _dissolveFactor = 0.0;
 };
 
 } // End of namespace MediaStation
diff --git a/engines/mediastation/assets/font.cpp b/engines/mediastation/assets/font.cpp
index 8f07342e600..bfc94798cdc 100644
--- a/engines/mediastation/assets/font.cpp
+++ b/engines/mediastation/assets/font.cpp
@@ -37,6 +37,17 @@ Font::~Font() {
 	_glyphs.clear();
 }
 
+void Font::readParameter(Chunk &chunk, AssetHeaderSectionType paramType) {
+	switch (paramType) {
+	case kAssetHeaderChunkReference:
+		_chunkReference = chunk.readTypedChunkReference();
+		break;
+
+	default:
+		Asset::readParameter(chunk, paramType);
+	}
+}
+
 void Font::readChunk(Chunk &chunk) {
 	debugC(5, kDebugLoading, "Font::readChunk(): Reading font glyph (@0x%llx)", static_cast<long long int>(chunk.pos()));
 	uint asciiCode = chunk.readTypedUint16();
diff --git a/engines/mediastation/assets/font.h b/engines/mediastation/assets/font.h
index 5a599af6e61..c53f2436b0f 100644
--- a/engines/mediastation/assets/font.h
+++ b/engines/mediastation/assets/font.h
@@ -23,7 +23,6 @@
 #define MEDIASTATION_FONT_H
 
 #include "mediastation/asset.h"
-#include "mediastation/assetheader.h"
 #include "mediastation/bitmap.h"
 #include "mediastation/datafile.h"
 #include "mediastation/mediascript/scriptvalue.h"
@@ -43,9 +42,10 @@ private:
 
 class Font : public Asset {
 public:
-	Font(AssetHeader *header) : Asset(header) {};
+	Font() : Asset(kAssetTypeFont) {};
 	~Font();
 
+	virtual void readParameter(Chunk &chunk, AssetHeaderSectionType paramType) override;
 	virtual void readChunk(Chunk &chunk) override;
 
 private:
diff --git a/engines/mediastation/assets/hotspot.cpp b/engines/mediastation/assets/hotspot.cpp
index 989ed995be4..5081b8fb176 100644
--- a/engines/mediastation/assets/hotspot.cpp
+++ b/engines/mediastation/assets/hotspot.cpp
@@ -24,30 +24,48 @@
 
 namespace MediaStation {
 
-Hotspot::Hotspot(AssetHeader *header) : SpatialEntity(header) {
-	if (header->_startup == kAssetStartupActive) {
-		_isActive = true;
+void Hotspot::readParameter(Chunk &chunk, AssetHeaderSectionType paramType) {
+	switch (paramType) {
+	case kAssetHeaderMouseActiveArea: {
+		uint16 total_points = chunk.readTypedUint16();
+		for (int i = 0; i < total_points; i++) {
+			Common::Point point = chunk.readTypedPoint();
+			_mouseActiveArea.push_back(point);
+		}
+		break;
+	}
+
+	case kAssetHeaderCursorResourceId:
+		_cursorResourceId = chunk.readTypedUint16();
+		break;
+
+	case kAssetHeaderGetOffstageEvents:
+		_getOffstageEvents = static_cast<bool>(chunk.readTypedByte());
+		break;
+
+	default:
+		SpatialEntity::readParameter(chunk, paramType);
 	}
 }
 
 bool Hotspot::isInside(const Common::Point &pointToCheck) {
 	// No sense checking the polygon if we're not even in the bbox.
-	if (!_header->_boundingBox.contains(pointToCheck)) {
+	if (!_boundingBox.contains(pointToCheck)) {
 		return false;
 	}
 
 	// We're in the bbox, but there might not be a polygon to check.
-	if (_header->_mouseActiveArea.empty()) {
+	if (_mouseActiveArea.empty()) {
 		return true;
 	}
 
 	// Polygon intersection code adapted from HADESCH engine, might need more
 	// refinement once more testing is possible.
-	Common::Point point = pointToCheck - Common::Point(_header->_boundingBox.left, _header->_boundingBox.top);
+	Common::Point point = pointToCheck - Common::Point(_boundingBox.left, _boundingBox.top);
 	int rcross = 0; // Number of right-side overlaps
 
 	// Each edge is checked whether it cuts the outgoing stream from the point
-	Common::Array<Common::Point> _polygon = _header->_mouseActiveArea;
+	Common::Array<Common::Point> _polygon = _mouseActiveArea;
 	for (unsigned i = 0; i < _polygon.size(); i++) {
 		const Common::Point &edgeStart = _polygon[i];
 		const Common::Point &edgeEnd = _polygon[(i + 1) % _polygon.size()];
diff --git a/engines/mediastation/assets/hotspot.h b/engines/mediastation/assets/hotspot.h
index dfde0dcad94..6994a716afc 100644
--- a/engines/mediastation/assets/hotspot.h
+++ b/engines/mediastation/assets/hotspot.h
@@ -23,7 +23,6 @@
 #define MEDIASTATION_HOTSPOT_H
 
 #include "mediastation/asset.h"
-#include "mediastation/assetheader.h"
 #include "mediastation/mediascript/scriptvalue.h"
 #include "mediastation/mediascript/scriptconstants.h"
 
@@ -31,12 +30,17 @@ namespace MediaStation {
 
 class Hotspot : public SpatialEntity {
 public:
-	Hotspot(AssetHeader *header);
+	Hotspot() : SpatialEntity(kAssetTypeHotspot) {};
+	virtual ~Hotspot() { _mouseActiveArea.clear(); }
 
 	bool isInside(const Common::Point &pointToCheck);
 	virtual bool isVisible() const override { return false; }
 
+	virtual void readParameter(Chunk &chunk, AssetHeaderSectionType paramType) override;
 	virtual ScriptValue callMethod(BuiltInMethod methodId, Common::Array<ScriptValue> &args) override;
+
+	uint _cursorResourceId = 0;
+	Common::Array<Common::Point> _mouseActiveArea;
 };
 
 } // End of namespace MediaStation
diff --git a/engines/mediastation/assets/image.cpp b/engines/mediastation/assets/image.cpp
index e2e92d9d28b..9e91db1f263 100644
--- a/engines/mediastation/assets/image.cpp
+++ b/engines/mediastation/assets/image.cpp
@@ -25,14 +25,8 @@
 
 namespace MediaStation {
 
-Image::Image(AssetHeader *header) : SpatialEntity(header) {
-	if (header->_startup == kAssetStartupActive) {
-		_isActive = true;
-	}
-}
-
 Image::~Image() {
-	if (_header->_assetReference == 0) {
+	if (_assetReference == 0) {
 		// If we're just referencing another asset's bitmap,
 		// don't delete that bitmap.
 		delete _bitmap;
@@ -40,6 +34,33 @@ Image::~Image() {
 	_bitmap = nullptr;
 }
 
+void Image::readParameter(Chunk &chunk, AssetHeaderSectionType paramType) {
+	switch (paramType) {
+	case kAssetHeaderChunkReference:
+		_chunkReference = chunk.readTypedChunkReference();
+		break;
+
+	case kAssetHeaderLoadType:
+		_loadType = chunk.readTypedByte();
+		break;
+
+	case kAssetHeaderDissolveFactor:
+		_dissolveFactor = chunk.readTypedDouble();
+		break;
+
+	case kAssetHeaderX:
+		_xOffset = chunk.readTypedUint16();
+		break;
+
+	case kAssetHeaderY:
+		_yOffset = chunk.readTypedUint16();
+		break;
+
+	default:
+		SpatialEntity::readParameter(chunk, paramType);
+	}
+}
+
 ScriptValue Image::callMethod(BuiltInMethod methodId, Common::Array<ScriptValue> &args) {
 	ScriptValue returnValue;
 	switch (methodId) {
@@ -56,8 +77,9 @@ ScriptValue Image::callMethod(BuiltInMethod methodId, Common::Array<ScriptValue>
 	}
 
 	case kSetDissolveFactorMethod: {
+		warning("STUB: setDissolveFactor");
 		assert(args.size() == 1);
-		warning("Image::callMethod(): setDissolveFactor not implemented yet");
+		_dissolveFactor = args[0].asFloat();
 		return returnValue;
 	}
 
@@ -98,7 +120,7 @@ void Image::spatialHide() {
 }
 
 Common::Point Image::getLeftTop() {
-	return Common::Point(_header->_x + _header->_boundingBox.left, _header->_y + _header->_boundingBox.top);
+	return Common::Point(_xOffset + _boundingBox.left, _yOffset + _boundingBox.top);
 }
 
 void Image::readChunk(Chunk &chunk) {
diff --git a/engines/mediastation/assets/image.h b/engines/mediastation/assets/image.h
index a0953058e25..a729504c0be 100644
--- a/engines/mediastation/assets/image.h
+++ b/engines/mediastation/assets/image.h
@@ -25,7 +25,6 @@
 #include "mediastation/asset.h"
 #include "mediastation/datafile.h"
 #include "mediastation/bitmap.h"
-#include "mediastation/assetheader.h"
 #include "mediastation/mediascript/scriptvalue.h"
 #include "mediastation/mediascript/scriptconstants.h"
 
@@ -35,17 +34,20 @@ class Image : public SpatialEntity {
 friend class Context;
 
 public:
-	Image(AssetHeader *header);
+	Image() : SpatialEntity(kAssetTypeImage) {};
 	virtual ~Image() override;
 
 	virtual void readChunk(Chunk &chunk) override;
+	virtual void readParameter(Chunk &chunk, AssetHeaderSectionType paramType) override;
 	virtual ScriptValue callMethod(BuiltInMethod methodId, Common::Array<ScriptValue> &args) override;
 	virtual void redraw(Common::Rect &rect) override;
-	virtual bool isVisible() const override { return _isVisible; }
 
 private:
 	Bitmap *_bitmap = nullptr;
-	bool _isVisible = false;
+	uint _loadType = 0;
+	double _dissolveFactor = 0.0;
+	int _xOffset = 0;
+	int _yOffset = 0;
 
 	// Script method implementations.
 	void spatialShow();
diff --git a/engines/mediastation/assets/movie.cpp b/engines/mediastation/assets/movie.cpp
index 59588f039d0..3887330e49f 100644
--- a/engines/mediastation/assets/movie.cpp
+++ b/engines/mediastation/assets/movie.cpp
@@ -154,13 +154,6 @@ MovieFrame::~MovieFrame() {
 	_footer = nullptr;
 }
 
-Movie::Movie(AssetHeader *header) : SpatialEntity(header) {
-	if (header->_startup == kAssetStartupActive) {
-		setActive();
-		_showByDefault = true;
-	}
-}
-
 Movie::~Movie() {
 	g_engine->_mixer->stopHandle(_soundHandle);
 
@@ -185,6 +178,61 @@ Movie::~Movie() {
 	_footers.clear();
 }
 
+void Movie::readParameter(Chunk &chunk, AssetHeaderSectionType paramType) {
+	switch (paramType) {
+	case kAssetHeaderAssetId: {
+		// We already have this asset's ID, so we will just verify it is the same
+		// as the ID we have already read.
+		uint32 duplicateAssetId = chunk.readTypedUint16();
+		if (duplicateAssetId != _id) {
+			warning("Duplicate asset ID %d does not match original ID %d", duplicateAssetId, _id);
+		}
+		break;
+	}
+
+	case kAssetHeaderMovieLoadType:
+		_loadType = chunk.readTypedByte();
+		break;
+
+	case kAssetHeaderChunkReference:
+		_chunkReference = chunk.readTypedChunkReference();
+		break;
+
+	case kAssetHeaderHasOwnSubfile: {
+		bool hasOwnSubfile = static_cast<bool>(chunk.readTypedByte());
+		if (!hasOwnSubfile) {
+			error("Movie doesn't have a subfile");
+		}
+		break;
+	}
+
+	case kAssetHeaderDissolveFactor:
+		_dissolveFactor = chunk.readTypedDouble();
+		break;
+
+	case kAssetHeaderMovieAudioChunkReference:
+		_audioChunkReference = chunk.readTypedChunkReference();
+		break;
+
+	case kAssetHeaderMovieAnimationChunkReference:
+		_animationChunkReference = chunk.readTypedChunkReference();
+		break;
+
+	case kAssetHeaderSoundInfo:
+		_chunkCount = chunk.readTypedUint16();
+		_rate = chunk.readTypedUint32();
+		break;
+
+	case kAssetHeaderSoundEncoding1:
+	case kAssetHeaderSoundEncoding2:
+		_soundEncoding = static_cast<SoundEncoding>(chunk.readTypedUint16());
+		break;
+
+	default:
+		SpatialEntity::readParameter(chunk, paramType);
+	}
+}
+
 ScriptValue Movie::callMethod(BuiltInMethod methodId, Common::Array<ScriptValue> &args) {
 	ScriptValue returnValue;
 
@@ -221,7 +269,7 @@ ScriptValue Movie::callMethod(BuiltInMethod methodId, Common::Array<ScriptValue>
 
 	case kGetLeftXMethod: {
 		assert(args.empty());
-		double left = static_cast<double>(_header->_boundingBox.left);
+		double left = static_cast<double>(_boundingBox.left);
 		returnValue.setToFloat(left);
 		return returnValue;
 
@@ -229,14 +277,15 @@ ScriptValue Movie::callMethod(BuiltInMethod methodId, Common::Array<ScriptValue>
 
 	case kGetTopYMethod: {
 		assert(args.empty());
-		double top = static_cast<double>(_header->_boundingBox.top);
+		double top = static_cast<double>(_boundingBox.top);
 		returnValue.setToFloat(top);
 		return returnValue;
 	}
 
 	case kSetDissolveFactorMethod: {
+		warning("STUB: setDissolveFactor");
 		assert(args.size() == 1);
-		warning("Movie::callMethod(): setDissolveFactor not implemented yet");
+		_dissolveFactor = args[0].asFloat();
 		return returnValue;
 	}
 
@@ -247,13 +296,10 @@ ScriptValue Movie::callMethod(BuiltInMethod methodId, Common::Array<ScriptValue>
 
 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);
+		warning("Movie::spatialShow(): (%d) Attempted to spatialShow movie that is already playing", _id);
 		return;
 	} else if (_stills.empty()) {
-		warning("Movie::spatialShow(): (%d) No still frame to show", _header->_id);
+		warning("Movie::spatialShow(): (%d) No still frame to show", _id);
 		return;
 	}
 
@@ -267,16 +313,16 @@ void Movie::spatialShow() {
 	}
 
 	setActive();
-	_isShowing = true;
+	_isVisible = true;
 	_isPlaying = false;
 }
 
 void Movie::spatialHide() {
 	if (_isPlaying) {
-		warning("Movie::spatialShow(): (%d) Attempted to spatialHide movie that is playing", _header->_id);
+		warning("Movie::spatialShow(): (%d) Attempted to spatialHide movie that is playing", _id);
 		return;
-	} else if (!_isShowing) {
-		warning("Movie::spatialHide(): (%d) Attempted to spatialHide movie that is not showing", _header->_id);
+	} else if (!_isVisible) {
+		warning("Movie::spatialHide(): (%d) Attempted to spatialHide movie that is not showing", _id);
 		return;
 	}
 
@@ -286,7 +332,7 @@ void Movie::spatialHide() {
 	_framesOnScreen.clear();
 	_framesNotYetShown.clear();
 
-	_isShowing = false;
+	_isVisible = false;
 	_isPlaying = false;
 	setInactive();
 }
@@ -295,7 +341,7 @@ 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 (_isPlaying) {
-		warning("Movie::timePlay(): (%d) Attempted to play a movie that is already playing", _header->_id);
+		warning("Movie::timePlay(): (%d) Attempted to play a movie that is already playing", _id);
 		return;
 	}
 
@@ -313,18 +359,18 @@ void Movie::timePlay() {
 	}
 
 	_framesNotYetShown = _frames;
-	_isShowing = true;
+	_isVisible = true;
 	_isPlaying = true;
 	setActive();
 	runEventHandlerIfExists(kMovieBeginEvent);
 }
 
 void Movie::timeStop() {
-	if (!_isShowing) {
-		warning("Movie::timeStop(): (%d) Attempted to stop a movie that isn't showing", _header->_id);
+	if (!_isVisible) {
+		warning("Movie::timeStop(): (%d) Attempted to stop a movie that isn't showing", _id);
 		return;
 	} else if (!_isPlaying) {
-		warning("Movie::timePlay(): (%d) Attempted to stop a movie that isn't playing", _header->_id);
+		warning("Movie::timePlay(): (%d) Attempted to stop a movie that isn't playing", _id);
 		return;
 	}
 
@@ -359,13 +405,13 @@ void Movie::process() {
 }
 
 void Movie::updateFrameState() {
-	if (_showByDefault) {
+	if (_isVisible && _atFirstFrame) {
 		spatialShow();
-		_showByDefault = false;
+		_atFirstFrame = false;
 	}
 
 	if (!_isPlaying) {
-		debugC(6, kDebugGraphics, "Movie::updateFrameState (%d): Not playing", _header->_id);
+		debugC(6, kDebugGraphics, "Movie::updateFrameState (%d): Not playing", _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());
@@ -375,7 +421,7 @@ void Movie::updateFrameState() {
 
 	uint currentTime = g_system->getMillis();
 	uint movieTime = currentTime - _startTime;
-	debugC(5, kDebugGraphics, "Movie::updateFrameState (%d): Starting update (movie time: %d)", _header->_id, movieTime);
+	debugC(5, kDebugGraphics, "Movie::updateFrameState (%d): Starting update (movie time: %d)", _id, movieTime);
 
 	// This complexity is necessary becuase movies can have more than one frame
 	// showing at the same time - for instance, a movie background and an
@@ -453,7 +499,7 @@ void Movie::redraw(Common::Rect &rect) {
 		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);
+			areaToRedraw.translate(-frame->left() - _boundingBox.left, -frame->top() - _boundingBox.top);
 			areaToRedraw.clip(Common::Rect(0, 0, frame->width(), frame->height()));
 			g_engine->_screen->simpleBlitFrom(frame->_surface, areaToRedraw, originOnScreen);
 		}
@@ -462,7 +508,7 @@ void Movie::redraw(Common::Rect &rect) {
 
 Common::Rect Movie::getFrameBoundingBox(MovieFrame *frame) {
 	Common::Rect bbox = frame->boundingBox();
-	bbox.translate(_header->_boundingBox.left, _header->_boundingBox.top);
+	bbox.translate(_boundingBox.left, _boundingBox.top);
 	return bbox;
 }
 
@@ -515,7 +561,7 @@ void Movie::readSubfile(Subfile &subfile, Chunk &chunk) {
 
 		// READ ALL THE FRAMES IN THIS CHUNK.
 		debugC(5, kDebugLoading, "Movie::readSubfile(): (Frameset %d of %d) Reading animation chunks... (@0x%llx)", i, chunkCount, static_cast<long long int>(chunk.pos()));
-		bool isAnimationChunk = (chunk._id == _header->_animationChunkReference);
+		bool isAnimationChunk = (chunk._id == _animationChunkReference);
 		if (!isAnimationChunk) {
 			warning("Movie::readSubfile(): (Frameset %d of %d) No animation chunks found (@0x%llx)", i, chunkCount, static_cast<long long int>(chunk.pos()));
 		}
@@ -542,17 +588,17 @@ void Movie::readSubfile(Subfile &subfile, Chunk &chunk) {
 
 			// READ THE NEXT CHUNK.
 			chunk = subfile.nextChunk();
-			isAnimationChunk = (chunk._id == _header->_animationChunkReference);
+			isAnimationChunk = (chunk._id == _animationChunkReference);
 		}
 
 		// 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 == _audioChunkReference);
 		if (isAudioChunk) {
 			byte *buffer = (byte *)malloc(chunk._length);
 			chunk.read((void *)buffer, chunk._length);
 			Audio::SeekableAudioStream *stream = nullptr;
-			switch (_header->_soundEncoding) {
+			switch (_soundEncoding) {
 			case SoundEncoding::PCM_S16LE_MONO_22050:
 				stream = Audio::makeRawStream(buffer, chunk._length, 22050, Audio::FLAG_16BITS | Audio::FLAG_LITTLE_ENDIAN);
 				break;
@@ -562,7 +608,7 @@ void Movie::readSubfile(Subfile &subfile, Chunk &chunk) {
 				break;
 
 			default:
-				error("Movie::readSubfile(): Unknown audio encoding 0x%x", static_cast<uint>(_header->_soundEncoding));
+				error("Movie::readSubfile(): Unknown audio encoding 0x%x", static_cast<uint>(_soundEncoding));
 			}
 			_audioStreams.push_back(stream);
 			chunk = subfile.nextChunk();
@@ -572,7 +618,7 @@ void Movie::readSubfile(Subfile &subfile, Chunk &chunk) {
 
 		// READ THE FOOTER FOR THIS SUBFILE.
 		debugC(5, kDebugLoading, "Movie::readSubfile(): (Frameset %d of %d) Reading header chunk... (@0x%llx)", i, chunkCount, static_cast<long long int>(chunk.pos()));
-		bool isHeaderChunk = (chunk._id == _header->_chunkReference);
+		bool isHeaderChunk = (chunk._id == _chunkReference);
 		if (isHeaderChunk) {
 			if (chunk._length != 0x04) {
 				error("Movie::readSubfile(): Expected movie header chunk of size 0x04, got 0x%x (@0x%llx)", chunk._length, static_cast<long long int>(chunk.pos()));
diff --git a/engines/mediastation/assets/movie.h b/engines/mediastation/assets/movie.h
index 2dbdc1a9977..62c50bd0bf2 100644
--- a/engines/mediastation/assets/movie.h
+++ b/engines/mediastation/assets/movie.h
@@ -25,8 +25,8 @@
 #include "common/array.h"
 #include "audio/audiostream.h"
 
+#include "mediastation/asset.h"
 #include "mediastation/datafile.h"
-#include "mediastation/assetheader.h"
 #include "mediastation/bitmap.h"
 #include "mediastation/mediascript/scriptconstants.h"
 
@@ -91,23 +91,32 @@ enum MovieSectionType {
 
 class Movie : public SpatialEntity {
 public:
-	Movie(AssetHeader *header);
+	Movie() : SpatialEntity(kAssetTypeMovie) {};
 	virtual ~Movie() override;
 
 	virtual void readChunk(Chunk &chunk) override;
 	virtual void readSubfile(Subfile &subfile, Chunk &chunk) override;
 
+	virtual void readParameter(Chunk &chunk, AssetHeaderSectionType paramType) override;
 	virtual ScriptValue callMethod(BuiltInMethod methodId, Common::Array<ScriptValue> &args) override;
 	virtual void process() override;
 
 	virtual void redraw(Common::Rect &rect) override;
 
-	virtual bool isVisible() const override { return _isShowing; }
+	virtual bool isVisible() const override { return _isVisible; }
+
+	uint32 _audioChunkReference = 0;
+	uint32 _animationChunkReference = 0;
 
 private:
-	bool _showByDefault = false;
-	bool _isShowing = false;
+	SoundEncoding _soundEncoding;
+	uint _chunkCount = 0;
+	uint _rate = 0;
+
+	uint _loadType = 0;
+	double _dissolveFactor = 0.0;
 	bool _isPlaying = false;
+	bool _atFirstFrame = true;
 
 	Common::Array<MovieFrame *> _frames;
 	Common::Array<MovieFrame *> _stills;
diff --git a/engines/mediastation/assets/palette.cpp b/engines/mediastation/assets/palette.cpp
index c55e78ee6ee..a82d4536e5b 100644
--- a/engines/mediastation/assets/palette.cpp
+++ b/engines/mediastation/assets/palette.cpp
@@ -25,5 +25,25 @@
 
 namespace MediaStation {
 
-} // End of namespace MediaStation
+Palette::~Palette() {
+	delete _palette;
+	_palette = nullptr;
+}
+
+void Palette::readParameter(Chunk &chunk, AssetHeaderSectionType paramType) {
+	switch (paramType) {
+	case kAssetHeaderPalette: {
+		const uint PALETTE_ENTRIES = 256;
+		const uint PALETTE_BYTES = PALETTE_ENTRIES * 3;
+		byte *buffer = new byte[PALETTE_BYTES];
+		chunk.read(buffer, PALETTE_BYTES);
+		_palette = new Graphics::Palette(buffer, PALETTE_ENTRIES, DisposeAfterUse::YES);
+		break;
+	}
 
+	default:
+		Asset::readParameter(chunk, paramType);
+	}
+}
+
+} // End of namespace MediaStation
diff --git a/engines/mediastation/assets/palette.h b/engines/mediastation/assets/palette.h
index b3dd3114c70..b54dc21fe8b 100644
--- a/engines/mediastation/assets/palette.h
+++ b/engines/mediastation/assets/palette.h
@@ -22,7 +22,6 @@
 #ifndef MEDIASTATION_PALETTE_H
 #define MEDIASTATION_PALETTE_H
 
-#include "mediastation/assetheader.h"
 #include "mediastation/asset.h"
 #include "mediastation/mediascript/scriptvalue.h"
 #include "mediastation/mediascript/scriptconstants.h"
@@ -31,7 +30,12 @@ namespace MediaStation {
 
 class Palette : public Asset {
 public:
-	Palette(AssetHeader *header) : Asset(header) {};
+	Palette() : Asset(kAssetTypePalette) {};
+	virtual ~Palette() override;
+
+	virtual void readParameter(Chunk &chunk, AssetHeaderSectionType paramType) override;
+
+	Graphics::Palette *_palette = nullptr;
 };
 
 } // End of namespace MediaStation
diff --git a/engines/mediastation/assets/path.cpp b/engines/mediastation/assets/path.cpp
index 10e637cf38f..5c5b9cb9f02 100644
--- a/engines/mediastation/assets/path.cpp
+++ b/engines/mediastation/assets/path.cpp
@@ -20,12 +20,42 @@
  */
 
 #include "mediastation/assets/path.h"
+#include "mediastation/mediastation.h"
 #include "mediastation/debugchannels.h"
 
 namespace MediaStation {
 
-Path::~Path() {
-	_percentComplete = 0;
+void Path::readParameter(Chunk &chunk, AssetHeaderSectionType paramType) {
+	switch (paramType) {
+	case kAssetHeaderStartPoint:
+		_startPoint = chunk.readTypedPoint();
+		break;
+
+	case kAssetHeaderEndPoint:
+		_endPoint = chunk.readTypedPoint();
+		break;
+
+	case kAssetHeaderStepRate: {
+		double _stepRateFloat = chunk.readTypedDouble();
+		// This should always be an integer anyway,
+		// so we'll cast away any fractional part.
+		_stepRate = static_cast<uint32>(_stepRateFloat);
+		break;
+	}
+
+	case kAssetHeaderDuration:
+		// These are stored in the file as fractional seconds,
+		// but we want milliseconds.
+		_duration = static_cast<uint32>(chunk.readTypedTime() * 1000);
+		break;
+
+	case kAssetHeaderPathTotalSteps:
+		_totalSteps = chunk.readTypedUint16();
+		break;
+
+	default:
+		Asset::readParameter(chunk, paramType);
+	}
 }
 
 ScriptValue Path::callMethod(BuiltInMethod methodId, Common::Array<ScriptValue> &args) {
@@ -51,12 +81,6 @@ ScriptValue Path::callMethod(BuiltInMethod methodId, Common::Array<ScriptValue>
 		return returnValue;
 	}
 
-	case kSetDissolveFactorMethod: {
-		assert(args.size() == 1);
-		warning("Path::callMethod(): setDissolveFactor not implemented yet");
-		return returnValue;
-	}
-
 	case kIsPlayingMethod: {
 		assert(args.empty());
 		returnValue.setToBool(_isActive);
@@ -74,9 +98,9 @@ void Path::timePlay() {
 		return;
 	}
 
-	if (_header->_duration == 0) {
+	if (_duration == 0) {
 		warning("Path::timePlay(): Got zero duration");
-	} else if (_header->_stepRate == 0) {
+	} else if (_stepRate == 0) {
 		error("Path::timePlay(): Got zero step rate");
 	}
 
@@ -84,8 +108,8 @@ void Path::timePlay() {
 	_percentComplete = 0;
 	_nextPathStepTime = 0;
 	_currentStep = 0;
-	_totalSteps = (_header->_duration * _header->_stepRate) / 1000;
-	_stepDurationInMilliseconds = 1000 / _header->_stepRate;
+	_totalSteps = (_duration * _stepRate) / 1000;
+	_stepDurationInMilliseconds = 1000 / _stepRate;
 
 	// TODO: Run the path start event. Haven't seen one the wild yet, don't know its ID.
 	debugC(5, kDebugScript, "Path::timePlay(): No PathStart event handler");
@@ -125,14 +149,12 @@ void Path::process() {
 void Path::setDuration(uint durationInMilliseconds) {
 	// TODO: Do we need to save the original duration?
 	debugC(5, kDebugScript, "Path::setDuration(): Setting duration to %d ms", durationInMilliseconds);
-	_header->_duration = durationInMilliseconds;
+	_duration = durationInMilliseconds;
 }
 
-
 double Path::percentComplete() {
 	debugC(5, kDebugScript, "Path::percentComplete(): Returning percent complete %f%%", _percentComplete * 100);
 	return _percentComplete;
 }
 
 } // End of namespace MediaStation
-
diff --git a/engines/mediastation/assets/path.h b/engines/mediastation/assets/path.h
index 60239bc7ef5..fe204387efe 100644
--- a/engines/mediastation/assets/path.h
+++ b/engines/mediastation/assets/path.h
@@ -22,7 +22,6 @@
 #ifndef MEDIASTATION_PATH_H
 #define MEDIASTATION_PATH_H
 
-#include "mediastation/assetheader.h"
 #include "mediastation/asset.h"
 #include "mediastation/mediascript/scriptvalue.h"
 #include "mediastation/mediascript/scriptconstants.h"
@@ -31,11 +30,11 @@ namespace MediaStation {
 
 class Path : public Asset {
 public:
-	Path(AssetHeader *header) : Asset(header) {};
-	virtual ~Path() override;
+	Path() : Asset(kAssetTypePath) {};
 
 	virtual void process() override;
 
+	virtual void readParameter(Chunk &chunk, AssetHeaderSectionType paramType) override;
 	virtual ScriptValue callMethod(BuiltInMethod methodId, Common::Array<ScriptValue> &args) override;
 
 private:
@@ -45,6 +44,11 @@ private:
 	uint _nextPathStepTime = 0;
 	uint _stepDurationInMilliseconds = 0;
 
+	Common::Point _startPoint;
+	Common::Point _endPoint;
+	uint32 _stepRate = 0;
+	uint32 _duration = 0;
+
 	// Method implementations.
 	void timePlay();
 	void setDuration(uint durationInMilliseconds);
diff --git a/engines/mediastation/assets/screen.cpp b/engines/mediastation/assets/screen.cpp
index c9b6d131e52..2d6cb41a807 100644
--- a/engines/mediastation/assets/screen.cpp
+++ b/engines/mediastation/assets/screen.cpp
@@ -24,4 +24,15 @@
 
 namespace MediaStation {
 
+void Screen::readParameter(Chunk &chunk, AssetHeaderSectionType paramType) {
+	switch (paramType) {
+	case kAssetHeaderCursorResourceId:
+		_cursorResourceId = chunk.readTypedUint16();
+		break;
+
+	default:
+		Asset::readParameter(chunk, paramType);
+	}
+}
+
 } // End of namespace MediaStation
diff --git a/engines/mediastation/assets/screen.h b/engines/mediastation/assets/screen.h
index c5846240361..78ef10b597a 100644
--- a/engines/mediastation/assets/screen.h
+++ b/engines/mediastation/assets/screen.h
@@ -22,7 +22,6 @@
 #ifndef MEDIASTATION_SCREEN_H
 #define MEDIASTATION_SCREEN_H
 
-#include "mediastation/assetheader.h"
 #include "mediastation/asset.h"
 #include "mediastation/mediascript/scriptvalue.h"
 #include "mediastation/mediascript/scriptconstants.h"
@@ -34,7 +33,11 @@ namespace MediaStation {
 // then a Screen asset header.
 class Screen : public Asset {
 public:
-	Screen(AssetHeader *header) : Asset(header) {};
+	Screen() : Asset(kAssetTypeScreen) {};
+
+	virtual void readParameter(Chunk &chunk, AssetHeaderSectionType paramType) override;
+
+	uint _cursorResourceId = 0;
 };
 
 } // End of namespace MediaStation
diff --git a/engines/mediastation/assets/sound.cpp b/engines/mediastation/assets/sound.cpp
index 100fb9a2f31..a7aa6d33d68 100644
--- a/engines/mediastation/assets/sound.cpp
+++ b/engines/mediastation/assets/sound.cpp
@@ -28,12 +28,6 @@
 
 namespace MediaStation {
 
-Sound::Sound(AssetHeader *header) : Asset(header) {
-	if (_header != nullptr) {
-		_encoding = _header->_soundEncoding;
-	}
-}
-
 Sound::~Sound() {
 	g_engine->_mixer->stopHandle(_handle);
 
@@ -43,6 +37,45 @@ Sound::~Sound() {
 	_streams.clear();
 }
 
+void Sound::readParameter(Chunk &chunk, AssetHeaderSectionType paramType) {
+	switch (paramType) {
+	case kAssetHeaderAssetId: {
+		// We already have this asset's ID, so we will just verify it is the same
+		// as the ID we have already read.
+		uint32 duplicateAssetId = chunk.readTypedUint16();
+		if (duplicateAssetId != _id) {
+			warning("Duplicate asset ID %d does not match original ID %d", duplicateAssetId, _id);
+		}
+		break;
+	}
+
+	case kAssetHeaderChunkReference:
+		_chunkReference = chunk.readTypedChunkReference();
+		break;
+
+	case kAssetHeaderHasOwnSubfile:
+		_hasOwnSubfile = static_cast<bool>(chunk.readTypedByte());
+		break;
+
+	case kAssetHeaderSoundInfo:
+		_chunkCount = chunk.readTypedUint16();
+		_rate = chunk.readTypedUint32();
+		break;
+
+	case kAssetHeaderSoundEncoding1:
+	case kAssetHeaderSoundEncoding2:
+		_encoding = static_cast<SoundEncoding>(chunk.readTypedUint16());
+		break;
+
+	case kAssetHeaderMovieLoadType:
+		_loadType = chunk.readTypedByte();
+		break;
+
+	default:
+		Asset::readParameter(chunk, paramType);
+	}
+}
+
 void Sound::process() {
 	processTimeEventHandlers();
 
@@ -80,7 +113,7 @@ void Sound::readChunk(Chunk &chunk) {
 	byte *buffer = (byte *)malloc(chunk._length);
 	chunk.read((void *)buffer, chunk._length);
 	Audio::SeekableAudioStream *stream = nullptr;
-	switch (_header->_soundEncoding) {
+	switch (_encoding) {
 	case SoundEncoding::PCM_S16LE_MONO_22050:
 		stream = Audio::makeRawStream(buffer, chunk._length, 22050, Audio::FLAG_16BITS | Audio::FLAG_LITTLE_ENDIAN);
 		break;
@@ -96,20 +129,19 @@ void Sound::readChunk(Chunk &chunk) {
 		break;
 
 	default:
-		error("Sound::readChunk(): Unknown audio encoding 0x%x", static_cast<uint>(_header->_soundEncoding));
+		error("Sound::readChunk(): Unknown audio encoding 0x%x", static_cast<uint>(_encoding));
 	}
 	_streams.push_back(stream);
 	debugC(5, kDebugLoading, "Sound::readChunk(): Finished reading audio chunk (@0x%llx)", static_cast<long long int>(chunk.pos()));
 }
 
 void Sound::readSubfile(Subfile &subfile, Chunk &chunk) {
-	uint32 totalChunks = _header->_chunkCount;
 	uint32 expectedChunkId = chunk._id;
 
-	debugC(5, kDebugLoading, "Sound::readSubfile(): Reading %d chunks", totalChunks);
+	debugC(5, kDebugLoading, "Sound::readSubfile(): Reading %d chunks", _chunkCount);
 	readChunk(chunk);
-	for (uint i = 1; i < totalChunks; i++) {
-		debugC(5, kDebugLoading, "Sound::readSubfile(): Reading chunk %d of %d", i, totalChunks);
+	for (uint i = 1; i < _chunkCount; i++) {
+		debugC(5, kDebugLoading, "Sound::readSubfile(): Reading chunk %d of %d", i, _chunkCount);
 		chunk = subfile.nextChunk();
 		if (chunk._id != expectedChunkId) {
 			error("Sound::readSubfile(): Expected chunk %s, got %s", tag2str(expectedChunkId), tag2str(chunk._id));
diff --git a/engines/mediastation/assets/sound.h b/engines/mediastation/assets/sound.h
index 41435f730e4..d30124533ab 100644
--- a/engines/mediastation/assets/sound.h
+++ b/engines/mediastation/assets/sound.h
@@ -22,11 +22,11 @@
 #ifndef MEDIASTATION_ASSETS_SOUND_H
 #define MEDIASTATION_ASSETS_SOUND_H
 
+#include "audio/mixer.h"
 #include "audio/audiostream.h"
 
 #include "mediastation/asset.h"
 #include "mediastation/datafile.h"
-#include "mediastation/assetheader.h"
 #include "mediastation/mediascript/scriptvalue.h"
 #include "mediastation/mediascript/scriptconstants.h"
 
@@ -34,9 +34,10 @@ namespace MediaStation {
 
 class Sound : public Asset {
 public:
-	Sound(AssetHeader *header);
+	Sound() : Asset(kAssetTypeSound) {};
 	~Sound();
 
+	virtual void readParameter(Chunk &chunk, AssetHeaderSectionType paramType) override;
 	virtual ScriptValue callMethod(BuiltInMethod methodId, Common::Array<ScriptValue> &args) override;
 	virtual void process() override;
 
@@ -44,6 +45,10 @@ public:
 	virtual void readSubfile(Subfile &subFile, Chunk &chunk) override;
 
 private:
+	uint _loadType = 0;
+	uint _chunkCount = 0;
+	uint _rate = 0;
+	bool _hasOwnSubfile = false;
 	SoundEncoding _encoding;
 	Audio::SoundHandle _handle;
 	Common::Array<Audio::SeekableAudioStream *> _streams;
diff --git a/engines/mediastation/assets/sprite.cpp b/engines/mediastation/assets/sprite.cpp
index 575ee5d82f4..8fdd5094310 100644
--- a/engines/mediastation/assets/sprite.cpp
+++ b/engines/mediastation/assets/sprite.cpp
@@ -60,18 +60,11 @@ uint32 SpriteFrame::index() {
 	return _bitmapHeader->_index;
 }
 
-Sprite::Sprite(AssetHeader *header) : SpatialEntity(header) {
-	if (header->_startup == kAssetStartupActive) {
-		setActive();
-		_isShowing = true;
-		_showFirstFrame = true;
-	}
-}
 
 Sprite::~Sprite() {
 	// If we're just referencing another asset's frames,
 	// don't delete those frames.
-	if (_header->_assetReference == 0) {
+	if (_assetReference == 0) {
 		for (SpriteFrame *frame : _frames) {
 			delete frame;
 		}
@@ -79,6 +72,44 @@ Sprite::~Sprite() {
 	_frames.clear();
 }
 
+void Sprite::readParameter(Chunk &chunk, AssetHeaderSectionType paramType) {
+	switch (paramType) {
+	case kAssetHeaderChunkReference:
+		_chunkReference = chunk.readTypedChunkReference();
+		break;
+
+	case kAssetHeaderDissolveFactor:
+		_dissolveFactor = chunk.readTypedDouble();
+		break;
+
+	case kAssetHeaderFrameRate:
+		_frameRate = static_cast<uint32>(chunk.readTypedDouble());
+		break;
+
+	case kAssetHeaderLoadType:
+		_loadType = chunk.readTypedByte();
+		break;
+
+	case kAssetHeaderSpriteChunkCount:
+		_frameCount = chunk.readTypedUint16();
+		break;
+
+	case kAssetHeaderSpriteFrameMapping: {
+		uint32 externalFrameId = chunk.readTypedUint16();
+		uint32 internalFrameId = chunk.readTypedUint16();
+		uint32 unk1 = chunk.readTypedUint16();
+		if (unk1 != internalFrameId) {
+			warning("AssetHeader::readSection(): Repeated internalFrameId doesn't match");
+		}
+		_spriteFrameMapping.setVal(externalFrameId, internalFrameId);
+		break;
+	}
+
+	default:
+		SpatialEntity::readParameter(chunk, paramType);
+	}
+}
+
 ScriptValue Sprite::callMethod(BuiltInMethod methodId, Common::Array<ScriptValue> &args) {
 	ScriptValue returnValue;
 
@@ -95,6 +126,13 @@ ScriptValue Sprite::callMethod(BuiltInMethod methodId, Common::Array<ScriptValue
 		return returnValue;
 	}
 
+	case kSetDissolveFactorMethod: {
+		warning("STUB: setDissolveFactor");
+		assert(args.size() == 1);
+		_dissolveFactor = args[0].asFloat();
+		return returnValue;
+	}
+
 	case kTimePlayMethod: {
 		assert(args.empty());
 		timePlay();
@@ -116,7 +154,7 @@ ScriptValue Sprite::callMethod(BuiltInMethod methodId, Common::Array<ScriptValue
 	case kSetCurrentClipMethod: {
 		assert(args.size() <= 1);
 		if (args.size() == 1 && args[0].asParamToken() != 0) {
-			error("Sprite::callMethod(): (%d) setClip() called with unhandled arg: %d", _header->_id, args[0].asParamToken());
+			error("Sprite::callMethod(): (%d) setClip() called with unhandled arg: %d", _id, args[0].asParamToken());
 		}
 		setCurrentClip();
 		return returnValue;
@@ -125,7 +163,7 @@ ScriptValue Sprite::callMethod(BuiltInMethod methodId, Common::Array<ScriptValue
 	case kSetSpriteFrameByIdMethod: {
 		assert(args.size() == 1);
 		uint32 externalFrameId = args[0].asParamToken();
-		uint32 internalFrameId = _header->_spriteFrameMapping.getVal(externalFrameId);
+		uint32 internalFrameId = _spriteFrameMapping.getVal(externalFrameId);
 		showFrame(_frames[internalFrameId]);
 		return returnValue;
 	}
@@ -141,35 +179,35 @@ ScriptValue Sprite::callMethod(BuiltInMethod methodId, Common::Array<ScriptValue
 }
 
 void Sprite::spatialShow() {
-	if (_isShowing) {
-		warning("Sprite::spatialShow(): (%d) Attempted to spatialShow when already showing", _header->_id);
+	if (_isVisible) {
+		warning("Sprite::spatialShow(): (%d) Attempted to spatialShow when already showing", _id);
 		return;
 	}
 	showFrame(_frames[0]);
 
 	setActive();
-	_isShowing = true;
+	_isVisible = true;
 	_isPlaying = false;
 }
 
 void Sprite::spatialHide() {
-	if (!_isShowing) {
-		warning("Sprite::spatialHide(): (%d) Attempted to spatialHide when not showing", _header->_id);
+	if (!_isVisible) {
+		warning("Sprite::spatialHide(): (%d) Attempted to spatialHide when not showing", _id);
 		return;
 	}
 	showFrame(nullptr);
 
 	setInactive();
-	_isShowing = false;
+	_isVisible = false;
 	_isPlaying = false;
 }
 
 void Sprite::timePlay() {
-	if (!_isShowing) {
-		warning("Sprite::timePlay(): (%d) Attempted to timePlay when not showing", _header->_id);
+	if (!_isVisible) {
+		warning("Sprite::timePlay(): (%d) Attempted to timePlay when not showing", _id);
 		return;
 	} else if (_isPlaying) {
-		warning("Sprite::timePlay(): (%d) Attempted to timePlay when already playing", _header->_id);
+		warning("Sprite::timePlay(): (%d) Attempted to timePlay when already playing", _id);
 		return;
 	}
 
@@ -181,11 +219,11 @@ void Sprite::timePlay() {
 }
 
 void Sprite::timeStop() {
-	if (!_isShowing) {
-		warning("Sprite::timeStop(): (%d) Attempted to timeStop when not showing", _header->_id);
+	if (!_isVisible) {
+		warning("Sprite::timeStop(): (%d) Attempted to timeStop when not showing", _id);
 		return;
 	} else if (!_isPlaying) {
-		warning("Sprite::timeStop(): (%d) Attempted to timeStop when not playing", _header->_id);
+		warning("Sprite::timeStop(): (%d) Attempted to timeStop when not playing", _id);
 		return;
 	}
 
@@ -195,7 +233,7 @@ void Sprite::timeStop() {
 
 void Sprite::movieReset() {
 	setActive();
-	if (_isShowing) {
+	if (_isVisible) {
 		showFrame(_frames[0]);
 	} else {
 		showFrame(nullptr);
@@ -211,7 +249,7 @@ void Sprite::setCurrentClip() {
 	if (_currentFrameIndex < _frames.size()) {
 		showFrame(_frames[_currentFrameIndex++]);
 	} else {
-		warning("Sprite::setCurrentClip(): (%d) Attempted to increment past number of frames", _header->_id);
+		warning("Sprite::setCurrentClip(): (%d) Attempted to increment past number of frames", _id);
 	}
 }
 
@@ -229,15 +267,15 @@ void Sprite::readChunk(Chunk &chunk) {
 
 	// TODO: Are these in exactly reverse order? If we can just reverse the
 	// whole thing once.
-	Common::sort(_frames.begin(), _frames.end(), [](SpriteFrame * a, SpriteFrame * b) {
+	Common::sort(_frames.begin(), _frames.end(), [](SpriteFrame *a, SpriteFrame *b) {
 		return a->index() < b->index();
 	});
 }
 
 void Sprite::updateFrameState() {
-	if (_showFirstFrame) {
+	if (_isVisible && _atFirstFrame) {
 		showFrame(_frames[0]);
-		_showFirstFrame = false;
+		_atFirstFrame = false;
 		return;
 	}
 
@@ -248,15 +286,15 @@ void Sprite::updateFrameState() {
 	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());
+				_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);
+			debugC(6, kDebugGraphics, "Sprite::updateFrameState(): (%d): Not playing, no persistent frame", _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());
+		_id, _activeFrame->index(), _activeFrame->width(), _activeFrame->height(), _activeFrame->left(), _activeFrame->top());
 
 	uint currentTime = g_system->getMillis() - _startTime;
 	bool drawNextFrame = currentTime >= _nextFrameTime;
@@ -266,7 +304,7 @@ void Sprite::updateFrameState() {
 
 	showFrame(_frames[_currentFrameIndex]);
 
-	uint frameDuration = 1000 / _header->_frameRate;
+	uint frameDuration = 1000 / _frameRate;
 	_nextFrameTime = ++_currentFrameIndex * frameDuration;
 
 	bool spriteFinishedPlaying = (_currentFrameIndex == _frames.size());
@@ -291,7 +329,7 @@ void Sprite::updateFrameState() {
 }
 
 void Sprite::redraw(Common::Rect &rect) {
-	if (_activeFrame == nullptr || !_isShowing) {
+	if (_activeFrame == nullptr || !_isVisible) {
 		return;
 	}
 
@@ -299,7 +337,7 @@ void Sprite::redraw(Common::Rect &rect) {
 	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);
+		areaToRedraw.translate(-_activeFrame->left() - _boundingBox.left, -_activeFrame->top() - _boundingBox.top);
 		areaToRedraw.clip(Common::Rect(0, 0, _activeFrame->width(), _activeFrame->height()));
 		g_engine->_screen->simpleBlitFrom(_activeFrame->_surface, areaToRedraw, originOnScreen);
 	}
@@ -322,7 +360,7 @@ 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);
+	bbox.translate(_boundingBox.left, _boundingBox.top);
 	return bbox;
 }
 
diff --git a/engines/mediastation/assets/sprite.h b/engines/mediastation/assets/sprite.h
index 8994b412250..ebbebb98604 100644
--- a/engines/mediastation/assets/sprite.h
+++ b/engines/mediastation/assets/sprite.h
@@ -26,7 +26,6 @@
 #include "common/array.h"
 
 #include "mediastation/asset.h"
-#include "mediastation/assetheader.h"
 #include "mediastation/datafile.h"
 #include "mediastation/bitmap.h"
 #include "mediastation/mediascript/scriptvalue.h"
@@ -63,24 +62,29 @@ class Sprite : public SpatialEntity {
 friend class Context;
 
 public:
-	Sprite(AssetHeader *header);
+	Sprite() : SpatialEntity(kAssetTypeSprite) {};
 	~Sprite();
 
 	virtual void process() override;
 	virtual void redraw(Common::Rect &rect) override;
 
+	virtual void readParameter(Chunk &chunk, AssetHeaderSectionType paramType) override;
 	virtual ScriptValue callMethod(BuiltInMethod methodId, Common::Array<ScriptValue> &args) override;
 
-	virtual bool isVisible() const override { return _isShowing; }
+	virtual bool isVisible() const override { return _isVisible; }
 
 	virtual void readChunk(Chunk &chunk) override;
 
 private:
+	double _dissolveFactor = 0.0;
+	uint _loadType = 0;
+	uint _frameRate = 0;
+	uint _frameCount = 0;
+	Common::HashMap<uint32, uint32> _spriteFrameMapping;
 	Common::Array<SpriteFrame *> _frames;
 	SpriteFrame *_activeFrame = nullptr;
-	bool _showFirstFrame = true;
-	bool _isShowing = false;
 	bool _isPlaying = false;
+	bool _atFirstFrame = true;
 	uint _currentFrameIndex = 0;
 	uint _nextFrameTime = 0;
 
diff --git a/engines/mediastation/assets/stage.h b/engines/mediastation/assets/stage.h
deleted file mode 100644
index e6768a07719..00000000000
--- a/engines/mediastation/assets/stage.h
+++ /dev/null
@@ -1,50 +0,0 @@
-/* ScummVM - Graphic Adventure Engine
- *
- * ScummVM is the legal property of its developers, whose names
- * are too numerous to list here. Please refer to the COPYRIGHT
- * file distributed with this source distribution.
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- *
- */
-
-#ifndef MEDIASTATION_STAGE_H
-#define MEDIASTATION_STAGE_H
-
-#include "mediastation/asset.h"
-
-namespace MediaStation {
-
-class Camera : public Asset {
-public:
-	Camera(AssetHeader *header) : Asset(header) {};
-
-	virtual ScriptValue callMethod(BuiltInMethod methodId, Common::Array<ScriptValue> &args) override { error("CallMethod not implemented"); };
-	virtual void process() override {};
-};
-
-class Stage : public Asset {
-public:
-	Stage(AssetHeader *header) : Asset(header) {};
-
-	virtual ScriptValue callMethod(BuiltInMethod methodId, Common::Array<ScriptValue> &args) override { error("CallMethod not implemented"); };
-	virtual void process() override {};
-
-private:
-	Common::Array<Asset *> _children;
-};
-
-}
-
-#endif
\ No newline at end of file
diff --git a/engines/mediastation/assets/text.cpp b/engines/mediastation/assets/text.cpp
index 59dead1d9c2..1006a517039 100644
--- a/engines/mediastation/assets/text.cpp
+++ b/engines/mediastation/assets/text.cpp
@@ -23,6 +23,57 @@
 
 namespace MediaStation {
 
+void Text::readParameter(Chunk &chunk, AssetHeaderSectionType paramType) {
+	switch (paramType) {
+	case kAssetHeaderStartup:
+		_isVisible = static_cast<bool>(chunk.readTypedByte());
+		break;
+
+	case kAssetHeaderEditable:
+		_editable = chunk.readTypedByte();
+		break;
+
+	case kAssetHeaderLoadType:
+		_loadType = chunk.readTypedByte();
+		break;
+
+	case kAssetHeaderFontId:
+		_fontAssetId = chunk.readTypedUint16();
+		break;
+
+	case kAssetHeaderTextMaxLength:
+		_maxTextLength = chunk.readTypedUint16();
+		break;
+
+	case kAssetHeaderInitialText:
+		_text = chunk.readTypedString();
+		break;
+
+	case kAssetHeaderTextJustification:
+		_justification = static_cast<TextJustification>(chunk.readTypedUint16());
+		break;
+
+	case kAssetHeaderTextPosition:
+		_position = static_cast<TextPosition>(chunk.readTypedUint16());
+		break;
+
+	case kAssetHeaderTextCharacterClass: {
+		CharacterClass characterClass;
+		characterClass.firstAsciiCode = chunk.readTypedUint16();
+		characterClass.lastAsciiCode = chunk.readTypedUint16();
+		_acceptedInput.push_back(characterClass);
+		break;
+	}
+
+	case kAssetHeaderDissolveFactor:
+		_dissolveFactor = chunk.readTypedDouble();
+		break;
+
+	default:
+		SpatialEntity::readParameter(chunk, paramType);
+	}
+}
+
 ScriptValue Text::callMethod(BuiltInMethod methodId, Common::Array<ScriptValue> &args) {
 	ScriptValue returnValue;
 
@@ -57,7 +108,7 @@ ScriptValue Text::callMethod(BuiltInMethod methodId, Common::Array<ScriptValue>
 }
 
 Common::String Text::text() const {
-	return _header->_text;
+	return _text;
 }
 
 void Text::setText(Common::String text) {
diff --git a/engines/mediastation/assets/text.h b/engines/mediastation/assets/text.h
index e7dd97bb690..0e186f82220 100644
--- a/engines/mediastation/assets/text.h
+++ b/engines/mediastation/assets/text.h
@@ -25,21 +25,47 @@
 #include "common/str.h"
 
 #include "mediastation/asset.h"
-#include "mediastation/assetheader.h"
 #include "mediastation/mediascript/scriptvalue.h"
 #include "mediastation/mediascript/scriptconstants.h"
 
 namespace MediaStation {
 
+enum TextJustification {
+	kTextJustificationLeft = 0x25c,
+	kTextJustificationRight = 0x25d,
+	kTextJustificationCenter = 0x25e
+};
+
+enum TextPosition {
+	kTextPositionMiddle = 0x25e,
+	kTextPositionTop = 0x260,
+	kTextPositionBotom = 0x261
+};
+
+struct CharacterClass {
+	uint firstAsciiCode = 0;
+	uint lastAsciiCode = 0;
+};
+
 class Text : public SpatialEntity {
 public:
-	Text(AssetHeader *header) : SpatialEntity(header) {};
+	Text() : SpatialEntity(kAssetTypeText) {};
 
 	virtual bool isVisible() const override { return _isVisible; }
+	virtual void readParameter(Chunk &chunk, AssetHeaderSectionType paramType) override;
 	virtual ScriptValue callMethod(BuiltInMethod methodId, Common::Array<ScriptValue> &args) override;
 
 private:
+	bool _editable = false;
+	uint _loadType = 0;
+	double _dissolveFactor = 0.0;
 	bool _isVisible = false;
+	Common::String _text;
+	uint _maxTextLength = 0;
+	uint _fontAssetId = 0;
+	TextJustification _justification;
+	TextPosition _position;
+	Common::Array<CharacterClass> _acceptedInput;
 
 	// Method implementations.
 	Common::String text() const;
diff --git a/engines/mediastation/assets/timer.cpp b/engines/mediastation/assets/timer.cpp
index 284f971510e..323cf409858 100644
--- a/engines/mediastation/assets/timer.cpp
+++ b/engines/mediastation/assets/timer.cpp
@@ -68,8 +68,8 @@ void Timer::timePlay() {
 	// TODO: Is there a better way to find out what the max time is? Do we have to look
 	// through each of the timer event handlers to figure it out?
 	_duration = 0;
-	const Common::Array<EventHandler *> &_timeHandlers = _header->_eventHandlers.getValOrDefault(kTimerEvent);
-	for (EventHandler *timeEvent : _timeHandlers) {
+	const Common::Array<EventHandler *> &timeHandlers = _eventHandlers.getValOrDefault(kTimerEvent);
+	for (EventHandler *timeEvent : timeHandlers) {
 		// Indeed float, not time.
 		double timeEventInFractionalSeconds = timeEvent->_argumentValue.asFloat();
 		uint timeEventInMilliseconds = timeEventInFractionalSeconds * 1000;
diff --git a/engines/mediastation/assets/timer.h b/engines/mediastation/assets/timer.h
index ecf037becf8..2775b78001e 100644
--- a/engines/mediastation/assets/timer.h
+++ b/engines/mediastation/assets/timer.h
@@ -23,7 +23,6 @@
 #define MEDIASTATION_TIMER_H
 
 #include "mediastation/asset.h"
-#include "mediastation/assetheader.h"
 #include "mediastation/mediascript/scriptvalue.h"
 #include "mediastation/mediascript/scriptconstants.h"
 
@@ -31,7 +30,7 @@ namespace MediaStation {
 
 class Timer : public Asset {
 public:
-	Timer(AssetHeader *header) : Asset(header) {};
+	Timer() : Asset(kAssetTypeTimer) {};
 
 	virtual ScriptValue callMethod(BuiltInMethod methodId, Common::Array<ScriptValue> &args) override;
 	virtual void process() override;
diff --git a/engines/mediastation/bitmap.h b/engines/mediastation/bitmap.h
index 8ef973a6c2a..441396a33fe 100644
--- a/engines/mediastation/bitmap.h
+++ b/engines/mediastation/bitmap.h
@@ -26,7 +26,7 @@
 #include "graphics/managed_surface.h"
 
 #include "mediastation/datafile.h"
-#include "mediastation/assetheader.h"
+#include "mediastation/asset.h"
 
 namespace MediaStation {
 
diff --git a/engines/mediastation/context.cpp b/engines/mediastation/context.cpp
index 5fbd406c925..23a868975c3 100644
--- a/engines/mediastation/context.cpp
+++ b/engines/mediastation/context.cpp
@@ -79,7 +79,7 @@ Context::Context(const Common::Path &path) : Datafile(path) {
 	// these and create the appropriate references.
 	for (auto it = _assets.begin(); it != _assets.end(); ++it) {
 		Asset *asset = it->_value;
-		uint referencedAssetId = asset->getHeader()->_assetReference;
+		uint referencedAssetId = asset->_assetReference;
 		if (referencedAssetId != 0) {
 			switch (asset->type()) {
 			case kAssetTypeImage: {
@@ -155,7 +155,7 @@ void Context::registerActiveAssets() {
 	}
 }
 
-void Context::readParametersSection(Chunk &chunk) {
+void Context::readCreateContextData(Chunk &chunk) {
 	_fileNumber = chunk.readTypedUint16();
 
 	ContextParametersSectionType sectionType = static_cast<ContextParametersSectionType>(chunk.readTypedUint16());
@@ -182,7 +182,7 @@ void Context::readParametersSection(Chunk &chunk) {
 		}
 
 		case kContextParametersVariable: {
-			readVariable(chunk);
+			readCreateVariableData(chunk);
 			break;
 		}
 
@@ -200,20 +200,86 @@ void Context::readParametersSection(Chunk &chunk) {
 	}
 }
 
-void Context::readVariable(Chunk &chunk) {
+Asset *Context::readCreateAssetData(Chunk &chunk) {
+	uint contextId = chunk.readTypedUint16();
+	AssetType type = static_cast<AssetType>(chunk.readTypedUint16());
+	uint id = chunk.readTypedUint16();
+	debugC(4, kDebugLoading, "_type = 0x%x, _id = 0x%x", static_cast<uint>(type), id);
+
+	Asset *asset = nullptr;
+	switch (type) {
+	case kAssetTypeImage:
+		asset = new Image();
+		break;
+
+	case kAssetTypeMovie:
+		asset = new Movie();
+		break;
+
+	case kAssetTypeSound:
+		asset = new Sound();
+		break;
+
+	case kAssetTypePalette:
+		asset = new Palette();
+		break;
+
+	case kAssetTypePath:
+		asset = new Path();
+		break;
+
+	case kAssetTypeTimer:
+		asset = new Timer();
+		break;
+
+	case kAssetTypeHotspot:
+		asset = new Hotspot();
+		break;
+
+	case kAssetTypeSprite:
+		asset = new Sprite();
+		break;
+
+	case kAssetTypeCanvas:
+		asset = new Canvas();
+		break;
+
+	case kAssetTypeScreen:
+		asset = new Screen();
+		_screenAsset = static_cast<Screen *>(asset);
+		break;
+
+	case kAssetTypeFont:
+		asset = new Font();
+		break;
+
+	case kAssetTypeText:
+		asset = new Text();
+		break;
+
+	default:
+		error("No class for asset type 0x%x (@0x%llx)", static_cast<uint>(type), static_cast<long long int>(chunk.pos()));
+	}
+	asset->setId(id);
+	asset->setContextId(contextId);
+	asset->initFromParameterStream(chunk);
+	return asset;
+}
+
+void Context::readCreateVariableData(Chunk &chunk) {
 	uint repeatedFileNumber = chunk.readTypedUint16();
 	if (repeatedFileNumber != _fileNumber) {
-		warning("Context::readVariable(): Repeated file number didn't match: %d != %d", repeatedFileNumber, _fileNumber);
+		warning("Context::readCreateVariableData(): Repeated file number didn't match: %d != %d", repeatedFileNumber, _fileNumber);
 	}
 
 	uint id = chunk.readTypedUint16();
 	if (g_engine->getVariable(id) != nullptr) {
-		error("Context::readVariable(): Global variable %d already exists", id);
+		error("Global variable %d already exists", id);
 	}
 
 	ScriptValue *value = new ScriptValue(&chunk);
 	_variables.setVal(id, value);
-	debugC(5, kDebugScript, "Context::readVariable(): Created global variable %d (type: %s)",
+	debugC(5, kDebugScript, "Created global variable %d (type: %s)",
 		id, scriptValueTypeToStr(value->getType()));
 }
 
@@ -238,7 +304,7 @@ void Context::readNewStyleHeaderSections(Subfile &subfile, Chunk &chunk) {
 		}
 
 		// Read this header section.
-		moreSectionsToRead = readHeaderSection(subfile, chunk);
+		moreSectionsToRead = readHeaderSection(chunk);
 		if (subfile.atEnd()) {
 			break;
 		} else {
@@ -286,12 +352,12 @@ void Context::readAssetFromLaterSubfile(Subfile &subfile) {
 	asset->readSubfile(subfile, chunk);
 }
 
-bool Context::readHeaderSection(Subfile &subfile, Chunk &chunk) {
+bool Context::readHeaderSection(Chunk &chunk) {
 	uint16 sectionType = chunk.readTypedUint16();
 	debugC(5, kDebugLoading, "Context::readHeaderSection(): sectionType = 0x%x (@0x%llx)", static_cast<uint>(sectionType), static_cast<long long int>(chunk.pos()));
 	switch (sectionType) {
 	case kContextParametersSection: {
-		readParametersSection(chunk);
+		readCreateContextData(chunk);
 		break;
 	}
 
@@ -322,76 +388,24 @@ bool Context::readHeaderSection(Subfile &subfile, Chunk &chunk) {
 	}
 
 	case kContextAssetHeaderSection: {
-		Asset *asset = nullptr;
-		AssetHeader *header = new AssetHeader(chunk);
-		switch (header->_type) {
-		case kAssetTypeImage:
-			asset = new Image(header);
-			break;
-
-		case kAssetTypeMovie:
-			asset = new Movie(header);
-			break;
-
-		case kAssetTypeSound:
-			asset = new Sound(header);
-			break;
-
-		case kAssetTypePalette:
-			asset = new Palette(header);
-			break;
-
-		case kAssetTypePath:
-			asset = new Path(header);
-			break;
-
-		case kAssetTypeTimer:
-			asset = new Timer(header);
-			break;
-
-		case kAssetTypeHotspot:
-			asset = new Hotspot(header);
-			break;
-
-		case kAssetTypeSprite:
-			asset = new Sprite(header);
-			break;
-
-		case kAssetTypeCanvas:
-			asset = new Canvas(header);
-			break;
-
-		case kAssetTypeScreen:
-			asset = new Screen(header);
-			_screenAsset = asset;
-			break;
-
-		case kAssetTypeFont:
-			asset = new Font(header);
-			break;
-
-		case kAssetTypeText:
-			asset = new Text(header);
-			break;
-
-		default:
-			error("Context::readHeaderSection(): No class for asset type 0x%x (@0x%llx)", static_cast<uint>(header->_type), static_cast<long long int>(chunk.pos()));
+		Asset *asset = readCreateAssetData(chunk);
+		if (g_engine->getAssetById(asset->id())) {
+			error("Context::readHeaderSection(): Asset with ID 0x%d was already defined in this title", asset->id());
 		}
 
-		if (g_engine->getAssetById(header->_id)) {
-			error("Context::readHeaderSection(): Asset with ID 0x%d was already defined in this title", header->_id);
-		}
-		_assets.setVal(header->_id, asset);
-		if (header->_chunkReference != 0) {
-			debugC(5, kDebugLoading, "Context::readHeaderSection(): Storing asset with chunk ID \"%s\" (0x%x)", tag2str(header->_chunkReference), header->_chunkReference);
-			_assetsByChunkReference.setVal(header->_chunkReference, asset);
+		_assets.setVal(asset->id(), asset);
+		if (asset->_chunkReference != 0) {
+			debugC(5, kDebugLoading, "Context::readHeaderSection(): Storing asset with chunk ID \"%s\" (0x%x)", tag2str(asset->_chunkReference), asset->_chunkReference);
+			_assetsByChunkReference.setVal(asset->_chunkReference, asset);
 		}
-		// TODO: Store the movie chunk references better.
-		if (header->_audioChunkReference != 0) {
-			_assetsByChunkReference.setVal(header->_audioChunkReference, asset);
-		}
-		if (header->_animationChunkReference != 0) {
-			_assetsByChunkReference.setVal(header->_animationChunkReference, asset);
+		if (asset->type() == kAssetTypeMovie) {
+			Movie *movie = static_cast<Movie *>(asset);
+			if (movie->_audioChunkReference != 0) {
+				_assetsByChunkReference.setVal(movie->_audioChunkReference, asset);
+			}
+			if (movie->_animationChunkReference != 0) {
+				_assetsByChunkReference.setVal(movie->_animationChunkReference, asset);
+			}
 		}
 		// TODO: This datum only appears sometimes.
 		uint unk2 = chunk.readTypedUint16();
diff --git a/engines/mediastation/context.h b/engines/mediastation/context.h
index a7ed7e62853..986d64a9f1e 100644
--- a/engines/mediastation/context.h
+++ b/engines/mediastation/context.h
@@ -28,7 +28,7 @@
 #include "graphics/palette.h"
 
 #include "mediastation/datafile.h"
-#include "mediastation/assetheader.h"
+#include "mediastation/asset.h"
 #include "mediastation/mediascript/function.h"
 
 namespace MediaStation {
@@ -53,6 +53,8 @@ enum ContextSectionType {
 	kContextFunctionSection = 0x0031
 };
 
+class Screen;
+
 class Context : public Datafile {
 public:
 	Context(const Common::Path &path);
@@ -62,7 +64,7 @@ public:
 	uint32 _subfileCount;
 	uint32 _fileSize;
 	Graphics::Palette *_palette = nullptr;
-	Asset *_screenAsset = nullptr;
+	Screen *_screenAsset = nullptr;
 
 	Asset *getAssetById(uint assetId);
 	Asset *getAssetByChunkReference(uint chunkReference);
@@ -82,11 +84,13 @@ private:
 	Common::HashMap<uint, Asset *> _assetsByChunkReference;
 	Common::HashMap<uint, ScriptValue *> _variables;
 
-	void readParametersSection(Chunk &chunk);
-	void readVariable(Chunk &chunk);
 	void readOldStyleHeaderSections(Subfile &subfile, Chunk &chunk);
 	void readNewStyleHeaderSections(Subfile &subfile, Chunk &chunk);
-	bool readHeaderSection(Subfile &subfile, Chunk &chunk);
+
+	bool readHeaderSection(Chunk &chunk);
+	void readCreateContextData(Chunk &chunk);
+	Asset *readCreateAssetData(Chunk &chunk);
+	void readCreateVariableData(Chunk &chunk);
 
 	void readAssetInFirstSubfile(Chunk &chunk);
 	void readAssetFromLaterSubfile(Subfile &subfile);
diff --git a/engines/mediastation/mediastation.cpp b/engines/mediastation/mediastation.cpp
index 7001e7edf8e..69f417f4527 100644
--- a/engines/mediastation/mediastation.cpp
+++ b/engines/mediastation/mediastation.cpp
@@ -28,8 +28,10 @@
 #include "mediastation/boot.h"
 #include "mediastation/context.h"
 #include "mediastation/asset.h"
-#include "mediastation/assets/hotspot.h"
 #include "mediastation/assets/movie.h"
+#include "mediastation/assets/screen.h"
+#include "mediastation/assets/palette.h"
+#include "mediastation/assets/hotspot.h"
 #include "mediastation/mediascript/scriptconstants.h"
 
 namespace MediaStation {
@@ -268,7 +270,11 @@ void MediaStationEngine::setCursor(uint id) {
 }
 
 void MediaStationEngine::refreshActiveHotspot() {
-	Asset *hotspot = findAssetToAcceptMouseEvents();
+	Asset *asset = findAssetToAcceptMouseEvents();
+	if (asset != nullptr && asset->type() != kAssetTypeHotspot) {
+		return;
+	}
+	Hotspot *hotspot = static_cast<Hotspot *>(asset);
 	if (hotspot != _currentHotspot) {
 		if (_currentHotspot != nullptr) {
 			_currentHotspot->runEventHandlerIfExists(kMouseExitedEvent);
@@ -277,11 +283,11 @@ void MediaStationEngine::refreshActiveHotspot() {
 		_currentHotspot = hotspot;
 		if (hotspot != nullptr) {
 			debugC(5, kDebugEvents, "refreshActiveHotspot(): (%d, %d): Entered hotspot %d", _mousePos.x, _mousePos.y, hotspot->id());
-			setCursor(hotspot->getHeader()->_cursorResourceId);
+			setCursor(hotspot->_cursorResourceId);
 			hotspot->runEventHandlerIfExists(kMouseEnteredEvent);
 		} else {
 			// There is no hotspot, so set the default cursor for this screen instead.
-			setCursor(_currentContext->_screenAsset->getHeader()->_cursorResourceId);
+			setCursor(_currentContext->_screenAsset->_cursorResourceId);
 		}
 	}
 
@@ -373,17 +379,14 @@ Context *MediaStationEngine::loadContext(uint32 contextId) {
 	return context;
 }
 
-void MediaStationEngine::setPalette(Asset *palette) {
-	assert(palette != nullptr);
-	setPaletteFromHeader(palette->getHeader());
-}
-
-void MediaStationEngine::setPaletteFromHeader(AssetHeader *header) {
-	assert(header != nullptr);
-	if (header->_palette != nullptr) {
-		_screen->setPalette(*header->_palette);
+void MediaStationEngine::setPalette(Asset *asset) {
+	if (asset == nullptr) {
+		error("Requested palette not found");
+	} else if (asset->type() != kAssetTypePalette) {
+		error("Requested palette %d is not a palette", asset->id());
 	} else {
-		warning("MediaStationEngine::setPaletteFromHeader(): Asset %d does not have a palette. Current palette will be unchanged.", header->_id);
+		Palette *paletteAsset = static_cast<Palette *>(asset);
+		_screen->setPalette(*paletteAsset->_palette);
 	}
 }
 
diff --git a/engines/mediastation/mediastation.h b/engines/mediastation/mediastation.h
index b0e07b0064e..10e3f1a0f9f 100644
--- a/engines/mediastation/mediastation.h
+++ b/engines/mediastation/mediastation.h
@@ -46,6 +46,7 @@
 namespace MediaStation {
 
 struct MediaStationGameDescription;
+class Hotspot;
 
 // Most Media Station titles follow this file structure from the root directory
 // of the CD-ROM:
@@ -118,14 +119,13 @@ private:
 	Boot *_boot = nullptr;
 	Common::List<Asset *> _assetsPlaying;
 	Common::HashMap<uint, Context *> _loadedContexts;
-	Asset *_currentHotspot = nullptr;
+	Hotspot *_currentHotspot = nullptr;
 
 	uint _requestedScreenBranchId = 0;
 	Common::Array<uint> _requestedContextReleaseId;
 	void doBranchToScreen();
 
 	Context *loadContext(uint32 contextId);
-	void setPaletteFromHeader(AssetHeader *header);
 	void releaseContext(uint32 contextId);
 	Asset *findAssetToAcceptMouseEvents();
 
diff --git a/engines/mediastation/module.mk b/engines/mediastation/module.mk
index 8c43dddd48c..b0a38fea5f8 100644
--- a/engines/mediastation/module.mk
+++ b/engines/mediastation/module.mk
@@ -2,7 +2,6 @@ MODULE := engines/mediastation
 
 MODULE_OBJS = \
 	asset.o \
-	assetheader.o \
 	assets/canvas.o \
 	assets/font.o \
 	assets/hotspot.o \


Commit: a7aae9b0b1ab7797eae817535b650a8b30010894
    https://github.com/scummvm/scummvm/commit/a7aae9b0b1ab7797eae817535b650a8b30010894
Author: Nathanael Gentry (nathanael.gentrydb8 at gmail.com)
Date: 2025-06-14T11:07:14-04:00

Commit Message:
MEDIASTATION: Factor audio sequences into their own class

Changed paths:
  A engines/mediastation/audio.cpp
  A engines/mediastation/audio.h
    engines/mediastation/asset.h
    engines/mediastation/assets/movie.cpp
    engines/mediastation/assets/movie.h
    engines/mediastation/assets/sound.cpp
    engines/mediastation/assets/sound.h
    engines/mediastation/module.mk


diff --git a/engines/mediastation/asset.h b/engines/mediastation/asset.h
index f4f27c09445..0e6e759e0ec 100644
--- a/engines/mediastation/asset.h
+++ b/engines/mediastation/asset.h
@@ -60,8 +60,6 @@ enum AssetType {
 
 enum AssetHeaderSectionType {
 	kAssetHeaderEmptySection = 0x0000,
-	kAssetHeaderSoundEncoding1 = 0x0001,
-	kAssetHeaderSoundEncoding2 = 0x0002,
 	kAssetHeaderEventHandler = 0x0017,
 	kAssetHeaderStageId = 0x0019,
 	kAssetHeaderAssetId = 0x001a,
@@ -119,10 +117,6 @@ enum AssetHeaderSectionType {
 	kAssetHeaderSpriteFrameMapping = 0x03e9
 };
 
-enum SoundEncoding {
-	PCM_S16LE_MONO_22050 = 0x0010, // Uncompressed linear PCM
-	IMA_ADPCM_S16LE_MONO_22050 = 0x0004 // IMA ADPCM encoding, must be decoded
-};
 class Asset {
 public:
 	Asset(AssetType type) : _type(type) {};
diff --git a/engines/mediastation/assets/movie.cpp b/engines/mediastation/assets/movie.cpp
index 3887330e49f..e1bfe3f12d7 100644
--- a/engines/mediastation/assets/movie.cpp
+++ b/engines/mediastation/assets/movie.cpp
@@ -19,9 +19,6 @@
  *
  */
 
-#include "audio/decoders/raw.h"
-#include "audio/decoders/adpcm.h"
-
 #include "mediastation/mediastation.h"
 #include "mediastation/assets/movie.h"
 #include "mediastation/debugchannels.h"
@@ -155,8 +152,6 @@ MovieFrame::~MovieFrame() {
 }
 
 Movie::~Movie() {
-	g_engine->_mixer->stopHandle(_soundHandle);
-
 	for (MovieFrame *frame : _frames) {
 		delete frame;
 	}
@@ -167,11 +162,6 @@ Movie::~Movie() {
 	}
 	_stills.clear();
 
-	for (Audio::SeekableAudioStream *stream : _audioStreams) {
-		delete stream;
-	}
-	_audioStreams.clear();
-
 	for (MovieFrameFooter *footer : _footers) {
 		delete footer;
 	}
@@ -219,13 +209,8 @@ void Movie::readParameter(Chunk &chunk, AssetHeaderSectionType paramType) {
 		break;
 
 	case kAssetHeaderSoundInfo:
-		_chunkCount = chunk.readTypedUint16();
-		_rate = chunk.readTypedUint32();
-		break;
-
-	case kAssetHeaderSoundEncoding1:
-	case kAssetHeaderSoundEncoding2:
-		_soundEncoding = static_cast<SoundEncoding>(chunk.readTypedUint16());
+		_audioChunkCount = chunk.readTypedUint16();
+		_audioSequence.readParameters(chunk);
 		break;
 
 	default:
@@ -345,19 +330,8 @@ void Movie::timePlay() {
 		return;
 	}
 
-	// START PLAYING SOUND.
 	// TODO: This won't work when we have some chunks that don't have audio.
-	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!
-		g_engine->_mixer->playStream(Audio::Mixer::kPlainSoundType, &_soundHandle, audio, -1, Audio::Mixer::kMaxChannelVolume);
-		audio->finish();
-	}
-
+	_audioSequence.play();
 	_framesNotYetShown = _frames;
 	_isVisible = true;
 	_isPlaying = true;
@@ -379,9 +353,7 @@ void Movie::timeStop() {
 	}
 	_framesOnScreen.clear();
 	_framesNotYetShown.clear();
-
-	g_engine->_mixer->stopHandle(_soundHandle);
-	_soundHandle = Audio::SoundHandle();
+	_audioSequence.stop();
 
 	// Show the persistent frames.
 	_isPlaying = false;
@@ -595,22 +567,7 @@ void Movie::readSubfile(Subfile &subfile, Chunk &chunk) {
 		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 == _audioChunkReference);
 		if (isAudioChunk) {
-			byte *buffer = (byte *)malloc(chunk._length);
-			chunk.read((void *)buffer, chunk._length);
-			Audio::SeekableAudioStream *stream = nullptr;
-			switch (_soundEncoding) {
-			case SoundEncoding::PCM_S16LE_MONO_22050:
-				stream = Audio::makeRawStream(buffer, chunk._length, 22050, Audio::FLAG_16BITS | Audio::FLAG_LITTLE_ENDIAN);
-				break;
-
-			case SoundEncoding::IMA_ADPCM_S16LE_MONO_22050:
-				error("Movie::readSubfile(): ADPCM decoding not supported for movies");
-				break;
-
-			default:
-				error("Movie::readSubfile(): Unknown audio encoding 0x%x", static_cast<uint>(_soundEncoding));
-			}
-			_audioStreams.push_back(stream);
+			_audioSequence.readChunk(chunk);
 			chunk = subfile.nextChunk();
 		} else {
 			debugC(5, kDebugLoading, "Movie::readSubfile(): (Frameset %d of %d) No audio chunk to read. (@0x%llx)", i, chunkCount, static_cast<long long int>(chunk.pos()));
diff --git a/engines/mediastation/assets/movie.h b/engines/mediastation/assets/movie.h
index 62c50bd0bf2..aca2623f284 100644
--- a/engines/mediastation/assets/movie.h
+++ b/engines/mediastation/assets/movie.h
@@ -23,9 +23,9 @@
 #define MEDIASTATION_MOVIE_H
 
 #include "common/array.h"
-#include "audio/audiostream.h"
 
 #include "mediastation/asset.h"
+#include "mediastation/audio.h"
 #include "mediastation/datafile.h"
 #include "mediastation/bitmap.h"
 #include "mediastation/mediascript/scriptconstants.h"
@@ -109,9 +109,8 @@ public:
 	uint32 _animationChunkReference = 0;
 
 private:
-	SoundEncoding _soundEncoding;
-	uint _chunkCount = 0;
-	uint _rate = 0;
+	AudioSequence _audioSequence;
+	uint _audioChunkCount = 0;
 
 	uint _loadType = 0;
 	double _dissolveFactor = 0.0;
@@ -121,8 +120,6 @@ private:
 	Common::Array<MovieFrame *> _frames;
 	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 a7aa6d33d68..b7670596189 100644
--- a/engines/mediastation/assets/sound.cpp
+++ b/engines/mediastation/assets/sound.cpp
@@ -19,24 +19,12 @@
  *
  */
 
-#include "audio/decoders/raw.h"
-#include "audio/decoders/adpcm.h"
-
+#include "mediastation/audio.h"
 #include "mediastation/debugchannels.h"
 #include "mediastation/assets/sound.h"
-#include "mediastation/mediastation.h"
 
 namespace MediaStation {
 
-Sound::~Sound() {
-	g_engine->_mixer->stopHandle(_handle);
-
-	for (Audio::SeekableAudioStream *stream : _streams) {
-		delete stream;
-	}
-	_streams.clear();
-}
-
 void Sound::readParameter(Chunk &chunk, AssetHeaderSectionType paramType) {
 	switch (paramType) {
 	case kAssetHeaderAssetId: {
@@ -59,12 +47,7 @@ void Sound::readParameter(Chunk &chunk, AssetHeaderSectionType paramType) {
 
 	case kAssetHeaderSoundInfo:
 		_chunkCount = chunk.readTypedUint16();
-		_rate = chunk.readTypedUint32();
-		break;
-
-	case kAssetHeaderSoundEncoding1:
-	case kAssetHeaderSoundEncoding2:
-		_encoding = static_cast<SoundEncoding>(chunk.readTypedUint16());
+		_sequence.readParameters(chunk);
 		break;
 
 	case kAssetHeaderMovieLoadType:
@@ -79,10 +62,10 @@ void Sound::readParameter(Chunk &chunk, AssetHeaderSectionType paramType) {
 void Sound::process() {
 	processTimeEventHandlers();
 
-	if (_isActive && !g_engine->_mixer->isSoundHandleActive(_handle)) {
+	if (_isActive && !_sequence.isActive()) {
 		_isPlaying = false;
 		setInactive();
-		_handle = Audio::SoundHandle();
+		_sequence.stop();
 
 		runEventHandlerIfExists(kSoundEndEvent);
 	}
@@ -92,6 +75,14 @@ ScriptValue Sound::callMethod(BuiltInMethod methodId, Common::Array<ScriptValue>
 	ScriptValue returnValue;
 
 	switch (methodId) {
+	case kSpatialShowMethod:
+		// WORKAROUND: No-op to avoid triggering error on Dalmatians
+		// timer_6c06_AnsweringMachine, which calls SpatialShow on a sound.
+		// Since the engine is currently flagging errors on unimplemented
+		// methods for easier debugging, a no-op is used here to avoid the error.
+		assert(args.empty());
+		return returnValue;
+
 	case kTimePlayMethod: {
 		assert(args.empty());
 		timePlay();
@@ -109,32 +100,6 @@ ScriptValue Sound::callMethod(BuiltInMethod methodId, Common::Array<ScriptValue>
 	}
 }
 
-void Sound::readChunk(Chunk &chunk) {
-	byte *buffer = (byte *)malloc(chunk._length);
-	chunk.read((void *)buffer, chunk._length);
-	Audio::SeekableAudioStream *stream = nullptr;
-	switch (_encoding) {
-	case SoundEncoding::PCM_S16LE_MONO_22050:
-		stream = Audio::makeRawStream(buffer, chunk._length, 22050, Audio::FLAG_16BITS | Audio::FLAG_LITTLE_ENDIAN);
-		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);
-		warning("Sound::readSubfile(): ADPCM decoding not implemented yet");
-		chunk.skip(chunk.bytesRemaining());
-		break;
-
-	default:
-		error("Sound::readChunk(): Unknown audio encoding 0x%x", static_cast<uint>(_encoding));
-	}
-	_streams.push_back(stream);
-	debugC(5, kDebugLoading, "Sound::readChunk(): Finished reading audio chunk (@0x%llx)", static_cast<long long int>(chunk.pos()));
-}
-
 void Sound::readSubfile(Subfile &subfile, Chunk &chunk) {
 	uint32 expectedChunkId = chunk._id;
 
@@ -156,25 +121,15 @@ void Sound::timePlay() {
 		return;
 	}
 
-	if (_streams.empty()) {
+	if (_sequence.isEmpty()) {
 		warning("Sound::timePlay(): Sound has no contents, probably because the sound is in INSTALL.CXT and isn't loaded yet");
 		return;
 	}
+
 	_isPlaying = true;
 	setActive();
-
+	_sequence.play();
 	runEventHandlerIfExists(kSoundBeginEvent);
-
-	_handle = Audio::SoundHandle();
-	if (!_streams.empty()) {
-		Audio::QueuingAudioStream *audio = Audio::makeQueuingAudioStream(22050, false);
-		for (Audio::SeekableAudioStream *stream : _streams) {
-			stream->rewind();
-			audio->queueAudioStream(stream, DisposeAfterUse::NO);
-		}
-		g_engine->_mixer->playStream(Audio::Mixer::kPlainSoundType, &_handle, audio, -1, Audio::Mixer::kMaxChannelVolume, DisposeAfterUse::YES);
-		audio->finish();
-	}
 }
 
 void Sound::timeStop() {
@@ -184,10 +139,7 @@ void Sound::timeStop() {
 	}
 
 	setInactive();
-
-	g_engine->_mixer->stopHandle(_handle);
-	_handle = Audio::SoundHandle();
-
+	_sequence.stop();
 	runEventHandlerIfExists(kSoundStoppedEvent);
 }
 
diff --git a/engines/mediastation/assets/sound.h b/engines/mediastation/assets/sound.h
index d30124533ab..bdb7ac7a91e 100644
--- a/engines/mediastation/assets/sound.h
+++ b/engines/mediastation/assets/sound.h
@@ -22,10 +22,8 @@
 #ifndef MEDIASTATION_ASSETS_SOUND_H
 #define MEDIASTATION_ASSETS_SOUND_H
 
-#include "audio/mixer.h"
-#include "audio/audiostream.h"
-
 #include "mediastation/asset.h"
+#include "mediastation/audio.h"
 #include "mediastation/datafile.h"
 #include "mediastation/mediascript/scriptvalue.h"
 #include "mediastation/mediascript/scriptconstants.h"
@@ -35,24 +33,20 @@ namespace MediaStation {
 class Sound : public Asset {
 public:
 	Sound() : Asset(kAssetTypeSound) {};
-	~Sound();
 
 	virtual void readParameter(Chunk &chunk, AssetHeaderSectionType paramType) override;
 	virtual ScriptValue callMethod(BuiltInMethod methodId, Common::Array<ScriptValue> &args) override;
 	virtual void process() override;
 
-	virtual void readChunk(Chunk& chunk) override;
+	virtual void readChunk(Chunk& chunk) override { _sequence.readChunk(chunk); }
 	virtual void readSubfile(Subfile &subFile, Chunk &chunk) override;
 
 private:
 	uint _loadType = 0;
-	uint _chunkCount = 0;
-	uint _rate = 0;
 	bool _hasOwnSubfile = false;
-	SoundEncoding _encoding;
-	Audio::SoundHandle _handle;
-	Common::Array<Audio::SeekableAudioStream *> _streams;
 	bool _isPlaying = true;
+	uint _chunkCount = 0;
+	AudioSequence _sequence;
 
 	// Script method implementations
 	void timePlay();
diff --git a/engines/mediastation/audio.cpp b/engines/mediastation/audio.cpp
new file mode 100644
index 00000000000..78322f42e49
--- /dev/null
+++ b/engines/mediastation/audio.cpp
@@ -0,0 +1,91 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "audio/decoders/adpcm.h"
+#include "audio/decoders/raw.h"
+
+#include "mediastation/audio.h"
+#include "mediastation/debugchannels.h"
+#include "mediastation/mediastation.h"
+
+namespace MediaStation {
+
+AudioSequence::~AudioSequence() {
+	g_engine->_mixer->stopHandle(_handle);
+
+	for (Audio::SeekableAudioStream *stream : _streams) {
+		delete stream;
+	}
+	_streams.clear();
+}
+
+void AudioSequence::play() {
+	_handle = Audio::SoundHandle();
+	if (!_streams.empty()) {
+		Audio::QueuingAudioStream *audio = Audio::makeQueuingAudioStream(22050, false);
+		for (Audio::SeekableAudioStream *stream : _streams) {
+			stream->rewind();
+			audio->queueAudioStream(stream, DisposeAfterUse::NO);
+		}
+		g_engine->_mixer->playStream(Audio::Mixer::kPlainSoundType, &_handle, audio, -1, Audio::Mixer::kMaxChannelVolume, DisposeAfterUse::YES);
+		audio->finish();
+	}
+}
+
+void AudioSequence::stop() {
+	g_engine->_mixer->stopHandle(_handle);
+	_handle = Audio::SoundHandle();
+}
+
+void AudioSequence::readParameters(Chunk &chunk) {
+	_rate = chunk.readTypedUint32();
+	_channelCount = chunk.readTypedUint16();
+	_bitsPerSample = chunk.readTypedUint16();
+}
+
+void AudioSequence::readChunk(Chunk &chunk) {
+	byte *buffer = (byte *)malloc(chunk._length);
+	chunk.read((void *)buffer, chunk._length);
+	Audio::SeekableAudioStream *stream = nullptr;
+	switch (_bitsPerSample) {
+	case 16:
+		stream = Audio::makeRawStream(buffer, chunk._length, _rate, Audio::FLAG_16BITS | Audio::FLAG_LITTLE_ENDIAN);
+		break;
+
+	case 4: // IMA ADPCM-encoded
+		// TODO: The interface here is different. We can't pass in the
+		// buffers directly. We have to make a stream first.
+		warning("ADPCM decoding not implemented yet");
+		chunk.skip(chunk.bytesRemaining());
+		break;
+
+	default:
+		error("Unknown audio encoding 0x%x", static_cast<uint>(_bitsPerSample));
+	}
+	_streams.push_back(stream);
+	debugC(5, kDebugLoading, "Finished reading audio chunk (@0x%llx)", static_cast<long long int>(chunk.pos()));
+}
+
+bool AudioSequence::isActive() {
+	return g_engine->_mixer->isSoundHandleActive(_handle);
+}
+
+} // End of namespace MediaStation
diff --git a/engines/mediastation/audio.h b/engines/mediastation/audio.h
new file mode 100644
index 00000000000..bf62956cc20
--- /dev/null
+++ b/engines/mediastation/audio.h
@@ -0,0 +1,57 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#ifndef MEDIASTATION_AUDIO_H
+#define MEDIASTATION_AUDIO_H
+
+#include "common/array.h"
+#include "audio/audiostream.h"
+#include "audio/mixer.h"
+
+#include "mediastation/datafile.h"
+
+namespace MediaStation {
+
+class AudioSequence {
+public:
+	AudioSequence() {};
+	~AudioSequence();
+
+	void play();
+	void stop();
+
+	void readParameters(Chunk &chunk);
+	void readChunk(Chunk &chunk);
+	bool isActive();
+	bool isEmpty() { return _streams.empty(); }
+
+	uint _rate = 0;
+	uint _channelCount = 0;
+	uint _bitsPerSample = 0;
+
+private:
+	Common::Array<Audio::SeekableAudioStream *> _streams;
+	Audio::SoundHandle _handle;
+};
+
+} // End of namespace MediaStation
+
+#endif
diff --git a/engines/mediastation/module.mk b/engines/mediastation/module.mk
index b0a38fea5f8..abd4880133c 100644
--- a/engines/mediastation/module.mk
+++ b/engines/mediastation/module.mk
@@ -14,6 +14,7 @@ MODULE_OBJS = \
 	assets/sprite.o \
 	assets/text.o \
 	assets/timer.o \
+	audio.o \
 	bitmap.o \
 	boot.o \
 	context.o \


Commit: e56caa42fe808af800f6ab4e7e797ae5e3772bd5
    https://github.com/scummvm/scummvm/commit/e56caa42fe808af800f6ab4e7e797ae5e3772bd5
Author: Nathanael Gentry (nathanael.gentrydb8 at gmail.com)
Date: 2025-06-14T11:07:14-04:00

Commit Message:
MEDIASTATION: Don't keep separate "active" assets list

Because it's not necessary and not how the original does it.

Changed paths:
    engines/mediastation/asset.cpp
    engines/mediastation/asset.h
    engines/mediastation/assets/hotspot.cpp
    engines/mediastation/assets/hotspot.h
    engines/mediastation/assets/image.cpp
    engines/mediastation/assets/movie.cpp
    engines/mediastation/assets/path.cpp
    engines/mediastation/assets/path.h
    engines/mediastation/assets/sound.cpp
    engines/mediastation/assets/sound.h
    engines/mediastation/assets/sprite.cpp
    engines/mediastation/assets/text.cpp
    engines/mediastation/assets/timer.cpp
    engines/mediastation/assets/timer.h
    engines/mediastation/context.cpp
    engines/mediastation/context.h
    engines/mediastation/mediastation.cpp
    engines/mediastation/mediastation.h


diff --git a/engines/mediastation/asset.cpp b/engines/mediastation/asset.cpp
index 028241aa347..d2be6c294fb 100644
--- a/engines/mediastation/asset.cpp
+++ b/engines/mediastation/asset.cpp
@@ -83,25 +83,7 @@ void Asset::readSubfile(Subfile &subfile, Chunk &chunk) {
 	error("Asset::readSubfile(): Subfile reading for asset type 0x%x is not implemented", static_cast<uint>(_type));
 }
 
-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) {
-		warning("Asset::processTimeEventHandlers(): Attempted to process time event handlers while asset %d is not playing", _id);
-		return;
-	}
-
 	// TODO: Replace with a queue.
 	uint currentTime = g_system->getMillis();
 	const Common::Array<EventHandler *> &_timeHandlers = _eventHandlers.getValOrDefault(kTimerEvent);
@@ -238,13 +220,6 @@ void SpatialEntity::readParameter(Chunk &chunk, AssetHeaderSectionType paramType
 		_zIndex = chunk.readTypedGraphicUnit();
 		break;
 
-	case kAssetHeaderStartup:
-		_isVisible = static_cast<bool>(chunk.readTypedByte());
-		if (_isVisible) {
-			setActive();
-		}
-		break;
-
 	case kAssetHeaderTransparency:
 		_hasTransparency = static_cast<bool>(chunk.readTypedByte());
 		break;
diff --git a/engines/mediastation/asset.h b/engines/mediastation/asset.h
index 0e6e759e0ec..68138c74e4a 100644
--- a/engines/mediastation/asset.h
+++ b/engines/mediastation/asset.h
@@ -129,7 +129,6 @@ public:
 	virtual ScriptValue callMethod(BuiltInMethod methodId, Common::Array<ScriptValue> &args);
 
 	virtual bool isSpatialActor() const { return false; }
-	virtual bool isActive() const { return _isActive; }
 
 	virtual void initFromParameterStream(Chunk &chunk);
 	virtual void readParameter(Chunk &chunk, AssetHeaderSectionType paramType);
@@ -139,8 +138,6 @@ public:
 	virtual void readChunk(Chunk &chunk);
 	virtual void readSubfile(Subfile &subfile, Chunk &chunk);
 
-	void setInactive();
-	void setActive();
 	void processTimeEventHandlers();
 	void runEventHandlerIfExists(EventType eventType, const ScriptValue &arg);
 	void runEventHandlerIfExists(EventType eventType);
@@ -159,7 +156,6 @@ protected:
 	uint _id = 0;
 	uint _contextId = 0;
 
-	bool _isActive = false;
 	uint _startTime = 0;
 	uint _lastProcessedTime = 0;
 	uint _duration = 0;
diff --git a/engines/mediastation/assets/hotspot.cpp b/engines/mediastation/assets/hotspot.cpp
index 5081b8fb176..888babbc503 100644
--- a/engines/mediastation/assets/hotspot.cpp
+++ b/engines/mediastation/assets/hotspot.cpp
@@ -35,6 +35,10 @@ void Hotspot::readParameter(Chunk &chunk, AssetHeaderSectionType paramType) {
 		break;
 	}
 
+	case kAssetHeaderStartup:
+		_isActive = static_cast<bool>(chunk.readTypedByte());
+		break;
+
 	case kAssetHeaderCursorResourceId:
 		_cursorResourceId = chunk.readTypedUint16();
 		break;
@@ -93,7 +97,6 @@ ScriptValue Hotspot::callMethod(BuiltInMethod methodId, Common::Array<ScriptValu
 	case kMouseActivateMethod: {
 		assert(args.empty());
 		_isActive = true;
-		g_engine->addPlayingAsset(this);
 		g_engine->_needsHotspotRefresh = true;
 		return returnValue;
 	}
diff --git a/engines/mediastation/assets/hotspot.h b/engines/mediastation/assets/hotspot.h
index 6994a716afc..7dab8844e1f 100644
--- a/engines/mediastation/assets/hotspot.h
+++ b/engines/mediastation/assets/hotspot.h
@@ -35,12 +35,16 @@ public:
 
 	bool isInside(const Common::Point &pointToCheck);
 	virtual bool isVisible() const override { return false; }
+	bool isActive() const { return _isActive; }
 
 	virtual void readParameter(Chunk &chunk, AssetHeaderSectionType paramType) override;
 	virtual ScriptValue callMethod(BuiltInMethod methodId, Common::Array<ScriptValue> &args) override;
 
 	uint _cursorResourceId = 0;
 	Common::Array<Common::Point> _mouseActiveArea;
+
+private:
+	bool _isActive = false;
 };
 
 } // End of namespace MediaStation
diff --git a/engines/mediastation/assets/image.cpp b/engines/mediastation/assets/image.cpp
index 9e91db1f263..a3b1fb91e7a 100644
--- a/engines/mediastation/assets/image.cpp
+++ b/engines/mediastation/assets/image.cpp
@@ -40,6 +40,10 @@ void Image::readParameter(Chunk &chunk, AssetHeaderSectionType paramType) {
 		_chunkReference = chunk.readTypedChunkReference();
 		break;
 
+	case kAssetHeaderStartup:
+		_isVisible = static_cast<bool>(chunk.readTypedByte());
+		break;
+
 	case kAssetHeaderLoadType:
 		_loadType = chunk.readTypedByte();
 		break;
@@ -89,7 +93,7 @@ ScriptValue Image::callMethod(BuiltInMethod methodId, Common::Array<ScriptValue>
 }
 
 void Image::redraw(Common::Rect &rect) {
-	if (!_isActive) {
+	if (!_isVisible) {
 		return;
 	}
 
@@ -105,15 +109,12 @@ void Image::redraw(Common::Rect &rect) {
 }
 
 void Image::spatialShow() {
-	_isActive = true;
 	_isVisible = true;
-	g_engine->addPlayingAsset(this);
 	Common::Rect bbox(getLeftTop(), _bitmap->width(), _bitmap->height());
 	g_engine->_dirtyRects.push_back(bbox);
 }
 
 void Image::spatialHide() {
-	_isActive = false;
 	_isVisible = false;
 	Common::Rect bbox(getLeftTop(), _bitmap->width(), _bitmap->height());
 	g_engine->_dirtyRects.push_back(bbox);
diff --git a/engines/mediastation/assets/movie.cpp b/engines/mediastation/assets/movie.cpp
index e1bfe3f12d7..bdc6ff29a2d 100644
--- a/engines/mediastation/assets/movie.cpp
+++ b/engines/mediastation/assets/movie.cpp
@@ -196,6 +196,10 @@ void Movie::readParameter(Chunk &chunk, AssetHeaderSectionType paramType) {
 		break;
 	}
 
+	case kAssetHeaderStartup:
+		_isVisible = static_cast<bool>(chunk.readTypedByte());
+		break;
+
 	case kAssetHeaderDissolveFactor:
 		_dissolveFactor = chunk.readTypedDouble();
 		break;
@@ -297,7 +301,6 @@ void Movie::spatialShow() {
 		g_engine->_dirtyRects.push_back(getFrameBoundingBox(still));
 	}
 
-	setActive();
 	_isVisible = true;
 	_isPlaying = false;
 }
@@ -319,14 +322,12 @@ void Movie::spatialHide() {
 
 	_isVisible = false;
 	_isPlaying = false;
-	setInactive();
 }
 
 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 (_isPlaying) {
-		warning("Movie::timePlay(): (%d) Attempted to play a movie that is already playing", _id);
 		return;
 	}
 
@@ -335,8 +336,10 @@ void Movie::timePlay() {
 	_framesNotYetShown = _frames;
 	_isVisible = true;
 	_isPlaying = true;
-	setActive();
+	_startTime = g_system->getMillis();
+	_lastProcessedTime = 0;
 	runEventHandlerIfExists(kMovieBeginEvent);
+	process();
 }
 
 void Movie::timeStop() {
@@ -362,26 +365,24 @@ void Movie::timeStop() {
 			_framesOnScreen.push_back(still);
 			g_engine->_dirtyRects.push_back(getFrameBoundingBox(still));
 		}
-	} else {
-		setInactive();
 	}
 
 	runEventHandlerIfExists(kMovieStoppedEvent);
 }
 
 void Movie::process() {
+	if (_isVisible && _atFirstFrame) {
+		spatialShow();
+		_atFirstFrame = false;
+	}
+
 	if (_isPlaying) {
 		processTimeEventHandlers();
+		updateFrameState();
 	}
-	updateFrameState();
 }
 
 void Movie::updateFrameState() {
-	if (_isVisible && _atFirstFrame) {
-		spatialShow();
-		_atFirstFrame = false;
-	}
-
 	if (!_isPlaying) {
 		debugC(6, kDebugGraphics, "Movie::updateFrameState (%d): Not playing", _id);
 		for (MovieFrame *frame : _framesOnScreen) {
@@ -437,9 +438,7 @@ void Movie::updateFrameState() {
 	if (_framesOnScreen.empty() && _framesNotYetShown.empty()) {
 		_isPlaying = false;
 		_framesOnScreen.clear();
-		if (_stills.empty()) {
-			setInactive();
-		} else {
+		if (!_stills.empty()) {
 			showPersistentFrame();
 		}
 
diff --git a/engines/mediastation/assets/path.cpp b/engines/mediastation/assets/path.cpp
index 5c5b9cb9f02..e487eb6b0ed 100644
--- a/engines/mediastation/assets/path.cpp
+++ b/engines/mediastation/assets/path.cpp
@@ -83,7 +83,7 @@ ScriptValue Path::callMethod(BuiltInMethod methodId, Common::Array<ScriptValue>
 
 	case kIsPlayingMethod: {
 		assert(args.empty());
-		returnValue.setToBool(_isActive);
+		returnValue.setToBool(_isPlaying);
 		return returnValue;
 	}
 
@@ -93,8 +93,7 @@ ScriptValue Path::callMethod(BuiltInMethod methodId, Common::Array<ScriptValue>
 }
 
 void Path::timePlay() {
-	if (_isActive) {
-		warning("Path::timePlay(): Attempted to play a path that is already playing");
+	if (_isPlaying) {
 		return;
 	}
 
@@ -104,7 +103,9 @@ void Path::timePlay() {
 		error("Path::timePlay(): Got zero step rate");
 	}
 
-	setActive();
+	_isPlaying = true;
+	_startTime = g_system->getMillis();
+	_lastProcessedTime = 0;
 	_percentComplete = 0;
 	_nextPathStepTime = 0;
 	_currentStep = 0;
@@ -116,6 +117,10 @@ void Path::timePlay() {
 }
 
 void Path::process() {
+	if (!_isPlaying) {
+		return;
+	}
+
 	uint currentTime = g_system->getMillis();
 	uint pathTime = currentTime - _startTime;
 
@@ -135,7 +140,7 @@ void Path::process() {
 		runEventHandlerIfExists(kStepEvent);
 		_nextPathStepTime = ++_currentStep * _stepDurationInMilliseconds;
 	} else {
-		setInactive();
+		_isPlaying = false;
 		_percentComplete = 0;
 		_nextPathStepTime = 0;
 		_currentStep = 0;
diff --git a/engines/mediastation/assets/path.h b/engines/mediastation/assets/path.h
index fe204387efe..dc2f280bf8d 100644
--- a/engines/mediastation/assets/path.h
+++ b/engines/mediastation/assets/path.h
@@ -43,6 +43,7 @@ private:
 	uint _currentStep = 0;
 	uint _nextPathStepTime = 0;
 	uint _stepDurationInMilliseconds = 0;
+	bool _isPlaying = false;
 
 	Common::Point _startPoint;
 	Common::Point _endPoint;
diff --git a/engines/mediastation/assets/sound.cpp b/engines/mediastation/assets/sound.cpp
index b7670596189..c972f2595bd 100644
--- a/engines/mediastation/assets/sound.cpp
+++ b/engines/mediastation/assets/sound.cpp
@@ -60,13 +60,14 @@ void Sound::readParameter(Chunk &chunk, AssetHeaderSectionType paramType) {
 }
 
 void Sound::process() {
-	processTimeEventHandlers();
+	if (!_isPlaying) {
+		return;
+	}
 
-	if (_isActive && !_sequence.isActive()) {
+	processTimeEventHandlers();
+	if (!_sequence.isActive()) {
 		_isPlaying = false;
-		setInactive();
 		_sequence.stop();
-
 		runEventHandlerIfExists(kSoundEndEvent);
 	}
 }
@@ -116,29 +117,29 @@ void Sound::readSubfile(Subfile &subfile, Chunk &chunk) {
 }
 
 void Sound::timePlay() {
-	if (_isActive) {
-		warning("Sound::timePlay(): Attempt to play a sound that is already playing");
+	if (_isPlaying) {
 		return;
 	}
 
 	if (_sequence.isEmpty()) {
 		warning("Sound::timePlay(): Sound has no contents, probably because the sound is in INSTALL.CXT and isn't loaded yet");
+		_isPlaying = false;
 		return;
 	}
 
 	_isPlaying = true;
-	setActive();
+	_startTime = g_system->getMillis();
+	_lastProcessedTime = 0;
 	_sequence.play();
 	runEventHandlerIfExists(kSoundBeginEvent);
 }
 
 void Sound::timeStop() {
-	if (!_isActive) {
-		warning("Sound::timeStop(): Attempt to stop a sound that isn't playing");
+	if (!_isPlaying) {
 		return;
 	}
 
-	setInactive();
+	_isPlaying = false;
 	_sequence.stop();
 	runEventHandlerIfExists(kSoundStoppedEvent);
 }
diff --git a/engines/mediastation/assets/sound.h b/engines/mediastation/assets/sound.h
index bdb7ac7a91e..d1c94ccf6f8 100644
--- a/engines/mediastation/assets/sound.h
+++ b/engines/mediastation/assets/sound.h
@@ -38,13 +38,13 @@ public:
 	virtual ScriptValue callMethod(BuiltInMethod methodId, Common::Array<ScriptValue> &args) override;
 	virtual void process() override;
 
-	virtual void readChunk(Chunk& chunk) override { _sequence.readChunk(chunk); }
+	virtual void readChunk(Chunk &chunk) override { _sequence.readChunk(chunk); }
 	virtual void readSubfile(Subfile &subFile, Chunk &chunk) override;
 
 private:
 	uint _loadType = 0;
 	bool _hasOwnSubfile = false;
-	bool _isPlaying = true;
+	bool _isPlaying = false;
 	uint _chunkCount = 0;
 	AudioSequence _sequence;
 
diff --git a/engines/mediastation/assets/sprite.cpp b/engines/mediastation/assets/sprite.cpp
index 8fdd5094310..c9b3721d00b 100644
--- a/engines/mediastation/assets/sprite.cpp
+++ b/engines/mediastation/assets/sprite.cpp
@@ -90,6 +90,10 @@ void Sprite::readParameter(Chunk &chunk, AssetHeaderSectionType paramType) {
 		_loadType = chunk.readTypedByte();
 		break;
 
+	case kAssetHeaderStartup:
+		_isVisible = static_cast<bool>(chunk.readTypedByte());
+		break;
+
 	case kAssetHeaderSpriteChunkCount:
 		_frameCount = chunk.readTypedUint16();
 		break;
@@ -180,50 +184,37 @@ ScriptValue Sprite::callMethod(BuiltInMethod methodId, Common::Array<ScriptValue
 
 void Sprite::spatialShow() {
 	if (_isVisible) {
-		warning("Sprite::spatialShow(): (%d) Attempted to spatialShow when already showing", _id);
 		return;
 	}
 	showFrame(_frames[0]);
 
-	setActive();
 	_isVisible = true;
-	_isPlaying = false;
 }
 
 void Sprite::spatialHide() {
 	if (!_isVisible) {
-		warning("Sprite::spatialHide(): (%d) Attempted to spatialHide when not showing", _id);
 		return;
 	}
 	showFrame(nullptr);
 
-	setInactive();
 	_isVisible = false;
-	_isPlaying = false;
 }
 
 void Sprite::timePlay() {
-	if (!_isVisible) {
-		warning("Sprite::timePlay(): (%d) Attempted to timePlay when not showing", _id);
-		return;
-	} else if (_isPlaying) {
-		warning("Sprite::timePlay(): (%d) Attempted to timePlay when already playing", _id);
+	if (_isPlaying) {
 		return;
 	}
 
-	setActive();
 	_isPlaying = true;
+	_startTime = g_system->getMillis();
+	_lastProcessedTime = 0;
 	_nextFrameTime = 0;
 
 	runEventHandlerIfExists(kMovieBeginEvent);
 }
 
 void Sprite::timeStop() {
-	if (!_isVisible) {
-		warning("Sprite::timeStop(): (%d) Attempted to timeStop when not showing", _id);
-		return;
-	} else if (!_isPlaying) {
-		warning("Sprite::timeStop(): (%d) Attempted to timeStop when not playing", _id);
+	if (!_isPlaying) {
 		return;
 	}
 
@@ -232,11 +223,8 @@ void Sprite::timeStop() {
 }
 
 void Sprite::movieReset() {
-	setActive();
 	if (_isVisible) {
 		showFrame(_frames[0]);
-	} else {
-		showFrame(nullptr);
 	}
 	_isPlaying = false;
 	_startTime = 0;
@@ -279,10 +267,6 @@ void Sprite::updateFrameState() {
 		return;
 	}
 
-	if (!_isActive) {
-		return;
-	}
-
 	if (!_isPlaying) {
 		if (_activeFrame != nullptr) {
 			debugC(6, kDebugGraphics, "Sprite::updateFrameState(): (%d): Not playing. Persistent frame %d (%d x %d) @ (%d, %d)",
@@ -315,7 +299,6 @@ void Sprite::updateFrameState() {
 		_isPlaying = false;
 
 		// But otherwise, the sprite's params should be reset.
-		_isActive = true;
 		_startTime = 0;
 		_lastProcessedTime = 0;
 		_currentFrameIndex = 0;
diff --git a/engines/mediastation/assets/text.cpp b/engines/mediastation/assets/text.cpp
index 1006a517039..154c3b78205 100644
--- a/engines/mediastation/assets/text.cpp
+++ b/engines/mediastation/assets/text.cpp
@@ -90,14 +90,14 @@ ScriptValue Text::callMethod(BuiltInMethod methodId, Common::Array<ScriptValue>
 
 	case kSpatialShowMethod: {
 		assert(args.empty());
-		_isActive = true;
+		_isVisible = true;
 		warning("Text::callMethod(): spatialShow method not implemented yet");
 		return returnValue;
 	}
 
 	case kSpatialHideMethod: {
 		assert(args.empty());
-		_isActive = false;
+		_isVisible = false;
 		warning("Text::callMethod(): spatialHide method not implemented yet");
 		return returnValue;
 	}
diff --git a/engines/mediastation/assets/timer.cpp b/engines/mediastation/assets/timer.cpp
index 323cf409858..10a149a3cfd 100644
--- a/engines/mediastation/assets/timer.cpp
+++ b/engines/mediastation/assets/timer.cpp
@@ -44,7 +44,7 @@ ScriptValue Timer::callMethod(BuiltInMethod methodId, Common::Array<ScriptValue>
 
 	case kIsPlayingMethod: {
 		assert(args.size() == 0);
-		returnValue.setToBool(_isActive);
+		returnValue.setToBool(_isPlaying);
 		return returnValue;
 	}
 
@@ -54,15 +54,9 @@ ScriptValue Timer::callMethod(BuiltInMethod methodId, Common::Array<ScriptValue>
 }
 
 void Timer::timePlay() {
-	if (_isActive) {
-		warning("Timer::timePlay(): Attempted to play a timer that is already playing");
-		//return;
-	}
-
-	_isActive = true;
+	_isPlaying = true;
 	_startTime = g_system->getMillis();
 	_lastProcessedTime = 0;
-	g_engine->addPlayingAsset(this);
 
 	// Get the duration of the timer.
 	// TODO: Is there a better way to find out what the max time is? Do we have to look
@@ -82,18 +76,19 @@ void Timer::timePlay() {
 }
 
 void Timer::timeStop() {
-	if (!_isActive) {
-		warning("Timer::stop(): Attempted to stop a timer that is not playing");
+	if (!_isPlaying) {
 		return;
 	}
 
-	_isActive = false;
+	_isPlaying = false;
 	_startTime = 0;
 	_lastProcessedTime = 0;
 }
 
 void Timer::process() {
-	processTimeEventHandlers();
+	if (_isPlaying) {
+		processTimeEventHandlers();
+	}
 }
 
 } // End of namespace MediaStation
diff --git a/engines/mediastation/assets/timer.h b/engines/mediastation/assets/timer.h
index 2775b78001e..c1a3bc272d0 100644
--- a/engines/mediastation/assets/timer.h
+++ b/engines/mediastation/assets/timer.h
@@ -36,7 +36,8 @@ public:
 	virtual void process() override;
 
 private:
-	// Method implementations.
+	bool _isPlaying = false;
+
 	void timePlay();
 	void timeStop();
 };
diff --git a/engines/mediastation/context.cpp b/engines/mediastation/context.cpp
index 23a868975c3..09b4d1a5959 100644
--- a/engines/mediastation/context.cpp
+++ b/engines/mediastation/context.cpp
@@ -147,14 +147,6 @@ ScriptValue *Context::getVariable(uint variableId) {
 	return _variables.getValOrDefault(variableId);
 }
 
-void Context::registerActiveAssets() {
-	for (auto it = _assets.begin(); it != _assets.end(); ++it) {
-		if (it->_value->isActive()) {
-			g_engine->addPlayingAsset(it->_value);
-		}
-	}
-}
-
 void Context::readCreateContextData(Chunk &chunk) {
 	_fileNumber = chunk.readTypedUint16();
 
@@ -389,15 +381,13 @@ bool Context::readHeaderSection(Chunk &chunk) {
 
 	case kContextAssetHeaderSection: {
 		Asset *asset = readCreateAssetData(chunk);
-		if (g_engine->getAssetById(asset->id())) {
-			error("Context::readHeaderSection(): Asset with ID 0x%d was already defined in this title", asset->id());
-		}
-
 		_assets.setVal(asset->id(), asset);
+		g_engine->registerAsset(asset);
 		if (asset->_chunkReference != 0) {
 			debugC(5, kDebugLoading, "Context::readHeaderSection(): Storing asset with chunk ID \"%s\" (0x%x)", tag2str(asset->_chunkReference), asset->_chunkReference);
 			_assetsByChunkReference.setVal(asset->_chunkReference, asset);
 		}
+
 		if (asset->type() == kAssetTypeMovie) {
 			Movie *movie = static_cast<Movie *>(asset);
 			if (movie->_audioChunkReference != 0) {
diff --git a/engines/mediastation/context.h b/engines/mediastation/context.h
index 986d64a9f1e..846fcea05ef 100644
--- a/engines/mediastation/context.h
+++ b/engines/mediastation/context.h
@@ -70,7 +70,6 @@ public:
 	Asset *getAssetByChunkReference(uint chunkReference);
 	Function *getFunctionById(uint functionId);
 	ScriptValue *getVariable(uint variableId);
-	void registerActiveAssets();
 
 private:
 	// This is not an internal file ID, but the number of the file
diff --git a/engines/mediastation/mediastation.cpp b/engines/mediastation/mediastation.cpp
index 69f417f4527..161da5f2b39 100644
--- a/engines/mediastation/mediastation.cpp
+++ b/engines/mediastation/mediastation.cpp
@@ -40,7 +40,8 @@ MediaStationEngine *g_engine;
 
 MediaStationEngine::MediaStationEngine(OSystem *syst, const ADGameDescription *gameDesc) : Engine(syst),
 	_gameDescription(gameDesc),
-	_randomSource("MediaStation") {
+	_randomSource("MediaStation"),
+	_spatialEntities(MediaStationEngine::compareAssetByZIndex) {
 	g_engine = this;
 
 	_gameDataDir = Common::FSNode(ConfMan.getPath("path"));
@@ -68,9 +69,8 @@ MediaStationEngine::~MediaStationEngine() {
 }
 
 Asset *MediaStationEngine::getAssetById(uint assetId) {
-	for (auto it = _loadedContexts.begin(); it != _loadedContexts.end(); ++it) {
-		Asset *asset = it->_value->getAssetById(assetId);
-		if (asset != nullptr) {
+	for (auto asset : _assets) {
+		if (asset->id() == assetId) {
 			return asset;
 		}
 	}
@@ -174,11 +174,9 @@ Common::Error MediaStationEngine::run() {
 		}
 
 		debugC(5, kDebugGraphics, "***** START SCREEN UPDATE ***");
-		for (auto it = _assetsPlaying.begin(); it != _assetsPlaying.end();) {
-			(*it)->process();
+		for (Asset *asset : _assets) {
+			asset->process();
 
-			// If we're changing screens, exit out now so we don't try to access
-			// any assets that no longer exist.
 			if (_requestedScreenBranchId != 0) {
 				doBranchToScreen();
 				_requestedScreenBranchId = 0;
@@ -189,12 +187,6 @@ Common::Error MediaStationEngine::run() {
 				refreshActiveHotspot();
 				_needsHotspotRefresh = false;
 			}
-
-			if (!(*it)->isActive()) {
-				it = _assetsPlaying.erase(it);
-			} else {
-				++it;
-			}
 		}
 		redraw();
 		debugC(5, kDebugGraphics, "***** END SCREEN UPDATE ***");
@@ -302,20 +294,17 @@ void MediaStationEngine::redraw() {
 		return;
 	}
 
-	Common::sort(_assetsPlaying.begin(), _assetsPlaying.end(), [](Asset * a, Asset * b) {
-		if (!a->isSpatialActor() || !b->isSpatialActor()) {
-			return false;
-		}
-		return static_cast<SpatialEntity *>(a)->zIndex() > static_cast<SpatialEntity *>(b)->zIndex();
-	});
-
 	for (Common::Rect dirtyRect : _dirtyRects) {
-		for (Asset *asset : _assetsPlaying) {
+		for (Asset *asset : _spatialEntities) {
 			if (!asset->isSpatialActor()) {
 				continue;
 			}
 
 			SpatialEntity *entity = static_cast<SpatialEntity *>(asset);
+			if (!entity->isVisible()) {
+				continue;
+			}
+
 			Common::Rect bbox = entity->getBbox();
 			if (!bbox.isEmpty()) {
 				if (dirtyRect.intersects(bbox)) {
@@ -374,7 +363,6 @@ Context *MediaStationEngine::loadContext(uint32 contextId) {
 		_screen->setPalette(*context->_palette);
 	}
 
-	context->registerActiveAssets();
 	_loadedContexts.setVal(contextId, context);
 	return context;
 }
@@ -390,15 +378,15 @@ void MediaStationEngine::setPalette(Asset *asset) {
 	}
 }
 
-void MediaStationEngine::addPlayingAsset(Asset *assetToAdd) {
-	// If we're already marking the asset as played, we don't need to mark it
-	// played again.
-	for (Asset *asset : g_engine->_assetsPlaying) {
-		if (asset == assetToAdd) {
-			return;
-		}
+void MediaStationEngine::registerAsset(Asset *assetToAdd) {
+	if (getAssetById(assetToAdd->id())) {
+		error("Asset with ID 0x%d was already defined in this title", assetToAdd->id());
+	}
+
+	_assets.push_back(assetToAdd);
+	if (assetToAdd->isSpatialActor()) {
+		_spatialEntities.insert(static_cast<SpatialEntity *>(assetToAdd));
 	}
-	g_engine->_assetsPlaying.push_back(assetToAdd);
 }
 
 ScriptValue MediaStationEngine::callMethod(BuiltInMethod methodId, Common::Array<ScriptValue> &args) {
@@ -464,13 +452,19 @@ void MediaStationEngine::releaseContext(uint32 contextId) {
 		}
 	}
 
-	// Unload any assets currently playing from this context. They should have
-	// already been stopped by scripts, but this is a last check.
-	for (auto it = _assetsPlaying.begin(); it != _assetsPlaying.end();) {
-		uint assetId = (*it)->id();
-		Asset *asset = context->getAssetById(assetId);
-		if (asset != nullptr) {
-			it = _assetsPlaying.erase(it);
+	for (auto it = _assets.begin(); it != _assets.end();) {
+		uint assetContextId = (*it)->contextId();
+		if (assetContextId == contextId) {
+			it = _assets.erase(it);
+		} else {
+			++it;
+		}
+	}
+
+	for (auto it = _spatialEntities.begin(); it != _spatialEntities.end();) {
+		uint assetContextId = (*it)->contextId();
+		if (assetContextId == contextId) {
+			it = _spatialEntities.erase(it);
 		} else {
 			++it;
 		}
@@ -486,7 +480,7 @@ Asset *MediaStationEngine::findAssetToAcceptMouseEvents() {
 	// actually the lowest asset.
 	int lowestZIndex = INT_MAX;
 
-	for (Asset *asset : _assetsPlaying) {
+	for (Asset *asset : _assets) {
 		if (asset->type() == kAssetTypeHotspot) {
 			Hotspot *hotspot = static_cast<Hotspot *>(asset);
 			debugC(5, kDebugGraphics, "findAssetToAcceptMouseEvents(): Hotspot %d (z-index %d)", hotspot->id(), hotspot->zIndex());
@@ -530,4 +524,16 @@ ScriptValue MediaStationEngine::callBuiltInFunction(BuiltInFunction function, Co
 	}
 }
 
+int MediaStationEngine::compareAssetByZIndex(const SpatialEntity *a, const SpatialEntity *b) {
+	int diff = b->zIndex() - a->zIndex();
+	if (diff < 0)
+		return -1; // a should come before b
+	else if (diff > 0)
+		return 1; // b should come before a
+	else {
+		// If z-indices are equal, compare pointers for stable sort
+		return (a < b) ? -1 : 1;
+	}
+}
+
 } // End of namespace MediaStation
diff --git a/engines/mediastation/mediastation.h b/engines/mediastation/mediastation.h
index 10e3f1a0f9f..85e83ba43cc 100644
--- a/engines/mediastation/mediastation.h
+++ b/engines/mediastation/mediastation.h
@@ -81,7 +81,7 @@ public:
 	void redraw();
 
 	void setPalette(Asset *palette);
-	void addPlayingAsset(Asset *assetToAdd);
+	void registerAsset(Asset *assetToAdd);
 
 	Asset *getAssetById(uint assetId);
 	Asset *getAssetByChunkReference(uint chunkReference);
@@ -117,9 +117,10 @@ private:
 	void setCursor(uint id);
 
 	Boot *_boot = nullptr;
-	Common::List<Asset *> _assetsPlaying;
+	Common::Array<Asset *> _assets;
+	Common::SortedArray<SpatialEntity *, const SpatialEntity *> _spatialEntities;
 	Common::HashMap<uint, Context *> _loadedContexts;
-	Hotspot *_currentHotspot = nullptr;
+	Asset *_currentHotspot = nullptr;
 
 	uint _requestedScreenBranchId = 0;
 	Common::Array<uint> _requestedContextReleaseId;
@@ -130,6 +131,8 @@ private:
 	Asset *findAssetToAcceptMouseEvents();
 
 	void effectTransition(Common::Array<ScriptValue> &args);
+
+	static int compareAssetByZIndex(const SpatialEntity *a, const SpatialEntity *b);
 };
 
 extern MediaStationEngine *g_engine;


Commit: cb66b27c9fd7cf88a281b7c214a85477b40f788d
    https://github.com/scummvm/scummvm/commit/cb66b27c9fd7cf88a281b7c214a85477b40f788d
Author: Nathanael Gentry (nathanael.gentrydb8 at gmail.com)
Date: 2025-06-14T11:07:14-04:00

Commit Message:
MEDIASTATION: Improve Sprite rendering accuracy

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


diff --git a/engines/mediastation/asset.h b/engines/mediastation/asset.h
index 68138c74e4a..d322aa2519e 100644
--- a/engines/mediastation/asset.h
+++ b/engines/mediastation/asset.h
@@ -114,7 +114,8 @@ enum AssetHeaderSectionType {
 	kAssetHeaderTextCharacterClass = 0x0266,
 
 	// SPRITE FIELDS.
-	kAssetHeaderSpriteFrameMapping = 0x03e9
+	kAssetHeaderSpriteClip = 0x03e9,
+	kAssetHeaderCurrentSpriteClip = 0x03ea
 };
 
 class Asset {
diff --git a/engines/mediastation/assets/sprite.cpp b/engines/mediastation/assets/sprite.cpp
index c9b3721d00b..45cf88de556 100644
--- a/engines/mediastation/assets/sprite.cpp
+++ b/engines/mediastation/assets/sprite.cpp
@@ -60,7 +60,6 @@ uint32 SpriteFrame::index() {
 	return _bitmapHeader->_index;
 }
 
-
 Sprite::~Sprite() {
 	// If we're just referencing another asset's frames,
 	// don't delete those frames.
@@ -94,18 +93,31 @@ void Sprite::readParameter(Chunk &chunk, AssetHeaderSectionType paramType) {
 		_isVisible = static_cast<bool>(chunk.readTypedByte());
 		break;
 
-	case kAssetHeaderSpriteChunkCount:
+	case kAssetHeaderSpriteChunkCount: {
 		_frameCount = chunk.readTypedUint16();
+
+		// Set the default clip.
+		SpriteClip clip;
+		clip.id = DEFAULT_CLIP_ID;
+		clip.firstFrameIndex = 0;
+		clip.lastFrameIndex = _frameCount - 1;
+		_clips.setVal(clip.id, clip);
+		setCurrentClip(clip.id);
 		break;
+	}
 
-	case kAssetHeaderSpriteFrameMapping: {
-		uint32 externalFrameId = chunk.readTypedUint16();
-		uint32 internalFrameId = chunk.readTypedUint16();
-		uint32 unk1 = chunk.readTypedUint16();
-		if (unk1 != internalFrameId) {
-			warning("AssetHeader::readSection(): Repeated internalFrameId doesn't match");
-		}
-		_spriteFrameMapping.setVal(externalFrameId, internalFrameId);
+	case kAssetHeaderSpriteClip: {
+		SpriteClip spriteClip;
+		spriteClip.id = chunk.readTypedUint16();
+		spriteClip.firstFrameIndex = chunk.readTypedUint16();
+		spriteClip.lastFrameIndex = chunk.readTypedUint16();
+		_clips.setVal(spriteClip.id, spriteClip);
+		break;
+	}
+
+	case kAssetHeaderCurrentSpriteClip: {
+		uint clipId = chunk.readTypedUint16();
+		setCurrentClip(clipId);
 		break;
 	}
 
@@ -120,13 +132,13 @@ ScriptValue Sprite::callMethod(BuiltInMethod methodId, Common::Array<ScriptValue
 	switch (methodId) {
 	case kSpatialShowMethod: {
 		assert(args.empty());
-		spatialShow();
+		setVisibility(true);
 		return returnValue;
 	}
 
 	case kSpatialHideMethod: {
 		assert(args.empty());
-		spatialHide();
+		setVisibility(false);
 		return returnValue;
 	}
 
@@ -139,36 +151,65 @@ ScriptValue Sprite::callMethod(BuiltInMethod methodId, Common::Array<ScriptValue
 
 	case kTimePlayMethod: {
 		assert(args.empty());
-		timePlay();
+		play();
 		return returnValue;
 	}
 
 	case kTimeStopMethod: {
 		assert(args.empty());
-		timeStop();
+		stop();
 		return returnValue;
 	}
 
 	case kMovieResetMethod: {
 		assert(args.empty());
-		movieReset();
+		setCurrentFrameToInitial();
 		return returnValue;
 	}
 
 	case kSetCurrentClipMethod: {
 		assert(args.size() <= 1);
-		if (args.size() == 1 && args[0].asParamToken() != 0) {
-			error("Sprite::callMethod(): (%d) setClip() called with unhandled arg: %d", _id, args[0].asParamToken());
+		uint clipId = DEFAULT_CLIP_ID;
+		if (args.size() == 1) {
+			clipId = args[0].asParamToken();
 		}
-		setCurrentClip();
+		setCurrentClip(clipId);
 		return returnValue;
 	}
 
-	case kSetSpriteFrameByIdMethod: {
-		assert(args.size() == 1);
-		uint32 externalFrameId = args[0].asParamToken();
-		uint32 internalFrameId = _spriteFrameMapping.getVal(externalFrameId);
-		showFrame(_frames[internalFrameId]);
+	case kIncrementFrameMethod: {
+		assert(args.size() <= 1);
+		bool loopAround = false;
+		if (args.size() == 1) {
+			loopAround = args[0].asBool();
+		}
+
+		bool moreFrames = activateNextFrame();
+		if (!moreFrames) {
+			if (loopAround) {
+				setCurrentFrameToInitial();
+			}
+		}
+		return returnValue;
+	}
+
+	case kDecrementFrameMethod: {
+		bool shouldSetCurrentFrameToFinal = false;
+		if (args.size() == 1) {
+			shouldSetCurrentFrameToFinal = args[0].asBool();
+		}
+
+		bool moreFrames = activatePreviousFrame();
+		if (!moreFrames) {
+			if (shouldSetCurrentFrameToFinal) {
+				setCurrentFrameToFinal();
+			}
+		}
+		return returnValue;
+	}
+
+	case kGetCurrentClipIdMethod: {
+		returnValue.setToParamToken(_activeClip.id);
 		return returnValue;
 	}
 
@@ -182,62 +223,75 @@ ScriptValue Sprite::callMethod(BuiltInMethod methodId, Common::Array<ScriptValue
 	}
 }
 
-void Sprite::spatialShow() {
-	if (_isVisible) {
-		return;
+bool Sprite::activateNextFrame() {
+	if (_currentFrameIndex < _activeClip.lastFrameIndex) {
+		_currentFrameIndex++;
+		dirtyIfVisible();
+		return true;
 	}
-	showFrame(_frames[0]);
-
-	_isVisible = true;
+	return false;
 }
 
-void Sprite::spatialHide() {
-	if (!_isVisible) {
-		return;
+bool Sprite::activatePreviousFrame() {
+	if (_currentFrameIndex > _activeClip.firstFrameIndex) {
+		_currentFrameIndex--;
+		dirtyIfVisible();
+		return true;
 	}
-	showFrame(nullptr);
+	return false;
+}
 
-	_isVisible = false;
+void Sprite::dirtyIfVisible() {
+	if (_isVisible) {
+		invalidateLocalBounds();
+	}
 }
 
-void Sprite::timePlay() {
-	if (_isPlaying) {
-		return;
+void Sprite::setVisibility(bool visibility) {
+	if (_isVisible != visibility) {
+		_isVisible = visibility;
+		invalidateLocalBounds();
 	}
+}
 
+void Sprite::play() {
 	_isPlaying = true;
 	_startTime = g_system->getMillis();
 	_lastProcessedTime = 0;
 	_nextFrameTime = 0;
 
-	runEventHandlerIfExists(kMovieBeginEvent);
+	scheduleNextFrame();
 }
 
-void Sprite::timeStop() {
-	if (!_isPlaying) {
-		return;
+void Sprite::stop() {
+	_nextFrameTime = 0;
+	_isPlaying = false;
+}
+
+void Sprite::setCurrentClip(uint clipId) {
+	if (_activeClip.id != clipId) {
+		if (_clips.contains(clipId)) {
+			_activeClip = _clips.getVal(clipId);
+		} else {
+			_activeClip.id = clipId;
+			warning("Sprite clip %d not found in sprite %d", clipId, _id);
+		}
 	}
 
-	_isPlaying = false;
-	// TODO: Find the right event handler to run here.
+	setCurrentFrameToInitial();
 }
 
-void Sprite::movieReset() {
-	if (_isVisible) {
-		showFrame(_frames[0]);
+void Sprite::setCurrentFrameToInitial() {
+	if (_currentFrameIndex != _activeClip.firstFrameIndex) {
+		_currentFrameIndex = _activeClip.firstFrameIndex;
+		dirtyIfVisible();
 	}
-	_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", _id);
+void Sprite::setCurrentFrameToFinal() {
+	if (_currentFrameIndex != _activeClip.lastFrameIndex) {
+		_currentFrameIndex = _activeClip.lastFrameIndex;
+		dirtyIfVisible();
 	}
 }
 
@@ -260,91 +314,80 @@ void Sprite::readChunk(Chunk &chunk) {
 	});
 }
 
-void Sprite::updateFrameState() {
-	if (_isVisible && _atFirstFrame) {
-		showFrame(_frames[0]);
-		_atFirstFrame = false;
+void Sprite::scheduleNextFrame() {
+	if (!_isPlaying) {
 		return;
 	}
 
+	if (_currentFrameIndex < _activeClip.lastFrameIndex) {
+		scheduleNextTimerEvent();
+	} else {
+		stop();
+	}
+}
+
+void Sprite::scheduleNextTimerEvent() {
+	uint frameDuration = 1000 / _frameRate;
+	_nextFrameTime += frameDuration;
+}
+
+void Sprite::updateFrameState() {
 	if (!_isPlaying) {
-		if (_activeFrame != nullptr) {
-			debugC(6, kDebugGraphics, "Sprite::updateFrameState(): (%d): Not playing. Persistent frame %d (%d x %d) @ (%d, %d)",
-				_id, _activeFrame->index(), _activeFrame->width(), _activeFrame->height(), _activeFrame->left(), _activeFrame->top());
-		} else {
-			debugC(6, kDebugGraphics, "Sprite::updateFrameState(): (%d): Not playing, no persistent frame", _id);
-		}
 		return;
 	}
 
-	debugC(5, kDebugGraphics, "Sprite::updateFrameState(): (%d) Frame %d (%d x %d) @ (%d, %d)",
-		_id, _activeFrame->index(), _activeFrame->width(), _activeFrame->height(), _activeFrame->left(), _activeFrame->top());
-
 	uint currentTime = g_system->getMillis() - _startTime;
 	bool drawNextFrame = currentTime >= _nextFrameTime;
-	if (!drawNextFrame) {
+	debugC(kDebugGraphics, "nextFrameTime: %d; startTime: %d, currentTime: %d", _nextFrameTime, _startTime, currentTime);
+	if (drawNextFrame) {
+		timerEvent();
+	}
+}
+
+void Sprite::timerEvent() {
+	if (!_isPlaying) {
+		error("Attempt to activate sprite frame when sprite is not playing");
 		return;
 	}
 
-	showFrame(_frames[_currentFrameIndex]);
+	bool result = activateNextFrame();
+	if (!result) {
+		stop();
+	} else {
+		postMovieEndEventIfNecessary();
+		scheduleNextFrame();
+	}
+}
 
-	uint frameDuration = 1000 / _frameRate;
-	_nextFrameTime = ++_currentFrameIndex * frameDuration;
-
-	bool spriteFinishedPlaying = (_currentFrameIndex == _frames.size());
-	if (spriteFinishedPlaying) {
-		// Sprites always keep their last frame showing until they are hidden
-		// with spatialHide.
-		showFrame(_frames[_currentFrameIndex - 1]);
-		_isPlaying = false;
-
-		// But otherwise, the sprite's params should be reset.
-		_startTime = 0;
-		_lastProcessedTime = 0;
-		_currentFrameIndex = 0;
-		_nextFrameTime = 0;
-
-		ScriptValue defaultSpriteClip;
-		const uint DEFAULT_SPRITE_CLIP_ID = 1200;
-		defaultSpriteClip.setToParamToken(DEFAULT_SPRITE_CLIP_ID);
-		runEventHandlerIfExists(kSpriteMovieEndEvent, defaultSpriteClip);
+void Sprite::postMovieEndEventIfNecessary() {
+	if (_currentFrameIndex != _activeClip.lastFrameIndex) {
+		return;
 	}
+
+	_isPlaying = false;
+	_startTime = 0;
+	_nextFrameTime = 0;
+
+	ScriptValue value;
+	value.setToParamToken(_activeClip.id);
+	runEventHandlerIfExists(kSpriteMovieEndEvent, value);
 }
 
 void Sprite::redraw(Common::Rect &rect) {
-	if (_activeFrame == nullptr || !_isVisible) {
+	SpriteFrame *activeFrame = _frames[_currentFrameIndex];
+	if (activeFrame == nullptr || !_isVisible) {
 		return;
 	}
 
-	Common::Rect bbox = getActiveFrameBoundingBox();
+	Common::Rect bbox = activeFrame->boundingBox();
+	bbox.translate(_boundingBox.left, _boundingBox.top);
 	Common::Rect areaToRedraw = bbox.findIntersectingRect(rect);
 	if (!areaToRedraw.isEmpty()) {
 		Common::Point originOnScreen(areaToRedraw.left, areaToRedraw.top);
-		areaToRedraw.translate(-_activeFrame->left() - _boundingBox.left, -_activeFrame->top() - _boundingBox.top);
-		areaToRedraw.clip(Common::Rect(0, 0, _activeFrame->width(), _activeFrame->height()));
-		g_engine->_screen->simpleBlitFrom(_activeFrame->_surface, areaToRedraw, originOnScreen);
-	}
-}
-
-void Sprite::showFrame(SpriteFrame *frame) {
-	// Erase the previous frame.
-	if (_activeFrame != nullptr) {
-		g_engine->_dirtyRects.push_back(getActiveFrameBoundingBox());
+		areaToRedraw.translate(-activeFrame->left() - _boundingBox.left, -activeFrame->top() - _boundingBox.top);
+		areaToRedraw.clip(Common::Rect(0, 0, activeFrame->width(), activeFrame->height()));
+		g_engine->_screen->simpleBlitFrom(activeFrame->_surface, areaToRedraw, originOnScreen);
 	}
-
-	// Show the next frame.
-	_activeFrame = frame;
-	if (frame != nullptr) {
-		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(_boundingBox.left, _boundingBox.top);
-	return bbox;
 }
 
 } // End of namespace MediaStation
diff --git a/engines/mediastation/assets/sprite.h b/engines/mediastation/assets/sprite.h
index ebbebb98604..15d55001c8c 100644
--- a/engines/mediastation/assets/sprite.h
+++ b/engines/mediastation/assets/sprite.h
@@ -33,6 +33,12 @@
 
 namespace MediaStation {
 
+struct SpriteClip {
+	uint id = 0;
+	uint firstFrameIndex = 0;
+	uint lastFrameIndex = 0;
+};
+
 class SpriteFrameHeader : public BitmapHeader {
 public:
 	SpriteFrameHeader(Chunk &chunk);
@@ -76,29 +82,36 @@ public:
 	virtual void readChunk(Chunk &chunk) override;
 
 private:
+	static const uint DEFAULT_CLIP_ID = 1200;
 	double _dissolveFactor = 0.0;
 	uint _loadType = 0;
 	uint _frameRate = 0;
 	uint _frameCount = 0;
-	Common::HashMap<uint32, uint32> _spriteFrameMapping;
+	Common::HashMap<uint, SpriteClip> _clips;
 	Common::Array<SpriteFrame *> _frames;
-	SpriteFrame *_activeFrame = nullptr;
 	bool _isPlaying = false;
-	bool _atFirstFrame = true;
 	uint _currentFrameIndex = 0;
 	uint _nextFrameTime = 0;
+	SpriteClip _activeClip;
+
+	void play();
+	void stop();
+	void setCurrentClip(uint clipId);
+
+	bool activateNextFrame();
+	bool activatePreviousFrame();
+
+	void dirtyIfVisible();
+	void setCurrentFrameToInitial();
+	void setCurrentFrameToFinal();
 
-	// Method implementations.
-	void spatialShow();
-	void spatialHide();
-	void timePlay();
-	void timeStop();
-	void movieReset();
-	void setCurrentClip();
+	void scheduleNextFrame();
+	void scheduleNextTimerEvent();
+	void postMovieEndEventIfNecessary();
+	void setVisibility(bool visibility);
 
 	void updateFrameState();
-	void showFrame(SpriteFrame *frame);
-	Common::Rect getActiveFrameBoundingBox();
+	void timerEvent();
 };
 
 } // End of namespace MediaStation
diff --git a/engines/mediastation/context.cpp b/engines/mediastation/context.cpp
index 09b4d1a5959..2363b41c5f2 100644
--- a/engines/mediastation/context.cpp
+++ b/engines/mediastation/context.cpp
@@ -99,6 +99,7 @@ Context::Context(const Common::Path &path) : Datafile(path) {
 					error("Context::Context(): Asset %d references non-existent asset %d", asset->id(), referencedAssetId);
 				}
 				sprite->_frames = referencedSprite->_frames;
+				sprite->_clips = referencedSprite->_clips;
 				break;
 			}
 
diff --git a/engines/mediastation/mediascript/scriptconstants.cpp b/engines/mediastation/mediascript/scriptconstants.cpp
index 966e1e33e49..d8946caf7f1 100644
--- a/engines/mediastation/mediascript/scriptconstants.cpp
+++ b/engines/mediastation/mediascript/scriptconstants.cpp
@@ -171,10 +171,14 @@ const char *builtInMethodToStr(BuiltInMethod method) {
 		return "IsVisible";
 	case kMovieResetMethod:
 		return "MovieReset";
-	case kSetSpriteFrameByIdMethod:
-		return "SetSpriteFrameById";
 	case kSetCurrentClipMethod:
 		return "SetCurrentClip";
+	case kIncrementFrameMethod:
+		return "IncrementFrame";
+	case kDecrementFrameMethod:
+		return "DecrementFrame";
+	case kGetCurrentClipIdMethod:
+		return "GetCurrentClipId";
 	case kSetWorldSpaceExtentMethod:
 		return "SetWorldSpaceExtent";
 	case kSetBoundsMethod:
diff --git a/engines/mediastation/mediascript/scriptconstants.h b/engines/mediastation/mediascript/scriptconstants.h
index ae2f70a3f9c..9658dc49af1 100644
--- a/engines/mediastation/mediascript/scriptconstants.h
+++ b/engines/mediastation/mediascript/scriptconstants.h
@@ -122,8 +122,10 @@ enum BuiltInMethod {
 
 	// SPRITE METHODS.
 	kMovieResetMethod = 219, // PARAMS: 0
-	kSetSpriteFrameByIdMethod = 220, // PARAMS: 1
-	kSetCurrentClipMethod = 221, // PARAMS: 0-1
+	kSetCurrentClipMethod = 220, // PARAMS: 1
+	kIncrementFrameMethod = 221, // PARAMS: 0-1
+	kDecrementFrameMethod = 222, // PARAMS: 0-1
+	kGetCurrentClipIdMethod = 240, // PARAMS: 0
 
 	// STAGE METHODS.
 	kSetWorldSpaceExtentMethod = 363, // PARAMS: 2


Commit: 65578a45109d50620b812411b5d4a232224d4d0b
    https://github.com/scummvm/scummvm/commit/65578a45109d50620b812411b5d4a232224d4d0b
Author: Nathanael Gentry (nathanael.gentrydb8 at gmail.com)
Date: 2025-06-14T11:07:14-04:00

Commit Message:
MEDIASTATION: Improve Movie rendering accuracy

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


diff --git a/engines/mediastation/assets/movie.cpp b/engines/mediastation/assets/movie.cpp
index bdc6ff29a2d..d71928ba0db 100644
--- a/engines/mediastation/assets/movie.cpp
+++ b/engines/mediastation/assets/movie.cpp
@@ -31,124 +31,48 @@ MovieFrameHeader::MovieFrameHeader(Chunk &chunk) : BitmapHeader(chunk) {
 	_keyframeEndInMilliseconds = chunk.readTypedUint32();
 }
 
-MovieFrameFooter::MovieFrameFooter(Chunk &chunk) {
-	_unk1 = chunk.readTypedUint16();
-	_unk2 = chunk.readTypedUint32();
+MovieFrame::MovieFrame(Chunk &chunk) {
+	layerId = chunk.readTypedUint32();
 	if (g_engine->isFirstGenerationEngine()) {
-		_startInMilliseconds = chunk.readTypedUint32();
-		_endInMilliseconds = chunk.readTypedUint32();
-		_left = chunk.readTypedUint16();
-		_top = chunk.readTypedUint16();
-		_unk3 = chunk.readTypedUint16();
-		_unk4 = chunk.readTypedUint16();
-		_index = chunk.readTypedUint16();
+		startInMilliseconds = chunk.readTypedUint32();
+		endInMilliseconds = chunk.readTypedUint32();
+		// These are unsigned in the data files but ScummVM expects signed.
+		leftTop.x = static_cast<int16>(chunk.readTypedUint16());
+		leftTop.y = static_cast<int16>(chunk.readTypedUint16());
+		unk3 = chunk.readTypedUint16();
+		unk4 = chunk.readTypedUint16();
+		index = chunk.readTypedUint16();
 	} else {
-		_unk4 = chunk.readTypedUint16();
-		_startInMilliseconds = chunk.readTypedUint32();
-		_endInMilliseconds = chunk.readTypedUint32();
-		_left = chunk.readTypedUint16();
-		_top = chunk.readTypedUint16();
-		_zIndex = chunk.readTypedSint16();
-		// This represents the difference between the left coordinate of the
+		blitType = chunk.readTypedUint16();
+		startInMilliseconds = chunk.readTypedUint32();
+		endInMilliseconds = chunk.readTypedUint32();
+		// These are unsigned in the data files but ScummVM expects signed.
+		leftTop.x = static_cast<int16>(chunk.readTypedUint16());
+		leftTop.y = static_cast<int16>(chunk.readTypedUint16());
+		zIndex = chunk.readTypedSint16();
+		// This represents the difference between the left-top coordinate of the
 		// keyframe (if applicable) and the left coordinate of this frame. Zero
 		// if there is no keyframe.
-		_diffBetweenKeyframeAndFrameX = chunk.readTypedSint16();
-		// This represents the difference between the top coordinate of the
-		// keyframe (if applicable) and the top coordinate of this frame. Zero
-		// if there is no keyframe.
-		_diffBetweenKeyframeAndFrameY = chunk.readTypedSint16();
-		_index = chunk.readTypedUint32();
-		_keyframeIndex = chunk.readTypedUint32();
-		_unk9 = chunk.readTypedByte();
-		debugC(5, kDebugLoading, "MovieFrameFooter::MovieFrameFooter(): _startInMilliseconds = %d, _endInMilliseconds = %d, _left = %d, _top = %d, _index = %d, _keyframeIndex = %d (@0x%llx)",
-			_startInMilliseconds, _endInMilliseconds, _left, _top, _index, _keyframeIndex, static_cast<long long int>(chunk.pos()));
-		debugC(5, kDebugLoading, "MovieFrameFooter::MovieFrameFooter(): _zIndex = %d, _diffBetweenKeyframeAndFrameX = %d, _diffBetweenKeyframeAndFrameY = %d, _unk4 = %d, _unk9 = %d",
-			_zIndex, _diffBetweenKeyframeAndFrameX, _diffBetweenKeyframeAndFrameY, _unk4,_unk9);
+		diffBetweenKeyframeAndFrame.x = chunk.readTypedSint16();
+		diffBetweenKeyframeAndFrame.y = chunk.readTypedSint16();
+		index = chunk.readTypedUint32();
+		keyframeIndex = chunk.readTypedUint32();
+		keepAfterEnd = chunk.readTypedByte();
+		debugC(5, kDebugLoading, "MovieFrame::MovieFrame(): _blitType = %d, _startInMilliseconds = %d, \
+			_endInMilliseconds = %d, _left = %d, _top = %d, _zIndex = %d, _diffBetweenKeyframeAndFrameX = %d, \
+			_diffBetweenKeyframeAndFrameY = %d, _index = %d, _keyframeIndex = %d, _keepAfterEnd = %d (@0x%llx)",
+			blitType, startInMilliseconds, endInMilliseconds, leftTop.x, leftTop.y, zIndex, diffBetweenKeyframeAndFrame.x, \
+			diffBetweenKeyframeAndFrame.y, index, keyframeIndex, keepAfterEnd, static_cast<long long int>(chunk.pos()));
 	}
 }
 
-MovieFrame::MovieFrame(Chunk &chunk, MovieFrameHeader *header) :
-	Bitmap(chunk, header),
-	_footer(nullptr) {
+MovieFrameImage::MovieFrameImage(Chunk &chunk, MovieFrameHeader *header) : Bitmap(chunk, header) {
 	_bitmapHeader = header;
 }
 
-void MovieFrame::setFooter(MovieFrameFooter *footer) {
-	if (footer != nullptr) {
-		assert(footer->_index == _bitmapHeader->_index);
-	}
-	_footer = footer;
-}
-
-uint32 MovieFrame::left() {
-	if (_footer != nullptr) {
-		return _footer->_left;
-	} else {
-		error("MovieFrame::left(): Cannot get the left coordinate of a keyframe");
-	}
-}
-
-uint32 MovieFrame::top() {
-	if (_footer != nullptr) {
-		return _footer->_top;
-	} else {
-		error("MovieFrame::left(): Cannot get the top coordinate of a keyframe");
-	}
-}
-
-Common::Point MovieFrame::topLeft() {
-	if (_footer != nullptr) {
-		return Common::Point(_footer->_left, _footer->_top);
-	} else {
-		error("MovieFrame::topLeft(): Cannot get the top-left coordinate of a keyframe");
-	}
-}
-
-Common::Rect MovieFrame::boundingBox() {
-	if (_footer != nullptr) {
-		return Common::Rect(Common::Point(_footer->_left, _footer->_top), width(), height());
-	} else {
-		error("MovieFrame::boundingBox(): Cannot get the bounding box of a keyframe");
-	}
-}
-
-uint32 MovieFrame::index() {
-	return _bitmapHeader->_index;
-}
-
-uint32 MovieFrame::startInMilliseconds() {
-	if (_footer != nullptr) {
-		return _footer->_startInMilliseconds;
-	} else {
-		error("MovieFrame::startInMilliseconds(): Cannot get the start time of a keyframe");
-	}
-}
-
-uint32 MovieFrame::endInMilliseconds() {
-	if (_footer != nullptr) {
-		return _footer->_endInMilliseconds;
-	} else {
-		error("MovieFrame::endInMilliseconds(): Cannot get the end time of a keyframe");
-	}
-}
-
-uint32 MovieFrame::zCoordinate() {
-	if (_footer != nullptr) {
-		return _footer->_zIndex;
-	} else {
-		error("MovieFrame::zCoordinate(): Cannot get the z-coordinate of a keyframe");
-	}
-}
-
-uint32 MovieFrame::keyframeEndInMilliseconds() {
-	return _bitmapHeader->_keyframeEndInMilliseconds;
-}
-
-MovieFrame::~MovieFrame() {
+MovieFrameImage::~MovieFrameImage() {
 	// The base class destructor takes care of deleting the bitmap header, so
 	// we don't need to delete that here.
-	// The movie will delete the footer.
-	_footer = nullptr;
 }
 
 Movie::~Movie() {
@@ -157,15 +81,10 @@ Movie::~Movie() {
 	}
 	_frames.clear();
 
-	for (MovieFrame *still : _stills) {
-		delete still;
-	}
-	_stills.clear();
-
-	for (MovieFrameFooter *footer : _footers) {
-		delete footer;
+	for (MovieFrameImage *image : _images) {
+		delete image;
 	}
-	_footers.clear();
+	_images.clear();
 }
 
 void Movie::readParameter(Chunk &chunk, AssetHeaderSectionType paramType) {
@@ -234,7 +153,8 @@ ScriptValue Movie::callMethod(BuiltInMethod methodId, Common::Array<ScriptValue>
 
 	case kSpatialShowMethod: {
 		assert(args.empty());
-		spatialShow();
+		setVisibility(true);
+		updateFrameState();
 		return returnValue;
 	}
 
@@ -246,7 +166,7 @@ ScriptValue Movie::callMethod(BuiltInMethod methodId, Common::Array<ScriptValue>
 
 	case kSpatialHideMethod: {
 		assert(args.empty());
-		spatialHide();
+		setVisibility(false);
 		return returnValue;
 	}
 
@@ -261,7 +181,6 @@ ScriptValue Movie::callMethod(BuiltInMethod methodId, Common::Array<ScriptValue>
 		double left = static_cast<double>(_boundingBox.left);
 		returnValue.setToFloat(left);
 		return returnValue;
-
 	}
 
 	case kGetTopYMethod: {
@@ -283,47 +202,6 @@ ScriptValue Movie::callMethod(BuiltInMethod methodId, Common::Array<ScriptValue>
 	}
 }
 
-void Movie::spatialShow() {
-	if (_isPlaying) {
-		warning("Movie::spatialShow(): (%d) Attempted to spatialShow movie that is already playing", _id);
-		return;
-	} else if (_stills.empty()) {
-		warning("Movie::spatialShow(): (%d) No still frame to show", _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));
-	}
-
-	_isVisible = true;
-	_isPlaying = false;
-}
-
-void Movie::spatialHide() {
-	if (_isPlaying) {
-		warning("Movie::spatialShow(): (%d) Attempted to spatialHide movie that is playing", _id);
-		return;
-	} else if (!_isVisible) {
-		warning("Movie::spatialHide(): (%d) Attempted to spatialHide movie that is not showing", _id);
-		return;
-	}
-
-	for (MovieFrame *frame : _framesOnScreen) {
-		g_engine->_dirtyRects.push_back(getFrameBoundingBox(frame));
-	}
-	_framesOnScreen.clear();
-	_framesNotYetShown.clear();
-
-	_isVisible = false;
-	_isPlaying = false;
-}
-
 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.
@@ -331,10 +209,9 @@ void Movie::timePlay() {
 		return;
 	}
 
-	// TODO: This won't work when we have some chunks that don't have audio.
 	_audioSequence.play();
 	_framesNotYetShown = _frames;
-	_isVisible = true;
+	_framesOnScreen.clear();
 	_isPlaying = true;
 	_startTime = g_system->getMillis();
 	_lastProcessedTime = 0;
@@ -343,57 +220,47 @@ void Movie::timePlay() {
 }
 
 void Movie::timeStop() {
-	if (!_isVisible) {
-		warning("Movie::timeStop(): (%d) Attempted to stop a movie that isn't showing", _id);
-		return;
-	} else if (!_isPlaying) {
-		warning("Movie::timePlay(): (%d) Attempted to stop a movie that isn't playing", _id);
+	if (!_isPlaying) {
 		return;
 	}
 
 	for (MovieFrame *frame : _framesOnScreen) {
-		g_engine->_dirtyRects.push_back(getFrameBoundingBox(frame));
+		invalidateRect(getFrameBoundingBox(frame));
 	}
-	_framesOnScreen.clear();
-	_framesNotYetShown.clear();
 	_audioSequence.stop();
-
-	// Show the persistent frames.
-	_isPlaying = false;
-	if (!_stills.empty()) {
-		for (MovieFrame *still : _stills) {
-			_framesOnScreen.push_back(still);
-			g_engine->_dirtyRects.push_back(getFrameBoundingBox(still));
-		}
+	_framesNotYetShown.empty();
+	if (_hasStill) {
+		_framesNotYetShown = _frames;
 	}
-
+	_framesOnScreen.clear();
+	_startTime = 0;
+	_lastProcessedTime = 0;
+	_isPlaying = false;
 	runEventHandlerIfExists(kMovieStoppedEvent);
 }
 
 void Movie::process() {
-	if (_isVisible && _atFirstFrame) {
-		spatialShow();
-		_atFirstFrame = false;
+	if (_isVisible) {
+		if (_isPlaying) {
+			processTimeEventHandlers();
+		}
+		updateFrameState();
 	}
+}
 
-	if (_isPlaying) {
-		processTimeEventHandlers();
-		updateFrameState();
+void Movie::setVisibility(bool visibility) {
+	if (visibility != _isVisible) {
+		_isVisible = visibility;
+		invalidateLocalBounds();
 	}
 }
 
 void Movie::updateFrameState() {
-	if (!_isPlaying) {
-		debugC(6, kDebugGraphics, "Movie::updateFrameState (%d): Not playing", _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 movieTime = 0;
+	if (_isPlaying) {
+		uint currentTime = g_system->getMillis();
+		movieTime = currentTime - _startTime;
 	}
-
-	uint currentTime = g_system->getMillis();
-	uint movieTime = currentTime - _startTime;
 	debugC(5, kDebugGraphics, "Movie::updateFrameState (%d): Starting update (movie time: %d)", _id, movieTime);
 
 	// This complexity is necessary becuase movies can have more than one frame
@@ -406,10 +273,10 @@ void Movie::updateFrameState() {
 	// 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();
+		bool isAfterStart = movieTime >= frame->startInMilliseconds;
 		if (isAfterStart) {
-			_framesOnScreen.push_back(frame);
-			g_engine->_dirtyRects.push_back(getFrameBoundingBox(frame));
+			_framesOnScreen.insert(frame);
+			invalidateRect(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
@@ -425,61 +292,48 @@ void Movie::updateFrameState() {
 	// 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();
+		bool isAfterEnd = movieTime >= frame->endInMilliseconds;
 		if (isAfterEnd) {
-			g_engine->_dirtyRects.push_back(getFrameBoundingBox(frame));
+			invalidateRect(getFrameBoundingBox(frame));
 			it = _framesOnScreen.erase(it);
+
+			if (_framesOnScreen.empty() && movieTime >= _fullTime) {
+				_isPlaying = false;
+				if (_hasStill) {
+					_framesNotYetShown = _frames;
+					updateFrameState();
+				}
+				runEventHandlerIfExists(kMovieEndEvent);
+				break;
+			}
 		} else {
 			++it;
 		}
 	}
 
-	// Now see if we're at the end of the movie.
-	if (_framesOnScreen.empty() && _framesNotYetShown.empty()) {
-		_isPlaying = false;
-		_framesOnScreen.clear();
-		if (!_stills.empty()) {
-			showPersistentFrame();
-		}
-
-		runEventHandlerIfExists(kMovieEndEvent);
-		return;
-	}
-
 	// 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::showPersistentFrame() {
-	for (MovieFrame *still : _stills) {
-		_framesOnScreen.push_back(still);
-		g_engine->_dirtyRects.push_back(getFrameBoundingBox(still));
+		debugC(5, kDebugGraphics, "   (time: %d ms) Frame %d (%d x %d) @ (%d, %d); start: %d ms, end: %d ms, zIndex = %d", \
+			movieTime, frame->index, frame->image->width(), frame->image->height(), frame->leftTop.x, frame->leftTop.y, frame->startInMilliseconds, frame->endInMilliseconds, frame->zIndex);
 	}
 }
 
 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 : _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() - _boundingBox.left, -frame->top() - _boundingBox.top);
-			areaToRedraw.clip(Common::Rect(0, 0, frame->width(), frame->height()));
-			g_engine->_screen->simpleBlitFrom(frame->_surface, areaToRedraw, originOnScreen);
+			areaToRedraw.translate(-frame->leftTop.x - _boundingBox.left, -frame->leftTop.y - _boundingBox.top);
+			areaToRedraw.clip(Common::Rect(0, 0, frame->image->width(), frame->image->height()));
+			g_engine->_screen->simpleBlitFrom(frame->image->_surface, areaToRedraw, originOnScreen);
 		}
 	}
 }
 
 Common::Rect Movie::getFrameBoundingBox(MovieFrame *frame) {
-	Common::Rect bbox = frame->boundingBox();
-	bbox.translate(_boundingBox.left, _boundingBox.top);
+	Common::Point origin = _boundingBox.origin() + frame->leftTop;
+	Common::Rect bbox = Common::Rect(origin, frame->image->width(), frame->image->height());
 	return bbox;
 }
 
@@ -487,28 +341,21 @@ void Movie::readChunk(Chunk &chunk) {
 	// Individual chunks are "stills" and are stored in the first subfile.
 	uint sectionType = chunk.readTypedUint16();
 	switch ((MovieSectionType)sectionType) {
-	case kMovieFrameSection: {
-		debugC(5, kDebugLoading, "Movie::readStill(): Reading frame");
-		MovieFrameHeader *header = new MovieFrameHeader(chunk);
-		MovieFrame *frame = new MovieFrame(chunk, header);
-		_stills.push_back(frame);
+	case kMovieImageDataSection:
+		readImageData(chunk);
 		break;
-	}
 
-	case kMovieFooterSection: {
-		debugC(5, kDebugLoading, "Movie::readStill(): Reading footer");
-		MovieFrameFooter *footer = new MovieFrameFooter(chunk);
-		_footers.push_back(footer);
+	case kMovieFrameDataSection:
+		readFrameData(chunk);
 		break;
-	}
 
 	default:
 		error("Unknown movie still section type");
 	}
+	_hasStill = true;
 }
 
 void Movie::readSubfile(Subfile &subfile, Chunk &chunk) {
-	// READ THE METADATA FOR THE WHOLE MOVIE.
 	uint expectedRootSectionType = chunk.readTypedUint16();
 	debugC(5, kDebugLoading, "Movie::readSubfile(): sectionType = 0x%x (@0x%llx)", static_cast<uint>(expectedRootSectionType), static_cast<long long int>(chunk.pos()));
 	if (kMovieRootSection != (MovieSectionType)expectedRootSectionType) {
@@ -540,24 +387,18 @@ void Movie::readSubfile(Subfile &subfile, Chunk &chunk) {
 			uint sectionType = chunk.readTypedUint16();
 			debugC(5, kDebugLoading, "Movie::readSubfile(): sectionType = 0x%x (@0x%llx)", static_cast<uint>(sectionType), static_cast<long long int>(chunk.pos()));
 			switch (MovieSectionType(sectionType)) {
-			case kMovieFrameSection: {
-				MovieFrameHeader *header = new MovieFrameHeader(chunk);
-				MovieFrame *frame = new MovieFrame(chunk, header);
-				_frames.push_back(frame);
+			case kMovieImageDataSection:
+				readImageData(chunk);
 				break;
-			}
 
-			case kMovieFooterSection: {
-				MovieFrameFooter *footer = new MovieFrameFooter(chunk);
-				_footers.push_back(footer);
+			case kMovieFrameDataSection:
+				readFrameData(chunk);
 				break;
-			}
 
 			default:
 				error("Movie::readSubfile(): Unknown movie animation section type 0x%x (@0x%llx)", static_cast<uint>(sectionType), static_cast<long long int>(chunk.pos()));
 			}
 
-			// READ THE NEXT CHUNK.
 			chunk = subfile.nextChunk();
 			isAnimationChunk = (chunk._id == _animationChunkReference);
 		}
@@ -572,7 +413,6 @@ void Movie::readSubfile(Subfile &subfile, Chunk &chunk) {
 			debugC(5, kDebugLoading, "Movie::readSubfile(): (Frameset %d of %d) No audio chunk to read. (@0x%llx)", i, chunkCount, static_cast<long long int>(chunk.pos()));
 		}
 
-		// READ THE FOOTER FOR THIS SUBFILE.
 		debugC(5, kDebugLoading, "Movie::readSubfile(): (Frameset %d of %d) Reading header chunk... (@0x%llx)", i, chunkCount, static_cast<long long int>(chunk.pos()));
 		bool isHeaderChunk = (chunk._id == _chunkReference);
 		if (isHeaderChunk) {
@@ -585,21 +425,62 @@ 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) {
+		if (frame->endInMilliseconds > _fullTime) {
+			_fullTime = frame->endInMilliseconds;
 		}
 	}
 
-	for (MovieFrame *frame : _frames) {
-		for (MovieFrameFooter *footer : _footers) {
-			if (frame->index() == footer->_index) {
-				frame->setFooter(footer);
+	if (_hasStill) {
+		_framesNotYetShown = _frames;
+	}
+}
+
+void Movie::invalidateRect(const Common::Rect &rect) {
+	g_engine->_dirtyRects.push_back(rect);
+}
+
+void Movie::readImageData(Chunk &chunk) {
+	MovieFrameHeader *header = new MovieFrameHeader(chunk);
+	MovieFrameImage *frame = new MovieFrameImage(chunk, header);
+	_images.push_back(frame);
+}
+
+void Movie::readFrameData(Chunk &chunk) {
+	uint frameDataToRead = chunk.readTypedUint16();
+	for (uint i = 0; i < frameDataToRead; i++) {
+		MovieFrame *frame = new MovieFrame(chunk);
+
+		// We cannot use a hashmap here because multiple frames can have the
+		// same index, and frames are not necessarily in index order. So we'll
+		// do a linear search, which is how the original does it.
+		for (MovieFrameImage *image : _images) {
+			if (image->index() == frame->index) {
+				frame->image = image;
+				break;
+			}
+		}
+
+		if (frame->keyframeIndex != 0) {
+			for (MovieFrameImage *image : _images) {
+				if (image->index() == frame->keyframeIndex) {
+					frame->keyframeImage = image;
+					break;
+				}
 			}
 		}
+
+		_frames.push_back(frame);
+	}
+}
+
+int Movie::compareFramesByZIndex(const MovieFrame *a, const MovieFrame *b) {
+	if (b->zIndex > a->zIndex) {
+		return 1;
+	} else if (a->zIndex > b->zIndex) {
+		return -1;
+	} else {
+		return 0;
 	}
 }
 
diff --git a/engines/mediastation/assets/movie.h b/engines/mediastation/assets/movie.h
index aca2623f284..896a0311836 100644
--- a/engines/mediastation/assets/movie.h
+++ b/engines/mediastation/assets/movie.h
@@ -40,58 +40,44 @@ public:
 	uint _keyframeEndInMilliseconds = 0;
 };
 
-class MovieFrameFooter {
+class MovieFrameImage : public Bitmap {
 public:
-	MovieFrameFooter(Chunk &chunk);
-
-	uint _unk1 = 0;
-	uint _unk2 = 0;
-	uint _startInMilliseconds = 0;
-	uint _endInMilliseconds = 0;
-	uint _left = 0;
-	uint _top = 0;
-	uint _unk3 = 0;
-	uint _unk4 = 0;
-	uint _zIndex = 0; // TODO: This is still unconfirmed but seems likely.
-	uint _diffBetweenKeyframeAndFrameX = 0;
-	uint _diffBetweenKeyframeAndFrameY = 0;
-	uint _keyframeIndex = 0;
-	uint _unk9 = 0;
-	uint _index = 0;
-};
+	MovieFrameImage(Chunk &chunk, MovieFrameHeader *header);
+	virtual ~MovieFrameImage() override;
 
-class MovieFrame : public Bitmap {
-public:
-	MovieFrame(Chunk &chunk, MovieFrameHeader *header);
-	virtual ~MovieFrame() override;
-
-	void setFooter(MovieFrameFooter *footer);
-	uint32 left();
-	uint32 top();
-	Common::Point topLeft();
-	Common::Rect boundingBox();
-	uint32 index();
-	uint32 startInMilliseconds();
-	uint32 endInMilliseconds();
-	uint32 keyframeEndInMilliseconds();
-	// This is called zCoordinate because zIndex is too close to "index" and
-	// that could be confusing.
-	uint32 zCoordinate();
+	uint32 index() { return _bitmapHeader->_index; }
 
 private:
 	MovieFrameHeader *_bitmapHeader = nullptr;
-	MovieFrameFooter *_footer = nullptr;
 };
 
 enum MovieSectionType {
 	kMovieRootSection = 0x06a8,
-	kMovieFrameSection = 0x06a9,
-	kMovieFooterSection = 0x06aa
+	kMovieImageDataSection = 0x06a9,
+	kMovieFrameDataSection = 0x06aa
+};
+
+struct MovieFrame {
+	MovieFrame(Chunk &chunk);
+	uint unk3 = 0;
+	uint unk4 = 0;
+	uint layerId = 0;
+	uint startInMilliseconds = 0;
+	uint endInMilliseconds = 0;
+	Common::Point leftTop;
+	Common::Point diffBetweenKeyframeAndFrame;
+	uint blitType = 0;
+	int16 zIndex = 0;
+	uint keyframeIndex = 0;
+	bool keepAfterEnd = false;
+	uint index = 0;
+	MovieFrameImage *image = nullptr;
+	MovieFrameImage *keyframeImage = nullptr;
 };
 
 class Movie : public SpatialEntity {
 public:
-	Movie() : SpatialEntity(kAssetTypeMovie) {};
+	Movie() : _framesOnScreen(Movie::compareFramesByZIndex), SpatialEntity(kAssetTypeMovie) {}
 	virtual ~Movie() override;
 
 	virtual void readChunk(Chunk &chunk) override;
@@ -111,29 +97,32 @@ public:
 private:
 	AudioSequence _audioSequence;
 	uint _audioChunkCount = 0;
+	uint _fullTime = 0;
 
 	uint _loadType = 0;
 	double _dissolveFactor = 0.0;
 	bool _isPlaying = false;
-	bool _atFirstFrame = true;
+	bool _hasStill = false;
 
 	Common::Array<MovieFrame *> _frames;
-	Common::Array<MovieFrame *> _stills;
-	Common::Array<MovieFrameFooter *> _footers;
+	Common::Array<MovieFrameImage *> _images;
 
 	Common::Array<MovieFrame *> _framesNotYetShown;
-	Common::Array<MovieFrame *> _framesOnScreen;
+	Common::SortedArray<MovieFrame *, const MovieFrame *> _framesOnScreen;
 
 	// Script method implementations.
 	void timePlay();
 	void timeStop();
-	void spatialShow();
-	void spatialHide();
 
+	void setVisibility(bool visibility);
 	void updateFrameState();
-	void showPersistentFrame();
+	void invalidateRect(const Common::Rect &rect);
+
+	void readImageData(Chunk &chunk);
+	void readFrameData(Chunk &chunk);
 
 	Common::Rect getFrameBoundingBox(MovieFrame *frame);
+	static int compareFramesByZIndex(const MovieFrame *a, const MovieFrame *b);
 };
 
 } // End of namespace MediaStation


Commit: 6cb99deffe1d4e09726f5e30e70c3fae8214747b
    https://github.com/scummvm/scummvm/commit/6cb99deffe1d4e09726f5e30e70c3fae8214747b
Author: Nathanael Gentry (nathanael.gentrydb8 at gmail.com)
Date: 2025-06-14T11:07:14-04:00

Commit Message:
MEDIASTATION: JANITORIAL: FIx whitespace & namespace comments

Changed paths:
    engines/mediastation/bitmap.cpp
    engines/mediastation/bitmap.h
    engines/mediastation/boot.cpp
    engines/mediastation/boot.h
    engines/mediastation/context.cpp
    engines/mediastation/datafile.h
    engines/mediastation/mediascript/function.cpp
    engines/mediastation/mediascript/scriptconstants.h
    engines/mediastation/mediascript/scriptvalue.cpp
    engines/mediastation/mediastation.cpp


diff --git a/engines/mediastation/bitmap.cpp b/engines/mediastation/bitmap.cpp
index 389c03555b4..c0251d3a5a8 100644
--- a/engines/mediastation/bitmap.cpp
+++ b/engines/mediastation/bitmap.cpp
@@ -41,8 +41,7 @@ bool BitmapHeader::isCompressed() {
 	return (_compressionType != kUncompressedBitmap1) && (_compressionType != kUncompressedBitmap2);
 }
 
-Bitmap::Bitmap(Chunk &chunk, BitmapHeader *bitmapHeader) :
-	_bitmapHeader(bitmapHeader) {
+Bitmap::Bitmap(Chunk &chunk, BitmapHeader *bitmapHeader) : _bitmapHeader(bitmapHeader) {
 	// The header must be constructed beforehand.
 	int16 width = _bitmapHeader->_dimensions.x;
 	int16 height = _bitmapHeader->_dimensions.y;
@@ -212,4 +211,4 @@ void Bitmap::decompress(Chunk &chunk) {
 	}
 }
 
-}
+} // End of namespace MediaStation
diff --git a/engines/mediastation/bitmap.h b/engines/mediastation/bitmap.h
index 441396a33fe..2c92728fcc9 100644
--- a/engines/mediastation/bitmap.h
+++ b/engines/mediastation/bitmap.h
@@ -63,6 +63,6 @@ private:
 	void decompress(Chunk &chunk);
 };
 
-}
+} // End of namespace MediaStation
 
 #endif
\ No newline at end of file
diff --git a/engines/mediastation/boot.cpp b/engines/mediastation/boot.cpp
index fb767591453..3aabdda634a 100644
--- a/engines/mediastation/boot.cpp
+++ b/engines/mediastation/boot.cpp
@@ -144,7 +144,7 @@ SubfileDeclarationSectionType SubfileDeclaration::getSectionType(Chunk &chunk) {
 #pragma endregion
 
 #pragma region CursorDeclaration
-CursorDeclaration::CursorDeclaration(Chunk& chunk) {
+CursorDeclaration::CursorDeclaration(Chunk &chunk) {
 	uint16 unk1 = chunk.readTypedUint16(); // Always 0x0001
 	_id = chunk.readTypedUint16();
 	_unk = chunk.readTypedUint16();
@@ -154,7 +154,7 @@ CursorDeclaration::CursorDeclaration(Chunk& chunk) {
 #pragma endregion
 
 #pragma region Boot
-Boot::Boot(const Common::Path &path) : Datafile(path){
+Boot::Boot(const Common::Path &path) : Datafile(path) {
 	Subfile subfile = getNextSubfile();
 	Chunk chunk = subfile.nextChunk();
 
diff --git a/engines/mediastation/boot.h b/engines/mediastation/boot.h
index f77332bbdb9..664f22135be 100644
--- a/engines/mediastation/boot.h
+++ b/engines/mediastation/boot.h
@@ -69,7 +69,7 @@ public:
 	uint _screenId = 0;
 
 private:
-	ScreenDeclarationSectionType getSectionType(Chunk& chunk);
+	ScreenDeclarationSectionType getSectionType(Chunk &chunk);
 };
 
 enum FileDeclarationSectionType {
diff --git a/engines/mediastation/context.cpp b/engines/mediastation/context.cpp
index 2363b41c5f2..02756b834ee 100644
--- a/engines/mediastation/context.cpp
+++ b/engines/mediastation/context.cpp
@@ -367,7 +367,7 @@ bool Context::readHeaderSection(Chunk &chunk) {
 		// TODO: Avoid the copying here!
 		const uint PALETTE_ENTRIES = 256;
 		const uint PALETTE_BYTES = PALETTE_ENTRIES * 3;
-		byte* buffer = new byte[PALETTE_BYTES];
+		byte *buffer = new byte[PALETTE_BYTES];
 		chunk.read(buffer, PALETTE_BYTES);
 		_palette = new Graphics::Palette(buffer, PALETTE_ENTRIES);
 		delete[] buffer;
diff --git a/engines/mediastation/datafile.h b/engines/mediastation/datafile.h
index 33d3c4c272a..5a1b47e9519 100644
--- a/engines/mediastation/datafile.h
+++ b/engines/mediastation/datafile.h
@@ -115,7 +115,7 @@ public:
 
 	// ReadStream implementation
 	virtual bool eos() const { return _parentStream->eos(); };
-	virtual bool err() const {return _parentStream->err(); };
+	virtual bool err() const { return _parentStream->err(); };
 	virtual void clearErr() { _parentStream->clearErr(); };
 	virtual uint32 read(void *dataPtr, uint32 dataSize);
 	virtual int64 pos() const { return _parentStream->pos(); };
diff --git a/engines/mediastation/mediascript/function.cpp b/engines/mediastation/mediascript/function.cpp
index 89570482602..8f8de848757 100644
--- a/engines/mediastation/mediascript/function.cpp
+++ b/engines/mediastation/mediascript/function.cpp
@@ -46,4 +46,3 @@ ScriptValue Function::execute(Common::Array<ScriptValue> &args) {
 }
 
 } // End of namespace MediaStation
-
diff --git a/engines/mediastation/mediascript/scriptconstants.h b/engines/mediastation/mediascript/scriptconstants.h
index 9658dc49af1..232294a46f5 100644
--- a/engines/mediastation/mediascript/scriptconstants.h
+++ b/engines/mediastation/mediascript/scriptconstants.h
@@ -206,7 +206,7 @@ enum EventType {
 	kMovieStoppedEvent = 31,
 	kMovieBeginEvent = 32,
 
-	//SPRITE EVENTS.
+	// SPRITE EVENTS.
 	// Just "MovieEnd" in source.
 	kSpriteMovieEndEvent = 23,
 
diff --git a/engines/mediastation/mediascript/scriptvalue.cpp b/engines/mediastation/mediascript/scriptvalue.cpp
index bbfcef64734..75697963fa2 100644
--- a/engines/mediastation/mediascript/scriptvalue.cpp
+++ b/engines/mediastation/mediascript/scriptvalue.cpp
@@ -234,8 +234,7 @@ BuiltInMethod ScriptValue::asMethodId() const {
 
 bool ScriptValue::compare(Opcode op, const ScriptValue &lhs, const ScriptValue &rhs) {
 	if (lhs.getType() != rhs.getType()) {
-		error("Attempt to compare mismatched types %s and %s",
-		      scriptValueTypeToStr(lhs.getType()), scriptValueTypeToStr(rhs.getType()));
+		error("Attempt to compare mismatched types %s and %s", scriptValueTypeToStr(lhs.getType()), scriptValueTypeToStr(rhs.getType()));
 	}
 
 	switch (lhs.getType()) {
diff --git a/engines/mediastation/mediastation.cpp b/engines/mediastation/mediastation.cpp
index 161da5f2b39..649733a3e20 100644
--- a/engines/mediastation/mediastation.cpp
+++ b/engines/mediastation/mediastation.cpp
@@ -149,11 +149,11 @@ Common::Error MediaStationEngine::run() {
 	}
 	_cursor->showCursor();
 
-    if (ConfMan.hasKey("entry_context")) {
+	if (ConfMan.hasKey("entry_context")) {
 		// For development purposes, we can choose to start at an arbitrary context
 		// in this title. This might not work in all cases.
-        uint entryContextId = ConfMan.get("entry_context").asUint64();
-        warning("Starting at user-requested context %d", entryContextId);
+		uint entryContextId = ConfMan.get("entry_context").asUint64();
+		warning("Starting at user-requested context %d", entryContextId);
 		_requestedScreenBranchId = entryContextId;
 	} else {
 		_requestedScreenBranchId = _boot->_entryContextId;




More information about the Scummvm-git-logs mailing list