mirror of
https://github.com/RPCSX/rpcsx-ui.git
synced 2026-01-31 01:05:23 +01:00
kit: add C++ generator, add native-explorer extension
extension-host: load local extensions
This commit is contained in:
10
.github/dependabot.yml
vendored
10
.github/dependabot.yml
vendored
@@ -8,6 +8,14 @@ updates:
|
||||
deps:
|
||||
patterns:
|
||||
- "*"
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/electron"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
groups:
|
||||
deps:
|
||||
patterns:
|
||||
- "*"
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/"
|
||||
schedule:
|
||||
@@ -15,4 +23,4 @@ updates:
|
||||
groups:
|
||||
deps:
|
||||
patterns:
|
||||
- "*"
|
||||
- "*"
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -17,3 +17,4 @@ expo-env.d.ts
|
||||
.expo/
|
||||
android/
|
||||
/electron/out/
|
||||
extensions/cpp/rpcsx-ui
|
||||
|
||||
2
extensions/.gitignore
vendored
Normal file
2
extensions/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
build*/
|
||||
.cache/
|
||||
7
extensions/cpp/CMakeLists.txt
Normal file
7
extensions/cpp/CMakeLists.txt
Normal file
@@ -0,0 +1,7 @@
|
||||
cmake_minimum_required(VERSION 3.30)
|
||||
project(rpcsx-ui-extensions)
|
||||
set(CMAKE_CXX_STANDARD 23)
|
||||
|
||||
add_subdirectory(rpcsx-ui)
|
||||
add_subdirectory(rpcsx-ui-cpp)
|
||||
add_subdirectory(explorer)
|
||||
4
extensions/cpp/explorer/CMakeLists.txt
Normal file
4
extensions/cpp/explorer/CMakeLists.txt
Normal file
@@ -0,0 +1,4 @@
|
||||
add_rpcsx_extension(native-explorer 0.1.0
|
||||
src/extension.cpp
|
||||
src/sfo.cpp
|
||||
)
|
||||
29
extensions/cpp/explorer/extension.json
Normal file
29
extensions/cpp/explorer/extension.json
Normal file
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"name": [
|
||||
{
|
||||
"text": "@EXTENSION_NAME@"
|
||||
}
|
||||
],
|
||||
"version": "@EXTENSION_VERSION@",
|
||||
"executable": "@EXTENSION_EXECUTABLE@",
|
||||
"args": [],
|
||||
"launcher": {
|
||||
"type": "@EXTENSION_TARGET@",
|
||||
"requirements": {}
|
||||
},
|
||||
"contributions": {
|
||||
"settings": {
|
||||
"locations": {
|
||||
"type": "array",
|
||||
"required": true,
|
||||
"description": "Search locations",
|
||||
"minItems": 1,
|
||||
"items": {
|
||||
"type": "path",
|
||||
"mustExist": true,
|
||||
"entity": "directory"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
217
extensions/cpp/explorer/src/extension.cpp
Normal file
217
extensions/cpp/explorer/src/extension.cpp
Normal file
@@ -0,0 +1,217 @@
|
||||
#include "./sfo.hpp"
|
||||
#include <print>
|
||||
#include <rpcsx/ui/extension.hpp>
|
||||
#include <thread>
|
||||
|
||||
using namespace rpcsx::ui;
|
||||
|
||||
static std::size_t calcDirectorySize(const std::filesystem::path &path) {
|
||||
std::uint64_t result = 0;
|
||||
|
||||
for (auto dir : std::filesystem::recursive_directory_iterator(path)) {
|
||||
if (dir.is_regular_file()) {
|
||||
result += std::filesystem::file_size(dir);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
static std::optional<ExplorerItem>
|
||||
tryFetchFw(const std::filesystem::directory_entry &entry) {
|
||||
if (!std::filesystem::is_regular_file(entry.path() / "mini-syscore.elf")) {
|
||||
return {};
|
||||
}
|
||||
|
||||
if (!std::filesystem::is_regular_file(entry.path() / "safemode.elf")) {
|
||||
return {};
|
||||
}
|
||||
|
||||
if (!std::filesystem::is_regular_file(entry.path() / "system" / "sys" /
|
||||
"SceSysCore.elf")) {
|
||||
return {};
|
||||
}
|
||||
|
||||
if (!std::filesystem::is_regular_file(entry.path() / "system" / "sys" /
|
||||
"orbis_audiod.elf")) {
|
||||
return {};
|
||||
}
|
||||
|
||||
if (std::filesystem::is_regular_file(entry.path() / "system" / "sys" /
|
||||
"GnmCompositor.elf")) {
|
||||
return ExplorerItem{
|
||||
.type = "firmware",
|
||||
.name = {LocalizedString{
|
||||
.text = "PS4 Firmware",
|
||||
}},
|
||||
.location = "file://" + entry.path().string(),
|
||||
.size = calcDirectorySize(entry.path()),
|
||||
.launcher =
|
||||
LauncherInfo{
|
||||
.type = "dir-ps4-fw",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (std::filesystem::is_regular_file(entry.path() / "system" / "sys" /
|
||||
"AgcCompositor.elf")) {
|
||||
return ExplorerItem{
|
||||
.type = "firmware",
|
||||
.name = {LocalizedString{
|
||||
.text = "PS5 Firmware",
|
||||
}},
|
||||
.location = "file://" + entry.path().string(),
|
||||
.size = calcDirectorySize(entry.path()),
|
||||
.launcher =
|
||||
LauncherInfo{
|
||||
.type = "dir-ps5-fw",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
static std::optional<ExplorerItem>
|
||||
tryFetchGame(const std::filesystem::directory_entry &entry) {
|
||||
if (!entry.is_directory()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
auto sysPath = entry.path() / "sce_sys";
|
||||
auto paramSfoPath = sysPath / "param.sfo";
|
||||
|
||||
if (!std::filesystem::is_regular_file(entry.path() / "eboot.bin")) {
|
||||
return {};
|
||||
}
|
||||
|
||||
if (!std::filesystem::is_regular_file(paramSfoPath)) {
|
||||
return {};
|
||||
}
|
||||
|
||||
auto data = sfo::load(paramSfoPath);
|
||||
if (data.errc != sfo::error::ok) {
|
||||
std::println(stderr, "{}: error {}", entry.path().string(), data.errc);
|
||||
return {};
|
||||
}
|
||||
|
||||
auto category = sfo::get_string(data.sfo, "CATEGORY");
|
||||
|
||||
if (category == "gdd" || category == "gdf" || category == "gdp" ||
|
||||
category == "gdg") {
|
||||
return {};
|
||||
}
|
||||
|
||||
ExplorerItem info;
|
||||
|
||||
auto name = sfo::get_string(data.sfo, "TITLE");
|
||||
if (name.empty()) {
|
||||
name = sfo::get_string(data.sfo, "TITLE_ID");
|
||||
|
||||
if (name.empty()) {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
info.name = {LocalizedString{.text = std::string(name)}};
|
||||
|
||||
info.titleId = sfo::get_string(data.sfo, "TITLE_ID");
|
||||
info.version = sfo::get_string(data.sfo, "APP_VER");
|
||||
|
||||
if (info.version->empty()) {
|
||||
info.version = sfo::get_string(data.sfo, "VERSION", "1.0");
|
||||
}
|
||||
|
||||
if (std::filesystem::is_regular_file(sysPath / "icon0.png")) {
|
||||
info.icon = {
|
||||
LocalizedIcon{.uri = "file://" + (sysPath / "icon0.png").string()}};
|
||||
}
|
||||
|
||||
info.size = calcDirectorySize(entry.path());
|
||||
info.type = "game";
|
||||
info.launcher = LauncherInfo{
|
||||
.type = "fself-ps4-orbis" // FIXME: self/elf? ps3? ps5?
|
||||
// "fself-ps5-prospero"
|
||||
};
|
||||
info.location = "file://" + entry.path().string();
|
||||
return std::move(info);
|
||||
}
|
||||
|
||||
struct ExplorerExtension : rpcsx::ui::Extension<rpcsx::ui::Explorer> {
|
||||
std::jthread explorerThread;
|
||||
std::vector<std::string> locations;
|
||||
|
||||
using Base::Base;
|
||||
|
||||
Response<Initialize> handle(const Request<Initialize> &) override {
|
||||
return {};
|
||||
}
|
||||
|
||||
Response<Activate> handle(const Request<Activate> &request) override {
|
||||
std::fprintf(stderr, "activate request, settings = %s\n",
|
||||
json(request.settings).dump().c_str());
|
||||
|
||||
settingsGet({.path = "/"},
|
||||
[](const rpcsx::ui::SettingsGetResponse &response) {
|
||||
std::fprintf(stderr, "settings: schema: %s\n",
|
||||
nlohmann::json(response.schema).dump().c_str());
|
||||
std::fprintf(stderr, "settings: value: %s\n",
|
||||
response.value.dump().c_str());
|
||||
});
|
||||
|
||||
if (!request.settings.contains("locations")) {
|
||||
return {};
|
||||
}
|
||||
|
||||
locations = request.settings.at("locations");
|
||||
|
||||
explorerThread = std::jthread([this](std::stop_token token) {
|
||||
ExplorerItem batchItems[8];
|
||||
std::size_t batchSize = 0;
|
||||
|
||||
auto flush = [&] {
|
||||
if (batchSize > 0) {
|
||||
this->explorerAdd({.items = {batchItems, batchItems + batchSize}});
|
||||
batchSize = 0;
|
||||
}
|
||||
};
|
||||
|
||||
auto submit = [&](ExplorerItem item) {
|
||||
if (batchSize >= std::size(batchItems)) {
|
||||
flush();
|
||||
}
|
||||
|
||||
batchItems[batchSize++] = std::move(item);
|
||||
};
|
||||
|
||||
for (auto &location : locations) {
|
||||
for (auto &entry :
|
||||
std::filesystem::recursive_directory_iterator(location)) {
|
||||
if (token.stop_requested()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (auto game = tryFetchGame(entry)) {
|
||||
submit(std::move(*game));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (auto fw = tryFetchFw(entry)) {
|
||||
submit(std::move(*fw));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
flush();
|
||||
});
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
Response<Shutdown> handle(const Request<Shutdown> &) override { return {}; }
|
||||
};
|
||||
|
||||
auto extension_main() {
|
||||
return rpcsx::ui::createExtension<ExplorerExtension>();
|
||||
}
|
||||
280
extensions/cpp/explorer/src/sfo.cpp
Normal file
280
extensions/cpp/explorer/src/sfo.cpp
Normal file
@@ -0,0 +1,280 @@
|
||||
#include "sfo.hpp"
|
||||
#include "rpcsx/ui/file.hpp"
|
||||
#include "rpcsx/ui/format.hpp"
|
||||
#include "rpcsx/ui/log.hpp"
|
||||
#include <cassert>
|
||||
#include <cstring>
|
||||
#include <print>
|
||||
#include <source_location>
|
||||
#include <span>
|
||||
#include <utility>
|
||||
|
||||
using namespace rpcsx::ui;
|
||||
|
||||
template <typename T> using le_t = T;
|
||||
|
||||
struct header_t {
|
||||
le_t<std::uint32_t> magic;
|
||||
le_t<std::uint32_t> version;
|
||||
le_t<std::uint32_t> off_key_table;
|
||||
le_t<std::uint32_t> off_data_table;
|
||||
le_t<std::uint32_t> entries_num;
|
||||
};
|
||||
|
||||
static_assert(sizeof(header_t) == 20);
|
||||
|
||||
struct def_table_t {
|
||||
le_t<std::uint16_t> key_off;
|
||||
le_t<sfo::format> param_fmt;
|
||||
le_t<std::uint32_t> param_len;
|
||||
le_t<std::uint32_t> param_max;
|
||||
le_t<std::uint32_t> data_off;
|
||||
};
|
||||
static_assert(sizeof(def_table_t) == 16);
|
||||
|
||||
sfo::entry::entry(format type, std::uint32_t max_size, std::string_view value,
|
||||
bool allow_truncate) noexcept
|
||||
: m_format(type), m_max_size(max_size), m_value_string(value) {
|
||||
assert(type == format::string || type == format::array);
|
||||
assert(max_size > (type == format::string ? 1u : 0u));
|
||||
|
||||
if (allow_truncate && value.size() > max(false)) {
|
||||
m_value_string.resize(max(false));
|
||||
}
|
||||
}
|
||||
|
||||
sfo::entry::entry(std::uint32_t value) noexcept
|
||||
: m_format(format::integer), m_max_size(sizeof(std::uint32_t)),
|
||||
m_value_integer(value) {}
|
||||
|
||||
const std::string &sfo::entry::as_string() const {
|
||||
assert(m_format == format::string || m_format == format::array);
|
||||
return m_value_string;
|
||||
}
|
||||
|
||||
std::uint32_t sfo::entry::as_integer() const {
|
||||
assert(m_format == format::integer);
|
||||
return m_value_integer;
|
||||
}
|
||||
|
||||
sfo::entry &sfo::entry::operator=(std::string_view value) {
|
||||
assert(m_format == format::string || m_format == format::array);
|
||||
m_value_string = value;
|
||||
return *this;
|
||||
}
|
||||
|
||||
sfo::entry &sfo::entry::operator=(std::uint32_t value) {
|
||||
assert(m_format == format::integer);
|
||||
m_value_integer = value;
|
||||
return *this;
|
||||
}
|
||||
|
||||
std::uint32_t sfo::entry::size() const {
|
||||
switch (m_format) {
|
||||
case format::string:
|
||||
case format::array:
|
||||
return std::min(m_max_size, static_cast<std::uint32_t>(
|
||||
m_value_string.size() +
|
||||
(m_format == format::string ? 1 : 0)));
|
||||
|
||||
case format::integer:
|
||||
return sizeof(std::uint32_t);
|
||||
}
|
||||
|
||||
fatal("sfo: invalid format ({})", m_format);
|
||||
}
|
||||
|
||||
bool sfo::entry::is_valid() const {
|
||||
switch (m_format) {
|
||||
case format::string:
|
||||
case format::array:
|
||||
return m_value_string.size() <= this->max(false);
|
||||
|
||||
case format::integer:
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
sfo::load_result_t sfo::load(ReadableByteStream stream,
|
||||
std::string_view filename) {
|
||||
load_result_t result{};
|
||||
|
||||
#define PSF_CHECK(cond, err) \
|
||||
if (!static_cast<bool>(cond)) { \
|
||||
if (true || err != error::stream) \
|
||||
elog("sfo: Error loading '{}': {}. {}", filename, err, \
|
||||
std::source_location::current()); \
|
||||
result.sfo.clear(); \
|
||||
result.errc = err; \
|
||||
return result; \
|
||||
}
|
||||
|
||||
auto originalStream = stream;
|
||||
PSF_CHECK(!stream.empty(), error::stream);
|
||||
|
||||
// Get header
|
||||
header_t header;
|
||||
PSF_CHECK(stream.read(header), error::not_psf);
|
||||
|
||||
// Check magic and version
|
||||
le_t<std::uint32_t> expMagic;
|
||||
std::memcpy(&expMagic, "\0PSF", sizeof(expMagic));
|
||||
|
||||
PSF_CHECK(header.magic == expMagic, error::not_psf);
|
||||
PSF_CHECK(header.version == 0x101u, error::not_psf);
|
||||
PSF_CHECK(header.off_key_table >= sizeof(header_t), error::corrupt);
|
||||
PSF_CHECK(header.off_key_table <= header.off_data_table, error::corrupt);
|
||||
PSF_CHECK(header.off_data_table <= stream.size(), error::corrupt);
|
||||
|
||||
// Get indices
|
||||
std::vector<def_table_t> indices;
|
||||
PSF_CHECK(stream.read(indices, header.entries_num), error::corrupt);
|
||||
|
||||
// Get keys
|
||||
std::string keys;
|
||||
PSF_CHECK(originalStream.size() > header.off_key_table, error::corrupt);
|
||||
|
||||
stream = originalStream.subspan(header.off_key_table);
|
||||
PSF_CHECK(stream.read(keys, header.off_data_table - header.off_key_table),
|
||||
error::corrupt);
|
||||
|
||||
// Load entries
|
||||
for (std::uint32_t i = 0; i < header.entries_num; ++i) {
|
||||
PSF_CHECK(indices[i].key_off < header.off_data_table - header.off_key_table,
|
||||
error::corrupt);
|
||||
|
||||
// Get key name (null-terminated string)
|
||||
std::string_view key(keys.data() + indices[i].key_off);
|
||||
|
||||
// Check entry
|
||||
PSF_CHECK(!result.sfo.contains(key), error::corrupt);
|
||||
PSF_CHECK(indices[i].param_len <= indices[i].param_max, error::corrupt);
|
||||
PSF_CHECK(indices[i].data_off <
|
||||
originalStream.size() - header.off_data_table,
|
||||
error::corrupt);
|
||||
PSF_CHECK(indices[i].param_max <
|
||||
originalStream.size() - indices[i].data_off,
|
||||
error::corrupt);
|
||||
|
||||
// Seek data pointer
|
||||
PSF_CHECK(originalStream.size() >
|
||||
header.off_data_table + indices[i].data_off,
|
||||
error::corrupt);
|
||||
stream =
|
||||
originalStream.subspan(header.off_data_table + indices[i].data_off);
|
||||
|
||||
if (indices[i].param_fmt == format::integer &&
|
||||
indices[i].param_max == sizeof(std::uint32_t) &&
|
||||
indices[i].param_len == sizeof(std::uint32_t)) {
|
||||
// Integer data
|
||||
le_t<std::uint32_t> value;
|
||||
PSF_CHECK(stream.read(value), error::corrupt);
|
||||
|
||||
result.sfo.emplace(std::piecewise_construct,
|
||||
std::forward_as_tuple(std::move(key)),
|
||||
std::forward_as_tuple(value));
|
||||
} else if (indices[i].param_fmt == format::string ||
|
||||
indices[i].param_fmt == format::array) {
|
||||
// String/array data
|
||||
std::string value;
|
||||
PSF_CHECK(stream.read(value, indices[i].param_len), error::corrupt);
|
||||
|
||||
if (indices[i].param_fmt == format::string) {
|
||||
// Find null terminator
|
||||
value.resize(std::strlen(value.c_str()));
|
||||
}
|
||||
|
||||
result.sfo.emplace(
|
||||
std::piecewise_construct, std::forward_as_tuple(std::move(key)),
|
||||
std::forward_as_tuple(indices[i].param_fmt, indices[i].param_max,
|
||||
std::move(value)));
|
||||
} else {
|
||||
// Possibly unsupported format, entry ignored
|
||||
elog("sfo: Unknown entry format (key='{}', fmt={}, len=0x{:x}, "
|
||||
"max=0x{:x})",
|
||||
key, indices[i].param_fmt, indices[i].param_len,
|
||||
indices[i].param_max);
|
||||
}
|
||||
}
|
||||
|
||||
#undef PSF_CHECK
|
||||
return result;
|
||||
}
|
||||
|
||||
sfo::load_result_t sfo::load(const std::string &filename) {
|
||||
auto file = File::open(filename);
|
||||
if (!file.has_value()) {
|
||||
std::println(stderr, "file open error {}", file.error().message());
|
||||
return {{}, error::stream};
|
||||
}
|
||||
|
||||
auto data = file->map();
|
||||
|
||||
if (!data.has_value()) {
|
||||
std::println(stderr, "file map error {}", data.error().message());
|
||||
return {{}, error::stream};
|
||||
}
|
||||
|
||||
return load(data.value(), filename);
|
||||
}
|
||||
|
||||
std::string_view sfo::get_string(const registry &psf, std::string_view key,
|
||||
std::string_view def) {
|
||||
const auto found = psf.find(key);
|
||||
|
||||
if (found == psf.end() || (found->second.type() != format::string &&
|
||||
found->second.type() != format::array)) {
|
||||
return def;
|
||||
}
|
||||
|
||||
return found->second.as_string();
|
||||
}
|
||||
|
||||
std::uint32_t sfo::get_integer(const registry &psf, std::string_view key,
|
||||
std::uint32_t def) {
|
||||
const auto found = psf.find(key);
|
||||
|
||||
if (found == psf.end() || found->second.type() != format::integer) {
|
||||
return def;
|
||||
}
|
||||
|
||||
return found->second.as_integer();
|
||||
}
|
||||
|
||||
bool sfo::check_registry(
|
||||
const registry &psf,
|
||||
std::function<bool(bool ok, const std::string &key, const entry &value)>
|
||||
validate,
|
||||
std::source_location src_loc) {
|
||||
bool psf_ok = true;
|
||||
|
||||
for (const auto &[key, value] : psf) {
|
||||
bool entry_ok = value.is_valid();
|
||||
|
||||
if (validate) {
|
||||
// Validate against a custom condition as well (forward error)
|
||||
if (!validate(entry_ok, key, value)) {
|
||||
entry_ok = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!entry_ok) {
|
||||
if (value.type() == format::string) {
|
||||
elog("sfo: {}: Entry '{}' is invalid: string='{}'", src_loc, key,
|
||||
value.as_string());
|
||||
} else {
|
||||
// TODO: Better logging of other types
|
||||
elog("sfo: {}: Entry {} is invalid", src_loc, key, value.as_string());
|
||||
}
|
||||
}
|
||||
|
||||
if (!entry_ok) {
|
||||
// Do not break, run over all entries in order to report all errors
|
||||
psf_ok = false;
|
||||
}
|
||||
}
|
||||
|
||||
return psf_ok;
|
||||
}
|
||||
256
extensions/cpp/explorer/src/sfo.hpp
Normal file
256
extensions/cpp/explorer/src/sfo.hpp
Normal file
@@ -0,0 +1,256 @@
|
||||
#pragma once
|
||||
|
||||
#include "rpcsx/ui/file.hpp"
|
||||
#include "rpcsx/ui/refl.hpp"
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <format>
|
||||
#include <functional>
|
||||
#include <map>
|
||||
#include <source_location>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
namespace sfo {
|
||||
enum sound_format_flag : std::int32_t {
|
||||
lpcm_2 = 1 << 0, // Linear PCM 2 Ch.
|
||||
lpcm_5_1 = 1 << 2, // Linear PCM 5.1 Ch.
|
||||
lpcm_7_1 = 1 << 4, // Linear PCM 7.1 Ch.
|
||||
ac3 = 1 << 8, // Dolby Digital 5.1 Ch.
|
||||
dts = 1 << 9, // DTS 5.1 Ch.
|
||||
};
|
||||
|
||||
enum resolution_flag : std::int32_t {
|
||||
_480 = 1 << 0,
|
||||
_576 = 1 << 1,
|
||||
_720 = 1 << 2,
|
||||
_1080 = 1 << 3,
|
||||
_480_16_9 = 1 << 4,
|
||||
_576_16_9 = 1 << 5,
|
||||
};
|
||||
|
||||
enum class format : std::uint16_t {
|
||||
array = 0x0004, // claimed to be a non-NTS string (char array)
|
||||
string = 0x0204,
|
||||
integer = 0x0404,
|
||||
};
|
||||
|
||||
enum class error {
|
||||
ok,
|
||||
stream,
|
||||
not_psf,
|
||||
corrupt,
|
||||
};
|
||||
|
||||
class entry final {
|
||||
format m_format{};
|
||||
std::uint32_t
|
||||
m_max_size{}; // Entry max size (supplementary info, stored in PSF format)
|
||||
std::uint32_t m_value_integer{}; // TODO: is it really unsigned?
|
||||
std::string m_value_string{};
|
||||
|
||||
public:
|
||||
// Construct string entry, assign the value
|
||||
entry(format type, std::uint32_t max_size, std::string_view value,
|
||||
bool allow_truncate = false) noexcept;
|
||||
|
||||
// Construct integer entry, assign the value
|
||||
entry(std::uint32_t value) noexcept;
|
||||
|
||||
~entry() = default;
|
||||
|
||||
const std::string &as_string() const;
|
||||
std::uint32_t as_integer() const;
|
||||
|
||||
entry &operator=(std::string_view value);
|
||||
entry &operator=(std::uint32_t value);
|
||||
|
||||
format type() const { return m_format; }
|
||||
std::uint32_t max(bool with_nts) const {
|
||||
return m_max_size - (!with_nts && m_format == format::string ? 1 : 0);
|
||||
}
|
||||
std::uint32_t size() const;
|
||||
bool is_valid() const;
|
||||
};
|
||||
|
||||
// Define PSF registry as a sorted map of entries:
|
||||
using registry = std::map<std::string, entry, std::less<>>;
|
||||
|
||||
struct load_result_t {
|
||||
registry sfo;
|
||||
error errc;
|
||||
|
||||
explicit operator bool() const { return !sfo.empty(); }
|
||||
};
|
||||
|
||||
// Load PSF registry from SFO binary format
|
||||
load_result_t load(rpcsx::ui::ReadableByteStream data,
|
||||
std::string_view filename);
|
||||
load_result_t load(const std::string &filename);
|
||||
inline registry load_object(rpcsx::ui::ReadableByteStream data,
|
||||
std::string_view filename) {
|
||||
return load(data, filename).sfo;
|
||||
}
|
||||
|
||||
inline registry load_object(const std::string &filename) {
|
||||
return load(filename).sfo;
|
||||
}
|
||||
|
||||
// Convert PSF registry to SFO binary format
|
||||
std::vector<std::uint8_t>
|
||||
save_object(const registry &,
|
||||
std::vector<std::uint8_t> &&init = std::vector<std::uint8_t>{});
|
||||
|
||||
// Get string value or default value
|
||||
std::string_view get_string(const registry &psf, std::string_view key,
|
||||
std::string_view def = {});
|
||||
|
||||
// Get integer value or default value
|
||||
std::uint32_t get_integer(const registry &psf, std::string_view key,
|
||||
std::uint32_t def = 0);
|
||||
|
||||
bool check_registry(
|
||||
const registry &psf,
|
||||
std::function<bool(bool ok, const std::string &key, const entry &value)>
|
||||
validate = {},
|
||||
std::source_location src_loc = std::source_location::current());
|
||||
|
||||
// Assign new entry
|
||||
inline void assign(registry &psf, std::string_view key, entry &&_entry) {
|
||||
const auto found = psf.find(key);
|
||||
|
||||
if (found == psf.end()) {
|
||||
psf.emplace(key, std::move(_entry));
|
||||
return;
|
||||
}
|
||||
|
||||
found->second = std::move(_entry);
|
||||
return;
|
||||
}
|
||||
|
||||
// Make string entry
|
||||
inline entry string(std::uint32_t max_size, std::string_view value,
|
||||
bool allow_truncate = false) {
|
||||
return {format::string, max_size, value, allow_truncate};
|
||||
}
|
||||
|
||||
// Make string entry (from char[N])
|
||||
template <std::size_t CharN>
|
||||
inline entry string(std::uint32_t max_size, char (&value_array)[CharN],
|
||||
bool allow_truncate = false) {
|
||||
std::string_view value{value_array, CharN};
|
||||
value = value.substr(
|
||||
0, std::min<std::size_t>(value.find_first_of('\0'), value.size()));
|
||||
return string(max_size, value, allow_truncate);
|
||||
}
|
||||
|
||||
// Make array entry
|
||||
inline entry array(std::uint32_t max_size, std::string_view value) {
|
||||
return {format::array, max_size, value};
|
||||
}
|
||||
|
||||
// Checks if of HDD category (assumes a valid category is being passed)
|
||||
constexpr bool is_cat_hdd(std::string_view cat) {
|
||||
return cat.size() == 2u && cat[1] != 'D' && cat != "DG" && cat != "MS";
|
||||
}
|
||||
} // namespace sfo
|
||||
|
||||
template <> struct std::formatter<sfo::format> {
|
||||
constexpr std::format_parse_context::iterator
|
||||
parse(std::format_parse_context &ctx) {
|
||||
return ctx.begin();
|
||||
}
|
||||
|
||||
constexpr std::format_context::iterator
|
||||
format(sfo::format format, std::format_context &ctx) const {
|
||||
std::string_view name;
|
||||
switch (format) {
|
||||
case sfo::format::array:
|
||||
name = rpcsx::ui::getNameOf<sfo::format::array>();
|
||||
break;
|
||||
case sfo::format::string:
|
||||
name = rpcsx::ui::getNameOf<sfo::format::string>();
|
||||
break;
|
||||
case sfo::format::integer:
|
||||
name = rpcsx::ui::getNameOf<sfo::format::integer>();
|
||||
break;
|
||||
}
|
||||
|
||||
if (name.empty()) {
|
||||
std::format_to(ctx.out(), "({}){:#x}",
|
||||
rpcsx::ui::getNameOf<sfo::format>(),
|
||||
std::to_underlying(format));
|
||||
return ctx.out();
|
||||
}
|
||||
|
||||
std::format_to(ctx.out(), "{}", name);
|
||||
return ctx.out();
|
||||
}
|
||||
};
|
||||
|
||||
template <> struct std::formatter<std::source_location> {
|
||||
constexpr std::format_parse_context::iterator
|
||||
parse(std::format_parse_context &ctx) {
|
||||
return ctx.begin();
|
||||
}
|
||||
|
||||
constexpr std::format_context::iterator
|
||||
format(const std::source_location &location, std::format_context &ctx) const {
|
||||
std::format_to(ctx.out(), "{}:{}", location.file_name(), location.line());
|
||||
return ctx.out();
|
||||
}
|
||||
};
|
||||
|
||||
template <> struct std::formatter<sfo::registry> {
|
||||
constexpr std::format_parse_context::iterator
|
||||
parse(std::format_parse_context &ctx) {
|
||||
return ctx.begin();
|
||||
}
|
||||
|
||||
constexpr std::format_context::iterator
|
||||
format(const sfo::registry ®istry, std::format_context &ctx) const {
|
||||
for (const auto &entry : registry) {
|
||||
if (entry.second.type() == sfo::format::array) {
|
||||
// Format them last
|
||||
continue;
|
||||
}
|
||||
|
||||
std::format_to(ctx.out(), "{}: ", entry.first);
|
||||
|
||||
const sfo::entry &data = entry.second;
|
||||
|
||||
if (data.type() == sfo::format::integer) {
|
||||
std::format_to(ctx.out(), "0x{:x}\n", data.as_integer());
|
||||
} else {
|
||||
std::format_to(ctx.out(), "\"{}\"\n", data.as_string());
|
||||
}
|
||||
}
|
||||
|
||||
for (const auto &entry : registry) {
|
||||
if (entry.second.type() != sfo::format::array) {
|
||||
// Formatted before
|
||||
continue;
|
||||
}
|
||||
|
||||
std::format_to(ctx.out(), "{}: [", entry.first);
|
||||
|
||||
for (bool first = true; auto byte : std::span<const std::uint8_t>(
|
||||
reinterpret_cast<const std::uint8_t *>(
|
||||
entry.second.as_string().data()),
|
||||
entry.second.size())) {
|
||||
if (first) {
|
||||
first = false;
|
||||
} else {
|
||||
std::format_to(ctx.out(), ", ");
|
||||
}
|
||||
|
||||
std::format_to(ctx.out(), "{:x}", byte);
|
||||
}
|
||||
|
||||
std::format_to(ctx.out(), "]\n");
|
||||
}
|
||||
|
||||
return ctx.out();
|
||||
}
|
||||
};
|
||||
3
extensions/cpp/rpcsx-ui-cpp/3rdparty/nlohmann_json/CMakeLists.txt
vendored
Normal file
3
extensions/cpp/rpcsx-ui-cpp/3rdparty/nlohmann_json/CMakeLists.txt
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
add_library(nlohmann_json INTERFACE)
|
||||
target_include_directories(nlohmann_json INTERFACE single_include)
|
||||
add_library(nlohmann::json ALIAS nlohmann_json)
|
||||
21
extensions/cpp/rpcsx-ui-cpp/3rdparty/nlohmann_json/LICENSE.MIT
vendored
Normal file
21
extensions/cpp/rpcsx-ui-cpp/3rdparty/nlohmann_json/LICENSE.MIT
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2013-2025 Niels Lohmann
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
25510
extensions/cpp/rpcsx-ui-cpp/3rdparty/nlohmann_json/single_include/nlohmann/json.hpp
vendored
Normal file
25510
extensions/cpp/rpcsx-ui-cpp/3rdparty/nlohmann_json/single_include/nlohmann/json.hpp
vendored
Normal file
File diff suppressed because it is too large
Load Diff
187
extensions/cpp/rpcsx-ui-cpp/3rdparty/nlohmann_json/single_include/nlohmann/json_fwd.hpp
vendored
Normal file
187
extensions/cpp/rpcsx-ui-cpp/3rdparty/nlohmann_json/single_include/nlohmann/json_fwd.hpp
vendored
Normal file
@@ -0,0 +1,187 @@
|
||||
// __ _____ _____ _____
|
||||
// __| | __| | | | JSON for Modern C++
|
||||
// | | |__ | | | | | | version 3.11.3
|
||||
// |_____|_____|_____|_|___| https://github.com/nlohmann/json
|
||||
//
|
||||
// SPDX-FileCopyrightText: 2013 - 2025 Niels Lohmann <https://nlohmann.me>
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
#ifndef INCLUDE_NLOHMANN_JSON_FWD_HPP_
|
||||
#define INCLUDE_NLOHMANN_JSON_FWD_HPP_
|
||||
|
||||
#include <cstdint> // int64_t, uint64_t
|
||||
#include <map> // map
|
||||
#include <memory> // allocator
|
||||
#include <string> // string
|
||||
#include <vector> // vector
|
||||
|
||||
// #include <nlohmann/detail/abi_macros.hpp>
|
||||
// __ _____ _____ _____
|
||||
// __| | __| | | | JSON for Modern C++
|
||||
// | | |__ | | | | | | version 3.11.3
|
||||
// |_____|_____|_____|_|___| https://github.com/nlohmann/json
|
||||
//
|
||||
// SPDX-FileCopyrightText: 2013 - 2025 Niels Lohmann <https://nlohmann.me>
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
|
||||
|
||||
// This file contains all macro definitions affecting or depending on the ABI
|
||||
|
||||
#ifndef JSON_SKIP_LIBRARY_VERSION_CHECK
|
||||
#if defined(NLOHMANN_JSON_VERSION_MAJOR) && defined(NLOHMANN_JSON_VERSION_MINOR) && defined(NLOHMANN_JSON_VERSION_PATCH)
|
||||
#if NLOHMANN_JSON_VERSION_MAJOR != 3 || NLOHMANN_JSON_VERSION_MINOR != 11 || NLOHMANN_JSON_VERSION_PATCH != 3
|
||||
#warning "Already included a different version of the library!"
|
||||
#endif
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#define NLOHMANN_JSON_VERSION_MAJOR 3 // NOLINT(modernize-macro-to-enum)
|
||||
#define NLOHMANN_JSON_VERSION_MINOR 11 // NOLINT(modernize-macro-to-enum)
|
||||
#define NLOHMANN_JSON_VERSION_PATCH 3 // NOLINT(modernize-macro-to-enum)
|
||||
|
||||
#ifndef JSON_DIAGNOSTICS
|
||||
#define JSON_DIAGNOSTICS 0
|
||||
#endif
|
||||
|
||||
#ifndef JSON_DIAGNOSTIC_POSITIONS
|
||||
#define JSON_DIAGNOSTIC_POSITIONS 0
|
||||
#endif
|
||||
|
||||
#ifndef JSON_USE_LEGACY_DISCARDED_VALUE_COMPARISON
|
||||
#define JSON_USE_LEGACY_DISCARDED_VALUE_COMPARISON 0
|
||||
#endif
|
||||
|
||||
#if JSON_DIAGNOSTICS
|
||||
#define NLOHMANN_JSON_ABI_TAG_DIAGNOSTICS _diag
|
||||
#else
|
||||
#define NLOHMANN_JSON_ABI_TAG_DIAGNOSTICS
|
||||
#endif
|
||||
|
||||
#if JSON_DIAGNOSTIC_POSITIONS
|
||||
#define NLOHMANN_JSON_ABI_TAG_DIAGNOSTIC_POSITIONS _dp
|
||||
#else
|
||||
#define NLOHMANN_JSON_ABI_TAG_DIAGNOSTIC_POSITIONS
|
||||
#endif
|
||||
|
||||
#if JSON_USE_LEGACY_DISCARDED_VALUE_COMPARISON
|
||||
#define NLOHMANN_JSON_ABI_TAG_LEGACY_DISCARDED_VALUE_COMPARISON _ldvcmp
|
||||
#else
|
||||
#define NLOHMANN_JSON_ABI_TAG_LEGACY_DISCARDED_VALUE_COMPARISON
|
||||
#endif
|
||||
|
||||
#ifndef NLOHMANN_JSON_NAMESPACE_NO_VERSION
|
||||
#define NLOHMANN_JSON_NAMESPACE_NO_VERSION 0
|
||||
#endif
|
||||
|
||||
// Construct the namespace ABI tags component
|
||||
#define NLOHMANN_JSON_ABI_TAGS_CONCAT_EX(a, b, c) json_abi ## a ## b ## c
|
||||
#define NLOHMANN_JSON_ABI_TAGS_CONCAT(a, b, c) \
|
||||
NLOHMANN_JSON_ABI_TAGS_CONCAT_EX(a, b, c)
|
||||
|
||||
#define NLOHMANN_JSON_ABI_TAGS \
|
||||
NLOHMANN_JSON_ABI_TAGS_CONCAT( \
|
||||
NLOHMANN_JSON_ABI_TAG_DIAGNOSTICS, \
|
||||
NLOHMANN_JSON_ABI_TAG_LEGACY_DISCARDED_VALUE_COMPARISON, \
|
||||
NLOHMANN_JSON_ABI_TAG_DIAGNOSTIC_POSITIONS)
|
||||
|
||||
// Construct the namespace version component
|
||||
#define NLOHMANN_JSON_NAMESPACE_VERSION_CONCAT_EX(major, minor, patch) \
|
||||
_v ## major ## _ ## minor ## _ ## patch
|
||||
#define NLOHMANN_JSON_NAMESPACE_VERSION_CONCAT(major, minor, patch) \
|
||||
NLOHMANN_JSON_NAMESPACE_VERSION_CONCAT_EX(major, minor, patch)
|
||||
|
||||
#if NLOHMANN_JSON_NAMESPACE_NO_VERSION
|
||||
#define NLOHMANN_JSON_NAMESPACE_VERSION
|
||||
#else
|
||||
#define NLOHMANN_JSON_NAMESPACE_VERSION \
|
||||
NLOHMANN_JSON_NAMESPACE_VERSION_CONCAT(NLOHMANN_JSON_VERSION_MAJOR, \
|
||||
NLOHMANN_JSON_VERSION_MINOR, \
|
||||
NLOHMANN_JSON_VERSION_PATCH)
|
||||
#endif
|
||||
|
||||
// Combine namespace components
|
||||
#define NLOHMANN_JSON_NAMESPACE_CONCAT_EX(a, b) a ## b
|
||||
#define NLOHMANN_JSON_NAMESPACE_CONCAT(a, b) \
|
||||
NLOHMANN_JSON_NAMESPACE_CONCAT_EX(a, b)
|
||||
|
||||
#ifndef NLOHMANN_JSON_NAMESPACE
|
||||
#define NLOHMANN_JSON_NAMESPACE \
|
||||
nlohmann::NLOHMANN_JSON_NAMESPACE_CONCAT( \
|
||||
NLOHMANN_JSON_ABI_TAGS, \
|
||||
NLOHMANN_JSON_NAMESPACE_VERSION)
|
||||
#endif
|
||||
|
||||
#ifndef NLOHMANN_JSON_NAMESPACE_BEGIN
|
||||
#define NLOHMANN_JSON_NAMESPACE_BEGIN \
|
||||
namespace nlohmann \
|
||||
{ \
|
||||
inline namespace NLOHMANN_JSON_NAMESPACE_CONCAT( \
|
||||
NLOHMANN_JSON_ABI_TAGS, \
|
||||
NLOHMANN_JSON_NAMESPACE_VERSION) \
|
||||
{
|
||||
#endif
|
||||
|
||||
#ifndef NLOHMANN_JSON_NAMESPACE_END
|
||||
#define NLOHMANN_JSON_NAMESPACE_END \
|
||||
} /* namespace (inline namespace) NOLINT(readability/namespace) */ \
|
||||
} // namespace nlohmann
|
||||
#endif
|
||||
|
||||
|
||||
/*!
|
||||
@brief namespace for Niels Lohmann
|
||||
@see https://github.com/nlohmann
|
||||
@since version 1.0.0
|
||||
*/
|
||||
NLOHMANN_JSON_NAMESPACE_BEGIN
|
||||
|
||||
/*!
|
||||
@brief default JSONSerializer template argument
|
||||
|
||||
This serializer ignores the template arguments and uses ADL
|
||||
([argument-dependent lookup](https://en.cppreference.com/w/cpp/language/adl))
|
||||
for serialization.
|
||||
*/
|
||||
template<typename T = void, typename SFINAE = void>
|
||||
struct adl_serializer;
|
||||
|
||||
/// a class to store JSON values
|
||||
/// @sa https://json.nlohmann.me/api/basic_json/
|
||||
template<template<typename U, typename V, typename... Args> class ObjectType =
|
||||
std::map,
|
||||
template<typename U, typename... Args> class ArrayType = std::vector,
|
||||
class StringType = std::string, class BooleanType = bool,
|
||||
class NumberIntegerType = std::int64_t,
|
||||
class NumberUnsignedType = std::uint64_t,
|
||||
class NumberFloatType = double,
|
||||
template<typename U> class AllocatorType = std::allocator,
|
||||
template<typename T, typename SFINAE = void> class JSONSerializer =
|
||||
adl_serializer,
|
||||
class BinaryType = std::vector<std::uint8_t>, // cppcheck-suppress syntaxError
|
||||
class CustomBaseClass = void>
|
||||
class basic_json;
|
||||
|
||||
/// @brief JSON Pointer defines a string syntax for identifying a specific value within a JSON document
|
||||
/// @sa https://json.nlohmann.me/api/json_pointer/
|
||||
template<typename RefStringType>
|
||||
class json_pointer;
|
||||
|
||||
/*!
|
||||
@brief default specialization
|
||||
@sa https://json.nlohmann.me/api/json/
|
||||
*/
|
||||
using json = basic_json<>;
|
||||
|
||||
/// @brief a minimal map-like container that preserves insertion order
|
||||
/// @sa https://json.nlohmann.me/api/ordered_map/
|
||||
template<class Key, class T, class IgnoredLess, class Allocator>
|
||||
struct ordered_map;
|
||||
|
||||
/// @brief specialization that maintains the insertion order of object keys
|
||||
/// @sa https://json.nlohmann.me/api/ordered_json/
|
||||
using ordered_json = basic_json<nlohmann::ordered_map>;
|
||||
|
||||
NLOHMANN_JSON_NAMESPACE_END
|
||||
|
||||
#endif // INCLUDE_NLOHMANN_JSON_FWD_HPP_
|
||||
39
extensions/cpp/rpcsx-ui-cpp/CMakeLists.txt
Normal file
39
extensions/cpp/rpcsx-ui-cpp/CMakeLists.txt
Normal file
@@ -0,0 +1,39 @@
|
||||
add_subdirectory(3rdparty/nlohmann_json)
|
||||
|
||||
add_library(
|
||||
rpcsx-ui-cpp STATIC
|
||||
|
||||
src/extension.cpp
|
||||
src/file.cpp
|
||||
)
|
||||
|
||||
target_include_directories(rpcsx-ui-cpp PUBLIC include)
|
||||
target_link_libraries(rpcsx-ui-cpp PUBLIC nlohmann::json rpcsx::ui)
|
||||
|
||||
string(TOLOWER "${CMAKE_SYSTEM_PROCESSOR}" EXTENSION_TRIPLE_ARCH)
|
||||
string(TOLOWER "${CMAKE_SYSTEM_NAME}" EXTENSION_TRIPLE_OS)
|
||||
|
||||
if("${EXTENSION_TRIPLE_OS}" STREQUAL "windows")
|
||||
set(EXTENSION_TRIPLE_FORMAT "pe")
|
||||
else()
|
||||
set(EXTENSION_TRIPLE_FORMAT "elf")
|
||||
endif()
|
||||
|
||||
if("${EXTENSION_TRIPLE_ARCH}" STREQUAL "x86_64")
|
||||
set(EXTENSION_TRIPLE_ARCH "x64")
|
||||
endif()
|
||||
|
||||
set(EXTENSION_TARGET "${EXTENSION_TRIPLE_FORMAT}-${EXTENSION_TRIPLE_ARCH}-${EXTENSION_TRIPLE_OS}" PARENT_SCOPE)
|
||||
|
||||
function(add_rpcsx_extension name version)
|
||||
set(EXTENSION_NAME ${name})
|
||||
set(EXTENSION_VERSION ${version})
|
||||
set(EXTENSION_EXECUTABLE ${EXTENSION_NAME})
|
||||
|
||||
add_executable(${EXTENSION_EXECUTABLE} ${ARGN})
|
||||
target_link_libraries(${EXTENSION_EXECUTABLE} PUBLIC rpcsx-ui-cpp)
|
||||
target_compile_definitions(${EXTENSION_EXECUTABLE} PUBLIC EXTENSION_NAME="${EXTENSION_NAME}" EXTENSION_VERSION="${EXTENSION_VERSION}")
|
||||
set_target_properties(${EXTENSION_EXECUTABLE} PROPERTIES RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin/${EXTENSION_NAME})
|
||||
make_directory(${CMAKE_BINARY_DIR}/bin/${EXTENSION_NAME})
|
||||
configure_file(extension.json ${CMAKE_BINARY_DIR}/bin/${EXTENSION_NAME})
|
||||
endfunction()
|
||||
59
extensions/cpp/rpcsx-ui-cpp/include/rpcsx/ui/Protocol.hpp
Normal file
59
extensions/cpp/rpcsx-ui-cpp/include/rpcsx/ui/Protocol.hpp
Normal file
@@ -0,0 +1,59 @@
|
||||
#pragma once
|
||||
|
||||
#include "Transport.hpp"
|
||||
#include <functional>
|
||||
#include <nlohmann/json_fwd.hpp>
|
||||
#include <rpcsx-ui.hpp>
|
||||
#include <string_view>
|
||||
|
||||
namespace rpcsx::ui {
|
||||
using json = nlohmann::json;
|
||||
|
||||
struct ExtensionBase;
|
||||
|
||||
class Protocol {
|
||||
Transport *mTransport = nullptr;
|
||||
ExtensionBase *mHandlers = nullptr;
|
||||
|
||||
public:
|
||||
Protocol() = default;
|
||||
Protocol(Transport *transport) : mTransport(transport) {}
|
||||
|
||||
template <typename... Components> void registerComponents() {}
|
||||
void setHandlers(ExtensionBase *handlers) { mHandlers = handlers; }
|
||||
ExtensionBase &getHandlers() { return *mHandlers; }
|
||||
|
||||
virtual ~Protocol() = default;
|
||||
|
||||
virtual void call(std::string_view method, json params,
|
||||
std::function<void(json)> responseHandler) = 0;
|
||||
virtual void notify(std::string_view method, json params) = 0;
|
||||
virtual void onEvent(std::string_view method,
|
||||
std::function<void(json)> eventHandler) = 0;
|
||||
virtual int processMessages() = 0;
|
||||
virtual void sendLogMessage(LogLevel level, std::string_view message) = 0;
|
||||
|
||||
virtual void sendResponse(std::size_t id, json result) = 0;
|
||||
virtual void sendErrorResponse(std::size_t id, ErrorInstance error) = 0;
|
||||
virtual void sendErrorResponse(ErrorInstance error) = 0;
|
||||
|
||||
virtual void
|
||||
addMethodHandler(std::string_view method,
|
||||
std::function<void(std::size_t id, json body)> handler) = 0;
|
||||
|
||||
virtual void
|
||||
addNotificationHandler(std::string_view notification,
|
||||
std::function<void(json body)> handler) = 0;
|
||||
|
||||
static Protocol *getDefault() { return *getImpl(); }
|
||||
static void setDefault(Protocol *protocol) { *getImpl() = protocol; }
|
||||
|
||||
Transport *getTransport() { return mTransport; }
|
||||
|
||||
private:
|
||||
static Protocol **getImpl() {
|
||||
static Protocol *protocol = nullptr;
|
||||
return &protocol;
|
||||
}
|
||||
};
|
||||
} // namespace rpcsx::ui
|
||||
12
extensions/cpp/rpcsx-ui-cpp/include/rpcsx/ui/Transport.hpp
Normal file
12
extensions/cpp/rpcsx-ui-cpp/include/rpcsx/ui/Transport.hpp
Normal file
@@ -0,0 +1,12 @@
|
||||
#pragma once
|
||||
#include <span>
|
||||
|
||||
namespace rpcsx::ui {
|
||||
class Transport {
|
||||
public:
|
||||
virtual ~Transport() = default;
|
||||
virtual void write(std::span<const std::byte> bytes) = 0;
|
||||
virtual void read(std::span<std::byte> &bytes) = 0;
|
||||
virtual void flush() {}
|
||||
};
|
||||
} // namespace rpcsx::ui
|
||||
51
extensions/cpp/rpcsx-ui-cpp/include/rpcsx/ui/extension.hpp
Normal file
51
extensions/cpp/rpcsx-ui-cpp/include/rpcsx/ui/extension.hpp
Normal file
@@ -0,0 +1,51 @@
|
||||
#pragma once
|
||||
|
||||
#include "Protocol.hpp" // IWYU pragma: export
|
||||
#include "format.hpp" // IWYU pragma: export
|
||||
#include "refl.hpp" // IWYU pragma: export
|
||||
#include <expected>
|
||||
#include <rpcsx-ui.hpp>
|
||||
|
||||
namespace rpcsx::ui {
|
||||
template <typename T>
|
||||
using Response = std::expected<typename T::Response, ErrorInstance>;
|
||||
template <typename T> using Request = T::Request;
|
||||
|
||||
struct ExtensionBase {
|
||||
virtual ~ExtensionBase() = default;
|
||||
virtual Response<Initialize> handle(const Request<Initialize> &) {
|
||||
return {};
|
||||
}
|
||||
virtual Response<Activate> handle(const Request<Activate> &) { return {}; }
|
||||
virtual Response<Shutdown> handle(const Request<Shutdown> &) { return {}; }
|
||||
};
|
||||
|
||||
template <typename... Components>
|
||||
class Extension
|
||||
: public ExtensionBase,
|
||||
public Core::instance<Extension<Components...>>,
|
||||
public Components::template instance<Extension<Components...>>... {
|
||||
Protocol *m_protocol = nullptr;
|
||||
|
||||
public:
|
||||
using Base = Extension;
|
||||
Extension() = default;
|
||||
Extension(Protocol *protocol) : m_protocol(protocol) {
|
||||
m_protocol->template registerComponents<Components...>();
|
||||
m_protocol->setHandlers(this);
|
||||
}
|
||||
|
||||
Protocol &getProtocol() const { return *m_protocol; }
|
||||
};
|
||||
|
||||
using ExtensionBuilder =
|
||||
std::function<std::unique_ptr<ExtensionBase>(Protocol *)>;
|
||||
|
||||
template <typename T> ExtensionBuilder createExtension() {
|
||||
auto builder = [](Protocol *protocol) {
|
||||
return std::make_unique<T>(protocol);
|
||||
};
|
||||
|
||||
return builder;
|
||||
}
|
||||
} // namespace rpcsx::ui
|
||||
98
extensions/cpp/rpcsx-ui-cpp/include/rpcsx/ui/file.hpp
Normal file
98
extensions/cpp/rpcsx-ui-cpp/include/rpcsx/ui/file.hpp
Normal file
@@ -0,0 +1,98 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstring>
|
||||
#include <expected>
|
||||
#include <filesystem>
|
||||
#include <ios>
|
||||
#include <span>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <system_error>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace rpcsx::ui {
|
||||
struct ReadableByteStream : std::span<const std::byte> {
|
||||
using base = std::span<const std::byte>;
|
||||
using base::base;
|
||||
using base::operator=;
|
||||
|
||||
bool read(void *dest, std::size_t bytes) {
|
||||
if (size() < bytes) {
|
||||
return false;
|
||||
}
|
||||
|
||||
std::memcpy(dest, data(), bytes);
|
||||
*this = subspan(bytes);
|
||||
return true;
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
requires std::is_trivially_copyable_v<T>
|
||||
bool read(T &target) {
|
||||
return read(&target, sizeof(target));
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
requires std::is_trivially_copyable_v<T>
|
||||
bool read(std::vector<T> &target, std::size_t count) {
|
||||
target.resize(count);
|
||||
return read(target.data(), sizeof(T) * count);
|
||||
}
|
||||
|
||||
bool read(std::string &target, std::size_t count) {
|
||||
target.resize(count);
|
||||
return read(target.data(), count);
|
||||
}
|
||||
};
|
||||
|
||||
struct FileData : std::span<std::byte> {
|
||||
struct Impl;
|
||||
|
||||
FileData() = default;
|
||||
FileData(const FileData &) = delete;
|
||||
FileData &operator=(const FileData &) = delete;
|
||||
FileData(FileData &&other) : std::span<std::byte>(other), m_impl(std::exchange(other.m_impl, nullptr)) {
|
||||
static_cast<std::span<std::byte> &>(other) = std::span<std::byte>{};
|
||||
}
|
||||
|
||||
FileData &operator=(FileData &&other) {
|
||||
std::swap(m_impl, other.m_impl);
|
||||
std::swap(static_cast<std::span<std::byte> &>(*this),
|
||||
static_cast<std::span<std::byte> &>(other));
|
||||
return *this;
|
||||
}
|
||||
~FileData();
|
||||
|
||||
FileData(Impl *i, std::span<std::byte> data)
|
||||
: std::span<std::byte>(data), m_impl(i) {}
|
||||
|
||||
private:
|
||||
Impl *m_impl = nullptr;
|
||||
};
|
||||
|
||||
struct FileStat {};
|
||||
|
||||
struct File {
|
||||
struct Impl;
|
||||
|
||||
File() = default;
|
||||
File(const File &) = delete;
|
||||
File &operator=(const File &) = delete;
|
||||
File(File &&other) : m_impl(std::exchange(other.m_impl, nullptr)) {}
|
||||
File &operator=(File &&other) {
|
||||
std::swap(m_impl, other.m_impl);
|
||||
return *this;
|
||||
}
|
||||
~File();
|
||||
|
||||
std::expected<FileData, std::error_code> map();
|
||||
|
||||
static std::expected<File, std::error_code>
|
||||
open(const std::filesystem::path &path,
|
||||
std::ios::openmode mode = std::ios::binary | std::ios::in);
|
||||
|
||||
private:
|
||||
Impl *m_impl = nullptr;
|
||||
};
|
||||
} // namespace rpcsx::ui
|
||||
319
extensions/cpp/rpcsx-ui-cpp/include/rpcsx/ui/format.hpp
Normal file
319
extensions/cpp/rpcsx-ui-cpp/include/rpcsx/ui/format.hpp
Normal file
@@ -0,0 +1,319 @@
|
||||
#pragma once
|
||||
#include "refl.hpp"
|
||||
#include <array>
|
||||
#include <format>
|
||||
#include <string_view>
|
||||
#include <type_traits>
|
||||
#include <unordered_map>
|
||||
#include <utility>
|
||||
|
||||
namespace rpcsx::ui {
|
||||
namespace detail {
|
||||
struct StructFieldInfo {
|
||||
std::size_t size = 0;
|
||||
std::size_t align = 0;
|
||||
std::size_t offset = 0;
|
||||
std::format_context::iterator (*format)(void *,
|
||||
std::format_context &ctx) = nullptr;
|
||||
std::string_view name;
|
||||
};
|
||||
|
||||
struct StructFieldQuery {
|
||||
StructFieldInfo info;
|
||||
|
||||
template <typename T> constexpr operator T() {
|
||||
info.size = sizeof(T);
|
||||
info.align = alignof(T);
|
||||
if constexpr (std::is_default_constructible_v<std::formatter<T>>) {
|
||||
info.format =
|
||||
[](void *object,
|
||||
std::format_context &ctx) -> std::format_context::iterator {
|
||||
std::formatter<T> formatter;
|
||||
return formatter.format(*static_cast<T *>(object), ctx);
|
||||
};
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
};
|
||||
|
||||
template <typename StructT, typename T>
|
||||
std::size_t getFieldOffset(T StructT::*ptr) {
|
||||
StructT queryStruct;
|
||||
return std::bit_cast<std::byte *>(&(queryStruct.*ptr)) -
|
||||
std::bit_cast<std::byte *>(&queryStruct);
|
||||
}
|
||||
|
||||
template <typename> struct StructRuntimeInfo {
|
||||
std::unordered_map<std::size_t, std::string_view> fieldInfo;
|
||||
|
||||
template <auto Field> void registerField() {
|
||||
fieldInfo[getFieldOffset(Field)] = getNameOf<Field>();
|
||||
}
|
||||
|
||||
std::string_view getFieldName(std::size_t offset) {
|
||||
if (auto it = fieldInfo.find(offset); it != fieldInfo.end()) {
|
||||
return it->second;
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
};
|
||||
|
||||
struct VariableRuntimeInfo {
|
||||
std::unordered_map<void *, std::string_view> infos;
|
||||
|
||||
std::string_view getVariableName(void *pointer) {
|
||||
if (auto it = infos.find(pointer); it != infos.end()) {
|
||||
return it->second;
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
};
|
||||
|
||||
template <typename> struct UnwrapFieldInfo;
|
||||
|
||||
template <typename T, typename StructT> struct UnwrapFieldInfo<T StructT::*> {
|
||||
using struct_type = StructT;
|
||||
using field_type = T;
|
||||
};
|
||||
|
||||
template <typename StructT> auto &getStructStorage() {
|
||||
static StructRuntimeInfo<StructT> structStorage;
|
||||
return structStorage;
|
||||
}
|
||||
|
||||
inline auto &getVariableStorage() {
|
||||
static VariableRuntimeInfo storage;
|
||||
return storage;
|
||||
}
|
||||
|
||||
template <typename StructT, std::size_t N = fieldCount<StructT>>
|
||||
constexpr auto getConstStructInfo() {
|
||||
auto genInfo =
|
||||
[]<std::size_t... I>(
|
||||
std::index_sequence<I...>) -> std::array<StructFieldInfo, N> {
|
||||
std::array<StructFieldQuery, N> queries;
|
||||
static_cast<void>(StructT{queries[I]...});
|
||||
|
||||
auto result = std::array<StructFieldInfo, N>{queries[I].info...};
|
||||
|
||||
std::size_t nextOffset = 0;
|
||||
for (auto &elem : result) {
|
||||
elem.offset = (nextOffset + (elem.align - 1)) & ~(elem.align - 1);
|
||||
nextOffset = elem.offset + elem.size;
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
return genInfo(std::make_index_sequence<N>());
|
||||
}
|
||||
|
||||
template <typename StructT> constexpr auto getStructInfo() {
|
||||
auto structInfo = getConstStructInfo<StructT>();
|
||||
auto &runtimeInfo = getStructStorage<StructT>();
|
||||
for (auto &field : structInfo) {
|
||||
field.name = runtimeInfo.getFieldName(field.offset);
|
||||
}
|
||||
return structInfo;
|
||||
}
|
||||
} // namespace detail
|
||||
|
||||
template <auto Field> void registerField() {
|
||||
using Info = detail::UnwrapFieldInfo<decltype(Field)>;
|
||||
auto &storage = detail::getStructStorage<typename Info::struct_type>();
|
||||
storage.template registerField<Field>();
|
||||
}
|
||||
|
||||
template <auto &&Variable> void registerVariable() {
|
||||
auto &storage = detail::getVariableStorage();
|
||||
storage.infos[&Variable] = getNameOf<Variable>();
|
||||
}
|
||||
} // namespace rpcsx::ui
|
||||
|
||||
template <typename T>
|
||||
requires(std::is_standard_layout_v<T> && std::is_class_v<T> &&
|
||||
rpcsx::ui::fieldCount<T> > 0) &&
|
||||
(!requires(T value) { std::begin(value) != std::end(value); })
|
||||
struct std::formatter<T> {
|
||||
constexpr std::format_parse_context::iterator
|
||||
parse(std::format_parse_context &ctx) {
|
||||
return ctx.begin();
|
||||
}
|
||||
|
||||
std::format_context::iterator format(T &s, std::format_context &ctx) const {
|
||||
std::format_to(ctx.out(), "{}", rpcsx::ui::getNameOf<T>());
|
||||
std::format_to(ctx.out(), "{{");
|
||||
|
||||
auto structInfo = rpcsx::ui::detail::getStructInfo<T>();
|
||||
auto bytes = reinterpret_cast<std::byte *>(&s);
|
||||
for (std::size_t i = 0; i < rpcsx::ui::fieldCount<T>; ++i) {
|
||||
if (i != 0) {
|
||||
std::format_to(ctx.out(), ", ");
|
||||
}
|
||||
|
||||
if (!structInfo[i].name.empty()) {
|
||||
std::format_to(ctx.out(), ".{} = ", structInfo[i].name);
|
||||
}
|
||||
|
||||
structInfo[i].format(bytes + structInfo[i].offset, ctx);
|
||||
}
|
||||
|
||||
std::format_to(ctx.out(), "}}");
|
||||
return ctx.out();
|
||||
}
|
||||
};
|
||||
|
||||
template <typename T>
|
||||
requires(std::is_enum_v<T> && rpcsx::ui::fieldCount<T> > 0)
|
||||
struct std::formatter<T> {
|
||||
constexpr std::format_parse_context::iterator
|
||||
parse(std::format_parse_context &ctx) {
|
||||
return ctx.begin();
|
||||
}
|
||||
|
||||
std::format_context::iterator format(T value,
|
||||
std::format_context &ctx) const {
|
||||
auto getFieldName =
|
||||
[]<std::size_t... I>(std::underlying_type_t<T> value,
|
||||
std::index_sequence<I...>) -> std::string {
|
||||
std::string_view result;
|
||||
((value == I ? ((result = rpcsx::ui::getNameOf<static_cast<T>(I)>()), 0)
|
||||
: 0),
|
||||
...);
|
||||
|
||||
if (!result.empty()) {
|
||||
return std::string(result);
|
||||
}
|
||||
|
||||
return std::format("{}", value);
|
||||
};
|
||||
|
||||
auto queryUnknownField =
|
||||
[]<std::int64_t Offset, std::int64_t... I>(
|
||||
std::underlying_type_t<T> value,
|
||||
std::integral_constant<std::int64_t, Offset>,
|
||||
std::integer_sequence<std::int64_t, I...>) -> std::string {
|
||||
std::string_view result;
|
||||
if (value < 0) {
|
||||
((-value == I + Offset
|
||||
? ((result =
|
||||
rpcsx::ui::getNameOf<static_cast<T>(-(I + Offset))>()),
|
||||
0)
|
||||
: 0),
|
||||
...);
|
||||
} else {
|
||||
((value == I + Offset
|
||||
? ((result = rpcsx::ui::getNameOf<static_cast<T>(I + Offset)>()),
|
||||
0)
|
||||
: 0),
|
||||
...);
|
||||
}
|
||||
|
||||
if (!result.empty()) {
|
||||
return std::string(result);
|
||||
}
|
||||
|
||||
return std::format("{}", value);
|
||||
};
|
||||
|
||||
std::string fieldName;
|
||||
|
||||
auto underlying = std::to_underlying(value);
|
||||
|
||||
if (underlying < 0) {
|
||||
fieldName = queryUnknownField(
|
||||
underlying, std::integral_constant<std::int64_t, 0>{},
|
||||
std::make_integer_sequence<std::int64_t, 128>{});
|
||||
} else if (underlying >= rpcsx::ui::fieldCount<T>) {
|
||||
fieldName = queryUnknownField(
|
||||
underlying,
|
||||
std::integral_constant<std::int64_t, rpcsx::ui::fieldCount<T>>{},
|
||||
std::make_integer_sequence<std::int64_t, 128>{});
|
||||
} else {
|
||||
fieldName = getFieldName(
|
||||
underlying, std::make_index_sequence<rpcsx::ui::fieldCount<T>>());
|
||||
}
|
||||
|
||||
if (fieldName[0] >= '0' && fieldName[0] <= '9') {
|
||||
std::format_to(ctx.out(), "({}){}", rpcsx::ui::getNameOf<T>(), fieldName);
|
||||
} else {
|
||||
std::format_to(ctx.out(), "{}::{}", rpcsx::ui::getNameOf<T>(), fieldName);
|
||||
}
|
||||
|
||||
return ctx.out();
|
||||
}
|
||||
};
|
||||
|
||||
template <typename T>
|
||||
requires requires(T value) { std::begin(value) != std::end(value); }
|
||||
struct std::formatter<T> {
|
||||
constexpr std::format_parse_context::iterator
|
||||
parse(std::format_parse_context &ctx) {
|
||||
return ctx.begin();
|
||||
}
|
||||
|
||||
std::format_context::iterator format(T &s, std::format_context &ctx) const {
|
||||
std::format_to(ctx.out(), "[");
|
||||
|
||||
for (bool first = true; auto &elem : s) {
|
||||
if (first) {
|
||||
first = false;
|
||||
} else {
|
||||
std::format_to(ctx.out(), ", ");
|
||||
}
|
||||
|
||||
std::format_to(ctx.out(), "{}", elem);
|
||||
}
|
||||
|
||||
std::format_to(ctx.out(), "]");
|
||||
return ctx.out();
|
||||
}
|
||||
};
|
||||
|
||||
template <typename T>
|
||||
requires(!std::is_same_v<std::remove_cv_t<T>, char> &&
|
||||
!std::is_same_v<std::remove_cv_t<T>, wchar_t> &&
|
||||
!std::is_same_v<std::remove_cv_t<T>, char8_t> &&
|
||||
!std::is_same_v<std::remove_cv_t<T>, char16_t> &&
|
||||
!std::is_same_v<std::remove_cv_t<T>, char32_t> &&
|
||||
std::is_default_constructible_v<std::formatter<T>>)
|
||||
struct std::formatter<T *> {
|
||||
constexpr std::format_parse_context::iterator
|
||||
parse(std::format_parse_context &ctx) {
|
||||
return ctx.begin();
|
||||
}
|
||||
|
||||
std::format_context::iterator format(T *ptr, std::format_context &ctx) const {
|
||||
auto name = rpcsx::ui::detail::getVariableStorage().getVariableName(ptr);
|
||||
if (!name.empty()) {
|
||||
std::format_to(ctx.out(), "*{} = ", name);
|
||||
} else {
|
||||
std::format_to(ctx.out(), "*");
|
||||
}
|
||||
|
||||
if (ptr == nullptr) {
|
||||
std::format_to(ctx.out(), "nullptr");
|
||||
} else {
|
||||
std::format_to(ctx.out(), "{}:{}", static_cast<void *>(ptr), *ptr);
|
||||
}
|
||||
return ctx.out();
|
||||
}
|
||||
};
|
||||
|
||||
#define RX_CONCAT(a, b) RX_CONCAT_IMPL(a, b)
|
||||
#define RX_CONCAT_IMPL(a, b) a##b
|
||||
|
||||
#define RX_REGISTER_VARIABLE(x) \
|
||||
static auto RX_CONCAT(_RX_REGISTER_VARIABLE_, __LINE__) = ([] { \
|
||||
::rpcsx::ui::registerVariable<x>(); \
|
||||
return true; \
|
||||
}())
|
||||
|
||||
#define RX_REGISTER_FIELD(x) \
|
||||
static auto RX_CONCAT(_RX_REGISTER_FIELD_, __LINE__) = ([] { \
|
||||
::rpcsx::ui::registerField<&x>(); \
|
||||
return true; \
|
||||
}())
|
||||
38
extensions/cpp/rpcsx-ui-cpp/include/rpcsx/ui/log.hpp
Normal file
38
extensions/cpp/rpcsx-ui-cpp/include/rpcsx/ui/log.hpp
Normal file
@@ -0,0 +1,38 @@
|
||||
#pragma once
|
||||
#include "Protocol.hpp"
|
||||
#include <format>
|
||||
|
||||
namespace rpcsx::ui {
|
||||
template <typename... Args>
|
||||
void log(LogLevel level, std::format_string<Args...> fmt, Args &&...args) {
|
||||
Protocol::getDefault()->sendLogMessage(
|
||||
level, std::vformat(fmt.get(), std::make_format_args(args...)));
|
||||
}
|
||||
|
||||
template <typename... Args>
|
||||
void ilog(std::format_string<Args...> fmt, Args &&...args) {
|
||||
Protocol::getDefault()->sendLogMessage(
|
||||
LogLevel::Info, std::vformat(fmt.get(), std::make_format_args(args...)));
|
||||
}
|
||||
|
||||
template <typename... Args>
|
||||
void elog(std::format_string<Args...> fmt, Args &&...args) {
|
||||
Protocol::getDefault()->sendLogMessage(
|
||||
LogLevel::Error, std::vformat(fmt.get(), std::make_format_args(args...)));
|
||||
}
|
||||
|
||||
template <typename... Args>
|
||||
void wlog(std::format_string<Args...> fmt, Args &&...args) {
|
||||
Protocol::getDefault()->sendLogMessage(
|
||||
LogLevel::Warning,
|
||||
std::vformat(fmt.get(), std::make_format_args(args...)));
|
||||
}
|
||||
|
||||
template <typename... Args>
|
||||
[[noreturn]] void fatal(std::format_string<Args...> fmt, Args &&...args) {
|
||||
Protocol::getDefault()->sendLogMessage(
|
||||
LogLevel::Fatal, std::vformat(fmt.get(), std::make_format_args(args...)));
|
||||
|
||||
std::exit(1);
|
||||
}
|
||||
} // namespace rpcsx::ui
|
||||
159
extensions/cpp/rpcsx-ui-cpp/include/rpcsx/ui/refl.hpp
Normal file
159
extensions/cpp/rpcsx-ui-cpp/include/rpcsx/ui/refl.hpp
Normal file
@@ -0,0 +1,159 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
#include <string_view>
|
||||
|
||||
#ifdef _MSC_VER
|
||||
#define RX_PRETTY_FUNCTION __FUNCSIG__
|
||||
#elif defined(__GNUC__)
|
||||
#define RX_PRETTY_FUNCTION __PRETTY_FUNCTION__
|
||||
#else
|
||||
#define RX_PRETTY_FUNCTION ""
|
||||
#endif
|
||||
|
||||
namespace rpcsx::ui {
|
||||
namespace detail {
|
||||
struct AnyStructFieldQuery {
|
||||
template <typename T> constexpr operator T &&();
|
||||
};
|
||||
|
||||
template <typename StructT, std::size_t N = 1, std::size_t LastValidCount = 0>
|
||||
requires std::is_class_v<StructT>
|
||||
constexpr auto calcFieldCount() {
|
||||
|
||||
auto isValidFieldCount = []<std::size_t... I>(std::index_sequence<I...>) {
|
||||
return requires { StructT(((I, AnyStructFieldQuery{}))...); };
|
||||
};
|
||||
|
||||
if constexpr (isValidFieldCount(std::make_index_sequence<N>())) {
|
||||
return calcFieldCount<StructT, N + 1, N>();
|
||||
} else if constexpr (sizeof(StructT) <= N || LastValidCount > 0) {
|
||||
return LastValidCount;
|
||||
} else {
|
||||
return calcFieldCount<StructT, N + 1, LastValidCount>();
|
||||
}
|
||||
}
|
||||
|
||||
consteval std::string_view unwrapName(std::string_view prefix,
|
||||
std::string_view pretty,
|
||||
bool dropNamespace) {
|
||||
#ifdef _MSC_VER
|
||||
if (auto pos = pretty.find(prefix); pos != std::string_view::npos) {
|
||||
pretty.remove_prefix(pos + prefix.size());
|
||||
} else {
|
||||
pretty = {};
|
||||
}
|
||||
|
||||
if (auto pos = pretty.rfind('>'); pos != std::string_view::npos) {
|
||||
pretty.remove_suffix(pretty.size() - pos);
|
||||
} else {
|
||||
pretty = {};
|
||||
}
|
||||
|
||||
if (auto pos = pretty.rfind(')'); pos != std::string_view::npos) {
|
||||
pretty.remove_prefix(pos + 1);
|
||||
}
|
||||
|
||||
if (auto pos = pretty.find(' '); pos != std::string_view::npos) {
|
||||
pretty.remove_prefix(pos + 1);
|
||||
}
|
||||
#else
|
||||
if (auto pos = pretty.rfind('['); pos != std::string_view::npos) {
|
||||
pretty.remove_prefix(pos + 1);
|
||||
} else {
|
||||
pretty = {};
|
||||
}
|
||||
|
||||
if (auto pos = pretty.rfind(prefix); pos != std::string_view::npos) {
|
||||
pretty.remove_prefix(pos + prefix.size());
|
||||
} else {
|
||||
pretty = {};
|
||||
}
|
||||
if (auto pos = pretty.rfind(')'); pos != std::string_view::npos) {
|
||||
pretty.remove_prefix(pos + 1);
|
||||
}
|
||||
if (pretty.ends_with(']')) {
|
||||
pretty.remove_suffix(1);
|
||||
} else {
|
||||
pretty = {};
|
||||
}
|
||||
#endif
|
||||
|
||||
if (dropNamespace) {
|
||||
if (auto pos = pretty.rfind(':'); pos != std::string_view::npos) {
|
||||
pretty.remove_prefix(pos + 1);
|
||||
}
|
||||
}
|
||||
|
||||
return pretty;
|
||||
}
|
||||
|
||||
template <typename> constexpr bool isField = false;
|
||||
|
||||
template <typename BaseT, typename TypeT>
|
||||
constexpr bool isField<TypeT(BaseT::*)> = true;
|
||||
|
||||
} // namespace detail
|
||||
|
||||
template <auto &&V> consteval auto getNameOf() {
|
||||
std::string_view prefix;
|
||||
#ifdef _MSC_VER
|
||||
prefix = "getNameOf<";
|
||||
#else
|
||||
prefix = "V = ";
|
||||
#endif
|
||||
return detail::unwrapName(prefix, RX_PRETTY_FUNCTION, true);
|
||||
}
|
||||
|
||||
template <auto V>
|
||||
requires(detail::isField<decltype(V)> ||
|
||||
std::is_enum_v<std::remove_cvref_t<decltype(V)>> ||
|
||||
std::is_pointer_v<std::remove_cvref_t<decltype(V)>>)
|
||||
consteval auto getNameOf() {
|
||||
std::string_view prefix;
|
||||
#ifdef _MSC_VER
|
||||
prefix = "getNameOf<";
|
||||
#else
|
||||
prefix = "V = ";
|
||||
#endif
|
||||
return detail::unwrapName(prefix, RX_PRETTY_FUNCTION, true);
|
||||
}
|
||||
|
||||
template <typename T> consteval auto getNameOf() {
|
||||
std::string_view prefix;
|
||||
#ifdef _MSC_VER
|
||||
prefix = "getNameOf<";
|
||||
#else
|
||||
prefix = "T = ";
|
||||
#endif
|
||||
return detail::unwrapName(prefix, RX_PRETTY_FUNCTION, false);
|
||||
}
|
||||
|
||||
namespace detail {
|
||||
template <typename EnumT, std::size_t N = 0>
|
||||
requires std::is_enum_v<EnumT>
|
||||
constexpr auto calcFieldCount() {
|
||||
if constexpr (requires { EnumT::Count; }) {
|
||||
return static_cast<std::size_t>(EnumT::Count);
|
||||
} else if constexpr (requires { EnumT::_count; }) {
|
||||
return static_cast<std::size_t>(EnumT::_count);
|
||||
} else if constexpr (requires { EnumT::count; }) {
|
||||
return static_cast<std::size_t>(EnumT::count);
|
||||
} else if constexpr (!requires { getNameOf<EnumT(N)>()[0]; }) {
|
||||
return N;
|
||||
} else {
|
||||
constexpr auto c = getNameOf<EnumT(N)>()[0];
|
||||
if constexpr (!requires { getNameOf<EnumT(N)>()[0]; }) {
|
||||
return N;
|
||||
} else if constexpr (c >= '0' && c <= '9') {
|
||||
return N;
|
||||
} else {
|
||||
return calcFieldCount<EnumT, N + 1>();
|
||||
}
|
||||
}
|
||||
}
|
||||
} // namespace detail
|
||||
|
||||
template <typename StructT>
|
||||
inline constexpr std::size_t fieldCount = detail::calcFieldCount<StructT>();
|
||||
} // namespace rpcsx::ui
|
||||
365
extensions/cpp/rpcsx-ui-cpp/src/extension.cpp
Normal file
365
extensions/cpp/rpcsx-ui-cpp/src/extension.cpp
Normal file
@@ -0,0 +1,365 @@
|
||||
#include "rpcsx/ui/extension.hpp"
|
||||
#include "rpcsx/ui/Protocol.hpp"
|
||||
#include "rpcsx/ui/Transport.hpp"
|
||||
#include <charconv>
|
||||
#include <cstddef>
|
||||
#include <cstdio>
|
||||
#include <exception>
|
||||
#include <functional>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <rpcsx-ui.hpp>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <utility>
|
||||
|
||||
using namespace rpcsx::ui;
|
||||
using namespace nlohmann;
|
||||
|
||||
struct StdioTransport : Transport {
|
||||
void write(std::span<const std::byte> bytes) override {
|
||||
while (true) {
|
||||
auto count = std::fwrite(bytes.data(), 1, bytes.size(), stdout);
|
||||
|
||||
if (count <= 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (count == bytes.size()) {
|
||||
break;
|
||||
}
|
||||
|
||||
bytes = bytes.subspan(count);
|
||||
}
|
||||
}
|
||||
|
||||
void read(std::span<std::byte> &bytes) override {
|
||||
auto count = std::fread(bytes.data(), 1, bytes.size(), stdin);
|
||||
bytes = bytes.subspan(0, count);
|
||||
}
|
||||
|
||||
void flush() override { std::fflush(stdout); }
|
||||
};
|
||||
|
||||
template <typename T, typename Protocol>
|
||||
static auto createMethodHandler(Protocol *protocol) {
|
||||
return [=](std::size_t id, json params) {
|
||||
typename T::Request request;
|
||||
try {
|
||||
request = params;
|
||||
} catch (const std::exception &) {
|
||||
protocol->sendErrorResponse(id, {ErrorCode::InvalidParams});
|
||||
return;
|
||||
}
|
||||
|
||||
auto result = protocol->getHandlers().handle(request);
|
||||
if (!result.has_value()) {
|
||||
protocol->sendErrorResponse(id, result.error());
|
||||
return;
|
||||
}
|
||||
|
||||
protocol->sendResponse(id, json(result.value()));
|
||||
};
|
||||
};
|
||||
|
||||
template <typename T, typename Protocol>
|
||||
static auto createNotifyHandler(Protocol *protocol) {
|
||||
return [=](json params) {
|
||||
typename T::Request request;
|
||||
try {
|
||||
request = params;
|
||||
} catch (const std::exception &) {
|
||||
protocol->sendErrorResponse({ErrorCode::InvalidParams});
|
||||
return;
|
||||
}
|
||||
|
||||
protocol->getHandlers().handle(request);
|
||||
};
|
||||
};
|
||||
|
||||
static std::span<const std::byte> asBytes(std::string_view text) {
|
||||
return {reinterpret_cast<const std::byte *>(text.data()), text.size()};
|
||||
}
|
||||
|
||||
struct JsonRpcProtocol : Protocol {
|
||||
JsonRpcProtocol(Transport *transport) : Protocol(transport) {
|
||||
mMethodHandlers["$/initialize"] =
|
||||
createMethodHandler<rpcsx::ui::Initialize>(this);
|
||||
mMethodHandlers["$/activate"] =
|
||||
createMethodHandler<rpcsx::ui::Activate>(this);
|
||||
// mMethodHandlers["$/deactivate"] =
|
||||
// createMethodHandler<rpcsx::ui::Deactivate>(this);
|
||||
mNotifyHandlers["$/shutdown"] =
|
||||
createNotifyHandler<rpcsx::ui::Shutdown>(this);
|
||||
}
|
||||
|
||||
void call(std::string_view method, json params,
|
||||
std::function<void(json)> responseHandler) override {
|
||||
std::size_t id = mNextId++;
|
||||
send({
|
||||
{"jsonrpc", "2.0"},
|
||||
{"method", method},
|
||||
{"params", std::move(params)},
|
||||
{"id", id},
|
||||
});
|
||||
|
||||
mExpectedResponses.emplace(id, std::move(responseHandler));
|
||||
}
|
||||
|
||||
void notify(std::string_view method, json params) override {
|
||||
send({
|
||||
{"jsonrpc", "2.0"},
|
||||
{"method", method},
|
||||
{"params", std::move(params)},
|
||||
});
|
||||
}
|
||||
|
||||
void sendResponse(std::size_t id, json result) override {
|
||||
send({
|
||||
{"jsonrpc", "2.0"},
|
||||
{"id", id},
|
||||
{"result", std::move(result)},
|
||||
});
|
||||
}
|
||||
void sendErrorResponse(std::size_t id, ErrorInstance error) override {
|
||||
send({
|
||||
{"jsonrpc", "2.0"},
|
||||
{"id", id},
|
||||
{"error", error},
|
||||
});
|
||||
}
|
||||
void sendErrorResponse(ErrorInstance error) override {
|
||||
send({
|
||||
{"jsonrpc", "2.0"},
|
||||
{"id", nullptr},
|
||||
{"error", error},
|
||||
});
|
||||
}
|
||||
|
||||
void addNotificationHandler(std::string_view notification,
|
||||
std::function<void(json)> handler) override {
|
||||
mNotifyHandlers[std::string(notification)] = std::move(handler);
|
||||
}
|
||||
|
||||
void
|
||||
addMethodHandler(std::string_view method,
|
||||
std::function<void(std::size_t, json)> handler) override {
|
||||
mMethodHandlers[std::string(method)] = std::move(handler);
|
||||
}
|
||||
|
||||
void onEvent(std::string_view method,
|
||||
std::function<void(json)> eventHandler) override {
|
||||
mEventHandlers[std::string(method)].push_back(std::move(eventHandler));
|
||||
}
|
||||
|
||||
void sendLogMessage(LogLevel level, std::string_view message) override {
|
||||
// FIXME
|
||||
std::fprintf(stderr, "%s\n", std::string(message).c_str());
|
||||
}
|
||||
|
||||
int processMessages() override {
|
||||
std::string header;
|
||||
std::vector<std::byte> buffer;
|
||||
|
||||
while (true) {
|
||||
header.clear();
|
||||
|
||||
while (true) {
|
||||
std::byte b;
|
||||
std::span bytes = {&b, 1};
|
||||
getTransport()->read(bytes);
|
||||
if (!bytes.empty()) {
|
||||
header += static_cast<char>(b);
|
||||
}
|
||||
|
||||
if (header.ends_with("\r\n\r\n")) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
constexpr std::string_view contentLength = "Content-Length:";
|
||||
auto contentLengthPos = header.find_first_of(contentLength);
|
||||
if (contentLengthPos == std::string_view::npos) {
|
||||
continue;
|
||||
}
|
||||
|
||||
std::string_view lengthString = std::string_view(header).substr(
|
||||
contentLengthPos + contentLength.size());
|
||||
|
||||
auto lineEnd = lengthString.find_first_of("\r\n");
|
||||
if (lineEnd == std::string_view::npos) {
|
||||
continue;
|
||||
}
|
||||
|
||||
lengthString = lengthString.substr(0, lineEnd);
|
||||
|
||||
while (lengthString.starts_with(' ')) {
|
||||
lengthString.remove_prefix(1);
|
||||
}
|
||||
|
||||
std::size_t length = 0;
|
||||
auto [ptr, ec] =
|
||||
std::from_chars(lengthString.data(),
|
||||
lengthString.data() + lengthString.size(), length);
|
||||
|
||||
if (ec != std::errc{} ||
|
||||
ptr != lengthString.data() + lengthString.size()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
buffer.resize(length);
|
||||
|
||||
std::span bytes = {buffer};
|
||||
getTransport()->read(bytes);
|
||||
|
||||
if (bytes.size() != buffer.size()) {
|
||||
std::fprintf(stderr, "input truncated\n");
|
||||
std::abort();
|
||||
}
|
||||
|
||||
std::string_view content = {
|
||||
(char *)buffer.data(),
|
||||
buffer.size(),
|
||||
};
|
||||
|
||||
handleRequest(json::parse(content));
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
void handleRequest(json message) {
|
||||
auto handlers = getHandlers();
|
||||
|
||||
if (auto it = message.find("method"); it != message.end()) {
|
||||
std::string method = it.value();
|
||||
std::size_t id = 0;
|
||||
bool hasId = false;
|
||||
|
||||
if (auto it = message.find("id"); it != message.end()) {
|
||||
hasId = true;
|
||||
id = it.value();
|
||||
}
|
||||
|
||||
json params;
|
||||
|
||||
if (auto it = message.find("params"); it != message.end()) {
|
||||
params = it.value();
|
||||
}
|
||||
|
||||
if (hasId) {
|
||||
if (auto it = mMethodHandlers.find(method);
|
||||
it != mMethodHandlers.end()) {
|
||||
it->second(id, params);
|
||||
return;
|
||||
}
|
||||
|
||||
sendErrorResponse(id, {ErrorCode::MethodNotFound});
|
||||
return;
|
||||
}
|
||||
|
||||
if (auto it = mNotifyHandlers.find(method); it != mNotifyHandlers.end()) {
|
||||
it->second(params);
|
||||
return;
|
||||
}
|
||||
|
||||
sendErrorResponse({ErrorCode::MethodNotFound});
|
||||
return;
|
||||
}
|
||||
|
||||
if (auto it = message.find("result"); it != message.end()) {
|
||||
json result = it.value();
|
||||
bool hasId = false;
|
||||
std::size_t id = 0;
|
||||
|
||||
if (auto it = message.find("id"); it != message.end()) {
|
||||
hasId = true;
|
||||
id = it.value();
|
||||
}
|
||||
|
||||
if (!hasId) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (auto it = mExpectedResponses.find(id);
|
||||
it != mExpectedResponses.end()) {
|
||||
auto impl = std::move(it->second);
|
||||
mExpectedResponses.erase(it);
|
||||
impl(result);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
void send(json body) {
|
||||
std::string bodyText = body.dump();
|
||||
std::string header = "Content-Length: ";
|
||||
header += std::to_string(bodyText.length());
|
||||
header += "\r\n\r\n";
|
||||
|
||||
getTransport()->write(asBytes(header));
|
||||
getTransport()->write(asBytes(bodyText));
|
||||
getTransport()->flush();
|
||||
}
|
||||
|
||||
std::map<std::string, std::function<void(std::size_t, json)>> mMethodHandlers;
|
||||
std::map<std::string, std::function<void(json)>> mNotifyHandlers;
|
||||
std::map<std::string, std::vector<std::function<void(json)>>> mEventHandlers;
|
||||
std::map<std::size_t, std::function<void(json)>> mExpectedResponses;
|
||||
std::size_t mNextId = 1;
|
||||
};
|
||||
|
||||
ExtensionBuilder extension_main();
|
||||
|
||||
int main(int argc, const char *argv[]) {
|
||||
auto extensionBuilder = extension_main();
|
||||
|
||||
std::string_view transportId;
|
||||
std::string_view protocolId;
|
||||
|
||||
for (int i = 1; i < argc - 1; ++i) {
|
||||
if (argv[i] == std::string_view("--rpcsx-ui/transport")) {
|
||||
transportId = argv[i + 1];
|
||||
++i;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (argv[i] == std::string_view("--rpcsx-ui/protocol")) {
|
||||
protocolId = argv[i + 1];
|
||||
++i;
|
||||
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (transportId.empty()) {
|
||||
transportId = "stdio";
|
||||
}
|
||||
|
||||
if (protocolId.empty()) {
|
||||
protocolId = "json-rpc";
|
||||
}
|
||||
|
||||
std::unique_ptr<Transport> transport;
|
||||
|
||||
if (transportId == "stdio") {
|
||||
transport = std::make_unique<StdioTransport>();
|
||||
} else {
|
||||
return 1;
|
||||
}
|
||||
|
||||
std::unique_ptr<Protocol> protocol;
|
||||
|
||||
if (protocolId == "json-rpc") {
|
||||
protocol = std::make_unique<JsonRpcProtocol>(transport.get());
|
||||
} else {
|
||||
return 1;
|
||||
}
|
||||
|
||||
Protocol::setDefault(protocol.get());
|
||||
auto exptension = extensionBuilder(protocol.get());
|
||||
return protocol->processMessages();
|
||||
}
|
||||
|
||||
143
extensions/cpp/rpcsx-ui-cpp/src/file.cpp
Normal file
143
extensions/cpp/rpcsx-ui-cpp/src/file.cpp
Normal file
@@ -0,0 +1,143 @@
|
||||
#include "rpcsx/ui/file.hpp"
|
||||
#include <atomic>
|
||||
#include <cerrno>
|
||||
#include <cstddef>
|
||||
#include <cstring>
|
||||
|
||||
#if defined(__linux)
|
||||
#include <fcntl.h>
|
||||
#include <sys/mman.h>
|
||||
#include <sys/stat.h>
|
||||
#include <unistd.h>
|
||||
#else
|
||||
#include <fstream>
|
||||
#endif
|
||||
|
||||
using namespace rpcsx::ui;
|
||||
|
||||
#if defined(__linux)
|
||||
static const std::uint32_t gPageSize = getpagesize();
|
||||
|
||||
static int fileNativeHandle(void *impl) {
|
||||
std::uintptr_t rawHandle = 0;
|
||||
std::memcpy(&rawHandle, &impl, sizeof(void *));
|
||||
int fd = rawHandle;
|
||||
std::atomic<int> a;
|
||||
a.wait(5);
|
||||
return -fd;
|
||||
}
|
||||
|
||||
File::~File() {
|
||||
if (m_impl != nullptr) {
|
||||
::close(fileNativeHandle(m_impl));
|
||||
}
|
||||
}
|
||||
|
||||
std::expected<File, std::error_code>
|
||||
File::open(const std::filesystem::path &path, std::ios::openmode mode) {
|
||||
int flags = 0;
|
||||
if (mode & std::ios::out) {
|
||||
if (mode & std::ios::in) {
|
||||
flags |= O_RDWR;
|
||||
} else {
|
||||
flags |= O_WRONLY;
|
||||
}
|
||||
|
||||
flags |= O_CREAT;
|
||||
} else {
|
||||
flags |= O_RDONLY;
|
||||
}
|
||||
|
||||
if (mode & std::ios::trunc) {
|
||||
flags |= O_TRUNC;
|
||||
}
|
||||
|
||||
if (mode & std::ios::app) {
|
||||
flags |= O_APPEND;
|
||||
}
|
||||
|
||||
int fd = ::open(path.native().c_str(), flags, 0666);
|
||||
|
||||
if (fd < 0) {
|
||||
return std::unexpected(std::make_error_code(std::errc{errno}));
|
||||
}
|
||||
|
||||
Impl *handle{};
|
||||
std::uintptr_t rawHandle = -fd;
|
||||
std::memcpy(&handle, &rawHandle, sizeof(void *));
|
||||
File result;
|
||||
result.m_impl = handle;
|
||||
return result;
|
||||
}
|
||||
|
||||
FileData::~FileData() {
|
||||
if (data()) {
|
||||
::munmap(data(), (size() + gPageSize - 1) & ~(gPageSize - 1));
|
||||
}
|
||||
}
|
||||
|
||||
std::expected<FileData, std::error_code> File::map() {
|
||||
if (m_impl == nullptr) {
|
||||
return std::unexpected(std::make_error_code(std::errc::invalid_argument));
|
||||
}
|
||||
|
||||
int fd = fileNativeHandle(m_impl);
|
||||
struct stat fs;
|
||||
if (fstat(fd, &fs) < 0) {
|
||||
return std::unexpected(std::make_error_code(std::errc{errno}));
|
||||
}
|
||||
|
||||
void *mapping = ::mmap(nullptr, fs.st_size, PROT_READ, MAP_PRIVATE, fd, 0);
|
||||
|
||||
if (mapping == MAP_FAILED) {
|
||||
return std::unexpected(std::make_error_code(std::errc{errno}));
|
||||
}
|
||||
|
||||
return FileData(nullptr, {(std::byte *)mapping, std::size_t(fs.st_size)});
|
||||
}
|
||||
#else
|
||||
struct File::Impl {
|
||||
std::fstream stream;
|
||||
};
|
||||
|
||||
struct FileData::Impl {
|
||||
std::vector<std::byte> data;
|
||||
};
|
||||
|
||||
File::~File() { delete m_impl; }
|
||||
FileData::~FileData() { delete m_impl; }
|
||||
|
||||
std::expected<File, std::error_code>
|
||||
File::open(const std::filesystem::path &path, std::ios::openmode mode) {
|
||||
std::fstream f(path, mode);
|
||||
if (!f.is_open()) {
|
||||
return std::unexpected(std::make_error_code(std::errc{errno}));
|
||||
}
|
||||
|
||||
File result;
|
||||
result.m_impl = new Impl();
|
||||
result.m_impl->stream = std::move(f);
|
||||
return result;
|
||||
}
|
||||
|
||||
std::expected<FileData, std::error_code> File::map() {
|
||||
if (m_impl == nullptr) {
|
||||
return std::unexpected(std::make_error_code(std::errc::invalid_argument));
|
||||
}
|
||||
|
||||
std::size_t pos = m_impl->stream.tellg();
|
||||
m_impl->stream.seekg(0, std::ios::end);
|
||||
std::size_t size = m_impl->stream.tellg();
|
||||
m_impl->stream.seekg(pos, std::ios::beg);
|
||||
|
||||
std::vector<std::byte> buffer(size);
|
||||
|
||||
m_impl->stream.read(reinterpret_cast<char *>(buffer.data()), buffer.size());
|
||||
if (!m_impl->stream) {
|
||||
return std::unexpected(std::make_error_code(std::errc{errno}));
|
||||
}
|
||||
|
||||
auto dataImpl = new FileData::Impl();
|
||||
return FileData(dataImpl, dataImpl->data);
|
||||
}
|
||||
#endif
|
||||
1023
package-lock.json
generated
1023
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
12
package.json
12
package.json
@@ -9,10 +9,13 @@
|
||||
},
|
||||
"scripts": {
|
||||
"build:kit": "npm run -w rpcsx-ui-kit build",
|
||||
"build:extensions": "npm run build:api:cpp && cmake -B build-extensions -S extensions/cpp -DCMAKE_EXPORT_COMPILE_COMMANDS=on -DCMAKE_BUILD_TYPE=Release && cmake --build build-extensions --parallel",
|
||||
"install:extensions": "cp -r build-extensions/bin electron/build/",
|
||||
"build:api:cpp": "node rpcsx-ui-kit/build/cli.js generate --lang c++ --input rpcsx-ui --output extensions/cpp/rpcsx-ui --name rpcsx-ui",
|
||||
"build:web:server": "node ./build.mjs",
|
||||
"build:web:ui": "expo export --platform web --dev --output-dir electron/build/ui --no-minify --source-maps",
|
||||
"build:web:ui:release": "expo export --platform web --dev --output-dir electron/build/ui",
|
||||
"build:web": "npm run build:kit && npm run build:web:server && npm run build:web:ui",
|
||||
"build:web": "npm run build:kit && npm run build:web:server && npm run build:web:ui && npm run build:extensions && npm run install:extensions",
|
||||
"build:web:release": "npm run build:kit && npm run build:web:server && npm run build:web:ui:release",
|
||||
"build:android": "npm run build:kit && expo prebuild --platform android && ./android/gradlew assembleDebug -p ./android",
|
||||
"build:android:release": "npm run build:kit && expo prebuild --platform android && ./android/gradlew assembleRelease -p ./android",
|
||||
@@ -51,8 +54,6 @@
|
||||
"expo-system-ui": "~5.0.10",
|
||||
"expo-web-browser": "~14.2.0",
|
||||
"glob": "^11.0.3",
|
||||
"i18next": "^25.3.6",
|
||||
"i18next-browser-languagedetector": "^8.2.0",
|
||||
"json5": "^2.2.3",
|
||||
"monaco-editor": "^0.52.2",
|
||||
"prettier": "^3.6.2",
|
||||
@@ -69,9 +70,6 @@
|
||||
"devDependencies": {
|
||||
"@expo/metro-config": "~0.20.0",
|
||||
"@reforged/maker-appimage": "^5.0.0",
|
||||
"@tailwindcss/postcss": "^4.1.12",
|
||||
"@tailwindcss/vite": "^4.1.12",
|
||||
"@tauri-apps/cli": "^2.8.1",
|
||||
"@types/node": "^24.3.0",
|
||||
"@types/react": "~19.0.10",
|
||||
"electron": "^37.3.1",
|
||||
@@ -81,9 +79,7 @@
|
||||
"eslint-config-expo": "~9.2.0",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"metro": "^0.82.5",
|
||||
"postcss": "^8.5.6",
|
||||
"rpcsx-ui-kit": "file:rpcsx-ui-kit",
|
||||
"tailwindcss": "^4.1.12",
|
||||
"tslib": "^2.8.1",
|
||||
"typescript": "~5.8.3",
|
||||
"typescript-eslint": "^8.40.0"
|
||||
|
||||
@@ -27,7 +27,7 @@ const options = {
|
||||
format: 'esm',
|
||||
sourcemap: values.dev ? 'linked' : undefined,
|
||||
outdir: "build",
|
||||
entryPoints: ["src/main.ts"]
|
||||
entryPoints: ["src/main.ts", "src/cli.ts"]
|
||||
};
|
||||
|
||||
try {
|
||||
|
||||
@@ -17,14 +17,16 @@
|
||||
"esbuild": "^0.25.9",
|
||||
"glob": "^11.0.3",
|
||||
"json5": "^2.2.3",
|
||||
"prettier": "^3.6.2"
|
||||
"prettier": "^3.6.2",
|
||||
"yargs": "^18.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@sveltejs/kit": "^2.31.0",
|
||||
"typescript": "^5.9.2",
|
||||
"@types/yargs": "^17.0.33",
|
||||
"@typescript-eslint/eslint-plugin": "^8.39.1",
|
||||
"@typescript-eslint/parser": "^8.39.1",
|
||||
"eslint": "^9.33.0",
|
||||
"eslint-config-prettier": "^10.1.8"
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"typescript": "^5.9.2"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
52
rpcsx-ui-kit/src/cli.ts
Normal file
52
rpcsx-ui-kit/src/cli.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import yargs from 'yargs';
|
||||
import { hideBin } from 'yargs/helpers';
|
||||
import { ExtensionApiGenerator } from "./cpp-generators.js"
|
||||
import { ProjectGenerator, RpcsxKit } from "./generators.js"
|
||||
|
||||
const libProject: ProjectGenerator = {
|
||||
name: "libProject",
|
||||
projectId: "lib",
|
||||
};
|
||||
const rendererProject: ProjectGenerator = {
|
||||
name: "rendererProject",
|
||||
projectId: "renderer",
|
||||
};
|
||||
const serverProject: ProjectGenerator = {
|
||||
name: "serverProject",
|
||||
projectId: "server",
|
||||
};
|
||||
|
||||
async function cppGenerator(inDir: string, outDir: string, depLibs: string[], name: string) {
|
||||
const kit = new RpcsxKit(
|
||||
[
|
||||
libProject,
|
||||
rendererProject,
|
||||
serverProject,
|
||||
],
|
||||
[],
|
||||
[new ExtensionApiGenerator({ outDir, depLibs, libPrefix: name.split("-") })]
|
||||
);
|
||||
await kit.generate([inDir]);
|
||||
await kit.commit();
|
||||
}
|
||||
|
||||
yargs(hideBin(process.argv))
|
||||
.strict()
|
||||
.command("generate", "generate code for language", (yargs) => {
|
||||
return yargs
|
||||
.option('lang', {
|
||||
alias: 'l',
|
||||
type: 'string',
|
||||
choices: ["c++"],
|
||||
demandOption: true
|
||||
})
|
||||
.option("input", { type: 'string', normalize: true, description: "Input directory", demandOption: true })
|
||||
.option("output", { type: 'string', normalize: true, description: "Output directory", demandOption: true })
|
||||
.option("name", { type: 'string', description: "Name of extension", demandOption: true })
|
||||
.option("depLibs", { type: 'string', array: true, description: "List of libraries to add as dependencies for generated library" })
|
||||
}, (argv) => {
|
||||
cppGenerator(argv.input, argv.output, argv.depLibs ?? [], argv.name);
|
||||
})
|
||||
.parseSync();
|
||||
|
||||
|
||||
460
rpcsx-ui-kit/src/cpp-generators.ts
Normal file
460
rpcsx-ui-kit/src/cpp-generators.ts
Normal file
@@ -0,0 +1,460 @@
|
||||
import { FileDb, mergeTimestamps } from "./FileDatabase.js";
|
||||
import {
|
||||
Component,
|
||||
ConfigGenerator,
|
||||
ContributionGenerator,
|
||||
generateComponentLabelName,
|
||||
generateContributions,
|
||||
generateLabelName,
|
||||
Workspace
|
||||
} from "./generators.js";
|
||||
|
||||
type CmakeGeneratorConfig = {
|
||||
outDir: string;
|
||||
libPrefix: string[];
|
||||
depLibs: string[];
|
||||
};
|
||||
|
||||
function hasContributions(component: Component) {
|
||||
return component.manifest.contributions != undefined;
|
||||
}
|
||||
|
||||
export class ExtensionApiGenerator implements ConfigGenerator {
|
||||
constructor(private config: CmakeGeneratorConfig) {
|
||||
}
|
||||
|
||||
private getPrefix(sep: string) {
|
||||
return this.config.libPrefix.join(sep);
|
||||
}
|
||||
|
||||
async processComponent(component: Component, fileDb: FileDb) {
|
||||
if (!hasContributions(component)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const componentPath = `${this.config.outDir}/${component.manifest.name}`;
|
||||
const includePath = `${componentPath}/include/${this.getPrefix("/")}`;
|
||||
const typesFile = await fileDb.createFile(`${includePath}/${component.manifest.name}/types.hpp`, component.manifestFile);
|
||||
const namespace = this.getPrefix("::");
|
||||
const fullName = this.getPrefix("-");
|
||||
|
||||
if (typesFile) {
|
||||
try {
|
||||
typesFile.content = generateContributions(component, CppTypesGenerator, namespace, this.getPrefix("/")).toString();
|
||||
} catch (e) {
|
||||
throw Error(`${component.manifest.name}: ${e}`);
|
||||
}
|
||||
}
|
||||
|
||||
const apiFile = await fileDb.createFile(`${includePath}/${component.manifest.name}/api.hpp`, component.manifestFile);
|
||||
|
||||
if (apiFile) {
|
||||
try {
|
||||
apiFile.content = generateContributions(component, CppApiGenerator, namespace, component.manifest.name).toString();
|
||||
} catch (e) {
|
||||
throw Error(`${component.manifest.name}: ${e}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const mainFile = await fileDb.createFile(`${includePath}/${component.manifest.name}.hpp`, component.manifestFile);
|
||||
|
||||
if (mainFile) {
|
||||
mainFile.content = `#pragma once
|
||||
|
||||
#include "${component.manifest.name}/types.hpp"
|
||||
#include "${component.manifest.name}/api.hpp"
|
||||
`;
|
||||
}
|
||||
|
||||
const cmakeLists = await fileDb.createFile(`${componentPath}/CMakeLists.txt`, component.manifestFile);
|
||||
|
||||
if (cmakeLists) {
|
||||
cmakeLists.content = `
|
||||
add_library(${fullName}-${component.manifest.name} INTERFACE)
|
||||
add_library(${namespace}::${component.manifest.name} ALIAS ${fullName}-${component.manifest.name})
|
||||
target_include_directories(${fullName}-${component.manifest.name} INTERFACE include)
|
||||
target_link_libraries(${fullName}-${component.manifest.name} INTERFACE
|
||||
${component.dependencies.filter(x => hasContributions(x)).map(x => ` ${namespace}::${x.manifest.name}\n`).join("")}
|
||||
${this.config.depLibs.join("\n")}
|
||||
)
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
async processWorkspace(workspace: Workspace, fileDb: FileDb) {
|
||||
const components = Object.values(workspace).filter(p => hasContributions(p));
|
||||
|
||||
const timestamp = mergeTimestamps(components.map(x => x.manifestFile));
|
||||
const cmakeLists = await fileDb.createFile(
|
||||
`${this.config.outDir}/CMakeLists.txt`,
|
||||
timestamp
|
||||
);
|
||||
|
||||
|
||||
if (cmakeLists) {
|
||||
cmakeLists.content = `
|
||||
${components.map(x => `add_subdirectory(${x.manifest.name})\n`).join("")}
|
||||
|
||||
add_library(${this.getPrefix("-")} INTERFACE)
|
||||
add_library(${this.getPrefix("::")} ALIAS ${this.getPrefix("-")})
|
||||
target_include_directories(${this.getPrefix("-")} INTERFACE include)
|
||||
target_link_libraries(${this.getPrefix("-")} INTERFACE
|
||||
${components.map(x => ` ${this.getPrefix("::")}::${x.manifest.name}\n`).join("")}
|
||||
)
|
||||
`;
|
||||
}
|
||||
|
||||
const interfaceFile = await fileDb.createFile(
|
||||
`${this.config.outDir}/include/${this.getPrefix("-")}.hpp`,
|
||||
timestamp
|
||||
);
|
||||
|
||||
if (interfaceFile) {
|
||||
interfaceFile.content = `#pragma once
|
||||
|
||||
${components.map(x => `#include <${this.getPrefix("/")}/${x.manifest.name}.hpp>`).join("\n")}
|
||||
`
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
class CppTypesGenerator implements ContributionGenerator {
|
||||
generatedTypes: Record<string, string> = {};
|
||||
includes = new Set<string>();
|
||||
constructor(private namespace: string, private libPath: string) {
|
||||
}
|
||||
|
||||
toString() {
|
||||
let result = '#pragma once\n\n';
|
||||
result += [...this.includes].map(x => `#include <${x}>`).join("\n");
|
||||
result += `\n\nnamespace ${this.namespace} {\n`;
|
||||
result += Object.values(this.generatedTypes).join("\n");
|
||||
return result + `\n} // namespace ${this.namespace}\n`;
|
||||
}
|
||||
|
||||
generateType(component: string, type: object, name: string) {
|
||||
const labelName = generateComponentLabelName(component, name, true);
|
||||
const typeName = labelName;
|
||||
|
||||
if (typeof type != 'object') {
|
||||
throw `${type}: must be object`;
|
||||
}
|
||||
if (!("type" in type)) {
|
||||
throw `${type}: type must be present`;
|
||||
}
|
||||
|
||||
if (typeof type.type != "string") {
|
||||
throw `${name}: type must be string value`;
|
||||
}
|
||||
|
||||
if (!(typeName in this.generatedTypes)) {
|
||||
let paramsType = "";
|
||||
if (type.type === "object") {
|
||||
if (!("params" in type)) {
|
||||
throw `${type}: params must be present`;
|
||||
}
|
||||
|
||||
if ((typeof type.params != 'object') || !type.params) {
|
||||
throw `${type.params}: must be object`;
|
||||
}
|
||||
|
||||
paramsType += `struct ${labelName} {\n${this.generateObjectBody(component, type.params)}};\n\n`;
|
||||
paramsType += this.generateObjectSerializer(labelName, type.params);
|
||||
paramsType += this.generateObjectDeserializer(labelName, type.params);
|
||||
} else if (type.type === "enum") {
|
||||
if (!("enumerators" in type)) {
|
||||
throw `${type}: enumerators must be present`;
|
||||
}
|
||||
|
||||
if ((typeof type.enumerators != 'object') || !type.enumerators) {
|
||||
throw `${type.enumerators}: must be object`;
|
||||
}
|
||||
|
||||
paramsType += `enum class ${labelName} {\n${this.generateEnumBody(type.enumerators)}};\n`;
|
||||
}
|
||||
|
||||
this.generatedTypes[typeName] = paramsType;
|
||||
} else {
|
||||
throw new Error(`${name}: type ${typeName} already declared`);
|
||||
}
|
||||
}
|
||||
|
||||
generateEnumBody(enumerators: object) {
|
||||
let body = "";
|
||||
|
||||
Object.keys(enumerators).forEach(fieldName => {
|
||||
const value = (enumerators as Record<string, object>)[fieldName];
|
||||
body += ` ${generateLabelName(fieldName, true)} = ${value},\n`;
|
||||
});
|
||||
|
||||
return body;
|
||||
}
|
||||
|
||||
generateObjectBody(component: string, params: object) {
|
||||
let body = "";
|
||||
|
||||
Object.keys(params).forEach(fieldName => {
|
||||
const param = (params as Record<string, object>)[fieldName];
|
||||
if (typeof param != 'object') {
|
||||
throw `${fieldName}: must be object`;
|
||||
}
|
||||
if (!("type" in param)) {
|
||||
throw `${fieldName}: type must be present`;
|
||||
}
|
||||
|
||||
if (typeof param.type != "string") {
|
||||
throw `${fieldName}: type must be string value`;
|
||||
}
|
||||
|
||||
const isOptional = ("optional" in param) && param.optional === true;
|
||||
|
||||
const fieldLabel = generateLabelName(fieldName, false);
|
||||
let fieldType = this.getTypeName(component, param.type, param);
|
||||
|
||||
if (isOptional) {
|
||||
this.addInclude("optional");
|
||||
fieldType = `std::optional<${fieldType}>`;
|
||||
}
|
||||
|
||||
body += ` ${fieldType} ${fieldLabel};\n`;
|
||||
});
|
||||
|
||||
return body;
|
||||
}
|
||||
|
||||
generateMethod(component: string, method: object, name: string) {
|
||||
const labelName = generateComponentLabelName(component, name, true);
|
||||
const requestTypeName = `${labelName}Request`;
|
||||
const responseTypeName = `${labelName}Response`;
|
||||
|
||||
if (!(requestTypeName in this.generatedTypes)) {
|
||||
let paramsType = '';
|
||||
paramsType += `struct ${requestTypeName} {\n`;
|
||||
if ("params" in method && method.params && typeof method.params == "object") {
|
||||
paramsType += this.generateObjectBody(component, method.params);
|
||||
}
|
||||
paramsType += "};\n";
|
||||
paramsType += this.generateObjectSerializer(requestTypeName, "params" in method ? method.params ?? {} : {});
|
||||
paramsType += this.generateObjectDeserializer(requestTypeName, "params" in method ? method.params ?? {} : {});
|
||||
this.generatedTypes[requestTypeName] = paramsType;
|
||||
} else {
|
||||
throw new Error(`${name}: type ${requestTypeName} already declared`);
|
||||
}
|
||||
|
||||
if (!(responseTypeName in this.generatedTypes)) {
|
||||
let responseType = `struct ${responseTypeName} {\n`;
|
||||
if ("returns" in method && method.returns && typeof method.returns == "object") {
|
||||
responseType += this.generateObjectBody(component, method.returns);
|
||||
}
|
||||
responseType += "};\n";
|
||||
responseType += this.generateObjectSerializer(responseTypeName, "returns" in method ? method.returns ?? {} : {});
|
||||
responseType += this.generateObjectDeserializer(responseTypeName, "returns" in method ? method.returns ?? {} : {});
|
||||
|
||||
this.generatedTypes[responseTypeName] = responseType;
|
||||
} else {
|
||||
throw new Error(`${name}: type ${responseTypeName} already declared`);
|
||||
}
|
||||
|
||||
this.generatedTypes[labelName] = `struct ${labelName} {
|
||||
using Request = ${requestTypeName};
|
||||
using Response = ${responseTypeName};
|
||||
};
|
||||
`
|
||||
}
|
||||
|
||||
generateNotification(component: string, notification: object, name: string) {
|
||||
const labelName = generateComponentLabelName(component, name, true);
|
||||
const requestTypeName = `${labelName}Request`;
|
||||
|
||||
if (!(requestTypeName in this.generatedTypes)) {
|
||||
let paramsType = `struct ${requestTypeName} {\n`;
|
||||
if ("params" in notification && notification.params && typeof notification.params == "object") {
|
||||
paramsType += this.generateObjectBody(component, notification.params);
|
||||
}
|
||||
paramsType += "};\n";
|
||||
paramsType += this.generateObjectSerializer(requestTypeName, "params" in notification ? notification.params ?? {} : {});
|
||||
paramsType += this.generateObjectDeserializer(requestTypeName, "params" in notification ? notification.params ?? {} : {});
|
||||
this.generatedTypes[requestTypeName] = paramsType;
|
||||
} else {
|
||||
throw new Error(`${name}: type ${requestTypeName} already declared`);
|
||||
}
|
||||
|
||||
this.generatedTypes[labelName] = `struct ${labelName} {
|
||||
using Request = ${requestTypeName};
|
||||
};
|
||||
`
|
||||
}
|
||||
|
||||
generateEvent(component: string, event: object, name: string) {
|
||||
const labelName = generateComponentLabelName(component, name, true);
|
||||
const typeName = `${labelName}Event`;
|
||||
|
||||
if (!(typeName in this.generatedTypes)) {
|
||||
if (typeof event == 'object') {
|
||||
let paramsType = `struct ${typeName} {\n`;
|
||||
paramsType += this.generateObjectBody(component, event);
|
||||
paramsType += "\n};\n"
|
||||
paramsType += this.generateObjectSerializer(typeName, event);
|
||||
paramsType += this.generateObjectDeserializer(typeName, event);
|
||||
this.generatedTypes[typeName] = paramsType;
|
||||
} else if (typeof event == 'string') {
|
||||
this.generatedTypes[typeName] = `using ${typeName} = ${this.getTypeName(component, event)};\n`;
|
||||
} else {
|
||||
throw new Error(`${name}: must be object or string`);
|
||||
}
|
||||
} else {
|
||||
throw new Error(`${name}: type ${typeName} already declared`);
|
||||
}
|
||||
}
|
||||
|
||||
addInclude(include: string) {
|
||||
this.includes.add(include);
|
||||
}
|
||||
|
||||
getTypeName(component: string, type: string, object?: object): string {
|
||||
switch (type) {
|
||||
case "string":
|
||||
this.addInclude("string")
|
||||
return "std::string";
|
||||
case "number":
|
||||
return "int";
|
||||
case "void":
|
||||
return "void";
|
||||
case "boolean":
|
||||
return "bool";
|
||||
case "json":
|
||||
this.addInclude("nlohmann/json.hpp");
|
||||
return "nlohmann::json";
|
||||
case "json-object":
|
||||
return "nlohmann::json::object_t";
|
||||
case "json-array":
|
||||
return "nlohmann::json::array_t";
|
||||
|
||||
case "array":
|
||||
if (!object || !("item-type" in object) || typeof object["item-type"] != "string") {
|
||||
throw new Error(`item-type must be defined for array`);
|
||||
}
|
||||
|
||||
this.addInclude("vector");
|
||||
return `std::vector<${this.getTypeName(component, object["item-type"])}>`;
|
||||
|
||||
default:
|
||||
if (type.startsWith("$")) {
|
||||
const [refComponent, ...nameParts] = type.split("/");
|
||||
const typeName = nameParts.join("/");
|
||||
const refComponentName = refComponent.slice(1);
|
||||
if (refComponentName != component) {
|
||||
this.addInclude(`${this.libPath}/${refComponentName}.hpp`);
|
||||
}
|
||||
|
||||
return generateComponentLabelName(refComponentName, typeName, true);
|
||||
}
|
||||
|
||||
return generateComponentLabelName(component, type, true);
|
||||
}
|
||||
}
|
||||
|
||||
generateObjectSerializer(typename: string, body: object): string {
|
||||
this.addInclude("nlohmann/json.hpp");
|
||||
|
||||
return `inline void to_json(nlohmann::json &json, const ${typename} &value) {
|
||||
json = nlohmann::json::object_t{
|
||||
${Object.keys(body).map(field => ` { "${generateLabelName(field, false)}", value.${generateLabelName(field, false)} }`).join(",\n")}
|
||||
};
|
||||
}\n\n`;
|
||||
}
|
||||
|
||||
generateObjectDeserializer(typename: string, body: object): string {
|
||||
this.addInclude("nlohmann/json.hpp");
|
||||
|
||||
return `inline void from_json(const nlohmann::json &json, ${typename} &value) {
|
||||
${Object.keys(body).map(field => {
|
||||
const label = generateLabelName(field, false);
|
||||
const fieldObject = (body as any)[field];
|
||||
const isOptional = ("optional" in fieldObject) && fieldObject.optional === true;
|
||||
if (!isOptional) {
|
||||
return ` json.at("${label}").get_to(value.${label});`;
|
||||
}
|
||||
return ` if (json.contains("${label}")) {
|
||||
value.${label} = json["${label}"];
|
||||
} else {
|
||||
value.${label} = std::nullopt;
|
||||
}`;
|
||||
}).join("\n")}
|
||||
}\n`;
|
||||
}
|
||||
};
|
||||
|
||||
class CppApiGenerator implements ContributionGenerator {
|
||||
private content = '';
|
||||
constructor(private namespace: string, private componentName: string) {
|
||||
}
|
||||
|
||||
toString() {
|
||||
const label = generateLabelName(this.componentName, true);
|
||||
|
||||
return `#pragma once
|
||||
|
||||
#include "./types.hpp"
|
||||
#include <functional>
|
||||
#include <utility>
|
||||
|
||||
namespace ${this.namespace} {
|
||||
template <typename InstanceT> class ${label}Interface {
|
||||
private:
|
||||
auto &protocol() { return static_cast<InstanceT *>(this)->getProtocol(); }
|
||||
|
||||
public:${this.content}
|
||||
};
|
||||
|
||||
struct ${label} {
|
||||
template <typename InstanceT>
|
||||
using instance = ${label}Interface<InstanceT>;
|
||||
static constexpr auto name = "${this.componentName}";
|
||||
};
|
||||
} // namespace ${this.namespace}
|
||||
`;
|
||||
}
|
||||
|
||||
generateMethod(component: string, method: object, name: string) {
|
||||
const uLabel = generateComponentLabelName(component, name, true);
|
||||
const label = generateComponentLabelName(component, name, false);
|
||||
const returnType = "returns" in method ? `${uLabel}Response` : '';
|
||||
const params = "params" in method ? `const ${uLabel}Request ¶ms, ` : '';
|
||||
|
||||
this.content += `
|
||||
auto ${label}(${params}std::function<void(${returnType})> result) {
|
||||
return protocol().call("${name}", ${params ? "params" : "{}"}, std::move(result));
|
||||
}`
|
||||
}
|
||||
|
||||
generateNotification(component: string, notification: object, name: string) {
|
||||
const uLabel = generateComponentLabelName(component, name, true);
|
||||
const label = generateComponentLabelName(component, name, false);
|
||||
const params = "params" in notification ? `const ${uLabel}Request ¶ms` : '';
|
||||
|
||||
this.content += `
|
||||
void ${label}(${params}) {
|
||||
protocol().notify("${name}", ${params ? "params" : "{}"});
|
||||
}`
|
||||
}
|
||||
|
||||
generateEvent(component: string, event: object, name: string) {
|
||||
const uLabel = generateComponentLabelName(component, name, true);
|
||||
let typeName = '';
|
||||
|
||||
if (typeof event == 'object') {
|
||||
typeName = Object.keys(event).length > 0 ? `${uLabel}Event` : '';
|
||||
} else if (typeof event == 'string') {
|
||||
typeName = `${uLabel}Event`;
|
||||
} else {
|
||||
throw new Error(`${name}: must be object or string`);
|
||||
}
|
||||
|
||||
this.content += `
|
||||
auto on${uLabel}(std::function<void(${typeName})> callback) {
|
||||
return protocol().onEvent("${name}", std::move(callback));
|
||||
}`
|
||||
}
|
||||
};
|
||||
@@ -97,7 +97,7 @@ function pascalToCamelCase(name: string) {
|
||||
return name[0].toLowerCase() + name.slice(1);
|
||||
}
|
||||
|
||||
function generateLabelName(entityName: string, isPascalCase = false) {
|
||||
export function generateLabelName(entityName: string, isPascalCase = false) {
|
||||
const name = entityName.replaceAll(" ", "-").replaceAll("_", "-").replaceAll(".", "-").replaceAll("/", "-").split("-");
|
||||
return [...(isPascalCase ? name[0][0].toUpperCase() + name[0].slice(1).toLowerCase() : name[0].toLowerCase()), ...name.slice(1).map(word => {
|
||||
if (word.length == 0) {
|
||||
@@ -107,7 +107,7 @@ function generateLabelName(entityName: string, isPascalCase = false) {
|
||||
})].reduce((a, b) => a + b);
|
||||
}
|
||||
|
||||
function generateComponentLabelName(componentName: string, entityName: string, isPascalCase = false) {
|
||||
export function generateComponentLabelName(componentName: string, entityName: string, isPascalCase = false) {
|
||||
return generateLabelName(componentName == 'core' ? entityName : `${componentName}/${entityName}`, isPascalCase);
|
||||
}
|
||||
|
||||
@@ -869,7 +869,7 @@ export function set${name}View(target: Window, params: ${name}Props) {
|
||||
return `${generatedHeader}
|
||||
import { thisComponent } from "$/component-info";
|
||||
import * as ${generateLabelName(this.externalComponent, false)} from '$${this.externalComponent}/api';
|
||||
${this.viewBody && "import { Window } from '$core/Window';" }
|
||||
${this.viewBody && "import { Window } from '$core/Window';"}
|
||||
|
||||
${this.body}
|
||||
${this.viewBody}
|
||||
@@ -997,7 +997,7 @@ export function set${name}View(target: Window, params: ${name}Props) {
|
||||
import { createError } from "$core/Error";
|
||||
import { Component } from "$core/Component";
|
||||
import * as core from "$core";
|
||||
${this.viewBody && "import { Window } from '$core/Window';" }
|
||||
${this.viewBody && "import { Window } from '$core/Window';"}
|
||||
|
||||
export async function call(_caller: Component, _method: string, _params: JsonObject | undefined): Promise<JsonObject | void> {
|
||||
throw createError(ErrorCode.MethodNotFound);
|
||||
@@ -1018,7 +1018,7 @@ import { createError } from "$core/Error";
|
||||
import { Component } from "$core/Component";
|
||||
import { thisComponent } from "$/component-info";
|
||||
import * as core from "$core";
|
||||
${this.viewBody && "import { Window } from '$core/Window';" }
|
||||
${this.viewBody && "import { Window } from '$core/Window';"}
|
||||
export { thisComponent } from "$/component-info";
|
||||
|
||||
${this.body}
|
||||
@@ -1135,7 +1135,7 @@ export function popView() {
|
||||
}
|
||||
};
|
||||
|
||||
function generateContributions<Params extends [], RT extends ContributionGenerator>(component: Component, Generator: new (...params: Params) => RT, ...params: Params) {
|
||||
export function generateContributions<Params extends any[], RT extends ContributionGenerator>(component: Component, Generator: new (...params: Params) => RT, ...params: Params) {
|
||||
const generator = new Generator(...params);
|
||||
const contributions = component.manifest.contributions ?? {};
|
||||
|
||||
@@ -1345,7 +1345,7 @@ export class TsServerGenerator implements ProjectGenerator {
|
||||
return ["lib"].includes(projectId);
|
||||
}
|
||||
|
||||
async generateContributionFile<Params extends []>(sourceComponent: Component, project: Project, fileDb: FileDb, generatedFileName: string, Generator: new (...params: Params) => ContributionGenerator, ...params: Params) {
|
||||
async generateContributionFile<Params extends any[]>(sourceComponent: Component, project: Project, fileDb: FileDb, generatedFileName: string, Generator: new (...params: Params) => ContributionGenerator, ...params: Params) {
|
||||
const projectPath = path.join(this.config.outDir, project.component.manifest.name, project.name);
|
||||
const genDir = path.join(projectPath, "src");
|
||||
const generatedFilePath = path.join(genDir, generatedFileName);
|
||||
@@ -1360,7 +1360,7 @@ export class TsServerGenerator implements ProjectGenerator {
|
||||
}
|
||||
}
|
||||
|
||||
async generateProject<Params extends []>(project: Project, fileDb: FileDb, projectName: string, generatedFileName: string, Generator: new (...params: Params) => ContributionGenerator, ...params: Params) {
|
||||
async generateProject<Params extends any[]>(project: Project, fileDb: FileDb, projectName: string, generatedFileName: string, Generator: new (...params: Params) => ContributionGenerator, ...params: Params) {
|
||||
const newProjectPath = path.join(this.config.outDir, project.component.manifest.name, projectName);
|
||||
const genDir = path.join(newProjectPath, "src");
|
||||
const generatedFilePath = path.join(genDir, generatedFileName);
|
||||
|
||||
@@ -269,8 +269,7 @@
|
||||
}
|
||||
},
|
||||
"shutdown": {
|
||||
"handler": "shutdown",
|
||||
"params": {}
|
||||
"handler": "shutdown"
|
||||
},
|
||||
"component/activate": {
|
||||
"handler": "activateComponent",
|
||||
|
||||
@@ -4,4 +4,4 @@ export const builtinResourcesPath = import.meta.dirname;
|
||||
export const rootPath = path.dirname(process.execPath);
|
||||
export const configPath = rootPath;
|
||||
export const extensionsPath = path.join(rootPath, "extensions");
|
||||
export const localExtensionsPath = path.join(extensionsPath, ".local");
|
||||
export const localExtensionsPath = path.join(builtinResourcesPath, "extensions");
|
||||
|
||||
@@ -63,19 +63,26 @@ export class ComponentInstance implements ComponentContext {
|
||||
}
|
||||
|
||||
async activate(settings?: JsonObject) {
|
||||
if (!settings) {
|
||||
const schema = this.getContribution("settings");
|
||||
if (schema) {
|
||||
settings = settingsGet(this.getName(), schema as Record<string, Schema>);
|
||||
this.activated = true;
|
||||
|
||||
try {
|
||||
if (!settings) {
|
||||
const schema = this.getContribution("settings");
|
||||
if (schema) {
|
||||
settings = settingsGet(this.getName(), schema as Record<string, Schema>);
|
||||
}
|
||||
|
||||
settings ??= {};
|
||||
}
|
||||
|
||||
settings ??= {};
|
||||
onComponentActivation(this);
|
||||
await this.impl.activate(this, settings);
|
||||
} catch (e) {
|
||||
this.activated = false;
|
||||
throw e;
|
||||
}
|
||||
|
||||
await this.impl.activate(this, settings);
|
||||
onComponentActivation(this);
|
||||
this.emitEvent(activateEvent);
|
||||
this.activated = true;
|
||||
}
|
||||
|
||||
async deactivate() {
|
||||
|
||||
@@ -77,7 +77,7 @@ export class Extension implements IComponentImpl {
|
||||
if (line.length == 0) {
|
||||
continue;
|
||||
}
|
||||
process.stderr.write(`${date} [${this.manifest.name}-v${this.manifest.version}] ${line}\n`);
|
||||
process.stderr.write(`${date} [${this.manifest.name[0].text}-v${this.manifest.version}] ${line}\n`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ export async function loadExtension(request: ExtensionLoadRequest): Promise<Exte
|
||||
return;
|
||||
}
|
||||
|
||||
const extensionManifestLocation = path.join(locations.extensionsPath, request.id, "extension.json");
|
||||
const extensionManifestLocation = path.join(locations.localExtensionsPath, request.id, "extension.json");
|
||||
|
||||
const manifestText = await (async () => {
|
||||
try {
|
||||
@@ -36,7 +36,7 @@ export async function loadExtension(request: ExtensionLoadRequest): Promise<Exte
|
||||
|
||||
const process = await (async () => {
|
||||
try {
|
||||
return launcher.launch(path.join(locations.extensionsPath, request.id, manifest.executable), manifest.args ?? [], {
|
||||
return launcher.launch(path.join(locations.localExtensionsPath, request.id, manifest.executable), manifest.args ?? [], {
|
||||
launcherRequirements: manifest.launcher.requirements ?? {},
|
||||
});
|
||||
} catch {
|
||||
|
||||
@@ -59,7 +59,7 @@
|
||||
"optional": true
|
||||
},
|
||||
"launcher": {
|
||||
"type": "launcher-info",
|
||||
"type": "$core/launcher-info",
|
||||
"optional": true
|
||||
},
|
||||
"title-id": {
|
||||
@@ -113,7 +113,7 @@
|
||||
"optional": true
|
||||
},
|
||||
"launcher": {
|
||||
"type": "launcher-info",
|
||||
"type": "$core/launcher-info",
|
||||
"optional": true
|
||||
},
|
||||
"title-id": {
|
||||
|
||||
2
rpcsx-ui/src/extension-host/server/extension-host.ts
Normal file
2
rpcsx-ui/src/extension-host/server/extension-host.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export function activateLocalExtensions(_list: Set<string>) {
|
||||
}
|
||||
43
rpcsx-ui/src/extension-host/server/extension-host.web.ts
Normal file
43
rpcsx-ui/src/extension-host/server/extension-host.web.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import * as core from "$core";
|
||||
import * as locations from "$core/locations";
|
||||
import fs from 'fs/promises';
|
||||
import path from 'path';
|
||||
|
||||
export async function activateLocalExtensions(list: Set<string>) {
|
||||
try {
|
||||
for (const entry of await fs.readdir(locations.localExtensionsPath, { withFileTypes: true, encoding: 'utf-8' })) {
|
||||
if (!entry.isDirectory()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
await fs.stat(path.join(path.join(entry.parentPath, entry.name, "extension.json")));
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
await core.extensionLoad({ id: entry.name });
|
||||
} catch (e) {
|
||||
console.error(`failed to load local extension ${entry.name}`, e);
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
await core.componentActivate({ id: entry.name });
|
||||
} catch (e) {
|
||||
console.error(`failed to activate extension ${entry.name}`, e);
|
||||
try {
|
||||
await core.extensionUnload({ id: entry.name });
|
||||
} catch (e) {
|
||||
console.error(`failed to unload extension ${entry.name}`, e);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
list.add(entry.name);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(`failed to load local extensions`, e);
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,17 @@
|
||||
import { activateLocalExtensions } from './extension-host';
|
||||
import * as core from "$core";
|
||||
|
||||
const activatedExtensions = new Set<string>();
|
||||
|
||||
export function activate() {
|
||||
return activateLocalExtensions(activatedExtensions);
|
||||
}
|
||||
|
||||
export async function deactivate() {
|
||||
for (const extension of activatedExtensions) {
|
||||
core.componentDeactivate({
|
||||
id: extension
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
{
|
||||
"name": "github",
|
||||
"version": "0.1.0",
|
||||
|
||||
"contributions": {
|
||||
"settings": {
|
||||
"url": {
|
||||
@@ -10,6 +9,17 @@
|
||||
}
|
||||
},
|
||||
"types": {
|
||||
"asset": {
|
||||
"type": "object",
|
||||
"params": {
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"browser_download_url": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"release": {
|
||||
"type": "object",
|
||||
"params": {
|
||||
@@ -21,17 +31,6 @@
|
||||
"item-type": "asset"
|
||||
}
|
||||
}
|
||||
},
|
||||
"asset": {
|
||||
"type": "object",
|
||||
"params": {
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"browser_download_url": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"methods": {
|
||||
|
||||
Reference in New Issue
Block a user