[Scummvm-git-logs] scummvm master -> d45d8d20ddffeaf67c6594218ad2044e4a4c9517
neuromancer
noreply at scummvm.org
Mon Feb 23 10:39:50 UTC 2026
This automated email contains information about 8 new commits which have been
pushed to the 'scummvm' repo located at https://api.github.com/repos/scummvm/scummvm .
Summary:
be81ef1f3c FREESCAPE: fixes for castle master cpc
557c3e47ed FREESCAPE: initial code for eclipse music in atari
c67fbca751 FREESCAPE: fixes for eclipse music in atari
c267b8c31f FREESCAPE: improve eclipse ui in atari
8be72a7c1e FREESCAPE: improved eclipse ui in atari
0f68a39da5 FREESCAPE: improved eclipse ui in atari
1d61c84029 FREESCAPE: on-screen control for eclipse in atari
d45d8d20dd FREESCAPE: process indicator in eclipse in atari
Commit: be81ef1f3c7de1a514da354de813cdcf363f0d70
https://github.com/scummvm/scummvm/commit/be81ef1f3c7de1a514da354de813cdcf363f0d70
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-02-23T11:39:30+01:00
Commit Message:
FREESCAPE: fixes for castle master cpc
Changed paths:
engines/freescape/games/castle/castle.cpp
engines/freescape/games/castle/cpc.cpp
diff --git a/engines/freescape/games/castle/castle.cpp b/engines/freescape/games/castle/castle.cpp
index 31652e5133a..e524e203654 100644
--- a/engines/freescape/games/castle/castle.cpp
+++ b/engines/freescape/games/castle/castle.cpp
@@ -733,7 +733,7 @@ void CastleEngine::drawInfoMenu() {
keyRects.push_back(Common::Rect(80, y, 80 + _keysBorderFrames[i]->w / 2, y + _keysBorderFrames[i]->h));
}
}
- } else if (isSpectrum()) {
+ } else if (isSpectrum() || isCPC()) {
Common::Array<Common::String> lines;
lines.push_back(centerAndPadString("********************", 21));
@@ -972,7 +972,7 @@ void CastleEngine::drawFullscreenGameOverAndWait() {
Common::String scoreString;
if (isDOS())
scoreString = _messagesList[131];
- else if (isSpectrum()) {
+ else if (isSpectrum() || isCPC()) {
if (_language == Common::EN_ANY)
scoreString = "SCORE XXXXXXX";
else if (_language == Common::ES_ESP)
@@ -987,7 +987,7 @@ void CastleEngine::drawFullscreenGameOverAndWait() {
Common::String spiritsDestroyedString;
if (isDOS())
spiritsDestroyedString = _messagesList[133];
- else if (isSpectrum()) {
+ else if (isSpectrum() || isCPC()) {
if (_language == Common::EN_ANY)
spiritsDestroyedString = "X DESTROYED";
else if (_language == Common::ES_ESP)
@@ -1488,7 +1488,7 @@ bool CastleEngine::ghostInArea() {
}
void CastleEngine::drawSensorShoot(Sensor *sensor) {
- if (isSpectrum()) {
+ if (isSpectrum() || isCPC()) {
_gfx->_inkColor = 1 + (_gfx->_inkColor + 1) % 7;
} else if (isDOS()) {
float shakeIntensity = 10;
diff --git a/engines/freescape/games/castle/cpc.cpp b/engines/freescape/games/castle/cpc.cpp
index a451f469965..cd9f8022774 100644
--- a/engines/freescape/games/castle/cpc.cpp
+++ b/engines/freescape/games/castle/cpc.cpp
@@ -370,13 +370,6 @@ void CastleEngine::loadAssetsCPCFullGame() {
it._value->addObjectFromArea(id, _areaMap[255]);
}
}
- // Discard some global conditions
- // It is unclear why they hide/unhide objects that formed the spirits
- for (int i = 0; i < 3; i++) {
- debugC(kFreescapeDebugParser, "Discarding condition %s", _conditionSources[0].c_str());
- _conditions.remove_at(0);
- _conditionSources.remove_at(0);
- }
}
void CastleEngine::drawCPCUI(Graphics::Surface *surface) {
@@ -418,7 +411,7 @@ void CastleEngine::drawCPCUI(Graphics::Surface *surface) {
}
// Draw energy meter (strength)
- drawEnergyMeter(surface, Common::Point(38, 158));
+ drawEnergyMeter(surface, Common::Point(45, 158));
// Draw spirit meter
uint32 blackColor = _gfx->_texturePixelFormat.ARGBToColor(0xFF, 0, 0, 0);
@@ -439,7 +432,7 @@ void CastleEngine::drawCPCUI(Graphics::Surface *surface) {
if (!_flagFrames.empty()) {
int ticks = g_system->getMillis() / 20;
int flagFrameIndex = (ticks / 10) % 4;
- surface->copyRectToSurface(*_flagFrames[flagFrameIndex], 285, 5, Common::Rect(0, 0, _flagFrames[flagFrameIndex]->w, _flagFrames[flagFrameIndex]->h));
+ surface->copyRectToSurface(*_flagFrames[flagFrameIndex], 300, 4, Common::Rect(0, 0, _flagFrames[flagFrameIndex]->w, _flagFrames[flagFrameIndex]->h));
}
}
Commit: 557c3e47ede2af1cae45b24748267411fbac8b6b
https://github.com/scummvm/scummvm/commit/557c3e47ede2af1cae45b24748267411fbac8b6b
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-02-23T11:39:30+01:00
Commit Message:
FREESCAPE: initial code for eclipse music in atari
Changed paths:
A engines/freescape/games/eclipse/atari.music.cpp
engines/freescape/games/eclipse/atari.cpp
engines/freescape/games/eclipse/eclipse.cpp
engines/freescape/games/eclipse/eclipse.h
engines/freescape/module.mk
diff --git a/engines/freescape/games/eclipse/atari.cpp b/engines/freescape/games/eclipse/atari.cpp
index 67af6cdf4ca..1ad82118ed6 100644
--- a/engines/freescape/games/eclipse/atari.cpp
+++ b/engines/freescape/games/eclipse/atari.cpp
@@ -174,6 +174,15 @@ void EclipseEngine::loadAssetsAtariFullGame() {
loadPalettes(stream, 0x2a0fa);
loadSoundsFx(stream, 0x3030c, 6);
+ // Load TEMUSIC.ST (GEMDOS executable at file offset $11F5A, skip $1C header, TEXT size $11E8)
+ static const uint32 kTEMusicOffset = 0x11F5A;
+ static const uint32 kGemdosHeaderSize = 0x1C;
+ static const uint32 kTEMusicTextSize = 0x11E8;
+ stream->seek(kTEMusicOffset + kGemdosHeaderSize);
+ _musicData.resize(kTEMusicTextSize);
+ stream->read(_musicData.data(), kTEMusicTextSize);
+ debug(3, "TE-Atari: Loaded TEMUSIC.ST TEXT segment (%d bytes)", kTEMusicTextSize);
+
/*
loadFonts(stream, 0xd06b, _fontBig);
loadFonts(stream, 0xd49a, _fontMedium);
diff --git a/engines/freescape/games/eclipse/atari.music.cpp b/engines/freescape/games/eclipse/atari.music.cpp
new file mode 100644
index 00000000000..c342bdbb794
--- /dev/null
+++ b/engines/freescape/games/eclipse/atari.music.cpp
@@ -0,0 +1,788 @@
+/* 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/>.
+ *
+ */
+
+/**
+ * Total Eclipse Atari ST music player (YM2149 PSG).
+ *
+ * Plays background music from the TEMUSIC.ST embedded GEMDOS executable.
+ * Uses the same Wally Beben byte-stream pattern format as the Amiga
+ * Dark Side engine (wb.cpp), but outputs to the YM2149/AY-3-8912 PSG
+ * instead of Amiga Paula.
+ *
+ * TEMUSIC.ST data table offsets (TEXT-relative):
+ * $0B24 Period table (96 x uint16 BE)
+ * $0CC8 Arpeggio interval lookup (8 bytes)
+ * $0D60 Instrument table (12 x 8 bytes)
+ * $0DC0 Song table (2 songs x 3 channels x uint32 BE order-list pointers)
+ * $0DCC Pattern pointer table (up to 31 x uint32 BE)
+ */
+
+#include "audio/softsynth/ay8912.h"
+
+#include "freescape/freescape.h"
+
+#include "common/endian.h"
+#include "common/debug.h"
+#include "common/util.h"
+
+namespace Freescape {
+
+// TEXT-relative offsets for data tables within TEMUSIC.ST
+static const uint32 kTEPeriodTableOffset = 0x0B24; // 96 x uint16 BE
+static const uint32 kTEArpeggioIntervalsOffset = 0x0CC8; // 8 bytes
+static const uint32 kTEInstrumentTableOffset = 0x0D60; // 12 x 8 bytes
+static const uint32 kTESongTableOffset = 0x0DC0; // 2 songs x 3 ch x uint32 BE
+static const uint32 kTEPatternPtrTableOffset = 0x0DCC; // up to 31 x uint32 BE
+
+static const int kTENumChannels = 3;
+static const int kTENumPeriods = 96;
+static const int kTENumInstruments = 12;
+static const int kTEMaxPatterns = 31;
+
+class EclipseAtariMusicStream : public Audio::AY8912Stream {
+public:
+ EclipseAtariMusicStream(const byte *data, uint32 dataSize, int songNum, int rate = 44100);
+ ~EclipseAtariMusicStream() override {}
+
+ int readBuffer(int16 *buffer, const int numSamples) override;
+ bool endOfData() const override { return !_musicActive; }
+ bool endOfStream() const override { return !_musicActive; }
+
+private:
+ // --- Data tables ---
+ const byte *_data;
+ uint32 _dataSize;
+
+ uint16 _periods[kTENumPeriods];
+
+ struct InstrumentDesc {
+ byte volume; // Initial volume (0-$3F)
+ byte targetVol; // Sustain/target volume
+ byte attackRate; // Volume increment per tick
+ byte releaseRate; // Volume decrement per tick
+ byte envFlags; // Bit 7: hardware envelope
+ byte effectType; // Effect configuration
+ byte arpeggioData; // Arpeggio bit pattern
+ byte flags; // Additional flags
+ };
+ InstrumentDesc _instruments[kTENumInstruments];
+
+ // Song order list pointers (TEXT-relative)
+ uint32 _songOrderPtrs[2][kTENumChannels];
+
+ // Pattern pointer table
+ uint32 _patternPtrs[kTEMaxPatterns];
+ uint32 _numPatterns;
+
+ // Arpeggio interval lookup
+ byte _arpeggioIntervals[8];
+
+ // --- Per-channel state ---
+ struct ChannelState {
+ // Order list
+ uint32 orderListOffset;
+ int orderListPos;
+ int8 transpose;
+
+ // Current pattern
+ uint32 patternOffset;
+ int patternPos;
+
+ // Note state
+ byte note;
+ byte prevNote;
+ byte duration;
+ int durationCounter;
+
+ // Instrument
+ byte instrumentIdx;
+
+ // Volume envelope
+ byte volume; // Current volume (0-63 internal scale)
+ byte attackLevel; // Initial volume on note-on
+ byte decayTarget; // Target volume to hold
+ byte attackRate; // Increment per tick
+ byte releaseRate; // Decrement per tick
+ byte envelopePhase; // 0=attack, 1=decay, 2=sustain, 3=release
+
+ // Effects
+ byte effectMode; // 0=none, 1=porta/arp
+ bool portaUp;
+ bool portaDown;
+ int16 portaStep;
+ int16 portaTarget;
+ byte arpeggioMask;
+ byte arpeggioPos;
+
+ // Period
+ int16 basePeriod;
+ int16 outputPeriod;
+
+ bool active;
+ };
+ ChannelState _channels[kTENumChannels];
+
+ // --- Global state ---
+ bool _musicActive;
+ byte _tickSpeed;
+ byte _tickCounter;
+ int _tickSampleCount;
+
+ // Arpeggio working table
+ byte _arpeggioTable[16];
+ int _arpeggioTableLen;
+
+ // --- Methods ---
+ void loadTables();
+ void startSong(int songNum);
+ void initChannel(int ch);
+ void readOrderList(int ch);
+ void readPatternCommands(int ch);
+ void triggerNote(int ch);
+ void processEffects(int ch);
+ void processEnvelope(int ch);
+ void buildArpeggioTable(byte mask);
+ void tickUpdate();
+ void writeYMRegisters();
+
+ uint16 getPeriod(int note) const {
+ if (note < 0 || note >= kTENumPeriods)
+ return 0;
+ return _periods[note];
+ }
+
+ byte readDataByte(uint32 offset) const {
+ if (offset < _dataSize)
+ return _data[offset];
+ return 0;
+ }
+
+ uint16 readDataWord(uint32 offset) const {
+ if (offset + 1 < _dataSize)
+ return READ_BE_UINT16(_data + offset);
+ return 0;
+ }
+
+ uint32 readDataLong(uint32 offset) const {
+ if (offset + 3 < _dataSize)
+ return READ_BE_UINT32(_data + offset);
+ return 0;
+ }
+};
+
+// ---------------------------------------------------------------------------
+// Construction / data loading
+// ---------------------------------------------------------------------------
+
+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),
+ _musicActive(false), _tickSpeed(6), _tickCounter(0),
+ _tickSampleCount(0),
+ _arpeggioTableLen(0), _numPatterns(0) {
+
+ memset(_periods, 0, sizeof(_periods));
+ memset(_instruments, 0, sizeof(_instruments));
+ memset(_songOrderPtrs, 0, sizeof(_songOrderPtrs));
+ memset(_patternPtrs, 0, sizeof(_patternPtrs));
+ memset(_arpeggioIntervals, 0, sizeof(_arpeggioIntervals));
+ memset(_channels, 0, sizeof(_channels));
+ memset(_arpeggioTable, 0, sizeof(_arpeggioTable));
+
+ // Reset all YM registers
+ for (int r = 0; r < 14; r++)
+ setReg(r, 0);
+ // Mixer: all channels disabled initially
+ setReg(7, 0x3F);
+
+ loadTables();
+ startSong(songNum);
+}
+
+void EclipseAtariMusicStream::loadTables() {
+ // Period table: 96 x uint16 BE at TEXT+$0B24
+ for (int i = 0; i < kTENumPeriods; i++) {
+ _periods[i] = readDataWord(kTEPeriodTableOffset + i * 2);
+ }
+
+ // Arpeggio interval table: 8 bytes at TEXT+$0CC8
+ for (int i = 0; i < 8; i++) {
+ _arpeggioIntervals[i] = readDataByte(kTEArpeggioIntervalsOffset + i);
+ }
+
+ // Instrument table: 12 x 8 bytes at TEXT+$0D60
+ for (int i = 0; i < kTENumInstruments; i++) {
+ uint32 off = kTEInstrumentTableOffset + i * 8;
+ _instruments[i].volume = readDataByte(off + 0);
+ _instruments[i].targetVol = readDataByte(off + 1);
+ _instruments[i].attackRate = readDataByte(off + 2);
+ _instruments[i].releaseRate = readDataByte(off + 3);
+ _instruments[i].envFlags = readDataByte(off + 4);
+ _instruments[i].effectType = readDataByte(off + 5);
+ _instruments[i].arpeggioData = readDataByte(off + 6);
+ _instruments[i].flags = readDataByte(off + 7);
+ }
+
+ // Song table: 2 songs x 3 channels x uint32 BE at TEXT+$0DC0
+ for (int s = 0; s < 2; s++) {
+ for (int ch = 0; ch < kTENumChannels; ch++) {
+ _songOrderPtrs[s][ch] = readDataLong(kTESongTableOffset + s * 12 + ch * 4);
+ }
+ }
+
+ // Pattern pointer table at TEXT+$0DCC
+ _numPatterns = 0;
+ for (uint32 i = 0; i < kTEMaxPatterns; i++) {
+ uint32 ptr = readDataLong(kTEPatternPtrTableOffset + i * 4);
+ _patternPtrs[i] = ptr;
+ if (ptr > 0 && ptr < _dataSize)
+ _numPatterns = i + 1;
+ }
+
+ debug(3, "TE-Atari: Loaded music data (%u bytes)", _dataSize);
+ debug(3, "TE-Atari: %d valid patterns", _numPatterns);
+
+ for (int s = 0; s < 2; s++) {
+ debug(3, "TE-Atari: Song %d order ptrs: $%X $%X $%X",
+ s + 1, _songOrderPtrs[s][0], _songOrderPtrs[s][1], _songOrderPtrs[s][2]);
+ }
+
+ for (int i = 0; i < kTENumInstruments; i++) {
+ const InstrumentDesc &inst = _instruments[i];
+ if (inst.volume > 0 || inst.targetVol > 0)
+ debug(3, "TE-Atari: Inst %d: vol=%d target=%d atk=%d rel=%d envFlags=$%02X effect=$%02X arp=$%02X flags=$%02X",
+ i, inst.volume, inst.targetVol, inst.attackRate, inst.releaseRate,
+ inst.envFlags, inst.effectType, inst.arpeggioData, inst.flags);
+ }
+}
+
+// ---------------------------------------------------------------------------
+// Song init
+// ---------------------------------------------------------------------------
+
+void EclipseAtariMusicStream::startSong(int songNum) {
+ _musicActive = false;
+
+ if (songNum < 1 || songNum > 2)
+ return;
+
+ int songIdx = songNum - 1;
+ _tickSpeed = 6;
+ _tickCounter = 0;
+ _arpeggioTableLen = 0;
+
+ // Silence all YM channels
+ for (int r = 0; r < 14; r++)
+ setReg(r, 0);
+ setReg(7, 0x3F); // All disabled
+
+ for (int ch = 0; ch < kTENumChannels; ch++) {
+ initChannel(ch);
+ _channels[ch].orderListOffset = _songOrderPtrs[songIdx][ch];
+ _channels[ch].orderListPos = 0;
+ _channels[ch].active = true;
+ readOrderList(ch);
+ }
+
+ _musicActive = true;
+
+ debug(3, "TE-Atari: Song %d started, tickSpeed=%d", songNum, _tickSpeed);
+ for (int ch = 0; ch < kTENumChannels; ch++) {
+ debug(3, "TE-Atari: ch%d orderList=$%X pattern=$%X",
+ ch, _channels[ch].orderListOffset, _channels[ch].patternOffset);
+ }
+}
+
+void EclipseAtariMusicStream::initChannel(int ch) {
+ ChannelState &c = _channels[ch];
+ memset(&c, 0, sizeof(ChannelState));
+ c.duration = 1;
+ c.durationCounter = 0;
+ c.envelopePhase = 3; // Start in release (silent)
+ c.attackLevel = 0x36; // Default from instrument 1
+ c.decayTarget = 0x36;
+}
+
+// ---------------------------------------------------------------------------
+// Order list reader
+// Same format as wb.cpp: $00-$C0=pattern#, $C1-$FE=transpose, $FF=loop
+// ---------------------------------------------------------------------------
+
+void EclipseAtariMusicStream::readOrderList(int ch) {
+ ChannelState &c = _channels[ch];
+
+ for (int safety = 0; safety < 256; safety++) {
+ if (c.orderListOffset + c.orderListPos >= _dataSize)
+ break;
+
+ byte cmd = readDataByte(c.orderListOffset + c.orderListPos);
+ c.orderListPos++;
+
+ if (cmd == 0xFF) {
+ c.orderListPos = 0;
+ continue;
+ }
+
+ if (cmd > 0xC0) {
+ c.transpose = (int8)((cmd + 0x20) & 0xFF);
+ continue;
+ }
+
+ if (cmd < kTEMaxPatterns && _patternPtrs[cmd] > 0 && _patternPtrs[cmd] < _dataSize) {
+ c.patternOffset = _patternPtrs[cmd];
+ c.patternPos = 0;
+ debugC(3, kFreescapeDebugParser, "TE-Atari: ch%d order -> pattern %d (offset $%04X)", ch, cmd, c.patternOffset);
+ } else {
+ warning("TE-Atari: ch%d pattern index %d invalid", ch, cmd);
+ c.patternOffset = _patternPtrs[0];
+ c.patternPos = 0;
+ }
+ return;
+ }
+
+ warning("TE-Atari: ch%d order list safety limit hit", ch);
+}
+
+// ---------------------------------------------------------------------------
+// Pattern command reader
+// Same format as wb.cpp: $FF=end-pattern, $FE=end-song, $F0+=speed,
+// $C0+=instrument, $80+=duration, $7F/$7E=portamento,
+// $7D/$7C=vibrato/arpeggio, $00-$5F=note
+// ---------------------------------------------------------------------------
+
+void EclipseAtariMusicStream::readPatternCommands(int ch) {
+ ChannelState &c = _channels[ch];
+
+ for (int safety = 0; safety < 256; safety++) {
+ if (c.patternOffset + c.patternPos >= _dataSize)
+ break;
+
+ byte cmd = readDataByte(c.patternOffset + c.patternPos);
+ c.patternPos++;
+
+ if (cmd == 0xFF) {
+ readOrderList(ch);
+ continue;
+ }
+
+ if (cmd == 0xFE) {
+ _musicActive = false;
+ return;
+ }
+
+ if (cmd == 0xFC) {
+ // Song change command â read parameter, restart
+ byte newSong = readDataByte(c.patternOffset + c.patternPos);
+ c.patternPos++;
+ if (newSong >= 1 && newSong <= 2) {
+ startSong(newSong);
+ }
+ return;
+ }
+
+ if (cmd >= 0xF0) {
+ _tickSpeed = cmd & 0x0F;
+ if (_tickSpeed == 0)
+ _tickSpeed = 1;
+ continue;
+ }
+
+ if (cmd >= 0xC0) {
+ // Instrument select
+ // The TEMUSIC.ST encoding: (cmd & $1F) selects envelope/instrument
+ // The instrument index is encoded as (cmd & $1F) >> 3 for the
+ // instrument table. However, looking at the disassembly:
+ // AND $1F, ASL 3 â stored as 8*index in $3C(a0)
+ // Then at $0950: d0 = $3C(a0), lea $0D60, a2 += d0
+ // So the $3C field stores instrumentIdx * 8, and the table
+ // lookup uses it directly as byte offset.
+ // For our purposes: instrument index = (cmd & 0x1F)
+ byte instIdx = cmd & 0x1F;
+ if (instIdx < kTENumInstruments) {
+ c.instrumentIdx = instIdx;
+ const InstrumentDesc &inst = _instruments[instIdx];
+ c.attackLevel = inst.volume;
+ c.decayTarget = inst.targetVol;
+ c.attackRate = inst.attackRate;
+ c.releaseRate = inst.releaseRate;
+
+ if (inst.arpeggioData != 0) {
+ c.effectMode = 1;
+ c.arpeggioMask = inst.arpeggioData;
+ buildArpeggioTable(inst.arpeggioData);
+ }
+ }
+ continue;
+ }
+
+ if (cmd >= 0x80) {
+ c.duration = cmd & 0x3F;
+ if (c.duration == 0)
+ c.duration = 1;
+ continue;
+ }
+
+ if (cmd == 0x7F) {
+ c.portaUp = true;
+ c.portaDown = false;
+ c.effectMode = 1;
+ continue;
+ }
+
+ if (cmd == 0x7E) {
+ c.portaDown = true;
+ c.portaUp = false;
+ c.effectMode = 1;
+ continue;
+ }
+
+ if (cmd == 0x7D) {
+ byte param = readDataByte(c.patternOffset + c.patternPos);
+ c.patternPos++;
+ c.effectMode = 1;
+ c.arpeggioMask = param;
+ buildArpeggioTable(param);
+ continue;
+ }
+
+ if (cmd == 0x7C) {
+ byte param = readDataByte(c.patternOffset + c.patternPos);
+ c.patternPos++;
+ c.effectMode = 1;
+ c.arpeggioMask = param;
+ buildArpeggioTable(param);
+ continue;
+ }
+
+ if (cmd == 0x7A) {
+ // Delay command â skip parameter byte (same as wb.cpp)
+ c.patternPos++;
+ continue;
+ }
+
+ // Note value ($00-$5F)
+ c.prevNote = c.note;
+ c.note = cmd;
+ c.durationCounter = c.duration;
+ triggerNote(ch);
+ return;
+ }
+
+ warning("TE-Atari: ch%d pattern read safety limit hit", ch);
+}
+
+// ---------------------------------------------------------------------------
+// Note trigger â set YM period, reset envelope
+// ---------------------------------------------------------------------------
+
+void EclipseAtariMusicStream::triggerNote(int ch) {
+ ChannelState &c = _channels[ch];
+
+ if (c.note == 0) {
+ // Rest â silence channel
+ c.outputPeriod = 0;
+ c.volume = 0;
+ c.envelopePhase = 3;
+ return;
+ }
+
+ // Apply transpose and clamp
+ int note = c.note + c.transpose;
+ if (note < 1) note = 1;
+ if (note >= kTENumPeriods) note = kTENumPeriods - 1;
+
+ c.basePeriod = getPeriod(note);
+ c.outputPeriod = c.basePeriod;
+
+ if (c.basePeriod == 0) {
+ warning("TE-Atari: ch%d note %d has period 0", ch, note);
+ return;
+ }
+
+ // Reset envelope
+ c.envelopePhase = 0;
+ c.volume = c.attackLevel;
+
+ debugC(3, kFreescapeDebugParser, "TE-Atari: ch%d NOTE note=%d(+%d) period=%d inst=%d vol=%d",
+ ch, c.note, c.transpose, c.basePeriod, c.instrumentIdx, c.volume);
+
+ // Set up portamento if active
+ if (c.portaUp || c.portaDown) {
+ int prevNote = c.prevNote + c.transpose;
+ if (prevNote < 1) prevNote = 1;
+ if (prevNote >= kTENumPeriods) prevNote = kTENumPeriods - 1;
+
+ int16 prevPeriod = (c.prevNote > 0) ? getPeriod(prevNote) : c.basePeriod;
+ int16 delta = ABS(c.basePeriod - prevPeriod);
+ int steps = (_tickSpeed > 0) ? _tickSpeed : 1;
+ c.portaStep = delta / steps;
+ if (c.portaStep == 0)
+ c.portaStep = 1;
+ c.portaTarget = c.basePeriod;
+ c.basePeriod = prevPeriod;
+ c.outputPeriod = prevPeriod;
+ }
+}
+
+// ---------------------------------------------------------------------------
+// Effects processing â runs every tick (50 Hz)
+// ---------------------------------------------------------------------------
+
+void EclipseAtariMusicStream::processEffects(int ch) {
+ ChannelState &c = _channels[ch];
+
+ if (c.effectMode == 0) {
+ c.outputPeriod = c.basePeriod;
+ return;
+ }
+
+ if (c.portaUp) {
+ c.basePeriod -= c.portaStep;
+ if (c.basePeriod <= c.portaTarget) {
+ c.basePeriod = c.portaTarget;
+ c.portaUp = false;
+ c.effectMode = 0;
+ }
+ c.outputPeriod = c.basePeriod;
+ return;
+ }
+
+ if (c.portaDown) {
+ c.basePeriod += c.portaStep;
+ if (c.basePeriod >= c.portaTarget) {
+ c.basePeriod = c.portaTarget;
+ c.portaDown = false;
+ c.effectMode = 0;
+ }
+ c.outputPeriod = c.basePeriod;
+ return;
+ }
+
+ // Arpeggio
+ if (_arpeggioTableLen > 0) {
+ int note = c.note + c.transpose;
+ int offset = _arpeggioTable[c.arpeggioPos % _arpeggioTableLen];
+ note += offset;
+ if (note < 1) note = 1;
+ if (note >= kTENumPeriods) note = kTENumPeriods - 1;
+ c.outputPeriod = getPeriod(note);
+ c.arpeggioPos++;
+ if (c.arpeggioPos >= _arpeggioTableLen)
+ c.arpeggioPos = 0;
+ } else {
+ c.outputPeriod = c.basePeriod;
+ }
+}
+
+// ---------------------------------------------------------------------------
+// Volume envelope â runs every tick (50 Hz)
+// Volume range: 0-63 internal, written to YM as >>2 (0-15)
+// ---------------------------------------------------------------------------
+
+void EclipseAtariMusicStream::processEnvelope(int ch) {
+ ChannelState &c = _channels[ch];
+
+ switch (c.envelopePhase) {
+ case 0: // Attack â volume set to attackLevel in triggerNote
+ if (c.attackRate == 0) {
+ c.volume = c.decayTarget;
+ c.envelopePhase = 2;
+ } else {
+ c.envelopePhase = 1;
+ }
+ break;
+
+ case 1: // Decay â fade toward target
+ if (c.volume < c.decayTarget) {
+ c.volume++;
+ } else if (c.volume > c.decayTarget) {
+ c.volume--;
+ } else {
+ c.envelopePhase = 2;
+ }
+ break;
+
+ case 2: // Sustain â hold
+ break;
+
+ case 3: // Release
+ if (c.releaseRate > 0) {
+ if (c.volume > c.releaseRate) {
+ c.volume -= c.releaseRate;
+ } else {
+ c.volume = 0;
+ }
+ } else {
+ c.volume = 0;
+ }
+ break;
+ }
+
+ if (c.volume > 63)
+ c.volume = 63;
+}
+
+// ---------------------------------------------------------------------------
+// Arpeggio table builder
+// ---------------------------------------------------------------------------
+
+void EclipseAtariMusicStream::buildArpeggioTable(byte mask) {
+ _arpeggioTableLen = 0;
+ _arpeggioTable[_arpeggioTableLen++] = 0; // Base note
+
+ for (int i = 0; i < 8 && _arpeggioTableLen < 16; i++) {
+ if (mask & (1 << i)) {
+ _arpeggioTable[_arpeggioTableLen++] = _arpeggioIntervals[i];
+ }
+ }
+
+ if (_arpeggioTableLen <= 1)
+ _arpeggioTableLen = 0;
+}
+
+// ---------------------------------------------------------------------------
+// Write channel state to YM2149 registers
+// ---------------------------------------------------------------------------
+
+void EclipseAtariMusicStream::writeYMRegisters() {
+ byte mixer = 0x3F; // Start with all disabled
+
+ for (int ch = 0; ch < kTENumChannels; ch++) {
+ ChannelState &c = _channels[ch];
+
+ if (!c.active || c.outputPeriod == 0 || c.volume == 0) {
+ // Channel silent
+ setReg(8 + ch, 0); // Volume = 0
+ continue;
+ }
+
+ // Enable tone for this channel
+ mixer &= ~(1 << ch);
+
+ // Set tone period (2 registers per channel)
+ uint16 period = (uint16)c.outputPeriod;
+ setReg(ch * 2, period & 0xFF); // Fine tune
+ setReg(ch * 2 + 1, (period >> 8) & 0x0F); // Coarse tune
+
+ // Set volume (internal 0-63 â YM 0-15)
+ byte ymVol = c.volume >> 2;
+ if (ymVol > 15) ymVol = 15;
+ setReg(8 + ch, ymVol);
+ }
+
+ setReg(7, mixer);
+}
+
+// ---------------------------------------------------------------------------
+// Main tick update â called at 50 Hz
+// ---------------------------------------------------------------------------
+
+void EclipseAtariMusicStream::tickUpdate() {
+ if (!_musicActive)
+ return;
+
+ // Tick speed gating: the 68000 code uses subq.b #1; bpl; reset which
+ // gives a period of (tickSpeed + 1) ticks before the sequencer advances.
+ // Counter counts: tickSpeed â tickSpeed-1 â ... â 0 â -1(wrap&reset)
+ _tickCounter++;
+ bool sequencerTick = false;
+ if (_tickCounter > _tickSpeed) {
+ _tickCounter = 0;
+ sequencerTick = true;
+ }
+
+ if (sequencerTick) {
+ for (int ch = 0; ch < kTENumChannels; ch++) {
+ if (!_channels[ch].active)
+ continue;
+
+ if (_channels[ch].durationCounter > 0) {
+ _channels[ch].durationCounter--;
+ }
+
+ if (_channels[ch].durationCounter == 0) {
+ if (_channels[ch].envelopePhase < 3)
+ _channels[ch].envelopePhase = 3;
+ readPatternCommands(ch);
+ }
+ }
+ }
+
+ // Every tick: process effects and envelope
+ for (int ch = 0; ch < kTENumChannels; ch++) {
+ if (!_channels[ch].active)
+ continue;
+
+ processEffects(ch);
+ processEnvelope(ch);
+ }
+
+ writeYMRegisters();
+}
+
+// ---------------------------------------------------------------------------
+// Audio stream readBuffer â tick at 50 Hz, generate AY samples
+// ---------------------------------------------------------------------------
+
+int EclipseAtariMusicStream::readBuffer(int16 *buffer, const int numSamples) {
+ if (!_musicActive)
+ return 0;
+
+ int samplesGenerated = 0;
+ // AY8912Stream is stereo: 2 int16 values per frame
+ int samplesPerTick = (getRate() / 50) * 2;
+
+ while (samplesGenerated < numSamples && _musicActive) {
+ int remaining = samplesPerTick - _tickSampleCount;
+ int toGenerate = MIN(numSamples - samplesGenerated, remaining);
+
+ if (toGenerate > 0) {
+ generateSamples(buffer + samplesGenerated, toGenerate);
+ samplesGenerated += toGenerate;
+ _tickSampleCount += toGenerate;
+ }
+
+ if (_tickSampleCount >= samplesPerTick) {
+ _tickSampleCount -= samplesPerTick;
+ tickUpdate();
+ }
+ }
+
+ return samplesGenerated;
+}
+
+// ---------------------------------------------------------------------------
+// Factory function
+// ---------------------------------------------------------------------------
+
+Audio::AudioStream *makeEclipseAtariMusicStream(const byte *data, uint32 dataSize,
+ int songNum, int rate) {
+ if (!data || dataSize < 0x1000) {
+ warning("TE-Atari music: invalid data (size %u)", dataSize);
+ return nullptr;
+ }
+
+ EclipseAtariMusicStream *stream = new EclipseAtariMusicStream(data, dataSize, songNum, rate);
+ return stream->toAudioStream();
+}
+
+} // End of namespace Freescape
diff --git a/engines/freescape/games/eclipse/eclipse.cpp b/engines/freescape/games/eclipse/eclipse.cpp
index ee4001655ba..f052d25dfe7 100644
--- a/engines/freescape/games/eclipse/eclipse.cpp
+++ b/engines/freescape/games/eclipse/eclipse.cpp
@@ -21,6 +21,9 @@
#include "common/file.h"
+#include "audio/audiostream.h"
+#include "audio/mixer.h"
+
#include "backends/keymapper/action.h"
#include "backends/keymapper/keymap.h"
#include "backends/keymapper/standard-actions.h"
@@ -32,6 +35,10 @@
namespace Freescape {
+// Forward declaration (defined in atari.music.cpp)
+Audio::AudioStream *makeEclipseAtariMusicStream(const byte *data, uint32 dataSize,
+ int songNum = 1, int rate = 44100);
+
EclipseEngine::EclipseEngine(OSystem *syst, const ADGameDescription *gd) : FreescapeEngine(syst, gd) {
// These sounds can be overriden by the class of each platform
_soundIndexStartFalling = -1;
@@ -331,6 +338,16 @@ void EclipseEngine::gotoArea(uint16 areaID, int entranceID) {
if (isAmiga() || isAtariST())
_currentArea->_skyColor = 15;
+ // Start background music (Atari ST)
+ if (isAtariST() && !_musicData.empty() && !_mixer->isSoundHandleActive(_musicHandle)) {
+ Audio::AudioStream *musicStream = makeEclipseAtariMusicStream(
+ _musicData.data(), _musicData.size(), 1);
+ if (musicStream) {
+ _mixer->playStream(Audio::Mixer::kMusicSoundType,
+ &_musicHandle, musicStream);
+ }
+ }
+
resetInput();
}
diff --git a/engines/freescape/games/eclipse/eclipse.h b/engines/freescape/games/eclipse/eclipse.h
index b74d53fa470..5f8f942523d 100644
--- a/engines/freescape/games/eclipse/eclipse.h
+++ b/engines/freescape/games/eclipse/eclipse.h
@@ -96,6 +96,8 @@ public:
soundFx *load1bPCM(Common::SeekableReadStream *file, int offset);
+ Common::Array<byte> _musicData; // TEMUSIC.ST TEXT segment (Atari ST)
+
bool checkIfGameEnded() override;
void endGame() override;
void loadSoundsFx(Common::SeekableReadStream *file, int offset, int number) override;
diff --git a/engines/freescape/module.mk b/engines/freescape/module.mk
index ea12765c2a4..413a85bfdf8 100644
--- a/engines/freescape/module.mk
+++ b/engines/freescape/module.mk
@@ -31,6 +31,7 @@ MODULE_OBJS := \
games/driller/sounds.o \
games/driller/zx.o \
games/eclipse/atari.o \
+ games/eclipse/atari.music.o \
games/eclipse/c64.o \
games/eclipse/dos.o \
games/eclipse/eclipse.o \
Commit: c67fbca75160ab8596fc4d1ac009fe21dc03067f
https://github.com/scummvm/scummvm/commit/c67fbca75160ab8596fc4d1ac009fe21dc03067f
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-02-23T11:39:31+01:00
Commit Message:
FREESCAPE: fixes for eclipse music in atari
Changed paths:
engines/freescape/games/eclipse/atari.music.cpp
diff --git a/engines/freescape/games/eclipse/atari.music.cpp b/engines/freescape/games/eclipse/atari.music.cpp
index c342bdbb794..b3518d87c43 100644
--- a/engines/freescape/games/eclipse/atari.music.cpp
+++ b/engines/freescape/games/eclipse/atari.music.cpp
@@ -124,7 +124,7 @@ private:
byte envelopePhase; // 0=attack, 1=decay, 2=sustain, 3=release
// Effects
- byte effectMode; // 0=none, 1=porta/arp
+ byte effectMode; // 0=none, 1=vibrato, 2=arpeggio
bool portaUp;
bool portaDown;
int16 portaStep;
@@ -132,6 +132,16 @@ private:
byte arpeggioMask;
byte arpeggioPos;
+ // Vibrato
+ byte vibratoSpeed; // Phase increment per tick
+ byte vibratoDepth; // Amplitude in period units
+ int8 vibratoPos; // Current phase position (oscillates)
+ int8 vibratoDir; // +1 or -1
+
+ // Noise
+ bool noiseEnabled; // Instrument flags bit 1: noise mode
+ bool freqSweep; // Instrument flags bit 2: frequency sweep
+
// Period
int16 basePeriod;
int16 outputPeriod;
@@ -224,6 +234,13 @@ void EclipseAtariMusicStream::loadTables() {
_periods[i] = readDataWord(kTEPeriodTableOffset + i * 2);
}
+ // Fix note 46: corrupted by data artifact ($3095 instead of $010D).
+ // Correct value interpolated from surrounding notes (45=$011D, 47=$00FE).
+ if (_periods[46] == 0x3095) {
+ _periods[46] = 0x010D;
+ debug(3, "TE-Atari: Fixed corrupted period for note 46 ($3095 -> $010D)");
+ }
+
// Arpeggio interval table: 8 bytes at TEXT+$0CC8
for (int i = 0; i < 8; i++) {
_arpeggioIntervals[i] = readDataByte(kTEArpeggioIntervalsOffset + i);
@@ -347,14 +364,14 @@ void EclipseAtariMusicStream::readOrderList(int ch) {
continue;
}
- if (cmd < kTEMaxPatterns && _patternPtrs[cmd] > 0 && _patternPtrs[cmd] < _dataSize) {
+ if (cmd < _numPatterns && _patternPtrs[cmd] > 0 && _patternPtrs[cmd] < _dataSize) {
c.patternOffset = _patternPtrs[cmd];
c.patternPos = 0;
debugC(3, kFreescapeDebugParser, "TE-Atari: ch%d order -> pattern %d (offset $%04X)", ch, cmd, c.patternOffset);
} else {
- warning("TE-Atari: ch%d pattern index %d invalid", ch, cmd);
- c.patternOffset = _patternPtrs[0];
- c.patternPos = 0;
+ // Invalid pattern index â skip it and try next order entry
+ debugC(3, kFreescapeDebugParser, "TE-Atari: ch%d skipping invalid pattern index %d", ch, cmd);
+ continue;
}
return;
}
@@ -407,15 +424,7 @@ void EclipseAtariMusicStream::readPatternCommands(int ch) {
}
if (cmd >= 0xC0) {
- // Instrument select
- // The TEMUSIC.ST encoding: (cmd & $1F) selects envelope/instrument
- // The instrument index is encoded as (cmd & $1F) >> 3 for the
- // instrument table. However, looking at the disassembly:
- // AND $1F, ASL 3 â stored as 8*index in $3C(a0)
- // Then at $0950: d0 = $3C(a0), lea $0D60, a2 += d0
- // So the $3C field stores instrumentIdx * 8, and the table
- // lookup uses it directly as byte offset.
- // For our purposes: instrument index = (cmd & 0x1F)
+ // Instrument select: (cmd & $1F) = instrument index
byte instIdx = cmd & 0x1F;
if (instIdx < kTENumInstruments) {
c.instrumentIdx = instIdx;
@@ -425,11 +434,27 @@ void EclipseAtariMusicStream::readPatternCommands(int ch) {
c.attackRate = inst.attackRate;
c.releaseRate = inst.releaseRate;
- if (inst.arpeggioData != 0) {
- c.effectMode = 1;
+ // Instrument effect type: high nibble = vibrato speed,
+ // low nibble = vibrato depth (in period units)
+ if (inst.effectType != 0) {
+ c.effectMode = 1; // vibrato
+ c.vibratoSpeed = (inst.effectType >> 4) & 0x0F;
+ c.vibratoDepth = inst.effectType & 0x0F;
+ c.vibratoPos = 0;
+ c.vibratoDir = 1;
+ c.portaUp = false;
+ c.portaDown = false;
+ } else if (inst.arpeggioData != 0) {
+ c.effectMode = 2; // arpeggio
c.arpeggioMask = inst.arpeggioData;
buildArpeggioTable(inst.arpeggioData);
+ } else {
+ c.effectMode = 0;
}
+
+ // Noise flags from instrument byte 7
+ c.noiseEnabled = (inst.flags & 0x02) != 0;
+ c.freqSweep = (inst.flags & 0x04) != 0;
}
continue;
}
@@ -456,19 +481,23 @@ void EclipseAtariMusicStream::readPatternCommands(int ch) {
}
if (cmd == 0x7D) {
+ // Arpeggio / effect mode 1 â param is bitmask into interval table
byte param = readDataByte(c.patternOffset + c.patternPos);
c.patternPos++;
- c.effectMode = 1;
+ c.effectMode = 2; // arpeggio
c.arpeggioMask = param;
+ c.arpeggioPos = 0;
buildArpeggioTable(param);
continue;
}
if (cmd == 0x7C) {
+ // Effect mode 2 â alternate arpeggio/vibrato
byte param = readDataByte(c.patternOffset + c.patternPos);
c.patternPos++;
- c.effectMode = 1;
+ c.effectMode = 2; // arpeggio
c.arpeggioMask = param;
+ c.arpeggioPos = 0;
buildArpeggioTable(param);
continue;
}
@@ -550,17 +579,12 @@ void EclipseAtariMusicStream::triggerNote(int ch) {
void EclipseAtariMusicStream::processEffects(int ch) {
ChannelState &c = _channels[ch];
- if (c.effectMode == 0) {
- c.outputPeriod = c.basePeriod;
- return;
- }
-
+ // Portamento takes priority (active during porta regardless of effectMode)
if (c.portaUp) {
c.basePeriod -= c.portaStep;
if (c.basePeriod <= c.portaTarget) {
c.basePeriod = c.portaTarget;
c.portaUp = false;
- c.effectMode = 0;
}
c.outputPeriod = c.basePeriod;
return;
@@ -571,14 +595,30 @@ void EclipseAtariMusicStream::processEffects(int ch) {
if (c.basePeriod >= c.portaTarget) {
c.basePeriod = c.portaTarget;
c.portaDown = false;
- c.effectMode = 0;
}
c.outputPeriod = c.basePeriod;
return;
}
- // Arpeggio
- if (_arpeggioTableLen > 0) {
+ if (c.effectMode == 1 && c.vibratoDepth > 0) {
+ // Vibrato: triangle wave pitch oscillation around basePeriod
+ c.vibratoPos += c.vibratoDir;
+ if (c.vibratoPos >= (int8)c.vibratoDepth) {
+ c.vibratoPos = c.vibratoDepth;
+ c.vibratoDir = -1;
+ } else if (c.vibratoPos <= -(int8)c.vibratoDepth) {
+ c.vibratoPos = -(int8)c.vibratoDepth;
+ c.vibratoDir = 1;
+ }
+ // Scale vibrato offset by speed (higher speed = faster oscillation)
+ int16 offset = c.vibratoPos * c.vibratoSpeed;
+ c.outputPeriod = c.basePeriod + offset;
+ if (c.outputPeriod < 1) c.outputPeriod = 1;
+ return;
+ }
+
+ if (c.effectMode == 2 && _arpeggioTableLen > 0) {
+ // Arpeggio: cycle through note offsets from interval table
int note = c.note + c.transpose;
int offset = _arpeggioTable[c.arpeggioPos % _arpeggioTableLen];
note += offset;
@@ -588,9 +628,10 @@ void EclipseAtariMusicStream::processEffects(int ch) {
c.arpeggioPos++;
if (c.arpeggioPos >= _arpeggioTableLen)
c.arpeggioPos = 0;
- } else {
- c.outputPeriod = c.basePeriod;
+ return;
}
+
+ c.outputPeriod = c.basePeriod;
}
// ---------------------------------------------------------------------------
@@ -664,7 +705,7 @@ void EclipseAtariMusicStream::buildArpeggioTable(byte mask) {
// ---------------------------------------------------------------------------
void EclipseAtariMusicStream::writeYMRegisters() {
- byte mixer = 0x3F; // Start with all disabled
+ byte mixer = 0x3F; // Start with all disabled (bits 0-2=tone, bits 3-5=noise)
for (int ch = 0; ch < kTENumChannels; ch++) {
ChannelState &c = _channels[ch];
@@ -675,9 +716,18 @@ void EclipseAtariMusicStream::writeYMRegisters() {
continue;
}
- // Enable tone for this channel
+ // Enable tone for this channel (bits 0-2)
mixer &= ~(1 << ch);
+ // Enable noise for this channel if instrument has noise flag (bits 3-5)
+ if (c.noiseEnabled) {
+ mixer &= ~(1 << (ch + 3));
+ // Set noise period from note (lower period = higher pitched noise)
+ byte noisePeriod = (c.outputPeriod >> 4) & 0x1F;
+ if (noisePeriod == 0) noisePeriod = 1;
+ setReg(6, noisePeriod);
+ }
+
// Set tone period (2 registers per channel)
uint16 period = (uint16)c.outputPeriod;
setReg(ch * 2, period & 0xFF); // Fine tune
Commit: c267b8c31fb0209a8ba57def854571f9ceec5c56
https://github.com/scummvm/scummvm/commit/c267b8c31fb0209a8ba57def854571f9ceec5c56
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-02-23T11:39:31+01:00
Commit Message:
FREESCAPE: improve eclipse ui in atari
Changed paths:
engines/freescape/font.cpp
engines/freescape/font.h
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/font.cpp b/engines/freescape/font.cpp
index d83ac3b5980..1014f01c9fb 100644
--- a/engines/freescape/font.cpp
+++ b/engines/freescape/font.cpp
@@ -59,6 +59,8 @@ Common::String centerAndPadString(const Common::String &str, int size) {
Font::Font() {
_backgroundColor = 0;
_secondaryColor = 0;
+ _tertiaryColor = 0;
+ _quaternaryColor = 0;
_kerningOffset = 0;
_charWidth = 0;
_chars.clear();
@@ -68,6 +70,8 @@ Font::Font(Common::Array<Graphics::ManagedSurface *> &chars) {
_chars = chars;
_backgroundColor = 0;
_secondaryColor = 0;
+ _tertiaryColor = 0;
+ _quaternaryColor = 0;
_kerningOffset = 0;
_charWidth = 8;
}
@@ -95,6 +99,14 @@ void Font::setSecondaryColor(uint32 color) {
_secondaryColor = color;
}
+void Font::setTertiaryColor(uint32 color) {
+ _tertiaryColor = color;
+}
+
+void Font::setQuaternaryColor(uint32 color) {
+ _quaternaryColor = color;
+}
+
void Font::setBackground(uint32 color) {
_backgroundColor = color;
}
@@ -118,19 +130,25 @@ void Font::drawChar(Graphics::Surface *dst, uint32 chr, int x, int y, uint32 col
uint8 rb, gb, bb;
uint8 rp, gp, bp;
uint8 rs, gs, bs;
+ uint8 rt, gt, bt;
+ uint8 rq, gq, bq;
dst->format.colorToRGB(color, rp, gp, bp);
dst->format.colorToRGB(_secondaryColor, rs, gs, bs);
dst->format.colorToRGB(_backgroundColor, rb, gb, bb);
+ dst->format.colorToRGB(_tertiaryColor, rt, gt, bt);
+ dst->format.colorToRGB(_quaternaryColor, rq, gq, bq);
- byte palette[3][3] = {
+ byte palette[5][3] = {
{ rb, gb, bb },
{ rp, gp, bp },
{ rs, gs, bs },
+ { rt, gt, bt },
+ { rq, gq, bq },
};
if (surface.format != dst->format)
- surface.convertToInPlace(dst->format, (byte *)palette, 3);
+ surface.convertToInPlace(dst->format, (byte *)palette, 5);
if (_backgroundColor == dst->format.ARGBToColor(0x00, 0x00, 0x00, 0x00))
dst->copyRectToSurfaceWithKey(surface, x, y, Common::Rect(0, 0, MIN(int(surface.w), _charWidth), surface.h), dst->format.ARGBToColor(0xFF, 0x00, 0x00, 0x00));
@@ -214,6 +232,40 @@ Common::Array<Graphics::ManagedSurface *> FreescapeEngine::getCharsAmigaAtariInt
return chars;
}
+Common::Array<Graphics::ManagedSurface *> FreescapeEngine::getChars4Plane(Common::SeekableReadStream *file, int offset, int charsNumber) {
+ // 4-bitplane font: each glyph = 8 rows x 4 planes x 2 bytes = 64 bytes
+ // Used by Eclipse Atari ST for the bordered/embossed UI font
+ int glyphSize = 64;
+ int fontSize = glyphSize * charsNumber;
+ byte *fontBuffer = (byte *)malloc(fontSize);
+ file->seek(offset);
+ file->read(fontBuffer, fontSize);
+
+ Common::Array<Graphics::ManagedSurface *> chars;
+ for (int c = 0; c < charsNumber - 1; c++) {
+ Graphics::ManagedSurface *surface = new Graphics::ManagedSurface();
+ surface->create(8, 8, Graphics::PixelFormat::createFormatCLUT8());
+ for (int row = 0; row < 8; row++) {
+ int rowOff = c * glyphSize + row * 8;
+ uint16 p0 = READ_BE_UINT16(&fontBuffer[rowOff + 0]);
+ uint16 p1 = READ_BE_UINT16(&fontBuffer[rowOff + 2]);
+ uint16 p2 = READ_BE_UINT16(&fontBuffer[rowOff + 4]);
+ uint16 p3 = READ_BE_UINT16(&fontBuffer[rowOff + 6]);
+ for (int col = 0; col < 8; col++) {
+ int bit = 15 - col; // MSB = leftmost pixel
+ byte color = ((p0 >> bit) & 1)
+ | (((p1 >> bit) & 1) << 1)
+ | (((p2 >> bit) & 1) << 2)
+ | (((p3 >> bit) & 1) << 3);
+ surface->setPixel(col, row, color);
+ }
+ }
+ chars.push_back(surface);
+ }
+ free(fontBuffer);
+ return chars;
+}
+
Common::Array<Graphics::ManagedSurface *> FreescapeEngine::getCharsAmigaAtari(Common::SeekableReadStream *file, int offset, int charsNumber) {
return getCharsAmigaAtariInternal(8, 8, isEclipse() ? 0 : 1, isDriller() ? 33 : 16, isDriller() ? 32 : 16, file, offset, charsNumber);
}
diff --git a/engines/freescape/font.h b/engines/freescape/font.h
index 80ca9acbf94..65f5cc69113 100644
--- a/engines/freescape/font.h
+++ b/engines/freescape/font.h
@@ -37,6 +37,8 @@ public:
void setBackground(uint32 color);
void setSecondaryColor(uint32 color);
+ void setTertiaryColor(uint32 color);
+ void setQuaternaryColor(uint32 color);
int getFontHeight() const override;
int getMaxCharWidth() const override;
int getCharWidth(uint32 chr) const override;
@@ -51,6 +53,8 @@ private:
Common::Array<Graphics::ManagedSurface *> _chars;
uint32 _backgroundColor;
uint32 _secondaryColor;
+ uint32 _tertiaryColor;
+ uint32 _quaternaryColor;
int _kerningOffset;
int _charWidth;
};
diff --git a/engines/freescape/freescape.h b/engines/freescape/freescape.h
index 6a16947929d..0cd8bdeb912 100644
--- a/engines/freescape/freescape.h
+++ b/engines/freescape/freescape.h
@@ -107,6 +107,8 @@ enum FreescapeAction {
kActionSelectPrince,
kActionSelectPrincess,
kActionQuit,
+ kActionToggleFlashlight,
+
// Demo actions
kActionUnknownKey,
kActionWait
@@ -574,6 +576,7 @@ public:
Common::Array<Graphics::ManagedSurface *> getChars(Common::SeekableReadStream *file, int offset, int charsNumber);
Common::Array<Graphics::ManagedSurface *> getCharsAmigaAtariInternal(int sizeX, int sizeY, int additional, int m1, int m2, Common::SeekableReadStream *file, int offset, int charsNumber);
Common::Array<Graphics::ManagedSurface *> getCharsAmigaAtari(Common::SeekableReadStream *file, int offset, int charsNumber);
+ Common::Array<Graphics::ManagedSurface *> getChars4Plane(Common::SeekableReadStream *file, int offset, int charsNumber);
Common::StringArray _currentAreaMessages;
Common::StringArray _currentEphymeralMessages;
Font _font;
diff --git a/engines/freescape/games/eclipse/atari.cpp b/engines/freescape/games/eclipse/atari.cpp
index 1ad82118ed6..af96e2b0aea 100644
--- a/engines/freescape/games/eclipse/atari.cpp
+++ b/engines/freescape/games/eclipse/atari.cpp
@@ -158,13 +158,220 @@ void EclipseEngine::drawCPCUI(Graphics::Surface *surface) {
drawEclipseIndicator(surface, 228, 0, front, other);
}*/
+// 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] = {
+ 0, 0, 0, // 0: $000 black
+ 145, 72, 0, // 1: $420 dark brown
+ 182, 109, 36, // 2: $531 medium brown
+ 218, 145, 36, // 3: $641 golden brown
+ 255, 182, 36, // 4: $751 bright gold
+ 255, 218, 145, // 5: $764
+ 218, 218, 218, // 6: $666
+ 182, 182, 182, // 7: $555
+ 145, 145, 145, // 8: $444
+ 109, 109, 109, // 9: $333
+ 72, 72, 72, // 10: $222
+ 182, 36, 0, // 11: $510 dark red
+ 255, 72, 0, // 12: $720
+ 255, 109, 0, // 13: $730
+ 255, 145, 0, // 14: $740
+ 255, 255, 255, // 15: $777 white
+};
+
+// Load raw 4-plane pixel data (no mask) from stream into a CLUT8 surface.
+static Graphics::ManagedSurface *loadAtariSTRawSprite(Common::SeekableReadStream *stream,
+ int pixelOffset, int cols, int rows) {
+ stream->seek(pixelOffset);
+ Graphics::ManagedSurface *surface = new Graphics::ManagedSurface();
+ surface->create(cols * 16, rows, Graphics::PixelFormat::createFormatCLUT8());
+ for (int row = 0; row < rows; row++) {
+ for (int col = 0; col < cols; col++) {
+ uint16 p0 = stream->readUint16BE();
+ uint16 p1 = stream->readUint16BE();
+ uint16 p2 = stream->readUint16BE();
+ uint16 p3 = stream->readUint16BE();
+ for (int bit = 15; bit >= 0; bit--) {
+ int x = col * 16 + (15 - bit);
+ byte color = ((p0 >> bit) & 1)
+ | (((p1 >> bit) & 1) << 1)
+ | (((p2 >> bit) & 1) << 2)
+ | (((p3 >> bit) & 1) << 3);
+ surface->setPixel(x, row, color);
+ }
+ }
+ }
+ return surface;
+}
+
+static 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);
+ Common::Array<uint16> mask(cols);
+ for (int c = 0; c < cols; c++)
+ mask[c] = stream->readUint16BE();
+
+ // Read pixel data: sequential 4-plane words, cols per row
+ stream->seek(pixelOffset);
+ Graphics::ManagedSurface *surface = new Graphics::ManagedSurface();
+ surface->create(cols * 16, rows, Graphics::PixelFormat::createFormatCLUT8());
+ for (int row = 0; row < rows; row++) {
+ for (int col = 0; col < cols; col++) {
+ uint16 p0 = stream->readUint16BE();
+ uint16 p1 = stream->readUint16BE();
+ uint16 p2 = stream->readUint16BE();
+ uint16 p3 = stream->readUint16BE();
+ for (int bit = 15; bit >= 0; bit--) {
+ int x = col * 16 + (15 - bit);
+ byte color = ((p0 >> bit) & 1)
+ | (((p1 >> bit) & 1) << 1)
+ | (((p2 >> bit) & 1) << 2)
+ | (((p3 >> bit) & 1) << 3);
+ surface->setPixel(x, row, color);
+ }
+ }
+ }
+ return surface;
+}
+
+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
+ // the 3D viewport (area palette) and the border/UI area (border palette).
+ uint32 pal[5];
+ pal[0] = _gfx->_texturePixelFormat.ARGBToColor(0xFF, 0, 0, 0);
+ pal[1] = _gfx->_texturePixelFormat.ARGBToColor(0xFF, 145, 72, 0);
+ pal[2] = _gfx->_texturePixelFormat.ARGBToColor(0xFF, 182, 109, 36);
+ pal[3] = _gfx->_texturePixelFormat.ARGBToColor(0xFF, 218, 145, 36);
+ pal[4] = _gfx->_texturePixelFormat.ARGBToColor(0xFF, 255, 182, 36);
+ _font.setBackground(pal[0]);
+ _font.setSecondaryColor(pal[2]);
+ _font.setTertiaryColor(pal[3]);
+ _font.setQuaternaryColor(pal[4]);
+
+ // Score: Font B at x=$8B(139), y=$06(6) â from $11A66/$11A6E
+ int score = _gameStateVars[k8bitVariableScore];
+ drawScoreString(score, 139, 6, pal[1], pal[0], surface);
+
+ // Room name / messages: $CE2 at x=$55(85), y=$77(119) â from $11BDC-$11C02
+ Common::String message;
+ int deadline;
+ getLatestMessages(message, deadline);
+ if (deadline <= _countdown) {
+ drawStringInSurface(message, 85, 119, pal[1], pal[2], pal[0], surface);
+ _temporaryMessages.push_back(message);
+ _temporaryMessageDeadlines.push_back(deadline);
+ } else if (!_currentAreaMessages.empty())
+ drawStringInSurface(_currentArea->_name, 85, 119, pal[1], pal[2], pal[0], surface);
+
+ // Step indicator: $CDC at x=$4C(76), y=$77(119)
+ // d7 = $42 + (2 - _playerStepIndex) * 2, drawChar chr = d7 + 31
+ {
+ int d7 = 0x42 + (2 - _playerStepIndex) * 2;
+ int chr = d7 + 31;
+ _font.drawChar(surface, chr, 76, 119, pal[1]);
+ }
+
+ // Height indicator: $CDC at x=$E0(224), y=$77(119)
+ // d7 = $48 + (2 - _playerHeightNumber) * 2, drawChar chr = d7 + 31
+ {
+ int d7 = 0x48 + (2 - _playerHeightNumber) * 2;
+ int chr = d7 + 31;
+ _font.drawChar(surface, chr, 224, 119, pal[1]);
+ }
+
+ // Rotation/shooting indicator: $CDC at x=$E9(233), y=$77(119)
+ // d7 = $4E normally, $50 when shooting
+ if (_shootingFrames > 0) {
+ int chr = 0x50 + 31;
+ _font.drawChar(surface, chr, 233, 119, pal[1]);
+ } else {
+ int chr = 0x4E + 31;
+ _font.drawChar(surface, chr, 233, 119, pal[1]);
+ }
+
+ // Eclipse animation: sprite blit at x=$A0(160), y=$86(134) â from $1E9E/$1EA6
+ if (_eclipseSprites.size() >= 2) {
+ // Toggle between 2 frames based on countdown
+ int frame = (_countdown / 30) % 2;
+ surface->copyRectToSurface(*_eclipseSprites[frame], 160, 134,
+ Common::Rect(_eclipseSprites[frame]->w, _eclipseSprites[frame]->h));
+ }
+
+ // Shield energy bar: sprite blit at x=$80(128), y=$84(132) â from $11CD8/$11CE0
+ // 16 frames selected by shield level (0-15)
+ if (_shieldSprites.size() >= 16) {
+ int shieldLevel = _gameStateVars[k8bitVariableShield] * 15 / _maxShield;
+ shieldLevel = CLIP(shieldLevel, 0, 15);
+ surface->copyRectToSurface(*_shieldSprites[shieldLevel], 128, 132,
+ Common::Rect(_shieldSprites[shieldLevel]->w, _shieldSprites[shieldLevel]->h));
+ }
+
+ // Ankh indicators at y=$B6(182), x = (ankh_idx-1)*16 + 3 â from $11D88
+ drawIndicator(surface, 3, 182, 16);
+
+ // Compass at x=$30(48), y=$8B(139) â pre-rendered background + needle
+ if (_compassSprites.size() >= 37) {
+ // Map yaw (0-359) to lookup table index (0-71, 5 degrees each)
+ int lookupIdx = ((int)_yaw % 360) / 5;
+ if (lookupIdx < 0)
+ lookupIdx += 72;
+ int needleFrame = _compassLookup[lookupIdx];
+ if (needleFrame < (int)_compassSprites.size()) {
+ surface->copyRectToSurface(*_compassSprites[needleFrame], 48, 139,
+ Common::Rect(_compassSprites[needleFrame]->w, _compassSprites[needleFrame]->h));
+ }
+ }
+
+ // Lantern switch at x=$30(48), y=$91(145) â 2 frames (32x23), toggled with 'T' key
+ // Frame 0 = on, frame 1 = off
+ 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));
+ }
+
+ // Shooting crosshair overlay at x=$80(128), y=$9F(159)
+ if (_shootingFrames > 0 && _shootSprites.size() >= 2) {
+ int shootFrame = (_shootingFrames > 5) ? 1 : 0;
+ surface->copyRectToSurface(*_shootSprites[shootFrame], 128, 159,
+ Common::Rect(_shootSprites[shootFrame]->w, _shootSprites[shootFrame]->h));
+ }
+
+ // Analog clock â kept from existing implementation
+ uint8 r, g, b;
+ uint32 color = _currentArea->_underFireBackgroundColor;
+ _gfx->readFromPalette(color, r, g, b);
+ uint32 front = _gfx->_texturePixelFormat.ARGBToColor(0xFF, r, g, b);
+
+ color = _currentArea->_usualBackgroundColor;
+ if (_gfx->_colorRemaps && _gfx->_colorRemaps->contains(color))
+ color = (*_gfx->_colorRemaps)[color];
+ _gfx->readFromPalette(color, r, g, b);
+ uint32 back = _gfx->_texturePixelFormat.ARGBToColor(0xFF, r, g, b);
+
+ color = _currentArea->_inkColor;
+ _gfx->readFromPalette(color, r, g, b);
+ uint32 other = _gfx->_texturePixelFormat.ARGBToColor(0xFF, r, g, b);
+
+ drawAnalogClock(surface, 90, 172, back, other, front);
+}
+
void EclipseEngine::loadAssetsAtariFullGame() {
Common::File file;
file.open("0.tec");
_title = loadAndConvertNeoImage(&file, 0x17ac);
file.close();
- Common::SeekableReadStream *stream = decryptFileAmigaAtari("1.tec", "0.tec", 0x1774 - 4 * 1024);
+ Common::SeekableReadStream *stream = decryptFileAmigaAtari("1.tec", "0.tec", 0x1774 - 4 * 1024);
parseAmigaAtariHeader(stream);
loadMessagesVariableSize(stream, 0x87a6, 28);
@@ -183,17 +390,121 @@ void EclipseEngine::loadAssetsAtariFullGame() {
stream->read(_musicData.data(), kTEMusicTextSize);
debug(3, "TE-Atari: Loaded TEMUSIC.ST TEXT segment (%d bytes)", kTEMusicTextSize);
- /*
- loadFonts(stream, 0xd06b, _fontBig);
- loadFonts(stream, 0xd49a, _fontMedium);
- loadFonts(stream, 0xd49b, _fontSmall);
+ // UI font (Font A): 4-plane 16-color bordered font at prog $24C3E (file offset $24C5A)
+ // 85 characters: ASCII text (32-116) plus special indicator glyphs (65-84)
+ Common::Array<Graphics::ManagedSurface *> chars;
+ chars = getChars4Plane(stream, 0x24C5A, 85);
+ _font = Font(chars);
+
+ // Score font (Font B): 4-plane 10-glyph font at prog $249BE (file offset $249DA)
+ // Dedicated score digits 0-9 with different bordered style
+ Common::Array<Graphics::ManagedSurface *> scoreChars;
+ scoreChars = getChars4Plane(stream, 0x249DA, 11);
+ _fontScore = Font(scoreChars);
+
+ // All sprite addresses below are program addresses from the 68K disassembly.
+ // The decrypted stream includes a $1C-byte GEMDOS header, so add $1C to
+ // convert program addresses to stream offsets.
+ static const int kHdr = 0x1C;
+
+ // Eclipse animation sprites: 2 frames, 16x13 pixels
+ // Descriptor at prog $1D2B8 (1 col à 13 rows), mask at +6, pixels at +8
+ // Frame 1 pixels at prog $1D7AB
+ _eclipseSprites.resize(2);
+ _eclipseSprites[0] = loadAtariSTSprite(stream, 0x1D2BE + kHdr, 0x1D2C0 + kHdr, 1, 13);
+ _eclipseSprites[1] = loadAtariSTSprite(stream, 0x1D2BE + kHdr, 0x1D7AB + kHdr, 1, 13);
+
+ // Shield energy bar sprites: 16 frames, 16x16 pixels
+ // Descriptor at prog $1DA90 (1 col à 16 rows), mask at +6, pixels at +8
+ // Each frame = 128 bytes (16 rows à 4 words à 2 bytes)
+ _shieldSprites.resize(16);
+ for (int i = 0; i < 16; i++)
+ _shieldSprites[i] = loadAtariSTSprite(stream, 0x1DA96 + kHdr, 0x1DA98 + kHdr + i * 128, 1, 16);
+
+ // Ankh indicator: descriptor at prog $1B72C (1 col à 15 rows = 16x15), mask at +6, pixels at +8
+ Graphics::ManagedSurface *ankhManaged = loadAtariSTSprite(stream, 0x1B732 + kHdr, 0x1B734 + kHdr, 1, 15);
+ ankhManaged->convertToInPlace(_gfx->_texturePixelFormat, const_cast<byte *>(kBorderPalette), 16);
+ Graphics::Surface *ankhSurface = new Graphics::Surface();
+ ankhSurface->copyFrom(*ankhManaged);
+ delete ankhManaged;
+ _indicators.push_back(ankhSurface);
+
+ // 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.
+ {
+ 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;
+
+ // Convert to target format
+ composite->convertToInPlace(_gfx->_texturePixelFormat,
+ const_cast<byte *>(kBorderPalette), 16);
+ _compassSprites[f] = composite;
+ }
+ delete compassBG;
+ }
- load8bitBinary(stream, 0x20918, 16);
- loadMessagesVariableSize(stream, 0x3f6f, 66);
+ // Lantern light animation: 6 frames, 32x6, at prog $2026A, stride 96 bytes
+ _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);
+ }
- loadPalettes(stream, 0x204d6);
- loadGlobalObjects(stream, 0x32f6, 24);
- loadSoundsFx(stream, 0x266e8, 11);*/
+ // Lantern switch: 2 frames, 32x23, at prog $204B4, stride $170 bytes
+ // Frame 0 = on, frame 1 = off (toggled with 'T' key)
+ _lanternSwitchSprites.resize(2);
+ _lanternSwitchSprites[0] = loadAtariSTRawSprite(stream, 0x204B4 + kHdr, 2, 23);
+ _lanternSwitchSprites[1] = loadAtariSTRawSprite(stream, 0x204B4 + 0x170 + kHdr, 2, 23);
+ for (auto &sprite : _lanternSwitchSprites)
+ sprite->convertToInPlace(_gfx->_texturePixelFormat,
+ const_cast<byte *>(kBorderPalette), 16);
+
+ // Shooting crosshair sprites: 2 frames with mask, at prog $1CC26 and $1CDC0
+ // Frame 0: 32x25 (2 cols), frame 1: 48x25 (3 cols)
+ _shootSprites.resize(2);
+ _shootSprites[0] = loadAtariSTSprite(stream, 0x1CC2C + kHdr, 0x1CC30 + kHdr, 2, 25);
+ _shootSprites[1] = loadAtariSTSprite(stream, 0x1CDC6 + kHdr, 0x1CDCC + kHdr, 3, 25);
+ for (auto &sprite : _shootSprites)
+ sprite->convertToInPlace(_gfx->_texturePixelFormat,
+ const_cast<byte *>(kBorderPalette), 16);
+
+ // Convert eclipse and shield sprites from CLUT8 to target format using border palette
+ for (auto &sprite : _eclipseSprites)
+ sprite->convertToInPlace(_gfx->_texturePixelFormat, const_cast<byte *>(kBorderPalette), 16);
+ for (auto &sprite : _shieldSprites)
+ sprite->convertToInPlace(_gfx->_texturePixelFormat, const_cast<byte *>(kBorderPalette), 16);
+
+ _fontLoaded = true;
}
} // End of namespace Freescape
diff --git a/engines/freescape/games/eclipse/eclipse.cpp b/engines/freescape/games/eclipse/eclipse.cpp
index f052d25dfe7..6b5d0e5ef5d 100644
--- a/engines/freescape/games/eclipse/eclipse.cpp
+++ b/engines/freescape/games/eclipse/eclipse.cpp
@@ -94,6 +94,7 @@ EclipseEngine::EclipseEngine(OSystem *syst, const ADGameDescription *gd) : Frees
_lastFiveSeconds = 0;
_lastSecond = -1;
_resting = false;
+ _flashlightOn = false;
}
void EclipseEngine::initGameState() {
@@ -109,6 +110,7 @@ void EclipseEngine::initGameState() {
_lastThirtySeconds = seconds / 30;
_lastFiveSeconds = seconds / 5;
_resting = false;
+ _flashlightOn = false;
// Start playing music, if any, in any supported format
playMusic("Total Eclipse Theme");
@@ -292,6 +294,11 @@ void EclipseEngine::initKeymaps(Common::Keymap *engineKeyMap, Common::Keymap *in
act->setCustomEngineActionEvent(kActionFaceForward);
act->addDefaultInputMapping("f");
engineKeyMap->addAction(act);
+
+ act = new Common::Action("FLASHLIGHT", _("Toggle Flashlight"));
+ act->setCustomEngineActionEvent(kActionToggleFlashlight);
+ act->addDefaultInputMapping("t");
+ engineKeyMap->addAction(act);
}
void EclipseEngine::gotoArea(uint16 areaID, int entranceID) {
@@ -531,6 +538,8 @@ void EclipseEngine::pressedKey(const int keycode) {
} else if (keycode == kActionFaceForward) {
_pitch = 0;
updateCamera();
+ } else if (keycode == kActionToggleFlashlight) {
+ _flashlightOn = !_flashlightOn;
}
}
@@ -770,19 +779,46 @@ void EclipseEngine::drawScoreString(int score, int x, int y, uint32 front, uint3
drawStringInSurface(scoreStr, x, y, front, back, surface);
return;
}
+ }
+ // Atari ST: use Font B (_fontScore) with dedicated score digit glyphs.
+ // 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()) {
+ _fontScore.setBackground(back);
+ _fontScore.setSecondaryColor(front);
+ // Font B uses palette indices 1-4 like Font A
+ uint32 pal2 = _gfx->_texturePixelFormat.ARGBToColor(0xFF, 182, 109, 36);
+ uint32 pal3 = _gfx->_texturePixelFormat.ARGBToColor(0xFF, 218, 145, 36);
+ uint32 pal4 = _gfx->_texturePixelFormat.ARGBToColor(0xFF, 255, 182, 36);
+ _fontScore.setSecondaryColor(pal2);
+ _fontScore.setTertiaryColor(pal3);
+ _fontScore.setQuaternaryColor(pal4);
+ for (int i = 0; i < int(scoreStr.size()); i++) {
+ int chr = (scoreStr[i] - '0') + 32;
+ _fontScore.drawChar(surface, chr, x, y, front);
+ x += 8;
+ }
+ return;
}
// Start in x,y and draw each digit, from left to right, adding a gap every 3 digits
int gapSize = isC64() ? 8 : 4;
+ int charStep = 8;
+
+ Font *scoreFont = &_font;
+ scoreFont->setBackground(back);
+ scoreFont->setSecondaryColor(front);
for (int i = 0; i < int(scoreStr.size()); i++) {
- drawStringInSurface(Common::String(scoreStr[i]), x, y, front, back, surface);
- x += 8;
+ Common::String digit(scoreStr[i]);
+ digit.toUppercase();
+ scoreFont->drawString(surface, digit, x, y, _screenW, front);
+ x += charStep;
if ((i - scoreStr.size() + 1) % 3 == 1)
x += gapSize;
}
-
}
diff --git a/engines/freescape/games/eclipse/eclipse.h b/engines/freescape/games/eclipse/eclipse.h
index 5f8f942523d..c6589b090c2 100644
--- a/engines/freescape/games/eclipse/eclipse.h
+++ b/engines/freescape/games/eclipse/eclipse.h
@@ -59,6 +59,7 @@ public:
int _soundIndexEndFalling;
bool _resting;
+ bool _flashlightOn;
int _lastThirtySeconds;
int _lastFiveSeconds;
@@ -87,6 +88,7 @@ public:
void drawCPCUI(Graphics::Surface *surface) override;
void drawC64UI(Graphics::Surface *surface) override;
void drawZXUI(Graphics::Surface *surface) override;
+ void drawAmigaAtariSTUI(Graphics::Surface *surface) override;
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);
@@ -98,6 +100,16 @@ public:
Common::Array<byte> _musicData; // TEMUSIC.ST TEXT segment (Atari ST)
+ // Atari ST UI sprites (extracted from binary, pre-converted to target format)
+ 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 *> _shieldSprites; // 16 shield level frames (16x16)
+ Common::Array<Graphics::ManagedSurface *> _compassSprites; // 37 pre-composited compass frames (32x27)
+ 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)
+ byte _compassLookup[72]; // direction-to-needle-frame lookup table
+
bool checkIfGameEnded() override;
void endGame() override;
void loadSoundsFx(Common::SeekableReadStream *file, int offset, int number) override;
Commit: 8be72a7c1e9c2de86208d7c7d40f84d5ce2ab83a
https://github.com/scummvm/scummvm/commit/8be72a7c1e9c2de86208d7c7d40f84d5ce2ab83a
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-02-23T11:39:31+01:00
Commit Message:
FREESCAPE: improved eclipse ui in atari
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 af96e2b0aea..b8016af15a1 100644
--- a/engines/freescape/games/eclipse/atari.cpp
+++ b/engines/freescape/games/eclipse/atari.cpp
@@ -311,25 +311,22 @@ void EclipseEngine::drawAmigaAtariSTUI(Graphics::Surface *surface) {
// Ankh indicators at y=$B6(182), x = (ankh_idx-1)*16 + 3 â from $11D88
drawIndicator(surface, 3, 182, 16);
- // Compass at x=$30(48), y=$8B(139) â pre-rendered background + needle
+ // Compass at x=$B0(176), y=$97(151) â from sprite header at prog $2097C/$2097E
if (_compassSprites.size() >= 37) {
- // Map yaw (0-359) to lookup table index (0-71, 5 degrees each)
- int lookupIdx = ((int)_yaw % 360) / 5;
- if (lookupIdx < 0)
- lookupIdx += 72;
+ // 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], 48, 139,
+ surface->copyRectToSurface(*_compassSprites[needleFrame], 176, 151,
Common::Rect(_compassSprites[needleFrame]->w, _compassSprites[needleFrame]->h));
}
}
- // Lantern switch at x=$30(48), y=$91(145) â 2 frames (32x23), toggled with 'T' key
- // Frame 0 = on, frame 1 = off
- if (_lanternSwitchSprites.size() >= 2) {
- int switchFrame = _flashlightOn ? 0 : 1;
- surface->copyRectToSurface(*_lanternSwitchSprites[switchFrame], 48, 145,
- Common::Rect(_lanternSwitchSprites[switchFrame]->w, _lanternSwitchSprites[switchFrame]->h));
+ // 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 light animation overlay at (48, 139) â 6 frames, 32x6, toggled with 'T' key
Commit: 0f68a39da5551ee2ab39249cf79876b4c6c1f7db
https://github.com/scummvm/scummvm/commit/0f68a39da5551ee2ab39249cf79876b4c6c1f7db
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-02-23T11:39:31+01:00
Commit Message:
FREESCAPE: improved eclipse ui in atari
Changed paths:
engines/freescape/games/eclipse/atari.cpp
engines/freescape/games/eclipse/eclipse.h
diff --git a/engines/freescape/games/eclipse/atari.cpp b/engines/freescape/games/eclipse/atari.cpp
index b8016af15a1..4041921beba 100644
--- a/engines/freescape/games/eclipse/atari.cpp
+++ b/engines/freescape/games/eclipse/atari.cpp
@@ -291,16 +291,16 @@ void EclipseEngine::drawAmigaAtariSTUI(Graphics::Surface *surface) {
_font.drawChar(surface, chr, 233, 119, pal[1]);
}
- // Eclipse animation: sprite blit at x=$A0(160), y=$86(134) â from $1E9E/$1EA6
+ // Heart indicator: sprite blit at x=$A0(160), y=$86(134) â from $1E9E/$1EA6
+ // 2 frames: 0 = heart visible, 1 = heart hidden/dimmed. Blink cycle.
if (_eclipseSprites.size() >= 2) {
- // Toggle between 2 frames based on countdown
- int frame = (_countdown / 30) % 2;
+ int frame = (_ticks / 30) % 2;
surface->copyRectToSurface(*_eclipseSprites[frame], 160, 134,
Common::Rect(_eclipseSprites[frame]->w, _eclipseSprites[frame]->h));
}
- // Shield energy bar: sprite blit at x=$80(128), y=$84(132) â from $11CD8/$11CE0
- // 16 frames selected by shield level (0-15)
+ // Shield energy jar: sprite blit at x=$80(128), y=$84(132) â from $11CD8/$11CE0
+ // 16 frames showing jar fill level (0-15)
if (_shieldSprites.size() >= 16) {
int shieldLevel = _gameStateVars[k8bitVariableShield] * 15 / _maxShield;
shieldLevel = CLIP(shieldLevel, 0, 15);
@@ -308,8 +308,23 @@ void EclipseEngine::drawAmigaAtariSTUI(Graphics::Surface *surface) {
Common::Rect(_shieldSprites[shieldLevel]->w, _shieldSprites[shieldLevel]->h));
}
- // Ankh indicators at y=$B6(182), x = (ankh_idx-1)*16 + 3 â from $11D88
- drawIndicator(surface, 3, 182, 16);
+ // Ankh indicators at y=$B6(182), x = i*16 + 3 â from $11D88
+ // Draw collected ankhs with transparency (skip black/color-0 pixels)
+ if (_ankhSprites.size() >= 5) {
+ uint32 transparentColor = pal[0]; // black
+ Graphics::ManagedSurface *ankh = _ankhSprites[3]; // frame 3 = fully visible
+ for (int i = 0; i < _gameStateVars[kVariableEclipseAnkhs] && i < 5; i++) {
+ int destX = 3 + 16 * i;
+ int destY = 182;
+ for (int y = 0; y < ankh->h; y++) {
+ for (int x = 0; x < ankh->w; x++) {
+ uint32 pixel = ankh->getPixel(x, y);
+ if (pixel != transparentColor)
+ surface->setPixel(destX + x, destY + y, pixel);
+ }
+ }
+ }
+ }
// Compass at x=$B0(176), y=$97(151) â from sprite header at prog $2097C/$2097E
if (_compassSprites.size() >= 37) {
@@ -404,12 +419,12 @@ void EclipseEngine::loadAssetsAtariFullGame() {
// convert program addresses to stream offsets.
static const int kHdr = 0x1C;
- // Eclipse animation sprites: 2 frames, 16x13 pixels
+ // Heart indicator sprites: 2 frames, 16x13 pixels
// Descriptor at prog $1D2B8 (1 col à 13 rows), mask at +6, pixels at +8
- // Frame 1 pixels at prog $1D7AB
+ // Frame 0 = heart visible, Frame 1 = heart hidden/dimmed
_eclipseSprites.resize(2);
_eclipseSprites[0] = loadAtariSTSprite(stream, 0x1D2BE + kHdr, 0x1D2C0 + kHdr, 1, 13);
- _eclipseSprites[1] = loadAtariSTSprite(stream, 0x1D2BE + kHdr, 0x1D7AB + kHdr, 1, 13);
+ _eclipseSprites[1] = loadAtariSTSprite(stream, 0x1D2BE + kHdr, 0x1D2C0 + 104 + kHdr, 1, 13);
// Shield energy bar sprites: 16 frames, 16x16 pixels
// Descriptor at prog $1DA90 (1 col à 16 rows), mask at +6, pixels at +8
@@ -418,13 +433,14 @@ void EclipseEngine::loadAssetsAtariFullGame() {
for (int i = 0; i < 16; i++)
_shieldSprites[i] = loadAtariSTSprite(stream, 0x1DA96 + kHdr, 0x1DA98 + kHdr + i * 128, 1, 16);
- // Ankh indicator: descriptor at prog $1B72C (1 col à 15 rows = 16x15), mask at +6, pixels at +8
- Graphics::ManagedSurface *ankhManaged = loadAtariSTSprite(stream, 0x1B732 + kHdr, 0x1B734 + kHdr, 1, 15);
- ankhManaged->convertToInPlace(_gfx->_texturePixelFormat, const_cast<byte *>(kBorderPalette), 16);
- Graphics::Surface *ankhSurface = new Graphics::Surface();
- ankhSurface->copyFrom(*ankhManaged);
- delete ankhManaged;
- _indicators.push_back(ankhSurface);
+ // Ankh indicator: 5 fade-in frames at prog $1B734, 16x15 (1 col, stride 120 bytes)
+ // Mask at prog $1B732. Frame 3 = fully visible ankh.
+ _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 background at prog $20986 (32x27, raw 4-plane) and needle at prog $20B36
// (37 frames, 32x27 each, stride 432 bytes). Pre-composite background + needle.
@@ -486,6 +502,24 @@ void EclipseEngine::loadAssetsAtariFullGame() {
sprite->convertToInPlace(_gfx->_texturePixelFormat,
const_cast<byte *>(kBorderPalette), 16);
+ // Heartbeat/EKG animation: 5 frames, 16x11, at prog $20794, stride 96 bytes
+ // Drawn at (32, 138)
+ _heartbeatSprites.resize(5);
+ for (int i = 0; i < 5; i++) {
+ _heartbeatSprites[i] = loadAtariSTRawSprite(stream, 0x20794 + kHdr + i * 96, 1, 11);
+ _heartbeatSprites[i]->convertToInPlace(_gfx->_texturePixelFormat,
+ 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);
+ }
+
// Shooting crosshair sprites: 2 frames with mask, at prog $1CC26 and $1CDC0
// Frame 0: 32x25 (2 cols), frame 1: 48x25 (3 cols)
_shootSprites.resize(2);
diff --git a/engines/freescape/games/eclipse/eclipse.h b/engines/freescape/games/eclipse/eclipse.h
index c6589b090c2..e4b321c18ea 100644
--- a/engines/freescape/games/eclipse/eclipse.h
+++ b/engines/freescape/games/eclipse/eclipse.h
@@ -108,6 +108,9 @@ public:
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 *> _heartbeatSprites; // 5 heartbeat/EKG frames (16x11)
byte _compassLookup[72]; // direction-to-needle-frame lookup table
bool checkIfGameEnded() override;
Commit: 1d61c84029be808407f4cbf92ab6716474c392b7
https://github.com/scummvm/scummvm/commit/1d61c84029be808407f4cbf92ab6716474c392b7
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-02-23T11:39:31+01:00
Commit Message:
FREESCAPE: on-screen control for eclipse in 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 4041921beba..b8fbd2f6630 100644
--- a/engines/freescape/games/eclipse/atari.cpp
+++ b/engines/freescape/games/eclipse/atari.cpp
@@ -31,6 +31,33 @@ namespace Freescape {
void EclipseEngine::initAmigaAtari() {
_viewArea = Common::Rect(32, 16, 288, 118);
+
+ // On-screen control hotspots (from binary hotspot table at prog $869A)
+ // Right-side arrow buttons
+ _lookUpArea = Common::Rect(268, 133, 288, 153);
+ _lookDownArea = Common::Rect(290, 133, 310, 153);
+ _turnLeftArea = Common::Rect(268, 155, 288, 175);
+ _turnRightArea = Common::Rect(290, 155, 310, 175);
+ _uTurnArea = Common::Rect(268, 177, 288, 197);
+ _faceForwardArea = Common::Rect(290, 177, 310, 197);
+
+ // Left-side buttons
+ _moveBackwardArea = Common::Rect(9, 133, 29, 153);
+ _stepBackwardArea = Common::Rect(9, 155, 29, 175);
+ _interactArea = Common::Rect(31, 155, 51, 175);
+ _infoDisplayArea = Common::Rect(31, 133, 51, 153);
+
+ // Center/functional areas
+ _lanternArea = Common::Rect(57, 138, 75, 168);
+ _restArea = Common::Rect(85, 140, 127, 177);
+
+ // Status bar indicators
+ _stepSizeArea = Common::Rect(74, 117, 86, 129);
+ _heightArea = Common::Rect(222, 117, 234, 129);
+
+ // Save/load (menu screen)
+ _saveGameArea = Common::Rect(180, 36, 190, 47);
+ _loadGameArea = Common::Rect(180, 50, 190, 60);
}
/*void EclipseEngine::loadAssetsCPCFullGame() {
@@ -502,12 +529,12 @@ void EclipseEngine::loadAssetsAtariFullGame() {
sprite->convertToInPlace(_gfx->_texturePixelFormat,
const_cast<byte *>(kBorderPalette), 16);
- // Heartbeat/EKG animation: 5 frames, 16x11, at prog $20794, stride 96 bytes
- // Drawn at (32, 138)
- _heartbeatSprites.resize(5);
+ // Sound ON/OFF toggle: 5 frames, 16x11, at prog $20794, stride 96 bytes
+ // Drawn at (32, 138) when sound is toggled
+ _soundToggleSprites.resize(5);
for (int i = 0; i < 5; i++) {
- _heartbeatSprites[i] = loadAtariSTRawSprite(stream, 0x20794 + kHdr + i * 96, 1, 11);
- _heartbeatSprites[i]->convertToInPlace(_gfx->_texturePixelFormat,
+ _soundToggleSprites[i] = loadAtariSTRawSprite(stream, 0x20794 + kHdr + i * 96, 1, 11);
+ _soundToggleSprites[i]->convertToInPlace(_gfx->_texturePixelFormat,
const_cast<byte *>(kBorderPalette), 16);
}
diff --git a/engines/freescape/games/eclipse/eclipse.cpp b/engines/freescape/games/eclipse/eclipse.cpp
index 6b5d0e5ef5d..852df0197e2 100644
--- a/engines/freescape/games/eclipse/eclipse.cpp
+++ b/engines/freescape/games/eclipse/eclipse.cpp
@@ -543,6 +543,79 @@ void EclipseEngine::pressedKey(const int keycode) {
}
}
+bool EclipseEngine::onScreenControls(Common::Point mouse) {
+ if (!isAmiga() && !isAtariST())
+ return false;
+
+ // Right-side arrow buttons
+ if (_lookUpArea.contains(mouse)) {
+ rotate(0, -5, 0);
+ return true;
+ } else if (_lookDownArea.contains(mouse)) {
+ rotate(0, 5, 0);
+ return true;
+ } else if (_turnLeftArea.contains(mouse)) {
+ rotate(-5, 0, 0);
+ return true;
+ } else if (_turnRightArea.contains(mouse)) {
+ rotate(5, 0, 0);
+ return true;
+ } else if (_uTurnArea.contains(mouse)) {
+ _yaw += 180;
+ updateCamera();
+ return true;
+ } else if (_faceForwardArea.contains(mouse)) {
+ pressedKey(kActionFaceForward);
+ return true;
+ }
+
+ // Left-side buttons (movement buttons just consume click, like Driller)
+ if (_moveBackwardArea.contains(mouse)) {
+ return true;
+ } else if (_stepBackwardArea.contains(mouse)) {
+ return true;
+ } else if (_interactArea.contains(mouse)) {
+ activate();
+ return true;
+ } else if (_infoDisplayArea.contains(mouse)) {
+ drawInfoMenu();
+ return true;
+ }
+
+ // Center/functional areas
+ if (_lanternArea.contains(mouse)) {
+ pressedKey(kActionToggleFlashlight);
+ return true;
+ } else if (_restArea.contains(mouse)) {
+ pressedKey(kActionRest);
+ return true;
+ }
+
+ // Status bar indicators
+ if (_stepSizeArea.contains(mouse)) {
+ pressedKey(kActionChangeStepSize);
+ return true;
+ } else if (_heightArea.contains(mouse)) {
+ pressedKey(kActionToggleRiseLower);
+ return true;
+ }
+
+ // Save/load
+ if (_saveGameArea.contains(mouse)) {
+ _gfx->setViewport(_fullscreenViewArea);
+ saveGameDialog();
+ _gfx->setViewport(_viewArea);
+ return true;
+ } else if (_loadGameArea.contains(mouse)) {
+ _gfx->setViewport(_fullscreenViewArea);
+ loadGameDialog();
+ _gfx->setViewport(_viewArea);
+ return true;
+ }
+
+ return false;
+}
+
void EclipseEngine::releasedKey(const int keycode) {
if (keycode == kActionRiseOrFlyUp)
_resting = false;
diff --git a/engines/freescape/games/eclipse/eclipse.h b/engines/freescape/games/eclipse/eclipse.h
index e4b321c18ea..ddfe5e185b3 100644
--- a/engines/freescape/games/eclipse/eclipse.h
+++ b/engines/freescape/games/eclipse/eclipse.h
@@ -110,9 +110,29 @@ 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<Graphics::ManagedSurface *> _waterSprites; // 9 water ripple frames (32x9)
- Common::Array<Graphics::ManagedSurface *> _heartbeatSprites; // 5 heartbeat/EKG frames (16x11)
+ Common::Array<Graphics::ManagedSurface *> _soundToggleSprites; // 5 sound on/off toggle frames (16x11)
byte _compassLookup[72]; // direction-to-needle-frame lookup table
+ // Atari ST on-screen control hotspots (from binary hotspot table at prog $869A)
+ bool onScreenControls(Common::Point mouse) override;
+
+ Common::Rect _lookUpArea;
+ Common::Rect _lookDownArea;
+ Common::Rect _turnLeftArea;
+ Common::Rect _turnRightArea;
+ Common::Rect _uTurnArea;
+ Common::Rect _faceForwardArea;
+ Common::Rect _moveBackwardArea;
+ Common::Rect _stepBackwardArea;
+ Common::Rect _interactArea;
+ Common::Rect _infoDisplayArea;
+ Common::Rect _lanternArea;
+ Common::Rect _restArea;
+ Common::Rect _stepSizeArea;
+ Common::Rect _heightArea;
+ Common::Rect _saveGameArea;
+ Common::Rect _loadGameArea;
+
bool checkIfGameEnded() override;
void endGame() override;
void loadSoundsFx(Common::SeekableReadStream *file, int offset, int number) override;
Commit: d45d8d20ddffeaf67c6594218ad2044e4a4c9517
https://github.com/scummvm/scummvm/commit/d45d8d20ddffeaf67c6594218ad2044e4a4c9517
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-02-23T11:39:31+01:00
Commit Message:
FREESCAPE: process indicator in eclipse in atari
Changed paths:
engines/freescape/games/eclipse/atari.cpp
engines/freescape/games/eclipse/eclipse.h
diff --git a/engines/freescape/games/eclipse/atari.cpp b/engines/freescape/games/eclipse/atari.cpp
index b8fbd2f6630..1a361890019 100644
--- a/engines/freescape/games/eclipse/atari.cpp
+++ b/engines/freescape/games/eclipse/atari.cpp
@@ -326,13 +326,20 @@ void EclipseEngine::drawAmigaAtariSTUI(Graphics::Surface *surface) {
Common::Rect(_eclipseSprites[frame]->w, _eclipseSprites[frame]->h));
}
- // Shield energy jar: sprite blit at x=$80(128), y=$84(132) â from $11CD8/$11CE0
- // 16 frames showing jar fill level (0-15)
- if (_shieldSprites.size() >= 16) {
- int shieldLevel = _gameStateVars[k8bitVariableShield] * 15 / _maxShield;
- shieldLevel = CLIP(shieldLevel, 0, 15);
- surface->copyRectToSurface(*_shieldSprites[shieldLevel], 128, 132,
- Common::Rect(_shieldSprites[shieldLevel]->w, _shieldSprites[shieldLevel]->h));
+ // Eclipse progress indicator: sprite blit at x=$80(128), y=$84(132) â from $11CA0-$11CE0
+ // 16 frames showing sun being progressively eclipsed.
+ // Frame = countdown-based: frame 0 = full sun, frame 15 = nearly fully eclipsed.
+ if (_eclipseProgressSprites.size() >= 16) {
+ // Frame 0 = fully eclipsed, frame 15 = full sun
+ // progress: 1.0 at game start (full time left), 0.0 when time's up
+ int frame = 0;
+ if (_initialCountdown > 0 && _countdown > 0) {
+ float progress = float(_countdown) / float(_initialCountdown);
+ frame = (int)(15.0f * progress);
+ }
+ frame = CLIP(frame, 0, 15);
+ surface->copyRectToSurface(*_eclipseProgressSprites[frame], 128, 132,
+ Common::Rect(_eclipseProgressSprites[frame]->w, _eclipseProgressSprites[frame]->h));
}
// Ankh indicators at y=$B6(182), x = i*16 + 3 â from $11D88
@@ -453,12 +460,12 @@ void EclipseEngine::loadAssetsAtariFullGame() {
_eclipseSprites[0] = loadAtariSTSprite(stream, 0x1D2BE + kHdr, 0x1D2C0 + kHdr, 1, 13);
_eclipseSprites[1] = loadAtariSTSprite(stream, 0x1D2BE + kHdr, 0x1D2C0 + 104 + kHdr, 1, 13);
- // Shield energy bar sprites: 16 frames, 16x16 pixels
+ // Eclipse progress indicator: 16 frames, 16x16 pixels
// Descriptor at prog $1DA90 (1 col à 16 rows), mask at +6, pixels at +8
- // Each frame = 128 bytes (16 rows à 4 words à 2 bytes)
- _shieldSprites.resize(16);
+ // Frame 0 = full sun, frame 15 = nearly fully eclipsed. Each frame = 128 bytes.
+ _eclipseProgressSprites.resize(16);
for (int i = 0; i < 16; i++)
- _shieldSprites[i] = loadAtariSTSprite(stream, 0x1DA96 + kHdr, 0x1DA98 + kHdr + i * 128, 1, 16);
+ _eclipseProgressSprites[i] = loadAtariSTSprite(stream, 0x1DA96 + kHdr, 0x1DA98 + kHdr + i * 128, 1, 16);
// Ankh indicator: 5 fade-in frames at prog $1B734, 16x15 (1 col, stride 120 bytes)
// Mask at prog $1B732. Frame 3 = fully visible ankh.
@@ -556,10 +563,10 @@ void EclipseEngine::loadAssetsAtariFullGame() {
sprite->convertToInPlace(_gfx->_texturePixelFormat,
const_cast<byte *>(kBorderPalette), 16);
- // Convert eclipse and shield sprites from CLUT8 to target format using border palette
+ // Convert heart and eclipse progress sprites from CLUT8 to target format using border palette
for (auto &sprite : _eclipseSprites)
sprite->convertToInPlace(_gfx->_texturePixelFormat, const_cast<byte *>(kBorderPalette), 16);
- for (auto &sprite : _shieldSprites)
+ for (auto &sprite : _eclipseProgressSprites)
sprite->convertToInPlace(_gfx->_texturePixelFormat, const_cast<byte *>(kBorderPalette), 16);
_fontLoaded = true;
diff --git a/engines/freescape/games/eclipse/eclipse.h b/engines/freescape/games/eclipse/eclipse.h
index ddfe5e185b3..c8398c6828d 100644
--- a/engines/freescape/games/eclipse/eclipse.h
+++ b/engines/freescape/games/eclipse/eclipse.h
@@ -103,7 +103,7 @@ public:
// Atari ST UI sprites (extracted from binary, pre-converted to target format)
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 *> _shieldSprites; // 16 shield level frames (16x16)
+ Common::Array<Graphics::ManagedSurface *> _eclipseProgressSprites; // 16 eclipse animation frames (16x16)
Common::Array<Graphics::ManagedSurface *> _compassSprites; // 37 pre-composited compass frames (32x27)
Common::Array<Graphics::ManagedSurface *> _lanternLightSprites; // 6 lantern light animation frames (32x6)
Common::Array<Graphics::ManagedSurface *> _lanternSwitchSprites; // 2 lantern on/off frames (32x23)
More information about the Scummvm-git-logs
mailing list