[Scummvm-git-logs] scummvm master -> 1e2b3bb0410d2c0606ecee75b72a0d74421361b6
neuromancer
noreply at scummvm.org
Fri Apr 10 19:13:58 UTC 2026
This automated email contains information about 19 new commits which have been
pushed to the 'scummvm' repo located at https://api.github.com/repos/scummvm/scummvm .
Summary:
a0547351ab COLONY: improve sounds generation for the intro in dos
d273f9f302 COLONY: improve intro text for the intro in dos
aedad94f5a COLONY: more fixes for the intro in dos
bc63b4c9c4 COLONY: make sure the compass spins in dos
9abc59008b FREESCAPE: changed AY8912Stream by YM2149Emu to improve music playblack in eclipse atari
0cb47035c0 FREESCAPE: properly render eclipse UI for atari
4294c03aa8 FREESCAPE: render water jar in eclipse atari
d7ace101f7 FREESCAPE: dark rooms implemented in eclipse atari
e566f73b5b FREESCAPE: limited lantern batery in eclipse atari
d0da715423 FREESCAPE: better handling of palettes for eclipse atari
26fe85ba26 FREESCAPE: make sure dark areas have reasonable palettes in eclipse atarist
444ca7e18f FREESCAPE: refactored palette handling and light effect in eclipse atarist
956cbac5f1 FREESCAPE: simplify brightness handling in eclipse atarist
24bcddbf11 FREESCAPE: update brightness in real time in eclipse atarist
4ae107a7a7 FREESCAPE: use correct frames for brightness UI updates in eclipse atarist
8b44c75060 FREESCAPE: support more eclipse atarist releases
adf891981d FREESCAPE: initial support for eclipse amiga releases
0f28460d4a FREESCAPE: initial support music in eclipse amiga
1e2b3bb041 FREESCAPE: decay fix in wb music module
Commit: a0547351ab0a79896971b565a444f7835d72aa24
https://github.com/scummvm/scummvm/commit/a0547351ab0a79896971b565a444f7835d72aa24
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-04-10T21:12:35+02:00
Commit Message:
COLONY: improve sounds generation for the intro in dos
Changed paths:
engines/colony/intro.cpp
engines/colony/sound.cpp
diff --git a/engines/colony/intro.cpp b/engines/colony/intro.cpp
index 9115bde96c4..6d5bcfd1ad0 100644
--- a/engines/colony/intro.cpp
+++ b/engines/colony/intro.cpp
@@ -395,28 +395,21 @@ void ColonyEngine::playIntro() {
_gfx->clear(_gfx->black());
}
- // Final crash: DOS IBM_INTR.C lines 119-131
- // Original DOS: while(!SoundDone()) { EraseRect; PaintRect; } â flashes
- // for the full Explode2 sound (~7s). On Mac the digitized sample is shorter.
+ // Final crash: DOS IBM_INTR.C lines 119-131.
+ // Keep this much shorter than the original long PC speaker strobe.
_sound->stop();
- if (getPlatform() == Common::kPlatformMacintosh) {
- // Mac: play short digitized crash sample with brief flash
- _sound->play(Sound::kExplode);
- uint32 crashStart = _system->getMillis();
- int frame = 0;
- while (!shouldQuit() && _sound->isPlaying() && _system->getMillis() - crashStart < 2000) {
- _gfx->clear(frame % 2 ? _gfx->black() : _gfx->white());
- _gfx->copyToScreen();
- _system->delayMillis(125);
- frame++;
- }
- _sound->stop();
- } else {
- // DOS: skip the harsh 7-second PC speaker explode
- _gfx->clear(_gfx->black());
+ _sound->play(Sound::kExplode);
+ const uint32 crashFlashDurationMs = 700;
+ const uint32 crashFlashStepMs = 200;
+ const uint32 crashStart = _system->getMillis();
+ int frame = 0;
+ while (!shouldQuit() && _system->getMillis() - crashStart < crashFlashDurationMs) {
+ _gfx->clear(frame % 2 ? _gfx->black() : _gfx->white());
_gfx->copyToScreen();
- _system->delayMillis(1000);
+ _system->delayMillis(crashFlashStepMs);
+ frame++;
}
+ _sound->stop();
_gfx->clear(_gfx->black());
_gfx->copyToScreen();
@@ -452,7 +445,8 @@ bool ColonyEngine::scrollInfo(const Graphics::Font *macFont) {
};
const int storyLength = ARRAYSIZE(story);
- if (getPlatform() == Common::kPlatformMacintosh)
+ bool qt = checkSkipRequested();
+ if (!qt)
_sound->play(Sound::kBeamMe);
_gfx->clear(_gfx->black());
@@ -487,7 +481,6 @@ bool ColonyEngine::scrollInfo(const Graphics::Font *macFont) {
// moves up by inc=4 each frame until text is visible at its correct position.
// We simulate by drawing text with a y-offset that starts at _height and decreases to 0.
int inc = 4;
- bool qt = false;
for (int scrollOff = _height; scrollOff > 0 && !qt; scrollOff -= inc) {
if (checkSkipRequested()) {
@@ -525,10 +518,10 @@ bool ColonyEngine::scrollInfo(const Graphics::Font *macFont) {
if (!qt) {
for (int scrollOff = 0; scrollOff > -_height && !qt; scrollOff -= inc) {
if (checkSkipRequested()) {
- qt = true;
- _sound->stop();
- break;
- }
+ qt = true;
+ _sound->stop();
+ break;
+ }
_gfx->clear(_gfx->black());
for (int i = 0; i < storyLength; i++) {
@@ -541,9 +534,9 @@ bool ColonyEngine::scrollInfo(const Graphics::Font *macFont) {
}
}
- // Original does NOT stop the sound here BeamMe continues playing
- // and intro() waits for it with while(!SoundDone()) after ScrollInfo returns.
- // Only stop if skipping (qt already stops in the modifier+click handlers above).
+ // DOS stops BeamMe here; Mac lets it continue and waits in playIntro().
+ if (!qt && !macFont)
+ _sound->stop();
_gfx->clear(_gfx->black());
_gfx->copyToScreen();
return qt;
@@ -937,6 +930,7 @@ bool ColonyEngine::timeSquare(const Common::String &str, const Graphics::Font *m
// Mac original: blue starts at 0xFFFF and decreases by 4096 per line pair
// B&W Mac: white gradient instead of blue
const bool bwMac = (macFont && !_hasMacColors);
+ const bool macStyle = (macFont != nullptr);
for (int i = 0; i < 16; i++) {
int val = 255 - i * 16; // 255, 239, 223, ... 15
if (val < 0)
@@ -972,33 +966,43 @@ bool ColonyEngine::timeSquare(const Common::String &str, const Graphics::Font *m
_system->delayMillis(8);
}
- // Phase 2: Klaxon flash â original intro.c lines 312-322:
- // EndCSound(); for 6 iterations: wait for sound, stop, play klaxon,
- // InvertRect. The klaxon is short (~200ms). Inversions happen rapidly
- // at the start of each klaxon, creating a fast strobe effect.
+ // Phase 2: Klaxon flash â original intro.c lines 312-322.
+ // DOS does 4 full klaxon cycles here; Mac uses the longer 6-flash variant.
_sound->stop();
_gfx->setXorMode(true);
- for (int i = 0; i < 6; i++) {
+ const int klaxonCount = macStyle ? 6 : 4;
+ const uint32 dosKlaxonFlashMs = 12 * 1000 / 60;
+ for (int i = 0; i < klaxonCount; i++) {
if (checkSkipRequested()) {
_gfx->setXorMode(false);
return true;
}
- _sound->play(Sound::kKlaxon);
// InvertRect(&invrt) â XOR the text band
_gfx->fillRect(Common::Rect(0, centery + 1, _width, centery + 16), 0xFFFFFFFF);
_gfx->copyToScreen();
- // Brief pause matching klaxon duration (~200ms)
- _system->delayMillis(200);
+
+ _sound->play(Sound::kKlaxon);
+ if (macStyle) {
+ // Keep the snappier Mac timing.
+ _system->delayMillis(200);
+ } else {
+ // At modern frame rates, waiting for the synthesized klaxon to end
+ // drags these warning cards out too long. Keep a short fixed flash.
+ _system->delayMillis(dosKlaxonFlashMs);
+ }
}
_gfx->setXorMode(false);
- // Wait for last klaxon to finish
- while (_sound->isPlaying() && !shouldQuit())
- _system->delayMillis(10);
+ if (macStyle) {
+ // Wait for last klaxon to finish
+ while (_sound->isPlaying() && !shouldQuit())
+ _system->delayMillis(10);
+ }
_sound->stop();
- // Phase 3: PlayMars(), scroll text off to the left
- _sound->play(Sound::kMars);
+ // Phase 3: Mac resumes Mars here; DOS scrolls out silently.
+ if (macStyle)
+ _sound->play(Sound::kMars);
for (int x = targetX; x > -swidth; x -= 2) {
_gfx->fillRect(Common::Rect(0, centery + 1, _width, centery + 16), 0);
_gfx->drawString(font, str, x, centery + 2, 176, Graphics::kTextAlignLeft);
diff --git a/engines/colony/sound.cpp b/engines/colony/sound.cpp
index 1870bf6c615..3dea5fcc281 100644
--- a/engines/colony/sound.cpp
+++ b/engines/colony/sound.cpp
@@ -34,6 +34,73 @@
namespace Colony {
+namespace {
+
+struct MelodyStep {
+ uint32 divider;
+ uint8 ticks;
+};
+
+
+void queueMelody(Audio::PCSpeaker *speaker, const MelodyStep *steps, uint count, uint repeats, uint32 tickUs) {
+ if (!speaker || !steps || !count || !repeats)
+ return;
+
+ for (uint repeat = 0; repeat < repeats; ++repeat) {
+ for (uint i = 0; i < count; ++i) {
+ const MelodyStep &step = steps[i];
+ if (step.divider == 0)
+ speaker->playQueue(Audio::PCSpeaker::kWaveFormSilence, 0, step.ticks * tickUs);
+ else
+ speaker->playQueue(Audio::PCSpeaker::kWaveFormSquare, 1193180.0f / step.divider, step.ticks * tickUs);
+ }
+ }
+}
+
+const uint32 kRestDivider = 0;
+
+// Ambient DOS intro phrases from COLDAT.ASM.
+// These are sparse note patterns with long rests; exact VSP note decoding
+// is not available here, so we map them to stable PC speaker dividers.
+static const MelodyStep kStars1Phrase[] = {
+ { 4831, 3 }, { kRestDivider, 3 }, { kRestDivider, 3 }, { kRestDivider, 3 },
+ { 4063, 3 }, { kRestDivider, 3 }, { kRestDivider, 3 }, { kRestDivider, 3 },
+ { 1811, 3 }, { kRestDivider, 3 }, { kRestDivider, 3 }, { kRestDivider, 3 },
+ { 1715, 3 }, { kRestDivider, 3 }
+};
+
+static const MelodyStep kStars2Phrase[] = {
+ { 4831, 3 }, { kRestDivider, 3 }, { kRestDivider, 3 }, { 2712, 3 },
+ { 4063, 3 }, { kRestDivider, 3 }, { kRestDivider, 3 }, { 2032, 3 },
+ { 1811, 3 }, { kRestDivider, 3 }, { kRestDivider, 3 }, { 9121, 3 },
+ { 1715, 3 }, { kRestDivider, 3 }
+};
+
+static const MelodyStep kStars3Phrase[] = {
+ { 4831, 3 }, { kRestDivider, 3 }, { 9121, 3 }, { 2712, 3 },
+ { 4063, 3 }, { kRestDivider, 3 }, { 7670, 3 }, { 2032, 3 },
+ { 1811, 3 }, { kRestDivider, 3 }, { 4560, 3 }, { 9121, 3 },
+ { 1715, 3 }, { kRestDivider, 3 }
+};
+
+static const MelodyStep kStars4Phrase[] = {
+ { 4831, 3 }, { 1524, 3 }, { 9121, 3 }, { 2712, 3 },
+ { 4063, 3 }, { 4831, 3 }, { 7670, 3 }, { 2032, 3 },
+ { 1811, 3 }, { 3044, 3 }, { 4560, 3 }, { 9121, 3 },
+ { 1715, 3 }, { 2032, 3 }
+};
+
+
+// The original DOS VSP data loops these ambience patterns until killed.
+// In ScummVM that becomes grating, so keep them long enough for the intro
+// section but do not let them drone for many seconds.
+const uint kIntroStarsRepeats = 2;
+const int kBeamMeRamp1Steps = 20;
+const int kBeamMeRamp2Steps = 20;
+const int kBeamMeRamp3Steps = 80;
+
+} // namespace
+
Sound::Sound(ColonyEngine *vm) : _vm(vm), _resMan(nullptr), _appResMan(nullptr) {
_speaker = new Audio::PCSpeaker();
_speaker->init();
@@ -231,14 +298,31 @@ void Sound::playPCSpeaker(int soundID) {
queueTick(1000, 2);
queueTick(40000, 3);
break;
+ case kBeamMe:
+ queueTick(65535, 1);
+ for (int i = 0; i < kBeamMeRamp1Steps; ++i) {
+ queueTick(65535 - i * 20, 1);
+ }
+ queueTick(65531, 1);
+ for (int i = 0; i < kBeamMeRamp2Steps; ++i) {
+ queueTick(65531 - i * 20, 1);
+ }
+ queueTick(65535, 1);
+ for (int i = 0; i < kBeamMeRamp3Steps; ++i) {
+ queueTick(65535 - i * 20, 1);
+ }
+ break;
case kStars1:
+ queueMelody(_speaker, kStars1Phrase, ARRAYSIZE(kStars1Phrase), kIntroStarsRepeats, tickUs);
+ break;
case kStars2:
+ queueMelody(_speaker, kStars2Phrase, ARRAYSIZE(kStars2Phrase), kIntroStarsRepeats, tickUs);
+ break;
case kStars3:
+ queueMelody(_speaker, kStars3Phrase, ARRAYSIZE(kStars3Phrase), kIntroStarsRepeats, tickUs);
+ break;
case kStars4:
- queueTick(4000, 2);
- queueTick(8000, 2);
- queueTick(2000, 2);
- queueTick(6000, 2);
+ queueMelody(_speaker, kStars4Phrase, ARRAYSIZE(kStars4Phrase), kIntroStarsRepeats, tickUs);
break;
case kToilet: // "Sailor's Hornpipe"
queueTick(2651, 4); // G
Commit: d273f9f302ce44c99bf3d76d4b461680e7109af7
https://github.com/scummvm/scummvm/commit/d273f9f302ce44c99bf3d76d4b461680e7109af7
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-04-10T21:12:35+02:00
Commit Message:
COLONY: improve intro text for the intro in dos
Changed paths:
engines/colony/intro.cpp
diff --git a/engines/colony/intro.cpp b/engines/colony/intro.cpp
index 6d5bcfd1ad0..c243201bcf7 100644
--- a/engines/colony/intro.cpp
+++ b/engines/colony/intro.cpp
@@ -427,7 +427,22 @@ bool ColonyEngine::scrollInfo(const Graphics::Font *macFont) {
// waits for click, then scrolls it off the top.
// Mac original: TextFont(190 = Commando); TextSize(12);
// Text blue starts at 0xFFFF and fades by -4096 per visible line.
- const char *story[] = {
+ static const char *const kDosStory[] = {
+ "Mankind has left the",
+ "cradle of earth and",
+ "is beginning to eye",
+ "the galaxy. He has",
+ "begun to colonize",
+ "distant planets but has",
+ "yet to meet any alien",
+ "life forms.",
+ "****",
+ "Until now...",
+ "****",
+ "Press any key to begin",
+ "the Adventure..."
+ };
+ static const char *const kMacStory[] = {
"",
"Mankind has left the",
"cradle of earth and",
@@ -437,13 +452,14 @@ bool ColonyEngine::scrollInfo(const Graphics::Font *macFont) {
"distant planets but has",
"yet to meet any alien",
"life forms.",
- "", // null separator in original
+ "",
"Until now...",
- "", // null separator in original
+ "",
"Click to begin",
"the Adventure..."
};
- const int storyLength = ARRAYSIZE(story);
+ const char *const *story = macFont ? kMacStory : kDosStory;
+ const int storyLength = macFont ? ARRAYSIZE(kMacStory) : ARRAYSIZE(kDosStory);
bool qt = checkSkipRequested();
if (!qt)
@@ -454,9 +470,10 @@ bool ColonyEngine::scrollInfo(const Graphics::Font *macFont) {
Graphics::DosFont dosFont;
const Graphics::Font *font = macFont ? macFont : (const Graphics::Font *)&dosFont;
+ const bool macStyle = (macFont != nullptr);
- // Original uses 19px line height, centers vertically within height
- int lineHeight = 19;
+ // Mac and DOS use different title layouts here.
+ int lineHeight = macStyle ? 19 : 15;
int totalHeight = lineHeight * storyLength;
int ht = (_height - totalHeight) / 2;
@@ -476,31 +493,38 @@ bool ColonyEngine::scrollInfo(const Graphics::Font *macFont) {
}
_gfx->setPalette(pal, 200, storyLength);
- // Phase 1: Scroll text up from below screen
- // Original: scrollRect starts at bottom (stayRect.bottom..stayRect.bottom*2),
- // moves up by inc=4 each frame until text is visible at its correct position.
- // We simulate by drawing text with a y-offset that starts at _height and decreases to 0.
- int inc = 4;
+ int inc = macStyle ? 4 : 8;
+ if (macStyle) {
+ // Mac: scroll the text in from below.
+ for (int scrollOff = _height; scrollOff > 0 && !qt; scrollOff -= inc) {
+ if (checkSkipRequested()) {
+ qt = true;
+ _sound->stop();
+ break;
+ }
- for (int scrollOff = _height; scrollOff > 0 && !qt; scrollOff -= inc) {
- if (checkSkipRequested()) {
- qt = true;
- _sound->stop();
- break;
+ _gfx->clear(_gfx->black());
+ for (int i = 0; i < storyLength; i++) {
+ int drawY = ht + lineHeight * i + scrollOff;
+ if (strlen(story[i]) > 0 && drawY >= 0 && drawY < _height)
+ _gfx->drawString(font, story[i], _width / 2, drawY, 200 + i, Graphics::kTextAlignCenter);
+ }
+ _gfx->copyToScreen();
+ _system->delayMillis(16);
}
- _gfx->clear(_gfx->black());
- for (int i = 0; i < storyLength; i++) {
- int drawY = ht + lineHeight * i + scrollOff;
- if (strlen(story[i]) > 0 && drawY >= 0 && drawY < _height)
- _gfx->drawString(font, story[i], _width / 2, drawY, 200 + i, Graphics::kTextAlignCenter);
+ // Draw final position (scrollOff = 0)
+ if (!qt) {
+ _gfx->clear(_gfx->black());
+ for (int i = 0; i < storyLength; i++) {
+ if (strlen(story[i]) > 0)
+ _gfx->drawString(font, story[i], _width / 2, ht + lineHeight * i, 200 + i, Graphics::kTextAlignCenter);
+ }
+ _gfx->copyToScreen();
}
- _gfx->copyToScreen();
- _system->delayMillis(16);
- }
-
- // Draw final position (scrollOff = 0)
- if (!qt) {
+ } else {
+ // DOS: draw the whole story immediately, then wait for input before
+ // scrolling it off the top.
_gfx->clear(_gfx->black());
for (int i = 0; i < storyLength; i++) {
if (strlen(story[i]) > 0)
Commit: aedad94f5ac7a172620ffe87a5c1d69299a2c82e
https://github.com/scummvm/scummvm/commit/aedad94f5ac7a172620ffe87a5c1d69299a2c82e
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-04-10T21:12:35+02:00
Commit Message:
COLONY: more fixes for the intro in dos
Changed paths:
engines/colony/intro.cpp
diff --git a/engines/colony/intro.cpp b/engines/colony/intro.cpp
index c243201bcf7..be70a7aff33 100644
--- a/engines/colony/intro.cpp
+++ b/engines/colony/intro.cpp
@@ -934,13 +934,9 @@ bool ColonyEngine::makePlanet() {
bool ColonyEngine::timeSquare(const Common::String &str, const Graphics::Font *macFont) {
// Original: TimeSquare() in intro.c
- // 1. Draw horizontal blue gradient lines above/below center
- // 2. Scroll red text from right to center
- // 3. Flash klaxon 6 times with inverted rect
- // 4. Play Mars again, scroll text off to the left
- //
- // Mac original: fcolor starts at (0,0,0xFFFF) and subtracts 4096 per pair of lines.
- // Text is drawn in red (0xFFFF,0,0) on black background.
+ // Mac and DOS use different presentation here. DOS is a monochrome/gray
+ // warning band with 16-pixel blits and white text; Mac uses the colorful
+ // gradient treatment.
_gfx->clear(_gfx->black());
@@ -950,44 +946,67 @@ bool ColonyEngine::timeSquare(const Common::String &str, const Graphics::Font *m
int centery = _height / 2 - 10;
- // Set up gradient palette entries (160-175) for the gradient lines
- // Mac original: blue starts at 0xFFFF and decreases by 4096 per line pair
- // B&W Mac: white gradient instead of blue
const bool bwMac = (macFont && !_hasMacColors);
const bool macStyle = (macFont != nullptr);
- for (int i = 0; i < 16; i++) {
- int val = 255 - i * 16; // 255, 239, 223, ... 15
- if (val < 0)
- val = 0;
- byte pal[3] = { (byte)(bwMac ? val : 0), (byte)(bwMac ? val : 0), (byte)val };
- _gfx->setPalette(pal, 160 + i, 1);
- }
- // Set palette entry 176 for text (red in color, white in B&W)
- {
- byte pal[3] = { 255, (byte)(bwMac ? 255 : 0), (byte)(bwMac ? 255 : 0) };
- _gfx->setPalette(pal, 176, 1);
- }
+ const uint32 grayIndex = 160;
+ const uint32 textIndex = 176;
+ const Common::Rect textBand(0, centery + 1, _width, centery + 16);
+
+ if (macStyle) {
+ // Mac original: blue starts at 0xFFFF and decreases by 4096 per line pair.
+ for (int i = 0; i < 16; i++) {
+ int val = 255 - i * 16; // 255, 239, 223, ... 15
+ if (val < 0)
+ val = 0;
+ byte pal[3] = { (byte)(bwMac ? val : 0), (byte)(bwMac ? val : 0), (byte)val };
+ _gfx->setPalette(pal, 160 + i, 1);
+ }
+
+ // Text is red in color, white in B&W.
+ {
+ byte pal[3] = { 255, (byte)(bwMac ? 255 : 0), (byte)(bwMac ? 255 : 0) };
+ _gfx->setPalette(pal, textIndex, 1);
+ }
- // Draw blue gradient lines above/below center band
- for (int i = 0; i < 16; i++) {
- _gfx->drawLine(0, centery - 2 - i * 2, _width, centery - 2 - i * 2, 160 + i);
- _gfx->drawLine(0, centery - 2 - (i * 2 + 1), _width, centery - 2 - (i * 2 + 1), 160 + i);
- _gfx->drawLine(0, centery + 16 + i * 2, _width, centery + 16 + i * 2, 160 + i);
- _gfx->drawLine(0, centery + 16 + i * 2 + 1, _width, centery + 16 + i * 2 + 1, 160 + i);
+ // Draw the blue gradient lines above/below the center band.
+ for (int i = 0; i < 16; i++) {
+ _gfx->drawLine(0, centery - 2 - i * 2, _width, centery - 2 - i * 2, 160 + i);
+ _gfx->drawLine(0, centery - 2 - (i * 2 + 1), _width, centery - 2 - (i * 2 + 1), 160 + i);
+ _gfx->drawLine(0, centery + 16 + i * 2, _width, centery + 16 + i * 2, 160 + i);
+ _gfx->drawLine(0, centery + 16 + i * 2 + 1, _width, centery + 16 + i * 2 + 1, 160 + i);
+ }
+ } else {
+ // DOS warning band: white outer lines, gray inner lines, white text.
+ const byte grayPal[3] = { 170, 170, 170 };
+ const byte whitePal[3] = { 255, 255, 255 };
+ _gfx->setPalette(grayPal, grayIndex, 1);
+ _gfx->setPalette(whitePal, textIndex, 1);
+
+ _gfx->drawLine(0, centery - 3, _width, centery - 3, textIndex);
+ _gfx->drawLine(0, centery - 2, _width, centery - 2, grayIndex);
+ _gfx->drawLine(0, centery - 1, _width, centery - 1, textIndex);
+ _gfx->drawLine(0, centery + 17, _width, centery + 17, textIndex);
+ _gfx->drawLine(0, centery + 18, _width, centery + 18, grayIndex);
+ _gfx->drawLine(0, centery + 19, _width, centery + 19, textIndex);
}
_gfx->copyToScreen();
- // Phase 1: Scroll text in from the right to center
- // Original: if(Button()) if(qt=OptionKey()) break;
+ // Phase 1: Scroll text in from the right to center.
+ // DOS uses 16-pixel blits of a black text box; Mac scrolls smoothly.
int targetX = (_width - swidth) / 2;
- for (int x = _width; x > targetX; x -= 2) {
- _gfx->fillRect(Common::Rect(0, centery + 1, _width, centery + 16), 0);
- _gfx->drawString(font, str, x, centery + 2, 176, Graphics::kTextAlignLeft);
+ const int startX = macStyle ? _width : (_width + 16);
+ const int stepX = macStyle ? 2 : 16;
+ const int endX = macStyle ? -swidth : (-swidth - 16);
+ const uint32 scrollDelayMs = macStyle ? 8 : (1000 / 60);
+
+ for (int x = startX; x > targetX; x -= stepX) {
+ _gfx->fillRect(textBand, 0);
+ _gfx->drawString(font, str, x, centery + 2, textIndex, Graphics::kTextAlignLeft);
_gfx->copyToScreen();
if (checkSkipRequested())
return true;
- _system->delayMillis(8);
+ _system->delayMillis(scrollDelayMs);
}
// Phase 2: Klaxon flash â original intro.c lines 312-322.
@@ -1003,7 +1022,7 @@ bool ColonyEngine::timeSquare(const Common::String &str, const Graphics::Font *m
}
// InvertRect(&invrt) â XOR the text band
- _gfx->fillRect(Common::Rect(0, centery + 1, _width, centery + 16), 0xFFFFFFFF);
+ _gfx->fillRect(textBand, 0xFFFFFFFF);
_gfx->copyToScreen();
_sound->play(Sound::kKlaxon);
@@ -1027,14 +1046,14 @@ bool ColonyEngine::timeSquare(const Common::String &str, const Graphics::Font *m
// Phase 3: Mac resumes Mars here; DOS scrolls out silently.
if (macStyle)
_sound->play(Sound::kMars);
- for (int x = targetX; x > -swidth; x -= 2) {
- _gfx->fillRect(Common::Rect(0, centery + 1, _width, centery + 16), 0);
- _gfx->drawString(font, str, x, centery + 2, 176, Graphics::kTextAlignLeft);
+ for (int x = targetX; x > endX; x -= stepX) {
+ _gfx->fillRect(textBand, 0);
+ _gfx->drawString(font, str, x, centery + 2, textIndex, Graphics::kTextAlignLeft);
_gfx->copyToScreen();
if (checkSkipRequested())
return true;
- _system->delayMillis(8);
+ _system->delayMillis(scrollDelayMs);
}
return false;
Commit: bc63b4c9c410ad44ab164153a4d986938ad2751f
https://github.com/scummvm/scummvm/commit/bc63b4c9c410ad44ab164153a4d986938ad2751f
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-04-10T21:12:35+02:00
Commit Message:
COLONY: make sure the compass spins in dos
Changed paths:
engines/colony/ui.cpp
diff --git a/engines/colony/ui.cpp b/engines/colony/ui.cpp
index 22fb2d60b7b..a40a589018a 100644
--- a/engines/colony/ui.cpp
+++ b/engines/colony/ui.cpp
@@ -471,8 +471,8 @@ void ColonyEngine::drawDashboardStep1() {
// RCompass(): draw compass oval
// _compassRect stores the post-shrink compOval (inner erasable area)
if (_compassRect.width() > 2 && _compassRect.height() > 2) {
- // Original draws FillOval on the pre-shrink rect, then EraseOval on the shrunk rect.
- // Pre-shrink = _compassRect expanded by 2 on each side
+ // Original DOS draws a solid black outer oval, shrinks the rect by 2px on
+ // each side, then erases the inner oval to white, leaving a black annulus.
const int cx = (_compassRect.left + _compassRect.right) >> 1;
const int cy = (_compassRect.top + _compassRect.bottom) >> 1;
const int outerRx = (_compassRect.width() + 4) >> 1;
@@ -480,16 +480,14 @@ void ColonyEngine::drawDashboardStep1() {
const int innerRx = _compassRect.width() >> 1;
const int innerRy = _compassRect.height() >> 1;
- // FillOval: filled oval (vINTWHITE background)
- _gfx->fillEllipse(cx, cy, outerRx, outerRy, 15);
- // EraseOval: clear interior (also vINTWHITE)
+ _gfx->fillEllipse(cx, cy, outerRx, outerRy, 0);
_gfx->fillEllipse(cx, cy, innerRx, innerRy, 15);
- // Frame the outer oval
- _gfx->drawEllipse(cx, cy, outerRx, outerRy, 0);
- // Compass needle: updateDashBoard() uses Me.ang
- const int ex = cx + ((_cost[_me.ang] * innerRx) >> 8);
- const int ey = cy - ((_sint[_me.ang] * innerRy) >> 8);
+ // In the current ScummVM colony controls, the rendered camera follows
+ // _me.look. Using _me.ang here leaves the DOS compass static under
+ // mouse/camera rotation even though the scene is turning.
+ const int ex = cx + ((_cost[_me.look] * _compassRect.width()) >> 8);
+ const int ey = cy - ((_sint[_me.look] * _compassRect.height()) >> 8);
_gfx->drawLine(cx, cy, ex, ey, 0); // vBLACK needle
}
@@ -741,8 +739,9 @@ void ColonyEngine::drawMiniMap(uint32 lineColor) {
ccenterx = (_headsUpRect.left + _headsUpRect.right) >> 1;
ccentery = (_headsUpRect.top + _headsUpRect.bottom) >> 1;
}
- const int tsin = _sint[_me.look];
- const int tcos = _cost[_me.look];
+ const uint8 mapAngle = _me.look;
+ const int tsin = _sint[mapAngle];
+ const int tcos = _cost[mapAngle];
int xcorner[6];
int ycorner[6];
@@ -962,8 +961,9 @@ void ColonyEngine::drawAutomap() {
const int yloc = (lExt * ((_me.yindex << 8) - _me.yloc)) >> 8;
const int ccx = (vp.left + vp.right) >> 1;
const int ccy = (vp.top + vp.bottom) >> 1;
- const int tsin = _sint[_me.look];
- const int tcos = _cost[_me.look];
+ const uint8 mapAngle = _me.look;
+ const int tsin = _sint[mapAngle];
+ const int tcos = _cost[mapAngle];
const uint32 lineColor = 0;
const int radius = (int)(sqrtf((float)(vpW * vpW + vpH * vpH)) / (2.0f * lExt)) + 2;
Commit: 9abc59008b26d32ba9c792c6a15b82d2c2e850f5
https://github.com/scummvm/scummvm/commit/9abc59008b26d32ba9c792c6a15b82d2c2e850f5
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-04-10T21:12:35+02:00
Commit Message:
FREESCAPE: changed AY8912Stream by YM2149Emu to improve music playblack in eclipse atari
Changed paths:
audio/softsynth/ym2149.h
engines/freescape/games/eclipse/atari.music.cpp
diff --git a/audio/softsynth/ym2149.h b/audio/softsynth/ym2149.h
index 086d5179e74..953466c7dbb 100644
--- a/audio/softsynth/ym2149.h
+++ b/audio/softsynth/ym2149.h
@@ -26,7 +26,7 @@
namespace Audio {
-class YM2149Emu final : public YM2149::YM2149, public EmulatedChip {
+class YM2149Emu : public YM2149::YM2149, public EmulatedChip {
public:
YM2149Emu();
~YM2149Emu() override;
diff --git a/engines/freescape/games/eclipse/atari.music.cpp b/engines/freescape/games/eclipse/atari.music.cpp
index e4760ce4138..29a056f5991 100644
--- a/engines/freescape/games/eclipse/atari.music.cpp
+++ b/engines/freescape/games/eclipse/atari.music.cpp
@@ -35,7 +35,7 @@
* $0DCC Pattern pointer table (up to 31 x uint32 BE)
*/
-#include "audio/softsynth/ay8912.h"
+#include "audio/softsynth/ym2149.h"
#include "freescape/freescape.h"
#include "freescape/wb.h"
@@ -58,7 +58,7 @@ static const int kTENumPeriods = 96;
static const int kTENumInstruments = 12;
static const int kTEMaxPatterns = 31;
-class EclipseAtariMusicStream : public Audio::AY8912Stream {
+class EclipseAtariMusicStream : public Audio::YM2149Emu {
public:
EclipseAtariMusicStream(const byte *data, uint32 dataSize, int songNum, int rate = 44100);
~EclipseAtariMusicStream() override {}
@@ -66,6 +66,7 @@ public:
int readBuffer(int16 *buffer, const int numSamples) override;
bool endOfData() const override { return !_musicActive; }
bool endOfStream() const override { return !_musicActive; }
+ Audio::AudioStream *toAudioStream() { return this; }
private:
// --- Data tables ---
@@ -194,6 +195,7 @@ private:
void buildArpeggioTable(ChannelState &c, byte mask);
void tickUpdate();
void writeYMRegisters();
+ void setReg(int reg, byte value) { writeReg(reg, value); }
uint16 getPeriod(int note) const {
if (note < 0 || note >= kTENumPeriods)
@@ -225,9 +227,8 @@ private:
// ---------------------------------------------------------------------------
EclipseAtariMusicStream::EclipseAtariMusicStream(const byte *data, uint32 dataSize,
- int songNum, int rate)
- : AY8912Stream(rate, 2000000), // YM2149 at 2 MHz on Atari ST
- _data(data), _dataSize(dataSize),
+ int songNum, int)
+ : _data(data), _dataSize(dataSize),
_musicActive(false), _tickSpeed(6), _tickCounter(0),
_tickSampleCount(0), _hwEnvelopeDirty(false), _hwEnvelopePeriod(0), _hwEnvelopeShape(0),
_numPatterns(0) {
@@ -239,11 +240,7 @@ EclipseAtariMusicStream::EclipseAtariMusicStream(const byte *data, uint32 dataSi
memset(_arpeggioIntervals, 0, sizeof(_arpeggioIntervals));
memset(_channels, 0, sizeof(_channels));
- // Reset all YM registers
- for (int r = 0; r < 14; r++)
- setReg(r, 0);
- // Mixer: all channels disabled initially
- setReg(7, 0x3F);
+ init();
loadTables();
startSong(songNum);
@@ -1035,8 +1032,7 @@ int EclipseAtariMusicStream::readBuffer(int16 *buffer, const int numSamples) {
return 0;
int samplesGenerated = 0;
- // AY8912Stream is stereo: 2 int16 values per frame
- int samplesPerTick = (getRate() / 50) * 2;
+ int samplesPerTick = MAX(1, getRate() / 50);
while (samplesGenerated < numSamples && _musicActive) {
int remaining = samplesPerTick - _tickSampleCount;
Commit: 0cb47035c02a4954646014642d7ea5b119ea8978
https://github.com/scummvm/scummvm/commit/0cb47035c02a4954646014642d7ea5b119ea8978
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-04-10T21:12:35+02:00
Commit Message:
FREESCAPE: properly render eclipse UI for atari
Changed paths:
engines/freescape/freescape.cpp
engines/freescape/freescape.h
engines/freescape/games/eclipse/atari.cpp
engines/freescape/games/eclipse/eclipse.cpp
engines/freescape/games/eclipse/eclipse.h
diff --git a/engines/freescape/freescape.cpp b/engines/freescape/freescape.cpp
index d6cf35df8f1..fd02470e18b 100644
--- a/engines/freescape/freescape.cpp
+++ b/engines/freescape/freescape.cpp
@@ -1148,6 +1148,7 @@ void FreescapeEngine::rotate(float xoffset, float yoffset, float zoffset) {
if (_roll < 0.0f)
_roll += 360.0f;
+ onRotate(xoffset, yoffset, zoffset);
updateCamera();
}
diff --git a/engines/freescape/freescape.h b/engines/freescape/freescape.h
index e51774ec742..f35e0d1501b 100644
--- a/engines/freescape/freescape.h
+++ b/engines/freescape/freescape.h
@@ -427,6 +427,7 @@ public:
Math::Vector3d directionToVector(float pitch, float heading, bool useTable);
void updateCamera();
+ virtual void onRotate(float xoffset, float yoffset, float zoffset) {}
// Camera options
Common::Point _crossairPosition;
diff --git a/engines/freescape/games/eclipse/atari.cpp b/engines/freescape/games/eclipse/atari.cpp
index 376a2ec865a..b7dccfb08d8 100644
--- a/engines/freescape/games/eclipse/atari.cpp
+++ b/engines/freescape/games/eclipse/atari.cpp
@@ -29,6 +29,23 @@
namespace Freescape {
+const int kAtariCompassPhaseCount = 72;
+const int kAtariCompassBaseFrames = 19;
+const int kAtariCompassTotalFrames = 37;
+const int kAtariClockCenterX = 106;
+const int kAtariClockCenterY = 159;
+const int kAtariCompassX = 176;
+const int kAtariCompassY = 151;
+
+// Repaired ST phase-to-frame table from $1542. Six corrupt bytes in the dumped
+// binary break the intended 37-frame sweep built from $20B36 and $22B46.
+const int8 kAtariCompassPhaseToFrame[kAtariCompassPhaseCount] = {
+ 0, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35,
+ 36, 35, 34, 33, 32, 31, 30, 29, 28, 27, 26, 25, 24, 23, 22, 21, 20, 19,
+ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 17, 16,
+ 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1
+};
+
void EclipseEngine::initAmigaAtari() {
_viewArea = Common::Rect(32, 16, 288, 118);
@@ -231,7 +248,162 @@ static Graphics::ManagedSurface *loadAtariSTRawSprite(Common::SeekableReadStream
return surface;
}
-static Graphics::ManagedSurface *loadAtariSTSprite(Common::SeekableReadStream *stream,
+float atariCompassAbs(float value) {
+ return value < 0.0f ? -value : value;
+}
+
+int atariCompassRoundToNearestInt(float value) {
+ return value >= 0.0f ? (int)(value + 0.5f) : (int)(value - 0.5f);
+}
+
+int wrapAtariCompassPhase(int phase) {
+ phase %= kAtariCompassPhaseCount;
+ if (phase < 0)
+ phase += kAtariCompassPhaseCount;
+ return phase;
+}
+
+float normalizeAtariCompassYaw(float yaw) {
+ while (yaw < 0.0f)
+ yaw += 360.0f;
+ while (yaw >= 360.0f)
+ yaw -= 360.0f;
+ return yaw;
+}
+
+int atariCompassPhaseToYaw(int phase) {
+ int rotationY = wrapAtariCompassPhase(phase) * 5;
+ int yaw = 0;
+ if (rotationY < 90) {
+ yaw = 90 - rotationY;
+ } else if (rotationY <= 180) {
+ yaw = 450 - rotationY;
+ } else if (rotationY <= 225) {
+ yaw = rotationY;
+ } else if (rotationY < 270) {
+ yaw = rotationY - 90;
+ } else {
+ yaw = 450 - rotationY;
+ }
+
+ while (yaw < 0)
+ yaw += 360;
+ while (yaw >= 360)
+ yaw -= 360;
+ return yaw;
+}
+
+float atariCompassAngularDistance(float a, float b) {
+ float diff = atariCompassAbs(a - b);
+ if (diff > 180.0f)
+ diff = 360.0f - diff;
+ return diff;
+}
+
+int stepAtariCompassPhaseTowardTarget(int currentPhase, int targetPhase) {
+ if (currentPhase == targetPhase)
+ return 0;
+
+ int forwardDistance = wrapAtariCompassPhase(targetPhase - currentPhase);
+ int backwardDistance = wrapAtariCompassPhase(currentPhase - targetPhase);
+ return (forwardDistance < backwardDistance) ? 1 : -1;
+}
+
+bool atariCompassPhaseUsesVerticalFlip(int phase) {
+ int wrappedPhase = wrapAtariCompassPhase(phase);
+ return wrappedPhase < 18 || wrappedPhase > 54;
+}
+
+void atariCompassRoxl(uint32 &value, uint32 &extend) {
+ uint32 nextExtend = (value >> 31) & 1;
+ value = ((value << 1) | extend) & 0xFFFFFFFF;
+ extend = nextExtend;
+}
+
+void atariCompassRoxr(uint32 &value, uint32 &extend) {
+ uint32 nextExtend = value & 1;
+ value = ((extend << 31) | (value >> 1)) & 0xFFFFFFFF;
+ extend = nextExtend;
+}
+
+void decodeAtariSTNeedleRow(Graphics::ManagedSurface *surface, int row, const uint16 *rowWords,
+ const Graphics::PixelFormat &pixelFormat) {
+ for (int half = 0; half < 2; half++) {
+ int offset = half * 4;
+ for (int bit = 15; bit >= 0; bit--) {
+ byte colorIndex = ((rowWords[offset + 0] >> bit) & 1)
+ | (((rowWords[offset + 1] >> bit) & 1) << 1)
+ | (((rowWords[offset + 2] >> bit) & 1) << 2)
+ | (((rowWords[offset + 3] >> bit) & 1) << 3);
+ if (colorIndex == 0)
+ continue;
+
+ int x = half * 16 + (15 - bit);
+ uint32 color = pixelFormat.ARGBToColor(0xFF,
+ kBorderPalette[colorIndex * 3],
+ kBorderPalette[colorIndex * 3 + 1],
+ kBorderPalette[colorIndex * 3 + 2]);
+ surface->setPixel(x, row, color);
+ }
+ }
+}
+
+Graphics::ManagedSurface *loadAtariSTNeedleSprite(Common::SeekableReadStream *stream,
+ int pixelOffset, const Graphics::PixelFormat &pixelFormat) {
+ stream->seek(pixelOffset);
+
+ uint32 transparent = pixelFormat.ARGBToColor(0x00, 0, 0, 0);
+ Graphics::ManagedSurface *surface = new Graphics::ManagedSurface();
+ surface->create(32, 27, pixelFormat);
+ surface->fillRect(Common::Rect(0, 0, surface->w, surface->h), transparent);
+
+ for (int row = 0; row < 27; row++) {
+ uint16 sourceWords[8];
+ for (int word = 0; word < 8; word++)
+ sourceWords[word] = stream->readUint16BE();
+ decodeAtariSTNeedleRow(surface, row, sourceWords, pixelFormat);
+ }
+
+ return surface;
+}
+
+void buildAtariSTCompassMirrorCache(Common::SeekableReadStream *stream, int pixelOffset,
+ Common::Array<Graphics::ManagedSurface *> &sprites, const Graphics::PixelFormat &pixelFormat) {
+ stream->seek(pixelOffset + 432);
+
+ uint32 extend = 0;
+ uint32 transformed = 0;
+
+ for (int frame = 1; frame < kAtariCompassBaseFrames; frame++) {
+ uint32 transparent = pixelFormat.ARGBToColor(0x00, 0, 0, 0);
+ Graphics::ManagedSurface *surface = new Graphics::ManagedSurface();
+ surface->create(32, 27, pixelFormat);
+ surface->fillRect(Common::Rect(0, 0, surface->w, surface->h), transparent);
+
+ for (int row = 0; row < 27; row++) {
+ uint16 sourceWords[8];
+ uint16 rowWords[8];
+ for (int word = 0; word < 8; word++)
+ sourceWords[word] = stream->readUint16BE();
+
+ for (int plane = 0; plane < 4; plane++) {
+ uint32 value = ((uint32)sourceWords[plane] << 16) | sourceWords[plane + 4];
+ for (int bit = 0; bit <= 0x20; bit++) {
+ atariCompassRoxl(value, extend);
+ atariCompassRoxr(transformed, extend);
+ }
+ rowWords[plane] = transformed >> 16;
+ rowWords[plane + 4] = transformed & 0xFFFF;
+ }
+
+ decodeAtariSTNeedleRow(surface, row, rowWords, pixelFormat);
+ }
+
+ sprites[18 + frame] = surface;
+ }
+}
+
+Graphics::ManagedSurface *loadAtariSTSprite(Common::SeekableReadStream *stream,
int maskOffset, int pixelOffset, int cols, int rows) {
// Read per-column mask (1 word per column, same for all rows)
stream->seek(maskOffset);
@@ -262,6 +434,44 @@ static Graphics::ManagedSurface *loadAtariSTSprite(Common::SeekableReadStream *s
return surface;
}
+void drawAtariCompassNeedle(Graphics::Surface *surface, const Graphics::ManagedSurface *needle,
+ int x, int y, bool flipVertically, uint32 transparent) {
+ for (int destY = 0; destY < needle->h; destY++) {
+ int srcY = flipVertically ? (needle->h - 1 - destY) : destY;
+ for (int srcX = 0; srcX < needle->w; srcX++) {
+ uint32 pixel = needle->getPixel(srcX, srcY);
+ if (pixel != transparent)
+ surface->setPixel(x + srcX, y + destY, pixel);
+ }
+ }
+}
+
+int EclipseEngine::atariCompassPhaseFromRotationY(float rotationY) const {
+ return wrapAtariCompassPhase(atariCompassRoundToNearestInt(rotationY / 5.0f));
+}
+
+int EclipseEngine::atariCompassTargetPhaseFromYaw(float yaw, int referencePhase) const {
+ float normalizedYaw = normalizeAtariCompassYaw(yaw);
+ int wrappedReferencePhase = wrapAtariCompassPhase(referencePhase);
+ int bestPhase = wrappedReferencePhase;
+ float bestYawError = 361.0f;
+ int bestPhaseDistance = kAtariCompassPhaseCount + 1;
+
+ for (int phase = 0; phase < kAtariCompassPhaseCount; phase++) {
+ float yawError = atariCompassAngularDistance(normalizedYaw, (float)atariCompassPhaseToYaw(phase));
+ int phaseDistance = MIN(wrapAtariCompassPhase(phase - wrappedReferencePhase),
+ wrapAtariCompassPhase(wrappedReferencePhase - phase));
+ if (yawError + 0.001f < bestYawError ||
+ (atariCompassAbs(yawError - bestYawError) <= 0.001f && phaseDistance < bestPhaseDistance)) {
+ bestPhase = phase;
+ bestYawError = yawError;
+ bestPhaseDistance = phaseDistance;
+ }
+ }
+
+ return bestPhase;
+}
+
void EclipseEngine::drawAmigaAtariSTUI(Graphics::Surface *surface) {
// Border palette colors for the 4-plane font (from CONSOLE.NEO).
// The Atari ST uses raster interrupts to switch palettes between
@@ -367,15 +577,34 @@ void EclipseEngine::drawAmigaAtariSTUI(Graphics::Surface *surface) {
}
}
- // Compass at x=$B0(176), y=$97(151) â from sprite header at prog $2097C/$2097E
- if (_compassSprites.size() >= 37) {
- // Normalize yaw to 0-359 first (C++ modulo can return negative), then map to table index
- int deg = ((int)_yaw % 360 + 360) % 360;
- int lookupIdx = deg / 5;
- int needleFrame = _compassLookup[lookupIdx];
- if (needleFrame < (int)_compassSprites.size()) {
- surface->copyRectToSurface(*_compassSprites[needleFrame], 176, 151,
- Common::Rect(_compassSprites[needleFrame]->w, _compassSprites[needleFrame]->h));
+ // Compass at x=$B0(176), y=$97(151): restore the ST background, then overlay
+ // the animated needle.
+ if (_compassBackground) {
+ surface->copyRectToSurface(*_compassBackground, kAtariCompassX, kAtariCompassY,
+ Common::Rect(_compassBackground->w, _compassBackground->h));
+ }
+
+ if (_compassSprites.size() >= kAtariCompassTotalFrames) {
+ uint32 transparent = _gfx->_texturePixelFormat.ARGBToColor(0x00, 0, 0, 0);
+ if (!_atariCompassPhaseInitialized) {
+ int targetPhase = atariCompassTargetPhaseFromYaw(_yaw, 0);
+ _atariCompassPhase = targetPhase;
+ _atariCompassTargetPhase = targetPhase;
+ _atariCompassLastUpdateTick = _ticks;
+ _atariCompassPhaseInitialized = true;
+ }
+
+ if (_atariCompassLastUpdateTick != _ticks) {
+ _atariCompassPhase = wrapAtariCompassPhase(_atariCompassPhase +
+ stepAtariCompassPhaseTowardTarget(_atariCompassPhase, _atariCompassTargetPhase));
+ _atariCompassLastUpdateTick = _ticks;
+ }
+
+ int wrappedPhase = wrapAtariCompassPhase(_atariCompassPhase);
+ int needleFrame = _compassLookup[wrappedPhase];
+ if (needleFrame >= 0 && needleFrame < (int)_compassSprites.size()) {
+ drawAtariCompassNeedle(surface, _compassSprites[needleFrame], kAtariCompassX, kAtariCompassY,
+ atariCompassPhaseUsesVerticalFlip(wrappedPhase), transparent);
}
}
@@ -399,7 +628,8 @@ void EclipseEngine::drawAmigaAtariSTUI(Graphics::Surface *surface) {
Common::Rect(_shootSprites[shootFrame]->w, _shootSprites[shootFrame]->h));
}
- // Analog clock â kept from existing implementation
+ // The Atari border art uses a different clock pivot than the CPC-style UI.
+ // These coordinates are the center of the bezel drawn in CONSOLE.NEO.
uint8 r, g, b;
uint32 color = _currentArea->_underFireBackgroundColor;
_gfx->readFromPalette(color, r, g, b);
@@ -415,7 +645,7 @@ void EclipseEngine::drawAmigaAtariSTUI(Graphics::Surface *surface) {
_gfx->readFromPalette(color, r, g, b);
uint32 other = _gfx->_texturePixelFormat.ARGBToColor(0xFF, r, g, b);
- drawAnalogClock(surface, 90, 172, back, other, front);
+ drawAnalogClock(surface, kAtariClockCenterX, kAtariClockCenterY, back, other, front);
}
void EclipseEngine::loadAssetsAtariFullGame() {
@@ -483,47 +713,21 @@ void EclipseEngine::loadAssetsAtariFullGame() {
const_cast<byte *>(kBorderPalette), 16);
}
- // Compass background at prog $20986 (32x27, raw 4-plane) and needle at prog $20B36
- // (37 frames, 32x27 each, stride 432 bytes). Pre-composite background + needle.
+ // Compass background at $20986 and needle bank at $20B36.
+ _compassBackground = loadAtariSTRawSprite(stream, 0x20986 + kHdr, 2, 27);
+ _compassBackground->convertToInPlace(_gfx->_texturePixelFormat,
+ const_cast<byte *>(kBorderPalette), 16);
+
+ // The ST init code expands frames 1-18 into the second 18-frame bank in RAM.
{
- Graphics::ManagedSurface *compassBG = loadAtariSTRawSprite(stream, 0x20986 + kHdr, 2, 27);
-
- // Load compass direction lookup table (72 entries at prog $1542)
- stream->seek(0x1542 + kHdr);
- stream->read(_compassLookup, 72);
-
- // Find max needle frame index
- int maxFrame = 0;
- for (int i = 0; i < 72; i++)
- if (_compassLookup[i] < 200 && _compassLookup[i] > maxFrame)
- maxFrame = _compassLookup[i];
-
- int numFrames = maxFrame + 1;
- _compassSprites.resize(numFrames);
- for (int f = 0; f < numFrames; f++) {
- // Load needle frame (raw 4-plane, no mask)
- Graphics::ManagedSurface *needle = loadAtariSTRawSprite(stream,
- 0x20B36 + kHdr + f * 432, 2, 27);
-
- // Composite: copy background, then overlay needle where non-zero
- Graphics::ManagedSurface *composite = new Graphics::ManagedSurface();
- composite->create(32, 27, Graphics::PixelFormat::createFormatCLUT8());
- composite->copyFrom(*compassBG);
- for (int y = 0; y < 27; y++) {
- for (int x = 0; x < 32; x++) {
- byte needlePixel = *(const byte *)needle->getBasePtr(x, y);
- if (needlePixel != 0)
- composite->setPixel(x, y, needlePixel);
- }
- }
- delete needle;
+ Common::copy(kAtariCompassPhaseToFrame, kAtariCompassPhaseToFrame + kAtariCompassPhaseCount, _compassLookup);
- // Convert to target format
- composite->convertToInPlace(_gfx->_texturePixelFormat,
- const_cast<byte *>(kBorderPalette), 16);
- _compassSprites[f] = composite;
+ _compassSprites.resize(kAtariCompassTotalFrames);
+ for (int frame = 0; frame < kAtariCompassBaseFrames; frame++) {
+ _compassSprites[frame] = loadAtariSTNeedleSprite(stream,
+ 0x20B36 + kHdr + frame * 432, _gfx->_texturePixelFormat);
}
- delete compassBG;
+ buildAtariSTCompassMirrorCache(stream, 0x20B36 + kHdr, _compassSprites, _gfx->_texturePixelFormat);
}
// Lantern light animation: 6 frames, 32x6, at prog $2026A, stride 96 bytes
diff --git a/engines/freescape/games/eclipse/eclipse.cpp b/engines/freescape/games/eclipse/eclipse.cpp
index a219a81f5ca..4389ce7d4a8 100644
--- a/engines/freescape/games/eclipse/eclipse.cpp
+++ b/engines/freescape/games/eclipse/eclipse.cpp
@@ -35,6 +35,7 @@
#include "freescape/games/eclipse/ay.music.h"
#include "freescape/games/eclipse/opl.music.h"
#include "freescape/games/eclipse/eclipse.h"
+#include "freescape/objects/entrance.h"
#include "freescape/language/8bitDetokeniser.h"
namespace Freescape {
@@ -105,6 +106,12 @@ EclipseEngine::EclipseEngine(OSystem *syst, const ADGameDescription *gd) : Frees
_lastHeartbeatSoundTick = -1;
_lastHeartIndicatorFrame = 1;
_lastSecond = -1;
+ _compassBackground = nullptr;
+ _atariCompassPhase = 0;
+ _atariCompassTargetPhase = 0;
+ _atariCompassTargetRemainder = 0.0f;
+ _atariCompassLastUpdateTick = -1;
+ _atariCompassPhaseInitialized = false;
_resting = false;
_flashlightOn = false;
}
@@ -143,6 +150,10 @@ void EclipseEngine::restartBackgroundMusic() {
EclipseEngine::~EclipseEngine() {
stopBackgroundMusic();
+ if (_compassBackground) {
+ _compassBackground->free();
+ delete _compassBackground;
+ }
delete _playerOPLMusic;
delete _playerAYMusic;
delete _playerC64Music;
@@ -163,6 +174,11 @@ void EclipseEngine::initGameState() {
_lastFiveSeconds = seconds / 5;
_lastHeartbeatSoundTick = -1;
_lastHeartIndicatorFrame = 1;
+ _atariCompassPhase = 0;
+ _atariCompassTargetPhase = 0;
+ _atariCompassTargetRemainder = 0.0f;
+ _atariCompassLastUpdateTick = -1;
+ _atariCompassPhaseInitialized = false;
_resting = false;
_flashlightOn = false;
restartBackgroundMusic();
@@ -420,6 +436,18 @@ void EclipseEngine::gotoArea(uint16 areaID, int entranceID) {
if (isAmiga() || isAtariST())
_currentArea->_skyColor = 15;
+ if (isAtariST() && entranceID > 0) {
+ Entrance *entrance = (Entrance *)_currentArea->entranceWithID(entranceID);
+ if (entrance) {
+ int phase = atariCompassPhaseFromRotationY(entrance->getRotation().y());
+ _atariCompassPhase = phase;
+ _atariCompassTargetPhase = phase;
+ _atariCompassTargetRemainder = 0.0f;
+ _atariCompassLastUpdateTick = _ticks;
+ _atariCompassPhaseInitialized = true;
+ }
+ }
+
// Start background music (Atari ST)
if (isAtariST() && !_musicData.empty() && !_mixer->isSoundHandleActive(_musicHandle)) {
Audio::AudioStream *musicStream = makeEclipseAtariMusicStream(
@@ -627,6 +655,58 @@ void EclipseEngine::pressedKey(const int keycode) {
}
}
+void EclipseEngine::onRotate(float xoffset, float yoffset, float zoffset) {
+ (void)yoffset;
+ (void)zoffset;
+
+ if (!isAtariST() || xoffset == 0.0f)
+ return;
+
+ if (!_atariCompassPhaseInitialized) {
+ int phase = atariCompassTargetPhaseFromYaw(_yaw, 0);
+ _atariCompassPhase = phase;
+ _atariCompassTargetPhase = phase;
+ _atariCompassTargetRemainder = 0.0f;
+ _atariCompassLastUpdateTick = _ticks;
+ _atariCompassPhaseInitialized = true;
+ }
+
+ _atariCompassTargetRemainder += xoffset / 5.0f;
+ int phaseDelta = 0;
+ while (_atariCompassTargetRemainder >= 1.0f) {
+ _atariCompassTargetRemainder -= 1.0f;
+ phaseDelta++;
+ }
+ while (_atariCompassTargetRemainder <= -1.0f) {
+ _atariCompassTargetRemainder += 1.0f;
+ phaseDelta--;
+ }
+
+ if (phaseDelta == 0)
+ return;
+
+ _atariCompassTargetPhase = _atariCompassTargetPhase + phaseDelta;
+
+ // The original ST draw routine at $1CC0 always moves by the shortest path
+ // to a stored target phase. Clamp the target so that queued ScummVM input
+ // cannot place it more than half a turn away, which would otherwise trigger
+ // a wraparound reversal the original transition-driven code never presents.
+ while (_atariCompassTargetPhase < 0)
+ _atariCompassTargetPhase += 72;
+ while (_atariCompassTargetPhase >= 72)
+ _atariCompassTargetPhase -= 72;
+
+ if (phaseDelta > 0) {
+ int forwardDistance = (_atariCompassTargetPhase - _atariCompassPhase + 72) % 72;
+ if (forwardDistance > 35)
+ _atariCompassTargetPhase = (_atariCompassPhase + 35) % 72;
+ } else {
+ int backwardDistance = (_atariCompassPhase - _atariCompassTargetPhase + 72) % 72;
+ if (backwardDistance > 35)
+ _atariCompassTargetPhase = (_atariCompassPhase + 72 - 35) % 72;
+ }
+}
+
bool EclipseEngine::onScreenControls(Common::Point mouse) {
if (!isAmiga() && !isAtariST())
return false;
@@ -645,8 +725,7 @@ bool EclipseEngine::onScreenControls(Common::Point mouse) {
rotate(5, 0, 0);
return true;
} else if (_uTurnArea.contains(mouse)) {
- _yaw += 180;
- updateCamera();
+ rotate(180, 0, 0);
return true;
} else if (_faceForwardArea.contains(mouse)) {
pressedKey(kActionFaceForward);
@@ -1091,6 +1170,11 @@ Common::Error EclipseEngine::saveGameStreamExtended(Common::WriteStream *stream,
Common::Error EclipseEngine::loadGameStreamExtended(Common::SeekableReadStream *stream) {
_lastHeartbeatSoundTick = -1;
_lastHeartIndicatorFrame = 1;
+ _atariCompassPhase = 0;
+ _atariCompassTargetPhase = 0;
+ _atariCompassTargetRemainder = 0.0f;
+ _atariCompassLastUpdateTick = -1;
+ _atariCompassPhaseInitialized = false;
return Common::kNoError;
}
diff --git a/engines/freescape/games/eclipse/eclipse.h b/engines/freescape/games/eclipse/eclipse.h
index 9ba254de665..d82e155ae97 100644
--- a/engines/freescape/games/eclipse/eclipse.h
+++ b/engines/freescape/games/eclipse/eclipse.h
@@ -101,6 +101,9 @@ public:
void drawAnalogClock(Graphics::Surface *surface, int x, int y, uint32 colorHand1, uint32 colorHand2, uint32 colorBack);
void drawAnalogClockHand(Graphics::Surface *surface, int x, int y, double degrees, double magnitude, uint32 color);
void drawCompass(Graphics::Surface *surface, int x, int y, double degrees, double magnitude, uint32 color);
+ int atariCompassPhaseFromRotationY(float rotationY) const;
+ int atariCompassTargetPhaseFromYaw(float yaw, int referencePhase) const;
+ void onRotate(float xoffset, float yoffset, float zoffset) override;
void drawEclipseIndicator(Graphics::Surface *surface, int x, int y, uint32 color1, uint32 color2, uint32 color3 = 0);
Common::String getScoreString(int score);
void drawScoreString(int score, int x, int y, uint32 front, uint32 back, Graphics::Surface *surface);
@@ -132,14 +135,20 @@ public:
Font _fontScore; // Font B (10 score digit glyphs, 4-plane at $249BE)
Common::Array<Graphics::ManagedSurface *> _eclipseSprites; // 2 eclipse animation frames (16x13)
Common::Array<Graphics::ManagedSurface *> _eclipseProgressSprites; // 16 eclipse animation frames (16x16)
- Common::Array<Graphics::ManagedSurface *> _compassSprites; // 37 pre-composited compass frames (32x27)
+ Graphics::ManagedSurface *_compassBackground; // Atari ST compass background at $20986
+ Common::Array<Graphics::ManagedSurface *> _compassSprites; // signed Atari compass needle bank addressed by the $1542 lookup table
Common::Array<Graphics::ManagedSurface *> _lanternLightSprites; // 6 lantern light animation frames (32x6)
Common::Array<Graphics::ManagedSurface *> _lanternSwitchSprites; // 2 lantern on/off frames (32x23)
Common::Array<Graphics::ManagedSurface *> _shootSprites; // 2 shooting crosshair frames (32x25, 48x25)
Common::Array<Graphics::ManagedSurface *> _ankhSprites; // 5 ankh fade-in frames (16x15)
Common::Array<Graphics::ManagedSurface *> _waterSprites; // 9 water ripple frames (32x9)
Common::Array<Graphics::ManagedSurface *> _soundToggleSprites; // 5 sound on/off toggle frames (16x11)
- byte _compassLookup[72]; // direction-to-needle-frame lookup table
+ int8 _compassLookup[72]; // signed Atari ST phase-to-frame lookup table
+ int _atariCompassPhase;
+ int _atariCompassTargetPhase;
+ float _atariCompassTargetRemainder;
+ int _atariCompassLastUpdateTick;
+ bool _atariCompassPhaseInitialized;
// Atari ST on-screen control hotspots (from binary hotspot table at prog $869A)
bool onScreenControls(Common::Point mouse) override;
Commit: 4294c03aa8cfeb54b1b6116e8dadf47396946dd3
https://github.com/scummvm/scummvm/commit/4294c03aa8cfeb54b1b6116e8dadf47396946dd3
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-04-10T21:12:35+02:00
Commit Message:
FREESCAPE: render water jar in eclipse atari
Changed paths:
engines/freescape/games/eclipse/atari.cpp
engines/freescape/games/eclipse/eclipse.cpp
engines/freescape/games/eclipse/eclipse.h
diff --git a/engines/freescape/games/eclipse/atari.cpp b/engines/freescape/games/eclipse/atari.cpp
index b7dccfb08d8..5615aa59723 100644
--- a/engines/freescape/games/eclipse/atari.cpp
+++ b/engines/freescape/games/eclipse/atari.cpp
@@ -36,6 +36,9 @@ const int kAtariClockCenterX = 106;
const int kAtariClockCenterY = 159;
const int kAtariCompassX = 176;
const int kAtariCompassY = 151;
+const int kAtariWaterIndicatorX = 224;
+const int kAtariWaterIndicatorY = 154;
+const int kAtariWaterIndicatorMaxLevel = 29;
// Repaired ST phase-to-frame table from $1542. Six corrupt bytes in the dumped
// binary break the intended 37-frame sweep built from $20B36 and $22B46.
@@ -446,6 +449,36 @@ void drawAtariCompassNeedle(Graphics::Surface *surface, const Graphics::ManagedS
}
}
+uint32 getAtariBorderColor(const Graphics::PixelFormat &pixelFormat, byte colorIndex) {
+ return pixelFormat.ARGBToColor(0xFF,
+ kBorderPalette[colorIndex * 3],
+ kBorderPalette[colorIndex * 3 + 1],
+ kBorderPalette[colorIndex * 3 + 2]);
+}
+
+void drawAtariWaterSurfaceRow(Graphics::Surface *surface, int x, int y,
+ const Common::Array<uint16> &maskWords, const Common::Array<uint16> &pixelWords,
+ const Graphics::PixelFormat &pixelFormat) {
+ if (maskWords.size() != 2 || pixelWords.size() != 8)
+ return;
+
+ for (uint col = 0; col < maskWords.size(); col++) {
+ uint16 mask = maskWords[col];
+ int offset = col * 4;
+ for (int bit = 15; bit >= 0; bit--) {
+ if ((mask >> bit) & 1)
+ continue;
+
+ byte colorIndex = ((pixelWords[offset + 0] >> bit) & 1)
+ | (((pixelWords[offset + 1] >> bit) & 1) << 1)
+ | (((pixelWords[offset + 2] >> bit) & 1) << 2)
+ | (((pixelWords[offset + 3] >> bit) & 1) << 3);
+ surface->setPixel(x + col * 16 + (15 - bit), y,
+ getAtariBorderColor(pixelFormat, colorIndex));
+ }
+ }
+}
+
int EclipseEngine::atariCompassPhaseFromRotationY(float rotationY) const {
return wrapAtariCompassPhase(atariCompassRoundToNearestInt(rotationY / 5.0f));
}
@@ -502,6 +535,26 @@ void EclipseEngine::drawAmigaAtariSTUI(Graphics::Surface *surface) {
} else if (!_currentAreaMessages.empty())
drawStringInSurface(_currentArea->_name, 85, 119, pal[1], pal[2], pal[0], surface);
+ // Atari ST water indicator from TeDrawWaterIndicator at $1EF0:
+ // a fixed 32x31 body bitmap at $2003C and a highlighted top row at
+ // $2024C/$2025C.
+ int waterLevel = _gameStateVars[k8bitVariableEnergy];
+ if (waterLevel < 0)
+ waterLevel = 0;
+ if (waterLevel > kAtariWaterIndicatorMaxLevel)
+ waterLevel = kAtariWaterIndicatorMaxLevel;
+
+ if (_atariWaterBody) {
+ surface->copyRectToSurface(*_atariWaterBody, kAtariWaterIndicatorX, kAtariWaterIndicatorY,
+ Common::Rect(_atariWaterBody->w, _atariWaterBody->h));
+ }
+
+ if (waterLevel > 0) {
+ int topY = 183 - waterLevel;
+ drawAtariWaterSurfaceRow(surface, kAtariWaterIndicatorX, topY,
+ _atariWaterSurfaceMask, _atariWaterSurfacePixels, _gfx->_texturePixelFormat);
+ }
+
// Step indicator: $CDC at x=$4C(76), y=$77(119)
// d7 = $42 + (2 - _playerStepIndex) * 2, drawChar chr = d7 + 31
{
@@ -756,14 +809,22 @@ void EclipseEngine::loadAssetsAtariFullGame() {
const_cast<byte *>(kBorderPalette), 16);
}
- // Water ripple animation: 9 frames, 32x9, at prog $27714, stride 144 bytes
- // Mask at prog $27710. Drawn at (0, 28) in left border strip.
- _waterSprites.resize(9);
- for (int i = 0; i < 9; i++) {
- _waterSprites[i] = loadAtariSTSprite(stream, 0x27710 + kHdr, 0x27714 + kHdr + i * 144, 2, 9);
- _waterSprites[i]->convertToInPlace(_gfx->_texturePixelFormat,
- const_cast<byte *>(kBorderPalette), 16);
- }
+ // Water indicator data from TeDrawWaterIndicator at $1EF0:
+ // a 32x31 body bitmap at $2003C and a highlighted 32-pixel cap row at
+ // $2024C/$2025C.
+ _atariWaterBody = loadAtariSTRawSprite(stream, 0x2003C + kHdr, 2, 31);
+ _atariWaterBody->convertToInPlace(_gfx->_texturePixelFormat,
+ const_cast<byte *>(kBorderPalette), 16);
+
+ _atariWaterSurfacePixels.resize(8);
+ stream->seek(0x2024C + kHdr);
+ for (uint i = 0; i < _atariWaterSurfacePixels.size(); i++)
+ _atariWaterSurfacePixels[i] = stream->readUint16BE();
+
+ _atariWaterSurfaceMask.resize(2);
+ stream->seek(0x2025C + kHdr);
+ for (uint i = 0; i < _atariWaterSurfaceMask.size(); i++)
+ _atariWaterSurfaceMask[i] = stream->readUint16BE();
// Shooting crosshair sprites: 2 frames with mask, at prog $1CC26 and $1CDC0
// Frame 0: 32x25 (2 cols), frame 1: 48x25 (3 cols)
diff --git a/engines/freescape/games/eclipse/eclipse.cpp b/engines/freescape/games/eclipse/eclipse.cpp
index 4389ce7d4a8..769320f81ea 100644
--- a/engines/freescape/games/eclipse/eclipse.cpp
+++ b/engines/freescape/games/eclipse/eclipse.cpp
@@ -107,6 +107,7 @@ EclipseEngine::EclipseEngine(OSystem *syst, const ADGameDescription *gd) : Frees
_lastHeartIndicatorFrame = 1;
_lastSecond = -1;
_compassBackground = nullptr;
+ _atariWaterBody = nullptr;
_atariCompassPhase = 0;
_atariCompassTargetPhase = 0;
_atariCompassTargetRemainder = 0.0f;
@@ -150,6 +151,10 @@ void EclipseEngine::restartBackgroundMusic() {
EclipseEngine::~EclipseEngine() {
stopBackgroundMusic();
+ if (_atariWaterBody) {
+ _atariWaterBody->free();
+ delete _atariWaterBody;
+ }
if (_compassBackground) {
_compassBackground->free();
delete _compassBackground;
diff --git a/engines/freescape/games/eclipse/eclipse.h b/engines/freescape/games/eclipse/eclipse.h
index d82e155ae97..9cce3fb029f 100644
--- a/engines/freescape/games/eclipse/eclipse.h
+++ b/engines/freescape/games/eclipse/eclipse.h
@@ -136,12 +136,14 @@ public:
Common::Array<Graphics::ManagedSurface *> _eclipseSprites; // 2 eclipse animation frames (16x13)
Common::Array<Graphics::ManagedSurface *> _eclipseProgressSprites; // 16 eclipse animation frames (16x16)
Graphics::ManagedSurface *_compassBackground; // Atari ST compass background at $20986
+ Graphics::ManagedSurface *_atariWaterBody; // Atari ST water body bitmap at $2003C
Common::Array<Graphics::ManagedSurface *> _compassSprites; // signed Atari compass needle bank addressed by the $1542 lookup table
Common::Array<Graphics::ManagedSurface *> _lanternLightSprites; // 6 lantern light animation frames (32x6)
Common::Array<Graphics::ManagedSurface *> _lanternSwitchSprites; // 2 lantern on/off frames (32x23)
Common::Array<Graphics::ManagedSurface *> _shootSprites; // 2 shooting crosshair frames (32x25, 48x25)
Common::Array<Graphics::ManagedSurface *> _ankhSprites; // 5 ankh fade-in frames (16x15)
- Common::Array<Graphics::ManagedSurface *> _waterSprites; // 9 water ripple frames (32x9)
+ Common::Array<uint16> _atariWaterSurfacePixels; // Atari ST water surface row words at $2024C
+ Common::Array<uint16> _atariWaterSurfaceMask; // Atari ST water surface mask words at $2025C
Common::Array<Graphics::ManagedSurface *> _soundToggleSprites; // 5 sound on/off toggle frames (16x11)
int8 _compassLookup[72]; // signed Atari ST phase-to-frame lookup table
int _atariCompassPhase;
Commit: d7ace101f770f1dc6506b4d7e4b2d5a961839e7d
https://github.com/scummvm/scummvm/commit/d7ace101f770f1dc6506b4d7e4b2d5a961839e7d
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-04-10T21:12:35+02:00
Commit Message:
FREESCAPE: dark rooms implemented in eclipse atari
Changed paths:
engines/freescape/games/eclipse/atari.cpp
engines/freescape/games/eclipse/eclipse.cpp
engines/freescape/games/eclipse/eclipse.h
diff --git a/engines/freescape/games/eclipse/atari.cpp b/engines/freescape/games/eclipse/atari.cpp
index 5615aa59723..d88626b5a7a 100644
--- a/engines/freescape/games/eclipse/atari.cpp
+++ b/engines/freescape/games/eclipse/atari.cpp
@@ -39,6 +39,14 @@ const int kAtariCompassY = 151;
const int kAtariWaterIndicatorX = 224;
const int kAtariWaterIndicatorY = 154;
const int kAtariWaterIndicatorMaxLevel = 29;
+const int kAtariDarkLightRadius = 34;
+const int kAtariDarkLightRadiusStep = 4;
+const uint32 kAtariAreaRecordBase = 0x2A520;
+const uint32 kAtariAreaIndexBase = 0x2A6B0;
+const int kAtariAreaIndexCount = 64;
+const uint16 kAtariDarkAreaFlag = 0x8000;
+
+void fillCircle(Graphics::Surface *surface, int x, int y, int radius, int color);
// Repaired ST phase-to-frame table from $1542. Six corrupt bytes in the dumped
// binary break the intended 37-frame sweep built from $20B36 and $22B46.
@@ -456,6 +464,65 @@ uint32 getAtariBorderColor(const Graphics::PixelFormat &pixelFormat, byte colorI
kBorderPalette[colorIndex * 3 + 2]);
}
+bool containsAtariAreaID(const Common::Array<uint16> &areaIDs, uint16 areaID) {
+ for (uint i = 0; i < areaIDs.size(); i++) {
+ if (areaIDs[i] == areaID)
+ return true;
+ }
+ return false;
+}
+
+int getAtariLanternHoleRadius(int lanternFrame) {
+ if (lanternFrame < 0)
+ return 0;
+
+ int radius = kAtariDarkLightRadius - lanternFrame * kAtariDarkLightRadiusStep;
+ if (radius < 0)
+ radius = 0;
+ return radius;
+}
+
+void advanceAtariLanternAnimation(EclipseEngine *engine) {
+ if (engine->_atariLanternAnimationDirection == 0 || engine->_atariLanternLastUpdateTick == engine->_ticks)
+ return;
+
+ if (engine->_atariLanternAnimationDirection < 0) {
+ if (engine->_atariLanternLightFrame > 0)
+ engine->_atariLanternLightFrame--;
+ else
+ engine->_atariLanternAnimationDirection = 0;
+ } else if (engine->_atariLanternLightFrame < 5) {
+ engine->_atariLanternLightFrame++;
+ } else {
+ engine->_atariLanternLightFrame = -1;
+ engine->_atariLanternAnimationDirection = 0;
+ }
+
+ engine->_atariLanternLastUpdateTick = engine->_ticks;
+}
+
+void drawAtariDarknessMask(Graphics::Surface *surface, const Common::Rect &viewArea,
+ int holeX, int holeY, int holeRadius, const Graphics::PixelFormat &pixelFormat) {
+ uint32 blackout = pixelFormat.ARGBToColor(0xFF, 0, 0, 0);
+ int transparent = pixelFormat.ARGBToColor(0x00, 0, 0, 0);
+
+ surface->fillRect(viewArea, blackout);
+ if (holeRadius <= 0)
+ return;
+
+ if (holeX < viewArea.left)
+ holeX = viewArea.left;
+ else if (holeX >= viewArea.right)
+ holeX = viewArea.right - 1;
+
+ if (holeY < viewArea.top)
+ holeY = viewArea.top;
+ else if (holeY >= viewArea.bottom)
+ holeY = viewArea.bottom - 1;
+
+ fillCircle(surface, holeX, holeY, holeRadius, transparent);
+}
+
void drawAtariWaterSurfaceRow(Graphics::Surface *surface, int x, int y,
const Common::Array<uint16> &maskWords, const Common::Array<uint16> &pixelWords,
const Graphics::PixelFormat &pixelFormat) {
@@ -506,6 +573,13 @@ int EclipseEngine::atariCompassTargetPhaseFromYaw(float yaw, int referencePhase)
}
void EclipseEngine::drawAmigaAtariSTUI(Graphics::Surface *surface) {
+ int lanternFrame = _atariLanternLightFrame;
+ int lanternRadius = getAtariLanternHoleRadius(lanternFrame);
+
+ if (_atariAreaDark)
+ drawAtariDarknessMask(surface, _viewArea, _crossairPosition.x, _crossairPosition.y,
+ lanternRadius, _gfx->_texturePixelFormat);
+
// Border palette colors for the 4-plane font (from CONSOLE.NEO).
// The Atari ST uses raster interrupts to switch palettes between
// the 3D viewport (area palette) and the border/UI area (border palette).
@@ -661,17 +735,20 @@ void EclipseEngine::drawAmigaAtariSTUI(Graphics::Surface *surface) {
}
}
- // Lantern switch at x=$30(48), y=$91(145) â only drawn when lantern is ON
- if (_flashlightOn && _lanternSwitchSprites.size() >= 2) {
- surface->copyRectToSurface(*_lanternSwitchSprites[0], 48, 145,
- Common::Rect(_lanternSwitchSprites[0]->w, _lanternSwitchSprites[0]->h));
+ // Lantern switch at x=$30(48), y=$91(145). The ST code always draws the
+ // switch and picks frame 0/1 from the persistent lantern state.
+ if (_lanternSwitchSprites.size() >= 2) {
+ int switchFrame = _flashlightOn ? 0 : 1;
+ surface->copyRectToSurface(*_lanternSwitchSprites[switchFrame], 48, 145,
+ Common::Rect(_lanternSwitchSprites[switchFrame]->w, _lanternSwitchSprites[switchFrame]->h));
}
- // Lantern light animation overlay at (48, 139) â 6 frames, 32x6, toggled with 'T' key
- if (_flashlightOn && _lanternLightSprites.size() >= 6) {
- int lightFrame = (_ticks / 8) % 6;
- surface->copyRectToSurface(*_lanternLightSprites[lightFrame], 48, 139,
- Common::Rect(_lanternLightSprites[lightFrame]->w, _lanternLightSprites[lightFrame]->h));
+ // Lantern light strip at (48, 139). The ST code uses a finite frame index,
+ // not a looping idle animation, so keep drawing the settled frame instead
+ // of cycling on _ticks.
+ if (lanternFrame >= 0 && lanternFrame < 6 && _lanternLightSprites.size() >= 6) {
+ surface->copyRectToSurface(*_lanternLightSprites[lanternFrame], 48, 139,
+ Common::Rect(_lanternLightSprites[lanternFrame]->w, _lanternLightSprites[lanternFrame]->h));
}
// Shooting crosshair overlay at x=$80(128), y=$9F(159)
@@ -699,6 +776,7 @@ void EclipseEngine::drawAmigaAtariSTUI(Graphics::Surface *surface) {
uint32 other = _gfx->_texturePixelFormat.ARGBToColor(0xFF, r, g, b);
drawAnalogClock(surface, kAtariClockCenterX, kAtariClockCenterY, back, other, front);
+ advanceAtariLanternAnimation(this);
}
void EclipseEngine::loadAssetsAtariFullGame() {
@@ -743,6 +821,34 @@ void EclipseEngine::loadAssetsAtariFullGame() {
// convert program addresses to stream offsets.
static const int kHdr = 0x1C;
+ _atariDarkAreas.clear();
+ Common::Array<uint16> parsedAreas;
+ uint32 streamSize = stream->size();
+ for (int i = 0; i < kAtariAreaIndexCount; i++) {
+ uint32 indexOffset = kAtariAreaIndexBase + kHdr + i * 4;
+ if (indexOffset + 4 > streamSize)
+ break;
+
+ stream->seek(indexOffset);
+ uint16 lowWord = stream->readUint16BE();
+ uint16 highWord = stream->readUint16BE();
+ uint32 recordWordOffset = lowWord | ((uint32)highWord << 8);
+ uint32 recordOffset = kAtariAreaRecordBase + kHdr + recordWordOffset * 2;
+ if (recordOffset + 6 > streamSize)
+ continue;
+
+ stream->seek(recordOffset);
+ uint16 flags = stream->readUint16BE();
+ stream->readUint16BE();
+ uint16 areaID = stream->readUint16BE();
+ if (!_areaMap.contains(areaID) || containsAtariAreaID(parsedAreas, areaID))
+ continue;
+
+ parsedAreas.push_back(areaID);
+ if ((flags & kAtariDarkAreaFlag) != 0)
+ _atariDarkAreas.push_back(areaID);
+ }
+
// Heart indicator sprites: 2 frames, 16x13 pixels
// Descriptor at prog $1D2B8 (1 col à 13 rows), mask at +6, pixels at +8
// Frame 0 = heart visible, Frame 1 = heart hidden/dimmed
diff --git a/engines/freescape/games/eclipse/eclipse.cpp b/engines/freescape/games/eclipse/eclipse.cpp
index 769320f81ea..1f06f8c7cd2 100644
--- a/engines/freescape/games/eclipse/eclipse.cpp
+++ b/engines/freescape/games/eclipse/eclipse.cpp
@@ -113,6 +113,10 @@ EclipseEngine::EclipseEngine(OSystem *syst, const ADGameDescription *gd) : Frees
_atariCompassTargetRemainder = 0.0f;
_atariCompassLastUpdateTick = -1;
_atariCompassPhaseInitialized = false;
+ _atariLanternLightFrame = -1;
+ _atariLanternAnimationDirection = 0;
+ _atariLanternLastUpdateTick = -1;
+ _atariAreaDark = false;
_resting = false;
_flashlightOn = false;
}
@@ -184,6 +188,10 @@ void EclipseEngine::initGameState() {
_atariCompassTargetRemainder = 0.0f;
_atariCompassLastUpdateTick = -1;
_atariCompassPhaseInitialized = false;
+ _atariLanternLightFrame = -1;
+ _atariLanternAnimationDirection = 0;
+ _atariLanternLastUpdateTick = -1;
+ _atariAreaDark = false;
_resting = false;
_flashlightOn = false;
restartBackgroundMusic();
@@ -402,6 +410,7 @@ void EclipseEngine::gotoArea(uint16 areaID, int entranceID) {
assert(_areaMap.contains(areaID));
_currentArea = _areaMap[areaID];
_currentArea->show();
+ _atariAreaDark = isAtariST() && isAtariDarkArea(areaID);
_currentAreaMessages.clear();
_currentAreaMessages.push_back(_currentArea->_name);
@@ -466,6 +475,14 @@ void EclipseEngine::gotoArea(uint16 areaID, int entranceID) {
resetInput();
}
+bool EclipseEngine::isAtariDarkArea(uint16 areaID) const {
+ for (uint i = 0; i < _atariDarkAreas.size(); i++) {
+ if (_atariDarkAreas[i] == areaID)
+ return true;
+ }
+ return false;
+}
+
void EclipseEngine::drawBackground() {
clearBackground();
_gfx->drawBackground(_currentArea->_skyColor);
@@ -650,7 +667,22 @@ void EclipseEngine::pressedKey(const int keycode) {
_pitch = 0;
updateCamera();
} else if (keycode == kActionToggleFlashlight) {
- _flashlightOn = !_flashlightOn;
+ if (isAtariST()) {
+ if (_flashlightOn) {
+ _flashlightOn = false;
+ if (_atariLanternLightFrame < 0)
+ _atariLanternLightFrame = 0;
+ _atariLanternAnimationDirection = 1;
+ } else {
+ _flashlightOn = true;
+ if (_atariLanternLightFrame < 0 || _atariLanternLightFrame > 5)
+ _atariLanternLightFrame = 5;
+ _atariLanternAnimationDirection = -1;
+ }
+ _atariLanternLastUpdateTick = -1;
+ } else {
+ _flashlightOn = !_flashlightOn;
+ }
} else if (keycode == kActionRunModifier) {
// Shift-to-sprint: save current step, switch to max while held
if (_savedPlayerStepIndex < 0) {
@@ -1173,6 +1205,7 @@ Common::Error EclipseEngine::saveGameStreamExtended(Common::WriteStream *stream,
}
Common::Error EclipseEngine::loadGameStreamExtended(Common::SeekableReadStream *stream) {
+ (void)stream;
_lastHeartbeatSoundTick = -1;
_lastHeartIndicatorFrame = 1;
_atariCompassPhase = 0;
@@ -1180,6 +1213,10 @@ Common::Error EclipseEngine::loadGameStreamExtended(Common::SeekableReadStream *
_atariCompassTargetRemainder = 0.0f;
_atariCompassLastUpdateTick = -1;
_atariCompassPhaseInitialized = false;
+ _atariLanternAnimationDirection = 0;
+ _atariLanternLightFrame = _flashlightOn ? 0 : -1;
+ _atariLanternLastUpdateTick = -1;
+ _atariAreaDark = isAtariST() && _currentArea && isAtariDarkArea(_currentArea->getAreaID());
return Common::kNoError;
}
diff --git a/engines/freescape/games/eclipse/eclipse.h b/engines/freescape/games/eclipse/eclipse.h
index 9cce3fb029f..3e3ab57cc01 100644
--- a/engines/freescape/games/eclipse/eclipse.h
+++ b/engines/freescape/games/eclipse/eclipse.h
@@ -67,6 +67,7 @@ public:
bool _resting;
bool _flashlightOn;
+ bool _atariAreaDark;
int _lastThirtySeconds;
int _lastFiveSeconds;
int _lastHeartbeatSoundTick;
@@ -103,6 +104,7 @@ public:
void drawCompass(Graphics::Surface *surface, int x, int y, double degrees, double magnitude, uint32 color);
int atariCompassPhaseFromRotationY(float rotationY) const;
int atariCompassTargetPhaseFromYaw(float yaw, int referencePhase) const;
+ bool isAtariDarkArea(uint16 areaID) const;
void onRotate(float xoffset, float yoffset, float zoffset) override;
void drawEclipseIndicator(Graphics::Surface *surface, int x, int y, uint32 color1, uint32 color2, uint32 color3 = 0);
Common::String getScoreString(int score);
@@ -142,6 +144,7 @@ public:
Common::Array<Graphics::ManagedSurface *> _lanternSwitchSprites; // 2 lantern on/off frames (32x23)
Common::Array<Graphics::ManagedSurface *> _shootSprites; // 2 shooting crosshair frames (32x25, 48x25)
Common::Array<Graphics::ManagedSurface *> _ankhSprites; // 5 ankh fade-in frames (16x15)
+ Common::Array<uint16> _atariDarkAreas; // Atari ST dark areas from the $2A520 runtime table
Common::Array<uint16> _atariWaterSurfacePixels; // Atari ST water surface row words at $2024C
Common::Array<uint16> _atariWaterSurfaceMask; // Atari ST water surface mask words at $2025C
Common::Array<Graphics::ManagedSurface *> _soundToggleSprites; // 5 sound on/off toggle frames (16x11)
@@ -151,6 +154,9 @@ public:
float _atariCompassTargetRemainder;
int _atariCompassLastUpdateTick;
bool _atariCompassPhaseInitialized;
+ int _atariLanternLightFrame;
+ int _atariLanternAnimationDirection;
+ int _atariLanternLastUpdateTick;
// Atari ST on-screen control hotspots (from binary hotspot table at prog $869A)
bool onScreenControls(Common::Point mouse) override;
Commit: e566f73b5b48691448728819d8657f91533a4050
https://github.com/scummvm/scummvm/commit/e566f73b5b48691448728819d8657f91533a4050
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-04-10T21:12:35+02:00
Commit Message:
FREESCAPE: limited lantern batery in eclipse atari
Changed paths:
engines/freescape/games/eclipse/atari.cpp
engines/freescape/games/eclipse/eclipse.cpp
engines/freescape/games/eclipse/eclipse.h
diff --git a/engines/freescape/games/eclipse/atari.cpp b/engines/freescape/games/eclipse/atari.cpp
index d88626b5a7a..4d805da826d 100644
--- a/engines/freescape/games/eclipse/atari.cpp
+++ b/engines/freescape/games/eclipse/atari.cpp
@@ -574,7 +574,16 @@ int EclipseEngine::atariCompassTargetPhaseFromYaw(float yaw, int referencePhase)
void EclipseEngine::drawAmigaAtariSTUI(Graphics::Surface *surface) {
int lanternFrame = _atariLanternLightFrame;
- int lanternRadius = getAtariLanternHoleRadius(lanternFrame);
+
+ // Darkness radius based on battery level (5=full/bright â 0=dim â -1=dead).
+ // ROM uses TeLanternBrightnessFrame ($7f6c) directly: frame 5 = largest radius,
+ // frame 0 = smallest, frame -1 = no light. ScummVM's getAtariLanternHoleRadius
+ // maps frame 0â34px and frame 5â14px (inverted), so we pass (5 - battery).
+ int lanternRadius = 0;
+ if (_flashlightOn && _lanternBatteryLevel >= 0) {
+ int effectiveFrame = 5 - _lanternBatteryLevel;
+ lanternRadius = getAtariLanternHoleRadius(effectiveFrame);
+ }
if (_atariAreaDark)
drawAtariDarknessMask(surface, _viewArea, _crossairPosition.x, _crossairPosition.y,
@@ -743,12 +752,17 @@ void EclipseEngine::drawAmigaAtariSTUI(Graphics::Surface *surface) {
Common::Rect(_lanternSwitchSprites[switchFrame]->w, _lanternSwitchSprites[switchFrame]->h));
}
- // Lantern light strip at (48, 139). The ST code uses a finite frame index,
- // not a looping idle animation, so keep drawing the settled frame instead
- // of cycling on _ticks.
- if (lanternFrame >= 0 && lanternFrame < 6 && _lanternLightSprites.size() >= 6) {
- surface->copyRectToSurface(*_lanternLightSprites[lanternFrame], 48, 139,
- Common::Rect(_lanternLightSprites[lanternFrame]->w, _lanternLightSprites[lanternFrame]->h));
+ // Lantern light strip at (48, 139). During on/off animation use the animation
+ // frame; once settled, show the battery level (5=full â 0=dim, mapped to
+ // sprite index 0=bright â 5=dim via inversion).
+ {
+ int hudLanternFrame = lanternFrame;
+ if (_flashlightOn && _atariLanternAnimationDirection == 0 && _lanternBatteryLevel >= 0)
+ hudLanternFrame = 5 - _lanternBatteryLevel;
+ if (hudLanternFrame >= 0 && hudLanternFrame < 6 && _lanternLightSprites.size() >= 6) {
+ surface->copyRectToSurface(*_lanternLightSprites[hudLanternFrame], 48, 139,
+ Common::Rect(_lanternLightSprites[hudLanternFrame]->w, _lanternLightSprites[hudLanternFrame]->h));
+ }
}
// Shooting crosshair overlay at x=$80(128), y=$9F(159)
diff --git a/engines/freescape/games/eclipse/eclipse.cpp b/engines/freescape/games/eclipse/eclipse.cpp
index 1f06f8c7cd2..8b0ea7d82b7 100644
--- a/engines/freescape/games/eclipse/eclipse.cpp
+++ b/engines/freescape/games/eclipse/eclipse.cpp
@@ -116,6 +116,7 @@ EclipseEngine::EclipseEngine(OSystem *syst, const ADGameDescription *gd) : Frees
_atariLanternLightFrame = -1;
_atariLanternAnimationDirection = 0;
_atariLanternLastUpdateTick = -1;
+ _lanternBatteryLevel = 5;
_atariAreaDark = false;
_resting = false;
_flashlightOn = false;
@@ -191,6 +192,7 @@ void EclipseEngine::initGameState() {
_atariLanternLightFrame = -1;
_atariLanternAnimationDirection = 0;
_atariLanternLastUpdateTick = -1;
+ _lanternBatteryLevel = 5;
_atariAreaDark = false;
_resting = false;
_flashlightOn = false;
@@ -673,7 +675,7 @@ void EclipseEngine::pressedKey(const int keycode) {
if (_atariLanternLightFrame < 0)
_atariLanternLightFrame = 0;
_atariLanternAnimationDirection = 1;
- } else {
+ } else if (_lanternBatteryLevel >= 0) {
_flashlightOn = true;
if (_atariLanternLightFrame < 0 || _atariLanternLightFrame > 5)
_atariLanternLightFrame = 5;
@@ -1162,6 +1164,18 @@ void EclipseEngine::updateTimeVariables() {
if (_gameStateVars[k8bitVariableShield] < _maxShield) {
_gameStateVars[k8bitVariableShield] += 1;
}
+
+ // Lantern battery drain: non-rechargeable, one level per 30-second tick
+ // while the flashlight is on. ROM drains TeLanternBrightnessFrame ($7f6c)
+ // from 5 (brightest) down to -1 (dead). 6 levels total.
+ if (isAtariST() && _flashlightOn && _lanternBatteryLevel >= 0) {
+ _lanternBatteryLevel--;
+ if (_lanternBatteryLevel < 0) {
+ _flashlightOn = false;
+ _atariLanternLightFrame = -1;
+ _atariLanternAnimationDirection = 0;
+ }
+ }
}
if (isEclipse() && isSpectrum() && _currentArea->getAreaID() == 42) {
diff --git a/engines/freescape/games/eclipse/eclipse.h b/engines/freescape/games/eclipse/eclipse.h
index 3e3ab57cc01..6a050a1a8db 100644
--- a/engines/freescape/games/eclipse/eclipse.h
+++ b/engines/freescape/games/eclipse/eclipse.h
@@ -157,6 +157,7 @@ public:
int _atariLanternLightFrame;
int _atariLanternAnimationDirection;
int _atariLanternLastUpdateTick;
+ int _lanternBatteryLevel; // 5=full, 0=nearly dead, -1=dead (non-rechargeable)
// Atari ST on-screen control hotspots (from binary hotspot table at prog $869A)
bool onScreenControls(Common::Point mouse) override;
Commit: d0da7154230d1e1e217044c8f4184c396c78a0a7
https://github.com/scummvm/scummvm/commit/d0da7154230d1e1e217044c8f4184c396c78a0a7
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-04-10T21:12:35+02:00
Commit Message:
FREESCAPE: better handling of palettes for eclipse atari
Changed paths:
engines/freescape/games/eclipse/atari.cpp
engines/freescape/games/palettes.cpp
diff --git a/engines/freescape/games/eclipse/atari.cpp b/engines/freescape/games/eclipse/atari.cpp
index 4d805da826d..561fad49428 100644
--- a/engines/freescape/games/eclipse/atari.cpp
+++ b/engines/freescape/games/eclipse/atari.cpp
@@ -806,7 +806,21 @@ void EclipseEngine::loadAssetsAtariFullGame() {
load8bitBinary(stream, 0x2a53c, 16);
_border = loadAndConvertNeoImage(stream, 0x139c8);
- loadPalettes(stream, 0x2a0fa);
+ // The palette table is split across two regions in the binary: areas 32-51
+ // at prog $29E36, then areas 25-31, 127, 1-24 at prog $2A0DE. Load from
+ // the start of the first region so all areas get palettes.
+ loadPalettes(stream, 0x29e52);
+
+ // The original Atari ST game uses a Timer-B raster interrupt to split the
+ // hardware palette mid-screen: colors 0-5 always come from the border
+ // (CONSOLE.NEO) palette, while only colors 6-15 are swapped per area.
+ // Objects using indices 0-5 (e.g. ankhs at color 4 = bright gold) must
+ // therefore show the border palette values, not the area-specific ones.
+ for (auto &entry : _paletteByArea) {
+ byte *pal = entry._value;
+ memcpy(pal, kBorderPalette, 6 * 3);
+ }
+
loadSoundsFx(stream, 0x3030c, 6);
// Load TEMUSIC.ST (GEMDOS executable at file offset $11F5A, skip $1C header, TEXT size $11E8)
diff --git a/engines/freescape/games/palettes.cpp b/engines/freescape/games/palettes.cpp
index 933dea17616..e9a2a0dc42b 100644
--- a/engines/freescape/games/palettes.cpp
+++ b/engines/freescape/games/palettes.cpp
@@ -195,6 +195,8 @@ void FreescapeEngine::loadPalettes(Common::SeekableReadStream *file, int offset)
numberOfAreas += 5;
else if (isCastle())
numberOfAreas += 20;
+ else if (isEclipse())
+ numberOfAreas += 2;
for (uint i = 0; i < numberOfAreas; i++) {
int label = readField(file, 8);
@@ -215,7 +217,12 @@ void FreescapeEngine::loadPalettes(Common::SeekableReadStream *file, int offset)
palette[c][2] = b & 0xff;
debugC(1, kFreescapeDebugParser, "Color %d: (%04x) %02x %02x %02x", c, v, palette[c][0], palette[c][1], palette[c][2]);
}
- assert(!_paletteByArea.contains(label));
+ if (_paletteByArea.contains(label)) {
+ // Eclipse Atari ST has a duplicate palette entry for area 42
+ assert(isEclipse() && isAtariST());
+ delete[] palette;
+ continue;
+ }
_paletteByArea[label] = (byte *)palette;
}
}
Commit: 26fe85ba26a15cc2f3670ca3ec299bd0d4f05844
https://github.com/scummvm/scummvm/commit/26fe85ba26a15cc2f3670ca3ec299bd0d4f05844
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-04-10T21:12:35+02:00
Commit Message:
FREESCAPE: make sure dark areas have reasonable palettes in eclipse atarist
Changed paths:
engines/freescape/games/eclipse/atari.cpp
diff --git a/engines/freescape/games/eclipse/atari.cpp b/engines/freescape/games/eclipse/atari.cpp
index 561fad49428..f611f499d6c 100644
--- a/engines/freescape/games/eclipse/atari.cpp
+++ b/engines/freescape/games/eclipse/atari.cpp
@@ -811,11 +811,9 @@ void EclipseEngine::loadAssetsAtariFullGame() {
// the start of the first region so all areas get palettes.
loadPalettes(stream, 0x29e52);
- // The original Atari ST game uses a Timer-B raster interrupt to split the
- // hardware palette mid-screen: colors 0-5 always come from the border
- // (CONSOLE.NEO) palette, while only colors 6-15 are swapped per area.
- // Objects using indices 0-5 (e.g. ankhs at color 4 = bright gold) must
- // therefore show the border palette values, not the area-specific ones.
+ // The original game uses a Timer-B raster interrupt to split the hardware
+ // palette mid-screen: colors 0-5 always come from the border (CONSOLE.NEO)
+ // palette, while only colors 6-15 are swapped per area.
for (auto &entry : _paletteByArea) {
byte *pal = entry._value;
memcpy(pal, kBorderPalette, 6 * 3);
@@ -877,6 +875,52 @@ void EclipseEngine::loadAssetsAtariFullGame() {
_atariDarkAreas.push_back(areaID);
}
+ // Eclipse/dark areas use a fade palette table at prog $10EB6 instead of
+ // the per-area palette table. The table has 6 brightness levels (0=black,
+ // 5=brightest), 32 bytes each. The 68K copy routine at $10FDC applies
+ // LSR.L #1 to each packed longword pair (values are pre-doubled).
+ // For now we use level 5 (brightest, matching full lantern).
+ {
+ static const uint32 kEclipseFadePalette = 0x10EB6;
+ static const int kBrightest = 5;
+ static const int kHdrSize = 0x1C;
+ stream->seek(kEclipseFadePalette + kHdrSize + kBrightest * 32);
+ uint16 rawWords[16];
+ for (int w = 0; w < 16; w++)
+ rawWords[w] = stream->readUint16BE();
+
+ // Simulate the 68K LSR.L #1 on packed longword pairs, then mask to $0777
+ byte darkPal[16 * 3];
+ for (int i = 0; i < 16; i += 2) {
+ uint32 packed = ((uint32)rawWords[i] << 16) | rawWords[i + 1];
+ packed >>= 1;
+ uint16 w1 = (packed >> 16) & 0x0777;
+ uint16 w2 = packed & 0x0777;
+ for (int j = 0; j < 2; j++) {
+ uint16 v = (j == 0) ? w1 : w2;
+ int idx = i + j;
+ int r = (v >> 8) & 0xf;
+ r = r << 4 | r;
+ int g = (v >> 4) & 0xf;
+ g = g << 4 | g;
+ int b = v & 0xf;
+ b = b << 4 | b;
+ darkPal[idx * 3 + 0] = r;
+ darkPal[idx * 3 + 1] = g;
+ darkPal[idx * 3 + 2] = b;
+ }
+ }
+
+ // Overwrite dark area palettes: colors 0-5 from border, 6-15 from dark palette
+ for (uint i = 0; i < _atariDarkAreas.size(); i++) {
+ uint16 areaID = _atariDarkAreas[i];
+ if (_paletteByArea.contains(areaID)) {
+ byte *pal = _paletteByArea[areaID];
+ memcpy(pal + 6 * 3, darkPal + 6 * 3, 10 * 3);
+ }
+ }
+ }
+
// Heart indicator sprites: 2 frames, 16x13 pixels
// Descriptor at prog $1D2B8 (1 col à 13 rows), mask at +6, pixels at +8
// Frame 0 = heart visible, Frame 1 = heart hidden/dimmed
Commit: 444ca7e18f755ad3556ca60f7d5c261a7ad14260
https://github.com/scummvm/scummvm/commit/444ca7e18f755ad3556ca60f7d5c261a7ad14260
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-04-10T21:12:35+02:00
Commit Message:
FREESCAPE: refactored palette handling and light effect in eclipse atarist
Changed paths:
engines/freescape/games/eclipse/atari.cpp
engines/freescape/games/eclipse/eclipse.cpp
engines/freescape/games/eclipse/eclipse.h
diff --git a/engines/freescape/games/eclipse/atari.cpp b/engines/freescape/games/eclipse/atari.cpp
index f611f499d6c..679455a1589 100644
--- a/engines/freescape/games/eclipse/atari.cpp
+++ b/engines/freescape/games/eclipse/atari.cpp
@@ -39,8 +39,15 @@ const int kAtariCompassY = 151;
const int kAtariWaterIndicatorX = 224;
const int kAtariWaterIndicatorY = 154;
const int kAtariWaterIndicatorMaxLevel = 29;
-const int kAtariDarkLightRadius = 34;
-const int kAtariDarkLightRadiusStep = 4;
+// The original 68K code at $07BA renders the dark room light hole using a
+// pre-built bitplane mask. The fully-visible center spans 6 word-groups
+// (96px) with 2 masked border groups (32px each) on either side, for a
+// total visible width of ~160px in the 256px-wide viewport. The vertical
+// extent is similarly masked per-scanline. This corresponds to a circle
+// radius of approximately 80 pixels. The lantern battery (6 levels)
+// scales this down proportionally.
+const int kAtariDarkLightRadius = 80;
+const int kAtariDarkLightRadiusStep = 10;
const uint32 kAtariAreaRecordBase = 0x2A520;
const uint32 kAtariAreaIndexBase = 0x2A6B0;
const int kAtariAreaIndexCount = 64;
@@ -57,6 +64,15 @@ const int8 kAtariCompassPhaseToFrame[kAtariCompassPhaseCount] = {
15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1
};
+void EclipseEngine::applyEclipseFadePalette(uint16 areaID, int brightnessLevel) {
+ if (!_paletteByArea.contains(areaID))
+ return;
+ brightnessLevel = CLIP(brightnessLevel, 0, 5);
+ byte *pal = _paletteByArea[areaID];
+ // Colors 0-5 stay as the border palette; overwrite 6-15 from the fade table
+ memcpy(pal + 6 * 3, _eclipseFadePalettes[brightnessLevel] + 6 * 3, 10 * 3);
+}
+
void EclipseEngine::initAmigaAtari() {
_viewArea = Common::Rect(32, 16, 288, 118);
@@ -879,18 +895,13 @@ void EclipseEngine::loadAssetsAtariFullGame() {
// the per-area palette table. The table has 6 brightness levels (0=black,
// 5=brightest), 32 bytes each. The 68K copy routine at $10FDC applies
// LSR.L #1 to each packed longword pair (values are pre-doubled).
- // For now we use level 5 (brightest, matching full lantern).
- {
- static const uint32 kEclipseFadePalette = 0x10EB6;
- static const int kBrightest = 5;
- static const int kHdrSize = 0x1C;
- stream->seek(kEclipseFadePalette + kHdrSize + kBrightest * 32);
+ // Load all 6 levels into _eclipseFadePalettes for runtime use.
+ for (int level = 0; level < 6; level++) {
+ stream->seek(0x10EB6 + 0x1C + level * 32);
uint16 rawWords[16];
for (int w = 0; w < 16; w++)
rawWords[w] = stream->readUint16BE();
- // Simulate the 68K LSR.L #1 on packed longword pairs, then mask to $0777
- byte darkPal[16 * 3];
for (int i = 0; i < 16; i += 2) {
uint32 packed = ((uint32)rawWords[i] << 16) | rawWords[i + 1];
packed >>= 1;
@@ -899,28 +910,20 @@ void EclipseEngine::loadAssetsAtariFullGame() {
for (int j = 0; j < 2; j++) {
uint16 v = (j == 0) ? w1 : w2;
int idx = i + j;
- int r = (v >> 8) & 0xf;
- r = r << 4 | r;
- int g = (v >> 4) & 0xf;
- g = g << 4 | g;
- int b = v & 0xf;
- b = b << 4 | b;
- darkPal[idx * 3 + 0] = r;
- darkPal[idx * 3 + 1] = g;
- darkPal[idx * 3 + 2] = b;
- }
- }
-
- // Overwrite dark area palettes: colors 0-5 from border, 6-15 from dark palette
- for (uint i = 0; i < _atariDarkAreas.size(); i++) {
- uint16 areaID = _atariDarkAreas[i];
- if (_paletteByArea.contains(areaID)) {
- byte *pal = _paletteByArea[areaID];
- memcpy(pal + 6 * 3, darkPal + 6 * 3, 10 * 3);
+ int r = (v >> 8) & 0xf; r = r << 4 | r;
+ int g = (v >> 4) & 0xf; g = g << 4 | g;
+ int b = v & 0xf; b = b << 4 | b;
+ _eclipseFadePalettes[level][idx * 3 + 0] = r;
+ _eclipseFadePalettes[level][idx * 3 + 1] = g;
+ _eclipseFadePalettes[level][idx * 3 + 2] = b;
}
}
}
+ // Apply brightest fade palette to all dark areas at load time
+ for (uint i = 0; i < _atariDarkAreas.size(); i++)
+ applyEclipseFadePalette(_atariDarkAreas[i], _lanternBatteryLevel);
+
// Heart indicator sprites: 2 frames, 16x13 pixels
// Descriptor at prog $1D2B8 (1 col à 13 rows), mask at +6, pixels at +8
// Frame 0 = heart visible, Frame 1 = heart hidden/dimmed
diff --git a/engines/freescape/games/eclipse/eclipse.cpp b/engines/freescape/games/eclipse/eclipse.cpp
index 8b0ea7d82b7..0586dde8c8d 100644
--- a/engines/freescape/games/eclipse/eclipse.cpp
+++ b/engines/freescape/games/eclipse/eclipse.cpp
@@ -446,6 +446,8 @@ void EclipseEngine::gotoArea(uint16 areaID, int entranceID) {
}
_gfx->_keyColor = 0;
+ if (isAtariST() && isAtariDarkArea(areaID))
+ applyEclipseFadePalette(areaID, _lanternBatteryLevel);
swapPalette(areaID);
if (isCPC())
updateHeartFramesCPC();
diff --git a/engines/freescape/games/eclipse/eclipse.h b/engines/freescape/games/eclipse/eclipse.h
index 6a050a1a8db..1d30ba4883b 100644
--- a/engines/freescape/games/eclipse/eclipse.h
+++ b/engines/freescape/games/eclipse/eclipse.h
@@ -145,6 +145,8 @@ public:
Common::Array<Graphics::ManagedSurface *> _shootSprites; // 2 shooting crosshair frames (32x25, 48x25)
Common::Array<Graphics::ManagedSurface *> _ankhSprites; // 5 ankh fade-in frames (16x15)
Common::Array<uint16> _atariDarkAreas; // Atari ST dark areas from the $2A520 runtime table
+ byte _eclipseFadePalettes[6][16 * 3]; // 6 brightness levels from prog $10EB6 (post LSR.L #1)
+ void applyEclipseFadePalette(uint16 areaID, int brightnessLevel);
Common::Array<uint16> _atariWaterSurfacePixels; // Atari ST water surface row words at $2024C
Common::Array<uint16> _atariWaterSurfaceMask; // Atari ST water surface mask words at $2025C
Common::Array<Graphics::ManagedSurface *> _soundToggleSprites; // 5 sound on/off toggle frames (16x11)
Commit: 956cbac5f1d4f58d41de931122d51239732b2033
https://github.com/scummvm/scummvm/commit/956cbac5f1d4f58d41de931122d51239732b2033
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-04-10T21:12:35+02:00
Commit Message:
FREESCAPE: simplify brightness handling in eclipse atarist
Changed paths:
engines/freescape/games/eclipse/atari.cpp
diff --git a/engines/freescape/games/eclipse/atari.cpp b/engines/freescape/games/eclipse/atari.cpp
index 679455a1589..eec4d627636 100644
--- a/engines/freescape/games/eclipse/atari.cpp
+++ b/engines/freescape/games/eclipse/atari.cpp
@@ -892,32 +892,16 @@ void EclipseEngine::loadAssetsAtariFullGame() {
}
// Eclipse/dark areas use a fade palette table at prog $10EB6 instead of
- // the per-area palette table. The table has 6 brightness levels (0=black,
- // 5=brightest), 32 bytes each. The 68K copy routine at $10FDC applies
- // LSR.L #1 to each packed longword pair (values are pre-doubled).
- // Load all 6 levels into _eclipseFadePalettes for runtime use.
+ // the per-area palette table. 6 brightness levels (0=black, 5=brightest),
+ // 16 words each. The 68K code at $10FDC applies LSR.L #1 before writing
+ // to the raster interrupt buffer, but ScummVM reads normal area palettes
+ // from the same table format without that shift (and they look correct),
+ // so we load these the same way â via loadPalette, no shift.
for (int level = 0; level < 6; level++) {
stream->seek(0x10EB6 + 0x1C + level * 32);
- uint16 rawWords[16];
- for (int w = 0; w < 16; w++)
- rawWords[w] = stream->readUint16BE();
-
- for (int i = 0; i < 16; i += 2) {
- uint32 packed = ((uint32)rawWords[i] << 16) | rawWords[i + 1];
- packed >>= 1;
- uint16 w1 = (packed >> 16) & 0x0777;
- uint16 w2 = packed & 0x0777;
- for (int j = 0; j < 2; j++) {
- uint16 v = (j == 0) ? w1 : w2;
- int idx = i + j;
- int r = (v >> 8) & 0xf; r = r << 4 | r;
- int g = (v >> 4) & 0xf; g = g << 4 | g;
- int b = v & 0xf; b = b << 4 | b;
- _eclipseFadePalettes[level][idx * 3 + 0] = r;
- _eclipseFadePalettes[level][idx * 3 + 1] = g;
- _eclipseFadePalettes[level][idx * 3 + 2] = b;
- }
- }
+ byte *pal = loadPalette(stream);
+ memcpy(_eclipseFadePalettes[level], pal, 16 * 3);
+ delete[] pal;
}
// Apply brightest fade palette to all dark areas at load time
Commit: 24bcddbf11571dfd9d5ea0380fbd68e20b974727
https://github.com/scummvm/scummvm/commit/24bcddbf11571dfd9d5ea0380fbd68e20b974727
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-04-10T21:12:35+02:00
Commit Message:
FREESCAPE: update brightness in real time in eclipse atarist
Changed paths:
engines/freescape/games/eclipse/eclipse.cpp
diff --git a/engines/freescape/games/eclipse/eclipse.cpp b/engines/freescape/games/eclipse/eclipse.cpp
index 0586dde8c8d..0906ef80767 100644
--- a/engines/freescape/games/eclipse/eclipse.cpp
+++ b/engines/freescape/games/eclipse/eclipse.cpp
@@ -1177,6 +1177,10 @@ void EclipseEngine::updateTimeVariables() {
_atariLanternLightFrame = -1;
_atariLanternAnimationDirection = 0;
}
+ if (_atariAreaDark && _currentArea) {
+ applyEclipseFadePalette(_currentArea->getAreaID(), _lanternBatteryLevel);
+ swapPalette(_currentArea->getAreaID());
+ }
}
}
Commit: 4ae107a7a722fd5defe35451d0672894376296e7
https://github.com/scummvm/scummvm/commit/4ae107a7a722fd5defe35451d0672894376296e7
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-04-10T21:12:35+02:00
Commit Message:
FREESCAPE: use correct frames for brightness UI updates in eclipse atarist
Changed paths:
engines/freescape/games/eclipse/atari.cpp
diff --git a/engines/freescape/games/eclipse/atari.cpp b/engines/freescape/games/eclipse/atari.cpp
index eec4d627636..e8a88208f6d 100644
--- a/engines/freescape/games/eclipse/atari.cpp
+++ b/engines/freescape/games/eclipse/atari.cpp
@@ -769,12 +769,13 @@ void EclipseEngine::drawAmigaAtariSTUI(Graphics::Surface *surface) {
}
// Lantern light strip at (48, 139). During on/off animation use the animation
- // frame; once settled, show the battery level (5=full â 0=dim, mapped to
- // sprite index 0=bright â 5=dim via inversion).
+ // frame; once settled, show the battery level. The original 68K code at
+ // $1C1C uses eclipse_brightness_level ($7F6C) directly as the sprite index:
+ // level 5 = sprite 5 (brightest), level 0 = sprite 0 (dimmest).
{
int hudLanternFrame = lanternFrame;
if (_flashlightOn && _atariLanternAnimationDirection == 0 && _lanternBatteryLevel >= 0)
- hudLanternFrame = 5 - _lanternBatteryLevel;
+ hudLanternFrame = _lanternBatteryLevel;
if (hudLanternFrame >= 0 && hudLanternFrame < 6 && _lanternLightSprites.size() >= 6) {
surface->copyRectToSurface(*_lanternLightSprites[hudLanternFrame], 48, 139,
Common::Rect(_lanternLightSprites[hudLanternFrame]->w, _lanternLightSprites[hudLanternFrame]->h));
Commit: 8b44c75060d95ee8b012e19b93d42ce24c3a8b68
https://github.com/scummvm/scummvm/commit/8b44c75060d95ee8b012e19b93d42ce24c3a8b68
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-04-10T21:12:35+02:00
Commit Message:
FREESCAPE: support more eclipse atarist releases
Changed paths:
engines/freescape/detection.cpp
engines/freescape/freescape.h
engines/freescape/games/driller/atari.cpp
engines/freescape/games/driller/driller.h
engines/freescape/games/eclipse/atari.cpp
engines/freescape/loaders/8bitBinaryLoader.cpp
diff --git a/engines/freescape/detection.cpp b/engines/freescape/detection.cpp
index d69807c680d..5e6011b0196 100644
--- a/engines/freescape/detection.cpp
+++ b/engines/freescape/detection.cpp
@@ -766,6 +766,20 @@ static const ADGameDescription gameDescriptions[] = {
ADGF_UNSUPPORTED,
GUIO2(GUIO_NOMIDI, GUIO_RENDERAMIGA)
},
+ {
+ // Virtual Worlds release
+ "totaleclipse",
+ "",
+ {
+ {"TEC.ALL", 0, "e71e07b7d20ea9888dd0b35d66d30ec3", 48128},
+ {"1.TEC", 0, "7bc83a8293f3a7e09a827db1c278c8bc", 122620},
+ AD_LISTEND
+ },
+ Common::EN_ANY,
+ Common::kPlatformAtariST,
+ ADGF_TESTING,
+ GUIO4(GUIO_NOMIDI, GUIO_RENDERATARIST, GAMEOPTION_MODERN_MOVEMENT, GAMEOPTION_WASD_CONTROLS)
+ },
{
"totaleclipse",
"",
@@ -776,8 +790,8 @@ static const ADGameDescription gameDescriptions[] = {
},
Common::EN_ANY,
Common::kPlatformAtariST,
- ADGF_UNSTABLE,
- GUIO2(GUIO_NOMIDI, GUIO_RENDERATARIST)
+ ADGF_TESTING,
+ GUIO4(GUIO_NOMIDI, GUIO_RENDERATARIST, GAMEOPTION_MODERN_MOVEMENT, GAMEOPTION_WASD_CONTROLS)
},
{
// Stampede Atari, Issue 7
@@ -790,7 +804,7 @@ static const ADGameDescription gameDescriptions[] = {
},
Common::EN_ANY,
Common::kPlatformAtariST,
- ADGF_UNSUPPORTED,
+ ADGF_UNSTABLE,
GUIO2(GUIO_NOMIDI, GUIO_RENDERATARIST)
},
diff --git a/engines/freescape/freescape.h b/engines/freescape/freescape.h
index f35e0d1501b..f63766a0255 100644
--- a/engines/freescape/freescape.h
+++ b/engines/freescape/freescape.h
@@ -339,6 +339,7 @@ public:
void parseAmigaAtariHeader(Common::SeekableReadStream *file);
Common::SeekableReadStream *decryptFileAmigaAtari(const Common::Path &packed, const Common::Path &unpacker, uint32 unpackArrayOffset);
+ Common::SeekableReadStream *decryptFileAtariVirtualWorlds(const Common::Path &filename);
// Areas
uint16 _startArea;
diff --git a/engines/freescape/games/driller/atari.cpp b/engines/freescape/games/driller/atari.cpp
index 1819bd9a5f3..6d8e8c9345b 100644
--- a/engines/freescape/games/driller/atari.cpp
+++ b/engines/freescape/games/driller/atari.cpp
@@ -28,168 +28,6 @@
namespace Freescape {
-namespace {
-// A simple implementation of memmem, which is a non-standard GNU extension.
-const void *local_memmem(const void *haystack, size_t haystack_len, const void *needle, size_t needle_len) {
- if (needle_len == 0) {
- return haystack;
- }
- if (haystack_len < needle_len) {
- return nullptr;
- }
- const char *h = (const char *)haystack;
- for (size_t i = 0; i <= haystack_len - needle_len; ++i) {
- if (memcmp(h + i, needle, needle_len) == 0) {
- return h + i;
- }
- }
- return nullptr;
-}
-} // namespace
-
-Common::SeekableReadStream *DrillerEngine::decryptFileAtariVirtualWorlds(const Common::Path &filename) {
- Common::File file;
- if (!file.open(filename)) {
- error("Failed to open %s", filename.toString().c_str());
- }
- const int size = file.size();
- byte *data = (byte *)malloc(size);
- file.read(data, size);
-
- int start = 0;
- int valid_offset = -1;
- int chunk_size = 0;
-
- while (true) {
- const byte *found = (const byte *)local_memmem(data + start, size - start, "CBCP", 4);
- if (!found) break;
-
- int idx = found - data;
- if (idx + 8 <= size) {
- int sz = READ_BE_UINT32(data + idx + 4);
- if (sz > 0 && sz < size + 0x20000) {
- valid_offset = idx;
- chunk_size = sz;
- }
- }
- start = idx + 1;
- }
-
- if (valid_offset == -1) {
- error("No valid CBCP chunk found in %s", filename.toString().c_str());
- }
-
- const byte *payload = data + valid_offset + 8;
- const int payload_size = chunk_size;
-
- if (payload_size < 12) {
- error("Payload too short in %s", filename.toString().c_str());
- }
-
- uint32 bit_buf_init = READ_BE_UINT32(payload + payload_size - 12);
- uint32 checksum_init = READ_BE_UINT32(payload + payload_size - 8);
- uint32 decoded_size = READ_BE_UINT32(payload + payload_size - 4);
-
- byte *out_buffer = (byte *)malloc(decoded_size);
- int dst_idx = decoded_size;
-
- struct BitStream {
- const byte *_src_data;
- int _src_idx;
- uint32 _bit_buffer;
- uint32 _checksum;
- int _refill_carry;
-
- BitStream(const byte *src_data, int start_idx, uint32 bit_buffer, uint32 checksum) :
- _src_data(src_data), _src_idx(start_idx), _bit_buffer(bit_buffer), _checksum(checksum), _refill_carry(0) {}
-
- void refill() {
- if (_src_idx < 0) {
- _refill_carry = 0;
- _bit_buffer = 0x80000000;
- return;
- }
- uint32 val = READ_BE_UINT32(_src_data + _src_idx);
- _src_idx -= 4;
- _checksum ^= val;
- _refill_carry = val & 1;
- _bit_buffer = (val >> 1) | 0x80000000;
- }
-
- int getBits(int count) {
- uint32 result = 0;
- for (int i = 0; i < count; ++i) {
- int carry = _bit_buffer & 1;
- _bit_buffer >>= 1;
- if (_bit_buffer == 0) {
- refill();
- carry = _refill_carry;
- }
- result = (result << 1) | carry;
- }
- return result;
- }
- };
-
- int src_idx = payload_size - 16;
- uint32 checksum = checksum_init ^ bit_buf_init;
- BitStream bs(payload, src_idx, bit_buf_init, checksum);
-
- while (dst_idx > 0) {
- if (bs.getBits(1) == 0) {
- if (bs.getBits(1) == 1) {
- int offset = bs.getBits(8);
- for (int i = 0; i < 2; ++i) {
- dst_idx--;
- if (dst_idx >= 0) {
- out_buffer[dst_idx] = out_buffer[dst_idx + offset];
- }
- }
- } else {
- int count = bs.getBits(3) + 1;
- for (int i = 0; i < count; ++i) {
- dst_idx--;
- if (dst_idx >= 0) {
- out_buffer[dst_idx] = bs.getBits(8);
- }
- }
- }
- } else {
- int tag = bs.getBits(2);
- if (tag == 3) {
- int count = bs.getBits(8) + 9;
- for (int i = 0; i < count; ++i) {
- dst_idx--;
- if (dst_idx >= 0) {
- out_buffer[dst_idx] = bs.getBits(8);
- }
- }
- } else if (tag == 2) {
- int length = bs.getBits(8) + 1;
- int offset = bs.getBits(12);
- for (int i = 0; i < length; ++i) {
- dst_idx--;
- if (dst_idx >= 0) {
- out_buffer[dst_idx] = out_buffer[dst_idx + offset];
- }
- }
- } else {
- int bits_offset = 9 + tag;
- int length = 3 + tag;
- int offset = bs.getBits(bits_offset);
- for (int i = 0; i < length; ++i) {
- dst_idx--;
- if (dst_idx >= 0) {
- out_buffer[dst_idx] = out_buffer[dst_idx + offset];
- }
- }
- }
- }
- }
- free(data);
- return new Common::MemoryReadStream(out_buffer, decoded_size);
-}
-
Common::SeekableReadStream *DrillerEngine::decryptFileAtari(const Common::Path &filename) {
Common::File file;
file.open(filename);
diff --git a/engines/freescape/games/driller/driller.h b/engines/freescape/games/driller/driller.h
index 059b0bc1ea2..e427f1ab3d5 100644
--- a/engines/freescape/games/driller/driller.h
+++ b/engines/freescape/games/driller/driller.h
@@ -155,7 +155,6 @@ private:
Texture *_borderExtraTexture;
Common::SeekableReadStream *decryptFileAtari(const Common::Path &filename);
- Common::SeekableReadStream *decryptFileAtariVirtualWorlds(const Common::Path &filename);
};
enum DrillerReleaseFlags {
diff --git a/engines/freescape/games/eclipse/atari.cpp b/engines/freescape/games/eclipse/atari.cpp
index e8a88208f6d..9466bbe6758 100644
--- a/engines/freescape/games/eclipse/atari.cpp
+++ b/engines/freescape/games/eclipse/atari.cpp
@@ -813,10 +813,15 @@ void EclipseEngine::drawAmigaAtariSTUI(Graphics::Surface *surface) {
void EclipseEngine::loadAssetsAtariFullGame() {
Common::File file;
file.open("0.tec");
- _title = loadAndConvertNeoImage(&file, 0x17ac);
- file.close();
+ Common::SeekableReadStream *stream = nullptr;
+ if (!file.isOpen()) {
+ stream = decryptFileAtariVirtualWorlds("1.tec");
+ } else {
+ _title = loadAndConvertNeoImage(&file, 0x17ac);
+ file.close();
- Common::SeekableReadStream *stream = decryptFileAmigaAtari("1.tec", "0.tec", 0x1774 - 4 * 1024);
+ stream = decryptFileAmigaAtari("1.tec", "0.tec", 0x1774 - 4 * 1024);
+ }
parseAmigaAtariHeader(stream);
loadMessagesVariableSize(stream, 0x87a6, 28);
diff --git a/engines/freescape/loaders/8bitBinaryLoader.cpp b/engines/freescape/loaders/8bitBinaryLoader.cpp
index a69e1804c72..b63c6b4e231 100644
--- a/engines/freescape/loaders/8bitBinaryLoader.cpp
+++ b/engines/freescape/loaders/8bitBinaryLoader.cpp
@@ -1244,4 +1244,166 @@ Common::SeekableReadStream *FreescapeEngine::decryptFileAmigaAtari(const Common:
}
+namespace {
+// A simple implementation of memmem, which is a non-standard GNU extension.
+const void *local_memmem(const void *haystack, size_t haystack_len, const void *needle, size_t needle_len) {
+ if (needle_len == 0) {
+ return haystack;
+ }
+ if (haystack_len < needle_len) {
+ return nullptr;
+ }
+ const char *h = (const char *)haystack;
+ for (size_t i = 0; i <= haystack_len - needle_len; ++i) {
+ if (memcmp(h + i, needle, needle_len) == 0) {
+ return h + i;
+ }
+ }
+ return nullptr;
+}
+} // namespace
+
+Common::SeekableReadStream *FreescapeEngine::decryptFileAtariVirtualWorlds(const Common::Path &filename) {
+ Common::File file;
+ if (!file.open(filename)) {
+ error("Failed to open %s", filename.toString().c_str());
+ }
+ const int size = file.size();
+ byte *data = (byte *)malloc(size);
+ file.read(data, size);
+
+ int start = 0;
+ int valid_offset = -1;
+ int chunk_size = 0;
+
+ while (true) {
+ const byte *found = (const byte *)local_memmem(data + start, size - start, "CBCP", 4);
+ if (!found) break;
+
+ int idx = found - data;
+ if (idx + 8 <= size) {
+ int sz = READ_BE_UINT32(data + idx + 4);
+ if (sz > 0 && sz < size + 0x20000) {
+ valid_offset = idx;
+ chunk_size = sz;
+ }
+ }
+ start = idx + 1;
+ }
+
+ if (valid_offset == -1) {
+ error("No valid CBCP chunk found in %s", filename.toString().c_str());
+ }
+
+ const byte *payload = data + valid_offset + 8;
+ const int payload_size = chunk_size;
+
+ if (payload_size < 12) {
+ error("Payload too short in %s", filename.toString().c_str());
+ }
+
+ uint32 bit_buf_init = READ_BE_UINT32(payload + payload_size - 12);
+ uint32 checksum_init = READ_BE_UINT32(payload + payload_size - 8);
+ uint32 decoded_size = READ_BE_UINT32(payload + payload_size - 4);
+
+ byte *out_buffer = (byte *)malloc(decoded_size);
+ int dst_idx = decoded_size;
+
+ struct BitStream {
+ const byte *_src_data;
+ int _src_idx;
+ uint32 _bit_buffer;
+ uint32 _checksum;
+ int _refill_carry;
+
+ BitStream(const byte *src_data, int start_idx, uint32 bit_buffer, uint32 checksum) :
+ _src_data(src_data), _src_idx(start_idx), _bit_buffer(bit_buffer), _checksum(checksum), _refill_carry(0) {}
+
+ void refill() {
+ if (_src_idx < 0) {
+ _refill_carry = 0;
+ _bit_buffer = 0x80000000;
+ return;
+ }
+ uint32 val = READ_BE_UINT32(_src_data + _src_idx);
+ _src_idx -= 4;
+ _checksum ^= val;
+ _refill_carry = val & 1;
+ _bit_buffer = (val >> 1) | 0x80000000;
+ }
+
+ int getBits(int count) {
+ uint32 result = 0;
+ for (int i = 0; i < count; ++i) {
+ int carry = _bit_buffer & 1;
+ _bit_buffer >>= 1;
+ if (_bit_buffer == 0) {
+ refill();
+ carry = _refill_carry;
+ }
+ result = (result << 1) | carry;
+ }
+ return result;
+ }
+ };
+
+ int src_idx = payload_size - 16;
+ uint32 checksum = checksum_init ^ bit_buf_init;
+ BitStream bs(payload, src_idx, bit_buf_init, checksum);
+
+ while (dst_idx > 0) {
+ if (bs.getBits(1) == 0) {
+ if (bs.getBits(1) == 1) {
+ int offset = bs.getBits(8);
+ for (int i = 0; i < 2; ++i) {
+ dst_idx--;
+ if (dst_idx >= 0) {
+ out_buffer[dst_idx] = out_buffer[dst_idx + offset];
+ }
+ }
+ } else {
+ int count = bs.getBits(3) + 1;
+ for (int i = 0; i < count; ++i) {
+ dst_idx--;
+ if (dst_idx >= 0) {
+ out_buffer[dst_idx] = bs.getBits(8);
+ }
+ }
+ }
+ } else {
+ int tag = bs.getBits(2);
+ if (tag == 3) {
+ int count = bs.getBits(8) + 9;
+ for (int i = 0; i < count; ++i) {
+ dst_idx--;
+ if (dst_idx >= 0) {
+ out_buffer[dst_idx] = bs.getBits(8);
+ }
+ }
+ } else if (tag == 2) {
+ int length = bs.getBits(8) + 1;
+ int offset = bs.getBits(12);
+ for (int i = 0; i < length; ++i) {
+ dst_idx--;
+ if (dst_idx >= 0) {
+ out_buffer[dst_idx] = out_buffer[dst_idx + offset];
+ }
+ }
+ } else {
+ int bits_offset = 9 + tag;
+ int length = 3 + tag;
+ int offset = bs.getBits(bits_offset);
+ for (int i = 0; i < length; ++i) {
+ dst_idx--;
+ if (dst_idx >= 0) {
+ out_buffer[dst_idx] = out_buffer[dst_idx + offset];
+ }
+ }
+ }
+ }
+ }
+ free(data);
+ return new Common::MemoryReadStream(out_buffer, decoded_size);
+}
+
} // namespace Freescape
Commit: adf891981d7c26f8d1e5dbd71e25e8d3426d9186
https://github.com/scummvm/scummvm/commit/adf891981d7c26f8d1e5dbd71e25e8d3426d9186
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-04-10T21:12:35+02:00
Commit Message:
FREESCAPE: initial support for eclipse amiga releases
Changed paths:
A engines/freescape/games/eclipse/amiga.cpp
engines/freescape/detection.cpp
engines/freescape/games/eclipse/atari.cpp
engines/freescape/games/eclipse/eclipse.cpp
engines/freescape/games/eclipse/eclipse.h
engines/freescape/games/palettes.cpp
engines/freescape/module.mk
diff --git a/engines/freescape/detection.cpp b/engines/freescape/detection.cpp
index 5e6011b0196..23d98d3b388 100644
--- a/engines/freescape/detection.cpp
+++ b/engines/freescape/detection.cpp
@@ -749,7 +749,7 @@ static const ADGameDescription gameDescriptions[] = {
},
Common::EN_ANY,
Common::kPlatformAmiga,
- ADGF_UNSUPPORTED,
+ ADGF_UNSTABLE,
GUIO2(GUIO_NOMIDI, GUIO_RENDERAMIGA)
},
{
@@ -763,7 +763,7 @@ static const ADGameDescription gameDescriptions[] = {
},
Common::EN_ANY,
Common::kPlatformAmiga,
- ADGF_UNSUPPORTED,
+ ADGF_UNSTABLE,
GUIO2(GUIO_NOMIDI, GUIO_RENDERAMIGA)
},
{
diff --git a/engines/freescape/games/eclipse/amiga.cpp b/engines/freescape/games/eclipse/amiga.cpp
new file mode 100644
index 00000000000..ade5fbf9088
--- /dev/null
+++ b/engines/freescape/games/eclipse/amiga.cpp
@@ -0,0 +1,221 @@
+
+/* 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 "common/file.h"
+#include "common/memstream.h"
+
+#include "freescape/freescape.h"
+#include "freescape/games/eclipse/eclipse.h"
+#include "freescape/language/8bitDetokeniser.h"
+
+namespace Freescape {
+
+extern const byte kBorderPalette[16 * 3];
+extern const int8 kAtariCompassPhaseToFrame[];
+extern const int kAtariCompassPhaseCount;
+extern const int kAtariCompassBaseFrames;
+extern const int kAtariCompassTotalFrames;
+extern const uint32 kAtariAreaRecordBase;
+extern const uint32 kAtariAreaIndexBase;
+extern const int kAtariAreaIndexCount;
+extern const uint16 kAtariDarkAreaFlag;
+
+Graphics::ManagedSurface *loadAtariSTRawSprite(Common::SeekableReadStream *stream,
+ int pixelOffset, int cols, int rows);
+Graphics::ManagedSurface *loadAtariSTSprite(Common::SeekableReadStream *stream,
+ int maskOffset, int pixelOffset, int cols, int rows);
+Graphics::ManagedSurface *loadAtariSTNeedleSprite(Common::SeekableReadStream *stream,
+ int pixelOffset, const Graphics::PixelFormat &format);
+void buildAtariSTCompassMirrorCache(Common::SeekableReadStream *stream, int pixelOffset,
+ Common::Array<Graphics::ManagedSurface *> &sprites, const Graphics::PixelFormat &format);
+bool containsAtariAreaID(const Common::Array<uint16> &areaIDs, uint16 areaID);
+
+void EclipseEngine::loadAssetsAmigaFullGame() {
+ // Decrypt 1.tec using 0.tec as the unpacker (unpack array at file offset $454).
+ Common::SeekableReadStream *stream = decryptFileAmigaAtari("1.tec", "0.tec", 0x454);
+
+ parseAmigaAtariHeader(stream);
+
+ // The Amiga decrypted stream shares identical game data with the Atari ST
+ // binary, but at a consistent offset delta of +0xC87A for most resources.
+ // Messages and the eclipse fade palette are at different locations.
+ static const int kAmigaDelta = 0xC87A;
+
+ loadMessagesVariableSize(stream, 0x9864, 28);
+ load8bitBinary(stream, 0x2a53c + kAmigaDelta, 16);
+
+ _border = loadAndConvertNeoImage(stream, 0x139c8 + kAmigaDelta);
+ loadPalettes(stream, 0x29e52 + kAmigaDelta);
+
+ // Colors 0-5 from border palette (Copper list palette split, same as Atari ST Timer-B)
+ for (auto &entry : _paletteByArea) {
+ byte *pal = entry._value;
+ memcpy(pal, kBorderPalette, 6 * 3);
+ }
+
+ loadSoundsFx(stream, 0x3030c + kAmigaDelta, 6);
+
+ // UI font (Font A): same 4-plane format as Atari ST
+ Common::Array<Graphics::ManagedSurface *> chars;
+ chars = getChars4Plane(stream, 0x24C5A + kAmigaDelta, 85);
+ _font = Font(chars);
+
+ // Score font (Font B)
+ Common::Array<Graphics::ManagedSurface *> scoreChars;
+ scoreChars = getChars4Plane(stream, 0x249DA + kAmigaDelta, 11);
+ _fontScore = Font(scoreChars);
+
+ // Sprite offsets: Atari prog addr + 0x1C (GEMDOS hdr) + kAmigaDelta
+ static const int kHdr = 0x1C + kAmigaDelta;
+
+ _atariDarkAreas.clear();
+ Common::Array<uint16> parsedAreas;
+ uint32 streamSize = stream->size();
+ for (int i = 0; i < kAtariAreaIndexCount; i++) {
+ uint32 indexOffset = kAtariAreaIndexBase + kHdr + i * 4;
+ if (indexOffset + 4 > streamSize)
+ break;
+
+ stream->seek(indexOffset);
+ uint16 lowWord = stream->readUint16BE();
+ uint16 highWord = stream->readUint16BE();
+ uint32 recordWordOffset = lowWord | ((uint32)highWord << 8);
+ uint32 recordOffset = kAtariAreaRecordBase + kHdr + recordWordOffset * 2;
+ if (recordOffset + 6 > streamSize)
+ continue;
+
+ stream->seek(recordOffset);
+ uint16 flags = stream->readUint16BE();
+ stream->readUint16BE();
+ uint16 areaID = stream->readUint16BE();
+ if (!_areaMap.contains(areaID) || containsAtariAreaID(parsedAreas, areaID))
+ continue;
+
+ parsedAreas.push_back(areaID);
+ if ((flags & kAtariDarkAreaFlag) != 0)
+ _atariDarkAreas.push_back(areaID);
+ }
+
+ // Eclipse fade palettes for dark areas (at a different offset than the main delta)
+ for (int level = 0; level < 6; level++) {
+ stream->seek(0xFE66 + level * 32);
+ byte *pal = loadPalette(stream);
+ memcpy(_eclipseFadePalettes[level], pal, 16 * 3);
+ delete[] pal;
+ }
+
+ for (uint i = 0; i < _atariDarkAreas.size(); i++)
+ applyEclipseFadePalette(_atariDarkAreas[i], _lanternBatteryLevel);
+
+ // Heart indicator sprites
+ _eclipseSprites.resize(2);
+ _eclipseSprites[0] = loadAtariSTSprite(stream, 0x1D2BE + kHdr, 0x1D2C0 + kHdr, 1, 13);
+ _eclipseSprites[0]->convertToInPlace(_gfx->_texturePixelFormat,
+ const_cast<byte *>(kBorderPalette), 16);
+ _eclipseSprites[1] = loadAtariSTSprite(stream, 0x1D2BE + kHdr, 0x1D2C0 + 104 + kHdr, 1, 13);
+ _eclipseSprites[1]->convertToInPlace(_gfx->_texturePixelFormat,
+ const_cast<byte *>(kBorderPalette), 16);
+
+ // Eclipse progress indicator: 16 frames
+ _eclipseProgressSprites.resize(16);
+ for (int i = 0; i < 16; i++) {
+ _eclipseProgressSprites[i] = loadAtariSTSprite(stream, 0x1DA96 + kHdr, 0x1DA98 + kHdr + i * 128, 1, 16);
+ _eclipseProgressSprites[i]->convertToInPlace(_gfx->_texturePixelFormat,
+ const_cast<byte *>(kBorderPalette), 16);
+ }
+
+ // Ankh indicator: 5 fade-in frames
+ _ankhSprites.resize(5);
+ for (int i = 0; i < 5; i++) {
+ _ankhSprites[i] = loadAtariSTSprite(stream, 0x1B732 + kHdr, 0x1B734 + kHdr + i * 120, 1, 15);
+ _ankhSprites[i]->convertToInPlace(_gfx->_texturePixelFormat,
+ const_cast<byte *>(kBorderPalette), 16);
+ }
+
+ // Compass
+ _compassBackground = loadAtariSTRawSprite(stream, 0x20986 + kHdr, 2, 27);
+ _compassBackground->convertToInPlace(_gfx->_texturePixelFormat,
+ const_cast<byte *>(kBorderPalette), 16);
+
+ {
+ Common::copy(kAtariCompassPhaseToFrame, kAtariCompassPhaseToFrame + kAtariCompassPhaseCount, _compassLookup);
+
+ _compassSprites.resize(kAtariCompassTotalFrames);
+ for (int frame = 0; frame < kAtariCompassBaseFrames; frame++) {
+ _compassSprites[frame] = loadAtariSTNeedleSprite(stream,
+ 0x20B36 + kHdr + frame * 432, _gfx->_texturePixelFormat);
+ }
+ buildAtariSTCompassMirrorCache(stream, 0x20B36 + kHdr, _compassSprites, _gfx->_texturePixelFormat);
+ }
+
+ // Lantern light animation: 6 frames
+ _lanternLightSprites.resize(6);
+ for (int i = 0; i < 6; i++) {
+ _lanternLightSprites[i] = loadAtariSTRawSprite(stream, 0x2026A + kHdr + i * 96, 2, 6);
+ _lanternLightSprites[i]->convertToInPlace(_gfx->_texturePixelFormat,
+ const_cast<byte *>(kBorderPalette), 16);
+ }
+
+ // Lantern switch: 2 frames
+ _lanternSwitchSprites.resize(2);
+ for (int i = 0; i < 2; i++) {
+ _lanternSwitchSprites[i] = loadAtariSTRawSprite(stream, 0x204B4 + kHdr + i * 0x170, 2, 23);
+ _lanternSwitchSprites[i]->convertToInPlace(_gfx->_texturePixelFormat,
+ const_cast<byte *>(kBorderPalette), 16);
+ }
+
+ // Sound toggle: 5 frames
+ _soundToggleSprites.resize(5);
+ for (int i = 0; i < 5; i++) {
+ _soundToggleSprites[i] = loadAtariSTRawSprite(stream, 0x20794 + kHdr + i * 96, 1, 11);
+ _soundToggleSprites[i]->convertToInPlace(_gfx->_texturePixelFormat,
+ const_cast<byte *>(kBorderPalette), 16);
+ }
+
+ // Water body bitmap
+ _atariWaterBody = loadAtariSTRawSprite(stream, 0x2003C + kHdr, 2, 31);
+ _atariWaterBody->convertToInPlace(_gfx->_texturePixelFormat,
+ const_cast<byte *>(kBorderPalette), 16);
+
+ stream->seek(0x2024C + kHdr);
+ _atariWaterSurfacePixels.resize(8);
+ for (int w = 0; w < 8; w++)
+ _atariWaterSurfacePixels[w] = stream->readUint16BE();
+
+ _atariWaterSurfaceMask.resize(2);
+ stream->seek(0x2025C + kHdr);
+ for (int w = 0; w < 2; w++)
+ _atariWaterSurfaceMask[w] = stream->readUint16BE();
+
+ // Shooting crosshair: 2 frames
+ _shootSprites.resize(2);
+ _shootSprites[0] = loadAtariSTSprite(stream, 0x1CC2C + kHdr, 0x1CC30 + kHdr, 2, 25);
+ _shootSprites[0]->convertToInPlace(_gfx->_texturePixelFormat,
+ const_cast<byte *>(kBorderPalette), 16);
+ _shootSprites[1] = loadAtariSTSprite(stream, 0x1CDC6 + kHdr, 0x1CDCC + kHdr, 3, 25);
+ _shootSprites[1]->convertToInPlace(_gfx->_texturePixelFormat,
+ const_cast<byte *>(kBorderPalette), 16);
+
+ delete stream;
+}
+
+} // End of namespace Freescape
diff --git a/engines/freescape/games/eclipse/atari.cpp b/engines/freescape/games/eclipse/atari.cpp
index 9466bbe6758..0b33e3668fc 100644
--- a/engines/freescape/games/eclipse/atari.cpp
+++ b/engines/freescape/games/eclipse/atari.cpp
@@ -29,9 +29,9 @@
namespace Freescape {
-const int kAtariCompassPhaseCount = 72;
-const int kAtariCompassBaseFrames = 19;
-const int kAtariCompassTotalFrames = 37;
+extern const int kAtariCompassPhaseCount = 72;
+extern const int kAtariCompassBaseFrames = 19;
+extern const int kAtariCompassTotalFrames = 37;
const int kAtariClockCenterX = 106;
const int kAtariClockCenterY = 159;
const int kAtariCompassX = 176;
@@ -48,16 +48,16 @@ const int kAtariWaterIndicatorMaxLevel = 29;
// scales this down proportionally.
const int kAtariDarkLightRadius = 80;
const int kAtariDarkLightRadiusStep = 10;
-const uint32 kAtariAreaRecordBase = 0x2A520;
-const uint32 kAtariAreaIndexBase = 0x2A6B0;
-const int kAtariAreaIndexCount = 64;
-const uint16 kAtariDarkAreaFlag = 0x8000;
+extern const uint32 kAtariAreaRecordBase = 0x2A520;
+extern const uint32 kAtariAreaIndexBase = 0x2A6B0;
+extern const int kAtariAreaIndexCount = 64;
+extern const uint16 kAtariDarkAreaFlag = 0x8000;
void fillCircle(Graphics::Surface *surface, int x, int y, int radius, int color);
// Repaired ST phase-to-frame table from $1542. Six corrupt bytes in the dumped
// binary break the intended 37-frame sweep built from $20B36 and $22B46.
-const int8 kAtariCompassPhaseToFrame[kAtariCompassPhaseCount] = {
+extern const int8 kAtariCompassPhaseToFrame[kAtariCompassPhaseCount] = {
0, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35,
36, 35, 34, 33, 32, 31, 30, 29, 28, 27, 26, 25, 24, 23, 22, 21, 20, 19,
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 17, 16,
@@ -231,7 +231,7 @@ void EclipseEngine::drawCPCUI(Graphics::Surface *surface) {
// Border palette from CONSOLE.NEO (Atari ST 9-bit $0RGB, scaled to 8-bit).
// Used for font rendering and sprite conversion.
-static const byte kBorderPalette[16 * 3] = {
+extern const byte kBorderPalette[16 * 3] = {
0, 0, 0, // 0: $000 black
145, 72, 0, // 1: $420 dark brown
182, 109, 36, // 2: $531 medium brown
@@ -251,7 +251,7 @@ static const byte kBorderPalette[16 * 3] = {
};
// Load raw 4-plane pixel data (no mask) from stream into a CLUT8 surface.
-static Graphics::ManagedSurface *loadAtariSTRawSprite(Common::SeekableReadStream *stream,
+Graphics::ManagedSurface *loadAtariSTRawSprite(Common::SeekableReadStream *stream,
int pixelOffset, int cols, int rows) {
stream->seek(pixelOffset);
Graphics::ManagedSurface *surface = new Graphics::ManagedSurface();
diff --git a/engines/freescape/games/eclipse/eclipse.cpp b/engines/freescape/games/eclipse/eclipse.cpp
index 0906ef80767..4af6509e1e3 100644
--- a/engines/freescape/games/eclipse/eclipse.cpp
+++ b/engines/freescape/games/eclipse/eclipse.cpp
@@ -140,7 +140,7 @@ void EclipseEngine::restartBackgroundMusic() {
_playerAYMusic->startMusic();
} else if (isDOS() && _playerOPLMusic) {
_playerOPLMusic->startMusic();
- } else if (isAtariST() && !_musicData.empty()) {
+ } else if ((isAtariST() || isAmiga()) && !_musicData.empty()) {
if (_mixer)
_mixer->stopHandle(_musicHandle);
Audio::AudioStream *musicStream = makeEclipseAtariMusicStream(
@@ -412,7 +412,7 @@ void EclipseEngine::gotoArea(uint16 areaID, int entranceID) {
assert(_areaMap.contains(areaID));
_currentArea = _areaMap[areaID];
_currentArea->show();
- _atariAreaDark = isAtariST() && isAtariDarkArea(areaID);
+ _atariAreaDark = (isAtariST() || isAmiga()) && isAtariDarkArea(areaID);
_currentAreaMessages.clear();
_currentAreaMessages.push_back(_currentArea->_name);
@@ -446,7 +446,7 @@ void EclipseEngine::gotoArea(uint16 areaID, int entranceID) {
}
_gfx->_keyColor = 0;
- if (isAtariST() && isAtariDarkArea(areaID))
+ if ((isAtariST() || isAmiga()) && isAtariDarkArea(areaID))
applyEclipseFadePalette(areaID, _lanternBatteryLevel);
swapPalette(areaID);
if (isCPC())
@@ -454,7 +454,7 @@ void EclipseEngine::gotoArea(uint16 areaID, int entranceID) {
if (isAmiga() || isAtariST())
_currentArea->_skyColor = 15;
- if (isAtariST() && entranceID > 0) {
+ if ((isAtariST() || isAmiga()) && entranceID > 0) {
Entrance *entrance = (Entrance *)_currentArea->entranceWithID(entranceID);
if (entrance) {
int phase = atariCompassPhaseFromRotationY(entrance->getRotation().y());
@@ -466,8 +466,8 @@ void EclipseEngine::gotoArea(uint16 areaID, int entranceID) {
}
}
- // Start background music (Atari ST)
- if (isAtariST() && !_musicData.empty() && !_mixer->isSoundHandleActive(_musicHandle)) {
+ // Start background music (Atari ST / Amiga)
+ if ((isAtariST() || isAmiga()) && !_musicData.empty() && !_mixer->isSoundHandleActive(_musicHandle)) {
Audio::AudioStream *musicStream = makeEclipseAtariMusicStream(
_musicData.data(), _musicData.size(), 1);
if (musicStream) {
@@ -671,7 +671,7 @@ void EclipseEngine::pressedKey(const int keycode) {
_pitch = 0;
updateCamera();
} else if (keycode == kActionToggleFlashlight) {
- if (isAtariST()) {
+ if (isAtariST() || isAmiga()) {
if (_flashlightOn) {
_flashlightOn = false;
if (_atariLanternLightFrame < 0)
@@ -700,7 +700,7 @@ void EclipseEngine::onRotate(float xoffset, float yoffset, float zoffset) {
(void)yoffset;
(void)zoffset;
- if (!isAtariST() || xoffset == 0.0f)
+ if ((!isAtariST() && !isAmiga()) || xoffset == 0.0f)
return;
if (!_atariCompassPhaseInitialized) {
@@ -1100,7 +1100,7 @@ void EclipseEngine::drawScoreString(int score, int x, int y, uint32 front, uint3
// Font B has 10 glyphs (0-9) for digits. In the original, the score bytes
// have $2F subtracted to map '0'âglyph 0, '1'âglyph 1, etc.
// For drawChar: chr = glyph_index + 32, so digit '0' â chr 32, '9' â chr 41.
- if (isAtariST()) {
+ if (isAtariST() || isAmiga()) {
_fontScore.setBackground(back);
_fontScore.setSecondaryColor(front);
// Font B uses palette indices 1-4 like Font A
@@ -1170,7 +1170,7 @@ void EclipseEngine::updateTimeVariables() {
// Lantern battery drain: non-rechargeable, one level per 30-second tick
// while the flashlight is on. ROM drains TeLanternBrightnessFrame ($7f6c)
// from 5 (brightest) down to -1 (dead). 6 levels total.
- if (isAtariST() && _flashlightOn && _lanternBatteryLevel >= 0) {
+ if ((isAtariST() || isAmiga()) && _flashlightOn && _lanternBatteryLevel >= 0) {
_lanternBatteryLevel--;
if (_lanternBatteryLevel < 0) {
_flashlightOn = false;
@@ -1236,7 +1236,7 @@ Common::Error EclipseEngine::loadGameStreamExtended(Common::SeekableReadStream *
_atariLanternAnimationDirection = 0;
_atariLanternLightFrame = _flashlightOn ? 0 : -1;
_atariLanternLastUpdateTick = -1;
- _atariAreaDark = isAtariST() && _currentArea && isAtariDarkArea(_currentArea->getAreaID());
+ _atariAreaDark = (isAtariST() || isAmiga()) && _currentArea && isAtariDarkArea(_currentArea->getAreaID());
return Common::kNoError;
}
diff --git a/engines/freescape/games/eclipse/eclipse.h b/engines/freescape/games/eclipse/eclipse.h
index 1d30ba4883b..2ae3b43b1ba 100644
--- a/engines/freescape/games/eclipse/eclipse.h
+++ b/engines/freescape/games/eclipse/eclipse.h
@@ -85,6 +85,7 @@ public:
void loadAssetsZXFullGame() override;
void loadAssetsCPCFullGame() override;
void loadAssetsC64FullGame() override;
+ void loadAssetsAmigaFullGame() override;
void loadAssetsAtariFullGame() override;
void loadAssetsCPCDemo() override;
void loadAssetsZXDemo() override;
diff --git a/engines/freescape/games/palettes.cpp b/engines/freescape/games/palettes.cpp
index e9a2a0dc42b..bffb9800962 100644
--- a/engines/freescape/games/palettes.cpp
+++ b/engines/freescape/games/palettes.cpp
@@ -218,8 +218,8 @@ void FreescapeEngine::loadPalettes(Common::SeekableReadStream *file, int offset)
debugC(1, kFreescapeDebugParser, "Color %d: (%04x) %02x %02x %02x", c, v, palette[c][0], palette[c][1], palette[c][2]);
}
if (_paletteByArea.contains(label)) {
- // Eclipse Atari ST has a duplicate palette entry for area 42
- assert(isEclipse() && isAtariST());
+ // Eclipse Atari ST / Amiga has a duplicate palette entry for area 42
+ assert(isEclipse() && (isAtariST() || isAmiga()));
delete[] palette;
continue;
}
diff --git a/engines/freescape/module.mk b/engines/freescape/module.mk
index de18151c54d..363ddc47f30 100644
--- a/engines/freescape/module.mk
+++ b/engines/freescape/module.mk
@@ -34,6 +34,7 @@ MODULE_OBJS := \
games/driller/driller.o \
games/driller/sounds.o \
games/driller/zx.o \
+ games/eclipse/amiga.o \
games/eclipse/atari.o \
games/eclipse/atari.music.o \
games/eclipse/c64.o \
Commit: 0f28460d4a7310edbc58a4d536ac9f88e41e7b14
https://github.com/scummvm/scummvm/commit/0f28460d4a7310edbc58a4d536ac9f88e41e7b14
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-04-10T21:12:35+02:00
Commit Message:
FREESCAPE: initial support music in eclipse amiga
Changed paths:
engines/freescape/games/eclipse/amiga.cpp
engines/freescape/games/eclipse/eclipse.cpp
engines/freescape/wb.cpp
engines/freescape/wb.h
diff --git a/engines/freescape/games/eclipse/amiga.cpp b/engines/freescape/games/eclipse/amiga.cpp
index ade5fbf9088..f8bb9708554 100644
--- a/engines/freescape/games/eclipse/amiga.cpp
+++ b/engines/freescape/games/eclipse/amiga.cpp
@@ -74,6 +74,18 @@ void EclipseEngine::loadAssetsAmigaFullGame() {
loadSoundsFx(stream, 0x3030c + kAmigaDelta, 6);
+ // Load TEMUSIC.AM â Wally Beben custom Paula engine (same family as Dark Side)
+ // GEMDOS executable embedded at stream offset 0x10F5E, TEXT size 0xEB20
+ {
+ static const uint32 kTEMusicAmigaOffset = 0x10F5E;
+ static const uint32 kGemdosHeaderSize = 0x1C;
+ static const uint32 kTEMusicAmigaTextSize = 0xEB20;
+ stream->seek(kTEMusicAmigaOffset + kGemdosHeaderSize);
+ _musicData.resize(kTEMusicAmigaTextSize);
+ stream->read(_musicData.data(), kTEMusicAmigaTextSize);
+ debug(3, "TE-Amiga: Loaded TEMUSIC.AM TEXT segment (%d bytes)", kTEMusicAmigaTextSize);
+ }
+
// UI font (Font A): same 4-plane format as Atari ST
Common::Array<Graphics::ManagedSurface *> chars;
chars = getChars4Plane(stream, 0x24C5A + kAmigaDelta, 85);
diff --git a/engines/freescape/games/eclipse/eclipse.cpp b/engines/freescape/games/eclipse/eclipse.cpp
index 4af6509e1e3..23c9c54eb0b 100644
--- a/engines/freescape/games/eclipse/eclipse.cpp
+++ b/engines/freescape/games/eclipse/eclipse.cpp
@@ -30,6 +30,7 @@
#include "common/translation.h"
#include "freescape/freescape.h"
+#include "freescape/wb.h"
#include "freescape/games/eclipse/c64.music.h"
#include "freescape/games/eclipse/c64.sfx.h"
#include "freescape/games/eclipse/ay.music.h"
@@ -44,6 +45,18 @@ namespace Freescape {
Audio::AudioStream *makeEclipseAtariMusicStream(const byte *data, uint32 dataSize,
int songNum = 1, int rate = 44100);
+// Wally Beben table offsets for Total Eclipse Amiga TEMUSIC.AM
+static const WBTableOffsets kEclipseAmigaMusicOffsets = {
+ 0x0ACA, // periodTable
+ 0x0C5E, // samplePtrTable
+ 0x0CA6, // instrumentTable
+ 0x0D16, // arpeggioIntervals
+ 0x0D1E, // envelopeTable
+ 0x0D8E, // songTable
+ 0x0D9E, // patternPtrTable (songTable + 16, overlaps Song 2 like Dark Side)
+ 14, 14, 14 // numSamples, numInstruments, numEnvelopes
+};
+
EclipseEngine::EclipseEngine(OSystem *syst, const ADGameDescription *gd) : FreescapeEngine(syst, gd) {
_playerC64Music = nullptr;
_playerC64Sfx = nullptr;
@@ -143,8 +156,14 @@ void EclipseEngine::restartBackgroundMusic() {
} else if ((isAtariST() || isAmiga()) && !_musicData.empty()) {
if (_mixer)
_mixer->stopHandle(_musicHandle);
- Audio::AudioStream *musicStream = makeEclipseAtariMusicStream(
- _musicData.data(), _musicData.size(), 1);
+ Audio::AudioStream *musicStream = nullptr;
+ if (isAmiga())
+ musicStream = makeWallyBebenStream(
+ _musicData.data(), _musicData.size(), 1, 44100, true,
+ &kEclipseAmigaMusicOffsets);
+ else
+ musicStream = makeEclipseAtariMusicStream(
+ _musicData.data(), _musicData.size(), 1);
if (musicStream) {
_mixer->playStream(Audio::Mixer::kMusicSoundType,
&_musicHandle, musicStream);
@@ -468,8 +487,14 @@ void EclipseEngine::gotoArea(uint16 areaID, int entranceID) {
// Start background music (Atari ST / Amiga)
if ((isAtariST() || isAmiga()) && !_musicData.empty() && !_mixer->isSoundHandleActive(_musicHandle)) {
- Audio::AudioStream *musicStream = makeEclipseAtariMusicStream(
- _musicData.data(), _musicData.size(), 1);
+ Audio::AudioStream *musicStream = nullptr;
+ if (isAmiga())
+ musicStream = makeWallyBebenStream(
+ _musicData.data(), _musicData.size(), 1, 44100, true,
+ &kEclipseAmigaMusicOffsets);
+ else
+ musicStream = makeEclipseAtariMusicStream(
+ _musicData.data(), _musicData.size(), 1);
if (musicStream) {
_mixer->playStream(Audio::Mixer::kMusicSoundType,
&_musicHandle, musicStream);
diff --git a/engines/freescape/wb.cpp b/engines/freescape/wb.cpp
index dfa2ce8d429..3083b68d98d 100644
--- a/engines/freescape/wb.cpp
+++ b/engines/freescape/wb.cpp
@@ -87,32 +87,42 @@ byte buildArpeggioTable(const byte intervals[8], byte mask, byte *outTable, byte
} // End of namespace WBCommon
-// TEXT-relative offsets for data tables within HDSMUSIC.AM
+// Default TEXT-relative offsets for Dark Side HDSMUSIC.AM
// All addresses verified against disassembly of the 68000 code.
-static const uint32 kPeriodTableOffset = 0x0AAE; // 48 x uint16 BE (note 0=silence, 1-47=C-1..B-3)
-static const uint32 kSamplePtrTableOffset = 0x0C42; // 16 x uint32 BE (TEXT-relative PCM offsets)
-static const uint32 kInstrumentTableOffset = 0x0C82; // 16 x 8 bytes (sample#, loopFlag, len, loopOff, loopLen)
-static const uint32 kArpeggioIntervalsOffset = 0x0D02; // 8 bytes (semitone offsets for arpeggio bitmask)
-static const uint32 kEnvelopeTableOffset = 0x0D0A; // 10 x 8 bytes (atk, dec, fadeRate, rel, mod, vib, arp, flags)
-static const uint32 kSongTableOffset = 0x0DBA; // 2 songs x 4 channels x uint32 BE order-list pointers
-static const uint32 kPatternPtrTableOffset = 0x0DCA; // up to 128 x uint32 BE (overlaps Song 2 order ptrs)
-static const uint32 kMaxPatternEntries = 128;
+static const WBTableOffsets kDarkSideOffsets = {
+ 0x0AAE, // periodTable: 48 x uint16 BE
+ 0x0C42, // samplePtrTable: 16 x uint32 BE
+ 0x0C82, // instrumentTable: 16 x 8 bytes
+ 0x0D02, // arpeggioIntervals: 8 bytes
+ 0x0D0A, // envelopeTable: 10 x 8 bytes
+ 0x0DBA, // songTable: 2 songs x 4 channels
+ 0x0DCA, // patternPtrTable: up to 128 x uint32 BE
+ 16, 16, 10 // numSamples, numInstruments, numEnvelopes
+};
+
+static const uint32 kMaxPatternEntries = 128;
class WallyBebenStream : public Audio::Paula {
public:
- WallyBebenStream(const byte *data, uint32 dataSize, int songNum, int rate, bool stereo);
+ WallyBebenStream(const byte *data, uint32 dataSize, int songNum, int rate, bool stereo,
+ const WBTableOffsets &offsets);
~WallyBebenStream() override;
private:
void interrupt() override;
- // --- Data tables (parsed from HDSMUSIC.AM TEXT segment) ---
+ // --- Data tables (parsed from TEXT segment) ---
const byte *_data;
uint32 _dataSize;
+ WBTableOffsets _offsets;
uint16 _periods[48];
+ static const int kMaxSamples = 16;
+ static const int kMaxInstruments = 16;
+ static const int kMaxEnvelopes = 16;
+
struct InstrumentDesc {
byte sampleIndex;
byte loopFlag;
@@ -120,7 +130,7 @@ private:
uint16 loopOffset;
uint16 loopLength;
};
- InstrumentDesc _instruments[16];
+ InstrumentDesc _instruments[kMaxInstruments];
struct EnvelopeDesc {
byte attackLevel;
@@ -132,10 +142,10 @@ private:
byte arpeggioMask;
byte flags;
};
- EnvelopeDesc _envelopes[10];
+ EnvelopeDesc _envelopes[kMaxEnvelopes];
// Sample offsets within the data buffer
- uint32 _sampleOffsets[16];
+ uint32 _sampleOffsets[kMaxSamples];
// Song order list pointers (TEXT-relative)
uint32 _songOrderPtrs[2][4];
@@ -253,9 +263,10 @@ private:
// ---------------------------------------------------------------------------
WallyBebenStream::WallyBebenStream(const byte *data, uint32 dataSize,
- int songNum, int rate, bool stereo)
+ int songNum, int rate, bool stereo,
+ const WBTableOffsets &offsets)
: Paula(stereo, rate, rate / 50), // 50 Hz PAL VBI interrupt rate
- _data(data), _dataSize(dataSize),
+ _data(data), _dataSize(dataSize), _offsets(offsets),
_musicActive(false), _tickSpeed(6), _tickCounter(0),
_pendingSongCommand(-1), _numPatterns(0), _firstSampleOffset(0) {
@@ -282,20 +293,19 @@ WallyBebenStream::~WallyBebenStream() {
}
void WallyBebenStream::loadTables() {
- // Period table: 48 x uint16 BE at TEXT+$AAE
+ // Period table: 48 x uint16 BE
for (int i = 0; i < 48; i++) {
- _periods[i] = readDataWord(kPeriodTableOffset + i * 2);
+ _periods[i] = readDataWord(_offsets.periodTable + i * 2);
}
- // Sample pointer table: 16 x uint32 BE at TEXT+$C42
- // These are TEXT-relative offsets to PCM sample data
- for (int i = 0; i < 16; i++) {
- _sampleOffsets[i] = readDataLong(kSamplePtrTableOffset + i * 4);
+ // Sample pointer table: TEXT-relative offsets to PCM sample data
+ for (int i = 0; i < _offsets.numSamples; i++) {
+ _sampleOffsets[i] = readDataLong(_offsets.samplePtrTable + i * 4);
}
- // Instrument table: 16 x 8 bytes at TEXT+$C82
- for (int i = 0; i < 16; i++) {
- uint32 off = kInstrumentTableOffset + i * 8;
+ // Instrument table
+ for (int i = 0; i < _offsets.numInstruments; i++) {
+ uint32 off = _offsets.instrumentTable + i * 8;
_instruments[i].sampleIndex = readDataByte(off + 0);
_instruments[i].loopFlag = readDataByte(off + 1);
_instruments[i].totalLength = readDataWord(off + 2);
@@ -303,14 +313,14 @@ void WallyBebenStream::loadTables() {
_instruments[i].loopLength = readDataWord(off + 6);
}
- // Arpeggio interval table: 8 bytes at TEXT+$D02
+ // Arpeggio interval table: 8 bytes
for (int i = 0; i < 8; i++) {
- _arpeggioIntervals[i] = readDataByte(kArpeggioIntervalsOffset + i);
+ _arpeggioIntervals[i] = readDataByte(_offsets.arpeggioIntervals + i);
}
- // Envelope table: 10 x 8 bytes at TEXT+$D0A
- for (int i = 0; i < 10; i++) {
- uint32 off = kEnvelopeTableOffset + i * 8;
+ // Envelope table
+ for (int i = 0; i < _offsets.numEnvelopes; i++) {
+ uint32 off = _offsets.envelopeTable + i * 8;
_envelopes[i].attackLevel = readDataByte(off + 0);
_envelopes[i].decayTarget = readDataByte(off + 1);
_envelopes[i].sustainLevel = readDataByte(off + 2);
@@ -321,47 +331,47 @@ void WallyBebenStream::loadTables() {
_envelopes[i].flags = readDataByte(off + 7);
}
- // Song table: 2 songs x 4 channels x uint32 BE at TEXT+$DBA
+ // Song table: 2 songs x 4 channels x uint32 BE
for (int s = 0; s < 2; s++) {
for (int ch = 0; ch < 4; ch++) {
- _songOrderPtrs[s][ch] = readDataLong(kSongTableOffset + s * 16 + ch * 4);
+ _songOrderPtrs[s][ch] = readDataLong(_offsets.songTable + s * 16 + ch * 4);
}
}
// Compute the first sample offset â this is the upper bound for valid
// pattern/order-list data. Anything at or above this is PCM sample data.
_firstSampleOffset = _dataSize;
- for (int i = 0; i < 16; i++) {
+ for (int i = 0; i < _offsets.numSamples; i++) {
if (_sampleOffsets[i] > 0 && _sampleOffsets[i] < _firstSampleOffset)
_firstSampleOffset = _sampleOffsets[i];
}
- // Pattern pointer table at TEXT+$DCA.
+ // Pattern pointer table.
// Valid pattern pointers must be > 0 and < _firstSampleOffset (i.e., they
// point into the song data area between the tables and the PCM samples).
// Entries at or beyond _firstSampleOffset are stale data, not real patterns.
_numPatterns = 0;
for (uint32 i = 0; i < kMaxPatternEntries; i++) {
- uint32 ptr = readDataLong(kPatternPtrTableOffset + i * 4);
+ uint32 ptr = readDataLong(_offsets.patternPtrTable + i * 4);
_patternPtrs[i] = ptr;
if (ptr > 0 && ptr < _firstSampleOffset)
_numPatterns = i + 1;
}
// Debug: dump loaded data tables for verification
- debug(3, "WB: Data loaded from HDSMUSIC.AM TEXT segment (%u bytes)", _dataSize);
+ debug(3, "WB: Data loaded from TEXT segment (%u bytes)", _dataSize);
debug(3, "WB: Period table (first 12): %d %d %d %d %d %d %d %d %d %d %d %d",
_periods[0], _periods[1], _periods[2], _periods[3],
_periods[4], _periods[5], _periods[6], _periods[7],
_periods[8], _periods[9], _periods[10], _periods[11]);
- for (int i = 0; i < 16; i++) {
+ for (int i = 0; i < _offsets.numSamples; i++) {
if (_sampleOffsets[i])
debug(3, "WB: Sample %d: offset=$%X", i, _sampleOffsets[i]);
}
- for (int i = 0; i < 16; i++) {
+ for (int i = 0; i < _offsets.numInstruments; i++) {
const InstrumentDesc &inst = _instruments[i];
if (inst.totalLength > 0)
debug(3, "WB: Inst %d: sample=%d loop=%d len=%d loopOff=%d loopLen=%d",
@@ -369,7 +379,7 @@ void WallyBebenStream::loadTables() {
inst.loopOffset, inst.loopLength);
}
- for (int i = 0; i < 10; i++) {
+ for (int i = 0; i < _offsets.numEnvelopes; i++) {
const EnvelopeDesc &env = _envelopes[i];
debug(3, "WB: Env %d: atk=%d dec=%d sus=%d rel=%d mod=%d arp=$%02X",
i, env.attackLevel, env.decayTarget, env.sustainLevel,
@@ -545,12 +555,12 @@ void WallyBebenStream::readPatternCommands(int ch) {
}
if (cmd >= 0xC0) {
- // Set envelope: low 5 bits (0-31, but only 1-9 valid)
+ // Set envelope: low 5 bits (0-31)
// Asm ref: TEXT+$2C8 â envelope command handler
// $C0 (index 0) is a no-op: Env 0 is all zeros (sentinel entry).
// The original engine treats index 0 as "no envelope change".
byte envIdx = cmd & 0x1F;
- if (envIdx == 0 || envIdx > 9) {
+ if (envIdx == 0 || envIdx >= _offsets.numEnvelopes) {
// Index 0 or out-of-range: skip, keep current envelope params
continue;
}
@@ -720,7 +730,7 @@ void WallyBebenStream::triggerNote(int ch) {
// Load instrument
const InstrumentDesc &inst = _instruments[c.instrumentIdx];
byte sampleIdx = inst.sampleIndex;
- if (sampleIdx >= 16)
+ if (sampleIdx >= _offsets.numSamples)
sampleIdx = 0;
uint32 sampleOffset = _sampleOffsets[sampleIdx];
@@ -898,13 +908,18 @@ void WallyBebenStream::processEnvelope(int ch) {
if (c.volume > c.decayTarget) {
c.volume--;
} else {
- c.volume = c.sustainLevel;
c.envelopePhase = 2;
}
break;
- case 2: // Sustain â hold sustain level until note-off.
- c.volume = c.sustainLevel;
+ case 2: // Sustain â hold at decay target; sustainLevel is the fade rate
+ // (0 = hold forever, >0 = fade by sustainLevel per tick toward 0).
+ if (c.sustainLevel > 0) {
+ if (c.volume > c.sustainLevel)
+ c.volume -= c.sustainLevel;
+ else
+ c.volume = 0;
+ }
break;
case 3: // Release â decrease volume on note-off
@@ -1034,13 +1049,15 @@ void WallyBebenStream::interrupt() {
// ---------------------------------------------------------------------------
Audio::AudioStream *makeWallyBebenStream(const byte *data, uint32 dataSize,
- int songNum, int rate, bool stereo) {
- if (!data || dataSize < 0xF000) {
+ int songNum, int rate, bool stereo,
+ const WBTableOffsets *offsets) {
+ if (!data || dataSize < 0x1000) {
warning("WallyBeben: invalid data (size %u)", dataSize);
return nullptr;
}
- return new WallyBebenStream(data, dataSize, songNum, rate, stereo);
+ const WBTableOffsets &tbl = offsets ? *offsets : kDarkSideOffsets;
+ return new WallyBebenStream(data, dataSize, songNum, rate, stereo, tbl);
}
} // End of namespace Freescape
diff --git a/engines/freescape/wb.h b/engines/freescape/wb.h
index f39ee5c6be6..8dfc10ffa02 100644
--- a/engines/freescape/wb.h
+++ b/engines/freescape/wb.h
@@ -59,20 +59,36 @@ byte buildArpeggioTable(const byte intervals[8], byte mask, byte *outTable, byte
} // End of namespace WBCommon
+/** Table offsets and sizes for a Wally Beben music module. */
+struct WBTableOffsets {
+ uint32 periodTable; // 48 x uint16 BE
+ uint32 samplePtrTable; // numSamples x uint32 BE
+ uint32 instrumentTable; // numInstruments x 8 bytes
+ uint32 arpeggioIntervals; // 8 bytes
+ uint32 envelopeTable; // numEnvelopes x 8 bytes
+ uint32 songTable; // 2 songs x 4 channels x uint32 BE
+ uint32 patternPtrTable; // up to 128 x uint32 BE
+ int numSamples;
+ int numInstruments;
+ int numEnvelopes;
+};
+
/**
* Create a music stream for the Wally Beben custom music engine
- * used in the Amiga version of Dark Side.
+ * used in the Amiga versions of Dark Side and Total Eclipse.
*
- * @param data Raw TEXT segment data from HDSMUSIC.AM (after 0x1C GEMDOS header)
- * @param dataSize Size of the TEXT segment (0xF4BC for Dark Side)
+ * @param data Raw TEXT segment data (after 0x1C GEMDOS header)
+ * @param dataSize Size of the TEXT segment
* @param songNum Song number to play (1 or 2)
* @param rate Output sample rate
* @param stereo Whether to produce stereo output
+ * @param offsets Table offsets (nullptr = Dark Side defaults)
* @return A new AudioStream, or nullptr on error
*/
Audio::AudioStream *makeWallyBebenStream(const byte *data, uint32 dataSize,
int songNum = 1, int rate = 44100,
- bool stereo = true);
+ bool stereo = true,
+ const WBTableOffsets *offsets = nullptr);
} // End of namespace Freescape
Commit: 1e2b3bb0410d2c0606ecee75b72a0d74421361b6
https://github.com/scummvm/scummvm/commit/1e2b3bb0410d2c0606ecee75b72a0d74421361b6
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-04-10T21:12:35+02:00
Commit Message:
FREESCAPE: decay fix in wb music module
Changed paths:
engines/freescape/wb.cpp
diff --git a/engines/freescape/wb.cpp b/engines/freescape/wb.cpp
index 3083b68d98d..2e7d8dc8469 100644
--- a/engines/freescape/wb.cpp
+++ b/engines/freescape/wb.cpp
@@ -905,10 +905,15 @@ void WallyBebenStream::processEnvelope(int ch) {
break;
case 1: // Decay â decrease toward decay target.
+ // The 68K code decreases volume each tick; when it reaches exactly
+ // decayTarget it enters sustain. If attack == decay, the volume
+ // never changes and the engine stays in this phase (holds forever).
if (c.volume > c.decayTarget) {
c.volume--;
- } else {
- c.envelopePhase = 2;
+ if (c.volume <= c.decayTarget) {
+ c.volume = c.decayTarget;
+ c.envelopePhase = 2;
+ }
}
break;
More information about the Scummvm-git-logs
mailing list