/* 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 "common/platform.h" #include "director/director.h" #include "director/debugger.h" #include "director/archive.h" #include "director/cast.h" #include "director/channel.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("info", WRAP_METHOD(Debugger, cmdInfo)); 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("actions", WRAP_METHOD(Debugger, cmdActions)); registerCmd("act", WRAP_METHOD(Debugger, cmdActions)); registerCmd("markers", WRAP_METHOD(Debugger, cmdMarkers)); registerCmd("mk", WRAP_METHOD(Debugger, cmdMarkers)); 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("windows", WRAP_METHOD(Debugger, cmdWindows)); registerCmd("w", WRAP_METHOD(Debugger, cmdWindows)); 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("bpprop", WRAP_METHOD(Debugger, cmdBpProp)); registerCmd("bp", WRAP_METHOD(Debugger, cmdBpProp)); registerCmd("bpvar", WRAP_METHOD(Debugger, cmdBpVar)); registerCmd("bv", WRAP_METHOD(Debugger, cmdBpVar)); registerCmd("bpevent", WRAP_METHOD(Debugger, cmdBpEvent)); registerCmd("bn", WRAP_METHOD(Debugger, cmdBpEvent)); registerCmd("bpdel", WRAP_METHOD(Debugger, cmdBpDel)); registerCmd("bpenable", WRAP_METHOD(Debugger, cmdBpEnable)); registerCmd("bpdisable", WRAP_METHOD(Debugger, cmdBpDisable)); registerCmd("bplist", WRAP_METHOD(Debugger, cmdBpList)); registerCmd("draw", WRAP_METHOD(Debugger, cmdDraw)); registerCmd("forceredraw", WRAP_METHOD(Debugger, cmdForceRedraw)); _nextFrame = false; _nextFrameCounter = 0; _nextMovie = false; _step = false; _stepCounter = 0; _finish = false; _finishCounter = 0; _next = false; _nextCounter = 0; _lingoEval = false; _lingoReplMode = 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(" info - Shows information about the current movie\n"); debugPrintf(" movie / m [moviePath] - Get or sets 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(" actions / act - Lists all of the action scripts available in the current score\n"); debugPrintf(" var / v - Lists all of the variables available in the current script frame\n"); debugPrintf(" markers / mk - Lists all of the frame markers in the current score\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(" windows / w - Lists all of the windows\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\n"); debugPrintf(" bpentity / be [entityName] [r/w/rw] - Create a breakpoint on a Lingo \"the\" entity being accessed in a specific way\n"); debugPrintf(" bpentity / be [entityName:fieldName] - Create a breakpoint on a Lingo \"the\" field being read or modified\n"); debugPrintf(" bpentity / be [entityName:fieldName] [r/w/rw] - Create a breakpoint on a Lingo \"the\" field being accessed in a specific way\n"); debugPrintf(" bpprop / bp [varName] - Create a breakpoint on a Lingo object property being read or modified\n"); debugPrintf(" bpprop / bp [varName] [r/w/rw] - Create a breakpoint on a Lingo object property being accessed in a specific way\n"); debugPrintf(" bpvar / bv [varName] - Create a breakpoint on a Lingo variable being read or modified\n"); debugPrintf(" bpvar / bv [varName] [r/w/rw] - Create a breakpoint on a Lingo variable being accessed in a specific way\n"); debugPrintf(" bpevent / bn [eventName] - Create a breakpoint on a Lingo event\n"); 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"); debugPrintf("\n"); debugPrintf("GFX:\n"); debugPrintf(" draw [cast|frame|off] - Draws debug outlines for cast or frame number\n"); return true; } Common::String Breakpoint::format() const { Common::String result = Common::String::format("Breakpoint %d, ", id); switch (type) { case kBreakpointFunction: result += "Function "; if (scriptId) result += Common::String::format("%d:", scriptId); result += funcName; if (funcOffset) result += Common::String::format(" [%5d]", funcOffset); break; case kBreakpointMovie: result += "Movie " + moviePath; break; case kBreakpointMovieFrame: result += Common::String::format("Movie %s:%d", moviePath.c_str(), frameOffset); break; case kBreakpointProperty: result += "Property "+ varName + ":"; result += varRead ? "r" : ""; result += varWrite ? "w" : ""; break; case kBreakpointVariable: result += "Variable "+ varName + ":"; result += varRead ? "r" : ""; result += varWrite ? "w" : ""; break; case kBreakpointEntity: result += "Entity "; result += g_lingo->entity2str(entity); result += field ? ":" : ""; result += field ? g_lingo->field2str(field) : ""; result += ":"; result += varRead ? "r" : ""; result += varWrite ? "w" : ""; break; case kBreakpointEvent: result += "Event "; if (eventId == kEventNone) { result += "none"; } else { result += g_lingo->_eventHandlerTypes[eventId]; } break; default: break; } return result; } 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::cmdInfo(int argc, const char **argv) { Movie *movie = g_director->getCurrentMovie(); Score *score = movie->getScore(); Archive *archive = movie->getArchive(); Cast *cast = movie->getCast(); debugPrintf("Movie path: %s\n", archive->getPathName().toString(g_director->_dirSeparator).c_str()); debugPrintf("Movie file size: %d\n", archive->getFileSize()); debugPrintf("Movie archive format: %s\n", archive->formatArchiveInfo().c_str()); debugPrintf("Movie platform: %s (%s)\n", Common::getPlatformCode(movie->_platform), Common::getPlatformDescription(movie->_platform)); debugPrintf("Movie format version: 0x%x\n", movie->_version); debugPrintf("Created by: %s\n", movie->_createdBy.c_str()); debugPrintf("Modified by: %s\n", movie->_changedBy.c_str()); debugPrintf("Original directory: %s\n", movie->_origDirectory.c_str()); debugPrintf("Stage size: %dx%d\n", movie->_movieRect.width(), movie->_movieRect.height()); debugPrintf("Default palette ID: %s\n", movie->_defaultPalette.asString().c_str()); debugPrintf("Default stage color: %d\n", cast->_stageColor); debugPrintf("Copy protected: %d\n", cast->_isProtected); debugPrintf("Remap palettes when needed flag: %d\n", movie->_remapPalettesWhenNeeded); debugPrintf("Allow outdated Lingo flag: %d\n", movie->_allowOutdatedLingo); debugPrintf("Frame count: %d\n", score->getFramesNum()); debugPrintf("Cast member count: %d\n", cast->getCastSize()); debugPrintf("Search paths:\n"); if (g_lingo->_searchPath.isArray() && g_lingo->_searchPath.u.farr->arr.size() > 0) { for (auto &it : g_lingo->_searchPath.u.farr->arr) { debugPrintf(" %s\n", it.asString().c_str()); } } else { debugPrintf(" [empty]\n"); } 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->getCurrentFrameNum()); } 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->getFramesNum(); int frameId = score->getCurrentFrameNum(); 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); Frame *frame = score->_scoreCache[frameId - 1]; if (frame) { debugPrintf("%s\n", frame->formatChannelInfo().c_str()); } else { debugPrintf(" not found\n"); } } else { debugPrintf("Must specify a frame number between 1 and %d.\n", maxSize); } return true; } bool Debugger::cmdCast(int argc, const char **argv) { Movie *movie = g_director->getCurrentMovie(); Cast *sharedCast = movie->getSharedCast(); int castId = -1; if (argc == 2) castId = atoi(argv[1]); for (auto it : *movie->getCasts()) { debugPrintf("Cast %d (%s, %s):\n", it._key, it._value->getMacName().c_str(), it._value->getCastName().c_str()); Cast *cast = it._value; if (!cast) { debugPrintf("[empty]\n"); } else if (castId > -1 && !cast->getCastMember(castId, false)) { 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, false)) { 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; setPrompt(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->getCurrentFrameNum()); debugPrintf(" %d:", csc->_id); debugPrintf("%s", csc->formatFunctionList(" ").c_str()); } else { debugPrintf("Functions attached to frame %d:\n", score->getCurrentFrameNum()); debugPrintf(" [empty]\n"); } debugPrintf("\n"); for (auto it : *movie->getCasts()) { debugPrintf("Cast %d functions:\n", it._key); Cast *cast = it._value; 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"); debugPrintf("Frame script mappings:\n"); for (int i = 0; i < (int)score->_scoreCache.size(); i++) { Frame *frame = score->_scoreCache[i]; if (frame && frame->_mainChannels.actionId.member) { debugPrintf(" %d: %s\n", i + 1, frame->_mainChannels.actionId.asString().c_str()); } } debugPrintf("Sprite script mappings:\n"); for (int i = 0; i < (int)score->_scoreCache.size(); i++) { Frame *frame = score->_scoreCache[i]; if (frame) { for (int j = 0; j < (int)frame->_sprites.size(); j++) { Sprite *sprite = frame->_sprites[j]; if (sprite->_scriptId.member) { debugPrintf(" %d, sprite %d: %s\n", i + 1, j, sprite->_scriptId.asString().c_str()); } } } } return true; } bool Debugger::cmdActions(int argc, const char **argv) { Movie *movie = g_director->getCurrentMovie(); Score *score = movie->getScore(); debugPrintf("Actions:\n"); for (auto &it : score->_actions) { debugPrintf(" %d:\n", it._key); debugPrintf("%s\n", formatStringForDump(it._value).c_str()); } debugPrintf("D3 movie script:\n"); debugPrintf("%s\n", formatStringForDump(movie->_script).c_str()); 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(); Cast *targets[2] = {movie->getCast(), movie->getSharedCast()}; const char *targetNames[2] = {"Cast", "Shared cast"}; ScriptContext *csc = lingo->_state->context; if (csc) { debugPrintf("Functions attached to frame %d:\n", score->getCurrentFrameNum()); 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->getCurrentFrameNum()); debugPrintf(" [empty]\n"); } debugPrintf("\n"); for (int t = 0; t < 2; t++) { debugPrintf("%s functions:\n", targetNames[t]); Cast *cast = targets[t]; 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()); } } } debugPrintf(" Factories:\n"); if (cast->_lingoArchive->factoryContexts.empty()) { debugPrintf(" [empty]\n"); } else { for (auto it : cast->_lingoArchive->factoryContexts) { debugPrintf(" %d:\n", it._key); if (it._value->empty()) { debugPrintf(" [empty]\n"); } else { for (auto jt : *it._value) { debugPrintf(" %s:\n", jt._key.c_str()); for (auto &kt : jt._value->_functionHandlers) { debugPrintf("%s\n", g_lingo->formatFunctionBody(kt._value).c_str()); } } } } } } else { debugPrintf(" [empty]\n"); } debugPrintf("\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 *targets[2] = {movie->getCast(), movie->getSharedCast()}; for (int i = 0; i < 2; i++) { Cast *cast = targets[i]; 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; } if (cast->_lingoArchive->factoryContexts.contains(scriptId)) { for (auto &it : *cast->_lingoArchive->factoryContexts.getVal(scriptId)) { Common::String prefix = Common::String::format("%s:", it._key.c_str()); if (funcName.hasPrefixIgnoreCase(prefix)) { Common::String handler = funcName.substr(prefix.size()); if (it._value->_functionHandlers.contains(handler)) { debugPrintf("%s\n", lingo->formatFunctionBody(it._value->_functionHandlers[handler]).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::cmdMarkers(int argc, const char **argv) { Score *score = g_director->getCurrentMovie()->getScore(); if (score->_labels && score->_labels->size()) { debugPrintf("Score markers:\n"); for (auto &it : *score->_labels) { debugPrintf("\"%s\" -> %d\n", it->name.c_str(), it->number); } } else { debugPrintf("No score markers found.\n"); } 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::cmdWindows(int argc, const char **argv) { debugPrintf("Stage:\n%s\n\n", g_director->getStage()->formatWindowInfo().c_str()); debugPrintf("Windows:\n"); for (auto &it : *g_director->getWindowList()) { debugPrintf("%s\n", it->formatWindowInfo().c_str()); } debugPrintf("\n"); return true; } bool Debugger::cmdBpSet(int argc, const char **argv) { Breakpoint bp; 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; } g_lingo->addBreakpoint(bp); bpUpdateState(); debugPrintf("Added %s\n", bp.format().c_str()); return true; } bool Debugger::cmdBpMovie(int argc, const char **argv) { if (argc == 2) { Breakpoint bp; bp.type = kBreakpointMovie; bp.moviePath = argv[1]; g_lingo->addBreakpoint(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; } g_lingo->addBreakpoint(bp); bpUpdateState(); debugPrintf("Added %s\n", bp.format().c_str()); } else { debugPrintf("Must specify a variable.\n"); } return true; } bool Debugger::cmdBpProp(int argc, const char **argv) { if (argc == 2 || argc == 3) { Breakpoint bp; bp.type = kBreakpointProperty; 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; } g_lingo->addBreakpoint(bp); bpUpdateState(); debugPrintf("Added %s\n", bp.format().c_str()); } else { debugPrintf("Must specify a property.\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; } g_lingo->addBreakpoint(bp); bpUpdateState(); debugPrintf("Added %s\n", bp.format().c_str()); } else { debugPrintf("Must specify a variable.\n"); } return true; } bool Debugger::cmdBpEvent(int argc, const char **argv) { if (argc == 2) { Breakpoint bp; bp.type = kBreakpointEvent; for (auto &it : g_lingo->_eventHandlerTypeIds) { if (it._key.equalsIgnoreCase(argv[1])) { bp.eventId = (LEvent)it._value; break; } } if (bp.eventId == kEventNone) { debugPrintf("Event %s not found.\n", argv[1]); return true; } g_lingo->addBreakpoint(bp); bpUpdateState(); debugPrintf("Added %s\n", bp.format().c_str()); } else { debugPrintf("Must specify an event name. Choices are:\n"); for (auto &it : g_lingo->_eventHandlerTypeIds) { debugPrintf("%s ", it._key.c_str()); } debugPrintf("\n"); } return true; } bool Debugger::cmdBpFrame(int argc, const char **argv) { Movie *movie = g_director->getCurrentMovie(); if (argc == 2 || argc == 3) { Breakpoint bp; 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; } g_lingo->addBreakpoint(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) { if (g_lingo->delBreakpoint(atoi(argv[1]))) { debugPrintf("Deleted breakpoint %s.\n", argv[1]); } else { debugPrintf("No breakpoint with ID %s.\n", argv[1]); } bpUpdateState(); } 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) { Breakpoint *bp = g_lingo->getBreakpoint(atoi(argv[1])); if (bp) { bp->enabled = true; bpUpdateState(); debugPrintf("Enabled breakpoint %s.\n", argv[1]); } else 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) { Breakpoint *bp = g_lingo->getBreakpoint(atoi(argv[1])); if (bp) { bp->enabled = false; bpUpdateState(); debugPrintf("Disabled breakpoint %s.\n", argv[1]); } else 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) { const Common::Array &bps = g_lingo->getBreakpoints(); if (bps.size()) { for (auto &it : bps) { debugPrintf("%s (%s)\n", it.format().c_str(), it.enabled ? "enabled" : "disabled"); } } else { debugPrintf("No breakpoints set.\n"); } return true; } bool Debugger::cmdDraw(int argc, const char **argv) { if (argc > 1) { for (int i = 1; i < argc; i++) { if (!scumm_stricmp(argv[i], "off")) { g_director->_debugDraw = 0; } else if (!strncmp(argv[i], "cast", 4)) { // allow "castS" g_director->_debugDraw |= kDebugDrawCast; } else if (!strncmp(argv[i], "frame", 5)) { // allow "frameS" g_director->_debugDraw |= kDebugDrawFrame; } else if (!scumm_stricmp(argv[i], "all")) { g_director->_debugDraw |= kDebugDrawCast | kDebugDrawFrame; } else { debugPrintf("Valid parameters are 'cast', 'frame', 'all' or 'off'.\n"); return true; } } } debugPrintf("Draw: "); if (g_director->_debugDraw & kDebugDrawCast) debugPrintf("cast "); if (g_director->_debugDraw & kDebugDrawFrame) debugPrintf("frame "); if (!g_director->_debugDraw) debugPrintf("off "); debugPrintf("\n"); return true; } static void forceWindowRedraw(Window *window) { if (!window->getCurrentMovie()) return; Score *score = window->getCurrentMovie()->getScore(); if (!score) return; for (uint16 c = 0; c < score->_channels.size(); c++) score->_channels[c]->_dirty = true; } bool Debugger::cmdForceRedraw(int argc, const char **argv) { forceWindowRedraw(g_director->getStage()); FArray *windowList = g_lingo->_windowList.u.farr; for (uint i = 0; i < windowList->arr.size(); i++) { if (windowList->arr[i].type != OBJECT || windowList->arr[i].u.obj->getObjType() != kWindowObj) continue; Window *window = static_cast(windowList->arr[i].u.obj); forceWindowRedraw(window); } debugPrintf("Requested full refresh\n"); return true; } void Debugger::bpUpdateState() { _bpCheckFunc = false; _bpCheckMoviePath = false; _bpNextMovieMatch = false; _bpMatchFuncOffsets.clear(); _bpMatchFuncName.clear(); _bpMatchScriptId = 0; _bpMatchMoviePath.clear(); _bpMatchFrameOffsets.clear(); _bpCheckPropRead = false; _bpCheckPropWrite = false; _bpCheckVarRead = false; _bpCheckVarWrite = false; _bpCheckEntityRead = false; _bpCheckEntityWrite = false; _bpCheckEvent = false; Movie *movie = g_director->getCurrentMovie(); Common::Array &callstack = g_lingo->_state->callstack; for (auto &it : g_lingo->getBreakpoints()) { 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; // check for a straight function name match bool nameTest = it.funcName.equalsIgnoreCase(*head->sp.name); if (head->sp.ctx->isFactory()) { // for factories, check for a "FactoryName:funcName" match nameTest |= it.funcName.equalsIgnoreCase(Common::String::format("%s:%s", head->sp.ctx->getName().c_str(), head->sp.name->c_str())); } if (nameTest) { 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 == kBreakpointProperty) { _bpCheckPropRead |= it.varRead; _bpCheckPropWrite |= it.varWrite; } else if (it.type == kBreakpointVariable) { _bpCheckVarRead |= it.varRead; _bpCheckVarWrite |= it.varWrite; } else if (it.type == kBreakpointEntity) { _bpCheckEntityRead |= it.varRead; _bpCheckEntityWrite |= it.varWrite; } else if (it.type == kBreakpointEvent) { _bpCheckEvent = true; } } } 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->getCurrentFrameNum(); 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 : g_lingo->getBreakpoints()) { 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; resetPrompt(); return true; } bool ret = lingoEval(inputOrig); return ret; } bool Debugger::lingoEval(const char *inputOrig) { Common::String inputSan = inputOrig; inputSan.trim(); if (inputSan.empty()) return true; // Compile the code to an anonymous function and call it ScriptContext *sc = g_lingo->_compiler->compileAnonymous(inputSan); if (!sc) { debugPrintf("Failed to parse expression!\n"); return true; } Symbol sym = sc->_eventHandlers[kEventGeneric]; _lingoEval = true; LC::call(sym, 0, true); g_lingo->execute(); debugPrintf("\n"); return true; } 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", result.asString(true).c_str()); } 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::eventHook(LEvent eventId) { if (_bpCheckEvent) { for (auto &it : g_lingo->getBreakpoints()) { if (it.type == kBreakpointEvent && eventId == it.eventId) { debugPrintf("Hit a breakpoint:\n"); debugPrintf("%s\n", it.format().c_str()); cmdScriptFrame(0, nullptr); attach(); g_system->updateScreen(); break; } } } } 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 : g_lingo->getBreakpoints()) { if (it.type != kBreakpointFunction) continue; if (it.funcName.equalsIgnoreCase(*funcSym.name)) { builtinMatch = true; break; } } } bpTest(builtinMatch); } void Debugger::propReadHook(const Common::String &name) { if (name.empty()) return; if (_bpCheckPropRead) { for (auto &it : g_lingo->getBreakpoints()) { if (it.type == kBreakpointProperty && 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::propWriteHook(const Common::String &name) { if (name.empty()) return; if (_bpCheckPropWrite) { for (auto &it : g_lingo->getBreakpoints()) { if (it.type == kBreakpointProperty && 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::varReadHook(const Common::String &name) { if (name.empty()) return; if (_bpCheckVarRead) { for (auto &it : g_lingo->getBreakpoints()) { 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 : g_lingo->getBreakpoints()) { 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 : g_lingo->getBreakpoints()) { 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 : g_lingo->getBreakpoints()) { 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