[Scummvm-git-logs] scummvm master -> 4fa1ca2ea34e8bc7274081e82b601af29ca412ac

neuromancer noreply at scummvm.org
Fri Sep 25 11:35:35 UTC 2026


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

Summary:
0e3d9c6591 COLONY: stair related render and sound fixes
c0b89770e8 COLONY: new command and adjusted door behavior when power is off
ba109d8eb2 COLONY: improved end game animations
4fa1ca2ea3 COLONY: adjust text scrolling speed in intro


Commit: 0e3d9c659152757139c392c6da2b90895e6cd317
    https://github.com/scummvm/scummvm/commit/0e3d9c659152757139c392c6da2b90895e6cd317
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-09-25T13:34:53+02:00

Commit Message:
COLONY: stair related render and sound fixes

Changed paths:
    engines/colony/colony.h
    engines/colony/movement.cpp
    engines/colony/render.cpp
    engines/colony/render_features.cpp


diff --git a/engines/colony/colony.h b/engines/colony/colony.h
index 58bbb585aee..7c19b740675 100644
--- a/engines/colony/colony.h
+++ b/engines/colony/colony.h
@@ -729,10 +729,11 @@ private:
 	void initRobots();
 	void renderCorridor3D();
 	void drawWallFeatures3D();
+	bool isWallFeatureFacingCamera(int cellX, int cellY, int direction) const;
 	void drawWallFeature3D(int cellX, int cellY, int direction);
 	void drawCellFeature3D(int cellX, int cellY);
 	void getWallFace3D(int cellX, int cellY, int direction, float corners[4][3]);
-	bool isRecessFeature(int x, int y, int direction) const;
+	bool isVisibleRecessFeature(int x, int y, int direction) const;
 	bool wallSegmentIsOpenWell(int x, int y, uint8 bit) const;
 	void getWallRecess3D(const float corners[4][3], float farC[4][3]) const;
 	void recessPoint(const float nearC[4][3], const float farC[4][3], float u, float v, float depth, float out[3]) const;
diff --git a/engines/colony/movement.cpp b/engines/colony/movement.cpp
index 4e6818edc26..ee2108f0ab0 100644
--- a/engines/colony/movement.cpp
+++ b/engines/colony/movement.cpp
@@ -882,16 +882,19 @@ int ColonyEngine::tryPassThroughFeature(int fromX, int fromY, int direction, Loc
 		if (pobject != &_me)
 			return 0; // robots don't use stairs
 
+		// goToDestination() can load another level and overwrite map.
+		const bool goingDown = (map[0] == kWallFeatureDnStairs);
+
 		// UpStairs(): the forklift cannot be driven up a staircase.
-		if (map[0] == kWallFeatureUpStairs && _fl)
+		if (!goingDown && _fl)
 			return 0;
 
 		const int result = goToDestination(map, pobject);
-		if (map[0] == kWallFeatureDnStairs && _fl)
+		if (goingDown && _fl)
 			doDnStairs();
 		if (result == 2) {
 			debugC(1, kColonyDebugMove, "Level change via %s: level=%d pos=(%d,%d)",
-				map[0] == kWallFeatureUpStairs ? "upstairs" : "downstairs",
+				goingDown ? "downstairs" : "upstairs",
 				_level, pobject->xindex, pobject->yindex);
 		}
 		return result;
diff --git a/engines/colony/render.cpp b/engines/colony/render.cpp
index a35f1ae59e0..33223e58624 100644
--- a/engines/colony/render.cpp
+++ b/engines/colony/render.cpp
@@ -436,19 +436,23 @@ uint8 ColonyEngine::wallAt(int x, int y) const {
 	return _wall[x][y];
 }
 
-bool ColonyEngine::isRecessFeature(int x, int y, int direction) const {
+bool ColonyEngine::isVisibleRecessFeature(int x, int y, int direction) const {
 	const uint8 *map = mapFeatureAt(x, y, direction);
 	if (!map)
 		return false;
-	return map[0] == kWallFeatureUpStairs || map[0] == kWallFeatureDnStairs;
+	return (map[0] == kWallFeatureUpStairs || map[0] == kWallFeatureDnStairs)
+		&& _visibleCell[x][y] && isWallFeatureFacingCamera(x, y, direction);
 }
 
-// Bit 0x01 spans (x,y-1)/(x,y); bit 0x02 spans (x-1,y)/(x,y). Either side may
-// record the well.
+// Only remove a wall when drawWallFeatures3D() will replace it with a well.
+// Bit 0x01 spans (x,y-1)/(x,y); bit 0x02 spans (x-1,y)/(x,y).
 bool ColonyEngine::wallSegmentIsOpenWell(int x, int y, uint8 bit) const {
+	if (_corePower[_coreIndex] == 0)
+		return false;
+
 	if (bit == 0x01)
-		return isRecessFeature(x, y, kDirSouth) || isRecessFeature(x, y - 1, kDirNorth);
-	return isRecessFeature(x, y, kDirWest) || isRecessFeature(x - 1, y, kDirEast);
+		return isVisibleRecessFeature(x, y, kDirSouth) || isVisibleRecessFeature(x, y - 1, kDirNorth);
+	return isVisibleRecessFeature(x, y, kDirWest) || isVisibleRecessFeature(x - 1, y, kDirEast);
 }
 
 const uint8 *ColonyEngine::mapFeatureAt(int x, int y, int direction) const {
diff --git a/engines/colony/render_features.cpp b/engines/colony/render_features.cpp
index b8d1a07382f..4e863c7545e 100644
--- a/engines/colony/render_features.cpp
+++ b/engines/colony/render_features.cpp
@@ -585,33 +585,28 @@ float stairStepHeight(const float *vf, const float *vc, int d, int s) {
 	return vf[d] + (s + 1) / 8.0f * (vc[d] - vf[d]);
 }
 
-void ColonyEngine::drawWallFeature3D(int cellX, int cellY, int direction) {
-	const uint8 *map = mapFeatureAt(cellX, cellY, direction);
-	if (!map || map[0] == kWallFeatureNone)
-		return;
-
+bool ColonyEngine::isWallFeatureFacingCamera(int cellX, int cellY, int direction) const {
 	// Backface culling: only draw features for the side facing the camera.
 	// This prevents backside decorations (like Level 2 lines) from bleeding through.
-	// We use non-inclusive comparisons so features remain visible while standing on the boundary.
+	// Keep features visible while standing on the boundary.
 	switch (direction) {
 	case kDirNorth:
-		if (_me.yloc > (cellY + 1) * 256)
-			return;
-		break;
+		return _me.yloc <= (cellY + 1) * 256;
 	case kDirSouth:
-		if (_me.yloc < cellY * 256)
-			return;
-		break;
+		return _me.yloc >= cellY * 256;
 	case kDirWest:
-		if (_me.xloc < cellX * 256)
-			return;
-		break;
+		return _me.xloc >= cellX * 256;
 	case kDirEast:
-		if (_me.xloc > (cellX + 1) * 256)
-			return;
-		break;
-	default: break;
+		return _me.xloc <= (cellX + 1) * 256;
+	default:
+		return true;
 	}
+}
+
+void ColonyEngine::drawWallFeature3D(int cellX, int cellY, int direction) {
+	const uint8 *map = mapFeatureAt(cellX, cellY, direction);
+	if (!map || map[0] == kWallFeatureNone || !isWallFeatureFacingCamera(cellX, cellY, direction))
+		return;
 
 	float corners[4][3];
 	getWallFace3D(cellX, cellY, direction, corners);
@@ -649,6 +644,20 @@ void ColonyEngine::drawWallFeature3D(int cellX, int cellY, int direction) {
 		_gfx->setStippleData(nullptr);
 	};
 
+	auto fillStairwellSides = [&](const float farC[4][3], float nearD, float farD, float bottomV, float topV) {
+		if (!macMode && _wireframe)
+			return;
+		const float sideV[4] = {topV, bottomV, bottomV, topV};
+		const float sideD[4] = {nearD, nearD, farD, farD};
+		const float leftU[4] = {0.0f, 0.0f, 0.0f, 0.0f};
+		const float rightU[4] = {1.0f, 1.0f, 1.0f, 1.0f};
+		_gfx->setStippleData(nullptr);
+		_gfx->setWireframe(false);
+		recessQuad(corners, farC, leftU, sideV, sideD, 4, wallFeatureFill);
+		recessQuad(corners, farC, rightU, sideV, sideD, 4, wallFeatureFill);
+		_gfx->setWireframe(true, wallFeatureFill);
+	};
+
 	switch (map[0]) {
 	case kWallFeatureDoor: {
 		const uint32 doorColor = macColors ? (uint32)0xFF000000 :
@@ -813,7 +822,11 @@ void ColonyEngine::drawWallFeature3D(int cellX, int cellY, int direction) {
 			hgt[i] = (float)(i + 1) / 8.0f;
 		}
 
-		// ColorWall(), now the back of the hole.
+		// The original ColorWall() hid the corridor behind the entire opening.
+		// A recessed staircase also needs opaque sides to enclose the well.
+		fillStairwellSides(farC, 0.0f, 1.0f, 0.0f, 1.0f);
+
+		// Back of the well.
 		if (isMacRenderMode()) {
 			const float uw[4] = {0.0f, 1.0f, 1.0f, 0.0f};
 			const float vw[4] = {0.0f, 0.0f, 1.0f, 1.0f};
@@ -904,22 +917,13 @@ void ColonyEngine::drawWallFeature3D(int cellX, int cellY, int direction) {
 			hgt[i] = -(float)(i + 1) / 32.0f;
 		}
 
-		// Fill the shaft sides without framing each panel, so their shared edges
-		// form one continuous wall rather than a row of transparent-looking bars.
-		if (isMacRenderMode() || !_wireframe) {
-			_gfx->setStippleData(nullptr);
-			_gfx->setWireframe(false);
-			for (int i = 1; i <= 7; i++) {
-				const float nearD = dep[i - 1];
-				const float farD = (i < 7) ? dep[i] : 1.0f;
-				const float bottomV = hgt[i - 1];
-				const float sideV[4] = {0.0f, bottomV, bottomV, 0.0f};
-				const float sideD[4] = {nearD, nearD, farD, farD};
-				const float leftU[4] = {0.0f, 0.0f, 0.0f, 0.0f};
-				const float rightU[4] = {1.0f, 1.0f, 1.0f, 1.0f};
-				recessQuad(corners, farC, leftU, sideV, sideD, 4, wallFeatureFill);
-				recessQuad(corners, farC, rightU, sideV, sideD, 4, wallFeatureFill);
-			}
+		// Enclose the well above the floor, then extend its sides down to each
+		// tread. Leave the panels unframed so their shared edges stay invisible.
+		fillStairwellSides(farC, 0.0f, 1.0f, 0.0f, 1.0f);
+		for (int i = 1; i <= 7; i++) {
+			const float nearD = dep[i - 1];
+			const float farD = (i < 7) ? dep[i] : 1.0f;
+			fillStairwellSides(farC, nearD, farD, hgt[i - 1], 0.0f);
 		}
 
 		if (isMacRenderMode()) {


Commit: c0b89770e8e82a50dda9300920788de45928f638
    https://github.com/scummvm/scummvm/commit/c0b89770e8e82a50dda9300920788de45928f638
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-09-25T13:34:53+02:00

Commit Message:
COLONY: new command and adjusted door behavior when power is off

Changed paths:
    engines/colony/colony.h
    engines/colony/console.cpp
    engines/colony/console.h
    engines/colony/map.cpp
    engines/colony/movement.cpp
    engines/colony/render.cpp
    engines/colony/render_features.cpp
    engines/colony/savegame.cpp


diff --git a/engines/colony/colony.h b/engines/colony/colony.h
index 7c19b740675..6b54c6e59b5 100644
--- a/engines/colony/colony.h
+++ b/engines/colony/colony.h
@@ -390,7 +390,7 @@ struct PassPatch {
 	uint8 ang;
 };
 
-// Per-level persistence: wall state changes (airlock locks) and visit flags.
+// Per-level persistence: door and airlock states, and visit flags.
 struct LevelData {
 	uint8 visit;
 	uint8 queen;
@@ -399,6 +399,7 @@ struct LevelData {
 	uint8 size;            // number of saved wall changes (max 10)
 	uint8 location[10][3]; // [x, y, direction] of each changed wall
 	uint8 data[10][5];     // saved wall feature bytes (5 per location)
+	uint8 openDoors[31][31]; // direction bits for open ordinary doors
 };
 
 struct MacColor {
@@ -788,6 +789,7 @@ private:
 	void resetObjectSlot(int slot, int type, int xloc, int yloc, uint8 ang);
 	bool createObject(int type, int xloc, int yloc, uint8 ang);
 	void saveLevelState();
+	void saveOpenDoors();
 	void doPatch();
 	void saveWall(int x, int y, int direction);
 	void getWall();
diff --git a/engines/colony/console.cpp b/engines/colony/console.cpp
index a39cfc43ab1..873c42cc9b3 100644
--- a/engines/colony/console.cpp
+++ b/engines/colony/console.cpp
@@ -116,6 +116,7 @@ Debugger::Debugger(ColonyEngine *vm) : GUI::Debugger(), _vm(vm) {
 	registerCmd("goals", WRAP_METHOD(Debugger, cmdGoals));
 	registerCmd("win", WRAP_METHOD(Debugger, cmdWin));
 	registerCmd("robots", WRAP_METHOD(Debugger, cmdRobots));
+	registerCmd("eradicate", WRAP_METHOD(Debugger, cmdEradicate));
 	registerCmd("map", WRAP_METHOD(Debugger, cmdMap));
 	registerCmd("give", WRAP_METHOD(Debugger, cmdGive));
 	registerCmd("power", WRAP_METHOD(Debugger, cmdPower));
@@ -488,6 +489,52 @@ bool Debugger::cmdRobots(int argc, const char **argv) {
 	return true;
 }
 
+bool Debugger::cmdEradicate(int argc, const char **argv) {
+	if (argc != 1) {
+		debugPrintf("Usage: eradicate\n");
+		debugPrintf("Kills all monsters and eggs on every level.\n");
+		return true;
+	}
+
+	auto isCreature = [this](uint objectNum) {
+		if (objectNum == 0 || objectNum == kMeNum || objectNum > _vm->_objects.size())
+			return false;
+		const int type = _vm->_objects[objectNum - 1].type;
+		return type >= kRobEye && type <= kRobSnoop;
+	};
+
+	for (uint i = 0; i < _vm->_objects.size(); i++) {
+		if (isCreature(i + 1)) {
+			_vm->_objects[i].alive = 0;
+			_vm->_objects[i].visible = 0;
+		}
+	}
+	for (int x = 0; x < 32; x++) {
+		for (int y = 0; y < 32; y++) {
+			if (isCreature(_vm->_robotArray[x][y]))
+				_vm->_robotArray[x][y] = 0;
+			if (isCreature(_vm->_foodArray[x][y]))
+				_vm->_foodArray[x][y] = 0;
+		}
+	}
+	if (isCreature(_vm->_bumpedObject))
+		_vm->_bumpedObject = 0;
+	_vm->_insight = false;
+	_vm->_allGrow = false;
+
+	// Restore empty populations on later visits, including the first visit to
+	// a level. Keep the saved wall and airlock state in each LevelData intact.
+	for (uint i = 0; i < ARRAYSIZE(_vm->_levelData); i++) {
+		LevelData &ld = _vm->_levelData[i];
+		ld.visit = 1;
+		ld.queen = 0;
+		memset(ld.object, 0, sizeof(ld.object));
+	}
+
+	debugPrintf("Eradicated all monsters and eggs from all levels.\n");
+	return true;
+}
+
 bool Debugger::cmdMap(int argc, const char **argv) {
 	int cx = _vm->_me.xindex;
 	int cy = _vm->_me.yindex;
diff --git a/engines/colony/console.h b/engines/colony/console.h
index b7cb9af782c..ea1a95b96e1 100644
--- a/engines/colony/console.h
+++ b/engines/colony/console.h
@@ -50,6 +50,7 @@ private:
 	bool cmdGoals(int argc, const char **argv);
 	bool cmdWin(int argc, const char **argv);
 	bool cmdRobots(int argc, const char **argv);
+	bool cmdEradicate(int argc, const char **argv);
 	bool cmdMap(int argc, const char **argv);
 	bool cmdGive(int argc, const char **argv);
 	bool cmdPower(int argc, const char **argv);
diff --git a/engines/colony/map.cpp b/engines/colony/map.cpp
index 9b68d0bcbb9..69e66fd66f7 100644
--- a/engines/colony/map.cpp
+++ b/engines/colony/map.cpp
@@ -171,7 +171,7 @@ void ColonyEngine::loadMap(int mnum) {
 	_coreIndex = (mnum == 1) ? 0 : 1;
 	_me.type = kMeNum;
 
-	getWall();  // restore saved wall state changes (airlocks)
+	getWall();  // restore saved door and airlock states
 	doPatch();  // apply object relocations from patch table
 	initRobots();  // spawn robot objects for this level
 
@@ -357,11 +357,37 @@ void ColonyEngine::saveWall(int x, int y, int direction) {
 	ld.size++;
 }
 
-// PATCH.C: getwall()  restore saved wall bytes into _mapData after level load.
+void ColonyEngine::saveOpenDoors() {
+	if (_level < 1 || _level > 8)
+		return;
+
+	// Ordinary doors stay open after passage. Keep their state separately from
+	// the original ten-entry wall patch table, which only had room for airlocks.
+	LevelData &ld = _levelData[_level - 1];
+	for (int x = 0; x < 31; x++) {
+		for (int y = 0; y < 31; y++) {
+			ld.openDoors[x][y] = 0;
+			for (int dir = 0; dir < 4; dir++) {
+				if (_mapData[x][y][dir][0] == kWallFeatureDoor && _mapData[x][y][dir][1] == 0)
+					ld.openDoors[x][y] |= 1 << dir;
+			}
+		}
+	}
+}
+
+// Restore saved door states and the original PATCH.C wall patches after loading a level.
 void ColonyEngine::getWall() {
 	if (_level < 1 || _level > 8)
 		return;
 	const LevelData &ld = _levelData[_level - 1];
+	for (int x = 0; x < 31; x++) {
+		for (int y = 0; y < 31; y++) {
+			for (int dir = 0; dir < 4; dir++) {
+				if ((ld.openDoors[x][y] & (1 << dir)) && _mapData[x][y][dir][0] == kWallFeatureDoor)
+					_mapData[x][y][dir][1] = 0;
+			}
+		}
+	}
 	for (int i = 0; i < ld.size; i++) {
 		int x = ld.location[i][0];
 		int y = ld.location[i][1];
diff --git a/engines/colony/movement.cpp b/engines/colony/movement.cpp
index ee2108f0ab0..0d9c6fcff0e 100644
--- a/engines/colony/movement.cpp
+++ b/engines/colony/movement.cpp
@@ -704,6 +704,9 @@ bool ColonyEngine::setDoorState(int x, int y, int direction, int state) {
 			_mapData[nx][ny][opposite][1] = (uint8)state;
 	}
 
+	if (wallType == kWallFeatureDoor)
+		saveOpenDoors();
+
 	// Persist airlock state changes across level loads
 	if (wallType == kWallFeatureAirlock) {
 		if (oldState != state && _level >= 1 && _level <= 8) {
diff --git a/engines/colony/render.cpp b/engines/colony/render.cpp
index 33223e58624..d4e2b3cfa0a 100644
--- a/engines/colony/render.cpp
+++ b/engines/colony/render.cpp
@@ -447,7 +447,7 @@ bool ColonyEngine::isVisibleRecessFeature(int x, int y, int direction) const {
 // Only remove a wall when drawWallFeatures3D() will replace it with a well.
 // Bit 0x01 spans (x,y-1)/(x,y); bit 0x02 spans (x-1,y)/(x,y).
 bool ColonyEngine::wallSegmentIsOpenWell(int x, int y, uint8 bit) const {
-	if (_corePower[_coreIndex] == 0)
+	if (!isMacColorMode() && _corePower[_coreIndex] == 0)
 		return false;
 
 	if (bit == 0x01)
diff --git a/engines/colony/render_features.cpp b/engines/colony/render_features.cpp
index 4e863c7545e..140e250ec21 100644
--- a/engines/colony/render_features.cpp
+++ b/engines/colony/render_features.cpp
@@ -189,6 +189,12 @@ void ColonyEngine::clipToWallFace(const float corners[4][3]) {
 void ColonyEngine::macFillRecess(const float nearC[4][3], const float farC[4][3],
 		const float *u, const float *v, const float *d, int count, int macIdx, bool macColors) {
 	if (macColors) {
+		if (_corePower[_coreIndex] == 0) {
+			_gfx->setWireframe(true, 0);
+			recessQuad(nearC, farC, u, v, d, count, 0);
+			return;
+		}
+
 		uint32 fg = packMacColor(_macColors[macIdx].fg);
 		uint32 bg = packMacColor(_macColors[macIdx].bg);
 		const byte *stipple = setupMacPattern(_gfx, _macColors[macIdx].pattern, fg, bg);
@@ -208,7 +214,6 @@ void ColonyEngine::wallLine(const float corners[4][3], float u1, float v1, float
 	float p1[3], p2[3];
 	wallPoint(corners, u1, v1, p1);
 	wallPoint(corners, u2, v2, p2);
-	// We assume this is only called when lit (handled in drawWallFeatures3D)
 	_gfx->draw3DLine(p1[0], p1[1], p1[2], p2[0], p2[1], p2[2], color);
 }
 
@@ -370,7 +375,8 @@ void ColonyEngine::wallChar(const float corners[4][3], uint8 cnum) {
 
 	const bool macMode = isMacRenderMode();
 	const bool macColors = isMacColorMode();
-	const uint32 fillColor = macColors ? packMacColor(_macColors[8 + _level - 1].bg) : 0;
+	const bool lit = (_corePower[_coreIndex] > 0);
+	const uint32 fillColor = macColors && lit ? packMacColor(_macColors[8 + _level - 1].bg) : 0;
 	const uint32 lineColor = macColors ? (uint32)0xFF000000 : 0;
 
 	auto drawFilledCharPolygon = [&](const float *u, const float *v, int count) {
@@ -471,8 +477,8 @@ void ColonyEngine::wallChar(const float corners[4][3], uint8 cnum) {
 
 	if (macMode) {
 		const uint32 wallFill = macColors
-			? packMacColor(_macColors[8 + _level - 1].fg)
-			: (uint32)255;
+			? packMacColor(lit ? _macColors[8 + _level - 1].fg : _macColors[6].bg)
+			: (lit ? 255u : 0u);
 		_gfx->setWireframe(true, wallFill);
 	}
 }
@@ -515,6 +521,12 @@ void ColonyEngine::drawCellFeature3D(int cellX, int cellY) {
 	// Helper lambda: draw a filled hole polygon with the platform material.
 	auto drawHolePoly = [&](const float *u, const float *v, int cnt, int macIdx) {
 		if (macColors) {
+			if (!lit) {
+				_gfx->setWireframe(true, 0);
+				wallPolygon(corners, u, v, cnt, 0);
+				return;
+			}
+
 			uint32 fg = packMacColor(_macColors[macIdx].fg);
 			uint32 bg = packMacColor(_macColors[macIdx].bg);
 			int pat = _macColors[macIdx].pattern;
@@ -576,8 +588,8 @@ void ColonyEngine::drawCellFeature3D(int cellX, int cellY) {
 
 	_gfx->setStippleData(nullptr);
 	const uint32 wallFill = macColors
-		? packMacColor(_macColors[8 + _level - 1].fg)
-		: (macMode ? 255u : 7u);
+		? packMacColor(lit ? _macColors[8 + _level - 1].fg : _macColors[6].bg)
+		: (lit ? (macMode ? 255u : 7u) : 0u);
 	_gfx->setWireframe(true, wallFill);
 }
 
@@ -617,11 +629,18 @@ void ColonyEngine::drawWallFeature3D(int cellX, int cellY, int direction) {
 		? packMacColor(lit ? _macColors[8 + _level - 1].fg : _macColors[6].bg)
 		: (lit ? (macMode ? 255u : 7u) : 0u);
 
-	// Wall faces are already filled with level-specific color (c_char0+level-1.fg)
-	// by the wall grid in renderCorridor3D(). Features are drawn on top.
+	// Wall faces are already filled by the wall grid in renderCorridor3D().
+	// Features are drawn on top, including silhouettes when the power is off.
 
 	// Helper lambda: Mac color fill for a wall feature polygon
 	auto macFillPoly = [&](const float *u, const float *v, int cnt, int macIdx) {
+		// Mac SuperPoly() fills features with black when the power is off.
+		if (!lit) {
+			_gfx->setWireframe(true, 0);
+			wallPolygon(corners, u, v, cnt, 0);
+			return;
+		}
+
 		uint32 fg = packMacColor(_macColors[macIdx].fg);
 		uint32 bg = packMacColor(_macColors[macIdx].bg);
 		const byte *stipple = setupMacPattern(_gfx, _macColors[macIdx].pattern, fg, bg);
@@ -1141,7 +1160,7 @@ void ColonyEngine::drawWallFeature3D(int cellX, int cellY, int direction) {
 						// BLACK: solid black fill
 						_gfx->setWireframe(true, (uint32)0xFF000000);
 						wallPolygon(corners, ub, vb4, 4, 0xFF000000);
-						_gfx->setWireframe(true, packMacColor(_macColors[8 + _level - 1].fg));
+						_gfx->setWireframe(true, wallFeatureFill);
 					} else {
 						macFillPoly(ub, vb4, 4, 26 + val); // c_color0 + val
 					}
@@ -1234,7 +1253,8 @@ void ColonyEngine::drawWallFeature3D(int cellX, int cellY, int direction) {
 }
 
 void ColonyEngine::drawWallFeatures3D() {
-	if (_corePower[_coreIndex] == 0)
+	// The Mac color version draws features as black silhouettes without power.
+	if (!isMacColorMode() && _corePower[_coreIndex] == 0)
 		return;
 
 	for (int y = 0; y < 31; y++) {
diff --git a/engines/colony/savegame.cpp b/engines/colony/savegame.cpp
index 93be2061633..09d332b4ab6 100644
--- a/engines/colony/savegame.cpp
+++ b/engines/colony/savegame.cpp
@@ -29,6 +29,7 @@
 namespace Colony {
 
 const uint32 kSaveVersion = 1;
+const uint32 kDoorStateTag = MKTAG('D', 'O', 'O', 'R');
 const uint32 kMaxSaveObjects = 4096;
 const uint32 kMaxSavePatches = 100;
 
@@ -201,10 +202,11 @@ void writeLevelData(Common::WriteStream *stream, const LevelData &levelData) {
 		for (int j = 0; j < 5; j++)
 			stream->writeByte(levelData.data[i][j]);
 	}
+	stream->writeUint32BE(kDoorStateTag);
+	stream->write(levelData.openDoors, sizeof(levelData.openDoors));
 }
 
-LevelData readLevelData(Common::SeekableReadStream *stream) {
-	LevelData levelData;
+bool readLevelData(Common::SeekableReadStream *stream, LevelData &levelData) {
 	levelData.visit = stream->readByte();
 	levelData.queen = stream->readByte();
 	for (int i = 0; i <= kBaseObject; i++)
@@ -219,7 +221,18 @@ LevelData readLevelData(Common::SeekableReadStream *stream) {
 		for (int j = 0; j < 5; j++)
 			levelData.data[i][j] = stream->readByte();
 	}
-	return levelData;
+	// Door states are required. The tag prevents saves without this block from
+	// being decoded using the following level's data as door states.
+	if (stream->readUint32BE() != kDoorStateTag ||
+			stream->read(levelData.openDoors, sizeof(levelData.openDoors)) != sizeof(levelData.openDoors))
+		return false;
+	for (int x = 0; x < 31; x++) {
+		for (int y = 0; y < 31; y++) {
+			if (levelData.openDoors[x][y] & ~0x0F)
+				return false;
+		}
+	}
+	return !stream->err() && !stream->eos();
 }
 
 bool validateGridReferences(const uint8 grid[32][32], uint32 objectCount, bool allowPlayerMarker) {
@@ -459,8 +472,10 @@ Common::Error ColonyEngine::loadGameStream(Common::SeekableReadStream *stream) {
 	for (int i = 0; i < 3; i++)
 		_corePower[i] = stream->readSint32LE();
 
-	for (uint i = 0; i < ARRAYSIZE(_levelData); i++)
-		_levelData[i] = readLevelData(stream);
+	for (uint i = 0; i < ARRAYSIZE(_levelData); i++) {
+		if (!readLevelData(stream, _levelData[i]))
+			return makeCorruptSaveError(Common::String::format("missing or invalid door state data for level %u", i + 1).c_str());
+	}
 
 	const uint32 patchCount = stream->readUint32LE();
 	if (patchCount > kMaxSavePatches)
@@ -527,7 +542,7 @@ Common::Error ColonyEngine::loadGameStream(Common::SeekableReadStream *stream) {
 		}
 	}
 
-	if (stream->err())
+	if (stream->err() || stream->eos())
 		return makeCorruptSaveError("stream read error while decoding save payload");
 
 	if (_coreIndex < 0 || _coreIndex > 1)


Commit: ba109d8eb27f0363c6bda7cc9df4785be5f9fc0c
    https://github.com/scummvm/scummvm/commit/ba109d8eb27f0363c6bda7cc9df4785be5f9fc0c
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-09-25T13:34:53+02:00

Commit Message:
COLONY: improved end game animations

Changed paths:
    engines/colony/colony.h
    engines/colony/intro.cpp
    engines/colony/sound.cpp
    engines/colony/sound.h


diff --git a/engines/colony/colony.h b/engines/colony/colony.h
index 6b54c6e59b5..decc2a5d8f2 100644
--- a/engines/colony/colony.h
+++ b/engines/colony/colony.h
@@ -914,6 +914,8 @@ private:
 	bool makeStars(const Common::Rect &r, int btn);
 	bool makeBlackHole();
 	bool makePlanet();
+	bool leavePlanet();
+	bool explodePlanet();
 	bool timeSquare(const Common::String &str, const Graphics::Font *macFont = nullptr, bool gameOver = false);
 	bool drawPict(int resID);
 	bool loadAnimation(const Common::String &name);
diff --git a/engines/colony/intro.cpp b/engines/colony/intro.cpp
index ba15bac6f9e..4d7063f2993 100644
--- a/engines/colony/intro.cpp
+++ b/engines/colony/intro.cpp
@@ -1053,6 +1053,218 @@ bool ColonyEngine::makePlanet() {
 	return false;
 }
 
+// leaveplanet() and explodeplanet() retain their XOR drawing between frames.
+// Keep that image in software: an OpenGL back buffer need not retain its pixels
+// after copyToScreen(), and the explosion deliberately accumulates dot trails.
+class PlanetAnimation {
+public:
+	PlanetAnimation(int width, int height, const int *sint, const int *cost, Common::RandomSource &random)
+		: _surface(width, height, Graphics::PixelFormat::createFormatRGBA32()),
+		  _sint(sint), _cost(cost), _center(width / 2, height / 2) {
+		const uint32 black = _surface.format.RGBToColor(0, 0, 0);
+		_color = _surface.format.RGBToColor(255, 255, 255);
+		_xorMask = _color ^ black;
+		_surface.clear(black);
+
+		for (int distance = 800; distance > 32; distance -= 16) {
+			const int32 radius = (160 * 128) / distance;
+			for (int i = 0; i < 4; i++) {
+				const int angle = random.getRandomNumber(255);
+				_stars[_remainingStars++] = Common::Point(
+					_center.x + ((radius * _sint[angle]) >> 7),
+					_center.y + ((radius * _cost[angle]) >> 7));
+			}
+		}
+	}
+
+	void revealStars(int count) {
+		// Pre-decrement and stop at zero: the original reads an uninitialized
+		// star first, then runs below zero at the end of takeoff.
+		while (count-- > 0 && _remainingStars > 0)
+			plot(_stars[--_remainingStars]);
+	}
+
+	void drawPlanet(int distance, bool erasePrevious) {
+		const int32 radius = (160 * 128) / distance;
+		const int sintheta = _sint[210];
+		const int costheta = _cost[210];
+		int point = 0;
+
+		for (int j = 0; j < 256; j += 16) {
+			for (int k = _phase; k < 128; k += 16, point++) {
+				if (erasePrevious && _visible[point])
+					plot(_points[point]);
+
+				const int x = (((radius * _sint[j]) >> 7) * _cost[k]) >> 7;
+				const int z = (((radius * _sint[j]) >> 7) * _sint[k]) >> 7;
+				_visible[point] = ((_cost[j] * sintheta + z * costheta) >> 7) >= 0;
+				if (_visible[point]) {
+					const int y = ((((radius * _cost[j]) >> 7) * costheta - z * sintheta) >> 7);
+					_points[point] = Common::Point(_center.x + x, _center.y + y);
+					plot(_points[point]);
+				}
+			}
+		}
+		_phase = (_phase + 1) % 16;
+	}
+
+	void invert() {
+		for (int y = 0; y < _surface.h; y++) {
+			uint32 *pixels = (uint32 *)_surface.getBasePtr(0, y);
+			for (int x = 0; x < _surface.w; x++)
+				pixels[x] ^= _xorMask;
+		}
+	}
+
+	void setXorMode(bool enable) { _xorMode = enable; }
+
+	void setColor(byte r, byte g, byte b) {
+		_color = _surface.format.RGBToColor(r, g, b);
+	}
+
+	void present(Renderer *gfx) {
+		gfx->setXorMode(false);
+		gfx->clear(gfx->black());
+		gfx->drawSurface(&_surface.rawSurface(), 0, 0);
+		gfx->copyToScreen();
+	}
+
+private:
+	void plot(const Common::Point &p) {
+		if (p.x < 0 || p.y < 0 || p.x >= _surface.w || p.y >= _surface.h)
+			return;
+
+		uint32 *pixel = (uint32 *)_surface.getBasePtr(p.x, p.y);
+		if (_xorMode)
+			*pixel ^= _xorMask;
+		else
+			*pixel = _color;
+	}
+
+	Graphics::ManagedSurface _surface;
+	const int *_sint;
+	const int *_cost;
+	Common::Point _center;
+	Common::Point _stars[((800 - 32) / 16) * 4];
+	Common::Point _points[(256 / 16) * (128 / 16)];
+	bool _visible[(256 / 16) * (128 / 16)] = {};
+	int _remainingStars = 0;
+	int _phase = 0;
+	uint32 _color;
+	uint32 _xorMask;
+	bool _xorMode = true;
+};
+
+bool ColonyEngine::leavePlanet() {
+	PlanetAnimation planet(_width, _height, _sint, _cost, _randomSource);
+	planet.drawPlanet(32, false);
+	planet.present(_gfx);
+
+	// intro.c / IBM_INTR.C leaveplanet(): rotate close to the surface, then
+	// recede into the starfield, reversing the introductory planet approach.
+	for (int frame = 0; frame < 25; frame++) {
+		if (checkSkipRequested())
+			return true;
+		planet.drawPlanet(32, true);
+		planet.present(_gfx);
+		_system->delayMillis(33);
+	}
+	for (int distance = 32; distance <= 800; distance += 16) {
+		if (checkSkipRequested())
+			return true;
+		planet.revealStars(4);
+		planet.drawPlanet(distance, true);
+		planet.present(_gfx);
+		_system->delayMillis(16);
+	}
+
+	if (getPlatform() == Common::kPlatformMacintosh) {
+		while (_sound->isPlaying()) {
+			if (checkSkipRequested())
+				return true;
+			planet.present(_gfx);
+			_system->delayMillis(10);
+		}
+	}
+	return false;
+}
+
+bool ColonyEngine::explodePlanet() {
+	const bool isMac = getPlatform() == Common::kPlatformMacintosh;
+	PlanetAnimation planet(_width, _height, _sint, _cost, _randomSource);
+	_sound->play(Sound::kPShot);
+	planet.revealStars(192);
+	planet.drawPlanet(800, false);
+	planet.present(_gfx);
+
+	// DOS rotates until PlanetShot finishes (100 frames without sound).
+	// Mac instead rotates for 30 frames, inverting the last eight of them.
+	const bool waitForShot = !isMac && _sound->isPlaying();
+	const int rotationFrames = isMac ? 30 : 100;
+	for (int frame = 0; waitForShot ? _sound->isPlaying() : frame < rotationFrames; frame++) {
+		if (checkSkipRequested())
+			return true;
+		if (isMac && frame > 21) {
+			planet.invert();
+			if (!_sound->isPlaying())
+				_sound->play(Sound::kExplode);
+		}
+		planet.drawPlanet(800, true);
+		planet.present(_gfx);
+		_system->delayMillis(33);
+	}
+
+	if (!isMac) {
+		_sound->play(Sound::kExplode);
+		const int flashes = _renderMode == Common::kRenderEGA ? 2 : 4;
+		for (int i = 0; i < flashes; i++) {
+			if (checkSkipRequested())
+				return true;
+			planet.invert();
+			planet.present(_gfx);
+			_system->delayMillis(50);
+		}
+		while (_sound->isPlaying()) {
+			if (checkSkipRequested())
+				return true;
+			planet.present(_gfx);
+			_system->delayMillis(10);
+		}
+		_sound->play(Sound::kExplode);
+	}
+
+	// Stop erasing the old points: the globe fills in and then bursts into
+	// expanding trails. Mac switches to patCopy and shades blue toward red;
+	// DOS keeps drawing the white points in XOR mode.
+	for (int frame = 0; frame < (isMac ? 15 : 25); frame++) {
+		if (checkSkipRequested())
+			return true;
+		if (isMac && !_sound->isPlaying())
+			_sound->play(Sound::kExplode);
+		planet.drawPlanet(800, false);
+		planet.present(_gfx);
+		_system->delayMillis(33);
+	}
+
+	if (isMac) {
+		_sound->play(Sound::kEnd);
+		planet.setXorMode(false);
+	}
+	uint16 red = 0, blue = 0xFFFF;
+	for (int distance = 800; distance > 32; distance -= 16) {
+		if (checkSkipRequested())
+			return true;
+		red += 1024;
+		blue -= 1024;
+		if (isMacColorMode())
+			planet.setColor(red >> 8, 0, blue >> 8);
+		planet.drawPlanet(distance, false);
+		planet.present(_gfx);
+		_system->delayMillis(16);
+	}
+	return false;
+}
+
 bool ColonyEngine::timeSquare(const Common::String &str, const Graphics::Font *macFont, bool gameOver) {
 	// Original: TimeSquare() in intro.c
 	// Mac and DOS use different presentation here. DOS is a monochrome/gray
@@ -1423,17 +1635,19 @@ void ColonyEngine::takeOff() {
 	_centerY = _height / 2;
 
 	debugC(1, kColonyDebugUI, "takeOff()");
+	const bool cursorWasVisible = CursorMan.isVisible();
+	CursorMan.showMouse(false);
 
 	_sound->stop();
 	_gfx->clear(_gfx->black());
 	_gfx->copyToScreen();
 
 	if (getPlatform() == Common::kPlatformMacintosh)
-		_sound->play(Sound::kMars);
+		_sound->play(Sound::kSwish);
 	else
 		_sound->play(Sound::kStars1);
 
-	makeStars(_screenR, 0);
+	leavePlanet();
 	_sound->stop();
 
 	_gfx->clear(_gfx->black());
@@ -1443,6 +1657,7 @@ void ColonyEngine::takeOff() {
 	_clip = savedClip;
 	_centerX = savedCenterX;
 	_centerY = savedCenterY;
+	CursorMan.showMouse(cursorWasVisible);
 }
 
 // Touching the monolith (the SCREEN object) blacks the screen out and runs the
@@ -1516,7 +1731,7 @@ void ColonyEngine::gameOver(bool kill, int savedCryos) {
 	_mouseLocked = false;
 	_system->lockMouse(false);
 	CursorMan.setDefaultArrowCursor(true);
-	CursorMan.showMouse(true);
+	CursorMan.showMouse(false);
 
 	int textEntry;
 
@@ -1532,23 +1747,22 @@ void ColonyEngine::gameOver(bool kill, int savedCryos) {
 	_gfx->copyToScreen();
 
 	if (kill) {
-		_sound->play(Sound::kPShot);
-		makeStars(_screenR, 0);
-		_sound->stop();
-		_sound->play(Sound::kExplode);
-		for (int i = 0; i < 4; i++) {
-			_gfx->clear((i & 1) ? _gfx->black() : _gfx->white());
-			_gfx->copyToScreen();
-			_system->delayMillis(50);
-		}
-	} else {
+		explodePlanet();
+		_gfx->clear(_gfx->black());
+		_gfx->copyToScreen();
+	} else if (getPlatform() == Common::kPlatformMacintosh) {
+		_sound->play(Sound::kSwish);
+	}
+	if (getPlatform() != Common::kPlatformMacintosh)
 		_sound->play(Sound::kStars4);
-		makeStars(_screenR, 0);
+	makeStars(_screenR, 0);
+	// Mac lets the ending sound finish over the text; DOS stops Stars4 here.
+	if (getPlatform() != Common::kPlatformMacintosh)
 		_sound->stop();
-	}
 
 	_gfx->clear(_gfx->black());
 	_gfx->copyToScreen();
+	CursorMan.showMouse(true);
 	doText(textEntry, 2);
 
 	auto playFinalExplosion = [&]() {
diff --git a/engines/colony/sound.cpp b/engines/colony/sound.cpp
index 85f3cc5b8ba..0356757541c 100644
--- a/engines/colony/sound.cpp
+++ b/engines/colony/sound.cpp
@@ -340,6 +340,7 @@ void Sound::playPCSpeaker(int soundID) {
 bool Sound::playMacSound(int soundID, bool loop) {
 	// Primary resource IDs from original sound.c
 	int resID = -1;
+	int sampleRate = 11127;
 	switch (soundID) {
 	case kKlaxon: resID = 27317; break;
 	case kAirlock: resID = 5141; break;
@@ -372,10 +373,14 @@ bool Sound::playMacSound(int soundID, bool loop) {
 	case kMars: resID = 23390; break;
 	case kBeamMe: resID = 5342; break;
 	case kDave: resID = 13651; break;   // DAVE (the monolith's "full of stars" clip)
+	// LoadSound(SWISH, 5) and LoadSound(END, 4): StartSound's free-form
+	// synthesizer plays at 22254 Hz divided by the requested speed.
+	case kSwish: resID = 7089; sampleRate = 22254 / 5; break;
+	case kEnd: resID = 18282; sampleRate = 22254 / 4; break;
 	default: break;
 	}
 
-	if (resID != -1 && playResource(resID, loop))
+	if (resID != -1 && playResource(resID, loop, sampleRate))
 		return true;
 
 	// Fallback resource IDs for sounds missing from this binary version.
@@ -396,7 +401,7 @@ bool Sound::playMacSound(int soundID, bool loop) {
 	return false;
 }
 
-bool Sound::playResource(int resID, bool loop) {
+bool Sound::playResource(int resID, bool loop, int sampleRate) {
 	Common::SeekableReadStream *snd = nullptr;
 
 	// Search Zounds first (has most sounds)
@@ -422,7 +427,7 @@ bool Sound::playResource(int resID, bool loop) {
 	snd->read(data, dataSize);
 	delete snd;
 
-	Audio::RewindableAudioStream *raw = Audio::makeRawStream(data, dataSize, 11127, Audio::FLAG_UNSIGNED, DisposeAfterUse::YES);
+	Audio::RewindableAudioStream *raw = Audio::makeRawStream(data, dataSize, sampleRate, Audio::FLAG_UNSIGNED, DisposeAfterUse::YES);
 	Audio::AudioStream *stream = loop ? Audio::makeLoopingAudioStream(raw, 0) : raw;
 	_vm->_mixer->playStream(Audio::Mixer::kSFXSoundType, &_handle, stream);
 	return true;
diff --git a/engines/colony/sound.h b/engines/colony/sound.h
index 60e4ec147f4..56f44fe98ba 100644
--- a/engines/colony/sound.h
+++ b/engines/colony/sound.h
@@ -82,7 +82,9 @@ public:
 		kBath,
 		kMars,
 		kBeamMe,
-		kDave
+		kDave,
+		kSwish,
+		kEnd
 	};
 
 private:
@@ -94,7 +96,7 @@ private:
 
 	void playPCSpeaker(int soundID);
 	bool playMacSound(int soundID, bool loop);
-	bool playResource(int resID, bool loop);
+	bool playResource(int resID, bool loop, int sampleRate = 11127);
 };
 
 } // End of namespace Colony


Commit: 4fa1ca2ea34e8bc7274081e82b601af29ca412ac
    https://github.com/scummvm/scummvm/commit/4fa1ca2ea34e8bc7274081e82b601af29ca412ac
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-09-25T13:34:53+02:00

Commit Message:
COLONY: adjust text scrolling speed in intro

Changed paths:
    engines/colony/intro.cpp


diff --git a/engines/colony/intro.cpp b/engines/colony/intro.cpp
index 4d7063f2993..a3692482499 100644
--- a/engines/colony/intro.cpp
+++ b/engines/colony/intro.cpp
@@ -1329,14 +1329,16 @@ bool ColonyEngine::timeSquare(const Common::String &str, const Graphics::Font *m
 	_gfx->copyToScreen();
 
 	// Phase 1: Scroll text in from the right to center.
-	// DOS uses 16-pixel blits of a black text box; Mac scrolls smoothly.
+	// Original steps are one pixel on Mac and 16 on DOS. Double the intro's
+	// distance per frame so it also scrolls twice as fast with VSync enabled.
 	int targetX = (_width - swidth) / 2;
 	const int startX = macStyle ? _width : (_width + 16);
-	const int stepX = macStyle ? 1 : 16;
+	const int stepX = (macStyle ? 1 : 16) * (gameOver ? 1 : 2);
 	const int endX = macStyle ? -swidth : (-swidth - 16);
 	const uint32 scrollDelayMs = macStyle ? 8 : (1000 / 60);
 
-	for (int x = startX; x > targetX; x -= stepX) {
+	for (int x = startX; x > targetX;) {
+		x = MAX(targetX, x - stepX);
 		_gfx->fillRect(textBand, 0);
 		_gfx->drawString(font, str, x, centery + 2, textIndex, Graphics::kTextAlignLeft);
 		_gfx->copyToScreen();




More information about the Scummvm-git-logs mailing list