mirror of
https://github.com/stenzek/duckstation.git
synced 2024-11-22 21:39:40 +00:00
Feature: Add scripting interface
This commit is contained in:
parent
8c1228a7aa
commit
5f7037f347
@ -21,6 +21,11 @@
|
||||
<PreprocessorDefinitions Condition="'$(Platform)'=='x64'">XBYAK_NO_EXCEPTION=1;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
|
||||
<AdditionalIncludeDirectories Condition="'$(Platform)'=='ARM' Or '$(Platform)'=='ARM64'">%(AdditionalIncludeDirectories);$(SolutionDir)dep\vixl\include</AdditionalIncludeDirectories>
|
||||
|
||||
<AdditionalIncludeDirectories>%(AdditionalIncludeDirectories);$(LOCALAPPDATA)\Programs\Python\Python311\include</AdditionalIncludeDirectories>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<AdditionalLibraryDirectories>%(AdditionalLibraryDirectories);$(LOCALAPPDATA)\Programs\Python\Python311\libs</AdditionalLibraryDirectories>
|
||||
</Link>
|
||||
</ItemDefinitionGroup>
|
||||
</Project>
|
||||
|
@ -78,6 +78,7 @@
|
||||
<ClCompile Include="playstation_mouse.cpp" />
|
||||
<ClCompile Include="psf_loader.cpp" />
|
||||
<ClCompile Include="resources.cpp" />
|
||||
<ClCompile Include="scriptengine.cpp" />
|
||||
<ClCompile Include="settings.cpp" />
|
||||
<ClCompile Include="sio.cpp" />
|
||||
<ClCompile Include="spu.cpp" />
|
||||
@ -158,6 +159,7 @@
|
||||
<ClInclude Include="psf_loader.h" />
|
||||
<ClInclude Include="resources.h" />
|
||||
<ClInclude Include="save_state_version.h" />
|
||||
<ClInclude Include="scriptengine.h" />
|
||||
<ClInclude Include="settings.h" />
|
||||
<ClInclude Include="shader_cache_version.h" />
|
||||
<ClInclude Include="sio.h" />
|
||||
|
@ -68,6 +68,7 @@
|
||||
<ClCompile Include="justifier.cpp" />
|
||||
<ClCompile Include="pine_server.cpp" />
|
||||
<ClCompile Include="gdb_server.cpp" />
|
||||
<ClCompile Include="scriptengine.cpp" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClInclude Include="types.h" />
|
||||
@ -140,7 +141,11 @@
|
||||
<ClInclude Include="cpu_newrec_compiler_aarch32.h" />
|
||||
<ClInclude Include="achievements_private.h" />
|
||||
<ClInclude Include="justifier.h" />
|
||||
<<<<<<< Updated upstream
|
||||
<ClInclude Include="pine_server.h" />
|
||||
<ClInclude Include="gdb_server.h" />
|
||||
=======
|
||||
<ClInclude Include="scriptengine.h" />
|
||||
>>>>>>> Stashed changes
|
||||
</ItemGroup>
|
||||
</Project>
|
532
src/core/scriptengine.cpp
Normal file
532
src/core/scriptengine.cpp
Normal file
@ -0,0 +1,532 @@
|
||||
#include "scriptengine.h"
|
||||
#include "cpu_core.h"
|
||||
#include "fullscreen_ui.h"
|
||||
#include "host.h"
|
||||
#include "system.h"
|
||||
|
||||
#include "common/error.h"
|
||||
#include "common/log.h"
|
||||
#include "common/small_string.h"
|
||||
|
||||
#include "fmt/format.h"
|
||||
|
||||
#ifdef _DEBUG
|
||||
#undef _DEBUG
|
||||
#include "Python.h"
|
||||
#define _DEBUG
|
||||
#else
|
||||
#include "Python.h"
|
||||
#endif
|
||||
|
||||
#include <mutex>
|
||||
|
||||
Log_SetChannel(ScriptEngine);
|
||||
|
||||
// TODO: File loading
|
||||
// TODO: Expose imgui
|
||||
// TODO: hexdump, assembler, disassembler
|
||||
// TODO: save/load state
|
||||
// TODO: controller inputs
|
||||
// TODO: Intepreter reset
|
||||
|
||||
namespace ScriptEngine {
|
||||
static void SetErrorFromStatus(Error* error, const PyStatus& status, std::string_view prefix);
|
||||
static void SetPyErrFromError(Error* error);
|
||||
static bool RedirectOutput(Error* error);
|
||||
static void WriteOutput(std::string_view message);
|
||||
static bool CheckVMValid();
|
||||
|
||||
static PyObject* output_redirector_write(PyObject* self, PyObject* args);
|
||||
|
||||
static PyObject* dspy_inittab();
|
||||
|
||||
static PyObject* dspy_exit(PyObject* self, PyObject* args);
|
||||
|
||||
static PyObject* dspy_vm_valid(PyObject* self, PyObject* args);
|
||||
static PyObject* dspy_vm_start(PyObject* self, PyObject* args, PyObject* kwargs);
|
||||
static PyObject* dspy_vm_pause(PyObject* self, PyObject* args);
|
||||
static PyObject* dspy_vm_resume(PyObject* self, PyObject* args);
|
||||
static PyObject* dspy_vm_reset(PyObject* self, PyObject* args);
|
||||
static PyObject* dspy_vm_shutdown(PyObject* self, PyObject* args);
|
||||
|
||||
static PyMethodDef s_vm_methods[] = {
|
||||
{"valid", dspy_vm_valid, METH_NOARGS, "Returns true if a virtual machine is active."},
|
||||
{"start", reinterpret_cast<PyCFunction>(dspy_vm_start), METH_VARARGS | METH_KEYWORDS,
|
||||
"Starts a new virtual machine with the specified arguments."},
|
||||
{"pause", dspy_vm_pause, METH_NOARGS, "Pauses VM if it is not currently running."},
|
||||
{"resume", dspy_vm_resume, METH_NOARGS, "Resumes VM if it is currently paused."},
|
||||
{"reset", dspy_vm_reset, METH_NOARGS, "Resets current VM."},
|
||||
{"shutdown", dspy_vm_shutdown, METH_NOARGS, "Shuts down current VM."},
|
||||
{},
|
||||
};
|
||||
|
||||
template<typename T>
|
||||
static PyObject* dspy_mem_readT(PyObject* self, PyObject* args);
|
||||
template<typename T>
|
||||
static PyObject* dspy_mem_writeT(PyObject* self, PyObject* args);
|
||||
|
||||
static PyMethodDef s_mem_methods[] = {
|
||||
{"read8", dspy_mem_readT<u8>, METH_VARARGS, "Reads a byte from the specified address."},
|
||||
{"reads8", dspy_mem_readT<u8>, METH_VARARGS, "Reads a signed byte from the specified address."},
|
||||
{"read16", dspy_mem_readT<u16>, METH_VARARGS, "Reads a halfword from the specified address."},
|
||||
{"reads16", dspy_mem_readT<s16>, METH_VARARGS, "Reads a signed halfword from the specified address."},
|
||||
{"read32", dspy_mem_readT<u32>, METH_VARARGS, "Reads a word from the specified address."},
|
||||
{"reads32", dspy_mem_readT<s32>, METH_VARARGS, "Reads a word from the specified address."},
|
||||
{"write8", dspy_mem_writeT<u8>, METH_VARARGS, "Reads a byte from the specified address."},
|
||||
{"write16", dspy_mem_writeT<u16>, METH_VARARGS, "Reads a halfword from the specified address."},
|
||||
{"write32", dspy_mem_writeT<u32>, METH_VARARGS, "Reads a word from the specified address."},
|
||||
{},
|
||||
};
|
||||
|
||||
template<typename... T>
|
||||
ALWAYS_INLINE static void WriteOutput(fmt::format_string<T...> fmt, T&&... args)
|
||||
{
|
||||
SmallString message;
|
||||
fmt::vformat_to(std::back_inserter(message), fmt, fmt::make_format_args(args...));
|
||||
WriteOutput(message);
|
||||
}
|
||||
|
||||
static std::mutex s_output_mutex;
|
||||
static OutputCallback s_output_callback;
|
||||
static void* s_output_callback_userdata;
|
||||
|
||||
static const char* INITIALIZATION_SCRIPT = "import dspy;"
|
||||
"from dspy import vm;"
|
||||
"from dspy import mem;";
|
||||
|
||||
} // namespace ScriptEngine
|
||||
|
||||
void ScriptEngine::SetErrorFromStatus(Error* error, const PyStatus& status, std::string_view prefix)
|
||||
{
|
||||
Error::SetStringFmt(error, "func={} err_msg={} exitcode={}", prefix, status.func ? status.func : "",
|
||||
status.err_msg ? status.err_msg : "", status.exitcode);
|
||||
}
|
||||
|
||||
void ScriptEngine::SetPyErrFromError(Error* error)
|
||||
{
|
||||
PyErr_SetString(PyExc_RuntimeError, error ? error->GetDescription().c_str() : "unknown error");
|
||||
}
|
||||
|
||||
bool ScriptEngine::Initialize(Error* error)
|
||||
{
|
||||
PyPreConfig pre_config;
|
||||
PyPreConfig_InitIsolatedConfig(&pre_config);
|
||||
pre_config.utf8_mode = true;
|
||||
|
||||
PyStatus status = Py_PreInitialize(&pre_config);
|
||||
if (PyStatus_IsError(status)) [[unlikely]]
|
||||
{
|
||||
SetErrorFromStatus(error, status, "Py_PreInitialize() failed: ");
|
||||
Shutdown();
|
||||
return false;
|
||||
}
|
||||
|
||||
if (const int istatus = PyImport_AppendInittab("dspy", &dspy_inittab); istatus != 0)
|
||||
{
|
||||
Error::SetStringFmt(error, "PyImport_AppendInittab() failed: {}", istatus);
|
||||
Shutdown();
|
||||
return false;
|
||||
}
|
||||
|
||||
PyConfig config;
|
||||
PyConfig_InitIsolatedConfig(&config);
|
||||
config.pythonpath_env = Py_DecodeLocale("C:\\Users\\Me\\AppData\\Local\\Programs\\Python\\Python311\\Lib", nullptr);
|
||||
|
||||
status = Py_InitializeFromConfig(&config);
|
||||
|
||||
PyMem_RawFree(config.pythonpath_env);
|
||||
|
||||
if (PyStatus_IsError(status)) [[unlikely]]
|
||||
{
|
||||
SetErrorFromStatus(error, status, "Py_InitializeFromConfig() failed: ");
|
||||
Shutdown();
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!RedirectOutput(error)) [[unlikely]]
|
||||
{
|
||||
Error::AddPrefix(error, "Failed to redirect output: ");
|
||||
Shutdown();
|
||||
return false;
|
||||
}
|
||||
|
||||
if (PyRun_SimpleString(INITIALIZATION_SCRIPT) < 0)
|
||||
{
|
||||
PyErr_Print();
|
||||
Error::SetStringFmt(error, "Failed to run initialization script.");
|
||||
Shutdown();
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void ScriptEngine::Shutdown()
|
||||
{
|
||||
if (const int ret = Py_FinalizeEx(); ret != 0)
|
||||
{
|
||||
ERROR_LOG("Py_FinalizeEx() returned {}", ret);
|
||||
}
|
||||
}
|
||||
|
||||
void ScriptEngine::SetOutputCallback(OutputCallback callback, void* userdata)
|
||||
{
|
||||
std::unique_lock lock(s_output_mutex);
|
||||
s_output_callback = callback;
|
||||
s_output_callback_userdata = userdata;
|
||||
}
|
||||
|
||||
bool ScriptEngine::RedirectOutput(Error* error)
|
||||
{
|
||||
PyObject* dspy_module = PyImport_ImportModule("dspy");
|
||||
if (!dspy_module)
|
||||
{
|
||||
Error::SetStringView(error, "PyImport_ImportModule(dspy) failed");
|
||||
return false;
|
||||
}
|
||||
|
||||
PyObject* module_dict = PyModule_GetDict(dspy_module);
|
||||
if (!module_dict)
|
||||
{
|
||||
Error::SetStringView(error, "PyModule_GetDict() failed");
|
||||
Py_DECREF(dspy_module);
|
||||
return false;
|
||||
}
|
||||
|
||||
PyObject* output_redirector_class = PyDict_GetItemString(module_dict, "output_redirector");
|
||||
Py_DECREF(dspy_module);
|
||||
if (!output_redirector_class)
|
||||
{
|
||||
Error::SetStringView(error, "PyDict_GetItemString() failed");
|
||||
return false;
|
||||
}
|
||||
|
||||
PyObject* output_redirector;
|
||||
if (!PyCallable_Check(output_redirector_class) ||
|
||||
!(output_redirector = PyObject_CallObject(output_redirector_class, nullptr)))
|
||||
{
|
||||
Error::SetStringView(error, "PyObject_CallObject() failed");
|
||||
Py_DECREF(output_redirector_class);
|
||||
return false;
|
||||
}
|
||||
|
||||
Py_DECREF(output_redirector_class);
|
||||
|
||||
PyObject* sys_module = PyImport_ImportModule("sys");
|
||||
if (!sys_module)
|
||||
{
|
||||
Error::SetStringView(error, "PyImport_ImportModule(sys) failed");
|
||||
Py_DECREF(output_redirector);
|
||||
return false;
|
||||
}
|
||||
|
||||
module_dict = PyModule_GetDict(sys_module);
|
||||
if (!module_dict)
|
||||
{
|
||||
Error::SetStringView(error, "PyModule_GetDict(sys) failed");
|
||||
Py_DECREF(sys_module);
|
||||
Py_DECREF(output_redirector);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (PyDict_SetItemString(module_dict, "stdout", output_redirector) < 0 ||
|
||||
PyDict_SetItemString(module_dict, "stderr", output_redirector) < 0)
|
||||
{
|
||||
Error::SetStringView(error, "PyDict_SetItemString() failed");
|
||||
Py_DECREF(sys_module);
|
||||
Py_DECREF(output_redirector);
|
||||
return false;
|
||||
}
|
||||
|
||||
Py_DECREF(sys_module);
|
||||
Py_DECREF(output_redirector);
|
||||
return true;
|
||||
}
|
||||
|
||||
void ScriptEngine::WriteOutput(std::string_view message)
|
||||
{
|
||||
INFO_LOG("Python: {}", message);
|
||||
|
||||
if (s_output_callback)
|
||||
{
|
||||
std::unique_lock lock(s_output_mutex);
|
||||
s_output_callback(message, s_output_callback_userdata);
|
||||
}
|
||||
}
|
||||
|
||||
void ScriptEngine::EvalString(const char* str)
|
||||
{
|
||||
WriteOutput(">>> {}\n", str);
|
||||
|
||||
const int res = PyRun_SimpleString(str);
|
||||
if (res == 0)
|
||||
return;
|
||||
|
||||
WriteOutput("PyRun_SimpleString() returned {}\n", res);
|
||||
PyErr_Print();
|
||||
}
|
||||
|
||||
#define PYBOOL(b) ((b) ? Py_NewRef(Py_True) : Py_NewRef(Py_False))
|
||||
|
||||
PyObject* ScriptEngine::output_redirector_write(PyObject* self, PyObject* args)
|
||||
{
|
||||
const char* msg;
|
||||
if (!PyArg_ParseTuple(args, "s", &msg))
|
||||
return nullptr;
|
||||
|
||||
WriteOutput(msg);
|
||||
Py_RETURN_NONE;
|
||||
}
|
||||
|
||||
PyObject* ScriptEngine::dspy_inittab()
|
||||
{
|
||||
static PyMethodDef root_methods[] = {
|
||||
{"exit", dspy_exit, METH_NOARGS, "Exits the hosting application."},
|
||||
{},
|
||||
};
|
||||
static PyModuleDef root_module_def = {
|
||||
PyModuleDef_HEAD_INIT, "dspy", nullptr, -1, root_methods, nullptr, nullptr, nullptr, nullptr};
|
||||
|
||||
static PyModuleDef vm_module_def = {
|
||||
PyModuleDef_HEAD_INIT, "vm", nullptr, -1, s_vm_methods, nullptr, nullptr, nullptr, nullptr};
|
||||
|
||||
static PyModuleDef mem_module_def = {
|
||||
PyModuleDef_HEAD_INIT, "mem", nullptr, -1, s_mem_methods, nullptr, nullptr, nullptr, nullptr};
|
||||
|
||||
static PyMethodDef output_redirectory_methods[] = {
|
||||
{"write", output_redirector_write, METH_VARARGS, "Writes to script console."},
|
||||
{},
|
||||
};
|
||||
|
||||
static PyTypeObject output_redirector = {
|
||||
.ob_base = PyVarObject_HEAD_INIT(nullptr, 0).tp_name = "dspy.output_redirector",
|
||||
.tp_basicsize = sizeof(PyObject),
|
||||
.tp_itemsize = 0,
|
||||
.tp_flags = Py_TPFLAGS_DEFAULT,
|
||||
.tp_doc = PyDoc_STR("Output Redirector"),
|
||||
.tp_methods = output_redirectory_methods,
|
||||
.tp_new = PyType_GenericNew,
|
||||
};
|
||||
|
||||
PyObject* root_module = PyModule_Create(&root_module_def);
|
||||
if (!root_module)
|
||||
return nullptr;
|
||||
|
||||
PyObject* vm_module = PyModule_Create(&vm_module_def);
|
||||
if (!vm_module)
|
||||
{
|
||||
Py_DECREF(root_module);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
PyObject* mem_module = PyModule_Create(&mem_module_def);
|
||||
if (!vm_module)
|
||||
{
|
||||
Py_DECREF(vm_module);
|
||||
Py_DECREF(root_module);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
if (PyType_Ready(&output_redirector) < 0 || PyModule_AddObjectRef(root_module, "vm", vm_module) < 0 ||
|
||||
PyModule_AddObjectRef(root_module, "mem", mem_module) < 0 ||
|
||||
PyModule_AddObjectRef(root_module, "output_redirector", reinterpret_cast<PyObject*>(&output_redirector)) < 0)
|
||||
{
|
||||
Py_DECREF(mem_module);
|
||||
Py_DECREF(vm_module);
|
||||
Py_DECREF(root_module);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
Py_DECREF(mem_module);
|
||||
Py_DECREF(vm_module);
|
||||
return root_module;
|
||||
}
|
||||
|
||||
PyObject* ScriptEngine::dspy_exit(PyObject* self, PyObject* args)
|
||||
{
|
||||
Host::RequestExitApplication(false);
|
||||
Py_RETURN_NONE;
|
||||
}
|
||||
|
||||
bool ScriptEngine::CheckVMValid()
|
||||
{
|
||||
if (System::IsValid())
|
||||
{
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
PyErr_SetString(PyExc_RuntimeError, "VM has not been started.");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
PyObject* ScriptEngine::dspy_vm_valid(PyObject* self, PyObject* args)
|
||||
{
|
||||
return PYBOOL(System::IsValid());
|
||||
}
|
||||
|
||||
PyObject* ScriptEngine::dspy_vm_start(PyObject* self, PyObject* args, PyObject* kwargs)
|
||||
{
|
||||
static constexpr const char* kwlist[] = {
|
||||
"path", "savestate", "exe", "override_fastboot", "override_slowboot", "start_fullscreen", "start_paused", nullptr};
|
||||
|
||||
if (System::GetState() != System::State::Shutdown)
|
||||
{
|
||||
PyErr_SetString(PyExc_RuntimeError, "VM has already been started.");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const char* path = nullptr;
|
||||
const char* savestate = nullptr;
|
||||
const char* override_exe = nullptr;
|
||||
int override_fastboot = 0;
|
||||
int override_slowboot = 0;
|
||||
int start_fullscreen = 0;
|
||||
int start_paused = 0;
|
||||
if (!PyArg_ParseTupleAndKeywords(args, kwargs, "|$ssspppp", const_cast<char**>(kwlist), &path, &savestate,
|
||||
&override_exe, &override_fastboot, &override_slowboot, &start_fullscreen,
|
||||
&start_paused))
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
SystemBootParameters params;
|
||||
if (path)
|
||||
params.filename = path;
|
||||
if (savestate)
|
||||
params.save_state = savestate;
|
||||
if (override_exe)
|
||||
params.override_exe = override_exe;
|
||||
if (override_fastboot)
|
||||
params.override_fast_boot = true;
|
||||
else if (override_slowboot)
|
||||
params.override_fast_boot = false;
|
||||
if (start_fullscreen)
|
||||
params.override_fullscreen = true;
|
||||
if (start_paused)
|
||||
params.override_start_paused = true;
|
||||
|
||||
WriteOutput("Starting system with path={}\n", params.filename);
|
||||
|
||||
Error error;
|
||||
if (!System::BootSystem(std::move(params), &error))
|
||||
{
|
||||
WriteOutput("Starting system failed: {}\n", error.GetDescription());
|
||||
SetPyErrFromError(&error);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
Py_RETURN_NONE;
|
||||
}
|
||||
|
||||
PyObject* ScriptEngine::dspy_vm_pause(PyObject* self, PyObject* args)
|
||||
{
|
||||
if (!CheckVMValid())
|
||||
return nullptr;
|
||||
|
||||
System::PauseSystem(true);
|
||||
Py_RETURN_NONE;
|
||||
}
|
||||
|
||||
PyObject* ScriptEngine::dspy_vm_resume(PyObject* self, PyObject* args)
|
||||
{
|
||||
if (!CheckVMValid())
|
||||
return nullptr;
|
||||
|
||||
System::PauseSystem(false);
|
||||
Py_RETURN_NONE;
|
||||
}
|
||||
|
||||
PyObject* ScriptEngine::dspy_vm_reset(PyObject* self, PyObject* args)
|
||||
{
|
||||
if (!CheckVMValid())
|
||||
return nullptr;
|
||||
|
||||
System::ResetSystem();
|
||||
Py_RETURN_NONE;
|
||||
}
|
||||
|
||||
PyObject* ScriptEngine::dspy_vm_shutdown(PyObject* self, PyObject* args)
|
||||
{
|
||||
if (!CheckVMValid())
|
||||
return nullptr;
|
||||
|
||||
System::ShutdownSystem(false);
|
||||
Py_RETURN_NONE;
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
PyObject* ScriptEngine::dspy_mem_readT(PyObject* self, PyObject* args)
|
||||
{
|
||||
if (!CheckVMValid())
|
||||
return nullptr;
|
||||
|
||||
unsigned int address;
|
||||
if (!PyArg_ParseTuple(args, "I", &address))
|
||||
return nullptr;
|
||||
|
||||
if constexpr (std::is_same_v<T, u8> || std::is_same_v<T, s8>)
|
||||
{
|
||||
u8 result;
|
||||
if (CPU::SafeReadMemoryByte(address, &result)) [[likely]]
|
||||
return std::is_signed_v<T> ? PyLong_FromLong(static_cast<s8>(result)) : PyLong_FromUnsignedLong(result);
|
||||
}
|
||||
else if constexpr (std::is_same_v<T, u16> || std::is_same_v<T, s16>)
|
||||
{
|
||||
u16 result;
|
||||
if (CPU::SafeReadMemoryHalfWord(address, &result)) [[likely]]
|
||||
return std::is_signed_v<T> ? PyLong_FromLong(static_cast<s16>(result)) : PyLong_FromUnsignedLong(result);
|
||||
}
|
||||
else if constexpr (std::is_same_v<T, u32> || std::is_same_v<T, s32>)
|
||||
{
|
||||
u32 result;
|
||||
if (CPU::SafeReadMemoryWord(address, &result)) [[likely]]
|
||||
return std::is_signed_v<T> ? PyLong_FromLong(static_cast<s32>(result)) : PyLong_FromUnsignedLong(result);
|
||||
}
|
||||
|
||||
PyErr_SetString(PyExc_RuntimeError, "Address was not valid.");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
template PyObject* ScriptEngine::dspy_mem_readT<u8>(PyObject*, PyObject*);
|
||||
template PyObject* ScriptEngine::dspy_mem_readT<s8>(PyObject*, PyObject*);
|
||||
template PyObject* ScriptEngine::dspy_mem_readT<u16>(PyObject*, PyObject*);
|
||||
template PyObject* ScriptEngine::dspy_mem_readT<s16>(PyObject*, PyObject*);
|
||||
template PyObject* ScriptEngine::dspy_mem_readT<u32>(PyObject*, PyObject*);
|
||||
template PyObject* ScriptEngine::dspy_mem_readT<s32>(PyObject*, PyObject*);
|
||||
|
||||
template<typename T>
|
||||
PyObject* ScriptEngine::dspy_mem_writeT(PyObject* self, PyObject* args)
|
||||
{
|
||||
if (!CheckVMValid())
|
||||
return nullptr;
|
||||
|
||||
unsigned int address;
|
||||
long long value;
|
||||
if (!PyArg_ParseTuple(args, "IL", &address, &value))
|
||||
return nullptr;
|
||||
|
||||
if constexpr (std::is_same_v<T, u8>)
|
||||
{
|
||||
if (CPU::SafeWriteMemoryByte(address, static_cast<u8>(value))) [[likely]]
|
||||
Py_RETURN_NONE;
|
||||
}
|
||||
else if constexpr (std::is_same_v<T, u16>)
|
||||
{
|
||||
if (CPU::SafeWriteMemoryHalfWord(address, static_cast<u16>(value))) [[likely]]
|
||||
Py_RETURN_NONE;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (CPU::SafeWriteMemoryWord(address, static_cast<u32>(value))) [[likely]]
|
||||
Py_RETURN_NONE;
|
||||
}
|
||||
|
||||
PyErr_SetString(PyExc_RuntimeError, "Address was not valid.");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
template PyObject* ScriptEngine::dspy_mem_writeT<u8>(PyObject*, PyObject*);
|
||||
template PyObject* ScriptEngine::dspy_mem_writeT<u16>(PyObject*, PyObject*);
|
||||
template PyObject* ScriptEngine::dspy_mem_writeT<u32>(PyObject*, PyObject*);
|
19
src/core/scriptengine.h
Normal file
19
src/core/scriptengine.h
Normal file
@ -0,0 +1,19 @@
|
||||
#pragma once
|
||||
|
||||
#include "types.h"
|
||||
|
||||
#include <string_view>
|
||||
|
||||
class Error;
|
||||
|
||||
namespace ScriptEngine {
|
||||
|
||||
bool Initialize(Error* error);
|
||||
void Shutdown();
|
||||
|
||||
using OutputCallback = void (*)(std::string_view, void*);
|
||||
void SetOutputCallback(OutputCallback callback, void* userdata);
|
||||
|
||||
void EvalString(const char* str);
|
||||
|
||||
} // namespace ScriptEngine
|
@ -48,6 +48,7 @@
|
||||
<ClCompile Include="qtkeycodes.cpp" />
|
||||
<ClCompile Include="qtprogresscallback.cpp" />
|
||||
<ClCompile Include="qtutils.cpp" />
|
||||
<ClCompile Include="scriptconsole.cpp" />
|
||||
<ClCompile Include="selectdiscdialog.cpp" />
|
||||
<ClCompile Include="settingswindow.cpp" />
|
||||
<ClCompile Include="setupwizarddialog.cpp" />
|
||||
@ -89,6 +90,7 @@
|
||||
<ClInclude Include="pch.h" />
|
||||
<ClInclude Include="resource.h" />
|
||||
<QtMoc Include="selectdiscdialog.h" />
|
||||
<QtMoc Include="scriptconsole.h" />
|
||||
<ClInclude Include="settingwidgetbinder.h" />
|
||||
<QtMoc Include="consolesettingswidget.h" />
|
||||
<QtMoc Include="emulationsettingswidget.h" />
|
||||
@ -257,6 +259,7 @@
|
||||
<ClCompile Include="$(IntDir)moc_selectdiscdialog.cpp" />
|
||||
<ClCompile Include="$(IntDir)moc_qthost.cpp" />
|
||||
<ClCompile Include="$(IntDir)moc_qtprogresscallback.cpp" />
|
||||
<ClCompile Include="$(IntDir)moc_scriptconsole.cpp" />
|
||||
<ClCompile Include="$(IntDir)moc_settingswindow.cpp" />
|
||||
<ClCompile Include="$(IntDir)moc_setupwizarddialog.cpp" />
|
||||
<ClCompile Include="$(IntDir)qrc_resources.cpp" />
|
||||
|
@ -179,6 +179,10 @@
|
||||
<Filter>moc</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="vcruntimecheck.cpp" />
|
||||
<ClCompile Include="scriptconsole.cpp" />
|
||||
<ClCompile Include="$(IntDir)moc_scriptconsole.cpp">
|
||||
<Filter>moc</Filter>
|
||||
</ClCompile>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClInclude Include="qtutils.h" />
|
||||
@ -242,6 +246,7 @@
|
||||
<QtMoc Include="graphicssettingswidget.h" />
|
||||
<QtMoc Include="memoryscannerwindow.h" />
|
||||
<QtMoc Include="selectdiscdialog.h" />
|
||||
<QtMoc Include="scriptconsole.h" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<QtUi Include="consolesettingswidget.ui" />
|
||||
|
@ -20,6 +20,7 @@
|
||||
#include "selectdiscdialog.h"
|
||||
#include "settingswindow.h"
|
||||
#include "settingwidgetbinder.h"
|
||||
#include "scriptconsole.h"
|
||||
|
||||
#include "core/achievements.h"
|
||||
#include "core/game_list.h"
|
||||
@ -181,6 +182,8 @@ void MainWindow::initialize()
|
||||
CocoaTools::AddThemeChangeHandler(this,
|
||||
[](void* ctx) { QtHost::RunOnUIThread([] { g_main_window->updateTheme(); }); });
|
||||
#endif
|
||||
|
||||
ScriptConsole::updateSettings();
|
||||
}
|
||||
|
||||
void MainWindow::reportError(const QString& title, const QString& message)
|
||||
@ -770,6 +773,7 @@ void MainWindow::destroySubWindows()
|
||||
|
||||
SettingsWindow::closeGamePropertiesDialogs();
|
||||
|
||||
ScriptConsole::destroy();
|
||||
LogWindow::destroy();
|
||||
}
|
||||
|
||||
|
@ -21,6 +21,7 @@
|
||||
#include "core/host.h"
|
||||
#include "core/imgui_overlays.h"
|
||||
#include "core/memory_card.h"
|
||||
#include "core/scriptengine.h"
|
||||
#include "core/spu.h"
|
||||
#include "core/system.h"
|
||||
|
||||
@ -1735,6 +1736,8 @@ void EmuThread::run()
|
||||
createBackgroundControllerPollTimer();
|
||||
startBackgroundControllerPollTimer();
|
||||
|
||||
ScriptEngine::Initialize(nullptr);
|
||||
|
||||
// main loop
|
||||
while (!m_shutdown_flag)
|
||||
{
|
||||
|
272
src/duckstation-qt/scriptconsole.cpp
Normal file
272
src/duckstation-qt/scriptconsole.cpp
Normal file
@ -0,0 +1,272 @@
|
||||
// SPDX-FileCopyrightText: 2019-2024 Connor McLaughlin <stenzek@gmail.com>
|
||||
// SPDX-License-Identifier: (GPL-3.0 OR CC-BY-NC-ND-4.0)
|
||||
|
||||
#include "scriptconsole.h"
|
||||
#include "mainwindow.h"
|
||||
#include "qthost.h"
|
||||
#include "settingwidgetbinder.h"
|
||||
|
||||
#include "core/scriptengine.h"
|
||||
#include "util/host.h"
|
||||
|
||||
#include <QtCore/QLatin1StringView>
|
||||
#include <QtCore/QUtf8StringView>
|
||||
#include <QtGui/QIcon>
|
||||
#include <QtWidgets/QMenuBar>
|
||||
#include <QtWidgets/QScrollBar>
|
||||
|
||||
// TODO: Since log callbacks are synchronized, no mutex is needed here.
|
||||
// But once I get rid of that, there will be.
|
||||
ScriptConsole* g_script_console;
|
||||
|
||||
static constexpr const char* INITIAL_TEXT =
|
||||
R"(----------------------------------------------------------------------------------
|
||||
This is the DuckStation script console.
|
||||
|
||||
You can run commands, or evaluate expressions using the text box below,
|
||||
and click Execute, or press Enter.
|
||||
|
||||
The vm and mem modules have already been imported into the main namespace for you.
|
||||
----------------------------------------------------------------------------------
|
||||
|
||||
)";
|
||||
|
||||
ScriptConsole::ScriptConsole() : QMainWindow()
|
||||
{
|
||||
restoreSize();
|
||||
createUi();
|
||||
|
||||
ScriptEngine::SetOutputCallback(&ScriptConsole::outputCallback, this);
|
||||
}
|
||||
|
||||
ScriptConsole::~ScriptConsole() = default;
|
||||
|
||||
void ScriptConsole::updateSettings()
|
||||
{
|
||||
const bool new_enabled = true; // Host::GetBaseBoolSettingValue("Logging", "LogToWindow", false);
|
||||
const bool curr_enabled = (g_script_console != nullptr);
|
||||
if (new_enabled == curr_enabled)
|
||||
return;
|
||||
|
||||
if (new_enabled)
|
||||
{
|
||||
g_script_console = new ScriptConsole();
|
||||
g_script_console->show();
|
||||
}
|
||||
else if (g_script_console)
|
||||
{
|
||||
g_script_console->m_destroying = true;
|
||||
g_script_console->close();
|
||||
g_script_console->deleteLater();
|
||||
g_script_console = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
void ScriptConsole::destroy()
|
||||
{
|
||||
if (!g_script_console)
|
||||
return;
|
||||
|
||||
g_script_console->m_destroying = true;
|
||||
g_script_console->close();
|
||||
g_script_console->deleteLater();
|
||||
g_script_console = nullptr;
|
||||
}
|
||||
|
||||
void ScriptConsole::createUi()
|
||||
{
|
||||
QIcon icon;
|
||||
icon.addFile(QString::fromUtf8(":/icons/duck.png"), QSize(), QIcon::Normal, QIcon::Off);
|
||||
setWindowIcon(icon);
|
||||
setWindowFlag(Qt::WindowCloseButtonHint, false);
|
||||
setWindowTitle(tr("Script Console"));
|
||||
|
||||
QAction* action;
|
||||
|
||||
QMenuBar* menu = new QMenuBar(this);
|
||||
setMenuBar(menu);
|
||||
|
||||
QMenu* log_menu = menu->addMenu("&Log");
|
||||
action = log_menu->addAction(tr("&Clear"));
|
||||
connect(action, &QAction::triggered, this, &ScriptConsole::onClearTriggered);
|
||||
action = log_menu->addAction(tr("&Save..."));
|
||||
connect(action, &QAction::triggered, this, &ScriptConsole::onSaveTriggered);
|
||||
|
||||
log_menu->addSeparator();
|
||||
|
||||
action = log_menu->addAction(tr("Cl&ose"));
|
||||
connect(action, &QAction::triggered, this, &ScriptConsole::close);
|
||||
|
||||
QWidget* main_widget = new QWidget(this);
|
||||
QVBoxLayout* main_layout = new QVBoxLayout(main_widget);
|
||||
|
||||
m_text = new QPlainTextEdit(main_widget);
|
||||
m_text->setReadOnly(true);
|
||||
m_text->setUndoRedoEnabled(false);
|
||||
m_text->setTextInteractionFlags(Qt::TextSelectableByKeyboard);
|
||||
m_text->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOn);
|
||||
|
||||
#if defined(_WIN32)
|
||||
QFont font("Consolas");
|
||||
font.setPointSize(10);
|
||||
#elif defined(__APPLE__)
|
||||
QFont font("Monaco");
|
||||
font.setPointSize(11);
|
||||
#else
|
||||
QFont font("Monospace");
|
||||
font.setStyleHint(QFont::TypeWriter);
|
||||
#endif
|
||||
m_text->setFont(font);
|
||||
main_layout->addWidget(m_text, 1);
|
||||
|
||||
QHBoxLayout* command_layout = new QHBoxLayout();
|
||||
|
||||
m_command = new QLineEdit(main_widget);
|
||||
command_layout->addWidget(m_command, 1);
|
||||
|
||||
m_execute = new QPushButton(tr("&Execute"), main_widget);
|
||||
m_execute->setEnabled(false);
|
||||
connect(m_execute, &QPushButton::clicked, this, &ScriptConsole::executeClicked);
|
||||
command_layout->addWidget(m_execute);
|
||||
|
||||
main_layout->addLayout(command_layout);
|
||||
|
||||
setCentralWidget(main_widget);
|
||||
|
||||
m_command->setFocus();
|
||||
connect(m_command, &QLineEdit::textChanged, this, &ScriptConsole::commandChanged);
|
||||
connect(m_command, &QLineEdit::returnPressed, this, &ScriptConsole::executeClicked);
|
||||
|
||||
appendMessage(QString::fromUtf8(INITIAL_TEXT));
|
||||
}
|
||||
|
||||
void ScriptConsole::onClearTriggered()
|
||||
{
|
||||
m_text->clear();
|
||||
}
|
||||
|
||||
void ScriptConsole::onSaveTriggered()
|
||||
{
|
||||
const QString path = QFileDialog::getSaveFileName(this, tr("Select Log File"), QString(), tr("Log Files (*.txt)"));
|
||||
if (path.isEmpty())
|
||||
return;
|
||||
|
||||
QFile file(path);
|
||||
if (!file.open(QFile::WriteOnly | QFile::Text))
|
||||
{
|
||||
QMessageBox::critical(this, tr("Error"), tr("Failed to open file for writing."));
|
||||
return;
|
||||
}
|
||||
|
||||
file.write(m_text->toPlainText().toUtf8());
|
||||
file.close();
|
||||
|
||||
appendMessage(tr("Log was written to %1.\n").arg(path));
|
||||
}
|
||||
|
||||
void ScriptConsole::outputCallback(std::string_view message, void* userdata)
|
||||
{
|
||||
if (message.empty())
|
||||
return;
|
||||
|
||||
ScriptConsole* this_ptr = static_cast<ScriptConsole*>(userdata);
|
||||
|
||||
// TODO: Split message based on lines.
|
||||
// I don't like the memory allocations here either...
|
||||
|
||||
QString qmessage = QtUtils::StringViewToQString(message);
|
||||
|
||||
DebugAssert(!g_emu_thread->isOnUIThread());
|
||||
QMetaObject::invokeMethod(this_ptr, "appendMessage", Qt::QueuedConnection, Q_ARG(const QString&, qmessage));
|
||||
}
|
||||
|
||||
void ScriptConsole::closeEvent(QCloseEvent* event)
|
||||
{
|
||||
if (!m_destroying)
|
||||
{
|
||||
event->ignore();
|
||||
return;
|
||||
}
|
||||
|
||||
ScriptEngine::SetOutputCallback(nullptr, nullptr);
|
||||
|
||||
saveSize();
|
||||
|
||||
QMainWindow::closeEvent(event);
|
||||
}
|
||||
|
||||
void ScriptConsole::appendMessage(const QString& message)
|
||||
{
|
||||
QTextCursor temp_cursor = m_text->textCursor();
|
||||
QScrollBar* scrollbar = m_text->verticalScrollBar();
|
||||
const bool cursor_at_end = temp_cursor.atEnd();
|
||||
const bool scroll_at_end = scrollbar->sliderPosition() == scrollbar->maximum();
|
||||
|
||||
temp_cursor.movePosition(QTextCursor::End);
|
||||
|
||||
{
|
||||
QTextCharFormat format = temp_cursor.charFormat();
|
||||
|
||||
format.setForeground(QBrush(QColor(0xCC, 0xCC, 0xCC)));
|
||||
temp_cursor.setCharFormat(format);
|
||||
temp_cursor.insertText(message);
|
||||
}
|
||||
|
||||
if (cursor_at_end)
|
||||
{
|
||||
if (scroll_at_end)
|
||||
{
|
||||
m_text->setTextCursor(temp_cursor);
|
||||
scrollbar->setSliderPosition(scrollbar->maximum());
|
||||
}
|
||||
else
|
||||
{
|
||||
// Can't let changing the cursor affect the scroll bar...
|
||||
const int pos = scrollbar->sliderPosition();
|
||||
m_text->setTextCursor(temp_cursor);
|
||||
scrollbar->setSliderPosition(pos);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ScriptConsole::commandChanged(const QString& text)
|
||||
{
|
||||
m_execute->setEnabled(!text.isEmpty());
|
||||
}
|
||||
|
||||
void ScriptConsole::executeClicked()
|
||||
{
|
||||
const QString text = m_command->text();
|
||||
if (text.isEmpty())
|
||||
return;
|
||||
|
||||
Host::RunOnCPUThread([code = text.toStdString()]() { ScriptEngine::EvalString(code.c_str()); });
|
||||
m_command->clear();
|
||||
}
|
||||
|
||||
void ScriptConsole::saveSize()
|
||||
{
|
||||
const QByteArray geometry = saveGeometry();
|
||||
const QByteArray geometry_b64 = geometry.toBase64();
|
||||
const std::string old_geometry_b64 = Host::GetBaseStringSettingValue("UI", "ScriptConsoleGeometry");
|
||||
if (old_geometry_b64 != geometry_b64.constData())
|
||||
{
|
||||
Host::SetBaseStringSettingValue("UI", "ScriptConsoleGeometry", geometry_b64.constData());
|
||||
Host::CommitBaseSettingChanges();
|
||||
}
|
||||
}
|
||||
|
||||
void ScriptConsole::restoreSize()
|
||||
{
|
||||
const std::string geometry_b64 = Host::GetBaseStringSettingValue("UI", "ScriptConsoleGeometry");
|
||||
const QByteArray geometry = QByteArray::fromBase64(QByteArray::fromStdString(geometry_b64));
|
||||
if (!geometry.isEmpty())
|
||||
{
|
||||
restoreGeometry(geometry);
|
||||
}
|
||||
else
|
||||
{
|
||||
// default size
|
||||
resize(DEFAULT_WIDTH, DEFAULT_WIDTH);
|
||||
}
|
||||
}
|
54
src/duckstation-qt/scriptconsole.h
Normal file
54
src/duckstation-qt/scriptconsole.h
Normal file
@ -0,0 +1,54 @@
|
||||
// SPDX-FileCopyrightText: 2019-2024 Connor McLaughlin <stenzek@gmail.com>
|
||||
// SPDX-License-Identifier: (GPL-3.0 OR CC-BY-NC-ND-4.0)
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "common/log.h"
|
||||
|
||||
#include <QtWidgets/QLineEdit>
|
||||
#include <QtWidgets/QMainWindow>
|
||||
#include <QtWidgets/QPlainTextEdit>
|
||||
#include <QtWidgets/QPushButton>
|
||||
#include <span>
|
||||
|
||||
class ScriptConsole : public QMainWindow
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
ScriptConsole();
|
||||
~ScriptConsole();
|
||||
|
||||
static void updateSettings();
|
||||
static void destroy();
|
||||
|
||||
private:
|
||||
void createUi();
|
||||
|
||||
static void outputCallback(std::string_view message, void* userdata);
|
||||
|
||||
protected:
|
||||
void closeEvent(QCloseEvent* event);
|
||||
|
||||
private Q_SLOTS:
|
||||
void onClearTriggered();
|
||||
void onSaveTriggered();
|
||||
void appendMessage(const QString& message);
|
||||
void commandChanged(const QString& text);
|
||||
void executeClicked();
|
||||
|
||||
private:
|
||||
static constexpr int DEFAULT_WIDTH = 750;
|
||||
static constexpr int DEFAULT_HEIGHT = 400;
|
||||
|
||||
void saveSize();
|
||||
void restoreSize();
|
||||
|
||||
QPlainTextEdit* m_text;
|
||||
QLineEdit* m_command;
|
||||
QPushButton* m_execute;
|
||||
|
||||
bool m_destroying = false;
|
||||
};
|
||||
|
||||
extern ScriptConsole* g_script_console;
|
Loading…
Reference in New Issue
Block a user