More work on the lua console

This commit is contained in:
Henrik Rydgård 2025-03-27 14:26:48 +01:00
parent 2d69d78e71
commit acad90a041
8 changed files with 207 additions and 106 deletions

View file

@ -31,7 +31,10 @@ void AttachThreadToJNI() {
if (g_attach) {
g_attach();
} else {
#if PPSSPP_PLATFORM(ANDROID)
// Not relevant on other platforms.
ERROR_LOG(Log::System, "Couldn't attach thread - g_attach not set");
#endif
}
}

View file

@ -43,13 +43,13 @@ struct DisassemblyLineInfo
u32 totalSize;
};
enum LineType { LINE_UP, LINE_DOWN, LINE_RIGHT };
enum DisasmLineType { LINE_UP, LINE_DOWN, LINE_RIGHT };
struct BranchLine
{
u32 first;
u32 second;
LineType type;
DisasmLineType type;
int laneIndex;
bool operator<(const BranchLine& other) const

View file

@ -3,22 +3,65 @@
#include "Common/Log.h"
#include "Common/StringUtils.h"
#include "Core/LuaContext.h"
std::string g_stringBuf;
#include "Core/MemMap.h"
// Sol is expensive to include so we only do it here.
#include "ext/sol/sol.hpp"
LuaContext g_lua;
static bool IsProbablyExpression(std::string_view input) {
// Heuristic: If it's a single-line statement without assignment or keywords, assume it's an expression.
return !(input.find("=") != std::string_view::npos ||
input.find("function") != std::string_view::npos ||
input.find("do") != std::string_view::npos ||
input.find("end") != std::string_view::npos ||
input.find("return") != std::string_view::npos ||
input.find("local") != std::string_view::npos);
}
// Custom print function
static void log(const std::string& message) {
static void print(const std::string& message) {
g_lua.Print(message);
}
// TODO: Should these also echo to the console?
static void debug(const std::string &message) {
DEBUG_LOG(Log::System, "%s", message.c_str());
}
static void info(const std::string &message) {
INFO_LOG(Log::System, "%s", message.c_str());
g_stringBuf = message;
}
static void warn(const std::string &message) {
WARN_LOG(Log::System, "%s", message.c_str());
}
static void error(const std::string &message) {
ERROR_LOG(Log::System, "%s", message.c_str());
}
// TODO: We should probably disallow or at least discourage raw read/writes and instead
// only support read/writes that refer to the name of a memory region.
static int r32(int address) {
if (Memory::IsValid4AlignedAddress(address)) {
return Memory::Read_U32(address);
} else {
g_lua.Print(LogLineType::Error, StringFromFormat("r32: bad address %08x", address));
return 0;
}
}
static void w32(int address, int value) {
if (Memory::IsValid4AlignedAddress(address)) {
Memory::Write_U32(value, address); // NOTE: These are backwards for historical reasons.
} else {
g_lua.Print(LogLineType::Error, StringFromFormat("w32: bad address %08x trying to write %08x", address, value));
}
}
void LuaContext::Init() {
_dbg_assert_(lua_ == nullptr);
lua_.reset(new sol::state());
lua_->open_libraries(sol::lib::base);
@ -27,25 +70,72 @@ void LuaContext::Init() {
lua_->open_libraries(sol::lib::string);
lua_->open_libraries(sol::lib::math);
// Not sure if we can safely override print(). So making a new function.
lua_->set("log", &log);
extern const char *PPSSPP_GIT_VERSION;
lua_->set("ver", PPSSPP_GIT_VERSION);
lua_->set("print", &print);
lua_->set("debug", &debug);
lua_->set("info", &info);
lua_->set("warn", &warn);
lua_->set("error", &error);
lua_->set("r32", &r32);
}
void LuaContext::Shutdown() {
lua_.reset();
}
void LuaContext::Load(const char *code) {
}
void LuaContext::Execute(std::string_view cmd, std::string *output) {
try {
lua_->script(cmd);
*output = g_stringBuf;
g_stringBuf.clear();
} catch (sol::error e) {
ERROR_LOG(Log::System, "Exception: %s", e.what());
*output = e.what();
const char *SolTypeToString(sol::type type) {
switch (type) {
case sol::type::boolean: return "boolean";
default: return "other";
}
}
void LuaContext::Print(LogLineType type, std::string_view text) {
lines_.push_back(LuaLogLine{ type, std::string(text)});
}
void LuaContext::ExecuteConsoleCommand(std::string_view cmd) {
// TODO: Also rewrite expressions like:
// print "hello"
// to
// print("hello") ?
try {
std::string command;
if (IsProbablyExpression(cmd)) {
command = "return ";
command += cmd;
} else {
command = cmd;
}
auto result = lua_->script(command);
if (result.valid()) {
for (const sol::stack_proxy &item : result) {
switch (item.get_type()) {
case sol::type::number:
{
int num = item.get<int>();
lines_.push_back(LuaLogLine{ LogLineType::Integer, StringFromFormat("%08x (%d)", num, num), item.get<int>()});
break;
}
case sol::type::string:
{
// TODO: Linebreak multi-line strings.
lines_.push_back(LuaLogLine{ LogLineType::String, item.get<std::string>() });
break;
}
default:
break;
}
}
} else {
sol::error err = result;
lines_.push_back(LuaLogLine{ LogLineType::Error, std::string(err.what()) });
}
} catch (sol::error e) {
ERROR_LOG(Log::System, "Lua exception: %s", e.what());
lines_.push_back(LuaLogLine{ LogLineType::Error, std::string(e.what()) });
}
}

View file

@ -8,20 +8,44 @@
struct lua_State;
enum class LogLineType {
Cmd,
String,
Integer,
Error,
External,
Url,
};
// A bit richer than regular log lines, so we can display them in color, and allow various UI tricks.
// All have a string, but some may also have a number or other value.
struct LuaLogLine {
LogLineType type;
std::string line;
int number;
};
class LuaContext {
public:
void Init();
void Shutdown();
void Load(const char *code);
const std::vector<LuaLogLine> GetLines() const {
return lines_;
}
void Clear() { lines_.clear(); }
void Print(LogLineType type, std::string_view text);
void Print(std::string_view text) {
Print(LogLineType::External, text);
}
// For the console.
void Execute(std::string_view cmd, std::string *output);
void ExecuteConsoleCommand(std::string_view cmd);
private:
std::unique_ptr<sol::state> lua_;
// Naming it L is a common convention.
lua_State *L = nullptr;
std::vector<LuaLogLine> lines_;
};
extern LuaContext g_lua;

View file

@ -7,10 +7,11 @@
#include "UI/ImDebugger/ImDebugger.h"
#include "UI/ImDebugger/ImConsole.h"
#include "Core/LuaContext.h"
#include "Common/StringUtils.h"
ImConsole::ImConsole() {
ClearLog();
memset(InputBuf, 0, sizeof(InputBuf));
HistoryPos = -1;
// "CLASSIFY" is here to provide the test case where "C"+[tab] completes to "CL" and display multiple matches.
@ -19,14 +20,11 @@ ImConsole::ImConsole() {
Commands.push_back("CLEAR");
AutoScroll = true;
ScrollToBottom = false;
AddLog("Welcome to Dear ImGui!");
}
ImConsole::~ImConsole() {
ClearLog();
for (int i = 0; i < History.Size; i++)
ImGui::MemFree(History[i]);
AddLog("# Enter 'HELP' for help.");
}
// Portable helpers
@ -35,23 +33,6 @@ static int Strnicmp(const char* s1, const char* s2, int n) { int d = 0; while
static char* Strdup(const char* s) { IM_ASSERT(s); size_t len = strlen(s) + 1; void* buf = ImGui::MemAlloc(len); IM_ASSERT(buf); return (char*)memcpy(buf, (const void*)s, len); }
static void Strtrim(char* s) { char* str_end = s + strlen(s); while (str_end > s && str_end[-1] == ' ') str_end--; *str_end = 0; }
void ImConsole::ClearLog() {
for (int i = 0; i < Items.Size; i++)
ImGui::MemFree(Items[i]);
Items.clear();
}
void ImConsole::AddLog(const char* fmt, ...) IM_FMTARGS(2) {
// FIXME-OPT
char buf[1024];
va_list args;
va_start(args, fmt);
vsnprintf(buf, IM_ARRAYSIZE(buf), fmt, args);
buf[IM_ARRAYSIZE(buf) - 1] = 0;
va_end(args);
Items.push_back(Strdup(buf));
}
// In C++11 you'd be better off using lambdas for this sort of forwarding callbacks
static int TextEditCallbackStub(ImGuiInputTextCallbackData* data) {
ImConsole* console = (ImConsole*)data->UserData;
@ -60,27 +41,18 @@ static int TextEditCallbackStub(ImGuiInputTextCallbackData* data) {
void ImConsole::Draw(ImConfig &cfg) {
ImGui::SetNextWindowSize(ImVec2(520, 600), ImGuiCond_FirstUseEver);
if (!ImGui::Begin("Console", &cfg.luaConsoleOpen)) {
if (!ImGui::Begin("Lua Console", &cfg.luaConsoleOpen)) {
ImGui::End();
return;
}
ImGui::TextWrapped("Lua console. Enter 'HELP' for help.");
if (ImGui::SmallButton("Add Debug Text")) {
AddLog("%d some text", Items.Size);
}
ImGui::SameLine();
if (ImGui::SmallButton("Add Debug Error")) {
AddLog("[error] something went wrong");
}
ImGui::SameLine();
if (ImGui::SmallButton("Clear")) {
ClearLog();
g_lua.Clear();
}
ImGui::SameLine();
bool copy_to_clipboard = ImGui::SmallButton("Copy");
ImGui::Separator();
// Options menu
@ -99,10 +71,10 @@ void ImConsole::Draw(ImConfig &cfg) {
// Reserve enough left-over height for 1 separator + 1 input text
const float footer_height_to_reserve = ImGui::GetStyle().ItemSpacing.y + ImGui::GetFrameHeightWithSpacing();
if (ImGui::BeginChild("ScrollingRegion", ImVec2(0, -footer_height_to_reserve), ImGuiChildFlags_None, ImGuiWindowFlags_HorizontalScrollbar | ImGuiWindowFlags_NavFlattened)) {
if (ImGui::BeginChild("ScrollingRegion", ImVec2(0, -footer_height_to_reserve), ImGuiChildFlags_None, ImGuiWindowFlags_HorizontalScrollbar | ImGuiChildFlags_NavFlattened)) {
if (ImGui::BeginPopupContextWindow()) {
if (ImGui::Selectable("Clear"))
ClearLog();
g_lua.Clear();
ImGui::EndPopup();
}
@ -133,22 +105,35 @@ void ImConsole::Draw(ImConfig &cfg) {
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(4, 1)); // Tighten spacing
if (copy_to_clipboard)
ImGui::LogToClipboard();
for (const char* item : Items) {
if (!Filter.PassFilter(item))
for (const auto &item : g_lua.GetLines()) {
if (!Filter.PassFilter(item.line.c_str()))
continue;
// Normally you would store more information in your item than just a string.
// (e.g. make Items[] an array of structure, store color/type etc.)
ImVec4 color;
bool has_color = false;
if (strstr(item, "[error]")) {
color = ImVec4(1.0f, 0.4f, 0.4f, 1.0f); has_color = true;
} else if (strncmp(item, "# ", 2) == 0) {
color = ImVec4(1.0f, 0.8f, 0.6f, 1.0f); has_color = true;
bool has_color = true;
switch (item.type) {
case LogLineType::Cmd: color = ImVec4(1.0f, 1.0f, 1.0f, 1.0f); break;
case LogLineType::Error: color = ImVec4(1.0f, 0.4f, 0.4f, 1.0f); break;
case LogLineType::External: color = ImVec4(0.8f, 0.8f, 1.0f, 1.0f); break;
case LogLineType::Integer: color = ImVec4(1.0f, 1.0f, 0.8f, 1.0f); break;
case LogLineType::String: color = ImVec4(0.8f, 1.0f, 0.8f, 1.0f); break;
default:
has_color = false;
break;
}
if (has_color)
ImGui::PushStyleColor(ImGuiCol_Text, color);
ImGui::TextUnformatted(item);
switch (item.type) {
case LogLineType::Url:
if (ImGui::TextLink(item.line.c_str())) {
System_LaunchUrl(LaunchUrlType::BROWSER_URL, item.line.c_str());
}
break;
default:
ImGui::TextUnformatted(item.line.data(), item.line.data() + item.line.size());
break;
}
if (has_color)
ImGui::PopStyleColor();
}
@ -187,8 +172,6 @@ void ImConsole::Draw(ImConfig &cfg) {
}
void ImConsole::ExecCommand(const char* command_line) {
AddLog("# %s\n", command_line);
// Insert into history. First find match and delete it so it can be pushed to the back.
// This isn't trying to be smart or optimal.
HistoryPos = -1;
@ -201,23 +184,24 @@ void ImConsole::ExecCommand(const char* command_line) {
}
History.push_back(Strdup(command_line));
g_lua.Print(LogLineType::Cmd, std::string(command_line));
// Process command
if (Stricmp(command_line, "CLEAR") == 0) {
ClearLog();
} else if (Stricmp(command_line, "HELP") == 0) {
AddLog("Commands:");
if (Stricmp(command_line, "clear") == 0) {
g_lua.Clear();
} else if (Stricmp(command_line, "help") == 0) {
g_lua.Print("Available non-Lua commands:");
for (int i = 0; i < Commands.Size; i++)
AddLog("- %s", Commands[i]);
} else if (Stricmp(command_line, "HISTORY") == 0) {
g_lua.Print(StringFromFormat("- %s", Commands[i]));
g_lua.Print("For Lua help:");
g_lua.Print(LogLineType::Url, "https://www.lua.org/manual/5.3/");
// TODO: Also print available Lua commands.
} else if (Stricmp(command_line, "history") == 0) {
int first = History.Size - 10;
for (int i = first > 0 ? first : 0; i < History.Size; i++)
AddLog("%3d: %s\n", i, History[i]);
g_lua.Print(StringFromFormat("%3d: %s", i, History[i]));
} else {
// TODO: Anything else, forward to Lua.
// AddLog("Unknown command: '%s'\n", command_line);
std::string response;
g_lua.Execute(command_line, &response);
AddLog("%s", response.c_str());
g_lua.ExecuteConsoleCommand(command_line);
}
// On command input, we scroll to bottom even if AutoScroll==false
@ -248,9 +232,11 @@ int ImConsole::TextEditCallback(ImGuiInputTextCallbackData* data) {
if (Strnicmp(Commands[i], word_start, (int)(word_end - word_start)) == 0)
candidates.push_back(Commands[i]);
// TODO: Add lua globals to candidates!
if (candidates.Size == 0) {
// No match
AddLog("No match for \"%.*s\"!\n", (int)(word_end - word_start), word_start);
// No match. TODO: Match against lua globals.
g_lua.Print(StringFromFormat("No match for \"%.*s\"!", (int)(word_end - word_start), word_start));
} else if (candidates.Size == 1) {
// Single match. Delete the beginning of the word and replace it entirely so we've got nice casing.
data->DeleteChars((int)(word_start - data->Buf), (int)(word_end - word_start));
@ -279,9 +265,9 @@ int ImConsole::TextEditCallback(ImGuiInputTextCallbackData* data) {
}
// List matches
AddLog("Possible matches:\n");
g_lua.Print("Possible matches:");
for (int i = 0; i < candidates.Size; i++) {
AddLog("- %s\n", candidates[i]);
g_lua.Print(StringFromFormat("- %s", candidates[i]));
}
}

View file

@ -6,25 +6,22 @@
#include "ext/imgui/imgui.h"
// Adapted from the ImGui demo.
struct ImConsole {
class ImConsole {
public:
ImConsole();
~ImConsole();
void Draw(ImConfig &cfg);
void ExecCommand(const char* command_line);
int TextEditCallback(ImGuiInputTextCallbackData* data);
private:
char InputBuf[256];
ImVector<char*> Items;
ImVector<const char*> Commands;
ImVector<char*> History;
int HistoryPos; // -1: new line, 0..History.Size-1 browsing history.
ImGuiTextFilter Filter;
bool AutoScroll;
bool ScrollToBottom;
ImConsole();
~ImConsole();
void ClearLog();
void AddLog(const char* fmt, ...) IM_FMTARGS(2);
void Draw(ImConfig &cfg);
void ExecCommand(const char* command_line);
int TextEditCallback(ImGuiInputTextCallbackData* data);
};

View file

@ -1757,6 +1757,7 @@ void ImDebugger::Frame(MIPSDebugInterface *mipsDebug, GPUDebugInterface *gpuDebu
ImGui::EndMenu();
}
if (ImGui::BeginMenu("Tools")) {
ImGui::MenuItem("Lua Console", nullptr, &cfg_.luaConsoleOpen);
ImGui::MenuItem("Debug stats", nullptr, &cfg_.debugStatsOpen);
ImGui::MenuItem("Struct viewer", nullptr, &cfg_.structViewerOpen);
ImGui::MenuItem("Log channels", nullptr, &cfg_.logConfigOpen);
@ -2320,6 +2321,7 @@ void ImConfig::SyncConfig(IniFile *ini, bool save) {
sync.Sync("internalsOpen", &internalsOpen, false);
sync.Sync("sasAudioOpen", &sasAudioOpen, false);
sync.Sync("logConfigOpen", &logConfigOpen, false);
sync.Sync("luaConsoleOpen", &luaConsoleOpen, false);
sync.Sync("utilityModulesOpen", &utilityModulesOpen, false);
for (int i = 0; i < 4; i++) {
char name[64];

View file

@ -40,7 +40,6 @@
#include "Common/File/FileUtil.h"
#include "Common/TimeUtil.h"
#include "Common/StringUtils.h"
#include "Common/System/System.h"
#include "Common/System/OSD.h"
#include "Core/System.h"
#include "Core/Util/RecentFiles.h"