/* 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/language.h"
#include "director/director.h"
#include "director/debugger.h"
#include "director/cast.h"
#include "director/castmember.h"
#include "director/frame.h"
#include "director/movie.h"
#include "director/score.h"
#include "director/util.h"
#include "director/window.h"
#include "director/lingo/lingo.h"
#include "director/lingo/lingo-code.h"
#include "director/lingo/lingo-codegen.h"
#include "director/lingo/lingo-object.h"
#include "director/lingo/lingo-the.h"
namespace Director {
#define PROMPT "lingo"
Debugger *g_debugger;
Debugger::Debugger(): GUI::Debugger() {
g_debugger = this;
registerCmd("help", WRAP_METHOD(Debugger, cmdHelp));
registerCmd("version", WRAP_METHOD(Debugger, cmdVersion));
registerCmd("movie", WRAP_METHOD(Debugger, cmdMovie));
registerCmd("m", WRAP_METHOD(Debugger, cmdMovie));
registerCmd("frame", WRAP_METHOD(Debugger, cmdFrame));
registerCmd("f", WRAP_METHOD(Debugger, cmdFrame));
registerCmd("channels", WRAP_METHOD(Debugger, cmdChannels));
registerCmd("chan", WRAP_METHOD(Debugger, cmdChannels));
registerCmd("cast", WRAP_METHOD(Debugger, cmdCast));
registerCmd("nextframe", WRAP_METHOD(Debugger, cmdNextFrame));
registerCmd("nf", WRAP_METHOD(Debugger, cmdNextFrame));
registerCmd("nextmovie", WRAP_METHOD(Debugger, cmdNextMovie));
registerCmd("nm", WRAP_METHOD(Debugger, cmdNextMovie));
registerCmd("print", WRAP_METHOD(Debugger, cmdPrint));
registerCmd("p", WRAP_METHOD(Debugger, cmdPrint));
registerCmd("repl", WRAP_METHOD(Debugger, cmdRepl));
registerCmd("stack", WRAP_METHOD(Debugger, cmdStack));
registerCmd("st", WRAP_METHOD(Debugger, cmdStack));
registerCmd("scriptframe", WRAP_METHOD(Debugger, cmdScriptFrame));
registerCmd("sf", WRAP_METHOD(Debugger, cmdScriptFrame));
registerCmd("funcs", WRAP_METHOD(Debugger, cmdFuncs));
registerCmd("backtrace", WRAP_METHOD(Debugger, cmdBacktrace));
registerCmd("bt", WRAP_METHOD(Debugger, cmdBacktrace));
registerCmd("disasm", WRAP_METHOD(Debugger, cmdDisasm));
registerCmd("da", WRAP_METHOD(Debugger, cmdDisasm));
registerCmd("var", WRAP_METHOD(Debugger, cmdVar));
registerCmd("v", WRAP_METHOD(Debugger, cmdVar));
registerCmd("step", WRAP_METHOD(Debugger, cmdStep));
registerCmd("s", WRAP_METHOD(Debugger, cmdStep));
registerCmd("next", WRAP_METHOD(Debugger, cmdNext));
registerCmd("n", WRAP_METHOD(Debugger, cmdNext));
registerCmd("finish", WRAP_METHOD(Debugger, cmdFinish));
registerCmd("fin", WRAP_METHOD(Debugger, cmdFinish));
registerCmd("continue", WRAP_METHOD(Debugger, cmdExit));
registerCmd("c", WRAP_METHOD(Debugger, cmdExit));
registerCmd("bpset", WRAP_METHOD(Debugger, cmdBpSet));
registerCmd("b", WRAP_METHOD(Debugger, cmdBpSet));
registerCmd("bpmovie", WRAP_METHOD(Debugger, cmdBpMovie));
registerCmd("bm", WRAP_METHOD(Debugger, cmdBpMovie));
registerCmd("bpframe", WRAP_METHOD(Debugger, cmdBpFrame));
registerCmd("bf", WRAP_METHOD(Debugger, cmdBpFrame));
registerCmd("bpentity", WRAP_METHOD(Debugger, cmdBpEntity));
registerCmd("be", WRAP_METHOD(Debugger, cmdBpEntity));
registerCmd("bpvar", WRAP_METHOD(Debugger, cmdBpVar));
registerCmd("bv", WRAP_METHOD(Debugger, cmdBpVar));
registerCmd("bpdel", WRAP_METHOD(Debugger, cmdBpDel));
registerCmd("bpenable", WRAP_METHOD(Debugger, cmdBpEnable));
registerCmd("bpdisable", WRAP_METHOD(Debugger, cmdBpDisable));
registerCmd("bplist", WRAP_METHOD(Debugger, cmdBpList));
_nextFrame = false;
_nextFrameCounter = 0;
_nextMovie = false;
_step = false;
_stepCounter = 0;
_finish = false;
_finishCounter = 0;
_next = false;
_nextCounter = 0;
_lingoEval = false;
_lingoReplMode = false;
_bpNextId = 1;
_bpCheckFunc = false;
_bpCheckMoviePath = false;
_bpNextMovieMatch = false;
_bpMatchScriptId = 0;
_bpCheckVarRead = false;
_bpCheckVarWrite = false;
_bpCheckEntityRead = false;
_bpCheckEntityWrite = false;
}
Debugger::~Debugger() {
if (_out.isOpen())
_out.close();
}
bool Debugger::cmdHelp(int argc, const char **argv) {
debugPrintf("\n");
debugPrintf("Debug flags\n");
debugPrintf("-----------\n");
debugPrintf("debugflag_list - Lists the available debug flags and their status\n");
debugPrintf("debugflag_enable - Enables a debug flag\n");
debugPrintf("debugflag_disable - Disables a debug flag\n");
debugPrintf("debuglevel - Shows or sets debug level\n");
debugPrintf("\n");
debugPrintf("Commands\n");
debugPrintf("--------\n");
debugPrintf("Player:\n");
debugPrintf(" version - Shows the Director version\n");
debugPrintf(" movie / m [moviePath] - Get or sets the current movie\n");
//debugPrintf(" movieinfo / mi - Show information for the current movie\n");
debugPrintf(" frame / f [frameNum] - Gets or sets the current score frame\n");
debugPrintf(" channels / chan [frameNum] - Shows channel information for a score frame\n");
debugPrintf(" cast [castNum] - Shows the cast list or castNum for the current movie\n");
debugPrintf(" nextframe / nf [n] - Steps forward one or more score frames\n");
debugPrintf(" nextmovie / nm - Steps forward until the next change of movie\n");
debugPrintf("\n");
debugPrintf("Lingo execution:\n");
debugPrintf(" print / p [statement] - Evaluates a single Lingo statement\n");
debugPrintf(" repl - Switches to a REPL interface for evaluating Lingo statements\n");
debugPrintf(" backtrace / bt - Prints a backtrace of all stack frames\n");
debugPrintf(" disasm / da [scriptId:funcName] - Lists the bytecode disassembly for a script function\n");
debugPrintf(" disasm / da all - Lists the bytecode disassembly for all available script functions\n");
debugPrintf(" stack / st - Lists the elements on the stack\n");
debugPrintf(" scriptframe / sf - Prints the current script frame\n");
debugPrintf(" funcs - Lists all of the functions available in the current script frame\n");
debugPrintf(" var / v - Lists all of the variables available in the current script frame\n");
debugPrintf(" step / s [n] - Steps forward one or more operations\n");
debugPrintf(" next / n [n] - Steps forward one or more operations, skips over calls\n");
debugPrintf(" finish / fin - Steps until the current stack frame returns\n");
debugPrintf(" continue / c - Continues execution\n");
debugPrintf("\n");
debugPrintf("Breakpoints:\n");
debugPrintf(" bpset / b - Creates a breakpoint at the current Lingo function and offset\n");
debugPrintf(" bpset / b [funcName] - Creates a breakpoint on a Lingo function matching a name\n");
debugPrintf(" bpset / b [offset] - Creates a breakpoint on the current Lingo function matching an offset\n");
debugPrintf(" bpset / b [funcName] [offset] - Creates a breakpoint on a Lingo function matching a name and offset\n");
debugPrintf(" bpset / b [scriptId:funcName] - Creates a breakpoint on a Lingo function matching a script ID and name\n");
debugPrintf(" bpset / b [scriptId:funcName] [offset] - Creates a breakpoint on a Lingo function matching a script ID, name and offset\n");
debugPrintf(" bpmovie / bm [moviePath] - Create a breakpoint on a switch to a movie\n");
debugPrintf(" bpframe / bf [frameId] - Create a breakpoint on a frame in the score\n");
debugPrintf(" bpframe / bf [moviePath] [frameId] - Create a breakpoint on a frame in the score of a specific movie\n");
debugPrintf(" bpentity / be [entityName] - Create a breakpoint on a Lingo \"the\" entity being read or modified");
debugPrintf(" bpentity / be [entityName] [r/w/rw] - Create a breakpoint on a Lingo \"the\" entity being accessed in a specific way");
debugPrintf(" bpentity / be [entityName:fieldName] - Create a breakpoint on a Lingo \"the\" field being read or modified");
debugPrintf(" bpentity / be [entityName:fieldName] [r/w/rw] - Create a breakpoint on a Lingo \"the\" field being accessed in a specific way");
debugPrintf(" bpvar / bv [varName] - Create a breakpoint on a Lingo variable being read or modified");
debugPrintf(" bpvar / bv [varName] [r/w/rw] - Create a breakpoint on a Lingo variable being accessed in a specific way");
debugPrintf(" bpdel [n] - Deletes a specific breakpoint\n");
debugPrintf(" bpenable [n] - Enables a specific breakpoint\n");
debugPrintf(" bpdisable [n] - Disables a specific breakpoint\n");
debugPrintf(" bplist - Lists all breakpoints\n");
return true;
}
bool Debugger::cmdVersion(int argc, const char **argv) {
debugPrintf("Director version: %d\n", g_director->getVersion());
debugPrintf("Director platform: %s\n", Common::getPlatformCode(g_director->getPlatform()));
debugPrintf("Game ID: %s\n", g_director->getGameId());
debugPrintf("Game variant: %s\n", g_director->getExtra());
debugPrintf("Language: %s\n", Common::getLanguageCode(g_director->getLanguage()));
debugPrintf("Expected Director version: %d\n", g_director->getDescriptionVersion());
debugPrintf("Executable name: %s\n", g_director->getEXEName().c_str());
debugPrintf("Startup file name: %s\n", g_director->_gameDescription->desc.filesDescriptions[0].fileName);
debugPrintf("Startup file MD5: %s\n", g_director->_gameDescription->desc.filesDescriptions[0].md5);
debugPrintf("\n");
return true;
}
bool Debugger::cmdFrame(int argc, const char **argv) {
Lingo *lingo = g_director->getLingo();
Score *score = g_director->getCurrentMovie()->getScore();
if (argc == 2 && atoi(argv[1]) > 0) {
Datum frame, movie;
if (atoi(argv[1]) > 0) {
frame = Datum(atoi(argv[1]));
} else {
frame = Datum(argv[1]);
}
lingo->func_goto(frame, movie);
} else {
debugPrintf("%d\n", score->getCurrentFrame());
}
return true;
}
bool Debugger::cmdMovie(int argc, const char **argv) {
Lingo *lingo = g_director->getLingo();
Movie *movie = g_director->getCurrentMovie();
if (argc == 2) {
Datum frame, mov(argv[1]);
lingo->func_goto(frame, mov);
} else {
debugPrintf("%s\n", movie->getArchive()->getFileName().c_str());
}
return true;
}
bool Debugger::cmdChannels(int argc, const char **argv) {
Score *score = g_director->getCurrentMovie()->getScore();
int maxSize = (int)score->_frames.size();
int frameId = score->getCurrentFrame();
if (argc == 1) {
debugPrintf("Channel info for current frame %d of %d\n", frameId, maxSize);
debugPrintf("%s\n", score->formatChannelInfo().c_str());
return true;
}
if (argc == 2)
frameId = atoi(argv[1]);
if (frameId >= 1 && frameId <= maxSize) {
debugPrintf("Channel info for frame %d of %d\n", frameId, maxSize);
debugPrintf("%s\n", score->_frames[frameId-1]->formatChannelInfo().c_str());
} else {
debugPrintf("Must specify a frame number between 1 and %d.\n", maxSize);
}
return true;
}
bool Debugger::cmdCast(int argc, const char **argv) {
Cast *cast = g_director->getCurrentMovie()->getCast();
Cast *sharedCast = g_director->getCurrentMovie()->getSharedCast();
int castId = -1;
if (argc == 2)
castId = atoi(argv[1]);
debugPrintf("Cast:\n");
if (!cast) {
debugPrintf("[empty]\n");
} else if (castId > -1 && !cast->getCastMember(castId)) {
debugPrintf("[not found]\n");
} else {
debugPrintf("%s\n", cast->formatCastSummary(castId).c_str());
}
debugPrintf("\n");
debugPrintf("Shared cast:\n");
if (!sharedCast) {
debugPrintf("[empty]\n");
} else if (castId > -1 && !sharedCast->getCastMember(castId)) {
debugPrintf("[not found]\n");
} else {
debugPrintf("%s\n", sharedCast->formatCastSummary(castId).c_str());
}
debugPrintf("\n");
return true;
}
bool Debugger::cmdNextFrame(int argc, const char **argv) {
_nextFrame = true;
if (argc == 2 && atoi(argv[1]) > 0) {
_nextFrameCounter = atoi(argv[1]);
} else {
_nextFrameCounter = 1;
}
return cmdExit(0, nullptr);
}
bool Debugger::cmdNextMovie(int argc, const char **argv) {
_nextMovie = true;
return cmdExit(0, nullptr);
}
bool Debugger::cmdPrint(int argc, const char **argv) {
if (argc == 1) {
debugPrintf("Missing expression");
return true;
}
Common::String command;
for (int i = 1; i < argc; i++) {
command += " ";
command += argv[i];
}
command.trim();
return lingoEval(command.c_str());
}
bool Debugger::cmdRepl(int argc, const char **argv) {
debugPrintf("Switching to Lingo REPL mode, type 'lingo off' to return to the debug console.\n");
registerDefaultCmd(WRAP_DEFAULTCOMMAND(Debugger, lingoCommandProcessor));
_lingoReplMode = true;
debugPrintf(PROMPT);
return true;
}
bool Debugger::cmdStack(int argc, const char **argv) {
Lingo *lingo = g_director->getLingo();
debugPrintf("%s\n", lingo->formatStack().c_str());
debugPrintf("\n");
return true;
}
bool Debugger::cmdScriptFrame(int argc, const char **argv) {
Lingo *lingo = g_director->getLingo();
debugPrintf("%s\n", lingo->formatFrame().c_str());
debugPrintf("%s\n", lingo->formatCurrentInstruction().c_str());
return true;
}
bool Debugger::cmdFuncs(int argc, const char **argv) {
Lingo *lingo = g_director->getLingo();
Movie *movie = g_director->getCurrentMovie();
Score *score = movie->getScore();
ScriptContext *csc = lingo->_state->context;
if (csc) {
debugPrintf("Functions attached to frame %d:\n", score->getCurrentFrame());
debugPrintf(" %d:", csc->_id);
debugPrintf("%s", csc->formatFunctionList(" ").c_str());
} else {
debugPrintf("Functions attached to frame %d:\n", score->getCurrentFrame());
debugPrintf(" [empty]\n");
}
debugPrintf("\n");
debugPrintf("Cast functions:\n");
Cast *cast = movie->getCast();
if (cast && cast->_lingoArchive) {
debugPrintf("%s", cast->_lingoArchive->formatFunctionList(" ").c_str());
} else {
debugPrintf(" [empty]\n");
}
debugPrintf("\n");
debugPrintf("Shared cast functions:\n");
Cast *sharedCast = movie->getSharedCast();
if (sharedCast && sharedCast->_lingoArchive) {
debugPrintf("%s", sharedCast->_lingoArchive->formatFunctionList(" ").c_str());
} else {
debugPrintf(" [empty]\n");
}
debugPrintf("\n");
return true;
}
bool Debugger::cmdBacktrace(int argc, const char **argv) {
Lingo *lingo = g_director->getLingo();
debugPrintf("%s\n", lingo->formatCallStack(lingo->_state->pc).c_str());
return true;
}
bool Debugger::cmdDisasm(int argc, const char **argv) {
Lingo *lingo = g_director->getLingo();
if (argc == 2) {
if (!strcmp(argv[1], "all")) {
Movie *movie = g_director->getCurrentMovie();
Score *score = movie->getScore();
ScriptContext *csc = lingo->_state->context;
if (csc) {
debugPrintf("Functions attached to frame %d:\n", score->getCurrentFrame());
for (auto &it : csc->_functionHandlers) {
debugPrintf("%s\n\n", g_lingo->formatFunctionBody(it._value).c_str());
}
} else {
debugPrintf("Functions attached to frame %d:\n", score->getCurrentFrame());
debugPrintf(" [empty]\n");
}
debugPrintf("\n");
debugPrintf("Cast functions:\n");
Cast *cast = movie->getCast();
if (cast && cast->_lingoArchive) {
for (int i = 0; i <= kMaxScriptType; i++) {
debugPrintf(" %s:\n", scriptType2str((ScriptType)i));
if (cast->_lingoArchive->scriptContexts[i].size() == 0)
debugPrintf(" [empty]\n");
for (auto &it : cast->_lingoArchive->scriptContexts[i]) {
for (auto &jt : it._value->_functionHandlers) {
debugPrintf("%s\n", g_lingo->formatFunctionBody(jt._value).c_str());
}
}
}
} else {
debugPrintf(" [empty]\n");
}
debugPrintf("\n");
debugPrintf("Shared cast functions:\n");
Cast *sharedCast = movie->getSharedCast();
if (sharedCast && sharedCast->_lingoArchive) {
for (int i = 0; i <= kMaxScriptType; i++) {
debugPrintf(" %s:\n", scriptType2str((ScriptType)i));
if (sharedCast->_lingoArchive->scriptContexts[i].size() == 0)
debugPrintf(" [empty]\n");
for (auto &it : sharedCast->_lingoArchive->scriptContexts[i]) {
for (auto &jt : it._value->_functionHandlers) {
debugPrintf("%s\n", g_lingo->formatFunctionBody(jt._value).c_str());
}
}
}
} else {
debugPrintf(" [empty]\n");
}
return true;
}
Common::String target(argv[1]);
uint splitPoint = target.findFirstOf(":");
if (splitPoint == Common::String::npos) {
debugPrintf("Must provide target in format scriptid:funcname.\n");
return true;
}
Common::String scriptIdStr = target.substr(0, splitPoint);
int scriptId = atoi(scriptIdStr.c_str());
if (!scriptId) {
debugPrintf("Invalid scriptid, must be an integer.\n");
return true;
}
Common::String funcName = target.substr(splitPoint + 1, Common::String::npos);
Movie *movie = g_director->getCurrentMovie();
Cast *cast = movie->getCast();
if (cast && cast->_lingoArchive) {
ScriptContext *ctx = cast->_lingoArchive->findScriptContext(scriptId);
if (ctx && ctx->_functionHandlers.contains(funcName)) {
debugPrintf("%s\n", lingo->formatFunctionBody(ctx->_functionHandlers[funcName]).c_str());
return true;
}
}
Cast *sharedCast = movie->getSharedCast();
if (sharedCast && sharedCast->_lingoArchive) {
ScriptContext *ctx = sharedCast->_lingoArchive->findScriptContext(scriptId);
if (ctx && ctx->_functionHandlers.contains(funcName)) {
debugPrintf("%s\n", lingo->formatFunctionBody(ctx->_functionHandlers[funcName]).c_str());
return true;
}
}
} else {
Common::Array &callstack = g_lingo->_state->callstack;
if (callstack.size() == 0) {
debugPrintf("Lingo is not executing, nothing to disassemble.\n");
return true;
} else {
CFrame *frame = callstack[callstack.size() - 1];
debugPrintf("%s\n", lingo->formatFunctionBody(frame->sp).c_str());
return true;
}
}
debugPrintf("Script not found.\n");
return true;
}
bool Debugger::cmdVar(int argc, const char **argv) {
Lingo *lingo = g_director->getLingo();
debugPrintf("%s\n", lingo->formatAllVars().c_str());
return true;
}
bool Debugger::cmdStep(int argc, const char **argv) {
_step = true;
if (argc == 2 && atoi(argv[1]) > 0) {
_stepCounter = atoi(argv[1]);
} else {
_stepCounter = 1;
}
return cmdExit(0, nullptr);
}
bool Debugger::cmdNext(int argc, const char **argv) {
_step = true;
_next = true;
if (argc == 2 && atoi(argv[1]) > 0) {
_stepCounter = atoi(argv[1]);
} else {
_stepCounter = 1;
}
return cmdExit(0, nullptr);
}
bool Debugger::cmdFinish(int argc, const char **argv) {
_finish = true;
_finishCounter = 1;
return cmdExit(0, nullptr);
}
bool Debugger::cmdBpSet(int argc, const char **argv) {
Breakpoint bp;
bp.id = _bpNextId;
bp.type = kBreakpointFunction;
if (argc == 1) {
Common::Array &callstack = g_lingo->_state->callstack;
if (callstack.size() == 0) {
debugPrintf("Lingo is not executing, no current function to add breakpoint to.\n");
return true;
}
CFrame *frame = callstack[callstack.size() - 1];
if (!frame->sp.ctx) {
debugPrintf("Unable to add breakpoint, current script context is not addressable.\n");
return true;
}
if (!frame->sp.name) {
debugPrintf("Unable to add breakpoint, current function is not addressable.\n");
return true;
}
bp.scriptId = frame->sp.ctx->_id;
bp.funcName = *frame->sp.name;
bp.funcOffset = g_lingo->_state->pc;
} else if (argc == 2 || argc == 3) {
Common::String target(argv[1]);
uint splitPoint = target.findFirstOf(":");
if (splitPoint == Common::String::npos) {
if (argc == 2 && atoi(argv[1]) > 0) {
// first and only argument is a number, use as an offset for the current function
Common::Array &callstack = g_lingo->_state->callstack;
if (callstack.size() == 0) {
debugPrintf("Lingo is not executing, no current function to add breakpoint to.\n");
return true;
}
CFrame *frame = callstack[callstack.size() - 1];
if (!frame->sp.ctx) {
debugPrintf("Unable to add breakpoint, current script context is not addressable.\n");
return true;
}
if (!frame->sp.name) {
debugPrintf("Unable to add breakpoint, current function is not addressable.\n");
return true;
}
bp.scriptId = frame->sp.ctx->_id;
bp.funcName = *frame->sp.name;
bp.funcOffset = atoi(argv[1]);
} else {
// first argument is a string, do a function name match
bp.funcName = target;
}
} else {
// first argument is n:funcname, do an exact function match
bp.scriptId = atoi(target.substr(0, splitPoint).c_str());
bp.funcName = target.substr(splitPoint + 1, Common::String::npos);
}
if (argc == 3) {
// if there's a second argument, use it as the function offset
bp.funcOffset = atoi(argv[2]);
}
} else {
debugPrintf("Too many arguments.\n");
return true;
}
_breakpoints.push_back(bp);
bpUpdateState();
debugPrintf("Added %s\n", bp.format().c_str());
_bpNextId++;
return true;
}
bool Debugger::cmdBpMovie(int argc, const char **argv) {
if (argc == 2) {
Breakpoint bp;
bp.id = _bpNextId;
_bpNextId++;
bp.type = kBreakpointMovie;
bp.moviePath = argv[1];
_breakpoints.push_back(bp);
bpUpdateState();
debugPrintf("Added %s\n", bp.format().c_str());
} else {
debugPrintf("Must specify a movie path.\n");
}
return true;
}
bool Debugger::cmdBpEntity(int argc, const char **argv) {
if (argc == 2 || argc == 3) {
Breakpoint bp;
bp.type = kBreakpointEntity;
Common::String entityName = Common::String(argv[1]);
Common::String fieldName;
uint splitPoint = entityName.findFirstOf(":");
if (splitPoint != Common::String::npos) {
fieldName = entityName.substr(splitPoint + 1, Common::String::npos);
entityName = entityName.substr(0, splitPoint);
}
if (!g_lingo->_theEntities.contains(entityName)) {
debugPrintf("Entity %s not found.\n", entityName.c_str());
return true;
}
bp.entity = g_lingo->_theEntities[entityName]->entity;
if (!fieldName.empty()) {
Common::String target = Common::String::format("%d%s", bp.entity, fieldName.c_str());
if (!g_lingo->_theEntityFields.contains(target)) {
debugPrintf("Field %s not found for entity %s.\n", fieldName.c_str(), entityName.c_str());
return true;
}
bp.field = g_lingo->_theEntityFields[target]->field;
}
if (argc == 3) {
Common::String props = argv[2];
bp.varRead = props.contains("r") || props.contains("R");
bp.varWrite = props.contains("w") || props.contains("W");
if (!(bp.varRead || bp.varWrite)) {
debugPrintf("Must specify r, w, or rw.\n");
return true;
}
} else {
bp.varRead = true;
bp.varWrite = true;
}
bp.id = _bpNextId;
_bpNextId++;
_breakpoints.push_back(bp);
bpUpdateState();
debugPrintf("Added %s\n", bp.format().c_str());
} else {
debugPrintf("Must specify a variable.\n");
}
return true;
}
bool Debugger::cmdBpVar(int argc, const char **argv) {
if (argc == 2 || argc == 3) {
Breakpoint bp;
bp.type = kBreakpointVariable;
bp.varName = argv[1];
if (argc == 3) {
Common::String props = argv[2];
bp.varRead = props.contains("r") || props.contains("R");
bp.varWrite = props.contains("w") || props.contains("W");
if (!(bp.varRead || bp.varWrite)) {
debugPrintf("Must specify r, w, or rw.");
return true;
}
} else {
bp.varRead = true;
bp.varWrite = true;
}
bp.id = _bpNextId;
_bpNextId++;
_breakpoints.push_back(bp);
bpUpdateState();
debugPrintf("Added %s\n", bp.format().c_str());
} else {
debugPrintf("Must specify a variable.\n");
}
return true;
}
bool Debugger::cmdBpFrame(int argc, const char **argv) {
Movie *movie = g_director->getCurrentMovie();
if (argc == 2 || argc == 3) {
Breakpoint bp;
bp.id = _bpNextId;
_bpNextId++;
bp.type = kBreakpointMovieFrame;
Common::String target(argv[1]);
if (argc == 3) {
bp.moviePath = argv[1];
bp.frameOffset = atoi(argv[2]);
} else {
bp.moviePath = movie->getArchive()->getFileName();
bp.frameOffset = atoi(argv[1]);
}
if (bp.frameOffset == 0) {
debugPrintf("Must specify a valid frame ID.\n");
return true;
}
_breakpoints.push_back(bp);
bpUpdateState();
debugPrintf("Added %s\n", bp.format().c_str());
} else {
debugPrintf("Must specify a valid frame ID.\n");
}
return true;
}
bool Debugger::cmdBpDel(int argc, const char **argv) {
if (argc == 2 && atoi(argv[1]) > 0) {
bool found = false;
for (auto it = _breakpoints.begin(); it != _breakpoints.end(); ++it) {
if (it->id == atoi(argv[1])) {
it = _breakpoints.erase(it);
found = true;
bpUpdateState();
debugPrintf("Deleted breakpoint %s.\n", argv[1]);
break;
}
}
if (!found)
debugPrintf("No breakpoint with ID %s.\n", argv[1]);
} else {
debugPrintf("Must specify a breakpoint ID.\n");
}
return true;
}
bool Debugger::cmdBpEnable(int argc, const char **argv) {
if (argc == 2 && atoi(argv[1]) > 0) {
bool found = false;
for (auto &it : _breakpoints) {
if (it.id == atoi(argv[1])) {
it.enabled = true;
found = true;
bpUpdateState();
debugPrintf("Enabled breakpoint %s.\n", argv[1]);
break;
}
}
if (!found)
debugPrintf("No breakpoint with ID %s.\n", argv[1]);
} else {
debugPrintf("Must specify a breakpoint ID.\n");
}
return true;
}
bool Debugger::cmdBpDisable(int argc, const char **argv) {
if (argc == 2 && atoi(argv[1]) > 0) {
bool found = false;
for (auto &it : _breakpoints) {
if (it.id == atoi(argv[1])) {
it.enabled = false;
found = true;
bpUpdateState();
debugPrintf("Disabled breakpoint %s.\n", argv[1]);
break;
}
}
if (!found)
debugPrintf("No breakpoint with ID %s.\n", argv[1]);
} else {
debugPrintf("Must specify a breakpoint ID.\n");
}
return true;
}
bool Debugger::cmdBpList(int argc, const char **argv) {
if (_breakpoints.size()) {
for (auto &it : _breakpoints) {
debugPrintf("%s (%s)\n", it.format().c_str(), it.enabled ? "enabled" : "disabled");
}
} else {
debugPrintf("No breakpoints set.\n");
}
return true;
}
void Debugger::bpUpdateState() {
_bpCheckFunc = false;
_bpCheckMoviePath = false;
_bpNextMovieMatch = false;
_bpMatchFuncOffsets.clear();
_bpMatchFuncName.clear();
_bpMatchScriptId = 0;
_bpMatchMoviePath.clear();
_bpMatchFrameOffsets.clear();
_bpCheckVarRead = false;
_bpCheckVarWrite = false;
_bpCheckEntityRead = false;
_bpCheckEntityWrite = false;
Movie *movie = g_director->getCurrentMovie();
Common::Array &callstack = g_lingo->_state->callstack;
for (auto &it : _breakpoints) {
if (!it.enabled)
continue;
if (it.type == kBreakpointFunction) {
_bpCheckFunc = true;
if (!callstack.size())
continue;
CFrame *head = callstack[callstack.size() - 1];
if (!head->sp.name)
continue;
if (!head->sp.ctx)
continue;
if (it.funcName.equalsIgnoreCase(*head->sp.name)) {
if (it.scriptId) {
if (it.scriptId == head->sp.ctx->_id) {
_bpMatchScriptId = head->sp.ctx->_id;
_bpMatchFuncName = it.funcName;
_bpMatchFuncOffsets.setVal(it.funcOffset, nullptr);
}
} else {
_bpMatchFuncName = it.funcName;
_bpMatchFuncOffsets.setVal(it.funcOffset, nullptr);
}
}
} else if (it.type == kBreakpointMovie || it.type == kBreakpointMovieFrame) {
_bpCheckMoviePath = true;
if (it.moviePath.equalsIgnoreCase(movie->getArchive()->getFileName())) {
_bpNextMovieMatch |= it.type == kBreakpointMovie;
_bpMatchMoviePath = it.moviePath;
_bpMatchFrameOffsets.setVal(it.frameOffset, nullptr);
}
} else if (it.type == kBreakpointVariable) {
_bpCheckVarRead |= it.varRead;
_bpCheckVarWrite |= it.varWrite;
} else if (it.type == kBreakpointEntity) {
_bpCheckEntityRead |= it.varRead;
_bpCheckEntityWrite |= it.varWrite;
}
}
}
void Debugger::bpTest(bool forceCheck) {
// don't check for breakpoints if we're in eval mode
if (_lingoEval)
return;
// Check if there's a funcName/offset or frame/movie match
bool stop = forceCheck;
uint funcOffset = g_lingo->_state->pc;
Score *score = g_director->getCurrentMovie()->getScore();
uint frameOffset = score->getCurrentFrame();
if (_bpCheckFunc) {
stop |= _bpMatchFuncOffsets.contains(funcOffset);
}
if (_bpCheckMoviePath) {
stop |= _bpMatchFrameOffsets.contains(frameOffset);
}
// Print the breakpoints that matched
if (stop) {
debugPrintf("Hit a breakpoint:\n");
for (auto &it : _breakpoints) {
if (!it.enabled)
continue;
if (it.type == kBreakpointFunction) {
if (it.funcName.equalsIgnoreCase(_bpMatchFuncName) && it.scriptId == _bpMatchScriptId && it.funcOffset == funcOffset)
debugPrintf("%s\n", it.format().c_str());
} else if (it.type == kBreakpointMovie && _bpNextMovieMatch) {
if (it.moviePath.equalsIgnoreCase(_bpMatchMoviePath))
debugPrintf("%s\n", it.format().c_str());
} else if (it.type == kBreakpointMovieFrame) {
if (it.moviePath.equalsIgnoreCase(_bpMatchMoviePath) && it.frameOffset == frameOffset)
debugPrintf("%s\n", it.format().c_str());
}
}
// reset all step commands before returning to console
_nextMovie = false;
_nextFrame = false;
cmdScriptFrame(0, nullptr);
attach();
g_system->updateScreen();
}
}
bool Debugger::lingoCommandProcessor(const char *inputOrig) {
if (!strcmp(inputOrig, "lingo off")) {
registerDefaultCmd(nullptr);
_lingoReplMode = false;
return true;
}
return lingoEval(inputOrig);
}
bool Debugger::lingoEval(const char *inputOrig) {
Common::String inputSan = inputOrig;
inputSan.trim();
if (inputSan.empty())
return true;
Common::String expr = Common::String::format("return %s", inputSan.c_str());
// Compile the code to an anonymous function and call it
ScriptContext *sc = g_lingo->_compiler->compileAnonymous(expr);
if (!sc) {
debugPrintf("Failed to parse expression!\n%s", _lingoReplMode ? PROMPT : "");
return true;
}
Symbol sym = sc->_eventHandlers[kEventGeneric];
_lingoEval = true;
LC::call(sym, 0, true);
_finish = true;
_finishCounter = 1;
return cmdExit(0, nullptr);
}
void Debugger::stepHook() {
bpTest();
if (_step && _nextCounter == 0) {
_stepCounter--;
if (_stepCounter == 0) {
_step = false;
_next = false;
cmdScriptFrame(0, nullptr);
attach();
g_system->updateScreen();
}
}
if (_finish && _finishCounter == 0) {
_finish = false;
if (_lingoEval) {
_lingoEval = false;
Datum result = g_lingo->pop();
debugPrintf("%s\n\n%s", result.asString(true).c_str(), _lingoReplMode ? PROMPT : "");
} else {
cmdScriptFrame(0, nullptr);
}
attach();
g_system->updateScreen();
}
}
void Debugger::frameHook() {
bpTest();
if (_nextFrame) {
_nextFrameCounter--;
if (_nextFrameCounter == 0) {
_nextFrame = false;
cmdFrame(0, nullptr);
attach();
g_system->updateScreen();
}
}
}
void Debugger::movieHook() {
bpUpdateState();
bpTest(_bpNextMovieMatch);
if (_nextMovie) {
_nextMovie = false;
cmdMovie(0, nullptr);
attach();
g_system->updateScreen();
}
}
void Debugger::pushContextHook() {
if (_next)
_nextCounter++;
if (_finish)
_finishCounter++;
bpUpdateState();
}
void Debugger::popContextHook() {
if (_next && _nextCounter > 0)
_nextCounter--;
if (_finish)
_finishCounter--;
bpUpdateState();
}
void Debugger::builtinHook(const Symbol &funcSym) {
if (!funcSym.name)
return;
bpUpdateState();
bool builtinMatch = false;
if (_bpCheckFunc) {
for (auto &it : _breakpoints) {
if (it.type != kBreakpointFunction)
continue;
if (it.funcName.equalsIgnoreCase(*funcSym.name)) {
builtinMatch = true;
break;
}
}
}
bpTest(builtinMatch);
}
void Debugger::varReadHook(const Common::String &name) {
if (name.empty())
return;
if (_bpCheckVarRead) {
for (auto &it : _breakpoints) {
if (it.type == kBreakpointVariable && it.varRead && it.varName.equalsIgnoreCase(name)) {
debugPrintf("Hit a breakpoint:\n");
debugPrintf("%s\n", it.format().c_str());
cmdScriptFrame(0, nullptr);
attach();
g_system->updateScreen();
break;
}
}
}
}
void Debugger::varWriteHook(const Common::String &name) {
if (name.empty())
return;
if (_bpCheckVarWrite) {
for (auto &it : _breakpoints) {
if (it.type == kBreakpointVariable && it.varWrite && it.varName.equalsIgnoreCase(name)) {
debugPrintf("Hit a breakpoint:\n");
debugPrintf("%s\n", it.format().c_str());
cmdScriptFrame(0, nullptr);
attach();
g_system->updateScreen();
break;
}
}
}
}
void Debugger::entityReadHook(int entity, int field) {
if (_bpCheckEntityRead) {
for (auto &it : _breakpoints) {
if (it.type == kBreakpointEntity && it.varRead && it.entity == entity && it.field == field) {
debugPrintf("Hit a breakpoint:\n");
debugPrintf("%s\n", it.format().c_str());
cmdScriptFrame(0, nullptr);
attach();
g_system->updateScreen();
break;
}
}
}
}
void Debugger::entityWriteHook(int entity, int field) {
if (_bpCheckEntityWrite) {
for (auto &it : _breakpoints) {
if (it.type == kBreakpointEntity && it.varWrite && it.entity == entity && it.field == field) {
debugPrintf("Hit a breakpoint:\n");
debugPrintf("%s\n", it.format().c_str());
cmdScriptFrame(0, nullptr);
attach();
g_system->updateScreen();
break;
}
}
}
}
void Debugger::debugLogFile(Common::String logs, bool prompt) {
if (prompt)
debugPrintf("-- %s", logs.c_str());
else
debugPrintf("%s", logs.c_str());
if (g_director->_traceLogFile.empty()) {
if (_out.isOpen())
_out.close();
_outName.clear();
} else {
if (_outName != g_director->_traceLogFile) {
if (_out.isOpen())
_out.close();
if (!_out.open(g_director->_traceLogFile, true))
return;
_outName = g_director->_traceLogFile;
}
if(_out.isOpen()) {
_out.seek(_out.size());
_out.write(logs.c_str(), logs.size());
_out.flush();
}
}
}
} // End of namespace Director