mirror of
https://github.com/libretro/MesenCE.git
synced 2026-11-09 17:27:06 -10:00
Also fixed a few scenarios where the PPU memory write callbacks did not allow the Lua script to change the value about to be written (because the callback was called too late)
404 lines
12 KiB
C++
404 lines
12 KiB
C++
#include "pch.h"
|
|
#include <algorithm>
|
|
#include <regex>
|
|
#include "Lua/lua.hpp"
|
|
#include "Lua/luasocket.hpp"
|
|
#include "Debugger/ScriptingContext.h"
|
|
#include "Debugger/LuaApi.h"
|
|
#include "Debugger/LuaCallHelper.h"
|
|
#include "Debugger/DebugTypes.h"
|
|
#include "Debugger/Debugger.h"
|
|
#include "Debugger/ScriptManager.h"
|
|
#include "Shared/Emulator.h"
|
|
#include "Shared/EmuSettings.h"
|
|
#include "Shared/EventType.h"
|
|
#include "Utilities/magic_enum.hpp"
|
|
#include "Utilities/StringUtilities.h"
|
|
|
|
ScriptingContext* ScriptingContext::_context = nullptr;
|
|
|
|
ScriptingContext::ScriptingContext(Debugger* debugger)
|
|
{
|
|
_debugger = debugger;
|
|
_settings = debugger->GetEmulator()->GetSettings();
|
|
_defaultCpuType = debugger->GetEmulator()->GetCpuTypes()[0];
|
|
_defaultMemType = DebugUtilities::GetCpuMemoryType(_defaultCpuType);
|
|
}
|
|
|
|
ScriptingContext::~ScriptingContext()
|
|
{
|
|
if(_lua) {
|
|
//Cleanup all references, this is required to prevent crashes that can occur when calling lua_close
|
|
std::unordered_set<int> references;
|
|
for(int i = (int)CallbackType::Read; i <= (int)CallbackType::Exec; i++) {
|
|
for(MemoryCallback& callback : _callbacks[i]) {
|
|
references.emplace(callback.Reference);
|
|
}
|
|
}
|
|
|
|
for(auto& entry : magic_enum::enum_entries<EventType>()) {
|
|
for(int& ref : _eventCallbacks[(int)entry.first]) {
|
|
references.emplace(ref);
|
|
}
|
|
}
|
|
|
|
for(const int& ref : references) {
|
|
luaL_unref(_lua, LUA_REGISTRYINDEX, ref);
|
|
}
|
|
|
|
lua_close(_lua);
|
|
_lua = nullptr;
|
|
}
|
|
}
|
|
|
|
bool ScriptingContext::LoadScript(string scriptName, string path, string scriptContent, Debugger* debugger)
|
|
{
|
|
_scriptName = scriptName;
|
|
|
|
int iErr = 0;
|
|
_lua = luaL_newstate();
|
|
|
|
_context = this;
|
|
LuaApi::SetContext(this);
|
|
|
|
EmuSettings* settings = debugger->GetEmulator()->GetSettings();
|
|
bool allowIoOsAccess = settings->GetDebugConfig().ScriptAllowIoOsAccess;
|
|
LuaOpenLibs(_lua, allowIoOsAccess);
|
|
|
|
//Prevent lua code from loading any files
|
|
SANDBOX_ALLOW_LOADFILE = allowIoOsAccess ? 1 : 0;
|
|
|
|
//Load LuaSocket into Lua core
|
|
if(allowIoOsAccess && settings->GetDebugConfig().ScriptAllowNetworkAccess) {
|
|
lua_getglobal(_lua, "package");
|
|
lua_getfield(_lua, -1, "preload");
|
|
lua_pushcfunction(_lua, luaopen_socket_core);
|
|
lua_setfield(_lua, -2, "socket.core");
|
|
lua_pushcfunction(_lua, luaopen_mime_core);
|
|
lua_setfield(_lua, -2, "mime.core");
|
|
lua_pop(_lua, 2);
|
|
}
|
|
|
|
if(allowIoOsAccess) {
|
|
//Escape backslashes
|
|
std::regex r("\\\\");
|
|
path = std::regex_replace(path, r, "\\\\");
|
|
|
|
//Add path for the current Lua script to package.path to allow
|
|
//using require() without specifying an absolute path, etc.
|
|
string cmd = "package.path = package.path .. ';" + path + "?.lua'";
|
|
luaL_dostring(_lua, cmd.c_str());
|
|
}
|
|
|
|
luaL_requiref(_lua, "emu", LuaApi::GetLibrary, 1);
|
|
Log("Loading script...");
|
|
if((iErr = luaL_loadbufferx(_lua, scriptContent.c_str(), scriptContent.size(), ("@" + scriptName).c_str(), nullptr)) == 0) {
|
|
_timer.Reset();
|
|
lua_setwatchdogtimer(_lua, ScriptingContext::ExecutionCountHook, 1000);
|
|
if((iErr = lua_pcall(_lua, 0, LUA_MULTRET, 0)) == 0) {
|
|
//Script loaded properly
|
|
Log("Script loaded successfully.");
|
|
_initDone = true;
|
|
return true;
|
|
}
|
|
}
|
|
|
|
ProcessLuaError();
|
|
|
|
return false;
|
|
}
|
|
|
|
string ScriptingContext::GetErrorMessage()
|
|
{
|
|
string errorMsg = "";
|
|
if(lua_type(_lua, -1) == LUA_TSTRING) {
|
|
errorMsg = lua_tostring(_lua, -1);
|
|
} else if(lua_istable(_lua, -1)) {
|
|
//Serialize tables to Lua-formatted string
|
|
errorMsg = "error: " + LuaApi::SerializeTable(_lua);
|
|
} else {
|
|
//Convert all other values to a string
|
|
luaL_tolstring(_lua, -1, nullptr);
|
|
errorMsg = string("error: ") + lua_tostring(_lua, -1);
|
|
lua_pop(_lua, 1);
|
|
}
|
|
return errorMsg;
|
|
}
|
|
|
|
void ScriptingContext::ProcessLuaError()
|
|
{
|
|
string errorMsg = GetErrorMessage();
|
|
if(StringUtilities::Contains(errorMsg, "attempt to call a nil value (global 'require')") || StringUtilities::Contains(errorMsg, "attempt to index a nil value (global 'os')") || StringUtilities::Contains(errorMsg, "attempt to index a nil value (global 'io')")) {
|
|
Log("I/O and OS libraries are disabled by default for security.\nYou can enable them here:\nScript->Settings->Script Window->Restrictions->Allow access to I/O and OS functions.");
|
|
} else if(StringUtilities::Contains(errorMsg, "module 'socket.core' not found")) {
|
|
Log("Lua sockets are disabled by default for security.\nYou can enable them here:\nScript->Settings->Script Window->Restrictions->Allow network access.");
|
|
} else {
|
|
Log(errorMsg);
|
|
}
|
|
lua_pop(_lua, 1);
|
|
}
|
|
|
|
void ScriptingContext::ExecutionCountHook(lua_State* lua)
|
|
{
|
|
uint32_t timeout = _context->_settings->GetDebugConfig().ScriptTimeout;
|
|
if(_context->_timer.GetElapsedMS() > timeout * 1000) {
|
|
luaL_error(lua, (std::string("Maximum execution time (") + std::to_string(timeout) + " seconds) exceeded.").c_str());
|
|
}
|
|
lua_setwatchdogtimer(lua, ScriptingContext::ExecutionCountHook, 1000);
|
|
}
|
|
|
|
void ScriptingContext::LuaOpenLibs(lua_State* L, bool allowIoOsAccess)
|
|
{
|
|
constexpr luaL_Reg loadedlibs[] = {
|
|
{ "_G", luaopen_base },
|
|
{ LUA_LOADLIBNAME, luaopen_package },
|
|
{ LUA_COLIBNAME, luaopen_coroutine },
|
|
{ LUA_TABLIBNAME, luaopen_table },
|
|
{ LUA_IOLIBNAME, luaopen_io },
|
|
{ LUA_OSLIBNAME, luaopen_os },
|
|
{ LUA_STRLIBNAME, luaopen_string },
|
|
{ LUA_MATHLIBNAME, luaopen_math },
|
|
{ LUA_UTF8LIBNAME, luaopen_utf8 },
|
|
{ LUA_DBLIBNAME, luaopen_debug },
|
|
{ NULL, NULL }
|
|
};
|
|
|
|
const luaL_Reg* lib;
|
|
/* "require" functions from 'loadedlibs' and set results to global table */
|
|
for(lib = loadedlibs; lib->func; lib++) {
|
|
if(!allowIoOsAccess) {
|
|
//Skip loading IO, OS and Package lib when sandboxed
|
|
if(strcmp(lib->name, LUA_IOLIBNAME) == 0 || strcmp(lib->name, LUA_OSLIBNAME) == 0 || strcmp(lib->name, LUA_LOADLIBNAME) == 0) {
|
|
continue;
|
|
}
|
|
}
|
|
luaL_requiref(L, lib->name, lib->func, 1);
|
|
lua_pop(L, 1); /* remove lib */
|
|
}
|
|
}
|
|
|
|
void ScriptingContext::Log(string message)
|
|
{
|
|
auto lock = _logLock.AcquireSafe();
|
|
size_t start = 0;
|
|
|
|
if(message.size() <= 200) {
|
|
_logRows.push_back(message);
|
|
if(_logRows.size() > 500) {
|
|
_logRows.pop_front();
|
|
}
|
|
} else {
|
|
//Split large strings into several separate lines
|
|
//This is needed to prevent performance issues in the UI
|
|
//when a single line is several thousand characters long.
|
|
while(start < message.size()) {
|
|
size_t pos = message.find_first_of(',', start + 200);
|
|
|
|
if(pos == string::npos) {
|
|
pos = message.find_first_of(' ', start + 200);
|
|
if(pos == string::npos) {
|
|
pos = std::min<size_t>(message.size() - 1, start + 300);
|
|
}
|
|
}
|
|
|
|
_logRows.push_back(message.substr(start, pos - start + 1));
|
|
start = pos + 1;
|
|
|
|
if(_logRows.size() > 500) {
|
|
_logRows.pop_front();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
string ScriptingContext::GetLog()
|
|
{
|
|
auto lock = _logLock.AcquireSafe();
|
|
stringstream ss;
|
|
for(string& msg : _logRows) {
|
|
ss << msg << "\n";
|
|
}
|
|
return ss.str();
|
|
}
|
|
|
|
Debugger* ScriptingContext::GetDebugger()
|
|
{
|
|
return _debugger;
|
|
}
|
|
|
|
string ScriptingContext::GetScriptName()
|
|
{
|
|
return _scriptName;
|
|
}
|
|
|
|
template<typename T>
|
|
void ScriptingContext::CallMemoryCallback(AddressInfo relAddr, T& value, CallbackType type, CpuType cpuType)
|
|
{
|
|
_allowSaveState = type == CallbackType::Exec && cpuType == _defaultCpuType;
|
|
InternalCallMemoryCallback(relAddr, value, type, cpuType);
|
|
_allowSaveState = false;
|
|
}
|
|
|
|
bool ScriptingContext::CheckInitDone()
|
|
{
|
|
return _initDone;
|
|
}
|
|
|
|
bool ScriptingContext::IsSaveStateAllowed()
|
|
{
|
|
return _allowSaveState;
|
|
}
|
|
|
|
void ScriptingContext::RegisterMemoryCallback(CallbackType type, int startAddr, int endAddr, MemoryType memType, CpuType cpuType, int reference)
|
|
{
|
|
if(endAddr < startAddr) {
|
|
return;
|
|
}
|
|
|
|
MemoryCallback callback;
|
|
callback.StartAddress = (uint32_t)startAddr;
|
|
callback.EndAddress = (uint32_t)endAddr;
|
|
callback.Reference = reference;
|
|
callback.Cpu = cpuType;
|
|
callback.MemType = memType;
|
|
|
|
if(DebugUtilities::IsPpuMemory(memType)) {
|
|
_debugger->GetScriptManager()->EnablePpuMemoryCallbacks();
|
|
} else {
|
|
_debugger->GetScriptManager()->EnableCpuMemoryCallbacks();
|
|
}
|
|
|
|
_callbacks[(int)type].push_back(callback);
|
|
}
|
|
|
|
void ScriptingContext::RefreshMemoryCallbackFlags()
|
|
{
|
|
for(int i = (int)CallbackType::Read; i <= (int)CallbackType::Exec; i++) {
|
|
for(size_t j = 0, len = _callbacks[i].size(); j < len; j++) {
|
|
if(DebugUtilities::IsPpuMemory(_callbacks[i][j].MemType)) {
|
|
_debugger->GetScriptManager()->EnablePpuMemoryCallbacks();
|
|
} else {
|
|
_debugger->GetScriptManager()->EnableCpuMemoryCallbacks();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void ScriptingContext::UnregisterMemoryCallback(CallbackType type, int startAddr, int endAddr, MemoryType memType, CpuType cpuType, int reference)
|
|
{
|
|
if(endAddr < startAddr) {
|
|
return;
|
|
}
|
|
|
|
for(size_t i = 0; i < _callbacks[(int)type].size(); i++) {
|
|
MemoryCallback& callback = _callbacks[(int)type][i];
|
|
bool isMatch =
|
|
callback.Reference == reference &&
|
|
callback.Cpu == cpuType &&
|
|
callback.MemType == memType &&
|
|
(int)callback.StartAddress == startAddr &&
|
|
(int)callback.EndAddress == endAddr;
|
|
|
|
if(isMatch) {
|
|
_callbacks[(int)type].erase(_callbacks[(int)type].begin() + i);
|
|
break;
|
|
}
|
|
}
|
|
|
|
luaL_unref(_lua, LUA_REGISTRYINDEX, reference);
|
|
}
|
|
|
|
void ScriptingContext::RegisterEventCallback(EventType type, int reference)
|
|
{
|
|
_eventCallbacks[(int)type].push_back(reference);
|
|
}
|
|
|
|
void ScriptingContext::UnregisterEventCallback(EventType type, int reference)
|
|
{
|
|
vector<int>& callbacks = _eventCallbacks[(int)type];
|
|
callbacks.erase(std::remove(callbacks.begin(), callbacks.end(), reference), callbacks.end());
|
|
luaL_unref(_lua, LUA_REGISTRYINDEX, reference);
|
|
}
|
|
|
|
bool ScriptingContext::IsAddressMatch(MemoryCallback& callback, AddressInfo addr)
|
|
{
|
|
return addr.Type == callback.MemType && addr.Address >= (int32_t)callback.StartAddress && addr.Address <= (int32_t)callback.EndAddress;
|
|
}
|
|
|
|
template<typename T>
|
|
void ScriptingContext::InternalCallMemoryCallback(AddressInfo relAddr, T& value, CallbackType type, CpuType cpuType)
|
|
{
|
|
if(_callbacks[(int)type].empty()) {
|
|
return;
|
|
}
|
|
|
|
_context = this;
|
|
bool needTimerReset = true;
|
|
lua_setwatchdogtimer(_lua, ScriptingContext::ExecutionCountHook, 1000);
|
|
LuaApi::SetContext(this);
|
|
for(MemoryCallback& callback : _callbacks[(int)type]) {
|
|
if(callback.Cpu != cpuType) {
|
|
continue;
|
|
}
|
|
|
|
int32_t address;
|
|
if(DebugUtilities::IsRelativeMemory(callback.MemType)) {
|
|
if(!IsAddressMatch(callback, relAddr)) {
|
|
continue;
|
|
}
|
|
address = relAddr.Address;
|
|
} else {
|
|
AddressInfo absAddr = _debugger->GetAbsoluteAddress(relAddr);
|
|
if(!IsAddressMatch(callback, absAddr)) {
|
|
continue;
|
|
}
|
|
address = absAddr.Address;
|
|
}
|
|
|
|
if(needTimerReset) {
|
|
_timer.Reset();
|
|
needTimerReset = false;
|
|
}
|
|
|
|
int top = lua_gettop(_lua);
|
|
lua_rawgeti(_lua, LUA_REGISTRYINDEX, callback.Reference);
|
|
lua_pushinteger(_lua, address);
|
|
lua_pushinteger(_lua, value);
|
|
if(lua_pcall(_lua, 2, LUA_MULTRET, 0) != 0) {
|
|
ProcessLuaError();
|
|
} else {
|
|
int returnParamCount = lua_gettop(_lua) - top;
|
|
if(returnParamCount && lua_isinteger(_lua, -1)) {
|
|
int newValue = (int)lua_tointeger(_lua, -1);
|
|
value = (T)newValue;
|
|
}
|
|
lua_settop(_lua, top);
|
|
}
|
|
}
|
|
}
|
|
|
|
int ScriptingContext::CallEventCallback(EventType type, CpuType cpuType)
|
|
{
|
|
if(_eventCallbacks[(int)type].empty()) {
|
|
return 0;
|
|
}
|
|
|
|
_timer.Reset();
|
|
_context = this;
|
|
lua_setwatchdogtimer(_lua, ScriptingContext::ExecutionCountHook, 1000);
|
|
LuaApi::SetContext(this);
|
|
LuaCallHelper l(_lua);
|
|
for(int& ref : _eventCallbacks[(int)type]) {
|
|
lua_rawgeti(_lua, LUA_REGISTRYINDEX, ref);
|
|
lua_pushinteger(_lua, (int)cpuType);
|
|
if(lua_pcall(_lua, 1, 0, 0) != 0) {
|
|
ProcessLuaError();
|
|
}
|
|
}
|
|
return l.ReturnCount();
|
|
}
|
|
|
|
template void ScriptingContext::CallMemoryCallback<uint8_t>(AddressInfo relAddr, uint8_t& value, CallbackType type, CpuType cpuType);
|
|
template void ScriptingContext::CallMemoryCallback<uint16_t>(AddressInfo relAddr, uint16_t& value, CallbackType type, CpuType cpuType);
|
|
template void ScriptingContext::CallMemoryCallback<uint32_t>(AddressInfo relAddr, uint32_t& value, CallbackType type, CpuType cpuType);
|