Files
scummvm-cursorfix/engines/dgds/ads.cpp
2026-02-02 04:50:13 +01:00

1149 lines
35 KiB
C++

/* ScummVM - Graphic Adventure Engine
*
* ScummVM is the legal property of its developers, whose names
* are too numerous to list here. Please refer to the COPYRIGHT
* file distributed with this source distribution.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
#include "dgds/ads.h"
#include "dgds/globals.h"
namespace Dgds {
Common::Error ADSData::syncState(Common::Serializer &s) {
uint16 arrSize = ARRAYSIZE(_state);
s.syncAsUint16LE(arrSize);
if (arrSize != ARRAYSIZE(_state))
error("Expected fixed size state array");
for (uint i = 0; i < arrSize; i++)
s.syncAsSint16LE(_state[i]);
uint16 nenvs = _scriptEnvs.size();
s.syncAsUint16LE(nenvs);
// This should be the same on load as the data comes from the ADS/TTM files.
if (nenvs != _scriptEnvs.size())
error("Unexpected number of script envs (%d in save vs %d in ADS)", nenvs, _scriptEnvs.size());
for (auto &env : _scriptEnvs)
env.syncState(s);
uint16 nseqs = _ttmSeqs.size();
s.syncAsUint16LE(nseqs);
if (nseqs != _ttmSeqs.size())
error("Unexpected number of ttm seqeunces (%d in save vs %d in ADS)", nseqs, _ttmSeqs.size());
for (auto &seq : _ttmSeqs)
seq->syncState(s);
return Common::kNoError;
}
ADSInterpreter::ADSInterpreter(DgdsEngine *vm) : _vm(vm), _currentTTMSeq(nullptr), _adsData(nullptr) {
_ttmInterpreter = new TTMInterpreter(_vm);
}
ADSInterpreter::~ADSInterpreter() {
delete _ttmInterpreter;
_ttmInterpreter = nullptr;
for (auto &data : _adsTexts)
delete data._value.scr;
}
bool ADSInterpreter::load(const Common::String &filename) {
// Don't clear current _adsData, we reset that below.
_currentTTMSeq = nullptr;
// For high detail, replace extension ADS with ADH. Low detail is ADL.
Common::String detailfile = filename.substr(0, filename.size() - 1);
if (_vm->getDetailLevel() == kDgdsDetailLow)
detailfile += "L";
else
detailfile += "H";
if (!_vm->getResourceManager()->hasResource(detailfile))
detailfile = filename;
debug(1, "ADSInterpreter: load %s", detailfile.c_str());
// Reset the state
_vm->setFlipMode(false);
_adsTexts.setVal(detailfile, ADSData());
_adsData = &(_adsTexts.getVal(detailfile));
ADSParser dgds(_vm->getResourceManager(), _vm->getDecompressor());
dgds.parse(_adsData, detailfile);
for (const auto &file : _adsData->_scriptNames) {
// Environments are numbered based on this list, so extend the list even if there
// is no file to load. Eg, Willy Beamish TVO.ADS uses env 5 but only loads 4
// files (and has 1 empty name)
_adsData->_scriptEnvs.resize(_adsData->_scriptEnvs.size() + 1);
if (file.empty())
continue;
debug(1, " load TTM %s to env %d", file.c_str(), _adsData->_scriptEnvs.size());
TTMEnviro &data = _adsData->_scriptEnvs.back();
data._enviro = _adsData->_scriptEnvs.size();
_ttmInterpreter->load(file, data);
_ttmInterpreter->findAndAddSequences(data, _adsData->_ttmSeqs);
}
_adsData->scr->seek(0);
uint16 opcode = 0;
int segcount = 0;
findUsedSequencesForSegment(0);
_adsData->_segments[0] = 0;
opcode = _adsData->scr->readUint16LE();
while (_adsData->scr->pos() < _adsData->scr->size()) {
if (opcode == 0xffff) {
segcount++;
_adsData->_segments[segcount] = _adsData->scr->pos();
findUsedSequencesForSegment(segcount);
} else {
_adsData->scr->skip(numArgs(opcode) * 2);
}
opcode = _adsData->scr->readUint16LE();
}
for (uint i = segcount + 1; i < ARRAYSIZE(_adsData->_segments); i++)
_adsData->_segments[i] = -1;
_adsData->_maxSegments = segcount + 1;
_adsData->_filename = filename;
for (uint i = 0; i < ARRAYSIZE(_adsData->_state); i++)
_adsData->_state[i] = 8;
for (auto &seq : _adsData->_ttmSeqs)
seq->reset();
return true;
}
bool ADSInterpreter::updateSeqTimeAndFrame(const TTMEnviro *env, Common::SharedPtr<TTMSeq> seq) {
if (seq->_timeInterval != 0) {
uint32 now = DgdsEngine::getInstance()->getThisFrameMs();
if (now < seq->_timeNext) {
debug(10, "env %d seq %d (%s) not advancing from frame %d (now %d timeNext %d interval %d)", seq->_enviro,
seq->_seqNum, env->_tags.getValOrDefault(seq->_seqNum).c_str(), seq->_currentFrame, now, seq->_timeNext, seq->_timeInterval);
return false;
}
seq->_timeNext = now + seq->_timeInterval;
}
seq->_executed = false;
if (seq->_gotoFrame == -1) {
debug(10, "env %d seq %d (%s) advance to frame %d->%d (start %d last %d)", seq->_enviro, seq->_seqNum,
env->_tags.getValOrDefault(seq->_seqNum).c_str(), seq->_currentFrame, seq->_currentFrame + 1, seq->_startFrame, seq->_lastFrame);
seq->_currentFrame++;
} else {
debug(10, "env %d seq %d (%s) goto to frame %d->%d (start %d last %d)", seq->_enviro, seq->_seqNum,
env->_tags.getValOrDefault(seq->_seqNum).c_str(), seq->_currentFrame, seq->_gotoFrame, seq->_startFrame, seq->_lastFrame);
seq->_currentFrame = seq->_gotoFrame;
seq->_gotoFrame = -1;
}
return true;
}
static const uint16 ADS_USED_SEQ_OPCODES[] = {
0x2000, 0x2005, 0x2010, 0x2015, 0x4000, 0x4010, 0x1330,
0x1340, 0x1360, 0x1370, 0x1320, 0x1310, 0x1350
};
void ADSInterpreter::findUsedSequencesForSegment(int idx) {
_adsData->_usedSeqs[idx].clear();
int64 startoff = _adsData->scr->pos();
uint16 opcode = 0;
// Skip the segment number.
int16 segno = _adsData->scr->readUint16LE();
DgdsGameId gameId = DgdsEngine::getInstance()->getGameId();
//
// HoC and Dragon call a sequence "used" if there is any dependency on
// the sequence (reordering, if conditions), but to simplify the use of
// getStateForSceneOp, later games only call it "used" if it is directly
// started (0x2000 or 0x2005).
//
int n_ops_to_check = (gameId == GID_DRAGON || gameId == GID_HOC) ? ARRAYSIZE(ADS_USED_SEQ_OPCODES) : 2;
while (opcode != 0xffff && _adsData->scr->pos() < _adsData->scr->size()) {
opcode = _adsData->scr->readUint16LE();
for (int i = 0; i < n_ops_to_check; i++) {
uint16 op = ADS_USED_SEQ_OPCODES[i];
if (opcode == op) {
int16 envno = _adsData->scr->readSint16LE();
int16 seqno = _adsData->scr->readSint16LE();
Common::SharedPtr<TTMSeq> seq = findTTMSeq(envno, seqno);
if (!seq) {
warning("ADS opcode %04x at offset %d references unknown seq %d %d",
opcode, (int)_adsData->scr->pos(), envno, seqno);
} else {
bool already_added = false;
for (const Common::SharedPtr<TTMSeq> &s : _adsData->_usedSeqs[idx]) {
if (s == seq) {
already_added = true;
break;
}
}
if (!already_added) {
debug(10, "ADS seg no %d (idx %d) uses env %d seq %d", segno, idx, envno, seqno);
_adsData->_usedSeqs[idx].push_back(seq);
}
}
// Rewind as we will go forward again outside this loop.
_adsData->scr->seek(-4, SEEK_CUR);
break;
}
}
_adsData->scr->skip(numArgs(opcode) * 2);
}
_adsData->scr->seek(startoff);
}
void ADSInterpreter::unload() {
_adsData = nullptr;
_currentTTMSeq = nullptr;
_adsTexts.clear();
}
bool ADSInterpreter::playScene() {
if (!_currentTTMSeq)
return false;
TTMEnviro *env = findTTMEnviro(_currentTTMSeq->_enviro);
if (!env)
error("Couldn't find environment num %d", _currentTTMSeq->_enviro);
_adsData->_gotoTarget = -1;
return _ttmInterpreter->run(*env, *_currentTTMSeq);
}
bool ADSInterpreter::skipSceneLogicBranch() {
Common::SeekableReadStream *scr = _adsData->scr;
bool result = true;
while (scr->pos() < scr->size()) {
uint16 op = scr->readUint16LE();
if (op == 0x1510 || op == 0x1500) { // else or endif
scr->seek(-2, SEEK_CUR);
return true;
} else if (op == 0 || op == 0xffff) {
// end of segment
return false;
} else if ((op & 0xff00) == 0x1300) {
// A nested IF (0x13xx) block. Skip to endif ignoring else.
scr->skip(numArgs(op) * 2);
result = skipToEndIf();
} else {
scr->skip(numArgs(op) * 2);
}
}
return result && scr->pos() < scr->size();
}
bool ADSInterpreter::skipToElseOrEndif() {
Common::SeekableReadStream *scr = _adsData->scr;
bool result = skipSceneLogicBranch();
if (result) {
uint16 op = scr->readUint16LE();
if (op == 0x1500)
// Hit else block - we want to run that
result = runUntilBranchOpOrEnd();
// don't rewind - the calls to this should always return ptr+2
}
return result;
}
bool ADSInterpreter::skipToEndIf() {
//
// This is similar to skipSceneLogicBranch, but it does not stop for ELSE,
// only ENDIF. It's used to skip nested IF blocks inside another skipped
// block.
// It should be called with the pointer at the op after the IF, which can
// also be AND or OR ops.
//
Common::SeekableReadStream *scr = _adsData->scr;
while (scr->pos() < scr->size()) {
uint16 op = scr->readUint16LE();
// don't rewind - the calls to this should always return after the last op.
if (op == 0x1510) { // ENDIF
return true;
} else if (op == 0 || op == 0xffff) {
return false;
} else if (op == 0x1420 || op == 0x1430) {
// AND or OR. Skip the IF that follows like a nested block
// (this should work even for multiple AND/OR conditions)
uint16 op2 = scr->readUint16LE();
if ((op2 & 0xff00) != 0x1300)
error("AND/OR ADS op not followed by another IF (got 0x%04x)", op2);
scr->skip(numArgs(op) * 2);
return skipToEndIf();
} else if ((op & 0xff00) == 0x1300) {
// A nested IF (0x13xx) block. Skip to endif ignoring else.
scr->skip(numArgs(op) * 2);
if (!skipToEndIf())
return false;
} else {
scr->skip(numArgs(op) * 2);
}
}
return false;
}
bool ADSInterpreter::skipToEndWhile() {
Common::SeekableReadStream *scr = _adsData->scr;
while (scr->pos() < scr->size()) {
uint16 op = scr->readUint16LE();
// don't rewind - the calls to this should always return after the last op.
if (op == 0x1520)
return true;
else if (op == 0 || op == 0xffff)
return false;
scr->skip(numArgs(op) * 2);
}
return false;
}
TTMEnviro *ADSInterpreter::findTTMEnviro(int16 enviro) {
for (auto & env : _adsData->_scriptEnvs) {
if (env._enviro == enviro)
return &env;
}
return nullptr;
}
Common::SharedPtr<TTMSeq> ADSInterpreter::findTTMSeq(int16 enviro, int16 seqno) {
for (auto &seq : _adsData->_ttmSeqs) {
if (seq->_enviro == enviro && seq->_seqNum == seqno)
return seq;
}
return nullptr;
}
void ADSInterpreter::segmentOrState(int16 seg, uint16 val) {
int idx = getArrIndexOfSegNum(seg);
if (idx >= 0) {
_adsData->_charWhile[idx] = 0;
_adsData->_state[idx] = (_adsData->_state[idx] & 8) | val;
}
}
void ADSInterpreter::segmentSetState(int16 seg, uint16 val) {
int idx = getArrIndexOfSegNum(seg);
if (idx >= 0) {
_adsData->_charWhile[idx] = 0;
if (_adsData->_state[idx] != 8)
_adsData->_state[idx] = val;
}
}
void ADSInterpreter::findEndOrInitOp() {
Common::SeekableReadStream *scr = _adsData->scr;
int32 startoff = scr->pos();
while (scr->pos() < scr->size()) {
uint16 opcode = scr->readUint16LE();
// on FFFF return the original offset
if (opcode == 0xffff) {
scr->seek(startoff);
return;
}
// on 5 (init) return the next offset (don't rewind)
if (opcode == 0x0005)
return;
// everything else just go forward.
scr->skip(numArgs(opcode) * 2);
}
}
bool ADSInterpreter::logicOpResult(uint16 code, const TTMEnviro *env, const TTMSeq *seq, uint16 arg) {
const char *tag = (seq && env) ? env->_tags.getValOrDefault(seq->_seqNum).c_str() : "";
int16 envNum = env ? env->_enviro : 0;
int16 seqNum = seq ? seq->_seqNum : 0;
const char *optype = (code < 0x1300 ? "while" : "if");
Globals *globals = DgdsEngine::getInstance()->getGameGlobals();
switch (code) {
case 0x1010: // WHILE paused
case 0x1310: // IF paused, 2 params
debugN(10, "ADS 0x%04x: %s paused env %d seq %d (%s)", code, optype, envNum, seqNum, tag);
return seq && seq->_runFlag == kRunTypePaused;
case 0x1020: // WHILE not paused
case 0x1320: // IF not paused, 2 params
debugN(10, "ADS 0x%04x: %s not paused env %d seq %d (%s)", code, optype, envNum, seqNum, tag);
return !seq || seq->_runFlag != kRunTypePaused;
case 0x1030: // WHILE NOT PLAYED
case 0x1330: // IF_NOT_PLAYED, 2 params
debugN(10, "ADS 0x%04x: %s not played env %d seq %d (%s)", code, optype, envNum, seqNum, tag);
return !seq || !seq->_runPlayed;
case 0x1040: // WHILE PLAYED
case 0x1340: // IF_PLAYED, 2 params
debugN(10, "ADS 0x%04x: %s played env %d seq %d (%s)", code, optype, envNum, seqNum, tag);
return seq && seq->_runPlayed;
case 0x1050: // WHILE FINISHED
case 0x1350: // IF_FINISHED, 2 params
debugN(10, "ADS 0x%04x: %s finished env %d seq %d (%s)", code, optype, envNum, seqNum, tag);
return seq && seq->_runFlag == kRunTypeFinished;
case 0x1060: // WHILE NOT RUNNING
case 0x1360: { // IF_NOT_RUNNING, 2 params
debugN(10, "ADS 0x%04x: %s not running env %d seq %d (%s)", code, optype, envNum, seqNum, tag);
// Dragon only checks kRunTypeStopped, HoC onward also check for kRunTypeFinished
bool isDragon = _vm->getGameId() == GID_DRAGON;
return !seq || seq->_runFlag == kRunTypeStopped || (!isDragon && seq->_runFlag == kRunTypeFinished);
}
case 0x1070: // WHILE RUNNING
case 0x1370: // IF_RUNNING, 2 params
debugN(10, "ADS 0x%04x: %s running env %d seq %d (%s)", code, optype, envNum, seqNum, tag);
return seq && (seq->_runFlag == kRunTypeKeepGoing || seq->_runFlag == kRunTypeMulti || seq->_runFlag == kRunTypeTimeLimited);
case 0x1080:
case 0x1090:
warning("Unimplemented IF/WHILE operation 0x%x", code);
return true;
case 0x1380: // IF_DETAIL_LTE, 1 param
debugN(10, "ADS 0x%04x: if detail <= %d", code, arg);
// TODO: This check is right, but we only have detail 0/1 and HOC onward use
// different numbers. HOC intro checks for >= 4, and default in HOC is 4.
return false;
//return ((int)DgdsEngine::getInstance()->getDetailLevel() <= arg);
case 0x1390: // IF_DETAIL_GTE, 1 param
debugN(10, "ADS 0x%04x: if detail >= %d", code, arg);
return true;
//return ((int)DgdsEngine::getInstance()->getDetailLevel() >= arg);
//
// NOTE: The globals for the following ops use the numbers from Willy
// Beamish (0x4F, 0x50). If these ops are used in any of the other newer
// games (Quarky or Johnny Castaway) they may need updating.
//
case 0x13A0: // IF some_ads_variable[0] <=
debugN(10, "ADS 0x%04x: if adsVariable[0] <= %d", code, arg);
return globals->getGlobal(0x50) <= arg;
case 0x13A1: // IF some_ads_variable[1] <=
debugN(10, "ADS 0x%04x: if adsVariable[1] <= %d", code, arg);
return globals->getGlobal(0x4F) <= arg;
case 0x13B0: // IF some_ads_variable[0] >
debugN(10, "ADS 0x%04x: if adsVariable[0] > %d", code, arg);
return globals->getGlobal(0x50) > arg;
case 0x13B1: // IF some_ads_variable[1] >
debugN(10, "ADS 0x%04x: if adsVariable[1] > %d", code, arg);
return globals->getGlobal(0x4F) > arg;
case 0x13C0: // IF some_ads_variable[0] ==
debugN(10, "ADS 0x%04x: if adsVariable[0] == %d", code, arg);
return globals->getGlobal(0x50) == arg;
case 0x13C1: // IF some_ads_variable[1] ==
debugN(10, "ADS 0x%04x: if adsVariable[1] == %d", code, arg);
return globals->getGlobal(0x4F) == arg;
default:
error("Not an ADS logic op: %04x, how did we get here?", code);
}
}
bool ADSInterpreter::handleLogicOp(uint16 code, Common::SeekableReadStream *scr) {
bool testval = true;
uint16 andor = 0x1420; // start with "true" AND..
int32 startPos = scr->pos() - 2;
while (scr->pos() < scr->size()) {
uint16 enviro;
uint16 seqnum;
Common::SharedPtr<TTMSeq> seq;
TTMEnviro *env = nullptr;
if ((code & 0xFF) < 0x80) {
enviro = scr->readUint16LE();
seqnum = scr->readUint16LE();
seq = findTTMSeq(enviro, seqnum);
env = findTTMEnviro(enviro);
if (!seq) {
warning("ADS if op 0x%04x referenced non-existent env %d seq %d", code, enviro, seqnum);
// Dragon always returns false for this. Others return result
// based on opcode - eg, "if stopped" is true for non-existent sequence.
if (DgdsEngine::getInstance()->getGameId() == GID_DRAGON)
return false;
}
} else {
// We load this into "enviro" but it's just the parameter of the op.
enviro = scr->readUint16LE();
}
bool logicResult = logicOpResult(code, env, seq.get(), enviro);
if (andor == 0x1420) // AND
testval &= logicResult;
else // OR
testval |= logicResult;
debug(10, " -> %s (overall %s)", logicResult ? "true" : "false", testval ? "true" : "false");
bool isWhile = code < 0x1300;
code = scr->readUint16LE();
if (code == 0x1420 || code == 0x1430) {
andor = code;
debug(10, " ADS 0x%04x: %s", code, code == 0x1420 ? "AND" : "OR");
code = scr->readUint16LE();
// The next op should be another logic op
} else {
// No AND or OR, next op is just what to do.
scr->seek(-2, SEEK_CUR);
if (testval) {
if (isWhile) {
_adsData->_countdown[_adsData->_runningSegmentIdx]++;
_adsData->_charWhile[_adsData->_runningSegmentIdx] = startPos;
}
bool runResult = runUntilBranchOpOrEnd();
// WHILE (10x0) series always return false
return (!isWhile) && runResult;
} else {
if (isWhile) {
_adsData->_countdown[_adsData->_runningSegmentIdx] = 0;
_adsData->_charWhile[_adsData->_runningSegmentIdx] = 0;
return skipToEndWhile();
} else {
return skipToElseOrEndif();
}
}
}
}
error("didn't return from ADS logic test");
}
int16 ADSInterpreter::randomOpGetProportion(uint16 code, Common::SeekableReadStream *scr) {
// Leaves the pointer at the same place it started
int argsize = numArgs(code) * 2;
if (argsize == 0)
error("Unexpected 0-arg ADS opcode 0x%04x inside random block", code);
// skip args before the random proportion
if (argsize > 2)
scr->seek(argsize - 2, SEEK_CUR);
int16 result = scr->readSint16LE();
scr->seek(-argsize, SEEK_CUR);
return result;
}
void ADSInterpreter::handleRandomOp(Common::SeekableReadStream *scr) {
int16 max = 0;
int64 startpos = scr->pos();
// Collect the random proportions
uint16 code = scr->readUint16LE();
while (code != 0 && code != 0x30FF && scr->pos() < scr->size()) {
int16 val = randomOpGetProportion(code, scr);
// leaves pointer at beginning of next op
max += val;
scr->skip(numArgs(code) * 2);
if (scr->pos() >= scr->size())
break;
code = scr->readUint16LE();
}
if (!max)
return;
int64 endpos = scr->pos();
int16 randval = _vm->getRandom().getRandomNumber(max - 1) + 1; // Random from 1-max.
scr->seek(startpos, SEEK_SET);
// Now find the random bit to jump to
code = scr->readUint16LE();
do {
int16 val = randomOpGetProportion(code, scr);
randval -= val;
if (randval < 1) {
// This is the opcode we want to execute
break;
}
scr->skip(numArgs(code) * 2);
if (scr->pos() >= scr->size())
break;
code = scr->readUint16LE();
} while (code != 0 && scr->pos() < scr->size());
if (code && code != 0x3020)
handleOperation(code, scr);
scr->seek(endpos, SEEK_SET);
}
bool ADSInterpreter::handleOperation(uint16 code, Common::SeekableReadStream *scr) {
uint16 enviro, seqnum;
const char *segname; // for debug messages
switch (code) {
case 0x0001:
case 0x0005:
debug(10, "ADS 0x%04x: init", code);
// "init". 0x0005 can be used for searching for next thing.
break;
case 0x1010: // WHILE runtype, 2 params
case 0x1020: // WHILE not runtype, 2 params
case 0x1030: // WHILE not played, 2 params
case 0x1040: // WHILE played, 2 params
case 0x1050: // WHILE finished, 2 params
case 0x1060: // WHILE not running, 2 params
case 0x1070: // WHILE running, 2 params
case 0x1080: // WHILE countdown <= , 1 param (HOC+ only)
case 0x1090: // WHILE ??, 1 param (HOC+ only)
case 0x1310: // IF paused, 2 params
case 0x1320: // IF not paused, 2 params
case 0x1330: // IF NOT_PLAYED, 2 params
case 0x1340: // IF PLAYED, 2 params
case 0x1350: // IF FINISHED, 2 params
case 0x1360: // IF NOT_RUNNING, 2 params
case 0x1370: // IF RUNNING, 2 params
case 0x1380: // IF DETAIL LEVEL <= x, 1 param (HOC+ only)
case 0x1390: // IF DETAIL LEVEL >= x, 1 param (HOC+ only)
// The next 6 are in HoC code but maybe never used?
case 0x13A0: // IF _adsVariable[0] <=
case 0x13A1: // IF _adsVariable[1] <=
case 0x13B0: // IF _adsVariable[0] >
case 0x13B1: // IF _adsVariable[1] >
case 0x13C0: // IF _adsVariable[0] ==
case 0x13C1: // IF _adsVariable[1] ==
return handleLogicOp(code, scr);
case 0x1500: // ELSE / Skip to end-if, 0 params
debug(10, "ADS 0x%04x: else (skip to end if)", code);
skipToEndIf();
_adsData->_hitBranchOp = true;
return true;
case 0x1510: // END IF 0 params
debug(10, "ADS 0x%04x: hit branch op endif", code);
_adsData->_hitBranchOp = true;
return true;
case 0x1520: // END WHILE 0 params
debug(10, "ADS 0x%04x: hit branch op endwhile", code);
_adsData->_hitBranchOp = true;
return false;
case 0x2000:
case 0x2005: { // ADD sequence, set run count
enviro = scr->readUint16LE();
seqnum = scr->readUint16LE();
int16 runCount = scr->readSint16LE();
uint16 unk = scr->readUint16LE(); // proportion
Common::SharedPtr<TTMSeq> seq = findTTMSeq(enviro, seqnum);
TTMEnviro *env = findTTMEnviro(enviro);
// Set this even if null, check for null below.
_currentTTMSeq = seq;
if (!seq || !env) {
warning("ADS op %04x invalid env + seq requested %d %d", code, enviro, seqnum);
break;
}
debug(10, "ADS 0x%04x: add scene - env %d seq %d (%s) runCount %d prop %d", code,
enviro, seqnum, env->_tags.getValOrDefault(seqnum).c_str(), runCount, unk);
if (code == 0x2000) {
//
// HACKY WORKAROUND? If we change the frame here we should also reset
// the `executed` flag surely? Without this, the cola vendor disappears
// after you buy cola from him in Willy Beamish scene 31 (park).
//
if (seq->_currentFrame != seq->_startFrame && _vm->getGameId() == GID_WILLY)
seq->_executed = false;
seq->_currentFrame = seq->_startFrame;
}
if (runCount == 0) {
seq->_runFlag = kRunTypeKeepGoing;
} else if (runCount < 0) {
// Negative run count sets the cut time
seq->_timeCut = DgdsEngine::getInstance()->getThisFrameMs() + (-runCount * MS_PER_FRAME);
seq->_runFlag = kRunTypeTimeLimited;
} else {
seq->_runFlag = kRunTypeMulti;
seq->_runCount = runCount - 1;
}
seq->_runPlayed++;
break;
}
case 0x2010: { // STOP SCENE, 3 params (ttmenv, ttmseq, proportion)
enviro = scr->readUint16LE();
seqnum = scr->readUint16LE();
uint16 unk = scr->readUint16LE();
_currentTTMSeq = findTTMSeq(enviro, seqnum);
const TTMEnviro *env = findTTMEnviro(enviro);
debug(10, "ADS 0x2010: stop seq env %d seq %d (%s) prop %d", enviro, seqnum,
env->_tags.getValOrDefault(seqnum).c_str(), unk);
if (_currentTTMSeq)
_currentTTMSeq->_runFlag = kRunTypeStopped;
break;
}
case 0x2015: { // PAUSE SEQ, 3 params (ttmenv, ttmseq, proportion)
enviro = scr->readUint16LE();
seqnum = scr->readUint16LE();
uint16 unk = scr->readUint16LE();
_currentTTMSeq = findTTMSeq(enviro, seqnum);
const TTMEnviro *env = findTTMEnviro(enviro);
debug(10, "ADS 0x2015: set paused env %d seq %d (%s) prop %d", enviro, seqnum,
env->_tags.getValOrDefault(seqnum).c_str(), unk);
if (_currentTTMSeq)
_currentTTMSeq->_runFlag = kRunTypePaused;
break;
}
case 0x2020: { // RESET SEQ, 2 params (env, seq, proportion)
enviro = scr->readUint16LE();
seqnum = scr->readUint16LE();
uint16 unk = scr->readUint16LE();
_currentTTMSeq = findTTMSeq(enviro, seqnum);
const TTMEnviro *env = findTTMEnviro(enviro);
debug(10, "ADS 0x2020: reset scene env %d seq %d (%s) prop %d", enviro, seqnum,
env->_tags.getValOrDefault(seqnum).c_str(), unk);
if (_currentTTMSeq)
_currentTTMSeq->reset();
break;
}
case 0x3020: {// RANDOM_NOOP, 1 param (proportion)
uint16 unk = scr->readUint16LE();
debug(10, "ADS 0x3020: random noop? prop %d", unk);
return true;
}
case 0x3010: // RANDOM_START, 0 params
debug(10, "ADS 0x3010: random start");
handleRandomOp(scr);
break;
case 0x30FF: // RANDOM_END, 0 params
debug(10, "ADS 0x30FF: random end");
error("Unexpected RANDOM END mid-stream (no RANDOM START?).");
case 0x4000: { // MOVE SEQ TO BACK
enviro = scr->readUint16LE();
seqnum = scr->readUint16LE();
debug(10, "ADS 0x%04x: mov seq to back env %d seq %d", code, enviro, seqnum);
/*uint16 unk = */scr->readUint16LE();
// This is O(N) but the N is small and it's not called often.
Common::SharedPtr<TTMSeq> seq;
for (uint i = 0; i < _adsData->_ttmSeqs.size(); i++) {
if (_adsData->_ttmSeqs[i]->_enviro == enviro && _adsData->_ttmSeqs[i]->_seqNum == seqnum) {
seq = _adsData->_ttmSeqs[i];
_adsData->_ttmSeqs.remove_at(i);
break;
}
}
if (seq)
_adsData->_ttmSeqs.push_back(seq);
else
warning("ADS: 0x4000 Request to move env %d seq %d which doesn't exist", enviro, seqnum);
break;
}
case 0x4010: { // MOVE SEQ TO FRONT
enviro = scr->readUint16LE();
seqnum = scr->readUint16LE();
debug(10, "ADS 0x%04x: mov seq to front env %d seq %d", code, enviro, seqnum);
/*uint16 unk = */scr->readUint16LE();
// This is O(N) but the N is small and it's not called often.
Common::SharedPtr<TTMSeq> seq;
for (uint i = 0; i < _adsData->_ttmSeqs.size(); i++) {
if (_adsData->_ttmSeqs[i]->_enviro == enviro && _adsData->_ttmSeqs[i]->_seqNum == seqnum) {
seq = _adsData->_ttmSeqs[i];
_adsData->_ttmSeqs.remove_at(i);
break;
}
}
if (seq)
_adsData->_ttmSeqs.insert_at(0, seq);
else
warning("ADS: 0x4010 Request to move env %d seq %d which doesn't exist", enviro, seqnum);
break;
}
case 0x1420: // AND, 0 params - should not hit this here.
case 0x1430: // OR, 0 params - should not hit this here.
warning("ADS: Unexpected logic opcode 0x%04x - should be after IF/WHILE", code);
break;
case 0xF000:
debug(10, "ADS 0x%04x: set state 2, current segment idx (%d)", code, _adsData->_runningSegmentIdx);
if (_adsData->_runningSegmentIdx != -1)
_adsData->_state[_adsData->_runningSegmentIdx] = 2;
return false;
case 0xF010: { // FADE_OUT, 1 param
int16 segment = scr->readSint16LE();
int16 idx = _adsData->_runningSegmentIdx;
if (segment >= 0)
idx = getArrIndexOfSegNum(segment);
segname = _adsData->_tags.contains(segment) ? _adsData->_tags.getVal(segment).c_str() : "";
debug(10, "ADS 0x%04x: set state 2, seg %d idx %d %s", code, segment, idx, segname);
if (idx >= 0)
_adsData->_state[idx] = 2;
if (idx == _adsData->_runningSegmentIdx)
return false;
else
return true;
}
case 0xF200: { // RUN_SCRIPT, 1 param
int16 segment = scr->readSint16LE();
int16 idx = getArrIndexOfSegNum(segment);
segname = _adsData->_tags.contains(segment) ? _adsData->_tags.getVal(segment).c_str() : "";
debug(10, "ADS 0x%04x: run seg %d idx %d %s", code, segment, idx, segname);
if (segment >= 0 && idx >= 0) {
int state = (_adsData->_state[idx] & 8) | 4;
_adsData->_state[idx] = state;
}
return true;
}
case 0xF210: { // RESTART_SCRIPT, 1 param
int16 segment = scr->readSint16LE();
int16 idx = getArrIndexOfSegNum(segment);
segname = _adsData->_tags.contains(segment) ? _adsData->_tags.getVal(segment).c_str() : "";
debug(10, "ADS 0x%04x: restart seg %d idx %d %s", code, segment, idx, segname);
if (segment >= 0 && idx >= 0) {
int state = (_adsData->_state[idx] & 8) | 3;
_adsData->_state[idx] = state;
}
return true;
}
case 0xFFFF: // END
debug(10, "ADS 0xFFFF: end");
return false;
//// unknown / to-be-implemented
case 0xFF10:
case 0xFFF0: // END_IF, 0 params
default: {
int nops = numArgs(code);
warning("ADS 0x%04x: Unimplemented opcode: (skip %d args)", code, nops);
for (int i = 0; i < nops; i++)
scr->readUint16LE();
break;
}
}
return true;
}
int16 ADSInterpreter::getStateForSceneOp(uint16 segnum) {
int idx = getArrIndexOfSegNum(segnum);
if (idx < 0)
return 0;
// Slightly different implementation after Dragon.
// Finished is also a "stopped" state in HoC+
if (DgdsEngine::getInstance()->getGameId() == GID_DRAGON) {
if (!(_adsData->_state[idx] & 4)) {
for (const Common::SharedPtr<TTMSeq> &seq: _adsData->_usedSeqs[idx]) {
if (!seq)
error("getStateForSceneOp: used seq for seg %d should not be null", segnum);
if (seq->_runFlag != kRunTypeStopped && !seq->_selfLoop)
return 1;
}
return 0;
}
} else {
int state = (_adsData->_state[idx] & 0xfff7);
if (state != 4 && state != 1) {
for (const Common::SharedPtr<TTMSeq> &seq: _adsData->_usedSeqs[idx]) {
if (!seq)
error("getStateForSceneOp: used seq for seg %d should not be null", segnum);
if (seq->_runFlag != kRunTypeStopped && seq->_runFlag != kRunTypeFinished && !seq->_selfLoop)
return 1;
}
return 0;
}
}
return 1;
}
int ADSInterpreter::getArrIndexOfSegNum(uint16 segnum) {
const int32 startoff = _adsData->scr->pos();
int result = -1;
for (int i = 0; i < _adsData->_maxSegments; i++) {
_adsData->scr->seek(_adsData->_segments[i]);
int16 seg = _adsData->scr->readSint16LE();
if (seg == segnum) {
result = i;
break;
}
}
_adsData->scr->seek(startoff);
return result;
}
bool ADSInterpreter::run() {
if (!_adsData || _adsData->_ttmSeqs.empty())
return false;
for (int idx = 0; idx < _adsData->_maxSegments; idx++) {
int16 flag = _adsData->_state[idx] & 0xfff7;
for (auto seq : _adsData->_usedSeqs[idx]) {
if (flag == 3) {
debug(10, "ADS: Segment idx %d, Reset env %d seq %d", idx, seq->_enviro, seq->_seqNum);
seq->reset();
} else {
if (flag != seq->_scriptFlag) {
//debug(10, "ADS: Segment idx %d, update seq %d scriptflag %d -> %d",
// idx, seq->_seqNum, seq->_scriptFlag, flag);
seq->_scriptFlag = flag;
}
}
}
}
assert(_adsData->scr || !_adsData->_maxSegments);
for (int idx = 0; idx < _adsData->_maxSegments; idx++) {
int16 state = _adsData->_state[idx];
int32 offset = _adsData->_segments[idx];
_adsData->scr->seek(offset);
// skip over the segment num
offset += 2;
int16 segnum = _adsData->scr->readSint16LE();
if (state & 8) {
state &= 0xfff7;
_adsData->_state[idx] = state;
} else {
findEndOrInitOp();
offset = _adsData->scr->pos();
}
if (_adsData->_charWhile[idx])
offset = _adsData->_charWhile[idx];
if (state == 3 || state == 4) {
_adsData->_state[idx] = 1;
state = 1;
}
_adsData->_runningSegmentIdx = idx;
if (state == 1) {
_adsData->scr->seek(offset);
const char *tag = "";
if (_adsData->_tags.contains(segnum))
tag = _adsData->_tags[segnum].c_str();
debug(10, "ADS: Run segment %d idx %d/%d %s", segnum, idx, _adsData->_maxSegments, tag);
runUntilBranchOpOrEnd();
}
}
bool result = false;
for (Common::SharedPtr<TTMSeq> seq : _adsData->_ttmSeqs) {
_currentTTMSeq = seq;
seq->_lastFrame = -1;
int sflag = seq->_scriptFlag;
TTMRunType rflag = seq->_runFlag;
if (sflag == 6 || (rflag != kRunTypeKeepGoing && rflag != kRunTypeTimeLimited && rflag != kRunTypeMulti && rflag != kRunTypePaused)) {
if (sflag != 6 && sflag != 5 && rflag == kRunTypeFinished) {
seq->_runFlag = kRunTypeStopped;
}
} else {
int16 curframe = seq->_currentFrame;
TTMEnviro *env = findTTMEnviro(seq->_enviro);
_adsData->_hitTTMOp0110 = false;
_adsData->_scriptDelay = -1;
bool scriptresult = false;
// Next few lines of code in a separate function in the original..
if (curframe < env->_totalFrames && curframe > -1 && env->_frameOffsets[curframe] > -1) {
env->scr->seek(env->_frameOffsets[curframe]);
scriptresult = playScene();
}
if (scriptresult && sflag != 5) {
seq->_executed = true;
seq->_lastFrame = seq->_currentFrame;
result = true;
if (_adsData->_scriptDelay != -1 && seq->_timeInterval != _adsData->_scriptDelay) {
uint32 now = DgdsEngine::getInstance()->getThisFrameMs();
seq->_timeNext = now + _adsData->_scriptDelay;
seq->_timeInterval = _adsData->_scriptDelay;
}
if (!_adsData->_hitTTMOp0110) {
if (_adsData->_gotoTarget != -1) {
seq->_gotoFrame = _adsData->_gotoTarget;
if (seq->_currentFrame == _adsData->_gotoTarget)
seq->_selfLoop = true;
}
if (seq->_runFlag != kRunTypePaused)
updateSeqTimeAndFrame(env, seq);
} else {
seq->_gotoFrame = seq->_startFrame;
if (seq->_runFlag == kRunTypeMulti && seq->_runCount != 0) {
bool updated = updateSeqTimeAndFrame(env, seq);
if (updated) {
seq->_runCount--;
}
} else if (seq->_runFlag == kRunTypeTimeLimited && seq->_timeCut != 0) {
updateSeqTimeAndFrame(env, seq);
} else {
bool updated = updateSeqTimeAndFrame(env, seq);
if (updated) {
seq->_runFlag = kRunTypeFinished;
seq->_timeInterval = 0;
}
}
}
} else if (sflag != 5) {
seq->_gotoFrame = seq->_startFrame;
seq->_runFlag = kRunTypeFinished;
}
}
if (rflag == kRunTypeTimeLimited && seq->_timeCut <= DgdsEngine::getInstance()->getThisFrameMs()) {
seq->_runFlag = kRunTypeFinished;
}
}
return result;
}
bool ADSInterpreter::runUntilBranchOpOrEnd() {
Common::SeekableReadStream *scr = _adsData->scr;
if (!scr || scr->pos() >= scr->size())
return false;
bool more = true;
do {
uint16 code = scr->readUint16LE();
if (code == 0xffff)
return false;
more = handleOperation(code, scr);
} while (!_adsData->_hitBranchOp && more && scr->pos() < scr->size());
_adsData->_hitBranchOp = false;
return more;
}
void ADSInterpreter::setHitTTMOp0110(bool val /* = true */) {
_adsData->_hitTTMOp0110 = val;
}
void ADSInterpreter::setGotoTarget(int32 target) {
_adsData->_gotoTarget = target;
}
int ADSInterpreter::numArgs(uint16 opcode) const {
switch (opcode) {
case 0x1080:
case 0x1090:
case 0x1380:
case 0x1390:
case 0x13A0:
case 0x13A1:
case 0x13B0:
case 0x13B1:
case 0x13C0:
case 0x13C1:
case 0x3020:
case 0xF010:
case 0xF200:
case 0xF210:
return 1;
case 0x1010:
case 0x1020:
case 0x1030:
case 0x1040:
case 0x1050:
case 0x1060:
case 0x1070:
case 0x1310:
case 0x1320:
case 0x1330:
case 0x1340:
case 0x1350:
case 0x1360:
case 0x1370:
return 2;
case 0x2010:
case 0x2015:
case 0x2020:
case 0x4000:
case 0x4010:
return 3;
case 0x2000:
case 0x2005:
return 4;
default:
return 0;
}
}
Common::Error ADSInterpreter::syncState(Common::Serializer &s) {
//TODO: Currently sync all states then set the active one,
// do we need to load/save all?
uint32 numTexts = _adsTexts.size();
s.syncAsUint32LE(numTexts);
Common::Array<Common::String> scriptNames;
Common::String activeScript;
if (s.isLoading()) {
for (uint32 i = 0; i < numTexts; i++) {
Common::String txtName;
s.syncString(txtName);
// We save all the names, but we only actually need to reload one script -
// the most recent one. Do that at the end of this function.
if (s.getVersion() < 3) {
load(txtName);
scriptNames.push_back(txtName);
}
}
} else {
for (const auto &node : _adsTexts) {
Common::String txtName = node._key;
s.syncString(txtName);
scriptNames.push_back(txtName);
if (&node._value == _adsData)
activeScript = txtName;
}
}
// Text order should be the same
if (s.getVersion() < 3) {
for (const Common::String &name : scriptNames) {
_adsTexts[name].syncState(s);
}
}
for (const Common::String &name : scriptNames) {
load(name);
}
s.syncString(activeScript);
if (s.getVersion() >= 3 && !activeScript.empty()) {
load(activeScript);
scriptNames.push_back(activeScript);
}
assert(activeScript.empty() || _adsTexts.contains(activeScript));
_adsData = activeScript.empty() ? nullptr : &_adsTexts[activeScript];
return Common::kNoError;
}
} // end namespace Dgds