mirror of
https://github.com/ayozetr/bloodborne_linux.git
synced 2026-11-12 18:08:46 -10:00
Compare commits
27
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d5c2611f72 | ||
|
|
73f3bb512a | ||
|
|
5e86d0bde7 | ||
|
|
30b3409674 | ||
|
|
b3c8b1dadd | ||
|
|
697af8936f | ||
|
|
576a214b5f | ||
|
|
8d1db30f9e | ||
|
|
16a08695c3 | ||
|
|
b4d74c88f7 | ||
|
|
bb3a9d126d | ||
|
|
3d2b8b3067 | ||
|
|
186a342750 | ||
|
|
2bcc093bd9 | ||
|
|
8937a721ca | ||
|
|
4f3187843f | ||
|
|
da5561d6c7 | ||
|
|
f9186dbb8d | ||
|
|
1b93552dca | ||
|
|
d7a94e5dfc | ||
|
|
e2b3f73e71 | ||
|
|
1ce06532c2 | ||
|
|
49a37b4459 | ||
|
|
25cdef7482 | ||
|
|
9413c82bfb | ||
|
|
f4a12043f6 | ||
|
|
7e02e06139 |
No files matched your search
@@ -1,5 +1,11 @@
|
||||
# Bloodborne for Linux
|
||||
|
||||
[](https://github.com/ayozetr/bloodborne_linux/releases/latest)
|
||||
[](https://github.com/ayozetr/bloodborne_linux/releases/latest)
|
||||
[](LICENSE)
|
||||
[](#features)
|
||||
[](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
@@ -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
|
||||
@@ -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;
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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 |
@@ -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 |
@@ -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
@@ -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
@@ -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};
|
||||
|
||||
@@ -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()
|
||||
@@ -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
@@ -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");
|
||||
}
|
||||
@@ -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')
|
||||
|
||||
Reference in new issue
Block a user