mirror of
https://github.com/MyLuxy/overdrive.git
synced 2026-10-18 01:01:58 +02:00
620 lines
25 KiB
C++
620 lines
25 KiB
C++
// Copyright (C) 2026 half144 and PS5 React contributors
|
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
// Additional attribution term: see LICENSE-ATTRIBUTION.
|
|
#include "host_api.hpp"
|
|
#include "app_config.hpp"
|
|
#include "removals.hpp"
|
|
#include "text_shaper.hpp"
|
|
|
|
extern "C" {
|
|
#include "er_runtime.h"
|
|
}
|
|
#include <fcntl.h>
|
|
#include <sys/stat.h>
|
|
#include <unistd.h>
|
|
#include <cerrno>
|
|
#include <cstdio>
|
|
#include <climits>
|
|
#include <cmath>
|
|
#include <cstdlib>
|
|
#include <cstring>
|
|
#include <ctime>
|
|
#include <iterator>
|
|
#include <string>
|
|
#include <utility>
|
|
#include <vector>
|
|
|
|
std::atomic<const char*> host::current_call{nullptr};
|
|
|
|
namespace {
|
|
bool exit_requested = false;
|
|
int refresh_rate = PS5_REACT_REFRESH_RATE, refresh_request = 0;
|
|
GamepadState gamepad;
|
|
|
|
// A JS path argument and its platform path; frees the JS string on scope exit.
|
|
class Path {
|
|
public:
|
|
Path(JSContext* ctx, JSValueConst value) : ctx_(ctx), app_(JS_ToCString(ctx, value)) {
|
|
resolved_ = app_ && host::resolve_path(app_, real_, sizeof real_);
|
|
}
|
|
~Path() { if (app_) JS_FreeCString(ctx_, app_); }
|
|
Path(const Path&) = delete;
|
|
Path& operator=(const Path&) = delete;
|
|
|
|
explicit operator bool() const { return resolved_; }
|
|
const char* app() const { return app_; }
|
|
const char* real() const { return real_; }
|
|
|
|
// Call immediately after the failing operation so errno still describes it.
|
|
JSValue error(const char* call) const {
|
|
if (!app_) return JS_EXCEPTION;
|
|
if (!resolved_) return JS_ThrowPlainError(ctx_, "%s %s: Path is not allowed", call, app_);
|
|
return JS_ThrowPlainError(ctx_, "%s %s: %s", call, app_, std::strerror(errno));
|
|
}
|
|
|
|
private:
|
|
JSContext* ctx_;
|
|
const char* app_;
|
|
char real_[PATH_MAX] = "";
|
|
bool resolved_ = false;
|
|
};
|
|
|
|
double modified_ms(const struct stat& st) {
|
|
#ifdef __APPLE__
|
|
const timespec& t = st.st_mtimespec;
|
|
#else
|
|
const timespec& t = st.st_mtim;
|
|
#endif
|
|
return static_cast<double>(t.tv_sec) * 1000.0 + static_cast<double>(t.tv_nsec / 1000000);
|
|
}
|
|
|
|
const char* base_name(const char* path) {
|
|
const char* slash = std::strrchr(path, '/');
|
|
return slash && slash[1] ? slash + 1 : path;
|
|
}
|
|
|
|
JSValue entry(JSContext* ctx, const char* name, const struct stat& st) {
|
|
JSValue object = JS_NewObject(ctx);
|
|
JS_SetPropertyStr(ctx, object, "name", JS_NewString(ctx, name));
|
|
JS_SetPropertyStr(ctx, object, "isDirectory", JS_NewBool(ctx, S_ISDIR(st.st_mode)));
|
|
JS_SetPropertyStr(ctx, object, "isFile", JS_NewBool(ctx, S_ISREG(st.st_mode)));
|
|
JS_SetPropertyStr(ctx, object, "size", JS_NewInt64(ctx, st.st_size));
|
|
JS_SetPropertyStr(ctx, object, "modified", JS_NewFloat64(ctx, modified_ms(st)));
|
|
JS_SetPropertyStr(ctx, object, "device", JS_NewString(ctx, std::to_string(st.st_dev).c_str()));
|
|
JS_SetPropertyStr(ctx, object, "inode", JS_NewString(ctx, std::to_string(st.st_ino).c_str()));
|
|
return object;
|
|
}
|
|
|
|
struct Listing {
|
|
JSContext* ctx;
|
|
const char* directory;
|
|
JSValue list;
|
|
std::uint32_t count;
|
|
};
|
|
|
|
void add_entry(const char* name, void* user) {
|
|
Listing& listing = *static_cast<Listing*>(user);
|
|
char child[PATH_MAX];
|
|
struct stat st;
|
|
if (std::snprintf(child, sizeof child, "%s/%s", listing.directory, name) >= static_cast<int>(sizeof child)) return;
|
|
// Fall back to the link itself so a dangling symlink is still listed.
|
|
if (stat(child, &st) != 0 && lstat(child, &st) != 0) return;
|
|
JS_SetPropertyUint32(listing.ctx, listing.list, listing.count++, entry(listing.ctx, name, st));
|
|
}
|
|
|
|
JSValue fs_read_dir(JSContext* ctx, JSValueConst, int, JSValueConst* argv) {
|
|
Path path(ctx, argv[0]);
|
|
if (!path) return path.error("fs.readDir");
|
|
Listing listing{ctx, path.real(), JS_NewArray(ctx), 0};
|
|
if (!host::read_dir(path.real(), add_entry, &listing)) {
|
|
JS_FreeValue(ctx, listing.list);
|
|
return path.error("fs.readDir");
|
|
}
|
|
return listing.list;
|
|
}
|
|
|
|
JSValue fs_stat(JSContext* ctx, JSValueConst, int, JSValueConst* argv) {
|
|
Path path(ctx, argv[0]);
|
|
struct stat st;
|
|
if (!path || stat(path.real(), &st) != 0)
|
|
return path && errno == ENOENT ? JS_NULL : path.error("fs.stat");
|
|
return entry(ctx, base_name(path.app()), st);
|
|
}
|
|
|
|
JSValue fs_read_file(JSContext* ctx, JSValueConst, int, JSValueConst* argv) {
|
|
Path path(ctx, argv[0]);
|
|
const int fd = path ? open(path.real(), O_RDONLY) : -1;
|
|
if (fd < 0) return path.error("fs.readFile");
|
|
struct stat st;
|
|
if (fstat(fd, &st) != 0) {
|
|
const JSValue error = path.error("fs.readFile");
|
|
close(fd);
|
|
return error;
|
|
}
|
|
if (st.st_size > static_cast<off_t>(kMaxReadBytes)) {
|
|
close(fd);
|
|
return JS_ThrowPlainError(ctx, "fs.readFile %s: File is larger than %zu bytes", path.app(), kMaxReadBytes);
|
|
}
|
|
// Size from fstat is only a hint; special files report 0, so read until EOF within the cap. The
|
|
// buffer starts at the reported size, so reading a small file does not take the cap from the heap.
|
|
std::size_t capacity = st.st_size > 0 ? static_cast<std::size_t>(st.st_size) + 1 : kMaxReadBytes + 1;
|
|
char* buffer = static_cast<char*>(std::malloc(capacity));
|
|
std::size_t length = 0;
|
|
ssize_t count = 0;
|
|
while (buffer && length <= kMaxReadBytes && (count = read(fd, buffer + length, capacity - length)) > 0) {
|
|
length += static_cast<std::size_t>(count);
|
|
if (length == capacity && capacity <= kMaxReadBytes) {
|
|
char* grown = static_cast<char*>(std::realloc(buffer, kMaxReadBytes + 1));
|
|
if (!grown) std::free(buffer);
|
|
buffer = grown;
|
|
capacity = kMaxReadBytes + 1;
|
|
}
|
|
}
|
|
if (!buffer) {
|
|
close(fd);
|
|
return JS_ThrowOutOfMemory(ctx);
|
|
}
|
|
JSValue result;
|
|
if (count < 0) result = path.error("fs.readFile");
|
|
else if (length > kMaxReadBytes)
|
|
result = JS_ThrowPlainError(ctx, "fs.readFile %s: File is larger than %zu bytes", path.app(), kMaxReadBytes);
|
|
else result = JS_NewStringLen(ctx, buffer, length);
|
|
std::free(buffer);
|
|
close(fd);
|
|
return result;
|
|
}
|
|
|
|
JSValue fs_write_file(JSContext* ctx, JSValueConst, int, JSValueConst* argv) {
|
|
Path path(ctx, argv[0]);
|
|
if (!path) return path.error("fs.writeFile");
|
|
std::size_t length = 0;
|
|
const char* text = JS_ToCStringLen(ctx, &length, argv[1]);
|
|
if (!text) return JS_EXCEPTION;
|
|
const int append = JS_ToBool(ctx, argv[2]);
|
|
// A state file saved then renamed over the old one must reach the disk first, or a power cut or
|
|
// forced close right after can leave it empty under its final name.
|
|
const int sync = JS_ToBool(ctx, argv[3]);
|
|
const int fd = open(path.real(), O_WRONLY | O_CREAT | (append > 0 ? O_APPEND : O_TRUNC), 0644);
|
|
bool ok = fd >= 0;
|
|
for (std::size_t written = 0; ok && written < length;) {
|
|
const ssize_t count = write(fd, text + written, length - written);
|
|
ok = count > 0 || (count < 0 && errno == EINTR);
|
|
if (count > 0) written += static_cast<std::size_t>(count);
|
|
}
|
|
JS_FreeCString(ctx, text);
|
|
if (ok && sync > 0) ok = fsync(fd) == 0;
|
|
JSValue result = ok ? JS_UNDEFINED : path.error("fs.writeFile");
|
|
if (fd >= 0 && close(fd) != 0 && ok) result = path.error("fs.writeFile");
|
|
return result;
|
|
}
|
|
|
|
bool make_directories(char* path) {
|
|
for (char* slash = std::strchr(path + 1, '/'); slash; slash = std::strchr(slash + 1, '/')) {
|
|
*slash = '\0';
|
|
const bool ok = mkdir(path, 0755) == 0 || errno == EEXIST;
|
|
*slash = '/';
|
|
if (!ok) return false;
|
|
}
|
|
struct stat st;
|
|
if (mkdir(path, 0755) == 0) return true;
|
|
if (errno != EEXIST || stat(path, &st) != 0) return false;
|
|
errno = ENOTDIR;
|
|
return S_ISDIR(st.st_mode);
|
|
}
|
|
|
|
JSValue fs_mkdir(JSContext* ctx, JSValueConst, int, JSValueConst* argv) {
|
|
Path path(ctx, argv[0]);
|
|
if (!path) return path.error("fs.mkdir");
|
|
char real[PATH_MAX];
|
|
std::strcpy(real, path.real());
|
|
const bool ok = JS_ToBool(ctx, argv[1]) > 0 ? make_directories(real) : mkdir(real, 0755) == 0;
|
|
return ok ? JS_UNDEFINED : path.error("fs.mkdir");
|
|
}
|
|
|
|
JSValue fs_remove(JSContext* ctx, JSValueConst, int, JSValueConst* argv) {
|
|
Path path(ctx, argv[0]);
|
|
struct stat st;
|
|
if (!path || lstat(path.real(), &st) != 0) return path.error("fs.remove");
|
|
const int status = S_ISDIR(st.st_mode) ? rmdir(path.real()) : unlink(path.real());
|
|
return status == 0 ? JS_UNDEFINED : path.error("fs.remove");
|
|
}
|
|
|
|
JSValue fs_remove_tree(JSContext* ctx, JSValueConst, int, JSValueConst* argv) {
|
|
Path path(ctx, argv[0]);
|
|
if (!path || !removals::remove_tree(path.real(), false)) return path.error("fs.removeTree");
|
|
return JS_UNDEFINED;
|
|
}
|
|
|
|
JSValue fs_remove_async(JSContext* ctx, JSValueConst, int, JSValueConst* argv) {
|
|
Path path(ctx, argv[0]);
|
|
if (!path) return path.error("fs.removeAsync");
|
|
std::string error;
|
|
const auto id = removals::start(path.real(), error);
|
|
if (!id) return JS_ThrowPlainError(ctx, "fs.removeAsync %s: %s", path.app(), error.c_str());
|
|
return JS_NewUint32(ctx, id);
|
|
}
|
|
|
|
JSValue removals_poll(JSContext* ctx, JSValueConst, int, JSValueConst*) {
|
|
JSValue out = JS_NewArray(ctx);
|
|
std::uint32_t index = 0;
|
|
for (const auto& snapshot : removals::poll()) {
|
|
JSValue item = JS_NewObject(ctx);
|
|
JS_SetPropertyStr(ctx, item, "id", JS_NewUint32(ctx, snapshot.id));
|
|
JS_SetPropertyStr(ctx, item, "state", JS_NewString(ctx, snapshot.state.c_str()));
|
|
JS_SetPropertyStr(ctx, item, "error", JS_NewString(ctx, snapshot.error.c_str()));
|
|
JS_SetPropertyUint32(ctx, out, index++, item);
|
|
}
|
|
return out;
|
|
}
|
|
|
|
JSValue fs_rename(JSContext* ctx, JSValueConst, int, JSValueConst* argv) {
|
|
Path from(ctx, argv[0]);
|
|
if (!from) return from.error("fs.rename");
|
|
Path to(ctx, argv[1]);
|
|
if (!to) return to.error("fs.rename");
|
|
if (rename(from.real(), to.real()) == 0) return JS_UNDEFINED;
|
|
return JS_ThrowPlainError(ctx, "fs.rename %s -> %s: %s", from.app(), to.app(), std::strerror(errno));
|
|
}
|
|
|
|
JSValue fs_chmod(JSContext* ctx, JSValueConst, int, JSValueConst* argv) {
|
|
Path path(ctx, argv[0]);
|
|
if (!path) return path.error("fs.chmod");
|
|
std::uint32_t mode = 0;
|
|
if (JS_ToUint32(ctx, &mode, argv[1]) || mode > 07777) return JS_ThrowTypeError(ctx, "fs.chmod: mode must be 0 to 0o7777");
|
|
if (chmod(path.real(), static_cast<mode_t>(mode)) == 0) return JS_UNDEFINED;
|
|
return JS_ThrowPlainError(ctx, "fs.chmod %s: %s", path.app(), std::strerror(errno));
|
|
}
|
|
|
|
JSValue mount_object(JSContext* ctx, const char* device, const char* path, const char* type) {
|
|
JSValue mount = JS_NewObject(ctx);
|
|
JS_SetPropertyStr(ctx, mount, "device", JS_NewString(ctx, device));
|
|
JS_SetPropertyStr(ctx, mount, "path", JS_NewString(ctx, path));
|
|
JS_SetPropertyStr(ctx, mount, "type", JS_NewString(ctx, type));
|
|
return mount;
|
|
}
|
|
|
|
JSValue fs_mounts(JSContext* ctx, JSValueConst, int, JSValueConst*) {
|
|
static host::MountEntry mounts[64];
|
|
const int count = host::list_mounts(mounts, static_cast<int>(std::size(mounts)));
|
|
if (count < 0) return JS_ThrowPlainError(ctx, "fs.mounts: %s", std::strerror(errno));
|
|
JSValue list = JS_NewArray(ctx);
|
|
for (int i = 0; i < count; ++i)
|
|
JS_SetPropertyUint32(ctx, list, static_cast<std::uint32_t>(i),
|
|
mount_object(ctx, mounts[i].device, mounts[i].path, mounts[i].type));
|
|
return list;
|
|
}
|
|
|
|
JSValue fs_disk_usage(JSContext* ctx, JSValueConst, int, JSValueConst* argv) {
|
|
Path path(ctx, argv[0]);
|
|
double total = 0, free = 0;
|
|
if (!path || !host::disk_usage(path.real(), total, free)) return path.error("fs.diskUsage");
|
|
JSValue usage = JS_NewObject(ctx);
|
|
JS_SetPropertyStr(ctx, usage, "total", JS_NewFloat64(ctx, total));
|
|
JS_SetPropertyStr(ctx, usage, "free", JS_NewFloat64(ctx, free));
|
|
return usage;
|
|
}
|
|
|
|
JSValue known(JSContext* ctx, std::int64_t value) {
|
|
return value < 0 ? JS_NULL : JS_NewInt64(ctx, value);
|
|
}
|
|
|
|
JSValue known(JSContext* ctx, const char* value) {
|
|
return *value ? JS_NewString(ctx, value) : JS_NULL;
|
|
}
|
|
|
|
JSValue device_info(JSContext* ctx, JSValueConst, int, JSValueConst*) {
|
|
host::DeviceInfo info;
|
|
host::device_info(info);
|
|
JSValue object = JS_NewObject(ctx);
|
|
JS_SetPropertyStr(ctx, object, "model", known(ctx, info.model));
|
|
JS_SetPropertyStr(ctx, object, "firmware", known(ctx, info.firmware));
|
|
JS_SetPropertyStr(ctx, object, "cpuTemperature", known(ctx, info.cpu_temperature));
|
|
JS_SetPropertyStr(ctx, object, "socTemperature", known(ctx, info.soc_temperature));
|
|
JS_SetPropertyStr(ctx, object, "cpuFrequency", known(ctx, info.cpu_frequency));
|
|
JS_SetPropertyStr(ctx, object, "freeMemory", known(ctx, info.free_memory));
|
|
JS_SetPropertyStr(ctx, object, "processTime", known(ctx, info.process_time));
|
|
JS_SetPropertyStr(ctx, object, "language", known(ctx, info.language));
|
|
return object;
|
|
}
|
|
|
|
long long days_from_civil(long long y, int m, int d) {
|
|
y -= m <= 2;
|
|
const long long era = (y >= 0 ? y : y - 399) / 400, yoe = y - era * 400;
|
|
const long long doy = (153 * (m + (m > 2 ? -3 : 9)) + 2) / 5 + d - 1;
|
|
return era * 146097 + yoe * 365 + yoe / 4 - yoe / 100 + doy - 719468;
|
|
}
|
|
|
|
// Minutes east of UTC at an instant: localtime's calendar fields less the instant itself. Not
|
|
// tm_gmtoff: the PS5's libc leaves it 0 while the fields carry the console's time zone.
|
|
JSValue device_utc_offset(JSContext* ctx, JSValueConst, int, JSValueConst* argv) {
|
|
double ms = 0;
|
|
if (JS_ToFloat64(ctx, &ms, argv[0]) || !std::isfinite(ms)) {
|
|
return JS_ThrowTypeError(ctx, "device.utcOffset: time must be milliseconds since the epoch");
|
|
}
|
|
const time_t seconds = static_cast<time_t>(std::floor(ms / 1000));
|
|
tm local = {};
|
|
if (!localtime_r(&seconds, &local)) return JS_ThrowInternalError(ctx, "device.utcOffset: %s", strerror(errno));
|
|
const long long fields = days_from_civil(local.tm_year + 1900LL, local.tm_mon + 1, local.tm_mday) * 86400
|
|
+ local.tm_hour * 3600 + local.tm_min * 60 + local.tm_sec;
|
|
return JS_NewInt32(ctx, static_cast<int>((fields - seconds) / 60));
|
|
}
|
|
|
|
JSValue user_object(JSContext* ctx, const host::User& user) {
|
|
JSValue object = JS_NewObject(ctx);
|
|
JS_SetPropertyStr(ctx, object, "id", JS_NewInt32(ctx, user.id));
|
|
JS_SetPropertyStr(ctx, object, "name", JS_NewString(ctx, user.name));
|
|
return object;
|
|
}
|
|
|
|
JSValue users_foreground(JSContext* ctx, JSValueConst, int, JSValueConst*) {
|
|
host::User user;
|
|
return host::foreground_user(user) ? user_object(ctx, user) : JS_NULL;
|
|
}
|
|
|
|
JSValue users_logged_in(JSContext* ctx, JSValueConst, int, JSValueConst*) {
|
|
host::User users[16];
|
|
const int count = host::logged_in_users(users, 16);
|
|
JSValue list = JS_NewArray(ctx);
|
|
for (int i = 0; i < count; ++i)
|
|
JS_SetPropertyUint32(ctx, list, static_cast<std::uint32_t>(i), user_object(ctx, users[i]));
|
|
return list;
|
|
}
|
|
|
|
JSValue notify(JSContext* ctx, JSValueConst, int, JSValueConst* argv) {
|
|
const char* message = JS_ToCString(ctx, argv[0]);
|
|
if (!message) return JS_EXCEPTION;
|
|
const bool has_sub = !JS_IsUndefined(argv[1]) && !JS_IsNull(argv[1]);
|
|
const char* sub = has_sub ? JS_ToCString(ctx, argv[1]) : nullptr;
|
|
if (has_sub && !sub) {
|
|
JS_FreeCString(ctx, message);
|
|
return JS_EXCEPTION;
|
|
}
|
|
const bool ok = host::notify(message, sub ? sub : "");
|
|
if (sub) JS_FreeCString(ctx, sub);
|
|
JS_FreeCString(ctx, message);
|
|
return JS_NewBool(ctx, ok);
|
|
}
|
|
|
|
JSValue open_url(JSContext* ctx, JSValueConst, int, JSValueConst* argv) {
|
|
const char* url = JS_ToCString(ctx, argv[0]);
|
|
if (!url) return JS_EXCEPTION;
|
|
const bool ok = host::open_url(url);
|
|
JS_FreeCString(ctx, url);
|
|
return JS_NewBool(ctx, ok);
|
|
}
|
|
|
|
JSValue pad_set_light_bar(JSContext* ctx, JSValueConst, int, JSValueConst* argv) {
|
|
std::uint8_t rgb[3];
|
|
for (int i = 0; i < 3; ++i) {
|
|
std::int32_t value = 0;
|
|
if (JS_ToInt32(ctx, &value, argv[i])) return JS_EXCEPTION;
|
|
rgb[i] = static_cast<std::uint8_t>(value < 0 ? 0 : value > 255 ? 255 : value);
|
|
}
|
|
host::set_light_bar(rgb[0], rgb[1], rgb[2]);
|
|
return JS_UNDEFINED;
|
|
}
|
|
|
|
JSValue pad_reset_light_bar(JSContext*, JSValueConst, int, JSValueConst*) {
|
|
host::reset_light_bar();
|
|
return JS_UNDEFINED;
|
|
}
|
|
|
|
JSValue pad_set_trigger_effect(JSContext* ctx, JSValueConst, int, JSValueConst* argv) {
|
|
std::int32_t mask = 0, mode = 0, a = 0, b = 0, c = 0;
|
|
if (JS_ToInt32(ctx, &mask, argv[0]) || JS_ToInt32(ctx, &mode, argv[1]))
|
|
return JS_EXCEPTION;
|
|
if (mask < 1 || mask > 3) return JS_ThrowRangeError(ctx, "pad.setTriggerEffect: mask 1-3; got %d", mask);
|
|
// Off takes no parameters; the other modes take two or three bytes.
|
|
const int count = mode == 0 ? 0 : mode == 1 ? 2 : 3;
|
|
if (count && (JS_ToInt32(ctx, &a, argv[2]) || JS_ToInt32(ctx, &b, argv[3]) || (count == 3 && JS_ToInt32(ctx, &c, argv[4]))))
|
|
return JS_EXCEPTION;
|
|
switch (mode) {
|
|
case 0: break;
|
|
case 1:
|
|
if (a < 0 || a > 9 || b < 1 || b > 8)
|
|
return JS_ThrowRangeError(ctx, "pad.setTriggerEffect: feedback position 0-9, strength 1-8; got %d, %d", a, b);
|
|
break;
|
|
case 2:
|
|
if (a < 2 || a > 7 || b <= a || b > 8 || c < 1 || c > 8)
|
|
return JS_ThrowRangeError(ctx, "pad.setTriggerEffect: weapon start 2-7, end start+1-8, strength 1-8; got %d, %d, %d",
|
|
a, b, c);
|
|
break;
|
|
case 3:
|
|
if (a < 0 || a > 9 || b < 1 || b > 8 || c < 1 || c > 255)
|
|
return JS_ThrowRangeError(ctx, "pad.setTriggerEffect: vibration position 0-9, amplitude 1-8, frequency 1-255; got %d, %d, %d",
|
|
a, b, c);
|
|
break;
|
|
default: return JS_ThrowRangeError(ctx, "pad.setTriggerEffect: mode 0 off, 1 feedback, 2 weapon or 3 vibration; got %d", mode);
|
|
}
|
|
const std::uint8_t params[] = {static_cast<std::uint8_t>(a), static_cast<std::uint8_t>(b), static_cast<std::uint8_t>(c)};
|
|
host::set_trigger_effect(static_cast<unsigned>(mask), mode, params, static_cast<std::size_t>(count));
|
|
return JS_UNDEFINED;
|
|
}
|
|
|
|
JSValue pad_vibrate(JSContext* ctx, JSValueConst, int, JSValueConst* argv) {
|
|
double strength = 0, seconds = 0;
|
|
if (JS_ToFloat64(ctx, &strength, argv[0]) || JS_ToFloat64(ctx, &seconds, argv[1])) return JS_EXCEPTION;
|
|
// Comparisons are written so NaN collapses to zero.
|
|
strength = strength > 0 ? (strength < 1 ? strength : 1) : 0;
|
|
host::vibrate(static_cast<float>(strength), static_cast<float>(seconds > 0 ? seconds : 0));
|
|
return JS_UNDEFINED;
|
|
}
|
|
|
|
JSValue power_keep_awake(JSContext* ctx, JSValueConst, int, JSValueConst* argv) {
|
|
host::keep_awake(JS_ToBool(ctx, argv[0]) > 0);
|
|
return JS_UNDEFINED;
|
|
}
|
|
|
|
JSValue display_refresh_rate(JSContext* ctx, JSValueConst, int, JSValueConst*) {
|
|
return JS_NewInt32(ctx, refresh_rate);
|
|
}
|
|
|
|
JSValue display_set_refresh_rate(JSContext* ctx, JSValueConst, int, JSValueConst* argv) {
|
|
std::int32_t hz = 0;
|
|
if (JS_ToInt32(ctx, &hz, argv[0])) return JS_EXCEPTION;
|
|
const int bit = hz == 60 ? 1 : hz == 120 ? 2 : 0;
|
|
if (!bit) return JS_ThrowRangeError(ctx, "display.setRefreshRate: 60 or 120 Hz; got %d", hz);
|
|
if (!(PS5_REACT_REFRESH_RATES & bit))
|
|
return JS_ThrowTypeError(ctx, "display.setRefreshRate: %d Hz is not in app.json \"refreshRates\"", hz);
|
|
// Asking for the current rate cancels a switch that has not happened yet.
|
|
refresh_request = hz == refresh_rate ? 0 : hz;
|
|
return JS_UNDEFINED;
|
|
}
|
|
|
|
JSValue text_set_language(JSContext* ctx, JSValueConst, int, JSValueConst* argv) {
|
|
const char* tag = JS_ToCString(ctx, argv[0]);
|
|
if (!tag) return JS_EXCEPTION;
|
|
text_shaper::set_language(tag);
|
|
JS_FreeCString(ctx, tag);
|
|
return JS_UNDEFINED;
|
|
}
|
|
|
|
JSValue pad_state(JSContext* ctx, JSValueConst, int, JSValueConst*) {
|
|
JSValue state = JS_NewObject(ctx);
|
|
JS_SetPropertyStr(ctx, state, "connected", JS_NewBool(ctx, gamepad.connected));
|
|
JS_SetPropertyStr(ctx, state, "leftX", JS_NewFloat64(ctx, gamepad.left_x));
|
|
JS_SetPropertyStr(ctx, state, "leftY", JS_NewFloat64(ctx, gamepad.left_y));
|
|
JS_SetPropertyStr(ctx, state, "rightX", JS_NewFloat64(ctx, gamepad.right_x));
|
|
JS_SetPropertyStr(ctx, state, "rightY", JS_NewFloat64(ctx, gamepad.right_y));
|
|
JS_SetPropertyStr(ctx, state, "l2", JS_NewFloat64(ctx, gamepad.l2));
|
|
JS_SetPropertyStr(ctx, state, "r2", JS_NewFloat64(ctx, gamepad.r2));
|
|
JSValue buttons = JS_NewArray(ctx);
|
|
std::uint32_t count = 0;
|
|
for (std::uint32_t i = 0; i < sizeof kButtonNames / sizeof *kButtonNames; ++i)
|
|
if (gamepad.buttons & (1u << i)) JS_SetPropertyUint32(ctx, buttons, count++, JS_NewString(ctx, kButtonNames[i]));
|
|
JS_SetPropertyStr(ctx, state, "buttons", buttons);
|
|
return state;
|
|
}
|
|
|
|
JSValue request_exit(JSContext* ctx, JSValueConst, int, JSValueConst* argv) {
|
|
exit_requested = true;
|
|
if (!JS_ToBool(ctx, argv[0])) return JS_UNDEFINED;
|
|
char error[256] = "";
|
|
return host::arrange_relaunch(error, sizeof error) ? JS_TRUE : JS_NewString(ctx, error);
|
|
}
|
|
|
|
struct Function {
|
|
const char* name;
|
|
JSCFunction* call;
|
|
int length; // QuickJS pads missing arguments with undefined up to this count.
|
|
};
|
|
|
|
// Every native call goes through `traced`, which reports its first use to host::trace:
|
|
// a native fault on the console then leaves the name of the call that caused it.
|
|
struct Traced {
|
|
const Function* function;
|
|
char name[64];
|
|
bool reported;
|
|
};
|
|
Traced g_traced[64];
|
|
int g_traced_count = 0;
|
|
|
|
JSValue traced(JSContext* ctx, JSValueConst self, int argc, JSValueConst* argv, int index, JSValue*) {
|
|
Traced& entry = g_traced[index];
|
|
if (!entry.reported) {
|
|
entry.reported = true;
|
|
host::trace(entry.name);
|
|
}
|
|
host::CallScope scope(entry.name);
|
|
return entry.function->call(ctx, self, argc, argv);
|
|
}
|
|
|
|
template <std::size_t N>
|
|
JSValue namespace_object(JSContext* ctx, const char* space, const Function (&functions)[N]) {
|
|
JSValue object = JS_NewObject(ctx);
|
|
for (const Function& function : functions) {
|
|
int index = 0;
|
|
while (index < g_traced_count && g_traced[index].function != &function) ++index;
|
|
if (index == g_traced_count) {
|
|
Traced& entry = g_traced[g_traced_count++];
|
|
entry.function = &function;
|
|
std::snprintf(entry.name, sizeof entry.name, "%s%s%s", space, *space ? "." : "", function.name);
|
|
}
|
|
JS_SetPropertyStr(ctx, object, function.name,
|
|
JS_NewCFunctionData(ctx, traced, function.length, index, 0, nullptr));
|
|
}
|
|
return object;
|
|
}
|
|
|
|
constexpr Function kFs[] = {
|
|
{"readDir", fs_read_dir, 1}, {"stat", fs_stat, 1}, {"readFile", fs_read_file, 1},
|
|
{"writeFile", fs_write_file, 4}, {"mkdir", fs_mkdir, 2}, {"remove", fs_remove, 1},
|
|
{"removeTree", fs_remove_tree, 1}, {"removeAsync", fs_remove_async, 1},
|
|
{"rename", fs_rename, 2}, {"chmod", fs_chmod, 2}, {"mounts", fs_mounts, 0}, {"diskUsage", fs_disk_usage, 1},
|
|
};
|
|
constexpr Function kRemovals[] = {{"poll", removals_poll, 0}};
|
|
constexpr Function kDevice[] = {{"info", device_info, 0}, {"utcOffset", device_utc_offset, 1}};
|
|
constexpr Function kUsers[] = {{"foreground", users_foreground, 0}, {"loggedIn", users_logged_in, 0}};
|
|
constexpr Function kPad[] = {
|
|
{"setLightBar", pad_set_light_bar, 3}, {"resetLightBar", pad_reset_light_bar, 0},
|
|
{"vibrate", pad_vibrate, 2}, {"setTriggerEffect", pad_set_trigger_effect, 5},
|
|
{"state", pad_state, 0},
|
|
};
|
|
constexpr Function kPower[] = {{"keepAwake", power_keep_awake, 1}};
|
|
constexpr Function kDisplay[] = {{"refreshRate", display_refresh_rate, 0}, {"setRefreshRate", display_set_refresh_rate, 1}};
|
|
constexpr Function kText[] = {{"setLanguage", text_set_language, 1}};
|
|
constexpr Function kRoot[] = {{"notify", notify, 2}, {"openURL", open_url, 1}, {"exit", request_exit, 1}};
|
|
} // namespace
|
|
|
|
void ps5_react_install_host_api(JSContext* ctx) {
|
|
timespec now = {};
|
|
clock_gettime(CLOCK_REALTIME, &now);
|
|
er_runtime_set_wall_clock(static_cast<std::int64_t>(now.tv_sec) * 1000 + now.tv_nsec / 1000000);
|
|
|
|
JSValue api = namespace_object(ctx, "", kRoot);
|
|
JS_SetPropertyStr(ctx, api, "platform", JS_NewString(ctx, host::platform_name()));
|
|
JS_SetPropertyStr(ctx, api, "fs", namespace_object(ctx, "fs", kFs));
|
|
JS_SetPropertyStr(ctx, api, "removals", namespace_object(ctx, "removals", kRemovals));
|
|
JS_SetPropertyStr(ctx, api, "device", namespace_object(ctx, "device", kDevice));
|
|
JS_SetPropertyStr(ctx, api, "users", namespace_object(ctx, "users", kUsers));
|
|
JS_SetPropertyStr(ctx, api, "pad", namespace_object(ctx, "pad", kPad));
|
|
JS_SetPropertyStr(ctx, api, "power", namespace_object(ctx, "power", kPower));
|
|
JS_SetPropertyStr(ctx, api, "display", namespace_object(ctx, "display", kDisplay));
|
|
JS_SetPropertyStr(ctx, api, "text", namespace_object(ctx, "text", kText));
|
|
JS_SetPropertyStr(ctx, api, "network", ps5_react_network_api(ctx));
|
|
JS_SetPropertyStr(ctx, api, "archives", ps5_react_archive_api(ctx));
|
|
JS_SetPropertyStr(ctx, api, "packages", ps5_react_package_api(ctx));
|
|
JS_SetPropertyStr(ctx, api, "browser", ps5_react_browser_capture_api(ctx));
|
|
JS_SetPropertyStr(ctx, api, "lan", ps5_react_lan_api(ctx));
|
|
JS_SetPropertyStr(ctx, api, "video", ps5_react_video_api(ctx));
|
|
JS_SetPropertyStr(ctx, api, "image", ps5_react_image_api(ctx));
|
|
JS_SetPropertyStr(ctx, api, "sound", ps5_react_sound_api(ctx));
|
|
JS_SetPropertyStr(ctx, api, "audioOutput", ps5_react_audio_output_api(ctx));
|
|
JSValue global = JS_GetGlobalObject(ctx);
|
|
JS_SetPropertyStr(ctx, global, "__ps5ReactNative", api);
|
|
JS_FreeValue(ctx, global);
|
|
}
|
|
|
|
bool ps5_react_exit_requested() {
|
|
return exit_requested;
|
|
}
|
|
|
|
void ps5_react_set_refresh_rate(int hz) {
|
|
refresh_rate = hz;
|
|
}
|
|
|
|
int ps5_react_take_refresh_request() {
|
|
return std::exchange(refresh_request, 0);
|
|
}
|
|
|
|
void ps5_react_set_gamepad(const GamepadState& state) {
|
|
gamepad = state;
|
|
}
|
|
|
|
bool ps5_react_frame(JSContext* ctx, double elapsed_ms) {
|
|
JSValue global = JS_GetGlobalObject(ctx);
|
|
JSValue fn = JS_GetPropertyStr(ctx, global, "__ps5ReactFrame");
|
|
bool ok = true;
|
|
if (JS_IsFunction(ctx, fn)) {
|
|
JSValue arg = JS_NewFloat64(ctx, elapsed_ms);
|
|
JSValue result = JS_Call(ctx, fn, global, 1, &arg);
|
|
ok = !JS_IsException(result);
|
|
if (!ok) JS_FreeValue(ctx, JS_GetException(ctx));
|
|
JS_FreeValue(ctx, result);
|
|
JS_FreeValue(ctx, arg);
|
|
}
|
|
JS_FreeValue(ctx, fn);
|
|
JS_FreeValue(ctx, global);
|
|
return ok;
|
|
}
|