mirror of
https://github.com/ayozetr/bloodborne_linux.git
synced 2026-11-08 08:03:07 -10:00
Compare commits
56
Commits
main
...
0.51-linux.2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
97a60dd497 | ||
|
|
a3a8a3af6e | ||
|
|
f1091cc0b1 | ||
|
|
c496667284 | ||
|
|
a8b7a8eaa1 | ||
|
|
4d934b7529 | ||
|
|
442c2b66bd | ||
|
|
d7a42fde94 | ||
|
|
d0318b1ab5 | ||
|
|
1bc4f37504 | ||
|
|
44a6b7c3ce | ||
|
|
efdf7b176e | ||
|
|
4f7df3de5c | ||
|
|
e3c51c48aa | ||
|
|
33393185b9 | ||
|
|
c294942db4 | ||
|
|
6c6974a742 | ||
|
|
83246710d7 | ||
|
|
b71eb7cfee | ||
|
|
b6c25e1fb4 | ||
|
|
0e2b626358 | ||
|
|
b060b9fa2b | ||
|
|
18a7dd7e39 | ||
|
|
28a9233034 | ||
|
|
9c056c437f | ||
|
|
2683273a17 | ||
|
|
4d297b96bb | ||
|
|
917c4bdd20 | ||
|
|
8e24cfdcf5 | ||
|
|
cfede387ed | ||
|
|
f44390da19 | ||
|
|
c89f4d432c | ||
|
|
4fd1cd6ee6 | ||
|
|
cfe7af67a5 | ||
|
|
124384eac1 | ||
|
|
5227432b1b | ||
|
|
81db68f552 | ||
|
|
f723f6a972 | ||
|
|
82a96e0d25 | ||
|
|
71717863d5 | ||
|
|
c4281227da | ||
|
|
ee20c8b147 | ||
|
|
a39a0073de | ||
|
|
09a1e4981a | ||
|
|
992fc95ed1 | ||
|
|
851712537b | ||
|
|
e289b722b5 | ||
|
|
c7cae2a585 | ||
|
|
7d53c5e9d6 | ||
|
|
7dfbf49367 | ||
|
|
52a6daf288 | ||
|
|
912641c298 | ||
|
|
dca76e386f | ||
|
|
0b465f8ee1 | ||
|
|
82782d8155 | ||
|
|
89ea3deb70 |
No files matched your search
@@ -0,0 +1 @@
|
||||
ko_fi: ayozetr
|
||||
@@ -23,6 +23,8 @@ perf.data
|
||||
/bloodborne_upscalers_*.md
|
||||
mods.json
|
||||
patches.json
|
||||
# The mods folder of runs from source (the launcher's default, DATA_DIR/mods)
|
||||
/mods/
|
||||
# Local: game logs, messages and archives from users, internal plans
|
||||
/logs/
|
||||
/message*.txt
|
||||
@@ -30,3 +32,10 @@ patches.json
|
||||
/docs/ISSUE_ACTION_PLAN_*.md
|
||||
# AMD FidelityFX DLLs (FSR 4.1.1 is built from the user's own): never in the repository
|
||||
*.dll
|
||||
# Game files and dumps: never in the repository
|
||||
eboot.bin
|
||||
param.sfo
|
||||
*.pkg
|
||||
*.dcx
|
||||
*.7z
|
||||
/CUSA*/
|
||||
@@ -1,6 +1,8 @@
|
||||
[submodule "gpu/third_party/fsr-vulkan"]
|
||||
path = gpu/third_party/fsr-vulkan
|
||||
url = https://github.com/FireBurn/FSR-Vulkan.git
|
||||
# build.sh applies gpu/patches/fsr-vulkan to its working tree
|
||||
ignore = dirty
|
||||
[submodule "gpu/third_party/imgui"]
|
||||
path = gpu/third_party/imgui
|
||||
url = https://github.com/ocornut/imgui.git
|
||||
|
||||
@@ -0,0 +1,436 @@
|
||||
THIS PROJECT IS NOT RELATED TO SHADPS4. ALL QUESTIONS RELATED TO THIS PROJECT SHOULD BE SENT TO THE DISCORD SERVER https://discord.gg/KYZRKk9CB, NOT TO THE SHADPS4 SERVER.
|
||||
|
||||
|
||||
# bbport — a native Linux port of Bloodborne
|
||||
|
||||
> This is the README of the original project (deadinside28/bloodborne_pc), kept for its
|
||||
> technical details. The fork's own README is [README.md](README.md).
|
||||
|
||||
**English** · [Русский](BBPORT.ru.md)
|
||||
|
||||
bbport is the counterpart of Wine + DXVK for a single game: *Bloodborne* for PlayStation 4
|
||||
(CUSA03173, game version 1.09) on an x86-64 Linux PC. The game's original executable runs
|
||||
directly on the PC:
|
||||
|
||||
- **as in Wine**, the game's x86-64 code runs on the CPU directly, and a runtime written for
|
||||
this one game replaces the PS4 system libraries;
|
||||
- **as in DXVK**, the game's graphics are translated to Vulkan — by a renderer derived from
|
||||
[shadPS4](https://github.com/shadps4-emu/shadPS4) and heavily extended for this game, including
|
||||
temporal upscaling with AMD FSR 3.1, FSR 4 and FSR 4.1.1;
|
||||
- memory and the GPU's work are moving to the PC model: the launcher has an experimental
|
||||
*New memory and translation model* mode; without it the game runs on the old
|
||||
memory model, as in 0.3.
|
||||
|
||||
One step remains to the full Wine + DXVK model: make the new mode the default and remove the old
|
||||
memory model (see below).
|
||||
|
||||
> **No game files are included.** You need your own dump of Bloodborne with update 1.09: any retail
|
||||
> release (CUSA00900 in the US, CUSA03173 for the European Game of the Year edition, ...), as they
|
||||
> share the same executable.
|
||||
> This project is not affiliated with Sony Interactive Entertainment, FromSoftware or AMD.
|
||||
|
||||
**Status: experimental, playable.** The game boots, loads saves and plays (the Hunter's Dream
|
||||
and several areas of Yharnam were played with it) with sound, gamepad and saving.
|
||||
A full play-through has not been verified, and only one machine (Linux, AMD Radeon RX 7800 XT,
|
||||
Mesa/RADV) has been tested thoroughly.
|
||||
|
||||
## Highlights
|
||||
|
||||
- **Native execution.** The eboot is converted offline into a flat memory image; PS4 libc and
|
||||
libSceFios2 are linked into it as native code. No CPU emulation and no per-instruction
|
||||
translation: the game code runs at full speed.
|
||||
- **Two modes** (launcher → *Mode*):
|
||||
- **Switch off (the default) — as in 0.3.** The old memory model: VRAM copies of the game's
|
||||
memory, writes tracked through page protection. With every fix made since 0.3 (motion
|
||||
vectors and the upscaler, flicker with DoF on, a damaged shader cache).
|
||||
- **New memory and translation model — experimental.** The game's memory lives
|
||||
in system RAM and the GPU reads it where it is, as a PC game's buffers; data it reads often
|
||||
is kept in VRAM and given back when unused (textures after 20 s, buffers after 60 s). The
|
||||
layer's memory module tracks CPU writes to mirrored blocks to keep those copies coherent;
|
||||
it does not copy the whole GPU-visible memory. The PS4 command processor's work
|
||||
is translated into Vulkan commands rather than emulated on the CPU: memory writes
|
||||
(`WRITE_DATA`, DMA) are done by the GPU in command-stream order, and fences are written by the
|
||||
GPU itself at the end of the pipeline, at the same point of the stream as the PS4's command
|
||||
processor writes them. **It may crash**; hardware testing has been performed on an
|
||||
RX 7800 XT, and performance depends on the scene and settings. On AMD the game's memory is kept
|
||||
in a sparse buffer (the arena). **On NVIDIA, Intel and others** it goes through the layer's
|
||||
memory module: the game's memory is bound in place and VRAM copies are separate buffers, with
|
||||
no sparse rebinding. This avoids the sparse-binding path associated with reported freezes
|
||||
on a GTX 1660 Ti. On NVIDIA the module runs on a tester's GTX 1660 Ti, its speed is still
|
||||
being checked; not tested on Intel. Without the launcher: `BB_PC_MODEL=1`; to choose the memory
|
||||
path: `BB_GUEST_MEMORY=host|dmabuf`; the memory module: `BB_LAYER_MEMORY=1/0`.
|
||||
|
||||
[0.5-pre4 changes](docs/CHANGES_0.5-pre4.md): no stutter of the memory module (NVIDIA) while areas
|
||||
stream in, shader compilation in the background, VRAM on 4–6 GB cards, damaged game extractions
|
||||
reported, the settings menu as in 0.3. [0.5-pre3](docs/CHANGES_0.5-pre3.md): the memory module on
|
||||
NVIDIA — VRAM copies, one VRAM budget with the texture cache, watched blocks. [0.5-pre2](docs/CHANGES_0.5-pre2.md): the memory
|
||||
module, native GPU copy shader, portable GPU completion labels, and shader-cache migration.
|
||||
|
||||
Unused textures are freed in both modes, so VRAM no longer grows with every area visited.
|
||||
- **Unlocked frame rate.** Community patches (`patches/Bloodborne.xml`) make the simulation
|
||||
use the real frame time; ~90 FPS at 4K with FSR 4 Balanced on an RX 7800 XT, ~150 FPS at
|
||||
1440p with FSR 4 Quality. Also 30/60/90 FPS modes.
|
||||
- **Temporal upscaling built for this game.** Bloodborne has no velocity buffer, so bbport
|
||||
computes motion vectors itself: camera motion from depth and the scene matrices, and object
|
||||
motion (characters, cloth, weapons) from the vertex positions of the previous frame. The
|
||||
scene is jittered sub-pixel (Halton) and rendered at a reduced resolution; the upscaler fills
|
||||
the output (720p for the Steam Deck, 1080p, 1440p or 2160p) and the UI is drawn natively at the output resolution.
|
||||
- **FSR 3.1** (FireBurn/FSR-Vulkan).
|
||||
- **FSR 4 (INT8, model v07)** on GPUs exposing the required Vulkan shader features —
|
||||
RDNA2/3 included (see Requirements).
|
||||
- **FSR 4.1.1**: AMD's 4.1.1 DLL is recorded once under vkd3d-proton and its passes are
|
||||
replayed natively on Vulkan; the output is **bit-exact** with the DLL. Two variants, as in
|
||||
the DLL: INT8 on any GPU with the required shader features, and FP8 matrices on RDNA4 (RX
|
||||
9000; picked automatically). The assets are built on your machine from one DLL of your own
|
||||
(4.1.x): the launcher's *FSR 4.1.1 from your own AMD DLL → Choose DLL…* button (2–5 minutes,
|
||||
twice that on RDNA4; needs a recent Proton), or `tools/fsr4cap`.
|
||||
- Faster than AMD's own shaders on RDNA3: the final passes of FSR 4 and 4.1.1 were rewritten
|
||||
to store through workgroup memory (3.5× and 2.3× faster, bit-exact); FSR 4 costs ~4 ms at
|
||||
4K on an RX 7800 XT instead of ~6 ms.
|
||||
- **Multi-threaded GPU command processing.** The PS4 command stream is decoded on one thread
|
||||
and draws are bound and recorded on another (two-stage pipeline), with a Vulkan recording
|
||||
thread and helper threads for memory copies. Early on the single GPU thread capped the game
|
||||
at ~26 FPS; now it runs at 90–150 FPS depending on resolution and scene.
|
||||
- **Settings inside the game's own menu:** *System → Display / Game effects / Game patches*
|
||||
(after *Screen/Sound*), built and drawn by the game itself — output resolution, upscaler,
|
||||
preset, sharpness, FPS counter, model detail and the effect patches, in the game's language.
|
||||
- **Overlay menu** (Insert or L3+R3), styled like the game's dialogs: everything above plus the
|
||||
advanced settings, with gamepad, keyboard and mouse.
|
||||
- **GTK4 launcher** and an **AppImage** for the Steam Deck.
|
||||
|
||||
## How it differs from shadPS4
|
||||
|
||||
| | shadPS4 | bbport |
|
||||
|---|---|---|
|
||||
| Scope | General PS4 emulator, many games | One game: Bloodborne v1.09 |
|
||||
| Loading | Its own ELF loader and kernel emulation at run time | The eboot is converted offline (`scripts/`) into an image with PS4 libc/Fios2 linked in; a C loader maps it and jumps into the game (loader and runtime: ~5k lines) |
|
||||
| Memory | The GPU's view of PS4 memory is kept in VRAM copies, synchronized through page-protection write tracking | By default the same model (as in 0.3); in the *New memory and translation model* mode: the game's memory in system RAM, used by the GPU in place, frequently read data in VRAM, freed when unused |
|
||||
| Command processor | Emulated: memory writes, DMA and fences are done by the CPU while decoding | By default the same; in the new mode translated into Vulkan commands that the GPU runs in stream order, fences written after the work has really finished |
|
||||
| System libraries | Broad HLE of the PS4 OS | A small runtime (`src/runtime_*.c`) that implements exactly what Bloodborne calls: memory, threads, sync, files, audio (incl. ATRAC9), pad, saves, AppContent |
|
||||
| GPU | shadPS4 video core and shader recompiler | The same core (vendored, GPL) with ~200 marked changes (`bbport:`) plus new modules: two-stage draw pipeline, render-state and texture-set memoization, render-scale proxies, motion vectors, FSR 3.1/4/4.1.1, frame capture and GPU profiler |
|
||||
| GPU thread | One thread processes the whole command stream (the bottleneck in Bloodborne) | Decode and draw recording run on separate threads; the work scales with the hardware threads (Steam Deck included) |
|
||||
| Upscaling | — | Temporal (FSR 3.1, FSR 4, FSR 4.1.1) with the game's own motion vectors and jitter |
|
||||
| Game patches | Patch files applied by the emulator | The same community patches, compiled at start (`scripts/patches.py`); render resolution, effects and FPS from the launcher |
|
||||
|
||||
Without shadPS4 there would be no bbport: its renderer and shader recompiler are the base of
|
||||
the graphics side.
|
||||
|
||||
### Wine + DXVK for one game
|
||||
|
||||
- **CPU.** The PS4 CPU is x86-64, so the game's code runs directly on the PC's CPU, with no
|
||||
emulation and no instruction translation.
|
||||
- **System libraries.** As Wine replaces the Windows API, the bbport runtime (`src/runtime_*.c`)
|
||||
implements exactly the PS4 OS functions Bloodborne calls: memory, threads, files, audio, pad,
|
||||
saves.
|
||||
- **Memory.** In the new mode, as in a PC game: the game's data in system RAM, VRAM used the way
|
||||
a PC game uses it. The mode is still experimental.
|
||||
- **Graphics.** The PS4 GPU's command stream (PM4) is a recording of the game's graphics API
|
||||
calls: it is written by 99 functions of the statically linked libGnm, so decoding the stream
|
||||
and translating the calls themselves come to the same thing. GCN shaders are translated to
|
||||
SPIR-V. In the new mode the command processor's work (memory writes, DMA, fences) is
|
||||
translated into Vulkan commands as well; fences are written by the GPU itself. Resource
|
||||
descriptors (textures, buffers) are read by the translator on the CPU, as DXVK reads resource
|
||||
bindings: a PS4 texture has to be converted from the PS4 tiling into a Vulkan image before the
|
||||
draw runs, so the CPU must know its descriptor beforehand — translation, not emulation. The
|
||||
descriptor tables (the constant engine's dumps) are read only by the translator, so the CPU
|
||||
performs them. The game's occlusion queries (~60 a frame) are
|
||||
Vulkan occlusion queries; predication, `COPY_DATA` and `COND_EXEC` are translated too (check:
|
||||
`BB_PM4_SELFTEST=1`). Details:
|
||||
[docs/EMULATION_REMOVAL_PLAN.ru.md](docs/EMULATION_REMOVAL_PLAN.ru.md), "Шаг 4" (Russian).
|
||||
|
||||
"Port" here means a build for this one game, not a rewrite of its source code, which the project
|
||||
neither has nor includes.
|
||||
|
||||
## Requirements
|
||||
|
||||
- Linux x86-64, a Vulkan 1.3 GPU. Tested: AMD RX 7800 XT with Mesa 26 (RADV).
|
||||
The *New memory and translation model* mode is not tested on NVIDIA yet.
|
||||
FSR 4 / 4.1.1 require shader Float16, Int8/Int16, integer dot products, linear compute
|
||||
derivatives and extended storage image formats; FSR 4.1.1 additionally requires
|
||||
`VK_VALVE_shader_mixed_float_dot_product`. Unsupported choices fall back to FSR 3.1
|
||||
before the first frame and are disabled in the in-game menu.
|
||||
- Your decrypted game dump: the `CUSA03173` folder (eboot.bin, sce_module, ...), version 1.09.
|
||||
The US release (`CUSA00900`) runs too: its 1.09 executable is the same as this European
|
||||
Game of the Year edition's.
|
||||
A dumped update is a separate folder: copy it over the base game, replacing files. The base
|
||||
game alone (1.00) crashes at start (guest offset 0x20348b8); the launcher and `run.sh` check
|
||||
the executable and say what is missing (`BB_SKIP_GAME_CHECK=1` skips the check). The other
|
||||
retail releases (the US `CUSA00900`, the Asian `CUSA03023`, ...) run the same 1.09 executable. Some dumps ship it
|
||||
with Lance McDonald's 60 fps patch baked in, which crashes when the gestures menu opens: the
|
||||
check asks for a clean eboot.bin then (bbport has its own 60 fps).
|
||||
- To build: GCC, CMake, Ninja, Python 3, glslang, SDL3, Vulkan headers and the libraries in
|
||||
`shell.nix`. With [Nix](https://nixos.org) everything comes from `shell.nix` automatically.
|
||||
|
||||
## Build and run
|
||||
|
||||
```bash
|
||||
git clone --recursive https://github.com/deadinside28/bloodborne_pc.git bbport && cd bbport
|
||||
bash build.sh # builds out/bb-probe and out/gpu/libbbgpu.so
|
||||
BB_GAME_DIR=/path/to/CUSA03173 bash run.sh
|
||||
```
|
||||
|
||||
or the launcher (pick the game folder, settings, *Start*):
|
||||
|
||||
```bash
|
||||
bash launcher/bb-launcher.sh # launcher/install-desktop.sh adds it to the app menu
|
||||
```
|
||||
|
||||
By default the game folder is expected next to the repository (`../CUSA03173`). Saves and the
|
||||
shader cache go to `user/` (the launcher lets you choose another folder); settings to
|
||||
`bbport.ini`. A gamepad is used through SDL3 (the launcher's *Controls → Controller* picks one
|
||||
when several are connected; `BB_GAMEPAD=<GUID or part of the name>`). The keyboard works too,
|
||||
also next to a connected gamepad (the Steam Deck always has one); both are remapped in the
|
||||
launcher (*Controls*). The character name is typed on the keyboard in a box over the game.
|
||||
The touchpad: its left half (Tab, Back/Select) opens the gestures, the right half (Backspace)
|
||||
the key items. There is no PSN: the *Play online / offline* screen is skipped and the game opens
|
||||
its main menu offline (a game patch; the launcher's *Game effects → Skip the “play online / offline” choice*,
|
||||
`BB_SKIP_NETWORK_CHOICE=0` shows it). With several monitors, *Screen → Monitor* in the launcher
|
||||
(`BB_DISPLAY=<number or part of the name>`) picks the one for the game.
|
||||
|
||||
Add-ons: `BB_DLC_DIR=/path/to/dlc` (the launcher's *DLC folder*) holds the dumped add-ons of
|
||||
the game, one folder each named by its entitlement label (`SPEXPANSIONDLC03/sce_sys/...`).
|
||||
Their data ships with the 1.09 update, so a folder with only `sce_sys` is enough. `run.sh` links
|
||||
it as `user/addcont/<title id>`, where the runtime finds add-ons (see **DLC** below); a real
|
||||
folder already there is kept. The GOTY edition (CUSA03173) needs none.
|
||||
|
||||
**Resolution and preset changes:** for outputs other than 1080p (720p on the Steam Deck,
|
||||
1440p, 4K) the whole game renders at the preset's resolution, set by a patch at start — the
|
||||
fastest path. Changing the output or the preset in the in-game menu then needs *Apply and
|
||||
restart the game*. The *Live resolution changes* setting (launcher, in-game menu,
|
||||
`bbport.ini` `live_resolution=0|1|auto`; **off by default**) instead keeps the game at
|
||||
1080p internally and scales its render targets at run time, so 720p/1080p/1440p/4K and the
|
||||
presets switch without a restart. It costs more: the game then believes it renders 1080p
|
||||
and draws more (e.g. ~8× more small lights), and some targets are copied between sizes —
|
||||
use it on strong desktop GPUs only (`auto` turns it on for discrete GPUs with 8+ GB that are
|
||||
not pre-Turing NVIDIA). 1080p output and TAA always use the live path.
|
||||
|
||||
**TAA:** a separate native-resolution temporal AA mode in the launcher and overlay,
|
||||
switchable live without an FSR model. The saved FSR preset is restored when returning
|
||||
to FSR. FSR Native AA adds reconstruction on top of full-resolution rendering and can
|
||||
be slower than disabling AA. TAA also adds work compared with no temporal AA.
|
||||
The RCAS switch and the 0–2 sharpness control also work with TAA. Sharpening runs after
|
||||
temporal accumulation and leaves its history and HUD unchanged.
|
||||
|
||||
**Mods:** the launcher accepts separate loose-file mod folders (with `dvdroot_ps4/`, an extra
|
||||
wrapper folder, or the game folders such as `chr/` directly; file name case does not matter),
|
||||
with enable switches and load order. A sibling `CUSA03173-mods/` overlay also works.
|
||||
The original game is preserved; later mods override conflicting files.
|
||||
**Third-party patches:** shadPS4-format XML patch files in the data directory's `patches/`,
|
||||
switched on and off in the launcher. See [mods and patches](docs/MODS.md).
|
||||
**DLC:** put your add-on dumps in `user/addcont/<title id>/<entitlement label>/` (shadPS4's
|
||||
layout, e.g. `user/addcont/CUSA03173/SPEXPANSIONDLC03` for The Old Hunters): each folder there is
|
||||
reported to the game as an installed add-on.
|
||||
|
||||
**Launcher language:** Russian, English, Brazilian Portuguese, Spanish, Italian, French, German or Simplified Chinese (follows the system language by default).
|
||||
**Game language:** the PS4 system language, *System language* in the launcher; by default it
|
||||
follows the desktop's locale (Japanese, English, French, Spanish, German, Italian or Russian,
|
||||
English for the rest). `run.sh` takes it as `BB_LANGUAGE` (PS4 code: 0 Japanese, 1 English,
|
||||
2 French, 3 Spanish, 4 German, 5 Italian, 8 Russian; without it, the desktop's locale as above).
|
||||
**Game region:** *Game region* in the launcher, `BB_REGION=eu|na` for `run.sh`: the regional text
|
||||
of a language, such as Spanish from Spain (`eu`) or Latin American Spanish (`na`). It sets the
|
||||
game's `USER_DEFINED_PARAM_1` (13 Europe, 2 the Americas) for the launch; by default the dump's
|
||||
`param.sfo` decides.
|
||||
|
||||
**Free camera and game debug menu** (v1.09): enable the corresponding switches in the
|
||||
launcher or in-game menu and restart. Free camera uses Lance McDonald's
|
||||
[GoldHEN patch](https://github.com/GoldHEN/GoldHEN_Patch_Repository/blob/main/patches/xml/Bloodborne-Orbis.xml):
|
||||
hold Cross and press L3 to cycle modes (keyboard: hold Space and press Z). It needs no fonts
|
||||
and conflicts with *Enemy Control*.
|
||||
For the game debug menu, install `DbgFont14h.ccm` and `DbgFont14h.tpf` from
|
||||
[Debug Menu and XML Patch](https://www.nexusmods.com/bloodborne/mods/253) into the game's
|
||||
`dvdroot_ps4/font/` first. Startup rejects missing or empty font files instead of launching
|
||||
the unsafe patch. Open it with the left touchpad / Tab (with the debug menu on, the left half
|
||||
no longer opens the gestures); Backspace is the right touchpad.
|
||||
Touch coordinates are forwarded from SDL gamepads; Back/Select emulates a left click on
|
||||
pads without a touch surface. The port's settings menu remains Insert / L3+R3.
|
||||
|
||||
GPU occlusion queries still use synthetic pixel counters (`PixelPipeStatDump`), and
|
||||
`IT_SET_PREDICATION` is unimplemented. Free camera allows visual investigation; it does
|
||||
not implement GPU occlusion culling.
|
||||
|
||||
**Upscaler assets** (not included; FSR 3.1 needs none):
|
||||
|
||||
```bash
|
||||
bash tools/fetch_fsr4_assets.sh # FSR 4 v07 (MIT, built from AMD's source by Q2RTX)
|
||||
# FSR 4.1.1, from your own AMD DLLs (e.g. OptiScaler's FSR4_LATEST), needs GE-Proton 10 or newer:
|
||||
bash tools/fsr4cap/build_assets.sh <amd_fidelityfx_upscaler_dx12.dll>
|
||||
```
|
||||
|
||||
**FSR 4.1.1 from your own DLL.** The easiest way is the launcher: *Upscaler → FSR 4.1.1 from your
|
||||
own AMD DLL → Choose DLL…*, then pick `amd_fidelityfx_upscaler_dx12.dll` version 4.1.x: from
|
||||
OptiScaler's `FSR4_LATEST` folder or from a game with FSR 4.1. That one file is enough: AMD's DLL
|
||||
exports the FidelityFX API itself, so no loader is needed (only a DLL without these exports would
|
||||
be recorded through `amd_fidelityfx_loader_dx12.dll`, and the launcher would ask for it). The DLL is checked at once,
|
||||
without a long capture; these do not fit: AMD's official FSR 4.0.x (e.g. Pragmata's 4.0.3 —
|
||||
AMD enables it on RDNA4 only, and under Proton it does not start on other GPUs) and community
|
||||
builds (4.0.2b, 4.1.1b and similar OptiScaler INT8 builds: another model and pass count). Then
|
||||
the DLL runs under Proton and is recorded at 20 sizes (progress in the row and in the *Log* tab;
|
||||
on RDNA4 a second time, for the FP8 variant),
|
||||
the passes are translated to SPIR-V, and FSR 4.1.1 is selected. The Proton build is picked
|
||||
automatically — the first under which the DLL enables FSR 4.1: GE-Proton 10 or newer,
|
||||
Proton-CachyOS, Proton Experimental (tested: GE-Proton 11, Proton-CachyOS 11, Experimental of
|
||||
October 2026; Steam's Proton 11.0 and GE-Proton 9 do not). It runs in the Steam runtime it
|
||||
requires (Steam installs it the first time any game runs with that Proton), or through
|
||||
umu-launcher when installed. On NixOS umu-launcher comes from `nix-shell` (downloaded the first
|
||||
time, ~1.7 GB); the AppImage runs the recording on the system itself, through the user's systemd
|
||||
(`systemd-run --user`), since the system's `/nix` is out of its sight. Recording on the Steam Deck is not verified yet; if the DLL does not
|
||||
enable FSR 4.1 there, build on a PC and copy the `fsr4_411` folder. The same from the command
|
||||
line: the launcher's (and the AppImage's) `--build-fsr411 <DLL>`.
|
||||
|
||||
**DLSS (NVIDIA GeForce RTX).** NVIDIA's DLSS SDK is not included: check out
|
||||
[github.com/NVIDIA/DLSS](https://github.com/NVIDIA/DLSS) (`include/` and `lib/Linux_x86_64` are
|
||||
enough) and build with `DLSS_SDK_ROOT=/path/to/DLSS bash build.sh`. That also builds the bridge
|
||||
`out/libbbport_dlss.so` (`gpu/dlss_bridge`, the only code that uses the SDK) and copies NVIDIA's
|
||||
`libnvidia-ngx-dlss.so.<version>` next to `out/bb-probe`; the driver provides the rest. Then pick
|
||||
*DLSS* in the launcher or the in-game menu: the preset sets the render size, Native AA is DLAA.
|
||||
DLSS gets the inputs FSR 3.1 gets (scene color, depth, motion vectors, jitter). Without the
|
||||
libraries, on other GPUs or with `BB_DLSS=0` it is listed as unavailable, and a DLSS setting falls
|
||||
back to FSR 3.1.
|
||||
|
||||
On Linux, DLSS also needs NVIDIA's CUDA/UVM runtime. If device creation fails with the DLSS
|
||||
extensions, the renderer retries without them and uses FSR 3.1. If `/dev/nvidia-uvm` is missing,
|
||||
load the installed driver module with `sudo modprobe nvidia_uvm` and restart the game. Installing
|
||||
the distribution's `nvidia-modprobe` package lets NVIDIA's libraries load the module when needed.
|
||||
If `modprobe` reports `Key was rejected by service` with Secure Boot enabled, enroll the
|
||||
module's signing key using the distribution's [MOK procedure](https://documentation.ubuntu.com/security/security-features/platform-protections/secure-boot/#mok-generation-and-signing-process).
|
||||
|
||||
The AppImage built with `DLSS_SDK_ROOT` carries the bridge and NVIDIA's library (NVIDIA's RTX SDK
|
||||
license allows that in an application; its text is `NVIDIA-DLSS-LICENSE.txt` in the package). A
|
||||
newer `libnvidia-ngx-dlss.so.<version>` (from the SDK's `lib/Linux_x86_64/rel`) is chosen in the
|
||||
launcher: *Upscaler → DLSS: NVIDIA's library → Choose a file…*, as the FSR 4.1.1 DLL; it is
|
||||
copied to `<user>/dlss/`, which the game prefers. A game's `nvngx_dlss.dll` is the Windows
|
||||
library: NVIDIA's Linux driver does not load it.
|
||||
|
||||
From source the script takes its tools from the system (MinGW GCC, CMake, Ninja, Python 3,
|
||||
SPIRV-Tools, Git) or from Nix; the AppImage has them prebuilt.
|
||||
|
||||
**FP8 on RDNA4.** When vkd3d-proton offers FP8 cooperative matrices (RDNA4), the DLL runs
|
||||
another variant of its passes: other shaders and weights, other dispatch sizes. The build records
|
||||
both, INT8 into `fsr4_411/` and FP8 into `fsr4_411/fp8/`, and the game picks FP8 itself when the
|
||||
GPU has FP8 matrices (`VK_EXT_shader_float8`); the log says `Upscaler: FSR 4.1.1 replay, FP8 …`.
|
||||
`BB_FSR411_VARIANT=int8` forces INT8. Checked on an RX 7800 XT through vkd3d-proton's FP16
|
||||
emulation of FP8 (`BB_FSR4CAP_FP8=1` records it there too, `fp8emu/`): bit-exact with the DLL;
|
||||
**not yet run on RDNA4 hardware.**
|
||||
|
||||
**AppImage** (Steam Deck): `bash build.sh && bash packaging/appimage.sh` →
|
||||
`dist/Bloodborne-bbport-x86_64.AppImage`; data in `~/.local/share/bbport`, `--play` starts the
|
||||
game without the launcher window (Game Mode). FSR 4.1.1 models are not packaged: build them with
|
||||
the launcher's button (see above); they go to `~/.local/share/bbport/fsr4_411`
|
||||
(`BB_PACKAGE_FSR411=1` bundles a local `fsr4_411` into an AppImage for your own devices). On the Steam Deck pick the 1280×720 output (the
|
||||
game is 16:9; on the 1280×800 screen it gets thin bars).
|
||||
|
||||
**Adding the AppImage to Steam** (*Add a Non-Steam Game*) needs no options; the compatibility tool
|
||||
does not matter. (Steam preloads its overlay into every non-Steam game; the AppImage removes it
|
||||
before its own programs start, so the Steam overlay is not shown in the game.) Where Steam
|
||||
runs games without FUSE (NixOS: Steam's FHS sandbox; the AppImage then exits with *Cannot mount
|
||||
AppImage*), set the launch options to
|
||||
|
||||
```
|
||||
TMPDIR=$HOME/.cache APPIMAGE_EXTRACT_AND_RUN=1 NO_CLEANUP=1 %command%
|
||||
```
|
||||
|
||||
The AppImage then unpacks itself (~2 GB, `~/.cache/appimage_extracted_*`) on the first start
|
||||
(~10 s) and reuses that copy afterwards; a new AppImage version gets a new copy, the old one can
|
||||
be deleted. Without `TMPDIR` it would unpack into Steam's `/tmp`, which is in RAM there. Add
|
||||
` --play` after `%command%` to skip the launcher.
|
||||
|
||||
**NVIDIA in the AppImage:** startup discovers the host's installed 64-bit NVIDIA Vulkan ICD
|
||||
and exposes its vendor libraries alongside the bundled AMD/Intel drivers. This keeps the
|
||||
NVIDIA userspace driver matched to the host kernel module. Standard Linux distributions keep
|
||||
these libraries under `/usr/lib*`; on NixOS the AppImage's internal `/nix/store` may hide them.
|
||||
In that case copy the NVIDIA libraries into an accessible directory and set
|
||||
`BB_NVIDIA_LIB_DIR=/path/to/libraries` (the NVIDIA manifest must also be accessible).
|
||||
Explicit `VK_DRIVER_FILES`/`VK_ICD_FILENAMES` overrides are preserved. Diagnose drivers inside
|
||||
the package with:
|
||||
|
||||
```bash
|
||||
./Bloodborne-bbport-x86_64.AppImage --vulkan-info 2>&1 | tee bbport-vulkan.log
|
||||
```
|
||||
|
||||
A user reported successful startup with FSR 3 on a GTX 1060 6GB (Fedora 44, NVIDIA
|
||||
580.178.04); selecting FSR 4 caused a black window. Use FSR 3 on this configuration.
|
||||
|
||||
MangoHud is bundled in the AppImage; enable its checkbox in the launcher. If MangoHud is also
|
||||
installed system-wide, or Steam's performance overlay is on (Steam Deck game mode), only one
|
||||
overlay is drawn (two drew doubled, offset text).
|
||||
When running from source, install MangoHud separately. A diagnostic launch with
|
||||
`VK_LOADER_LAYERS_DISABLE=~implicit~` also disables MangoHud.
|
||||
|
||||
Useful variables: `BB_FRAME_STATS=1` (frame statistics, including a `Memory:` line: VRAM, GTT,
|
||||
RSS, images and guest blocks in VRAM), `BB_PC_MODEL=1` (the new memory and translation model,
|
||||
0, the old model as in 0.3, is the default), `BB_ANISO=N` (anisotropic filtering of scene textures; 16 by
|
||||
default, 0 = the game's own),
|
||||
`BB_GC_IDLE_SECONDS=N` / `BB_VRAM_IDLE_SECONDS=N` (how long unused textures / buffers stay in VRAM;
|
||||
20 / 60), `BB_BREADCRUMBS=0` (no GPU breadcrumbs; with them a GPU hang names the draw or dispatch
|
||||
it is stuck in), `BB_GPU_PROFILE=1` (GPU time per
|
||||
pass), `BB_FSR4_PROFILE=1` (GPU time per FSR 4 pass), `BB_UPSCALER=taa|fsr3|fsr4|fsr411|off|none`,
|
||||
`BB_FRAMES_AHEAD=N` (how many frames the GPU command thread may run ahead of the GPU; 1 by default,
|
||||
0 = unbounded), `BB_PRESENT_THREAD=0` (present on the vblank thread, as before),
|
||||
`BB_LIVE_RES=1` (live resolution changes instead of the startup patch for outputs other than 1080p),
|
||||
`BB_PAD_RECORD=file` / `BB_PAD_REPLAY=file` (record a route with F9, replay it in scripted tests),
|
||||
`BB_GC_BUDGET_MB=N` (texture cache budget, as on integrated GPUs), `BB_VRAM_LIMIT_MB=N` (the VRAM
|
||||
budget taken as at most N MiB: checks the behaviour of cards with little VRAM), `BB_ASYNC_PIPELINES=0`
|
||||
(new pipelines compile at once and the game waits; by default in the background), `BB_PRESENT_DUMP_TRIGGER=file`
|
||||
with `BB_PRESENT_DUMP_COUNT=N` (dump N consecutive presented frames),
|
||||
`BB_FSR411_VARIANT=int8|fp8|fp8emu` (FSR 4.1.1 variant; by default FP8 where the GPU has FP8
|
||||
matrices), `BB_READBACKS=0|1|2` (reads of GPU-written memory by the game: 1 by default, 2
|
||||
precise and slow, 0 off).
|
||||
More in [docs/](docs); recent changes: [docs/CHANGES_0.4.md](docs/CHANGES_0.4.md) (in Russian),
|
||||
[docs/CHANGES_2026-10-06.md](docs/CHANGES_2026-10-06.md).
|
||||
|
||||
**Online play (shadNet).** A separate module, `out/gpu/libbbnet.so` (`gpu/bbnet`: shadPS4's network
|
||||
and PSN libraries from shadp2p, a shadPS4 fork, with the shadNet client), built when protobuf,
|
||||
nlohmann_json, OpenSSL, zlib and miniupnpc are there (`shell.nix`) and included in the AppImage.
|
||||
The launcher's *Online* page turns it on (`BB_ONLINE=1`): messages, bloodstains and ghosts come
|
||||
from The Hunter's Dream, bells and summons go through a shadNet server (`srv.shadps4.net:31313` by
|
||||
default) with a shadNet account (Online ID and password). Offline the module is never loaded; a
|
||||
build without it plays offline only.
|
||||
|
||||
## Repository layout
|
||||
|
||||
| Path | Contents |
|
||||
|---|---|
|
||||
| `src/` | Loader (`probe.c`) and the HLE runtime |
|
||||
| `scripts/` | Offline preparation of the game image, module linking, patch compiler |
|
||||
| `gpu/` | Renderer library: vendored shadPS4 video core with this port's changes (`gpu/VENDOR.txt`), shims, ImGui menu, FSR 4.1.1 runtime (`gpu/shadps4/video_core/renderer_vulkan/fsr411`) |
|
||||
| `launcher/`, `packaging/` | GTK4 launcher; Nix package and AppImage |
|
||||
| `patches/` | Community patches for Bloodborne |
|
||||
| `tools/` | Developer tools: scripted runs, A/B toggles, FSR benchmark helpers, FSR 4 shader rewrites, `fsr4cap` (FSR 4.1.1 recording/extraction) |
|
||||
| `tests/` | Loader, runtime, patch and renderer tests |
|
||||
| `docs/` | Design notes and measurements ([upscaler](docs/upscaler.md), [parallel GPU](docs/parallel_gpu.md), [motion vectors](docs/motion_vectors.md), [roadmap](docs/ROADMAP.md)) |
|
||||
|
||||
Tests: `bash build.sh --test`, `python3 -m unittest discover -s tests`, and
|
||||
`ninja -C out/gpu motion-history-test ui-composition-test scene-resolution-test motion-shader-test settings-test`.
|
||||
|
||||
## Roadmap
|
||||
|
||||
- The new memory and translation model on by default once it is stable and tested on NVIDIA;
|
||||
the old model is removed after that.
|
||||
- Textures as objects created at load time (less guessing by address, uploads outside the
|
||||
frame).
|
||||
- Shaders translated ahead of time, at install, not during play.
|
||||
- More CPU parallelism in GPU command processing (split the draw-recording stage further),
|
||||
scaling to all hardware threads — most important for the Steam Deck.
|
||||
- Async compute for the upscaler (the frame is GPU-bound at 4K).
|
||||
- XeSS (super resolution) and XeFG frame generation through a Wine helper sharing Vulkan
|
||||
memory (a memory-bridge prototype is in `tools/bridge_helper`); inputs exposed so that OptiScaler-style mapping works.
|
||||
- Frame generation (FSR 3.1 FG first), reactive and transparency masks for particles and fog.
|
||||
- Fix the races in AMD's FSR 4.1.1 shaders at output widths that are not multiples of 64
|
||||
(e.g. 1600×900), as already done for the left-edge race in FSR 4 v07 at 1080p.
|
||||
- Steam Deck validation of the AppImage; HDR output.
|
||||
|
||||
## Credits and licenses
|
||||
|
||||
bbport is licensed under the **GNU GPL v2 or later** ([LICENSE](LICENSE)) — it contains code
|
||||
from shadPS4 (GPL-2.0-or-later). Third-party components keep their licenses:
|
||||
[shadPS4](https://github.com/shadps4-emu/shadPS4) video core and shader recompiler (GPL-2.0+),
|
||||
[sirit](https://github.com/shadps4-emu/sirit), [half](https://half.sourceforge.net/),
|
||||
[FSR-Vulkan](https://github.com/FireBurn/FSR-Vulkan) by FireBurn (MIT; FSR 3.1 on Vulkan and the
|
||||
FSR 4 v07 provider), AMD FidelityFX SDK (MIT), [LibAtrac9](https://github.com/Thealexbarney/LibAtrac9)
|
||||
(MIT), [Dear ImGui](https://github.com/ocornut/imgui) (MIT), DejaVu fonts,
|
||||
[dxil-spirv](https://github.com/HansKristian-Work/dxil-spirv) (MIT, used to build the
|
||||
FSR 4.1.1 assets). Game patches by Kyo, Lance McDonald, auser1337, illusion, emoose and other
|
||||
community members (`patches/Bloodborne.xml`). The DLSS bridge (`gpu/dlss_bridge`, MIT) and
|
||||
loader come from [Supermedo's Windows port](https://github.com/Supermedo/bloodborne_pc), adapted
|
||||
from [IFreemz/shadPS4-Bloodborne-DLSS-FSR](https://github.com/IFreemz/shadPS4-Bloodborne-DLSS-FSR).
|
||||
AMD's FSR 4 DLLs and model data and NVIDIA's DLSS SDK and libraries are not distributed here.
|
||||
@@ -2,7 +2,10 @@
|
||||
|
||||
# bbport — нативный порт Bloodborne для Linux
|
||||
|
||||
[English](README.md) · **Русский**
|
||||
> README оригинального проекта (deadinside28/bloodborne_pc), сохранён ради технических
|
||||
> подробностей. README этого форка — [README.md](README.md) (на английском).
|
||||
|
||||
[English](BBPORT.md) · **Русский**
|
||||
|
||||
bbport — это аналог Wine + DXVK, только для одной игры: *Bloodborne* для PlayStation 4
|
||||
(CUSA03173, версия игры 1.09) на ПК с Linux x86-64. Оригинальный исполняемый файл игры
|
||||
@@ -256,6 +259,14 @@ Experimental от октября 2026; Proton 11.0 из Steam и GE-Proton 9 —
|
||||
векторы движения, jitter). Без библиотек, на других видеокартах или с `BB_DLSS=0` он недоступен,
|
||||
а выбранный DLSS заменяется на FSR 3.1.
|
||||
|
||||
На Linux DLSS также нужен CUDA/UVM из драйвера NVIDIA. Если устройство не создаётся с
|
||||
расширениями DLSS, рендерер повторяет попытку без них и использует FSR 3.1. Если отсутствует
|
||||
`/dev/nvidia-uvm`, загрузи установленный модуль командой `sudo modprobe nvidia_uvm` и перезапусти
|
||||
игру. Пакет `nvidia-modprobe` из репозитория дистрибутива позволяет библиотекам NVIDIA загружать
|
||||
модуль по мере необходимости.
|
||||
Если при включённом Secure Boot команда `modprobe` сообщает `Key was rejected by service`,
|
||||
зарегистрируй ключ подписи модуля по [процедуре MOK своего дистрибутива](https://documentation.ubuntu.com/security/security-features/platform-protections/secure-boot/#mok-generation-and-signing-process).
|
||||
|
||||
AppImage, собранный с `DLSS_SDK_ROOT`, содержит мост и библиотеку NVIDIA (лицензия RTX SDK от NVIDIA
|
||||
это разрешает в составе приложения; её текст — `NVIDIA-DLSS-LICENSE.txt` в пакете). Более новая
|
||||
`libnvidia-ngx-dlss.so.<версия>` (из `lib/Linux_x86_64/rel` в SDK) выбирается в лаунчере:
|
||||
@@ -1,408 +1,167 @@
|
||||
THIS PROJECT IS NOT RELATED TO SHADPS4. ALL QUESTIONS RELATED TO THIS PROJECT SHOULD BE SENT TO THE DISCORD SERVER https://discord.gg/KYZRKk9CB, NOT TO THE SHADPS4 SERVER.
|
||||
# Bloodborne for Linux
|
||||
|
||||
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.
|
||||
|
||||
# bbport — a native Linux port of Bloodborne
|
||||
This fork builds on [bbport](https://github.com/deadinside28/bloodborne_pc), the native Linux port
|
||||
by deadinside28. It adds the features of [Supermedo's Windows port](https://github.com/Supermedo/bloodborne_pc)
|
||||
(online play, DLSS, cheats) brought to Linux, plus a few of its own.
|
||||
|
||||
**English** · [Русский](README.ru.md)
|
||||
> **No game files are included.** You need your own decrypted dump of Bloodborne with update
|
||||
> 1.09, from a retail release: [which ones, and which have been checked](docs/COMPATIBILITY.md).
|
||||
> This project is not affiliated with Sony Interactive Entertainment or FromSoftware.
|
||||
|
||||
bbport is the counterpart of Wine + DXVK for a single game: *Bloodborne* for PlayStation 4
|
||||
(CUSA03173, game version 1.09) on an x86-64 Linux PC. The game's original executable runs
|
||||
directly on the PC:
|
||||
## Features
|
||||
|
||||
- **as in Wine**, the game's x86-64 code runs on the CPU directly, and a runtime written for
|
||||
this one game replaces the PS4 system libraries;
|
||||
- **as in DXVK**, the game's graphics are translated to Vulkan — by a renderer derived from
|
||||
[shadPS4](https://github.com/shadps4-emu/shadPS4) and heavily extended for this game, including
|
||||
temporal upscaling with AMD FSR 3.1, FSR 4 and FSR 4.1.1;
|
||||
- memory and the GPU's work follow the PC model on AMD GPUs by default (the new memory and
|
||||
translation model); NVIDIA and other GPUs keep the old memory model of 0.3 for now. The
|
||||
launcher's *Memory model* switches either way.
|
||||
|
||||
One step remains to the full Wine + DXVK model: the new model on NVIDIA as well, then removing the
|
||||
old memory model (see below).
|
||||
|
||||
> **No game files are included.** You need your own dump of Bloodborne with update 1.09: any retail
|
||||
> release (CUSA00900 in the US, CUSA03173 for the European Game of the Year edition, ...), as they
|
||||
> share the same executable.
|
||||
> This project is not affiliated with Sony Interactive Entertainment, FromSoftware or AMD.
|
||||
|
||||
**Status: experimental, playable.** The game boots, loads saves and plays (the Hunter's Dream
|
||||
and several areas of Yharnam were played with it) with sound, gamepad and saving.
|
||||
A full play-through has not been verified, and only one machine (Linux, AMD Radeon RX 7800 XT,
|
||||
Mesa/RADV) has been tested thoroughly.
|
||||
|
||||
## Highlights
|
||||
|
||||
- **Native execution.** The eboot is converted offline into a flat memory image; PS4 libc and
|
||||
libSceFios2 are linked into it as native code. No CPU emulation and no per-instruction
|
||||
translation: the game code runs at full speed.
|
||||
- **Two memory models** (launcher → *Mode → Memory and translation model*; *Auto*, the default,
|
||||
picks by GPU):
|
||||
- **The old memory model, as in 0.3 — the default on NVIDIA, Intel and others.** VRAM copies
|
||||
of the game's memory, writes tracked through page protection. With every fix made since 0.3
|
||||
(motion vectors and the upscaler, flicker with DoF on, a damaged shader cache).
|
||||
- **The new memory and translation model — the default on AMD.** The game's memory lives
|
||||
in system RAM and the GPU reads it where it is, as a PC game's buffers; data it reads often
|
||||
is kept in VRAM and given back when unused (textures after 20 s, buffers after 60 s). The
|
||||
layer's memory module tracks CPU writes to mirrored blocks to keep those copies coherent;
|
||||
it does not copy the whole GPU-visible memory. The PS4 command processor's work
|
||||
is translated into Vulkan commands rather than emulated on the CPU: memory writes
|
||||
(`WRITE_DATA`, DMA) are done by the GPU in command-stream order, and fences are written by the
|
||||
GPU itself at the end of the pipeline, at the same point of the stream as the PS4's command
|
||||
processor writes them. On AMD the game's memory is kept in a sparse buffer (the arena); on
|
||||
NVIDIA, Intel and others it goes through the layer's memory module: bound in place, VRAM
|
||||
copies as separate buffers, no sparse rebinding (NVIDIA's sparse binding froze the GPU for
|
||||
seconds); `BB_LAYER_MEMORY=1/0` chooses by hand. Tested on an RX 7800 XT. On NVIDIA it can be chosen by hand (*New*); testers' results differ by scene,
|
||||
some scenes are slower than with the old model, so it is not the default there yet. Not
|
||||
tested on Intel. Without the launcher: `BB_PC_MODEL=1/0` (unset: by GPU, as *Auto*).
|
||||
|
||||
[Changes in 0.5](docs/CHANGES_0.5.md); the prereleases: [pre4](docs/CHANGES_0.5-pre4.md),
|
||||
[pre3](docs/CHANGES_0.5-pre3.md), [pre2](docs/CHANGES_0.5-pre2.md).
|
||||
|
||||
Unused textures are freed in both modes, so VRAM no longer grows with every area visited.
|
||||
- **Unlocked frame rate.** Community patches (`patches/Bloodborne.xml`) make the simulation
|
||||
use the real frame time; ~90 FPS at 4K with FSR 4 Balanced on an RX 7800 XT, ~150 FPS at
|
||||
1440p with FSR 4 Quality. Also 30/60/90 FPS modes.
|
||||
- **Temporal upscaling built for this game.** Bloodborne has no velocity buffer, so bbport
|
||||
computes motion vectors itself: camera motion from depth and the scene matrices, and object
|
||||
motion (characters, cloth, weapons) from the vertex positions of the previous frame. The
|
||||
scene is jittered sub-pixel (Halton) and rendered at a reduced resolution; the upscaler fills
|
||||
the output (720p for the Steam Deck, 1080p, 1440p or 2160p) and the UI is drawn natively at the output resolution.
|
||||
- **FSR 3.1** (FireBurn/FSR-Vulkan).
|
||||
- **FSR 4 (INT8, model v07)** on GPUs exposing the required Vulkan shader features —
|
||||
RDNA2/3 included (see Requirements).
|
||||
- **FSR 4.1.1**: AMD's 4.1.1 DLL is recorded once under vkd3d-proton and its passes are
|
||||
replayed natively on Vulkan; the output is **bit-exact** with the DLL. Two variants, as in
|
||||
the DLL: INT8 on any GPU with the required shader features, and FP8 matrices on RDNA4 (RX
|
||||
9000; picked automatically). The assets are built on your machine from one DLL of your own
|
||||
(4.1.x): the launcher's *FSR 4.1.1 from your own AMD DLL → Choose DLL…* button (2–5 minutes,
|
||||
twice that on RDNA4; needs a recent Proton), or `tools/fsr4cap`.
|
||||
- Faster than AMD's own shaders on RDNA3: the final passes of FSR 4 and 4.1.1 were rewritten
|
||||
to store through workgroup memory (3.5× and 2.3× faster, bit-exact); FSR 4 costs ~4 ms at
|
||||
4K on an RX 7800 XT instead of ~6 ms.
|
||||
- **Multi-threaded GPU command processing.** The PS4 command stream is decoded on one thread
|
||||
and draws are bound and recorded on another (two-stage pipeline), with a Vulkan recording
|
||||
thread and helper threads for memory copies. Early on the single GPU thread capped the game
|
||||
at ~26 FPS; now it runs at 90–150 FPS depending on resolution and scene.
|
||||
- **Settings inside the game's own menu:** *System → Display / Effects* (after *Screen/Sound*),
|
||||
built and drawn by the game itself — output resolution, upscaler, preset, sharpness, FPS
|
||||
counter, model detail and the game's effects, in the game's language.
|
||||
- **Overlay menu** (Insert or L3+R3), styled like the game's dialogs: everything above plus the
|
||||
advanced settings, with gamepad, keyboard and mouse.
|
||||
- **GTK4 launcher** and an **AppImage** for the Steam Deck.
|
||||
|
||||
## How it differs from shadPS4
|
||||
|
||||
| | shadPS4 | bbport |
|
||||
|---|---|---|
|
||||
| Scope | General PS4 emulator, many games | One game: Bloodborne v1.09 |
|
||||
| Loading | Its own ELF loader and kernel emulation at run time | The eboot is converted offline (`scripts/`) into an image with PS4 libc/Fios2 linked in; a C loader maps it and jumps into the game (loader and runtime: ~5k lines) |
|
||||
| Memory | The GPU's view of PS4 memory is kept in VRAM copies, synchronized through page-protection write tracking | By default the same model (as in 0.3); in the *New memory and translation model* mode: the game's memory in system RAM, used by the GPU in place, frequently read data in VRAM, freed when unused |
|
||||
| Command processor | Emulated: memory writes, DMA and fences are done by the CPU while decoding | By default the same; in the new mode translated into Vulkan commands that the GPU runs in stream order, fences written after the work has really finished |
|
||||
| System libraries | Broad HLE of the PS4 OS | A small runtime (`src/runtime_*.c`) that implements exactly what Bloodborne calls: memory, threads, sync, files, audio (incl. ATRAC9), pad, saves, AppContent |
|
||||
| GPU | shadPS4 video core and shader recompiler | The same core (vendored, GPL) with ~200 marked changes (`bbport:`) plus new modules: two-stage draw pipeline, render-state and texture-set memoization, render-scale proxies, motion vectors, FSR 3.1/4/4.1.1, frame capture and GPU profiler |
|
||||
| GPU thread | One thread processes the whole command stream (the bottleneck in Bloodborne) | Decode and draw recording run on separate threads; the work scales with the hardware threads (Steam Deck included) |
|
||||
| Upscaling | — | Temporal (FSR 3.1, FSR 4, FSR 4.1.1) with the game's own motion vectors and jitter |
|
||||
| Game patches | Patch files applied by the emulator | The same community patches, compiled at start (`scripts/patches.py`); render resolution, effects and FPS from the launcher |
|
||||
|
||||
Without shadPS4 there would be no bbport: its renderer and shader recompiler are the base of
|
||||
the graphics side.
|
||||
|
||||
### Wine + DXVK for one game
|
||||
|
||||
- **CPU.** The PS4 CPU is x86-64, so the game's code runs directly on the PC's CPU, with no
|
||||
emulation and no instruction translation.
|
||||
- **System libraries.** As Wine replaces the Windows API, the bbport runtime (`src/runtime_*.c`)
|
||||
implements exactly the PS4 OS functions Bloodborne calls: memory, threads, files, audio, pad,
|
||||
saves.
|
||||
- **Memory.** In the new mode, as in a PC game: the game's data in system RAM, VRAM used the way
|
||||
a PC game uses it. The mode is still experimental.
|
||||
- **Graphics.** The PS4 GPU's command stream (PM4) is a recording of the game's graphics API
|
||||
calls: it is written by 99 functions of the statically linked libGnm, so decoding the stream
|
||||
and translating the calls themselves come to the same thing. GCN shaders are translated to
|
||||
SPIR-V. In the new mode the command processor's work (memory writes, DMA, fences) is
|
||||
translated into Vulkan commands as well; fences are written by the GPU itself. Resource
|
||||
descriptors (textures, buffers) are read by the translator on the CPU, as DXVK reads resource
|
||||
bindings: a PS4 texture has to be converted from the PS4 tiling into a Vulkan image before the
|
||||
draw runs, so the CPU must know its descriptor beforehand — translation, not emulation. The
|
||||
descriptor tables (the constant engine's dumps) are read only by the translator, so the CPU
|
||||
performs them. The game's occlusion queries (~60 a frame) are
|
||||
Vulkan occlusion queries; predication, `COPY_DATA` and `COND_EXEC` are translated too (check:
|
||||
`BB_PM4_SELFTEST=1`). Details:
|
||||
[docs/EMULATION_REMOVAL_PLAN.ru.md](docs/EMULATION_REMOVAL_PLAN.ru.md), "Шаг 4" (Russian).
|
||||
|
||||
"Port" here means a build for this one game, not a rewrite of its source code, which the project
|
||||
neither has nor includes.
|
||||
- **Online play (beta)** through [The Hunter's Dream](https://thehuntersdream.com) and shadNet:
|
||||
messages, bloodstains, ghosts, bells and summons. Linux and Windows players can play together,
|
||||
through shadPS4's public server or [one of your own](docs/SERVER.md).
|
||||
- **Upscaling:** AMD FSR 3.1, FSR 4 and FSR 4.1.1, **NVIDIA DLSS** on GeForce RTX, and
|
||||
native-resolution TAA.
|
||||
- **Unlocked frame rate**, capped at your display's refresh rate up to 120 FPS (above that the
|
||||
game's own movement timing breaks), or 30/60/90 FPS. Output from 720p to 4K.
|
||||
- **Launcher** with every setting in one window, in English, Spanish, Italian, French, German,
|
||||
Portuguese and Russian, like the in-game menu (Insert or L3+R3).
|
||||
- **Settings in the game's own System menu** as well (Display and Effects pages), in the game's
|
||||
language.
|
||||
- **Two memory models:** the new one, where the GPU works with the game's memory as in a PC game
|
||||
(the default on AMD), and the one of 0.3 (the default elsewhere); the launcher switches them.
|
||||
- **Xbox, Switch and keyboard button prompts**, picked by the controller in use, and a gestures
|
||||
menu without the motion-control icons.
|
||||
- **Cheats and gameplay tweaks:** never die, stealth, silent steps, enemy control, camera
|
||||
distance, ragdoll physics and more.
|
||||
- **Trophies:** a notice in the game, in the style of the PS4's, and a Trophies page in the
|
||||
launcher with every trophy, from your own dump. Their names and descriptions need the PS4's
|
||||
trophy key (shadPS4's TrophyKey), which you enter in the launcher; without it, only the icons.
|
||||
- **Game effects** on and off, **mods** and community **patches**, loaded without changing the
|
||||
game files ([how](docs/MODS.en.md)).
|
||||
- **Add to Steam** in one click (native or Flatpak Steam), and **Discord Rich Presence** with the
|
||||
area you are in and your co-op partners.
|
||||
- **AppImage:** one file with everything inside, for any distribution and the Steam Deck. It
|
||||
updates itself: the launcher's **About** page checks this repository's releases and installs
|
||||
a new AppImage over the old one.
|
||||
|
||||
## Requirements
|
||||
|
||||
- Linux x86-64, a Vulkan 1.3 GPU. Tested: AMD RX 7800 XT with Mesa 26 (RADV).
|
||||
The *New memory and translation model* mode is not tested on NVIDIA yet.
|
||||
FSR 4 / 4.1.1 require shader Float16, Int8/Int16, integer dot products, linear compute
|
||||
derivatives and extended storage image formats; FSR 4.1.1 additionally requires
|
||||
`VK_VALVE_shader_mixed_float_dot_product`. Unsupported choices fall back to FSR 3.1
|
||||
before the first frame and are disabled in the in-game menu.
|
||||
- Your decrypted game dump: the `CUSA03173` folder (eboot.bin, sce_module, ...), version 1.09.
|
||||
A dumped update is a separate folder: copy it over the base game, replacing files. The base
|
||||
game alone (1.00) crashes at start (guest offset 0x20348b8); the launcher and `run.sh` check
|
||||
the executable and say what is missing (`BB_SKIP_GAME_CHECK=1` skips the check). The other
|
||||
retail releases (the US `CUSA00900`, the Asian `CUSA03023`, ...) run the same 1.09 executable. Some dumps ship it
|
||||
with Lance McDonald's 60 fps patch baked in, which crashes when the gestures menu opens: the
|
||||
check asks for a clean eboot.bin then (bbport has its own 60 fps).
|
||||
- To build: GCC, CMake, Ninja, Python 3, glslang, SDL3, Vulkan headers and the libraries in
|
||||
`shell.nix`. With [Nix](https://nixos.org) everything comes from `shell.nix` automatically.
|
||||
- Linux x86-64 and a GPU with Vulkan 1.3: AMD or Intel with Mesa, or NVIDIA with NVIDIA's driver
|
||||
(open kernel modules included). Mesa's NVK has not been tested.
|
||||
- DLSS needs a GeForce RTX (20 series or newer) and NVIDIA's driver; it does not work with NVK.
|
||||
- About 6 GB of free memory, 10 GB for 1440p or 4K output.
|
||||
|
||||
## Build and run
|
||||
## Getting started
|
||||
|
||||
### AppImage
|
||||
|
||||
Build it with `bash build.sh && bash packaging/appimage-sharun.sh`, then run
|
||||
`dist/Bloodborne-bbport-x86_64.AppImage`. Choose your game folder (the one with `eboot.bin`) and
|
||||
press **Play**. Saves and settings go to `~/.local/share/bbport`.
|
||||
|
||||
### From source
|
||||
|
||||
On Arch Linux (or CachyOS, Manjaro…):
|
||||
|
||||
```bash
|
||||
git clone --recursive https://github.com/deadinside28/bloodborne_pc.git bbport && cd bbport
|
||||
bash build.sh # builds out/bb-probe and out/gpu/libbbgpu.so
|
||||
BB_GAME_DIR=/path/to/CUSA03173 bash run.sh
|
||||
sudo pacman -S --needed base-devel cmake ninja python python-gobject gtk4 libadwaita sdl3 \
|
||||
vulkan-headers vulkan-icd-loader glslang spirv-headers spirv-cross ffmpeg boost fmt \
|
||||
robin-map xxhash zydis miniz protobuf nlohmann-json openssl miniupnpc python-pillow ttf-dejavu
|
||||
paru -S magic_enum vulkan-memory-allocator xbyak # from the AUR
|
||||
|
||||
git clone --recursive https://github.com/ayozetr/bloodborne_linux
|
||||
cd bloodborne_linux
|
||||
bash build.sh
|
||||
python3 launcher/bbport_launcher.py # or launcher/install-desktop.sh for a menu entry
|
||||
```
|
||||
|
||||
or the launcher (pick the game folder, settings, *Start*):
|
||||
With [Nix](https://nixos.org), `bash build.sh` gets every dependency from `shell.nix` by itself.
|
||||
|
||||
```bash
|
||||
bash launcher/bb-launcher.sh # launcher/install-desktop.sh adds it to the app menu
|
||||
```
|
||||
### Online play
|
||||
|
||||
By default the game folder is expected next to the repository (`../CUSA03173`). Saves and the
|
||||
shader cache go to `user/` (the launcher lets you choose another folder); settings to
|
||||
`bbport.ini`. A gamepad is used through SDL3 (the launcher's *Controls → Controller* picks one
|
||||
when several are connected; `BB_GAMEPAD=<GUID or part of the name>`). The keyboard works too,
|
||||
also next to a connected gamepad (the Steam Deck always has one); both are remapped in the
|
||||
launcher (*Controls*). The character name is typed on the keyboard in a box over the game.
|
||||
The touchpad: its left half (Tab, Back/Select) opens the gestures, the right half (Backspace)
|
||||
the key items. There is no PSN: the *Play online / offline* screen is skipped and the game opens
|
||||
its main menu offline (a game patch; the launcher's *Game effects → Skip the “play online / offline” choice*,
|
||||
`BB_SKIP_NETWORK_CHOICE=0` shows it). With several monitors, *Screen → Monitor* in the launcher
|
||||
(`BB_DISPLAY=<number or part of the name>`) picks the one for the game.
|
||||
1. Register a shadNet account at [shadnet.shadps4.net](https://shadnet.shadps4.net), or get one
|
||||
from the owner of your group's own server.
|
||||
2. In the launcher, open **Online**, turn on **Play online**, choose the shadNet server and enter
|
||||
your account name (not your email) and password. **Test the connection**: every line should
|
||||
be green.
|
||||
3. Start the game and choose online at the title screen.
|
||||
|
||||
**Resolution and preset changes:** for outputs other than 1080p (720p on the Steam Deck,
|
||||
1440p, 4K) the whole game renders at the preset's resolution, set by a patch at start — the
|
||||
fastest path. Changing the output or the preset in the in-game menu then needs *Apply and
|
||||
restart the game*. The *Live resolution changes* setting (launcher, in-game menu,
|
||||
`bbport.ini` `live_resolution=0|1|auto`; **off by default**) instead keeps the game at
|
||||
1080p internally and scales its render targets at run time, so 720p/1080p/1440p/4K and the
|
||||
presets switch without a restart. It costs more: the game then believes it renders 1080p
|
||||
and draws more (e.g. ~8× more small lights), and some targets are copied between sizes —
|
||||
use it on strong desktop GPUs only (`auto` turns it on for discrete GPUs with 8+ GB that are
|
||||
not pre-Turing NVIDIA). 1080p output and TAA always use the live path.
|
||||
Everyone in a session needs game version 1.09 and the same servers. Turn cheats off when you
|
||||
play with others.
|
||||
|
||||
**TAA:** a separate native-resolution temporal AA mode in the launcher and overlay,
|
||||
switchable live without an FSR model. The saved FSR preset is restored when returning
|
||||
to FSR. FSR Native AA adds reconstruction on top of full-resolution rendering and can
|
||||
be slower than disabling AA. TAA also adds work compared with no temporal AA.
|
||||
The RCAS switch and the 0–2 sharpness control also work with TAA. Sharpening runs after
|
||||
temporal accumulation and leaves its history and HUD unchanged.
|
||||
**Co-op needs your own server for now:** shadPS4's public server does not answer on its UDP port,
|
||||
so summons do not connect through it (messages, bloodstains and ghosts do).
|
||||
[docs/SERVER.md](docs/SERVER.md) sets one up with Docker and Tailscale; it has been tested with a
|
||||
Linux and a Windows player.
|
||||
|
||||
**Mods:** the launcher accepts separate loose-file mod folders (with `dvdroot_ps4/`, an extra
|
||||
wrapper folder, or the game folders such as `chr/` directly; file name case does not matter),
|
||||
with enable switches and load order. A sibling `CUSA03173-mods/` overlay also works.
|
||||
The original game is preserved; later mods override conflicting files.
|
||||
**Third-party patches:** shadPS4-format XML patch files in the data directory's `patches/`,
|
||||
switched on and off in the launcher. See [mods and patches](docs/MODS.md).
|
||||
**DLC:** put your add-on dumps in `user/addcont/<title id>/<entitlement label>/` (shadPS4's
|
||||
layout, e.g. `user/addcont/CUSA03173/SPEXPANSIONDLC03` for The Old Hunters): each folder there is
|
||||
reported to the game as an installed add-on.
|
||||
Co-op connects the players directly over UDP. UPnP opens the port on the router by itself; with
|
||||
ufw or firewalld on, the launcher's **Online** page says so and gives the commands: pin the
|
||||
**Co-op port** (for example 3658), open it (`sudo ufw allow 3658/udp`), and for UPnP let the
|
||||
router's answers in (`sudo ufw allow from <router> proto udp`). [More in docs/ONLINE.md](docs/ONLINE.md).
|
||||
|
||||
**Launcher language:** Russian, English, Brazilian Portuguese or Simplified Chinese (follows the system language by default).
|
||||
### DLSS
|
||||
|
||||
**Free camera and game debug menu** (v1.09): enable the corresponding switches in the
|
||||
launcher or in-game menu and restart. Free camera uses Lance McDonald's
|
||||
[GoldHEN patch](https://github.com/GoldHEN/GoldHEN_Patch_Repository/blob/main/patches/xml/Bloodborne-Orbis.xml):
|
||||
hold Cross and press L3 to cycle modes (keyboard: hold Space and press Z). It needs no fonts
|
||||
and conflicts with *Enemy Control*.
|
||||
For the game debug menu, install `DbgFont14h.ccm` and `DbgFont14h.tpf` from
|
||||
[Debug Menu and XML Patch](https://www.nexusmods.com/bloodborne/mods/253) into the game's
|
||||
`dvdroot_ps4/font/` first. Startup rejects missing or empty font files instead of launching
|
||||
the unsafe patch. Open it with the left touchpad / Tab (with the debug menu on, the left half
|
||||
no longer opens the gestures); Backspace is the right touchpad.
|
||||
Touch coordinates are forwarded from SDL gamepads; Back/Select emulates a left click on
|
||||
pads without a touch surface. The port's settings menu remains Insert / L3+R3.
|
||||
On a PC with a GeForce RTX, run `bash tools/build_dlss.sh` once after `build.sh`. It downloads
|
||||
what it needs from the NVIDIA DLSS SDK and builds the bridge. Then choose **DLSS** as the
|
||||
upscaler.
|
||||
|
||||
GPU occlusion queries still use synthetic pixel counters (`PixelPipeStatDump`), and
|
||||
`IT_SET_PREDICATION` is unimplemented. Free camera allows visual investigation; it does
|
||||
not implement GPU occlusion culling.
|
||||
### DLC
|
||||
|
||||
**Upscaler assets** (not included; FSR 3.1 needs none):
|
||||
The Game of the Year edition (`CUSA03173`) already includes The Old Hunters. With the base game
|
||||
(such as the US `CUSA00900`), dump your add-ons too: each one is a folder (`SPEXPANSIONDLC03`,
|
||||
`SPDLCMESSENGER00`…) with only `sce_sys` inside, because their data ships with the 1.09 update.
|
||||
Put them in one folder and pick it in the launcher (**Game → DLC folder**), or set
|
||||
`BB_DLC_DIR=/path/to/dlc` for `run.sh`; the launcher shows how many it found. Each folder must
|
||||
keep the add-on's name (its entitlement label, such as `SPEXPANSIONDLC03`). Copying them to
|
||||
`user/addcont/<title id>/`, shadPS4's layout, works too.
|
||||
|
||||
```bash
|
||||
bash tools/fetch_fsr4_assets.sh # FSR 4 v07 (MIT, built from AMD's source by Q2RTX)
|
||||
# FSR 4.1.1, from your own AMD DLLs (e.g. OptiScaler's FSR4_LATEST), needs GE-Proton 10 or newer:
|
||||
bash tools/fsr4cap/build_assets.sh <amd_fidelityfx_upscaler_dx12.dll>
|
||||
```
|
||||
### Add to Steam
|
||||
|
||||
**FSR 4.1.1 from your own DLL.** The easiest way is the launcher: *Upscaler → FSR 4.1.1 from your
|
||||
own AMD DLL → Choose DLL…*, then pick `amd_fidelityfx_upscaler_dx12.dll` version 4.1.x: from
|
||||
OptiScaler's `FSR4_LATEST` folder or from a game with FSR 4.1. That one file is enough: AMD's DLL
|
||||
exports the FidelityFX API itself, so no loader is needed (only a DLL without these exports would
|
||||
be recorded through `amd_fidelityfx_loader_dx12.dll`, and the launcher would ask for it). The DLL is checked at once,
|
||||
without a long capture; these do not fit: AMD's official FSR 4.0.x (e.g. Pragmata's 4.0.3 —
|
||||
AMD enables it on RDNA4 only, and under Proton it does not start on other GPUs) and community
|
||||
builds (4.0.2b, 4.1.1b and similar OptiScaler INT8 builds: another model and pass count). Then
|
||||
the DLL runs under Proton and is recorded at 20 sizes (progress in the row and in the *Log* tab;
|
||||
on RDNA4 a second time, for the FP8 variant),
|
||||
the passes are translated to SPIR-V, and FSR 4.1.1 is selected. The Proton build is picked
|
||||
automatically — the first under which the DLL enables FSR 4.1: GE-Proton 10 or newer,
|
||||
Proton-CachyOS, Proton Experimental (tested: GE-Proton 11, Proton-CachyOS 11, Experimental of
|
||||
October 2026; Steam's Proton 11.0 and GE-Proton 9 do not). It runs in the Steam runtime it
|
||||
requires (Steam installs it the first time any game runs with that Proton), or through
|
||||
umu-launcher when installed. On NixOS umu-launcher comes from `nix-shell` (downloaded the first
|
||||
time, ~1.7 GB); the AppImage runs the recording on the system itself, through the user's systemd
|
||||
(`systemd-run --user`), since the system's `/nix` is out of its sight. Recording on the Steam Deck is not verified yet; if the DLL does not
|
||||
enable FSR 4.1 there, build on a PC and copy the `fsr4_411` folder. The same from the command
|
||||
line: the launcher's (and the AppImage's) `--build-fsr411 <DLL>`.
|
||||
**Game → Add to Steam** puts Bloodborne in your Steam library; it starts with the launcher's
|
||||
settings. Steam has to be closed while the game is added, and the launcher offers to do that.
|
||||
Flatpak Steam runs games inside its sandbox, which has neither the game nor your drivers, so
|
||||
the shortcut starts the game outside it with `flatpak-spawn --host`. That needs the
|
||||
`org.freedesktop.Flatpak` permission for Steam, which the launcher asks for once: it lets Steam
|
||||
start programs on your system.
|
||||
|
||||
**DLSS (NVIDIA GeForce RTX).** NVIDIA's DLSS SDK is not included: check out
|
||||
[github.com/NVIDIA/DLSS](https://github.com/NVIDIA/DLSS) (`include/` and `lib/Linux_x86_64` are
|
||||
enough) and build with `DLSS_SDK_ROOT=/path/to/DLSS bash build.sh`. That also builds the bridge
|
||||
`out/libbbport_dlss.so` (`gpu/dlss_bridge`, the only code that uses the SDK) and copies NVIDIA's
|
||||
`libnvidia-ngx-dlss.so.<version>` next to `out/bb-probe`; the driver provides the rest. Then pick
|
||||
*DLSS* in the launcher or the in-game menu: the preset sets the render size, Native AA is DLAA.
|
||||
DLSS gets the inputs FSR 3.1 gets (scene color, depth, motion vectors, jitter). Without the
|
||||
libraries, on other GPUs or with `BB_DLSS=0` it is listed as unavailable, and a DLSS setting falls
|
||||
back to FSR 3.1.
|
||||
### Button prompts
|
||||
|
||||
The AppImage built with `DLSS_SDK_ROOT` carries the bridge and NVIDIA's library (NVIDIA's RTX SDK
|
||||
license allows that in an application; its text is `NVIDIA-DLSS-LICENSE.txt` in the package). A
|
||||
newer `libnvidia-ngx-dlss.so.<version>` (from the SDK's `lib/Linux_x86_64/rel`) is chosen in the
|
||||
launcher: *Upscaler → DLSS: NVIDIA's library → Choose a file…*, as the FSR 4.1.1 DLL; it is
|
||||
copied to `<user>/dlss/`, which the game prefers. A game's `nvngx_dlss.dll` is the Windows
|
||||
library: NVIDIA's Linux driver does not load it.
|
||||
The game shows PlayStation buttons. In the launcher, **Controls → Button prompts** switches them to
|
||||
Xbox, Nintendo Switch or keyboard ones; *Automatic* follows the controller in use, and the
|
||||
keyboard's when none is connected. Keyboard prompts show the keys bound in **Controls →
|
||||
Keyboard**. **Gestures menu without motion-control icons** cleans up the gestures menu. The
|
||||
prompts are put into your own game's menu files at each start, and nothing in the game folder
|
||||
changes.
|
||||
|
||||
From source the script takes its tools from the system (MinGW GCC, CMake, Ninja, Python 3,
|
||||
SPIRV-Tools, Git) or from Nix; the AppImage has them prebuilt.
|
||||
## Support
|
||||
|
||||
**FP8 on RDNA4.** When vkd3d-proton offers FP8 cooperative matrices (RDNA4), the DLL runs
|
||||
another variant of its passes: other shaders and weights, other dispatch sizes. The build records
|
||||
both, INT8 into `fsr4_411/` and FP8 into `fsr4_411/fp8/`, and the game picks FP8 itself when the
|
||||
GPU has FP8 matrices (`VK_EXT_shader_float8`); the log says `Upscaler: FSR 4.1.1 replay, FP8 …`.
|
||||
`BB_FSR411_VARIANT=int8` forces INT8. Checked on an RX 7800 XT through vkd3d-proton's FP16
|
||||
emulation of FP8 (`BB_FSR4CAP_FP8=1` records it there too, `fp8emu/`): bit-exact with the DLL;
|
||||
**not yet run on RDNA4 hardware.**
|
||||
If this fork is useful to you and you would like to support it, you can on
|
||||
[Ko-fi](https://ko-fi.com/ayozetr). The port itself is deadinside28's work (see below).
|
||||
|
||||
**AppImage** (Steam Deck): `bash build.sh && bash packaging/appimage.sh` →
|
||||
`dist/Bloodborne-bbport-x86_64.AppImage`; data in `~/.local/share/bbport`, `--play` starts the
|
||||
game without the launcher window (Game Mode). FSR 4.1.1 models are not packaged: build them with
|
||||
the launcher's button (see above); they go to `~/.local/share/bbport/fsr4_411`
|
||||
(`BB_PACKAGE_FSR411=1` bundles a local `fsr4_411` into an AppImage for your own devices). On the Steam Deck pick the 1280×720 output (the
|
||||
game is 16:9; on the 1280×800 screen it gets thin bars).
|
||||
## Credits
|
||||
|
||||
**Adding the AppImage to Steam** (*Add a Non-Steam Game*) needs no options; the compatibility tool
|
||||
does not matter. (Steam preloads its overlay into every non-Steam game; the AppImage removes it
|
||||
before its own programs start, so the Steam overlay is not shown in the game.) Where Steam
|
||||
runs games without FUSE (NixOS: Steam's FHS sandbox; the AppImage then exits with *Cannot mount
|
||||
AppImage*), set the launch options to
|
||||
- [deadinside28/bloodborne_pc](https://github.com/deadinside28/bloodborne_pc): **bbport**, the
|
||||
native Linux port this fork is built on, and almost all of the work behind it. Its README, with
|
||||
the technical details, is in [BBPORT.md](BBPORT.md).
|
||||
- [Supermedo/bloodborne_pc](https://github.com/Supermedo/bloodborne_pc): the Windows port, whose
|
||||
online play, DLSS support, cheats page, Add to Steam and icon were brought to Linux here.
|
||||
- [shadPS4](https://github.com/shadps4-emu/shadPS4): the renderer and shader recompiler.
|
||||
- [shadp2p](https://github.com/Wozzardman/shadp2p) by Wozzardman: the network and PSN libraries
|
||||
for online play. Servers by the [shadPS4](https://shadps4.net) team and
|
||||
[The Hunter's Dream](https://thehuntersdream.com) by droogie and its team.
|
||||
- [IFreemz/shadPS4-Bloodborne-DLSS-FSR](https://github.com/IFreemz/shadPS4-Bloodborne-DLSS-FSR):
|
||||
the DLSS bridge, and the [NVIDIA DLSS SDK](https://github.com/NVIDIA/DLSS).
|
||||
- [FSR-Vulkan](https://github.com/FireBurn/FSR-Vulkan) by FireBurn and the AMD FidelityFX SDK.
|
||||
- **Dommo**: the Xbox and Switch button prompts (*DS3 Style - Modern Xbox prompts*, *DS3 Style -
|
||||
Nintendo Switch prompts*) and the *Stripped down gesture menu*, from
|
||||
[Nexus Mods (Bloodborne, mod 30)](https://www.nexusmods.com/bloodborne/mods/30), uploaded by
|
||||
goomab. Only their drawn prompts are in this repository ([assets/menu_prompts](assets/menu_prompts)).
|
||||
- **Abken**: the [KBM Icons Generator](https://www.nexusmods.com/bloodborne/mods/537), which showed
|
||||
which textures keyboard prompts need (after the idea of
|
||||
[mod 460](https://www.nexusmods.com/bloodborne/mods/460)); bbport draws its own.
|
||||
- [quick-sharun](https://github.com/pkgforge-dev/Anylinux-AppImages) for the AppImage.
|
||||
- Game patches by Kyo, Lance McDonald, auser1337, illusion, emoose and the Bloodborne community.
|
||||
|
||||
```
|
||||
TMPDIR=$HOME/.cache APPIMAGE_EXTRACT_AND_RUN=1 NO_CLEANUP=1 %command%
|
||||
```
|
||||
Full list of components and licenses: [BBPORT.md](BBPORT.md#credits-and-licenses).
|
||||
|
||||
The AppImage then unpacks itself (~2 GB, `~/.cache/appimage_extracted_*`) on the first start
|
||||
(~10 s) and reuses that copy afterwards; a new AppImage version gets a new copy, the old one can
|
||||
be deleted. Without `TMPDIR` it would unpack into Steam's `/tmp`, which is in RAM there. Add
|
||||
` --play` after `%command%` to skip the launcher.
|
||||
## License
|
||||
|
||||
**NVIDIA in the AppImage:** startup discovers the host's installed 64-bit NVIDIA Vulkan ICD
|
||||
and exposes its vendor libraries alongside the bundled AMD/Intel drivers. This keeps the
|
||||
NVIDIA userspace driver matched to the host kernel module. Standard Linux distributions keep
|
||||
these libraries under `/usr/lib*`; on NixOS the AppImage's internal `/nix/store` may hide them.
|
||||
In that case copy the NVIDIA libraries into an accessible directory and set
|
||||
`BB_NVIDIA_LIB_DIR=/path/to/libraries` (the NVIDIA manifest must also be accessible).
|
||||
With the host's NVIDIA driver the AppImage also passes the host's EGL vendor files, NVIDIA's
|
||||
first (`__EGL_VENDOR_LIBRARY_FILENAMES`; without them NVIDIA's ICD gave no `vkCreateInstance` on
|
||||
some hosts, #107; `BB_NVIDIA_EGL=0`: off). Explicit `VK_DRIVER_FILES`/`VK_ICD_FILENAMES` overrides are preserved. Diagnose drivers inside
|
||||
the package with:
|
||||
|
||||
```bash
|
||||
./Bloodborne-bbport-x86_64.AppImage --vulkan-info 2>&1 | tee bbport-vulkan.log
|
||||
```
|
||||
|
||||
A user reported successful startup with FSR 3 on a GTX 1060 6GB (Fedora 44, NVIDIA
|
||||
580.178.04); selecting FSR 4 caused a black window. Use FSR 3 on this configuration.
|
||||
|
||||
MangoHud is bundled in the AppImage; enable its checkbox in the launcher. If MangoHud is also
|
||||
installed system-wide, or Steam's performance overlay is on (Steam Deck game mode), only one
|
||||
overlay is drawn (two drew doubled, offset text).
|
||||
When running from source, install MangoHud separately. A diagnostic launch with
|
||||
`VK_LOADER_LAYERS_DISABLE=~implicit~` also disables MangoHud.
|
||||
|
||||
Useful variables: `BB_FRAME_STATS=1` (frame statistics, including a `Memory:` line: VRAM, GTT,
|
||||
RSS, images and guest blocks in VRAM), `BB_PC_MODEL=1` (the new memory and translation model,
|
||||
0, the old model as in 0.3, is the default), `BB_ANISO=N` (anisotropic filtering of scene textures; 16 by
|
||||
default, 0 = the game's own),
|
||||
`BB_GC_IDLE_SECONDS=N` / `BB_VRAM_IDLE_SECONDS=N` (how long unused textures / buffers stay in VRAM;
|
||||
20 / 60), `BB_BREADCRUMBS=0` (no GPU breadcrumbs; with them a GPU hang names the draw or dispatch
|
||||
it is stuck in), `BB_GPU_PROFILE=1` (GPU time per
|
||||
pass), `BB_FSR4_PROFILE=1` (GPU time per FSR 4 pass), `BB_UPSCALER=taa|fsr3|fsr4|fsr411|off|none`,
|
||||
`BB_FRAMES_AHEAD=N` (how many frames the GPU command thread may run ahead of the GPU; 1 by default,
|
||||
0 = unbounded), `BB_PRESENT_THREAD=0` (present on the vblank thread, as before),
|
||||
`BB_LIVE_RES=1` (live resolution changes instead of the startup patch for outputs other than 1080p),
|
||||
`BB_PAD_RECORD=file` / `BB_PAD_REPLAY=file` (record a route with F9, replay it in scripted tests),
|
||||
`BB_GC_BUDGET_MB=N` (texture cache budget, as on integrated GPUs), `BB_VRAM_LIMIT_MB=N` (the VRAM
|
||||
budget taken as at most N MiB: checks the behaviour of cards with little VRAM), `BB_ASYNC_PIPELINES=0`
|
||||
(new pipelines compile at once and the game waits; by default in the background), `BB_PRESENT_DUMP_TRIGGER=file`
|
||||
with `BB_PRESENT_DUMP_COUNT=N` (dump N consecutive presented frames),
|
||||
`BB_FSR411_VARIANT=int8|fp8|fp8emu` (FSR 4.1.1 variant; by default FP8 where the GPU has FP8
|
||||
matrices), `BB_READBACKS=0|1|2` (reads of GPU-written memory by the game: 1 by default, 2
|
||||
precise and slow, 0 off).
|
||||
More in [docs/](docs); recent changes: [docs/CHANGES_0.4.md](docs/CHANGES_0.4.md) (in Russian),
|
||||
[docs/CHANGES_2026-10-06.md](docs/CHANGES_2026-10-06.md).
|
||||
|
||||
**Online play (shadNet).** A separate module, `out/gpu/libbbnet.so` (`gpu/bbnet`: shadPS4's network
|
||||
and PSN libraries from shadp2p, a shadPS4 fork, with the shadNet client), built when protobuf,
|
||||
nlohmann_json, OpenSSL, zlib and miniupnpc are there (`shell.nix`) and included in the AppImage.
|
||||
The launcher's *Online* page turns it on (`BB_ONLINE=1`): messages, bloodstains and ghosts come
|
||||
from The Hunter's Dream, bells and summons go through a shadNet server (`srv.shadps4.net:31313` by
|
||||
default) with a shadNet account (Online ID and password). Offline the module is never loaded; a
|
||||
build without it plays offline only.
|
||||
|
||||
## Repository layout
|
||||
|
||||
| Path | Contents |
|
||||
|---|---|
|
||||
| `src/` | Loader (`probe.c`) and the HLE runtime |
|
||||
| `scripts/` | Offline preparation of the game image, module linking, patch compiler |
|
||||
| `gpu/` | Renderer library: vendored shadPS4 video core with this port's changes (`gpu/VENDOR.txt`), shims, ImGui menu, FSR 4.1.1 runtime (`gpu/shadps4/video_core/renderer_vulkan/fsr411`) |
|
||||
| `launcher/`, `packaging/` | GTK4 launcher; Nix package and AppImage |
|
||||
| `patches/` | Community patches for Bloodborne |
|
||||
| `tools/` | Developer tools: scripted runs, A/B toggles, FSR benchmark helpers, FSR 4 shader rewrites, `fsr4cap` (FSR 4.1.1 recording/extraction) |
|
||||
| `tests/` | Loader, runtime, patch and renderer tests |
|
||||
| `docs/` | Design notes and measurements ([upscaler](docs/upscaler.md), [parallel GPU](docs/parallel_gpu.md), [motion vectors](docs/motion_vectors.md), [roadmap](docs/ROADMAP.md)) |
|
||||
|
||||
Tests: `bash build.sh --test`, `python3 -m unittest discover -s tests`, and
|
||||
`ninja -C out/gpu motion-history-test ui-composition-test scene-resolution-test motion-shader-test settings-test`.
|
||||
|
||||
## Roadmap
|
||||
|
||||
- The new memory and translation model on by default on NVIDIA too, once it is as fast there;
|
||||
the old model is removed after that.
|
||||
- Textures as objects created at load time (less guessing by address, uploads outside the
|
||||
frame).
|
||||
- Shaders translated ahead of time, at install, not during play.
|
||||
- More CPU parallelism in GPU command processing (split the draw-recording stage further),
|
||||
scaling to all hardware threads — most important for the Steam Deck.
|
||||
- Async compute for the upscaler (the frame is GPU-bound at 4K).
|
||||
- XeSS (super resolution) and XeFG frame generation through a Wine helper sharing Vulkan
|
||||
memory (a memory-bridge prototype is in `tools/bridge_helper`); inputs exposed so that OptiScaler-style mapping works.
|
||||
- Frame generation (FSR 3.1 FG first), reactive and transparency masks for particles and fog.
|
||||
- Fix the races in AMD's FSR 4.1.1 shaders at output widths that are not multiples of 64
|
||||
(e.g. 1600×900), as already done for the left-edge race in FSR 4 v07 at 1080p.
|
||||
- Steam Deck validation of the AppImage; HDR output.
|
||||
|
||||
## Credits and licenses
|
||||
|
||||
bbport is licensed under the **GNU GPL v2 or later** ([LICENSE](LICENSE)) — it contains code
|
||||
from shadPS4 (GPL-2.0-or-later). Third-party components keep their licenses:
|
||||
[shadPS4](https://github.com/shadps4-emu/shadPS4) video core and shader recompiler (GPL-2.0+),
|
||||
[sirit](https://github.com/shadps4-emu/sirit), [half](https://half.sourceforge.net/),
|
||||
[FSR-Vulkan](https://github.com/FireBurn/FSR-Vulkan) by FireBurn (MIT; FSR 3.1 on Vulkan and the
|
||||
FSR 4 v07 provider), AMD FidelityFX SDK (MIT), [LibAtrac9](https://github.com/Thealexbarney/LibAtrac9)
|
||||
(MIT), [Dear ImGui](https://github.com/ocornut/imgui) (MIT), DejaVu fonts,
|
||||
[dxil-spirv](https://github.com/HansKristian-Work/dxil-spirv) (MIT, used to build the
|
||||
FSR 4.1.1 assets). Game patches by Kyo, Lance McDonald, auser1337, illusion, emoose and other
|
||||
community members (`patches/Bloodborne.xml`). The DLSS bridge (`gpu/dlss_bridge`, MIT) and
|
||||
loader come from [Supermedo's Windows port](https://github.com/Supermedo/bloodborne_pc), adapted
|
||||
from [IFreemz/shadPS4-Bloodborne-DLSS-FSR](https://github.com/IFreemz/shadPS4-Bloodborne-DLSS-FSR).
|
||||
AMD's FSR 4 DLLs and model data and NVIDIA's DLSS SDK and libraries are not distributed here.
|
||||
GNU GPL v2 or later ([LICENSE](LICENSE)). Third-party components keep their own licenses. NVIDIA
|
||||
and DLSS are trademarks of NVIDIA Corporation.
|
||||
@@ -0,0 +1,16 @@
|
||||
# Button prompts and gestures menu
|
||||
|
||||
By **Dommo**, from [Nexus Mods: Bloodborne, mod 30](https://www.nexusmods.com/bloodborne/mods/30)
|
||||
(uploaded by goomab): *DS3 Style - Modern Xbox prompts*, *DS3 Style - Nintendo Switch prompts* and
|
||||
*Stripped down gesture menu*. The mod page allows using its assets and uploading them elsewhere
|
||||
with credit to the author, but not in anything sold.
|
||||
|
||||
Only what the mods drew is here, never the game's own files:
|
||||
|
||||
- `xbox/`, `switch/`: the 20 button icons (`KG_*.dds`, BC7 exactly as in the mods) and the strip
|
||||
of big prompts from the shared UI atlas (`atlas.dds`, scaled down to the game's atlas size).
|
||||
- `gesturetop.json`: the edits that turn the game's `menu/gesturetop.gfx` into the mod's.
|
||||
|
||||
`scripts/menu_prompts.py` puts them into the player's own `menu/common.tpf.dcx` and
|
||||
`menu/gesturetop.gfx` at each start; `tools/extract_prompt_assets.py` rebuilds this folder from
|
||||
the mods' ZIPs. Keyboard prompts are drawn by `scripts/menu_prompts.py` itself.
|
||||
@@ -0,0 +1,91 @@
|
||||
{
|
||||
"original_sha256": "5d7bae1451d97296e88a6add62b1fd9d1425901acc5c907e90f52354bc0b624e",
|
||||
"result_sha256": "c77f7398c02613bd3d1a9613ce773fa8b6b74143053c6092b52a3c8d959dd868",
|
||||
"edits": [
|
||||
[
|
||||
4,
|
||||
1,
|
||||
"2f"
|
||||
],
|
||||
[
|
||||
2548,
|
||||
1,
|
||||
"18"
|
||||
],
|
||||
[
|
||||
2556,
|
||||
1,
|
||||
"8e"
|
||||
],
|
||||
[
|
||||
2563,
|
||||
0,
|
||||
"c9"
|
||||
],
|
||||
[
|
||||
2564,
|
||||
0,
|
||||
"0034bca0480238"
|
||||
],
|
||||
[
|
||||
3189,
|
||||
1,
|
||||
"92"
|
||||
],
|
||||
[
|
||||
3229,
|
||||
1,
|
||||
"92"
|
||||
],
|
||||
[
|
||||
3242,
|
||||
5,
|
||||
"207b2673a5e0"
|
||||
],
|
||||
[
|
||||
3268,
|
||||
1,
|
||||
"98"
|
||||
],
|
||||
[
|
||||
3281,
|
||||
5,
|
||||
"2077b30a7b80"
|
||||
],
|
||||
[
|
||||
3519,
|
||||
1,
|
||||
"41"
|
||||
],
|
||||
[
|
||||
3529,
|
||||
1,
|
||||
"33"
|
||||
],
|
||||
[
|
||||
3539,
|
||||
5,
|
||||
"c900004000207d11920000"
|
||||
],
|
||||
[
|
||||
5795,
|
||||
1,
|
||||
"ab"
|
||||
],
|
||||
[
|
||||
5817,
|
||||
1,
|
||||
"1a"
|
||||
],
|
||||
[
|
||||
5826,
|
||||
5,
|
||||
"c900004000207a015deba0"
|
||||
],
|
||||
[
|
||||
6076,
|
||||
12,
|
||||
""
|
||||
]
|
||||
]
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,48 @@
|
||||
# Game compatibility
|
||||
|
||||
bbport runs Bloodborne for PS4 with **update 1.09**. Its hooks and patches use the addresses of
|
||||
one executable: the launcher hashes the loaded `eboot.bin` image and runs only that one 1.09
|
||||
executable (`scripts/game_check.py`). A dump that passes the check runs; one that does not is
|
||||
told why (no 1.09 update, another executable, or an `eboot.bin` with Lance McDonald's 60 fps
|
||||
patch baked in).
|
||||
|
||||
## Releases the launcher accepts
|
||||
|
||||
Every retail release of Bloodborne. Title and content IDs as listed in
|
||||
[deadinside28/bloodborne_pc#117](https://github.com/deadinside28/bloodborne_pc/pull/117).
|
||||
|
||||
| Edition | Region | Title ID | Content ID | 1.09 executable checked |
|
||||
|---|---|---|---|---|
|
||||
| Bloodborne | US | `CUSA00900` | `UP9000-CUSA00900_00-BLOODBORNE000000` | Yes |
|
||||
| Bloodborne | Europe | `CUSA00207` | `EP9000-CUSA00207_00-BLOODBORNE0000EU` | Not yet |
|
||||
| Bloodborne | UK | `CUSA00208` | `EP9000-CUSA00208_00-BLOODBORNE0000UK` | Not yet |
|
||||
| Bloodborne | Japan | `CUSA00299` | `JP9000-CUSA00299_00-BLOODBORNE0000JP` | Not yet |
|
||||
| Bloodborne | Asia | `CUSA01363` | `HP9000-CUSA01363_00-BLOODBORNE0000AS` | Not yet |
|
||||
| The Old Hunters Edition | US | `CUSA03179` | `UP9000-CUSA03179_00-BLOODBORNE000000` | Not yet |
|
||||
| Game of the Year Edition | Europe | `CUSA03173` | `EP9000-CUSA03173_00-BLOODBORNE0000EU` | Yes |
|
||||
| The Old Hunters Edition | Japan | `CUSA03014` | `JP9000-CUSA03014_00-BLOODBORNE0000JP` | Not yet |
|
||||
| The Old Hunters Edition | Asia | `CUSA03023` | `HP9000-CUSA03023_00-BLOODBORNE0000AS` | Not yet |
|
||||
|
||||
*Checked* means a dump of that release with update 1.09 had exactly the executable bbport runs.
|
||||
The others are accepted on the expectation that every release shares the same 1.09 executable;
|
||||
if one does not, the launcher says so instead of starting a game that would crash. A report of
|
||||
how yours goes is welcome either way.
|
||||
|
||||
The alpha tests (`CUSA01318`, `CUSA01322`, `CUSA01333`, `CUSA01335`) are not supported.
|
||||
|
||||
The Game of the Year and The Old Hunters editions include the DLC. With a base game, dump the
|
||||
add-ons too and choose their folder in the launcher (see the README's *DLC* section).
|
||||
|
||||
## Tested with this fork
|
||||
|
||||
On Linux (CachyOS, NVIDIA GeForce RTX 3060), with the US release (`CUSA00900`):
|
||||
|
||||
- The base game with update 1.09 and the add-ons dumped separately (The Old Hunters and the two
|
||||
messenger add-ons), from the launcher's DLC folder.
|
||||
- A dump whose `eboot.bin` had Lance McDonald's 60 fps patch baked in: the launcher reports it,
|
||||
and the clean 1.09 `eboot.bin` runs.
|
||||
- Both regional texts through *Game region*: Spanish from Spain (Europe) and Latin American
|
||||
Spanish (the Americas).
|
||||
|
||||
The European Game of the Year edition (`CUSA03173`) is the release the original bbport is
|
||||
developed and tested with.
|
||||
+109
@@ -0,0 +1,109 @@
|
||||
# Mods and third-party patches
|
||||
|
||||
Both are on the launcher's **Settings** page and apply at the next start. The game folder itself
|
||||
is never changed.
|
||||
|
||||
## File mods
|
||||
|
||||
The **Mods** group has the folder, a **Load mods** switch, and the list of mods: each one can be
|
||||
turned on or off and moved up or down. The lowest mod that is on wins: when two mods replace the
|
||||
same file, the log says which one replaced the other.
|
||||
|
||||
The mods folder is `~/.local/share/bbport/mods/` with the AppImage (`mods/` in the data
|
||||
directory when running from the repository). Extract each mod into a folder of its own:
|
||||
|
||||
```text
|
||||
mods/
|
||||
Mod name/
|
||||
dvdroot_ps4/
|
||||
chr/...
|
||||
param/...
|
||||
menu/...
|
||||
```
|
||||
|
||||
These layouts work as well:
|
||||
|
||||
- `Mod name/app0/dvdroot_ps4/...`, and `Mod name/<title ID>/dvdroot_ps4/...` with whichever
|
||||
release the mod was made for (`CUSA03173`, `CUSA00900`, ...).
|
||||
- One extra wrapper folder, as extracting an archive into a folder of its name leaves it:
|
||||
`Mod name/Mod name v1.2/dvdroot_ps4/...` (readme files and pictures may sit next to it).
|
||||
- The game's folders without `dvdroot_ps4`: `Mod name/chr/...`, `Mod name/parts/...` (`chr`,
|
||||
`parts`, `map`, `menu`, `msg`, `param`, `sfx`, `sound`, `font` and the other folders of
|
||||
`dvdroot_ps4`).
|
||||
|
||||
Upper or lower case does not matter: a mod made on Windows with
|
||||
`DVDROOT_PS4/Chr/C0000.chrbnd.dcx` replaces the game's `dvdroot_ps4/chr/c0000.chrbnd.dcx`.
|
||||
The log shows how many game files the mods replaced and added, e.g.
|
||||
`Mods: 12 game files replaced, 0 added`. A mod meant to replace files that says
|
||||
`0 game files replaced` has its files in the wrong place.
|
||||
|
||||
What works: ready-made replacements of `dvdroot_ps4` files (textures, models, params,
|
||||
interface, sound, fonts). What does not:
|
||||
|
||||
- ZIP or 7z archives: extract them first.
|
||||
- Two mods changing different parts of the same `.dcx` or `.bnd` file: the last one replaces
|
||||
it whole, the files are not merged.
|
||||
- Mods that replace `eboot.bin`, `sce_module` or `sce_sys`, DLL injectors and script plugins.
|
||||
Changes to the game's code are patches (below).
|
||||
- Symbolic links inside a mod.
|
||||
|
||||
Whether a mod works with Bloodborne 1.09 is up to its author.
|
||||
|
||||
A shadPS4-style folder next to the game folder, `<game folder>-mods/dvdroot_ps4/...` (for
|
||||
example `CUSA03173-mods` next to `CUSA03173`), is loaded too, before the mods in the list. You
|
||||
can also choose a folder that holds `dvdroot_ps4` directly as the mods folder: it is then loaded
|
||||
as one mod. **Load mods** turns all of them off. The launcher's button prompts and gestures
|
||||
menu work the same way, as files laid under the mods; they are settings of their own and stay
|
||||
on when mods are off.
|
||||
|
||||
### How it works
|
||||
|
||||
For each start, the data directory gets a merged tree of links, `out/mod-game-*`: the game sees
|
||||
the mods' files where they replace its own and the original files everywhere else. `/app0` is
|
||||
read-only, and saves go to the usual folder. The tree is removed when the game exits normally
|
||||
or the launcher stops it; one left behind by a crash is not reused.
|
||||
|
||||
The order and the mods turned off are kept in `mods.json` in the data directory:
|
||||
|
||||
```json
|
||||
{"order": ["First", "Second"], "disabled": ["First"]}
|
||||
```
|
||||
|
||||
New folders are turned on and added at the end, in alphabetical order.
|
||||
|
||||
From a terminal:
|
||||
|
||||
```sh
|
||||
BB_GAME_DIR=/path/to/game BB_MODS_DIR=/path/to/mods bash run.sh
|
||||
BB_GAME_DIR=/path/to/game BB_MODS_ENABLED=0 bash run.sh
|
||||
```
|
||||
|
||||
`BB_MODS_CONFIG` sets another `mods.json`. `BB_MOD_TRACE=1` (with `BB_MODS_DIR`) logs the
|
||||
first reads of files that come from mods, to see whether a mod is being used.
|
||||
|
||||
## Third-party patches
|
||||
|
||||
The **Third-party patches** group loads patches in the shadPS4/GoldHEN XML format from the
|
||||
patches folder: `~/.local/share/bbport/patches/` with the AppImage, or another folder chosen in
|
||||
the launcher.
|
||||
|
||||
- Only patches for version `01.09` of `eboot.bin` are listed. Files for other games are
|
||||
skipped: a file applies when it lists any retail release of Bloodborne, or none.
|
||||
- Each patch starts on or off as the file says (`isEnabled`); the choice in the launcher is
|
||||
kept in `patches.json` in the data directory:
|
||||
|
||||
```json
|
||||
{"enabled": ["file.xml/Patch name"], "disabled": ["file.xml/Other patch"]}
|
||||
```
|
||||
|
||||
- Lines of type `bytes`, `bytes16`, `bytes32`, `bytes64`, `float32`, `float64`, `utf8` and
|
||||
`utf16` work. A patch with a `mask` line (a pattern search) or an address outside the
|
||||
executable is skipped whole, and the log says so.
|
||||
- They apply after bbport's own patches (`patches/Bloodborne.xml`), so where both change the
|
||||
same bytes, the third-party patch wins.
|
||||
- Patches that replace pointers in tables, such as *60 FPS++*, are moved to where the game is
|
||||
loaded.
|
||||
|
||||
From a terminal: `BB_PATCHES_DIR=/path BB_PATCHES_CONFIG=/path/patches.json bash run.sh`.
|
||||
|
||||
This guide is based on bbport's [MODS.md](MODS.md) (Russian), by deadinside28.
|
||||
@@ -0,0 +1,76 @@
|
||||
# Online play and co-op
|
||||
|
||||
Online play is a beta. Messages, bloodstains and ghosts come from the community game server,
|
||||
[The Hunter's Dream](https://thehuntersdream.com). Bells and summons go through a shadNet server,
|
||||
and then the players' games connect to each other directly. Linux and Windows players can play
|
||||
together: everyone needs the game at version 1.09 and the same servers.
|
||||
|
||||
> **Co-op does not connect through shadPS4's public server.** It does not answer the UDP ping
|
||||
> that tells it each player's address, so a summon times out
|
||||
> ([deadinside28/bloodborne_pc#121](https://github.com/deadinside28/bloodborne_pc/issues/121));
|
||||
> the shadPS4 team does not support forks. Messages, bloodstains and ghosts work. For co-op, a
|
||||
> group runs its own server: [SERVER.md](SERVER.md). *Test the connection* tells which case
|
||||
> yours is: the shadNet server's UDP line is red on the public server.
|
||||
|
||||
## Account
|
||||
|
||||
1. On shadPS4's public server, register at [shadnet.shadps4.net](https://shadnet.shadps4.net).
|
||||
On your own server, its owner creates the account ([SERVER.md](SERVER.md)).
|
||||
2. In the launcher's **Online** page, turn on **Play online**, choose the **shadNet server**
|
||||
(shadPS4's public one or your own, with its address), and enter the **account name** (not the
|
||||
email) and the password. The password is kept on this computer with the launcher's settings.
|
||||
3. **Test the connection**: the game server, the shadNet server, its UDP port and its WebAPI
|
||||
should all answer.
|
||||
|
||||
If the game says you are not signed in to PlayStation Network, the shadNet login failed. The
|
||||
usual cause is the password: the log then has a shadNet login error (code 8 is a wrong
|
||||
password). *Test the connection* only checks that the servers answer, not the account.
|
||||
|
||||
## Summoning in the game
|
||||
|
||||
- The host rings the **Beckoning Bell** (one Insight each time); the helper rings the **Small
|
||||
Resonant Bell**.
|
||||
- Use the same **password** in the game's network settings, so you find each other whatever
|
||||
your levels.
|
||||
- Be in the same area, close to each other, with the area's boss still alive in the host's
|
||||
world.
|
||||
|
||||
## Ports
|
||||
|
||||
Co-op traffic is UDP, straight between the players' computers. With your own server on
|
||||
Tailscale ([SERVER.md](SERVER.md)) it goes through Tailscale: turn **UPnP** off and skip this
|
||||
section. Otherwise each player has to be reachable from the Internet:
|
||||
|
||||
1. **The router.** With **UPnP** on (the default), the game opens its port on the router itself:
|
||||
the log says `UPNP: mapped UDP port <port>`. A router without UPnP needs a port forward by
|
||||
hand (UDP, the pinned port below, to your computer's LAN address).
|
||||
2. **Pin the port** when you open it by hand or in a firewall: **Co-op port (UDP)** in the
|
||||
Online page, for example 3658 (0, the default, is a random port each time). The log then says
|
||||
`P2P transport ready: UDP physical_port=3658`.
|
||||
3. **Open it in the firewall.** UPnP opens the port on the router only, not in this computer's
|
||||
firewall. With ufw or firewalld on, the launcher's **Online** page says
|
||||
so and gives the commands. For ufw:
|
||||
```sh
|
||||
sudo ufw allow 3658/udp
|
||||
sudo ufw allow from <router IP> proto udp # the router's UPnP answers
|
||||
```
|
||||
On Windows, in a terminal as administrator:
|
||||
```
|
||||
netsh advfirewall firewall add rule name="Bloodborne co-op" dir=in action=allow protocol=UDP localport=3658
|
||||
```
|
||||
|
||||
A router whose public address is shared by many customers (CG-NAT) cannot forward ports: the
|
||||
log's `UPNP: IGD found - external IP=...` should be your real public address. Tailscale works
|
||||
behind CG-NAT too.
|
||||
|
||||
## Reading the log
|
||||
|
||||
Run with `BB_GPU_LOG=info` in the extra variables for the online details. In a summon:
|
||||
|
||||
| Line | Meaning |
|
||||
|---|---|
|
||||
| `Logged in npid='...'` | The shadNet login worked. |
|
||||
| `summon_messenger/create` | Your sign is placed (helper). |
|
||||
| `summon_messenger/get`, `sceNpMatching2CreateJoinRoom`, `summon_messenger/request` | The host found the sign, made the room and sent the summon. |
|
||||
| `peer '...' endpoint unresolved` | shadNet has no address for the other player: its UDP port did not answer (the public server, above). |
|
||||
| `event=0(DEAD)` | The connection gave up. |
|
||||
@@ -0,0 +1,40 @@
|
||||
# Documentation
|
||||
|
||||
## For players
|
||||
|
||||
- [COMPATIBILITY.md](COMPATIBILITY.md): which releases of Bloodborne run, and which have been
|
||||
checked.
|
||||
- [MODS.en.md](MODS.en.md): file mods and third-party patches.
|
||||
- [ONLINE.md](ONLINE.md): online play and co-op: the account, the bells, the ports.
|
||||
- [SERVER.md](SERVER.md): hosting your own co-op server (shadnet-p2p) for a group of friends.
|
||||
- [TROUBLESHOOTING.md](TROUBLESHOOTING.md): common problems and what to do.
|
||||
|
||||
## For this fork
|
||||
|
||||
- [RELEASE_TEMPLATE.md](RELEASE_TEMPLATE.md): the format of every release's notes.
|
||||
|
||||
## bbport's development notes
|
||||
|
||||
Everything else here comes from [bbport](https://github.com/deadinside28/bloodborne_pc) by
|
||||
deadinside28, kept as it is so that its updates merge cleanly. Many are in Russian (`.ru.md`, or
|
||||
a Russian title).
|
||||
|
||||
- **What changed in each version:** [CHANGES_0.5.md](CHANGES_0.5.md),
|
||||
[CHANGES_0.5-pre4.md](CHANGES_0.5-pre4.md), [CHANGES_0.5-pre3.md](CHANGES_0.5-pre3.md),
|
||||
[CHANGES_0.5-pre2.md](CHANGES_0.5-pre2.md), [CHANGES_after_0.4.md](CHANGES_after_0.4.md),
|
||||
[CHANGES_0.4.md](CHANGES_0.4.md), [CHANGES_2026-10-06.md](CHANGES_2026-10-06.md) (0.3),
|
||||
[CHANGES_2026-10-03.md](CHANGES_2026-10-03.md), [CHANGES_2026-10-02.md](CHANGES_2026-10-02.md).
|
||||
- **Where the port is going:** [ROADMAP.md](ROADMAP.md),
|
||||
[EMULATION_REMOVAL_PLAN.ru.md](EMULATION_REMOVAL_PLAN.ru.md),
|
||||
[TRANSLATOR_ARCHITECTURE.ru.md](TRANSLATOR_ARCHITECTURE.ru.md).
|
||||
- **Memory and GPU commands:** [MEMORY_MODULE_PLAN.ru.md](MEMORY_MODULE_PLAN.ru.md),
|
||||
[NVIDIA_MEMORY_MODEL.ru.md](NVIDIA_MEMORY_MODEL.ru.md),
|
||||
[TRANSLATION_GPU_COPY_LABELS.ru.md](TRANSLATION_GPU_COPY_LABELS.ru.md),
|
||||
[parallel_gpu.md](parallel_gpu.md).
|
||||
- **Upscaling, TAA and motion vectors:** [upscaler.md](upscaler.md),
|
||||
[DEPTH_ADAPTIVE_TAA.md](DEPTH_ADAPTIVE_TAA.md),
|
||||
[TEMPORAL_REVIEW_2026-10-01.md](TEMPORAL_REVIEW_2026-10-01.md),
|
||||
[motion_vectors.md](motion_vectors.md).
|
||||
- **Mods, patches and TAA sharpness** (the original of MODS.en.md): [MODS.md](MODS.md).
|
||||
- **The first experiments:** [DEVELOPMENT_LOG.ru.md](DEVELOPMENT_LOG.ru.md), and the session
|
||||
notes in [history/](history/).
|
||||
@@ -0,0 +1,46 @@
|
||||
# Release notes template
|
||||
|
||||
Every release uses this format: same sections, same order. A section with nothing to say keeps
|
||||
its heading and says "None.". Notes are in English. "New", "Improved" and "Fixed" cover the
|
||||
changes since the previous release. Third-party work is credited in the line that uses it.
|
||||
|
||||
- Tag: `<upstream bbport version>-linux.<n>` (e.g. `0.4-linux.1`).
|
||||
- Title: `bbport Linux <tag>`.
|
||||
- Asset: `dist/Bloodborne-bbport-x86_64.AppImage` (`bash build.sh && bash packaging/appimage-sharun.sh`).
|
||||
|
||||
```markdown
|
||||
<One or two sentences: what this release is and which upstream bbport it is based on.>
|
||||
|
||||
**Requirements:** your own decrypted dump of Bloodborne with update 1.09, any retail release (US,
|
||||
Europe, UK, Japan, Asia, and the editions with The Old Hunters). No game files are included.
|
||||
Linux x86-64 and a Vulkan 1.3 GPU.
|
||||
|
||||
## Download
|
||||
|
||||
| File | For |
|
||||
|---|---|
|
||||
| `Bloodborne-bbport-x86_64.AppImage` | Any x86-64 distribution and the Steam Deck |
|
||||
|
||||
Make it executable, run it, choose your game folder and press **Play**. Saves and settings go
|
||||
to `~/.local/share/bbport`.
|
||||
|
||||
SHA-256: `<sha256sum of the AppImage>`
|
||||
|
||||
## New
|
||||
- **<Feature>**: <what it does>.
|
||||
|
||||
## Improved
|
||||
- <Change to something that existed>.
|
||||
|
||||
## Fixed
|
||||
- <Bug fixed>.
|
||||
|
||||
## Upgrading
|
||||
- <What someone coming from the previous release has to do, or "None.">
|
||||
|
||||
## Known issues
|
||||
- <Limitations and known bugs.>
|
||||
|
||||
**Full changes:** https://github.com/ayozetr/bloodborne_linux/compare/<previous tag>...<tag>
|
||||
**Credits and licenses:** https://github.com/ayozetr/bloodborne_linux#credits
|
||||
```
|
||||
+180
@@ -0,0 +1,180 @@
|
||||
# Hosting your own co-op server
|
||||
|
||||
Bells and summons go through a shadNet server. Players who connect through shadPS4's public one
|
||||
(`srv.shadps4.net`) get no co-op at the moment: it does not answer on its UDP port, so a summon
|
||||
never connects ([deadinside28/bloodborne_pc#121](https://github.com/deadinside28/bloodborne_pc/issues/121)).
|
||||
A group of friends can run their own server instead:
|
||||
[shadnet-p2p](https://github.com/Wozzardman/shadnet-p2p) by Wozzardman, the shadNet server with
|
||||
that UDP port working and a summon service of its own.
|
||||
|
||||
Messages, bloodstains and ghosts still come from The Hunter's Dream, whichever shadNet server you
|
||||
use. Windows players of [Supermedo's port](https://github.com/Supermedo/bloodborne_pc) can join
|
||||
the same server.
|
||||
|
||||
Tested with two players, one on this Linux port and one on Supermedo's Windows port, over
|
||||
Tailscale, with the server below on a home server (Ubuntu 26.04): summons, a boss fight together
|
||||
and summoning again after the helper died.
|
||||
|
||||
## How it fits together
|
||||
|
||||
- **One always-on computer runs the server:** a VPS, a home server, or one of the players' PCs
|
||||
(then it has to be on while you play).
|
||||
- **Everyone joins one [Tailscale](https://tailscale.com) network** (a *tailnet*): the server and
|
||||
every player. Tailscale gives each computer an address starting with `100.` and encrypts the
|
||||
traffic between them. This matters: shadNet's login on TCP 31313 is not encrypted, so it should
|
||||
not be open to the Internet. And the games then reach each other over Tailscale too, so nobody
|
||||
has to open ports on their router.
|
||||
|
||||
| Protocol | Port | Used for |
|
||||
|---|---|---|
|
||||
| TCP | 31313 | Login and matchmaking |
|
||||
| UDP | 31314 | Each player's address, for the other player of a summon |
|
||||
| TCP | 31315 | WebAPI and the summon service |
|
||||
|
||||
## 1. Tailscale
|
||||
|
||||
On the server and on every player's computer, install Tailscale
|
||||
([download](https://tailscale.com/download); on Linux `curl -fsSL https://tailscale.com/install.sh | sh`)
|
||||
and sign in:
|
||||
|
||||
```sh
|
||||
sudo tailscale up
|
||||
```
|
||||
|
||||
The server's owner invites the players from the
|
||||
[admin console](https://login.tailscale.com/admin/users) (**Users → Invite external users**),
|
||||
and each player accepts the invitation and signs in on the computer they play on. Then, on the
|
||||
server:
|
||||
|
||||
```sh
|
||||
tailscale ip -4 # the server's address, 100.x.y.z: the players need it
|
||||
tailscale status # every player's computer should be listed
|
||||
```
|
||||
|
||||
## 2. The server
|
||||
|
||||
### With Docker (any Linux)
|
||||
|
||||
This repository has a Docker image for it: [tools/shadnet-server](../tools/shadnet-server). It
|
||||
builds shadnet-p2p on Debian 13, because shadnet-p2p needs Qt 6.8 or newer and some
|
||||
distributions are older (Ubuntu 24.04 has Qt 6.4). It runs on any Linux with Docker, Ubuntu 24.04
|
||||
included.
|
||||
|
||||
```sh
|
||||
mkdir shadnet-server && cd shadnet-server
|
||||
for f in Dockerfile entrypoint.sh; do
|
||||
curl -fsSLO "https://raw.githubusercontent.com/ayozetr/bloodborne_linux/main/tools/shadnet-server/$f"
|
||||
done
|
||||
docker build -t bbport-shadnet .
|
||||
```
|
||||
|
||||
Then start it, with the server's Tailscale address and a signup code of your own (letters,
|
||||
digits, `-` and `_`; the players never need it, only you, to create their accounts):
|
||||
|
||||
```sh
|
||||
docker run -d --name shadnet --restart unless-stopped --network host \
|
||||
-v shadnet-data:/data \
|
||||
-e SHADNET_HOST=100.x.y.z \
|
||||
-e SHADNET_REGISTRATION_KEY=a-long-code-of-your-own \
|
||||
bbport-shadnet
|
||||
docker logs shadnet
|
||||
```
|
||||
|
||||
The log should end with the three listeners on the Tailscale address:
|
||||
|
||||
```text
|
||||
Unsecured TCP listener on: 100.x.y.z:31313. No encryption, use it only on trusted networks
|
||||
STUN UDP listener on: 100.x.y.z:31314
|
||||
WebApiServer listening on: 100.x.y.z:31315
|
||||
```
|
||||
|
||||
`SHADNET_HOST` keeps the server off the computer's other networks. After a reboot, it waits until
|
||||
Tailscale is up. The configuration (`shadnet.cfg`) and the accounts (`db/shadnet.db`) live in the
|
||||
`shadnet-data` volume. To stop or update the server:
|
||||
|
||||
```sh
|
||||
docker stop shadnet && docker rm shadnet # the volume, with the accounts, stays
|
||||
docker build -t bbport-shadnet . # after downloading a newer Dockerfile
|
||||
```
|
||||
|
||||
Then run the same `docker run` again.
|
||||
|
||||
### Without Docker
|
||||
|
||||
Follow [shadnet-p2p's README](https://github.com/Wozzardman/shadnet-p2p#readme). It needs Qt 6.8
|
||||
or newer: Arch Linux and its derivatives, Debian 13, Ubuntu 26.04, Fedora 41 or newer. On Debian
|
||||
and Ubuntu, install `qt6-websockets-dev` along with the packages it lists, or CMake does not find
|
||||
Qt's HTTP server module (checked on Debian 13 and Ubuntu 26.04). In `build/shadnet.cfg`, set `Host` to the server's Tailscale address (or
|
||||
`0.0.0.0`), `Matching2Enabled=true` and `RegistrationSecretKey` to your signup code.
|
||||
|
||||
### A firewall on the server
|
||||
|
||||
With ufw, let the tailnet in:
|
||||
|
||||
```sh
|
||||
sudo ufw allow in on tailscale0
|
||||
```
|
||||
|
||||
## 3. Accounts
|
||||
|
||||
One account per player, created on the server with the signup code. The name is 3 to 16
|
||||
letters, digits, `-` or `_`:
|
||||
|
||||
```sh
|
||||
docker exec shadnet shadnet-sample 127.0.0.1 31313 register <name> <password> <email> <signup code>
|
||||
```
|
||||
|
||||
`127.0.0.1` works only when the server listens on every address (`SHADNET_HOST` unset or
|
||||
`0.0.0.0`). With `SHADNET_HOST` set, use that address instead. The tool answers
|
||||
`Account created successfully.`, or `FAILED: Unauthorized` when the code is wrong. Give each player
|
||||
their own name and password.
|
||||
|
||||
To back up the accounts, stop the server and copy the volume's `db/shadnet.db`.
|
||||
|
||||
## 4. Each player's launcher
|
||||
|
||||
In the **Online** page:
|
||||
|
||||
| Setting | Value |
|
||||
|---|---|
|
||||
| Play online | On |
|
||||
| shadNet server | **Your own server** |
|
||||
| Server address | `100.x.y.z` (the port, 31313, is added) |
|
||||
| WebAPI address | Empty (the server's port 31315) |
|
||||
| Online ID / Password | The player's own account |
|
||||
| UPnP | Off (Tailscale needs no router ports) |
|
||||
|
||||
Then **Test the connection**. Every line should be green, the shadNet server's UDP among them: it
|
||||
shows the address the server sees for you, a `100.` one through Tailscale. A red UDP line means
|
||||
summons cannot connect: check that the server is running and that its log lists the UDP listener.
|
||||
|
||||
Summons go through the server's own summon service: the log of a game says
|
||||
`Co-op: summon signs through http://100.x.y.z:31315`.
|
||||
|
||||
Windows players set the same server, WebAPI and account in the Windows port's launcher.
|
||||
|
||||
## Seamless co-op (experimental)
|
||||
|
||||
shadnet-p2p can also summon between different areas: the guest is moved to the host's area. To
|
||||
use it, the server and every player must have it on:
|
||||
|
||||
- The server: add `-e SHADNET_BLOODBORNE_SEAMLESS_COOP=1` to `docker run`, or set
|
||||
`BloodborneSeamlessCoop=true` in `shadnet.cfg`. Its log then says `seamless co-op enabled`.
|
||||
- Each player: **Seamless co-op (experimental)** in the launcher's Online page.
|
||||
|
||||
Ring the host's Beckoning Bell first, then the guest's Small Resonant Bell. Travelling together
|
||||
by lantern once the session has started does not work yet.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
- **The log shows `127.0.0.1` or another address in the listeners:** check `SHADNET_HOST`, or
|
||||
`Host` in the `shadnet.cfg` beside the server's executable.
|
||||
- **`Waiting for 100.x.y.z` in the log:** Tailscale is not up on the server, or the address is
|
||||
wrong (`tailscale ip -4`).
|
||||
- **The game says it is not signed in to PlayStation Network:** a wrong name or password (code 8 in
|
||||
the log), or two players using one account.
|
||||
- **The test is green but summons fail:** every player's computer must be in the tailnet, not only
|
||||
the server; `tailscale ping <player>` from each side. Everyone needs the game at version 1.09
|
||||
and the same co-op mode.
|
||||
|
||||
See also [ONLINE.md](ONLINE.md) for the bells, passwords and log lines of a summon.
|
||||
@@ -0,0 +1,62 @@
|
||||
# Troubleshooting
|
||||
|
||||
When asking for help, attach the log: in the launcher's **Log** page, **Export log…** saves it to
|
||||
a file and **Copy** puts it on the clipboard.
|
||||
|
||||
## The launcher rejects the game
|
||||
|
||||
The launcher checks the game folder before starting. What it says, and what to do:
|
||||
|
||||
| Message | What to do |
|
||||
|---|---|
|
||||
| bbport needs the 1.09 update merged into the game folder | Copy everything from the dumped 1.09 update into the game folder, replacing files (`eboot.bin` and `sce_sys` too). |
|
||||
| param.sfo is from 1.09 but eboot.bin is not | Copy `eboot.bin` from the dumped 1.09 update over the old one. |
|
||||
| This eboot.bin has the 60 fps patch by Lance McDonald applied | Use a clean `eboot.bin` from the 1.09 update. bbport has its own 60 FPS. |
|
||||
| This is not a retail release of Bloodborne | Alpha tests and other games do not run. See [COMPATIBILITY.md](COMPATIBILITY.md). |
|
||||
| eboot.bin could not be read as a decrypted PS4 executable | Dump the game and the update again. |
|
||||
| The game files are damaged | The extraction tool broke compressed files (LibOrbisPkg PkgTool on .NET 6 or newer); the game would hang while loading. Extract the game and the update again with a fixed tool. |
|
||||
|
||||
## The game shows *New Game* instead of my save
|
||||
|
||||
Saves live in the data directory, and each way of running the port has its own:
|
||||
|
||||
- The AppImage: `~/.local/share/bbport/user/savedata/1/<title ID>/`.
|
||||
- From the repository (`run.sh`): `user/savedata/1/<title ID>/` in the repository.
|
||||
|
||||
To move a save, close the game, back up both folders, and copy the `savedata` folder over.
|
||||
|
||||
## DLSS is "not supported by this GPU"
|
||||
|
||||
The log says why, on a line starting with `DLSS:`.
|
||||
|
||||
- DLSS needs a GeForce RTX (20 series or newer) and NVIDIA's own driver; it does not work with
|
||||
NVK.
|
||||
- The 0.4-linux.1 AppImage ships a modified copy of NVIDIA's DLSS library, which the driver
|
||||
refuses (`Feature requirement query failed: 0xbad00012`): update to 0.5-linux.1 or newer.
|
||||
- From the repository, DLSS needs `bash tools/build_dlss.sh` once after `build.sh`.
|
||||
- If the log says `Device creation with DLSS extensions failed`, NVIDIA's CUDA/UVM
|
||||
module is missing: the game then uses FSR 3.1. Load it with `sudo modprobe nvidia_uvm` (the
|
||||
`nvidia-modprobe` package loads it when needed); with Secure Boot, a module signing key that
|
||||
is not enrolled stops it (`Key was rejected by service`).
|
||||
|
||||
## Spanish shows the Latin American text (or the other way round)
|
||||
|
||||
The game picks a language's regional text by the dump's region. Choose it in **Settings → Game
|
||||
region**: *Europe* for Spanish from Spain, *The Americas* for Latin American Spanish.
|
||||
|
||||
## The game crashes after playing a while, on a card with little VRAM
|
||||
|
||||
On a 6 GB card the texture cache can run out of video memory: the log then repeats
|
||||
`Texture cache: memory pressure, ... (critical ...)`. Lower what uses VRAM: a 60 FPS limit,
|
||||
a lower output resolution, lower **Model detail** in the game's *Display* settings, and close
|
||||
other programs that use the GPU (browsers, video players, other games).
|
||||
|
||||
## Summons never connect
|
||||
|
||||
Run **Test the connection** in the **Online** page. A red line for the shadNet server's UDP means
|
||||
the server never learns your address, and no summon can connect: shadPS4's public server does
|
||||
this. Use your own server: [SERVER.md](SERVER.md).
|
||||
|
||||
## Other online problems
|
||||
|
||||
See [ONLINE.md](ONLINE.md): the account, the bells, the ports and the log lines of a summon.
|
||||
@@ -30,6 +30,9 @@ int bbgpu_text_input_begin(const char *initial_utf8, const char *prompt_utf8);
|
||||
int bbgpu_text_input_poll(char *out_utf8, uint64_t size);
|
||||
/* 1 while the in-game settings menu is open: the game's pad input is held neutral. */
|
||||
int bbgpu_overlay_captures_input(void);
|
||||
/* A trophy the game unlocked (runtime_services.c): the overlay's notice. grade: P, G, S, B or '-';
|
||||
* icon: a TROPxxx.rgba file of scripts/trophies.py (u32 width, u32 height, RGBA), or NULL. */
|
||||
void bbgpu_trophy_notice(int id, char grade, const char *name_utf8, const char *icon_path);
|
||||
/* Mouse look (runtime_pad.c): the window takes the mouse when it is clicked (relative mode) and
|
||||
* lets it go on F1, losing focus or the menu opening. take: motion (pixels) and wheel notches
|
||||
* since the last call, reset; captured: 1 while the window holds it; enable: mouse_look=0 in
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -378,6 +378,29 @@ void VideoOutDriver::Flip(const Request& req) {
|
||||
|
||||
// bbport: BB_FRAME_STATS=1 prints flip rate and frame time spread every 5 seconds.
|
||||
static const bool frame_stats = EmulatorSettingsImpl::Flag("BB_FRAME_STATS", false);
|
||||
// bbport: without it, one short line every 30 seconds, so a player's last_run.log says how the
|
||||
// game ran: game frames per second, the slowest 1% of frames, the worst frame and the
|
||||
// shader/pipeline compiles of the window.
|
||||
if (!frame_stats) {
|
||||
using Clock = std::chrono::steady_clock;
|
||||
static Clock::time_point window_start = Clock::now(), last = window_start;
|
||||
static std::vector<float> intervals;
|
||||
const auto now = Clock::now();
|
||||
intervals.push_back(std::chrono::duration<float, std::milli>(now - last).count());
|
||||
last = now;
|
||||
const double window = std::chrono::duration<double>(now - window_start).count();
|
||||
if (window >= 30.0) {
|
||||
std::sort(intervals.begin(), intervals.end());
|
||||
const size_t n = intervals.size();
|
||||
const double slow_ms = intervals[n - 1 - n / 100];
|
||||
std::printf("Perf: %.1f FPS over %.0f s, 1%% low %.1f FPS, worst frame %.1f ms, %u "
|
||||
"shader/pipeline compiles\n",
|
||||
n / window, window, 1000.0 / std::max(slow_ms, 0.001),
|
||||
static_cast<double>(intervals.back()), Vulkan::g_bb_compiles.exchange(0));
|
||||
intervals.clear();
|
||||
window_start = now;
|
||||
}
|
||||
}
|
||||
if (frame_stats) {
|
||||
using Clock = std::chrono::steady_clock;
|
||||
static Clock::time_point window_start = Clock::now(), last = window_start;
|
||||
|
||||
@@ -218,6 +218,10 @@ struct Info : InfoPersistent {
|
||||
std::memcpy(&base, &ud[ptr_index], sizeof(base));
|
||||
base = reinterpret_cast<const u32*>(VAddr(base) & 0xFFFFFFFFFFFFULL);
|
||||
}
|
||||
// bbport: an unbound descriptor table must reach the existing empty-sharp handling.
|
||||
if (!base) {
|
||||
return T{};
|
||||
}
|
||||
std::memcpy(&data, base + dword_offset, sizeof(T));
|
||||
return data;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2024-2026 shadPS4 Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <thread>
|
||||
#include <pthread.h>
|
||||
@@ -1278,6 +1279,12 @@ void ScanDcb(std::span<const u32> dcb, int depth) {
|
||||
st = DcbStats{};
|
||||
}
|
||||
}
|
||||
// bbport: a dword that can start a type 3 packet: reserved bits clear and the packet within the
|
||||
// `remaining` dwords.
|
||||
bool PlausibleType3(u32 dword, std::size_t remaining) {
|
||||
const PM4Header header{.raw = dword};
|
||||
return header.type == 3 && (dword & 0xfc) == 0 && header.type3.NumWords() < remaining;
|
||||
}
|
||||
// bbport: an invalid packet header (Steam Deck: "PM4 type 0" with dword 0, 8 or 0x10, mid-game): where
|
||||
// in which buffer, what surrounds it, and which logged writes of ours landed in the buffer.
|
||||
void ReportBadPacket(uintptr_t base, std::size_t dwords, const u32* at, u64 seq, int depth) {
|
||||
@@ -1460,14 +1467,29 @@ Liverpool::Task Liverpool::ProcessGraphics(std::span<const u32> dcb, std::span<c
|
||||
|
||||
switch (type) {
|
||||
default:
|
||||
ReportBadPacket(base_addr, dcb_dwords, dcb.data(), seq, dcb_depth);
|
||||
UNREACHABLE_MSG("Wrong PM4 type {}", type);
|
||||
break;
|
||||
case 0:
|
||||
ReportBadPacket(base_addr, dcb_dwords, dcb.data(), seq, dcb_depth);
|
||||
UNREACHABLE_MSG("Unimplemented PM4 type 0, base reg: {}, size: {}",
|
||||
header->type0.base.Value(), header->type0.NumWords());
|
||||
break;
|
||||
case 0: {
|
||||
// bbport: an invalid header ended the game (UNREACHABLE: "Unimplemented PM4 type
|
||||
// 0"). Seen at area loads: a SET_SH_REG cut short, whose declared body holds the
|
||||
// next packets, leaves the decoder inside them. Decoding resumes at the next dword
|
||||
// that can start a type 3 packet, so the rest of the buffer and its fences still
|
||||
// run (dropping them would leave the guest waiting for them).
|
||||
static std::atomic<int> reports{0};
|
||||
if (reports.fetch_add(1, std::memory_order_relaxed) < 8) {
|
||||
ReportBadPacket(base_addr, dcb_dwords, dcb.data(), seq, dcb_depth);
|
||||
}
|
||||
std::size_t skip = 1;
|
||||
while (skip < dcb.size() && !PlausibleType3(dcb[skip], dcb.size() - skip)) {
|
||||
++skip;
|
||||
}
|
||||
std::fprintf(stderr,
|
||||
"PM4: invalid header %#x at dword %zu of the buffer, resynced %zu dwords "
|
||||
"later\n",
|
||||
header->raw, std::size_t((reinterpret_cast<uintptr_t>(dcb.data()) -
|
||||
base_addr) / sizeof(u32)),
|
||||
skip);
|
||||
dcb = NextPacket(dcb, skip);
|
||||
continue;
|
||||
}
|
||||
case 2:
|
||||
// Type-2 packet are used for padding purposes
|
||||
dcb = NextPacket(dcb, 1);
|
||||
|
||||
@@ -356,7 +356,10 @@ constexpr NumberConversion MapNumberConversion(const NumberFormat num_fmt,
|
||||
case DataFormat::Format16_16_16_16:
|
||||
return NumberConversion::Sint16ToSnormNz;
|
||||
default:
|
||||
UNREACHABLE_MSG("data_fmt = {}", u32(data_fmt));
|
||||
// bbport: was UNREACHABLE. Seen on macOS with data format 45 (FMASK 8 S4 F1) and
|
||||
// SnormNz, a combination no real resource has (a stale or unused descriptor); it
|
||||
// ended the game. No conversion is the harmless answer.
|
||||
return NumberConversion::None;
|
||||
}
|
||||
}
|
||||
default:
|
||||
|
||||
@@ -95,9 +95,16 @@ void main() {
|
||||
// Both depths are full precision. Do not accept an old foreground vector merely
|
||||
// because perspective depth compresses distant surfaces into a small interval.
|
||||
float depth_threshold = max(2e-7, (proj.z - depth) * 0.01);
|
||||
// bbport: an object vector far from the camera's at the same pixel read history that is
|
||||
// not this object's: still bushes behind the Hunter's Dream workshop got 100-650 px a
|
||||
// frame while the camera vectors around them were right. Real object motion beyond 5% of
|
||||
// the frame width per frame (32 px at low render sizes) is rarer than such history, so
|
||||
// the camera vector stays then.
|
||||
const float max_object_delta = max(0.05 * size.x, 32.0);
|
||||
if (object_motion.b > 0.99 && abs(object_motion.a - depth) < depth_threshold &&
|
||||
!any(isnan(object_motion.rg)) &&
|
||||
!any(isinf(object_motion.rg))) {
|
||||
!any(isinf(object_motion.rg)) &&
|
||||
distance(object_motion.rg, motion) < max_object_delta) {
|
||||
result = object_motion.rg;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <cstdlib>
|
||||
#include <limits>
|
||||
#include <span>
|
||||
#include <unordered_map>
|
||||
@@ -39,8 +40,22 @@ VertexRange IndexedRange(std::span<const Index, Extent> indices, int32_t base_ve
|
||||
// Vertex shader constant buffers (stride 16) of Bloodborne's G-buffer shaders: the 864-byte
|
||||
// scene constants (camera), 416-byte model constants and palettes of 3x4 bone matrices
|
||||
// (48 bytes per bone). Characters have 656+ bytes; weapons and props 2-8 bones (96-384).
|
||||
enum class BufferRole { Other, Skeleton, SmallSkeleton };
|
||||
// bbport: palettes of 16-64 KiB in whole bones (24576, 49152: 512-1024 bones, seen in Central
|
||||
// Yharnam where the large beasts smeared under FSR) are LargeSkeleton: gated like small ones
|
||||
// (history only while the palette changes), since instance data of static props may look the
|
||||
// same. BB_MOTION_LARGE=0 leaves them to camera vectors as before.
|
||||
enum class BufferRole { Other, Skeleton, SmallSkeleton, LargeSkeleton };
|
||||
inline bool LargePalettesEnabled() {
|
||||
static const bool enabled = [] {
|
||||
const char* env = std::getenv("BB_MOTION_LARGE");
|
||||
return !env || env[0] != '0';
|
||||
}();
|
||||
return enabled;
|
||||
}
|
||||
inline BufferRole ClassifyBuffer(uint32_t size) {
|
||||
if (size > 16384 && size <= 65536 && size % 48 == 0 && LargePalettesEnabled()) {
|
||||
return BufferRole::LargeSkeleton;
|
||||
}
|
||||
if (size == 864 || size > 16384) {
|
||||
return BufferRole::Other;
|
||||
}
|
||||
|
||||
@@ -14,10 +14,24 @@
|
||||
// Define assert-on-result to nothing to instead return the result for our handling.
|
||||
#define VULKAN_HPP_ASSERT_ON_RESULT
|
||||
|
||||
// vulkan.hpp adds fields to DispatchLoaderBase when NDEBUG is undefined, so the layout of
|
||||
// vk::detail::DispatchLoaderDynamic depends on it. libbbgpu is built with NDEBUG while the
|
||||
// renderer tests build with -UNDEBUG (their asserts must run); the inline init() copies the
|
||||
// linker keeps would then read the dispatcher at offsets another translation unit did not write
|
||||
// (a call through a null vkGetInstanceProcAddr). Include it with NDEBUG defined everywhere.
|
||||
#ifndef NDEBUG
|
||||
#define NDEBUG
|
||||
#define BB_VK_COMMON_RESTORE_ASSERTS
|
||||
#endif
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Wunused-value"
|
||||
#include <vulkan/vulkan.hpp>
|
||||
#pragma clang diagnostic pop
|
||||
#ifdef BB_VK_COMMON_RESTORE_ASSERTS
|
||||
#undef BB_VK_COMMON_RESTORE_ASSERTS
|
||||
#undef NDEBUG
|
||||
#include <cassert> // vulkan.hpp's own include of it just turned assert() off
|
||||
#endif
|
||||
|
||||
#define VMA_STATIC_VULKAN_FUNCTIONS 0
|
||||
#define VMA_DYNAMIC_VULKAN_FUNCTIONS 1
|
||||
@@ -58,11 +58,15 @@ void* FindSymbol(Module module, const char* name) {
|
||||
}
|
||||
#else
|
||||
// bbport (Linux): libbbport_dlss.so (gpu/dlss_bridge) and NVIDIA's libnvidia-ngx-dlss.so.<version>
|
||||
// next to bb-probe; NGX finds the latter through the bridge's path list.
|
||||
// next to bb-probe, or in BB_DLSS_DIR (packaged builds whose /proc/self/exe is the dynamic
|
||||
// loader, such as an AppImage); NGX finds the latter through the bridge's path list.
|
||||
constexpr char BridgeName[] = "libbbport_dlss.so";
|
||||
using Module = void*;
|
||||
|
||||
std::filesystem::path ExecutableDirectory() {
|
||||
if (const char* dir = std::getenv("BB_DLSS_DIR"); dir && dir[0]) {
|
||||
return dir;
|
||||
}
|
||||
std::error_code error;
|
||||
return std::filesystem::read_symlink("/proc/self/exe", error).parent_path();
|
||||
}
|
||||
@@ -256,6 +260,10 @@ void Dlss::AppendDeviceExtensions(vk::Instance instance, vk::PhysicalDevice phys
|
||||
impl->device_ready = true;
|
||||
}
|
||||
|
||||
void Dlss::Disable(std::string_view reason) {
|
||||
impl->Disable(reason);
|
||||
}
|
||||
|
||||
void Dlss::Initialize(vk::Instance instance, vk::PhysicalDevice physical, vk::Device device) {
|
||||
if (!impl->eligible || !impl->device_ready || impl->initialized) {
|
||||
return;
|
||||
@@ -343,6 +351,7 @@ Dlss* Dlss::Get() {
|
||||
void Dlss::AppendInstanceExtensions(std::vector<const char*>&) {}
|
||||
void Dlss::AppendDeviceExtensions(vk::Instance, vk::PhysicalDevice, std::vector<const char*>&) {}
|
||||
void Dlss::Initialize(vk::Instance, vk::PhysicalDevice, vk::Device) {}
|
||||
void Dlss::Disable(std::string_view) {}
|
||||
void Dlss::Shutdown() {}
|
||||
bool Dlss::Available() const {
|
||||
return false;
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
#include "common/types.h"
|
||||
@@ -31,6 +32,8 @@ public:
|
||||
std::vector<const char*>& enabled);
|
||||
/// After the device exists.
|
||||
void Initialize(vk::Instance instance, vk::PhysicalDevice physical, vk::Device device);
|
||||
/// Device creation can fail despite advertised NGX extensions (e.g. missing CUDA/UVM).
|
||||
void Disable(std::string_view reason);
|
||||
void Shutdown();
|
||||
|
||||
[[nodiscard]] bool Available() const;
|
||||
|
||||
@@ -31,6 +31,25 @@ struct Skipped {
|
||||
s64 NowNs() {
|
||||
return std::chrono::steady_clock::now().time_since_epoch().count();
|
||||
}
|
||||
|
||||
// BB_INDIRECT_GUARD_MAX: the most workgroups one indirect dispatch may launch. A value that is not
|
||||
// a positive number keeps the default: 0 would make the shader skip every indirect dispatch.
|
||||
u32 MaxTotal() {
|
||||
constexpr u32 Default = 1u << 22;
|
||||
const char* text = std::getenv("BB_INDIRECT_GUARD_MAX");
|
||||
if (!text || !*text) {
|
||||
return Default;
|
||||
}
|
||||
char* end = nullptr;
|
||||
const unsigned long long value = std::strtoull(text, &end, 0);
|
||||
if (*end != '\0' || value == 0 || value > 0xFFFFFFFFull) {
|
||||
std::printf("Indirect dispatches: BB_INDIRECT_GUARD_MAX=\"%s\" is not a number from 1 to "
|
||||
"4294967295, keeping the default %u\n",
|
||||
text, Default);
|
||||
return Default;
|
||||
}
|
||||
return static_cast<u32>(value);
|
||||
}
|
||||
} // namespace
|
||||
|
||||
bool IndirectGuard::Enabled() {
|
||||
@@ -57,8 +76,7 @@ IndirectGuard::IndirectGuard(const Instance& instance_, Scheduler& scheduler_)
|
||||
for (u32 i = 0; i < 3; ++i) {
|
||||
max_count[i] = limits.maxComputeWorkGroupCount[i];
|
||||
}
|
||||
const char* max = std::getenv("BB_INDIRECT_GUARD_MAX");
|
||||
max_total = max && *max ? static_cast<u32>(std::strtoul(max, nullptr, 0)) : 1u << 22;
|
||||
max_total = MaxTotal();
|
||||
|
||||
std::array<vk::DescriptorSetLayoutBinding, 3> bindings{};
|
||||
for (u32 i = 0; i < bindings.size(); ++i) {
|
||||
|
||||
@@ -196,7 +196,7 @@ Instance::Instance(const Frontend::WindowSystemInfo& window_info, s32 physical_d
|
||||
VK_VERSION_MAJOR(TargetVulkanApiVersion), VK_VERSION_MINOR(TargetVulkanApiVersion),
|
||||
VK_VERSION_MAJOR(properties.apiVersion), VK_VERSION_MINOR(properties.apiVersion));
|
||||
|
||||
CreateDevice();
|
||||
ASSERT_MSG(CreateDevice(), "Unable to initialize Vulkan device");
|
||||
CollectPhysicalMemoryInfo();
|
||||
CollectImageFormatInfo();
|
||||
CollectToolingInfo();
|
||||
@@ -459,6 +459,7 @@ bool Instance::CreateDevice() {
|
||||
const auto vk11_features = feature_chain.get<vk::PhysicalDeviceVulkan11Features>();
|
||||
vk12_features = feature_chain.get<vk::PhysicalDeviceVulkan12Features>();
|
||||
vk13_features = feature_chain.get<vk::PhysicalDeviceVulkan13Features>();
|
||||
const auto renderer_extension_count = enabled_extensions.size();
|
||||
// bbport: DLSS (optional bridge library) needs its own device extensions on NVIDIA GPUs.
|
||||
std::vector<const char*> dlss_extensions;
|
||||
if (Dlss* dlss = Dlss::Get()) {
|
||||
@@ -701,6 +702,24 @@ bool Instance::CreateDevice() {
|
||||
}
|
||||
|
||||
auto [device_result, dev] = physical_device.createDeviceUnique(device_chain.get());
|
||||
if (device_result != vk::Result::eSuccess &&
|
||||
enabled_extensions.size() > renderer_extension_count) {
|
||||
// NGX can advertise its extensions even when their runtime dependencies are missing
|
||||
// (e.g. NVIDIA UVM/CUDA on Linux). DLSS is optional: retry the renderer's original
|
||||
// extension set, and keep NGX from initializing against a device without its extensions.
|
||||
LOG_WARNING(Render_Vulkan, "Device creation with DLSS extensions failed: {}; "
|
||||
"retrying without DLSS", vk::to_string(device_result));
|
||||
if (Dlss* dlss = Dlss::Get()) {
|
||||
dlss->Disable(fmt::format("Vulkan device creation failed with DLSS extensions: {} "
|
||||
"(check NVIDIA CUDA/UVM availability)",
|
||||
vk::to_string(device_result)));
|
||||
}
|
||||
enabled_extensions.resize(renderer_extension_count);
|
||||
device_chain.get().enabledExtensionCount = static_cast<u32>(enabled_extensions.size());
|
||||
auto retry = physical_device.createDeviceUnique(device_chain.get());
|
||||
device_result = retry.result;
|
||||
dev = std::move(retry.value);
|
||||
}
|
||||
if (device_result != vk::Result::eSuccess) {
|
||||
LOG_CRITICAL(Render_Vulkan, "Failed to create device: {}", vk::to_string(device_result));
|
||||
return false;
|
||||
|
||||
@@ -1701,7 +1701,8 @@ void Rasterizer::DrawRecord(const GraphicsPipeline* pipeline, const PreparedDraw
|
||||
break;
|
||||
}
|
||||
const VAddr address = buffer.base_address;
|
||||
if (role == Motion::BufferRole::SmallSkeleton &&
|
||||
if ((role == Motion::BufferRole::SmallSkeleton ||
|
||||
role == Motion::BufferRole::LargeSkeleton) &&
|
||||
memory->IsValidGpuMapping(address, 0) &&
|
||||
memory->ClampRangeSize(address, size) == size) {
|
||||
palette = XXH3_64bits_withSeed(reinterpret_cast<const void*>(address), size,
|
||||
|
||||
@@ -17,6 +17,17 @@
|
||||
|
||||
namespace Vulkan {
|
||||
|
||||
/// bbport: records `regions` as one command per slice. A deferred command's data shares a
|
||||
/// recording chunk with it (128 KiB), and a download of the buffer cache can carry far more
|
||||
/// regions than that (178563 copies, 4.3 MB, loading an area on NVIDIA in 0.5).
|
||||
template <typename T, typename Record>
|
||||
static void RecordInSlices(Scheduler& scheduler, std::span<const T> regions, Record&& record) {
|
||||
constexpr std::size_t PerCommand = 64 * 1024 / sizeof(T);
|
||||
for (std::size_t at = 0; at < regions.size(); at += PerCommand) {
|
||||
record(scheduler.RecordData(regions.subspan(at, std::min(PerCommand, regions.size() - at))));
|
||||
}
|
||||
}
|
||||
|
||||
static vk::ImageType ConvertImageType(AmdGpu::ImageType type) noexcept {
|
||||
switch (type) {
|
||||
case AmdGpu::ImageType::Color1D:
|
||||
@@ -142,9 +153,11 @@ void Runtime::CopyBuffer(const VideoCore::Buffer* src, const VideoCore::Buffer*
|
||||
FlushBarriers();
|
||||
}
|
||||
|
||||
scheduler.Record([src_handle = src->Handle(), dst_handle = dst->Handle(),
|
||||
regions = scheduler.RecordData(copies)](vk::CommandBuffer cmdbuf) {
|
||||
cmdbuf.copyBuffer(src_handle, dst_handle, regions.size(), regions.data());
|
||||
RecordInSlices(scheduler, copies, [&](std::span<const vk::BufferCopy> regions) {
|
||||
scheduler.Record([src_handle = src->Handle(), dst_handle = dst->Handle(),
|
||||
regions](vk::CommandBuffer cmdbuf) {
|
||||
cmdbuf.copyBuffer(src_handle, dst_handle, regions.size(), regions.data());
|
||||
});
|
||||
});
|
||||
|
||||
if (bounded) {
|
||||
@@ -190,9 +203,10 @@ void Runtime::CopyFromGuestChunk(vk::Buffer src, const VideoCore::Buffer* dst,
|
||||
if (needs_flush) {
|
||||
FlushBarriers();
|
||||
}
|
||||
scheduler.Record([src, dst_handle = dst->Handle(),
|
||||
regions = scheduler.RecordData(copies)](vk::CommandBuffer cmdbuf) {
|
||||
cmdbuf.copyBuffer(src, dst_handle, regions.size(), regions.data());
|
||||
RecordInSlices(scheduler, copies, [&](std::span<const vk::BufferCopy> regions) {
|
||||
scheduler.Record([src, dst_handle = dst->Handle(), regions](vk::CommandBuffer cmdbuf) {
|
||||
cmdbuf.copyBuffer(src, dst_handle, regions.size(), regions.data());
|
||||
});
|
||||
});
|
||||
for (const auto& copy : copies) {
|
||||
AccessBuffer(dst, copy.dstOffset, copy.size, vk::PipelineStageFlagBits2::eCopy,
|
||||
@@ -286,10 +300,13 @@ void Runtime::UploadImage(VideoCore::Image* dst, const VideoCore::Buffer* src,
|
||||
FlushBarriers();
|
||||
}
|
||||
|
||||
scheduler.Record([src_handle = src->Handle(), image = dst->GetImage(),
|
||||
regions = scheduler.RecordData(upload_copies)](vk::CommandBuffer cmdbuf) {
|
||||
cmdbuf.copyBufferToImage(src_handle, image, vk::ImageLayout::eTransferDstOptimal,
|
||||
regions.size(), regions.data());
|
||||
RecordInSlices(scheduler, std::span<const vk::BufferImageCopy>{upload_copies},
|
||||
[&](std::span<const vk::BufferImageCopy> regions) {
|
||||
scheduler.Record([src_handle = src->Handle(), image = dst->GetImage(),
|
||||
regions](vk::CommandBuffer cmdbuf) {
|
||||
cmdbuf.copyBufferToImage(src_handle, image, vk::ImageLayout::eTransferDstOptimal,
|
||||
regions.size(), regions.data());
|
||||
});
|
||||
});
|
||||
|
||||
for (const auto& copy : upload_copies) {
|
||||
|
||||
@@ -1471,6 +1471,22 @@ void TemporalUpscaler::OnDraw(u64 vs_hash, VideoCore::ImageId color,
|
||||
}
|
||||
return;
|
||||
}
|
||||
// bbport: a UI draw without a color target (a Scaleform mask: color writes off) writes the
|
||||
// stencil that the UI's next draws test. It must go into the UI's output-size depth like
|
||||
// them: drawn into the guest depth, the mask was lost when the next UI draw switched to the
|
||||
// UI depth (PrepareUiDepth clears its stencil), and masked images (the loading screen's item
|
||||
// picture, cut out of a sheet of item pictures) vanished.
|
||||
if (ui_phase && !color && depth && depth != ui_depth && ui_color) {
|
||||
const auto& depth_image = texture_cache.GetImage(depth);
|
||||
const auto& ui_target = texture_cache.GetImage(ui_color);
|
||||
if (depth_image.info.size.width == ui_target.info.size.width &&
|
||||
depth_image.info.size.height == ui_target.info.size.height) {
|
||||
EnsureUiResources(ui_width, ui_height, ui_format, depth_image.info.pixel_format);
|
||||
PrepareUiDepth(depth);
|
||||
ui_depth = depth;
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (!Scaled() || !color) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -181,11 +181,18 @@ void TextureCache::DownloadImageMemory(ImageId image_id, bool sync) {
|
||||
}
|
||||
}
|
||||
|
||||
/// bbport: the guest memory a MaybeCpuDirty check compares, the same at marking and at
|
||||
/// refresh (they hashed different ranges, the whole image and its first 8x8 pixels, so the
|
||||
/// first check never matched). Such an image lies within the faulting page: cheap to hash
|
||||
/// whole, and a CPU write past its first pixels still counts.
|
||||
u64 TextureCache::MaybeDirtyHash(const Image& image) {
|
||||
return XXH3_64bits(std::bit_cast<const u8*>(image.info.guest_address), image.info.guest_size);
|
||||
}
|
||||
|
||||
void TextureCache::MarkAsMaybeDirty(ImageId image_id, Image& image) {
|
||||
if (image.hash == 0) {
|
||||
// Initialize hash
|
||||
const u8* addr = std::bit_cast<u8*>(image.info.guest_address);
|
||||
image.hash = XXH3_64bits(addr, image.info.guest_size);
|
||||
image.hash = MaybeDirtyHash(image);
|
||||
}
|
||||
image.flags |= ImageFlagBits::MaybeCpuDirty;
|
||||
UntrackImage(image_id);
|
||||
@@ -881,17 +888,7 @@ void TextureCache::RefreshImage(Image& image) {
|
||||
|
||||
if (True(image.flags & ImageFlagBits::MaybeCpuDirty) &&
|
||||
False(image.flags & ImageFlagBits::CpuDirty)) {
|
||||
// The image size should be less than page size to be considered MaybeCpuDirty
|
||||
// So this calculation should be very uncommon and reasonably fast
|
||||
// For now we'll just check up to 64 first pixels
|
||||
const auto addr = std::bit_cast<u8*>(image.info.guest_address);
|
||||
const u32 w = std::min(image.info.size.width, u32(8));
|
||||
const u32 h = std::min(image.info.size.height, u32(8));
|
||||
|
||||
const u32 s_w = image.info.props.is_block ? Common::DivCeil(w, 4u) : w;
|
||||
const u32 s_h = image.info.props.is_block ? Common::DivCeil(h, 4u) : h;
|
||||
const u32 size = s_w * s_h * (image.info.num_bits / 8);
|
||||
const u64 hash = XXH3_64bits(addr, size);
|
||||
const u64 hash = MaybeDirtyHash(image);
|
||||
if (image.hash == hash) {
|
||||
image.flags &= ~ImageFlagBits::MaybeCpuDirty;
|
||||
return;
|
||||
@@ -957,14 +954,18 @@ void TextureCache::RefreshImage(Image& image) {
|
||||
const auto [mip_size, mip_pitch, mip_height, mip_offset] = image.info.mips_layout[m];
|
||||
|
||||
// Protect GPU modified resources from accidental CPU reuploads.
|
||||
if (is_gpu_modified && !is_gpu_dirty) {
|
||||
const u8* addr = std::bit_cast<u8*>(image.info.guest_address);
|
||||
const u64 hash = XXH3_64bits(addr + mip_offset, mip_size);
|
||||
if (image.mip_hashes[m] == hash) {
|
||||
continue;
|
||||
}
|
||||
image.mip_hashes[m] = hash;
|
||||
// bbport: every upload records the guest memory it saw, not only uploads of images
|
||||
// the GPU had already written. An image the GPU writes after an upload from the buffer
|
||||
// cache (GpuDirty: a storage image's first binding) or from a plain texture otherwise
|
||||
// had no reference, and the first CPU write anywhere in its page replaced the GPU's
|
||||
// contents with stale guest memory (a 1x1 exposure texture computed once: the
|
||||
// character creation preview went black after one frame).
|
||||
const u8* mip_addr = std::bit_cast<u8*>(image.info.guest_address) + mip_offset;
|
||||
const u64 mip_hash = XXH3_64bits(mip_addr, mip_size);
|
||||
if (is_gpu_modified && !is_gpu_dirty && image.mip_hashes[m] == mip_hash) {
|
||||
continue;
|
||||
}
|
||||
image.mip_hashes[m] = mip_hash;
|
||||
|
||||
const u32 extent_width = mip_pitch ? std::min(mip_pitch, width) : width;
|
||||
const u32 extent_height = mip_height ? std::min(mip_height, height) : height;
|
||||
|
||||
@@ -380,6 +380,7 @@ private:
|
||||
void UntrackImageTail(ImageId image_id);
|
||||
|
||||
void MarkAsMaybeDirty(ImageId image_id, Image& image);
|
||||
static u64 MaybeDirtyHash(const Image& image);
|
||||
|
||||
/// Removes the image and any views/surface metas that reference it.
|
||||
void DeleteImage(ImageId image_id);
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
#include <boost/asio/io_context.hpp>
|
||||
#include "common/polyfill_thread.h"
|
||||
#include "video_core/renderdoc.h"
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
@@ -398,6 +399,27 @@ extern "C" int bbgpu_overlay_captures_input(void) {
|
||||
return BbOverlay::CapturesInput() ? 1 : 0;
|
||||
}
|
||||
|
||||
extern "C" void bbgpu_trophy_notice(int id, char grade, const char* name_utf8, const char* icon_path) {
|
||||
std::vector<u8> icon;
|
||||
int size = 0;
|
||||
if (icon_path != nullptr) {
|
||||
if (std::FILE* file = std::fopen(icon_path, "rb")) {
|
||||
u32 header[2]{};
|
||||
if (std::fread(header, sizeof(header), 1, file) == 1 && header[0] == header[1] &&
|
||||
header[0] > 0 && header[0] <= 512) {
|
||||
icon.resize(size_t(header[0]) * header[0] * 4);
|
||||
if (std::fread(icon.data(), 1, icon.size(), file) == icon.size()) {
|
||||
size = int(header[0]);
|
||||
} else {
|
||||
icon.clear();
|
||||
}
|
||||
}
|
||||
std::fclose(file);
|
||||
}
|
||||
}
|
||||
BbOverlay::TrophyNotice(id, grade, name_utf8 ? name_utf8 : "", std::move(icon), size);
|
||||
}
|
||||
|
||||
extern "C" void bbgpu_mouse_take(double* dx, double* dy, int* wheel_up, int* wheel_down) {
|
||||
double x = 0, y = 0;
|
||||
int up = 0, down = 0;
|
||||
|
||||
@@ -54,8 +54,9 @@ F Game(u64 va) {
|
||||
|
||||
// ---- Texts: ids from IdBase up, looked up by the game like its own messages. ----
|
||||
constexpr u32 IdBase = 0x7f0000;
|
||||
/// A text in every menu language, as the game's UTF-16.
|
||||
struct Text {
|
||||
std::u16string english, russian;
|
||||
std::array<std::u16string, int(BbSettings::MenuLanguage::Count)> languages;
|
||||
};
|
||||
std::vector<Text> texts;
|
||||
|
||||
@@ -84,21 +85,47 @@ std::u16string Utf16(const char* utf8) {
|
||||
return out;
|
||||
}
|
||||
u32 AddText(const char* english, const char* russian) {
|
||||
texts.push_back({Utf16(english), Utf16(russian)});
|
||||
Text text;
|
||||
for (int l = 0; l < int(BbSettings::MenuLanguage::Count); ++l) {
|
||||
text.languages[l] = Utf16(BbSettings::MenuTextIn(BbSettings::MenuLanguage(l), english, russian));
|
||||
}
|
||||
texts.push_back(std::move(text));
|
||||
return IdBase + u32(texts.size() - 1);
|
||||
}
|
||||
/// The game's own language decides (its "Screen/Sound" text in Cyrillic: Russian, else English);
|
||||
/// the port's menu language until the game has looked that text up.
|
||||
enum class GameLanguage { Unknown, English, Russian };
|
||||
std::atomic<GameLanguage> game_language{GameLanguage::Unknown};
|
||||
/// The game's language as the runtime gives it to the game (SystemService: BB_LANGUAGE, else the
|
||||
/// desktop's locale), as a menu language; English for the languages the menu does not have.
|
||||
BbSettings::MenuLanguage LanguageOfTheGame() {
|
||||
using BbSettings::MenuLanguage;
|
||||
int id = 1;
|
||||
if (const char* v = std::getenv("BB_LANGUAGE"); v && *v) {
|
||||
id = std::atoi(v);
|
||||
} else {
|
||||
for (const char* key : {"LC_ALL", "LC_MESSAGES", "LANG", "LANGUAGE"}) {
|
||||
if (const char* locale = std::getenv(key); locale && *locale) {
|
||||
const MenuLanguage l = BbSettings::MenuLanguageFromCode(locale);
|
||||
return l == MenuLanguage::PortugueseBrazil ? MenuLanguage::English : l;
|
||||
}
|
||||
}
|
||||
}
|
||||
switch (id) {
|
||||
case 2: return MenuLanguage::French;
|
||||
case 3: return MenuLanguage::Spanish;
|
||||
case 4: return MenuLanguage::German;
|
||||
case 5: return MenuLanguage::Italian;
|
||||
case 8: return MenuLanguage::Russian;
|
||||
default: return MenuLanguage::English;
|
||||
}
|
||||
}
|
||||
/// The game's own language decides (its "Screen/Sound" text in Cyrillic: Russian, else the
|
||||
/// language the runtime gave the game); the port's menu language until the game has looked that
|
||||
/// text up. -1: not known yet.
|
||||
std::atomic<int> game_language{-1};
|
||||
|
||||
const char16_t* TextOf(u32 id) {
|
||||
const Text& t = texts[id - IdBase];
|
||||
const GameLanguage game = game_language.load();
|
||||
const bool russian = game == GameLanguage::Unknown
|
||||
? BbSettings::Get().menu_language == BbSettings::MenuLanguage::Russian
|
||||
: game == GameLanguage::Russian;
|
||||
return russian ? t.russian.c_str() : t.english.c_str();
|
||||
const int game = game_language.load();
|
||||
const int language = game < 0 ? int(BbSettings::Get().menu_language.load()) : game;
|
||||
return t.languages[language].c_str();
|
||||
}
|
||||
|
||||
// ---- The port's values the game's rows edit (ints: the list rows bind ints). ----
|
||||
@@ -463,7 +490,7 @@ const char16_t* LookupHook(void* repository, u32 table, u32 category, u32 id) {
|
||||
for (const char16_t* c = text; *c; ++c) {
|
||||
cyrillic = cyrillic || (*c >= 0x400 && *c < 0x500);
|
||||
}
|
||||
game_language = cyrillic ? GameLanguage::Russian : GameLanguage::English;
|
||||
game_language = int(cyrillic ? BbSettings::MenuLanguage::Russian : LanguageOfTheGame());
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,683 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
// Overlay translations other than Russian (which sits next to the English in each MenuText call):
|
||||
// English text -> Spanish, Italian, French, German, Brazilian Portuguese. Only bbport_settings.cpp includes this.
|
||||
#pragma once
|
||||
|
||||
#include <array>
|
||||
#include <string_view>
|
||||
#include <unordered_map>
|
||||
|
||||
#include "bbport_settings.h"
|
||||
|
||||
namespace BbSettings {
|
||||
|
||||
/// The English text in another MenuLanguage (Spanish onwards); the English when missing.
|
||||
inline const char* MenuTranslation(MenuLanguage language, const char* english) {
|
||||
static constexpr int First = int(MenuLanguage::Spanish);
|
||||
using Row = std::array<const char*, int(MenuLanguage::Count) - First>;
|
||||
static const std::unordered_map<std::string_view, Row> table{
|
||||
{"Bloodborne - Graphics (Insert / L3+R3)###bbport_settings",
|
||||
{"Bloodborne - Gráficos (Insert / L3+R3)###bbport_settings",
|
||||
"Bloodborne - Grafica (Insert / L3+R3)###bbport_settings",
|
||||
"Bloodborne - Graphismes (Insert / L3+R3)###bbport_settings",
|
||||
"Bloodborne - Grafik (Insert / L3+R3)###bbport_settings",
|
||||
"Bloodborne - Gráficos (Insert / L3+R3)###bbport_settings"}},
|
||||
{"You have earned a trophy.",
|
||||
{"Has conseguido un trofeo.",
|
||||
"Hai ottenuto un trofeo.",
|
||||
"Vous avez obtenu un trophée.",
|
||||
"Du hast eine Trophäe erhalten.",
|
||||
"Você ganhou um troféu."}},
|
||||
{"%.0f FPS (%.1f ms)",
|
||||
{"%.0f FPS (%.1f ms)",
|
||||
"%.0f FPS (%.1f ms)",
|
||||
"%.0f FPS (%.1f ms)",
|
||||
"%.0f FPS (%.1f ms)",
|
||||
"%.0f FPS (%.1f ms)"}},
|
||||
{"Temporal upscaler",
|
||||
{"Escalador temporal",
|
||||
"Upscaler temporale",
|
||||
"Upscaler temporel",
|
||||
"Temporaler Upscaler",
|
||||
"Upscaler temporal"}},
|
||||
{"Off",
|
||||
{"Desactivado",
|
||||
"Disattivato",
|
||||
"Désactivé",
|
||||
"Aus",
|
||||
"Desativado"}},
|
||||
{"TAA (native anti-aliasing)",
|
||||
{"TAA (antialiasing nativo)",
|
||||
"TAA (anti-aliasing nativo)",
|
||||
"TAA (anticrénelage natif)",
|
||||
"TAA (native Kantenglättung)",
|
||||
"TAA (anti-aliasing nativo)"}},
|
||||
{"Upscaler",
|
||||
{"Escalador",
|
||||
"Upscaler",
|
||||
"Upscaler",
|
||||
"Upscaler",
|
||||
"Upscaler"}},
|
||||
{"— not supported by this GPU",
|
||||
{"— no compatible con esta GPU",
|
||||
"— non supportato da questa GPU",
|
||||
"— non pris en charge par ce GPU",
|
||||
"— von dieser GPU nicht unterstützt",
|
||||
"— não suportado por esta GPU"}},
|
||||
{"— in development",
|
||||
{"— en desarrollo",
|
||||
"— in sviluppo",
|
||||
"— en développement",
|
||||
"— in Entwicklung",
|
||||
"— em desenvolvimento"}},
|
||||
{"FSR 4 unavailable: %s",
|
||||
{"FSR 4 no disponible: %s",
|
||||
"FSR 4 non disponibile: %s",
|
||||
"FSR 4 indisponible : %s",
|
||||
"FSR 4 nicht verfügbar: %s",
|
||||
"FSR 4 indisponível: %s"}},
|
||||
{"The mode selected above is active. You can select FSR 4 again.",
|
||||
{"El modo seleccionado arriba está activo. Puedes volver a elegir FSR 4.",
|
||||
"La modalità selezionata sopra è attiva. Puoi selezionare di nuovo FSR 4.",
|
||||
"Le mode sélectionné ci-dessus est actif. Vous pouvez sélectionner FSR 4 à nouveau.",
|
||||
"Der oben gewählte Modus ist aktiv. Du kannst FSR 4 erneut auswählen.",
|
||||
"O modo selecionado acima está ativo. Você pode selecionar o FSR 4 novamente."}},
|
||||
{"FSR 4.1.1 in INT8 mode: the model from AMD's 4.1.1 DLL, reproduced in Vulkan (output matches the DLL). One model for Native through Performance and another for Ultra Performance. Assets: tools/fsr4cap/build_assets.sh (requires the DLL and Proton).",
|
||||
{"FSR 4.1.1 en modo INT8: el modelo de la DLL 4.1.1 de AMD, reproducido en Vulkan (la salida coincide con la DLL). Un modelo para Native hasta Performance y otro para Ultra Performance. Recursos: tools/fsr4cap/build_assets.sh (requiere la DLL y Proton).",
|
||||
"FSR 4.1.1 in modalità INT8: il modello della DLL AMD 4.1.1, riprodotto in Vulkan (l'output coincide con quello della DLL). Un modello per Native fino a Performance e un altro per Ultra Performance. Risorse: tools/fsr4cap/build_assets.sh (richiede la DLL e Proton).",
|
||||
"FSR 4.1.1 en mode INT8 : le modèle de la DLL 4.1.1 d'AMD, reproduit en Vulkan (la sortie correspond à la DLL). Un modèle pour Native à Performance et un autre pour Ultra Performance. Ressources : tools/fsr4cap/build_assets.sh (nécessite la DLL et Proton).",
|
||||
"FSR 4.1.1 im INT8-Modus: das Modell aus der 4.1.1-DLL von AMD, in Vulkan nachgebaut (Ausgabe identisch mit der DLL). Ein Modell für Native bis Performance und ein weiteres für Ultra Performance. Assets: tools/fsr4cap/build_assets.sh (benötigt die DLL und Proton).",
|
||||
"FSR 4.1.1 em modo INT8: o modelo da DLL 4.1.1 da AMD, reproduzido em Vulkan (a saída é igual à da DLL). Um modelo do Native ao Performance e outro para o Ultra Performance. Recursos: tools/fsr4cap/build_assets.sh (requer a DLL e o Proton)."}},
|
||||
{"FSR 4 in INT8 mode (v07 model from AMD FidelityFX SDK sources). Higher quality than FSR 3.1, but the pass is more demanding. Changing the preset rebuilds the model (a brief pause). Assets: tools/fetch_fsr4_assets.sh.",
|
||||
{"FSR 4 en modo INT8 (modelo v07 de las fuentes del FidelityFX SDK de AMD). Mayor calidad que FSR 3.1, pero la pasada es más exigente. Al cambiar el preajuste se reconstruye el modelo (una breve pausa). Recursos: tools/fetch_fsr4_assets.sh.",
|
||||
"FSR 4 in modalità INT8 (modello v07 dai sorgenti di AMD FidelityFX SDK). Qualità superiore a FSR 3.1, ma il passaggio è più pesante. Cambiando il preset il modello viene ricostruito (breve pausa). Risorse: tools/fetch_fsr4_assets.sh.",
|
||||
"FSR 4 en mode INT8 (modèle v07 issu des sources du FidelityFX SDK d'AMD). Qualité supérieure à FSR 3.1, mais la passe est plus gourmande. Changer de préréglage reconstruit le modèle (courte pause). Ressources : tools/fetch_fsr4_assets.sh.",
|
||||
"FSR 4 im INT8-Modus (Modell v07 aus den AMD-FidelityFX-SDK-Quellen). Höhere Qualität als FSR 3.1, aber der Durchlauf ist aufwendiger. Beim Wechsel der Voreinstellung wird das Modell neu aufgebaut (kurze Pause). Assets: tools/fetch_fsr4_assets.sh.",
|
||||
"FSR 4 em modo INT8 (modelo v07 das fontes do FidelityFX SDK da AMD). Qualidade maior que a do FSR 3.1, mas a passada é mais pesada. Mudar a predefinição reconstrói o modelo (uma breve pausa). Recursos: tools/fetch_fsr4_assets.sh."}},
|
||||
{"FSR 4: auto exposure",
|
||||
{"FSR 4: exposición automática",
|
||||
"FSR 4: esposizione automatica",
|
||||
"FSR 4 : exposition automatique",
|
||||
"FSR 4: Auto-Belichtung",
|
||||
"FSR 4: exposição automática"}},
|
||||
{"FSR 4: invert jitter sign",
|
||||
{"FSR 4: invertir signo del jitter",
|
||||
"FSR 4: inverti segno del jitter",
|
||||
"FSR 4 : inverser le signe du jitter",
|
||||
"FSR 4: Jitter-Vorzeichen invertieren",
|
||||
"FSR 4: inverter o sinal do jitter"}},
|
||||
{"For diagnosing ghosting: the FSR 4 network normalizes color by exposure and uses it to decide when to discard previous frames. Changes apply immediately, without a restart.",
|
||||
{"Para diagnosticar ghosting: la red de FSR 4 normaliza el color según la exposición y la usa para decidir cuándo descartar fotogramas anteriores. Los cambios se aplican al instante, sin reiniciar.",
|
||||
"Per diagnosticare il ghosting: la rete FSR 4 normalizza il colore in base all'esposizione e la usa per decidere quando scartare i fotogrammi precedenti. Le modifiche si applicano subito, senza riavvio.",
|
||||
"Pour diagnostiquer le ghosting : le réseau FSR 4 normalise la couleur selon l'exposition et s'en sert pour décider quand écarter les images précédentes. Les changements s'appliquent immédiatement, sans redémarrage.",
|
||||
"Zur Diagnose von Geisterbildern: Das FSR-4-Netz normalisiert die Farbe anhand der Belichtung und entscheidet damit, wann vorherige Frames verworfen werden. Änderungen gelten sofort, ohne Neustart.",
|
||||
"Para diagnosticar ghosting: a rede do FSR 4 normaliza a cor pela exposição e a usa para decidir quando descartar quadros anteriores. As mudanças valem na hora, sem reiniciar."}},
|
||||
{"Preset",
|
||||
{"Preajuste",
|
||||
"Preset",
|
||||
"Préréglage",
|
||||
"Voreinstellung",
|
||||
"Predefinição"}},
|
||||
{"%s (x%.1f, render %dx%d)",
|
||||
{"%s (x%.1f, render %dx%d)",
|
||||
"%s (x%.1f, render %dx%d)",
|
||||
"%s (x%.1f, rendu %dx%d)",
|
||||
"%s (x%.1f, Render %dx%d)",
|
||||
"%s (x%.1f, render %dx%d)"}},
|
||||
{"TAA anti-aliases the scene at output resolution, without an FSR model or upscaling. Your saved FSR preset is restored when you select FSR.",
|
||||
{"TAA aplica antialiasing a la escena en la resolución de salida, sin modelo de FSR ni escalado. Tu preajuste de FSR guardado se restaura al elegir FSR.",
|
||||
"Il TAA applica l'anti-aliasing alla scena alla risoluzione di uscita, senza modello FSR né upscaling. Il preset FSR salvato viene ripristinato quando selezioni FSR.",
|
||||
"Le TAA applique l'anticrénelage à la scène à la résolution de sortie, sans modèle FSR ni upscaling. Votre préréglage FSR enregistré est restauré quand vous sélectionnez FSR.",
|
||||
"TAA glättet die Szene in Ausgabeauflösung, ohne FSR-Modell und ohne Upscaling. Deine gespeicherte FSR-Voreinstellung wird wiederhergestellt, sobald du FSR wählst.",
|
||||
"O TAA aplica anti-aliasing à cena na resolução de saída, sem modelo do FSR nem upscaling. Sua predefinição salva do FSR volta ao selecionar o FSR."}},
|
||||
{"Active scene render: %d x %d",
|
||||
{"Render activo de la escena: %d x %d",
|
||||
"Render attivo della scena: %d x %d",
|
||||
"Rendu actif de la scène : %d x %d",
|
||||
"Aktives Szenen-Rendering: %d x %d",
|
||||
"Render atual da cena: %d x %d"}},
|
||||
{"Startup preset: %s",
|
||||
{"Preajuste de inicio: %s",
|
||||
"Preset all'avvio: %s",
|
||||
"Préréglage au démarrage : %s",
|
||||
"Startvoreinstellung: %s",
|
||||
"Predefinição de início: %s"}},
|
||||
{"When output is not 1080p, the entire game renders at the preset resolution (startup patch): this is fastest on Steam Deck and weaker GPUs. Preset or output resolution changes require a restart. Enable Live resolution changes below to change them without restarting (post-processing then stays at 1080p, which is slower).",
|
||||
{"Si la salida no es 1080p, todo el juego se renderiza en la resolución del preajuste (parche de inicio): es lo más rápido en la Steam Deck y en GPU modestas. Los cambios de preajuste o de resolución de salida requieren reiniciar. Activa Cambio de resolución en tiempo real más abajo para cambiarlos sin reiniciar (el posprocesado se queda entonces en 1080p, que es más lento).",
|
||||
"Se l'uscita non è 1080p, tutto il gioco viene renderizzato alla risoluzione del preset (patch all'avvio): è la scelta più veloce su Steam Deck e GPU meno potenti. Per cambiare preset o risoluzione di uscita serve riavviare. Attiva Cambio di risoluzione in tempo reale qui sotto per cambiarli senza riavviare (la post-elaborazione resta però a 1080p, che è più lento).",
|
||||
"Quand la sortie n'est pas en 1080p, tout le jeu est rendu à la résolution du préréglage (patch de démarrage) : c'est le plus rapide sur Steam Deck et les GPU modestes. Changer de préréglage ou de résolution de sortie nécessite un redémarrage. Activez Changement de résolution en direct ci-dessous pour les changer sans redémarrer (le post-traitement reste alors en 1080p, ce qui est plus lent).",
|
||||
"Ist die Ausgabe nicht 1080p, rendert das ganze Spiel in der Auflösung der Voreinstellung (Startpatch): Das ist am schnellsten auf dem Steam Deck und schwächeren GPUs. Änderungen an Voreinstellung oder Ausgabeauflösung erfordern einen Neustart. Aktiviere unten Live-Auflösungswechsel, um sie ohne Neustart zu ändern (die Nachbearbeitung bleibt dann bei 1080p, was langsamer ist).",
|
||||
"Quando a saída não é 1080p, o jogo inteiro é renderizado na resolução da predefinição (patch de início): é o mais rápido no Steam Deck e em GPUs mais fracas. Mudanças de predefinição ou de resolução de saída exigem reiniciar. Ative Mudança de resolução em tempo real abaixo para mudá-las sem reiniciar (o pós-processamento fica então em 1080p, o que é mais lento)."}},
|
||||
{"BB_RENDER_RES fixes the scene size at startup. Remove this explicit environment variable to change resolution and presets without restarting the game.",
|
||||
{"BB_RENDER_RES fija el tamaño de la escena al iniciar. Elimina esta variable de entorno explícita para cambiar la resolución y los preajustes sin reiniciar el juego.",
|
||||
"BB_RENDER_RES fissa la dimensione della scena all'avvio. Rimuovi questa variabile d'ambiente esplicita per cambiare risoluzione e preset senza riavviare il gioco.",
|
||||
"BB_RENDER_RES fixe la taille de la scène au démarrage. Supprimez cette variable d'environnement explicite pour changer la résolution et les préréglages sans redémarrer le jeu.",
|
||||
"BB_RENDER_RES legt die Szenengröße beim Start fest. Entferne diese explizite Umgebungsvariable, um Auflösung und Voreinstellungen ohne Neustart des Spiels zu ändern.",
|
||||
"BB_RENDER_RES fixa o tamanho da cena no início. Remova essa variável de ambiente explícita para mudar a resolução e as predefinições sem reiniciar o jogo."}},
|
||||
{"Native AA: FSR acts as anti-aliasing. Other presets reduce the scene render resolution relative to the output. The UI renders at output resolution. The preset applies from the next frame without restarting the game.",
|
||||
{"Native AA: FSR actúa como antialiasing. Los demás preajustes reducen la resolución de render de la escena respecto a la salida. La interfaz se renderiza en la resolución de salida. El preajuste se aplica desde el siguiente fotograma, sin reiniciar el juego.",
|
||||
"Native AA: FSR agisce come anti-aliasing. Gli altri preset riducono la risoluzione di render della scena rispetto all'uscita. L'interfaccia viene renderizzata alla risoluzione di uscita. Il preset si applica dal fotogramma successivo, senza riavviare il gioco.",
|
||||
"Native AA : FSR sert d'anticrénelage. Les autres préréglages réduisent la résolution de rendu de la scène par rapport à la sortie. L'interface est rendue à la résolution de sortie. Le préréglage s'applique dès l'image suivante, sans redémarrer le jeu.",
|
||||
"Native AA: FSR dient als Kantenglättung. Andere Voreinstellungen verringern die Render-Auflösung der Szene relativ zur Ausgabe. Die UI rendert in Ausgabeauflösung. Die Voreinstellung gilt ab dem nächsten Frame, ohne Neustart des Spiels.",
|
||||
"Native AA: o FSR atua como anti-aliasing. As outras predefinições reduzem a resolução de render da cena em relação à saída. A interface é renderizada na resolução de saída. A predefinição vale a partir do próximo quadro, sem reiniciar o jogo."}},
|
||||
{"Sharpening (RCAS)",
|
||||
{"Nitidez (RCAS)",
|
||||
"Nitidezza (RCAS)",
|
||||
"Netteté (RCAS)",
|
||||
"Schärfen (RCAS)",
|
||||
"Nitidez (RCAS)"}},
|
||||
{"Sharpness",
|
||||
{"Nitidez",
|
||||
"Nitidezza",
|
||||
"Netteté",
|
||||
"Schärfe",
|
||||
"Nitidez"}},
|
||||
{"Up to 1: the upscaler's own sharpening (RCAS). Above 1 adds another RCAS pass. Ctrl+click the slider to enter an exact value.",
|
||||
{"Hasta 1: el afilado propio del escalador (RCAS). Por encima de 1 se añade otra pasada de RCAS. Ctrl+clic en el control deslizante para escribir un valor exacto.",
|
||||
"Fino a 1: la nitidezza propria dell'upscaler (RCAS). Oltre 1 aggiunge un altro passaggio RCAS. Ctrl+clic sul cursore per inserire un valore esatto.",
|
||||
"Jusqu'à 1 : la netteté propre à l'upscaler (RCAS). Au-delà de 1, une passe RCAS supplémentaire s'ajoute. Ctrl+clic sur le curseur pour saisir une valeur exacte.",
|
||||
"Bis 1: die eigene Schärfung des Upscalers (RCAS). Über 1 kommt ein weiterer RCAS-Durchlauf hinzu. Strg+Klick auf den Regler, um einen genauen Wert einzugeben.",
|
||||
"Até 1: a nitidez do próprio upscaler (RCAS). Acima de 1 é adicionada outra passada de RCAS. Ctrl+clique no controle deslizante para digitar um valor exato."}},
|
||||
{"Subpixel jitter",
|
||||
{"Jitter subpíxel",
|
||||
"Jitter subpixel",
|
||||
"Jitter subpixel",
|
||||
"Subpixel-Jitter",
|
||||
"Jitter de subpixel"}},
|
||||
{"Each frame shifts the scene by a fraction of a pixel, letting the upscaler reconstruct more detail from multiple frames. Without it, only history-based anti-aliasing remains.",
|
||||
{"En cada fotograma la escena se desplaza una fracción de píxel, lo que permite al escalador reconstruir más detalle a partir de varios fotogramas. Sin él, solo queda el antialiasing basado en el historial.",
|
||||
"A ogni fotogramma la scena si sposta di una frazione di pixel, così l'upscaler ricostruisce più dettagli da più fotogrammi. Senza, resta solo l'anti-aliasing basato sulla cronologia.",
|
||||
"À chaque image, la scène est décalée d'une fraction de pixel, ce qui permet à l'upscaler de reconstruire plus de détails à partir de plusieurs images. Sans lui, il ne reste que l'anticrénelage basé sur l'historique.",
|
||||
"Jeder Frame verschiebt die Szene um einen Bruchteil eines Pixels, sodass der Upscaler aus mehreren Frames mehr Details rekonstruieren kann. Ohne Jitter bleibt nur historienbasierte Kantenglättung.",
|
||||
"A cada quadro a cena se desloca uma fração de pixel, o que permite ao upscaler reconstruir mais detalhes a partir de vários quadros. Sem ele, resta apenas o anti-aliasing baseado no histórico."}},
|
||||
{"Reactive mask",
|
||||
{"Máscara reactiva",
|
||||
"Maschera reattiva",
|
||||
"Masque réactif",
|
||||
"Reaktive Maske",
|
||||
"Máscara reativa"}},
|
||||
{"Enable mask",
|
||||
{"Activar máscara",
|
||||
"Attiva maschera",
|
||||
"Activer le masque",
|
||||
"Maske aktivieren",
|
||||
"Ativar máscara"}},
|
||||
{"Marks transparent effects (haze, light, water, particles). FSR 4 takes no mask itself: its output is blended with the current frame there, so the effects do not drag previous frames along.",
|
||||
{"Marca los efectos transparentes (neblina, luz, agua, partículas). FSR 4 no admite máscara: en esas zonas su salida se mezcla con el fotograma actual, para que los efectos no arrastren fotogramas anteriores.",
|
||||
"Segna gli effetti trasparenti (foschia, luce, acqua, particelle). FSR 4 non usa una maschera: lì il suo output viene fuso con il fotogramma corrente, così gli effetti non trascinano i fotogrammi precedenti.",
|
||||
"Marque les effets transparents (brume, lumière, eau, particules). FSR 4 n'utilise pas de masque : sa sortie est mélangée avec l'image actuelle à ces endroits, pour que les effets n'entraînent pas les images précédentes.",
|
||||
"Markiert transparente Effekte (Dunst, Licht, Wasser, Partikel). FSR 4 nimmt selbst keine Maske: Dort wird seine Ausgabe mit dem aktuellen Frame gemischt, damit die Effekte keine früheren Frames mitziehen.",
|
||||
"Marca os efeitos transparentes (névoa, luz, água, partículas). O FSR 4 não aceita máscara: nessas áreas sua saída é misturada ao quadro atual, para que os efeitos não arrastem quadros anteriores."}},
|
||||
{"Marks transparent effects (particles, haze) so the upscaler relies less on previous frames. Reduces trails behind effects, but shimmering returns underneath them.",
|
||||
{"Marca los efectos transparentes (partículas, neblina) para que el escalador dependa menos de los fotogramas anteriores. Reduce las estelas tras los efectos, pero vuelve el centelleo bajo ellos.",
|
||||
"Segna gli effetti trasparenti (particelle, foschia) così l'upscaler si affida meno ai fotogrammi precedenti. Riduce le scie dietro gli effetti, ma sotto di essi torna lo sfarfallio.",
|
||||
"Marque les effets transparents (particules, brume) pour que l'upscaler s'appuie moins sur les images précédentes. Réduit les traînées derrière les effets, mais le scintillement revient en dessous.",
|
||||
"Markiert transparente Effekte (Partikel, Dunst), damit sich der Upscaler weniger auf frühere Frames stützt. Verringert Schlieren hinter Effekten, aber darunter kehrt das Flimmern zurück.",
|
||||
"Marca os efeitos transparentes (partículas, névoa) para que o upscaler dependa menos dos quadros anteriores. Reduz os rastros atrás dos efeitos, mas a cintilação volta embaixo deles."}},
|
||||
{"Scale",
|
||||
{"Escala",
|
||||
"Scala",
|
||||
"Échelle",
|
||||
"Skalierung",
|
||||
"Escala"}},
|
||||
{"Threshold",
|
||||
{"Umbral",
|
||||
"Soglia",
|
||||
"Seuil",
|
||||
"Schwellenwert",
|
||||
"Limiar"}},
|
||||
{"Maximum",
|
||||
{"Máximo",
|
||||
"Massimo",
|
||||
"Maximum",
|
||||
"Maximum",
|
||||
"Máximo"}},
|
||||
{"Show mask (debug)",
|
||||
{"Mostrar máscara (depuración)",
|
||||
"Mostra maschera (debug)",
|
||||
"Afficher le masque (debug)",
|
||||
"Maske anzeigen (Debug)",
|
||||
"Mostrar máscara (depuração)"}},
|
||||
{"Character motion vectors",
|
||||
{"Vectores de movimiento de personajes",
|
||||
"Vettori di movimento dei personaggi",
|
||||
"Vecteurs de mouvement des personnages",
|
||||
"Bewegungsvektoren von Figuren",
|
||||
"Vetores de movimento dos personagens"}},
|
||||
{"Accurate vectors for animated objects: clothing and weapons break up less during movement. The static scene does not get an additional pass. Changes apply after restarting the game.",
|
||||
{"Vectores precisos para objetos animados: la ropa y las armas se deshacen menos al moverse. La escena estática no recibe una pasada adicional. Los cambios se aplican al reiniciar el juego.",
|
||||
"Vettori accurati per gli oggetti animati: vestiti e armi si deformano meno durante il movimento. La scena statica non riceve un passaggio aggiuntivo. Le modifiche si applicano dopo il riavvio del gioco.",
|
||||
"Vecteurs précis pour les objets animés : vêtements et armes se dégradent moins pendant le mouvement. La scène statique n'a pas de passe supplémentaire. Les changements s'appliquent après le redémarrage du jeu.",
|
||||
"Genaue Vektoren für animierte Objekte: Kleidung und Waffen lösen sich bei Bewegung weniger auf. Die statische Szene bekommt keinen zusätzlichen Durchlauf. Änderungen gelten nach einem Neustart des Spiels.",
|
||||
"Vetores precisos para objetos animados: roupas e armas se desfazem menos em movimento. A cena estática não recebe uma passada adicional. As mudanças valem após reiniciar o jogo."}},
|
||||
{"Show motion vectors (debug)",
|
||||
{"Mostrar vectores de movimiento (depuración)",
|
||||
"Mostra vettori di movimento (debug)",
|
||||
"Afficher les vecteurs de mouvement (debug)",
|
||||
"Bewegungsvektoren anzeigen (Debug)",
|
||||
"Mostrar vetores de movimento (depuração)"}},
|
||||
{"Red/green: horizontal/vertical motion (8 pixels = full brightness). Blue: the pixel received an accurate object vector, not just camera motion. The upscaler treats a moving object with no blue or red/green as stationary, causing trails.",
|
||||
{"Rojo/verde: movimiento horizontal/vertical (8 píxeles = brillo máximo). Azul: el píxel ha recibido un vector de objeto preciso, no solo el movimiento de la cámara. El escalador trata un objeto en movimiento sin azul ni rojo/verde como estático, lo que causa estelas.",
|
||||
"Rosso/verde: movimento orizzontale/verticale (8 pixel = luminosità piena). Blu: il pixel ha ricevuto un vettore accurato dell'oggetto, non solo il movimento della camera. L'upscaler tratta un oggetto in movimento senza blu né rosso/verde come fermo, causando scie.",
|
||||
"Rouge/vert : mouvement horizontal/vertical (8 pixels = luminosité maximale). Bleu : le pixel a reçu un vecteur d'objet précis, et pas seulement le mouvement de la caméra. L'upscaler traite un objet en mouvement sans bleu ni rouge/vert comme immobile, ce qui cause des traînées.",
|
||||
"Rot/Grün: horizontale/vertikale Bewegung (8 Pixel = volle Helligkeit). Blau: Der Pixel hat einen genauen Objektvektor erhalten, nicht nur die Kamerabewegung. Der Upscaler behandelt ein bewegtes Objekt ohne Blau oder Rot/Grün als ruhend, was Schlieren verursacht.",
|
||||
"Vermelho/verde: movimento horizontal/vertical (8 pixels = brilho máximo). Azul: o pixel recebeu um vetor de objeto preciso, não só o movimento da câmera. O upscaler trata um objeto em movimento sem azul nem vermelho/verde como parado, o que causa rastros."}},
|
||||
{"Output resolution",
|
||||
{"Resolución de salida",
|
||||
"Risoluzione di uscita",
|
||||
"Résolution de sortie",
|
||||
"Ausgabeauflösung",
|
||||
"Resolução de saída"}},
|
||||
{"Size of the final frame and UI. The preset sets the scene size relative to the output: 4K Performance = 1920x1080. Applies after restarting the game.",
|
||||
{"Tamaño del fotograma final y de la interfaz. El preajuste fija el tamaño de la escena respecto a la salida: 4K Performance = 1920x1080. Se aplica al reiniciar el juego.",
|
||||
"Dimensione del fotogramma finale e dell'interfaccia. Il preset imposta la dimensione della scena rispetto all'uscita: 4K Performance = 1920x1080. Si applica dopo il riavvio del gioco.",
|
||||
"Taille de l'image finale et de l'interface. Le préréglage définit la taille de la scène par rapport à la sortie : 4K Performance = 1920x1080. S'applique après le redémarrage du jeu.",
|
||||
"Größe des finalen Frames und der UI. Die Voreinstellung legt die Szenengröße relativ zur Ausgabe fest: 4K Performance = 1920x1080. Gilt nach einem Neustart des Spiels.",
|
||||
"Tamanho do quadro final e da interface. A predefinição define o tamanho da cena em relação à saída: 4K Performance = 1920x1080. Vale após reiniciar o jogo."}},
|
||||
{"The final frame and UI size changes at the next frame boundary. The preset sets the scene size relative to the output: 4K Performance = 1920x1080. Changing the size resets FSR history and may cause a brief pause.",
|
||||
{"El tamaño del fotograma final y de la interfaz cambia en el siguiente límite de fotograma. El preajuste fija el tamaño de la escena respecto a la salida: 4K Performance = 1920x1080. Al cambiar el tamaño se reinicia el historial de FSR y puede haber una breve pausa.",
|
||||
"La dimensione del fotogramma finale e dell'interfaccia cambia al fotogramma successivo. Il preset imposta la dimensione della scena rispetto all'uscita: 4K Performance = 1920x1080. Cambiare la dimensione azzera la cronologia FSR e può causare una breve pausa.",
|
||||
"La taille de l'image finale et de l'interface change à la prochaine image. Le préréglage définit la taille de la scène par rapport à la sortie : 4K Performance = 1920x1080. Changer la taille réinitialise l'historique FSR et peut causer une courte pause.",
|
||||
"Größe des finalen Frames und der UI ändert sich an der nächsten Frame-Grenze. Die Voreinstellung legt die Szenengröße relativ zur Ausgabe fest: 4K Performance = 1920x1080. Eine Größenänderung setzt den FSR-Verlauf zurück und kann eine kurze Pause verursachen.",
|
||||
"O tamanho do quadro final e da interface muda no próximo limite de quadro. A predefinição define o tamanho da cena em relação à saída: 4K Performance = 1920x1080. Mudar o tamanho reinicia o histórico do FSR e pode causar uma breve pausa."}},
|
||||
{"Auto (based on GPU)",
|
||||
{"Automático (según la GPU)",
|
||||
"Auto (in base alla GPU)",
|
||||
"Auto (selon le GPU)",
|
||||
"Auto (je nach GPU)",
|
||||
"Automático (pela GPU)"}},
|
||||
{"Off (faster)",
|
||||
{"Desactivado (más rápido)",
|
||||
"Disattivato (più veloce)",
|
||||
"Désactivé (plus rapide)",
|
||||
"Aus (schneller)",
|
||||
"Desativado (mais rápido)"}},
|
||||
{"On",
|
||||
{"Activado",
|
||||
"Attivato",
|
||||
"Activé",
|
||||
"An",
|
||||
"Ativado"}},
|
||||
{"Live resolution changes",
|
||||
{"Cambio de resolución en tiempo real",
|
||||
"Cambio di risoluzione in tempo reale",
|
||||
"Changement de résolution en direct",
|
||||
"Live-Auflösungswechsel",
|
||||
"Mudança de resolução em tempo real"}},
|
||||
{"On: output resolution and preset change without restarting, but game post-processing stays at 1080p, which is noticeably slower on Steam Deck and older GPUs. Off: everything renders at the preset resolution, and changes require a restart. Auto enables this on powerful discrete GPUs. Applies after restarting the game.",
|
||||
{"Activado: la resolución de salida y el preajuste cambian sin reiniciar, pero el posprocesado del juego se queda en 1080p, bastante más lento en la Steam Deck y en GPU antiguas. Desactivado: todo se renderiza en la resolución del preajuste y los cambios requieren reiniciar. Automático lo activa en GPU dedicadas potentes. Se aplica al reiniciar el juego.",
|
||||
"Attivato: risoluzione di uscita e preset cambiano senza riavviare, ma la post-elaborazione del gioco resta a 1080p, il che è sensibilmente più lento su Steam Deck e GPU meno recenti. Disattivato: tutto viene renderizzato alla risoluzione del preset e i cambi richiedono un riavvio. Auto lo attiva sulle GPU discrete potenti. Si applica dopo il riavvio del gioco.",
|
||||
"Activé : la résolution de sortie et le préréglage changent sans redémarrage, mais le post-traitement du jeu reste en 1080p, ce qui est nettement plus lent sur Steam Deck et les anciens GPU. Désactivé : tout est rendu à la résolution du préréglage, et les changements nécessitent un redémarrage. Auto l'active sur les GPU dédiés puissants. S'applique après le redémarrage du jeu.",
|
||||
"An: Ausgabeauflösung und Voreinstellung ändern sich ohne Neustart, aber die Nachbearbeitung des Spiels bleibt bei 1080p, was auf dem Steam Deck und älteren GPUs deutlich langsamer ist. Aus: Alles rendert in der Auflösung der Voreinstellung, Änderungen erfordern einen Neustart. Auto aktiviert dies auf leistungsstarken dedizierten GPUs. Gilt nach einem Neustart des Spiels.",
|
||||
"Ativado: a resolução de saída e a predefinição mudam sem reiniciar, mas o pós-processamento do jogo fica em 1080p, bem mais lento no Steam Deck e em GPUs antigas. Desativado: tudo é renderizado na resolução da predefinição e as mudanças exigem reiniciar. Automático ativa em GPUs dedicadas potentes. Vale após reiniciar o jogo."}},
|
||||
{"Game effects (restart required)",
|
||||
{"Efectos del juego (requiere reinicio)",
|
||||
"Effetti del gioco (riavvio necessario)",
|
||||
"Effets du jeu (redémarrage requis)",
|
||||
"Spieleffekte (Neustart nötig)",
|
||||
"Efeitos do jogo (requer reinício)"}},
|
||||
{"Highest (-2)",
|
||||
{"Máximo (-2)",
|
||||
"Massimo (-2)",
|
||||
"Maximal (-2)",
|
||||
"Höchste (-2)",
|
||||
"Máximo (-2)"}},
|
||||
{"Game default",
|
||||
{"Predeterminado del juego",
|
||||
"Predefinito del gioco",
|
||||
"Comme dans le jeu",
|
||||
"Spielstandard",
|
||||
"Padrão do jogo"}},
|
||||
{"Lower (1)",
|
||||
{"Bajo (1)",
|
||||
"Basso (1)",
|
||||
"Plus bas (1)",
|
||||
"Niedriger (1)",
|
||||
"Baixo (1)"}},
|
||||
{"Lowest (2)",
|
||||
{"Mínimo (2)",
|
||||
"Minimo (2)",
|
||||
"Minimal (2)",
|
||||
"Niedrigste (2)",
|
||||
"Mínimo (2)"}},
|
||||
{"Model detail",
|
||||
{"Detalle de modelos",
|
||||
"Dettaglio dei modelli",
|
||||
"Détail des modèles",
|
||||
"Modelldetails",
|
||||
"Detalhe dos modelos"}},
|
||||
{"Effects are enabled and disabled by game patches at startup (patches/Bloodborne.xml). Motion blur and shadows from dynamic lights place a significant load on the GPU.",
|
||||
{"Los parches del juego activan y desactivan los efectos al iniciar (patches/Bloodborne.xml). El desenfoque de movimiento y las sombras de luces dinámicas suponen una carga considerable para la GPU.",
|
||||
"Gli effetti vengono attivati e disattivati dalle patch del gioco all'avvio (patches/Bloodborne.xml). Motion blur e ombre delle luci dinamiche pesano molto sulla GPU.",
|
||||
"Les effets sont activés et désactivés par des correctifs du jeu au démarrage (patches/Bloodborne.xml). Le flou de mouvement et les ombres des lumières dynamiques pèsent lourd sur le GPU.",
|
||||
"Effekte werden beim Start durch Spiel-Patches aktiviert und deaktiviert (patches/Bloodborne.xml). Bewegungsunschärfe und Schatten dynamischer Lichter belasten die GPU erheblich.",
|
||||
"Os efeitos são ativados e desativados por patches do jogo no início (patches/Bloodborne.xml). O desfoque de movimento e as sombras de luzes dinâmicas pesam bastante na GPU."}},
|
||||
{"Free camera: hold Cross and press L3 (keyboard: Space + Z). Debug menu: left touchpad / Tab. Requires DbgFont14h.ccm and DbgFont14h.tpf in dvdroot_ps4/font from Nexus mod #253. Right touchpad: Backspace.",
|
||||
{"Cámara libre: mantén Cross y pulsa L3 (teclado: Space + Z). Debug menu: panel táctil izquierdo / Tab. Requiere DbgFont14h.ccm y DbgFont14h.tpf en dvdroot_ps4/font del mod de Nexus n.º 253. Panel táctil derecho: Backspace.",
|
||||
"Camera libera: tieni premuto Cross e premi L3 (tastiera: Space + Z). Debug menu: touchpad sinistro / Tab. Richiede DbgFont14h.ccm e DbgFont14h.tpf in dvdroot_ps4/font dalla mod Nexus #253. Touchpad destro: Backspace.",
|
||||
"Caméra libre : maintenez Cross et appuyez sur L3 (clavier : Space + Z). Debug menu : pavé tactile gauche / Tab. Nécessite DbgFont14h.ccm et DbgFont14h.tpf dans dvdroot_ps4/font, depuis le mod Nexus #253. Pavé tactile droit : Backspace.",
|
||||
"Freie Kamera: Cross halten und L3 drücken (Tastatur: Space + Z). Debug menu: linkes Touchpad / Tab. Benötigt DbgFont14h.ccm und DbgFont14h.tpf in dvdroot_ps4/font aus Nexus-Mod #253. Rechtes Touchpad: Backspace.",
|
||||
"Câmera livre: segure Cross e aperte L3 (teclado: Space + Z). Debug menu: touchpad esquerdo / Tab. Requer DbgFont14h.ccm e DbgFont14h.tpf em dvdroot_ps4/font, do mod n.º 253 do Nexus. Touchpad direito: Backspace."}},
|
||||
{"Changes apply after restarting the game",
|
||||
{"Los cambios se aplican al reiniciar el juego",
|
||||
"Le modifiche si applicano dopo il riavvio del gioco",
|
||||
"Les changements s'appliquent après le redémarrage du jeu",
|
||||
"Änderungen gelten nach einem Neustart des Spiels",
|
||||
"As mudanças valem após reiniciar o jogo"}},
|
||||
{"Apply and restart game",
|
||||
{"Aplicar y reiniciar el juego",
|
||||
"Applica e riavvia il gioco",
|
||||
"Appliquer et redémarrer le jeu",
|
||||
"Anwenden und Spiel neu starten",
|
||||
"Aplicar e reiniciar o jogo"}},
|
||||
{"Other",
|
||||
{"Otros",
|
||||
"Altro",
|
||||
"Autre",
|
||||
"Sonstiges",
|
||||
"Outros"}},
|
||||
{"FPS counter in corner",
|
||||
{"Contador de FPS en la esquina",
|
||||
"Contatore FPS nell'angolo",
|
||||
"Compteur de FPS dans le coin",
|
||||
"FPS-Anzeige in der Ecke",
|
||||
"Contador de FPS no canto"}},
|
||||
{"Close",
|
||||
{"Cerrar",
|
||||
"Chiudi",
|
||||
"Fermer",
|
||||
"Schließen",
|
||||
"Fechar"}},
|
||||
{"Settings are saved to bbport.ini",
|
||||
{"Los ajustes se guardan en bbport.ini",
|
||||
"Le impostazioni vengono salvate in bbport.ini",
|
||||
"Les paramètres sont enregistrés dans bbport.ini",
|
||||
"Einstellungen werden in bbport.ini gespeichert",
|
||||
"As configurações são salvas em bbport.ini"}},
|
||||
{"%.0f FPS %.1f ms %s",
|
||||
{"%.0f FPS %.1f ms %s",
|
||||
"%.0f FPS %.1f ms %s",
|
||||
"%.0f FPS %.1f ms %s",
|
||||
"%.0f FPS %.1f ms %s",
|
||||
"%.0f FPS %.1f ms %s"}},
|
||||
{"Chromatic aberration",
|
||||
{"Aberración cromática",
|
||||
"Aberrazione cromatica",
|
||||
"Aberration chromatique",
|
||||
"Chromatische Aberration",
|
||||
"Aberração cromática"}},
|
||||
{"Depth of field (DoF)",
|
||||
{"Profundidad de campo (DoF)",
|
||||
"Profondità di campo (DoF)",
|
||||
"Profondeur de champ (DoF)",
|
||||
"Tiefenunschärfe (DoF)",
|
||||
"Profundidade de campo (DoF)"}},
|
||||
{"Motion blur",
|
||||
{"Desenfoque de movimiento",
|
||||
"Motion blur",
|
||||
"Flou de mouvement",
|
||||
"Bewegungsunschärfe",
|
||||
"Desfoque de movimento"}},
|
||||
{"Ambient occlusion (SSAO)",
|
||||
{"Oclusión ambiental (SSAO)",
|
||||
"Occlusione ambientale (SSAO)",
|
||||
"Occlusion ambiante (SSAO)",
|
||||
"Ambient Occlusion (SSAO)",
|
||||
"Oclusão ambiente (SSAO)"}},
|
||||
{"Game's own anti-aliasing",
|
||||
{"Antialiasing propio del juego",
|
||||
"Anti-aliasing del gioco",
|
||||
"Anticrénelage propre au jeu",
|
||||
"Spieleigene Kantenglättung",
|
||||
"Anti-aliasing do próprio jogo"}},
|
||||
{"Shadows from dynamic lights",
|
||||
{"Sombras de luces dinámicas",
|
||||
"Ombre delle luci dinamiche",
|
||||
"Ombres des lumières dynamiques",
|
||||
"Schatten dynamischer Lichter",
|
||||
"Sombras de luzes dinâmicas"}},
|
||||
{"Screen-space reflections (not in original game)",
|
||||
{"Reflejos en espacio de pantalla (no están en el juego original)",
|
||||
"Riflessi nello spazio dello schermo (SSR) (assenti nel gioco originale)",
|
||||
"Réflexions SSR (absentes du jeu original)",
|
||||
"Screen-Space-Reflexionen (nicht im Originalspiel)",
|
||||
"Reflexos em espaço de tela (não existem no jogo original)"}},
|
||||
{"Skip startup intros",
|
||||
{"Saltar las intros de inicio",
|
||||
"Salta le intro all'avvio",
|
||||
"Passer les vidéos d'introduction",
|
||||
"Start-Intros überspringen",
|
||||
"Pular as intros de início"}},
|
||||
{"Free camera (Cross + L3)",
|
||||
{"Cámara libre (Cross + L3)",
|
||||
"Camera libera (Cross + L3)",
|
||||
"Caméra libre (Cross + L3)",
|
||||
"Freie Kamera (Cross + L3)",
|
||||
"Câmera livre (Cross + L3)"}},
|
||||
{"Debug menu (requires font files)",
|
||||
{"Debug menu (requiere archivos de fuente)",
|
||||
"Debug menu (richiede i file dei font)",
|
||||
"Debug menu (fichiers de police requis)",
|
||||
"Debug menu (benötigt Schriftdateien)",
|
||||
"Debug menu (requer arquivos de fonte)"}},
|
||||
{"Cheat: never die (not below 1 HP)",
|
||||
{"Truco: no morir (nunca por debajo de 1 HP)",
|
||||
"Cheat: non morire mai (mai sotto 1 HP)",
|
||||
"Triche : ne jamais mourir (pas sous 1 HP)",
|
||||
"Cheat: nie sterben (nicht unter 1 HP)",
|
||||
"Truque: não morrer (nunca abaixo de 1 HP)"}},
|
||||
{"Cheat: enemies do not see you",
|
||||
{"Truco: los enemigos no te ven",
|
||||
"Cheat: i nemici non ti vedono",
|
||||
"Triche : les ennemis ne vous voient pas",
|
||||
"Cheat: Gegner sehen dich nicht",
|
||||
"Truque: os inimigos não te veem"}},
|
||||
{"Cheat: enemies do not hear you",
|
||||
{"Truco: los enemigos no te oyen",
|
||||
"Cheat: i nemici non ti sentono",
|
||||
"Triche : les ennemis ne vous entendent pas",
|
||||
"Cheat: Gegner hören dich nicht",
|
||||
"Truque: os inimigos não te ouvem"}},
|
||||
{"Cheat: Rally never fades",
|
||||
{"Truco: Rally nunca se desvanece",
|
||||
"Cheat: Rally non svanisce mai",
|
||||
"Triche : le Rally ne s'estompe jamais",
|
||||
"Cheat: Rally verblasst nie",
|
||||
"Truque: o Rally nunca some"}},
|
||||
{"Cheat: enemy control (R3 / L3)",
|
||||
{"Truco: control de enemigos (R3 / L3)",
|
||||
"Cheat: controllo dei nemici (R3 / L3)",
|
||||
"Triche : contrôle des ennemis (R3 / L3)",
|
||||
"Cheat: Gegnerkontrolle (R3 / L3)",
|
||||
"Truque: controle de inimigos (R3 / L3)"}},
|
||||
{"No Rally (health regain)",
|
||||
{"Sin Rally (recuperación de salud)",
|
||||
"Niente Rally (recupero di salute)",
|
||||
"Pas de Rally (regain de santé)",
|
||||
"Ohne Rally (Lebensrückgewinn)",
|
||||
"Sem Rally (recuperação de vida)"}},
|
||||
{"Camera further away",
|
||||
{"Cámara más alejada",
|
||||
"Camera più lontana",
|
||||
"Caméra plus éloignée",
|
||||
"Kamera weiter entfernt",
|
||||
"Câmera mais afastada"}},
|
||||
{"No camera auto-rotation",
|
||||
{"Sin giro automático de la cámara",
|
||||
"Niente rotazione automatica della camera",
|
||||
"Pas de rotation automatique de la caméra",
|
||||
"Keine automatische Kameradrehung",
|
||||
"Sem giro automático da câmera"}},
|
||||
{"Run with less stick tilt",
|
||||
{"Correr con menos inclinación del stick",
|
||||
"Corri con meno inclinazione dello stick",
|
||||
"Courir en inclinant moins le stick",
|
||||
"Laufen bei geringerer Stick-Neigung",
|
||||
"Correr com menos inclinação do analógico"}},
|
||||
{"Dark Souls ragdoll physics",
|
||||
{"Físicas ragdoll de Dark Souls",
|
||||
"Fisica ragdoll di Dark Souls",
|
||||
"Physique ragdoll de Dark Souls",
|
||||
"Ragdoll-Physik wie in Dark Souls",
|
||||
"Física ragdoll de Dark Souls"}},
|
||||
// The game's System menu pages (bbport_game_menu.cpp).
|
||||
{"Display",
|
||||
{"Imagen",
|
||||
"Immagine",
|
||||
"Image",
|
||||
"Bild",
|
||||
"Imagem"}},
|
||||
{"Resolution, upscaler, sharpness and model detail (bbport)",
|
||||
{"Resolución, escalador, nitidez y detalle de modelos (bbport)",
|
||||
"Risoluzione, upscaler, nitidezza e dettaglio dei modelli (bbport)",
|
||||
"Résolution, upscaler, netteté et détail des modèles (bbport)",
|
||||
"Auflösung, Upscaler, Schärfe und Modelldetails (bbport)",
|
||||
"Resolução, upscaler, nitidez e detalhe dos modelos (bbport)"}},
|
||||
{"Quality preset",
|
||||
{"Preajuste de calidad",
|
||||
"Preset di qualità",
|
||||
"Préréglage de qualité",
|
||||
"Qualitätsstufe",
|
||||
"Predefinição de qualidade"}},
|
||||
{"Scene resolution: 0-1 Native AA, 2-3 Quality, 4-6 Balanced, 7-8 Performance, 9-10 Ultra",
|
||||
{"Resolución de la escena: 0-1 Native AA, 2-3 Quality, 4-6 Balanced, 7-8 Performance, 9-10 Ultra",
|
||||
"Risoluzione della scena: 0-1 Native AA, 2-3 Quality, 4-6 Balanced, 7-8 Performance, 9-10 Ultra",
|
||||
"Résolution de la scène : 0-1 Native AA, 2-3 Quality, 4-6 Balanced, 7-8 Performance, 9-10 Ultra",
|
||||
"Szenenauflösung: 0-1 Native AA, 2-3 Quality, 4-6 Balanced, 7-8 Performance, 9-10 Ultra",
|
||||
"Resolução da cena: 0-1 Native AA, 2-3 Quality, 4-6 Balanced, 7-8 Performance, 9-10 Ultra"}},
|
||||
{"RCAS after the upscaler: 0 off, 10 strongest",
|
||||
{"RCAS tras el escalador: 0 desactivado, 10 el más fuerte",
|
||||
"RCAS dopo l'upscaler: 0 disattivato, 10 il più forte",
|
||||
"RCAS après l'upscaler : 0 désactivé, 10 le plus fort",
|
||||
"RCAS nach dem Upscaler: 0 aus, 10 am stärksten",
|
||||
"RCAS após o upscaler: 0 desativado, 10 o mais forte"}},
|
||||
{"Final frame: 0-1 1280x720, 2-4 1920x1080, 5-7 2560x1440, 8-10 3840x2160",
|
||||
{"Imagen final: 0-1 1280x720, 2-4 1920x1080, 5-7 2560x1440, 8-10 3840x2160",
|
||||
"Immagine finale: 0-1 1280x720, 2-4 1920x1080, 5-7 2560x1440, 8-10 3840x2160",
|
||||
"Image finale : 0-1 1280x720, 2-4 1920x1080, 5-7 2560x1440, 8-10 3840x2160",
|
||||
"Fertiges Bild: 0-1 1280x720, 2-4 1920x1080, 5-7 2560x1440, 8-10 3840x2160",
|
||||
"Imagem final: 0-1 1280x720, 2-4 1920x1080, 5-7 2560x1440, 8-10 3840x2160"}},
|
||||
{"FPS counter",
|
||||
{"Contador de FPS",
|
||||
"Contatore FPS",
|
||||
"Compteur de FPS",
|
||||
"FPS-Anzeige",
|
||||
"Contador de FPS"}},
|
||||
{"Frame rate in the top right corner",
|
||||
{"Fotogramas por segundo en la esquina superior derecha",
|
||||
"Fotogrammi al secondo nell'angolo in alto a destra",
|
||||
"Images par seconde dans le coin supérieur droit",
|
||||
"Bildrate in der oberen rechten Ecke",
|
||||
"Quadros por segundo no canto superior direito"}},
|
||||
{"After a restart: 0-1 highest, 2-4 game default, 5-7 lower, 8-10 lowest",
|
||||
{"Tras reiniciar: 0-1 máximo, 2-4 como en el juego, 5-7 menor, 8-10 mínimo",
|
||||
"Dopo un riavvio: 0-1 massimo, 2-4 come nel gioco, 5-7 inferiore, 8-10 minimo",
|
||||
"Après un redémarrage : 0-1 maximal, 2-4 comme dans le jeu, 5-7 réduit, 8-10 minimal",
|
||||
"Nach einem Neustart: 0-1 höchste, 2-4 Spielstandard, 5-7 niedriger, 8-10 niedrigste",
|
||||
"Após reiniciar: 0-1 máximo, 2-4 como no jogo, 5-7 menor, 8-10 mínimo"}},
|
||||
{"Highest",
|
||||
{"Máximo",
|
||||
"Massimo",
|
||||
"Maximal",
|
||||
"Höchste",
|
||||
"Máximo"}},
|
||||
{"Lower",
|
||||
{"Menor",
|
||||
"Inferiore",
|
||||
"Réduit",
|
||||
"Niedriger",
|
||||
"Menor"}},
|
||||
{"Lowest",
|
||||
{"Mínimo",
|
||||
"Minimo",
|
||||
"Minimal",
|
||||
"Niedrigste",
|
||||
"Mínimo"}},
|
||||
{"Temporal upscaler and anti-aliasing",
|
||||
{"Escalador temporal y suavizado",
|
||||
"Upscaler temporale e anti-aliasing",
|
||||
"Upscaler temporel et anticrénelage",
|
||||
"Temporaler Upscaler und Kantenglättung",
|
||||
"Upscaler temporal e anti-aliasing"}},
|
||||
{"Effects",
|
||||
{"Efectos",
|
||||
"Effetti",
|
||||
"Effets",
|
||||
"Effekte",
|
||||
"Efeitos"}},
|
||||
{"The game's effects (bbport; after a restart)",
|
||||
{"Los efectos del juego (bbport; tras reiniciar)",
|
||||
"Gli effetti del gioco (bbport; dopo un riavvio)",
|
||||
"Les effets du jeu (bbport ; après un redémarrage)",
|
||||
"Die Effekte des Spiels (bbport; nach einem Neustart)",
|
||||
"Os efeitos do jogo (bbport; após reiniciar)"}},
|
||||
{"Colour fringes at the frame edges (after a restart)",
|
||||
{"Bordes de color en los extremos de la imagen (tras reiniciar)",
|
||||
"Bordi colorati ai margini dell'immagine (dopo un riavvio)",
|
||||
"Franges colorées sur les bords de l'image (après un redémarrage)",
|
||||
"Farbsäume an den Bildrändern (nach einem Neustart)",
|
||||
"Franjas coloridas nas bordas da imagem (após reiniciar)"}},
|
||||
{"Depth of field",
|
||||
{"Profundidad de campo",
|
||||
"Profondità di campo",
|
||||
"Profondeur de champ",
|
||||
"Tiefenschärfe",
|
||||
"Profundidade de campo"}},
|
||||
{"Blur of distant and near objects (DoF) (after a restart)",
|
||||
{"Desenfoque de objetos lejanos y cercanos (DoF) (tras reiniciar)",
|
||||
"Sfocatura degli oggetti lontani e vicini (DoF) (dopo un riavvio)",
|
||||
"Flou des objets lointains et proches (DoF) (après un redémarrage)",
|
||||
"Unschärfe ferner und naher Objekte (DoF) (nach einem Neustart)",
|
||||
"Desfoque de objetos distantes e próximos (DoF) (após reiniciar)"}},
|
||||
{"Blur when the camera turns (after a restart)",
|
||||
{"Desenfoque al girar la cámara (tras reiniciar)",
|
||||
"Sfocatura quando la telecamera ruota (dopo un riavvio)",
|
||||
"Flou quand la caméra tourne (après un redémarrage)",
|
||||
"Unschärfe beim Drehen der Kamera (nach einem Neustart)",
|
||||
"Desfoque ao girar a câmera (após reiniciar)"}},
|
||||
{"Ambient occlusion",
|
||||
{"Oclusión ambiental",
|
||||
"Occlusione ambientale",
|
||||
"Occlusion ambiante",
|
||||
"Umgebungsverdeckung",
|
||||
"Oclusão de ambiente"}},
|
||||
{"Shading in corners and contacts (SSAO) (after a restart)",
|
||||
{"Sombreado en esquinas y zonas de contacto (SSAO) (tras reiniciar)",
|
||||
"Ombreggiatura negli angoli e nei punti di contatto (SSAO) (dopo un riavvio)",
|
||||
"Ombrage dans les coins et aux contacts (SSAO) (après un redémarrage)",
|
||||
"Schattierung in Ecken und an Kontaktstellen (SSAO) (nach einem Neustart)",
|
||||
"Sombreamento em cantos e pontos de contato (SSAO) (após reiniciar)"}},
|
||||
{"Dynamic shadows",
|
||||
{"Sombras dinámicas",
|
||||
"Ombre dinamiche",
|
||||
"Ombres dynamiques",
|
||||
"Dynamische Schatten",
|
||||
"Sombras dinâmicas"}},
|
||||
{"Shadows from dynamic lights (after a restart)",
|
||||
{"Sombras de las luces dinámicas (tras reiniciar)",
|
||||
"Ombre delle luci dinamiche (dopo un riavvio)",
|
||||
"Ombres des lumières dynamiques (après un redémarrage)",
|
||||
"Schatten dynamischer Lichtquellen (nach einem Neustart)",
|
||||
"Sombras das luzes dinâmicas (após reiniciar)"}},
|
||||
{"SSR reflections",
|
||||
{"Reflejos SSR",
|
||||
"Riflessi SSR",
|
||||
"Reflets SSR",
|
||||
"SSR-Spiegelungen",
|
||||
"Reflexos SSR"}},
|
||||
{"Screen-space reflections (not in the original) (after a restart)",
|
||||
{"Reflejos en espacio de pantalla (no estaban en el original) (tras reiniciar)",
|
||||
"Riflessi in spazio schermo (non presenti nell'originale) (dopo un riavvio)",
|
||||
"Reflets en espace écran (absents de l'original) (après un redémarrage)",
|
||||
"Bildschirmraum-Spiegelungen (nicht im Original) (nach einem Neustart)",
|
||||
"Reflexos em espaço de tela (não existiam no original) (após reiniciar)"}},
|
||||
};
|
||||
const int column = int(language) - First;
|
||||
if (column < 0 || column >= int(Row{}.size())) {
|
||||
return english;
|
||||
}
|
||||
const auto it = table.find(english);
|
||||
return it != table.end() ? it->second[column] : english;
|
||||
}
|
||||
|
||||
} // namespace BbSettings
|
||||
+146
-6
@@ -4,18 +4,23 @@
|
||||
#include <algorithm>
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <cfloat>
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <deque>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
#include "bbport_settings.h"
|
||||
#include "imgui.h"
|
||||
#include "imgui_impl_vulkan.h"
|
||||
#include "imgui_internal.h" // RegisterUserTexture: the trophies' icons
|
||||
#include "video_core/renderer_vulkan/vk_instance.h"
|
||||
#include "video_core/renderer_vulkan/vk_scheduler.h"
|
||||
|
||||
@@ -55,6 +60,24 @@ std::string prompt_title, prompt_text;
|
||||
std::chrono::steady_clock::time_point last_present{};
|
||||
float frame_ms_avg = 0.0f;
|
||||
|
||||
// Trophy notices (bbgpu_trophy_notice): one card at a time in the top-left corner, in the style
|
||||
// of the PS4's, with the trophy's icon (a cup in its grade's colour when it has none).
|
||||
struct Notice {
|
||||
int id = 0;
|
||||
char grade = '-';
|
||||
std::string name;
|
||||
std::vector<u8> icon; // RGBA
|
||||
int icon_size = 0;
|
||||
};
|
||||
std::mutex notice_mutex;
|
||||
std::deque<Notice> notices;
|
||||
std::atomic<bool> notice_busy{false}; // queued or on screen
|
||||
bool notice_shown = false; // the present thread's (imgui_mutex)
|
||||
Notice notice;
|
||||
std::chrono::steady_clock::time_point notice_start;
|
||||
std::unordered_map<int, ImTextureData*> notice_icons; // kept: one per trophy, 64 KiB each
|
||||
constexpr float NoticeIn = 0.35f, NoticeHold = 5.0f, NoticeOut = 0.6f;
|
||||
|
||||
float PixelDensity(SDL_WindowID id);
|
||||
|
||||
void SetOpen(bool value) {
|
||||
@@ -194,11 +217,10 @@ void Menu() {
|
||||
}
|
||||
}
|
||||
// Always readable, even when the rest of the menu is in Russian.
|
||||
static const char* languages[] = {"English", "Русский"};
|
||||
int language = s.menu_language == BbSettings::MenuLanguage::Russian ? 1 : 0;
|
||||
if (ImGui::Combo("Language", &language, languages, 2)) {
|
||||
s.menu_language =
|
||||
language == 1 ? BbSettings::MenuLanguage::Russian : BbSettings::MenuLanguage::English;
|
||||
int language = int(s.menu_language.load());
|
||||
if (ImGui::Combo("Language", &language, BbSettings::MenuLanguageNames,
|
||||
int(BbSettings::MenuLanguage::Count))) {
|
||||
s.menu_language = BbSettings::MenuLanguage(language);
|
||||
BbSettings::Save();
|
||||
}
|
||||
ImGui::Text(BbSettings::MenuText("%.0f FPS (%.1f ms)", "%.0f FPS (%.1f мс)"),
|
||||
@@ -594,6 +616,111 @@ void FpsCounter() {
|
||||
ImGui::End();
|
||||
}
|
||||
|
||||
ImU32 GradeColour(char grade, float alpha) {
|
||||
const int a = int(255.0f * alpha);
|
||||
switch (grade) {
|
||||
case 'P':
|
||||
return IM_COL32(170, 200, 228, a);
|
||||
case 'G':
|
||||
return IM_COL32(226, 186, 72, a);
|
||||
case 'S':
|
||||
return IM_COL32(196, 200, 206, a);
|
||||
case 'B':
|
||||
return IM_COL32(186, 118, 68, a);
|
||||
default:
|
||||
return IM_COL32(200, 200, 200, a);
|
||||
}
|
||||
}
|
||||
|
||||
// A small cup: bowl, handles, stem and base, `size` high from `top`.
|
||||
void DrawCup(ImDrawList* draw, ImVec2 top, float size, ImU32 colour) {
|
||||
const float w = size * 0.8f, x = top.x, y = top.y;
|
||||
draw->PathLineTo(ImVec2(x, y));
|
||||
draw->PathLineTo(ImVec2(x + w, y));
|
||||
draw->PathBezierCubicCurveTo(ImVec2(x + w, y + size * 0.45f), ImVec2(x + w * 0.65f, y + size * 0.6f),
|
||||
ImVec2(x + w * 0.5f, y + size * 0.6f));
|
||||
draw->PathBezierCubicCurveTo(ImVec2(x + w * 0.35f, y + size * 0.6f), ImVec2(x, y + size * 0.45f),
|
||||
ImVec2(x, y));
|
||||
draw->PathFillConvex(colour);
|
||||
const float handle = size * 0.12f;
|
||||
draw->AddCircle(ImVec2(x - handle * 0.4f, y + size * 0.2f), handle, colour, 12, size * 0.07f);
|
||||
draw->AddCircle(ImVec2(x + w + handle * 0.4f, y + size * 0.2f), handle, colour, 12, size * 0.07f);
|
||||
draw->AddRectFilled(ImVec2(x + w * 0.44f, y + size * 0.58f), ImVec2(x + w * 0.56f, y + size * 0.82f),
|
||||
colour);
|
||||
draw->AddRectFilled(ImVec2(x + w * 0.22f, y + size * 0.82f), ImVec2(x + w * 0.78f, y + size * 0.95f),
|
||||
colour, size * 0.04f);
|
||||
}
|
||||
|
||||
ImTextureData* NoticeIcon(const Notice& n) {
|
||||
if (n.icon_size <= 0) {
|
||||
return nullptr;
|
||||
}
|
||||
auto& texture = notice_icons[n.id];
|
||||
if (texture == nullptr) {
|
||||
texture = new ImTextureData();
|
||||
texture->Create(ImTextureFormat_RGBA32, n.icon_size, n.icon_size);
|
||||
std::memcpy(texture->GetPixels(), n.icon.data(), n.icon.size());
|
||||
ImGui::RegisterUserTexture(texture);
|
||||
}
|
||||
return texture;
|
||||
}
|
||||
|
||||
void TrophyCard() {
|
||||
const auto now = std::chrono::steady_clock::now();
|
||||
if (!notice_shown) {
|
||||
std::scoped_lock lock{notice_mutex};
|
||||
if (notices.empty()) {
|
||||
notice_busy = false;
|
||||
return;
|
||||
}
|
||||
notice = std::move(notices.front());
|
||||
notices.pop_front();
|
||||
notice_shown = true;
|
||||
notice_start = now;
|
||||
}
|
||||
const float t = std::chrono::duration<float>(now - notice_start).count();
|
||||
if (t > NoticeIn + NoticeHold + NoticeOut) {
|
||||
notice_shown = false;
|
||||
return;
|
||||
}
|
||||
const float in = std::min(t / NoticeIn, 1.0f);
|
||||
const float alpha = t < NoticeIn + NoticeHold ? in : 1.0f - (t - NoticeIn - NoticeHold) / NoticeOut;
|
||||
const float ease = 1.0f - (1.0f - in) * (1.0f - in);
|
||||
const float s = base_scale;
|
||||
const ImGuiViewport* viewport = ImGui::GetMainViewport();
|
||||
ImDrawList* draw = ImGui::GetForegroundDrawList();
|
||||
ImFont* font = ImGui::GetFont();
|
||||
const float text_size = ImGui::GetFontSize();
|
||||
const float pad = 12.0f * s, icon = 72.0f * s, width = 470.0f * s;
|
||||
const float text_x = pad + icon + 16.0f * s, text_w = width - text_x - pad;
|
||||
const char* earned = BbSettings::MenuText("You have earned a trophy.", "Вы получили трофей.");
|
||||
const ImVec2 name_size =
|
||||
font->CalcTextSizeA(text_size * 1.05f, FLT_MAX, text_w, notice.name.c_str());
|
||||
const float height = std::max(icon + 2.0f * pad, pad * 2.0f + text_size * 1.6f + name_size.y);
|
||||
const ImVec2 p0(viewport->WorkPos.x + 36.0f * s - (1.0f - ease) * 48.0f * s,
|
||||
viewport->WorkPos.y + 36.0f * s);
|
||||
const ImVec2 p1(p0.x + width, p0.y + height);
|
||||
draw->AddRectFilled(ImVec2(p0.x + 3.0f * s, p0.y + 4.0f * s), ImVec2(p1.x + 3.0f * s, p1.y + 4.0f * s),
|
||||
IM_COL32(0, 0, 0, int(90 * alpha)), 8.0f * s);
|
||||
draw->AddRectFilled(p0, p1, IM_COL32(24, 24, 30, int(232 * alpha)), 8.0f * s);
|
||||
draw->AddRect(p0, p1, IM_COL32(255, 255, 255, int(28 * alpha)), 8.0f * s);
|
||||
const ImVec2 icon0(p0.x + pad, p0.y + (height - icon) * 0.5f);
|
||||
if (ImTextureData* texture = NoticeIcon(notice)) {
|
||||
draw->AddImageRounded(texture->GetTexRef(), icon0, ImVec2(icon0.x + icon, icon0.y + icon),
|
||||
ImVec2(0, 0), ImVec2(1, 1), IM_COL32(255, 255, 255, int(255 * alpha)),
|
||||
4.0f * s);
|
||||
} else {
|
||||
draw->AddRectFilled(icon0, ImVec2(icon0.x + icon, icon0.y + icon),
|
||||
IM_COL32(50, 50, 58, int(255 * alpha)), 4.0f * s);
|
||||
DrawCup(draw, ImVec2(icon0.x + icon * 0.28f, icon0.y + icon * 0.22f), icon * 0.56f,
|
||||
GradeColour(notice.grade, alpha));
|
||||
}
|
||||
const ImVec2 line1(p0.x + text_x, p0.y + pad + 2.0f * s);
|
||||
draw->AddText(font, text_size * 0.92f, line1, IM_COL32(205, 205, 212, int(255 * alpha)), earned);
|
||||
draw->AddText(font, text_size * 1.05f, ImVec2(line1.x, line1.y + text_size * 1.45f),
|
||||
IM_COL32(255, 255, 255, int(255 * alpha)), notice.name.c_str(), nullptr, text_w);
|
||||
}
|
||||
|
||||
void TextPrompt() {
|
||||
std::string title, text;
|
||||
{
|
||||
@@ -848,7 +975,17 @@ bool HandleEvent(const SDL_Event& event) {
|
||||
}
|
||||
|
||||
bool Visible() {
|
||||
return initialized && (menu_open || prompt_active || BbSettings::Get().show_fps);
|
||||
return initialized && (menu_open || prompt_active || notice_busy || BbSettings::Get().show_fps);
|
||||
}
|
||||
|
||||
void TrophyNotice(int id, char grade, std::string name, std::vector<u8> icon, int icon_size) {
|
||||
if (icon.size() != size_t(icon_size) * size_t(icon_size) * 4) {
|
||||
icon.clear();
|
||||
icon_size = 0;
|
||||
}
|
||||
std::scoped_lock lock{notice_mutex};
|
||||
notices.push_back(Notice{id, grade, std::move(name), std::move(icon), icon_size});
|
||||
notice_busy = true;
|
||||
}
|
||||
|
||||
bool MenuOpen() {
|
||||
@@ -900,6 +1037,9 @@ void Render(vk::CommandBuffer cmdbuf, vk::ImageView view, vk::Extent2D extent) {
|
||||
if (prompt_active && !menu_open) {
|
||||
TextPrompt();
|
||||
}
|
||||
if (notice_busy) {
|
||||
TrophyCard();
|
||||
}
|
||||
ImGui::Render();
|
||||
|
||||
const vk::RenderingAttachmentInfo attachment{
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include "common/types.h"
|
||||
#include "video_core/renderer_vulkan/vk_common.h"
|
||||
|
||||
@@ -41,4 +42,8 @@ bool MenuOpen();
|
||||
/// Window thread: the game's text dialog (ImeDialog) state, drawn as a box over the frame.
|
||||
void SetTextPrompt(bool active, const std::string& prompt, const std::string& text);
|
||||
|
||||
/// A trophy the game unlocked: a card in the top-left corner for a few seconds (queued while one is
|
||||
/// shown). `icon`: `icon_size` x `icon_size` RGBA, or empty for a cup in the grade's colour.
|
||||
void TrophyNotice(int id, char grade, std::string name, std::vector<u8> icon, int icon_size);
|
||||
|
||||
} // namespace BbOverlay
|
||||
@@ -1,10 +1,13 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
#include "bbport_settings.h"
|
||||
#include "bbport_menu_i18n.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <iterator>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <utility>
|
||||
@@ -27,9 +30,13 @@ void Set(Values& v, const std::string& key, const std::string& value) {
|
||||
const float f = float(std::atof(value.c_str()));
|
||||
const int i = std::atoi(value.c_str());
|
||||
if (key == "menu_language") {
|
||||
if (value == "en") v.menu_language = MenuLanguage::English;
|
||||
else if (value == "ru") v.menu_language = MenuLanguage::Russian;
|
||||
else std::printf("Settings: unknown menu_language '%s' (expected en or ru)\n", value.c_str());
|
||||
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));
|
||||
} else {
|
||||
std::printf("Settings: unknown menu_language '%s' (expected en, ru, es, it, fr, de or pt_BR)\n",
|
||||
value.c_str());
|
||||
}
|
||||
} else if (key == "upscaler") {
|
||||
for (int u = 0; u < UpscalerCount; ++u) {
|
||||
if (value == UpscalerName(u)) {
|
||||
@@ -96,7 +103,30 @@ Values& Get() {
|
||||
}
|
||||
|
||||
const char* MenuText(const char* english, const char* russian) {
|
||||
return Get().menu_language == MenuLanguage::Russian ? russian : english;
|
||||
return MenuTextIn(Get().menu_language, english, russian);
|
||||
}
|
||||
|
||||
const char* MenuTextIn(MenuLanguage language, const char* english, const char* russian) {
|
||||
switch (language) {
|
||||
case MenuLanguage::English:
|
||||
return english;
|
||||
case MenuLanguage::Russian:
|
||||
return russian;
|
||||
default:
|
||||
return MenuTranslation(language, english);
|
||||
}
|
||||
}
|
||||
|
||||
MenuLanguage MenuLanguageFromCode(const char* code) {
|
||||
if (code && code[0] && code[1]) {
|
||||
for (int l = 0; l < int(MenuLanguage::Count); ++l) {
|
||||
if (std::tolower(code[0]) == MenuLanguageCodes[l][0] &&
|
||||
std::tolower(code[1]) == MenuLanguageCodes[l][1]) {
|
||||
return MenuLanguage(l);
|
||||
}
|
||||
}
|
||||
}
|
||||
return MenuLanguage::English;
|
||||
}
|
||||
|
||||
void Load() {
|
||||
@@ -116,9 +146,7 @@ void Load() {
|
||||
}
|
||||
}
|
||||
}
|
||||
v.menu_language = lang && (lang[0] == 'r' || lang[0] == 'R') && (lang[1] == 'u' || lang[1] == 'U')
|
||||
? MenuLanguage::Russian
|
||||
: MenuLanguage::English;
|
||||
v.menu_language = MenuLanguageFromCode(lang);
|
||||
}
|
||||
if (FILE* file = std::fopen(Path(), "r")) {
|
||||
char line[256];
|
||||
@@ -222,7 +250,7 @@ void Save() {
|
||||
return std::string(text);
|
||||
};
|
||||
const auto flag = [](bool value) { return std::string(value ? "1" : "0"); };
|
||||
put("menu_language", v.menu_language == MenuLanguage::Russian ? "ru" : "en");
|
||||
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));
|
||||
|
||||
@@ -17,7 +17,14 @@ inline bool IsFsr4(int upscaler) {
|
||||
}
|
||||
enum Preset : int { NativeAA = 0, Quality, Balanced, Performance, UltraPerformance, PresetCount };
|
||||
enum DebugView : int { DebugNone = 0, DebugReactive = 1, DebugMotion = 2, DebugViewCount };
|
||||
enum class MenuLanguage { English, Russian };
|
||||
/// Overlay languages, in the order of the menu's Language selector.
|
||||
enum class MenuLanguage { English, Russian, Spanish, Italian, French, German, PortugueseBrazil, Count };
|
||||
/// bbport.ini / BB_MENU_LANGUAGE codes, indexed by MenuLanguage.
|
||||
inline constexpr const char* MenuLanguageCodes[] = {"en", "ru", "es", "it", "fr", "de", "pt_BR"};
|
||||
/// Each language's own name, for the selector.
|
||||
inline constexpr const char* MenuLanguageNames[] = {"English", "Русский", "Español",
|
||||
"Italiano", "Français", "Deutsch",
|
||||
"Português (Brasil)"};
|
||||
|
||||
/// Game effects switched by the community patches at start (patches.py EFFECTS): ini key,
|
||||
/// menu label, default (the game's own behaviour).
|
||||
@@ -38,6 +45,17 @@ inline constexpr Effect Effects[] = {
|
||||
{"skip_intro", "Skip startup intros", "Пропуск заставок при запуске", false},
|
||||
{"debug_camera", "Free camera (Cross + L3)", "Свободная камера (Cross + L3)", false},
|
||||
{"debug_menu", "Debug menu (requires font files)", "Debug menu (нужны файлы шрифтов)", false},
|
||||
// Cheats and gameplay tweaks (launcher "Cheats" page).
|
||||
{"cheat_no_death", "Cheat: never die (not below 1 HP)", "Чит: бессмертие (не ниже 1 HP)", false},
|
||||
{"cheat_stealth", "Cheat: enemies do not see you", "Чит: враги не замечают", false},
|
||||
{"cheat_silent", "Cheat: enemies do not hear you", "Чит: враги не слышат", false},
|
||||
{"cheat_rally_no_decay", "Cheat: Rally never fades", "Чит: Rally не угасает", false},
|
||||
{"cheat_enemy_control", "Cheat: enemy control (R3 / L3)", "Чит: управление врагом (R3 / L3)", false},
|
||||
{"tweak_no_rally", "No Rally (health regain)", "Без Rally (возврата HP)", false},
|
||||
{"tweak_camera_distance", "Camera further away", "Камера дальше", false},
|
||||
{"tweak_no_camera_rotation", "No camera auto-rotation", "Без автоповорота камеры", false},
|
||||
{"tweak_easy_run", "Run with less stick tilt", "Бег с меньшим наклоном стика", false},
|
||||
{"tweak_ragdoll", "Dark Souls ragdoll physics", "Физика тел как в Dark Souls", false},
|
||||
};
|
||||
inline constexpr int EffectCount = int(sizeof(Effects) / sizeof(Effects[0]));
|
||||
/// Live output resolutions: the upscaler's output and the UI host targets.
|
||||
@@ -91,8 +109,13 @@ struct Values {
|
||||
};
|
||||
|
||||
Values& Get();
|
||||
/// Localized overlay text; the Language selector itself stays in English.
|
||||
/// Localized overlay text; the Language selector itself stays in English. Languages other than
|
||||
/// English and Russian look the English text up in bbport_menu_i18n.h (English when missing).
|
||||
const char* MenuText(const char* english, const char* russian);
|
||||
/// The same text in a given language (the game's own System menu follows the game's language).
|
||||
const char* MenuTextIn(MenuLanguage language, const char* english, const char* russian);
|
||||
/// MenuLanguage for a code or locale ("es", "fr_FR.UTF-8"); English when unknown.
|
||||
MenuLanguage MenuLanguageFromCode(const char* code);
|
||||
|
||||
/// Reads the file, then the environment overrides. Called once at start.
|
||||
void Load();
|
||||
|
||||
+2739
-35
File diff suppressed because it is too large.
Load diff
+645
-54
@@ -15,12 +15,14 @@ import os
|
||||
import re
|
||||
import signal
|
||||
import subprocess
|
||||
import threading
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
from pathlib import Path
|
||||
from bbport_assets import fsr411_problem
|
||||
from bbport_i18n import language, set_language, tr
|
||||
from bbport_i18n import language, set_language, system_game_language, tr
|
||||
import bbport_steam
|
||||
import bbport_update
|
||||
|
||||
import gi
|
||||
|
||||
@@ -30,10 +32,13 @@ from gi.repository import Adw, 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'))
|
||||
from menu_prompts import prompts_for # noqa: E402
|
||||
from mods import discover as discover_mods # noqa: E402
|
||||
from prepare import sfo # noqa: E402
|
||||
import game_check # noqa: E402
|
||||
import online # noqa: E402 (the online module: addresses and the connection test)
|
||||
import online # noqa: E402
|
||||
from patches import external_patches # noqa: E402
|
||||
import trophies # noqa: E402
|
||||
# Packaged (AppImage): generated files, saves and bbport.ini live in BB_DATA_DIR.
|
||||
PACKAGED = bool(os.environ.get("BB_PREBUILT"))
|
||||
DATA_DIR = Path(os.environ.get("BB_DATA_DIR", PORT_DIR))
|
||||
@@ -79,6 +84,7 @@ FSR411_BUILD_ERRORS = {
|
||||
|
||||
# Choices: (label, value). The first entry is the default. Labels are translated when shown.
|
||||
UI_LANGUAGES = [("Как в системе", ""), ("Русский", "ru"), ("English", "en"), ("Português (Brasil)", "pt_BR"),
|
||||
("Español", "es"), ("Italiano", "it"), ("Français", "fr"), ("Deutsch", "de"),
|
||||
("简体中文", "zh_CN")]
|
||||
UPSCALERS = [("FSR 4", "fsr4"), ("FSR 4.1.1", "fsr411"), ("FSR 3", "fsr3"),
|
||||
("DLSS (NVIDIA RTX)", "dlss"),
|
||||
@@ -99,6 +105,24 @@ EFFECTS = [
|
||||
("debug_camera", "Свободная камера (Cross + L3 / Space + Z)", False),
|
||||
("debug_menu", "Debug menu (левый touchpad / Tab; нужны шрифты)", False),
|
||||
]
|
||||
# Cheats and gameplay tweaks (community patches for 1.09, applied at start): bbport.ini key, title,
|
||||
# subtitle. All off by default.
|
||||
CHEATS = [
|
||||
("cheat_no_death", "Бессмертие", "Урон проходит, но здоровье не опускается ниже 1"),
|
||||
("cheat_stealth", "Невидимость", "Враги не видят вас и не реагируют, пока вы не атакуете"),
|
||||
("cheat_silent", "Бесшумность", "Враги не слышат вас, но видят"),
|
||||
("cheat_rally_no_decay", "Rally не угасает", "Здоровье для Rally не исчезает со временем"),
|
||||
("cheat_enemy_control", "Управление врагом", "R3: управлять целью, L3: вернуться. "
|
||||
"Не вместе со свободной камерой"),
|
||||
]
|
||||
TWEAKS = [
|
||||
("tweak_no_rally", "Без Rally", "Атаки не возвращают здоровье"),
|
||||
("tweak_camera_distance", "Камера дальше", "Камера дальше от персонажа"),
|
||||
("tweak_no_camera_rotation", "Без автоповорота камеры",
|
||||
"Камера не поворачивается сама при движении; ручное управление и захват цели работают"),
|
||||
("tweak_easy_run", "Бег с меньшим наклоном стика", "Бег примерно с 70% наклона стика вместо 90%"),
|
||||
("tweak_ragdoll", "Физика тел как в Dark Souls", "Тела и предметы разлетаются от ударов"),
|
||||
]
|
||||
MODEL_LOD = [("Как в игре", "0"), ("Максимальная (-2)", "-2"), ("Ниже (1)", "1"),
|
||||
("Минимальная (2)", "2")]
|
||||
FPS_MODES = [("Без ограничения (патч)", "uncap"), ("60", "60"), ("90", "90"),
|
||||
@@ -109,8 +133,16 @@ PRESENT_MODES = [("Mailbox", "Mailbox"), ("FIFO (VSync)", "Fifo"),
|
||||
# choice labels stay short, so the selected one is shown in full.
|
||||
DRAW_PIPE = [("Авто", "", "Включён при 8 и более потоках процессора"), ("Включён", "1"),
|
||||
("Выключен", "0", "Стабильнее, но медленнее")]
|
||||
LANGUAGES = [("Английский", "1"), ("Русский", "8"), ("Японский", "0"), ("Французский", "2"),
|
||||
# The button prompts (scripts/menu_prompts.py): automatic follows the controller in use, the
|
||||
# keyboard without one.
|
||||
PROMPT_CHOICES = [("Автоматически (по контроллеру)", ""), ("PlayStation (оригинал)", "playstation"),
|
||||
("Xbox", "xbox"), ("Nintendo Switch", "switch"), ("Клавиатура", "keyboard")]
|
||||
# The game's language: the PS4 system language ("": from the desktop's locale).
|
||||
LANGUAGES = [("Как в системе", ""), ("Английский", "1"), ("Русский", "8"), ("Японский", "0"), ("Французский", "2"),
|
||||
("Испанский", "3"), ("Немецкий", "4"), ("Итальянский", "5")]
|
||||
# The game's region (BB_REGION; scripts/content_profile.py): the regional text of a language, such
|
||||
# as Spanish from Spain (Europe) or Latin American Spanish (the Americas). "": the dump's param.sfo.
|
||||
REGIONS = [("Как в дампе игры", ""), ("Европа", "eu"), ("Америка", "na")]
|
||||
LIVE_RESOLUTION = [("Авто (по видеокарте)", "auto"), ("Выключена (быстрее)", "0"), ("Включена", "1")]
|
||||
READBACKS = [("Relaxed", "", "По умолчанию"), ("Выключены", "0"), ("Precise", "2")]
|
||||
# Background pre-upload of the game's GPU memory into VRAM (BB_PREUPLOAD): auto = on with a discrete GPU.
|
||||
@@ -121,15 +153,20 @@ DEFAULTS = {
|
||||
"ui_language": "",
|
||||
"game_dir": "" if PACKAGED else str(PORT_DIR.parent / "CUSA03173"),
|
||||
"user_dir": "",
|
||||
"dlc_dir": "",
|
||||
"mods_dir": "",
|
||||
"mods_enabled": True,
|
||||
"patches_dir": "",
|
||||
"language": "1",
|
||||
"language": "",
|
||||
"region": "",
|
||||
"check_updates": True,
|
||||
"fullscreen": False,
|
||||
"hdr": False,
|
||||
"present_mode": "Mailbox",
|
||||
"gamepad": "",
|
||||
"gamepad_name": "",
|
||||
"prompts": "",
|
||||
"gestures_no_motion": False,
|
||||
"display": "",
|
||||
"skip_network_choice": True,
|
||||
"fps_mode": "uncap",
|
||||
@@ -146,15 +183,21 @@ DEFAULTS = {
|
||||
"gpu_profile": False,
|
||||
"vk_validation": False,
|
||||
"extra_env": "",
|
||||
# Online play (Online page, the online module gpu/bbnet): off by default; shadPS4's public
|
||||
# shadNet server and The Hunter's Dream.
|
||||
"discord": True,
|
||||
# The PS4's trophy key (shadPS4's TrophyKey): the trophies' names (scripts/trophies.py).
|
||||
"trophy_key": "",
|
||||
# Co-op (Online page): off by default; shadPS4's public shadNet server and The Hunter's Dream.
|
||||
# online_server_kind: "public" or "own" (docs/SERVER.md); unset, online.server_kind decides.
|
||||
"online": False,
|
||||
"online_community": online.COMMUNITY_SERVER,
|
||||
"online_server_kind": None,
|
||||
"online_server": online.SHADNET_SERVER,
|
||||
"online_webapi": "",
|
||||
"online_npid": "",
|
||||
"online_password": "",
|
||||
"online_upnp": True,
|
||||
"online_p2p_port": 0, # 0: a random port each time (SHADPS4_P2P_PORT)
|
||||
"online_seamless": False,
|
||||
}
|
||||
|
||||
# bbport.ini keys the launcher edits; the rest of the file is kept.
|
||||
@@ -172,6 +215,7 @@ INI_DEFAULTS = {
|
||||
"mouse_sensitivity": "1.00",
|
||||
"mouse_invert_y": "0",
|
||||
**{key: "1" if default else "0" for key, _, default in EFFECTS},
|
||||
**{key: "0" for key, _, _ in CHEATS + TWEAKS},
|
||||
}
|
||||
|
||||
|
||||
@@ -235,6 +279,22 @@ def patches_dir(settings):
|
||||
return Path(settings.get("patches_dir") or DATA_DIR / "patches").expanduser()
|
||||
|
||||
|
||||
def dlc_addons(dlc, title_id):
|
||||
"""Entitlement labels of this title's add-ons in a DLC folder: subfolders named by the label
|
||||
(as the runtime reports them, from <user>/addcont/<title id>/<label>) with a param.sfo of
|
||||
CATEGORY ac and this title's CONTENT_ID."""
|
||||
found = []
|
||||
for folder in sorted(Path(dlc).iterdir()) if Path(dlc).is_dir() else []:
|
||||
try:
|
||||
info = sfo((folder / "sce_sys/param.sfo").read_bytes())
|
||||
except (OSError, ValueError, IndexError):
|
||||
continue
|
||||
content = str(info.get("CONTENT_ID", ""))
|
||||
if info.get("CATEGORY") == "ac" and content[7:16] == title_id and content[20:] == folder.name:
|
||||
found.append(folder.name)
|
||||
return found
|
||||
|
||||
|
||||
# DLSS (vk_dlss.cpp): the bridge (libbbport_dlss.so) and NVIDIA's runtime library
|
||||
# libnvidia-ngx-dlss.so.<version>, beside bb-probe (the package, a build with DLSS_SDK_ROOT) or the
|
||||
# player's own in <user>/dlss, which the game prefers. Linux needs the .so from NVIDIA's DLSS SDK
|
||||
@@ -247,6 +307,12 @@ def binaries_dir():
|
||||
return PORT_DIR / ("bin" if PACKAGED else "out")
|
||||
|
||||
|
||||
def dlss_dir():
|
||||
"""Where the game looks for the bridge and NVIDIA's library: BB_DLSS_DIR (the AppImage's
|
||||
copy, tools/build_dlss.sh), else beside bb-probe."""
|
||||
return Path(os.environ["BB_DLSS_DIR"]) if os.environ.get("BB_DLSS_DIR") else binaries_dir()
|
||||
|
||||
|
||||
def ngx_libraries(directory):
|
||||
"""NVIDIA's DLSS runtime libraries in directory, newest version last."""
|
||||
def version(path):
|
||||
@@ -282,9 +348,23 @@ def fsr411_build_command(upscaler, loader=None):
|
||||
return command + ([str(loader)] if loader else []), env
|
||||
|
||||
|
||||
# Trophy grades (scripts/trophies.py) as the Trophies page names them.
|
||||
TROPHY_GRADES = {"P": "Платиновый", "G": "Золотой", "S": "Серебряный", "B": "Бронзовый"}
|
||||
|
||||
# The Online page's shadNet server choice (online.server_kind).
|
||||
SERVER_KINDS = [
|
||||
("Публичный сервер shadPS4", "public", "srv.shadps4.net:31313"),
|
||||
("Свой сервер", "own", "shadnet-p2p: на вашем ПК или VPS"),
|
||||
]
|
||||
SERVER_GUIDE_URL = f"{bbport_update.REPOSITORY_URL}/blob/main/docs/SERVER.md"
|
||||
|
||||
|
||||
def online_module():
|
||||
"""The online module beside the GPU library (gpu/bbnet; a build without its libraries has none)."""
|
||||
return (PORT_DIR / ("bin" if PACKAGED else "out") / "gpu" / "libbbnet.so").is_file()
|
||||
"""The online module beside the GPU library (gpu/bbnet; a build without its libraries has none),
|
||||
or BB_NET_MODULE (the AppImage's copy)."""
|
||||
if os.environ.get("BB_NET_MODULE"):
|
||||
return Path(os.environ["BB_NET_MODULE"]).is_file()
|
||||
return (binaries_dir() / "gpu" / "libbbnet.so").is_file()
|
||||
|
||||
|
||||
def game_environment(s):
|
||||
@@ -293,12 +373,18 @@ def game_environment(s):
|
||||
env["BB_GAME_DIR"] = str(Path(s["game_dir"]).expanduser())
|
||||
if s["user_dir"]:
|
||||
env["BB_USER_DIR"] = s["user_dir"]
|
||||
if s.get("dlc_dir"):
|
||||
env["BB_DLC_DIR"] = str(Path(s["dlc_dir"]).expanduser())
|
||||
env["BB_MODS_DIR"] = str(Path(s.get("mods_dir") or DATA_DIR / "mods").expanduser())
|
||||
env["BB_MODS_CONFIG"] = str(DATA_DIR / "mods.json")
|
||||
env["BB_MODS_ENABLED"] = "1" if s.get("mods_enabled", True) else "0"
|
||||
# Button prompts (scripts/menu_prompts.py): automatic follows the controller in use.
|
||||
env["BB_PROMPTS"] = s.get("prompts") or prompts_for(gamepad_type(s))
|
||||
env["BB_GESTURES_NO_MOTION"] = "1" if s.get("gestures_no_motion") else "0"
|
||||
env["BB_PATCHES_DIR"] = str(patches_dir(s))
|
||||
env["BB_PATCHES_CONFIG"] = str(DATA_DIR / "patches.json")
|
||||
env["BB_LANGUAGE"] = s["language"]
|
||||
env["BB_LANGUAGE"] = s["language"] or str(system_game_language())
|
||||
env["BB_REGION"] = s.get("region", "")
|
||||
# The in-game menu speaks the launcher's language until one is chosen in it (menu_language).
|
||||
env["BB_MENU_LANGUAGE"] = language()
|
||||
env["BB_FULLSCREEN"] = "1" if s["fullscreen"] else "0"
|
||||
@@ -343,16 +429,11 @@ def game_environment(s):
|
||||
env["BB_GPU_PROFILE"] = "1"
|
||||
if s["vk_validation"]:
|
||||
env["BB_VK_VALIDATION"] = "1"
|
||||
env["BB_DISCORD"] = "1" if s.get("discord", True) else "0"
|
||||
if s.get("trophy_key", "").strip():
|
||||
env["BB_TROPHY_KEY"] = s["trophy_key"].strip()
|
||||
if s.get("online") and online_module():
|
||||
server = s["online_server"].strip() or online.SHADNET_SERVER
|
||||
community = online.community_address(s["online_community"])
|
||||
env["BB_ONLINE"] = "1"
|
||||
env["BB_SHADNET_SERVER"] = server
|
||||
env["BB_SHADNET_WEBAPI"] = online.webapi_setting(s["online_webapi"], server, community)
|
||||
env["BB_SHADNET_NPID"] = s["online_npid"].strip()
|
||||
env["BB_SHADNET_PASSWORD"] = s["online_password"]
|
||||
env["BB_UPNP"] = "1" if s["online_upnp"] else "0"
|
||||
env["BB_COMMUNITY_SERVER"] = community
|
||||
env.update(online.environment(s))
|
||||
for item in s["extra_env"].split():
|
||||
if "=" in item:
|
||||
key, value = item.split("=", 1)
|
||||
@@ -429,14 +510,25 @@ CONTROLS = [
|
||||
]
|
||||
|
||||
|
||||
def connected_gamepads():
|
||||
"""(GUID, name) of the connected gamepads (bb-gpu-capabilities --gamepads), [] if unknown."""
|
||||
def connected_gamepads(types=False):
|
||||
"""(GUID, name) of the connected gamepads (bb-gpu-capabilities --gamepads), [] if unknown;
|
||||
with `types`, (GUID, name, SDL type) ("" for a generic gamepad)."""
|
||||
tool = PORT_DIR / ("bin" if PACKAGED else "out") / "bb-gpu-capabilities"
|
||||
try:
|
||||
run = subprocess.run([str(tool), "--gamepads"], capture_output=True, text=True, timeout=5)
|
||||
except (OSError, subprocess.SubprocessError):
|
||||
return []
|
||||
return [tuple(line.split("\t", 1)) for line in run.stdout.splitlines() if "\t" in line]
|
||||
pads = [(line.split("\t") + [""])[:3] for line in run.stdout.splitlines() if "\t" in line]
|
||||
return [tuple(pad if types else pad[:2]) for pad in pads]
|
||||
|
||||
|
||||
def gamepad_type(s):
|
||||
"""SDL type of the gamepad the game will use (the chosen one when connected, else the first
|
||||
one); None without a gamepad."""
|
||||
pads = connected_gamepads(types=True)
|
||||
want = s.get("gamepad", "").casefold()
|
||||
chosen = [pad for pad in pads if want and (want == pad[0].casefold() or want in pad[1].casefold())]
|
||||
return (chosen or pads or [(None, None, None)])[0][2]
|
||||
|
||||
|
||||
def connected_displays():
|
||||
@@ -481,6 +573,8 @@ class LauncherWindow(Adw.ApplicationWindow):
|
||||
self.process = None
|
||||
self.stream = None
|
||||
self.fsr411_build = None # tools/fsr4cap/build_assets.sh while it runs
|
||||
self.latest = None # bbport_update.latest_release() once checked
|
||||
self.update_state = "idle" # idle, checking, downloading, installed
|
||||
self.build()
|
||||
self.connect("close-request", self.on_close)
|
||||
|
||||
@@ -506,13 +600,26 @@ class LauncherWindow(Adw.ApplicationWindow):
|
||||
"preferences-system-symbolic")
|
||||
self.stack.add_titled_with_icon(self.build_online_page(), "online", tr("Онлайн"),
|
||||
"network-workgroup-symbolic")
|
||||
self.stack.add_titled_with_icon(self.build_cheats_page(), "cheats", tr("Читы"),
|
||||
"applications-games-symbolic")
|
||||
self.stack.add_titled_with_icon(self.build_trophies_page(), "trophies", tr("Трофеи"),
|
||||
"starred-symbolic")
|
||||
self.stack.connect("notify::visible-child-name", lambda *_: self.stack.get_visible_child_name()
|
||||
== "trophies" and self.refresh_trophies())
|
||||
self.stack.add_titled_with_icon(self.build_log_page(), "log", tr("Журнал"),
|
||||
"utilities-terminal-symbolic")
|
||||
self.stack.add_titled_with_icon(self.build_about_page(), "about", tr("О программе"),
|
||||
"help-about-symbolic")
|
||||
self.log_view.get_buffer().set_text(log_text)
|
||||
self.update_launch_button()
|
||||
self.update_game_status()
|
||||
self.update_user_status()
|
||||
self.update_dlc_status()
|
||||
self.update_upscaler_status()
|
||||
self.show_update_state()
|
||||
if self.settings.get("check_updates", True) and self.latest is None and \
|
||||
self.update_state == "idle":
|
||||
self.on_check_update(None, quiet=True)
|
||||
|
||||
def update_launch_button(self):
|
||||
running = self.process is not None
|
||||
@@ -538,7 +645,7 @@ class LauncherWindow(Adw.ApplicationWindow):
|
||||
page.add(launcher)
|
||||
|
||||
game = Adw.PreferencesGroup(title=tr("Игра"))
|
||||
self.game_row = Adw.ActionRow(title=tr("Папка игры (CUSA03173)"))
|
||||
self.game_row = Adw.ActionRow(title=tr("Папка игры"))
|
||||
self.game_status = Gtk.Image()
|
||||
self.game_row.add_suffix(self.game_status)
|
||||
self.game_row.add_suffix(flat_button("folder-open-symbolic", tr("Выбрать папку с eboot.bin"),
|
||||
@@ -560,8 +667,34 @@ class LauncherWindow(Adw.ApplicationWindow):
|
||||
self.on_reset_user)
|
||||
self.user_row.add_suffix(self.user_reset)
|
||||
game.add(self.user_row)
|
||||
# Dumped add-ons (one folder each); their data ships with the 1.09 update.
|
||||
self.dlc_row = Adw.ActionRow(title=tr("Папка DLC"))
|
||||
self.dlc_status = Gtk.Image()
|
||||
self.dlc_row.add_suffix(self.dlc_status)
|
||||
self.dlc_row.add_suffix(flat_button("folder-open-symbolic", tr("Выбрать папку DLC"),
|
||||
self.on_choose_dlc))
|
||||
self.dlc_reset = flat_button("edit-undo-symbolic", tr("Не использовать DLC"), self.on_reset_dlc)
|
||||
self.dlc_row.add_suffix(self.dlc_reset)
|
||||
game.add(self.dlc_row)
|
||||
self.language_row = combo_row(tr("Язык системы"), None, LANGUAGES, self.settings["language"])
|
||||
game.add(self.language_row)
|
||||
self.region_row = combo_row(
|
||||
tr("Регион игры"),
|
||||
tr("Вариант текста у языков с региональными версиями: испанский Испании (Европа) или "
|
||||
"Латинской Америки (Америка)"),
|
||||
REGIONS, self.settings.get("region", ""))
|
||||
game.add(self.region_row)
|
||||
steam = Adw.ActionRow(title=tr("Добавить в Steam"),
|
||||
subtitle=tr("Игра в библиотеке Steam запускается сразу, с настройками "
|
||||
"этого лаунчера"))
|
||||
steam_button = Gtk.Button(label=tr("Добавить"), valign=Gtk.Align.CENTER)
|
||||
steam_button.connect("clicked", self.on_add_to_steam)
|
||||
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))
|
||||
game.add(self.discord_row)
|
||||
page.add(game)
|
||||
|
||||
# Auto: the new memory model on AMD, the 0.3 one elsewhere; or either by hand.
|
||||
@@ -641,6 +774,14 @@ class LauncherWindow(Adw.ApplicationWindow):
|
||||
lambda _button: self.fill_gamepads()))
|
||||
self.fill_gamepads()
|
||||
controls.add(self.gamepad_row)
|
||||
# Xbox / Switch prompts and the gestures menu: Dommo's mods (Nexus, Bloodborne mod 30);
|
||||
# keyboard prompts show the keys bound below.
|
||||
self.prompts_row = combo_row(tr("Иконки кнопок"), tr("Применяется при запуске игры"),
|
||||
PROMPT_CHOICES, self.settings["prompts"])
|
||||
controls.add(self.prompts_row)
|
||||
self.gestures_row = Adw.SwitchRow(title=tr("Меню жестов без значков управления движением"),
|
||||
active=self.settings["gestures_no_motion"])
|
||||
controls.add(self.gestures_row)
|
||||
# Bindings: "Assign" waits for a key or button (bb-gpu-capabilities --read-input).
|
||||
self.control_rows = {}
|
||||
for kind, title, icon in (("key", tr("Клавиатура и мышь"), "input-keyboard-symbolic"),
|
||||
@@ -846,6 +987,35 @@ class LauncherWindow(Adw.ApplicationWindow):
|
||||
else tr("Сохранений пока нет: игра создаст их здесь"))
|
||||
self.user_reset.set_sensitive(custom)
|
||||
|
||||
def update_dlc_status(self):
|
||||
path = self.settings.get("dlc_dir", "")
|
||||
found = []
|
||||
if path:
|
||||
try:
|
||||
info = sfo((self.game_dir() / "sce_sys/param.sfo").read_bytes())
|
||||
found = dlc_addons(Path(path).expanduser(), info.get("TITLE_ID"))
|
||||
except (OSError, ValueError, IndexError):
|
||||
pass
|
||||
self.dlc_row.set_subtitle(f"{path}\n{tr('Найдено DLC: {}').format(len(found))}" if path
|
||||
else tr("Не используется"))
|
||||
self.dlc_status.set_from_icon_name("object-select-symbolic" if found else
|
||||
"dialog-warning-symbolic" if path else None)
|
||||
self.dlc_status.set_tooltip_text(", ".join(found)
|
||||
or (tr("Нет DLC этой игры в папке") if path else None))
|
||||
self.dlc_reset.set_sensitive(bool(path))
|
||||
|
||||
def on_choose_dlc(self, _button):
|
||||
def chosen(path):
|
||||
self.settings["dlc_dir"] = path
|
||||
self.update_dlc_status()
|
||||
self.store()
|
||||
self.choose_folder(tr("Папка DLC"), self.settings.get("dlc_dir") or self.game_dir(), chosen)
|
||||
|
||||
def on_reset_dlc(self, _button):
|
||||
self.settings["dlc_dir"] = ""
|
||||
self.update_dlc_status()
|
||||
self.store()
|
||||
|
||||
def choose_folder(self, title, current, done):
|
||||
dialog = Gtk.FileDialog(title=title)
|
||||
if Path(current).is_dir():
|
||||
@@ -884,9 +1054,8 @@ class LauncherWindow(Adw.ApplicationWindow):
|
||||
hint = tr("Ассеты для выбранного режима найдены") if not problem else (
|
||||
tr("{}. Соберите FSR 4.1.1 из своей DLL кнопкой «Выбрать DLL…» ниже").format(problem))
|
||||
elif value == "dlss":
|
||||
# The game looks for the bridge and NVIDIA's library next to bb-probe (vk_dlss.cpp); the
|
||||
# AppImage has neither (NVIDIA's DLSS SDK is not bundled): DLSS then falls back to FSR 3.1.
|
||||
binaries = PORT_DIR / ("bin" if PACKAGED else "out")
|
||||
# The bridge and NVIDIA's library (vk_dlss.cpp); without them DLSS falls back to FSR 3.1.
|
||||
binaries = dlss_dir()
|
||||
found = (binaries / "libbbport_dlss.so").is_file() and bool(
|
||||
ngx_libraries(self.user_dir() / "dlss") or ngx_libraries(binaries))
|
||||
hint = tr("DLSS найден (нужна видеокарта NVIDIA RTX)") if found else tr(
|
||||
@@ -915,6 +1084,12 @@ class LauncherWindow(Adw.ApplicationWindow):
|
||||
path = self.game_dir()
|
||||
ok = bool(self.settings["game_dir"]) and (path / "eboot.bin").is_file()
|
||||
self.game_row.set_subtitle(str(path) if self.settings["game_dir"] else tr("не выбрана"))
|
||||
# The title ID of the release in the folder (CUSA00900, CUSA03173, ...), from its param.sfo.
|
||||
try:
|
||||
title_id = str(sfo((path / "sce_sys/param.sfo").read_bytes()).get("TITLE_ID", "")) if ok else ""
|
||||
except (OSError, ValueError, IndexError):
|
||||
title_id = ""
|
||||
self.game_row.set_title(tr("Папка игры ({})").format(title_id) if title_id else tr("Папка игры"))
|
||||
# Other versions of the game start and then crash in its code (issues #7, #13, #14).
|
||||
problem = self.game_problem(path) if ok else None
|
||||
if not ok:
|
||||
@@ -931,7 +1106,7 @@ class LauncherWindow(Adw.ApplicationWindow):
|
||||
}[kind]
|
||||
self.game_row.set_subtitle(f"{path}\n{tooltip}")
|
||||
else:
|
||||
tooltip = tr("Bloodborne CUSA03173, версия 1.09")
|
||||
tooltip = tr("Bloodborne {}, версия 1.09").format(title_id or "")
|
||||
self.game_status.set_from_icon_name("object-select-symbolic" if ok and not problem else "dialog-warning-symbolic")
|
||||
self.game_status.set_tooltip_text(tooltip)
|
||||
self.launch_button.set_sensitive(ok or self.process is not None)
|
||||
@@ -940,6 +1115,7 @@ class LauncherWindow(Adw.ApplicationWindow):
|
||||
def chosen(path):
|
||||
self.settings["game_dir"] = path
|
||||
self.update_game_status()
|
||||
self.update_dlc_status()
|
||||
self.store()
|
||||
self.choose_folder(tr("Папка игры (с eboot.bin)"), self.game_dir(), chosen)
|
||||
|
||||
@@ -1016,9 +1192,13 @@ class LauncherWindow(Adw.ApplicationWindow):
|
||||
s = self.settings
|
||||
s["gamepad"] = combo_value(self.gamepad_row)
|
||||
s["gamepad_name"] = self.gamepad_row.names[self.gamepad_row.get_selected()]
|
||||
s["prompts"] = combo_value(self.prompts_row)
|
||||
s["gestures_no_motion"] = self.gestures_row.get_active()
|
||||
s["display"] = combo_value(self.display_row)
|
||||
s["skip_network_choice"] = self.skip_network_row.get_active()
|
||||
s["language"] = combo_value(self.language_row)
|
||||
s["region"] = combo_value(self.region_row)
|
||||
s["check_updates"] = self.check_updates_row.get_active()
|
||||
s["fullscreen"] = self.fullscreen_row.get_active()
|
||||
s["present_mode"] = combo_value(self.present_row)
|
||||
s["hdr"] = self.hdr_row.get_active()
|
||||
@@ -1037,13 +1217,10 @@ class LauncherWindow(Adw.ApplicationWindow):
|
||||
s["vk_validation"] = self.validation_row.get_active()
|
||||
s["extra_env"] = self.extra_row.get_text().strip()
|
||||
s["mods_enabled"] = self.mods_enabled_row.get_active()
|
||||
s["discord"] = self.discord_row.get_active()
|
||||
s["trophy_key"] = self.trophy_key_row.get_text().strip()
|
||||
s["online"] = self.online_row.get_active()
|
||||
s["online_community"] = self.community_row.get_text().strip()
|
||||
s["online_server"] = self.server_row.get_text().strip()
|
||||
s["online_webapi"] = self.webapi_row.get_text().strip()
|
||||
s["online_npid"] = self.npid_row.get_text().strip()
|
||||
s["online_password"] = self.password_row.get_text()
|
||||
s["online_upnp"] = self.upnp_row.get_active()
|
||||
s.update(self.online_settings())
|
||||
self.save_mod_profile()
|
||||
self.save_patch_profile()
|
||||
save_settings(s)
|
||||
@@ -1061,6 +1238,7 @@ class LauncherWindow(Adw.ApplicationWindow):
|
||||
"mouse_sensitivity": f"{self.mouse_sensitivity_row.get_value():.2f}",
|
||||
"mouse_invert_y": "1" if self.mouse_invert_row.get_active() else "0",
|
||||
**{key: "1" if row.get_active() else "0" for key, row in self.effect_rows.items()},
|
||||
**{key: "1" if row.get_active() else "0" for key, row in self.cheat_rows.items()},
|
||||
})
|
||||
save_ini(self.ini, self.ini_lines)
|
||||
self.ini, self.ini_lines = load_ini()
|
||||
@@ -1068,6 +1246,98 @@ class LauncherWindow(Adw.ApplicationWindow):
|
||||
def environment(self):
|
||||
return game_environment(self.settings)
|
||||
|
||||
# --- Steam ---------------------------------------------------------------------------
|
||||
|
||||
def on_add_to_steam(self, _button):
|
||||
"""Bloodborne in the Steam library: a non-Steam game that runs --play (no window)."""
|
||||
steam = bbport_steam.steam_folder()
|
||||
configs = bbport_steam.user_configs(steam) if steam else []
|
||||
flatpak = bool(steam) and bbport_steam.is_flatpak(steam)
|
||||
if not configs or not bbport_steam.steam_command(flatpak):
|
||||
self.alert("Steam", tr("Steam не найден (или в него ещё никто не входил)"))
|
||||
return
|
||||
self.store() # --play starts the game with the saved settings
|
||||
if flatpak and not bbport_steam.flatpak_can_spawn():
|
||||
self.ask_flatpak_spawn(steam, configs)
|
||||
return
|
||||
self.close_steam_then_add(steam, configs)
|
||||
|
||||
def ask_flatpak_spawn(self, steam, configs):
|
||||
"""Flathub's Steam runs its games in its sandbox: the shortcut needs flatpak-spawn --host."""
|
||||
body = tr("Steam из Flatpak запускает игры в своей песочнице, где нет ни папки игры, ни драйверов: "
|
||||
"ярлык должен запускать игру вне её (flatpak-spawn --host). Для этого Steam нужно право "
|
||||
"org.freedesktop.Flatpak, с которым он может запускать программы в вашей системе. Выдать его?")
|
||||
if not hasattr(Adw, "AlertDialog"):
|
||||
self.alert("Steam", body)
|
||||
return
|
||||
dialog = Adw.AlertDialog(heading="Steam (Flatpak)", body=body)
|
||||
dialog.add_response("cancel", tr("Отмена"))
|
||||
dialog.add_response("allow", tr("Выдать право"))
|
||||
dialog.set_response_appearance("allow", Adw.ResponseAppearance.SUGGESTED)
|
||||
|
||||
def on_response(_dialog, response):
|
||||
if response != "allow":
|
||||
return
|
||||
try:
|
||||
bbport_steam.allow_flatpak_spawn()
|
||||
except (OSError, subprocess.CalledProcessError) as error:
|
||||
detail = getattr(error, "stderr", "") or str(error)
|
||||
self.alert("Steam", tr("Не удалось выдать право Steam: {}").format(detail.strip()))
|
||||
return
|
||||
self.close_steam_then_add(steam, configs)
|
||||
dialog.connect("response", on_response)
|
||||
dialog.present(self)
|
||||
|
||||
def close_steam_then_add(self, steam, configs):
|
||||
if not bbport_steam.steam_running():
|
||||
self.add_to_steam(steam, configs, restart=False)
|
||||
return
|
||||
if not hasattr(Adw, "AlertDialog"):
|
||||
self.alert("Steam", tr("Закройте Steam и нажмите «Добавить» ещё раз"))
|
||||
return
|
||||
dialog = Adw.AlertDialog(heading="Steam", body=tr(
|
||||
"Пока игра добавляется, Steam нужно закрыть; потом он запустится снова. Закрыть Steam?"))
|
||||
dialog.add_response("cancel", tr("Отмена"))
|
||||
dialog.add_response("close", tr("Закрыть Steam"))
|
||||
dialog.set_response_appearance("close", Adw.ResponseAppearance.SUGGESTED)
|
||||
|
||||
def on_response(_dialog, response):
|
||||
if response == "close":
|
||||
bbport_steam.shutdown_steam(bbport_steam.is_flatpak(steam))
|
||||
self.wait_for_steam(steam, configs, tries=60)
|
||||
dialog.connect("response", on_response)
|
||||
dialog.present(self)
|
||||
|
||||
def wait_for_steam(self, steam, configs, tries):
|
||||
"""Polls until Steam has exited (it writes its files as it goes), then adds the game."""
|
||||
def poll():
|
||||
nonlocal tries
|
||||
if bbport_steam.steam_running():
|
||||
tries -= 1
|
||||
if tries == 0:
|
||||
self.alert("Steam", tr("Steam не закрылся. Закройте его сами и попробуйте ещё раз"))
|
||||
return False
|
||||
return True
|
||||
GLib.timeout_add(1000, lambda: self.add_to_steam(steam, configs, restart=True) and False)
|
||||
return False
|
||||
GLib.timeout_add(500, poll)
|
||||
|
||||
def add_to_steam(self, steam, configs, restart):
|
||||
# Packaged: the AppImage itself; from a checkout: the launcher script.
|
||||
target = os.environ.get("APPIMAGE") or str(PORT_DIR / "launcher" / "bb-launcher.sh")
|
||||
exe, start_dir, options = bbport_steam.shortcut_command(target, str(PORT_DIR), "--play", steam)
|
||||
icon = str(PORT_DIR / "launcher" / "bloodborne.png")
|
||||
try:
|
||||
for config in configs:
|
||||
bbport_steam.add_shortcut(config / "shortcuts.vdf", "Bloodborne", exe,
|
||||
start_dir, icon, options)
|
||||
except (OSError, ValueError) as error:
|
||||
self.alert("Steam", tr("Не удалось добавить игру в Steam: {}").format(error))
|
||||
return
|
||||
if restart:
|
||||
bbport_steam.start_steam(bbport_steam.is_flatpak(steam))
|
||||
self.toasts.add_toast(Adw.Toast(title=tr("Bloodborne добавлен в библиотеку Steam"), timeout=6))
|
||||
|
||||
# --- mods ----------------------------------------------------------------------------
|
||||
|
||||
def mods_dir(self):
|
||||
@@ -1202,15 +1472,27 @@ class LauncherWindow(Adw.ApplicationWindow):
|
||||
game.add(self.community_row)
|
||||
page.add(game)
|
||||
|
||||
coop = Adw.PreferencesGroup(
|
||||
title=tr("Сервер кооператива (колокола и призывы)"),
|
||||
description=tr("srv.shadps4.net:31313 — публичный сервер shadPS4. Для частного сервера "
|
||||
"укажите host:port от его владельца. WebAPI: оставьте пустым, тогда "
|
||||
"берётся адрес сервера с портом 31315"))
|
||||
self.server_row = Adw.EntryRow(title=tr("Адрес сервера"), text=s["online_server"])
|
||||
self.webapi_row = Adw.EntryRow(title=tr("Адрес WebAPI"), text=s["online_webapi"])
|
||||
coop.add(self.server_row)
|
||||
coop.add(self.webapi_row)
|
||||
coop = Adw.PreferencesGroup(title=tr("Сервер кооператива (колокола и призывы)"))
|
||||
self.server_kind_row = combo_row(tr("Сервер shadNet"), None, SERVER_KINDS, online.server_kind(s))
|
||||
coop.add(self.server_kind_row)
|
||||
own = s["online_server"] if s["online_server"] != online.SHADNET_SERVER else ""
|
||||
self.server_row = Adw.EntryRow(title=tr("Адрес сервера (host:port)"), text=own)
|
||||
self.webapi_row = Adw.EntryRow(title=tr("Адрес WebAPI (пусто: порт 31315 сервера)"),
|
||||
text=s["online_webapi"])
|
||||
self.seamless_row = Adw.SwitchRow(
|
||||
title=tr("Бесшовный кооператив (экспериментально)"),
|
||||
subtitle=tr("Призывы между разными локациями. Только если на сервере "
|
||||
"BloodborneSeamlessCoop=true, и у всех игроков так же"),
|
||||
active=s["online_seamless"])
|
||||
guide = Adw.ActionRow(title=tr("Как запустить свой сервер"), subtitle=SERVER_GUIDE_URL,
|
||||
activatable=True)
|
||||
guide.add_suffix(Gtk.Image(icon_name="adw-external-link-symbolic"))
|
||||
guide.connect("activated", lambda _row: self.open_uri(SERVER_GUIDE_URL))
|
||||
for row in (self.server_row, self.webapi_row, self.seamless_row, guide):
|
||||
coop.add(row)
|
||||
self.own_server_rows = (self.server_row, self.webapi_row, self.seamless_row)
|
||||
self.server_kind_row.connect("notify::selected", lambda *_: self.show_server_kind())
|
||||
self.show_server_kind()
|
||||
page.add(coop)
|
||||
|
||||
account = Adw.PreferencesGroup(
|
||||
@@ -1232,6 +1514,24 @@ class LauncherWindow(Adw.ApplicationWindow):
|
||||
subtitle=tr("Выключите при Tailscale или другом VPN"),
|
||||
active=s["online_upnp"])
|
||||
connection.add(self.upnp_row)
|
||||
self.p2p_port_row = Adw.SpinRow.new_with_range(0, 65535, 1)
|
||||
self.p2p_port_row.set_title(tr("Порт кооператива (UDP)"))
|
||||
self.p2p_port_row.set_subtitle(tr("0: случайный при каждом запуске. Закрепите его, чтобы "
|
||||
"открыть в брандмауэре или на роутере вручную"))
|
||||
self.p2p_port_row.set_value(int(s["online_p2p_port"] or 0))
|
||||
connection.add(self.p2p_port_row)
|
||||
# ufw and firewalld drop the peers' packets and, for UPnP, the router's answers: UPnP opens
|
||||
# the port on the router only.
|
||||
self.firewall_rows = []
|
||||
gateway = online.default_gateway()
|
||||
for firewall in online.active_firewalls():
|
||||
warning = Adw.ActionRow(title=tr("Включён брандмауэр: {}").format(firewall),
|
||||
subtitle_selectable=True)
|
||||
warning.add_prefix(Gtk.Image(icon_name="dialog-warning-symbolic"))
|
||||
self.firewall_rows.append((warning, firewall, gateway))
|
||||
connection.add(warning)
|
||||
self.p2p_port_row.connect("notify::value", lambda *_: self.show_firewall_rows())
|
||||
self.show_firewall_rows()
|
||||
test = Adw.ActionRow(title=tr("Проверить соединение"))
|
||||
self.test_button = Gtk.Button(label=tr("Проверить"), valign=Gtk.Align.CENTER)
|
||||
self.test_button.connect("clicked", self.on_test_connection)
|
||||
@@ -1242,15 +1542,42 @@ class LauncherWindow(Adw.ApplicationWindow):
|
||||
page.add(connection)
|
||||
return page
|
||||
|
||||
def online_addresses(self):
|
||||
server = self.server_row.get_text().strip() or online.SHADNET_SERVER
|
||||
community = online.community_address(self.community_row.get_text())
|
||||
webapi = online.webapi_setting(self.webapi_row.get_text(), server, community)
|
||||
return server, webapi, community
|
||||
def online_settings(self):
|
||||
"""The Online page's settings (the keys of DEFAULTS)."""
|
||||
return {
|
||||
"online_community": self.community_row.get_text().strip(),
|
||||
"online_server_kind": combo_value(self.server_kind_row),
|
||||
"online_server": self.server_row.get_text().strip(),
|
||||
"online_webapi": self.webapi_row.get_text().strip(),
|
||||
"online_npid": self.npid_row.get_text().strip(),
|
||||
"online_password": self.password_row.get_text(),
|
||||
"online_upnp": self.upnp_row.get_active(),
|
||||
"online_p2p_port": int(self.p2p_port_row.get_value()),
|
||||
"online_seamless": self.seamless_row.get_active(),
|
||||
}
|
||||
|
||||
def show_server_kind(self):
|
||||
own = combo_value(self.server_kind_row) == "own"
|
||||
for row in self.own_server_rows:
|
||||
row.set_visible(own)
|
||||
|
||||
def show_firewall_rows(self):
|
||||
port = int(self.p2p_port_row.get_value())
|
||||
for row, firewall, gateway in self.firewall_rows:
|
||||
port_rule, router_rule = online.firewall_commands(firewall, gateway, port or online.P2P_PORT)
|
||||
lines = [tr("UPnP открывает порт только на роутере; брандмауэр этого компьютера всё "
|
||||
"равно блокирует кооператив. Откройте порт: {}").format(port_rule)]
|
||||
if not port:
|
||||
lines.insert(0, tr("Закрепите порт кооператива выше (например, {}): случайный "
|
||||
"порт в брандмауэре не открыть").format(online.P2P_PORT))
|
||||
if router_rule:
|
||||
lines.append(tr("Для UPnP нужны ответы роутера: {}").format(router_rule))
|
||||
row.set_subtitle(GLib.markup_escape_text("\n".join(lines)))
|
||||
|
||||
def on_test_connection(self, _button):
|
||||
server, webapi, community = self.online_addresses()
|
||||
names = {"game": tr("Игровой сервер"), "shadnet": tr("Сервер shadNet"), "webapi": "WebAPI"}
|
||||
server, webapi, community = online.addresses(self.online_settings())
|
||||
names = {"game": tr("Игровой сервер"), "shadnet": tr("Сервер shadNet"),
|
||||
"stun": tr("UDP сервера shadNet (призывы)"), "webapi": "WebAPI"}
|
||||
for row in self.test_rows:
|
||||
self.connection_group.remove(row)
|
||||
self.test_rows = []
|
||||
@@ -1259,10 +1586,15 @@ class LauncherWindow(Adw.ApplicationWindow):
|
||||
|
||||
def show(results):
|
||||
for name, address, ok, detail in results:
|
||||
if name == "stun" and ok:
|
||||
status = tr("отвечает; ваш публичный адрес: {}").format(detail)
|
||||
elif name == "stun" and detail == "timeout":
|
||||
status = tr("не отвечает: призывы не соединятся")
|
||||
else:
|
||||
status = tr("доступен") if ok else f'{tr("недоступен")} ({detail})'
|
||||
# Plain text: an error such as "<urlopen error ...>" is not Pango markup.
|
||||
row = Adw.ActionRow(title=GLib.markup_escape_text(f"{names[name]}: {address}"),
|
||||
subtitle=GLib.markup_escape_text(
|
||||
tr("доступен") if ok else f'{tr("недоступен")} ({detail})'))
|
||||
subtitle=GLib.markup_escape_text(status))
|
||||
row.add_prefix(Gtk.Image(icon_name="object-select-symbolic" if ok else "dialog-warning-symbolic"))
|
||||
self.connection_group.add(row)
|
||||
self.test_rows.append(row)
|
||||
@@ -1275,6 +1607,260 @@ class LauncherWindow(Adw.ApplicationWindow):
|
||||
GLib.idle_add(show, results)
|
||||
threading.Thread(target=work, daemon=True).start()
|
||||
|
||||
# --- about page ----------------------------------------------------------------------
|
||||
|
||||
def build_about_page(self):
|
||||
page = Adw.PreferencesPage()
|
||||
about = Adw.PreferencesGroup(title=tr("О программе"))
|
||||
app = Adw.ActionRow(title="bbport Linux",
|
||||
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),
|
||||
(tr("Основано на bbport от deadinside28"), bbport_update.UPSTREAM_URL,
|
||||
bbport_update.UPSTREAM_URL),
|
||||
(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)):
|
||||
row = Adw.ActionRow(title=title, subtitle=subtitle, activatable=True)
|
||||
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)
|
||||
support = Adw.ActionRow(title=tr("Поддержать проект"), subtitle="ko-fi.com/ayozetr",
|
||||
activatable=True)
|
||||
support.add_prefix(Gtk.Image(icon_name="emblem-favorite-symbolic"))
|
||||
support.add_suffix(Gtk.Image(icon_name="adw-external-link-symbolic"))
|
||||
support.connect("activated", lambda _row: self.open_uri(bbport_update.KOFI_URL))
|
||||
about.add(support)
|
||||
page.add(about)
|
||||
|
||||
updates = Adw.PreferencesGroup(title=tr("Обновления"))
|
||||
self.check_updates_row = Adw.SwitchRow(
|
||||
title=tr("Проверять обновления при запуске"),
|
||||
subtitle=tr("Спрашивает GitHub о последнем выпуске; ничего не скачивается без вашего "
|
||||
"согласия"),
|
||||
active=self.settings.get("check_updates", True))
|
||||
self.check_updates_row.connect("notify::active", lambda *_: self.store())
|
||||
updates.add(self.check_updates_row)
|
||||
self.update_row = Adw.ActionRow(title=tr("Последняя версия"))
|
||||
self.update_button = Gtk.Button(valign=Gtk.Align.CENTER)
|
||||
self.update_button.connect("clicked", self.on_update_button)
|
||||
self.update_row.add_suffix(self.update_button)
|
||||
updates.add(self.update_row)
|
||||
page.add(updates)
|
||||
return page
|
||||
|
||||
def open_uri(self, uri):
|
||||
Gtk.UriLauncher.new(uri).launch(self, None, None, None)
|
||||
|
||||
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"])
|
||||
appimage = bbport_update.running_appimage()
|
||||
button, sensitive = tr("Проверить сейчас"), True
|
||||
if state == "checking":
|
||||
subtitle, sensitive = tr("Проверка…"), False
|
||||
elif state == "downloading":
|
||||
subtitle, button, sensitive = progress or tr("Загрузка…"), tr("Обновить"), False
|
||||
elif state == "installed":
|
||||
subtitle, button = tr("Версия {} установлена: перезапустите лаунчер").format(
|
||||
latest["tag"]), tr("Перезапустить")
|
||||
elif isinstance(latest, str):
|
||||
subtitle = tr("Не удалось проверить: {}").format(latest)
|
||||
elif latest is None:
|
||||
subtitle = tr("Установлена версия {}").format(bbport_update.VERSION)
|
||||
elif not newer:
|
||||
subtitle = tr("Установлена последняя версия ({})").format(bbport_update.VERSION)
|
||||
elif appimage and latest["appimage"]:
|
||||
subtitle, button = tr("Доступна версия {} (установлена {})").format(
|
||||
latest["tag"], bbport_update.VERSION), tr("Обновить")
|
||||
else:
|
||||
subtitle, button = tr("Доступна версия {} (установлена {})").format(
|
||||
latest["tag"], bbport_update.VERSION), tr("Открыть страницу")
|
||||
self.update_row.set_subtitle(subtitle)
|
||||
self.update_button.set_label(button)
|
||||
self.update_button.set_sensitive(sensitive)
|
||||
self.update_button.remove_css_class("suggested-action")
|
||||
if button in (tr("Обновить"), tr("Перезапустить")):
|
||||
self.update_button.add_css_class("suggested-action")
|
||||
|
||||
def on_update_button(self, _button):
|
||||
latest = self.latest
|
||||
if self.update_state == "installed":
|
||||
self.restart_launcher()
|
||||
elif isinstance(latest, dict) and bbport_update.is_newer(latest["tag"]):
|
||||
if bbport_update.running_appimage() and latest["appimage"]:
|
||||
self.install_update()
|
||||
else:
|
||||
self.open_uri(latest["url"])
|
||||
else:
|
||||
self.on_check_update(None)
|
||||
|
||||
def on_check_update(self, _button, quiet=False):
|
||||
self.update_state = "checking"
|
||||
self.show_update_state()
|
||||
|
||||
def done(result):
|
||||
self.latest, self.update_state = result, "idle"
|
||||
self.show_update_state()
|
||||
if quiet and isinstance(result, dict) and bbport_update.is_newer(result["tag"]):
|
||||
toast = Adw.Toast(title=tr("Доступна версия {}").format(result["tag"]),
|
||||
button_label=tr("Подробнее"), timeout=8)
|
||||
toast.connect("button-clicked", lambda *_: self.stack.set_visible_child_name("about"))
|
||||
self.toasts.add_toast(toast)
|
||||
return False
|
||||
|
||||
def work():
|
||||
try:
|
||||
result = bbport_update.latest_release()
|
||||
except (OSError, ValueError) as error:
|
||||
result = str(getattr(error, "reason", None) or error)
|
||||
GLib.idle_add(done, result)
|
||||
threading.Thread(target=work, daemon=True).start()
|
||||
|
||||
def install_update(self):
|
||||
target = bbport_update.running_appimage()
|
||||
self.update_state = "downloading"
|
||||
self.show_update_state()
|
||||
|
||||
def progress(done, total):
|
||||
text = tr("Загрузка… {} %").format(done * 100 // total) if total else \
|
||||
tr("Загрузка… {} МБ").format(done >> 20)
|
||||
GLib.idle_add(self.show_update_state, text)
|
||||
|
||||
def finished(error):
|
||||
self.update_state = "idle" if error else "installed"
|
||||
self.show_update_state()
|
||||
if error:
|
||||
self.toasts.add_toast(Adw.Toast(title=tr("Не удалось обновить: {}").format(error)))
|
||||
return False
|
||||
|
||||
def work():
|
||||
try:
|
||||
bbport_update.install(self.latest["appimage"], target, progress)
|
||||
error = None
|
||||
except OSError as failure:
|
||||
error = str(getattr(failure, "reason", None) or failure)
|
||||
GLib.idle_add(finished, error)
|
||||
threading.Thread(target=work, daemon=True).start()
|
||||
|
||||
def restart_launcher(self):
|
||||
if self.process is not None:
|
||||
self.toasts.add_toast(Adw.Toast(title=tr("Сначала закройте игру")))
|
||||
return
|
||||
target = bbport_update.running_appimage()
|
||||
env = {k: v for k, v in os.environ.items()
|
||||
if k not in ("APPDIR", "APPIMAGE", "ARGV0", "OWD", "LD_LIBRARY_PATH", "PYTHONHOME",
|
||||
"PYTHONPATH")}
|
||||
# The new launcher starts once this one has exited: with the same application ID while
|
||||
# this one runs, it would only raise this window and quit.
|
||||
subprocess.Popen(["sh", "-c", 'while kill -0 "$1" 2>/dev/null; do sleep 0.2; done; exec "$2"',
|
||||
"sh", str(os.getpid()), str(target)], env=env, start_new_session=True)
|
||||
self.close() # on_close saves the settings
|
||||
|
||||
# --- trophies page -------------------------------------------------------------------
|
||||
|
||||
def build_trophies_page(self):
|
||||
page = Adw.PreferencesPage()
|
||||
key = Adw.PreferencesGroup(title=tr("Ключ трофеев"), description=tr(
|
||||
"Ключ трофеев PS4 (TrophyKey в shadPS4), 32 шестнадцатеричных символа: с ним видны "
|
||||
"названия и описания. Хранится на этом компьютере. Без него видны только значки"))
|
||||
self.trophy_key_row = Adw.PasswordEntryRow(title=tr("Ключ трофеев"),
|
||||
text=self.settings.get("trophy_key", ""),
|
||||
show_apply_button=True)
|
||||
self.trophy_key_row.connect("apply", lambda _row: (self.store(), self.refresh_trophies()))
|
||||
key.add(self.trophy_key_row)
|
||||
page.add(key)
|
||||
self.trophy_group = Adw.PreferencesGroup(title=tr("Трофеи"), description=tr(
|
||||
"Записываются, когда игра их открывает, и показываются в игре"))
|
||||
page.add(self.trophy_group)
|
||||
self.trophy_rows = []
|
||||
return page
|
||||
|
||||
def refresh_trophies(self):
|
||||
game = Path(self.settings.get("game_dir") or "").expanduser()
|
||||
user = self.user_dir()
|
||||
key = trophies.parse_key(self.trophy_key_row.get_text())
|
||||
language = int(self.settings.get("language") or system_game_language())
|
||||
|
||||
def show(directory, problem):
|
||||
for row in self.trophy_rows:
|
||||
self.trophy_group.remove(row)
|
||||
self.trophy_rows = []
|
||||
rows = []
|
||||
if directory is None:
|
||||
rows.append(Adw.ActionRow(title=problem or tr("В дампе игры нет трофеев (sce_sys/trophy)")))
|
||||
else:
|
||||
listed, earned = trophies.read_list(directory), trophies.read_earned(directory)
|
||||
counted = [i for i, t in listed.items() if t["grade"] != "P"] or list(listed)
|
||||
done = sum(1 for i in counted if i in earned)
|
||||
self.trophy_group.set_description(tr("Получено {} из {}").format(done, len(counted)))
|
||||
if problem:
|
||||
rows.append(Adw.ActionRow(title=problem))
|
||||
for i, t in sorted(listed.items(), key=lambda item: (item[0] not in earned, item[0])):
|
||||
got = i in earned
|
||||
name = t["name"] or f"#{i}"
|
||||
detail = t["detail"]
|
||||
if t["hidden"] and not got:
|
||||
name, detail = tr("Скрытый трофей"), ""
|
||||
when = time.strftime("%Y-%m-%d %H:%M", time.localtime(earned[i])) if got else None
|
||||
subtitle = "\n".join(x for x in (detail, tr("Получен {}").format(when) if when else None) if x)
|
||||
row = Adw.ActionRow(title=GLib.markup_escape_text(name),
|
||||
subtitle=GLib.markup_escape_text(subtitle))
|
||||
icon = Path(directory) / "icons" / f"TROP{i:03d}.PNG"
|
||||
image = Gtk.Image(file=str(icon), pixel_size=48) if icon.is_file() else \
|
||||
Gtk.Image(icon_name="starred-symbolic", pixel_size=48)
|
||||
row.add_prefix(image)
|
||||
grade = TROPHY_GRADES.get(t["grade"])
|
||||
if grade:
|
||||
row.add_suffix(Gtk.Label(label=tr(grade), css_classes=["dim-label"]))
|
||||
if not got:
|
||||
image.set_opacity(0.35)
|
||||
rows.append(row)
|
||||
for row in rows:
|
||||
self.trophy_group.add(row)
|
||||
self.trophy_rows.append(row)
|
||||
return False
|
||||
|
||||
def work():
|
||||
problem = None
|
||||
try:
|
||||
directory = trophies.prepare(game, user, key, language)
|
||||
except (OSError, ValueError, trophies.ET.ParseError):
|
||||
problem = tr("Неверный ключ трофеев: названия недоступны")
|
||||
try:
|
||||
directory = trophies.prepare(game, user, None, language)
|
||||
except (OSError, ValueError):
|
||||
directory = None
|
||||
GLib.idle_add(show, directory, problem)
|
||||
threading.Thread(target=work, daemon=True).start()
|
||||
|
||||
# --- cheats page ---------------------------------------------------------------------
|
||||
|
||||
def build_cheats_page(self):
|
||||
page = Adw.PreferencesPage()
|
||||
self.cheat_rows = {}
|
||||
for title, description, items in (
|
||||
(tr("Читы"), tr("Патчи игры версии 1.09, применяются при запуске. Для обычного "
|
||||
"прохождения и игры по сети оставьте их выключенными"), CHEATS),
|
||||
(tr("Изменения игрового процесса"), None, TWEAKS)):
|
||||
group = Adw.PreferencesGroup(title=title, description=description)
|
||||
for key, name, subtitle in items:
|
||||
row = Adw.SwitchRow(title=tr(name), subtitle=tr(subtitle),
|
||||
active=self.ini.get(key, "0") == "1")
|
||||
self.cheat_rows[key] = row
|
||||
group.add(row)
|
||||
page.add(group)
|
||||
# Enemy control and the free camera use the same buttons: one at a time.
|
||||
control, camera = self.cheat_rows["cheat_enemy_control"], self.effect_rows["debug_camera"]
|
||||
control.connect("notify::active", lambda row, _p: row.get_active() and camera.set_active(False))
|
||||
camera.connect("notify::active", lambda row, _p: row.get_active() and control.set_active(False))
|
||||
return page
|
||||
|
||||
def build_log_page(self):
|
||||
box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
|
||||
self.log_view = Gtk.TextView(editable=False, monospace=True, cursor_visible=False,
|
||||
@@ -1462,8 +2048,8 @@ class LauncherWindow(Adw.ApplicationWindow):
|
||||
|
||||
def update_dlss_row(self):
|
||||
own = ngx_libraries(self.user_dir() / "dlss")
|
||||
bundled = ngx_libraries(binaries_dir())
|
||||
bridge = (binaries_dir() / "libbbport_dlss.so").is_file()
|
||||
bundled = ngx_libraries(dlss_dir())
|
||||
bridge = (dlss_dir() / "libbbport_dlss.so").is_file()
|
||||
if own:
|
||||
text = tr("Своя: {}").format(own[-1].name[len(NGX_PREFIX):])
|
||||
elif bundled:
|
||||
@@ -1699,7 +2285,12 @@ def play():
|
||||
print("bbport: choose the game folder in the launcher first", file=sys.stderr)
|
||||
return 1
|
||||
os.chdir(PORT_DIR)
|
||||
os.execvpe("bash", ["bash", str(PORT_DIR / "run.sh")], game_environment(settings))
|
||||
env = game_environment(settings)
|
||||
# Started by Steam: its runtime's libraries are first in LD_LIBRARY_PATH; the port is built
|
||||
# against the system's. Steam keeps the original in SYSTEM_LD_LIBRARY_PATH.
|
||||
if "SYSTEM_LD_LIBRARY_PATH" in env:
|
||||
env["LD_LIBRARY_PATH"] = env["SYSTEM_LD_LIBRARY_PATH"]
|
||||
os.execvpe("bash", ["bash", str(PORT_DIR / "run.sh")], env)
|
||||
|
||||
|
||||
def build_fsr411(arguments):
|
||||
|
||||
@@ -0,0 +1,221 @@
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
"""Adds the game to Steam as a non-Steam shortcut (Steam's userdata/<id>/config/shortcuts.vdf).
|
||||
From the Windows launcher of Supermedo/bloodborne_pc; finding, closing and starting Steam are
|
||||
Linux's here.
|
||||
|
||||
shortcuts.vdf is Valve's binary KeyValues: a type byte (0 = map, 1 = string, 2 = int32,
|
||||
7 = uint64), a NUL-terminated key, then the value; 8 ends a map. The file holds one map,
|
||||
"shortcuts", whose entries are maps named "0", "1", ...
|
||||
"""
|
||||
import os
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import struct
|
||||
import subprocess
|
||||
import zlib
|
||||
|
||||
MAP, STRING, INT32, UINT64, END = 0, 1, 2, 7, 8
|
||||
|
||||
|
||||
def _cstring(data, at):
|
||||
end = data.index(b'\0', at)
|
||||
return data[at:end].decode('utf-8', errors='replace'), end + 1
|
||||
|
||||
|
||||
def parse(data, at=0):
|
||||
"""A map as a list of (type, key, value); nested maps are lists too. Returns (items, next)."""
|
||||
items = []
|
||||
while at < len(data):
|
||||
kind = data[at]
|
||||
at += 1
|
||||
if kind == END:
|
||||
return items, at
|
||||
key, at = _cstring(data, at)
|
||||
if kind == MAP:
|
||||
value, at = parse(data, at)
|
||||
elif kind == STRING:
|
||||
value, at = _cstring(data, at)
|
||||
elif kind == INT32:
|
||||
value, at = struct.unpack_from('<i', data, at)[0], at + 4
|
||||
elif kind == UINT64:
|
||||
value, at = struct.unpack_from('<Q', data, at)[0], at + 8
|
||||
else:
|
||||
raise ValueError(f'unknown field type {kind} in shortcuts.vdf')
|
||||
items.append((kind, key, value))
|
||||
return items, at
|
||||
|
||||
|
||||
def dump(items):
|
||||
out = bytearray()
|
||||
for kind, key, value in items:
|
||||
out += bytes([kind]) + key.encode('utf-8') + b'\0'
|
||||
if kind == MAP:
|
||||
out += dump(value) + bytes([END])
|
||||
elif kind == STRING:
|
||||
out += str(value).encode('utf-8') + b'\0'
|
||||
elif kind == INT32:
|
||||
out += struct.pack('<i', value)
|
||||
elif kind == UINT64:
|
||||
out += struct.pack('<Q', value)
|
||||
return bytes(out)
|
||||
|
||||
|
||||
def shortcut_id(exe, name):
|
||||
"""The id Steam gives a non-Steam game: CRC32 of the quoted exe and the name, top bit set."""
|
||||
return (zlib.crc32((exe + name).encode('utf-8')) & 0xffffffff) | 0x80000000
|
||||
|
||||
|
||||
def entry(name, exe, start_dir, icon, options=''):
|
||||
quoted = f'"{exe}"'
|
||||
appid = shortcut_id(quoted, name)
|
||||
return [
|
||||
(INT32, 'appid', struct.unpack('<i', struct.pack('<I', appid))[0]),
|
||||
(STRING, 'AppName', name), (STRING, 'Exe', quoted), (STRING, 'StartDir', f'"{start_dir}"'),
|
||||
(STRING, 'icon', icon), (STRING, 'ShortcutPath', ''), (STRING, 'LaunchOptions', options),
|
||||
(INT32, 'IsHidden', 0), (INT32, 'AllowDesktopConfig', 1), (INT32, 'AllowOverlay', 1),
|
||||
(INT32, 'OpenVR', 0), (INT32, 'Devkit', 0), (STRING, 'DevkitGameID', ''),
|
||||
(INT32, 'DevkitOverrideAppID', 0), (INT32, 'LastPlayTime', 0), (STRING, 'FlatpakAppID', ''),
|
||||
(MAP, 'tags', []),
|
||||
]
|
||||
|
||||
|
||||
def add_shortcut(vdf_path, name, exe, start_dir, icon, options=''):
|
||||
"""Adds the shortcut to one shortcuts.vdf, or updates the entry that starts the same exe or
|
||||
has the same name and options (this port from another place: a checkout, then the AppImage);
|
||||
through flatpak-spawn (Flathub's Steam), only an entry of the same name that runs --play.
|
||||
Returns 'added' or 'updated'. A copy of the old file is kept as shortcuts.vdf.bak."""
|
||||
vdf_path = Path(vdf_path)
|
||||
data = vdf_path.read_bytes() if vdf_path.exists() else b''
|
||||
root = parse(data)[0] if data else []
|
||||
shortcuts = next((value for kind, key, value in root if kind == MAP and key.lower() == 'shortcuts'), None)
|
||||
if shortcuts is None:
|
||||
shortcuts = []
|
||||
root.append((MAP, 'shortcuts', shortcuts))
|
||||
new = entry(name, exe, start_dir, icon, options)
|
||||
target = f'"{exe}"'.lower()
|
||||
for index, (kind, key, value) in enumerate(shortcuts):
|
||||
fields = {k.lower(): v for _t, k, v in value} if kind == MAP else {}
|
||||
same_port = fields.get('appname') == name and fields.get('launchoptions') == options and options
|
||||
same_exe = str(fields.get('exe', '')).lower() == target
|
||||
if exe == SPAWN:
|
||||
# flatpak-spawn starts other launchers' games too: only this port's entry is ours.
|
||||
same_exe = same_exe and fields.get('appname') == name and \
|
||||
str(fields.get('launchoptions', '')).endswith('--play')
|
||||
if same_exe or same_port:
|
||||
# Keep what Steam or the player added (play time, tags).
|
||||
kept = {k.lower() for k in ('LastPlayTime', 'tags')}
|
||||
old = {k.lower(): (t, k, v) for t, k, v in value}
|
||||
shortcuts[index] = (MAP, key, [old[k.lower()] if k.lower() in kept and k.lower() in old else (t, k, v)
|
||||
for t, k, v in new])
|
||||
result = 'updated'
|
||||
break
|
||||
else:
|
||||
shortcuts.append((MAP, str(len(shortcuts)), new))
|
||||
result = 'added'
|
||||
if data:
|
||||
vdf_path.with_name(vdf_path.name + '.bak').write_bytes(data)
|
||||
vdf_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
vdf_path.write_bytes(dump(root) + bytes([END]))
|
||||
return result
|
||||
|
||||
|
||||
FLATPAK_ID = 'com.valvesoftware.Steam'
|
||||
|
||||
|
||||
def flatpak_data(home=None):
|
||||
"""The data folder of Flathub's Steam (inside its sandbox's home)."""
|
||||
return Path(home or Path.home()) / '.var/app' / FLATPAK_ID / '.local/share/Steam'
|
||||
|
||||
|
||||
def steam_folder(home=None):
|
||||
"""Steam's data folder: the native package's (~/.steam/steam links to it), else Flathub's
|
||||
Steam's; None without either."""
|
||||
home = Path(home or Path.home())
|
||||
data = Path(os.environ.get('XDG_DATA_HOME', home / '.local/share')) if home == Path.home() \
|
||||
else home / '.local/share'
|
||||
for path in (data / 'Steam', home / '.steam/steam', home / '.steam/root', flatpak_data(home),
|
||||
home / '.var/app' / FLATPAK_ID / '.steam/steam'):
|
||||
if (path / 'userdata').is_dir():
|
||||
return path.resolve()
|
||||
return None
|
||||
|
||||
|
||||
def is_flatpak(steam):
|
||||
"""Whether a Steam data folder is Flathub's Steam's."""
|
||||
return f'/.var/app/{FLATPAK_ID}/' in str(Path(steam).resolve()) + '/'
|
||||
|
||||
|
||||
def user_configs(steam):
|
||||
"""userdata/<account>/config folders of the accounts that used Steam on this PC."""
|
||||
users = Path(steam) / 'userdata'
|
||||
return [d / 'config' for d in sorted(users.iterdir()) if d.is_dir() and d.name.isdigit() and d.name != '0'] \
|
||||
if users.is_dir() else []
|
||||
|
||||
|
||||
def steam_running():
|
||||
return subprocess.run(['pgrep', '-x', 'steam'], capture_output=True).returncode == 0
|
||||
|
||||
|
||||
def steam_command(flatpak=False):
|
||||
"""The command that starts Steam (a list), or None."""
|
||||
if flatpak:
|
||||
return ['flatpak', 'run', FLATPAK_ID] if shutil.which('flatpak') else None
|
||||
return [shutil.which('steam')] if shutil.which('steam') else None
|
||||
|
||||
|
||||
def shutdown_steam(flatpak=False):
|
||||
"""Asks Steam to exit (it writes shortcuts.vdf as it goes, so it must be closed first)."""
|
||||
subprocess.Popen([*steam_command(flatpak), '-shutdown'], stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL, start_new_session=True)
|
||||
|
||||
|
||||
def start_steam(flatpak=False):
|
||||
"""Starts Steam on its library, detached from the launcher."""
|
||||
subprocess.Popen([*steam_command(flatpak), 'steam://open/games'], stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL, start_new_session=True)
|
||||
|
||||
|
||||
# Flathub's Steam starts non-Steam games inside its sandbox, which sees neither the game folder
|
||||
# nor the host's drivers and cannot mount an AppImage: the shortcut runs flatpak-spawn --host,
|
||||
# which needs Steam to be allowed to talk to org.freedesktop.Flatpak.
|
||||
SPAWN = '/usr/bin/flatpak-spawn'
|
||||
SPAWN_PERMISSION = 'org.freedesktop.Flatpak'
|
||||
|
||||
|
||||
def flatpak_can_spawn(run=subprocess.run):
|
||||
"""Whether Flathub's Steam may start programs on the host (flatpak-spawn --host)."""
|
||||
try:
|
||||
info = run(['flatpak', 'info', '--show-permissions', FLATPAK_ID], capture_output=True, text=True)
|
||||
except OSError:
|
||||
return False
|
||||
return any(line.strip() == f'{SPAWN_PERMISSION}=talk' for line in info.stdout.splitlines())
|
||||
|
||||
|
||||
def allow_flatpak_spawn(run=subprocess.run):
|
||||
"""Gives Flathub's Steam that permission for this user (flatpak override --user)."""
|
||||
run(['flatpak', 'override', '--user', f'--talk-name={SPAWN_PERMISSION}', FLATPAK_ID],
|
||||
capture_output=True, text=True, check=True)
|
||||
|
||||
|
||||
def shortcut_command(exe, start_dir, options, steam):
|
||||
"""(exe, start dir, launch options) of the shortcut that runs `exe options` in start_dir for
|
||||
this Steam: through flatpak-spawn --host for Flathub's Steam."""
|
||||
if is_flatpak(steam):
|
||||
return SPAWN, str(Path(steam).parent), f'--host --directory="{start_dir}" "{exe}" {options}'.strip()
|
||||
return exe, start_dir, options
|
||||
|
||||
|
||||
if __name__ == '__main__': # self-test on a temporary file
|
||||
import tempfile
|
||||
with tempfile.TemporaryDirectory() as folder:
|
||||
vdf = Path(folder) / 'shortcuts.vdf'
|
||||
print(add_shortcut(vdf, 'Bloodborne', '/games/bb/launcher/bb-launcher.sh', '/games/bb', 'x.png', '--play'))
|
||||
print(add_shortcut(vdf, 'Other', '/other/o', '/other', ''))
|
||||
print(add_shortcut(vdf, 'Bloodborne', '/games/bb/launcher/bb-launcher.sh', '/games/bb', 'y.png', '--play'))
|
||||
print(add_shortcut(vdf, 'Bloodborne', '/apps/Bloodborne.AppImage', '/apps', 'y.png', '--play'))
|
||||
print(add_shortcut(vdf, 'Bloodborne', '/shadps4/shadps4', '/shadps4', '', 'CUSA03173'))
|
||||
root = parse(vdf.read_bytes())[0]
|
||||
print([[f for f in e[2] if f[1] in ('AppName', 'Exe')] for e in root[0][2]])
|
||||
assert len(root[0][2]) == 3
|
||||
assert dump(root) + bytes([END]) == vdf.read_bytes()
|
||||
print(os.path.basename(__file__), 'ok')
|
||||
@@ -0,0 +1,82 @@
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
"""The launcher's version, and updates of the AppImage from this fork's GitHub releases.
|
||||
|
||||
Releases are tagged <upstream version>-linux.<n> (0.4-linux.1, 0.5-linux.1) and carry the
|
||||
AppImage as an asset. An update downloads it next to the running AppImage ($APPIMAGE) and
|
||||
renames it over that file: the running copy keeps its own mount, and the next start is the new
|
||||
version. Without an AppImage (a git checkout) the launcher only links the release.
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import re
|
||||
import tempfile
|
||||
import urllib.request
|
||||
|
||||
VERSION = "0.51-linux.2"
|
||||
REPOSITORY = "ayozetr/bloodborne_linux"
|
||||
REPOSITORY_URL = f"https://github.com/{REPOSITORY}"
|
||||
UPSTREAM_URL = "https://github.com/deadinside28/bloodborne_pc"
|
||||
WINDOWS_URL = "https://github.com/Supermedo/bloodborne_pc"
|
||||
LICENSE_URL = f"{REPOSITORY_URL}/blob/main/LICENSE"
|
||||
KOFI_URL = "https://ko-fi.com/ayozetr"
|
||||
RELEASES_URL = f"{REPOSITORY_URL}/releases"
|
||||
LATEST_API = f"https://api.github.com/repos/{REPOSITORY}/releases/latest"
|
||||
|
||||
|
||||
def version_key(tag):
|
||||
"""Comparable key of a tag (0.5-linux.1 -> (0, 5, 1)); None for another scheme."""
|
||||
match = re.fullmatch(r"v?(\d+)\.(\d+)(?:\.(\d+))?-linux\.(\d+)", tag.strip())
|
||||
if not match:
|
||||
return None
|
||||
major, minor, patch, build = match.groups()
|
||||
return int(major), int(minor), int(patch or 0), int(build)
|
||||
|
||||
|
||||
def is_newer(tag, current=VERSION):
|
||||
new, old = version_key(tag), version_key(current)
|
||||
return new is not None and old is not None and new > old
|
||||
|
||||
|
||||
def latest_release(url=LATEST_API, timeout=10):
|
||||
"""{"tag", "url", "appimage", "size"} of the latest release ("appimage" None without one)."""
|
||||
request = urllib.request.Request(url, headers={
|
||||
"Accept": "application/vnd.github+json", "User-Agent": f"bbport-launcher/{VERSION}"})
|
||||
with urllib.request.urlopen(request, timeout=timeout) as reply:
|
||||
data = json.load(reply)
|
||||
asset = next((a for a in data.get("assets", []) if a.get("name", "").endswith(".AppImage")), None)
|
||||
return {"tag": data.get("tag_name", ""), "url": data.get("html_url", RELEASES_URL),
|
||||
"appimage": asset and asset.get("browser_download_url"),
|
||||
"size": asset and asset.get("size")}
|
||||
|
||||
|
||||
def running_appimage():
|
||||
"""The AppImage this launcher runs from, when it is one and can be replaced."""
|
||||
path = os.environ.get("APPIMAGE")
|
||||
if not path:
|
||||
return None
|
||||
path = Path(path)
|
||||
return path if path.is_file() and os.access(path.parent, os.W_OK) else None
|
||||
|
||||
|
||||
def install(url, target, progress=None, chunk=1 << 20):
|
||||
"""Downloads `url` next to `target` and renames it over `target`, executable.
|
||||
progress(done, total) after every chunk; total is 0 when the server does not say."""
|
||||
request = urllib.request.Request(url, headers={"User-Agent": f"bbport-launcher/{VERSION}"})
|
||||
handle, partial = tempfile.mkstemp(prefix=".bbport-update-", dir=target.parent)
|
||||
try:
|
||||
with os.fdopen(handle, "wb") as out, urllib.request.urlopen(request, timeout=30) as reply:
|
||||
total = int(reply.headers.get("Content-Length") or 0)
|
||||
done = 0
|
||||
while block := reply.read(chunk):
|
||||
out.write(block)
|
||||
done += len(block)
|
||||
if progress:
|
||||
progress(done, total)
|
||||
if total and done != total:
|
||||
raise OSError(f"download ended at {done} of {total} bytes")
|
||||
os.chmod(partial, 0o755)
|
||||
os.replace(partial, target)
|
||||
except BaseException:
|
||||
Path(partial).unlink(missing_ok=True)
|
||||
raise
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 12 KiB |
@@ -0,0 +1,64 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024" width="1024" height="1024">
|
||||
<!-- Bloodborne port icon: blood moon over Yharnam's spires (original artwork, GPL-2.0-or-later) -->
|
||||
<defs>
|
||||
<radialGradient id="sky" cx="50%" cy="38%" r="70%">
|
||||
<stop offset="0" stop-color="#3a0d0b"/>
|
||||
<stop offset="0.55" stop-color="#170807"/>
|
||||
<stop offset="1" stop-color="#070404"/>
|
||||
</radialGradient>
|
||||
<radialGradient id="moon" cx="46%" cy="42%" r="55%">
|
||||
<stop offset="0" stop-color="#e2362a"/>
|
||||
<stop offset="0.6" stop-color="#b3191a"/>
|
||||
<stop offset="1" stop-color="#6e0b0e"/>
|
||||
</radialGradient>
|
||||
<radialGradient id="glow" cx="50%" cy="50%" r="50%">
|
||||
<stop offset="0.55" stop-color="#c0201c" stop-opacity="0.45"/>
|
||||
<stop offset="1" stop-color="#c0201c" stop-opacity="0"/>
|
||||
</radialGradient>
|
||||
<linearGradient id="frame" x1="0" y1="0" x2="1" y2="1">
|
||||
<stop offset="0" stop-color="#d9b779"/>
|
||||
<stop offset="0.5" stop-color="#8a6a36"/>
|
||||
<stop offset="1" stop-color="#c9a15e"/>
|
||||
</linearGradient>
|
||||
<clipPath id="tile"><rect x="40" y="40" width="944" height="944" rx="170"/></clipPath>
|
||||
</defs>
|
||||
<rect x="40" y="40" width="944" height="944" rx="170" fill="url(#sky)"/>
|
||||
<g clip-path="url(#tile)">
|
||||
<circle cx="512" cy="400" r="330" fill="url(#glow)"/>
|
||||
<circle cx="512" cy="400" r="250" fill="url(#moon)"/>
|
||||
<!-- city silhouette -->
|
||||
<g fill="#050303">
|
||||
<!-- central cathedral spire -->
|
||||
<polygon points="512,150 528,300 540,300 552,470 472,470 484,300 496,300"/>
|
||||
<rect x="500" y="118" width="24" height="40"/>
|
||||
<rect x="507" y="96" width="10" height="34"/>
|
||||
<rect x="494" y="106" width="36" height="8"/>
|
||||
<polygon points="440,470 584,470 600,1000 424,1000"/>
|
||||
<!-- flanking towers -->
|
||||
<polygon points="408,330 420,430 432,430 446,560 370,560 384,430 396,430"/>
|
||||
<polygon points="616,330 628,430 640,430 654,560 578,560 592,430 604,430"/>
|
||||
<rect x="360" y="550" width="100" height="450"/>
|
||||
<rect x="564" y="550" width="100" height="450"/>
|
||||
<!-- outer spires -->
|
||||
<polygon points="300,470 312,560 322,560 334,660 266,660 278,560 288,560"/>
|
||||
<polygon points="724,470 736,560 746,560 758,660 690,660 702,560 712,560"/>
|
||||
<rect x="250" y="650" width="100" height="350"/>
|
||||
<rect x="674" y="650" width="100" height="350"/>
|
||||
<polygon points="190,600 200,680 214,680 224,760 160,760 170,680 180,680"/>
|
||||
<polygon points="834,600 844,680 858,680 868,760 804,760 814,680 824,680"/>
|
||||
<rect x="40" y="750" width="230" height="250"/>
|
||||
<rect x="754" y="750" width="230" height="250"/>
|
||||
<polygon points="40,820 120,700 150,760 200,740 270,800 270,1000 40,1000"/>
|
||||
<polygon points="984,820 904,700 874,760 824,740 754,800 754,1000 984,1000"/>
|
||||
</g>
|
||||
<!-- lit windows -->
|
||||
<g fill="#d9922f" opacity="0.85">
|
||||
<rect x="505" y="560" width="14" height="40" rx="7"/>
|
||||
<rect x="388" y="620" width="10" height="28" rx="5"/>
|
||||
<rect x="626" y="620" width="10" height="28" rx="5"/>
|
||||
<rect x="292" y="720" width="10" height="24" rx="5"/>
|
||||
<rect x="722" y="720" width="10" height="24" rx="5"/>
|
||||
</g>
|
||||
</g>
|
||||
<rect x="40" y="40" width="944" height="944" rx="170" fill="none" stroke="url(#frame)" stroke-width="22"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.3 KiB |
@@ -4,6 +4,6 @@ set -euo pipefail
|
||||
here=$(cd -- "$(dirname -- "$0")" && pwd)
|
||||
dest=${XDG_DATA_HOME:-$HOME/.local/share}/applications
|
||||
mkdir -p "$dest"
|
||||
sed "s|@LAUNCHER@|$here/bb-launcher.sh|" "$here/io.github.bbport.Launcher.desktop.in" \
|
||||
sed -e "s|@LAUNCHER@|$here/bb-launcher.sh|" -e "s|@ICON@|$here/bloodborne.png|" "$here/io.github.bbport.Launcher.desktop.in" \
|
||||
> "$dest/io.github.bbport.Launcher.desktop"
|
||||
echo "Installed $dest/io.github.bbport.Launcher.desktop"
|
||||
@@ -3,5 +3,6 @@ Type=Application
|
||||
Name=Bloodborne
|
||||
Comment=Native Bloodborne port launcher
|
||||
Exec=@LAUNCHER@
|
||||
Icon=applications-games
|
||||
Icon=@ICON@
|
||||
Categories=Game;
|
||||
StartupWMClass=io.github.bbport.Launcher
|
||||
Executable
+118
@@ -0,0 +1,118 @@
|
||||
#!/usr/bin/env bash
|
||||
# Builds dist/Bloodborne-bbport-x86_64.AppImage without Nix: the port's current build (run
|
||||
# build.sh first), its scripts and the GTK4 launcher, with every library they load (glibc, Python
|
||||
# and PyGObject, GTK 4, libadwaita, SDL3, FFmpeg, Mesa's Vulkan drivers) collected from this
|
||||
# system by quick-sharun (github.com/pkgforge-dev/Anylinux-AppImages). The AppImage then runs on
|
||||
# other distributions too; build it on an up-to-date Arch Linux (or CachyOS) with the launcher's
|
||||
# dependencies installed. NVIDIA's driver comes from the host at run time.
|
||||
# Running it opens the launcher; `--play` starts the game with the launcher's saved settings.
|
||||
# Data (generated files, saves, bbport.ini): ~/.local/share/bbport (BB_DATA_DIR).
|
||||
set -euo pipefail
|
||||
cd -- "$(dirname -- "$0")/.."
|
||||
[[ -f out/bb-probe && -f out/gpu/libbbgpu.so ]] || { echo 'Build first: bash build.sh' >&2; exit 1; }
|
||||
# FSR 4 assets ship with the package (downloaded once; files already there are kept).
|
||||
bash tools/fetch_fsr4_assets.sh
|
||||
root=$PWD
|
||||
work=$root/out/appimage
|
||||
appdir=$work/AppDir
|
||||
rm -rf "$appdir"
|
||||
mkdir -p "$appdir/bin" "$appdir/share/bbport" "$work/tmp"
|
||||
|
||||
# quick-sharun at a fixed revision; it downloads sharun and appimagetool itself (checksummed).
|
||||
quick_rev=7eead3068c040e3b530aa06fd46522b4a45a62dc
|
||||
quick_sha=2ae6fc29242b5c45b63f3638fff71cead679c8c41671a2c3970a6947e332470c
|
||||
quick=$work/quick-sharun.sh
|
||||
if ! echo "$quick_sha $quick" | sha256sum -c --status 2>/dev/null; then
|
||||
curl -fsSL -o "$quick" \
|
||||
"https://raw.githubusercontent.com/pkgforge-dev/Anylinux-AppImages/$quick_rev/useful-tools/quick-sharun.sh"
|
||||
echo "$quick_sha $quick" | sha256sum -c --quiet
|
||||
fi
|
||||
chmod +x "$quick"
|
||||
|
||||
# The port as run.sh expects it with BB_PREBUILT=1 (same layout as packaging/default.nix):
|
||||
# share/bbport/{run.sh,scripts,...,bin/bb-probe}; bin/ is the AppImage's own bin directory.
|
||||
d=$appdir/share/bbport
|
||||
cp run.sh "$d/"
|
||||
cp -r scripts patches fsr4_shaders launcher assets "$d/"
|
||||
# The keyboard's button prompts (scripts/menu_prompts.py) are drawn with Pillow in this font.
|
||||
font=$(fc-match -f '%{file}' 'DejaVu Sans:bold')
|
||||
[[ $font == */DejaVuSans-Bold.ttf ]] || { echo 'DejaVu Sans Bold not found (ttf-dejavu).' >&2; exit 1; }
|
||||
install -Dm644 "$font" "$d/fonts/DejaVuSans-Bold.ttf"
|
||||
mkdir -p "$d/tools"
|
||||
cp -r tools/fsr4cap "$d/tools/"
|
||||
find "$d" -name __pycache__ -type d -prune -exec rm -rf {} +
|
||||
ln -s ../../bin "$d/bin"
|
||||
|
||||
# The AppImage's main program: the launcher (or, with --play, the game alone).
|
||||
cat > "$appdir/bin/bloodborne" <<'EOF'
|
||||
#!/bin/sh
|
||||
# Entry point of the AppImage (AppRun runs it with APPDIR set and APPDIR/bin first on PATH).
|
||||
export BB_PREBUILT=1
|
||||
export PYTHON="$APPDIR/bin/python3"
|
||||
export BB_VULKANINFO="$APPDIR/bin/vulkaninfo"
|
||||
export BB_DLSS_DIR="$APPDIR/share/bbport/dlss"
|
||||
export BB_NET_MODULE="$APPDIR/share/bbport/net/libbbnet.so"
|
||||
export BB_DATA_DIR="${BB_DATA_DIR:-${XDG_DATA_HOME:-$HOME/.local/share}/bbport}"
|
||||
mkdir -p "$BB_DATA_DIR"
|
||||
exec "$APPDIR/bin/python3" "$APPDIR/share/bbport/launcher/bbport_launcher.py" "$@"
|
||||
EOF
|
||||
chmod +x "$appdir/bin/bloodborne"
|
||||
sed -e 's|@LAUNCHER@|bloodborne|' -e 's|@ICON@|bloodborne|' \
|
||||
launcher/io.github.bbport.Launcher.desktop.in > "$work/bloodborne.desktop"
|
||||
|
||||
# GTK, libadwaita and their typelibs are loaded by PyGObject at run time (not linked): named here.
|
||||
# So are Python's ssl and hashlib modules, which quick-sharun leaves out by default: the launcher's
|
||||
# connection test and scripts/online.py talk HTTPS to the co-op servers. Pillow (in site-packages,
|
||||
# copied with Python) is named too, for the libraries its modules link (keyboard prompts).
|
||||
pydyn=$(python3 -c 'import sysconfig; print(sysconfig.get_path("platstdlib"))')/lib-dynload
|
||||
pil=$(python3 -c 'import os, PIL; print(os.path.dirname(PIL.__file__))')
|
||||
cd "$work"
|
||||
export APPDIR=$appdir TMPDIR=$work/tmp OUTPATH=$work
|
||||
export MAIN_BIN=bloodborne DESKTOP=$work/bloodborne.desktop ICON=$root/launcher/bloodborne.png
|
||||
export DEPLOY_PYTHON=1 DEPLOY_GTK=1 GTK_DIR=gtk-4.0 DEPLOY_SDL=1 DEPLOY_VULKAN=1 DEPLOY_PIPEWIRE=1
|
||||
# Strace mode starts the programs to find what they dlopen: the game needs its game files.
|
||||
export STRACE_MODE=0
|
||||
# The AppDir first, the AppImage once the build host's NVIDIA driver is out of it (below).
|
||||
export OUTPUT_APPIMAGE=0
|
||||
# The online module (gpu/bbnet), when the build has it: named so that its libraries (protobuf,
|
||||
# OpenSSL, miniupnpc) come along; the runtime dlopens it at BB_NET_MODULE (below).
|
||||
net=$(ls "$root/out/gpu/libbbnet.so" 2>/dev/null || true)
|
||||
"$quick" \
|
||||
"$root/out/bb-probe" "$root/out/bb-gpu-capabilities" ${net:+"$net"} \
|
||||
"$(command -v python3)" "$(command -v vulkaninfo)" \
|
||||
/usr/lib/libgtk-4.so* /usr/lib/libadwaita-1.so* /usr/lib/libgirepository-*.so* \
|
||||
"$pydyn"/_ssl.*.so "$pydyn"/_hashlib.*.so "$pil"/_imaging.*.so "$pil"/_imagingft.*.so
|
||||
|
||||
# quick-sharun's OpenGL and Vulkan deployment copies every libEGL*/libGLX* and every ICD manifest
|
||||
# of the build host: on a host with NVIDIA's driver, its user-space libraries (one exact driver
|
||||
# version) and manifests went in, and the game loaded them next to the player's own driver
|
||||
# (DLSS failed with NGX 0xbad00012; another driver version would not match the kernel module).
|
||||
# NVIDIA's driver always comes from the host: launcher/bbport_vulkan.py points Vulkan at it.
|
||||
find "$appdir" \( -iname '*nvidia*' \( -name '*.so*' -o -name '*.json' \) -o -name 'libcuda.so*' \
|
||||
-o -name 'libnvcuvid.so*' \) -print -exec rm -f {} +
|
||||
# DEPLOY_PYTHON copies the build host's whole site-packages (scipy, Qt bindings, ... 431 MB on
|
||||
# the build PC). The launcher imports only PyGObject (gi, with pycairo) and Pillow.
|
||||
for site in "$appdir"/lib/python3*/site-packages; do
|
||||
find "$site" -mindepth 1 -maxdepth 1 ! -name gi ! -name cairo ! -name PIL \
|
||||
! -name 'PyGObject-*.dist-info' ! -name 'pycairo-*.dist-info' ! -name 'pillow-*.dist-info' \
|
||||
-exec rm -rf {} +
|
||||
done
|
||||
# DLSS (NVIDIA RTX), when tools/build_dlss.sh has built the bridge: it and NVIDIA's library, copied
|
||||
# after quick-sharun, which strips and patches every library in the AppDir. NVIDIA's DLSS library
|
||||
# is signed: once changed, NGX refused it (0xbad00012) and DLSS was unavailable in the AppImage.
|
||||
if [[ -f $root/out/libbbport_dlss.so ]]; then
|
||||
mkdir -p "$appdir/share/bbport/dlss"
|
||||
cp "$root"/out/libbbport_dlss.so "$root"/out/libnvidia-ngx-dlss.so.* \
|
||||
"$root"/out/NVIDIA-DLSS-LICENSE.txt "$appdir/share/bbport/dlss/"
|
||||
fi
|
||||
if [[ -n $net ]]; then
|
||||
module=$(find "$appdir/lib" -name libbbnet.so -print -quit)
|
||||
[[ -n $module ]] || { echo 'libbbnet.so is missing from the AppDir' >&2; exit 1; }
|
||||
mkdir -p "$d/net"
|
||||
ln -sf "$(realpath --relative-to="$d/net" "$module")" "$d/net/libbbnet.so"
|
||||
fi
|
||||
"$quick" --make-appimage
|
||||
|
||||
mkdir -p "$root/dist"
|
||||
mv -f "$work"/*.AppImage "$root/dist/Bloodborne-bbport-x86_64.AppImage"
|
||||
ls -lh "$root/dist/Bloodborne-bbport-x86_64.AppImage"
|
||||
@@ -12,7 +12,7 @@ let
|
||||
# FSR 4.1.1 models are extracted from AMD's DLLs: never in a public package. BB_PACKAGE_FSR411=1
|
||||
# (appimage.sh runs nix with --impure) bundles the local fsr4_411 for one's own devices.
|
||||
fsr411 = builtins.getEnv "BB_PACKAGE_FSR411" == "1";
|
||||
assetDirs = [ "scripts" "patches" "fsr4_shaders" "launcher" "tools/fsr4cap" ]
|
||||
assetDirs = [ "scripts" "patches" "fsr4_shaders" "launcher" "tools/fsr4cap" "assets" ]
|
||||
++ lib.optional fsr411 "fsr4_411";
|
||||
# Only what the package needs (the tree also holds builds, profiles and captures).
|
||||
wanted = [
|
||||
@@ -31,7 +31,9 @@ let
|
||||
in builtins.elem rel wanted || builtins.elem rel dlssFiles || isNgx rel
|
||||
|| lib.any (dir: lib.hasPrefix (dir + "/") rel) assetDirs;
|
||||
};
|
||||
python = pkgs.python3.withPackages (ps: [ ps.pygobject3 ]);
|
||||
python = pkgs.python3.withPackages (ps: [ ps.pygobject3 ps.pillow ]);
|
||||
# run.sh's Python: Pillow draws the keyboard's button prompts (scripts/menu_prompts.py).
|
||||
gamePython = pkgs.python3.withPackages (ps: [ ps.pillow ]);
|
||||
# FSR 4.1.1 from the user's own AMD DLL (the launcher's button, tools/fsr4cap/build_assets.sh):
|
||||
# its two tools built here, so that the package needs no compiler and no network for it. Proton
|
||||
# (GE-Proton 10+) and Steam's runtime come from the user's Steam. On NixOS the recording runs
|
||||
@@ -156,7 +158,8 @@ pkgs.stdenv.mkDerivation {
|
||||
d=$out/share/bbport
|
||||
mkdir -p $d/bin $out/bin
|
||||
cp run.sh $d/
|
||||
cp -r scripts patches fsr4_shaders launcher $d/
|
||||
cp -r scripts patches fsr4_shaders launcher assets $d/
|
||||
install -Dm644 ${pkgs.dejavu_fonts}/share/fonts/truetype/DejaVuSans-Bold.ttf $d/fonts/DejaVuSans-Bold.ttf
|
||||
mkdir -p $d/tools
|
||||
cp -r tools/fsr4cap $d/tools/
|
||||
rm -rf $d/tools/fsr4cap/__pycache__
|
||||
@@ -208,7 +211,7 @@ pkgs.stdenv.mkDerivation {
|
||||
"''${gappsWrapperArgs[@]}" \
|
||||
${common} --add-flags "$out/share/bbport/launcher/bbport_vulkan.py ${python}/bin/python3 $out/share/bbport/launcher/bbport_launcher.py" \
|
||||
--set BB_PREBUILT 1 \
|
||||
--set PYTHON ${pkgs.python3}/bin/python3 \
|
||||
--set PYTHON ${gamePython}/bin/python3 \
|
||||
--set BB_BUNDLED_VK_DRIVER_FILES ${icds} \
|
||||
--prefix PATH : ${lib.makeBinPath [ pkgs.bash pkgs.coreutils pkgs.util-linux pkgs.procps
|
||||
pkgs.gawk pkgs.gnused pkgs.gnugrep pkgs.spirv-tools
|
||||
@@ -218,7 +221,7 @@ pkgs.stdenv.mkDerivation {
|
||||
makeShellWrapper ${pkgs.python3}/bin/python3 $out/bin/bbport-game \
|
||||
${common} --add-flags "$out/share/bbport/launcher/bbport_vulkan.py ${pkgs.bash}/bin/bash $out/share/bbport/run.sh" \
|
||||
--set BB_PREBUILT 1 \
|
||||
--set PYTHON ${pkgs.python3}/bin/python3 \
|
||||
--set PYTHON ${gamePython}/bin/python3 \
|
||||
--set BB_BUNDLED_VK_DRIVER_FILES ${icds} \
|
||||
--prefix PATH : ${lib.makeBinPath [ pkgs.bash pkgs.coreutils pkgs.procps ]} \
|
||||
--run 'export BB_DATA_DIR=''${BB_DATA_DIR:-''${XDG_DATA_HOME:-$HOME/.local/share}/bbport}; mkdir -p "$BB_DATA_DIR"'
|
||||
|
||||
@@ -64,9 +64,13 @@ if [[ -z ${PYTHON:-} ]]; then echo 'Install Python 3 or set PYTHON.' >&2; exit 1
|
||||
game=${BB_GAME_DIR:-../CUSA03173}
|
||||
if [[ ! -f $game/eboot.bin ]]; then echo "No eboot.bin in $game (set BB_GAME_DIR)." >&2; exit 1; fi
|
||||
original_game=$game
|
||||
# BB_PROMPTS (playstation, xbox, switch, keyboard) and BB_GESTURES_NO_MOTION=1: the menu's button
|
||||
# prompts and the gestures menu, made from the game's files (scripts/menu_prompts.py).
|
||||
ui_mods=$("$PYTHON" scripts/menu_prompts.py "$game" --out "$out" --prompts "${BB_PROMPTS:-playstation}" \
|
||||
--gestures "${BB_GESTURES_NO_MOTION:-0}" --config "$BB_CONFIG")
|
||||
game=$("$PYTHON" scripts/mods.py "$game" --out "$out" \
|
||||
--mods-dir "${BB_MODS_DIR:-$data/mods}" --config "${BB_MODS_CONFIG:-$data/mods.json}" \
|
||||
--enabled "${BB_MODS_ENABLED:-1}")
|
||||
--enabled "${BB_MODS_ENABLED:-1}" --ui-mods "$ui_mods")
|
||||
# A private merged view lasts for this launch, including restarts. Cleanup only our own view.
|
||||
if [[ $game != "$(realpath "$original_game")" ]]; then
|
||||
mod_game=$game
|
||||
@@ -75,7 +79,21 @@ fi
|
||||
"$PYTHON" scripts/prepare.py "$game" --out "$out"
|
||||
"$PYTHON" scripts/link_libc.py "$game" --out "$out"
|
||||
"$PYTHON" scripts/link_modules.py "$game" --out "$out"
|
||||
"$PYTHON" scripts/content_profile.py "$game" --out "$out" --sku "${BB_CONTENT_SKU:-full}"
|
||||
# BB_DLC_DIR (the launcher's DLC folder): the dumped add-ons of this title, one folder each (e.g.
|
||||
# SPEXPANSIONDLC03). The runtime reports <user>/addcont/<title id>/<label> as installed (shadPS4's
|
||||
# layout), so the folder is linked there; a real folder already in its place is left alone.
|
||||
if [[ -n ${BB_DLC_DIR:-} && -d $BB_DLC_DIR ]]; then
|
||||
title=$("$PYTHON" -c 'import sys; sys.path.insert(0, "scripts"); from prepare import sfo
|
||||
print(sfo(open(sys.argv[1] + "/sce_sys/param.sfo", "rb").read()).get("TITLE_ID", ""))' "$game" 2>/dev/null) || title=
|
||||
addcont=${BB_USER_DIR:-$data/user}/addcont
|
||||
if [[ $title =~ ^[A-Z]{4}[0-9]{5}$ && ( ! -e $addcont/$title || -L $addcont/$title ) ]]; then
|
||||
mkdir -p "$addcont"
|
||||
ln -sfn "$(realpath "$BB_DLC_DIR")" "$addcont/$title"
|
||||
fi
|
||||
fi
|
||||
# BB_REGION=eu|na (the launcher's Region): the game's region for this launch, whatever the dump's
|
||||
# param.sfo says; it picks the regional text (Spanish from Spain or Latin American).
|
||||
"$PYTHON" scripts/content_profile.py "$game" --out "$out" --sku "${BB_CONTENT_SKU:-full}" --region "${BB_REGION:-}"
|
||||
# Sizes chosen below for the previous launch are recomputed after an in-game restart.
|
||||
if [[ ${BB_AUTO_RENDER_RES:-} == 1 ]]; then
|
||||
unset BB_RENDER_RES BB_OUTPUT_RES BB_AUTO_RENDER_RES
|
||||
@@ -121,6 +139,13 @@ elif [[ -n ${scaled_output:-} ]]; then
|
||||
export BB_DMEM_MB=${BB_DMEM_MB:-9152}
|
||||
echo "Output ${scaled_output}: scene ${scaled_render}, direct memory ${BB_DMEM_MB} MiB (live_resolution=1: live changes)"
|
||||
fi
|
||||
# An explicit render size above 1080p skips the scaled path above, but patches.py still grows the
|
||||
# graphics heap: the direct memory has to follow, or the game stops at start (exit 139).
|
||||
if [[ -z ${BB_DMEM_MB:-} && ${BB_RENDER_RES:-} =~ ^[[:space:]]*([0-9]+)[[:space:]]*[xX][[:space:]]*([0-9]+)[[:space:]]*$ ]] &&
|
||||
(( 10#${BASH_REMATCH[1]} * 10#${BASH_REMATCH[2]} > 1920 * 1080 )); then
|
||||
export BB_DMEM_MB=9152
|
||||
echo "Render ${BB_RENDER_RES}: direct memory ${BB_DMEM_MB} MiB"
|
||||
fi
|
||||
"$PYTHON" scripts/patches.py --out "$out" --fps "$fps" --extra "${BB_PATCHES:-}" --settings "$BB_CONFIG" --game-dir "$game" --render-res "${BB_RENDER_RES:-}" --output-res "${BB_OUTPUT_RES:-}" \
|
||||
--patches-dir "${BB_PATCHES_DIR:-$data/patches}" --patches-config "${BB_PATCHES_CONFIG:-$data/patches.json}"
|
||||
# Background upload of the game's GPU memory into VRAM ahead of use (BufferCache::Preupload):
|
||||
@@ -228,6 +253,22 @@ if [[ ${BB_ONLINE:-0} == 1 && -f scripts/online.py ]]; then
|
||||
SHADPS4_HTTP_HOST_OVERRIDES_JSON=$("$PYTHON" scripts/online.py overrides --user "${BB_USER_DIR:-$data/user}") || true
|
||||
export SHADPS4_HTTP_HOST_OVERRIDES_JSON
|
||||
fi
|
||||
# Trophies (scripts/trophies.py): the dump's list and icons in the user directory, for the
|
||||
# runtime's notices and the launcher; BB_TROPHY_KEY (the PS4's trophy key) decrypts the names.
|
||||
if trophies=$("$PYTHON" scripts/trophies.py prepare --game "$original_game" \
|
||||
--user "${BB_USER_DIR:-$data/user}" --language "${BB_LANGUAGE:-1}") && [[ -n $trophies ]]; then
|
||||
export BB_TROPHY_DIR=$trophies
|
||||
fi
|
||||
# Discord Rich Presence (launcher: Discord; BB_DISCORD=0 turns it off): $$ becomes the game
|
||||
# below (exec), and the helper ends with it. The game writes its area and co-op partners into
|
||||
# BB_PRESENCE_DIR (scripts/discord_presence.py), new for each start.
|
||||
if [[ ${BB_DISCORD:-1} != 0 ]]; then
|
||||
BB_PRESENCE_DIR=$(cd -- "$out" && pwd)/presence; export BB_PRESENCE_DIR
|
||||
rm -rf "$BB_PRESENCE_DIR" && mkdir -p "$BB_PRESENCE_DIR"
|
||||
presence=(--pid $$ --presence-dir "$BB_PRESENCE_DIR")
|
||||
[[ ${BB_ONLINE:-0} == 1 ]] && presence+=(--online)
|
||||
"$PYTHON" scripts/discord_presence.py "${presence[@]}" </dev/null &
|
||||
fi
|
||||
probe_args=("$out/boot-linked.bin" --content-profile "$out/content.bin" --patches "$out/patches.bin" --app0 "$game" --user "${BB_USER_DIR:-$data/user}" --timeout "${BB_TIMEOUT:-0}" "$@")
|
||||
if [[ -n ${mod_game:-} ]]; then
|
||||
"$probe" "${probe_args[@]}" &
|
||||
|
||||
@@ -1,16 +1,24 @@
|
||||
"""Prepare explicit offline AppContent metadata; SKU is a probe setting, not a license check."""
|
||||
"""Prepare explicit offline AppContent metadata; SKU is a probe setting, not a license check.
|
||||
|
||||
USER_DEFINED_PARAM_1 is Bloodborne's region: 2 the Americas (msg/na: Latin American Spanish), 13
|
||||
Europe (msg/eu: Spanish from Spain). --region eu|na sets it for this launch (BB_REGION, the
|
||||
launcher's Region), whatever the dump's param.sfo says."""
|
||||
import argparse
|
||||
import json
|
||||
import struct
|
||||
from pathlib import Path
|
||||
from prepare import sfo
|
||||
|
||||
REGIONS={'na':2,'eu':13}
|
||||
|
||||
def prepare(game, out, sku='full'):
|
||||
|
||||
def prepare(game, out, sku='full', region=''):
|
||||
values=sfo((game/'sce_sys/param.sfo').read_bytes())
|
||||
params=[values.get(f'USER_DEFINED_PARAM_{i}',0) for i in range(1,5)]
|
||||
if any(type(v) is not int or not 0<=v<=0xffffffff for v in params):
|
||||
raise ValueError('invalid user-defined parameter')
|
||||
if region:
|
||||
params[0]=REGIONS[region]
|
||||
profile=dict(title_id=values.get('TITLE_ID'),sku=sku,sku_source='explicit probe setting',
|
||||
user_params=params, mounted_addons=[],boot_attr=0)
|
||||
out.mkdir(parents=True,exist_ok=True)
|
||||
@@ -23,4 +31,5 @@ if __name__=='__main__':
|
||||
p.add_argument('game',type=Path)
|
||||
p.add_argument('--out',type=Path,default=Path(__file__).resolve().parent.parent/'out')
|
||||
p.add_argument('--sku',choices=['full','trial'],default='full')
|
||||
a=p.parse_args();prepare(a.game,a.out,a.sku)
|
||||
p.add_argument('--region',choices=['','eu','na'],default='')
|
||||
a=p.parse_args();prepare(a.game,a.out,a.sku,a.region)
|
||||
@@ -0,0 +1,207 @@
|
||||
#!/usr/bin/env python3
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
"""Discord Rich Presence while the game runs: "Playing Bloodborne", the area the player is in,
|
||||
online or offline or the co-op partners, and the time played. run.sh starts it next to the game
|
||||
(BB_DISCORD=0 turns it off); it ends with the game.
|
||||
|
||||
The game writes what the helper shows into BB_PRESENCE_DIR (--presence-dir): `area`, the map of
|
||||
the player's area (src/runtime_file.c), and `coop`, the online IDs of the players connected with
|
||||
this one, one per line (the online module, np_signaling_state.cpp).
|
||||
|
||||
Discord's local IPC: a Unix socket discord-ipc-N in the runtime directory (also inside the
|
||||
Flatpak, Snap and Vesktop sandboxes' directories). Frames are a little-endian op code and length,
|
||||
then JSON: a handshake (op 0) with the application id, then SET_ACTIVITY commands (op 1).
|
||||
Without Discord running the helper keeps looking for it and does nothing else.
|
||||
"""
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import socket
|
||||
import struct
|
||||
import time
|
||||
import uuid
|
||||
|
||||
# The "Bloodborne" application on Discord's developer portal (its icon is the art asset
|
||||
# "bloodborne").
|
||||
CLIENT_ID = '1558176258121269389'
|
||||
HANDSHAKE, FRAME, CLOSE = 0, 1, 2
|
||||
# The areas by map (map/mapstudio/mXX_YY_...): the names of Smithbox's Bloodborne map aliases
|
||||
# (github.com/vawser/Smithbox, MIT). The Chalice Dungeons (m29) are one name.
|
||||
AREAS = {
|
||||
'm21_00': "Hunter's Dream",
|
||||
'm21_01': 'Abandoned Old Workshop',
|
||||
'm22_00': 'Hemwick Charnel Lane',
|
||||
'm23_00': 'Old Yharnam',
|
||||
'm24_00': 'Cathedral Ward',
|
||||
'm24_01': 'Central Yharnam',
|
||||
'm24_02': 'Upper Cathedral Ward',
|
||||
'm25_00': 'Forsaken Castle Cainhurst',
|
||||
'm26_00': 'Nightmare of Mensis',
|
||||
'm27_00': 'Forbidden Woods',
|
||||
'm28_00': "Yahar'gul, Unseen Village",
|
||||
'm32_00': 'Byrgenwerth',
|
||||
'm33_00': 'Nightmare Frontier',
|
||||
'm34_00': "Hunter's Nightmare",
|
||||
'm35_00': 'Research Hall',
|
||||
'm36_00': 'Fishing Hamlet',
|
||||
}
|
||||
CHALICE_DUNGEON = 'Chalice Dungeon'
|
||||
# Where Discord clients put their socket, relative to the runtime/temporary directory.
|
||||
SUBDIRS = ('', 'app/com.discordapp.Discord', 'app/com.discordapp.DiscordCanary',
|
||||
'app/dev.vencord.Vesktop', '.flatpak/com.discordapp.Discord/xdg-run',
|
||||
'.flatpak/dev.vencord.Vesktop/xdg-run', 'snap.discord', 'snap.discord-canary')
|
||||
|
||||
|
||||
def socket_paths(env=os.environ):
|
||||
bases = [env.get(name) for name in ('XDG_RUNTIME_DIR', 'TMPDIR', 'TMP', 'TEMP')] + ['/tmp']
|
||||
for base in dict.fromkeys(b for b in bases if b):
|
||||
for sub in SUBDIRS:
|
||||
for n in range(10):
|
||||
yield Path(base) / sub / f'discord-ipc-{n}'
|
||||
|
||||
|
||||
def frame(op, payload):
|
||||
data = json.dumps(payload).encode()
|
||||
return struct.pack('<II', op, len(data)) + data
|
||||
|
||||
|
||||
def read_frame(sock):
|
||||
header = b''
|
||||
while len(header) < 8:
|
||||
chunk = sock.recv(8 - len(header))
|
||||
if not chunk:
|
||||
raise ConnectionError('Discord closed the connection')
|
||||
header += chunk
|
||||
op, length = struct.unpack('<II', header)
|
||||
body = b''
|
||||
while len(body) < length:
|
||||
chunk = sock.recv(length - len(body))
|
||||
if not chunk:
|
||||
raise ConnectionError('Discord closed the connection')
|
||||
body += chunk
|
||||
return op, json.loads(body or b'{}')
|
||||
|
||||
|
||||
def connect(client_id, env=os.environ):
|
||||
"""A connected, handshaken socket to the first Discord client found, or None."""
|
||||
for path in socket_paths(env):
|
||||
if not path.is_socket():
|
||||
continue
|
||||
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
||||
sock.settimeout(5)
|
||||
try:
|
||||
sock.connect(str(path))
|
||||
sock.sendall(frame(HANDSHAKE, {'v': 1, 'client_id': client_id}))
|
||||
op, reply = read_frame(sock)
|
||||
if op == FRAME and reply.get('evt') == 'READY':
|
||||
return sock
|
||||
except (OSError, ValueError, ConnectionError):
|
||||
pass
|
||||
sock.close()
|
||||
return None
|
||||
|
||||
|
||||
def area_name(map_id):
|
||||
"""The area of a map ("m24_01"), or None for an unknown one."""
|
||||
map_id = (map_id or '').strip().lower()
|
||||
if map_id.startswith('m29_'):
|
||||
return CHALICE_DUNGEON
|
||||
return AREAS.get(map_id)
|
||||
|
||||
|
||||
def read_state(directory):
|
||||
"""(map, co-op partners) from the game's presence directory; (None, []) without it."""
|
||||
if directory is None:
|
||||
return None, []
|
||||
directory = Path(directory)
|
||||
try:
|
||||
area = (directory / 'area').read_text(encoding='utf-8').strip() or None
|
||||
except OSError:
|
||||
area = None
|
||||
try:
|
||||
lines = (directory / 'coop').read_text(encoding='utf-8', errors='replace').splitlines()
|
||||
except OSError:
|
||||
lines = []
|
||||
return area, [line.strip() for line in lines if line.strip()]
|
||||
|
||||
|
||||
def activity(online, start, area=None, partners=()):
|
||||
"""The area on the first line and the session on the second: "Co-op with <partners>",
|
||||
"Online" or "Offline". Before the game is in an area, the session alone."""
|
||||
session = (f'Co-op with {", ".join(partners)}' if partners
|
||||
else 'Online' if online else 'Offline')
|
||||
value = {
|
||||
'details': area_name(area) or session,
|
||||
'timestamps': {'start': start},
|
||||
'assets': {'large_image': 'bloodborne', 'large_text': 'Bloodborne · native Linux port'},
|
||||
}
|
||||
if area_name(area):
|
||||
value['state'] = session
|
||||
return value
|
||||
|
||||
|
||||
def set_activity(sock, pid, value):
|
||||
sock.sendall(frame(FRAME, {'cmd': 'SET_ACTIVITY', 'args': {'pid': pid, 'activity': value},
|
||||
'nonce': str(uuid.uuid4())}))
|
||||
op, reply = read_frame(sock)
|
||||
if reply.get('evt') == 'ERROR':
|
||||
raise ConnectionError(reply.get('data', {}).get('message', 'SET_ACTIVITY failed'))
|
||||
|
||||
|
||||
def alive(pid):
|
||||
try:
|
||||
os.kill(pid, 0)
|
||||
return True
|
||||
except ProcessLookupError:
|
||||
return False
|
||||
except PermissionError:
|
||||
return True
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__.split('\n')[0])
|
||||
parser.add_argument('--pid', type=int, required=True, help='the game process: ends with it')
|
||||
parser.add_argument('--online', action='store_true')
|
||||
parser.add_argument('--presence-dir', help="the game's area and co-op partners")
|
||||
parser.add_argument('--retry', type=float, default=15.0, help='seconds between attempts')
|
||||
a = parser.parse_args()
|
||||
start = int(time.time())
|
||||
sock = None
|
||||
shown = None
|
||||
while alive(a.pid):
|
||||
value = activity(a.online, start, *read_state(a.presence_dir))
|
||||
if sock is None:
|
||||
sock = connect(CLIENT_ID)
|
||||
shown = None
|
||||
if sock is not None and value != shown:
|
||||
# Discord takes a few updates a minute; the area changes at loading screens only.
|
||||
try:
|
||||
set_activity(sock, a.pid, value)
|
||||
if shown is None:
|
||||
print('Discord: Rich Presence on', flush=True)
|
||||
shown = value
|
||||
except (OSError, ConnectionError, ValueError) as error:
|
||||
print(f'Discord: {error}', flush=True)
|
||||
sock.close()
|
||||
sock = None
|
||||
elif sock is not None:
|
||||
# Discord closed or restarted: connect again on the next round.
|
||||
try:
|
||||
sock.settimeout(0)
|
||||
if sock.recv(1, socket.MSG_PEEK) == b'':
|
||||
raise ConnectionError
|
||||
except BlockingIOError:
|
||||
pass
|
||||
except (OSError, ConnectionError):
|
||||
sock.close()
|
||||
sock = None
|
||||
if sock is not None:
|
||||
sock.settimeout(5)
|
||||
time.sleep(a.retry if sock is None else 2.0)
|
||||
if sock is not None:
|
||||
sock.close()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env python3
|
||||
"""The game files bbport runs: Bloodborne with the 1.09 update merged in. The retail releases (the
|
||||
US CUSA00900, the European Game of the Year edition CUSA03173, the Asian The Old Hunters Edition
|
||||
CUSA03023, ...) share the same 1.09 executable.
|
||||
"""The game files bbport runs: Bloodborne with the 1.09 update merged in. The US release
|
||||
(CUSA00900) and the European Game of the Year edition (CUSA03173) have the same 1.09 executable;
|
||||
the other retail releases are expected to, and the image hash decides (docs/COMPATIBILITY.md).
|
||||
|
||||
Other versions start and then fail inside the game's code (the base game 1.00 faults at guest
|
||||
offset 0x20348b8): hooks and patches use the addresses of this one executable. The check compares
|
||||
@@ -19,8 +19,8 @@ import sys
|
||||
import zlib
|
||||
from pathlib import Path
|
||||
|
||||
# The retail releases (the list of content IDs from PR #117): Bloodborne, then the editions with
|
||||
# The Old Hunters. The executable decides in the end: SUPPORTED_IMAGE.
|
||||
# The retail releases (content IDs listed in deadinside28/bloodborne_pc PR #117): Bloodborne, then
|
||||
# the editions with The Old Hunters. The executable decides in the end: SUPPORTED_IMAGE.
|
||||
SUPPORTED_TITLES = ('CUSA00900', 'CUSA00207', 'CUSA00208', 'CUSA00299', 'CUSA01363', # US EU UK JP AS
|
||||
'CUSA03179', 'CUSA03173', 'CUSA03014', 'CUSA03023') # US EU JP AS
|
||||
SUPPORTED_VERSION = '01.09'
|
||||
|
||||
@@ -0,0 +1,294 @@
|
||||
#!/usr/bin/env python3
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
"""Button prompts and the gestures menu for this launch, made from the player's own game files.
|
||||
|
||||
menu_prompts.py <game> --out <out> --prompts playstation|xbox|switch|keyboard
|
||||
[--gestures 0|1] [--config bbport.ini]
|
||||
|
||||
prints the folders (os.pathsep-separated) that mods.py lays under the mods (--ui-mods):
|
||||
- Xbox and Switch: the icons of Dommo's "DS3 Style" prompts (Nexus, Bloodborne mod 30) from
|
||||
assets/menu_prompts, put into the game's menu/common.tpf.dcx.
|
||||
- keyboard: icons drawn for the keys bbport.ini binds (key.<input>=, else the defaults); needs
|
||||
Pillow. Which textures to draw (the KG_* icons and three atlas spots) follows Abken's KBM
|
||||
Icons Generator (Nexus, Bloodborne mod 537); nothing of it is used.
|
||||
- --gestures 1: Dommo's "Stripped down gesture menu" (no motion-control icons), as edits of the
|
||||
game's menu/gesturetop.gfx.
|
||||
Only the prompt textures change; every other texture is the game's, untouched. Results are
|
||||
kept in <out>/menu-prompts, keyed by their inputs (game file, keys, these scripts and assets), so
|
||||
a launch with the same choice is instant.
|
||||
"""
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import sys
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
ASSETS = ROOT / 'assets' / 'menu_prompts'
|
||||
|
||||
# The prompt textures of menu/common.tpf.dcx and the PS4 input each shows (KG_OK is circle and
|
||||
# KG_Cancel cross, Japanese style); clusters list (up, down, left, right).
|
||||
ICONS = {
|
||||
'KG_OK': 'circle', 'KG_Cancel': 'cross', 'KG_R_U': 'triangle', 'KG_R_L': 'square',
|
||||
'KG_L1': 'l1', 'KG_R1': 'r1', 'KG_L2': 'l2', 'KG_R2': 'r2', 'KG_L3': 'l3', 'KG_R3': 'r3',
|
||||
'KG_Start': 'options', 'KG_TP_L': 'touchpad', 'KG_TP_R': 'touchpad_right',
|
||||
'KG_LS': ('move_up', 'move_down', 'move_left', 'move_right'),
|
||||
'KG_RS': ('look_up', 'look_down', 'look_left', 'look_right'),
|
||||
'KG_L_U': 'up', 'KG_L_D': 'down', 'KG_L_LR': (None, None, 'left', 'right'),
|
||||
'KG_L_UD': ('up', 'down', None, None), 'KG_L_UDLR': ('up', 'down', 'left', 'right'),
|
||||
}
|
||||
# The shared UI atlas also has big circle, cross and L3 prompts (26x26 at these spots); the
|
||||
# block-aligned strip around them is all that is replaced in it.
|
||||
ATLAS = 'MENU_Common_00091'
|
||||
ATLAS_WIDTH = 1024
|
||||
ATLAS_STRIP = (660, 120, 92, 32)
|
||||
ATLAS_GLYPHS = {'KG_OK': (661, 123), 'KG_Cancel': (694, 123), 'KG_L3': (726, 123)}
|
||||
# The keyboard's default keys (src/runtime_pad.c bind_defaults; the launcher's CONTROLS).
|
||||
DEFAULT_KEYS = {
|
||||
'cross': 'Space', 'circle': 'Left Shift', 'square': 'E', 'triangle': 'Q', 'l1': '1',
|
||||
'r1': '3', 'l2': 'R', 'r2': 'F', 'l3': 'Z', 'r3': 'C', 'options': 'Return',
|
||||
'touchpad': 'Tab', 'touchpad_right': 'Backspace', 'up': 'I', 'down': 'K', 'left': 'J',
|
||||
'right': 'L', 'move_up': 'W', 'move_down': 'S', 'move_left': 'A', 'move_right': 'D',
|
||||
'look_up': 'Up', 'look_down': 'Down', 'look_left': 'Left', 'look_right': 'Right',
|
||||
}
|
||||
PROMPTS = ('playstation', 'xbox', 'switch', 'keyboard')
|
||||
# DejaVu Sans Bold: next to the scripts in packaged builds (packaging/appimage-sharun.sh), else
|
||||
# the system's; Pillow's own font otherwise (it lacks the arrows).
|
||||
FONTS = (ROOT / 'fonts' / 'DejaVuSans-Bold.ttf', Path('/usr/share/fonts/TTF/DejaVuSans-Bold.ttf'),
|
||||
Path('/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf'),
|
||||
Path('/usr/share/fonts/dejavu/DejaVuSans-Bold.ttf'),
|
||||
Path('/usr/share/fonts/dejavu-sans-fonts/DejaVuSans-Bold.ttf'))
|
||||
# SDL key names (as bbport.ini has them) shortened to fit a key cap.
|
||||
KEY_LABELS = {
|
||||
'space': 'Space', 'left shift': 'Shift', 'right shift': 'Shift', 'left ctrl': 'Ctrl',
|
||||
'right ctrl': 'Ctrl', 'left alt': 'Alt', 'right alt': 'AltGr', 'return': 'Enter',
|
||||
'keypad enter': 'Enter', 'backspace': '⌫', 'tab': 'Tab', 'escape': 'Esc', 'up': '↑',
|
||||
'down': '↓', 'left': '←', 'right': '→', 'capslock': 'Caps', 'delete': 'Del',
|
||||
'insert': 'Ins', 'home': 'Home', 'end': 'End', 'pageup': 'PgUp', 'pagedown': 'PgDn',
|
||||
}
|
||||
|
||||
|
||||
def prompts_for(pad_type):
|
||||
"""The prompts for an SDL gamepad type (xbox360, xboxone, ps4, ps5, switchpro, joyconpair,
|
||||
...; "" for a generic gamepad, None without one: the keyboard's)."""
|
||||
if pad_type is None:
|
||||
return 'keyboard'
|
||||
kind = pad_type.casefold()
|
||||
if kind.startswith('ps'):
|
||||
return 'playstation'
|
||||
if kind.startswith(('switch', 'joycon', 'gamecube')):
|
||||
return 'switch'
|
||||
# Xbox pads, the Steam Deck and most generic PC gamepads are labelled A/B/X/Y.
|
||||
return 'xbox'
|
||||
|
||||
|
||||
def key_bindings(config):
|
||||
"""input -> its first key (SDL name; '' unbound), from bbport.ini over the defaults."""
|
||||
keys = dict(DEFAULT_KEYS)
|
||||
try:
|
||||
lines = Path(config).read_text().splitlines() if config else []
|
||||
except OSError:
|
||||
lines = []
|
||||
for line in lines:
|
||||
name, _, value = line.partition('=')
|
||||
name = name.strip()
|
||||
if name.startswith('key.') and name[4:] in keys:
|
||||
keys[name[4:]] = value.split(',')[0].strip()
|
||||
return keys
|
||||
|
||||
|
||||
def key_label(key):
|
||||
if not key:
|
||||
return '–'
|
||||
label = KEY_LABELS.get(key.casefold())
|
||||
if label:
|
||||
return label
|
||||
if key.casefold().startswith('keypad '):
|
||||
return 'Num' + key[7:]
|
||||
return key.upper() if len(key) == 1 else key[:5]
|
||||
|
||||
|
||||
def _font(size):
|
||||
from PIL import ImageFont
|
||||
for path in FONTS:
|
||||
if path.is_file():
|
||||
return ImageFont.truetype(str(path), size)
|
||||
return ImageFont.load_default(size)
|
||||
|
||||
|
||||
def _cap(draw, box, label, scale):
|
||||
"""One key cap with its label, fitted into box (x0, y0, x1, y1)."""
|
||||
x0, y0, x1, y1 = box
|
||||
draw.rounded_rectangle(box, radius=max(2, (y1 - y0) // 5), fill=(58, 58, 58, 238),
|
||||
outline=(170, 170, 170, 220), width=max(1, scale))
|
||||
size = int((y1 - y0) * 0.7)
|
||||
while size > 4:
|
||||
font = _font(size)
|
||||
left, top, right, bottom = draw.textbbox((0, 0), label, font=font)
|
||||
if right - left <= (x1 - x0) - 4 * scale:
|
||||
break
|
||||
size -= 1
|
||||
draw.text(((x0 + x1 - (right - left)) / 2 - left, (y0 + y1 - (bottom - top)) / 2 - top), label,
|
||||
font=font, fill=(216, 214, 210, 255))
|
||||
|
||||
|
||||
def key_icon(keys, inputs):
|
||||
"""A 32x32 prompt: one key cap, or a cluster (up, down, left, right; None left out)."""
|
||||
from PIL import Image, ImageDraw
|
||||
scale = 4
|
||||
size = 32 * scale
|
||||
image = Image.new('RGBA', (size, size), (0, 0, 0, 0))
|
||||
draw = ImageDraw.Draw(image)
|
||||
if isinstance(inputs, str):
|
||||
_cap(draw, (0, 3 * scale, size, size - 3 * scale), key_label(keys[inputs]), scale)
|
||||
else:
|
||||
up, down, left, right = inputs
|
||||
if up and down and left and right: # a T: up above, left/down/right below
|
||||
w, h = (size - 2 * scale) // 3, 15 * scale
|
||||
spots = {'up': ((size - w) // 2, scale), 'left': (0, 16 * scale),
|
||||
'down': ((size - w) // 2, 16 * scale), 'right': (size - w, 16 * scale)}
|
||||
elif left and right: # a pair side by side
|
||||
w, h = 16 * scale, 24 * scale
|
||||
spots = {'left': (0, 4 * scale), 'right': (16 * scale, 4 * scale)}
|
||||
else: # a pair one above the other
|
||||
w, h = 22 * scale, 16 * scale
|
||||
spots = {'up': (5 * scale, 0), 'down': (5 * scale, 16 * scale)}
|
||||
for name, which in zip(('up', 'down', 'left', 'right'), (up, down, left, right)):
|
||||
if which:
|
||||
x, y = spots[name]
|
||||
_cap(draw, (x, y, x + w - scale // 2, y + h - scale // 2), key_label(keys[which]), scale)
|
||||
return image.resize((32, 32), Image.LANCZOS)
|
||||
|
||||
|
||||
def keyboard_assets(keys):
|
||||
"""{slot: RGBA 32x32} and the atlas strip (at the game's scale) for keyboard prompts."""
|
||||
from PIL import Image
|
||||
icons = {slot: key_icon(keys, inputs) for slot, inputs in ICONS.items()}
|
||||
x0, y0, w, h = ATLAS_STRIP
|
||||
strip = Image.new('RGBA', (w, h), (0, 0, 0, 0))
|
||||
for slot, (x, y) in ATLAS_GLYPHS.items():
|
||||
strip.alpha_composite(icons[slot].resize((26, 26), Image.LANCZOS), (x - x0, y - y0))
|
||||
return icons, strip
|
||||
|
||||
|
||||
def build_common(base, prompts, keys):
|
||||
"""The game's menu/common.tpf.dcx (bytes `base`) with `prompts` (xbox, switch, keyboard)."""
|
||||
import menu_textures as mt
|
||||
tpf, header = mt.read_dcx(base)
|
||||
tpf = bytearray(tpf)
|
||||
textures = mt.textures(tpf)
|
||||
if prompts == 'keyboard':
|
||||
icons, strip = keyboard_assets(keys)
|
||||
blocks = {slot: mt.encode(image) for slot, image in icons.items()}
|
||||
strip = mt.encode(strip)
|
||||
else:
|
||||
folder = ASSETS / prompts
|
||||
blocks = {slot: mt.dds_blocks((folder / f'{slot}.dds').read_bytes()) for slot in ICONS}
|
||||
strip = mt.dds_blocks((folder / 'atlas.dds').read_bytes())
|
||||
for slot, new in blocks.items():
|
||||
texture = textures[slot]
|
||||
if len(new) != (texture['width'] // 4) * (texture['height'] // 4):
|
||||
raise ValueError(f'{slot}: unexpected size')
|
||||
mt.put_blocks(tpf, texture, dict(enumerate(new)))
|
||||
atlas = textures[ATLAS]
|
||||
if atlas['width'] != ATLAS_WIDTH:
|
||||
raise ValueError(f'{ATLAS} is {atlas["width"]} wide, expected {ATLAS_WIDTH}')
|
||||
which, rect = mt.rect_blocks(atlas, *ATLAS_STRIP)
|
||||
if rect != ATLAS_STRIP or len(which) != len(strip):
|
||||
raise ValueError('unexpected atlas strip')
|
||||
mt.put_blocks(tpf, atlas, dict(zip(which, strip)))
|
||||
return mt.write_dcx(bytes(tpf), header)
|
||||
|
||||
|
||||
def apply_gestures(original):
|
||||
"""Dommo's gestures menu: the edits of assets/menu_prompts/gesturetop.json."""
|
||||
patch = json.loads((ASSETS / 'gesturetop.json').read_text())
|
||||
if hashlib.sha256(original).hexdigest() != patch['original_sha256']:
|
||||
raise ValueError('menu/gesturetop.gfx is not the one the gestures patch was made for')
|
||||
out, at = bytearray(), 0
|
||||
for offset, length, data in patch['edits']:
|
||||
out += original[at:offset] + bytes.fromhex(data)
|
||||
at = offset + length
|
||||
out += original[at:]
|
||||
if hashlib.sha256(out).hexdigest() != patch['result_sha256']:
|
||||
raise ValueError('gestures patch gave an unexpected result')
|
||||
return bytes(out)
|
||||
|
||||
|
||||
def game_file(game, relative):
|
||||
"""A file under the game's dvdroot_ps4/menu, whatever the case of its name."""
|
||||
folder = Path(game) / 'dvdroot_ps4'
|
||||
for part in ('menu', relative):
|
||||
match = [p for p in folder.iterdir() if p.name.casefold() == part] if folder.is_dir() else []
|
||||
if not match:
|
||||
raise FileNotFoundError(f'{relative} not found in the game')
|
||||
folder = match[0]
|
||||
return folder
|
||||
|
||||
|
||||
def sources(*extra):
|
||||
"""A hash of what the results are made from: these scripts and the files used."""
|
||||
digest = hashlib.sha256()
|
||||
for path in (Path(__file__), Path(__file__).with_name('menu_textures.py'), *extra):
|
||||
digest.update(path.read_bytes())
|
||||
return digest.hexdigest()
|
||||
|
||||
|
||||
def cached(out, key, relative, make):
|
||||
"""<out>/menu-prompts/<key>/dvdroot_ps4/menu/<relative>, made once by make()."""
|
||||
folder = Path(out) / 'menu-prompts' / key
|
||||
target = folder / 'dvdroot_ps4' / 'menu' / relative
|
||||
if not target.is_file():
|
||||
target.parent.mkdir(parents=True, exist_ok=True)
|
||||
partial = target.with_name(target.name + '.part')
|
||||
partial.write_bytes(make())
|
||||
os.replace(partial, target)
|
||||
return folder
|
||||
|
||||
|
||||
def main(argv=None):
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument('game', type=Path)
|
||||
parser.add_argument('--out', required=True, type=Path)
|
||||
parser.add_argument('--prompts', choices=PROMPTS, default='playstation')
|
||||
parser.add_argument('--gestures', choices=('0', '1'), default='0')
|
||||
parser.add_argument('--config')
|
||||
args = parser.parse_args(argv)
|
||||
folders, used = [], set()
|
||||
if args.prompts != 'playstation':
|
||||
base = game_file(args.game, 'common.tpf.dcx')
|
||||
stat = base.stat()
|
||||
keys = key_bindings(args.config) if args.prompts == 'keyboard' else {}
|
||||
labels = sorted((name, key_label(key)) for name, key in keys.items())
|
||||
used_assets = sorted((ASSETS / args.prompts).glob('*.dds')) if args.prompts != 'keyboard' else []
|
||||
key = hashlib.sha256(json.dumps([sources(*used_assets), args.prompts, labels, str(base),
|
||||
stat.st_size, stat.st_mtime_ns]).encode()).hexdigest()[:16]
|
||||
folders.append(cached(args.out, f'prompts-{key}', 'common.tpf.dcx',
|
||||
lambda: build_common(base.read_bytes(), args.prompts, keys)))
|
||||
used.add(f'prompts-{key}')
|
||||
if args.gestures == '1':
|
||||
base = game_file(args.game, 'gesturetop.gfx')
|
||||
stat = base.stat()
|
||||
key = hashlib.sha256(json.dumps([sources(ASSETS / 'gesturetop.json'), str(base), stat.st_size,
|
||||
stat.st_mtime_ns]).encode()).hexdigest()[:16]
|
||||
folders.append(cached(args.out, f'gestures-{key}', 'gesturetop.gfx',
|
||||
lambda: apply_gestures(base.read_bytes())))
|
||||
used.add(f'gestures-{key}')
|
||||
# Older results (other keys, other game folders) are dropped.
|
||||
cache = Path(args.out) / 'menu-prompts'
|
||||
if cache.is_dir():
|
||||
for entry in cache.iterdir():
|
||||
if entry.name not in used:
|
||||
shutil.rmtree(entry, ignore_errors=True)
|
||||
print(os.pathsep.join(str(f) for f in folders))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||||
try:
|
||||
main()
|
||||
except (OSError, ValueError) as error:
|
||||
print(f'Button prompts: {error}; the game\'s own are used', file=sys.stderr)
|
||||
print('')
|
||||
@@ -0,0 +1,232 @@
|
||||
#!/usr/bin/env python3
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
"""Textures of the game's menu archives (menu/*.tpf.dcx), read and replaced in place.
|
||||
|
||||
A .tpf.dcx is a zlib-compressed (DCX "DFLT") TPF: a list of textures, here PS4 ones. Their
|
||||
data is the GPU's tiled layout: BC blocks in 8x8-block tiles, Morton order inside a tile.
|
||||
Replacing a texture keeps its size, so the TPF's offsets stay as they are.
|
||||
|
||||
Encoding is BC7 mode 6 (one RGBA line per 4x4 block, 16 levels), plenty for the button prompts
|
||||
this is used for; plain Python, so packaged builds need nothing more. Decoding (tools, tests)
|
||||
goes through Pillow's BC7 decoder."""
|
||||
import io
|
||||
import struct
|
||||
import zlib
|
||||
|
||||
BC7 = 98 # DXGI_FORMAT_BC7_UNORM
|
||||
BLOCK = 16 # bytes per BC7 block
|
||||
DDS_HEADER = 148 # DDS magic, header and DX10 header before the blocks
|
||||
|
||||
|
||||
def read_dcx(data):
|
||||
"""(TPF bytes, DCX header) of a DCX "DFLT" archive."""
|
||||
if data[:4] != b'DCX\0' or data[0x28:0x2c] != b'DFLT':
|
||||
raise ValueError('not a DCX DFLT archive')
|
||||
size, = struct.unpack_from('>I', data, 0x1c)
|
||||
tpf = zlib.decompress(data[0x4c:])
|
||||
if len(tpf) != size:
|
||||
raise ValueError('DCX size mismatch')
|
||||
return tpf, data[:0x4c]
|
||||
|
||||
|
||||
def write_dcx(tpf, header):
|
||||
packed = zlib.compress(tpf, 9)
|
||||
header = bytearray(header)
|
||||
struct.pack_into('>II', header, 0x1c, len(tpf), len(packed))
|
||||
return bytes(header) + packed
|
||||
|
||||
|
||||
def textures(tpf):
|
||||
"""name -> dict(offset, size, width, height, dxgi) of a PS4 TPF."""
|
||||
if tpf[:4] != b'TPF\0' or tpf[12] != 4:
|
||||
raise ValueError('not a PS4 TPF')
|
||||
count, = struct.unpack_from('<I', tpf, 8)
|
||||
utf16 = tpf[14] == 1
|
||||
out = {}
|
||||
for i in range(count):
|
||||
offset, size, _fmt, _type, _mips, _flags, width, height, _u1, _u2, name_at, _f2, dxgi = \
|
||||
struct.unpack_from('<IIBBBBHHiiiii', tpf, 16 + i * 0x24)
|
||||
if utf16:
|
||||
end = name_at
|
||||
while tpf[end:end + 2] != b'\0\0':
|
||||
end += 2
|
||||
name = tpf[name_at:end].decode('utf-16-le')
|
||||
else:
|
||||
name = tpf[name_at:tpf.index(b'\0', name_at)].decode('shift_jis')
|
||||
out[name] = dict(offset=offset, size=size, width=width, height=height, dxgi=dxgi)
|
||||
return out
|
||||
|
||||
|
||||
def tile_order(blocks_x, blocks_y):
|
||||
"""For each block of the tiled data, in order, its row-major index (None: padding)."""
|
||||
order = []
|
||||
for ty in range((blocks_y + 7) // 8):
|
||||
for tx in range((blocks_x + 7) // 8):
|
||||
for t in range(64):
|
||||
x = tx * 8 + ((t & 1) | (t >> 1 & 2) | (t >> 2 & 4))
|
||||
y = ty * 8 + ((t >> 1 & 1) | (t >> 2 & 2) | (t >> 3 & 4))
|
||||
order.append(y * blocks_x + x if x < blocks_x and y < blocks_y else None)
|
||||
return order
|
||||
|
||||
|
||||
def _check(texture):
|
||||
if texture['dxgi'] != BC7:
|
||||
raise ValueError(f"DXGI format {texture['dxgi']}, expected BC7")
|
||||
return texture['width'] // 4, texture['height'] // 4
|
||||
|
||||
|
||||
def blocks(tpf, texture):
|
||||
"""The texture's BC7 blocks, row-major: a list of 16-byte strings."""
|
||||
bx, by = _check(texture)
|
||||
out = [b''] * (bx * by)
|
||||
at = texture['offset']
|
||||
for slot, index in enumerate(tile_order(bx, by)):
|
||||
if index is not None:
|
||||
out[index] = bytes(tpf[at + slot * BLOCK:at + (slot + 1) * BLOCK])
|
||||
return out
|
||||
|
||||
|
||||
def put_blocks(tpf, texture, new):
|
||||
"""Writes BC7 blocks into a bytearray TPF: `new` maps row-major indices to 16 bytes."""
|
||||
bx, by = _check(texture)
|
||||
at = texture['offset']
|
||||
for slot, index in enumerate(tile_order(bx, by)):
|
||||
if index in new:
|
||||
tpf[at + slot * BLOCK:at + (slot + 1) * BLOCK] = new[index]
|
||||
|
||||
|
||||
def dds(width, height, row_blocks):
|
||||
"""A DDS file (DX10 header, BC7) of row-major blocks."""
|
||||
data = b''.join(row_blocks)
|
||||
header = struct.pack('<4sIIIIIII44sIII20sIIIII', b'DDS ', 124, 0x81007, height, width,
|
||||
len(data), 0, 1, b'\0' * 44, 32, 4, 0x30315844, b'\0' * 20, 0x1000,
|
||||
0, 0, 0, 0)
|
||||
return header + struct.pack('<IIIII', BC7, 3, 0, 1, 0) + data
|
||||
|
||||
|
||||
def dds_blocks(data):
|
||||
"""The row-major blocks of a DDS file written by dds()."""
|
||||
data = data[DDS_HEADER:]
|
||||
return [data[i:i + BLOCK] for i in range(0, len(data), BLOCK)]
|
||||
|
||||
|
||||
def decode(width, height, row_blocks):
|
||||
"""RGBA Pillow image of row-major BC7 blocks."""
|
||||
from PIL import Image
|
||||
return Image.open(io.BytesIO(dds(width, height, row_blocks))).convert('RGBA')
|
||||
|
||||
|
||||
_WEIGHTS = (0, 4, 9, 13, 17, 21, 26, 30, 34, 38, 43, 47, 51, 55, 60, 64)
|
||||
|
||||
|
||||
def _principal_axis(pixels, mean):
|
||||
cov = [[0.0] * 4 for _ in range(4)]
|
||||
for p in pixels:
|
||||
d = [p[c] - mean[c] for c in range(4)]
|
||||
for i in range(4):
|
||||
for j in range(4):
|
||||
cov[i][j] += d[i] * d[j]
|
||||
axis = [1.0, 1.0, 1.0, 1.0]
|
||||
for _ in range(8):
|
||||
axis = [sum(cov[i][j] * axis[j] for j in range(4)) for i in range(4)]
|
||||
norm = sum(a * a for a in axis) ** 0.5
|
||||
if norm < 1e-9:
|
||||
return [0.0, 0.0, 0.0, 0.0]
|
||||
axis = [a / norm for a in axis]
|
||||
return axis
|
||||
|
||||
|
||||
def encode_block(pixels):
|
||||
"""One BC7 mode 6 block of 16 RGBA tuples (row-major)."""
|
||||
# The colour of a see-through pixel does not show: it takes the block's visible colour, and
|
||||
# colour errors count as much as the pixel is opaque.
|
||||
visible = sum(p[3] for p in pixels)
|
||||
shown = [sum(p[c] * p[3] for p in pixels) / visible if visible else 0 for c in range(3)]
|
||||
pixels = [p if p[3] else (round(shown[0]), round(shown[1]), round(shown[2]), 0) for p in pixels]
|
||||
# Transparency counts more than colour: a see-through pixel turned faintly visible shows.
|
||||
weights = [(p[3] / 255, p[3] / 255, p[3] / 255, 4.0) for p in pixels]
|
||||
mean = [sum(p[c] for p in pixels) / 16 for c in range(4)]
|
||||
axis = _principal_axis(pixels, mean)
|
||||
project = [sum((p[c] - mean[c]) * axis[c] for c in range(4)) for p in pixels]
|
||||
# Endpoint candidates: the extremes along the principal axis, and the darkest/lightest
|
||||
# visible pixel with the alpha range.
|
||||
ends = [([mean[c] + min(project) * axis[c] for c in range(4)],
|
||||
[mean[c] + max(project) * axis[c] for c in range(4)])]
|
||||
shade = sorted(pixels, key=lambda p: (p[3] > 0, sum(p[:3])))
|
||||
alphas = [p[3] for p in pixels]
|
||||
ends.append((list(shade[0][:3]) + [min(alphas)], list(shade[-1][:3]) + [max(alphas)]))
|
||||
ends.append((list(shade[-1][:3]) + [min(alphas)], list(shade[0][:3]) + [max(alphas)]))
|
||||
def fit(lo, hi):
|
||||
"""The best p-bits for these endpoints: (error, q0, q1, p0, p1, indices)."""
|
||||
best = None
|
||||
for p0 in (0, 1):
|
||||
for p1 in (0, 1):
|
||||
q0 = [min(127, max(0, round((v - p0) / 2))) for v in lo]
|
||||
q1 = [min(127, max(0, round((v - p1) / 2))) for v in hi]
|
||||
e0 = [q * 2 + p0 for q in q0]
|
||||
e1 = [q * 2 + p1 for q in q1]
|
||||
palette = [[((64 - w) * e0[c] + w * e1[c] + 32) >> 6 for c in range(4)] for w in _WEIGHTS]
|
||||
error, index = 0.0, []
|
||||
for p, wt in zip(pixels, weights):
|
||||
dists = [sum((p[c] - col[c]) ** 2 * wt[c] for c in range(4)) for col in palette]
|
||||
k = min(range(16), key=dists.__getitem__)
|
||||
index.append(k)
|
||||
error += dists[k]
|
||||
if best is None or error < best[0]:
|
||||
best = (error, q0, q1, p0, p1, index)
|
||||
return best
|
||||
|
||||
best = min((fit(lo, hi) for lo, hi in ends), key=lambda b: b[0])
|
||||
# Least squares: with the indices fixed, the endpoints that fit the pixels best.
|
||||
for _ in range(2):
|
||||
t = [_WEIGHTS[k] / 64 for k in best[5]]
|
||||
a00 = sum((1 - x) ** 2 for x in t)
|
||||
a01 = sum((1 - x) * x for x in t)
|
||||
a11 = sum(x * x for x in t)
|
||||
det = a00 * a11 - a01 * a01
|
||||
if abs(det) < 1e-9:
|
||||
break
|
||||
lo, hi = [], []
|
||||
for c in range(4):
|
||||
b0 = sum((1 - x) * p[c] for x, p in zip(t, pixels))
|
||||
b1 = sum(x * p[c] for x, p in zip(t, pixels))
|
||||
lo.append(min(255, max(0, (a11 * b0 - a01 * b1) / det)))
|
||||
hi.append(min(255, max(0, (a00 * b1 - a01 * b0) / det)))
|
||||
refined = fit(lo, hi)
|
||||
if refined[0] >= best[0]:
|
||||
break
|
||||
best = refined
|
||||
_, q0, q1, p0, p1, index = best
|
||||
# The first pixel's index has no top bit: swap the endpoints when it would need one.
|
||||
if index[0] >= 8:
|
||||
q0, q1, p0, p1 = q1, q0, p1, p0
|
||||
index = [15 - k for k in index]
|
||||
bits, shift = 1 << 6, 7
|
||||
for c in range(4):
|
||||
bits |= q0[c] << shift | q1[c] << (shift + 7)
|
||||
shift += 14
|
||||
bits |= p0 << shift | p1 << (shift + 1)
|
||||
shift += 2
|
||||
for k, value in enumerate(index):
|
||||
bits |= value << shift
|
||||
shift += 3 if k == 0 else 4
|
||||
return bits.to_bytes(16, 'little')
|
||||
|
||||
|
||||
def encode(image):
|
||||
"""Row-major BC7 blocks of an RGBA Pillow image whose sides are multiples of 4."""
|
||||
width, height = image.size
|
||||
raw = image.convert('RGBA').tobytes()
|
||||
data = [tuple(raw[i:i + 4]) for i in range(0, len(raw), 4)]
|
||||
return [encode_block([data[(by * 4 + y) * width + bx * 4 + x] for y in range(4) for x in range(4)])
|
||||
for by in range(height // 4) for bx in range(width // 4)]
|
||||
|
||||
|
||||
def rect_blocks(texture, x, y, width, height):
|
||||
"""Row-major indices of the blocks a pixel rectangle touches, and the block-aligned
|
||||
rectangle around it (x, y, width, height)."""
|
||||
bx = texture['width'] // 4
|
||||
x0, y0 = x // 4, y // 4
|
||||
x1, y1 = (x + width + 3) // 4, (y + height + 3) // 4
|
||||
which = [row * bx + col for row in range(y0, y1) for col in range(x0, x1)]
|
||||
return which, (x0 * 4, y0 * 4, (x1 - x0) * 4, (y1 - y0) * 4)
|
||||
+12
-8
@@ -5,6 +5,7 @@ import argparse
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import re
|
||||
import shutil
|
||||
import sys
|
||||
import tempfile
|
||||
@@ -14,9 +15,6 @@ import tempfile
|
||||
GAME_FOLDERS = {'action', 'adhoc', 'chr', 'event', 'facegen', 'font', 'map', 'menu', 'movie', 'msg', 'mtd',
|
||||
'obj', 'other', 'param', 'paramdef', 'parts', 'remo', 'script', 'sfx', 'shader',
|
||||
'sound'}
|
||||
# The retail releases' folder names (game_check.py SUPPORTED_TITLES), as a mod's wrapper folder.
|
||||
SERIALS = ('CUSA03173', 'CUSA00900', 'CUSA00207', 'CUSA00208', 'CUSA00299', 'CUSA01363',
|
||||
'CUSA03179', 'CUSA03014', 'CUSA03023')
|
||||
|
||||
|
||||
def child(folder, name):
|
||||
@@ -32,13 +30,16 @@ def child(folder, name):
|
||||
def content_root(folder):
|
||||
"""(root, prefix): where a mod's files are and the game path they go to, or None.
|
||||
|
||||
Accepted layouts: <mod>/dvdroot_ps4, <mod>/app0/dvdroot_ps4, <mod>/<serial>/dvdroot_ps4 (CUSA03173, CUSA00900, ...),
|
||||
one wrapper folder around any of these (an archive extracted into a folder of its name), and
|
||||
the game's folders without dvdroot_ps4 (<mod>/chr, <mod>/parts, ...)."""
|
||||
Accepted layouts: <mod>/dvdroot_ps4, <mod>/app0/dvdroot_ps4, <mod>/<title ID>/dvdroot_ps4
|
||||
(CUSA03173, CUSA00900, ... whichever release the mod was made with), one wrapper folder
|
||||
around any of these (an archive extracted into a folder of its name), and the game's folders
|
||||
without dvdroot_ps4 (<mod>/chr, <mod>/parts, ...)."""
|
||||
folder = Path(folder)
|
||||
if not folder.is_dir():
|
||||
return None
|
||||
for wrapper in ('', 'app0', *SERIALS):
|
||||
title_ids = sorted(e.name for e in folder.iterdir()
|
||||
if e.is_dir() and re.fullmatch(r'[A-Za-z]{4}\d{5}', e.name))
|
||||
for wrapper in ('', 'app0', *title_ids):
|
||||
base = child(folder, wrapper) if wrapper else folder
|
||||
dvdroot = child(base, 'dvdroot_ps4')
|
||||
if dvdroot.is_dir():
|
||||
@@ -173,8 +174,11 @@ def main():
|
||||
parser.add_argument('--mods-dir', required=True, type=Path)
|
||||
parser.add_argument('--config', type=Path)
|
||||
parser.add_argument('--enabled', choices=('0', '1'), default='1')
|
||||
# The menu's button prompts and gestures menu (menu_prompts.py), each a folder,
|
||||
# os.pathsep-separated: settings of their own, so laid first and kept with mods switched off.
|
||||
parser.add_argument('--ui-mods', default='')
|
||||
args = parser.parse_args()
|
||||
layers = []
|
||||
layers = [(Path(p).name, Path(p)) for p in args.ui_mods.split(os.pathsep) if p]
|
||||
if args.enabled == '1':
|
||||
# shadPS4's loose overlay convention, beside the original game.
|
||||
legacy = Path(str(args.game.resolve()) + '-mods')
|
||||
|
||||
+129
-2
@@ -2,7 +2,7 @@
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
"""Co-op addresses (BB_ONLINE=1): the game's own server, the shadNet server and its WebAPI.
|
||||
|
||||
From run.py and the launcher of the Windows port of bbport. Messages,
|
||||
From run.py and the launcher of the Windows port (Supermedo/bloodborne_pc). Messages,
|
||||
bloodstains and ghosts come from the community game server (The Hunter's Dream by default);
|
||||
bells and summons go through a shadNet server (shadPS4's public one by default).
|
||||
|
||||
@@ -10,12 +10,16 @@ bells and summons go through a shadNet server (shadPS4's public one by default).
|
||||
server -> the community server; summon signs -> the shadNet WebAPI when it has the Bloodborne
|
||||
summon service) and prints its path for SHADPS4_HTTP_HOST_OVERRIDES_JSON. A player who needs
|
||||
other addresses puts them in DIR/host_overrides.custom.json, which is then used as it is.
|
||||
`online.py check` tests the servers, the shadNet server's UDP port included (docs/SERVER.md).
|
||||
"""
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import re
|
||||
import socket
|
||||
import struct
|
||||
import subprocess
|
||||
import sys
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
@@ -28,6 +32,14 @@ GAME_SERVER = 'https://ss4.scej-network.jp:20443'
|
||||
COMMUNITY_API_PORT = 18671
|
||||
|
||||
|
||||
# The shadNet server's UDP port (shadnet.cfg MatchingUdpPort): the game pings it to learn its public
|
||||
# address, which the server hands to the other player of a summon.
|
||||
STUN_PORT = 31314
|
||||
# The online ID of the connection test's ping: the server keeps one address per online ID, and the
|
||||
# player's own would replace the address of a game running meanwhile.
|
||||
STUN_TEST_ID = b'bbport-udp-test'
|
||||
|
||||
|
||||
def community_address(value):
|
||||
"""The game server as https://host (the game picks the ports), The Hunter's Dream by default."""
|
||||
value = str(value or '').strip() or COMMUNITY_SERVER
|
||||
@@ -53,6 +65,68 @@ def webapi_setting(webapi, server, community):
|
||||
return webapi or webapi_address(server)
|
||||
|
||||
|
||||
def server_kind(settings):
|
||||
"""'public' (shadPS4's server) or 'own'; settings from before the choice existed are 'own'
|
||||
when they name another server."""
|
||||
kind = settings.get('online_server_kind')
|
||||
if kind in ('public', 'own'):
|
||||
return kind
|
||||
server = str(settings.get('online_server') or '').strip()
|
||||
return 'own' if server and server != SHADNET_SERVER else 'public'
|
||||
|
||||
|
||||
def addresses(settings):
|
||||
"""(shadNet server, WebAPI, game server) for the launcher's settings. A server without a port
|
||||
gets shadNet's 31313; shadPS4's public server ignores the WebAPI field."""
|
||||
community = community_address(settings.get('online_community'))
|
||||
server = str(settings.get('online_server') or '').strip()
|
||||
if server_kind(settings) == 'public' or not server:
|
||||
return SHADNET_SERVER, webapi_address(SHADNET_SERVER), community
|
||||
if ':' not in server:
|
||||
server += ':31313'
|
||||
return server, webapi_setting(settings.get('online_webapi'), server, community), community
|
||||
|
||||
|
||||
def environment(settings):
|
||||
"""The game's online variables (run.sh and the online module) for the launcher's settings."""
|
||||
server, webapi, community = addresses(settings)
|
||||
env = {
|
||||
'BB_ONLINE': '1',
|
||||
'BB_SHADNET_SERVER': server,
|
||||
'BB_SHADNET_WEBAPI': webapi,
|
||||
'BB_SHADNET_NPID': str(settings.get('online_npid') or '').strip(),
|
||||
'BB_SHADNET_PASSWORD': settings.get('online_password') or '',
|
||||
'BB_UPNP': '1' if settings.get('online_upnp', True) else '0',
|
||||
'BB_COMMUNITY_SERVER': community,
|
||||
}
|
||||
port = int(settings.get('online_p2p_port') or 0)
|
||||
if 0 < port < 65536:
|
||||
env['SHADPS4_P2P_PORT'] = str(port)
|
||||
# Summons through the server's WebAPI (http.cpp); the server needs BloodborneSeamlessCoop=true.
|
||||
if server_kind(settings) == 'own' and settings.get('online_seamless'):
|
||||
env['SHADPS4_BLOODBORNE_SEAMLESS_COOP'] = '1'
|
||||
return env
|
||||
|
||||
|
||||
def stun_ping(host, port=STUN_PORT, timeout=2.0, attempts=3):
|
||||
"""The public (address, port) the shadNet server sees for this computer, from the ping the
|
||||
game sends it (np_signaling_helpers.cpp: a 0xffff signaling header, command 1, the online ID
|
||||
and the local address); OSError, TimeoutError when nothing answers."""
|
||||
ping = b'\xff\xff\xff\xff' + b'\x01' + STUN_TEST_ID.ljust(16, b'\0') + bytes(4)
|
||||
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as sock:
|
||||
sock.settimeout(timeout)
|
||||
address = socket.gethostbyname(host)
|
||||
for _ in range(attempts):
|
||||
sock.sendto(ping, (address, port))
|
||||
try:
|
||||
reply, sender = sock.recvfrom(64)
|
||||
except TimeoutError:
|
||||
continue
|
||||
if sender[0] == address and len(reply) == 10 and reply[:4] == b'\xff' * 4:
|
||||
return socket.inet_ntoa(reply[4:8]), struct.unpack('!H', reply[8:10])[0]
|
||||
raise TimeoutError(f'no answer from {host}:{port} (UDP)')
|
||||
|
||||
|
||||
def summon_service(webapi):
|
||||
"""True when a shadNet WebAPI answers Bloodborne's summon calls: the route exists there
|
||||
(an empty request is refused with 400), where other servers answer 404."""
|
||||
@@ -90,8 +164,54 @@ def write_overrides(user_dir, community, webapi):
|
||||
return path
|
||||
|
||||
|
||||
# The co-op port the launcher suggests pinning (SHADPS4_P2P_PORT) and opening.
|
||||
P2P_PORT = 3658
|
||||
|
||||
|
||||
def active_firewalls(ufw_conf=Path('/etc/ufw/ufw.conf'), systemctl='/usr/bin/systemctl'):
|
||||
"""Firewalls that drop unrequested incoming UDP by default, read without root: ufw when its
|
||||
configuration enables it, firewalld when its service runs. The host's systemctl runs without
|
||||
the AppImage's library paths."""
|
||||
found = []
|
||||
try:
|
||||
if re.search(r'^\s*ENABLED\s*=\s*yes', ufw_conf.read_text(errors='replace'), re.M):
|
||||
found.append('ufw')
|
||||
except OSError:
|
||||
pass
|
||||
try:
|
||||
env = {k: v for k, v in os.environ.items() if k not in ('LD_LIBRARY_PATH', 'LD_PRELOAD')}
|
||||
if subprocess.run([systemctl, 'is-active', '--quiet', 'firewalld'], env=env,
|
||||
timeout=3).returncode == 0:
|
||||
found.append('firewalld')
|
||||
except (OSError, subprocess.SubprocessError):
|
||||
pass
|
||||
return found
|
||||
|
||||
|
||||
def default_gateway(route=Path('/proc/net/route')):
|
||||
"""The IPv4 default gateway (the router, which answers UPnP), or None."""
|
||||
try:
|
||||
for line in route.read_text().splitlines()[1:]:
|
||||
fields = line.split()
|
||||
if len(fields) > 2 and fields[1] == '00000000' and fields[2] != '00000000':
|
||||
return socket.inet_ntoa(int(fields[2], 16).to_bytes(4, 'little'))
|
||||
except (OSError, ValueError):
|
||||
pass
|
||||
return None
|
||||
|
||||
|
||||
def firewall_commands(firewall, gateway=None, port=P2P_PORT):
|
||||
"""(open the co-op port, let the router's UPnP replies in or None) for a firewall."""
|
||||
if firewall == 'ufw':
|
||||
return (f'sudo ufw allow {port}/udp',
|
||||
f'sudo ufw allow from {gateway} proto udp' if gateway else None)
|
||||
return (f'sudo firewall-cmd --permanent --add-port={port}/udp && sudo firewall-cmd --reload',
|
||||
None)
|
||||
|
||||
|
||||
def check(server, webapi, community):
|
||||
"""(name, address, ok, detail) for the game server, the shadNet server and its WebAPI."""
|
||||
"""(name, address, ok, detail) for the game server, the shadNet server, its UDP port (detail:
|
||||
the public address it sees, or 'timeout') and its WebAPI."""
|
||||
results = []
|
||||
try:
|
||||
with urllib.request.urlopen(community + ':20443/bb-eu/ss.info', timeout=6) as reply:
|
||||
@@ -105,6 +225,13 @@ def check(server, webapi, community):
|
||||
results.append(('shadnet', server, True, ''))
|
||||
except (OSError, ValueError) as error:
|
||||
results.append(('shadnet', server, False, str(error)))
|
||||
try:
|
||||
public, public_port = stun_ping(host or server)
|
||||
results.append(('stun', f'{host or server}:{STUN_PORT}', True, f'{public}:{public_port}'))
|
||||
except TimeoutError:
|
||||
results.append(('stun', f'{host or server}:{STUN_PORT}', False, 'timeout'))
|
||||
except OSError as error:
|
||||
results.append(('stun', f'{host or server}:{STUN_PORT}', False, str(error)))
|
||||
try:
|
||||
with urllib.request.urlopen(webapi.rstrip('/') + '/status', timeout=4) as reply:
|
||||
ok = b'"ok":true' in reply.read(512).replace(b' ', b'')
|
||||
|
||||
Loaded 100 of 126 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user