/* 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 "common/file.h"
#include "graphics/macgui/macwindowmanager.h"
#include "director/director.h"
#include "director/cast.h"
#include "director/debugger.h"
#include "director/frame.h"
#include "director/movie.h"
#include "director/picture.h"
#include "director/score.h"
#include "director/sprite.h"
#include "director/util.h"
#include "director/window.h"
#include "director/castmember/castmember.h"
#include "director/castmember/text.h"
#include "director/lingo/lingo-ast.h"
#include "director/lingo/lingo-code.h"
#include "director/lingo/lingo-codegen.h"
#include "director/lingo/lingo-the.h"
namespace Director {
Lingo *g_lingo;
int calcStringAlignment(const char *s) {
return calcCodeAlignment(strlen(s) + 1);
}
int calcCodeAlignment(int l) {
int instLen = sizeof(inst);
return (l + instLen - 1) / instLen;
}
Symbol::Symbol() {
name = nullptr;
type = VOIDSYM;
u.s = nullptr;
refCount = new int;
*refCount = 1;
nargs = 0;
maxArgs = 0;
targetType = kNoneObj;
argNames = nullptr;
varNames = nullptr;
ctx = nullptr;
target = nullptr;
anonymous = false;
}
Symbol::Symbol(const Symbol &s) {
name = s.name;
type = s.type;
u = s.u;
refCount = s.refCount;
*refCount += 1;
nargs = s.nargs;
maxArgs = s.maxArgs;
targetType = s.targetType;
argNames = s.argNames;
varNames = s.varNames;
ctx = s.ctx;
target = s.target;
anonymous = s.anonymous;
}
Symbol& Symbol::operator=(const Symbol &s) {
if (this == &s)
return *this;
reset();
name = s.name;
type = s.type;
u = s.u;
refCount = s.refCount;
*refCount += 1;
nargs = s.nargs;
maxArgs = s.maxArgs;
targetType = s.targetType;
argNames = s.argNames;
varNames = s.varNames;
ctx = s.ctx;
target = s.target;
anonymous = s.anonymous;
return *this;
}
bool Symbol::operator==(Symbol &s) const {
return ctx == s.ctx && (name->equalsIgnoreCase(*s.name));
}
void Symbol::reset() {
*refCount -= 1;
// Coverity thinks that we always free memory, as it assumes
// (correctly) that there are cases when refCount == 0
// Thus, DO NOT COMPILE, trick it and shut tons of false positives
#ifndef __COVERITY__
if (*refCount <= 0) {
if (name)
delete name;
if (type == HANDLER)
delete u.defn;
if (argNames)
delete argNames;
if (varNames)
delete varNames;
delete refCount;
}
#endif
}
Symbol::~Symbol() {
reset();
}
PCell::PCell() {
}
PCell::PCell(const Datum &prop, const Datum &val) {
p = prop;
v = val;
}
MenuReference::MenuReference() {
menuIdNum = -1;
menuIdStr = nullptr;
menuItemIdNum = -1;
menuItemIdStr = nullptr;
}
LingoState::~LingoState() {
for (uint i = 0; i < callstack.size(); i++) {
if (callstack[i]->retLocalVars)
delete callstack[i]->retLocalVars;
if (callstack[i]->retContext) {
callstack[i]->retContext->decRefCount();
}
delete callstack[i];
}
if (localVars)
delete localVars;
if (context) {
context->decRefCount();
}
}
Lingo::Lingo(DirectorEngine *vm) : _vm(vm) {
g_lingo = this;
_state = nullptr;
_currentChannelId = -1;
_globalCounter = 0;
_freezeState = false;
_freezePlay = false;
_playDone = false;
_abort = false;
_expectError = false;
_caughtError = false;
_floatPrecision = 4;
_floatPrecisionFormat = "%.4f";
//kTheEntities
_actorList.type = ARRAY;
_actorList.u.farr = new FArray;
_itemDelimiter = ',';
_exitLock = false;
_preLoadEventAbort = false;
_romanLingo = (_vm->getLanguage() != Common::JA_JPN); // Japanese games typically require double-byte encoding
_searchPath.type = ARRAY;
_searchPath.u.farr = new FArray;
_trace = false;
_traceLoad = 0;
_updateMovieEnabled = false;
// events
_passEvent = false;
_perFrameHook = Datum();
_windowList.type = ARRAY;
_windowList.u.farr = new FArray;
_compiler = new LingoCompiler;
initEventHandlerTypes();
initCharNormalizations();
initBuiltIns();
initFuncs();
initBytecode();
initTheEntities();
initMethods();
initXLibs();
debugC(1, kDebugLingoExec, "Lingo inited");
}
Lingo::~Lingo() {
cleanupLingo();
cleanupFuncs();
cleanupMethods();
delete _compiler;
for (auto &it : _openXLibsState) {
delete it._value;
}
for (auto &it : _openXtrasState) {
delete it._value;
}
}
void Lingo::reloadBuiltIns() {
debugC(1, kDebugLingoExec, "Reloading builtins");
cleanupBuiltIns();
cleanUpTheEntities();
cleanupMethods();
cleanupXLibs();
initBuiltIns();
initTheEntities();
initMethods();
initXLibs();
reloadOpenXLibs();
}
LingoArchive::~LingoArchive() {
// First cleanup the ScriptContexts that are only in LctxContexts.
// LctxContexts has a huge overlap with scriptContexts.
for (auto &it : lctxContexts){
ScriptContext *script = it._value;
if (script->getOnlyInLctxContexts()) {
script->decRefCount();
}
}
for (int i = 0; i <= kMaxScriptType; i++) {
for (auto &it : scriptContexts[i]) {
it._value->decRefCount();
}
}
for (auto &it : factoryContexts) {
for (auto &jt : *it._value) {
jt._value->decRefCount();
}
delete it._value;
}
}
ScriptContext *LingoArchive::getScriptContext(ScriptType type, uint16 id) {
if (!scriptContexts[type].contains(id)) {
return nullptr;
}
return scriptContexts[type][id];
}
ScriptContext *LingoArchive::findScriptContext(uint16 id) {
for (int i = 0; i < kMaxScriptType + 1; i++) {
if (scriptContexts[i].contains(id)) {
return scriptContexts[i][id];
}
}
return nullptr;
}
Common::String LingoArchive::getName(uint16 id) {
Common::String result;
if (id >= names.size()) {
warning("LingoArchive::getName: Name id %d not in list", id);
return result;
}
result = names[id];
return result;
}
Common::String LingoArchive::formatFunctionList(const char *prefix) {
Common::String result;
for (int i = 0; i <= kMaxScriptType; i++) {
result += Common::String::format("%s%s:\n", prefix, scriptType2str((ScriptType)i));
if (scriptContexts[i].size() == 0)
result += Common::String::format("%s [empty]\n", prefix);
for (auto &it : scriptContexts[i]) {
result += Common::String::format("%s %d", prefix, it._key);
CastMemberInfo *cmi = cast->getCastMemberInfo(it._key);
if (cmi && !cmi->name.empty()) {
result += Common::String::format(" \"%s\"", cmi->name.c_str());
}
result += ":\n";
result += it._value->formatFunctionList(Common::String::format("%s ", prefix).c_str());
}
}
result += Common::String::format("%sFactories:\n", prefix);
if (factoryContexts.empty()) {
result += Common::String::format("%s [empty]\n", prefix);
} else {
for (auto it : factoryContexts) {
result += Common::String::format("%s %d:\n", prefix, it._key);
if (it._value->empty()) {
result += Common::String::format("%s [empty]\n", prefix);
} else {
for (auto jt : *it._value) {
result += Common::String::format("%s %s:\n", prefix, jt._key.c_str());
result += jt._value->formatFunctionList(Common::String::format("%s ", prefix).c_str());
}
}
}
}
return result;
}
Symbol Lingo::getHandler(const Common::String &name) {
Symbol sym;
// local functions
if (_state->context && _state->context->_functionHandlers.contains(name))
return _state->context->_functionHandlers[name];
sym = g_director->getCurrentMovie()->getHandler(name, _state->context ? _state->context->_castLibHint : 0);
if (sym.type != VOIDSYM)
return sym;
sym.type = VOIDSYM;
sym.name = new Common::String(name);
return sym;
}
void LingoArchive::patchCode(const Common::U32String &code, ScriptType type, uint16 id, const char *scriptName, uint32 preprocFlags) {
debugC(1, kDebugCompile, "LingoArchive::patchCode: Patching code for type %s(%d) with id %d in '%s%s'\n"
"***********\n%s\n\n***********", scriptType2str(type), type, id, utf8ToPrintable(g_director->getCurrentPath()).c_str(), utf8ToPrintable(cast->getMacName()).c_str(), formatStringForDump(code.encode()).c_str());
if (!getScriptContext(type, id)) {
// If there's no existing script context, don't try and patch it.
warning("Script not defined for type %d, id %d", type, id);
return;
}
ScriptContext *sc = g_lingo->_compiler->compileLingo(code, nullptr, type, CastMemberID(id, cast->_castLibID), scriptName, false, preprocFlags);
if (sc) {
for (auto &it : sc->_functionHandlers) {
it._value.ctx = scriptContexts[type][id];
scriptContexts[type][id]->_functionHandlers[it._key] = it._value;
functionHandlers[it._key] = it._value;
if (g_lingo->_eventHandlerTypeIds.contains(it._key)) {
scriptContexts[type][id]->_eventHandlers[g_lingo->_eventHandlerTypeIds[it._key]] = it._value;
}
}
sc->_functionHandlers.clear();
delete sc;
}
}
void LingoArchive::addCode(const Common::U32String &code, ScriptType type, uint16 id, const char *scriptName, uint32 preprocFlags) {
debugC(1, kDebugCompile, "LingoArchive::addCode: Add code for type %s(%d) with id %d in '%s%s'\n"
"***********\n%s\n\n***********", scriptType2str(type), type, id, utf8ToPrintable(g_director->getCurrentPath()).c_str(), utf8ToPrintable(cast->getMacName()).c_str(), formatStringForDump(code.encode()).c_str());
if (getScriptContext(type, id)) {
// Replace the pre-existing context but warn about it.
// For cases where replacing the script context is expected (e.g. 'when' event handlers)
// use replaceCode instead of addCode.
warning("Script already defined for type %d, id %d", type, id);
removeCode(type, id);
}
Common::String contextName;
if (scriptName && strlen(scriptName) > 0)
contextName = Common::String(scriptName);
else
contextName = Common::String::format("%d", id);
ScriptContext *sc = g_lingo->_compiler->compileLingo(code, this, type, CastMemberID(id, cast->_castLibID), contextName, false, preprocFlags);
if (sc) {
scriptContexts[type][id] = sc;
sc->incRefCount();
}
}
void LingoArchive::removeCode(ScriptType type, uint16 id) {
ScriptContext *ctx = getScriptContext(type, id);
if (!ctx)
return;
ctx->decRefCount();
scriptContexts[type].erase(id);
}
void LingoArchive::replaceCode(const Common::U32String &code, ScriptType type, uint16 id, const char *scriptName) {
removeCode(type, id);
addCode(code, type, id, scriptName);
}
Common::String Lingo::formatStack() {
Common::String stack;
for (uint i = 0; i < _state->stack.size(); i++) {
Datum d = _state->stack[i];
stack += Common::String::format("<%s> ", d.asString(true).c_str());
}
return stack;
}
void Lingo::printStack(const char *s, uint pc) {
Common::String stack(s);
stack += formatStack();
debugC(5, kDebugLingoExec, "[%5d]: %s", pc, stack.c_str());
}
Common::String Lingo::formatCallStack(uint pc) {
Common::String result;
Common::Array &callstack = _state->callstack;
if (callstack.size() == 0) {
result += Common::String("End of execution\n");
return result;
}
result += Common::String("Call stack:\n");
for (int i = 0; i < (int)callstack.size(); i++) {
CFrame *frame = callstack[callstack.size() - i - 1];
uint framePc = pc;
if (i > 0)
framePc = callstack[callstack.size() - i]->retPC;
if (frame->sp.type != VOIDSYM) {
result += Common::String::format("#%d ", i);
if (frame->sp.ctx && frame->sp.ctx->_id) {
result += Common::String::format("%d:", frame->sp.ctx->_id);
}
if (frame->sp.ctx && frame->sp.ctx->isFactory()) {
result += Common::String::format("%s:", frame->sp.ctx->getName().c_str());
}
result += Common::String::format("%s at [%5d]\n",
frame->sp.name->c_str(),
framePc
);
} else {
result += Common::String::format("#%d [unknown] at [%5d]\n", i,
framePc
);
}
}
return result;
}
void Lingo::printCallStack(uint pc) {
debugC(2, kDebugLingoExec, "\n%s", formatCallStack(pc).c_str());
}
Common::String Lingo::formatFrame() {
Common::String result;
Common::Array &callstack = _state->callstack;
if (callstack.size() == 0) {
return Common::String("End of execution");
}
if (_state->context->_id)
result += Common::String::format("%d:", _state->context->_id);
CFrame *frame = callstack[callstack.size() - 1];
if (frame->sp.ctx && frame->sp.ctx->isFactory()) {
result += Common::String::format("%s:", frame->sp.ctx->getName().c_str());
}
if (frame->sp.type == VOIDSYM || !frame->sp.name)
result += "[unknown]";
else
result += frame->sp.name->c_str();
result += Common::String::format(" at [%5d]", _state->pc);
return result;
}
Common::String Lingo::formatCurrentInstruction() {
Common::String instr = decodeInstruction(_state->script, _state->pc);
if (instr.empty())
return instr;
return Common::String::format("[%5d]: %s", _state->pc, instr.c_str());
}
Common::String Lingo::decodeInstruction(ScriptData *sd, uint pc, uint *newPc) {
void *opcodeFunc;
Common::String res;
if (!sd || pc >= sd->size())
return res;
opcodeFunc = (void *)(*sd)[pc++];
if (_functions.contains(opcodeFunc)) {
res = _functions[opcodeFunc]->name;
const char *pars = _functions[opcodeFunc]->proto;
inst i;
uint start = pc;
while (*pars) {
switch (*pars++) {
case 'i':
{
i = (*sd)[pc++];
int v = READ_UINT32(&i);
res += Common::String::format(" %d", v);
break;
}
case 'f':
{
i = (*sd)[pc++];
double d = *(double *)(&i);
res += Common::String::format(" %f", d);
break;
}
case 'o':
{
i = (*sd)[pc++];
int v = READ_UINT32(&i);
res += Common::String::format(" [%5d]", v + start - 1);
break;
}
case 's':
{
char *s = (char *)&(*sd)[pc];
pc += calcStringAlignment(s);
res += Common::String::format(" \"%s\"", s);
break;
}
case 'E':
{
i = (*sd)[pc++];
int v = READ_UINT32(&i);
res += Common::String::format(" %s", entity2str(v));
break;
}
case 'F':
{
i = (*sd)[pc++];
int v = READ_UINT32(&i);
res += Common::String::format(" %s", field2str(v));
break;
}
default:
warning("Lingo::decodeInstruction(): Unknown parameter type: %c", pars[-1]);
}
if (*pars)
res += ',';
}
} else {
res = "";
}
if (newPc)
*newPc = pc;
return res;
}
Common::String Lingo::decodeScript(ScriptData *sd) {
uint pc = 0;
Common::String result;
while (pc < sd->size()) {
result += Common::String::format("[%5d] ", pc);
result += Common::String::format("%s\n", Lingo::decodeInstruction(sd, pc, &pc).c_str());
}
return result;
}
Common::String Lingo::formatFunctionName(Symbol &sym) {
Common::String result;
if (sym.type != HANDLER)
return result;
if (sym.name && sym.name->size())
result += Common::String::format("%s(", sym.name->c_str());
else
result += "(";
for (int i = 0; i < sym.nargs; i++) {
result += (*sym.argNames)[i].c_str();
if (i < (sym.nargs - 1))
result += ", ";
}
result += ")";
return result;
}
Common::String Lingo::formatFunctionBody(Symbol &sym) {
Common::String result;
if (sym.type != HANDLER)
return result;
if (sym.ctx && sym.ctx->_id)
result += Common::String::format("%d:", sym.ctx->_id);
result += formatFunctionName(sym);
result += "\n";
result += decodeScript(sym.u.defn);
return result;
}
bool Lingo::execute(int targetFrame) {
uint localCounter = 0;
uint lastUpdate = 0;
while (!_abort && !_freezeState && !_playDone && _state->script && (*_state->script)[_state->pc] != STOP) {
if (targetFrame != -1 && (int)_state->callstack.size() == targetFrame)
break;
if ((_exec._state == kPause) || (_exec._shouldPause && _exec._shouldPause())) {
// if execution is in pause -> poll event + update screen
_exec._state = kPause;
Common::EventManager *eventMan = g_system->getEventManager();
while (_exec._state == kPause && !eventMan->shouldQuit() && (!g_engine || !eventMan->shouldReturnToLauncher())) {
Common::Event event;
while (eventMan->pollEvent(event)) {
}
g_system->delayMillis(10);
g_system->updateScreen();
}
}
if (_globalCounter > 1000 && debugChannelSet(-1, kDebugFewFramesOnly)) {
warning("Lingo::execute(): Stopping due to debug few frames only");
_vm->getCurrentMovie()->getScore()->_playState = kPlayStopped;
break;
}
// process events every so often
if (localCounter > 0 && localCounter % 100 == 0) {
_vm->processEvents();
// Also process update widgets!
Movie *movie = g_director->getCurrentMovie();
Score *score = movie->getScore();
score->updateWidgets(true);
if (g_system->getMillis() - lastUpdate > 20) {
lastUpdate = g_system->getMillis();
g_system->updateScreen();
}
}
uint current = _state->pc;
if (debugChannelSet(5, kDebugLingoExec))
printStack("Stack before: ", current);
if (debugChannelSet(9, kDebugLingoExec)) {
debug("Vars before");
printAllVars();
if (_state->me.type == OBJECT)
debug("me: %s", _state->me.asString(true).c_str());
}
if (debugChannelSet(4, kDebugLingoExec)) {
Common::String instr = decodeInstruction(_state->script, _state->pc);
debugC(4, kDebugLingoExec, "[%5d]: %s", current, instr.c_str());
}
g_debugger->stepHook();
if (_state->script == nullptr) {
debugC(1, kDebugLingoExec, "Lingo::execute(): PANIC: No script to execute (1)");
break;
}
_state->pc++;
(*((*_state->script)[_state->pc - 1]))();
if (debugChannelSet(5, kDebugLingoExec))
printStack("Stack after: ", current);
if (debugChannelSet(9, kDebugLingoExec)) {
debug("Vars after");
printAllVars();
}
_globalCounter++;
localCounter++;
if (!_abort && _state->script == nullptr) {
debugC(1, kDebugLingoExec, "Lingo::execute(): PANIC: No script to execute (2)");
break;
}
if (!_abort && _state->pc >= (*_state->script).size()) {
warning("Lingo::execute(): Bad PC (%d)", _state->pc);
break;
}
}
bool result = !_freezeState;
if (_freezePlay) {
debugC(5, kDebugLingoExec, "Lingo::execute(): Called play, pausing execution to the play buffer");
freezePlayState();
} else if (_freezeState) {
debugC(5, kDebugLingoExec, "Lingo::execute(): Context is frozen, pausing execution");
freezeState();
// Returning from a script with "play done" does not freeze the state. Instead it obliterates it.
} else if (_abort || _playDone || _vm->getCurrentMovie()->getScore()->_playState == kPlayStopped) {
// Clean up call stack
while (_state->callstack.size()) {
popContext(true);
}
}
_abort = false;
_freezeState = false;
_freezePlay = false;
g_debugger->stepHook();
// return true if execution finished, false if the context froze for later
return result;
}
void Lingo::executeScript(ScriptType type, CastMemberID id) {
Movie *movie = _vm->getCurrentMovie();
if (!movie) {
warning("Lingo::executeScript: Request to execute script with no movie");
return;
}
ScriptContext *sc = movie->getScriptContext(type, id);
if (!sc) {
debugC(3, kDebugLingoExec, "Lingo::executeScript: Request to execute non-existent script type %d id %d of castLib %d", type, id.member, id.castLib);
return;
}
if (!sc->_eventHandlers.contains(kEventGeneric)) {
debugC(3, kDebugLingoExec, "Lingo::executeScript: Request to execute script type %d id %d of castLib %d with no scopeless lingo", type, id.member, id.castLib);
return;
}
debugC(1, kDebugLingoExec, "Executing script type: %s, id: %d, castLib %d", scriptType2str(type), id.member, id.castLib);
Symbol sym = sc->_eventHandlers[kEventGeneric];
LC::call(sym, 0, false);
execute();
}
void Lingo::executeHandler(const Common::String &name, int numargs) {
debugC(1, kDebugLingoExec, "Executing script handler : %s", name.c_str());
Symbol sym = getHandler(name);
int frame = _state->callstack.size();
LC::call(sym, numargs, false);
execute(frame);
}
void Lingo::lingoError(const char *s, ...) {
char buf[1024];
va_list va;
va_start(va, s);
vsnprintf(buf, 1024, s, va);
va_end(va);
if (_expectError) {
warning("Caught Lingo error: %s", buf);
_caughtError = true;
} else {
warning("BUILDBOT: Uncaught Lingo error: %s", buf);
if (debugChannelSet(-1, kDebugLingoStrict)) {
error("Uncaught Lingo error");
}
_abort = true;
}
}
void Lingo::resetLingoGo() {
// Reset lingo items that are reset on `go` command
// Director 4 Lingo Dictionary p.102
Datum emptyDatum = Datum("");
Datum dZero = Datum(0);
Datum nullId;
g_lingo->setTheEntity(kTheBeepOn, nullId, kTheNOField, dZero);
g_lingo->setTheEntity(kTheKeyDownScript, nullId, kTheNOField, emptyDatum);
g_lingo->setTheEntity(kTheMouseDownScript, nullId, kTheNOField, emptyDatum);
g_lingo->setTheEntity(kTheMouseUpScript, nullId, kTheNOField, emptyDatum);
// TODO
// Should also be reset based on: Director 4 Lingo Dictionary p.102
// the constraint properties
// cursor of sprite
// immediate of sprite
// cursor
// puppetSprite
}
void Lingo::cleanupLingo() {
g_director->_wm->removeMenu();
while (_state->callstack.size()) {
popContext(true);
}
}
void Lingo::resetLingo() {
debugC(3, kDebugLingoExec, "Resetting Lingo!");
cleanupLingo();
resetLingoGo();
}
int Lingo::getAlignedType(const Datum &d1, const Datum &d2, bool equality) {
int opType = VOID;
int d1Type = d1.type;
int d2Type = d2.type;
if (equality) {
if (d1Type == STRING && d2Type == STRING)
return STRING;
}
if (d1Type == STRING) {
Common::String src = d1.asString();
if (!src.empty()) {
char *endPtr = nullptr;
strtod(src.c_str(), &endPtr);
if (*endPtr == 0) {
d1Type = FLOAT;
} else if (!equality) {
d1Type = INT;
}
} else {
d1Type = VOID;
}
}
if (d2Type == STRING) {
Common::String src = d2.asString();
if (!src.empty()) {
char *endPtr = nullptr;
strtod(src.c_str(), &endPtr);
if (*endPtr == 0) {
d2Type = FLOAT;
} else if (!equality) {
d2Type = INT;
}
} else {
d2Type = VOID;
}
}
// VOID equals to 0
if (d1Type == VOID)
d1Type = INT;
if (d2Type == VOID)
d2Type = INT;
if (d1Type == OBJECT)
d1Type = STRING;
if (d2Type == OBJECT)
d2Type = STRING;
if ((d1Type == FLOAT && d2Type == INT) || (d1Type == INT && d2Type == FLOAT)) {
opType = FLOAT;
} else if ((d1Type == STRING && d2Type == INT) || (d1Type == INT && d2Type == STRING)) {
opType = STRING;
} else if (d1Type == d2Type) {
opType = d1Type;
}
return opType;
}
Datum::Datum() {
u.s = nullptr;
type = VOID;
refCount = new int;
*refCount = 1;
ignoreGlobal = false;
}
Datum::Datum(const Datum &d) {
type = d.type;
u = d.u;
refCount = d.refCount;
*refCount += 1;
ignoreGlobal = false;
}
Datum& Datum::operator=(const Datum &d) {
if (this != &d && refCount != d.refCount) {
reset();
type = d.type;
u = d.u;
refCount = d.refCount;
*refCount += 1;
}
ignoreGlobal = false;
return *this;
}
Datum::Datum(int val) {
u.i = val;
type = INT;
refCount = new int;
*refCount = 1;
ignoreGlobal = false;
}
Datum::Datum(double val) {
u.f = val;
type = FLOAT;
refCount = new int;
*refCount = 1;
ignoreGlobal = false;
}
Datum::Datum(const Common::String &val) {
u.s = new Common::String(val);
type = STRING;
refCount = new int;
*refCount = 1;
ignoreGlobal = false;
}
Datum::Datum(AbstractObject *val) {
u.obj = val;
if (val) {
type = OBJECT;
refCount = val->getRefCount();
*refCount += 1;
} else {
type = VOID;
refCount = new int;
*refCount = 1;
}
ignoreGlobal = false;
}
Datum::Datum(CastMember *val) {
u.obj = val;
if (val) {
type = MEDIA;
refCount = val->getRefCount();
*refCount += 1;
} else {
type = VOID;
refCount = new int;
*refCount = 1;
}
ignoreGlobal = false;
}
Datum::Datum(const CastMemberID &val) {
u.cast = new CastMemberID(val);
type = CASTREF;
refCount = new int;
*refCount = 1;
ignoreGlobal = false;
}
Datum::Datum(const Common::Point &point) {
type = POINT;
u.farr = new FArray;
u.farr->arr.push_back(Datum(point.x));
u.farr->arr.push_back(Datum(point.y));
refCount = new int;
*refCount = 1;
ignoreGlobal = false;
}
Datum::Datum(const Common::Rect &rect) {
type = RECT;
u.farr = new FArray;
u.farr->arr.push_back(Datum(rect.left));
u.farr->arr.push_back(Datum(rect.top));
u.farr->arr.push_back(Datum(rect.right));
u.farr->arr.push_back(Datum(rect.bottom));
refCount = new int;
*refCount = 1;
ignoreGlobal = false;
}
void Datum::reset() {
if (!refCount)
return;
*refCount -= 1;
// Coverity thinks that we always free memory, as it assumes
// (correctly) that there are cases when refCount == 0
// Thus, DO NOT COMPILE, trick it and shut tons of false positives
#ifndef __COVERITY__
if (*refCount <= 0) {
switch (type) {
case VOID:
case INT:
case FLOAT:
case ARGC:
case ARGCNORET:
case CASTLIBREF:
case SPRITEREF:
break;
case VARREF:
case GLOBALREF:
case LOCALREF:
case PROPREF:
case STRING:
case SYMBOL:
delete u.s;
break;
case ARRAY:
case POINT:
case RECT:
delete u.farr;
break;
case PARRAY:
delete u.parr;
break;
case MEDIA:
delete u.obj;
break;
case OBJECT:
if (u.obj->getObjType() == kWindowObj) {
// Window has an override for decRefCount, use it directly
*refCount += 1;
static_cast(u.obj)->decRefCount();
} else {
// *refCount is copied between the Datum and the Object,
// so should be safe to delete the Object
delete u.obj;
}
break;
case CHUNKREF:
delete u.cref;
break;
case CASTREF:
case FIELDREF:
delete u.cast;
break;
case MENUREF:
delete u.menu;
break;
case PICTUREREF:
delete u.picture;
break;
default:
warning("Datum::reset(): Unprocessed REF type %d", type);
break;
}
if (type != OBJECT && type != MEDIA) // object owns refCount
delete refCount;
}
#endif
}
Datum Datum::eval() const {
if (isRef()) {
return g_lingo->varFetch(*this);
}
return Datum(*this);
}
int Datum::asInt() const {
int res = 0;
switch (type) {
case STRING:
case SYMBOL:
{
Common::String src = asString();
char *endPtr = nullptr;
float result = strtof(src.c_str(), &endPtr);
if (*endPtr == 0) {
res = (int)result;
} else {
warning("Invalid number '%s'", src.c_str());
res = (int)((uint64)u.s & 0xffffffffL);
}
}
break;
case VOID:
// no-op
break;
case INT:
res = u.i;
break;
case FLOAT:
if (g_director->getVersion() < 400) {
res = round(u.f);
} else {
res = (int)u.f;
}
break;
default:
warning("Incorrect operation asInt() for type: %s", type2str());
}
return res;
}
double Datum::asFloat() const {
double res = 0.0;
switch (type) {
case STRING: {
Common::String src = asString();
char *endPtr = nullptr;
double result = strtod(src.c_str(), &endPtr);
if (*endPtr == 0) {
res = result;
} else {
warning("Invalid number '%s'", src.c_str());
res = (int)((uint64)u.s & 0xffffffffL);
}
}
break;
case VOID:
// no-op
break;
case INT:
res = (double)u.i;
break;
case FLOAT:
res = u.f;
break;
default:
warning("Incorrect operation makeFloat() for type: %s", type2str());
}
return res;
}
Common::String Datum::asString(bool printonly) const {
Common::String s;
switch (type) {
case INT:
s = Common::String::format("%d", u.i);
break;
case ARGC:
s = Common::String::format("argc: %d", u.i);
break;
case ARGCNORET:
s = Common::String::format("argcnoret: %d", u.i);
break;
case FLOAT:
s = Common::String::format(g_lingo->_floatPrecisionFormat.c_str(), u.f);
break;
case STRING:
if (!printonly) {
s = *u.s;
} else {
s = Common::String::format("\"%s\"", u.s->c_str());
}
break;
case SYMBOL:
if (!printonly) {
s = *u.s;
} else {
s = Common::String::format("#%s", u.s->c_str());
}
break;
case MEDIA:
s = Common::String::format("media %08x", ((uint32)(size_t)((void *)u.obj)) & 0xffffffff);
break;
case OBJECT:
if (!printonly) {
// Object names in Director are: ""
// the starting '<' is important, it's used when comparing objects and integers
s = Common::String::format("", ((uint32)(size_t)((void *)u.obj)) & 0xffffffff);
} else {
s = u.obj->asString();
}
break;
case VOID:
if (!printonly) {
s = "";
} else {
if (g_director->getVersion() < 400) {
s = "";
} else {
s = "";
}
}
break;
case VARREF:
s = Common::String::format("var: #%s", u.s->c_str());
break;
case GLOBALREF:
s = Common::String::format("global: #%s", u.s->c_str());
break;
case LOCALREF:
s = Common::String::format("local: #%s", u.s->c_str());
break;
case PROPREF:
s = Common::String::format("property: #%s", u.s->c_str());
break;
case CASTREF:
s = Common::String::format("member %d of castLib %d", u.cast->member, u.cast->castLib);
break;
case CASTLIBREF:
s = Common::String::format("castLib %d", u.i);
break;
case FIELDREF:
s = Common::String::format("field %d of castLib %d", u.cast->member, u.cast->castLib);
break;
case SPRITEREF:
s = Common::String::format("sprite %d", u.i);
break;
case CHUNKREF:
{
Common::String chunkType;
switch (u.cref->type) {
case kChunkChar:
chunkType = "char";
break;
case kChunkWord:
chunkType = "word";
break;
case kChunkItem:
chunkType = "item";
break;
case kChunkLine:
chunkType = "line";
break;
}
Common::String src = u.cref->source.asString(true);
Common::String chunk = eval().asString(true);
s += Common::String::format("chunk: %s %d to %d of %s (%s)", chunkType.c_str(), u.cref->startChunk, u.cref->endChunk, src.c_str(), chunk.c_str());
}
break;
case ARRAY:
s += "[";
for (uint i = 0; i < u.farr->arr.size(); i++) {
if (i > 0)
s += ", ";
Datum d = u.farr->arr[i];
s += d.asString(true);
}
s += "]";
break;
case PARRAY:
s = "[";
if (u.parr->arr.size() == 0)
s += ":";
for (uint i = 0; i < u.parr->arr.size(); i++) {
if (i > 0)
s += ", ";
Datum p = u.parr->arr[i].p;
Datum v = u.parr->arr[i].v;
s += Common::String::format("%s: %s", p.asString(true).c_str(), v.asString(true).c_str());
}
s += "]";
break;
case POINT:
s = "point(";
for (uint i = 0; i < u.farr->arr.size(); i++) {
if (i > 0)
s += ", ";
s += Common::String::format("%d", u.farr->arr[i].asInt());
}
s += ")";
break;
case RECT:
s = "rect(";
for (uint i = 0; i < u.farr->arr.size(); i++) {
if (i > 0)
s += ", ";
s += Common::String::format("%d", u.farr->arr[i].asInt());
}
s += ")";
break;
case MENUREF:
s = Common::String::format("menu(%d, %d)", u.menu->menuIdNum, u.menu->menuItemIdNum);
break;
case PICTUREREF:
s = Common::String::format("picture: %p", (void*)u.picture->_picture);
break;
default:
warning("Incorrect operation asString() for type: %s", type2str());
}
return s;
}
CastMemberID Datum::asMemberID(CastType castType, int castLib) const {
if (type == CASTREF || type == FIELDREF)
return *u.cast;
return g_lingo->resolveCastMember(*this, castLib, castType);
}
Common::Point Datum::asPoint() const {
if (type != POINT) {
warning("Incorrect operation asPoint() for type: %s", type2str());
return Common::Point(0, 0);
}
return Common::Point(u.farr->arr[0].asInt(), u.farr->arr[1].asInt());
}
Datum Datum::clone() const {
Datum result;
switch (type) {
case ARRAY:
result.type = ARRAY;
result.u.farr = new FArray;
for (auto &it : u.farr->arr) {
result.u.farr->arr.push_back(it.clone());
}
result.u.farr->_sorted = u.farr->_sorted;
break;
case PARRAY:
result.type = PARRAY;
result.u.parr = new PArray;
for (auto &it : u.parr->arr) {
result.u.parr->arr.push_back(PCell(it.p.clone(), it.v.clone()));
}
result.u.parr->_sorted = u.parr->_sorted;
break;
default:
result = *this;
break;
}
return result;
}
bool Datum::isRef() const {
return (isVarRef() || isCastRef() || type == CHUNKREF);
}
bool Datum::isVarRef() const {
return (type == VARREF || type == GLOBALREF || type == LOCALREF || type == PROPREF);
}
bool Datum::isCastRef() const {
return (type == CASTREF || type == FIELDREF);
}
bool Datum::isArray() const {
return (type == ARRAY || type == POINT || type == RECT);
}
bool Datum::isNumeric() const {
return (type == INT || type == FLOAT);
}
const char *Datum::type2str(bool ilk) const {
static char res[20];
switch (type) {
case ARGC:
return "ARGC";
case ARGCNORET:
return "ARGCNORET";
case ARRAY:
return ilk ? "linearlist" : "ARRAY";
case CASTREF:
return "CASTREF";
case CASTLIBREF:
return "CASTLIBREF";
case CHUNKREF:
return "CHUNKREF";
case FIELDREF:
return "FIELDREF";
case FLOAT:
return ilk ? "float" : "FLOAT";
case GLOBALREF:
return "GLOBALREF";
case INT:
return ilk ? "integer" : "INT";
case LOCALREF:
return "LOCALREF";
case MENUREF:
return "MENUREF";
case MEDIA:
return ilk ? "media" : "MEDIA";
case OBJECT:
return ilk ? "object" : "OBJECT";
case PARRAY:
return ilk ? "proplist" : "PARRAY";
case PICTUREREF:
return ilk ? "picture" : "PICTUREREF";
case POINT:
return ilk ? "point" : "POINT";
case PROPREF:
return "PROPREF";
case RECT:
return ilk ? "rect" : "RECT";
case STRING:
return ilk ? "string" : "STRING";
case SYMBOL:
return ilk ? "symbol" : "SYMBOL";
case VARREF:
return "VARREF";
case VOID:
return ilk ? "void" : "VOID";
default:
snprintf(res, 20, "-- (%d) --", type);
return res;
}
}
int Datum::equalTo(const Datum &d, bool ignoreCase) const {
// VOID can only be equal to VOID and INT 0 (on the right)
if (type == VOID && d.type == VOID) {
return 1;
} else if (type == VOID) {
return d.type == INT && d.u.i == 0;
} else if (d.type == VOID) {
return 0;
}
int alignType = g_lingo->getAlignedType(*this, d, true);
switch (alignType) {
case FLOAT:
return asFloat() == d.asFloat();
case INT:
return asInt() == d.asInt();
case STRING:
case SYMBOL:
if (ignoreCase) {
return compareStringEquality(g_lingo->normalizeString(asString()), g_lingo->normalizeString(d.asString()));
} else {
return compareStringEquality(asString(), d.asString());
}
case MEDIA:
case OBJECT:
return u.obj == d.u.obj;
case CASTREF:
return *u.cast == *d.u.cast;
case CASTLIBREF:
case PICTUREREF:
return 0; // Original always returns 0 on picture reference comparison
default:
debugC(1, kDebugLingoExec, "Datum::equalTo(): Invalid equality check between types %s and %s", type2str(), d.type2str());
break;
}
return 0;
}
bool Datum::operator==(const Datum &d) const {
return equalTo(d);
}
bool Datum::operator>(const Datum &d) const {
return compareTo(d) & kCompareGreater;
}
bool Datum::operator<(const Datum &d) const {
return compareTo(d) & kCompareLess;
}
bool Datum::operator>=(const Datum &d) const {
return compareTo(d) & kCompareGreaterEqual;
}
bool Datum::operator<=(const Datum &d) const {
return compareTo(d) & kCompareLessEqual;
}
uint32 Datum::compareTo(const Datum &d) const {
// VOID will always be treated as:
// - equal to VOID
// - less than -and- equal to INT 0 (yes, really)
// - less than any other type
if (type == VOID && d.type == VOID) {
return kCompareEqual | kCompareLessEqual | kCompareGreaterEqual;
} else if (type == VOID && d.type == INT && d.u.i == 0) {
return kCompareLess | kCompareEqual | kCompareLessEqual;
} else if (type == VOID) {
return kCompareLess | kCompareLessEqual;
} else if (d.type == VOID) {
return kCompareGreater | kCompareGreaterEqual;
}
int alignType = g_lingo->getAlignedType(*this, d, true);
if (alignType == FLOAT) {
double f1 = asFloat();
double f2 = d.asFloat();
if (f1 < f2) {
return kCompareLess | kCompareLessEqual;
} else if (f1 == f2) {
return kCompareEqual | kCompareLessEqual | kCompareGreaterEqual;
} else {
return kCompareGreater | kCompareGreaterEqual;
}
} else if (alignType == INT) {
double i1 = asInt();
double i2 = d.asInt();
if (i1 < i2) {
return kCompareLess | kCompareLessEqual;
} else if (i1 == i2) {
return kCompareEqual | kCompareLessEqual | kCompareGreaterEqual;
} else {
return kCompareGreater | kCompareGreaterEqual;
}
} else if (alignType == STRING || alignType == SYMBOL) {
uint32 result = 0;
// Strings can be equal and less/greater than at the same time.
// Equality is determined by whether the characters
// match based on the equality table, whereas less/greater
// than status is determined by the order of the characters
// in the order table.
bool eq = compareStringEquality(asString(), d.asString());
int res = compareStringOrder(asString(), d.asString());
if (res < 0) {
result = kCompareLess | kCompareLessEqual;
if (eq) {
result |= kCompareEqual;
}
} else if (res == 0) {
result = kCompareEqual | kCompareLessEqual | kCompareGreaterEqual;
} else {
result = kCompareGreater | kCompareGreaterEqual;
if (eq) {
result |= kCompareEqual;
}
}
return result;
// non-coercable strings always outrank numbers and VOID
} else if ((this->type == FLOAT || this->type == INT || this->type == VOID) && (d.type == STRING || d.type == SYMBOL)) {
return kCompareLessEqual | kCompareLess;
} else if ((d.type == FLOAT || d.type == INT || d.type == VOID) && (this->type == STRING || this->type == SYMBOL)) {
return kCompareGreaterEqual | kCompareGreater;
} else {
warning("Datum::compareTo(): Invalid comparison between types %s and %s", type2str(), d.type2str());
return kCompareError;
}
}
void Lingo::runTests() {
Common::File inFile;
Common::ArchiveMemberList fsList;
SearchMan.listMatchingMembers(fsList, "*.lingo");
Common::Array fileList;
LingoArchive *mainArchive = g_director->getCurrentMovie()->getMainLingoArch();
Common::Path startMovie = Common::Path(_vm->getStartMovie().startMovie, g_director->_dirSeparator);
if (!startMovie.empty()) {
fileList.push_back(startMovie);
} else {
for (auto &it : fsList)
fileList.push_back(it->getPathInArchive());
}
Common::sort(fileList.begin(), fileList.end());
int counter = 1;
for (uint i = 0; i < fileList.size(); i++) {
Common::SeekableReadStream *const stream = SearchMan.createReadStreamForMember(fileList[i]);
if (stream) {
uint size = stream->size();
char *script = (char *)calloc(size + 1, 1);
stream->read(script, size);
debug(">> Compiling file %s of size %d, id: %d", fileList[i].toString(g_director->_dirSeparator).c_str(), size, counter);
mainArchive->addCode(Common::U32String(script, Common::kMacRoman), kTestScript, counter);
if (!debugChannelSet(-1, kDebugCompileOnly)) {
if (!_compiler->_hadError)
executeScript(kTestScript, CastMemberID(counter, DEFAULT_CAST_LIB));
else
debug(">> Skipping execution");
}
free(script);
delete stream;
counter++;
}
inFile.close();
}
}
void Lingo::executeImmediateScripts(Frame *frame) {
for (uint16 i = 0; i <= _vm->getCurrentMovie()->getScore()->_numChannelsDisplayed; i++) {
if (_vm->getCurrentMovie()->getScore()->_immediateActions.contains(frame->_sprites[i]->_scriptId.member)) {
// From D5 only explicit event handlers are processed
// Before that you could specify commands which will be executed on mouse up
if (_vm->getVersion() < 500)
processEvent(kEventGeneric, kScoreScript, frame->_sprites[i]->_scriptId, i);
else
processEvent(kEventMouseUp, kScoreScript, frame->_sprites[i]->_scriptId, i);
}
}
}
void Lingo::executePerFrameHook(int frame, int subframe, bool stepFrame) {
// Execute perFrameHook and actorList stepFrame, if any is available
// Starting D4, stepFrame of each objects in actorList is executed
// however the support for legacy mAtFrame is still there. (in future versions)
if (_perFrameHook.type == OBJECT) {
Symbol method = _perFrameHook.u.obj->getMethod("mAtFrame");
if (method.type != VOIDSYM) {
debugC(1, kDebugLingoExec, "Executing perFrameHook : <%s>(mAtFrame, %d, %d)", _perFrameHook.asString(true).c_str(), frame, subframe);
push(_perFrameHook);
push(frame);
push(subframe);
LC::call(method, 3, false);
execute();
}
}
if (stepFrame && _actorList.u.farr->arr.size() > 0 && _vm->getVersion() >= 400) {
for (uint i = 0; i < _actorList.u.farr->arr.size(); i++) {
Datum actor = _actorList.u.farr->arr[i];
Symbol method = actor.u.obj->getMethod("stepFrame");
if (method.type != VOIDSYM) {
debugC(1, kDebugLingoExec, "Executing perFrameHook : <%s>, frame %d, subframe %d", actor.asString(true).c_str(), frame, subframe);
if (method.nargs == 1)
push(actor);
LC::call(method, method.nargs, false);
execute();
}
}
}
}
void Lingo::cleanLocalVars() {
// Clean up current scope local variables and clean up memory
debugC(3, kDebugLingoExec, "cleanLocalVars: have %d vars", _state->localVars->size());
_state->localVars->clear();
delete _state->localVars;
_state->localVars = nullptr;
}
Common::String Lingo::formatAllVars() {
Common::String result;
Common::Array keyBuffer;
result += Common::String(" Local vars:\n");
if (_state->localVars) {
for (auto &it : *_state->localVars) {
keyBuffer.push_back(it._key);
}
Common::sort(keyBuffer.begin(), keyBuffer.end());
for (auto &i : keyBuffer) {
Datum &val = _state->localVars->getVal(i);
result += Common::String::format(" %s - [%s] %s\n", i.c_str(), val.type2str(), formatStringForDump(val.asString(true)).c_str());
}
keyBuffer.clear();
} else {
result += Common::String(" (no local vars)\n");
}
result += Common::String("\n");
if (_state->me.type == OBJECT && _state->me.u.obj->getObjType() & (kFactoryObj | kScriptObj)) {
ScriptContext *script = static_cast(_state->me.u.obj);
result += Common::String(" Instance/property vars: \n");
for (uint32 i = 1; i <= script->getPropCount(); i++) {
keyBuffer.push_back(script->getPropAt(i));
}
Common::sort(keyBuffer.begin(), keyBuffer.end());
for (auto &i : keyBuffer) {
Datum val = script->getProp(i);
result += Common::String::format(" %s - [%s] %s\n", i.c_str(), val.type2str(), formatStringForDump(val.asString(true)).c_str());
}
keyBuffer.clear();
result += Common::String("\n");
}
result += Common::String(" Global vars:\n");
for (auto &it : _globalvars) {
keyBuffer.push_back(it._key);
}
Common::sort(keyBuffer.begin(), keyBuffer.end());
for (auto &i : keyBuffer) {
Datum &val = _globalvars.getVal(i);
result += Common::String::format(" %s - [%s] %s\n", i.c_str(), val.type2str(), formatStringForDump(val.asString(true)).c_str());
}
keyBuffer.clear();
result += Common::String("\n");
return result;
}
void Lingo::printAllVars() {
debugN("%s", formatAllVars().c_str());
}
int Lingo::getInt(uint pc) {
return (int)READ_UINT32(&((*_state->script)[pc]));
}
void Lingo::varAssign(const Datum &var, const Datum &value) {
switch (var.type) {
case VARREF:
{
Common::String name = *var.u.s;
if (_state->localVars && _state->localVars->contains(name)) {
(*_state->localVars)[name] = value;
g_debugger->varWriteHook(name);
return;
}
if (_state->me.type == OBJECT && _state->me.u.obj->hasProp(name)) {
_state->me.u.obj->setProp(name, value);
g_debugger->varWriteHook(name);
return;
}
_globalvars[name] = value;
g_debugger->varWriteHook(name);
}
break;
case GLOBALREF:
// Global variables declared by `global varname` within a handler are not listed anywhere
// in Lscr, unlike globals declared outside of a handler and every other variable type.
// So while we require other variable types to be initialized before assigning to them,
// let's not enforce that for globals.
_globalvars[*var.u.s] = value;
break;
case LOCALREF:
{
Common::String name = *var.u.s;
if (_state->localVars && _state->localVars->contains(name)) {
(*_state->localVars)[name] = value;
g_debugger->varWriteHook(name);
} else {
warning("varAssign: local variable %s not defined", name.c_str());
}
}
break;
case PROPREF:
{
Common::String name = *var.u.s;
if (_state->me.type == OBJECT && _state->me.u.obj->hasProp(name)) {
_state->me.u.obj->setProp(name, value);
g_debugger->varWriteHook(name);
} else {
warning("varAssign: property %s not defined", name.c_str());
}
}
break;
case FIELDREF:
case CASTREF:
{
Movie *movie = g_director->getCurrentMovie();
if (!movie) {
warning("varAssign: Assigning to a reference to an empty movie");
return;
}
CastMember *member = movie->getCastMember(*var.u.cast);
if (!member) {
warning("varAssign: Unknown %s", var.u.cast->asString().c_str());
return;
}
switch (member->_type) {
case kCastText:
((TextCastMember *)member)->setRawText(value.asString());
break;
default:
warning("varAssign: Unhandled cast type %d", member->_type);
break;
}
}
break;
case CHUNKREF:
{
Common::U32String src = evalChunkRef(var.u.cref->source);
Common::U32String res;
if (var.u.cref->start >= 0) {
res = src.substr(0, var.u.cref->start) + value.asString().decode(Common::kUtf8) + src.substr(var.u.cref->end);
} else {
// non-existent chunk - insert more chars, items, or lines
res = src;
int numberOfChunks = LC::lastChunk(var.u.cref->type, var.u.cref->source).u.cref->startChunk;
switch (var.u.cref->type) {
case kChunkChar:
while (numberOfChunks < var.u.cref->startChunk - 1) {
res += ' ';
numberOfChunks++;
}
break;
case kChunkWord:
break;
case kChunkItem:
while (numberOfChunks < var.u.cref->startChunk ) {
res += _itemDelimiter;
numberOfChunks++;
}
break;
case kChunkLine:
while (numberOfChunks < var.u.cref->startChunk ) {
res += '\r';
numberOfChunks++;
}
break;
}
res += value.asString();
}
varAssign(var.u.cref->source, res.encode(Common::kUtf8));
}
break;
default:
warning("varAssign: assignment to non-variable");
break;
}
}
Datum Lingo::varFetch(const Datum &var, bool silent) {
Datum result;
switch (var.type) {
case VARREF:
{
Datum d;
Common::String name = *var.u.s;
g_debugger->varReadHook(name);
if (_state->localVars && _state->localVars->contains(name)) {
return (*_state->localVars)[name];
}
if (_state->me.type == OBJECT && _state->me.u.obj->hasProp(name)) {
return _state->me.u.obj->getProp(name);
}
if (_globalvars.contains(name)) {
return _globalvars[name];
}
if (!silent)
debugC(1, kDebugLingoExec, "varFetch: variable %s not found", name.c_str());
return result;
}
break;
case GLOBALREF:
{
Common::String name = *var.u.s;
g_debugger->varReadHook(name);
if (_globalvars.contains(name)) {
return _globalvars[name];
}
debugC(1, kDebugLingoExec, "varFetch: global variable %s not defined", name.c_str());
return result;
}
break;
case LOCALREF:
{
Common::String name = *var.u.s;
g_debugger->varReadHook(name);
if (_state->localVars && _state->localVars->contains(name)) {
return (*_state->localVars)[name];
}
debugC(1, kDebugLingoExec, "varFetch: local variable %s not defined", name.c_str());
return result;
}
break;
case PROPREF:
{
Common::String name = *var.u.s;
g_debugger->varReadHook(name);
if (_state->me.type == OBJECT && _state->me.u.obj->hasProp(name)) {
return _state->me.u.obj->getProp(name);
}
warning("varFetch: property %s not defined", name.c_str());
return result;
}
break;
case FIELDREF:
case CASTREF:
case CHUNKREF:
{
Common::String chunk(evalChunkRef(var), Common::kUtf8);
result = Datum(chunk);
}
break;
default:
warning("varFetch: fetch from non-variable");
break;
}
return result;
}
Common::U32String Lingo::evalChunkRef(const Datum &var) {
Common::U32String result;
switch (var.type) {
case VARREF:
case GLOBALREF:
case LOCALREF:
case PROPREF:
result = varFetch(var).asString().decode(Common::kUtf8);
break;
case FIELDREF:
case CASTREF:
{
Movie *movie = g_director->getCurrentMovie();
if (!movie) {
warning("evalChunkRef: Assigning to a reference to an empty movie");
return result;
}
CastMember *member = movie->getCastMember(*var.u.cast);
if (!member) {
warning("evalChunkRef: Unknown %s", var.u.cast->asString().c_str());
return result;
}
switch (member->_type) {
case kCastText:
result = ((TextCastMember *)member)->getText();
break;
default:
warning("evalChunkRef: Unhandled cast type %d", member->_type);
break;
}
}
break;
case CHUNKREF:
{
Common::U32String src = evalChunkRef(var.u.cref->source);
if (var.u.cref->start >= 0) {
result = src.substr(var.u.cref->start, var.u.cref->end - var.u.cref->start);
}
}
break;
default:
result = var.asString().decode(Common::kUtf8);
break;
}
return result;
}
CastMemberID Lingo::resolveCastMember(const Datum &memberID, const Datum &castLib, CastType type) {
Movie *movie = g_director->getCurrentMovie();
if (!movie) {
warning("Lingo::resolveCastMember: No movie");
return CastMemberID(-1, castLib.asInt());
}
switch (memberID.type) {
case STRING:
return movie->getCastMemberIDByNameAndType(memberID.asString(), castLib.asInt(), type);
break;
case INT:
case FLOAT:
if (g_director->getVersion() >= 500 && memberID.asInt() > 0x20000) {
// Composite ID
return CastMemberID().fromMultiplex(memberID.asInt());
}
if (castLib.asInt() == 0) {
// When specifying 0 as the castlib, D5 will assume this
// means the default (i.e. first) cast library. It will not
// try other libraries for matches if the member is a number.
return CastMemberID(memberID.asInt(), DEFAULT_CAST_LIB);
} else {
return CastMemberID(memberID.asInt(), castLib.asInt());
}
break;
case VOID:
warning("Lingo::resolveCastMember: reference to VOID member ID");
break;
default:
error("Lingo::resolveCastMember: unsupported member ID type %s", memberID.type2str());
}
return CastMemberID(-1, castLib.asInt());
}
CastMemberID Lingo::toCastMemberID(const Datum &member, const Datum &castLib) {
// Used specifically for unpacking CastMemberIDs when provided by the bytecode
// as two Datums. Multiplex IDs are supported, but auto-truncated.
Movie *movie = g_director->getCurrentMovie();
if (!movie) {
warning("Lingo::toCastMemberID: No movie set");
return CastMemberID(-1, 0);
}
CastMemberID res;
if (castLib.type == VOID) {
if (member.isCastRef()) {
res = member.asMemberID();
} else if (member.isNumeric()) {
res = movie->getCastMemberIDByMember(member.asInt());
} else {
res = movie->getCastMemberIDByName(member.asString());
}
} else {
int libId = -1;
if (castLib.type == CASTLIBREF) {
libId = castLib.u.i;
} else if (castLib.isNumeric()) {
libId = castLib.asInt();
} else {
libId = movie->getCastLibIDByName(castLib.asString());
}
if (member.isCastRef()) {
res = member.asMemberID();
} else if (member.isNumeric()) {
res = CastMemberID().fromMultiplex(member.asInt());
if (libId != 0)
res.castLib = libId;
} else {
res = movie->getCastMemberIDByNameAndType(member.asString(), libId, kCastTypeAny);
}
}
return res;
}
void Lingo::exposeXObject(const char *name, Datum obj) {
_globalvars[name] = obj;
_globalvars[name].ignoreGlobal = true;
}
void Lingo::addBreakpoint(Breakpoint &bp) {
bp.id = _bpNextId;
_breakpoints.push_back(bp);
_bpNextId++;
}
bool Lingo::delBreakpoint(int id) {
for (auto it = _breakpoints.begin(); it != _breakpoints.end(); ++it) {
if (it->id == id) {
it = _breakpoints.erase(it);
return true;
}
}
return false;
}
Breakpoint *Lingo::getBreakpoint(int id) {
for (auto &it : _breakpoints) {
if (it.id == id) {
return ⁢
}
}
return nullptr;
}
PictureReference::~PictureReference() {
delete _picture;
}
} // End of namespace Director