/* 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 . * */ #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 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 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 &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 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 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 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 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 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 &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 &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 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 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