[Scummvm-git-logs] scummvm master -> 8b6fb34dc8d1a35b58436c905c894839c3c00813

bluegr noreply at scummvm.org
Tue Apr 21 21:19:36 UTC 2026


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

Summary:
8b6fb34dc8 AGOS: Implement day/night palette fading for PN Amiga/Atari ST


Commit: 8b6fb34dc8d1a35b58436c905c894839c3c00813
    https://github.com/scummvm/scummvm/commit/8b6fb34dc8d1a35b58436c905c894839c3c00813
Author: Robert Megone (robert.megone at gmail.com)
Date: 2026-04-22T00:19:31+03:00

Commit Message:
AGOS: Implement day/night palette fading for PN Amiga/Atari ST

Changed paths:
    engines/agos/agos.cpp
    engines/agos/agos.h
    engines/agos/debugger.cpp
    engines/agos/debugger.h
    engines/agos/event.cpp
    engines/agos/script.cpp
    engines/agos/vga.cpp


diff --git a/engines/agos/agos.cpp b/engines/agos/agos.cpp
index b9f3d9e612e..d5b26a481af 100644
--- a/engines/agos/agos.cpp
+++ b/engines/agos/agos.cpp
@@ -515,6 +515,13 @@ AGOSEngine::AGOSEngine(OSystem *system, const AGOSGameDescription *gd)
 
 	memset(_currentPalette, 0, sizeof(_currentPalette));
 	memset(_displayPalette, 0, sizeof(_displayPalette));
+	memset(_pnPaletteBanks, 0, sizeof(_pnPaletteBanks));
+	memset(_pnFadeCurrent, 0, sizeof(_pnFadeCurrent));
+	memset(_pnHavePaletteBank, 0, sizeof(_pnHavePaletteBank));
+	_pnDayNightControllerSelectorMask = 0xFFFF;
+	_pnDayNightControllerLastStage = 0xFF;
+	_pnLastClockMinutes = -1;
+	_pnDayNightControllerTickCounter = 0x00C8;
 
 	memset(_videoBuf1, 0, sizeof(_videoBuf1));
 	memset(_videoWindows, 0, sizeof(_videoWindows));
diff --git a/engines/agos/agos.h b/engines/agos/agos.h
index f4ba808cea7..3f06b6a331b 100644
--- a/engines/agos/agos.h
+++ b/engines/agos/agos.h
@@ -220,7 +220,8 @@ enum EventType {
 	ANIMATE_EVENT = 1 << 2,
 	SCROLL_EVENT  = 1 << 3,
 	PLAYER_DAMAGE_EVENT = 1 << 4,
-	MONSTER_DAMAGE_EVENT = 1 << 5
+	MONSTER_DAMAGE_EVENT = 1 << 5,
+	PN_FADE_EVENT = 1 << 6
 };
 
 struct GameSpecificSettings;
@@ -625,6 +626,13 @@ protected:
 	uint8 _simon2LanguageFlagTimer;
 	bool _simon2LanguageFlagClearPending;
 
+	uint16 _pnPaletteBanks[2][16];
+	uint16 _pnFadeCurrent[16];
+	uint16 _pnDayNightControllerSelectorMask;
+	uint8 _pnDayNightControllerLastStage;
+	int16 _pnLastClockMinutes;
+	bool _pnHavePaletteBank[2];
+	uint16 _pnDayNightControllerTickCounter;
 	byte *_planarBuf;
 	byte _videoBuf1[32000];
 	uint16 _videoWindows[128];
@@ -1303,6 +1311,16 @@ protected:
 	void clearVideoBackGround(uint16 windowNum, uint16 color);
 
 	void setPaletteSlot(uint16 srcOffs, uint8 dstOffs);
+	bool isPNDayNightPaletteMode() const;
+	uint8 getPNDesiredPaletteBank() const;
+	void notePNClockValueChange();
+	void resetPNRoomPaletteState();
+	void buildPNPaletteTarget(uint16 selectorMask, uint16 *target) const;
+	uint16 blendPNPaletteColor(uint16 source, uint16 target, uint8 steps) const;
+	uint8 getPNDayNightControllerStage() const;
+	void startPNDayNightController(uint16 selectorMask);
+	void updatePNDayNightController(uint16 selectorMask);
+	void applyPNDayNightPalette(const uint16 *palette);
 	void checkOnStopTable();
 	void checkWaitEndTable();
 
@@ -1316,6 +1334,8 @@ protected:
 	void addVgaEvent(uint16 num, uint8 type, const byte *codePtr, uint16 curSprite, uint16 curZoneNum);
 	void deleteVgaEvent(VgaTimerEntry * vte);
 	void processVgaEvents();
+	void schedulePNFadeEvent();
+	void removePNFadeEvent();
 	void animateEvent(const byte *codePtr, uint16 curZoneNum, uint16 curSprite);
 	void scrollEvent();
 	void drawStuff(const byte *src, uint offs);
@@ -1677,6 +1697,9 @@ protected:
 	void addChar(uint8 chr);
 	void clearCursor(WindowBlock *window);
 	void clearInputLine();
+	bool tryHandleDebugTimeCommand();
+	bool tryParseDebugTimeCommand(const char *typed, uint16 &hour, uint16 &minute) const;
+	void setDebugTime(uint16 hour, uint16 minute);
 	void handleKeyboard();
 	void handleMouseMoved() override;
 	void interact(char *buffer, uint8 size);
diff --git a/engines/agos/debugger.cpp b/engines/agos/debugger.cpp
index 2edc8bb3888..5870a8ce3fe 100644
--- a/engines/agos/debugger.cpp
+++ b/engines/agos/debugger.cpp
@@ -45,6 +45,15 @@ Debugger::Debugger(AGOSEngine *vm)
 	registerCmd("dumpimage",      WRAP_METHOD(Debugger, Cmd_dumpImage));
 	registerCmd("dumpscript",     WRAP_METHOD(Debugger, Cmd_dumpScript));
 
+	if (_vm->getGameType() == GType_PN) {
+		registerCmd("gettime",      WRAP_METHOD(Debugger, Cmd_GetTime));
+
+		if (_vm->getPlatform() == Common::kPlatformAmiga || _vm->getPlatform() == Common::kPlatformAtariST) {
+			registerCmd("startday",   WRAP_METHOD(Debugger, Cmd_StartDay));
+			registerCmd("startnight", WRAP_METHOD(Debugger, Cmd_StartNight));
+		}
+	}
+
 }
 
 bool Debugger::Cmd_PlayMusic(int argc, const char **argv) {
@@ -265,4 +274,25 @@ bool Debugger::Cmd_dumpScript(int argc, const char **argv) {
 	return true;
 }
 
+
+bool Debugger::setPNDebugTime(uint16 hour, uint16 minute) {
+	_vm->writeVariable(84, hour);
+	_vm->writeVariable(85, minute);
+	debugPrintf("Set time to %02u:%02u\n", hour, minute);
+	return true;
+}
+
+bool Debugger::Cmd_GetTime(int argc, const char **argv) {
+	debugPrintf("Time is %02u:%02u\n", (uint)_vm->readVariable(84), (uint)_vm->readVariable(85));
+	return true;
+}
+
+bool Debugger::Cmd_StartDay(int argc, const char **argv) {
+	return setPNDebugTime(6, 58);
+}
+
+bool Debugger::Cmd_StartNight(int argc, const char **argv) {
+	return setPNDebugTime(17, 58);
+}
+
 } // End of namespace AGOS
diff --git a/engines/agos/debugger.h b/engines/agos/debugger.h
index 783d809bcb8..edbc9d9f994 100644
--- a/engines/agos/debugger.h
+++ b/engines/agos/debugger.h
@@ -46,6 +46,10 @@ private:
 	bool Cmd_StartSubroutine(int argc, const char **argv);
 	bool Cmd_dumpImage(int argc, const char **argv);
 	bool Cmd_dumpScript(int argc, const char **argv);
+	bool Cmd_GetTime(int argc, const char **argv);
+	bool Cmd_StartDay(int argc, const char **argv);
+	bool Cmd_StartNight(int argc, const char **argv);
+	bool setPNDebugTime(uint16 hour, uint16 minute);
 };
 
 } // End of namespace AGOS
diff --git a/engines/agos/event.cpp b/engines/agos/event.cpp
index b1b84baf9c0..785c692e2c5 100644
--- a/engines/agos/event.cpp
+++ b/engines/agos/event.cpp
@@ -230,6 +230,27 @@ void AGOSEngine::deleteVgaEvent(VgaTimerEntry * vte) {
 	_videoLockOut &= ~1;
 }
 
+void AGOSEngine::schedulePNFadeEvent() {
+	if (!isPNDayNightPaletteMode())
+		return;
+
+	for (VgaTimerEntry *vte = _vgaTimerList; vte->delay; ++vte) {
+		if (vte->type == PN_FADE_EVENT)
+			return;
+	}
+
+	addVgaEvent(_vgaBaseDelay, PN_FADE_EVENT, nullptr, 0, 0);
+}
+
+void AGOSEngine::removePNFadeEvent() {
+	for (VgaTimerEntry *vte = _vgaTimerList; vte->delay; ++vte) {
+		if (vte->type == PN_FADE_EVENT) {
+			deleteVgaEvent(vte);
+			return;
+		}
+	}
+}
+
 void AGOSEngine::processVgaEvents() {
 	VgaTimerEntry *vte = _vgaTimerList;
 
@@ -246,6 +267,8 @@ void AGOSEngine::processVgaEvents() {
 			case ANIMATE_INT:
 				vte->delay = (getGameType() == GType_SIMON2) ? 5 : _frameCount;
 				animateSprites();
+				if (isPNDayNightPaletteMode())
+					schedulePNFadeEvent();
 				vte++;
 				break;
 			case ANIMATE_EVENT:
@@ -268,6 +291,20 @@ void AGOSEngine::processVgaEvents() {
 				monsterDamageEvent(vte, curZoneNum);
 				vte = _nextVgaTimerToProcess;
 				break;
+			case PN_FADE_EVENT:
+				if (isPNDayNightPaletteMode()) {
+					if (_pnDayNightControllerTickCounter > _vgaBaseDelay) {
+						_pnDayNightControllerTickCounter -= _vgaBaseDelay;
+					} else {
+						_pnDayNightControllerTickCounter = 0x00C8;
+						updatePNDayNightController(_pnDayNightControllerSelectorMask);
+					}
+				}
+
+				_nextVgaTimerToProcess = vte + 1;
+				deleteVgaEvent(vte);
+				vte = _nextVgaTimerToProcess;
+				break;
 			default:
 				error("processVgaEvents: Unknown event type %d", vte->type);
 			}
diff --git a/engines/agos/script.cpp b/engines/agos/script.cpp
index 31bdeac3415..9d8fb351528 100644
--- a/engines/agos/script.cpp
+++ b/engines/agos/script.cpp
@@ -970,10 +970,19 @@ void AGOSEngine::writeVariable(uint16 variable, uint16 contents) {
 	if (variable >= _numVars)
 		error("writeVariable: Variable %d out of range", variable);
 
+	const bool pnDayNightMode = isPNDayNightPaletteMode();
+	const uint16 oldValue = _variableArray[variable];
+	const bool pnDayNightVar = pnDayNightMode && variable == 249 && oldValue != contents;
+
 	if (getGameType() == GType_FF && getBitFlag(83))
 		_variableArray2[variable] = contents;
 	else
 		_variableArray[variable] = contents;
+
+	if (pnDayNightVar) {
+		const uint16 selectorMask = (contents == 2) ? 0xFFFF : 0x0000;
+		startPNDayNightController(selectorMask);
+	}
 }
 
 int AGOSEngine::runScript() {
diff --git a/engines/agos/vga.cpp b/engines/agos/vga.cpp
index 7d3f96f538e..cf01a1c5e33 100644
--- a/engines/agos/vga.cpp
+++ b/engines/agos/vga.cpp
@@ -289,6 +289,9 @@ uint AGOSEngine::vcReadVar(uint var) {
 void AGOSEngine::vcWriteVar(uint var, int16 value) {
 	assert(var < _numVars);
 	_variableArrayPtr[var] = value;
+
+	if (isPNDayNightPaletteMode() && (var == 84 || var == 85))
+		notePNClockValueChange();
 }
 
 void AGOSEngine::vcSkipNextInstruction() {
@@ -388,6 +391,181 @@ void AGOSEngine::vcSkipNextInstruction() {
 	debugCN(kDebugVGAOpcode, "; skipped\n");
 }
 
+bool AGOSEngine::isPNDayNightPaletteMode() const {
+	return getGameType() == GType_PN && !(getFeatures() & GF_EGA) &&
+		(getPlatform() == Common::kPlatformAtariST || getPlatform() == Common::kPlatformAmiga);
+}
+
+uint8 AGOSEngine::getPNDesiredPaletteBank() const {
+	if (!isPNDayNightPaletteMode())
+		return 0;
+
+	if (_variableArray != nullptr && _numVars > 85) {
+		const int16 hour = _variableArray[84];
+		const int16 minute = _variableArray[85];
+		if (hour >= 0 && hour < 24 && minute >= 0 && minute < 60) {
+			if (!(_pnLastClockMinutes == -1 && hour == 0 && minute == 0))
+				return (hour >= 23 || hour < 7) ? 1 : 0;
+		}
+	}
+
+	if (_itemArrayPtr != nullptr && _itemArraySize > 289) {
+		const Item *item196 = _itemArrayPtr[196];
+		const Item *item289 = _itemArrayPtr[289];
+		const bool nightState = (item196 && item196->state == 7) || (item289 && item289->state == 7);
+		const bool dayState = (item196 && item196->state == 27) || (item289 && item289->state == 27);
+
+		if (nightState && !dayState)
+			return 1;
+		if (dayState && !nightState)
+			return 0;
+	}
+
+	return 0;
+}
+
+void AGOSEngine::notePNClockValueChange() {
+	if (!isPNDayNightPaletteMode() || _variableArray == nullptr || _numVars <= 85)
+		return;
+
+	int16 hour = _variableArray[84];
+	int16 minute = _variableArray[85];
+	if (hour < 0 || minute < 0)
+		return;
+
+	if (minute >= 60) {
+		hour += minute / 60;
+		minute %= 60;
+		_variableArray[84] = hour;
+		_variableArray[85] = minute;
+	}
+
+	if (hour >= 24) {
+		hour %= 24;
+		_variableArray[84] = hour;
+	}
+
+	const int16 clockMinutes = hour * 60 + minute;
+	if (_pnLastClockMinutes == -1) {
+		_pnLastClockMinutes = clockMinutes;
+		return;
+	}
+
+	if (clockMinutes != _pnLastClockMinutes)
+		_pnLastClockMinutes = clockMinutes;
+}
+
+void AGOSEngine::resetPNRoomPaletteState() {
+	if (!isPNDayNightPaletteMode())
+		return;
+
+	memset(_pnHavePaletteBank, 0, sizeof(_pnHavePaletteBank));
+	_pnDayNightControllerSelectorMask = 0xFFFF;
+	_pnDayNightControllerLastStage = 0xFF;
+	_pnDayNightControllerTickCounter = 0x00C8;
+	removePNFadeEvent();
+}
+
+void AGOSEngine::buildPNPaletteTarget(uint16 selectorMask, uint16 *target) const {
+	for (int i = 0; i < 16; ++i) {
+		const bool useBank1 = (selectorMask & (0x8000 >> i)) != 0;
+		if (useBank1 && _pnHavePaletteBank[1]) {
+			target[i] = _pnPaletteBanks[1][i];
+		} else if (_pnHavePaletteBank[0]) {
+			target[i] = _pnPaletteBanks[0][i];
+		} else if (_pnHavePaletteBank[1]) {
+			target[i] = _pnPaletteBanks[1][i];
+		} else {
+			target[i] = _pnFadeCurrent[i];
+		}
+	}
+}
+
+uint16 AGOSEngine::blendPNPaletteColor(uint16 source, uint16 target, uint8 steps) const {
+	uint16 current = source;
+
+	for (uint8 step = 0; step < steps; ++step) {
+		const uint16 currentRed = current & 0x0f00;
+		const uint16 targetRed = target & 0x0f00;
+		if (currentRed < targetRed)
+			current += 0x0100;
+		else if (currentRed > targetRed)
+			current -= 0x0100;
+
+		const uint16 currentGreen = current & 0x00f0;
+		const uint16 targetGreen = target & 0x00f0;
+		if (currentGreen < targetGreen)
+			current += 0x0010;
+		else if (currentGreen > targetGreen)
+			current -= 0x0010;
+
+		const uint16 currentBlue = current & 0x000f;
+		const uint16 targetBlue = target & 0x000f;
+		if (currentBlue < targetBlue)
+			current += 0x0001;
+		else if (currentBlue > targetBlue)
+			current -= 0x0001;
+	}
+
+	return current;
+}
+
+uint8 AGOSEngine::getPNDayNightControllerStage() const {
+	if (!isPNDayNightPaletteMode() || _variableArray == nullptr || _numVars <= 212)
+		return 0;
+
+	return (uint8)(_variableArray[212] & 7);
+}
+
+void AGOSEngine::startPNDayNightController(uint16 selectorMask) {
+	if (!isPNDayNightPaletteMode())
+		return;
+
+	_pnDayNightControllerSelectorMask = selectorMask;
+	_pnDayNightControllerLastStage = 0xFF;
+	_pnDayNightControllerTickCounter = 0x00C8;
+	updatePNDayNightController(selectorMask);
+	schedulePNFadeEvent();
+}
+
+void AGOSEngine::updatePNDayNightController(uint16 selectorMask) {
+	if (!isPNDayNightPaletteMode())
+		return;
+	if (!_pnHavePaletteBank[0] || !_pnHavePaletteBank[1])
+		return;
+
+	const uint8 stage = getPNDayNightControllerStage();
+	if (stage == _pnDayNightControllerLastStage)
+		return;
+
+	uint16 fadeTarget[16];
+	buildPNPaletteTarget(selectorMask, fadeTarget);
+	for (int i = 0; i < 16; ++i)
+		_pnFadeCurrent[i] = blendPNPaletteColor(_pnPaletteBanks[0][i], fadeTarget[i], stage);
+
+	_pnDayNightControllerLastStage = stage;
+	applyPNDayNightPalette(_pnFadeCurrent);
+}
+
+void AGOSEngine::applyPNDayNightPalette(const uint16 *palette) {
+	for (int i = 0; i < 16; ++i) {
+		const uint16 color = palette[i];
+		_displayPalette[i * 3 + 0] = ((color & 0x0f00) >> 8) * 32;
+		_displayPalette[i * 3 + 1] = ((color & 0x00f0) >> 4) * 32;
+		_displayPalette[i * 3 + 2] = ((color & 0x000f) >> 0) * 32;
+	}
+
+	memcpy(_currentPalette, _displayPalette, sizeof(_displayPalette));
+
+	const bool canPushImmediately = isPNDayNightPaletteMode() && _pnHavePaletteBank[0] && _pnHavePaletteBank[1] && _system->getPaletteManager();
+	if (canPushImmediately) {
+		_system->getPaletteManager()->setPalette(_displayPalette, 0, 16);
+		_paletteFlag = 0;
+	} else {
+		_paletteFlag = 1;
+	}
+}
+
 // VGA Script commands
 void AGOSEngine::vc1_fadeOut() {
 	/* dummy opcode */
@@ -443,8 +621,39 @@ void AGOSEngine::vc3_loadSprite() {
 }
 
 void AGOSEngine::vc4_fadeIn() {
-	/* dummy opcode */
-	_vcPtr += 6;
+	if (!isPNDayNightPaletteMode()) {
+		_vcPtr += 6;
+		return;
+	}
+
+	const uint16 selectorMask = vcReadNextWord();
+	vcReadNextWord();
+	const int16 fadeMode = (int16)vcReadNextWord();
+
+	const bool hasAlternateBank = _pnHavePaletteBank[1];
+	const bool isDayPalette = selectorMask == 0;
+
+	if (!hasAlternateBank) {
+		if (_pnHavePaletteBank[0]) {
+			memcpy(_pnFadeCurrent, _pnPaletteBanks[0], sizeof(_pnFadeCurrent));
+		}
+		return;
+	}
+
+	const bool shouldApply = (getPNDesiredPaletteBank() == 0) ? isDayPalette : !isDayPalette;
+
+	if (fadeMode == -1) {
+		startPNDayNightController(selectorMask);
+		return;
+	}
+
+	if (!shouldApply)
+		return;
+
+	uint16 fadeTarget[16];
+	buildPNPaletteTarget(selectorMask, fadeTarget);
+	memcpy(_pnFadeCurrent, fadeTarget, sizeof(_pnFadeCurrent));
+	applyPNDayNightPalette(_pnFadeCurrent);
 }
 
 void AGOSEngine::vc5_ifEqual() {
@@ -944,6 +1153,8 @@ void AGOSEngine::vc22_setPalette() {
 	byte *offs, *palptr, *src;
 	uint16 b, num;
 	const uint16 origB = b = vcReadNextWord();
+	uint16 pnPalette[16];
+	const bool pnDayNightPaletteMode = isPNDayNightPaletteMode();
 
 	// PC EGA version of Personal Nightmare uses standard EGA palette
 	if (getGameType() == GType_PN && (getFeatures() & GF_EGA))
@@ -954,10 +1165,14 @@ void AGOSEngine::vc22_setPalette() {
 	palptr = _displayPalette;
 	_bottomPalette = 1;
 
+	uint8 pnBank = 0;
 	if (getGameType() == GType_PN) {
+		if (origB > 128)
+			pnBank = 1;
 		if (b > 128) {
 			b -= 128;
-			palptr = _displayPalette + 3 * 16;
+			if (!pnDayNightPaletteMode)
+				palptr = _displayPalette + 3 * 16;
 		}
 	} else if (getGameType() == GType_ELVIRA1) {
 		if (b >= 1000) {
@@ -1000,22 +1215,42 @@ void AGOSEngine::vc22_setPalette() {
 	offs = _curVgaFile1 + READ_BE_UINT16(_curVgaFile1 + 6);
 	src = offs + b * 32;
 
+	uint pnPaletteIndex = 0;
 	do {
 		uint16 color = READ_BE_UINT16(src);
-		if (getGameType() == GType_ELVIRA2 && getPlatform() == Common::kPlatformAtariST) {
-			palptr[0] = atariSTColorNibbleToComponent((color & 0xf00) >> 8);
-			palptr[1] = atariSTColorNibbleToComponent((color & 0x0f0) >> 4);
-			palptr[2] = atariSTColorNibbleToComponent((color & 0x00f) >> 0);
-		} else {
-			palptr[0] = ((color & 0xf00) >> 8) * 32;
-			palptr[1] = ((color & 0x0f0) >> 4) * 32;
-			palptr[2] = ((color & 0x00f) >> 0) * 32;
+		if (pnDayNightPaletteMode)
+			pnPalette[pnPaletteIndex] = color;
+
+		if (!pnDayNightPaletteMode || pnBank == 0) {
+			if (getGameType() == GType_ELVIRA2 && getPlatform() == Common::kPlatformAtariST) {
+				palptr[0] = atariSTColorNibbleToComponent((color & 0xf00) >> 8);
+				palptr[1] = atariSTColorNibbleToComponent((color & 0x0f0) >> 4);
+				palptr[2] = atariSTColorNibbleToComponent((color & 0x00f) >> 0);
+			} else {
+				palptr[0] = ((color & 0xf00) >> 8) * 32;
+				palptr[1] = ((color & 0x0f0) >> 4) * 32;
+				palptr[2] = ((color & 0x00f) >> 0) * 32;
+			}
+			palptr += 3;
 		}
 
-		palptr += 3;
 		src += 2;
+		pnPaletteIndex++;
 	} while (--num);
 
+	if (pnDayNightPaletteMode) {
+		while (pnPaletteIndex < 16)
+			pnPalette[pnPaletteIndex++] = 0;
+
+		memcpy(_pnPaletteBanks[pnBank], pnPalette, sizeof(pnPalette));
+		_pnHavePaletteBank[pnBank] = true;
+
+		if (pnBank == 0) {
+			memcpy(_pnFadeCurrent, _pnPaletteBanks[0], sizeof(_pnFadeCurrent));
+		}
+
+	}
+
 	if (getGameType() == GType_PN && origB == 9 &&
 			(getPlatform() == Common::kPlatformAtariST || getPlatform() == Common::kPlatformAmiga)) {
 		// Workaround for Atari ST and Amiga 'time passes' screen palette.




More information about the Scummvm-git-logs mailing list