mirror of
https://github.com/stenzek/duckstation.git
synced 2024-11-26 15:40:41 +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>
|
<PreprocessorDefinitions Condition="'$(Platform)'=='x64'">XBYAK_NO_EXCEPTION=1;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||||
|
|
||||||
<AdditionalIncludeDirectories Condition="'$(Platform)'=='ARM' Or '$(Platform)'=='ARM64'">%(AdditionalIncludeDirectories);$(SolutionDir)dep\vixl\include</AdditionalIncludeDirectories>
|
<AdditionalIncludeDirectories Condition="'$(Platform)'=='ARM' Or '$(Platform)'=='ARM64'">%(AdditionalIncludeDirectories);$(SolutionDir)dep\vixl\include</AdditionalIncludeDirectories>
|
||||||
|
|
||||||
|
<AdditionalIncludeDirectories>%(AdditionalIncludeDirectories);$(LOCALAPPDATA)\Programs\Python\Python311\include</AdditionalIncludeDirectories>
|
||||||
</ClCompile>
|
</ClCompile>
|
||||||
|
<Link>
|
||||||
|
<AdditionalLibraryDirectories>%(AdditionalLibraryDirectories);$(LOCALAPPDATA)\Programs\Python\Python311\libs</AdditionalLibraryDirectories>
|
||||||
|
</Link>
|
||||||
</ItemDefinitionGroup>
|
</ItemDefinitionGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
@ -78,6 +78,7 @@
|
|||||||
<ClCompile Include="playstation_mouse.cpp" />
|
<ClCompile Include="playstation_mouse.cpp" />
|
||||||
<ClCompile Include="psf_loader.cpp" />
|
<ClCompile Include="psf_loader.cpp" />
|
||||||
<ClCompile Include="resources.cpp" />
|
<ClCompile Include="resources.cpp" />
|
||||||
|
<ClCompile Include="scriptengine.cpp" />
|
||||||
<ClCompile Include="settings.cpp" />
|
<ClCompile Include="settings.cpp" />
|
||||||
<ClCompile Include="sio.cpp" />
|
<ClCompile Include="sio.cpp" />
|
||||||
<ClCompile Include="spu.cpp" />
|
<ClCompile Include="spu.cpp" />
|
||||||
@ -158,6 +159,7 @@
|
|||||||
<ClInclude Include="psf_loader.h" />
|
<ClInclude Include="psf_loader.h" />
|
||||||
<ClInclude Include="resources.h" />
|
<ClInclude Include="resources.h" />
|
||||||
<ClInclude Include="save_state_version.h" />
|
<ClInclude Include="save_state_version.h" />
|
||||||
|
<ClInclude Include="scriptengine.h" />
|
||||||
<ClInclude Include="settings.h" />
|
<ClInclude Include="settings.h" />
|
||||||
<ClInclude Include="shader_cache_version.h" />
|
<ClInclude Include="shader_cache_version.h" />
|
||||||
<ClInclude Include="sio.h" />
|
<ClInclude Include="sio.h" />
|
||||||
|
@ -68,6 +68,7 @@
|
|||||||
<ClCompile Include="justifier.cpp" />
|
<ClCompile Include="justifier.cpp" />
|
||||||
<ClCompile Include="pine_server.cpp" />
|
<ClCompile Include="pine_server.cpp" />
|
||||||
<ClCompile Include="gdb_server.cpp" />
|
<ClCompile Include="gdb_server.cpp" />
|
||||||
|
<ClCompile Include="scriptengine.cpp" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ClInclude Include="types.h" />
|
<ClInclude Include="types.h" />
|
||||||
@ -140,7 +141,11 @@
|
|||||||
<ClInclude Include="cpu_newrec_compiler_aarch32.h" />
|
<ClInclude Include="cpu_newrec_compiler_aarch32.h" />
|
||||||
<ClInclude Include="achievements_private.h" />
|
<ClInclude Include="achievements_private.h" />
|
||||||
<ClInclude Include="justifier.h" />
|
<ClInclude Include="justifier.h" />
|
||||||
|
<<<<<<< Updated upstream
|
||||||
<ClInclude Include="pine_server.h" />
|
<ClInclude Include="pine_server.h" />
|
||||||
<ClInclude Include="gdb_server.h" />
|
<ClInclude Include="gdb_server.h" />
|
||||||
|
=======
|
||||||
|
<ClInclude Include="scriptengine.h" />
|
||||||
|
>>>>>>> Stashed changes
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</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="qtkeycodes.cpp" />
|
||||||
<ClCompile Include="qtprogresscallback.cpp" />
|
<ClCompile Include="qtprogresscallback.cpp" />
|
||||||
<ClCompile Include="qtutils.cpp" />
|
<ClCompile Include="qtutils.cpp" />
|
||||||
|
<ClCompile Include="scriptconsole.cpp" />
|
||||||
<ClCompile Include="selectdiscdialog.cpp" />
|
<ClCompile Include="selectdiscdialog.cpp" />
|
||||||
<ClCompile Include="settingswindow.cpp" />
|
<ClCompile Include="settingswindow.cpp" />
|
||||||
<ClCompile Include="setupwizarddialog.cpp" />
|
<ClCompile Include="setupwizarddialog.cpp" />
|
||||||
@ -89,6 +90,7 @@
|
|||||||
<ClInclude Include="pch.h" />
|
<ClInclude Include="pch.h" />
|
||||||
<ClInclude Include="resource.h" />
|
<ClInclude Include="resource.h" />
|
||||||
<QtMoc Include="selectdiscdialog.h" />
|
<QtMoc Include="selectdiscdialog.h" />
|
||||||
|
<QtMoc Include="scriptconsole.h" />
|
||||||
<ClInclude Include="settingwidgetbinder.h" />
|
<ClInclude Include="settingwidgetbinder.h" />
|
||||||
<QtMoc Include="consolesettingswidget.h" />
|
<QtMoc Include="consolesettingswidget.h" />
|
||||||
<QtMoc Include="emulationsettingswidget.h" />
|
<QtMoc Include="emulationsettingswidget.h" />
|
||||||
@ -257,6 +259,7 @@
|
|||||||
<ClCompile Include="$(IntDir)moc_selectdiscdialog.cpp" />
|
<ClCompile Include="$(IntDir)moc_selectdiscdialog.cpp" />
|
||||||
<ClCompile Include="$(IntDir)moc_qthost.cpp" />
|
<ClCompile Include="$(IntDir)moc_qthost.cpp" />
|
||||||
<ClCompile Include="$(IntDir)moc_qtprogresscallback.cpp" />
|
<ClCompile Include="$(IntDir)moc_qtprogresscallback.cpp" />
|
||||||
|
<ClCompile Include="$(IntDir)moc_scriptconsole.cpp" />
|
||||||
<ClCompile Include="$(IntDir)moc_settingswindow.cpp" />
|
<ClCompile Include="$(IntDir)moc_settingswindow.cpp" />
|
||||||
<ClCompile Include="$(IntDir)moc_setupwizarddialog.cpp" />
|
<ClCompile Include="$(IntDir)moc_setupwizarddialog.cpp" />
|
||||||
<ClCompile Include="$(IntDir)qrc_resources.cpp" />
|
<ClCompile Include="$(IntDir)qrc_resources.cpp" />
|
||||||
|
@ -179,6 +179,10 @@
|
|||||||
<Filter>moc</Filter>
|
<Filter>moc</Filter>
|
||||||
</ClCompile>
|
</ClCompile>
|
||||||
<ClCompile Include="vcruntimecheck.cpp" />
|
<ClCompile Include="vcruntimecheck.cpp" />
|
||||||
|
<ClCompile Include="scriptconsole.cpp" />
|
||||||
|
<ClCompile Include="$(IntDir)moc_scriptconsole.cpp">
|
||||||
|
<Filter>moc</Filter>
|
||||||
|
</ClCompile>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ClInclude Include="qtutils.h" />
|
<ClInclude Include="qtutils.h" />
|
||||||
@ -242,6 +246,7 @@
|
|||||||
<QtMoc Include="graphicssettingswidget.h" />
|
<QtMoc Include="graphicssettingswidget.h" />
|
||||||
<QtMoc Include="memoryscannerwindow.h" />
|
<QtMoc Include="memoryscannerwindow.h" />
|
||||||
<QtMoc Include="selectdiscdialog.h" />
|
<QtMoc Include="selectdiscdialog.h" />
|
||||||
|
<QtMoc Include="scriptconsole.h" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<QtUi Include="consolesettingswidget.ui" />
|
<QtUi Include="consolesettingswidget.ui" />
|
||||||
|
@ -20,6 +20,7 @@
|
|||||||
#include "selectdiscdialog.h"
|
#include "selectdiscdialog.h"
|
||||||
#include "settingswindow.h"
|
#include "settingswindow.h"
|
||||||
#include "settingwidgetbinder.h"
|
#include "settingwidgetbinder.h"
|
||||||
|
#include "scriptconsole.h"
|
||||||
|
|
||||||
#include "core/achievements.h"
|
#include "core/achievements.h"
|
||||||
#include "core/game_list.h"
|
#include "core/game_list.h"
|
||||||
@ -181,6 +182,8 @@ void MainWindow::initialize()
|
|||||||
CocoaTools::AddThemeChangeHandler(this,
|
CocoaTools::AddThemeChangeHandler(this,
|
||||||
[](void* ctx) { QtHost::RunOnUIThread([] { g_main_window->updateTheme(); }); });
|
[](void* ctx) { QtHost::RunOnUIThread([] { g_main_window->updateTheme(); }); });
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
ScriptConsole::updateSettings();
|
||||||
}
|
}
|
||||||
|
|
||||||
void MainWindow::reportError(const QString& title, const QString& message)
|
void MainWindow::reportError(const QString& title, const QString& message)
|
||||||
@ -770,6 +773,7 @@ void MainWindow::destroySubWindows()
|
|||||||
|
|
||||||
SettingsWindow::closeGamePropertiesDialogs();
|
SettingsWindow::closeGamePropertiesDialogs();
|
||||||
|
|
||||||
|
ScriptConsole::destroy();
|
||||||
LogWindow::destroy();
|
LogWindow::destroy();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -21,6 +21,7 @@
|
|||||||
#include "core/host.h"
|
#include "core/host.h"
|
||||||
#include "core/imgui_overlays.h"
|
#include "core/imgui_overlays.h"
|
||||||
#include "core/memory_card.h"
|
#include "core/memory_card.h"
|
||||||
|
#include "core/scriptengine.h"
|
||||||
#include "core/spu.h"
|
#include "core/spu.h"
|
||||||
#include "core/system.h"
|
#include "core/system.h"
|
||||||
|
|
||||||
@ -1735,6 +1736,8 @@ void EmuThread::run()
|
|||||||
createBackgroundControllerPollTimer();
|
createBackgroundControllerPollTimer();
|
||||||
startBackgroundControllerPollTimer();
|
startBackgroundControllerPollTimer();
|
||||||
|
|
||||||
|
ScriptEngine::Initialize(nullptr);
|
||||||
|
|
||||||
// main loop
|
// main loop
|
||||||
while (!m_shutdown_flag)
|
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