mirror of
https://github.com/hrydgard/ppsspp.git
synced 2026-07-11 01:25:07 +02:00
568 lines
16 KiB
C++
568 lines
16 KiB
C++
#include <string>
|
|
#include <cstdarg>
|
|
|
|
#include "Common/Log.h"
|
|
#include "Common/StringUtils.h"
|
|
#include "Core/LuaContext.h"
|
|
#include "Core/MemMap.h"
|
|
#include "Core/System.h"
|
|
#include "Core/MIPS/MIPS.h"
|
|
#include "Core/MIPS/MIPSDebugInterface.h"
|
|
#include "Core/MIPS/MIPSAsm.h"
|
|
#include "Core/ELF/ParamSFO.h"
|
|
#include "Core/RetroAchievements.h"
|
|
#include "Common/System/System.h"
|
|
#include "Common/System/Request.h"
|
|
#include "Common/System/OSD.h"
|
|
|
|
// Sol is expensive to include so we only do it here.
|
|
#include "ext/sol/sol.hpp"
|
|
|
|
LuaInteractiveContext 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);
|
|
}
|
|
|
|
// TODO: Should these also echo to the console?
|
|
static void debug(const char *message) {
|
|
DEBUG_LOG(Log::Script, "%s", message);
|
|
}
|
|
|
|
static void info(const char *message) {
|
|
INFO_LOG(Log::Script, "%s", message);
|
|
}
|
|
|
|
static void warn(const char *message) {
|
|
WARN_LOG(Log::Script, "%s", message);
|
|
}
|
|
|
|
static void error(const char *message) {
|
|
ERROR_LOG(Log::Script, "%s", message);
|
|
}
|
|
|
|
static int gpr(int reg) {
|
|
return currentDebugMIPS ? currentDebugMIPS->GetGPR32Value(reg) : 0;
|
|
}
|
|
|
|
static void set_gpr(int reg, int value) {
|
|
if (currentDebugMIPS)
|
|
currentDebugMIPS->SetGPR32Value(reg, value);
|
|
}
|
|
|
|
static double bitcast_s32_to_float(int value) {
|
|
float fvalue;
|
|
memcpy(&fvalue, &value, 4);
|
|
return fvalue;
|
|
}
|
|
|
|
static int bitcast_float_to_s32(double value) {
|
|
float fvalue = (float)value;
|
|
int ivalue;
|
|
memcpy(&ivalue, &value, 4);
|
|
return ivalue;
|
|
}
|
|
|
|
static void stop() {
|
|
System_PostUIMessage(UIMessage::REQUEST_GAME_STOP);
|
|
}
|
|
|
|
static void reset() {
|
|
System_PostUIMessage(UIMessage::REQUEST_GAME_RESET);
|
|
}
|
|
|
|
sol::table get_strings(sol::this_state ts) {
|
|
sol::state_view lua(ts);
|
|
sol::table t = lua.create_table();
|
|
t[1] = "apple";
|
|
t[2] = "banana";
|
|
t[3] = "cherry";
|
|
return t;
|
|
}
|
|
|
|
// Not yet working
|
|
void sys_call(sol::variadic_args va) {
|
|
// First two arguments will be the name of the module and function, then the rest of the arguments will
|
|
// be passed in through MIPS GPR registers.
|
|
// This will be tricky for string args, we'll need to put them in some unallocated kernel space I guess.
|
|
// Also we'll probably need to create specialized wrappers for calls that take structs like sceCtrlReadPositive etc.
|
|
|
|
/*
|
|
for (auto v : va) {
|
|
if (v.is<int>()) {
|
|
int i = v.as<int>();
|
|
} else if (v.is<std::string>()) {
|
|
std::string s = v.as<std::string>();
|
|
}
|
|
}
|
|
*/
|
|
}
|
|
|
|
void LuaContext::SetupContext(sol::state &lua) {
|
|
// Add stuff we want to add.
|
|
lua.open_libraries(sol::lib::base);
|
|
lua.open_libraries(sol::lib::table);
|
|
lua.open_libraries(sol::lib::bit32);
|
|
lua.open_libraries(sol::lib::string);
|
|
lua.open_libraries(sol::lib::math);
|
|
|
|
// Remove some stuff we don't want to expose.
|
|
// TODO: The sandbox environmeent method would be better.
|
|
sol::table globals = lua.globals();
|
|
globals["dofile"] = sol::lua_nil;
|
|
globals["load"] = sol::lua_nil;
|
|
globals["loadstring"] = sol::lua_nil;
|
|
globals["loadfile"] = sol::lua_nil;
|
|
globals["collectgarbage"] = sol::lua_nil;
|
|
globals["rawequal"] = sol::lua_nil;
|
|
globals["rawset"] = sol::lua_nil;
|
|
globals["rawget"] = sol::lua_nil;
|
|
globals["setmetatable"] = sol::lua_nil;
|
|
globals["debug"] = sol::lua_nil;
|
|
globals["require"] = sol::lua_nil;
|
|
|
|
extern const char *PPSSPP_GIT_VERSION;
|
|
lua.set("ver", PPSSPP_GIT_VERSION);
|
|
|
|
lua.set_function("print", [this](const std::string& message) {
|
|
Print(LogLineType::String, message);
|
|
});
|
|
lua.set_function("debug", &debug);
|
|
lua.set_function("info", &info);
|
|
lua.set_function("warn", &warn);
|
|
lua.set_function("error", &error);
|
|
|
|
// lua.set("module_list", &module_list);
|
|
|
|
// Memory accessors
|
|
|
|
// 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.
|
|
|
|
lua.set_function("r32", [this](int address) -> int {
|
|
if (!Memory::IsValid4AlignedAddress(address)) {
|
|
PrintF(LogLineType::Error, "r32: bad address %08x", address);
|
|
return 0;
|
|
}
|
|
return Memory::ReadUnchecked_U32(address);
|
|
});
|
|
|
|
lua.set_function("w32", [this](int address, int value) -> void {
|
|
if (!Memory::IsValid4AlignedAddress(address)) {
|
|
PrintF(LogLineType::Error, "w32: bad address %08x trying to write %08x", address, value);
|
|
}
|
|
Memory::WriteUnchecked_U32(value, address); // NOTE: These are backwards for historical reasons.
|
|
});
|
|
|
|
lua.set_function("r16", [this](int address) -> int {
|
|
if (!Memory::IsValid2AlignedAddress(address)) {
|
|
PrintF(LogLineType::Error, "r16: bad address %08x", address);
|
|
return 0;
|
|
}
|
|
return Memory::ReadUnchecked_U16(address);
|
|
});
|
|
|
|
lua.set_function("rs16", [this](int address) -> int {
|
|
if (!Memory::IsValid2AlignedAddress(address)) {
|
|
PrintF(LogLineType::Error, "rs16: bad address %08x", address);
|
|
return 0;
|
|
}
|
|
|
|
return (s16)Memory::ReadUnchecked_U16(address);
|
|
});
|
|
|
|
lua.set_function("w16", [this](int address, int value) -> void {
|
|
if (!Memory::IsValid2AlignedAddress(address)) {
|
|
PrintF(LogLineType::Error, "w16: bad address %08x trying to write %04x", address, value & 0xFFFF);
|
|
}
|
|
|
|
Memory::WriteUnchecked_U16(value, address); // NOTE: These are backwards for historical reasons.
|
|
});
|
|
|
|
lua.set_function("r8", [this](int address) -> int {
|
|
if (!Memory::IsValidAddress(address)) {
|
|
PrintF(LogLineType::Error, "r8: bad address %08x", address);
|
|
return 0;
|
|
}
|
|
return Memory::ReadUnchecked_U8(address);
|
|
});
|
|
|
|
lua.set_function("rs8", [this](int address) -> int {
|
|
if (!Memory::IsValidAddress(address)) {
|
|
PrintF(LogLineType::Error, "rs8: bad address %08x", address);
|
|
return 0;
|
|
}
|
|
|
|
return (s16)Memory::ReadUnchecked_U8(address);
|
|
});
|
|
|
|
lua.set_function("w8", [this](int address, int value) -> void {
|
|
if (!Memory::IsValidAddress(address)) {
|
|
PrintF(LogLineType::Error, "w8: bad address %08x trying to write %02x", address, value & 0xFF);
|
|
}
|
|
|
|
Memory::WriteUnchecked_U8(value, address); // NOTE: These are backwards for historical reasons.
|
|
});
|
|
|
|
// 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.
|
|
lua.set_function("rf", [this](int address) -> double {
|
|
if (!Memory::IsValid4AlignedAddress(address)) {
|
|
PrintF(LogLineType::Error, "rf: bad address %08x", address);
|
|
return 0;
|
|
}
|
|
|
|
return (double)Memory::ReadUnchecked_Float(address);
|
|
});
|
|
|
|
lua.set_function("wf", [this](int address, double value) -> void {
|
|
float fvalue = (float)value;
|
|
if (!Memory::IsValid4AlignedAddress(address)) {
|
|
PrintF(LogLineType::Error, "wf: bad address %08x trying to write float %f", address, fvalue);
|
|
}
|
|
|
|
Memory::WriteUnchecked_Float((float)value, address); // NOTE: These are backwards for historical reasons.
|
|
});
|
|
|
|
// Memory scans
|
|
lua.set_function("scan32", [this](int address, int size, int value) -> int {
|
|
if (Memory::IsValidRange(address, size)) {
|
|
Print(LogLineType::Error, "bad range");
|
|
return 0;
|
|
}
|
|
|
|
for (int i = 0; i < size; i += 4) {
|
|
if (Memory::ReadUnchecked_U32(address + i) == value) {
|
|
return address + i;
|
|
}
|
|
}
|
|
return 0;
|
|
});
|
|
|
|
// Register accessors
|
|
lua.set_function("gpr", &gpr);
|
|
lua.set_function("set_gpr", &set_gpr);
|
|
|
|
// Data conversion utilities, bitcasts
|
|
lua.set_function("bitcast_s32_to_float", &bitcast_s32_to_float);
|
|
lua.set_function("bitcast_float_to_s32", &bitcast_float_to_s32);
|
|
|
|
// MIPS instruction utilities
|
|
lua.set_function("asm", [this](int destAddress, const char *code) -> void {
|
|
if (!Memory::IsValid4AlignedAddress(destAddress)) {
|
|
PrintF(LogLineType::Error, "assemble: bad address %08x", destAddress);
|
|
return;
|
|
}
|
|
// TODO: Better error handling.
|
|
std::string error;
|
|
if (!MipsAssembleOpcode(code, currentDebugMIPS, (u32)destAddress, &error)) {
|
|
PrintF(LogLineType::Error, "Failed to assemble '%s': %s", code, error.c_str());
|
|
}
|
|
});
|
|
|
|
// Test functions
|
|
lua.set_function("get_strings", &get_strings);
|
|
|
|
// not yet working
|
|
lua.set_function("sys_call", &sys_call);
|
|
|
|
// Memory tools
|
|
sol::table mem = lua.create_table();
|
|
lua["mem"] = mem;
|
|
|
|
// After modifying code, this needs to be used.
|
|
mem.set_function("invalidate_cache", [this](int start, int size) -> void {
|
|
if (!Memory::IsValidRange(start, size)) {
|
|
PrintF(LogLineType::Error, "invalidate_cache: bad range %08x + %08x", start, size);
|
|
return;
|
|
}
|
|
|
|
u32 aligned = start & ~3;
|
|
int alignedSize = (start + size - aligned + 3) & ~3;
|
|
if (currentMIPS) {
|
|
currentMIPS->InvalidateICache(aligned, alignedSize);
|
|
}
|
|
});
|
|
|
|
mem.set_function("copy", [this](int dest, int src, int size) -> void {
|
|
if (!Memory::IsValidRange(dest, size) || !Memory::IsValidRange(src, size)) {
|
|
PrintF(LogLineType::Error, "memcpy: bad range dest %08x + %08x or src %08x + %08x", dest, size, src, size);
|
|
return;
|
|
}
|
|
Memory::MemcpyUnchecked(dest, src, size);
|
|
});
|
|
|
|
mem.set_function("set", [this](int dest, int byte, int size) -> void {
|
|
if (!Memory::IsValidRange(dest, size)) {
|
|
PrintF(LogLineType::Error, "memset(%d): bad range dest %08x + %08x", (u8)byte, dest, size);
|
|
return;
|
|
}
|
|
Memory::MemsetUnchecked(dest, byte, size);
|
|
});
|
|
mem.set_function("is_valid", [](int address) -> bool {
|
|
return Memory::IsValidAddress(address);
|
|
});
|
|
|
|
// Missing functions after studying Thirteen's plugins
|
|
// sceIoDevctl("kemulator:", EMULATOR_DEVCTL__TOGGLE_FASTFORWARD, (void*)1, 0, NULL, 0);
|
|
// Easy-to-use scan functions for hex patterns
|
|
|
|
// Functionality required to replace cwcheats
|
|
// https://datacrystal.tcrf.net/wiki/CwCheat
|
|
// https://github.com/raing3/psp-cheat-documentation/blob/master/cheat-devices/cwcheat.md
|
|
// Multi-write (though can easily be replaced with loops, but cleaner to have a specific way)
|
|
// Pointer commands and boolean commands are easily replaced with simple logic and r32/w32 etc.
|
|
// Conditional and Multiple Skip types can be replaced with simple if statements and loops.
|
|
// Button-peek functions are needed.
|
|
// We don't properly support the "Pause" code type, I don't think?
|
|
// Icache invalidation will not be automatic, unlike in cwcheats.
|
|
|
|
// Game information constants.
|
|
lua["game"] = lua.create_table_with(
|
|
"ID", g_paramSFO.GetDiscID(),
|
|
"TITLE", g_paramSFO.GetValueString("TITLE")
|
|
);
|
|
|
|
// Emulator control
|
|
//
|
|
// TODO: Add a numeric version number that's easy to check for.
|
|
sol::table emu = lua.create_table_with(
|
|
"VERSION", PPSSPP_GIT_VERSION,
|
|
"stop", &stop,
|
|
"reset", &reset
|
|
);
|
|
lua["emu"] = emu;
|
|
// UI interactions
|
|
emu.set_function("notify", [](const char *str) {
|
|
g_OSD.Show(OSDType::MESSAGE_INFO, str, 0.0f, nullptr);
|
|
});
|
|
emu.set_function("warn", [](const char *str) {
|
|
g_OSD.Show(OSDType::MESSAGE_WARNING, str, 0.0f, nullptr);
|
|
});
|
|
|
|
lua["btn"] = lua.create_table_with(
|
|
"SELECT", 0x00000001,
|
|
"START", 0x00000008,
|
|
"UP", 0x00000010,
|
|
"RIGHT", 0x00000020,
|
|
"DOWN", 0x00000040,
|
|
"LEFT", 0x00000080,
|
|
"LTRIGGER", 0x00000100,
|
|
"RTRIGGER", 0x00000200,
|
|
"TRIANGLE", 0x00001000,
|
|
"CIRCLE", 0x00002000,
|
|
"CROSS", 0x00004000,
|
|
"SQUARE", 0x00008000,
|
|
"HOME", 0x00010000,
|
|
"HOLD", 0x00020000,
|
|
"WLAN_UP", 0x00040000,
|
|
"REMOTE", 0x00080000,
|
|
"VOLUP", 0x00100000,
|
|
"VOLDOWN", 0x00200000,
|
|
"SCREEN", 0x00400000,
|
|
"NOTE", 0x00800000,
|
|
"DISC", 0x01000000,
|
|
"MS", 0x02000000
|
|
);
|
|
|
|
// Load a function to dump values to console in a readable way.
|
|
lua.script(R"(
|
|
function dump(value, indent, visited)
|
|
indent = indent or ""
|
|
visited = visited or {}
|
|
|
|
local t = type(value)
|
|
|
|
if t == "table" then
|
|
if visited[value] then
|
|
print(indent .. "{ -- already seen }")
|
|
return
|
|
end
|
|
visited[value] = true
|
|
|
|
print(indent .. "{")
|
|
local nextIndent = indent .. " "
|
|
for k, v in pairs(value) do
|
|
if type(v) == "table" then
|
|
print(nextIndent .. tostring(k) .. " =")
|
|
dump(v, nextIndent, visited)
|
|
else
|
|
print(nextIndent .. tostring(k) .. " = " .. tostring(v))
|
|
end
|
|
end
|
|
print(indent .. "}")
|
|
|
|
else
|
|
print(indent .. tostring(value))
|
|
end
|
|
end
|
|
)");
|
|
}
|
|
|
|
void LuaContext::RunCode(const std::string &code) {
|
|
if (!lua_) {
|
|
Print(LogLineType::Error, "Lua context not initialized.");
|
|
return;
|
|
}
|
|
try {
|
|
auto result = lua_->script(code);
|
|
if (!result.valid()) {
|
|
sol::error err = result;
|
|
Print(LogLineType::Error, err.what());
|
|
}
|
|
} catch (const sol::error& e) {
|
|
ERROR_LOG(Log::Script, "Lua exception: %s", e.what());
|
|
Print(LogLineType::Error, e.what());
|
|
}
|
|
}
|
|
|
|
void LuaContext::Init() {
|
|
_dbg_assert_(lua_ == nullptr);
|
|
lua_.reset(new sol::state());
|
|
|
|
SetupContext(*lua_);
|
|
}
|
|
|
|
void LuaContext::Shutdown() {
|
|
lua_.reset();
|
|
}
|
|
|
|
static const char *SolTypeToString(sol::type type) {
|
|
switch (type) {
|
|
case sol::type::none: return "none";
|
|
case sol::type::lua_nil: return "nil";
|
|
case sol::type::string: return "string";
|
|
case sol::type::number: return "number";
|
|
case sol::type::thread: return "thread";
|
|
case sol::type::boolean: return "boolean";
|
|
case sol::type::function: return "function";
|
|
case sol::type::userdata: return "userdata";
|
|
case sol::type::lightuserdata: return "lightuserdata";
|
|
case sol::type::table: return "table";
|
|
case sol::type::poly: return "poly"; // special variant type
|
|
default: return "other";
|
|
}
|
|
}
|
|
|
|
void LuaContext::PrintF(LogLineType type, const char* fmt, ...) {
|
|
va_list args;
|
|
va_start(args, fmt);
|
|
|
|
char buffer[1024];
|
|
vsnprintf(buffer, sizeof(buffer), fmt, args);
|
|
|
|
va_end(args);
|
|
|
|
Print(type, std::string_view(buffer));
|
|
}
|
|
|
|
void LuaContext::Print(LogLineType type, std::string_view text) {
|
|
INFO_LOG(Log::Script, "%.*s", (int)text.size(), text.data());
|
|
}
|
|
|
|
void LuaInteractiveContext::Print(LogLineType type, std::string_view text) {
|
|
lines_.push_back(LuaLogLine{ type, std::string(text)});
|
|
}
|
|
|
|
std::vector<std::string> LuaInteractiveContext::AutoComplete(std::string_view cmd) const {
|
|
auto globals = GetGlobals();
|
|
std::vector<std::string> candidates;
|
|
for (auto &g : globals) {
|
|
if (startsWithNoCase(g, cmd)) {
|
|
candidates.push_back(g);
|
|
}
|
|
}
|
|
return candidates;
|
|
}
|
|
|
|
std::vector<std::string> LuaInteractiveContext::GetGlobals() const {
|
|
std::vector<std::string> globalStrings;
|
|
sol::table globals = lua_->globals();
|
|
|
|
for (auto& kv : globals) {
|
|
sol::object key = kv.first;
|
|
sol::object value = kv.second;
|
|
|
|
if (value.is<sol::function>()) {
|
|
globalStrings.push_back(key.as<std::string>());
|
|
}
|
|
}
|
|
return globalStrings;
|
|
}
|
|
|
|
void LuaInteractiveContext::ExecuteConsoleCommand(std::string_view cmd) {
|
|
if (Achievements::HardcoreModeActive()) {
|
|
Print(LogLineType::Error, "RetroAchievemnts hardcore mode is active, lua console disabled.");
|
|
return;
|
|
}
|
|
|
|
// 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:
|
|
{
|
|
double num = item.get<double>();
|
|
// Check if it's an integer value
|
|
if (std::floor(num) == num && num >= static_cast<double>(std::numeric_limits<int>::min()) &&
|
|
num <= static_cast<double>(std::numeric_limits<int>::max())) {
|
|
|
|
int int_val = static_cast<int>(num);
|
|
lines_.push_back(LuaLogLine{
|
|
LogLineType::Integer,
|
|
StringFromFormat("%08x (%d)", int_val, int_val),
|
|
int_val
|
|
});
|
|
} else {
|
|
lines_.push_back(LuaLogLine{
|
|
LogLineType::Float,
|
|
StringFromFormat("%f", num),
|
|
0,
|
|
num
|
|
});
|
|
}
|
|
break;
|
|
}
|
|
case sol::type::string:
|
|
{
|
|
// TODO: Linebreak multi-line strings.
|
|
lines_.push_back(LuaLogLine{ LogLineType::String, item.get<std::string>() });
|
|
break;
|
|
}
|
|
default:
|
|
{
|
|
// More complex object, let's call our lua function 'dump'.
|
|
lua_->script("return dump(" + std::string(cmd) + ")");
|
|
// lines_.push_back(LuaLogLine{LogLineType::String, "(other result)"});
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
sol::error err = result;
|
|
lines_.push_back(LuaLogLine{ LogLineType::Error, std::string(err.what()) });
|
|
}
|
|
} catch (const sol::error& e) {
|
|
ERROR_LOG(Log::Script, "Lua exception: %s", e.what());
|
|
lines_.push_back(LuaLogLine{ LogLineType::Error, std::string(e.what()) });
|
|
}
|
|
}
|