Feature: Add scripting interface

This commit is contained in:
Stenzek 2024-06-16 20:48:20 +10:00
parent 8c1228a7aa
commit 5f7037f347
No known key found for this signature in database
11 changed files with 904 additions and 0 deletions

View File

@ -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>

View File

@ -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" />

View File

@ -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
View 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
View 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

View File

@ -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" />

View File

@ -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" />

View File

@ -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();
}

View File

@ -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)
{

View 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);
}
}

View 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;