Compare commits

..
Author SHA1 Message Date
ayozetr d5c2611f72 0.51-linux.5 2026-10-14 13:20:52 +01:00
ayozetr 73f3bb512a Merge branch 'fix/issue5-loading-freeze' 2026-10-14 13:13:42 +01:00
ayozetr 5e86d0bde7 Thanks: Supermedo's line names the skipped pipelines too
49a37b4 (draws of a pipeline the driver cannot create are skipped) came from
Supermedo/bloodborne_pc 188c1f6; his line in About → Thanks now says so, in
7 languages.
2026-10-14 13:13:37 +01:00
ayozetr 30b3409674 Merge branch 'main' into fix/issue5-loading-freeze 2026-10-14 13:00:42 +01:00
ayozetr b3c8b1dadd Online: a server that refuses this build is said plainly, and not retried
The public shadNet server now refuses bbport's client version (ClientRejected,
"doesn't accept your shadPS4 build") and signs the game out. We then retried
the sign-in, which the server refuses with LoginError for a while, longer with
each try, and the player only saw the game's "not signed in" message.

- the client remembers the refusal; the handler signs that user out without
  reconnecting
- the game prints "Online: the shadNet server ... does not accept this build"
  with the server's message, and the launcher shows it in a dialog that points
  to the own-server option (7 languages)
2026-10-14 12:48:44 +01:00
ayozetr 697af8936f Merge branch 'main' into fix/issue5-loading-freeze
# Conflicts:
#	launcher/bbport_i18n.py
#	launcher/bbport_launcher.py
2026-10-14 12:21:14 +01:00
ayozetr 576a214b5f Online: the game's play log telemetry is not sent
With online play on, the game uploads FromSoftware's "Playlog" telemetry
(deaths, map changes, weapon switches, ...) to bb-playlog-prod or
bb-playlog-test on Amazon S3, hosts written in the game's own executable.
Their names no longer resolve, as with no network; the log says so once.
2026-10-14 12:16:06 +01:00
ayozetrandpmwork-personal 8d1db30f9e Ultrawide: widen the view for outputs wider than 16:9, and keep custom sizes
- An output_res wider than 16:9 (5120x2160, 3440x1440, ...) filled the screen
  but kept the game's 16:9 projection, so the picture was stretched. patches.py
  now takes the community resolution patch for that size (Kyo's 21:9 and 32:9
  ones in Bloodborne.xml, or one of the same aspect) and applies only its writes
  that widen the game's screen: not the scene and UI sizes or the heap patch,
  which bbport sets itself. bbport.ini aspect_patch=0 turns it off.
- The in-game menu and the launcher listed four sizes: a custom output_res
  became 3840x2160 with the first saved setting, and 1280x720 in the launcher.
  Both keep it now until another size is chosen; tests/test_settings.cpp
  covers the menu side.

From deadinside28/bloodborne_pc#183.

Thanks: pmwork-personal, in 7 languages.

Co-Authored-By: pmwork-personal <[email protected]>
2026-10-14 12:14:54 +01:00
ayozetrandAryxa 16a08695c3 Launcher: expose the host's libcuda.so.1 to the AppImage for DLSS on NVIDIA
With DLSS loaded, NGX makes NVIDIA's driver dlopen libcuda.so.1, but the
driver cache linked only libnvidia-*, libGLX_nvidia and libEGL_nvidia, and the
AppImage's glibc does not search the host's library folders: the load failed.
libcuda.so* joins the patterns; test_packaged_vulkan checks the link.

From deadinside28/bloodborne_pc#137.

Thanks: Aryxa, in 7 languages.

Co-Authored-By: Aryxa <[email protected]>
2026-10-14 12:11:52 +01:00
ayozetrandAyrtonSanabio b4d74c88f7 Menu language: --play passes the launcher's, and only a chosen one is saved
- play() never called set_language(), so --play (Steam Deck game mode) gave
  the in-game menu BB_MENU_LANGUAGE=ru whatever the launcher's language was.
- Save() wrote menu_language every time, so the language the menu merely
  started with went into bbport.ini with the first changed setting and stayed
  there. It is written now only when chosen in the menu or already in the file.
  tests/test_settings.cpp covers it.

From deadinside28/bloodborne_pc#153 and #154.

Thanks: AyrtonSanabio, in 7 languages.

Co-Authored-By: AyrtonSanabio <[email protected]>
2026-10-14 12:11:31 +01:00
ayozetr bb3a9d126d GPU breadcrumbs: off when the marker function is missing, instead of a crash
deadinside28#177: built against other Vulkan headers, the buffer marker
function stayed null although VK_AMD_buffer_marker was on, and the first
marker crashed. Breadcrumbs now check the function too and say they are off.
2026-10-14 12:10:36 +01:00
ayozetrandemurphy04 3d2b8b3067 AvPlayer: a queue cleared while a frame is taken no longer crashes
AvPlayerQueue::Pop checked the size outside the lock, so a Clear() from
another thread (the player stopping, the game closing) between the check and
front() read an empty queue, and GetVideoData/GetAudioData used the empty
result without checking it. The check is under the lock now and both callers
give up when nothing came back.

Thanks: emurphy04, in 7 languages.

Co-Authored-By: emurphy04 <[email protected]>
2026-10-14 12:10:04 +01:00
ayozetrandparsifal295 186a342750 Overlay: Quit game works from a controller, and the menu stays on screen
- The Quit button's label was its ImGui ID: at the first press the label
  changed, the new button did not have the pad's or keyboard's focus and the
  button disarmed at once, so quitting worked only with the mouse over it.
  "###bb_quit" keeps one ID for both labels.
- The menu sized itself to its contents: on a short screen its last row
  (Close, Quit game) was off screen. Its height is now limited to the screen
  below where it opens, and the contents scroll.

Thanks: parsifal295, in 7 languages.

Co-Authored-By: parsifal295 <[email protected]>
2026-10-14 12:09:42 +01:00
ayozetrandStefano Scalfari 2bcc093bd9 shadNet: report the client version after signing in
A shadNet server with clientversions.cfg rules signs out a client that never
sends SetClientVersion, then refuses its logins with LoginError for a while.
We sent nothing, so on the public server the game went offline a few seconds
after signing in and the next logins failed.

- send SetClientVersion (command 13) right after the login reply; the string
  is "shadPS4 bbport", BB_CLIENT_VERSION overrides it
- log the ClientRejected notification (19) with the server's message
- keep the SetClientVersion reply away from OnScoreReply

Thanks: Vorshim92, in 7 languages.

Co-Authored-By: Stefano Scalfari <[email protected]>
2026-10-14 12:08:54 +01:00
ayozetr 8937a721ca GPU: say so when a submission is not finished after 10 s
Semaphore::Wait waited without a limit, so a GPU stuck on a draw (the
presenter waiting for frames ahead, as in deadinside28#184) froze the game with
nothing in the log. It now waits in 10 s slices, prints once that the
submission is late with the GPU breadcrumbs, and says when it finishes after all.
2026-10-14 12:08:16 +01:00
ayozetr 4f3187843f Launcher: a failed update check says why instead of staying on "Checking"
When the check failed (offline, GitHub's rate limit) the result is the
error text, and the update row read its "tag" before seeing that, so it
raised and the row stayed on "Checking…". It now shows the error.
2026-10-13 22:22:55 +01:00
artemkaVG da5561d6c7 Texture cache: do not take a color image at the stencil address as the stencil
FindDepthTarget linked whatever image sat at the stencil address as the
depth target's stencil. When a color target reuses that memory, reads and
storage writes to it went to the depth buffer through an invalid view.
Only images with a stencil-compatible format (or none yet) are linked now.

From shadps4-emu/shadPS4#5118. Its author saw the NVIDIA GPU hang (Xid 109)
go away in Uncharted 2 with it; maybe related to #5. Thanks entry added.
2026-10-13 22:22:55 +01:00
ayozetr f9186dbb8d Presenter: say so when a frame or a swapchain image does not come for 10 s
The frame fences and the swapchain acquire were waited for without a
limit, so a GPU that never finished a frame, or a compositor that kept
every image, froze the game with nothing in the log (#5: frozen loading
screens, GPU at 0 %, NVIDIA Xid 109). They are now waited for in 10 s
slices: past the first one the log says which wait it is, once, with the
GPU breadcrumbs for a frame, and again when it does finish.
2026-10-13 22:22:55 +01:00
Mrsuss60 1b93552dca Pad: the touchpad from the keyboard as a real touch, for the gestures and personal effects menus
Tab and Backspace (and an injected touchpad press) sent a touch with id 0
and no hold time. Each press now gets its own id (1..127) and the time held
since it went down, as the pad reports a touch.

From Mrsuss60/bloodborne_pc_windows_port@fb0d4db (#56). Thanks entry updated.
2026-10-13 22:17:40 +01:00
ayozetr d7a94e5dfc README: badges for downloads, latest release, license, platform and Ko-fi 2026-10-13 16:52:03 +01:00
ayozetr e2b3f73e71 Launcher: icons in About and Settings, whole tab names, uncut text
- About: GitHub's mark beside the three project links and a certificate
  beside the license, which now reads "GNU GPL v2 or later · No game files
  included."; that Bloodborne belongs to Sony Interactive Entertainment and
  FromSoftware is plain text under everything.
- Settings: Steam's and Discord's marks beside Add to Steam and the Discord
  status.
- The icons are the desktop theme's (github-, steam-, discord-symbolic); a
  theme without them gets the launcher's own (launcher/icons, from Simple
  Icons, CC0), not the installed app's colour icon.
- The tabs show their names whole (icon above the name), where they were cut
  off ("Ajus…", "Trof…") at the default width.
- Text is drawn with GTK's cairo renderer: its GPU renderers cut the top row
  of pixels off the letters at fractional display scales. GSK_RENDERER set by
  the user still wins; the game draws with its own Vulkan.
- The launcher language's title says "Launcher language" once in English.
2026-10-13 12:58:05 +01:00
ayozetr 1ce06532c2 Thanks: GerasimosKan's trophy chime too
His line in About → Thanks names the chime as well as the English trophy names.
2026-10-13 11:26:47 +01:00
ayozetr 49a37b4459 GPU: skip the draws of a pipeline the driver cannot create
"Failed to create graphics pipeline: ErrorUnknown" (some AMD and older
drivers) stopped the game with code 23. The pipeline now stays empty and is
logged; its draws and dispatches are skipped, on the GPU thread and when a
background compile finishes, and it is not saved to the cache, so the next
start tries it again. A cached one the driver rejects already rebuilt the
cache.

After Supermedo/bloodborne_pc 188c1f6, adapted to the background compiles.
2026-10-13 11:26:21 +01:00
ayozetr 25cdef7482 Trophies: a short chime when one is earned
When a trophy's notice shows, a bell of our own plays: two notes, a third for
the platinum, synthesized on its own SDL stream beside the game's (no audio
from the console). "Sound when a trophy is earned" in the menu's Other section
turns it off (trophy_sound in bbport.ini); BB_AUDIO=none keeps it silent like
the game.

The chime is GerasimosKan's, from deadinside28/bloodborne_pc#127.
2026-10-13 11:26:21 +01:00
ayozetr 9413c82bfb Trophies: English names without the trophy key
Without the PS4's trophy key the lists in the dump stay encrypted, and the
notices and the Trophies page had only icons and numbers. For Bloodborne the
names, descriptions, grades and hidden flags now come from PSN's English list
(scripts/trophies.py BLOODBORNE_ENGLISH), with the platinum counting 1-33; with
the key they are still the game's own, in its language. Checked against the
dump's encrypted list.

The list is GerasimosKan's, from deadinside28/bloodborne_pc#127; About →
Thanks names him.
2026-10-13 11:26:16 +01:00
ayozetr f4a12043f6 Launcher: the shader cache's size, and a button to clear it
Settings → Performance shows how much the port's shader cache takes (the
shaders the GPU library translated, <user>/cache) and clears it after asking,
while the game is not running: the next start builds the shaders again, with
some stutter at first in each area. For a picture that looks wrong after an
update. Saves, settings and the drivers' own caches stay.
2026-10-13 11:15:23 +01:00
ayozetr 7e02e06139 FSR 4: the balanced model for Performance and Ultra Performance
The v07 performance and ultra performance models leave trails behind moving
characters in Bloodborne at 2x and 3x upscaling; the balanced model handles
those ratios cleanly. Both presets keep their render size and use it. Tested
at 1280x720 to 2560x1440 and the Ultra Performance ratio.

From IFreemz/shadPS4-Bloodborne-DLSS-FSR (9b2766e), whose line in About →
Thanks now says so.
2026-10-13 11:08:33 +01:00
37 changed files with 4961 additions and 4169 deletions

No files matched your search

+6
View File
@@ -1,5 +1,11 @@
# Bloodborne for Linux
[![Total Download Count](https://img.shields.io/github/downloads/ayozetr/bloodborne_linux/total.svg?color=2ea44f)](https://github.com/ayozetr/bloodborne_linux/releases/latest)
[![Latest Release](https://img.shields.io/github/v/release/ayozetr/bloodborne_linux.svg?color=8a2be2)](https://github.com/ayozetr/bloodborne_linux/releases/latest)
[![License: GPL-2.0](https://img.shields.io/github/license/ayozetr/bloodborne_linux.svg?color=d73a49)](LICENSE)
[![Platform: Linux | Steam Deck](https://img.shields.io/badge/platform-Linux%20%7C%20Steam%20Deck-blue.svg)](#features)
[![Ko-fi](https://img.shields.io/badge/Ko--fi-support-FF5E5B.svg?logo=ko-fi&logoColor=white)](https://ko-fi.com/ayozetr)
The PlayStation 4 version of *Bloodborne* running natively on Linux. The game's own x86-64 code
runs directly on the CPU and its graphics are translated to Vulkan. It is not an emulator.
@@ -1,416 +1,427 @@
// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#ifdef _WIN32
#define _WINSOCK_DEPRECATED_NO_WARNINGS
#include <Ws2tcpip.h>
#include <iphlpapi.h>
#include <winsock2.h>
typedef SOCKET net_socket;
typedef int socklen_t;
#else
#include <cerrno>
#include <arpa/inet.h>
#include <ifaddrs.h>
#include <net/if.h>
#include <netdb.h>
#include <netinet/in.h>
#include <netinet/tcp.h>
#include <sys/ioctl.h>
#include <sys/socket.h>
#include <unistd.h>
typedef int net_socket;
#endif
#if defined(__APPLE__)
#include <net/if_dl.h>
#include <net/route.h>
#endif
#if defined(__linux__) || defined(__FreeBSD__)
#include <fstream>
#include <iostream>
#include <sstream>
#endif
#include <map>
#include <memory>
#include <mutex>
#include <vector>
#include <string.h>
#include "common/assert.h"
#include "common/logging/log.h"
#include "core/libraries/error_codes.h"
#include "net.h"
#include "net_error.h"
#include "net_util.h"
namespace NetUtil {
const std::array<u8, 6>& NetUtilInternal::GetEthernetAddr() const {
return ether_address;
}
bool NetUtilInternal::RetrieveEthernetAddr() {
std::scoped_lock lock{m_mutex};
#ifdef _WIN32
std::vector<u8> adapter_infos(sizeof(IP_ADAPTER_INFO));
ULONG size_infos = sizeof(IP_ADAPTER_INFO);
if (GetAdaptersInfo(reinterpret_cast<PIP_ADAPTER_INFO>(adapter_infos.data()), &size_infos) ==
ERROR_BUFFER_OVERFLOW)
adapter_infos.resize(size_infos);
if (GetAdaptersInfo(reinterpret_cast<PIP_ADAPTER_INFO>(adapter_infos.data()), &size_infos) ==
NO_ERROR &&
size_infos) {
PIP_ADAPTER_INFO info = reinterpret_cast<PIP_ADAPTER_INFO>(adapter_infos.data());
memcpy(ether_address.data(), info[0].Address, 6);
return true;
}
#elif defined(__APPLE__)
ifaddrs* ifap;
if (getifaddrs(&ifap) == 0) {
ifaddrs* p;
for (p = ifap; p; p = p->ifa_next) {
if (p->ifa_addr->sa_family == AF_LINK) {
sockaddr_dl* sdp = reinterpret_cast<sockaddr_dl*>(p->ifa_addr);
memcpy(ether_address.data(), sdp->sdl_data + sdp->sdl_nlen, 6);
freeifaddrs(ifap);
return true;
}
}
freeifaddrs(ifap);
}
#elif defined(__FreeBSD__)
// todo
#else
ifreq ifr;
ifconf ifc;
char buf[1024];
int success = 0;
int sock = socket(AF_INET, SOCK_DGRAM, IPPROTO_IP);
if (sock == -1)
return false;
ifc.ifc_len = sizeof(buf);
ifc.ifc_buf = buf;
if (ioctl(sock, SIOCGIFCONF, &ifc) == -1)
return false;
ifreq* it = ifc.ifc_req;
const ifreq* const end = it + (ifc.ifc_len / sizeof(ifreq));
for (; it != end; ++it) {
strcpy(ifr.ifr_name, it->ifr_name);
if (ioctl(sock, SIOCGIFFLAGS, &ifr) == 0) {
if (!(ifr.ifr_flags & IFF_LOOPBACK)) {
if (ioctl(sock, SIOCGIFHWADDR, &ifr) == 0) {
success = 1;
break;
}
}
}
}
close(sock);
if (success) {
memcpy(ether_address.data(), ifr.ifr_hwaddr.sa_data, 6);
return true;
}
#endif
return false;
}
const std::string& NetUtilInternal::GetDefaultGateway() const {
return default_gateway;
}
bool NetUtilInternal::RetrieveDefaultGateway() {
std::scoped_lock lock{m_mutex};
#ifdef _WIN32
ULONG flags = GAA_FLAG_INCLUDE_GATEWAYS;
ULONG family = AF_INET; // Only IPv4
ULONG buffer_size = 15000;
std::vector<BYTE> buffer(buffer_size);
PIP_ADAPTER_ADDRESSES adapter_addresses =
reinterpret_cast<PIP_ADAPTER_ADDRESSES>(buffer.data());
DWORD result = GetAdaptersAddresses(family, flags, nullptr, adapter_addresses, &buffer_size);
if (result != NO_ERROR) {
return false;
}
for (PIP_ADAPTER_ADDRESSES adapter = adapter_addresses; adapter != nullptr;
adapter = adapter->Next) {
if (adapter->OperStatus != IfOperStatusUp)
continue;
IP_ADAPTER_GATEWAY_ADDRESS_LH* gateway = adapter->FirstGatewayAddress;
while (gateway) {
sockaddr* sa = gateway->Address.lpSockaddr;
if (sa->sa_family == AF_INET) {
char str[INET_ADDRSTRLEN];
sockaddr_in* sa_in = reinterpret_cast<sockaddr_in*>(sa);
if (inet_ntop(AF_INET, &sa_in->sin_addr, str, sizeof(str))) {
this->default_gateway = str;
return true;
}
}
gateway = gateway->Next;
}
}
return false;
#elif defined(__APPLE__)
// adapted from
// https://github.com/seladb/PcapPlusPlus/blob/a49a79e0b67b402ad75ffa96c1795def36df75c8/Pcap%2B%2B/src/PcapLiveDevice.cpp#L1236
// route message struct for communication in APPLE device
struct BSDRoutingMessage {
struct rt_msghdr header;
char messageSpace[512];
};
struct BSDRoutingMessage routingMessage;
// It creates a raw socket that can be used for routing-related operations
int sockfd = socket(PF_ROUTE, SOCK_RAW, 0);
if (sockfd < 0) {
return false;
}
memset(reinterpret_cast<char*>(&routingMessage), 0, sizeof(routingMessage));
routingMessage.header.rtm_msglen = sizeof(struct rt_msghdr);
routingMessage.header.rtm_version = RTM_VERSION;
routingMessage.header.rtm_type = RTM_GET;
routingMessage.header.rtm_addrs = RTA_DST | RTA_NETMASK;
routingMessage.header.rtm_flags = RTF_UP | RTF_GATEWAY | RTF_STATIC;
routingMessage.header.rtm_msglen += 2 * sizeof(sockaddr_in);
if (write(sockfd, reinterpret_cast<char*>(&routingMessage), routingMessage.header.rtm_msglen) <
0) {
return false;
}
// Read the response from the route socket
if (read(sockfd, reinterpret_cast<char*>(&routingMessage), sizeof(routingMessage)) < 0) {
return false;
}
struct in_addr* gateAddr = nullptr;
struct sockaddr* sa = nullptr;
char* spacePtr = (reinterpret_cast<char*>(&routingMessage.header + 1));
auto rtmAddrs = routingMessage.header.rtm_addrs;
int index = 1;
auto roundUpClosestMultiple = [](int multiple, int num) {
return ((num + multiple - 1) / multiple) * multiple;
};
while (rtmAddrs) {
if (rtmAddrs & 1) {
sa = reinterpret_cast<sockaddr*>(spacePtr);
if (index == RTA_GATEWAY) {
gateAddr = &((sockaddr_in*)sa)->sin_addr;
break;
}
spacePtr += sa->sa_len > 0 ? roundUpClosestMultiple(sizeof(uint32_t), sa->sa_len)
: sizeof(uint32_t);
}
index++;
rtmAddrs >>= 1;
}
if (gateAddr == nullptr) {
return false;
}
char str[INET_ADDRSTRLEN];
inet_ntop(AF_INET, gateAddr, str, sizeof(str));
this->default_gateway = str;
return true;
#elif defined(__FreeBSD__)
return true;
#else
std::ifstream route{"/proc/net/route"};
std::string line;
std::getline(route, line);
while (std::getline(route, line)) {
std::istringstream iss{line};
std::string iface, destination, gateway;
int flags;
iss >> iface >> destination >> gateway >> std::hex >> flags;
if (destination == "00000000") {
u64 default_gateway{};
std::stringstream ss;
ss << std::hex << gateway;
ss >> default_gateway;
char str[INET_ADDRSTRLEN];
in_addr addr;
addr.s_addr = default_gateway;
inet_ntop(AF_INET, &addr, str, sizeof(str));
this->default_gateway = str;
return true;
}
}
#endif
return false;
}
const std::string& NetUtilInternal::GetNetmask() const {
return netmask;
}
bool NetUtilInternal::RetrieveNetmask() {
std::scoped_lock lock{m_mutex};
char netmaskStr[INET_ADDRSTRLEN];
auto success = false;
#ifdef _WIN32
ULONG flags = GAA_FLAG_SKIP_ANYCAST | GAA_FLAG_SKIP_MULTICAST | GAA_FLAG_SKIP_DNS_SERVER;
ULONG family = AF_INET; // Only IPv4
ULONG buffer_size = 15000;
std::vector<BYTE> buffer(buffer_size);
auto adapter_addresses = reinterpret_cast<PIP_ADAPTER_ADDRESSES>(buffer.data());
DWORD result = GetAdaptersAddresses(family, flags, nullptr, adapter_addresses, &buffer_size);
if (result == ERROR_BUFFER_OVERFLOW) {
buffer.resize(buffer_size);
adapter_addresses = reinterpret_cast<PIP_ADAPTER_ADDRESSES>(buffer.data());
result = GetAdaptersAddresses(family, flags, nullptr, adapter_addresses, &buffer_size);
}
if (result != NO_ERROR)
return false;
for (auto adapter = adapter_addresses; adapter != nullptr; adapter = adapter->Next) {
// Skip loopback and down interfaces
if (adapter->IfType == IF_TYPE_SOFTWARE_LOOPBACK || adapter->OperStatus != IfOperStatusUp)
continue;
for (auto unicast = adapter->FirstUnicastAddress; unicast != nullptr;
unicast = unicast->Next) {
if (unicast->Address.lpSockaddr->sa_family == AF_INET) {
ULONG prefix_length = unicast->OnLinkPrefixLength;
ULONG mask = prefix_length == 0 ? 0 : 0xFFFFFFFF << (32 - prefix_length);
in_addr mask_addr{};
mask_addr.S_un.S_addr = htonl(mask);
if (inet_ntop(AF_INET, &mask_addr, netmaskStr, INET_ADDRSTRLEN)) {
success = true;
}
}
}
}
#else
ifaddrs* ifap;
if (getifaddrs(&ifap) == 0) {
ifaddrs* p;
for (p = ifap; p; p = p->ifa_next) {
if (p->ifa_addr && p->ifa_addr->sa_family == AF_INET) {
auto sa = reinterpret_cast<sockaddr_in*>(p->ifa_netmask);
inet_ntop(AF_INET, &sa->sin_addr, netmaskStr, INET_ADDRSTRLEN);
success = true;
}
}
}
freeifaddrs(ifap);
#endif
if (success) {
netmask = netmaskStr;
}
return success;
}
const std::string& NetUtilInternal::GetIp() const {
return ip;
}
u32 NetUtilInternal::GetExternalIp() const {
return external_ip;
}
void NetUtilInternal::SetExternalIp(u32 addr) {
external_ip = addr;
}
u32 NetUtilInternal::GetNatType() const {
return nat_type;
}
bool NetUtilInternal::RetrieveIp() {
std::scoped_lock lock{m_mutex};
auto sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd == -1) {
return false;
}
sockaddr_in sa{};
socklen_t sa_len{sizeof(sa)};
sa.sin_family = AF_INET;
sa.sin_addr.s_addr = inet_addr("1.1.1.1");
sa.sin_port = htons(80);
#ifdef _WIN32
#define close closesocket
#endif
if (connect(sockfd, (sockaddr*)&sa, sa_len) == -1) {
close(sockfd);
return false;
}
if (getsockname(sockfd, (struct sockaddr*)&sa, &sa_len) == -1) {
close(sockfd);
return false;
}
char netmaskStr[INET_ADDRSTRLEN];
inet_ntop(AF_INET, &sa.sin_addr, netmaskStr, INET_ADDRSTRLEN);
ip = netmaskStr;
close(sockfd);
#ifdef _WIN32
#undef close
#endif
return true;
}
int NetUtilInternal::ResolveHostname(const char* hostname, Libraries::Net::OrbisNetInAddr* addr) {
const addrinfo hints = {
.ai_flags = AI_V4MAPPED | AI_ADDRCONFIG,
.ai_family = AF_INET,
};
addrinfo* info = nullptr;
auto gai_result = getaddrinfo(hostname, nullptr, &hints, &info);
auto ret = ORBIS_OK;
if (gai_result != 0) {
// handle more errors
LOG_ERROR(Lib_Net, "address resolution for {} failed: {}", hostname, gai_result);
ret = ORBIS_NET_ERETURN;
} else {
ASSERT(info && info->ai_addr);
in_addr resolved_addr = ((sockaddr_in*)info->ai_addr)->sin_addr;
LOG_DEBUG(Lib_Net, "resolved address for {}: {}", hostname, inet_ntoa(resolved_addr));
addr->inaddr_addr = resolved_addr.s_addr;
}
freeaddrinfo(info);
return ret;
}
} // namespace NetUtil
// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#ifdef _WIN32
#define _WINSOCK_DEPRECATED_NO_WARNINGS
#include <Ws2tcpip.h>
#include <iphlpapi.h>
#include <winsock2.h>
typedef SOCKET net_socket;
typedef int socklen_t;
#else
#include <cerrno>
#include <arpa/inet.h>
#include <ifaddrs.h>
#include <net/if.h>
#include <netdb.h>
#include <netinet/in.h>
#include <netinet/tcp.h>
#include <sys/ioctl.h>
#include <sys/socket.h>
#include <unistd.h>
typedef int net_socket;
#endif
#if defined(__APPLE__)
#include <net/if_dl.h>
#include <net/route.h>
#endif
#if defined(__linux__) || defined(__FreeBSD__)
#include <fstream>
#include <iostream>
#include <sstream>
#endif
#include <atomic>
#include <map>
#include <memory>
#include <mutex>
#include <string_view>
#include <vector>
#include <string.h>
#include "common/assert.h"
#include "common/logging/log.h"
#include "core/libraries/error_codes.h"
#include "net.h"
#include "net_error.h"
#include "net_util.h"
namespace NetUtil {
const std::array<u8, 6>& NetUtilInternal::GetEthernetAddr() const {
return ether_address;
}
bool NetUtilInternal::RetrieveEthernetAddr() {
std::scoped_lock lock{m_mutex};
#ifdef _WIN32
std::vector<u8> adapter_infos(sizeof(IP_ADAPTER_INFO));
ULONG size_infos = sizeof(IP_ADAPTER_INFO);
if (GetAdaptersInfo(reinterpret_cast<PIP_ADAPTER_INFO>(adapter_infos.data()), &size_infos) ==
ERROR_BUFFER_OVERFLOW)
adapter_infos.resize(size_infos);
if (GetAdaptersInfo(reinterpret_cast<PIP_ADAPTER_INFO>(adapter_infos.data()), &size_infos) ==
NO_ERROR &&
size_infos) {
PIP_ADAPTER_INFO info = reinterpret_cast<PIP_ADAPTER_INFO>(adapter_infos.data());
memcpy(ether_address.data(), info[0].Address, 6);
return true;
}
#elif defined(__APPLE__)
ifaddrs* ifap;
if (getifaddrs(&ifap) == 0) {
ifaddrs* p;
for (p = ifap; p; p = p->ifa_next) {
if (p->ifa_addr->sa_family == AF_LINK) {
sockaddr_dl* sdp = reinterpret_cast<sockaddr_dl*>(p->ifa_addr);
memcpy(ether_address.data(), sdp->sdl_data + sdp->sdl_nlen, 6);
freeifaddrs(ifap);
return true;
}
}
freeifaddrs(ifap);
}
#elif defined(__FreeBSD__)
// todo
#else
ifreq ifr;
ifconf ifc;
char buf[1024];
int success = 0;
int sock = socket(AF_INET, SOCK_DGRAM, IPPROTO_IP);
if (sock == -1)
return false;
ifc.ifc_len = sizeof(buf);
ifc.ifc_buf = buf;
if (ioctl(sock, SIOCGIFCONF, &ifc) == -1)
return false;
ifreq* it = ifc.ifc_req;
const ifreq* const end = it + (ifc.ifc_len / sizeof(ifreq));
for (; it != end; ++it) {
strcpy(ifr.ifr_name, it->ifr_name);
if (ioctl(sock, SIOCGIFFLAGS, &ifr) == 0) {
if (!(ifr.ifr_flags & IFF_LOOPBACK)) {
if (ioctl(sock, SIOCGIFHWADDR, &ifr) == 0) {
success = 1;
break;
}
}
}
}
close(sock);
if (success) {
memcpy(ether_address.data(), ifr.ifr_hwaddr.sa_data, 6);
return true;
}
#endif
return false;
}
const std::string& NetUtilInternal::GetDefaultGateway() const {
return default_gateway;
}
bool NetUtilInternal::RetrieveDefaultGateway() {
std::scoped_lock lock{m_mutex};
#ifdef _WIN32
ULONG flags = GAA_FLAG_INCLUDE_GATEWAYS;
ULONG family = AF_INET; // Only IPv4
ULONG buffer_size = 15000;
std::vector<BYTE> buffer(buffer_size);
PIP_ADAPTER_ADDRESSES adapter_addresses =
reinterpret_cast<PIP_ADAPTER_ADDRESSES>(buffer.data());
DWORD result = GetAdaptersAddresses(family, flags, nullptr, adapter_addresses, &buffer_size);
if (result != NO_ERROR) {
return false;
}
for (PIP_ADAPTER_ADDRESSES adapter = adapter_addresses; adapter != nullptr;
adapter = adapter->Next) {
if (adapter->OperStatus != IfOperStatusUp)
continue;
IP_ADAPTER_GATEWAY_ADDRESS_LH* gateway = adapter->FirstGatewayAddress;
while (gateway) {
sockaddr* sa = gateway->Address.lpSockaddr;
if (sa->sa_family == AF_INET) {
char str[INET_ADDRSTRLEN];
sockaddr_in* sa_in = reinterpret_cast<sockaddr_in*>(sa);
if (inet_ntop(AF_INET, &sa_in->sin_addr, str, sizeof(str))) {
this->default_gateway = str;
return true;
}
}
gateway = gateway->Next;
}
}
return false;
#elif defined(__APPLE__)
// adapted from
// https://github.com/seladb/PcapPlusPlus/blob/a49a79e0b67b402ad75ffa96c1795def36df75c8/Pcap%2B%2B/src/PcapLiveDevice.cpp#L1236
// route message struct for communication in APPLE device
struct BSDRoutingMessage {
struct rt_msghdr header;
char messageSpace[512];
};
struct BSDRoutingMessage routingMessage;
// It creates a raw socket that can be used for routing-related operations
int sockfd = socket(PF_ROUTE, SOCK_RAW, 0);
if (sockfd < 0) {
return false;
}
memset(reinterpret_cast<char*>(&routingMessage), 0, sizeof(routingMessage));
routingMessage.header.rtm_msglen = sizeof(struct rt_msghdr);
routingMessage.header.rtm_version = RTM_VERSION;
routingMessage.header.rtm_type = RTM_GET;
routingMessage.header.rtm_addrs = RTA_DST | RTA_NETMASK;
routingMessage.header.rtm_flags = RTF_UP | RTF_GATEWAY | RTF_STATIC;
routingMessage.header.rtm_msglen += 2 * sizeof(sockaddr_in);
if (write(sockfd, reinterpret_cast<char*>(&routingMessage), routingMessage.header.rtm_msglen) <
0) {
return false;
}
// Read the response from the route socket
if (read(sockfd, reinterpret_cast<char*>(&routingMessage), sizeof(routingMessage)) < 0) {
return false;
}
struct in_addr* gateAddr = nullptr;
struct sockaddr* sa = nullptr;
char* spacePtr = (reinterpret_cast<char*>(&routingMessage.header + 1));
auto rtmAddrs = routingMessage.header.rtm_addrs;
int index = 1;
auto roundUpClosestMultiple = [](int multiple, int num) {
return ((num + multiple - 1) / multiple) * multiple;
};
while (rtmAddrs) {
if (rtmAddrs & 1) {
sa = reinterpret_cast<sockaddr*>(spacePtr);
if (index == RTA_GATEWAY) {
gateAddr = &((sockaddr_in*)sa)->sin_addr;
break;
}
spacePtr += sa->sa_len > 0 ? roundUpClosestMultiple(sizeof(uint32_t), sa->sa_len)
: sizeof(uint32_t);
}
index++;
rtmAddrs >>= 1;
}
if (gateAddr == nullptr) {
return false;
}
char str[INET_ADDRSTRLEN];
inet_ntop(AF_INET, gateAddr, str, sizeof(str));
this->default_gateway = str;
return true;
#elif defined(__FreeBSD__)
return true;
#else
std::ifstream route{"/proc/net/route"};
std::string line;
std::getline(route, line);
while (std::getline(route, line)) {
std::istringstream iss{line};
std::string iface, destination, gateway;
int flags;
iss >> iface >> destination >> gateway >> std::hex >> flags;
if (destination == "00000000") {
u64 default_gateway{};
std::stringstream ss;
ss << std::hex << gateway;
ss >> default_gateway;
char str[INET_ADDRSTRLEN];
in_addr addr;
addr.s_addr = default_gateway;
inet_ntop(AF_INET, &addr, str, sizeof(str));
this->default_gateway = str;
return true;
}
}
#endif
return false;
}
const std::string& NetUtilInternal::GetNetmask() const {
return netmask;
}
bool NetUtilInternal::RetrieveNetmask() {
std::scoped_lock lock{m_mutex};
char netmaskStr[INET_ADDRSTRLEN];
auto success = false;
#ifdef _WIN32
ULONG flags = GAA_FLAG_SKIP_ANYCAST | GAA_FLAG_SKIP_MULTICAST | GAA_FLAG_SKIP_DNS_SERVER;
ULONG family = AF_INET; // Only IPv4
ULONG buffer_size = 15000;
std::vector<BYTE> buffer(buffer_size);
auto adapter_addresses = reinterpret_cast<PIP_ADAPTER_ADDRESSES>(buffer.data());
DWORD result = GetAdaptersAddresses(family, flags, nullptr, adapter_addresses, &buffer_size);
if (result == ERROR_BUFFER_OVERFLOW) {
buffer.resize(buffer_size);
adapter_addresses = reinterpret_cast<PIP_ADAPTER_ADDRESSES>(buffer.data());
result = GetAdaptersAddresses(family, flags, nullptr, adapter_addresses, &buffer_size);
}
if (result != NO_ERROR)
return false;
for (auto adapter = adapter_addresses; adapter != nullptr; adapter = adapter->Next) {
// Skip loopback and down interfaces
if (adapter->IfType == IF_TYPE_SOFTWARE_LOOPBACK || adapter->OperStatus != IfOperStatusUp)
continue;
for (auto unicast = adapter->FirstUnicastAddress; unicast != nullptr;
unicast = unicast->Next) {
if (unicast->Address.lpSockaddr->sa_family == AF_INET) {
ULONG prefix_length = unicast->OnLinkPrefixLength;
ULONG mask = prefix_length == 0 ? 0 : 0xFFFFFFFF << (32 - prefix_length);
in_addr mask_addr{};
mask_addr.S_un.S_addr = htonl(mask);
if (inet_ntop(AF_INET, &mask_addr, netmaskStr, INET_ADDRSTRLEN)) {
success = true;
}
}
}
}
#else
ifaddrs* ifap;
if (getifaddrs(&ifap) == 0) {
ifaddrs* p;
for (p = ifap; p; p = p->ifa_next) {
if (p->ifa_addr && p->ifa_addr->sa_family == AF_INET) {
auto sa = reinterpret_cast<sockaddr_in*>(p->ifa_netmask);
inet_ntop(AF_INET, &sa->sin_addr, netmaskStr, INET_ADDRSTRLEN);
success = true;
}
}
}
freeifaddrs(ifap);
#endif
if (success) {
netmask = netmaskStr;
}
return success;
}
const std::string& NetUtilInternal::GetIp() const {
return ip;
}
u32 NetUtilInternal::GetExternalIp() const {
return external_ip;
}
void NetUtilInternal::SetExternalIp(u32 addr) {
external_ip = addr;
}
u32 NetUtilInternal::GetNatType() const {
return nat_type;
}
bool NetUtilInternal::RetrieveIp() {
std::scoped_lock lock{m_mutex};
auto sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd == -1) {
return false;
}
sockaddr_in sa{};
socklen_t sa_len{sizeof(sa)};
sa.sin_family = AF_INET;
sa.sin_addr.s_addr = inet_addr("1.1.1.1");
sa.sin_port = htons(80);
#ifdef _WIN32
#define close closesocket
#endif
if (connect(sockfd, (sockaddr*)&sa, sa_len) == -1) {
close(sockfd);
return false;
}
if (getsockname(sockfd, (struct sockaddr*)&sa, &sa_len) == -1) {
close(sockfd);
return false;
}
char netmaskStr[INET_ADDRSTRLEN];
inet_ntop(AF_INET, &sa.sin_addr, netmaskStr, INET_ADDRSTRLEN);
ip = netmaskStr;
close(sockfd);
#ifdef _WIN32
#undef close
#endif
return true;
}
int NetUtilInternal::ResolveHostname(const char* hostname, Libraries::Net::OrbisNetInAddr* addr) {
// bbport: the game's "Playlog" telemetry (FromSoftware's bb-playlog-prod / bb-playlog-test
// buckets on Amazon S3) is not sent anywhere: its host does not resolve, as with no network.
if (std::string_view(hostname).find("bb-playlog") != std::string_view::npos) {
static std::atomic_flag told = ATOMIC_FLAG_INIT;
if (!told.test_and_set()) {
LOG_INFO(Lib_Net, "{}: the game's play log upload is blocked", hostname);
}
return ORBIS_NET_ERETURN;
}
const addrinfo hints = {
.ai_flags = AI_V4MAPPED | AI_ADDRCONFIG,
.ai_family = AF_INET,
};
addrinfo* info = nullptr;
auto gai_result = getaddrinfo(hostname, nullptr, &hints, &info);
auto ret = ORBIS_OK;
if (gai_result != 0) {
// handle more errors
LOG_ERROR(Lib_Net, "address resolution for {} failed: {}", hostname, gai_result);
ret = ORBIS_NET_ERETURN;
} else {
ASSERT(info && info->ai_addr);
in_addr resolved_addr = ((sockaddr_in*)info->ai_addr)->sin_addr;
LOG_DEBUG(Lib_Net, "resolved address for {}: {}", hostname, inet_ntoa(resolved_addr));
addr->inaddr_addr = resolved_addr.s_addr;
}
freeaddrinfo(info);
return ret;
}
} // namespace NetUtil
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
+395 -385
View File
@@ -1,385 +1,395 @@
// SPDX-FileCopyrightText: Copyright 2019-2026 rpcs3 Project
// SPDX-FileCopyrightText: Copyright 2024-2026 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <atomic>
#include <condition_variable>
#include <functional>
#include <mutex>
#include <optional>
#include <semaphore>
#include <string>
#include <thread>
#include <utility>
#include <vector>
#include "common/types.h"
#ifdef _WIN32
#include <winsock2.h>
#include <ws2tcpip.h>
using ShadSocketHandle = SOCKET;
static constexpr ShadSocketHandle SHAD_INVALID_SOCK = INVALID_SOCKET;
#define SHAD_CLOSE(s) ::closesocket(s)
#else
#include <arpa/inet.h>
#include <errno.h>
#include <fcntl.h>
#include <netdb.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <unistd.h>
using ShadSocketHandle = int;
static constexpr ShadSocketHandle SHAD_INVALID_SOCK = -1;
#define SHAD_CLOSE(s) ::close(s)
#endif
namespace ShadNet {
// Protocol constants
static constexpr u32 SHAD_HEADER_SIZE = 15;
static constexpr u32 SHAD_CONNECT_TIMEOUT_MS = 10000; // 10 second connect/handshake timeout
// MAX_ATTEMPTS * CONNECT_TIMEOUT + sum(backoffs).
static constexpr u32 SHAD_CONNECT_MAX_ATTEMPTS = 4;
static constexpr u32 SHAD_CONNECT_RETRY_BACKOFF_MS =
1000; // base backoff, doubled per retry (cap 8s)
static constexpr u32 SHAD_PROTOCOL_VERSION = 1;
static constexpr u32 SHAD_MAX_PACKET_SIZE = 0x800000; // 8 MiB
// Protocol enumerations (must match shadnet server protocol.h)
enum class PacketType : u8 {
Request = 0,
Reply = 1,
Notification = 2,
ServerInfo = 3,
};
enum class CommandType : u16 {
Login = 0,
Terminate = 1,
Create = 2,
Delete = 3,
SendToken = 4,
SendResetToken = 5,
ResetPassword = 6,
ResetState = 7,
AddFriend = 8,
RemoveFriend = 9,
AddBlock = 10,
RemoveBlock = 11,
GetServerFeatures = 12,
GetBoardInfos = 30,
RecordScore = 31,
RecordScoreData = 32,
GetScoreData = 33,
GetScoreRange = 34,
GetScoreFriends = 35,
GetScoreNpid = 36,
GetScoreAccountId = 37,
GetScoreGameDataByAccId = 38,
GetToken = 39,
SetAppearOffline = 40,
// Matchmaking
ContextStart = 100,
CreateRoom = 101,
JoinRoom = 102,
LeaveRoom = 103,
SearchRoom = 104,
RequestSignalingInfos = 105,
ContextStop = 106,
SetUserInfo = 107,
SetRoomDataInternal = 108,
SetRoomDataExternal = 109,
KickoutRoomMember = 110,
GetWorldInfoList = 111,
GetRoomDataExternalList = 112,
GetUserInfoList = 113,
GetRoomMemberDataExternalList = 114,
SendRoomMessage = 115,
};
enum class NotificationType : u16 {
FriendQuery = 5,
FriendNew = 6,
FriendLost = 7,
FriendStatus = 8,
RoomEvent = 10,
RoomMessage = 11,
WebApiPushEvent = 17, // Generic NP WebApi push event
};
enum class ErrorType : uint8_t {
NoError = 0,
Malformed = 1,
Invalid = 2,
InvalidInput = 3,
TooSoon = 4,
LoginError = 5,
LoginAlreadyLoggedIn = 6,
LoginInvalidUsername = 7,
LoginInvalidPassword = 8,
LoginInvalidToken = 9,
CreationError = 10,
CreationExistingUsername = 11,
CreationBannedEmailProvider = 12,
CreationExistingEmail = 13,
RoomMissing = 14,
RoomAlreadyJoined = 15,
RoomFull = 16,
RoomPasswordMismatch = 17,
RoomPasswordMissing = 18,
RoomGroupNoJoinLabel = 19,
RoomGroupFull = 20,
RoomGroupJoinLabelNotFound = 21,
RoomGroupMaxSlotMismatch = 22,
Unauthorized = 23,
DbFail = 24,
EmailFail = 25,
NotFound = 26,
Blocked = 27,
AlreadyFriend = 28,
ScoreNotBest = 29,
ScoreInvalid = 30,
ScoreHasData = 31,
CondFail = 32,
Unsupported = 33,
};
enum class ShadNetState {
Ok,
FailureInput,
FailureResolve,
FailureConnect,
FailureServerInfo,
FailureAuth,
FailureAlreadyIn,
FailureUsername,
FailurePassword,
FailureToken,
FailureProtocol,
FailureOther,
};
// Callback data structures
struct FriendEntry {
std::string npid;
bool online = false;
};
struct LoginResult {
ErrorType error = ErrorType::Malformed;
std::string avatarUrl;
u64 userId = 0;
std::vector<FriendEntry> friends;
std::vector<std::string> requestsSent;
std::vector<std::string> requestsReceived;
std::vector<std::string> blocked;
};
struct NotifyFriendQuery {
std::string fromNpid;
};
struct NotifyFriendNew {
std::string npid;
bool online = false;
};
struct NotifyFriendLost {
std::string npid;
};
struct NotifyFriendStatus {
std::string npid;
bool online = false;
u64 timestamp = 0;
};
struct NotifyWebApiPushEvent {
std::string npServiceName; // may be empty (catch-all listeners match any)
u32 npServiceLabel = 0;
std::string dataType; // e.g. "np:service:..."
std::string data; // raw event body (typically PSN-format JSON)
std::string fromNpid; // may be empty
std::string toNpid; // may be empty
// Optional extended-data (key,value) pairs (e.g. friendlist trigger/additionalTrigger,
// presence gameStatus/gameData). Empty when the server sends none / is older.
std::vector<std::pair<std::string, std::string>> extdData;
};
struct MatchingBinAttr {
u32 attr_id = 0;
u64 update_date = 0;
u32 update_member_id = 0;
std::vector<u8> data;
};
struct NotifyRoomEvent {
u32 ctx_id = 0;
u64 room_id = 0;
u32 event = 0;
u32 event_cause = 0;
s32 error_code = 0;
u32 flags = 0;
bool has_passwd_mask = false;
u64 passwd_slot_mask = 0;
std::string member_npid;
u64 member_account_id = 0;
u32 member_platform = 0;
u32 member_id = 0;
u32 member_team_id = 0;
bool member_is_owner = false;
u64 member_join_date = 0;
u32 member_nat_type = 0;
u32 member_flag_attr = 0;
u32 member_group_id = 0;
std::string member_addr;
u32 member_port = 0;
std::vector<MatchingBinAttr> member_bin_attrs;
std::vector<MatchingBinAttr> bin_attrs;
};
struct NotifyRoomMessage {
u32 ctx_id = 0;
u64 room_id = 0;
u32 src_member_id = 0;
u32 event = 0;
u32 cast_type = 0;
std::vector<u32> dst_member_ids;
std::string src_npid;
u64 src_account_id = 0;
u32 src_platform = 0;
std::vector<u8> msg;
};
// ShadNetClient
class ShadNetClient {
public:
ShadNetClient();
~ShadNetClient();
ShadNetClient(const ShadNetClient&) = delete;
ShadNetClient& operator=(const ShadNetClient&) = delete;
void Start(const std::string& host, u16 port, const std::string& npid,
const std::string& password, const std::string& token = {});
void Stop();
ShadNetState WaitForConnection();
ShadNetState WaitForAuthenticated();
bool IsConnected() const;
bool IsAuthenticated() const;
ShadNetState GetState() const;
const std::string& GetAvatarUrl() const;
u64 GetUserId() const;
u32 GetServerProtocolVersion() const {
return m_server_protocol_version.load();
}
u32 GetAddrLocal() const;
u32 GetAddrServer() const;
bool IsMatching2Enabled() const;
u32 GetNumFriends() const;
std::optional<std::string> GetFriendNpid(u32 index) const;
std::string GetBearerToken() const;
// Callbacks
std::function<void(const LoginResult&)> onLoginResult;
std::function<void(const NotifyFriendQuery&)> onFriendQuery;
std::function<void(const NotifyFriendNew&)> onFriendNew;
std::function<void(const NotifyFriendLost&)> onFriendLost;
std::function<void(const NotifyFriendStatus&)> onFriendStatus;
std::function<void(const NotifyRoomEvent&)> onRoomEvent;
std::function<void(const NotifyRoomMessage&)> onRoomMessage;
std::function<void(const NotifyWebApiPushEvent&)> onWebApiPushEvent;
// Async reply callback.
// cmd —command this reply is for (matches the request's cmd)
// pkt_id —packet id echoed back from the original request header
// error —ErrorType byte that prefixes every reply body
// body —reply payload AFTER the error byte (may be empty)
std::function<void(CommandType cmd, u64 pkt_id, ErrorType error, const std::vector<u8>& body)>
onAsyncReply;
// Submit a Request packet for async processing.
// Allocates a packet id, builds the packet, pushes it to the writer queue.
// Returns the packet id so callers can correlate the eventual reply.
u64 SubmitRequest(CommandType cmd, const std::vector<u8>& payload);
// Friend / block commands.
u64 AddFriend(const std::string& npid);
u64 RemoveFriend(const std::string& npid);
u64 AddBlock(const std::string& npid);
u64 RemoveBlock(const std::string& npid);
// Set the Appear-Offline preference mid-session (server handles us as offline while set).
u64 SetAppearOffline(bool enable);
private:
void ConnectThread();
void ReaderThread();
void WriterThread();
bool DoConnect();
void DoDisconnect();
bool RecvN(u8* buf, u32 n);
bool SendAll(const std::vector<u8>& data);
std::vector<u8> BuildPacket(CommandType cmd, u64 id, const std::vector<u8>& payload) const;
void DispatchPacket(PacketType type, u16 cmd_raw, u64 pkt_id, const std::vector<u8>& payload);
void HandleLoginReply(const std::vector<u8>& payload);
void HandleGetTokenReply(const std::vector<u8>& payload);
void HandleServerFeaturesReply(const std::vector<u8>& payload);
void HandleNotification(u16 cmd_raw, const std::vector<u8>& payload);
bool RequestServerFeatures();
// Helper: read a u32-LE-prefixed proto blob from a byte vector at pos.
static std::string ExtractBlob(const std::vector<u8>& p, int pos);
static void PutLE16(std::vector<u8>& b, size_t off, u16 v);
static void PutLE32(std::vector<u8>& b, size_t off, u32 v);
static void PutLE64(std::vector<u8>& b, size_t off, u64 v);
static u16 GetLE16(const u8* p);
static u32 GetLE32(const u8* p);
static u64 GetLE64(const u8* p);
ShadSocketHandle m_sock = SHAD_INVALID_SOCK;
std::string m_host;
u16 m_port = 31313;
std::string m_npid;
std::string m_password;
std::string m_token;
bool m_appear_offline = false; // user's Appear-Offline preference (sent at login)
std::atomic<bool> m_terminate{false};
std::atomic<bool> m_connected{false};
std::atomic<bool> m_authenticated{false};
std::atomic<ShadNetState> m_state{ShadNetState::Ok};
std::binary_semaphore m_sem_connected{0};
std::binary_semaphore m_sem_authenticated{0};
std::mutex m_mutex_connected;
std::mutex m_mutex_authenticated;
std::thread m_thread_connect;
std::thread m_thread_reader;
std::thread m_thread_writer;
std::mutex m_mutex_send_direct;
std::mutex m_mutex_send_queue;
std::condition_variable m_cv_send_queue;
std::vector<std::vector<u8>> m_send_queue;
std::string m_avatar_url;
u64 m_user_id = 0;
mutable std::mutex m_mutex_bearer;
std::string m_bearer_token;
std::atomic<u32> m_addr_local{0};
std::atomic<u32> m_addr_server{0};
std::atomic<u32> m_server_protocol_version{0};
std::atomic<bool> m_matching2_enabled{false};
std::atomic<bool> m_server_features_received{false};
mutable std::mutex m_mutex_friends;
std::vector<FriendEntry> m_friends;
std::atomic<u64> m_pkt_counter{1};
};
} // namespace ShadNet
// SPDX-FileCopyrightText: Copyright 2019-2026 rpcs3 Project
// SPDX-FileCopyrightText: Copyright 2024-2026 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <atomic>
#include <condition_variable>
#include <functional>
#include <mutex>
#include <optional>
#include <semaphore>
#include <string>
#include <thread>
#include <utility>
#include <vector>
#include "common/types.h"
#ifdef _WIN32
#include <winsock2.h>
#include <ws2tcpip.h>
using ShadSocketHandle = SOCKET;
static constexpr ShadSocketHandle SHAD_INVALID_SOCK = INVALID_SOCKET;
#define SHAD_CLOSE(s) ::closesocket(s)
#else
#include <arpa/inet.h>
#include <errno.h>
#include <fcntl.h>
#include <netdb.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <unistd.h>
using ShadSocketHandle = int;
static constexpr ShadSocketHandle SHAD_INVALID_SOCK = -1;
#define SHAD_CLOSE(s) ::close(s)
#endif
namespace ShadNet {
// Protocol constants
static constexpr u32 SHAD_HEADER_SIZE = 15;
static constexpr u32 SHAD_CONNECT_TIMEOUT_MS = 10000; // 10 second connect/handshake timeout
// MAX_ATTEMPTS * CONNECT_TIMEOUT + sum(backoffs).
static constexpr u32 SHAD_CONNECT_MAX_ATTEMPTS = 4;
static constexpr u32 SHAD_CONNECT_RETRY_BACKOFF_MS =
1000; // base backoff, doubled per retry (cap 8s)
static constexpr u32 SHAD_PROTOCOL_VERSION = 1;
static constexpr u32 SHAD_MAX_PACKET_SIZE = 0x800000; // 8 MiB
// Protocol enumerations (must match shadnet server protocol.h)
enum class PacketType : u8 {
Request = 0,
Reply = 1,
Notification = 2,
ServerInfo = 3,
};
enum class CommandType : u16 {
Login = 0,
Terminate = 1,
Create = 2,
Delete = 3,
SendToken = 4,
SendResetToken = 5,
ResetPassword = 6,
ResetState = 7,
AddFriend = 8,
RemoveFriend = 9,
AddBlock = 10,
RemoveBlock = 11,
GetServerFeatures = 12,
SetClientVersion = 13,
GetBoardInfos = 30,
RecordScore = 31,
RecordScoreData = 32,
GetScoreData = 33,
GetScoreRange = 34,
GetScoreFriends = 35,
GetScoreNpid = 36,
GetScoreAccountId = 37,
GetScoreGameDataByAccId = 38,
GetToken = 39,
SetAppearOffline = 40,
// Matchmaking
ContextStart = 100,
CreateRoom = 101,
JoinRoom = 102,
LeaveRoom = 103,
SearchRoom = 104,
RequestSignalingInfos = 105,
ContextStop = 106,
SetUserInfo = 107,
SetRoomDataInternal = 108,
SetRoomDataExternal = 109,
KickoutRoomMember = 110,
GetWorldInfoList = 111,
GetRoomDataExternalList = 112,
GetUserInfoList = 113,
GetRoomMemberDataExternalList = 114,
SendRoomMessage = 115,
};
enum class NotificationType : u16 {
FriendQuery = 5,
FriendNew = 6,
FriendLost = 7,
FriendStatus = 8,
RoomEvent = 10,
RoomMessage = 11,
WebApiPushEvent = 17, // Generic NP WebApi push event
ClientRejected = 19, // The server is signing this client out and says why
};
enum class ErrorType : uint8_t {
NoError = 0,
Malformed = 1,
Invalid = 2,
InvalidInput = 3,
TooSoon = 4,
LoginError = 5,
LoginAlreadyLoggedIn = 6,
LoginInvalidUsername = 7,
LoginInvalidPassword = 8,
LoginInvalidToken = 9,
CreationError = 10,
CreationExistingUsername = 11,
CreationBannedEmailProvider = 12,
CreationExistingEmail = 13,
RoomMissing = 14,
RoomAlreadyJoined = 15,
RoomFull = 16,
RoomPasswordMismatch = 17,
RoomPasswordMissing = 18,
RoomGroupNoJoinLabel = 19,
RoomGroupFull = 20,
RoomGroupJoinLabelNotFound = 21,
RoomGroupMaxSlotMismatch = 22,
Unauthorized = 23,
DbFail = 24,
EmailFail = 25,
NotFound = 26,
Blocked = 27,
AlreadyFriend = 28,
ScoreNotBest = 29,
ScoreInvalid = 30,
ScoreHasData = 31,
CondFail = 32,
Unsupported = 33,
};
enum class ShadNetState {
Ok,
FailureInput,
FailureResolve,
FailureConnect,
FailureServerInfo,
FailureAuth,
FailureAlreadyIn,
FailureUsername,
FailurePassword,
FailureToken,
FailureProtocol,
FailureOther,
};
// Callback data structures
struct FriendEntry {
std::string npid;
bool online = false;
};
struct LoginResult {
ErrorType error = ErrorType::Malformed;
std::string avatarUrl;
u64 userId = 0;
std::vector<FriendEntry> friends;
std::vector<std::string> requestsSent;
std::vector<std::string> requestsReceived;
std::vector<std::string> blocked;
};
struct NotifyFriendQuery {
std::string fromNpid;
};
struct NotifyFriendNew {
std::string npid;
bool online = false;
};
struct NotifyFriendLost {
std::string npid;
};
struct NotifyFriendStatus {
std::string npid;
bool online = false;
u64 timestamp = 0;
};
struct NotifyWebApiPushEvent {
std::string npServiceName; // may be empty (catch-all listeners match any)
u32 npServiceLabel = 0;
std::string dataType; // e.g. "np:service:..."
std::string data; // raw event body (typically PSN-format JSON)
std::string fromNpid; // may be empty
std::string toNpid; // may be empty
// Optional extended-data (key,value) pairs (e.g. friendlist trigger/additionalTrigger,
// presence gameStatus/gameData). Empty when the server sends none / is older.
std::vector<std::pair<std::string, std::string>> extdData;
};
struct MatchingBinAttr {
u32 attr_id = 0;
u64 update_date = 0;
u32 update_member_id = 0;
std::vector<u8> data;
};
struct NotifyRoomEvent {
u32 ctx_id = 0;
u64 room_id = 0;
u32 event = 0;
u32 event_cause = 0;
s32 error_code = 0;
u32 flags = 0;
bool has_passwd_mask = false;
u64 passwd_slot_mask = 0;
std::string member_npid;
u64 member_account_id = 0;
u32 member_platform = 0;
u32 member_id = 0;
u32 member_team_id = 0;
bool member_is_owner = false;
u64 member_join_date = 0;
u32 member_nat_type = 0;
u32 member_flag_attr = 0;
u32 member_group_id = 0;
std::string member_addr;
u32 member_port = 0;
std::vector<MatchingBinAttr> member_bin_attrs;
std::vector<MatchingBinAttr> bin_attrs;
};
struct NotifyRoomMessage {
u32 ctx_id = 0;
u64 room_id = 0;
u32 src_member_id = 0;
u32 event = 0;
u32 cast_type = 0;
std::vector<u32> dst_member_ids;
std::string src_npid;
u64 src_account_id = 0;
u32 src_platform = 0;
std::vector<u8> msg;
};
// ShadNetClient
class ShadNetClient {
public:
ShadNetClient();
~ShadNetClient();
ShadNetClient(const ShadNetClient&) = delete;
ShadNetClient& operator=(const ShadNetClient&) = delete;
void Start(const std::string& host, u16 port, const std::string& npid,
const std::string& password, const std::string& token = {});
void Stop();
ShadNetState WaitForConnection();
ShadNetState WaitForAuthenticated();
bool IsConnected() const;
/// The server refused this client's version (ClientRejected): signing in again cannot help.
bool WasRejected() const {
return m_rejected.load();
}
bool IsAuthenticated() const;
ShadNetState GetState() const;
const std::string& GetAvatarUrl() const;
u64 GetUserId() const;
u32 GetServerProtocolVersion() const {
return m_server_protocol_version.load();
}
u32 GetAddrLocal() const;
u32 GetAddrServer() const;
bool IsMatching2Enabled() const;
u32 GetNumFriends() const;
std::optional<std::string> GetFriendNpid(u32 index) const;
std::string GetBearerToken() const;
// Callbacks
std::function<void(const LoginResult&)> onLoginResult;
std::function<void(const NotifyFriendQuery&)> onFriendQuery;
std::function<void(const NotifyFriendNew&)> onFriendNew;
std::function<void(const NotifyFriendLost&)> onFriendLost;
std::function<void(const NotifyFriendStatus&)> onFriendStatus;
std::function<void(const NotifyRoomEvent&)> onRoomEvent;
std::function<void(const NotifyRoomMessage&)> onRoomMessage;
std::function<void(const NotifyWebApiPushEvent&)> onWebApiPushEvent;
// Async reply callback.
// cmd —command this reply is for (matches the request's cmd)
// pkt_id —packet id echoed back from the original request header
// error —ErrorType byte that prefixes every reply body
// body —reply payload AFTER the error byte (may be empty)
std::function<void(CommandType cmd, u64 pkt_id, ErrorType error, const std::vector<u8>& body)>
onAsyncReply;
// Submit a Request packet for async processing.
// Allocates a packet id, builds the packet, pushes it to the writer queue.
// Returns the packet id so callers can correlate the eventual reply.
u64 SubmitRequest(CommandType cmd, const std::vector<u8>& payload);
// Friend / block commands.
u64 AddFriend(const std::string& npid);
u64 RemoveFriend(const std::string& npid);
u64 AddBlock(const std::string& npid);
u64 RemoveBlock(const std::string& npid);
// Set the Appear-Offline preference mid-session (server handles us as offline while set).
u64 SetAppearOffline(bool enable);
private:
void ConnectThread();
void ReaderThread();
void WriterThread();
bool DoConnect();
void DoDisconnect();
bool RecvN(u8* buf, u32 n);
bool SendAll(const std::vector<u8>& data);
std::vector<u8> BuildPacket(CommandType cmd, u64 id, const std::vector<u8>& payload) const;
void DispatchPacket(PacketType type, u16 cmd_raw, u64 pkt_id, const std::vector<u8>& payload);
void HandleLoginReply(const std::vector<u8>& payload);
void HandleGetTokenReply(const std::vector<u8>& payload);
void HandleServerFeaturesReply(const std::vector<u8>& payload);
void HandleNotification(u16 cmd_raw, const std::vector<u8>& payload);
bool RequestServerFeatures();
// Tell the server which client this is; a shadNet server may deny clients that stay silent.
u64 ReportClientVersion();
static std::string BuildVersionString();
// Helper: read a u32-LE-prefixed proto blob from a byte vector at pos.
static std::string ExtractBlob(const std::vector<u8>& p, int pos);
static void PutLE16(std::vector<u8>& b, size_t off, u16 v);
static void PutLE32(std::vector<u8>& b, size_t off, u32 v);
static void PutLE64(std::vector<u8>& b, size_t off, u64 v);
static u16 GetLE16(const u8* p);
static u32 GetLE32(const u8* p);
static u64 GetLE64(const u8* p);
ShadSocketHandle m_sock = SHAD_INVALID_SOCK;
std::string m_host;
u16 m_port = 31313;
std::string m_npid;
std::string m_password;
std::string m_token;
bool m_appear_offline = false; // user's Appear-Offline preference (sent at login)
std::atomic<bool> m_terminate{false};
std::atomic<bool> m_connected{false};
std::atomic<bool> m_rejected{false};
std::atomic<bool> m_authenticated{false};
std::atomic<ShadNetState> m_state{ShadNetState::Ok};
std::binary_semaphore m_sem_connected{0};
std::binary_semaphore m_sem_authenticated{0};
std::mutex m_mutex_connected;
std::mutex m_mutex_authenticated;
std::thread m_thread_connect;
std::thread m_thread_reader;
std::thread m_thread_writer;
std::mutex m_mutex_send_direct;
std::mutex m_mutex_send_queue;
std::condition_variable m_cv_send_queue;
std::vector<std::vector<u8>> m_send_queue;
std::string m_avatar_url;
u64 m_user_id = 0;
mutable std::mutex m_mutex_bearer;
std::string m_bearer_token;
std::atomic<u32> m_addr_local{0};
std::atomic<u32> m_addr_server{0};
std::atomic<u32> m_server_protocol_version{0};
std::atomic<bool> m_matching2_enabled{false};
std::atomic<bool> m_server_features_received{false};
mutable std::mutex m_mutex_friends;
std::vector<FriendEntry> m_friends;
std::atomic<u64> m_pkt_counter{1};
};
} // namespace ShadNet
+16
View File
@@ -29,6 +29,22 @@ message SetAppearOfflineRequest {
bool appear_offline = 1;
}
// Sent right after signing in. A server with clientversions.cfg rules signs out a client
// that reports nothing, or a version it denies.
message SetClientVersionRequest {
string version = 1;
}
// Notification 19: the server is signing this client out, then disconnects.
// reason: 1 = client version not accepted
// message: text for the player, from the rule that matched
// version: the version the server checked ("" = none reported)
message NotifyClientRejected {
uint32 reason = 1;
string message = 2;
string version = 3;
}
message ServerFeaturesReply {
bool matching2_enabled = 1;
}
@@ -70,10 +70,12 @@ public:
}
std::optional<T> Pop() {
if (Size() == 0) {
// bbport: checked under the lock: a Clear() from another thread (the player stopping,
// the game closing) between the check and front() read an empty queue.
std::lock_guard guard(m_mutex);
if (m_queue.empty()) {
return std::nullopt;
}
std::lock_guard guard(m_mutex);
auto result = std::move(m_queue.front());
m_queue.pop();
return result;
@@ -387,6 +387,9 @@ bool AvPlayerSource::GetVideoData(AvPlayerFrameInfoEx& video_info) {
}
auto frame = m_video_frames.Pop();
if (!frame.has_value()) {
return false; // bbport: cleared since the size check (the player stopped)
}
video_info = frame->info;
m_current_video_frame = std::move(frame);
@@ -409,6 +412,9 @@ bool AvPlayerSource::GetAudioData(AvPlayerFrameInfo& audio_info) {
}
auto frame = m_audio_frames.Pop();
if (!frame.has_value()) {
return false; // bbport: cleared since the size check (the player stopped)
}
m_last_audio_ts = frame->info.timestamp;
m_last_data_time = std::chrono::high_resolution_clock::now();
m_pause_duration = std::chrono::high_resolution_clock::duration(0);
@@ -65,6 +65,13 @@ void Init(const Instance& instance) {
"located\n");
return;
}
// deadinside28#177: a build against other Vulkan headers left the extension's function null
// although the extension was on; the first marker then crashed.
if (!VULKAN_HPP_DEFAULT_DISPATCHER.vkCmdWriteBufferMarkerAMD) {
std::printf("GPU breadcrumbs: VK_AMD_buffer_marker has no function, GPU hangs will not be "
"located\n");
return;
}
const VkBufferCreateInfo buffer_ci = {
.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO,
.size = ArgsOffset + ArgSlots * 16,
@@ -1,6 +1,7 @@
// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <cstdio>
#include <boost/container/small_vector.hpp>
#include "common/serdes.h"
@@ -115,8 +116,13 @@ ComputePipeline::ComputePipeline(const Instance& instance, Scheduler& scheduler,
throw Serialization::CorruptData{"cached compute pipeline rejected by the driver: " +
vk::to_string(pipeline_result)};
}
ASSERT_MSG(pipeline_result == vk::Result::eSuccess, "Failed to create compute pipeline: {}",
vk::to_string(pipeline_result));
// bbport: as for graphics pipelines, one the driver cannot build skips its dispatches.
if (pipeline_result != vk::Result::eSuccess) {
std::printf("GPU: the driver could not create compute pipeline %s (%s): its dispatches "
"are skipped\n",
debug_str.c_str(), vk::to_string(pipeline_result).c_str());
return;
}
pipeline = std::move(pipe);
SetObjectName(device, *pipeline, "Compute Pipeline {}", debug_str);
}
@@ -37,18 +37,18 @@ bool ReadFile(const std::string& path, std::vector<u8>& data) {
return bool(file.read(reinterpret_cast<char*>(data.data()), std::streamsize(data.size())));
}
// The model for the menu's preset. bbport: Performance and Ultra Performance (2x and 3x) use the
// balanced model too: the v07 performance and ultra performance models leave trails behind moving
// characters in Bloodborne, and the balanced one handles those ratios cleanly (IFreemz's
// shadPS4-Bloodborne-DLSS-FSR, 9b2766e). The render size still follows the preset.
FfxFsr4ModelPreset ModelPreset(int preset) {
switch (preset) {
case 0:
return FFX_FSR4_MODEL_PRESET_NATIVE_AA;
case 1:
return FFX_FSR4_MODEL_PRESET_QUALITY;
case 2:
return FFX_FSR4_MODEL_PRESET_BALANCED;
case 3:
return FFX_FSR4_MODEL_PRESET_PERFORMANCE;
default:
return FFX_FSR4_MODEL_PRESET_ULTRA_PERFORMANCE;
return FFX_FSR4_MODEL_PRESET_BALANCED;
}
}
@@ -1,6 +1,7 @@
// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <cstdio>
#include <algorithm>
#include <utility>
#include <boost/container/small_vector.hpp>
@@ -429,8 +430,15 @@ GraphicsPipeline::GraphicsPipeline(
throw Serialization::CorruptData{"cached graphics pipeline rejected by the driver: " +
vk::to_string(pipeline_result)};
}
ASSERT_MSG(pipeline_result == vk::Result::eSuccess, "Failed to create graphics pipeline: {}",
vk::to_string(pipeline_result));
// bbport: a driver that cannot build one pipeline ("ErrorUnknown" on some AMD and older
// drivers) stopped the game here (code 23). The pipeline stays empty instead, and the draws
// that use it are skipped (PipelineCache): a missing effect rather than a closed game.
if (pipeline_result != vk::Result::eSuccess) {
std::printf("GPU: the driver could not create graphics pipeline %s (%s): its draws are "
"skipped\n",
debug_str.c_str(), vk::to_string(pipeline_result).c_str());
return;
}
pipeline = std::move(pipe);
SetObjectName(device, *pipeline, "Graphics Pipeline {}", debug_str);
}
@@ -674,6 +674,13 @@ const GraphicsPipeline* PipelineCache::GetGraphicsPipeline(const DrawIndirectPar
it.value() = std::make_unique<GraphicsPipeline>(
instance, scheduler, desc_heap, profile, sel.graphics_key, *pipeline_cache, sel.infos,
sel.runtime_infos, sel.fetch_shader, sel.modules, sdata, false);
// bbport: the driver could not create it. The empty entry keeps the key from compiling
// again on every draw (they are skipped); it is not saved, so the next start tries anew.
if (!it.value()->IsValid()) {
it.value().reset();
sel.fetch_shader.reset();
return nullptr;
}
RegisterPipelineData(sel.graphics_key, pipeline_hash, sdata);
++num_new_pipelines;
@@ -693,6 +700,10 @@ const GraphicsPipeline* PipelineCache::GetGraphicsPipeline(const DrawIndirectPar
const GraphicsPipeline* PipelineCache::FinishPipeline(const GraphicsPipelineKey& key,
PendingPipeline& job) {
if (!job.pipeline || !job.pipeline->IsValid()) {
graphics_pipelines[key].reset(); // bbport: as GetGraphicsPipeline, skipped and not saved
return nullptr;
}
const auto pipeline_hash = std::hash<GraphicsPipelineKey>{}(key);
RegisterPipelineData(key, pipeline_hash, job.sdata);
++num_new_pipelines;
@@ -774,6 +785,10 @@ const ComputePipeline* PipelineCache::GetComputePipeline() {
it.value() = std::make_unique<ComputePipeline>(instance, scheduler, desc_heap, profile,
*pipeline_cache, compute_key, *sel.infos[0],
sel.modules[0], sdata, false);
if (!it.value()->IsValid()) { // bbport: as for graphics pipelines
it.value().reset();
return nullptr;
}
RegisterPipelineData(compute_key, sdata);
++num_new_pipelines;
@@ -36,6 +36,11 @@ public:
return *pipeline;
}
/// bbport: false when the driver could not create the pipeline (its draws are skipped).
bool IsValid() const noexcept {
return static_cast<bool>(pipeline);
}
vk::PipelineLayout GetLayout() const noexcept {
return *pipeline_layout;
}
@@ -27,6 +27,7 @@
#include <chrono>
#include <cmath>
#include <csetjmp>
#include <cstdio>
#include <cstring>
#include <ctime>
#include <filesystem>
@@ -95,6 +96,30 @@ bool CanBlitToSwapchain(const vk::PhysicalDevice physical_device, vk::Format for
return MakeImageBlit(frame_width, frame_height, swapchain_width, swapchain_height, 0, 0);
}
// bbport: a frame's fence, waited for in 10 s slices. Past the first slice the log says so, once
// per wait, with the breadcrumbs (which command the GPU has not finished): without it a GPU that
// never finishes the frame froze the game with nothing in the log (issue #5).
static vk::Result WaitFrameFence(vk::Device device, vk::Fence fence, const char* where) {
constexpr u64 slice = 10'000'000'000ULL;
bool told = false;
while (true) {
const vk::Result result = device.waitForFences(fence, false, slice);
if (result != vk::Result::eTimeout) {
if (told) {
std::printf("Presenter: %s finished after all\n", where);
std::fflush(stdout);
}
return result;
}
if (!told) {
told = true;
std::printf("Presenter: %s not finished after 10 s: the GPU is stuck on it\n", where);
std::fflush(stdout);
Breadcrumbs::ReportStuck(where);
}
}
}
static vk::Rect2D FitImage(s32 frame_width, s32 frame_height, s32 swapchain_width,
s32 swapchain_height) {
float frame_aspect = static_cast<float>(frame_width) / frame_height;
@@ -251,14 +276,11 @@ Frame* Presenter::PrepareLastFrame() {
Frame* frame = last_submit_frame;
while (true) {
vk::Result result = instance.GetDevice().waitForFences(frame->present_done, false,
std::numeric_limits<u64>::max());
vk::Result result =
WaitFrameFence(instance.GetDevice(), frame->present_done, "the last frame");
if (result == vk::Result::eSuccess) {
break;
}
if (result == vk::Result::eTimeout) {
continue;
}
if (result == vk::Result::eErrorDeviceLost) {
Breadcrumbs::ReportDeviceLost("waiting for a frame");
}
@@ -760,7 +782,7 @@ Frame* Presenter::GetRenderFrame() {
vk::Result result{};
const auto wait = [&]() {
result = device.waitForFences(frame->present_done, false, std::numeric_limits<u64>::max());
result = WaitFrameFence(device, frame->present_done, "a frame to draw into");
return result;
};
@@ -1,7 +1,7 @@
// SPDX-FileCopyrightText: Copyright 2020 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <limits>
#include <cstdio>
#include "video_core/renderer_vulkan/vk_breadcrumbs.h"
#include "video_core/renderer_vulkan/vk_instance.h"
#include "video_core/renderer_vulkan/vk_semaphore.h"
@@ -10,7 +10,8 @@
namespace Vulkan {
constexpr u64 WAIT_TIMEOUT = std::numeric_limits<u64>::max();
// bbport: waits go in 10 s slices so a GPU that never finishes is reported, not waited on silently.
constexpr u64 WAIT_TIMEOUT = 10'000'000'000ULL;
Semaphore::Semaphore(const Instance& instance_) : instance{instance_} {
const vk::StructureChain semaphore_chain = {
@@ -66,12 +67,28 @@ void Semaphore::Wait(u64 tick) {
.pValues = &tick,
};
// bbport: a lost device ends the wait with a report instead of a silent endless loop.
// bbport: a lost device ends the wait with a report instead of a silent endless loop, and a
// submission still not finished after 10 s says so once with the GPU breadcrumbs.
bool told = false;
while (true) {
const vk::Result result = instance.GetDevice().waitSemaphores(&wait_info, WAIT_TIMEOUT);
if (result == vk::Result::eSuccess) {
if (told) {
std::printf("GPU: submission %llu finished after all\n", (unsigned long long)tick);
std::fflush(stdout);
}
break;
}
if (result == vk::Result::eTimeout) {
if (!told) {
told = true;
std::printf("GPU: submission %llu not finished after 10 s: the GPU is stuck on it\n",
(unsigned long long)tick);
std::fflush(stdout);
Breadcrumbs::ReportStuck("waiting for a submission");
}
continue;
}
if (result == vk::Result::eErrorDeviceLost) {
Breadcrumbs::ReportDeviceLost("waiting for a submission");
ASSERT_MSG(false, "Device lost while waiting for a submission");
@@ -2,6 +2,7 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#include <algorithm>
#include <cstdio>
#include <limits>
#include "common/assert.h"
#include "common/logging/log.h"
@@ -120,9 +121,25 @@ void Swapchain::SetHDR(bool hdr) {
bool Swapchain::AcquireNextImage() {
vk::Device device = instance.GetDevice();
vk::Result result =
device.acquireNextImageKHR(swapchain, std::numeric_limits<u64>::max(),
image_acquired[frame_index], VK_NULL_HANDLE, &image_index);
// bbport: in 10 s slices, saying so once when the compositor holds every image that long
// (issue #5: a frozen game with nothing in the log).
vk::Result result;
bool told = false;
while ((result = device.acquireNextImageKHR(swapchain, 10'000'000'000ULL,
image_acquired[frame_index], VK_NULL_HANDLE,
&image_index)) == vk::Result::eTimeout ||
result == vk::Result::eNotReady) {
if (!told) {
told = true;
std::printf("Swapchain: no image from the compositor in 10 s (it holds all %zu)\n",
images.size());
std::fflush(stdout);
}
}
if (told) {
std::printf("Swapchain: the compositor gave an image back\n");
std::fflush(stdout);
}
switch (result) {
case vk::Result::eSuccess:
@@ -14,6 +14,7 @@
#include "core/memory.h"
#include "video_core/buffer_cache/buffer_cache.h"
#include "video_core/page_manager.h"
#include "video_core/renderer_vulkan/liverpool_to_vk.h"
#include "video_core/renderer_vulkan/vk_instance.h"
#include "video_core/renderer_vulkan/vk_runtime.h"
#include "video_core/renderer_vulkan/vk_scheduler.h"
@@ -854,12 +855,18 @@ ImageView& TextureCache::FindDepthTarget(ImageId image_id, const ImageDesc& desc
// If there is a stencil attachment, link depth and stencil.
if (desc.info.stencil_addr != 0) {
ImageId stencil_id{};
ForEachImageInRegion(desc.info.stencil_addr, desc.info.stencil_size,
[&](ImageId image_id, Image& image) {
if (image.info.guest_address == desc.info.stencil_addr) {
stencil_id = image_id;
}
});
ForEachImageInRegion(
desc.info.stencil_addr, desc.info.stencil_size, [&](ImageId image_id, Image& image) {
if (image.info.guest_address != desc.info.stencil_addr) {
return;
}
// Not a color image that reuses the memory: its views would reach the depth
// buffer through an invalid view (shadPS4 #5118).
if (image.info.pixel_format == vk::Format::eUndefined ||
Vulkan::LiverpoolToVK::IsFormatStencilCompatible(image.info.pixel_format)) {
stencil_id = image_id;
}
});
if (!stencil_id) {
ImageInfo info{};
info.guest_address = desc.info.stencil_addr;
+7
View File
@@ -865,6 +865,13 @@ inline const char* MenuTranslation(MenuLanguage language, const char* english) {
"Tastatur: tippen, Rücktaste = löschen, Enter = OK, Esc = abbrechen",
"Teclado: digite, Backspace = apagar, Enter = OK, Esc = cancelar",
"键盘:直接输入,Backspace = 删除,Enter = 确定,Esc = 取消"}},
{"Sound when a trophy is earned",
{"Sonido al conseguir un trofeo",
"Suono quando si ottiene un trofeo",
"Son à l’obtention d’un trophée",
"Ton beim Erhalt einer Trophäe",
"Som ao conquistar um troféu",
"获得奖杯时播放音效"}},
{"Notice while shaders compile",
{"Aviso mientras se compilan shaders",
"Avviso durante la compilazione degli shader",
+72 -8
View File
@@ -239,6 +239,12 @@ void Menu() {
}
ImGui::SetNextWindowPos(pos, ImGuiCond_Appearing);
ImGui::SetNextWindowSize(ImVec2(620.0f * base_scale, 0.0f), ImGuiCond_Appearing);
// A menu taller than the screen would leave its last row (Close, Quit game) off screen: its
// height stays within the screen below where it opens, and the contents scroll.
ImGui::SetNextWindowSizeConstraints(
ImVec2(0.0f, 0.0f),
ImVec2(FLT_MAX, std::max(viewport->WorkPos.y + viewport->WorkSize.y - pos.y - 20.0f * base_scale,
200.0f * base_scale)));
bool keep_open = true;
if (!ImGui::Begin(
BbSettings::MenuText("Bloodborne - Graphics (Insert / L3+R3)###bbport_settings",
@@ -263,6 +269,7 @@ void Menu() {
if (ImGui::Combo("Language", &language, BbSettings::MenuLanguageNames,
int(BbSettings::MenuLanguage::Count))) {
s.menu_language = BbSettings::MenuLanguage(language);
s.menu_language_chosen = true;
BbSettings::Save();
}
ImGui::Text(BbSettings::MenuText("%.0f FPS (%.1f ms)", "%.0f FPS (%.1f мс)"),
@@ -371,13 +378,14 @@ void Menu() {
for (int i = 0; i < BbSettings::PresetCount; ++i) {
char label[64];
const float scale = BbSettings::PresetScale(i);
const int output = s.output_res;
int output_width = BbSettings::OutputWidths[s.output_res];
int output_height = BbSettings::OutputHeights[s.output_res];
BbSettings::CustomOutput(output_width, output_height);
std::snprintf(
label, sizeof(label),
BbSettings::MenuText("%s (x%.1f, render %dx%d)", "%s (x%.1f, рендер %dx%d)"),
BbSettings::PresetName(i), scale,
int(std::lround(BbSettings::OutputWidths[output] / scale / 2) * 2),
int(std::lround(BbSettings::OutputHeights[output] / scale / 2) * 2));
BbSettings::PresetName(i), scale, int(std::lround(output_width / scale / 2) * 2),
int(std::lround(output_height / scale / 2) * 2));
if (ImGui::Selectable(label, i == preset)) {
Store(s.preset, i, true);
}
@@ -500,10 +508,19 @@ void Menu() {
ImGui::SeparatorText(BbSettings::MenuText("Output resolution", "Разрешение вывода"));
static const char* outputs[] = {"1280 x 720", "1920 x 1080", "2560 x 1440", "3840 x 2160"};
int output = s.output_res;
// A size from bbport.ini that the list does not have stays until another one is chosen.
int custom_width = 0, custom_height = 0;
const bool custom = BbSettings::CustomOutput(custom_width, custom_height);
char custom_label[32];
std::snprintf(custom_label, sizeof(custom_label), "%d x %d", custom_width, custom_height);
if (ImGui::BeginCombo(BbSettings::MenuText("Output resolution", "Разрешение вывода"),
outputs[output])) {
custom ? custom_label : outputs[output])) {
if (custom) {
ImGui::Selectable(custom_label, true);
}
for (int i = 0; i < BbSettings::OutputCount; ++i) {
if (ImGui::Selectable(outputs[i], i == output)) {
if (ImGui::Selectable(outputs[i], !custom && i == output)) {
s.custom_output_width = 0; // a listed size replaces the one from bbport.ini
Store(s.output_res, i, true);
}
}
@@ -613,6 +630,8 @@ void Menu() {
"filling, not a problem. Each place only once.",
"«Компиляция шейдеров» в левом нижнем углу: рывок в это время — заполнение кэша "
"шейдеров, а не проблема. В каждом месте только один раз."));
Checkbox(BbSettings::MenuText("Sound when a trophy is earned", "Звук при получении трофея"),
s.trophy_sound);
ImGui::Spacing();
if (ImGui::Button(BbSettings::MenuText("Close", "Закрыть"))) {
@@ -620,10 +639,15 @@ void Menu() {
}
// Issues #74, #116: leaving the game from the pad (this menu opens with L3+R3). A second
// press confirms; the window thread then closes like the window's own close button.
// One ID for both labels ("###bb_quit"): with a new ID the button lost the pad's or keyboard's
// focus at the first press, which disarmed it at once.
static bool quit_armed;
ImGui::SameLine();
if (ImGui::Button(quit_armed ? BbSettings::MenuText("Press again to quit", "Нажмите ещё раз для выхода")
: BbSettings::MenuText("Quit game", "Выйти из игры"))) {
const std::string quit_label =
std::string(quit_armed ? BbSettings::MenuText("Press again to quit", "Нажмите ещё раз для выхода")
: BbSettings::MenuText("Quit game", "Выйти из игры")) +
"###bb_quit";
if (ImGui::Button(quit_label.c_str())) {
if (quit_armed) {
SDL_Event quit{};
quit.type = SDL_EVENT_QUIT;
@@ -744,6 +768,43 @@ ImTextureData* NoticeIcon(const Notice& n) {
return texture;
}
// The trophy chime: a short bell of our own (no audio from the console), two notes and a third
// for the platinum, on its own SDL stream beside the game's. From GerasimosKan's trophies
// (deadinside28/bloodborne_pc#127). BB_AUDIO=none keeps it silent like the game.
void TrophyChime(bool platinum) {
static SDL_AudioStream* stream = []() -> SDL_AudioStream* {
const char* mode = std::getenv("BB_AUDIO");
if ((mode && !std::strcmp(mode, "none")) || !SDL_InitSubSystem(SDL_INIT_AUDIO)) {
return nullptr;
}
const SDL_AudioSpec spec{SDL_AUDIO_F32, 1, 48000};
SDL_AudioStream* s =
SDL_OpenAudioDeviceStream(SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, &spec, nullptr, nullptr);
if (s) {
SDL_ResumeAudioStreamDevice(s);
}
return s;
}();
if (!stream) {
return;
}
constexpr int rate = 48000;
constexpr float two_pi = 6.2831853f, step = 0.09f;
std::vector<float> pcm(rate * 6 / 5);
const float notes[] = {1318.5f, 1975.5f, 2637.0f}; // E6, B6, E7
for (int k = 0; k < (platinum ? 3 : 2); ++k) {
for (std::size_t i = std::size_t(k * step * rate); i < pcm.size(); ++i) {
const float t = float(i) / rate - k * step, f = notes[k];
const float envelope = std::min(t / 0.004f, 1.0f) * std::exp(-t * 5.0f);
// A bell: the fundamental and a quickly fading inharmonic partial.
pcm[i] += 0.12f * envelope *
(std::sin(two_pi * f * t) +
0.35f * std::exp(-t * 8.0f) * std::sin(two_pi * 2.76f * f * t));
}
}
SDL_PutAudioStreamData(stream, pcm.data(), int(pcm.size() * sizeof(float)));
}
void TrophyCard() {
const auto now = std::chrono::steady_clock::now();
if (!notice_shown) {
@@ -756,6 +817,9 @@ void TrophyCard() {
notices.pop_front();
notice_shown = true;
notice_start = now;
if (BbSettings::Get().trophy_sound) {
TrophyChime(notice.grade == 'P');
}
}
const float t = std::chrono::duration<float>(now - notice_start).count();
if (t > NoticeIn + NoticeHold + NoticeOut) {
+38 -3
View File
@@ -33,6 +33,7 @@ void Set(Values& v, const std::string& key, const std::string& value) {
const auto it = std::find(std::begin(MenuLanguageCodes), std::end(MenuLanguageCodes), value);
if (it != std::end(MenuLanguageCodes)) {
v.menu_language = MenuLanguage(it - std::begin(MenuLanguageCodes));
v.menu_language_chosen = true;
} else {
std::printf("Settings: unknown menu_language '%s' (expected en, ru, es, it, fr, de, pt_BR or zh_CN)\n",
value.c_str());
@@ -67,6 +68,8 @@ void Set(Values& v, const std::string& key, const std::string& value) {
v.show_fps = i != 0;
} else if (key == "shader_notice") {
v.shader_notice = i != 0;
} else if (key == "trophy_sound") {
v.trophy_sound = i != 0;
} else if (key == "menu_pos") {
float x = -1.0f, y = -1.0f;
if (std::sscanf(value.c_str(), "%f,%f", &x, &y) == 2 && x >= 0.0f && x <= 1.0f && y >= 0.0f &&
@@ -83,11 +86,19 @@ void Set(Values& v, const std::string& key, const std::string& value) {
} else if (key == "live_resolution") {
v.live_resolution = value == "auto" ? -1 : std::clamp(i, 0, 1);
} else if (key == "output_res") {
bool listed = false;
for (int r = 0; r < OutputCount; ++r) {
if (value == std::to_string(OutputWidths[r]) + "x" + std::to_string(OutputHeights[r])) {
v.output_res = r;
listed = true;
}
}
int width = 0, height = 0;
if (!listed && std::sscanf(value.c_str(), "%dx%d", &width, &height) == 2 && width > 0 &&
height > 0) {
v.custom_output_width = width;
v.custom_output_height = height;
}
} else {
for (int e = 0; e < EffectCount; ++e) {
if (key == Effects[e].key) {
@@ -133,6 +144,7 @@ MenuLanguage MenuLanguageFromCode(const char* code) {
void Load() {
auto& v = Get();
v.custom_output_width = v.custom_output_height = 0;
for (int e = 0; e < EffectCount; ++e) {
v.effects[e] = Effects[e].default_on;
}
@@ -149,6 +161,7 @@ void Load() {
}
}
v.menu_language = MenuLanguageFromCode(lang);
v.menu_language_chosen = false;
}
if (FILE* file = std::fopen(Path(), "r")) {
char line[256];
@@ -190,6 +203,7 @@ void Load() {
}
v.startup_model_lod = v.model_lod;
v.startup_output_res = v.output_res;
v.startup_custom_output_width = v.custom_output_width;
v.startup_live_resolution = v.live_resolution;
}
@@ -231,11 +245,22 @@ int RenderPreset() {
v.upscaler == UpscalerTaa ? NativeAA : v.preset.load();
}
bool CustomOutput(int& width, int& height) {
const auto& v = Get();
if (v.custom_output_width <= 0 || v.output_res != v.startup_output_res) {
return false;
}
width = v.custom_output_width;
height = v.custom_output_height;
return true;
}
bool ResolutionNeedsRestart() {
const auto& v = Get();
// TAA needs the live path (native guest targets): run.sh selects it on restart.
return FixedRenderSession() &&
(v.preset != v.startup_preset || v.output_res != v.startup_output_res ||
v.custom_output_width != v.startup_custom_output_width ||
(v.upscaler == UpscalerOff) != (v.startup_upscaler == UpscalerOff) ||
(v.upscaler == UpscalerTaa) != (v.startup_upscaler == UpscalerTaa));
}
@@ -252,7 +277,11 @@ void Save() {
return std::string(text);
};
const auto flag = [](bool value) { return std::string(value ? "1" : "0"); };
put("menu_language", MenuLanguageCodes[int(v.menu_language.load())]);
// Not the launcher's or the system's language the menu started with: that one is not a choice,
// and once in the file it would stay whatever the launcher's language becomes.
if (v.menu_language_chosen) {
put("menu_language", MenuLanguageCodes[int(v.menu_language.load())]);
}
put("upscaler", UpscalerName(v.upscaler));
put("preset", std::to_string(v.preset.load()));
put("sharpen", flag(v.sharpen));
@@ -266,6 +295,7 @@ void Save() {
put("debug_view", std::to_string(v.debug_view.load()));
put("show_fps", flag(v.show_fps));
put("shader_notice", flag(v.shader_notice));
put("trophy_sound", flag(v.trophy_sound));
put("fsr4_auto_exposure", flag(v.fsr4_auto_exposure));
put("fsr4_invert_jitter", flag(v.fsr4_invert_jitter));
// Read by patches.py at start.
@@ -273,8 +303,13 @@ void Save() {
put(Effects[e].key, flag(v.effects[e]));
}
put("model_lod", std::to_string(v.model_lod.load()));
put("output_res", std::to_string(OutputWidths[v.output_res]) + "x" +
std::to_string(OutputHeights[v.output_res]));
int custom_width = 0, custom_height = 0;
if (CustomOutput(custom_width, custom_height)) {
put("output_res", std::to_string(custom_width) + "x" + std::to_string(custom_height));
} else {
put("output_res", std::to_string(OutputWidths[v.output_res]) + "x" +
std::to_string(OutputHeights[v.output_res]));
}
// Read by run.sh at start.
put("live_resolution", v.live_resolution < 0 ? "auto" : flag(v.live_resolution != 0));
if (v.menu_x >= 0.0f && v.menu_y >= 0.0f) {
+10
View File
@@ -69,6 +69,8 @@ inline constexpr int OutputDefault = 1; ///< 1920x1080, the game's own size
struct Values {
std::atomic<MenuLanguage> menu_language{MenuLanguage::Russian};
/// Chosen in the menu or read from the file: only then Save() writes menu_language.
std::atomic<bool> menu_language_chosen{false};
std::atomic<int> upscaler{UpscalerFsr3};
std::atomic<int> preset{NativeAA};
std::atomic<bool> sharpen{true};
@@ -83,6 +85,8 @@ struct Values {
std::atomic<bool> show_fps{false};
/// "Compiling shaders" in the bottom-left corner while it happens (stutter is expected then).
std::atomic<bool> shader_notice{true};
/// A short chime of our own when a trophy notice shows.
std::atomic<bool> trophy_sound{true};
/// The settings menu's position (fraction of the screen), -1 until it is moved.
std::atomic<float> menu_x{-1.0f}, menu_y{-1.0f};
// FSR 4 checks (menu): the provider's auto exposure, the jitter sign it is given.
@@ -93,6 +97,9 @@ struct Values {
std::atomic<bool> effects[EffectCount]{};
std::atomic<int> model_lod{0}; ///< -2 highest .. 2 lowest, 0 the game's
std::atomic<int> output_res{OutputDefault}; ///< index into OutputWidths
/// An output_res in bbport.ini that the list does not have (5120x2160, ...): run.sh and
/// patches.py use it, and the menu keeps it until another size is chosen.
int custom_output_width = 0, custom_output_height = 0;
/// Live resolution and preset changes (run.sh): 0 off by default (startup patch, fastest
/// on the Steam Deck and older GPUs), -1 auto (strong discrete GPUs), 1 on. On restart.
std::atomic<int> live_resolution{0};
@@ -110,6 +117,7 @@ struct Values {
bool startup_effects[EffectCount]{};
int startup_model_lod = 0;
int startup_output_res = OutputDefault;
int startup_custom_output_width = 0;
int startup_live_resolution = 0;
};
@@ -132,6 +140,8 @@ void ConfigureDlssSupport(bool available, const char* problem);
bool FixedRenderSession();
int RenderPreset();
bool ResolutionNeedsRestart();
/// The output size from bbport.ini that the menu does not list, while it is still the choice.
bool CustomOutput(int& width, int& height);
/// Writes the file (menu changes).
void Save();
+37
View File
@@ -0,0 +1,37 @@
# SPDX-License-Identifier: GPL-2.0-or-later
"""The port's shader cache: <user directory>/cache, one folder (or .zip) per game serial, with the
shaders the GPU library translated (cache_storage.cpp). The launcher shows its size and clears it;
the next start translates the shaders again. The drivers' own caches (NVIDIA's, Mesa's) belong to
the system and other games, and stay.
"""
import shutil
from pathlib import Path
def cache_dir(user_dir):
return Path(user_dir) / 'cache'
def size(user_dir):
"""Bytes the shader cache takes (its files' sizes); 0 when there is none."""
total = 0
for path in cache_dir(user_dir).rglob('*'):
try:
if path.is_file() and not path.is_symlink():
total += path.stat().st_size
except OSError:
continue
return total
def clear(user_dir):
"""Removes everything in the shader cache, not the folder itself. Returns the bytes freed."""
freed = size(user_dir)
directory = cache_dir(user_dir)
if directory.is_dir():
for entry in directory.iterdir():
if entry.is_dir() and not entry.is_symlink():
shutil.rmtree(entry, ignore_errors=True)
else:
entry.unlink(missing_ok=True)
return freed - size(user_dir)
+168 -48
View File
@@ -476,8 +476,8 @@ EN = {
"Windows port by Supermedo",
"Лицензия":
"License",
"GNU GPL v2 или новее. Файлов игры нет: Bloodborne принадлежит Sony Interactive Entertainment и FromSoftware":
"GNU GPL v2 or later. No game files included: Bloodborne belongs to Sony Interactive Entertainment and FromSoftware",
'GNU GPL v2 или новее · Файлы игры не входят.': 'GNU GPL v2 or later · No game files included.',
'Bloodborne принадлежит Sony Interactive Entertainment и FromSoftware.': 'Bloodborne belongs to Sony Interactive Entertainment and FromSoftware.',
"Проверить сейчас":
"Check now",
# Online page: your own shadNet server, the co-op port, seamless co-op, the UDP test
@@ -521,8 +521,8 @@ EN = {
'Recorded when the game unlocks them, and shown in the game',
'Ключ трофеев':
'Trophy key',
'Ключ трофеев PS4 (TrophyKey в shadPS4), 32 шестнадцатеричных символа: с ним видны названия и описания. Хранится на этом компьютере. Без него видны только значки':
"The PS4's trophy key (TrophyKey in shadPS4), 32 hexadecimal digits: with it, the names and descriptions show. Kept on this computer. Without it, only the icons show",
'Ключ трофеев PS4 (TrophyKey в shadPS4), 32 шестнадцатеричных символа: с ним названия и описания на языке игры. Хранится на этом компьютере. Без него они на английском':
"The PS4's trophy key (TrophyKey in shadPS4), 32 hexadecimal digits: with it, the names and descriptions are in the game's language. Kept on this computer. Without it, they are in English",
'Неверный ключ трофеев: названия недоступны':
'Wrong trophy key: the names are unavailable',
'Получен {}':
@@ -562,17 +562,17 @@ EN = {
'Помощь и тесты': 'Help and testing',
'Другие благодарности': 'Also thanks to',
'bbport — порт, на котором построен весь этот проект': 'bbport, the port this whole project is built on',
'Игра по сети, DLSS, страница читов, добавление в Steam и значок': 'Online play, DLSS, the cheats page, Add to Steam and the icon',
'Игра по сети, DLSS, страница читов, добавление в Steam, значок и пропуск конвейеров, которые драйвер не может создать': 'Online play, DLSS, the cheats page, Add to Steam, the icon and skipping pipelines the driver cannot create',
'Собрал и адаптировал десятки исправлений: вылеты при загрузке, чёрные изображения, апскейлер, строка производительности в журнале': 'Gathered and adapted dozens of fixes: crashes while loading, black images, the upscaler, the performance line in the log',
'Чёрный предпросмотр персонажа, картинка экрана загрузки и память кэша текстур': "The black character preview, the loading screen's picture and the texture cache's memory",
'Кэш текстур вовремя освобождает видеопамять': 'The texture cache frees video memory in time',
'Вылеты при загрузке областей': 'Crashes while loading areas',
'Интерфейс, пропадавший не в 1080p, векторы движения больших чудовищ и ошибка памяти': 'The interface vanishing outside 1080p, motion vectors for the large beasts and a memory fault',
'Вылеты при загрузке некоторых карт': 'Crashes when loading some maps',
'Вылеты при загрузке некоторых карт и меню жестов с клавиатуры': 'Crashes when loading some maps and the gestures menu from the keyboard',
'Экранная клавиатура для имени персонажа и уведомление о компиляции шейдеров': "The on-screen keyboard for the character's name and the shader compiling notice",
"Быстрая загрузка после смерти и перемещения (bbhost) и сервер The Hunter's Dream": "Fast loading after a death or travel (bbhost) and The Hunter's Dream server",
'Сетевые библиотеки игры по сети и сервер для своего кооператива': 'The online play libraries and the server for your own co-op',
'Основа поддержки DLSS': 'The groundwork for DLSS',
'Основа поддержки DLSS и модель FSR 4 без шлейфов за персонажами': 'The groundwork for DLSS, and the FSR 4 model without trails behind characters',
'Апскейлер FSR 3.1': 'The FSR 3.1 upscaler',
'Игра запускается, когда DLSS нельзя использовать': 'The game starts when DLSS cannot be used',
'Перевод на упрощённый китайский': 'The Simplified Chinese translation',
@@ -587,6 +587,23 @@ EN = {
'Рендерер и компилятор шейдеров': 'The renderer and shader recompiler',
'Инструмент для сборки AppImage': 'The tool that builds the AppImage',
'Нашёл ошибку, из-за которой игра не запускалась на некоторых видеокартах': 'Found the bug that kept the game from starting on some graphics cards',
# Shader cache
'Кэш шейдеров': 'Shader cache',
'Очистить': 'Clear',
'{}. Если после обновления картинка выглядит странно, очистите его: шейдеры соберутся заново (первые минуты в каждой зоне возможны рывки)': '{}. If the picture looks wrong after an update, clear it: the shaders are built again (some stutter in each area for the first minutes)',
'Пусто: шейдеры соберутся при игре': 'Empty: the shaders are built as you play',
'Очистить кэш шейдеров?': 'Clear the shader cache?',
'Игра соберёт шейдеры заново: первые минуты в каждой зоне возможны рывки. Сохранения и настройки не затрагиваются.': 'The game builds the shaders again: some stutter in each area for the first minutes. Saves and settings are not touched.',
'Названия трофеев на английском без ключа и звук при получении трофея': 'Trophy names in English without the key, and the sound when one is earned',
'Буфер трафарета, в который писали цветные изображения': 'The stencil buffer that colour images were writing into',
'Вход на публичный сервер shadNet без отключения': 'Staying signed in on the public shadNet server',
'Игра по сети': 'Online play',
'Сервер shadNet не принимает bbport, игра осталась офлайн. Для игры по сети используйте свой сервер (страница «Онлайн»).': 'The shadNet server does not accept bbport, so the game stayed offline. For online play, use your own server (Online page).',
'Выход из игры из меню с контроллера и меню в пределах экрана': 'Quitting from the menu with a controller, and the menu kept within the screen',
'Вылет видеоплеера при его остановке': 'A video player crash when it stops',
'Язык внутриигрового меню при запуске без окна лаунчера': "The in-game menu's language when starting without the launcher window",
'DLSS в AppImage на NVIDIA': 'DLSS in the AppImage on NVIDIA',
'Сверхширокие мониторы без растянутой картинки': 'Ultrawide monitors without a stretched picture',
}
PT_BR = {
@@ -1070,8 +1087,8 @@ PT_BR = {
"Port para Windows de Supermedo",
"Лицензия":
"Licença",
"GNU GPL v2 или новее. Файлов игры нет: Bloodborne принадлежит Sony Interactive Entertainment и FromSoftware":
"GNU GPL v2 ou posterior. Não inclui arquivos do jogo: Bloodborne pertence à Sony Interactive Entertainment e à FromSoftware",
'GNU GPL v2 или новее · Файлы игры не входят.': 'GNU GPL v2 ou posterior · Não inclui arquivos do jogo.',
'Bloodborne принадлежит Sony Interactive Entertainment и FromSoftware.': 'Bloodborne pertence à Sony Interactive Entertainment e à FromSoftware.',
"Проверить сейчас":
"Verificar agora",
# 0.51: mouse look, choosing NVIDIA's DLSS library, the online module
@@ -1162,8 +1179,8 @@ PT_BR = {
'Registrados quando o jogo os desbloqueia, e mostrados no jogo',
'Ключ трофеев':
'Chave de troféus',
'Ключ трофеев PS4 (TrophyKey в shadPS4), 32 шестнадцатеричных символа: с ним видны названия и описания. Хранится на этом компьютере. Без него видны только значки':
'A chave de troféus do PS4 (TrophyKey no shadPS4), 32 dígitos hexadecimais: com ela aparecem os nomes e as descrições. Fica neste computador. Sem ela, só aparecem os ícones',
'Ключ трофеев PS4 (TrophyKey в shadPS4), 32 шестнадцатеричных символа: с ним названия и описания на языке игры. Хранится на этом компьютере. Без него они на английском':
'A chave de troféus do PS4 (TrophyKey no shadPS4), 32 dígitos hexadecimais: com ela, os nomes e as descrições ficam no idioma do jogo. Fica neste computador. Sem ela, ficam em inglês',
'Неверный ключ трофеев: названия недоступны':
'Chave de troféus incorreta: os nomes não estão disponíveis',
'Получен {}':
@@ -1203,17 +1220,17 @@ PT_BR = {
'Помощь и тесты': 'Ajuda e testes',
'Другие благодарности': 'Também agradecemos a',
'bbport — порт, на котором построен весь этот проект': 'bbport, o port em que todo este projeto se baseia',
'Игра по сети, DLSS, страница читов, добавление в Steam и значок': 'Jogo online, DLSS, a página de trapaças, Adicionar à Steam e o ícone',
'Игра по сети, DLSS, страница читов, добавление в Steam, значок и пропуск конвейеров, которые драйвер не может создать': 'Jogo online, DLSS, a página de trapaças, Adicionar à Steam, o ícone e pular pipelines que o driver não consegue criar',
'Собрал и адаптировал десятки исправлений: вылеты при загрузке, чёрные изображения, апскейлер, строка производительности в журнале': 'Reuniu e adaptou dezenas de correções: travamentos ao carregar, imagens pretas, o upscaler, a linha de desempenho no log',
'Чёрный предпросмотр персонажа, картинка экрана загрузки и память кэша текстур': 'A prévia do personagem preta, a imagem da tela de carregamento e a memória do cache de texturas',
'Кэш текстур вовремя освобождает видеопамять': 'O cache de texturas libera a memória de vídeo a tempo',
'Вылеты при загрузке областей': 'Travamentos ao carregar áreas',
'Интерфейс, пропадавший не в 1080p, векторы движения больших чудовищ и ошибка памяти': 'A interface que sumia fora de 1080p, vetores de movimento das grandes feras e uma falha de memória',
'Вылеты при загрузке некоторых карт': 'Travamentos ao carregar alguns mapas',
'Вылеты при загрузке некоторых карт и меню жестов с клавиатуры': 'Travamentos ao carregar alguns mapas e o menu de gestos pelo teclado',
'Экранная клавиатура для имени персонажа и уведомление о компиляции шейдеров': 'O teclado na tela para o nome do personagem e o aviso de compilação de shaders',
"Быстрая загрузка после смерти и перемещения (bbhost) и сервер The Hunter's Dream": "Carregamento rápido após morrer ou viajar (bbhost) e o servidor The Hunter's Dream",
'Сетевые библиотеки игры по сети и сервер для своего кооператива': 'As bibliotecas do jogo online e o servidor para o seu próprio cooperativo',
'Основа поддержки DLSS': 'A base do suporte a DLSS',
'Основа поддержки DLSS и модель FSR 4 без шлейфов за персонажами': 'A base do suporte a DLSS e o modelo do FSR 4 sem rastros atrás dos personagens',
'Апскейлер FSR 3.1': 'O upscaler FSR 3.1',
'Игра запускается, когда DLSS нельзя использовать': 'O jogo inicia quando o DLSS não pode ser usado',
'Перевод на упрощённый китайский': 'A tradução para chinês simplificado',
@@ -1228,6 +1245,23 @@ PT_BR = {
'Рендерер и компилятор шейдеров': 'O renderizador e o recompilador de shaders',
'Инструмент для сборки AppImage': 'A ferramenta que gera a AppImage',
'Нашёл ошибку, из-за которой игра не запускалась на некоторых видеокартах': 'Encontrou o erro que impedia o jogo de iniciar em algumas placas de vídeo',
# Shader cache
'Кэш шейдеров': 'Cache de shaders',
'Очистить': 'Limpar',
'{}. Если после обновления картинка выглядит странно, очистите его: шейдеры соберутся заново (первые минуты в каждой зоне возможны рывки)': '{}. Se a imagem parecer estranha após uma atualização, limpe-o: os shaders são gerados de novo (pode haver travadas nos primeiros minutos de cada área)',
'Пусто: шейдеры соберутся при игре': 'Vazio: os shaders são gerados enquanto você joga',
'Очистить кэш шейдеров?': 'Limpar o cache de shaders?',
'Игра соберёт шейдеры заново: первые минуты в каждой зоне возможны рывки. Сохранения и настройки не затрагиваются.': 'O jogo gera os shaders de novo: pode haver travadas nos primeiros minutos de cada área. Saves e configurações não são afetados.',
'Названия трофеев на английском без ключа и звук при получении трофея': 'Nomes dos troféus em inglês sem a chave e o som ao conquistar um',
'Буфер трафарета, в который писали цветные изображения': 'O buffer de estêncil em que imagens de cor escreviam',
'Вход на публичный сервер shadNet без отключения': 'Continuar conectado no servidor público do shadNet',
'Игра по сети': 'Jogo online',
'Сервер shadNet не принимает bbport, игра осталась офлайн. Для игры по сети используйте свой сервер (страница «Онлайн»).': 'O servidor shadNet não aceita o bbport, então o jogo ficou offline. Para jogar online, use seu próprio servidor (página Online).',
'Выход из игры из меню с контроллера и меню в пределах экрана': 'Sair do jogo pelo menu com o controle e o menu dentro da tela',
'Вылет видеоплеера при его остановке': 'Um travamento do reprodutor de vídeo ao parar',
'Язык внутриигрового меню при запуске без окна лаунчера': 'O idioma do menu do jogo ao iniciar sem a janela do launcher',
'DLSS в AppImage на NVIDIA': 'DLSS no AppImage em NVIDIA',
'Сверхширокие мониторы без растянутой картинки': 'Monitores ultrawide sem imagem esticada',
}
@@ -1622,8 +1656,8 @@ ES = {
"Port para Windows, de Supermedo",
"Лицензия":
"Licencia",
"GNU GPL v2 или новее. Файлов игры нет: Bloodborne принадлежит Sony Interactive Entertainment и FromSoftware":
"GNU GPL v2 o posterior. No incluye archivos del juego: Bloodborne pertenece a Sony Interactive Entertainment y FromSoftware",
'GNU GPL v2 или новее · Файлы игры не входят.': 'GNU GPL v2 o posterior · No incluye archivos del juego.',
'Bloodborne принадлежит Sony Interactive Entertainment и FromSoftware.': 'Bloodborne pertenece a Sony Interactive Entertainment y FromSoftware.',
"Проверить сейчас":
"Comprobar ahora",
# 0.51: mouse look, choosing NVIDIA's DLSS library, the online module
@@ -1714,8 +1748,8 @@ ES = {
'Se guardan cuando el juego los desbloquea, y se muestran en el juego',
'Ключ трофеев':
'Clave de trofeos',
'Ключ трофеев PS4 (TrophyKey в shadPS4), 32 шестнадцатеричных символа: с ним видны названия и описания. Хранится на этом компьютере. Без него видны только значки':
'La clave de trofeos de PS4 (TrophyKey en shadPS4), 32 dígitos hexadecimales: con ella se ven los nombres y las descripciones. Se guarda en este equipo. Sin ella solo se ven los iconos',
'Ключ трофеев PS4 (TrophyKey в shadPS4), 32 шестнадцатеричных символа: с ним названия и описания на языке игры. Хранится на этом компьютере. Без него они на английском':
'La clave de trofeos de PS4 (TrophyKey en shadPS4), 32 dígitos hexadecimales: con ella, los nombres y las descripciones salen en el idioma del juego. Se guarda en este equipo. Sin ella salen en inglés',
'Неверный ключ трофеев: названия недоступны':
'Clave de trofeos incorrecta: los nombres no están disponibles',
'Получен {}':
@@ -1755,17 +1789,17 @@ ES = {
'Помощь и тесты': 'Ayuda y pruebas',
'Другие благодарности': 'También gracias a',
'bbport — порт, на котором построен весь этот проект': 'bbport, el port en el que se basa todo este proyecto',
'Игра по сети, DLSS, страница читов, добавление в Steam и значок': 'Juego online, DLSS, la página de trucos, Añadir a Steam y el icono',
'Игра по сети, DLSS, страница читов, добавление в Steam, значок и пропуск конвейеров, которые драйвер не может создать': 'Juego online, DLSS, la página de trucos, Añadir a Steam, el icono y saltar los pipelines que el driver no puede crear',
'Собрал и адаптировал десятки исправлений: вылеты при загрузке, чёрные изображения, апскейлер, строка производительности в журнале': 'Reunió y adaptó decenas de arreglos: cierres al cargar, imágenes en negro, el escalado, la línea de rendimiento del registro',
'Чёрный предпросмотр персонажа, картинка экрана загрузки и память кэша текстур': 'La vista previa del personaje en negro, la imagen de la pantalla de carga y la memoria de la caché de texturas',
'Кэш текстур вовремя освобождает видеопамять': 'Que la caché de texturas libere la memoria de vídeo a tiempo',
'Вылеты при загрузке областей': 'Cierres al cargar zonas',
'Интерфейс, пропадавший не в 1080p, векторы движения больших чудовищ и ошибка памяти': 'La interfaz que desaparecía fuera de 1080p, los vectores de movimiento de las bestias grandes y un fallo de memoria',
'Вылеты при загрузке некоторых карт': 'Cierres al cargar algunos mapas',
'Вылеты при загрузке некоторых карт и меню жестов с клавиатуры': 'Cierres al cargar algunos mapas y el menú de gestos desde el teclado',
'Экранная клавиатура для имени персонажа и уведомление о компиляции шейдеров': 'El teclado en pantalla para el nombre del personaje y el aviso de compilación de shaders',
"Быстрая загрузка после смерти и перемещения (bbhost) и сервер The Hunter's Dream": "Cargas rápidas tras morir o viajar (bbhost) y el servidor The Hunter's Dream",
'Сетевые библиотеки игры по сети и сервер для своего кооператива': 'Las librerías del juego online y el servidor para el cooperativo propio',
'Основа поддержки DLSS': 'La base del soporte de DLSS',
'Основа поддержки DLSS и модель FSR 4 без шлейфов за персонажами': 'La base del soporte de DLSS y el modelo de FSR 4 sin estelas tras los personajes',
'Апскейлер FSR 3.1': 'El escalado FSR 3.1',
'Игра запускается, когда DLSS нельзя использовать': 'Que el juego arranque cuando no se puede usar DLSS',
'Перевод на упрощённый китайский': 'La traducción al chino simplificado',
@@ -1780,6 +1814,23 @@ ES = {
'Рендерер и компилятор шейдеров': 'El renderizador y el recompilador de shaders',
'Инструмент для сборки AppImage': 'La herramienta que crea la AppImage',
'Нашёл ошибку, из-за которой игра не запускалась на некоторых видеокартах': 'Encontró el fallo que impedía arrancar el juego en algunas gráficas',
# Shader cache
'Кэш шейдеров': 'Caché de shaders',
'Очистить': 'Borrar',
'{}. Если после обновления картинка выглядит странно, очистите его: шейдеры соберутся заново (первые минуты в каждой зоне возможны рывки)': '{}. Si la imagen se ve rara tras una actualización, bórrala: los shaders se vuelven a generar (puede haber tirones los primeros minutos en cada zona)',
'Пусто: шейдеры соберутся при игре': 'Vacía: los shaders se generan mientras juegas',
'Очистить кэш шейдеров?': '¿Borrar la caché de shaders?',
'Игра соберёт шейдеры заново: первые минуты в каждой зоне возможны рывки. Сохранения и настройки не затрагиваются.': 'El juego volverá a generar los shaders: puede haber tirones los primeros minutos en cada zona. Las partidas y los ajustes no se tocan.',
'Названия трофеев на английском без ключа и звук при получении трофея': 'Los nombres de los trofeos en inglés sin la clave y el sonido al conseguir uno',
'Буфер трафарета, в который писали цветные изображения': 'El búfer de estarcido en el que escribían imágenes de color',
'Вход на публичный сервер shadNet без отключения': 'Seguir conectado en el servidor público de shadNet',
'Игра по сети': 'Juego en línea',
'Сервер shadNet не принимает bbport, игра осталась офлайн. Для игры по сети используйте свой сервер (страница «Онлайн»).': 'El servidor shadNet no acepta bbport, así que el juego se ha quedado sin conexión. Para jugar en línea, usa tu propio servidor (página Online).',
'Выход из игры из меню с контроллера и меню в пределах экрана': 'Salir del juego desde el menú con el mando y el menú dentro de la pantalla',
'Вылет видеоплеера при его остановке': 'Un cierre del reproductor de vídeo al detenerse',
'Язык внутриигрового меню при запуске без окна лаунчера': 'El idioma del menú del juego al arrancar sin la ventana del launcher',
'DLSS в AppImage на NVIDIA': 'DLSS en la AppImage con NVIDIA',
'Сверхширокие мониторы без растянутой картинки': 'Monitores ultrapanorámicos sin la imagen estirada',
}
@@ -2174,8 +2225,8 @@ IT = {
"Port per Windows di Supermedo",
"Лицензия":
"Licenza",
"GNU GPL v2 или новее. Файлов игры нет: Bloodborne принадлежит Sony Interactive Entertainment и FromSoftware":
"GNU GPL v2 o successiva. Nessun file del gioco incluso: Bloodborne appartiene a Sony Interactive Entertainment e FromSoftware",
'GNU GPL v2 или новее · Файлы игры не входят.': 'GNU GPL v2 o successiva · Nessun file del gioco incluso.',
'Bloodborne принадлежит Sony Interactive Entertainment и FromSoftware.': 'Bloodborne appartiene a Sony Interactive Entertainment e FromSoftware.',
"Проверить сейчас":
"Controlla ora",
# 0.51: mouse look, choosing NVIDIA's DLSS library, the online module
@@ -2266,8 +2317,8 @@ IT = {
'Registrati quando il gioco li sblocca, e mostrati nel gioco',
'Ключ трофеев':
'Chiave dei trofei',
'Ключ трофеев PS4 (TrophyKey в shadPS4), 32 шестнадцатеричных символа: с ним видны названия и описания. Хранится на этом компьютере. Без него видны только значки':
'La chiave dei trofei PS4 (TrophyKey in shadPS4), 32 cifre esadecimali: con essa si vedono nomi e descrizioni. Resta su questo computer. Senza, si vedono solo le icone',
'Ключ трофеев PS4 (TrophyKey в shadPS4), 32 шестнадцатеричных символа: с ним названия и описания на языке игры. Хранится на этом компьютере. Без него они на английском':
'La chiave dei trofei PS4 (TrophyKey in shadPS4), 32 cifre esadecimali: con essa nomi e descrizioni sono nella lingua del gioco. Resta su questo computer. Senza, sono in inglese',
'Неверный ключ трофеев: названия недоступны':
'Chiave dei trofei errata: i nomi non sono disponibili',
'Получен {}':
@@ -2307,17 +2358,17 @@ IT = {
'Помощь и тесты': 'Aiuto e test',
'Другие благодарности': 'Grazie anche a',
'bbport — порт, на котором построен весь этот проект': 'bbport, il port su cui si basa tutto questo progetto',
'Игра по сети, DLSS, страница читов, добавление в Steam и значок': 'Gioco online, DLSS, la pagina dei trucchi, Aggiungi a Steam e l’icona',
'Игра по сети, DLSS, страница читов, добавление в Steam, значок и пропуск конвейеров, которые драйвер не может создать': 'Gioco online, DLSS, la pagina dei trucchi, Aggiungi a Steam, l’icona e saltare le pipeline che il driver non riesce a creare',
'Собрал и адаптировал десятки исправлений: вылеты при загрузке, чёрные изображения, апскейлер, строка производительности в журнале': 'Ha raccolto e adattato decine di correzioni: crash durante il caricamento, immagini nere, l’upscaler, la riga delle prestazioni nel log',
'Чёрный предпросмотр персонажа, картинка экрана загрузки и память кэша текстур': 'L’anteprima nera del personaggio, l’immagine della schermata di caricamento e la memoria della cache delle texture',
'Кэш текстур вовремя освобождает видеопамять': 'La cache delle texture libera la memoria video in tempo',
'Вылеты при загрузке областей': 'Crash durante il caricamento delle aree',
'Интерфейс, пропадавший не в 1080p, векторы движения больших чудовищ и ошибка памяти': 'L’interfaccia che spariva fuori da 1080p, i vettori di movimento delle grandi bestie e un errore di memoria',
'Вылеты при загрузке некоторых карт': 'Crash al caricamento di alcune mappe',
'Вылеты при загрузке некоторых карт и меню жестов с клавиатуры': 'Crash al caricamento di alcune mappe e il menu dei gesti da tastiera',
'Экранная клавиатура для имени персонажа и уведомление о компиляции шейдеров': 'La tastiera su schermo per il nome del personaggio e l’avviso di compilazione degli shader',
"Быстрая загрузка после смерти и перемещения (bbhost) и сервер The Hunter's Dream": "Caricamenti rapidi dopo una morte o un viaggio (bbhost) e il server The Hunter's Dream",
'Сетевые библиотеки игры по сети и сервер для своего кооператива': 'Le librerie del gioco online e il server per la propria cooperativa',
'Основа поддержки DLSS': 'La base del supporto DLSS',
'Основа поддержки DLSS и модель FSR 4 без шлейфов за персонажами': 'La base del supporto DLSS e il modello FSR 4 senza scie dietro i personaggi',
'Апскейлер FSR 3.1': 'L’upscaler FSR 3.1',
'Игра запускается, когда DLSS нельзя использовать': 'Il gioco si avvia quando DLSS non può essere usato',
'Перевод на упрощённый китайский': 'La traduzione in cinese semplificato',
@@ -2332,6 +2383,23 @@ IT = {
'Рендерер и компилятор шейдеров': 'Il renderer e il ricompilatore degli shader',
'Инструмент для сборки AppImage': 'Lo strumento che crea l’AppImage',
'Нашёл ошибку, из-за которой игра не запускалась на некоторых видеокартах': 'Ha trovato il bug che impediva l’avvio del gioco su alcune schede video',
# Shader cache
'Кэш шейдеров': 'Cache degli shader',
'Очистить': 'Svuota',
'{}. Если после обновления картинка выглядит странно, очистите его: шейдеры соберутся заново (первые минуты в каждой зоне возможны рывки)': '{}. Se l’immagine sembra strana dopo un aggiornamento, svuotala: gli shader vengono ricreati (possibili scatti nei primi minuti di ogni area)',
'Пусто: шейдеры соберутся при игре': 'Vuota: gli shader vengono creati mentre giochi',
'Очистить кэш шейдеров?': 'Svuotare la cache degli shader?',
'Игра соберёт шейдеры заново: первые минуты в каждой зоне возможны рывки. Сохранения и настройки не затрагиваются.': 'Il gioco ricrea gli shader: possibili scatti nei primi minuti di ogni area. Salvataggi e impostazioni non vengono toccati.',
'Названия трофеев на английском без ключа и звук при получении трофея': 'I nomi dei trofei in inglese senza la chiave e il suono quando se ne ottiene uno',
'Буфер трафарета, в который писали цветные изображения': 'Lo stencil buffer in cui scrivevano immagini a colori',
'Вход на публичный сервер shadNet без отключения': 'Restare connessi al server pubblico di shadNet',
'Игра по сети': 'Gioco online',
'Сервер shadNet не принимает bbport, игра осталась офлайн. Для игры по сети используйте свой сервер (страница «Онлайн»).': 'Il server shadNet non accetta bbport, quindi il gioco è rimasto offline. Per giocare online usa un tuo server (pagina Online).',
'Выход из игры из меню с контроллера и меню в пределах экрана': 'Uscire dal gioco dal menu con il controller e il menu entro lo schermo',
'Вылет видеоплеера при его остановке': 'Un crash del lettore video quando si ferma',
'Язык внутриигрового меню при запуске без окна лаунчера': 'La lingua del menu di gioco avviando senza la finestra del launcher',
'DLSS в AppImage на NVIDIA': "DLSS nell'AppImage su NVIDIA",
'Сверхширокие мониторы без растянутой картинки': 'Monitor ultrawide senza immagine stirata',
}
@@ -2726,8 +2794,8 @@ FR = {
"Portage Windows de Supermedo",
"Лицензия":
"Licence",
"GNU GPL v2 или новее. Файлов игры нет: Bloodborne принадлежит Sony Interactive Entertainment и FromSoftware":
"GNU GPL v2 ou ultérieure. Aucun fichier du jeu inclus : Bloodborne appartient à Sony Interactive Entertainment et FromSoftware",
'GNU GPL v2 или новее · Файлы игры не входят.': 'GNU GPL v2 ou ultérieure · Aucun fichier du jeu inclus.',
'Bloodborne принадлежит Sony Interactive Entertainment и FromSoftware.': 'Bloodborne appartient à Sony Interactive Entertainment et FromSoftware.',
"Проверить сейчас":
"Vérifier maintenant",
# 0.51: mouse look, choosing NVIDIA's DLSS library, the online module
@@ -2818,8 +2886,8 @@ FR = {
'Enregistrés quand le jeu les débloque, et affichés dans le jeu',
'Ключ трофеев':
'Clé des trophées',
'Ключ трофеев PS4 (TrophyKey в shadPS4), 32 шестнадцатеричных символа: с ним видны названия и описания. Хранится на этом компьютере. Без него видны только значки':
'La clé des trophées PS4 (TrophyKey dans shadPS4), 32 chiffres hexadécimaux : avec elle, les noms et descriptions s’affichent. Conservée sur cet ordinateur. Sans elle, seules les icônes s’affichent',
'Ключ трофеев PS4 (TrophyKey в shadPS4), 32 шестнадцатеричных символа: с ним названия и описания на языке игры. Хранится на этом компьютере. Без него они на английском':
'La clé des trophées PS4 (TrophyKey dans shadPS4), 32 chiffres hexadécimaux : avec elle, les noms et descriptions sont dans la langue du jeu. Conservée sur cet ordinateur. Sans elle, ils sont en anglais',
'Неверный ключ трофеев: названия недоступны':
'Clé des trophées incorrecte : les noms ne sont pas disponibles',
'Получен {}':
@@ -2859,17 +2927,17 @@ FR = {
'Помощь и тесты': 'Aide et tests',
'Другие благодарности': 'Merci aussi à',
'bbport — порт, на котором построен весь этот проект': 'bbport, le port sur lequel repose tout ce projet',
'Игра по сети, DLSS, страница читов, добавление в Steam и значок': 'Jeu en ligne, DLSS, la page des triches, Ajouter à Steam et l’icône',
'Игра по сети, DLSS, страница читов, добавление в Steam, значок и пропуск конвейеров, которые драйвер не может создать': 'Jeu en ligne, DLSS, la page des triches, Ajouter à Steam, l’icône et ignorer les pipelines que le pilote ne peut pas créer',
'Собрал и адаптировал десятки исправлений: вылеты при загрузке, чёрные изображения, апскейлер, строка производительности в журнале': 'A rassemblé et adapté des dizaines de correctifs : plantages au chargement, images noires, l’upscaler, la ligne de performances du journal',
'Чёрный предпросмотр персонажа, картинка экрана загрузки и память кэша текстур': 'L’aperçu noir du personnage, l’image de l’écran de chargement et la mémoire du cache de textures',
'Кэш текстур вовремя освобождает видеопамять': 'Le cache de textures libère la mémoire vidéo à temps',
'Вылеты при загрузке областей': 'Plantages au chargement des zones',
'Интерфейс, пропадавший не в 1080p, векторы движения больших чудовищ и ошибка памяти': 'L’interface qui disparaissait hors 1080p, les vecteurs de mouvement des grandes bêtes et une erreur mémoire',
'Вылеты при загрузке некоторых карт': 'Plantages au chargement de certaines cartes',
'Вылеты при загрузке некоторых карт и меню жестов с клавиатуры': 'Plantages au chargement de certaines cartes et le menu des gestes au clavier',
'Экранная клавиатура для имени персонажа и уведомление о компиляции шейдеров': 'Le clavier à l’écran pour le nom du personnage et l’avis de compilation des shaders',
"Быстрая загрузка после смерти и перемещения (bbhost) и сервер The Hunter's Dream": "Chargements rapides après une mort ou un voyage (bbhost) et le serveur The Hunter's Dream",
'Сетевые библиотеки игры по сети и сервер для своего кооператива': 'Les bibliothèques du jeu en ligne et le serveur pour votre propre coopération',
'Основа поддержки DLSS': 'La base de la prise en charge de DLSS',
'Основа поддержки DLSS и модель FSR 4 без шлейфов за персонажами': 'La base de la prise en charge de DLSS et le modèle FSR 4 sans traînées derrière les personnages',
'Апскейлер FSR 3.1': 'L’upscaler FSR 3.1',
'Игра запускается, когда DLSS нельзя использовать': 'Le jeu démarre quand DLSS ne peut pas être utilisé',
'Перевод на упрощённый китайский': 'La traduction en chinois simplifié',
@@ -2884,6 +2952,23 @@ FR = {
'Рендерер и компилятор шейдеров': 'Le moteur de rendu et le recompilateur de shaders',
'Инструмент для сборки AppImage': 'L’outil qui crée l’AppImage',
'Нашёл ошибку, из-за которой игра не запускалась на некоторых видеокартах': 'A trouvé le bug qui empêchait le jeu de démarrer sur certaines cartes graphiques',
# Shader cache
'Кэш шейдеров': 'Cache des shaders',
'Очистить': 'Vider',
'{}. Если после обновления картинка выглядит странно, очистите его: шейдеры соберутся заново (первые минуты в каждой зоне возможны рывки)': '{}. Si l’image semble étrange après une mise à jour, videz-le : les shaders sont reconstruits (quelques saccades les premières minutes dans chaque zone)',
'Пусто: шейдеры соберутся при игре': 'Vide : les shaders sont construits pendant que vous jouez',
'Очистить кэш шейдеров?': 'Vider le cache des shaders ?',
'Игра соберёт шейдеры заново: первые минуты в каждой зоне возможны рывки. Сохранения и настройки не затрагиваются.': 'Le jeu reconstruit les shaders : quelques saccades les premières minutes dans chaque zone. Les sauvegardes et les réglages ne sont pas touchés.',
'Названия трофеев на английском без ключа и звук при получении трофея': 'Les noms des trophées en anglais sans la clé et le son à l’obtention d’un trophée',
'Буфер трафарета, в который писали цветные изображения': 'Le tampon de stencil dans lequel écrivaient des images couleur',
'Вход на публичный сервер shadNet без отключения': 'Rester connecté au serveur public shadNet',
'Игра по сети': 'Jeu en ligne',
'Сервер shadNet не принимает bbport, игра осталась офлайн. Для игры по сети используйте свой сервер (страница «Онлайн»).': 'Le serveur shadNet n’accepte pas bbport, le jeu est donc resté hors ligne. Pour jouer en ligne, utilisez votre propre serveur (page En ligne).',
'Выход из игры из меню с контроллера и меню в пределах экрана': 'Quitter le jeu depuis le menu à la manette et le menu qui tient dans l’écran',
'Вылет видеоплеера при его остановке': 'Un plantage du lecteur vidéo à son arrêt',
'Язык внутриигрового меню при запуске без окна лаунчера': 'La langue du menu en jeu au lancement sans la fenêtre du launcher',
'DLSS в AppImage на NVIDIA': 'DLSS dans l’AppImage sur NVIDIA',
'Сверхширокие мониторы без растянутой картинки': 'Les écrans ultra-larges sans image étirée',
}
@@ -3278,8 +3363,8 @@ DE = {
"Windows-Port von Supermedo",
"Лицензия":
"Lizenz",
"GNU GPL v2 или новее. Файлов игры нет: Bloodborne принадлежит Sony Interactive Entertainment и FromSoftware":
"GNU GPL v2 oder neuer. Keine Spieldateien enthalten: Bloodborne gehört Sony Interactive Entertainment und FromSoftware",
'GNU GPL v2 или новее · Файлы игры не входят.': 'GNU GPL v2 oder neuer · Keine Spieldateien enthalten.',
'Bloodborne принадлежит Sony Interactive Entertainment и FromSoftware.': 'Bloodborne gehört Sony Interactive Entertainment und FromSoftware.',
"Проверить сейчас":
"Jetzt prüfen",
# 0.51: mouse look, choosing NVIDIA's DLSS library, the online module
@@ -3370,8 +3455,8 @@ DE = {
'Werden gespeichert, wenn das Spiel sie freischaltet, und im Spiel angezeigt',
'Ключ трофеев':
'Trophäen-Schlüssel',
'Ключ трофеев PS4 (TrophyKey в shadPS4), 32 шестнадцатеричных символа: с ним видны названия и описания. Хранится на этом компьютере. Без него видны только значки':
'Der Trophäen-Schlüssel der PS4 (TrophyKey in shadPS4), 32 Hexadezimalziffern: damit erscheinen Namen und Beschreibungen. Bleibt auf diesem Computer. Ohne ihn erscheinen nur die Symbole',
'Ключ трофеев PS4 (TrophyKey в shadPS4), 32 шестнадцатеричных символа: с ним названия и описания на языке игры. Хранится на этом компьютере. Без него они на английском':
'Der Trophäen-Schlüssel der PS4 (TrophyKey in shadPS4), 32 Hexadezimalziffern: damit sind Namen und Beschreibungen in der Sprache des Spiels. Bleibt auf diesem Computer. Ohne ihn sind sie auf Englisch',
'Неверный ключ трофеев: названия недоступны':
'Falscher Trophäen-Schlüssel: die Namen sind nicht verfügbar',
'Получен {}':
@@ -3411,17 +3496,17 @@ DE = {
'Помощь и тесты': 'Hilfe und Tests',
'Другие благодарности': 'Dank auch an',
'bbport — порт, на котором построен весь этот проект': 'bbport, der Port, auf dem dieses ganze Projekt aufbaut',
'Игра по сети, DLSS, страница читов, добавление в Steam и значок': 'Online-Spiel, DLSS, die Cheat-Seite, Zu Steam hinzufügen und das Symbol',
'Игра по сети, DLSS, страница читов, добавление в Steam, значок и пропуск конвейеров, которые драйвер не может создать': 'Online-Spiel, DLSS, die Cheat-Seite, Zu Steam hinzufügen, das Symbol und das Überspringen von Pipelines, die der Treiber nicht erstellen kann',
'Собрал и адаптировал десятки исправлений: вылеты при загрузке, чёрные изображения, апскейлер, строка производительности в журнале': 'Hat Dutzende Korrekturen gesammelt und angepasst: Abstürze beim Laden, schwarze Bilder, der Upscaler, die Leistungszeile im Log',
'Чёрный предпросмотр персонажа, картинка экрана загрузки и память кэша текстур': 'Die schwarze Charaktervorschau, das Bild des Ladebildschirms und der Speicher des Texturcaches',
'Кэш текстур вовремя освобождает видеопамять': 'Der Texturcache gibt Videospeicher rechtzeitig frei',
'Вылеты при загрузке областей': 'Abstürze beim Laden von Gebieten',
'Интерфейс, пропадавший не в 1080p, векторы движения больших чудовищ и ошибка памяти': 'Die Oberfläche, die außerhalb von 1080p verschwand, Bewegungsvektoren der großen Bestien und ein Speicherfehler',
'Вылеты при загрузке некоторых карт': 'Abstürze beim Laden mancher Karten',
'Вылеты при загрузке некоторых карт и меню жестов с клавиатуры': 'Abstürze beim Laden mancher Karten und das Gestenmenü über die Tastatur',
'Экранная клавиатура для имени персонажа и уведомление о компиляции шейдеров': 'Die Bildschirmtastatur für den Charakternamen und der Hinweis zum Kompilieren der Shader',
"Быстрая загрузка после смерти и перемещения (bbhost) и сервер The Hunter's Dream": "Schnelles Laden nach Tod oder Reise (bbhost) und der Server The Hunter's Dream",
'Сетевые библиотеки игры по сети и сервер для своего кооператива': 'Die Bibliotheken für das Online-Spiel und der Server für den eigenen Koop',
'Основа поддержки DLSS': 'Die Grundlage für DLSS',
'Основа поддержки DLSS и модель FSR 4 без шлейфов за персонажами': 'Die Grundlage für DLSS und das FSR-4-Modell ohne Schlieren hinter Figuren',
'Апскейлер FSR 3.1': 'Der FSR-3.1-Upscaler',
'Игра запускается, когда DLSS нельзя использовать': 'Das Spiel startet, wenn DLSS nicht nutzbar ist',
'Перевод на упрощённый китайский': 'Die Übersetzung ins vereinfachte Chinesisch',
@@ -3436,6 +3521,23 @@ DE = {
'Рендерер и компилятор шейдеров': 'Der Renderer und der Shader-Recompiler',
'Инструмент для сборки AppImage': 'Das Werkzeug, das das AppImage baut',
'Нашёл ошибку, из-за которой игра не запускалась на некоторых видеокартах': 'Hat den Fehler gefunden, der den Spielstart auf manchen Grafikkarten verhinderte',
# Shader cache
'Кэш шейдеров': 'Shader-Cache',
'Очистить': 'Leeren',
'{}. Если после обновления картинка выглядит странно, очистите его: шейдеры соберутся заново (первые минуты в каждой зоне возможны рывки)': '{}. Wenn das Bild nach einem Update seltsam aussieht, leeren: Die Shader werden neu erstellt (in den ersten Minuten jedes Gebiets kann es ruckeln)',
'Пусто: шейдеры соберутся при игре': 'Leer: Die Shader werden beim Spielen erstellt',
'Очистить кэш шейдеров?': 'Shader-Cache leeren?',
'Игра соберёт шейдеры заново: первые минуты в каждой зоне возможны рывки. Сохранения и настройки не затрагиваются.': 'Das Spiel erstellt die Shader neu: In den ersten Minuten jedes Gebiets kann es ruckeln. Spielstände und Einstellungen bleiben unberührt.',
'Названия трофеев на английском без ключа и звук при получении трофея': 'Trophäennamen auf Englisch ohne Schlüssel und der Ton beim Erhalt einer Trophäe',
'Буфер трафарета, в который писали цветные изображения': 'Der Stencil-Puffer, in den Farbbilder schrieben',
'Вход на публичный сервер shadNet без отключения': 'Auf dem öffentlichen shadNet-Server angemeldet bleiben',
'Игра по сети': 'Online-Spiel',
'Сервер shadNet не принимает bbport, игра осталась офлайн. Для игры по сети используйте свой сервер (страница «Онлайн»).': 'Der shadNet-Server akzeptiert bbport nicht, daher blieb das Spiel offline. Für das Online-Spiel einen eigenen Server verwenden (Seite Online).',
'Выход из игры из меню с контроллера и меню в пределах экрана': 'Das Spiel im Menü mit dem Controller beenden und das Menü innerhalb des Bildschirms',
'Вылет видеоплеера при его остановке': 'Ein Absturz des Videoplayers beim Anhalten',
'Язык внутриигрового меню при запуске без окна лаунчера': 'Die Sprache des Spielmenüs beim Start ohne Launcher-Fenster',
'DLSS в AppImage на NVIDIA': 'DLSS im AppImage auf NVIDIA',
'Сверхширокие мониторы без растянутой картинки': 'Ultrawide-Monitore ohne gestrecktes Bild',
}
@@ -3790,8 +3892,8 @@ ZH_CN = {
"Supermedo 的 Windows 移植版",
"Лицензия":
"许可证",
"GNU GPL v2 или новее. Файлов игры нет: Bloodborne принадлежит Sony Interactive Entertainment и FromSoftware":
"GNU GPL v2 或更高版本。不含游戏文件:Bloodborne 归 Sony Interactive Entertainment 和 FromSoftware 所有",
'GNU GPL v2 или новее · Файлы игры не входят.': 'GNU GPL v2 或更高版本 · 不含游戏文件。',
'Bloodborne принадлежит Sony Interactive Entertainment и FromSoftware.': 'Bloodborne 归 Sony Interactive Entertainment 和 FromSoftware 所有。',
"Проверить сейчас":
"立即检查",
# Texts added after the upstream translation
@@ -3835,7 +3937,8 @@ ZH_CN = {
"Имя учётной записи shadNet, не email. Зарегистрируйтесь на shadnet.shadps4.net или спросите владельца частного сервера. Пароль хранится на этом компьютере вместе с настройками лаунчера": "shadNet 账号名,不是邮箱。请在 shadnet.shadps4.net 注册,或向私人服务器的主人索取。密码会和启动器设置一起保存在这台电脑上",
"Как запустить свой сервер": "如何搭建自己的服务器",
"Ключ трофеев": "奖杯密钥",
"Ключ трофеев PS4 (TrophyKey в shadPS4), 32 шестнадцатеричных символа: с ним видны названия и описания. Хранится на этом компьютере. Без него видны только значки": "PS4 的奖杯密钥(shadPS4 中的 TrophyKey),32 位十六进制数:填写后会显示奖杯名称和说明。仅保存在本机。未填写时只显示图标",
'Ключ трофеев PS4 (TrophyKey в shadPS4), 32 шестнадцатеричных символа: с ним названия и описания на языке игры. Хранится на этом компьютере. Без него они на английском':
'PS4 的奖杯密钥(shadPS4 中的 TrophyKey),32 位十六进制数:填写后奖杯名称和说明使用游戏语言。仅保存在本机。未填写时为英文',
"Копировать": "复制",
"Меню жестов без значков управления движением": "手势菜单不显示体感操作图标",
"Монитор": "显示器",
@@ -3905,17 +4008,17 @@ ZH_CN = {
'Помощь и тесты': '帮助与测试',
'Другие благодарности': '同样感谢',
'bbport — порт, на котором построен весь этот проект': 'bbport,整个项目所基于的移植',
'Игра по сети, DLSS, страница читов, добавление в Steam и значок': '联机、DLSS、作弊页面、添加到 Steam 与图标',
'Игра по сети, DLSS, страница читов, добавление в Steam, значок и пропуск конвейеров, которые драйвер не может создать': '联机、DLSS、作弊页面、添加到 Steam、图标,以及跳过驱动无法创建的管线',
'Собрал и адаптировал десятки исправлений: вылеты при загрузке, чёрные изображения, апскейлер, строка производительности в журнале': '收集并改编了数十项修复:加载时崩溃、黑色图像、超分辨率、日志中的性能行',
'Чёрный предпросмотр персонажа, картинка экрана загрузки и память кэша текстур': '黑色的角色预览、加载画面图片与纹理缓存的内存',
'Кэш текстур вовремя освобождает видеопамять': '纹理缓存及时释放显存',
'Вылеты при загрузке областей': '加载区域时的崩溃',
'Интерфейс, пропадавший не в 1080p, векторы движения больших чудовищ и ошибка памяти': '非 1080p 下消失的界面、大型野兽的运动矢量与一处内存错误',
'Вылеты при загрузке некоторых карт': '加载部分地图时的崩溃',
'Вылеты при загрузке некоторых карт и меню жестов с клавиатуры': '加载部分地图时的崩溃与键盘打开的手势菜单',
'Экранная клавиатура для имени персонажа и уведомление о компиляции шейдеров': '角色名的屏幕键盘与着色器编译提示',
"Быстрая загрузка после смерти и перемещения (bbhost) и сервер The Hunter's Dream": "死亡或传送后的快速加载(bbhost)与 The Hunter's Dream 服务器",
'Сетевые библиотеки игры по сети и сервер для своего кооператива': '联机库与自建合作服务器',
'Основа поддержки DLSS': 'DLSS 支持的基础',
'Основа поддержки DLSS и модель FSR 4 без шлейфов за персонажами': 'DLSS 支持的基础,以及角色身后无拖影的 FSR 4 模型',
'Апскейлер FSR 3.1': 'FSR 3.1 超分辨率',
'Игра запускается, когда DLSS нельзя использовать': '无法使用 DLSS 时游戏仍可启动',
'Перевод на упрощённый китайский': '简体中文翻译',
@@ -3930,6 +4033,23 @@ ZH_CN = {
'Рендерер и компилятор шейдеров': '渲染器与着色器重编译器',
'Инструмент для сборки AppImage': '构建 AppImage 的工具',
'Нашёл ошибку, из-за которой игра не запускалась на некоторых видеокартах': '发现了导致游戏在部分显卡上无法启动的错误',
# Shader cache
'Кэш шейдеров': '着色器缓存',
'Очистить': '清除',
'{}. Если после обновления картинка выглядит странно, очистите его: шейдеры соберутся заново (первые минуты в каждой зоне возможны рывки)': '{}。如果更新后画面异常,请清除:着色器会重新生成(每个区域的前几分钟可能卡顿)',
'Пусто: шейдеры соберутся при игре': '空:着色器会在游玩时生成',
'Очистить кэш шейдеров?': '清除着色器缓存?',
'Игра соберёт шейдеры заново: первые минуты в каждой зоне возможны рывки. Сохранения и настройки не затрагиваются.': '游戏会重新生成着色器:每个区域的前几分钟可能卡顿。存档和设置不受影响。',
'Названия трофеев на английском без ключа и звук при получении трофея': '无密钥时的英文奖杯名称与获得奖杯时的音效',
'Буфер трафарета, в который писали цветные изображения': '被颜色图像写入的模板缓冲区',
'Вход на публичный сервер shadNet без отключения': '在公共 shadNet 服务器上保持登录',
'Игра по сети': '联机游戏',
'Сервер shadNet не принимает bbport, игра осталась офлайн. Для игры по сети используйте свой сервер (страница «Онлайн»).': 'shadNet 服务器不接受 bbport,因此游戏保持离线。如需联机,请使用自己的服务器(“联机”页面)。',
'Выход из игры из меню с контроллера и меню в пределах экрана': '用手柄从菜单退出游戏,以及菜单不超出屏幕',
'Вылет видеоплеера при его остановке': '视频播放器停止时的崩溃',
'Язык внутриигрового меню при запуске без окна лаунчера': '不经启动器窗口启动时游戏内菜单的语言',
'DLSS в AppImage на NVIDIA': 'NVIDIA 上 AppImage 中的 DLSS',
'Сверхширокие мониторы без растянутой картинки': '超宽屏显示器画面不再被拉伸',
}
# Translations by language code; Russian is the key itself. Strings missing in a translation
+108 -17
View File
@@ -21,14 +21,19 @@ import time
from pathlib import Path
from bbport_assets import fsr411_problem
from bbport_i18n import language, set_language, system_game_language, tr
import bbport_cache
import bbport_steam
import bbport_update
import gi
# GTK's GPU renderers cut the top row of pixels off text at fractional display scales (115 %:
# the "I" of "Idioma" lost its serif); cairo draws it whole, and a settings window needs no GPU.
# The game draws with its own Vulkan. GSK_RENDERER set by the user still wins.
os.environ.setdefault("GSK_RENDERER", "cairo")
gi.require_version("Gtk", "4.0")
gi.require_version("Adw", "1")
from gi.repository import Adw, Gio, GLib, Gtk # noqa: E402
from gi.repository import Adw, Gdk, Gio, GLib, Gtk # noqa: E402
PORT_DIR = Path(__file__).resolve().parent.parent # native_probe (or the package's copy)
sys.path.insert(0, str(PORT_DIR / 'scripts'))
@@ -585,10 +590,22 @@ def _github(user, what):
return ("@" + user, what, "https://github.com/" + user)
def themed_icon(name):
"""The desktop icon theme's symbolic icon, or the launcher's own (launcher/icons/bbport-<name>.svg:
GitHub's, Steam's and Discord's marks) for a theme without it. Not GTK's own fallback: for
"steam-symbolic" it takes the installed app's colour icon."""
display = Gdk.Display.get_default()
if display and not Gtk.IconTheme.get_for_display(display).has_icon(name) and \
(PORT_DIR / "launcher" / "icons" / f"bbport-{name}.svg").is_file():
name = "bbport-" + name
return Gtk.Image(icon_name=name)
THANKS = (
("Порт и его код", (
_github("deadinside28", "bbport — порт, на котором построен весь этот проект"),
_github("Supermedo", "Игра по сети, DLSS, страница читов, добавление в Steam и значок"),
_github("Supermedo", "Игра по сети, DLSS, страница читов, добавление в Steam, значок и "
"пропуск конвейеров, которые драйвер не может создать"),
_github("0xCydral", "Собрал и адаптировал десятки исправлений: вылеты при загрузке, чёрные "
"изображения, апскейлер, строка производительности в журнале"),
_github("yumlevi", "Чёрный предпросмотр персонажа, картинка экрана загрузки и память кэша "
@@ -597,18 +614,26 @@ THANKS = (
_github("GoncaloLobo0", "Вылеты при загрузке областей"),
_github("bmy", "Интерфейс, пропадавший не в 1080p, векторы движения больших чудовищ и "
"ошибка памяти"),
_github("Mrsuss60", "Вылеты при загрузке некоторых карт"),
_github("Mrsuss60", "Вылеты при загрузке некоторых карт и меню жестов с клавиатуры"),
_github("SrShadowy", "Экранная клавиатура для имени персонажа и уведомление о компиляции "
"шейдеров"),
_github("droogie", "Быстрая загрузка после смерти и перемещения (bbhost) и сервер "
"The Hunter's Dream"),
_github("Wozzardman", "Сетевые библиотеки игры по сети и сервер для своего кооператива"),
_github("IFreemz", "Основа поддержки DLSS"),
_github("IFreemz", "Основа поддержки DLSS и модель FSR 4 без шлейфов за персонажами"),
_github("FireBurn", "Апскейлер FSR 3.1"),
_github("Obrikash", "Игра запускается, когда DLSS нельзя использовать"),
_github("Qiming-Liu", "Перевод на упрощённый китайский"),
_github("andrecabralgit", "Звук игры на полной громкости"),
_github("Jgamrot", "Флажки и ползунки внутриигрового меню"),
_github("GerasimosKan", "Названия трофеев на английском без ключа и звук при получении трофея"),
_github("artemkaVG", "Буфер трафарета, в который писали цветные изображения"),
_github("Vorshim92", "Вход на публичный сервер shadNet без отключения"),
_github("parsifal295", "Выход из игры из меню с контроллера и меню в пределах экрана"),
_github("emurphy04", "Вылет видеоплеера при его остановке"),
_github("AyrtonSanabio", "Язык внутриигрового меню при запуске без окна лаунчера"),
_github("Aryxa", "DLSS в AppImage на NVIDIA"),
_github("pmwork-personal", "Сверхширокие мониторы без растянутой картинки"),
)),
("Помощь и тесты", (
_github("willthehuman", "Подробные отчёты с журналами и проверка исправлений"),
@@ -650,7 +675,7 @@ class LauncherWindow(Adw.ApplicationWindow):
toolbar = Adw.ToolbarView()
header = Adw.HeaderBar()
self.stack = Adw.ViewStack()
switcher = Adw.ViewSwitcher(stack=self.stack, policy=Adw.ViewSwitcherPolicy.WIDE)
switcher = Adw.ViewSwitcher(stack=self.stack, policy=Adw.ViewSwitcherPolicy.NARROW)
header.set_title_widget(switcher)
self.launch_button = Gtk.Button()
self.launch_button.connect("clicked", self.on_launch)
@@ -698,6 +723,8 @@ class LauncherWindow(Adw.ApplicationWindow):
self.launch_button.set_sensitive(self.fsr411_build is None)
if hasattr(self, "fsr411_button"):
self.fsr411_button.set_sensitive(not running)
if hasattr(self, "cache_button"):
self.show_cache()
# --- settings page -------------------------------------------------------------------
@@ -705,7 +732,11 @@ class LauncherWindow(Adw.ApplicationWindow):
page = Adw.PreferencesPage()
launcher = Adw.PreferencesGroup()
self.ui_language_row = combo_row(tr("Язык лаунчера") + " / Launcher language", None,
# In English too, so that a language one cannot read still leads back here; once.
title = tr("Язык лаунчера")
if title != "Launcher language":
title += " / Launcher language"
self.ui_language_row = combo_row(title, None,
UI_LANGUAGES, self.settings.get("ui_language", ""))
self.ui_language_row.connect("notify::selected", self.on_ui_language)
launcher.add(self.ui_language_row)
@@ -756,11 +787,13 @@ class LauncherWindow(Adw.ApplicationWindow):
"этого лаунчера"))
steam_button = Gtk.Button(label=tr("Добавить"), valign=Gtk.Align.CENTER)
steam_button.connect("clicked", self.on_add_to_steam)
steam.add_prefix(themed_icon("steam-symbolic"))
steam.add_suffix(steam_button)
game.add(steam)
self.discord_row = Adw.SwitchRow(title=tr("Статус в Discord"),
subtitle=tr("«Играет в Bloodborne» в профиле Discord, онлайн или офлайн"),
active=self.settings.get("discord", True))
self.discord_row.add_prefix(themed_icon("discord-symbolic"))
game.add(self.discord_row)
page.add(game)
@@ -808,9 +841,13 @@ class LauncherWindow(Adw.ApplicationWindow):
page.add(self.patches_group)
screen = Adw.PreferencesGroup(title=tr("Экран"))
# A size written by hand in bbport.ini (5120x2160, ...) stays a choice instead of 720p.
output = self.ini.get("output_res", "1920x1080")
outputs = OUTPUT_RES + ([(output.replace("x", "×"), output)]
if output not in [value for _, value in OUTPUT_RES] else [])
self.output_row = combo_row(tr("Разрешение вывода"),
tr("Апскейлер дорисовывает кадр; Steam Deck — 720p"),
OUTPUT_RES, self.ini.get("output_res", "1920x1080"))
outputs, output)
screen.add(self.output_row)
self.live_row = combo_row(tr("Смена разрешения на лету"),
tr("Без перезапуска, но медленнее на Steam Deck и старых GPU"),
@@ -1001,6 +1038,12 @@ class LauncherWindow(Adw.ApplicationWindow):
tr("Меньше рывков при подгрузке зон"), PREUPLOAD,
self.settings.get("preupload", ""))
perf.add(self.preupload_row)
self.cache_row = Adw.ActionRow(title=tr("Кэш шейдеров"))
self.cache_button = Gtk.Button(label=tr("Очистить"), valign=Gtk.Align.CENTER)
self.cache_button.connect("clicked", self.on_clear_cache)
self.cache_row.add_suffix(self.cache_button)
perf.add(self.cache_row)
self.show_cache()
page.add(perf)
dev = Adw.PreferencesGroup(title=tr("Для разработчика"))
@@ -1059,6 +1102,31 @@ class LauncherWindow(Adw.ApplicationWindow):
def game_dir(self):
return Path(self.settings["game_dir"]).expanduser()
def show_cache(self):
"""The shader cache's size; it can be cleared while the game is not running."""
size = bbport_cache.size(self.user_dir())
self.cache_row.set_subtitle(
tr("{}. Если после обновления картинка выглядит странно, очистите его: шейдеры "
"соберутся заново (первые минуты в каждой зоне возможны рывки)").format(
GLib.format_size(size)) if size else tr("Пусто: шейдеры соберутся при игре"))
self.cache_button.set_sensitive(bool(size) and self.process is None)
def on_clear_cache(self, _button):
dialog = Adw.AlertDialog(heading=tr("Очистить кэш шейдеров?"), body=tr(
"Игра соберёт шейдеры заново: первые минуты в каждой зоне возможны рывки. Сохранения "
"и настройки не затрагиваются."))
dialog.add_response("cancel", tr("Отмена"))
dialog.add_response("clear", tr("Очистить"))
dialog.set_response_appearance("clear", Adw.ResponseAppearance.DESTRUCTIVE)
def answered(_dialog, response):
if response == "clear" and self.process is None:
bbport_cache.clear(self.user_dir())
self.show_cache()
dialog.connect("response", answered)
dialog.present(self)
def user_dir(self):
return Path(self.settings["user_dir"]).expanduser() if self.settings["user_dir"] \
else DATA_DIR / "user"
@@ -1073,6 +1141,8 @@ class LauncherWindow(Adw.ApplicationWindow):
self.user_status.set_tooltip_text(tr("Найдены сохранения") if found
else tr("Сохранений пока нет: игра создаст их здесь"))
self.user_reset.set_sensitive(custom)
if hasattr(self, "cache_row"):
self.show_cache()
def update_dlc_status(self):
path = self.settings.get("dlc_dir", "")
@@ -1754,16 +1824,17 @@ class LauncherWindow(Adw.ApplicationWindow):
subtitle=tr("Версия {}").format(bbport_update.VERSION))
app.add_prefix(Gtk.Image(file=str(PORT_DIR / "launcher" / "bloodborne.png"), pixel_size=48))
about.add(app)
for title, subtitle, url in (
(tr("Этот проект"), bbport_update.REPOSITORY_URL, bbport_update.REPOSITORY_URL),
for title, subtitle, url, icon in (
(tr("Этот проект"), bbport_update.REPOSITORY_URL, bbport_update.REPOSITORY_URL,
"github-symbolic"),
(tr("Основано на bbport от deadinside28"), bbport_update.UPSTREAM_URL,
bbport_update.UPSTREAM_URL),
bbport_update.UPSTREAM_URL, "github-symbolic"),
(tr("Порт для Windows от Supermedo"), bbport_update.WINDOWS_URL,
bbport_update.WINDOWS_URL),
(tr("Лицензия"), tr("GNU GPL v2 или новее. Файлов игры нет: Bloodborne принадлежит "
"Sony Interactive Entertainment и FromSoftware"),
bbport_update.LICENSE_URL)):
bbport_update.WINDOWS_URL, "github-symbolic"),
(tr("Лицензия"), tr("GNU GPL v2 или новее · Файлы игры не входят."),
bbport_update.LICENSE_URL, "application-certificate-symbolic")):
row = Adw.ActionRow(title=title, subtitle=subtitle, activatable=True)
row.add_prefix(themed_icon(icon))
row.add_suffix(Gtk.Image(icon_name="adw-external-link-symbolic"))
row.connect("activated", lambda _row, link=url: self.open_uri(link))
about.add(row)
@@ -1795,6 +1866,13 @@ class LauncherWindow(Adw.ApplicationWindow):
self.update_row.add_suffix(self.update_button)
updates.add(self.update_row)
page.add(updates)
# The game's owners, under everything, as plain text.
owners = Adw.PreferencesGroup()
owners.add(Gtk.Label(label=tr("Bloodborne принадлежит Sony Interactive Entertainment и "
"FromSoftware."),
wrap=True, justify=Gtk.Justification.CENTER,
css_classes=["dim-label", "caption"]))
page.add(owners)
return page
def show_thanks(self):
@@ -1820,7 +1898,7 @@ class LauncherWindow(Adw.ApplicationWindow):
def show_update_state(self, progress=None):
"""The update row and its button for the current state."""
latest, state = self.latest, self.update_state
newer = latest is not None and bbport_update.is_newer(latest["tag"])
newer = isinstance(latest, dict) and bbport_update.is_newer(latest["tag"])
appimage = bbport_update.running_appimage()
button, sensitive = tr("Проверить сейчас"), True
if state == "checking":
@@ -1928,8 +2006,8 @@ class LauncherWindow(Adw.ApplicationWindow):
def build_trophies_page(self):
page = Adw.PreferencesPage()
key = Adw.PreferencesGroup(title=tr("Ключ трофеев"), description=tr(
"Ключ трофеев PS4 (TrophyKey в shadPS4), 32 шестнадцатеричных символа: с ним видны "
"названия и описания. Хранится на этом компьютере. Без него видны только значки"))
"Ключ трофеев PS4 (TrophyKey в shadPS4), 32 шестнадцатеричных символа: с ним названия и "
"описания на языке игры. Хранится на этом компьютере. Без него они на английском"))
self.trophy_key_row = Adw.PasswordEntryRow(title=tr("Ключ трофеев"),
text=self.settings.get("trophy_key", ""),
show_apply_button=True)
@@ -2131,6 +2209,7 @@ class LauncherWindow(Adw.ApplicationWindow):
self.toasts.add_toast(Adw.Toast(title=tr("Не удалось запустить: {}").format(error.message)))
return
self.stream = Gio.DataInputStream.new(self.process.get_stdout_pipe())
self.server_refused = False
self.read_line()
self.process.wait_async(None, self.on_exit)
self.update_launch_button()
@@ -2147,6 +2226,12 @@ class LauncherWindow(Adw.ApplicationWindow):
if line is None:
return
self.append_log(line + "\n")
# The shadNet server refused this build (the game itself only says it is signed out).
if line.startswith("Online: the shadNet server ") and not self.server_refused:
self.server_refused = True
self.alert(tr("Игра по сети"), tr(
"Сервер shadNet не принимает bbport, игра осталась офлайн. Для игры по сети "
"используйте свой сервер (страница «Онлайн»)."))
self.read_line()
def stop_game(self):
@@ -2435,6 +2520,10 @@ class LauncherApp(Adw.Application):
flags=Gio.ApplicationFlags.DEFAULT_FLAGS)
def do_activate(self):
# The launcher's own icons (launcher/icons: GitHub's mark), symbolic like the theme's.
display = Gdk.Display.get_default()
if display:
Gtk.IconTheme.get_for_display(display).add_search_path(str(PORT_DIR / "launcher" / "icons"))
window = self.get_active_window() or LauncherWindow(self)
window.present()
@@ -2442,6 +2531,8 @@ class LauncherApp(Adw.Application):
def play():
"""--play: the game with the saved settings, no window (Steam Deck game mode)."""
settings = load_settings()
# No window sets the launcher's language here, and the in-game menu takes it (BB_MENU_LANGUAGE).
set_language(settings.get("ui_language", ""))
if not (Path(settings["game_dir"]).expanduser() / "eboot.bin").is_file():
print("bbport: choose the game folder in the launcher first", file=sys.stderr)
return 1
+1 -1
View File
@@ -13,7 +13,7 @@ import re
import tempfile
import urllib.request
VERSION = "0.51-linux.4"
VERSION = "0.51-linux.5"
REPOSITORY = "ayozetr/bloodborne_linux"
REPOSITORY_URL = f"https://github.com/{REPOSITORY}"
UPSTREAM_URL = "https://github.com/deadinside28/bloodborne_pc"
+3 -1
View File
@@ -91,8 +91,10 @@ def configure(env, manifest_dirs=MANIFEST_DIRS, library_dirs=LIBRARY_DIRS,
libraries.mkdir(parents=True, exist_ok=True)
# NVIDIA dlopens versioned shader/compiler libraries at runtime, so expose
# the driver's siblings as well as its ICD. Never link generic host libs.
# libcuda.so.1 is NVIDIA's too: with DLSS loaded, NGX makes the driver load it, and
# without it vkCreateDevice fails with VK_ERROR_INITIALIZATION_FAILED (#135).
sources = [driver]
for pattern in ("libnvidia-*.so*", "libGLX_nvidia.so*", "libEGL_nvidia.so*"):
for pattern in ("libnvidia-*.so*", "libGLX_nvidia.so*", "libEGL_nvidia.so*", "libcuda.so*"):
sources.extend(sorted(driver.parent.glob(pattern)))
for source in sources:
if not elf64(source):
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Discord's mark, from Simple Icons (simpleicons.org, CC0). -->
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24">
<path fill="#222222" d="M20.317 4.3698a19.7913 19.7913 0 00-4.8851-1.5152.0741.0741 0 00-.0785.0371c-.211.3753-.4447.8648-.6083 1.2495-1.8447-.2762-3.68-.2762-5.4868 0-.1636-.3933-.4058-.8742-.6177-1.2495a.077.077 0 00-.0785-.037 19.7363 19.7363 0 00-4.8852 1.515.0699.0699 0 00-.0321.0277C.5334 9.0458-.319 13.5799.0992 18.0578a.0824.0824 0 00.0312.0561c2.0528 1.5076 4.0413 2.4228 5.9929 3.0294a.0777.0777 0 00.0842-.0276c.4616-.6304.8731-1.2952 1.226-1.9942a.076.076 0 00-.0416-.1057c-.6528-.2476-1.2743-.5495-1.8722-.8923a.077.077 0 01-.0076-.1277c.1258-.0943.2517-.1923.3718-.2914a.0743.0743 0 01.0776-.0105c3.9278 1.7933 8.18 1.7933 12.0614 0a.0739.0739 0 01.0785.0095c.1202.099.246.1981.3728.2924a.077.077 0 01-.0066.1276 12.2986 12.2986 0 01-1.873.8914.0766.0766 0 00-.0407.1067c.3604.698.7719 1.3628 1.225 1.9932a.076.076 0 00.0842.0286c1.961-.6067 3.9495-1.5219 6.0023-3.0294a.077.077 0 00.0313-.0552c.5004-5.177-.8382-9.6739-3.5485-13.6604a.061.061 0 00-.0312-.0286zM8.02 15.3312c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9555-2.4189 2.157-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.9555 2.4189-2.1569 2.4189zm7.9748 0c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9554-2.4189 2.1569-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.946 2.4189-2.1568 2.4189Z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- GitHub's mark, from Simple Icons (simpleicons.org, CC0). -->
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24">
<path fill="#222222" d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"/>
</svg>

After

Width:  |  Height:  |  Size: 938 B

+5
View File
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Steam's mark, from Simple Icons (simpleicons.org, CC0). -->
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24">
<path fill="#222222" d="M11.979 0C5.678 0 .511 4.86.022 11.037l6.432 2.658c.545-.371 1.203-.59 1.912-.59.063 0 .125.004.188.006l2.861-4.142V8.91c0-2.495 2.028-4.524 4.524-4.524 2.494 0 4.524 2.031 4.524 4.527s-2.03 4.525-4.524 4.525h-.105l-4.076 2.911c0 .052.004.105.004.159 0 1.875-1.515 3.396-3.39 3.396-1.635 0-3.016-1.173-3.331-2.727L.436 15.27C1.862 20.307 6.486 24 11.979 24c6.627 0 11.999-5.373 11.999-12S18.605 0 11.979 0zM7.54 18.21l-1.473-.61c.262.543.714.999 1.314 1.25 1.297.539 2.793-.076 3.332-1.375.263-.63.264-1.319.005-1.949s-.75-1.121-1.377-1.383c-.624-.26-1.29-.249-1.878-.03l1.523.63c.956.4 1.409 1.5 1.009 2.455-.397.957-1.497 1.41-2.454 1.012H7.54zm11.415-9.303c0-1.662-1.353-3.015-3.015-3.015-1.665 0-3.015 1.353-3.015 3.015 0 1.665 1.35 3.015 3.015 3.015 1.663 0 3.015-1.35 3.015-3.015zm-5.273-.005c0-1.252 1.013-2.266 2.265-2.266 1.249 0 2.266 1.014 2.266 2.266 0 1.251-1.017 2.265-2.266 2.265-1.253 0-2.265-1.014-2.265-2.265z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

+43
View File
@@ -7,6 +7,7 @@ image places eboot vaddr 0 at image offset 0. Only literal writes are supported
import argparse
import json
import os
import re
import struct
import sys
import xml.etree.ElementTree as ET
@@ -170,6 +171,14 @@ SCENE_WIDTH=0x02196A6B-EBOOT_BASE
SCENE_HEIGHT=0x02196A7A-EBOOT_BASE
UI_WIDTH=0x02358554-EBOOT_BASE
UI_HEIGHT=0x0235855D-EBOOT_BASE
# Outputs wider than 16:9: the community resolution patch for that size (Kyo's "Resolution Patch
# WxH (21:9)" etc. in patches/Bloodborne.xml) also widens the game's logical screen -- the
# projection aspect, the 2D layout width and the lock-on/HP-bar coordinates -- so the view
# gets wider instead of stretched. bbport keeps its own scene/UI sizes (resolution_writes) and
# heap patch, so only that patch's remaining writes are applied. bbport.ini aspect_patch=0
# turns this off.
HEAP_SITES={0x04B36DDA-EBOOT_BASE,0x04B36DE2-EBOOT_BASE}
RESOLUTION_PATCH=re.compile(r'Resolution Patch (\d+)x(\d+) \(')
def read_settings(path):
@@ -242,6 +251,33 @@ def resolution_writes(xml,size,app_version,segments,ui=OUTPUT_SIZE):
return out
def aspect_patch_name(xml,app_version,output):
"""The community resolution patch for an output wider than 16:9: the one for this exact
size, else the closest-width one of the same aspect ratio. None for 16:9 and narrower
outputs, and for an aspect ratio no patch covers (that output is still stretched)."""
w,h=output
if w*9<=h*16: return None
same_aspect=[]
for meta in ET.parse(xml).getroot().iter('Metadata'):
m=RESOLUTION_PATCH.match(meta.get('Name',''))
if not m or meta.get('AppVer')!=app_version: continue
pw,ph=int(m[1]),int(m[2])
if pw*9<=ph*16: continue
if (pw,ph)==(w,h): return meta.get('Name')
if abs(pw/ph-w/h)<0.005: same_aspect.append((abs(pw-w),meta.get('Name')))
return min(same_aspect)[1] if same_aspect else None
def aspect_writes(xml,output,app_version,segments):
"""(patch name, writes) widening the game's view to the output's aspect, or (None, [])."""
name=aspect_patch_name(xml,app_version,output)
if not name: return None,[]
template=dict(compile_patches(xml,[RESOLUTION_TEMPLATE],app_version,segments))
skip={SCENE_WIDTH,SCENE_HEIGHT,UI_WIDTH,UI_HEIGHT}|HEAP_SITES
return name,[(offset,data) for offset,data in compile_patches(xml,[name],app_version,segments)
if offset not in skip and template.get(offset)!=data]
def eboot_segments(elf):
phoff,=struct.unpack_from('<Q',elf,0x20)
phentsize,phnum=struct.unpack_from('<HH',elf,0x36)
@@ -390,6 +426,13 @@ def main():
writes+=compile_patches(a.xml,[heap],a.app_version,segments)
names.append(heap)
print(f'Patches: scene {size[0]}x{size[1]}; UI {ui[0]}x{ui[1]}')
settings=read_settings(a.settings)
output=tuple(int(v) for v in a.output_res.lower().split('x')) if a.output_res else output_size(settings)
if settings.get('aspect_patch','1')!='0':
aspect_name,aspect=aspect_writes(a.xml,output,a.app_version,segments)
if aspect:
writes+=aspect
print(f'Patches: aspect {output[0]}x{output[1]} from {aspect_name!r} ({len(aspect)} writes)')
if a.patches_dir:
# After the built-in ones: an external patch of the same bytes wins.
writes+=compile_external(external_selection(external_patches(a.patches_dir,a.app_version,a.xml),
+57 -2
View File
@@ -5,7 +5,8 @@
The dump's sce_sys/trophy/trophy00.trp holds the icons (PNG) and the trophy lists (TROPCONF.ESFM,
TROP.ESFM and TROP_xx.ESFM per language). The lists are encrypted: AES-128-CBC with a key made by
encrypting the game's NP communication id (sce_sys/npbind.dat, NPWR05818_00) with the PS4's trophy
key, which the player supplies (shadPS4's TrophyKey). Without it, only the icons are known.
key, which the player supplies (shadPS4's TrophyKey). Without it, the icons, and for Bloodborne
PSN's English names (BLOODBORNE_ENGLISH).
`trophies.py prepare --game DIR --user DIR [--language N]` (key in BB_TROPHY_KEY) writes
DIR/trophies/<NP communication id>/: icons/ (the PNGs, and TROPxxx.rgba for the overlay's notice:
@@ -193,13 +194,67 @@ def parse_sfm(xml):
return (root.findtext('title-name') or '').strip(), trophies
# Bloodborne's trophies in English (PSN's list, ids 0-39; 34-39 are The Old Hunters') for a
# player without the trophy key: grade, hidden, name, detail. From GerasimosKan's list in
# deadinside28/bloodborne_pc#127.
BLOODBORNE = 'NPWR05818_00'
BLOODBORNE_ENGLISH = (
('P', False, 'Bloodborne', 'All trophies acquired. Hats off!'),
('G', True, 'Yharnam Sunrise', 'You lived through the hunt, and saw another day.'),
('G', True, 'Honoring Wishes', "Captivated by the Moon Presence, you pledge to watch over the Hunter's Dream."),
('G', True, "Childhood's Beginning", 'You became an infant Great One, lifting humanity into its next childhood.'),
('G', True, 'Yharnam, Pthumerian Queen', 'Defeat Yharnam, Blood Queen of the Old Labyrinth.'),
('G', False, "Hunter's Essence", 'Acquire all hunter weapons.'),
('G', False, "Hunter's Craft", 'Acquire all special hunter tools.'),
('S', False, 'Weapon Master', 'Acquire a weapon of the highest level.'),
('S', False, 'Blood Gem Master', 'Acquire an extremely precious blood gem.'),
('S', False, 'Rune Master', 'Acquire an extremely precious Caryll Rune.'),
('S', True, 'Cainhurst', 'Gain entry to Cainhurst, the lost and ruined castle.'),
('S', True, 'The Choir', 'Gain entry to the realm of the Choir, the high stratum of the Healing Church.'),
('S', True, 'The Source of the Dream', "Discover the abandoned old workshop, the source of the Hunter's Dream."),
('S', True, 'Nightmare Lecture Building', 'Gain entry into the Byrgenwerth lecture building, that drifts within the realm of nightmare.'),
('B', True, 'Father Gascoigne', 'Defeat the beast that once was Father Gascoigne.'),
('B', True, 'Vicar Amelia', 'Defeat the beast that once was Vicar Amelia.'),
('B', True, 'Shadow of Yharnam', 'Defeat the Shadow of Yharnam.'),
('B', True, 'Rom, the Vacuous Spider', 'Defeat Great One: Rom, the Vacuous Spider.'),
('B', True, 'The One Reborn', 'Defeat the One Reborn.'),
('B', True, 'Micolash, Host of the Nightmare', 'Defeat Micolash, Host of the Nightmare.'),
('B', True, "Mergo's Wet Nurse", "Defeat Great One: Mergo's Wet Nurse."),
('B', True, 'Cleric Beast', 'Defeat Cleric Beast.'),
('B', True, 'Blood-starved Beast', 'Defeat Blood-starved Beast.'),
('B', True, 'The Witch of Hemwick', 'Defeat the Witch of Hemwick.'),
('B', True, 'Darkbeast Paarl', 'Defeat Darkbeast Paarl.'),
('B', True, 'Amygdala', 'Defeat Great One: Amygdala.'),
('B', True, 'Martyr Logarius', 'Defeat Martyr Logarius.'),
('B', True, 'Celestial Emissary', 'Defeat Great One: Celestial Emissary.'),
('B', True, 'Ebrietas, Daughter of the Cosmos', 'Defeat Great One: Ebrietas, Daughter of the Cosmos.'),
('B', False, 'Blood Gem Contact', 'Acquire a blood gem that imbues hunter weapons with special strength.'),
('B', False, 'Rune Contact', 'Acquire a Caryll Rune that endows hunters with special strength.'),
('B', False, 'Chalice of Pthumeru', 'Acquire the Chalice of Pthumeru that seals the catacombs that form a web deep below Yharnam.'),
('B', True, 'Chalice of Ailing Loran', 'Acquire the Chalice of Ailing Loran that seals the tragic land lost to the sands.'),
('B', True, 'Chalice of Isz', 'Acquire the Great Chalice of Isz that seals the home of the cosmic kin.'),
('G', False, "Old Hunter's Essence", 'Acquire all old hunter weapons.'),
('S', True, 'Orphan of Kos', 'Defeat Great One: Orphan of Kos.'),
('B', True, 'Ludwig, the Holy Blade', 'Defeat the beast that was once Ludwig, the Holy Blade.'),
('B', True, 'Lady Maria of the Astral Clocktower', 'Defeat Lady Maria of the Astral Clocktower.'),
('B', True, 'Living Failures', 'Defeat the failed attempts to become Great Ones.'),
('B', True, 'Laurence, the First Vicar', 'Defeat the beast that was once Laurence, the First Vicar.'),
)
def trophy_list(entries, key=None, comm_id=None, language=None):
"""{id: {grade, hidden, name, detail, platinum}} from a TRP's entries: names in the game language
(TROP_xx, xx the PS4 language number) or the default list; without a key, the icons' ids."""
(TROP_xx, xx the PS4 language number) or the default list; without a key, the icons' ids
(Bloodborne's with PSN's English names)."""
icons = sorted(int(n[4:7]) for n in entries if re.fullmatch(r'TROP\d{3}\.PNG', n))
trophies = {i: {'grade': '-', 'hidden': False, 'name': '', 'detail': '', 'platinum': False}
for i in icons}
if key is None or comm_id is None:
if comm_id == BLOODBORNE: # without the key: PSN's English list, the platinum's 1-33
for i, (grade, hidden, name, detail) in enumerate(BLOODBORNE_ENGLISH):
if i in trophies:
trophies[i] = {'grade': grade, 'hidden': hidden, 'name': name,
'detail': detail, 'platinum': 1 <= i <= 33}
return trophies
_title, config = parse_sfm(decrypt_esfm(entries['TROPCONF.ESFM'], key, comm_id))
trophies.update(config)
+10 -1
View File
@@ -200,10 +200,18 @@ static uint8_t stick_axis(int axis,int16_t raw) {
static uint16_t touch_axis(float v, int max) {
return (uint16_t)(v<=0.0f ? 0 : v>=1.0f ? max : (int)(v*max+0.5f));
}
/* A touch as the pad reports one: a new id per press (1..127; 0 is not a valid one) and the
* time held since the first sample down. For the gestures (Tab) and personal effects (Backspace)
* menus. */
static uint8_t touch_id;
static uint64_t touch_down;
static void touch_click(PadData *d, int right) {
const uint64_t now=now_us();
if (!touch_down) { touch_down=now; touch_id=(uint8_t)(touch_id%127+1); }
d->buttons|=BTN_TOUCHPAD;
d->touch_count=1;
d->touches[0]=(PadTouch){.x=right ? 1440 : 480,.y=471,.id=0};
d->touch_held_time=(uint32_t)(now-touch_down);
d->touches[0]=(PadTouch){.x=right ? 1440 : 480,.y=471,.id=touch_id};
}
/* BB_GAMEPAD (the launcher's controller choice): its SDL GUID, or part of its name. Issue #15:
@@ -689,6 +697,7 @@ static void sample(PadData *d) {
d->buttons|=injected.buttons;
if (injected.touch_side>=0) touch_click(d,injected.touch_side);
else if ((d->buttons & BTN_TOUCHPAD) && !d->touch_count) touch_click(d,0);
if (!d->touch_count) touch_down=0;
if (injected.buttons & BTN_L2) d->l2=255;
if (injected.buttons & BTN_R2) d->r2=255;
uint8_t *axes[4]={&d->left_x,&d->left_y,&d->right_x,&d->right_y};
+41
View File
@@ -0,0 +1,41 @@
from paths import ROOT
import sys
import tempfile
import unittest
from pathlib import Path
sys.path.insert(0, str(ROOT / 'launcher'))
import bbport_cache # noqa: E402
class ShaderCacheTests(unittest.TestCase):
def test_size_and_clear_keep_the_saves(self):
with tempfile.TemporaryDirectory() as tmp:
user = Path(tmp)
self.assertEqual(bbport_cache.size(user), 0)
game = user / 'cache' / 'CUSA00900'
game.mkdir(parents=True)
(game / 'a.spv').write_bytes(b'x' * 100)
(user / 'cache' / 'CUSA03173.zip').write_bytes(b'y' * 50)
saves = user / 'savedata' / '1'
saves.mkdir(parents=True)
(saves / 'userdata0000').write_bytes(b'z' * 10)
self.assertEqual(bbport_cache.size(user), 150)
self.assertEqual(bbport_cache.clear(user), 150)
self.assertEqual(bbport_cache.size(user), 0)
self.assertTrue((user / 'cache').is_dir())
self.assertTrue((saves / 'userdata0000').is_file())
def test_symlinks_are_not_followed(self):
with tempfile.TemporaryDirectory() as tmp:
user, outside = Path(tmp) / 'user', Path(tmp) / 'outside'
(user / 'cache').mkdir(parents=True)
outside.mkdir()
(outside / 'keep').write_bytes(b'k')
(user / 'cache' / 'link').symlink_to(outside)
bbport_cache.clear(user)
self.assertTrue((outside / 'keep').is_file())
if __name__ == '__main__':
unittest.main()
+4
View File
@@ -60,6 +60,8 @@ class PackagedVulkanTests(unittest.TestCase):
driver = self.library("libGLX_nvidia.so.580.1")
(self.libs / "libGLX_nvidia.so.0").symlink_to(driver.name)
compiler = self.library("libnvidia-glvkspirv.so.580.1")
cuda = self.library("libcuda.so.580.1")
(self.libs / "libcuda.so.1").symlink_to(cuda.name)
self.library("libc.so.6")
self.manifest()
self.assertIn("host NVIDIA", self.configure())
@@ -69,6 +71,8 @@ class PackagedVulkanTests(unittest.TestCase):
self.assertEqual(Path(data["ICD"]["library_path"]).resolve(), driver)
cache = Path(self.env["LD_LIBRARY_PATH"].split(":")[0])
self.assertEqual((cache / compiler.name).resolve(), compiler)
# DLSS (NGX) makes the driver load libcuda.so.1 (#135).
self.assertEqual((cache / "libcuda.so.1").resolve(), cuda)
self.assertFalse((cache / "libc.so.6").exists())
self.assertTrue(self.env["VK_DRIVER_FILES"].endswith(":/bundled/radeon.json:/bundled/intel.json"))
# Repeated starts reuse valid links and do not accumulate per-launch caches.
+40 -1
View File
@@ -25,6 +25,7 @@ int main() {
assert(write(fd, ini, sizeof(ini) - 1) == ssize_t(sizeof(ini) - 1));
close(fd);
setenv("BB_CONFIG", path, 1);
setenv("BB_MENU_LANGUAGE", "ru", 1);
BbSettings::Load();
auto& s = BbSettings::Get();
@@ -42,11 +43,49 @@ int main() {
"fullscreen_hint=1\n") == 0);
assert(saved.find("menu_pos=0.6250,0.1250\n") != std::string::npos);
assert(saved.find("upscaler=fsr3") == std::string::npos);
// The language the menu started with (the launcher's) is not a choice: it is not written.
assert(s.menu_language == BbSettings::MenuLanguage::Russian);
assert(saved.find("menu_language") == std::string::npos);
s.menu_x = -1.0f;
s.menu_y = -1.0f;
BbSettings::Load();
assert(s.menu_x == 0.625f && s.menu_y == 0.125f && s.upscaler == BbSettings::UpscalerFsr411);
// So the menu follows the launcher's language when that changes...
setenv("BB_MENU_LANGUAGE", "en", 1);
BbSettings::Load();
assert(s.menu_language == BbSettings::MenuLanguage::English);
// ...until one is chosen in the menu: that one is written and wins.
s.menu_language = BbSettings::MenuLanguage::Russian;
s.menu_language_chosen = true;
BbSettings::Save();
assert(Read(path).find("menu_language=ru\n") != std::string::npos);
BbSettings::Load();
assert(s.menu_language == BbSettings::MenuLanguage::Russian);
s.show_fps = false;
BbSettings::Save();
assert(Read(path).find("menu_language=ru\n") != std::string::npos);
// An output size the menu does not list (deadinside28#183) survives a save...
{
FILE* f = std::fopen(path, "a");
std::fputs("output_res=5120x2160\n", f);
std::fclose(f);
}
BbSettings::Load();
int width = 0, height = 0;
assert(BbSettings::CustomOutput(width, height) && width == 5120 && height == 2160);
s.show_fps = true;
BbSettings::Save();
assert(Read(path).find("output_res=5120x2160\n") != std::string::npos);
// ...until a listed one is chosen.
s.custom_output_width = 0;
s.output_res = 2;
BbSettings::Save();
assert(Read(path).find("output_res=2560x1440\n") != std::string::npos);
assert(!BbSettings::CustomOutput(width, height));
unlink(path);
std::puts("PASS: settings save keeps other keys, menu position");
std::puts("PASS: settings save keeps other keys, menu position, only a chosen menu language, "
"a custom output size");
}
+10
View File
@@ -114,6 +114,16 @@ class TrophyFileTests(unittest.TestCase):
(directory / 'trophies.tsv').write_text('1\tB\t1\tOld\tFive fields\n')
self.assertFalse(trophies.read_list(directory)[1]['platinum'])
def test_bloodborne_without_the_key_in_english(self):
with tempfile.TemporaryDirectory() as tmp:
entries = trophies.read_trp(self.game(tmp) / 'sce_sys' / 'trophy' / 'trophy00.trp')
listed = trophies.trophy_list(entries, None, trophies.BLOODBORNE)
self.assertEqual(listed[0]['name'], 'Bloodborne')
self.assertEqual((listed[1]['grade'], listed[1]['hidden'], listed[1]['platinum']),
('G', True, True))
self.assertEqual(len(trophies.BLOODBORNE_ENGLISH), 40)
self.assertFalse(trophies.trophy_list(entries, None, COMM_ID)[1]['name'])
def test_wrong_key(self):
with tempfile.TemporaryDirectory() as tmp:
entries = trophies.read_trp(self.game(tmp) / 'sce_sys' / 'trophy' / 'trophy00.trp')