[Scummvm-git-logs] scummvm master -> d5c8d79a1104309a75c8ea84429b4bb960d45176
whoozle
noreply at scummvm.org
Mon Jun 15 22:27:02 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:
942242cd17 VIDEO: Add raw 4XM movie playback
6f23527360 PHOENIXVR: Add Dracula script command support
57053995e0 PHOENIXVR: Add Dracula game detection
78e2d343ab PHOENIXVR: Add Dracula script compatibility fixes
db4a85bee6 PHOENIXVR: change the way engine handles undefined variables
31742a6d3e PHOENIXVR: Fix Dracula 2 timer display
e6f3921aaa PHOENIXVR: Improve Dracula runtime script handling
d5c8d79a11 PHOENIXVR: Show VR debug regions as rainbow borders
Commit: 942242cd17b3eabf9d6806f6cbc61f16917bd60b
https://github.com/scummvm/scummvm/commit/942242cd17b3eabf9d6806f6cbc61f16917bd60b
Author: Scorp (scorp at mrs.mn)
Date: 2026-06-15T23:26:54+01:00
Commit Message:
VIDEO: Add raw 4XM movie playback
Changed paths:
video/4xm_decoder.cpp
video/4xm_decoder.h
video/4xm_utils.cpp
video/4xm_utils.h
diff --git a/video/4xm_decoder.cpp b/video/4xm_decoder.cpp
index 36d2e9ed977..f72ddb7f4f2 100644
--- a/video/4xm_decoder.cpp
+++ b/video/4xm_decoder.cpp
@@ -22,6 +22,7 @@
#include "video/4xm_decoder.h"
#include "audio/audiostream.h"
#include "audio/decoders/adpcm.h"
+#include "audio/decoders/adpcm_intern.h"
#include "audio/decoders/raw.h"
#include "common/bitstream.h"
#include "common/debug.h"
@@ -55,37 +56,211 @@ static const int8_t mv[256][2] = {
} // namespace
+namespace {
+
+enum Raw4XMChunkId {
+ kRaw4XMFile = 0xfb814000,
+ kRaw4XMFrameContainer = 0xfb814100,
+ kRaw4XMFullFrame = 0xfb814210,
+ kRaw4XMDeltaFrame = 0xfb814220,
+ kRaw4XMCompressedAudio = 0xfb814230,
+ kRaw4XMCachedFrame = 0xfb814240,
+ kRaw4XMRawAudio = 0xfb814250
+};
+
+struct Raw4XMCacheEntry {
+ int32 frame = -1;
+ uint32 declaredSize = 0;
+ Common::Array<byte> data;
+};
+
+struct Raw4XMDeltaDecoder {
+ FourXM::RawDeltaReader reader;
+ uint16 *dst = nullptr;
+ const uint16 *src = nullptr;
+ int frameWidth = 0;
+ int frameHeight = 0;
+ const Common::Array<int> &fullOffsets;
+ const Common::Array<int> &expOffsets;
+
+ Raw4XMDeltaDecoder(const byte *data, uint32 size, uint16 *dstPixels, const uint16 *srcPixels,
+ int width, int height, const Common::Array<int> &fullMotionOffsets, const Common::Array<int> &expMotionOffsets) : reader(data, size), dst(dstPixels), src(srcPixels), frameWidth(width), frameHeight(height),
+ fullOffsets(fullMotionOffsets), expOffsets(expMotionOffsets) {}
+
+ bool copyLeaf(int dstOffset, int blockWidth, int blockHeight, uint32 op) {
+ uint32 motionIndex = 0;
+ uint32 transform = 0;
+ uint32 addFlag = 0;
+ if (reader.mode == 0) {
+ uint16 packed = reader.readPair();
+ motionIndex = packed & 0xfff;
+ transform = (packed >> 12) & 7;
+ addFlag = packed >> 15;
+ } else {
+ motionIndex = reader.readByteIndex();
+ uint32 packed = reader.readNibble();
+ transform = packed & 7;
+ addFlag = (packed & 0xf) >> 3;
+ }
+
+ int add = 0;
+ if (op == 3 && !addFlag)
+ return true;
+
+ if (addFlag) {
+ uint16 packed = reader.readPair();
+ if (op == 3)
+ add = (((int8)(((packed >> 5) & 0x1f) - 0x10) & 0xf) * 0x40 +
+ ((int8)(((packed >> 10) & 0x1f) - 0x10) & 0xf) * 0x800 +
+ ((byte)((((byte)packed & 0x1f) - 0x10) * 2) & 0x1f));
+ else
+ add = ((((int8)(((packed >> 10) & 0x1f) - 0x10)) & 0x1f) * 0x20 +
+ (((int8)(((packed >> 5) & 0x1f) - 0x10)) & 0x1f)) *
+ 0x20 +
+ (((int8)((packed & 0x1f) - 0x10)) & 0x1f);
+ }
+
+ const Common::Array<int> &motionTable = reader.mode == 0 ? fullOffsets : expOffsets;
+ int srcOffset = dstOffset;
+ if (motionIndex < motionTable.size())
+ srcOffset += motionTable[motionIndex];
+ const int sourceWidth = transform < 4 ? blockWidth : blockHeight;
+ const int sourceHeight = transform < 4 ? blockHeight : blockWidth;
+ if (srcOffset < 0 || srcOffset + frameWidth * (sourceHeight - 1) + sourceWidth - 1 >= frameWidth * frameHeight)
+ return false;
+
+ FourXM::copyRawBlock(dst + dstOffset, src + srcOffset, frameWidth, blockWidth, blockHeight, transform, add);
+ return true;
+ }
+
+ bool decodeDctBlock(int dstOffset) {
+ byte scaleY = reader.readNibble();
+ byte scaleCb = reader.readNibble();
+ byte scaleCr = reader.readNibble();
+ int coeffs[3][64] = {};
+ int blocks[3][64] = {};
+ FourXM::readRawCoefficients(reader, coeffs);
+ FourXM::transformRawCoefficients(coeffs[0], blocks[0], scaleY, false);
+ FourXM::transformRawCoefficients(coeffs[1], blocks[1], scaleCb, true);
+ FourXM::transformRawCoefficients(coeffs[2], blocks[2], scaleCr, true);
+ FourXM::writeRawDctBlock(blocks[0], blocks[1], blocks[2], dst + dstOffset, frameWidth);
+ return true;
+ }
+
+ bool decodeCopyBlock(int dstOffset, int blockWidth, int blockHeight, bool allowDct) {
+ return copyLeaf(dstOffset, blockWidth, blockHeight, 0);
+ }
+
+ bool decodeVerticalSplit(int dstOffset, int blockWidth, int blockHeight, bool allowDct) {
+ if (blockHeight < 2)
+ return false;
+ decodeBlock(dstOffset, blockWidth, blockHeight / 2, false);
+ decodeBlock(dstOffset + frameWidth * (blockHeight / 2), blockWidth, blockHeight / 2, false);
+ return true;
+ }
+
+ bool decodeHorizontalSplit(int dstOffset, int blockWidth, int blockHeight, bool allowDct) {
+ if (blockWidth < 2)
+ return false;
+ decodeBlock(dstOffset, blockWidth / 2, blockHeight, false);
+ decodeBlock(dstOffset + blockWidth / 2, blockWidth / 2, blockHeight, false);
+ return true;
+ }
+
+ bool decodeDctOrCopyBlock(int dstOffset, int blockWidth, int blockHeight, bool allowDct) {
+ uint32 subOp = reader.readControl2();
+ if (subOp == 0 && allowDct)
+ return decodeDctBlock(dstOffset);
+ if (subOp == 2)
+ return copyLeaf(dstOffset, blockWidth, blockHeight, 3);
+ return false;
+ }
+
+ bool decodeBlock(int dstOffset, int blockWidth, int blockHeight, bool allowDct) {
+ typedef bool (Raw4XMDeltaDecoder::*BlockOp)(int, int, int, bool);
+ static const BlockOp kBlockOps[4] = {
+ &Raw4XMDeltaDecoder::decodeCopyBlock,
+ &Raw4XMDeltaDecoder::decodeVerticalSplit,
+ &Raw4XMDeltaDecoder::decodeHorizontalSplit,
+ &Raw4XMDeltaDecoder::decodeDctOrCopyBlock};
+
+ return (this->*kBlockOps[reader.readControl2() & 3])(dstOffset, blockWidth, blockHeight, allowDct);
+ }
+};
+
+static bool applyRaw4XMDelta(const byte *data, uint32 size, uint16 *dst, const uint16 *src,
+ int width, int height, const Common::Array<int> &fullOffsets, const Common::Array<int> &expOffsets) {
+ Raw4XMDeltaDecoder decoder(data, size, dst, src, width, height, fullOffsets, expOffsets);
+ if (!decoder.reader.valid())
+ return false;
+
+ const int blocksX = (width + 7) / 8;
+ const int blocksY = (height + 7) / 8;
+ int blockOffset = 0;
+
+ for (int y = 0; y < blocksY; ++y) {
+ for (int x = 0; x < blocksX; ++x) {
+ decoder.decodeBlock(blockOffset, 8, 8, true);
+ blockOffset += 8;
+ }
+ blockOffset += width * 7;
+ }
+
+ return true;
+}
+
+static Raw4XMCacheEntry &raw4XMFindCacheEntry(Common::Array<Raw4XMCacheEntry> &cache, int32 frame) {
+ for (uint i = 0; i < cache.size(); ++i) {
+ if (cache[i].frame == frame)
+ return cache[i];
+ }
+
+ for (uint i = 0; i < cache.size(); ++i) {
+ if (cache[i].frame == -1) {
+ cache[i].frame = frame;
+ cache[i].declaredSize = 0;
+ cache[i].data.clear();
+ return cache[i];
+ }
+ }
+
+ cache[0].frame = frame;
+ cache[0].declaredSize = 0;
+ cache[0].data.clear();
+ return cache[0];
+}
+
+Audio::PacketizedAudioStream *makeAudioStream(byte audioType, uint audioChannels, uint sampleRate) {
+ switch (audioType) {
+ case 0: {
+ byte flags = Audio::FLAG_16BITS;
+#ifdef SCUMM_LITTLE_ENDIAN
+ flags |= Audio::FLAG_LITTLE_ENDIAN;
+#endif
+ if (audioChannels > 1)
+ flags |= Audio::FLAG_STEREO;
+ return Audio::makePacketizedRawStream(sampleRate, flags);
+ }
+ case 1:
+ return Audio::makePacketizedADPCMStream(Audio::ADPCMType::kADPCM4XM, sampleRate, audioChannels);
+ default:
+ error("FourXMAudioTrack: unknown audio type: %d", audioType);
+ }
+}
+
+} // namespace
+
class FourXMDecoder::FourXMAudioTrack : public AudioTrack {
uint _audioType;
- uint _audioChannels;
Common::ScopedPtr<Audio::PacketizedAudioStream> _output;
public:
- FourXMAudioTrack(FourXMDecoder *dec, uint trackIdx, uint audioType, uint audioChannels, uint sampleRate) : AudioTrack(Audio::Mixer::SoundType::kPlainSoundType), _audioType(audioType), _audioChannels(audioChannels) {
- switch (_audioType) {
- case 0: {
- // Raw PCM data
- byte flags = Audio::FLAG_16BITS;
-#ifdef SCUMM_LITTLE_ENDIAN
- flags |= Audio::FLAG_LITTLE_ENDIAN;
-#endif
- if (_audioChannels > 1)
- flags |= Audio::FLAG_STEREO;
- _output.reset(Audio::makePacketizedRawStream(sampleRate, flags));
- break;
- }
- case 1:
- _output.reset(Audio::makePacketizedADPCMStream(Audio::ADPCMType::kADPCM4XM, sampleRate, audioChannels));
- break;
- default:
- error("FourXMAudioTrack: unknown audio type: %d", _audioType);
- }
- }
+ FourXMAudioTrack(byte audioType, uint audioChannels, uint sampleRate) : AudioTrack(Audio::Mixer::SoundType::kPlainSoundType), _audioType(audioType),
+ _output(makeAudioStream(audioType, audioChannels, sampleRate)) {}
byte getAudioType() const { return _audioType; }
- void decode(byte *buf, uint size) {
- auto *input = new Common::MemoryReadStream(buf, size, DisposeAfterUse::YES);
+ void decode(Common::SeekableReadStream *input) {
_output->queuePacket(input);
}
@@ -93,6 +268,107 @@ private:
Audio::AudioStream *getAudioStream() const override { return _output.get(); }
};
+class FourXMDecoder::FourXMRawAudioTrack : public AudioTrack {
+ uint _audioType;
+ uint _channels;
+ uint _bits;
+ int16 _predictor[2] = {0, 0};
+ int _stepIndex[2] = {0, 0};
+ Common::ScopedPtr<Audio::QueuingAudioStream> _output;
+
+public:
+ FourXMRawAudioTrack(uint audioType, uint channels, uint sampleRate, uint bits) : AudioTrack(Audio::Mixer::SoundType::kPlainSoundType), _audioType(audioType),
+ _channels(channels), _bits(bits) {
+ if ((audioType == 0 || audioType == 1) && sampleRate > 0 && (channels == 1 || channels == 2) && bits == 16)
+ _output.reset(Audio::makeQueuingAudioStream(sampleRate, channels == 2));
+ }
+
+ void queueRaw(const byte *payload, uint32 payloadSize) {
+ if (!_output || payloadSize < 4)
+ return;
+
+ byte *data = (byte *)malloc(payloadSize - 4);
+ if (!data)
+ return;
+
+ Common::copy(payload + 4, payload + payloadSize, data);
+ _output->queueBuffer(data, payloadSize - 4, DisposeAfterUse::YES, audioFlags());
+ }
+
+ void queueADPCM(const byte *payload, uint32 payloadSize) {
+ if (!_output || _audioType != 1 || _bits != 16 || payloadSize < 4)
+ return;
+
+ const uint32 decodedBytes = READ_LE_UINT32(payload);
+ if (decodedBytes == 0)
+ return;
+
+ const uint32 samplesPerChannel = decodedBytes / (_channels * 2);
+ if (samplesPerChannel == 0)
+ return;
+
+ byte *out = (byte *)malloc(decodedBytes);
+ if (!out)
+ return;
+ Common::fill(out, out + decodedBytes, 0);
+ int16 *pcm = (int16 *)out;
+
+ const byte *src = payload + 4;
+ const uint32 srcSize = payloadSize - 4;
+ if (_channels == 1) {
+ decodeADPCMChannel(src, srcSize, 0, pcm, samplesPerChannel);
+ } else {
+ Common::Array<int16> left;
+ Common::Array<int16> right;
+ left.resize(samplesPerChannel);
+ right.resize(samplesPerChannel);
+
+ const uint32 compressedChannelSize = ((samplesPerChannel + 7) / 8) * 4;
+ decodeADPCMChannel(src, MIN<uint32>(srcSize, compressedChannelSize), 0, left.data(), samplesPerChannel);
+ if (srcSize > compressedChannelSize)
+ decodeADPCMChannel(src + compressedChannelSize, srcSize - compressedChannelSize, 1, right.data(), samplesPerChannel);
+
+ for (uint32 i = 0; i < samplesPerChannel; ++i) {
+ pcm[i * 2] = left[i];
+ pcm[i * 2 + 1] = right[i];
+ }
+ }
+
+ _output->queueBuffer(out, decodedBytes, DisposeAfterUse::YES, audioFlags());
+ }
+
+private:
+ byte audioFlags() const {
+ byte flags = Audio::FLAG_16BITS | Audio::FLAG_LITTLE_ENDIAN;
+ if (_channels == 2)
+ flags |= Audio::FLAG_STEREO;
+ return flags;
+ }
+
+ int16 decodeNibble(byte nibble, uint channel) {
+ const int step = Audio::Ima_ADPCMStream::_imaTable[_stepIndex[channel]];
+ const int diff = ((step / 2) + (nibble & 7) * step) >> 3;
+ const int sample = CLIP<int>(_predictor[channel] + ((nibble & 8) ? -diff : diff), -32768, 32767);
+ _predictor[channel] = sample;
+ _stepIndex[channel] = CLIP<int>(_stepIndex[channel] + Audio::ADPCMStream::_stepAdjustTable[nibble], 0, 88);
+ return _predictor[channel];
+ }
+
+ void decodeADPCMChannel(const byte *src, uint32 srcSize, uint channel, int16 *dst, uint32 samples) {
+ for (uint32 i = 0; i < samples; ++i) {
+ uint32 byteOffset = (i >> 3) * 4 + ((i & 7) >> 1);
+ if (byteOffset >= srcSize)
+ break;
+
+ byte packed = src[byteOffset];
+ byte nibble = (i & 1) ? (packed >> 4) : (packed & 0xf);
+ dst[i] = decodeNibble(nibble, channel);
+ }
+ }
+
+ Audio::AudioStream *getAudioStream() const override { return _output.get(); }
+};
+
class FourXMDecoder::FourXMVideoTrack : public FixedRateVideoTrack {
FourXMDecoder *_dec;
Common::Rational _frameRate;
@@ -125,7 +401,7 @@ public:
int getFrameCount() const override;
const Graphics::Surface *decodeNextFrame() override;
- void decode(uint32 tag, byte *buf, uint size);
+ void decode(uint32 tag, Common::SeekableReadStream *stream);
void decode_ifrm(Common::SeekableReadStream *stream);
void decode_pfrm(Common::SeekableReadStream *stream);
void decode_cfrm(Common::SeekableReadStream *stream);
@@ -163,6 +439,149 @@ int FourXMDecoder::FourXMVideoTrack::getFrameCount() const {
FourXMDecoder::FourXMVideoTrack::~FourXMVideoTrack() = default;
+class FourXMDecoder::FourXMRawVideoTrack : public FixedRateVideoTrack {
+ FourXMDecoder *_dec;
+ Common::Rational _frameRate;
+ uint _w, _h;
+ Common::ScopedPtr<Graphics::ManagedSurface> _surface;
+ Common::Array<uint16> _frameBuffer1;
+ Common::Array<uint16> _frameBuffer2;
+ Common::Array<uint16> *_frame = nullptr;
+ Common::Array<uint16> *_previousFrame = nullptr;
+ Common::Array<int> _fullMotionOffsets;
+ Common::Array<int> _expMotionOffsets;
+ Common::Array<Raw4XMCacheEntry> _cache;
+
+public:
+ FourXMRawVideoTrack(FourXMDecoder *dec, const Common::Rational &frameRate, uint w, uint h) : _dec(dec), _frameRate(frameRate), _w(w), _h(h) {
+ _surface.reset(new Graphics::ManagedSurface());
+ _surface->create(w, h, getPixelFormat());
+ _frameBuffer1.resize(w * h);
+ _frameBuffer2.resize(w * h);
+ _frame = &_frameBuffer1;
+ _previousFrame = &_frameBuffer2;
+ _cache.resize(256);
+ FourXM::buildRawMotionTables(w, _fullMotionOffsets, _expMotionOffsets);
+ }
+
+ uint16 getWidth() const override { return _w; }
+ uint16 getHeight() const override { return _h; }
+
+ Graphics::PixelFormat getPixelFormat() const override {
+ return Graphics::PixelFormat(2, 5, 6, 5, 0, 11, 5, 0, 0);
+ }
+
+ int getCurFrame() const override { return _dec->_curFrame; }
+ int getFrameCount() const override { return _dec->_frames.size(); }
+
+ const Graphics::Surface *decodeNextFrame() override {
+ if (_dec->_curFrame >= _dec->_frames.size())
+ return _surface->surfacePtr();
+
+ const FourXMDecoder::Frame &frameInfo = _dec->_frames[_dec->_curFrame];
+ _dec->_stream->seek(frameInfo.offset);
+ const uint32 chunkId = _dec->_stream->readUint32LE();
+ const uint32 chunkSize = _dec->_stream->readUint32LE();
+ if (chunkId != kRaw4XMFrameContainer || chunkSize < 8 || frameInfo.offset + chunkSize > frameInfo.end) {
+ warning("invalid raw 4XM frame at offset %" PRId64, frameInfo.offset);
+ ++_dec->_curFrame;
+ return _surface->surfacePtr();
+ }
+
+ Common::Array<byte> payload;
+ payload.resize(chunkSize - 8);
+ _dec->_stream->read(payload.data(), payload.size());
+ const bool changed = decodeContainerPayload(payload.data(), payload.size(), _dec->_curFrame);
+ if (changed) {
+ copyFrameToSurface();
+ SWAP(_frame, _previousFrame);
+ }
+
+ ++_dec->_curFrame;
+ return _surface->surfacePtr();
+ }
+
+private:
+ Common::Rational getFrameRate() const override { return _frameRate; }
+
+ void copyFrameToSurface() {
+ Graphics::Surface frame;
+ frame.init(_w, _h, _w * sizeof(uint16), _frame->data(), Graphics::PixelFormat(2, 5, 5, 5, 0, 10, 5, 0, 0));
+ _surface->convertFrom(frame, getPixelFormat());
+ }
+
+ bool decodeFullFrame(const byte *payload, uint32 payloadSize) {
+ if (payloadSize < _w * _h * 2)
+ return false;
+
+ Common::MemoryReadStream fullFrameStream(payload, payloadSize);
+ for (uint i = 0; i < _w * _h; ++i)
+ (*_frame)[i] = fullFrameStream.readUint16LE();
+ return true;
+ }
+
+ bool decodeCachedFrame(const byte *payload, uint32 payloadSize, uint32 currentFrame) {
+ if (payloadSize < 8)
+ return false;
+
+ const int32 cacheFrame = (int32)READ_LE_UINT32(payload);
+ const uint32 declaredSize = READ_LE_UINT32(payload + 4);
+ Raw4XMCacheEntry &entry = findCacheEntry(cacheFrame);
+ entry.declaredSize = declaredSize;
+ const uint oldSize = entry.data.size();
+ entry.data.resize(oldSize + payloadSize - 8);
+ Common::copy(payload + 8, payload + payloadSize, entry.data.begin() + oldSize);
+
+ if (cacheFrame != (int32)currentFrame || entry.data.size() < 8 ||
+ READ_LE_UINT32(entry.data.data()) != kRaw4XMFrameContainer)
+ return false;
+
+ const uint32 nestedDeclaredSize = entry.declaredSize >= 8 ? entry.declaredSize : READ_LE_UINT32(entry.data.data() + 4);
+ const uint32 nestedPayloadSize = MIN<uint32>(entry.data.size() - 8, nestedDeclaredSize - 8);
+ const bool changed = decodeContainerPayload(entry.data.data() + 8, nestedPayloadSize, currentFrame);
+
+ entry.frame = -1;
+ entry.declaredSize = 0;
+ entry.data.clear();
+ return changed;
+ }
+
+ bool decodeContainerPayload(const byte *payload, uint32 payloadSize, uint32 currentFrame) {
+ bool changed = false;
+ uint32 pos = 0;
+ while (pos + 8 <= payloadSize) {
+ const uint32 subChunkId = READ_LE_UINT32(payload + pos);
+ const uint32 subChunkSize = READ_LE_UINT32(payload + pos + 4);
+ if (subChunkSize < 8 || pos + subChunkSize > payloadSize)
+ break;
+
+ const byte *subPayload = payload + pos + 8;
+ const uint32 subPayloadSize = subChunkSize - 8;
+ if (subChunkId == kRaw4XMFullFrame) {
+ changed = decodeFullFrame(subPayload, subPayloadSize) || changed;
+ } else if (subChunkId == kRaw4XMDeltaFrame) {
+ changed = applyRaw4XMDelta(subPayload, subPayloadSize, _frame->data(), _previousFrame->data(), _w, _h,
+ _fullMotionOffsets, _expMotionOffsets) ||
+ changed;
+ } else if (subChunkId == kRaw4XMCachedFrame) {
+ changed = decodeCachedFrame(subPayload, subPayloadSize, currentFrame) || changed;
+ } else if (subChunkId == kRaw4XMCompressedAudio && _dec->_rawAudio) {
+ _dec->_rawAudio->queueADPCM(subPayload, subPayloadSize);
+ } else if (subChunkId == kRaw4XMRawAudio && _dec->_rawAudio) {
+ _dec->_rawAudio->queueRaw(subPayload, subPayloadSize);
+ }
+
+ pos += subChunkSize;
+ }
+
+ return changed;
+ }
+
+ Raw4XMCacheEntry &findCacheEntry(int32 frame) {
+ return raw4XMFindCacheEntry(_cache, frame);
+ }
+};
+
namespace {
static const uint8_t iquant[64] = {
16,
@@ -487,17 +906,17 @@ void FourXMDecoder::FourXMVideoTrack::decode_cfrm(Common::SeekableReadStream *st
}
}
-void FourXMDecoder::FourXMVideoTrack::decode(uint32 tag, byte *buf, uint size) {
- Common::MemoryReadStream ms(buf, size, DisposeAfterUse::YES);
+void FourXMDecoder::FourXMVideoTrack::decode(uint32 tag, Common::SeekableReadStream *stream) {
+ Common::ScopedPtr<Common::SeekableReadStream> ms(stream);
switch (tag) {
case MKTAG('i', 'f', 'r', 'm'):
- decode_ifrm(&ms);
+ decode_ifrm(ms.get());
break;
case MKTAG('p', 'f', 'r', 'm'):
- decode_pfrm(&ms);
+ decode_pfrm(ms.get());
break;
case MKTAG('c', 'f', 'r', 'm'):
- decode_cfrm(&ms);
+ decode_cfrm(ms.get());
break;
default:
warning("uknown video frame %s", tagName(tag).c_str());
@@ -520,14 +939,6 @@ void FourXMDecoder::decodeNextFrameImpl() {
uint32 size = _stream->readUint32LE();
auto pos = _stream->pos();
- auto loadBuf = [this](uint bufSize) {
- byte *buf = static_cast<byte *>(malloc(bufSize));
- if (!buf)
- error("failed to allocate %u bytes", bufSize);
- if (_stream->read(buf, bufSize) != bufSize)
- error("loadBuf: short read");
- return buf;
- };
switch (tag) {
case MKTAG('s', 'n', 'd', '_'): {
if (!_audio)
@@ -539,7 +950,7 @@ void FourXMDecoder::decodeNextFrameImpl() {
auto trackIdx = _stream->readUint32LE();
auto packetSize = _stream->readUint32LE();
if (trackIdx == 0 && _audio) {
- _audio->decode(loadBuf(packetSize), packetSize);
+ _audio->decode(_stream->readStream(packetSize));
} else {
_stream->skip(packetSize);
}
@@ -550,7 +961,7 @@ void FourXMDecoder::decodeNextFrameImpl() {
auto trackIdx = _stream->readUint32LE();
_stream->skip(4);
if (trackIdx == 0 && _audio) {
- _audio->decode(loadBuf(size - 8), size - 8);
+ _audio->decode(_stream->readStream(size - 8));
}
} break;
default:
@@ -562,7 +973,7 @@ void FourXMDecoder::decodeNextFrameImpl() {
case MKTAG('c', 'f', 'r', 'm'): {
auto trackIdx = _stream->readUint32LE();
if (trackIdx == 0)
- _video->decode(tag, loadBuf(size - 4), size - 4);
+ _video->decode(tag, _stream->readStream(size - 4));
} break;
default:
warning("unknown frame type %s", tagName(tag).c_str());
@@ -626,7 +1037,7 @@ void FourXMDecoder::readList(uint32 listEnd) {
debug("audio track idx: %u type: %u channels: %u sample rate: %u bits: %u", trackIdx, audioType, audioChannels, sampleRate, sampleResolution);
if (sampleResolution != 16)
error("only 16 bit audio is supported");
- addTrack(_audio = new FourXMAudioTrack(this, trackIdx, audioType, audioChannels, sampleRate));
+ addTrack(_audio = new FourXMAudioTrack(audioType, audioChannels, sampleRate));
} break;
default:
break;
@@ -639,12 +1050,66 @@ void FourXMDecoder::readList(uint32 listEnd) {
}
}
+bool FourXMDecoder::loadRawStream() {
+ _stream->seek(0);
+ if (_stream->readUint32LE() != kRaw4XMFile || _stream->readUint32LE() != 0x01000000)
+ return false;
+
+ const uint32 width = _stream->readUint32LE();
+ const uint32 height = _stream->readUint32LE();
+ _stream->skip(8);
+ _stream->skip(4);
+ const float frameRate = _stream->readFloatLE();
+ const uint16 audioCodec = _stream->readUint16LE();
+ const uint16 audioChannels = _stream->readUint16LE();
+ const uint32 sampleRate = _stream->readUint32LE();
+ _stream->skip(6);
+ const uint16 bits = _stream->readUint16LE();
+
+ if (width == 0 || height == 0)
+ return false;
+
+ _frameRate = floatToRational(frameRate > 0.0f ? frameRate : 15.0f);
+ debug("raw 4XM video %ux%u, frame rate: %d/%d", width, height, _frameRate.getNumerator(), _frameRate.getDenominator());
+
+ _frames.clear();
+ _stream->seek(0x88);
+ while (_stream->pos() + 8 <= _stream->size()) {
+ const int64 frameOffset = _stream->pos();
+ const uint32 chunkId = _stream->readUint32LE();
+ const uint32 chunkSize = _stream->readUint32LE();
+ const int64 frameEnd = frameOffset + chunkSize;
+ if (chunkSize < 8 || frameEnd > _stream->size())
+ break;
+
+ if (chunkId == kRaw4XMFrameContainer)
+ _frames.push_back({frameOffset, frameEnd});
+ _stream->seek(frameEnd);
+ }
+
+ if (_frames.empty())
+ return false;
+
+ addTrack(_rawVideo = new FourXMRawVideoTrack(this, _frameRate, width, height));
+ if ((audioCodec == 0 || audioCodec == 1) && sampleRate > 0 &&
+ (audioChannels == 1 || audioChannels == 2) && bits == 16) {
+ _rawAudio = new FourXMRawAudioTrack(audioCodec, audioChannels, sampleRate, bits);
+ addTrack(_rawAudio);
+ }
+
+ return getNumTracks() != 0;
+}
+
bool FourXMDecoder::loadStream(Common::SeekableReadStream *stream) {
_stream.reset(stream);
if (!stream->size()) {
return false;
}
+ if (stream->readUint32LE() == kRaw4XMFile)
+ return loadRawStream();
+ stream->seek(0);
+
uint32 riffTag = stream->readUint32BE();
if (riffTag != MKTAG('R', 'I', 'F', 'F')) {
warning("Failed to find RIFF header");
diff --git a/video/4xm_decoder.h b/video/4xm_decoder.h
index ebe6b6077fe..cd926826b98 100644
--- a/video/4xm_decoder.h
+++ b/video/4xm_decoder.h
@@ -45,9 +45,12 @@ private:
class FourXMVideoTrack;
class FourXMAudioTrack;
+ class FourXMRawVideoTrack;
+ class FourXMRawAudioTrack;
void readList(uint32 size);
void decodeNextFrameImpl();
+ bool loadRawStream();
uint32 _dataRate = 0;
Common::Rational _frameRate;
@@ -56,6 +59,8 @@ private:
uint _curFrame = 0;
FourXMVideoTrack *_video = nullptr;
FourXMAudioTrack *_audio = nullptr;
+ FourXMRawVideoTrack *_rawVideo = nullptr;
+ FourXMRawAudioTrack *_rawAudio = nullptr;
};
} // namespace Video
diff --git a/video/4xm_utils.cpp b/video/4xm_utils.cpp
index a6e75361fb0..7ee78a7f138 100644
--- a/video/4xm_utils.cpp
+++ b/video/4xm_utils.cpp
@@ -22,7 +22,11 @@
#include "video/4xm_utils.h"
#include "common/bitstream.h"
#include "common/debug.h"
+#include "common/endian.h"
#include "common/memstream.h"
+#include "common/textconsole.h"
+#include "graphics/pixelformat.h"
+#include <math.h>
namespace Video {
namespace FourXM {
@@ -34,6 +38,192 @@ namespace FourXM {
#define MULTIPLY(var, const) ((int)((var) * (unsigned)(const)) >> 16)
+namespace {
+
+struct RawTransform {
+ bool swapAxes;
+ bool flipX;
+ bool flipY;
+};
+
+struct RawCoefficientToken {
+ byte zeroes;
+ int8 values[2];
+ byte valueCount;
+};
+
+static const RawTransform kRawTransforms[8] = {
+ {false, false, false},
+ {false, true, false},
+ {false, false, true},
+ {false, true, true},
+ {true, true, true},
+ {true, false, true},
+ {true, true, false},
+ {true, false, false}};
+
+static const int kRawCoefficientOrder[64] = {
+ 0, 1, 8, 9, 2, 3, 10, 11,
+ 16, 17, 24, 25, 18, 19, 26, 27,
+ 4, 5, 12, 20, 13, 6, 7, 14,
+ 21, 28, 29, 22, 15, 23, 30, 31,
+ 32, 33, 40, 48, 41, 34, 35, 42,
+ 49, 56, 57, 50, 43, 51, 58, 59,
+ 36, 37, 44, 52, 45, 38, 39, 46,
+ 53, 60, 61, 54, 47, 55, 62, 63};
+
+static const RawCoefficientToken kRawCoefficientTokens[8] = {
+ {64, {0, 0}, 0},
+ {5, {0, 0}, 0},
+ {1, {1, 0}, 1},
+ {1, {-1, 0}, 1},
+ {2, {1, 0}, 1},
+ {1, {0, -1}, 2},
+ {2, {0, 1}, 2},
+ {3, {-1, 0}, 1}};
+
+static const Graphics::PixelFormat kRawPixelFormat(2, 5, 5, 5, 0, 10, 5, 0, 0);
+
+int rawSign2(byte value) {
+ return (value & 2) ? (value | ~3) : (value + 1);
+}
+
+int rawSign4(byte value) {
+ return (value & 8) ? (value | ~15) : (value + 1);
+}
+
+void rawTransformPixel(int sx, int sy, int width, int height, uint32 mode, int &dx, int &dy) {
+ const RawTransform &transform = kRawTransforms[mode & 7];
+ dx = transform.swapAxes ? sy : sx;
+ dy = transform.swapAxes ? sx : sy;
+ if (transform.flipX)
+ dx = width - 1 - dx;
+ if (transform.flipY)
+ dy = height - 1 - dy;
+}
+
+void readRawCoefficientToken(RawDeltaReader &reader, int coeff[64], int &index) {
+ auto zero = [&](int count) {
+ for (int i = 0; i < count && index < 64; ++i, ++index)
+ coeff[kRawCoefficientOrder[index]] = 0;
+ };
+ auto write = [&](int value) {
+ if (index < 64)
+ coeff[kRawCoefficientOrder[index++]] = value;
+ };
+ auto writeSigned2 = [&](byte value) {
+ write(rawSign2(value));
+ };
+ auto writeSigned4 = [&]() {
+ write(rawSign4(reader.readNibble()));
+ };
+
+ byte code = reader.readNibble();
+ if (code <= 7) {
+ const RawCoefficientToken &token = kRawCoefficientTokens[code];
+
+ zero(code == 0 ? 64 - index : token.zeroes);
+ for (byte i = 0; i < token.valueCount; ++i)
+ write(token.values[i]);
+ return;
+ }
+
+ if (code == 8) {
+ byte header = reader.readNibble();
+ byte pair = reader.readNibble();
+ zero((header >> 2) + 1);
+ writeSigned2(pair >> 2);
+ writeSigned2(pair & 3);
+
+ int valueCount = header & 3;
+ if (valueCount > 0) {
+ byte extra = reader.readNibble();
+ writeSigned2(extra >> 2);
+ if (valueCount > 1)
+ writeSigned2(extra & 3);
+ if (valueCount > 2)
+ writeSigned2(reader.readNibble() >> 2);
+ }
+ } else if (code == 9) {
+ byte header = reader.readNibble();
+ zero((header >> 2) + 1);
+ for (int i = 0; i <= (header & 3); ++i)
+ writeSigned4();
+ } else if (code == 10) {
+ byte value = reader.readNibble();
+ writeSigned2(value >> 2);
+ writeSigned2(value & 3);
+ } else if (code >= 11 && code <= 13) {
+ for (byte i = 0; i < code - 10; ++i)
+ writeSigned4();
+ } else if (code == 14) {
+ write(0);
+ } else {
+ write(reader.readSignedByte());
+ }
+}
+
+} // namespace
+
+RawDeltaReader::RawDeltaReader(const byte *ptr, uint32 len) : data(ptr), size(len) {
+ if (size < 0x18)
+ return;
+
+ mode = READ_LE_UINT32(data);
+ pairOffset = READ_LE_UINT32(data + 8) + 0x18;
+ byteOffset = READ_LE_UINT32(data + 12) + 0x18;
+ nibbleOffset = READ_LE_UINT32(data + 16) + 0x18;
+ controlWord = READ_LE_UINT32(data + 0x18);
+ if (nibbleOffset + 4 <= size)
+ nibbleWord = READ_LE_UINT32(data + nibbleOffset);
+}
+
+bool RawDeltaReader::valid() const {
+ return size >= 0x18;
+}
+
+uint32 RawDeltaReader::readControl2() {
+ uint32 result = controlWord & 3;
+ if (--wordBitsLeft == 0) {
+ uint32 off = 0x18 + wordIndex * 4;
+ controlWord = off + 4 <= size ? READ_LE_UINT32(data + off) : 0;
+ ++wordIndex;
+ wordBitsLeft = 16;
+ } else {
+ controlWord >>= 2;
+ }
+ return result;
+}
+
+uint16 RawDeltaReader::readPair() {
+ uint32 off = pairOffset + pairIndex * 2;
+ ++pairIndex;
+ return off + 2 <= size ? READ_LE_UINT16(data + off) : 0;
+}
+
+byte RawDeltaReader::readByteIndex() {
+ uint32 off = byteOffset + byteIndex;
+ ++byteIndex;
+ return off < size ? data[off] : 0;
+}
+
+int8 RawDeltaReader::readSignedByte() {
+ return (int8)readByteIndex();
+}
+
+uint32 RawDeltaReader::readNibble() {
+ uint32 result = nibbleWord & 0xf;
+ if (--nibbleBitsLeft == 0) {
+ uint32 off = nibbleOffset + nibbleWordIndex * 4;
+ nibbleWord = off + 4 <= size ? READ_LE_UINT32(data + off) : 0;
+ ++nibbleWordIndex;
+ nibbleBitsLeft = 8;
+ } else {
+ nibbleWord >>= 4;
+ }
+ return result;
+}
+
void idct(int16_t block[64], int shift) {
int tmp0, tmp1, tmp2, tmp3, tmp4, tmp5, tmp6, tmp7;
int tmp10, tmp11, tmp12, tmp13;
@@ -118,5 +308,129 @@ void idct(int16_t block[64], int shift) {
}
}
+void buildRawMotionTables(int width, Common::Array<int> &fullOffsets, Common::Array<int> &expOffsets) {
+ Common::Array<int> sorted;
+ sorted.reserve(0x1000);
+ for (int radius = 0; radius < 0x801; ++radius) {
+ for (int y = -0x20; y < 0x20; ++y) {
+ for (int x = -0x20; x < 0x20; ++x) {
+ if (x * x + y * y == radius)
+ sorted.push_back(y * width + x);
+ }
+ }
+ }
+
+ fullOffsets.resize(sorted.size());
+ for (uint i = 0; i < sorted.size(); ++i)
+ fullOffsets[i] = sorted[i];
+
+ static const float kExpMotionScale = 0.021327873691916466f;
+ expOffsets.resize(0x100);
+ int lastIndex = -1;
+ uint out = 0;
+ for (int i = 0; out < expOffsets.size(); ++i) {
+ int index = static_cast<int>(floorf(expf(i * kExpMotionScale))) - 1;
+ if (index >= (int)fullOffsets.size())
+ index = fullOffsets.size() - 1;
+ if (lastIndex < index) {
+ expOffsets[out++] = fullOffsets[index];
+ lastIndex = index;
+ }
+ }
+}
+
+void copyRawBlock(uint16 *dst, const uint16 *src, int stride, int width, int height, uint32 mode, int add) {
+ const int sourceWidth = (mode >= 4) ? height : width;
+ const int sourceHeight = (mode >= 4) ? width : height;
+
+ for (int sy = 0; sy < sourceHeight; ++sy) {
+ for (int sx = 0; sx < sourceWidth; ++sx) {
+ int dx, dy;
+ rawTransformPixel(sx, sy, width, height, mode, dx, dy);
+ if (dx >= 0 && dx < width && dy >= 0 && dy < height)
+ dst[dy * stride + dx] = src[sy * stride + sx] + add;
+ }
+ }
+}
+
+void readRawCoefficients(RawDeltaReader &reader, int coeffs[3][64]) {
+ for (int channel = 0; channel < 3; ++channel) {
+ int *coeff = coeffs[channel];
+ int index = 1;
+ coeff[0] = reader.readSignedByte();
+
+ while (index <= 63)
+ readRawCoefficientToken(reader, coeff, index);
+ }
+}
+
+void transformRawCoefficients(int coeff[64], int dst[64], int scaleCode, bool chroma) {
+ if (!chroma)
+ coeff[0] <<= 1;
+
+ int temp[64] = {};
+ const int dcScale = 1 << scaleCode;
+ const int stage1Scale = 1 << (scaleCode + 2);
+ const int stage2Scale = 1 << (scaleCode + 4);
+
+ int first[10] = {};
+ int base0 = (coeff[8] + coeff[0]) * dcScale;
+ int base1 = (coeff[0] - coeff[8]) * dcScale;
+ int base2 = (coeff[1] + coeff[9]) * dcScale;
+ int base3 = (coeff[1] - coeff[9]) * dcScale;
+ first[0] = base2 + base0;
+ first[1] = base0 - base2;
+ first[8] = base3 + base1;
+ first[9] = base1 - base3;
+
+ for (int y = 0; y < 4; y += 2) {
+ for (int x = 0; x < 4; x += 2) {
+ int index = (x >> 1) + (y >> 1) * 8;
+ int v0 = first[index] + coeff[index + 0x10] * stage1Scale;
+ int v1 = first[index] - coeff[index + 0x10] * stage1Scale;
+ int v2 = coeff[index + 2] + coeff[index + 0x12];
+ int v3 = coeff[index + 2] * stage1Scale - coeff[index + 0x12] * stage1Scale;
+ temp[y * 8 + x] = v2 * stage1Scale + v0;
+ temp[y * 8 + x + 1] = v0 - v2 * stage1Scale;
+ temp[(y + 1) * 8 + x] = v3 + v1;
+ temp[(y + 1) * 8 + x + 1] = v1 - v3;
+ }
+ }
+
+ for (int y = 0; y < 8; y += 2) {
+ for (int x = 0; x < 8; x += 2) {
+ int index = (x >> 1) + (y >> 1) * 8;
+ int v0 = coeff[index + 0x20] * stage2Scale + temp[index];
+ int v1 = temp[index] - coeff[index + 0x20] * stage2Scale;
+ int v2 = coeff[index + 4] + coeff[index + 0x24];
+ int v3 = coeff[index + 4] * stage2Scale - coeff[index + 0x24] * stage2Scale;
+ dst[y * 8 + x] = (v0 + v2 * stage2Scale) >> 6;
+ dst[y * 8 + x + 1] = (v0 - v2 * stage2Scale) >> 6;
+ dst[(y + 1) * 8 + x] = (v3 + v1) >> 6;
+ dst[(y + 1) * 8 + x + 1] = (v1 - v3) >> 6;
+ }
+ }
+}
+
+void writeRawDctBlock(const int yBlock[64], const int cbBlock[64], const int crBlock[64],
+ uint16 *dst, int stride) {
+ for (int y = 0; y < 8; ++y) {
+ uint16 *dstPixel = dst + y * stride;
+ for (int x = 0; x < 8; ++x) {
+ int index = y * 8 + x;
+ int luma = yBlock[index] + 0x80;
+ int cr = crBlock[index];
+ int cb = cbBlock[index];
+ int red = (cr + luma) >> 3;
+ int green = (luma - ((cr + cb) >> 1)) >> 3;
+ int blue = (luma + cb * 2) >> 3;
+ red = CLIP(red, 0, 0x1f);
+ green = CLIP(green, 0, 0x1f);
+ blue = CLIP(blue, 0, 0x1f);
+ dstPixel[x] = kRawPixelFormat.RGBToColor(red << 3, green << 3, blue << 3);
+ }
+ }
+}
+
} // namespace FourXM
} // namespace Video
diff --git a/video/4xm_utils.h b/video/4xm_utils.h
index bd051e26460..b18839b5abb 100644
--- a/video/4xm_utils.h
+++ b/video/4xm_utils.h
@@ -26,6 +26,32 @@
namespace Video {
namespace FourXM {
+struct RawDeltaReader {
+ const byte *data = nullptr;
+ uint32 size = 0;
+ uint32 mode = 0;
+ uint32 wordBitsLeft = 16;
+ uint32 wordIndex = 1;
+ uint32 controlWord = 0;
+ uint32 pairIndex = 0;
+ uint32 byteIndex = 0;
+ uint32 nibbleBitsLeft = 8;
+ uint32 nibbleWordIndex = 1;
+ uint32 nibbleWord = 0;
+ uint32 pairOffset = 0;
+ uint32 byteOffset = 0;
+ uint32 nibbleOffset = 0;
+
+ RawDeltaReader(const byte *ptr, uint32 len);
+
+ bool valid() const;
+ uint32 readControl2();
+ uint16 readPair();
+ byte readByteIndex();
+ int8 readSignedByte();
+ uint32 readNibble();
+};
+
template<typename HuffmanType>
HuffmanType loadStatistics(const byte *&huff, uint &offset) {
Common::Array<uint32> freqs, symbols;
@@ -52,6 +78,12 @@ HuffmanType loadStatistics(const byte *&huff, uint &offset) {
}
void idct(int16_t block[64], int shift = 6);
+void buildRawMotionTables(int width, Common::Array<int> &fullOffsets, Common::Array<int> &expOffsets);
+void copyRawBlock(uint16 *dst, const uint16 *src, int stride, int width, int height, uint32 mode, int add = 0);
+void readRawCoefficients(RawDeltaReader &reader, int coeffs[3][64]);
+void transformRawCoefficients(int coeff[64], int dst[64], int scaleCode, bool chroma);
+void writeRawDctBlock(const int yBlock[64], const int cbBlock[64], const int crBlock[64],
+ uint16 *dst, int stride);
inline int readInt(int value, unsigned n) {
if (n == 0)
Commit: 6f2352736084d42c07f18cbabd8ee65e6ad3871d
https://github.com/scummvm/scummvm/commit/6f2352736084d42c07f18cbabd8ee65e6ad3871d
Author: Scorp (scorp at mrs.mn)
Date: 2026-06-15T23:26:54+01:00
Commit Message:
PHOENIXVR: Add Dracula script command support
Changed paths:
engines/phoenixvr/commands.h
engines/phoenixvr/phoenixvr.cpp
engines/phoenixvr/phoenixvr.h
engines/phoenixvr/script.cpp
diff --git a/engines/phoenixvr/commands.h b/engines/phoenixvr/commands.h
index ca03d986cd7..65ff23ed81a 100644
--- a/engines/phoenixvr/commands.h
+++ b/engines/phoenixvr/commands.h
@@ -31,6 +31,15 @@
namespace PhoenixVR {
namespace {
+struct MultiCD_Use_Install_Path : public Script::Command {
+ Common::String path;
+
+ MultiCD_Use_Install_Path(const Common::Array<Common::String> &args) : path(args[0]) {}
+ void exec(Script::ExecutionContext &ctx) const override {
+ debug("MultiCD_Use_Install_Path %s", path.c_str());
+ }
+};
+
struct MultiCD_Set_Transition_Script : public Script::Command {
Common::String path;
@@ -50,6 +59,16 @@ struct MultiCD_Set_Next_Script : public Script::Command {
}
};
+struct MultiCD_If_Next_Script : public Script::Command {
+ Common::String cd;
+ Common::String var;
+
+ MultiCD_If_Next_Script(const Common::Array<Common::String> &args) : cd(args[0]), var(args[1]) {}
+ void exec(Script::ExecutionContext &ctx) const override {
+ debug("MultiCD_If_Next_Script %s %s", cd.c_str(), var.c_str());
+ }
+};
+
struct LoadSave_Enter_Script : public Script::Command {
Common::String reloading, notReloading;
@@ -193,6 +212,18 @@ struct Sub : public Script::Command {
}
};
+struct CopyVar : public Script::Command {
+ Common::String srcVar;
+ Common::String dstVar;
+
+ CopyVar(const Common::Array<Common::String> &args) : srcVar(args[0]), dstVar(args[1]) {}
+
+ void exec(Script::ExecutionContext &ctx) const override {
+ debug("copyvar %s %s", srcVar.c_str(), dstVar.c_str());
+ g_engine->setVariable(dstVar, g_engine->getVariable(srcVar));
+ }
+};
+
struct Not : public Script::Command {
Common::String var;
@@ -272,8 +303,15 @@ struct Cmp : public Script::Command {
Common::String op;
Common::String arg1;
- Cmp(const Common::Array<Common::String> &args) : var(args[0]), negativeVar(args[1]),
- arg0(args[2]), op(args[3]), arg1(args[4]) {}
+ Cmp(const Common::Array<Common::String> &args) : var(args[0]), negativeVar(args[1]), arg0(args[2]), op(args[3]) {
+ if (args.size() == 5) {
+ arg1 = args[4];
+ } else {
+ uint opLength = (op.size() > 1 && op[1] == '=') ? 2 : 1;
+ arg1 = op.substr(opLength);
+ op = op.substr(0, opLength);
+ }
+ }
void exec(Script::ExecutionContext &ctx) const override {
debug("cmp %s %s %s %s %s", var.c_str(), negativeVar.c_str(), arg0.c_str(), op.c_str(), arg1.c_str());
@@ -282,6 +320,8 @@ struct Cmp : public Script::Command {
auto value1 = valueOf(arg1);
if (op == "==") {
r = value0 == value1;
+ } else if (op == "!=") {
+ r = value0 != value1;
} else if (op == "<") {
r = value0 < value1;
} else if (op == "<=") {
@@ -349,6 +389,72 @@ struct SaveCoffre : public Script::Command {
}
};
+struct ExeDemo : public Script::Command {
+ Common::String var;
+
+ ExeDemo(const Common::Array<Common::String> &args) : var(args[0]) {}
+ void exec(Script::ExecutionContext &ctx) const override {
+ debug("ExeDemo %s", var.c_str());
+ g_engine->setVariable(var, 1);
+ }
+};
+
+struct AfficheImage : public Script::Command {
+ Common::String image;
+ int x;
+ int y;
+
+ AfficheImage(const Common::Array<Common::String> &args) : image(args[0]), x(atoi(args[1].c_str())), y(atoi(args[2].c_str())) {}
+ void exec(Script::ExecutionContext &ctx) const override {
+ g_engine->showImageOverlay(image, x, y);
+ }
+};
+
+struct StopAffiche : public Script::Command {
+ StopAffiche(const Common::Array<Common::String> &args) {}
+ void exec(Script::ExecutionContext &ctx) const override {
+ g_engine->stopImageOverlay();
+ }
+};
+
+struct UpdateStage : public Script::Command {
+ UpdateStage(const Common::Array<Common::String> &args) {}
+ void exec(Script::ExecutionContext &ctx) const override {
+ g_engine->updateStage();
+ }
+};
+
+struct StartCible : public Script::Command {
+ Common::String name;
+ int periodSeconds;
+ Common::Array<int> bounds;
+
+ StartCible(const Common::Array<Common::String> &args) : name(args[0]), periodSeconds(atoi(args[1].c_str())) {
+ for (uint i = 2; i < args.size(); ++i)
+ bounds.push_back(atoi(args[i].c_str()));
+ }
+ void exec(Script::ExecutionContext &ctx) const override {
+ g_engine->startCible(name, periodSeconds, bounds);
+ }
+};
+
+struct StopCible : public Script::Command {
+ StopCible(const Common::Array<Common::String> &args) {}
+ void exec(Script::ExecutionContext &ctx) const override {
+ g_engine->stopCible();
+ }
+};
+
+struct TestCible : public Script::Command {
+ Common::String xVar;
+ Common::String yVar;
+
+ TestCible(const Common::Array<Common::String> &args) : xVar(args[0]), yVar(args[1]) {}
+ void exec(Script::ExecutionContext &ctx) const override {
+ g_engine->testCible(xVar, yVar);
+ }
+};
+
struct AfficheCoffre : public Script::Command {
AfficheCoffre(const Common::Array<Common::String> &args) {}
void exec(Script::ExecutionContext &ctx) const override {
@@ -752,14 +858,17 @@ struct End : public Script::Command {
E(AddCoffreObject) \
E(AddObject) \
E(AfficheCoffre) \
+ E(AfficheImage) \
E(AffichePorteF) \
E(AfficheSelection) \
E(CarteDestination) \
E(ChangeCurseur) \
+ E(CopyVar) \
E(Cmp) \
E(Discocier) \
E(DrawTextSelection) \
E(End) \
+ E(ExeDemo) \
E(GetMonde4) \
E(SetMonde4) \
E(IsPresent) \
@@ -787,6 +896,8 @@ struct End : public Script::Command {
E(LoadVariable) \
E(MemoryRelease) \
E(MultiCD_Set_Transition_Script) \
+ E(MultiCD_If_Next_Script) \
+ E(MultiCD_Use_Install_Path) \
E(MultiCD_Set_Next_Script) \
E(PauseTimer) \
E(Play_AnimBloc) \
@@ -804,11 +915,16 @@ struct End : public Script::Command {
E(Set_Global_Pan) \
E(Set_Global_Volume) \
E(Scroll) \
+ E(StartCible) \
+ E(StopAffiche) \
E(Stop_AnimBloc) \
+ E(StopCible) \
+ E(TestCible) \
E(DoAction) \
E(StartTimer) \
E(Sub) \
E(Until) \
+ E(UpdateStage) \
E(While) \
E(Waves) \
/* */
@@ -1100,6 +1216,15 @@ struct Fade : public Script::Command {
}
};
+struct Transfade : public Script::Command {
+ int speed;
+
+ Transfade(int a0) : speed(a0) {}
+ void exec(Script::ExecutionContext &ctx) const override {
+ g_engine->transFade(speed);
+ }
+};
+
} // namespace
} // namespace PhoenixVR
diff --git a/engines/phoenixvr/phoenixvr.cpp b/engines/phoenixvr/phoenixvr.cpp
index cc0623bbf3c..97a3c359279 100644
--- a/engines/phoenixvr/phoenixvr.cpp
+++ b/engines/phoenixvr/phoenixvr.cpp
@@ -80,8 +80,7 @@ static Common::String getAmerzoneLevelLabel(const Common::String &script) {
{"03VR_PUEBLO", "Le Pueblo"},
{"04VR_FLEUVE", "Le Fleuve"},
{"05VR_VILLAGEMARAIS", "Le Village"},
- {"07VRTEMPLE_VOLCAN", "Le Temple"}
- };
+ {"07VRTEMPLE_VOLCAN", "Le Temple"}};
for (const auto &level : levels) {
if (script.hasPrefixIgnoreCase(level.prefix))
@@ -93,12 +92,10 @@ static Common::String getAmerzoneLevelLabel(const Common::String &script) {
static const char *mfull[] = {
"January", "February", "March", "April", "May", "June",
- "July", "August", "September", "October", "November", "December"
-};
+ "July", "August", "September", "October", "November", "December"};
static const char *wday[] = {
- "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"
-};
+ "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"};
static Common::String makeSaveText(const Common::String &firstLine, const Common::String &secondLine) {
Common::String result = firstLine;
@@ -154,8 +151,8 @@ static void fillSaveSlotRect(Graphics::Surface &dst, const Common::Rect &rect, u
}
static int drawSaveTextBlock(Graphics::Surface &dst, const Graphics::Font *font, const Common::String &text,
- int x, int y, int width, uint32 color, Graphics::TextAlign align, int lineHeight, bool splitV, int tileY,
- bool reserveEmptyFinalLine = false) {
+ int x, int y, int width, uint32 color, Graphics::TextAlign align, int lineHeight, bool splitV, int tileY,
+ bool reserveEmptyFinalLine = false) {
bool hasText = false;
for (uint i = 0; i < text.size(); ++i) {
if (text[i] != '\n' && text[i] != '\0') {
@@ -249,8 +246,7 @@ static void projectSaveCard(Graphics::ManagedSurface &faceSurface, const Graphic
makeVertex(0.0f, 0.0f, srcW, srcH),
makeVertex(static_cast<float>(card.w), 0.0f, 0.0f, srcH),
makeVertex(static_cast<float>(card.w), static_cast<float>(card.h), 0.0f, 0.0f),
- makeVertex(0.0f, static_cast<float>(card.h), srcW, 0.0f)
- };
+ makeVertex(0.0f, static_cast<float>(card.h), srcW, 0.0f)};
auto rasterizeTriangle = [&](const Vertex &a, const Vertex &b, const Vertex &c) {
const float area = (b.x - a.x) * (c.y - a.y) - (b.y - a.y) * (c.x - a.x);
@@ -319,6 +315,9 @@ void PhoenixVREngine::resetState() {
_angleX.set(0);
_angleY.resetRange();
_angleY.set(-kPi2);
+ _imageOverlay.reset();
+ _cibleActive = false;
+ _cibleBounds.clear();
}
PhoenixVREngine::~PhoenixVREngine() {
@@ -575,6 +574,62 @@ void PhoenixVREngine::fade(int start, int stop, int speed) {
}
}
+static uint32 transFadePixel(const Graphics::PixelFormat &format, uint32 left, int leftAmount, uint32 right, int rightAmount) {
+ uint8 lr, lg, lb, rr, rg, rb;
+ format.colorToRGB(left, lr, lg, lb);
+ format.colorToRGB(right, rr, rg, rb);
+ return format.RGBToColor(
+ CLIP(CLIP(static_cast<int>(lr) + leftAmount, 0, 255) + CLIP(static_cast<int>(rr) + rightAmount, 0, 255), 0, 255),
+ CLIP(CLIP(static_cast<int>(lg) + leftAmount, 0, 255) + CLIP(static_cast<int>(rg) + rightAmount, 0, 255), 0, 255),
+ CLIP(CLIP(static_cast<int>(lb) + leftAmount, 0, 255) + CLIP(static_cast<int>(rb) + rightAmount, 0, 255), 0, 255));
+}
+
+void PhoenixVREngine::transFade(int speed) {
+ debug("transfade speed: %d", speed);
+
+ Graphics::ManagedSurface oldFrame(_screen->w, _screen->h, _screen->format);
+ Graphics::ManagedSurface newFrame(_screen->w, _screen->h, _screen->format);
+ Graphics::ManagedSurface workFrame(_screen->w, _screen->h, _screen->format);
+
+ oldFrame.simpleBlitFrom(*_screen);
+ renderVR(0);
+ newFrame.simpleBlitFrom(*_screen);
+
+ bool waiting = true;
+ float dt = 0;
+
+ auto renderTransition = [&](int oldAmount, int newAmount) {
+ for (int y = 0; y < _screen->h; ++y) {
+ for (int x = 0; x < _screen->w; ++x) {
+ workFrame.setPixel(x, y, transFadePixel(_screen->format, oldFrame.getPixel(x, y), oldAmount, newFrame.getPixel(x, y), newAmount));
+ }
+ }
+ _screen->simpleBlitFrom(workFrame);
+ };
+
+ auto runTransition = [&](int pos, int direction) {
+ while (!shouldQuit() && waiting && (direction > 0 ? pos < 0 : pos > -256)) {
+ Common::Event event;
+ while (g_system->getEventManager()->pollEvent(event)) {
+ if (event.type == Common::EVENT_KEYDOWN && event.kbd.ascii == ' ')
+ waiting = false;
+ }
+
+ renderTransition(direction > 0 ? 0 : pos, direction > 0 ? pos : 0);
+ _frameLimiter.delayBeforeSwap();
+ _screen->update();
+ dt = _frameLimiter.startFrame() / 1000.0f;
+
+ pos += direction * static_cast<int>(dt * speed * 1000.0f / 16);
+ if (direction > 0 ? pos < 0 : pos > -256)
+ pos += direction;
+ }
+ };
+
+ runTransition(-255, 1);
+ runTransition(0, -1);
+}
+
void PhoenixVREngine::until(const Common::String &var, int value) {
debug("until %s %d", var.c_str(), value);
unsigned frameDuration = 0;
@@ -923,6 +978,57 @@ void PhoenixVREngine::resetLockKey() {
_prevWarp = -1; // original game does only this o_O
}
+void PhoenixVREngine::showImageOverlay(const Common::String &image, int x, int y) {
+ debug("AfficheImage %s %d %d", image.c_str(), x, y);
+ _imageOverlay.reset(loadSurface(image));
+ _imageOverlayPos = Common::Point(x, y);
+}
+
+void PhoenixVREngine::stopImageOverlay() {
+ debug("StopAffiche");
+ _imageOverlay.reset();
+ updateStage();
+}
+
+void PhoenixVREngine::updateStage() {
+ renderVR(0);
+ _screen->update();
+}
+
+void PhoenixVREngine::startCible(const Common::String &name, int periodSeconds, const Common::Array<int> &bounds) {
+ debug("StartCible %s %d", name.c_str(), periodSeconds);
+ _cibleActive = true;
+ _cibleStartMillis = g_system->getMillis();
+ _ciblePeriodSeconds = periodSeconds;
+ _cibleBounds = bounds;
+}
+
+void PhoenixVREngine::stopCible() {
+ debug("StopCible");
+ _cibleActive = false;
+}
+
+void PhoenixVREngine::testCible(const Common::String &insideVar, const Common::String &outsideVar) {
+ debug("TestCible %s %s", insideVar.c_str(), outsideVar.c_str());
+ if (!_cibleActive)
+ return;
+
+ bool inside = false;
+ int periodMillis = _ciblePeriodSeconds * 1000;
+ if (periodMillis > 0) {
+ int elapsed = (g_system->getMillis() - _cibleStartMillis) % periodMillis;
+ for (uint i = 0; i + 1 < _cibleBounds.size() && _cibleBounds[i] != 0; i += 2) {
+ if (_cibleBounds[i] * 1000 < elapsed && elapsed < _cibleBounds[i + 1] * 1000) {
+ inside = true;
+ break;
+ }
+ }
+ }
+
+ setVariable(insideVar, inside ? 1 : 0);
+ setVariable(outsideVar, inside ? 0 : 1);
+}
+
void PhoenixVREngine::lockKey(int idx, const Common::String &warp) {
_lockKey[idx] = warp;
}
@@ -1056,9 +1162,15 @@ void PhoenixVREngine::renderVR(float dt) {
int16 y = _textRect.top + (_textRect.height() - _text->h) / 2;
_screen->blitFrom(*_text, {x, y});
}
+ renderImageOverlay();
renderTimer();
}
+void PhoenixVREngine::renderImageOverlay() {
+ if (_imageOverlay)
+ paint(*_imageOverlay, _imageOverlayPos);
+}
+
void PhoenixVREngine::saveVariables() {
debug("SaveVariable() - saving variable state");
_variableSnapshot.resize(_variableOrder.size());
diff --git a/engines/phoenixvr/phoenixvr.h b/engines/phoenixvr/phoenixvr.h
index 8261d67ac50..75f43b6e296 100644
--- a/engines/phoenixvr/phoenixvr.h
+++ b/engines/phoenixvr/phoenixvr.h
@@ -154,6 +154,7 @@ public:
}
void interpolateAngle(float x, float y, float speed, float zoom);
void fade(int start, int stop, int speed);
+ void transFade(int speed);
void setXMax(float max) {
_angleY.setRange(-max, max);
@@ -203,6 +204,12 @@ public:
bool setNextLevel();
void setGlobalVolume(int vol);
+ void showImageOverlay(const Common::String &image, int x, int y);
+ void stopImageOverlay();
+ void updateStage();
+ void startCible(const Common::String &name, int periodSeconds, const Common::Array<int> &bounds);
+ void stopCible();
+ void testCible(const Common::String &insideVar, const Common::String &outsideVar);
private:
static Common::String removeDrive(const Common::String &path);
@@ -219,6 +226,7 @@ private:
void tickTimer(float dt);
void loadNextScript();
void renderVR(float dt);
+ void renderImageOverlay();
void renderTimer();
void renderFade(int color);
void resetState();
@@ -293,6 +301,12 @@ private:
Common::ScopedPtr<Graphics::ManagedSurface> _text;
Common::Rect _textRect;
+ Common::ScopedPtr<Graphics::Surface> _imageOverlay;
+ Common::Point _imageOverlayPos;
+ bool _cibleActive = false;
+ uint32 _cibleStartMillis = 0;
+ int _ciblePeriodSeconds = 0;
+ Common::Array<int> _cibleBounds;
Common::Array<Common::String> _levels;
uint _currentLevel = 0;
diff --git a/engines/phoenixvr/script.cpp b/engines/phoenixvr/script.cpp
index c94900a60b1..409abc6d684 100644
--- a/engines/phoenixvr/script.cpp
+++ b/engines/phoenixvr/script.cpp
@@ -165,6 +165,8 @@ public:
expect(',');
auto arg2 = nextInt();
return CommandPtr(new Fade(arg0, arg1, arg2));
+ } else if (keyword("transfade")) {
+ return CommandPtr(new Transfade(nextInt()));
} else if (maybe("setzoom=")) {
return CommandPtr(new SetZoom(toRadian(nextInt())));
} else if (maybe("setangle=") || keyword("setangle")) {
Commit: 57053995e0dab2625da144ae62a8cc09687f58fe
https://github.com/scummvm/scummvm/commit/57053995e0dab2625da144ae62a8cc09687f58fe
Author: Scorp (scorp at mrs.mn)
Date: 2026-06-15T23:26:54+01:00
Commit Message:
PHOENIXVR: Add Dracula game detection
Changed paths:
engines/phoenixvr/detection_tables.h
diff --git a/engines/phoenixvr/detection_tables.h b/engines/phoenixvr/detection_tables.h
index b733ed3b108..bcfc6d010ba 100644
--- a/engines/phoenixvr/detection_tables.h
+++ b/engines/phoenixvr/detection_tables.h
@@ -28,6 +28,8 @@ const PlainGameDescriptor phoenixvrGames[] = {
{"necrono", "Necronomicon: The Dawning of Darkness"},
{"lochness", "The Cameron Files: The Secret at Loch Ness"},
{"messenger", "The Messenger/Louvre: The Final Curse"},
+ {"dracula1", "Dracula: Resurrection"},
+ {"dracula2", "Dracula 2: The Last Sanctuary"},
{"amerzone", "Amerzone: The Explorer's Legacy"},
{0, 0}
};
@@ -205,6 +207,26 @@ const ADGameDescription gameDescriptions[] = {
GUIO1(GUIO_NONE)
},
+ {"dracula1",
+ nullptr,
+ AD_ENTRY2s("script.lst", "78060b78cf403ddb7e22903ba7b269d6", 548,
+ "Compiler.dat", "0f330d5d674bf01a1bc8512e4c5321cc", 58688),
+ Common::EN_ANY,
+ Common::kPlatformWindows,
+ ADGF_DROPPLATFORM,
+ GUIO1(GUIO_NONE)
+ },
+
+ {"dracula2",
+ nullptr,
+ AD_ENTRY2s("script.pak", "ff52d2000eddc5c438f3c8ef50fea858", 277,
+ "Compiler.dat", "ff30c7ea065af1e86182b428e1f2cc7b", 42650),
+ Common::EN_ANY,
+ Common::kPlatformWindows,
+ ADGF_DROPPLATFORM,
+ GUIO1(GUIO_NONE)
+ },
+
// GOG release
{"amerzone",
nullptr,
Commit: 78e2d343ab88309be283d0fb27450e48fce062f3
https://github.com/scummvm/scummvm/commit/78e2d343ab88309be283d0fb27450e48fce062f3
Author: Scorp (scorp at mrs.mn)
Date: 2026-06-15T23:26:54+01:00
Commit Message:
PHOENIXVR: Add Dracula script compatibility fixes
Changed paths:
engines/phoenixvr/phoenixvr.cpp
diff --git a/engines/phoenixvr/phoenixvr.cpp b/engines/phoenixvr/phoenixvr.cpp
index 97a3c359279..53375942cc7 100644
--- a/engines/phoenixvr/phoenixvr.cpp
+++ b/engines/phoenixvr/phoenixvr.cpp
@@ -368,6 +368,8 @@ Common::SeekableReadStream *PhoenixVREngine::tryOpen(const Common::Path &name, C
if (s->open(name)) {
auto nameStr = name.toString();
debug("opened %s", nameStr.c_str());
+ if (nameStr.hasSuffixIgnoreCase(".pak"))
+ return unpack(*s, origName);
return s.release();
}
auto pakName = name.toString();
@@ -440,6 +442,10 @@ void PhoenixVREngine::loadNextScript() {
declareVariable(var);
if (gameIdMatches("amerzone"))
declareVariable("oeuf_pose"); // crash in chapter 7
+ if (gameIdMatches("dracula1")) {
+ declareVariable("P_Alliance"); // Referenced by 0M1Script.lst, declared by 0M2Script.lst
+ declareVariable("reloaddone"); // Referenced by InsertCD.lst, declared by chapter scripts
+ }
int numWarps = _script->numWarps();
_cursors.clear();
@@ -1821,6 +1827,9 @@ bool PhoenixVREngine::enterScript() {
Common::Error PhoenixVREngine::loadGameStream(Common::SeekableReadStream *slot) {
auto state = GameState::load(*slot);
+ while (!state.script.empty() &&
+ (state.script.lastChar() == '\n' || state.script.lastChar() == '\r'))
+ state.script = state.script.substr(0, state.script.size() - 1);
_loaded = true;
killTimer();
Commit: db4a85bee61d9bae8882ee6f4fb9fa6c9872ecb7
https://github.com/scummvm/scummvm/commit/db4a85bee61d9bae8882ee6f4fb9fa6c9872ecb7
Author: Scorp (scorp at mrs.mn)
Date: 2026-06-15T23:26:54+01:00
Commit Message:
PHOENIXVR: change the way engine handles undefined variables
Game breaking issue in Dracula 2, ifand/ifor ignores undefined variables. Add default for get and skip sets.
Changed paths:
engines/phoenixvr/commands.h
engines/phoenixvr/phoenixvr.cpp
engines/phoenixvr/phoenixvr.h
diff --git a/engines/phoenixvr/commands.h b/engines/phoenixvr/commands.h
index 65ff23ed81a..ea21d3b67a7 100644
--- a/engines/phoenixvr/commands.h
+++ b/engines/phoenixvr/commands.h
@@ -943,7 +943,7 @@ struct IfAnd : public Script::Conditional {
void exec(Script::ExecutionContext &ctx) const override {
bool result = true;
for (auto &var : vars) {
- if (var.empty())
+ if (var.empty() || !g_engine->hasVariable(var))
continue;
auto value = g_engine->getVariable(var);
debug("ifand, %s: %d", var.c_str(), value);
@@ -964,7 +964,7 @@ struct IfOr : public Script::Conditional {
void exec(Script::ExecutionContext &ctx) const override {
bool result = false;
for (auto &var : vars) {
- if (var.empty())
+ if (var.empty() || !g_engine->hasVariable(var))
continue;
auto value = g_engine->getVariable(var);
debug("ifor, %s: %d", var.c_str(), value);
diff --git a/engines/phoenixvr/phoenixvr.cpp b/engines/phoenixvr/phoenixvr.cpp
index 53375942cc7..a0e961055b2 100644
--- a/engines/phoenixvr/phoenixvr.cpp
+++ b/engines/phoenixvr/phoenixvr.cpp
@@ -781,15 +781,23 @@ void PhoenixVREngine::declareVariable(const Common::String &name) {
_variables.setVal(name, 0);
}
+bool PhoenixVREngine::hasVariable(const Common::String &name) const {
+ return _variables.contains(name);
+}
+
void PhoenixVREngine::setVariable(const Common::String &name, int value) {
+ if (!hasVariable(name)) {
+ debug("set %s %d - ignored, variable was not declared", name.c_str(), value);
+ return;
+ }
debug("set %s %d", name.c_str(), value);
_variables.setVal(name, value);
}
int PhoenixVREngine::getVariable(const Common::String &name) const {
- if (gameIdMatches("lochness") && name == "tumuAccpet")
- return _variables.getVal("tumuAccept");
- return _variables.getVal(name);
+ if (!hasVariable(name))
+ warning("get %s - variable was not declared", name.c_str());
+ return _variables.getValOrDefault(name, 0);
}
void PhoenixVREngine::playSound(const Common::String &sound, Audio::Mixer::SoundType type, uint8 volume, int loops, bool spatial, float angle) {
diff --git a/engines/phoenixvr/phoenixvr.h b/engines/phoenixvr/phoenixvr.h
index 75f43b6e296..06ca9559902 100644
--- a/engines/phoenixvr/phoenixvr.h
+++ b/engines/phoenixvr/phoenixvr.h
@@ -131,6 +131,7 @@ public:
void playMovie(const Common::String &movie);
void declareVariable(const Common::String &name);
+ bool hasVariable(const Common::String &name) const;
void setVariable(const Common::String &name, int value);
int getVariable(const Common::String &name) const;
Commit: 31742a6d3e31b8494042c007ee7de7df34882359
https://github.com/scummvm/scummvm/commit/31742a6d3e31b8494042c007ee7de7df34882359
Author: Scorp (scorp at mrs.mn)
Date: 2026-06-15T23:26:54+01:00
Commit Message:
PHOENIXVR: Fix Dracula 2 timer display
Changed paths:
engines/phoenixvr/commands.h
engines/phoenixvr/phoenixvr.cpp
engines/phoenixvr/phoenixvr.h
diff --git a/engines/phoenixvr/commands.h b/engines/phoenixvr/commands.h
index ea21d3b67a7..4373beefa4e 100644
--- a/engines/phoenixvr/commands.h
+++ b/engines/phoenixvr/commands.h
@@ -151,11 +151,12 @@ struct While : public Script::Command {
struct StartTimer : public Script::Command {
float seconds;
+ bool showTimer;
- StartTimer(const Common::Array<Common::String> &args) : seconds(atof(args[0].c_str())) {}
+ StartTimer(const Common::Array<Common::String> &args) : seconds(atof(args[0].c_str())), showTimer(args.size() < 2 || atoi(args[1].c_str()) != 0) {}
void exec(Script::ExecutionContext &ctx) const override {
- debug("starttimer %g", seconds);
- g_engine->startTimer(seconds);
+ debug("starttimer %g %d", seconds, showTimer);
+ g_engine->startTimer(seconds, showTimer);
}
};
diff --git a/engines/phoenixvr/phoenixvr.cpp b/engines/phoenixvr/phoenixvr.cpp
index a0e961055b2..d0272ac6b9c 100644
--- a/engines/phoenixvr/phoenixvr.cpp
+++ b/engines/phoenixvr/phoenixvr.cpp
@@ -1104,10 +1104,11 @@ void PhoenixVREngine::executeTest(int idx) {
warning("invalid test id %d", idx);
}
-void PhoenixVREngine::startTimer(float seconds) {
+void PhoenixVREngine::startTimer(float seconds, bool showTimer) {
_timer = seconds;
_initialTimer = seconds;
_timerFlags = 5;
+ _showTimer = showTimer;
}
void PhoenixVREngine::pauseTimer(bool pause, bool deactivate) {
@@ -1125,6 +1126,7 @@ void PhoenixVREngine::pauseTimer(bool pause, bool deactivate) {
void PhoenixVREngine::killTimer() {
_timerFlags = 0;
+ _showTimer = false;
}
void PhoenixVREngine::tickTimer(float dt) {
@@ -1148,21 +1150,25 @@ void PhoenixVREngine::tickTimer(float dt) {
}
void PhoenixVREngine::renderTimer() {
- if (_timerFlags == 0 || !_arn)
+ if (_timerFlags == 0 || !_showTimer || !_arn)
return;
auto timerBg = _arn->get("cadre.bmp");
auto timerFg = _arn->get("cadreB.bmp");
if (!timerBg || !timerFg)
return;
- // Loch-Ness rectangle for now.
// Necronomicon has timer in scripts, but does not contain bitmaps for timers.
Common::Rect bgRect{320, 16, 632, 44};
Common::Rect fgRect{333, 23, 619, 38};
+ if (gameIdMatches("dracula2")) {
+ bgRect = Common::Rect(165, 15, 474, 48);
+ fgRect = Common::Rect(177, 15, 461, 48);
+ }
assert(_initialTimer > 0);
auto timeLeft = _timer / _initialTimer;
fgRect.right = fgRect.left + fgRect.width() * timeLeft;
- Common::Rect fgSrcRect{static_cast<short>(timerFg->w * timeLeft), timerFg->h};
+ Common::Rect fgSrcRect(0, 0, timerFg->w, timerFg->h);
+ fgSrcRect.right = fgSrcRect.left + fgSrcRect.width() * timeLeft;
if (!fgRect.isValidRect() || !fgSrcRect.isValidRect())
return;
_screen->blitFrom(*timerBg, bgRect.origin());
diff --git a/engines/phoenixvr/phoenixvr.h b/engines/phoenixvr/phoenixvr.h
index 06ca9559902..d9e82daef41 100644
--- a/engines/phoenixvr/phoenixvr.h
+++ b/engines/phoenixvr/phoenixvr.h
@@ -145,7 +145,7 @@ public:
void resetLockKey();
void lockKey(int idx, const Common::String &warp);
- void startTimer(float seconds);
+ void startTimer(float seconds, bool showTimer);
void pauseTimer(bool pause, bool deactivate);
void killTimer();
void playAnimation(const Common::String &name, const Common::String &var, int varValue, float speed);
@@ -287,6 +287,7 @@ private:
static constexpr byte kPaused = 2;
static constexpr byte kActive = 4;
byte _timerFlags = 0;
+ bool _showTimer = false;
float _timer = 0, _initialTimer = 0;
Common::String _contextScript;
Commit: e6f3921aaae37e379dc7fd7484d41879daf5cc1f
https://github.com/scummvm/scummvm/commit/e6f3921aaae37e379dc7fd7484d41879daf5cc1f
Author: Scorp (scorp at mrs.mn)
Date: 2026-06-15T23:26:54+01:00
Commit Message:
PHOENIXVR: Improve Dracula runtime script handling
Adds support for whitespace angle-limit commands, direct region-click dispatch using the last duplicate test record, and ARN-backed image overlays used by Dracula scripts.
Missing cursors now warn instead of aborting, timed waits no longer skip on keypress, animation stops signal their completion variable, and save slot text keeps the intended line spacing.
Changed paths:
engines/phoenixvr/phoenixvr.cpp
engines/phoenixvr/script.cpp
engines/phoenixvr/script.h
engines/phoenixvr/vr.cpp
diff --git a/engines/phoenixvr/phoenixvr.cpp b/engines/phoenixvr/phoenixvr.cpp
index d0272ac6b9c..308f7795349 100644
--- a/engines/phoenixvr/phoenixvr.cpp
+++ b/engines/phoenixvr/phoenixvr.cpp
@@ -651,8 +651,8 @@ void PhoenixVREngine::until(const Common::String &var, int value) {
// Delay for a bit. All events loops should have a delay
// to prevent the system being unduly loaded
- _frameLimiter.delayBeforeSwap();
drawAudioSubtitles();
+ _frameLimiter.delayBeforeSwap();
_screen->update();
frameDuration = _frameLimiter.startFrame();
}
@@ -669,13 +669,6 @@ void PhoenixVREngine::wait(float seconds) {
renderVR(frameDuration / 1000.0f);
while (g_system->getEventManager()->pollEvent(event)) {
switch (event.type) {
- case Common::EVENT_KEYDOWN: {
- if (event.kbd.ascii == ' ') {
- waiting = false;
- }
- break;
- }
-
default:
break;
}
@@ -683,8 +676,8 @@ void PhoenixVREngine::wait(float seconds) {
// Delay for a bit. All events loops should have a delay
// to prevent the system being unduly loaded
- _frameLimiter.delayBeforeSwap();
drawAudioSubtitles();
+ _frameLimiter.delayBeforeSwap();
_screen->update();
frameDuration = _frameLimiter.startFrame();
}
@@ -994,7 +987,21 @@ void PhoenixVREngine::resetLockKey() {
void PhoenixVREngine::showImageOverlay(const Common::String &image, int x, int y) {
debug("AfficheImage %s %d %d", image.c_str(), x, y);
- _imageOverlay.reset(loadSurface(image));
+ _imageOverlay.reset();
+
+ const Graphics::Surface *surface = _arn ? _arn->get(image) : nullptr;
+ if (!surface && !image.contains('.'))
+ surface = _arn ? _arn->get(image + ".bmp") : nullptr;
+ if (!surface) {
+ warning("can't find image overlay %s", image.c_str());
+ return;
+ }
+
+ uint8 r, g, b;
+ surface->format.colorToRGB(surface->getPixel(surface->w - 1, surface->h - 1), r, g, b);
+ _imageOverlay.reset(surface->convertTo(Graphics::BlendBlit::getSupportedPixelFormat()));
+ if (_imageOverlay)
+ _imageOverlay->applyColorKey(r, g, b);
_imageOverlayPos = Common::Point(x, y);
}
@@ -1083,8 +1090,10 @@ Graphics::Surface *PhoenixVREngine::loadCursor(const Common::String &path) {
if (it != _cursorCache.end())
return it->_value;
auto s = loadSurface(path);
- if (!s)
- error("can't load cursor from %s", path.c_str());
+ if (!s) {
+ warning("can't load cursor from %s", path.c_str());
+ return nullptr;
+ }
_cursorCache[path] = s;
return s;
}
@@ -1626,7 +1635,11 @@ Common::Error PhoenixVREngine::run() {
if (_vr.isVR() ? region->contains3D(vrPos) : region->contains2D(event.mouse.x, event.mouse.y)) {
debug("click region %u", i);
- executeTest(i);
+ if (auto clickTest = _warp->getLastTest(i)) {
+ Script::ExecutionContext ctx;
+ clickTest->scope.exec(ctx);
+ } else
+ warning("invalid test id %u", i);
break;
}
}
@@ -1648,8 +1661,8 @@ Common::Error PhoenixVREngine::run() {
// Delay for a bit. All events loops should have a delay
// to prevent the system being unduly loaded
- _frameLimiter.delayBeforeSwap();
drawAudioSubtitles();
+ _frameLimiter.delayBeforeSwap();
_screen->update();
frameDuration = _frameLimiter.startFrame();
}
@@ -2035,8 +2048,8 @@ void PhoenixVREngine::drawSlot(int idx, int face, int x, int y) {
textY = drawSaveTextBlock(dst, font, state.game, textX, textY, textW, color, textAlign, lineHeight, splitV, tileY);
drawSaveTextBlock(dst, font, state.info, textX, textY, textW, color, textAlign, lineHeight, splitV, tileY);
} else {
- textY = drawSaveTextBlock(dst, font, state.game, textX, textY, textW, color, textAlign, lineHeight, splitV, tileY, true);
- drawSaveTextBlock(dst, font, state.info, textX, textY, textW, color, textAlign, lineHeight, splitV, tileY);
+ drawSaveTextBlock(dst, font, state.game, textX, textY, textW, color, textAlign, lineHeight, splitV, tileY, true);
+ drawSaveTextBlock(dst, font, state.info, textX, textY + lineHeight, textW, color, textAlign, lineHeight, splitV, tileY);
}
}
diff --git a/engines/phoenixvr/script.cpp b/engines/phoenixvr/script.cpp
index 409abc6d684..cd6a432e820 100644
--- a/engines/phoenixvr/script.cpp
+++ b/engines/phoenixvr/script.cpp
@@ -192,9 +192,9 @@ public:
// or
// x, y, zoom, speed
return CommandPtr(a3 != 0 ? new InterpolAngle(a0, a1, a3, toRadian(a2)) : new InterpolAngle(a0, a1, a2, 0));
- } else if (maybe("anglexmax=")) {
+ } else if (maybe("anglexmax=") || keyword("anglexmax")) {
return CommandPtr(new AngleXMax(toAngle(nextInt())));
- } else if (maybe("angleymax=")) {
+ } else if (maybe("angleymax=") || keyword("angleymax")) {
auto y0 = toAngle(nextInt());
expect(',');
auto y1 = toAngle(nextInt());
@@ -302,6 +302,15 @@ Script::TestPtr Script::Warp::getTest(int idx) const {
return it != tests.end() ? *it : Script::TestPtr{};
}
+Script::TestPtr Script::Warp::getLastTest(int idx) const {
+ for (uint i = tests.size(); i > 0; --i) {
+ if (tests[i - 1]->idx == idx)
+ return tests[i - 1];
+ }
+
+ return Script::TestPtr{};
+}
+
Script::Script(Common::SeekableReadStream &s) {
uint lineno = 1;
while (!s.eos()) {
diff --git a/engines/phoenixvr/script.h b/engines/phoenixvr/script.h
index 8764adb61c8..dc7e93ef0dd 100644
--- a/engines/phoenixvr/script.h
+++ b/engines/phoenixvr/script.h
@@ -105,6 +105,7 @@ public:
void parseLine(const Common::String &line, uint lineno);
TestPtr getTest(int idx) const;
+ TestPtr getLastTest(int idx) const;
TestPtr getDefaultTest() const {
return getTest(-1);
}
diff --git a/engines/phoenixvr/vr.cpp b/engines/phoenixvr/vr.cpp
index efc1c623ca7..42c4ad7162a 100644
--- a/engines/phoenixvr/vr.cpp
+++ b/engines/phoenixvr/vr.cpp
@@ -392,6 +392,7 @@ void VR::stopAnimation(const Common::String &name) {
}
auto &animation = *it;
animation.active = false;
+ g_engine->setVariable(animation.variable, animation.variableValue);
}
void VR::Animation::renderNextFrame(Graphics::Surface &pic) {
Commit: d5c8d79a1104309a75c8ea84429b4bb960d45176
https://github.com/scummvm/scummvm/commit/d5c8d79a1104309a75c8ea84429b4bb960d45176
Author: Scorp (scorp at mrs.mn)
Date: 2026-06-15T23:26:54+01:00
Commit Message:
PHOENIXVR: Show VR debug regions as rainbow borders
Replaces the previous wave displacement debug overlay with a rainbow outline for VR hit regions so interiors remain visible while inspecting hotspots.
Changed paths:
engines/phoenixvr/vr.cpp
diff --git a/engines/phoenixvr/vr.cpp b/engines/phoenixvr/vr.cpp
index 42c4ad7162a..299e287bbd0 100644
--- a/engines/phoenixvr/vr.cpp
+++ b/engines/phoenixvr/vr.cpp
@@ -72,6 +72,31 @@ uint32 YCbCr2RGB(const Graphics::PixelFormat &format, int16 y, int16 cb, int16 c
return format.RGBToColor(r, g, b);
}
+uint32 debugRegionColor(const Graphics::PixelFormat &format, float phase) {
+ phase = fmodf(phase, 6.0f);
+ if (phase < 0.0f)
+ phase += 6.0f;
+
+ const int sector = static_cast<int>(phase);
+ const byte up = static_cast<byte>((phase - sector) * 255.0f);
+ const byte down = 255 - up;
+
+ switch (sector) {
+ case 0:
+ return format.RGBToColor(255, up, 0);
+ case 1:
+ return format.RGBToColor(down, 255, 0);
+ case 2:
+ return format.RGBToColor(0, 255, up);
+ case 3:
+ return format.RGBToColor(0, down, 255);
+ case 4:
+ return format.RGBToColor(up, 0, 255);
+ default:
+ return format.RGBToColor(255, 0, down);
+ }
+}
+
struct Quantisation {
int quantY[64];
int quantCbCr[64];
@@ -480,7 +505,6 @@ void VR::renderVR(Graphics::Screen *screen, float ax, float ay, float fov, float
dst += y * dstPixelsPitchIncrement;
}
Vector3d ray = line;
- int dx = regSet ? static_cast<int>(5 * cosf(hint + 100.0f * dstY / h)) : 0;
for (int dstX = 0; dstX != w; ++dstX, ray += incrementX, ++dst) {
auto cube = toCube(ray.x(), ray.y(), ray.z());
@@ -493,20 +517,21 @@ void VR::renderVR(Graphics::Screen *screen, float ax, float ay, float fov, float
srcY += (tileId << 8);
auto color = _pic->getPixel(srcX, srcY);
if (regSet) {
- int x = 0;
regX += regDX;
if (regX >= kTau)
regX -= kTau;
+ const float regionY = kTau - regY;
for (auto ® : regSet->getRegions()) {
- if (reg.contains3D(regX, kTau - regY)) {
- x += dx;
+ if (reg.contains3D(regX, regionY) &&
+ (!reg.contains3D(regX - regDX, regionY) ||
+ !reg.contains3D(regX + regDX, regionY) ||
+ !reg.contains3D(regX, regionY - regDY) ||
+ !reg.contains3D(regX, regionY + regDY))) {
+ color = debugRegionColor(screen->format, hint + dstX * 0.015f + dstY * 0.025f);
+ break;
}
}
- if (dstX + x < 0)
- x = 0;
- else if (dstX + x >= screen->w)
- x = screen->w - 1 - dstX;
- dst[x] = color;
+ *dst = color;
} else
*dst = color;
}
More information about the Scummvm-git-logs
mailing list