mirror of
https://github.com/ayozetr/bloodborne_linux.git
synced 2026-11-08 08:03:07 -10:00
Compare commits
136
Commits
0.51-linux.8
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
98750d0687 | ||
|
|
0b3dac11d7 | ||
|
|
5041586f9a | ||
|
|
e77bce746f | ||
|
|
d1dea1b8e4 | ||
|
|
6796fbbd13 | ||
|
|
badd460277 | ||
|
|
1d0bbec978 | ||
|
|
843424de13 | ||
|
|
21e1ce4224 | ||
|
|
498a37d24b | ||
|
|
e8c7eaaf9c | ||
|
|
74b1a30870 | ||
|
|
62dbeefa6b | ||
|
|
7c6b25a560 | ||
|
|
43ffa68201 | ||
|
|
0f90125d4a | ||
|
|
02dc648c96 | ||
|
|
c16b6d3d58 | ||
|
|
b4f75ec15c | ||
|
|
c8373033ad | ||
|
|
42e8b4c606 | ||
|
|
afac1a4f39 | ||
|
|
4c1e2e3061 | ||
|
|
ab3088469a | ||
|
|
dbc374fcec | ||
|
|
8e9dbc20e1 | ||
|
|
027e834a79 | ||
|
|
0219c34163 | ||
|
|
f0306a62a1 | ||
|
|
5fd9e2ccb5 | ||
|
|
311aeb36ff | ||
|
|
3963108624 | ||
|
|
afef07c90f | ||
|
|
aae3f0566f | ||
|
|
dff4079ef6 | ||
|
|
e0543751fb | ||
|
|
76ebf406f3 | ||
|
|
c27d8fb36b | ||
|
|
2719bce35f | ||
|
|
8cfcdc1f76 | ||
|
|
a68b6a75c8 | ||
|
|
2bdee3185a | ||
|
|
6621a1f0a7 | ||
|
|
e2ac43b725 | ||
|
|
614602cf36 | ||
|
|
f0068ecb34 | ||
|
|
1f98a632b5 | ||
|
|
4e0aa911d2 | ||
|
|
312d119b5a | ||
|
|
0d34da1577 | ||
|
|
28eac08195 | ||
|
|
f0aa0fa2ae | ||
|
|
983c89fa9a | ||
|
|
da3ff0ecb0 | ||
|
|
d698ad290f | ||
|
|
e5fde7ed25 | ||
|
|
7c4a740338 | ||
|
|
91f63ff553 | ||
|
|
7331bb9410 | ||
|
|
2399b7a708 | ||
|
|
cefd12304c | ||
|
|
e1239378d6 | ||
|
|
7f8cbf12a4 | ||
|
|
7f69b82128 | ||
|
|
edfb811965 | ||
|
|
4a6177cf06 | ||
|
|
a238c6937b | ||
|
|
1441367174 | ||
|
|
8c6aeeb1c5 | ||
|
|
ae6c8728f0 | ||
|
|
5c8bf041d7 | ||
|
|
c93a1f915c | ||
|
|
b2a3990de2 | ||
|
|
4e715f48d5 | ||
|
|
5b8ac8efa5 | ||
|
|
d96d384538 | ||
|
|
683b593f45 | ||
|
|
a0ec0efaf3 | ||
|
|
90ba06f7fc | ||
|
|
456532bfa7 | ||
|
|
59da2f9a6f | ||
|
|
47085f688c | ||
|
|
10732fb349 | ||
|
|
e4d879ef3c | ||
|
|
c8bfa7f54c | ||
|
|
61824c2b2a | ||
|
|
a9dcce1eac | ||
|
|
96bd219320 | ||
|
|
c0cde157e3 | ||
|
|
fcca90e6fa | ||
|
|
0fd636c7f0 | ||
|
|
4bac966821 | ||
|
|
32383ee5d6 | ||
|
|
8cf70f6492 | ||
|
|
1bd6314b69 | ||
|
|
39a2174f47 | ||
|
|
a5e89fd8ac | ||
|
|
e64655f7f6 | ||
|
|
570ee9b579 | ||
|
|
fc17a2d7f4 | ||
|
|
afadddfaf9 | ||
|
|
63be02c67f | ||
|
|
3ed9330a6a | ||
|
|
f47ef822ee | ||
|
|
2824ea7ab2 | ||
|
|
a48a131590 | ||
|
|
8cff10a374 | ||
|
|
b902cbc5ff | ||
|
|
cddb964ebd | ||
|
|
cfc63a6766 | ||
|
|
64f7962cac | ||
|
|
1cb0fd24ed | ||
|
|
b2a6130b42 | ||
|
|
ed9d812b85 | ||
|
|
bdd7994cca | ||
|
|
4247abab8e | ||
|
|
fa0d7232d9 | ||
|
|
b2f516f0e0 | ||
|
|
773368a150 | ||
|
|
0d091c9335 | ||
|
|
95c548e6f1 | ||
|
|
55eb72e933 | ||
|
|
40031dd622 | ||
|
|
1eadf67c8c | ||
|
|
b8bb54a791 | ||
|
|
f760e22ebf | ||
|
|
c61dc69ae7 | ||
|
|
05807030a2 | ||
|
|
64660056b4 | ||
|
|
c2a45f36e6 | ||
|
|
47b276b389 | ||
|
|
71e6cc9592 | ||
|
+24 |
0fc2921c45 | ||
|
|
8185b88d80 | ||
|
|
72e5c8ce22 |
No files matched your search
@@ -0,0 +1,3 @@
|
||||
# GitHub's language bar: this C++ table of game texts (its <?name?> placeholders) is not PHP.
|
||||
gpu/bbnet20/bbsc/core/bbsc_messages.inc linguist-language=C++
|
||||
gpu/bbnet20/bbsc/core/bbsc_translations.inc linguist-language=C++
|
||||
@@ -12,7 +12,7 @@ body:
|
||||
id: version
|
||||
attributes:
|
||||
label: Version
|
||||
description: The launcher's **About** page shows it.
|
||||
description: The launcher's **About** page shows it, with the build of a test build; so does the first line of the log.
|
||||
placeholder: 0.51-linux.2
|
||||
validations:
|
||||
required: true
|
||||
|
||||
+8
-2
@@ -23,8 +23,6 @@ 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
|
||||
@@ -32,6 +30,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
|
||||
# The mods folder of runs from source (the launcher's default, DATA_DIR/mods), and the list of the
|
||||
# last run's mods
|
||||
/mods/
|
||||
/mods.last
|
||||
# Game files and dumps: never in the repository
|
||||
eboot.bin
|
||||
param.sfo
|
||||
@@ -39,3 +41,7 @@ param.sfo
|
||||
*.dcx
|
||||
*.7z
|
||||
/CUSA*/
|
||||
# The launcher's icon (shipped)
|
||||
!launcher/bloodborne.png
|
||||
# The players' guide, built by mkdocs (mkdocs.yml)
|
||||
_site/
|
||||
@@ -1,28 +1,23 @@
|
||||
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
|
||||
# bbport — a native Linux and Windows 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)
|
||||
> This is the README of the original project (deadinside28/bloodborne_pc, version 0.7), kept for
|
||||
> its technical details. The Linux fork's own README is [README.md](README.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:
|
||||
(CUSA03173, game version 1.09) on an x86-64 PC with Linux or Windows 10/11 (since 0.7, see
|
||||
[Windows](#windows)). 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).
|
||||
- **as in DXVK**, the game's graphics are translated to Vulkan — by the port's own renderer and
|
||||
shader translator (they started from [shadPS4](https://github.com/shadps4-emu/shadPS4)'s and were
|
||||
rewritten), including temporal upscaling with AMD FSR 3.1, FSR 4, FSR 4.1.1 and NVIDIA DLSS,
|
||||
and DLSS Frame Generation;
|
||||
- memory and the GPU's work follow the PC model on every GPU: the new memory and translation
|
||||
model is the only one (the default since 0.6; the old model of 0.3 was removed in 0.7).
|
||||
|
||||
> **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
|
||||
@@ -31,19 +26,17 @@ memory model (see below).
|
||||
|
||||
**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.
|
||||
A full play-through has not been verified. Tested thoroughly: AMD Radeon RX 7800 XT (Linux with
|
||||
Mesa/RADV, and Windows 11), the Steam Deck; on Windows also two laptops with NVIDIA GTX 1050 and
|
||||
GeForce 930MX; other NVIDIA and Intel GPUs by players.
|
||||
|
||||
## 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
|
||||
- **One memory and translation model** (the model of 0.3 is gone):
|
||||
- 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;
|
||||
@@ -51,25 +44,37 @@ Mesa/RADV) has been tested thoroughly.
|
||||
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`.
|
||||
processor writes them. On every GPU the game's memory goes through the layer's memory module:
|
||||
bound in place, VRAM copies as separate buffers, no sparse binding (NVIDIA's sparse binding
|
||||
froze the GPU for seconds; on AMD the module is faster than the former sparse arena too);
|
||||
`BB_LAYER_MEMORY=0` gives AMD's sparse arena. Tested thoroughly on an RX 7800 XT; on NVIDIA
|
||||
by testers. A driver that fails the startup checks stops the game with a line beginning
|
||||
`STOP: the GPU driver cannot use the game's memory in place`.
|
||||
|
||||
[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.
|
||||
The graphics heaps of the game grow as it takes memory and give it back when freed.
|
||||
|
||||
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.
|
||||
[Changes in 0.7](https://github.com/deadinside28/bloodborne_pc/blob/master/docs/CHANGES_0.7.md), [0.6](https://github.com/deadinside28/bloodborne_pc/blob/master/docs/CHANGES_0.6.md), [0.6-pre1](https://github.com/deadinside28/bloodborne_pc/blob/master/docs/CHANGES_0.6-pre1.md),
|
||||
[0.51](https://github.com/deadinside28/bloodborne_pc/blob/master/docs/CHANGES_0.51.md), [0.5](https://github.com/deadinside28/bloodborne_pc/blob/master/docs/CHANGES_0.5.md).
|
||||
|
||||
Unused textures are freed, so VRAM no longer grows with every area visited; *Texture quality*
|
||||
(full, half, quarter, auto = half on cards with up to 3 GB) skips the top levels of big
|
||||
textures on small cards.
|
||||
- **Unlocked frame rate.** Hooks in the game's code make the simulation use the real frame time
|
||||
(the community patches in `patches/Bloodborne.xml` remain for comparison, `BB_FRAME_HOOKS=0`);
|
||||
~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; **F3** switches a 30 FPS limit on and off while playing (some
|
||||
cinematics, e.g. Laurence's, are safest at 30).
|
||||
- **Any aspect ratio, live.** 4:3, 16:10, 16:9, 21:9 and 32:9 with the HUD at the screen's edges,
|
||||
through hooks (the game's code is not patched); cinematics are wide too.
|
||||
- **Keyboard and mouse.** Mouse look turns the game's camera directly; a mouse pointer in the
|
||||
game's menus (F1 releases the mouse); a layout close to Dark Souls III's (Space dodge, Left
|
||||
Shift run, Shift+Space jump); movement keys ramp up like a stick; button prompts show the bound
|
||||
key, or the buttons of the gamepad in use (Xbox and others), PlayStation icons only with a
|
||||
PlayStation pad.
|
||||
- **Online play and seamless co-op.** Two pools on shadPS4 0.20's network stack: **Seamless
|
||||
Hunt** (default; bells, summons, messages, bloodstains and seamless co-op, BBSC, up to five
|
||||
hunters; an account from seamlesshunt.com) and **The Hunter's Dream** (sign-in with Discord; no
|
||||
BBSC).
|
||||
- **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
|
||||
@@ -84,6 +89,18 @@ Mesa/RADV) has been tested thoroughly.
|
||||
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`.
|
||||
- **DLSS Super Resolution and DLSS Frame Generation** (NVIDIA GeForce RTX) through an
|
||||
optional bridge built against NVIDIA's SDK (see Building). Frame generation makes the frames
|
||||
between two of the game's (2x on RTX 40, up to 4x on RTX 50) from the finished frame, the
|
||||
frame before its UI, the scene depth, the motion vectors and the game's camera matrices;
|
||||
*Overlay menu → Frame generation*, or the launcher.
|
||||
- **FSR 4.1.2's models from the AMD driver** (INT8 on RDNA2/3 and the *Lite* model), taken
|
||||
from the driver's `amdxcffx64.dll` with the upscaler DLL (*FSR 4 model* in the menus).
|
||||
- **FSR 1** (spatial, no motion vectors) for weak GPUs, and **FSR 3.1 frame generation**
|
||||
(experimental, every GPU; the port's own optical-flow and interpolation shaders).
|
||||
- **Anti-aliasing** separate from the upscalers: *auto* (the game's AA only without an
|
||||
upscaler), the game's, FXAA (Kyo's *Force FXAA2*), SMAA 1x (the port's own, before the UI), off;
|
||||
sharpening (RCAS) also without an upscaler. **GT-VBAO** can replace the game's SSAO.
|
||||
- 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.
|
||||
@@ -91,12 +108,19 @@ Mesa/RADV) has been tested thoroughly.
|
||||
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
|
||||
- **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. Chromatic aberration,
|
||||
depth of field, motion blur, SSAO, the game's AA and dynamic-light shadows switch at once;
|
||||
screen-space reflections, the intro, free camera and the debug menu need a restart.
|
||||
- **Overlay menu** (Insert, or hold 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.
|
||||
- **Shaders before you play.** A pack of shader recipes from players' caches (655 programs, 1149
|
||||
pipelines) is compiled in the background at start, and the cache survives updates: a repeat
|
||||
session compiles nothing.
|
||||
- **Launchers:** GTK4 on Linux, WPF in the Windows 11 style on Windows (both with sections and
|
||||
descriptions); an **AppImage** for Linux and the Steam Deck, the same as a **folder**
|
||||
(`tar.zst`, `Bloodborne.sh`), a **zip** for Windows.
|
||||
|
||||
## How it differs from shadPS4
|
||||
|
||||
@@ -104,16 +128,17 @@ Mesa/RADV) has been tested thoroughly.
|
||||
|---|---|---|
|
||||
| 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 |
|
||||
| Memory | The GPU's view of PS4 memory is kept in VRAM copies, synchronized through page-protection write tracking | 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 | 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 | shadPS4 video core and shader recompiler | Its own command processor, renderer, caches and shader translator (they started from shadPS4's, `gpu/VENDOR.txt`): two-stage draw pipeline, render-state and texture-set memoization, render-scale proxies, motion vectors, FSR 3.1/4/4.1.1, DLSS, 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 |
|
||||
| Upscaling | — | Temporal (FSR 3.1, FSR 4, FSR 4.1.1/4.1.2, DLSS) with motion vectors computed for the game and jitter; frame generation (DLSS, FSR 3.1) |
|
||||
| Platforms | Linux, Windows, macOS | Linux and Windows (x86-64) |
|
||||
| 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.
|
||||
Without shadPS4 there would be no bbport: its renderer and shader recompiler were the starting
|
||||
point of the graphics side.
|
||||
|
||||
### Wine + DXVK for one game
|
||||
|
||||
@@ -122,8 +147,8 @@ the graphics side.
|
||||
- **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.
|
||||
- **Memory.** As in a PC game (the default model): the game's data in system RAM, VRAM used the
|
||||
way a PC game uses it.
|
||||
- **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
|
||||
@@ -136,22 +161,20 @@ the graphics side.
|
||||
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).
|
||||
[docs/EMULATION_REMOVAL_PLAN.ru.md](https://github.com/deadinside28/bloodborne_pc/blob/master/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.
|
||||
- Linux x86-64 or Windows 10/11 x86-64, a Vulkan 1.3 GPU. Tested: AMD RX 7800 XT with Mesa 26
|
||||
(RADV) and with AMD's Windows driver; NVIDIA and Intel by players (please report how they run).
|
||||
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
|
||||
@@ -176,41 +199,24 @@ bash launcher/bb-launcher.sh # launcher/install-desktop.sh adds it to th
|
||||
```
|
||||
|
||||
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, and imports saves from
|
||||
shadPS4, another port or a PS4 save exported unencrypted, after backing up the current ones in
|
||||
`user/savedata-backups/`; `scripts/save_import.py`); 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>`; with none picked, the one you press a button on or move
|
||||
takes over). The keyboard works too,
|
||||
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. On a controller without a touchpad (Xbox), Back + d-pad left / right clicks the
|
||||
left / right half. *Controls* in the launcher also swaps the sticks and inverts the controller's
|
||||
camera (`pad_swap_sticks`, `pad_invert_look_x`, `pad_invert_look_y` in bbport.ini). With *Direct
|
||||
mouse camera* (`mouse_camera=direct`) the mouse turns the camera itself, 1:1, with no stick dead
|
||||
zone or turn curve (the game's 1.09 code is hooked while the window holds the mouse; locked on, the
|
||||
mouse moves the stick again so a flick switches targets). There is no PSN: the *Play online / offline* screen is skipped and the game opens
|
||||
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.
|
||||
**Resolution and preset changes:** the game starts at the scene size of the chosen output
|
||||
and preset (a patch at start, the fastest path); the output is the monitor's resolution by
|
||||
default (*Auto*). With *Live resolution changes* (on by default; `bbport.ini`
|
||||
`live_resolution=0|1`) the preset, the output resolution and the aspect ratio then switch while
|
||||
playing, to smaller and to larger sizes, at the same FPS as a start at that size; only another
|
||||
upscaler kind needs a restart. GPUs that cannot give the game's depth a proxy (no D32S8 blits
|
||||
and no shader stencil export, e.g. GTX 10xx) apply a change at the next start instead.
|
||||
|
||||
**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
|
||||
@@ -222,27 +228,24 @@ 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.
|
||||
The original game is preserved; later mods override conflicting files. Texture packs with
|
||||
upscaled map textures (from the mods folder or already in the game folder) get larger graphics
|
||||
heaps by themselves (`BB_LARGE_HEAPS=1/0` forces it on or off).
|
||||
**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).
|
||||
switched on and off in the launcher. See [mods and patches](https://github.com/deadinside28/bloodborne_pc/blob/master/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.
|
||||
**Launcher language:** Russian, English, Brazilian Portuguese, Simplified Chinese or French (follows the system language by default).
|
||||
The game's own text language is chosen separately, from all 21 languages the game has.
|
||||
**GPU choice:** *Mode → GPU* picks the graphics card on machines with two (*Auto* prefers the
|
||||
discrete one; stored by name; `BB_GPU_ID=N` from the shell wins).
|
||||
|
||||
**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
|
||||
hold Cross and press L3 to cycle modes (keyboard: hold F and press Left Ctrl). 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
|
||||
@@ -250,12 +253,11 @@ For the game debug menu, install `DbgFont14h.ccm` and `DbgFont14h.tpf` from
|
||||
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, Back + d-pad left / right a click on that half (while Back is bound
|
||||
to the touchpad only, the default). The port's settings menu remains Insert / L3+R3.
|
||||
pads without a touch surface. The port's settings menu remains Insert / holding 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.
|
||||
With the *Restore Debug Camera* patch the mouse turns the free camera. The game's occlusion
|
||||
queries are Vulkan occlusion queries in the default model (`BB_OCCLUSION_QUERIES=0`: everything
|
||||
counts as visible, as in the old model).
|
||||
|
||||
**Upscaler assets** (not included; FSR 3.1 needs none):
|
||||
|
||||
@@ -276,7 +278,8 @@ builds (4.0.2b, 4.1.1b and similar OptiScaler INT8 builds: another model and pas
|
||||
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,
|
||||
automatically — the first under which the DLL enables FSR 4.1: GE-Proton 10 or newer (also the
|
||||
AUR's `proton-ge-custom` in `/usr/share/steam/compatibilitytools.d`),
|
||||
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
|
||||
@@ -296,13 +299,6 @@ DLSS gets the inputs FSR 3.1 gets (scene color, depth, motion vectors, jitter).
|
||||
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
|
||||
@@ -310,6 +306,31 @@ launcher: *Upscaler → DLSS: NVIDIA's library → Choose a file…*, as the FSR
|
||||
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.
|
||||
|
||||
**DLSS Frame Generation (GeForce RTX 40 and newer).** The same SDK (310.7 or newer) has NVIDIA's
|
||||
Linux frame generation library, `lib/Linux_x86_64/rel/libnvidia-ngx-dlssg.so.<version>`;
|
||||
`build.sh` copies it next to `bb-probe` with the Super Resolution one, and the launcher's
|
||||
*DLSS: NVIDIA's library → Choose a file…* takes it too (into `<user>/dlss/`, beside or instead of
|
||||
the package's). Then *Frame generation → DLSS* in the overlay menu (Insert, or hold L3+R3) or the
|
||||
launcher: between each two of the game's frames one generated frame is shown (RTX 40), or up to
|
||||
three (*Multiplier*, RTX 50); the display needs the refresh rate for them. It works with any
|
||||
upscaler, DLSS or FSR; the inputs are the frame as shown, the frame before its UI, the scene depth,
|
||||
the motion vectors bbport computes and the game's cameras as clip-space matrices. Without the
|
||||
library, on an RTX 30 or older or with `BB_DLSS=0` the entry says why it is unavailable, and the
|
||||
game runs without generated frames. `BB_FRAME_GEN=dlss` and `BB_FRAME_GEN_MULTIPLIER=2..4` set it
|
||||
from the command line. FSR 3.1 frame generation is offered on every GPU as experimental
|
||||
(*Frame generation → FSR 3.1*).
|
||||
|
||||
**Low latency (NVIDIA Reflex).** On NVIDIA's driver (550 or newer) the port uses the Vulkan
|
||||
form of Reflex, `VK_NV_low_latency2`, the same mechanism Proton gives Windows games: the game's
|
||||
thread waits for the driver before each frame so that frames do not queue up in front of the GPU,
|
||||
and the frame's simulation, submissions and present are marked for the driver. On by default where
|
||||
the driver has it: the overlay menu (*Output → Low latency (NVIDIA Reflex)*) and the launcher
|
||||
offer Off, On and On + Boost (`low_latency=off|on|boost` in `bbport.ini`, `BB_LOW_LATENCY`);
|
||||
on AMD the same setting uses **Anti-Lag 2** (`VK_AMD_anti_lag`: AMD's Windows driver, or Mesa's
|
||||
anti-lag layer on Linux; `BB_ANTI_LAG=0` leaves it out). With DLSS frame generation it is always
|
||||
at least On, as NVIDIA requires of games: the generated frames add latency that Reflex takes back.
|
||||
Reflex and DLSS frame generation came from PR #260 by adds39939.
|
||||
|
||||
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.
|
||||
|
||||
@@ -329,8 +350,9 @@ the launcher's button (see above); they go to `~/.local/share/bbport/fsr4_411`
|
||||
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
|
||||
does not matter. (Steam preloads its overlay into every non-Steam game; the AppImage keeps it out
|
||||
of the launcher and gives it to the game only, so it can open in the game with Shift+Tab; `BB_STEAM_OVERLAY=0`
|
||||
leaves it out.) Where Steam
|
||||
runs games without FUSE (NixOS: Steam's FHS sandbox; the AppImage then exits with *Cannot mount
|
||||
AppImage*), set the launch options to
|
||||
|
||||
@@ -349,7 +371,9 @@ NVIDIA userspace driver matched to the host kernel module. Standard Linux distri
|
||||
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
|
||||
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
|
||||
@@ -366,8 +390,7 @@ 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
|
||||
RSS, images and guest blocks in VRAM), `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
|
||||
@@ -383,18 +406,72 @@ budget taken as at most N MiB: checks the behaviour of cards with little VRAM),
|
||||
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).
|
||||
precise and slow, 0 off), `BB_RENDER_RESOURCE_CACHE_MB=160` (the game's render resource cache
|
||||
at its own size instead of 512 MiB of the port's memory; `BB_DMEM_EXTRA_MB`, 512 by default, is
|
||||
the direct memory the port keeps past the game's own). All of them, each with what it does:
|
||||
[docs/ENVIRONMENT.md](docs/ENVIRONMENT.md); the log's `Environment:` line names those set and any
|
||||
unknown `BB_*` name (a typo). More in [docs/](docs); recent changes: [docs/CHANGES_0.7.md](https://github.com/deadinside28/bloodborne_pc/blob/master/docs/CHANGES_0.7.md),
|
||||
[docs/CHANGES_0.6.md](https://github.com/deadinside28/bloodborne_pc/blob/master/docs/CHANGES_0.6.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 with a shadNet account (Online ID and password):
|
||||
The Hunter's Dream's pool through bbbridge, or a server of your own (`docs/SERVER.md`); shadPS4's
|
||||
public one (`srv.shadps4.net:31313`) no longer accepts bbport builds. Offline the module is never loaded; a
|
||||
build without it plays offline only.
|
||||
**Online play.** A separate module, `out/gpu/libbbnet20.so` (`gpu/bbnet20`: shadPS4 v0.20's
|
||||
network, NP and shadNet code, the port's BBSC and The Hunter's Dream backend), 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`) and offers two pools:
|
||||
- **Seamless Hunt** (default): a shadNet server with BBSC's game server
|
||||
(`play.seamlesshunt.com:31313`, WebAPI `:31315`); bells, summons, messages, bloodstains and
|
||||
**seamless co-op (BBSC)**: the session survives deaths, lamps, area changes, bosses and the
|
||||
Hunter's Dream, up to five hunters (*Seamless co-op (BBSC)*, on by default; every player needs
|
||||
it and the same server). The account (online id and password) is made on
|
||||
https://seamlesshunt.com. Details: [docs/BBSC.md](https://github.com/deadinside28/bloodborne_pc/blob/master/docs/BBSC.md).
|
||||
- **The Hunter's Dream** (thehuntersdream.com): sign in with Discord from the launcher (a code is
|
||||
confirmed on the site); no BBSC.
|
||||
|
||||
On Windows the module is `online\bbnet20.dll` (Seamless Hunt with BBSC; the launcher's *Online*
|
||||
section). The Linux folder package keeps it open as `online/libbbnet20.so`, and `BB_NET_MODULE`
|
||||
points the port at any other build of it (a newer BBSC, say).
|
||||
|
||||
The former module on shadp2p (`libbbnet.so`) was removed (0.7). shadPS4's
|
||||
public server (`srv.shadps4.net`) accepts only shadPS4's own builds. Offline the module is never
|
||||
loaded; a build without it plays offline only.
|
||||
|
||||
## Windows
|
||||
|
||||
Since 0.7 bbport runs natively on Windows 10/11 (x86-64, a Vulkan 1.3 GPU with a current driver:
|
||||
AMD Adrenalin, NVIDIA GeForce, Intel Arc). The same code as on Linux: the game's x86-64 code runs
|
||||
directly, the runtime's POSIX layer is implemented on Win32 (`gpu/platform/win32`), and the
|
||||
game's memory is shared with Vulkan through `VK_EXT_external_memory_host`.
|
||||
|
||||
**Package:** `Bloodborne-bbport-<version>-windows-x64.zip`. Unpack it anywhere (not into *Program Files*), start
|
||||
`Bloodborne.exe`, choose the game folder (the one with `eboot.bin`), press *Play*. Insert (or
|
||||
holding L3+R3) opens the port's menu in the game. Everything the port writes stays in that folder:
|
||||
`bbport.ini` (settings), `user\` (saves, shader cache), `out\` (prepared game files), `logs\` (one
|
||||
log per run: for a report send the newest one). `Bloodborne.exe --play` starts the game without
|
||||
the launcher window (through `bbport-run.exe`).
|
||||
|
||||
**Launcher:** WPF in the Windows 11 style, following the system's light or dark theme, in five
|
||||
languages; the same settings as on Linux (sections with descriptions), monitor, controller and
|
||||
GPU choice, the log of the running game, *Stop*.
|
||||
|
||||
**Upscalers:** FSR 3.1 and FSR 4 as on Linux. FSR 4.1.1: choose *Build from an AMD DLL…* and pick
|
||||
`amd_fidelityfx_upscaler_dx12.dll` 4.1.x; the recording runs on vkd3d-proton and DXVK from the
|
||||
package (`tools\fsr4cap\bin`), so any GPU with Vulkan can build the set (a minute or two, into
|
||||
`fsr4_411\`). FSR 4.1.2's models (INT8 for RDNA2/3, *Lite*): pick the driver's `amdxcffx64.dll`
|
||||
(Adrenalin 26.10.1 or newer, `C:\Windows\System32\DriverStore\FileRepository\u0*\`) together with
|
||||
the upscaler DLL. DLSS and DLSS Frame Generation (GeForce RTX): with `bin\bbport_dlss.dll` and
|
||||
NVIDIA's `nvngx_dlss.dll` / `nvngx_dlssg.dll` in the package.
|
||||
|
||||
**NVIDIA:** at start the port adds a *prefer maximum performance* power profile for
|
||||
`bb-probe.exe` to the driver, unless you chose a mode in NVIDIA's Control Panel yourself
|
||||
(`BB_NV_POWER=0`: leave it alone); presentation uses FIFO (`BB_NVIDIA_MAILBOX=1`: Mailbox).
|
||||
|
||||
**Online play:** the launcher's *Online* section, the same two pools as on Linux (Seamless Hunt
|
||||
with BBSC, The Hunter's Dream with Discord sign-in), UPnP and a connection check; the module is
|
||||
`online\bbnet20.dll`. **Mods:** the launcher's *Mods and patches* section, as on Linux.
|
||||
|
||||
**Building:** in MSYS2's CLANG64 shell (`mingw-w64-clang-x86_64` toolchain, CMake, Ninja, Python,
|
||||
SDL3, glslang), or cross-built from Linux with llvm-mingw; the package
|
||||
is made by `packaging/windows/package.sh` (the launcher needs the .NET 9 SDK). DLSS:
|
||||
`DLSS_SDK_ROOT=<NVIDIA's DLSS SDK for Windows>` (`sdk/DLSS-windows`).
|
||||
|
||||
## Repository layout
|
||||
|
||||
@@ -402,45 +479,71 @@ build without it plays offline only.
|
||||
|---|---|
|
||||
| `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 |
|
||||
| `gpu/` | Renderer library, the port's own (`gpu/VENDOR.txt`): command processor, shader translator, renderer, caches, ImGui menu, FSR 4.1.1 runtime (`gpu/upscale/fsr411`); the online module (`gpu/bbnet20`) |
|
||||
| `launcher/`, `packaging/` | GTK4 launcher (Linux), WPF launcher (`launcher/windows`), the shared settings schema; Nix package, AppImage, Windows package |
|
||||
| `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)) |
|
||||
| `docs/` | Design notes and measurements ([upscaler](docs/upscaler.md), [parallel GPU](docs/parallel_gpu.md), [motion vectors](https://github.com/deadinside28/bloodborne_pc/blob/master/docs/motion_vectors.md), [roadmap](https://github.com/deadinside28/bloodborne_pc/blob/master/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`.
|
||||
`ninja -C out/gpu motion-history-test ui-composition-test scene-resolution-test motion-shader-test settings-test camera-matrices-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.
|
||||
- NVIDIA on Windows: more reports from RTX cards in the heaviest views.
|
||||
- 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.
|
||||
- All of the game's shaders translated ahead of time (today: recipes from players' caches).
|
||||
- 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.
|
||||
- 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.
|
||||
|
||||
## Related projects
|
||||
|
||||
- [ducvd89/Bloodborne-Android](https://github.com/ducvd89/Bloodborne-Android): Bloodborne on
|
||||
Android, a fork of bbport (GSR2 / FSR 3.1 and LSFG, an on-screen controller; tested on
|
||||
Snapdragon 8 Gen 2 and 8 Elite).
|
||||
- [bloodborne-native/bloodborne-mac](https://github.com/bloodborne-native/bloodborne-mac):
|
||||
Bloodborne running natively on Apple Silicon, a macOS app built on bbport and shadPS4.
|
||||
|
||||
## 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+),
|
||||
from shadPS4 (GPL-2.0-or-later). Reusing bbport's code: keep its copyright and license notices
|
||||
(`SPDX-FileCopyrightText: Copyright 2026 bbport contributors` at the top of its files) and
|
||||
[NOTICE](NOTICE), and say where the code comes from, with a link to this repository. Third-party components keep their licenses:
|
||||
[shadPS4](https://github.com/shadps4-emu/shadPS4) (GPL-2.0+: the renderer and shader translator were derived from it and rewritten; the online module keeps its network and NP code),
|
||||
[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,
|
||||
[BBSC](https://github.com/CLSAMG22/BBSC) seamless co-op (GPL-2.0+, in the online module), the
|
||||
online module's network code from [shadPS4 v0.20](https://github.com/shadps4-emu/shadPS4/tree/v.0.20.0),
|
||||
The Hunter's Dream backend from
|
||||
[droogie/bbhost](https://github.com/droogie/bbhost) (GPL-3.0-or-later, so `libbbnet20.so` as a
|
||||
whole is GPL-3.0-or-later),
|
||||
[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
|
||||
FSR 4.1.1 assets), [wepoll](https://github.com/piscisaureus/wepoll) (BSD-2-Clause, the Windows
|
||||
online module). Game patches by Kyo, Lance McDonald, auser1337, illusion, emoose and other
|
||||
community members (`patches/Bloodborne.xml`). The DLSS bridge (`gpu/dlss_bridge`, MIT) is 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.
|
||||
|
||||
Contributions merged into bbport (thank you; the full list with each release is in
|
||||
[docs/CONTRIBUTORS_0.7.md](https://github.com/deadinside28/bloodborne_pc/blob/master/docs/CONTRIBUTORS_0.7.md)): SrDeTs (#10), IgorWarzocha (#36), itachi-re
|
||||
(#43), Bainianzzz (#71), hideoneric (#78), guitaripod (#80), unTanana (#83, #84, #89), andrR89
|
||||
(#93), SaeedShow (#108), AndiMajore (#112, #113), ayozetr (#117), JuanraCM (#120), Supermedo and Awaken (the online
|
||||
module's network stack, from Supermedo's Windows fork), wheat32 (#360 larger heaps for texture packs),
|
||||
Sarxzer (#347 French), whp199 (#210, #211, #214, #342, #343), DominickCobb-rs (#346), maiconjsv
|
||||
(#339), iannitram (#293), adds39939 (#260 DLSS Frame Generation and Reflex), AndiMajore (#166,
|
||||
#224, #225), Qiming-Liu (#75, #157 Chinese), AyrtonSanabio (#153, #154), axpico (#206), msisovic (#193),
|
||||
noelmiller (#235), Obrikash (#140), Aryxa (#137), ArenaMMORPG (#352/#353), guutol (trophies),
|
||||
redditor69 (GT-VBAO), SrShadowy and Teefpyre (engine and BBSC fixes from SrShadowy's fork), and
|
||||
the players who sent their shader caches and logs.
|
||||
-419
@@ -1,419 +0,0 @@
|
||||
ДАННЫЙ ПРОЕКТ НЕ ИМЕЕТ ОТНОШЕНИЯ К SHADPS4. ВСЕ ВОПРОСЫ, СВЯЗАННЫЕ С ЭТИМ ПРОЕКТОМ - ПИШИТЕ НА ДИСКОРД СЕРВЕР https://discord.gg/KYZRKk9CB, А НЕ НА СЕРВЕР SHADPS4
|
||||
|
||||
# bbport — нативный порт Bloodborne для Linux
|
||||
|
||||
> README оригинального проекта (deadinside28/bloodborne_pc), сохранён ради технических
|
||||
> подробностей. README этого форка — [README.md](README.md) (на английском).
|
||||
|
||||
[English](BBPORT.md) · **Русский**
|
||||
|
||||
bbport — это аналог Wine + DXVK, только для одной игры: *Bloodborne* для PlayStation 4
|
||||
(CUSA03173, версия игры 1.09) на ПК с Linux x86-64. Оригинальный исполняемый файл игры
|
||||
запускается прямо на ПК:
|
||||
|
||||
- **как в Wine**, x86-64-код игры выполняется на процессоре напрямую, а системные библиотеки PS4
|
||||
заменяет собственный рантайм, написанный под эту игру;
|
||||
- **как в DXVK**, графика игры переводится в Vulkan — рендером на основе
|
||||
[shadPS4](https://github.com/shadps4-emu/shadPS4), сильно доработанным под Bloodborne, вплоть
|
||||
до временного апскейлинга AMD FSR 3.1, FSR 4 и FSR 4.1.1;
|
||||
- память и работа видеочипа на AMD по умолчанию устроены как у игры для ПК (новая модель памяти
|
||||
и трансляции); NVIDIA и другие видеокарты пока работают со старой моделью памяти, как в 0.3.
|
||||
В лаунчере *Модель памяти* переключается в обе стороны.
|
||||
|
||||
До полной модели Wine + DXVK остался один шаг: новая модель и на NVIDIA, затем удаление старой
|
||||
модели памяти (см. ниже).
|
||||
|
||||
> **Файлов игры в репозитории нет.** Нужен свой дамп Bloodborne с обновлением 1.09: любое издание
|
||||
> из магазина (CUSA00900 в США, CUSA03173 — европейское Game of the Year, ...): у них один и тот же
|
||||
> исполняемый файл.
|
||||
> Проект не связан с Sony Interactive Entertainment, FromSoftware и AMD.
|
||||
|
||||
**Состояние: экспериментальный, играбельный.** Игра запускается, загружает сохранения и
|
||||
проходится (Сон Охотника и несколько локаций Ярнама) со звуком,
|
||||
геймпадом и сохранением. Полное прохождение не проверено; основательно проверена одна машина
|
||||
(Linux, AMD Radeon RX 7800 XT, Mesa/RADV).
|
||||
|
||||
## Что сделано
|
||||
|
||||
- **Нативное исполнение.** eboot заранее превращается в плоский образ памяти, в который как
|
||||
нативный код прилинкованы libc и libSceFios2 из PS4. Никакой эмуляции процессора и
|
||||
трансляции инструкций: код игры работает на полной скорости.
|
||||
- **Две модели памяти** (лаунчер → *Режим работы → Модель памяти и трансляции*; *Авто*, по
|
||||
умолчанию, выбирает по видеокарте):
|
||||
- **Старая модель памяти, как в 0.3, — по умолчанию на NVIDIA, Intel и других.** Копии памяти
|
||||
игры в VRAM, записи отслеживаются защитой страниц. Со всеми исправлениями после 0.3
|
||||
(векторы движения и апскейлер, мерцание при включённом DoF, повреждённый кэш шейдеров).
|
||||
- **Новая модель памяти и трансляции — по умолчанию на AMD.** Память
|
||||
игры находится в оперативной памяти, и GPU читает её на месте, как буферы игры для ПК; то,
|
||||
что он читает часто, держится в видеопамяти и освобождается, когда перестаёт использоваться
|
||||
(текстуры — через 20 с, буферы — через 60 с). Модуль памяти прослойки отслеживает записи CPU
|
||||
в блоки с зеркалами для согласования данных; вся память, видимая GPU, не копируется.
|
||||
Работа командного процессора PS4 переводится в команды Vulkan, а не
|
||||
эмулируется на процессоре: записи в память (`WRITE_DATA`, DMA) выполняет видеочип в порядке
|
||||
потока команд, метки (fences) пишет сам видеочип в конце конвейера, в той же точке потока,
|
||||
что и командный процессор PS4.
|
||||
На AMD память игры держится в sparse-буфере (арене); на NVIDIA, Intel и других — через модуль
|
||||
памяти прослойки: привязывается на месте, копии в VRAM — отдельные буферы, без
|
||||
sparse-перепривязок (на NVIDIA sparse binding держал видеочип секундами); `BB_LAYER_MEMORY=1/0`
|
||||
— выбрать вручную. Проверено на RX 7800 XT. На NVIDIA её можно выбрать
|
||||
вручную (*Новая*); у тестеров результат зависит от сцены, в части сцен медленнее старой
|
||||
модели, поэтому там она пока не по умолчанию. На Intel не проверялась. Без лаунчера —
|
||||
`BB_PC_MODEL=1/0` (не задано — по видеокарте, как *Авто*).
|
||||
|
||||
[Изменения в 0.5](docs/CHANGES_0.5.md); пререлизы: [pre4](docs/CHANGES_0.5-pre4.md),
|
||||
[pre3](docs/CHANGES_0.5-pre3.md), [pre2](docs/CHANGES_0.5-pre2.md).
|
||||
|
||||
Неиспользуемые текстуры освобождаются в обоих режимах, поэтому видеопамять больше не растёт
|
||||
с каждой локацией.
|
||||
- **Разблокированная частота кадров.** Патчи сообщества (`patches/Bloodborne.xml`) переводят
|
||||
симуляцию на реальное время кадра; ~90 FPS в 4K с FSR 4 Balanced на RX 7800 XT, ~150 FPS в
|
||||
1440p с FSR 4 Quality. Есть режимы 30/60/90 FPS.
|
||||
- **Временной апскейлинг под эту игру.** Буфера скоростей в Bloodborne нет, поэтому порт
|
||||
считает векторы движения сам: движение камеры — по глубине и матрицам сцены, движение
|
||||
объектов (персонажи, ткань, оружие) — по позициям вершин прошлого кадра. Сцена сдвигается
|
||||
субпиксельным jitter (Halton) и рисуется в уменьшенном разрешении, апскейлер достраивает
|
||||
вывод (720p для Steam Deck, 1080p, 1440p или 2160p), интерфейс рисуется сразу в выходном разрешении.
|
||||
- **FSR 3.1** (FireBurn/FSR-Vulkan).
|
||||
- **FSR 4 (INT8, модель v07)** на видеокартах с нужными возможностями шейдеров Vulkan,
|
||||
включая RDNA2/3 (см. требования).
|
||||
- **FSR 4.1.1**: DLL AMD 4.1.1 один раз записывается под vkd3d-proton, а её проходы
|
||||
воспроизводятся нативно в Vulkan; результат **побитово совпадает** с DLL. Два варианта, как
|
||||
в самой DLL: INT8 на любой видеокарте с нужными возможностями шейдеров и матрицы FP8 на
|
||||
RDNA4 (RX 9000; выбирается сам). Ассеты собираются на твоей машине из одной твоей DLL
|
||||
4.1.x: в лаунчере кнопка *FSR 4.1.1 из своей DLL AMD → Выбрать DLL…* (2–5 минут, на RDNA4
|
||||
вдвое дольше; нужен свежий Proton), или `tools/fsr4cap`.
|
||||
- Быстрее шейдеров самой AMD на RDNA3: финальные проходы FSR 4 и 4.1.1 переписаны с записью
|
||||
через shared-память (в 3.5 и 2.3 раза быстрее, побитово так же); FSR 4 в 4K на RX 7800 XT
|
||||
стоит ~4 мс вместо ~6.
|
||||
- **Многопоточная обработка GPU-команд.** Поток команд PS4 разбирается в одном потоке, а
|
||||
привязка ресурсов и запись draw — в другом (двухстадийный конвейер), плюс отдельный поток
|
||||
записи команд Vulkan и вспомогательные потоки для копий памяти. В начале проекта единственный
|
||||
поток GPU ограничивал игру ~26 FPS; сейчас 90–150 FPS в зависимости от разрешения и сцены.
|
||||
- **Настройки в меню самой игры:** *Система → Изображение / Эффекты* (после *Экран/звук*) — их
|
||||
строит и рисует сама игра: разрешение вывода, апскейлер, пресет, резкость, счётчик FPS,
|
||||
детализация моделей и эффекты игры, на языке игры.
|
||||
- **Меню поверх игры** (Insert или L3+R3) в стиле диалогов игры: всё то же и расширенные
|
||||
настройки; геймпад, клавиатура и мышь.
|
||||
- **Лаунчер на GTK4** и **AppImage** для Steam Deck.
|
||||
|
||||
## Чем отличается от shadPS4
|
||||
|
||||
| | shadPS4 | bbport |
|
||||
|---|---|---|
|
||||
| Охват | Универсальный эмулятор PS4, много игр | Одна игра: Bloodborne v1.09 |
|
||||
| Загрузка | Свой ELF-загрузчик и эмуляция ядра во время работы | eboot заранее превращается (`scripts/`) в образ с прилинкованными libc/Fios2 PS4; загрузчик на C отображает его и передаёт управление игре (загрузчик и рантайм — ~5 тыс. строк) |
|
||||
| Память | Вид памяти PS4 со стороны GPU хранится копиями в VRAM, синхронизация через отслеживание записей защитой страниц | По умолчанию та же модель (как в 0.3); в режиме *Новая модель памяти и трансляции*: память игры в оперативной памяти, GPU использует её на месте, часто читаемое — в VRAM, освобождается, когда не нужно |
|
||||
| Командный процессор | Эмулируется: записи в память, DMA и метки выполняет процессор во время разбора команд | По умолчанию так же; в новом режиме переводится в команды Vulkan, их выполняет видеочип в порядке потока, метки — после реального завершения работы |
|
||||
| Системные библиотеки | Широкий HLE ОС PS4 | Небольшой рантайм (`src/runtime_*.c`) ровно под то, что вызывает Bloodborne: память, потоки, синхронизация, файлы, звук (включая ATRAC9), геймпад, сохранения, AppContent |
|
||||
| GPU | Видеоядро и рекомпилятор шейдеров shadPS4 | То же ядро (вложено, GPL) с ~200 помеченными правками (`bbport:`) и новыми модулями: двухстадийный конвейер draw, мемоизация состояния прохода и наборов текстур, уменьшенные цели сцены, векторы движения, FSR 3.1/4/4.1.1, захват кадра, профайлер GPU |
|
||||
| Поток GPU | Один поток обрабатывает весь поток команд (узкое место в Bloodborne) | Разбор и запись draw идут в разных потоках; работа масштабируется под число потоков (в том числе Steam Deck) |
|
||||
| Апскейлинг | — | Временной (FSR 3.1, FSR 4, FSR 4.1.1) на собственных векторах движения и jitter игры |
|
||||
| Патчи игры | Патч-файлы применяет эмулятор | Те же патчи сообщества компилируются при запуске (`scripts/patches.py`); разрешение рендера, эффекты и FPS — из лаунчера |
|
||||
|
||||
Без shadPS4 bbport бы не было: его рендерер и рекомпилятор шейдеров — основа графической части.
|
||||
|
||||
### Wine + DXVK для одной игры
|
||||
|
||||
- **Процессор.** Процессор PS4 — x86-64, поэтому код игры выполняется прямо на процессоре ПК, без
|
||||
эмуляции и перевода инструкций.
|
||||
- **Системные библиотеки.** Как Wine подменяет Windows API, так рантайм bbport (`src/runtime_*.c`)
|
||||
реализует ровно те функции ОС PS4, которые вызывает Bloodborne: память, потоки, файлы, звук,
|
||||
геймпад, сохранения.
|
||||
- **Память.** В новом режиме — как у игры для ПК: данные игры в оперативной памяти, видеопамять
|
||||
используется так, как её использует игра для ПК. Пока режим экспериментальный.
|
||||
- **Графика.** Поток команд видеочипа PS4 (PM4) — это записанные вызовы графического API игры:
|
||||
его пишут 99 функций статически слинкованной libGnm, так что разбор потока и перевод самих
|
||||
вызовов дают одно и то же. Шейдеры GCN переводятся в SPIR-V. В новом режиме работа командного
|
||||
процессора (записи в память, DMA, метки) тоже переводится в команды Vulkan; метки пишет сам
|
||||
видеочип. Дескрипторы ресурсов (текстур, буферов) транслятор читает на процессоре, как DXVK
|
||||
читает привязки ресурсов: текстуру PS4 нужно перевести из раскладки PS4 в образ Vulkan до
|
||||
отрисовки, поэтому процессор должен знать её дескриптор заранее — это трансляция, а не
|
||||
эмуляция. Таблицы дескрипторов (дампы движка констант) читает только транслятор, поэтому их
|
||||
выполняет процессор. Occlusion-запросы игры (~60 за кадр) — настоящие запросы Vulkan; предикация, `COPY_DATA`,
|
||||
`COND_EXEC` тоже переводятся (проверка — `BB_PM4_SELFTEST=1`).
|
||||
Подробно — [docs/EMULATION_REMOVAL_PLAN.ru.md](docs/EMULATION_REMOVAL_PLAN.ru.md), «Шаг 4».
|
||||
|
||||
«Порт» здесь — сборка под одну игру, а не переписанный исходный код: исходного кода игры у
|
||||
проекта нет, и он его не содержит.
|
||||
|
||||
## Требования
|
||||
|
||||
- Linux x86-64, видеокарта с Vulkan 1.3. Проверено: AMD RX 7800 XT, Mesa 26 (RADV).
|
||||
Режим *Новая модель памяти и трансляции* на NVIDIA пока не проверен.
|
||||
FSR 4 / 4.1.1 требуют shader Float16, Int8/Int16, целочисленных dot-произведений,
|
||||
linear compute derivatives и extended storage image formats; FSR 4.1.1 дополнительно
|
||||
требует `VK_VALVE_shader_mixed_float_dot_product`. Неподдерживаемый вариант заменяется
|
||||
на FSR 3.1 до первого кадра и недоступен в игровом меню.
|
||||
- Свой расшифрованный дамп игры: папка `CUSA03173` (eboot.bin, sce_module, ...), версия 1.09.
|
||||
Дамп обновления — отдельная папка: скопируйте её поверх базовой игры с заменой файлов. Одна
|
||||
базовая игра (1.00) падает при запуске (guest offset 0x20348b8); лаунчер и `run.sh` проверяют
|
||||
исполняемый файл и пишут, чего не хватает (`BB_SKIP_GAME_CHECK=1` отключает проверку).
|
||||
Другие издания (американское `CUSA00900`, азиатское `CUSA03023`, ...) запускают тот же
|
||||
исполняемый файл 1.09. В некоторых дампах в него уже вшит патч 60 FPS от Lance McDonald, из-за которого игра
|
||||
падает при открытии меню жестов: проверка тогда просит чистый eboot.bin (у bbport свои 60 FPS).
|
||||
- Для сборки: GCC, CMake, Ninja, Python 3, glslang, SDL3, заголовки Vulkan и библиотеки из
|
||||
`shell.nix`. С [Nix](https://nixos.org) всё берётся из `shell.nix` автоматически.
|
||||
|
||||
## Сборка и запуск
|
||||
|
||||
```bash
|
||||
git clone --recursive https://github.com/deadinside28/bloodborne_pc.git bbport && cd bbport
|
||||
bash build.sh # собирает out/bb-probe и out/gpu/libbbgpu.so
|
||||
BB_GAME_DIR=/путь/к/CUSA03173 bash run.sh
|
||||
```
|
||||
|
||||
или через лаунчер (выбрать папку игры, настройки, «Запустить»):
|
||||
|
||||
```bash
|
||||
bash launcher/bb-launcher.sh # launcher/install-desktop.sh добавит его в меню приложений
|
||||
```
|
||||
|
||||
По умолчанию папка игры ищется рядом с репозиторием (`../CUSA03173`). Сохранения и кэш шейдеров
|
||||
— в `user/` (в лаунчере можно выбрать другую папку и импортировать сохранения из shadPS4,
|
||||
другого порта или выгруженные с PS4 без шифрования; текущие сначала копируются в
|
||||
`user/savedata-backups/`; `scripts/save_import.py`), настройки — в `bbport.ini`. Геймпад через
|
||||
SDL3 (если их несколько, нужный выбирается в лаунчере: *Управление → Контроллер*,
|
||||
`BB_GAMEPAD=<GUID или часть имени>`; если не выбран, управляет тот, на котором нажали кнопку или
|
||||
двинули стик). Клавиатура тоже работает, и при подключённом геймпаде
|
||||
(на Steam Deck он есть всегда); обе раскладки меняются в лаунчере (*Управление*). Имя персонажа
|
||||
вводится с клавиатуры в окне поверх игры. Тачпад: левая половина (Tab, Back/Select) открывает
|
||||
жесты, правая (Backspace) — личные вещи. На геймпаде без тачпада (Xbox) Back + крестовина
|
||||
влево / вправо нажимает левую / правую половину. В лаунчере (*Управление*) можно поменять стики
|
||||
местами и инвертировать камеру геймпада (`pad_swap_sticks`, `pad_invert_look_x`, `pad_invert_look_y` в bbport.ini). *Прямая камера мышью*
|
||||
(`mouse_camera=direct`): мышь поворачивает камеру сама, 1:1, без мёртвой зоны и кривой стика (код
|
||||
игры 1.09 перехватывается, пока окно держит мышь; при захвате цели мышь снова двигает стик). PSN нет: экран «Играть по сети / вне сети» пропускается,
|
||||
игра сразу открывает главное меню вне сети (патч игры; в лаунчере *Эффекты игры → Пропускать
|
||||
выбор «по сети / вне сети»*, `BB_SKIP_NETWORK_CHOICE=0` — показывать). При нескольких мониторах нужный выбирается в
|
||||
лаунчере: *Экран → Монитор* (`BB_DISPLAY=<номер или часть имени>`).
|
||||
|
||||
**Смена разрешения и пресетов:** для вывода не 1080p (720p на Steam Deck, 1440p, 4K) вся игра
|
||||
рисуется в разрешении пресета, которое задаёт патч при запуске, — это самый быстрый путь.
|
||||
Смена вывода или пресета в игровом меню тогда требует «Применить и перезапустить игру».
|
||||
Настройка «Смена разрешения на лету» (лаунчер, игровое меню, `bbport.ini`
|
||||
`live_resolution=0|1|auto`; **по умолчанию выключена**) вместо этого оставляет игре 1080p и
|
||||
масштабирует её цели на ходу: 720p/1080p/1440p/4K и пресеты меняются без перезапуска. Это
|
||||
дороже: игра считает, что рисует 1080p, и рисует больше (например, мелких источников света
|
||||
примерно в 8 раз больше), часть целей копируется между размерами — включай только на мощных
|
||||
настольных видеокартах (`auto` включает её на дискретных GPU с 8+ ГБ, кроме NVIDIA старше
|
||||
Turing). Вывод 1080p и TAA всегда идут живым путём.
|
||||
|
||||
**TAA:** отдельный пункт сглаживания в лаунчере и игровом меню, переключается на ходу.
|
||||
Работает в разрешении вывода без модели FSR; пресет FSR сохраняется для обратного
|
||||
переключения. Native AA у FSR выполняет реконструкцию поверх полного разрешения и
|
||||
может быть тяжелее выключенного сглаживания. TAA тоже добавляет обработку кадра.
|
||||
|
||||
**Моды:** лаунчер подключает распакованные моды из отдельных папок (с `dvdroot_ps4/`, с лишней
|
||||
папкой-обёрткой или сразу с папками игры вроде `chr/`; регистр имён не важен), с переключателями
|
||||
и порядком загрузки. **Сторонние патчи:** XML-файлы в формате shadPS4 в папке `patches/` каталога
|
||||
данных, включаются в лаунчере. Подробнее — [моды и патчи](docs/MODS.md).
|
||||
**DLC:** дампы дополнений клади в `user/addcont/<title id>/<метка>/` (как в shadPS4, например
|
||||
`user/addcont/CUSA03173/SPEXPANSIONDLC03` для The Old Hunters): каждая такая папка сообщается
|
||||
игре как установленное дополнение.
|
||||
|
||||
**Язык лаунчера:** русский, английский, бразильский португальский или упрощённый китайский (по умолчанию — как в системе).
|
||||
|
||||
**Свободная камера и debug menu игры** (v1.09): включи соответствующие галочки в лаунчере
|
||||
или игровом меню и перезапусти игру. Для камеры используется
|
||||
[патч Lance McDonald из GoldHEN](https://github.com/GoldHEN/GoldHEN_Patch_Repository/blob/main/patches/xml/Bloodborne-Orbis.xml):
|
||||
удерживай Cross и нажимай L3 для переключения режимов (клавиатура: Space + Z).
|
||||
Камере шрифты не нужны; с патчем Enemy Control она несовместима.
|
||||
Для debug menu сначала установи `DbgFont14h.ccm` и `DbgFont14h.tpf` из
|
||||
[Debug Menu and XML Patch](https://www.nexusmods.com/bloodborne/mods/253) в
|
||||
`dvdroot_ps4/font/` внутри папки игры. Если файлов нет или они пустые, запуск остановится
|
||||
с объяснением. Меню открывается левым touchpad / Tab (с включённым debug menu левая половина
|
||||
больше не открывает жесты); правый touchpad — Backspace.
|
||||
Координаты касания передаются с SDL-геймпадов; Back/Select заменяет левый клик на геймпадах
|
||||
без сенсорной поверхности. Меню настроек порта по-прежнему открывается Insert / L3+R3.
|
||||
|
||||
GPU-запросы occlusion пока возвращают искусственные счётчики пикселей
|
||||
(`PixelPipeStatDump`), а `IT_SET_PREDICATION` не реализован. Свободная камера позволяет
|
||||
проверять видимость, но не добавляет GPU occlusion culling.
|
||||
|
||||
**Ассеты апскейлеров** (в репозитории их нет; FSR 3.1 они не нужны):
|
||||
|
||||
```bash
|
||||
bash tools/fetch_fsr4_assets.sh # FSR 4 v07 (MIT, собраны Q2RTX из исходников AMD)
|
||||
# FSR 4.1.1 — из своих DLL AMD (например, FSR4_LATEST у OptiScaler), нужен GE-Proton 10 или новее:
|
||||
bash tools/fsr4cap/build_assets.sh <amd_fidelityfx_upscaler_dx12.dll>
|
||||
```
|
||||
|
||||
**FSR 4.1.1 из своей DLL.** Проще всего — в лаунчере: *Апскейлер → FSR 4.1.1 из своей DLL AMD →
|
||||
Выбрать DLL…* и указать `amd_fidelityfx_upscaler_dx12.dll` версии 4.1.x: из папки `FSR4_LATEST`
|
||||
OptiScaler или из игры с FSR 4.1. Этого одного файла достаточно: DLL AMD сама экспортирует
|
||||
FidelityFX API, загрузчик не нужен (через `amd_fidelityfx_loader_dx12.dll` записывалась бы только
|
||||
DLL без этих функций, и тогда лаунчер его спросит). DLL проверяется сразу, без долгой записи, и не подходят: официальная FSR 4.0.x
|
||||
(например, 4.0.3 из Pragmata — AMD включает её только на RDNA4, под Proton на других видеокартах
|
||||
она не запускается) и сборки сообщества (4.0.2b, 4.1.1b и подобные INT8-сборки OptiScaler:
|
||||
другая модель и число проходов). Дальше DLL запускается под Proton и записывается в 20
|
||||
разрешениях (ход виден в строке и во вкладке «Журнал»; на RDNA4 — второй раз, для варианта FP8), проходы переводятся в SPIR-V, и FSR 4.1.1
|
||||
сразу выбирается. Proton выбирается сам — первый, под которым DLL включает FSR 4.1: GE-Proton 10
|
||||
или новее, Proton-CachyOS, Proton Experimental (проверено: GE-Proton 11, Proton-CachyOS 11,
|
||||
Experimental от октября 2026; Proton 11.0 из Steam и GE-Proton 9 — нет). Он запускается в
|
||||
рантайме Steam, который требует (Steam ставит его при первом запуске любой игры с этим Proton),
|
||||
или через umu-launcher, если он установлен. На NixOS umu-launcher берётся из `nix-shell` (в первый
|
||||
раз скачивается, ~1,7 ГБ); AppImage запускает запись на самой системе, через systemd пользователя
|
||||
(`systemd-run --user`), так как системный `/nix` ей не виден. Запись на Steam Deck пока не проверена; если DLL не включит
|
||||
там FSR 4.1, собери на ПК и скопируй папку `fsr4_411`. То же из командной строки:
|
||||
`--build-fsr411 <DLL>` у лаунчера (и у AppImage).
|
||||
|
||||
**DLSS (NVIDIA GeForce RTX).** SDK DLSS от NVIDIA не входит в репозиторий: скачай
|
||||
[github.com/NVIDIA/DLSS](https://github.com/NVIDIA/DLSS) (достаточно `include/` и
|
||||
`lib/Linux_x86_64`) и собери с `DLSS_SDK_ROOT=/путь/к/DLSS bash build.sh`. Тогда собирается и мост
|
||||
`out/libbbport_dlss.so` (`gpu/dlss_bridge`, единственный код, использующий SDK), а
|
||||
`libnvidia-ngx-dlss.so.<версия>` от NVIDIA копируется рядом с `out/bb-probe`; остальное даёт
|
||||
драйвер. Затем выбери *DLSS* в лаунчере или во внутриигровом меню: пресет задаёт разрешение
|
||||
рендера, Native AA — это DLAA. DLSS получает те же входы, что и FSR 3.1 (цвет сцены, глубину,
|
||||
векторы движения, 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) выбирается в лаунчере:
|
||||
*Апскейлер → DLSS: библиотека NVIDIA → Выбрать файл…*, как DLL для FSR 4.1.1; она копируется в
|
||||
`<user>/dlss/`, и игра берёт её первой. `nvngx_dlss.dll` из игр — библиотека для Windows: драйвер
|
||||
NVIDIA на Linux её не загружает.
|
||||
|
||||
Скрипт из исходников берёт инструменты из системы (MinGW GCC, CMake, Ninja, Python 3,
|
||||
SPIRV-Tools, Git) или из Nix; в AppImage они уже собраны.
|
||||
|
||||
**FP8 на RDNA4.** Когда vkd3d-proton даёт кооперативные матрицы FP8 (RDNA4), DLL запускает
|
||||
другой вариант своих проходов: другие шейдеры и веса, другие размеры диспатчей. Сборка
|
||||
записывает оба: INT8 в `fsr4_411/`, FP8 в `fsr4_411/fp8/`, а игра сама выбирает FP8, если у
|
||||
видеокарты есть матрицы FP8 (`VK_EXT_shader_float8`); в журнале — `Upscaler: FSR 4.1.1 replay,
|
||||
FP8 …`. `BB_FSR411_VARIANT=int8` — принудительно INT8. Проверено на RX 7800 XT через эмуляцию FP8
|
||||
на FP16 в vkd3d-proton (`BB_FSR4CAP_FP8=1` записывает его и там, `fp8emu/`): побитово как DLL;
|
||||
**на самой RDNA4 ещё не запускалось.**
|
||||
|
||||
**AppImage** (Steam Deck): `bash build.sh && bash packaging/appimage.sh` →
|
||||
`dist/Bloodborne-bbport-x86_64.AppImage`; данные в `~/.local/share/bbport`, `--play` запускает
|
||||
игру без окна лаунчера (игровой режим). Модели FSR 4.1.1 в пакет не входят: собери их кнопкой
|
||||
лаунчера (см. выше), они лягут в `~/.local/share/bbport/fsr4_411` (`BB_PACKAGE_FSR411=1` кладёт
|
||||
локальную `fsr4_411` в AppImage для своих устройств). На Steam Deck выбирай вывод 1280×720 (игра 16:9, на
|
||||
экране 1280×800 будут узкие полосы).
|
||||
|
||||
**Добавление AppImage в Steam** («Добавить стороннюю игру»): ничего настраивать не нужно,
|
||||
инструмент совместимости не важен. (Steam подгружает свой оверлей во все сторонние игры; AppImage
|
||||
убирает его до запуска своих программ, поэтому оверлея Steam в игре нет.) Если Steam запускает игры без FUSE (NixOS: FHS-песочница Steam; AppImage тогда завершается с
|
||||
*Cannot mount AppImage*), в параметрах запуска укажи
|
||||
|
||||
```
|
||||
TMPDIR=$HOME/.cache APPIMAGE_EXTRACT_AND_RUN=1 NO_CLEANUP=1 %command%
|
||||
```
|
||||
|
||||
Тогда AppImage при первом запуске распакуется (~2 ГБ, `~/.cache/appimage_extracted_*`, ~10 с),
|
||||
а дальше будет запускать эту копию; новая версия AppImage распакуется заново, старую копию можно
|
||||
удалить. Без `TMPDIR` распаковка шла бы в `/tmp` Steam, а он там в оперативной памяти.
|
||||
` --play` после `%command%` — запуск без лаунчера.
|
||||
|
||||
**NVIDIA в AppImage:** пакет автоматически ищет установленный на машине 64-битный NVIDIA
|
||||
Vulkan-драйвер и подключает его вместе с комплектными AMD/Intel-драйверами. Используются
|
||||
библиотеки NVIDIA с этой машины, чтобы их версия совпадала с модулем ядра. На обычных Linux
|
||||
дистрибутивах они доступны в `/usr/lib*`; на NixOS внутренний `/nix/store` AppImage может скрывать
|
||||
драйвер хоста. В этом случае библиотеки NVIDIA нужно скопировать в доступную папку и указать
|
||||
`BB_NVIDIA_LIB_DIR=/путь/к/библиотекам` (манифест NVIDIA тоже должен быть доступен).
|
||||
С драйвером NVIDIA хоста AppImage передаёт и файлы EGL-вендоров хоста, NVIDIA первым
|
||||
(`__EGL_VENDOR_LIBRARY_FILENAMES`; без них ICD NVIDIA на некоторых системах не отдавал
|
||||
`vkCreateInstance`, #107; `BB_NVIDIA_EGL=0` — выключить). Явные `VK_DRIVER_FILES`/`VK_ICD_FILENAMES` сохраняются. Проверка драйвера внутри пакета:
|
||||
|
||||
```bash
|
||||
./Bloodborne-bbport-x86_64.AppImage --vulkan-info 2>&1 | tee bbport-vulkan.log
|
||||
```
|
||||
|
||||
Пользователь подтвердил запуск с FSR 3 на GTX 1060 6GB (Fedora 44, NVIDIA 580.178.04);
|
||||
выбор FSR 4 приводил к чёрному окну. На этой конфигурации используй FSR 3.
|
||||
|
||||
MangoHud включён в AppImage; включи его галочку в лаунчере. Если MangoHud установлен ещё и в
|
||||
системе или включён оверлей производительности Steam (игровой режим Steam Deck), рисуется только
|
||||
один оверлей (два давали двоящийся текст со сдвигом).
|
||||
При запуске из исходников MangoHud нужно установить отдельно. Диагностический запуск
|
||||
с `VK_LOADER_LAYERS_DISABLE=~implicit~` отключает и MangoHud.
|
||||
|
||||
Полезные переменные: `BB_FRAME_STATS=1` (статистика кадров, в том числе строка `Memory:` —
|
||||
VRAM, GTT, RSS, текстуры и блоки памяти игры в VRAM), `BB_PC_MODEL=1` (новая модель памяти и трансляции;
|
||||
по умолчанию 0 — старая модель, как в 0.3), `BB_ANISO=N` (анизотропная фильтрация текстур сцены; по умолчанию
|
||||
16, 0 — как в игре),
|
||||
`BB_GC_IDLE_SECONDS=N` / `BB_VRAM_IDLE_SECONDS=N` (сколько неиспользуемые текстуры / буферы
|
||||
остаются в VRAM; 20 / 60), `BB_BREADCRUMBS=0` (без меток GPU; с ними зависание видеокарты
|
||||
называет отрисовку или dispatch, на которой она встала), `BB_GPU_PROFILE=1` (время GPU по
|
||||
проходам), `BB_FSR4_PROFILE=1` (время каждого прохода FSR 4), `BB_UPSCALER=taa|fsr3|fsr4|fsr411|off|none`,
|
||||
`BB_FRAMES_AHEAD=N` (на сколько кадров поток команд может опережать GPU; по умолчанию 1, 0 — без
|
||||
ограничения), `BB_PRESENT_THREAD=0` (показ в потоке vblank, как раньше), `BB_LIVE_RES=1` (смена
|
||||
разрешения без перезапуска вместо патча при запуске для вывода не 1080p),
|
||||
`BB_PAD_RECORD=файл` / `BB_PAD_REPLAY=файл` (запись маршрута по F9 и его повтор в тестах),
|
||||
`BB_GC_BUDGET_MB=N` (бюджет кэша текстур, как на встроенных GPU), `BB_VRAM_LIMIT_MB=N` (бюджет
|
||||
VRAM считается не больше N МиБ: проверка поведения карт с малой VRAM), `BB_ASYNC_PIPELINES=0`
|
||||
(новые конвейеры компилируются сразу, игра ждёт; по умолчанию — в фоне), `BB_PRESENT_DUMP_TRIGGER=файл`
|
||||
с `BB_PRESENT_DUMP_COUNT=N` (снимок N показанных кадров подряд),
|
||||
`BB_FSR411_VARIANT=int8|fp8|fp8emu` (вариант FSR 4.1.1; по умолчанию FP8, где у видеокарты есть
|
||||
матрицы FP8), `BB_READBACKS=0|1|2` (чтение игрой памяти, записанной GPU: по умолчанию 1, 2 —
|
||||
точно и медленно, 0 — выключено).
|
||||
Подробнее — в [docs/](docs); последние изменения — [docs/CHANGES_0.4.md](docs/CHANGES_0.4.md),
|
||||
[docs/CHANGES_2026-10-06.md](docs/CHANGES_2026-10-06.md).
|
||||
|
||||
**Игра по сети (shadNet).** Отдельный модуль `out/gpu/libbbnet.so` (`gpu/bbnet`: сетевые и PSN-библиотеки
|
||||
shadPS4 из shadp2p, форка shadPS4, с клиентом shadNet); собирается, когда есть protobuf, nlohmann_json,
|
||||
OpenSSL, zlib и miniupnpc (`shell.nix`), входит в AppImage. Включается на странице лаунчера *Онлайн*
|
||||
(`BB_ONLINE=1`): сообщения, пятна крови и призраки — с The Hunter's Dream, колокола и призывы — через
|
||||
сервер shadNet с учётной записью shadNet (Online ID и пароль): пул The Hunter's Dream через bbbridge
|
||||
или свой сервер (`docs/SERVER.md`); публичный сервер shadPS4 (`srv.shadps4.net:31313`) больше не
|
||||
принимает сборки bbport.
|
||||
Офлайн модуль не загружается; сборка без него играет только офлайн.
|
||||
|
||||
## Устройство репозитория
|
||||
|
||||
| Путь | Содержимое |
|
||||
|---|---|
|
||||
| `src/` | Загрузчик (`probe.c`) и HLE-рантайм |
|
||||
| `scripts/` | Подготовка образа игры, линковка модулей, компилятор патчей |
|
||||
| `gpu/` | Библиотека рендерера: видеоядро shadPS4 с правками порта (`gpu/VENDOR.txt`), прослойки, меню ImGui, рантайм FSR 4.1.1 (`gpu/shadps4/video_core/renderer_vulkan/fsr411`) |
|
||||
| `launcher/`, `packaging/` | Лаунчер GTK4; пакет Nix и AppImage |
|
||||
| `patches/` | Патчи сообщества для Bloodborne |
|
||||
| `tools/` | Утилиты разработчика: скриптовые запуски, A/B-переключатели, бенчмарк FSR, переписывание шейдеров FSR 4, `fsr4cap` (запись и извлечение FSR 4.1.1) |
|
||||
| `tests/` | Тесты загрузчика, рантайма, патчей и рендерера |
|
||||
| `docs/` | Заметки и замеры ([апскейлер](docs/upscaler.md), [многопоточный GPU](docs/parallel_gpu.md), [векторы движения](docs/motion_vectors.md), [план](docs/ROADMAP.md), [журнал разработки](docs/DEVELOPMENT_LOG.ru.md)) |
|
||||
|
||||
Тесты: `bash build.sh --test`, `python3 -m unittest discover -s tests` и
|
||||
`ninja -C out/gpu motion-history-test ui-composition-test scene-resolution-test motion-shader-test settings-test`.
|
||||
|
||||
## Что будет дальше
|
||||
|
||||
- Новая модель памяти и трансляции по умолчанию и на NVIDIA, когда она станет там не медленнее;
|
||||
после этого старая модель удаляется.
|
||||
- Текстуры — объекты, которые создаются при загрузке (меньше угадывания по адресам, загрузка
|
||||
вне кадра).
|
||||
- Шейдеры переводятся заранее, при установке, а не во время игры.
|
||||
- Больше параллельности CPU в обработке GPU-команд (дальнейшее разделение стадии записи draw)
|
||||
с масштабированием на все потоки — важнее всего для Steam Deck.
|
||||
- Async compute для апскейлера (в 4K кадр упирается в GPU).
|
||||
- XeSS (суперразрешение) и генерация кадров XeFG через помощник под Wine с общей памятью Vulkan
|
||||
(прототип моста памяти — `tools/bridge_helper`); входы апскейлера
|
||||
в виде, совместимом с подходом OptiScaler.
|
||||
- Генерация кадров (сначала FSR 3.1 FG), маски реактивности и прозрачности для частиц и тумана.
|
||||
- Исправление гонок в шейдерах FSR 4.1.1 от AMD при ширине вывода, не кратной 64 (например,
|
||||
1600×900) — так же, как уже исправлена гонка у левого края в FSR 4 v07 при 1080p.
|
||||
- Проверка AppImage на Steam Deck; вывод HDR.
|
||||
|
||||
## Благодарности и лицензии
|
||||
|
||||
bbport распространяется по **GNU GPL v2 или новее** ([LICENSE](LICENSE)) — в нём код shadPS4
|
||||
(GPL-2.0-or-later). Сторонние компоненты сохраняют свои лицензии:
|
||||
видеоядро и рекомпилятор шейдеров [shadPS4](https://github.com/shadps4-emu/shadPS4) (GPL-2.0+),
|
||||
[sirit](https://github.com/shadps4-emu/sirit), [half](https://half.sourceforge.net/),
|
||||
[FSR-Vulkan](https://github.com/FireBurn/FSR-Vulkan) от FireBurn (MIT; FSR 3.1 на Vulkan и
|
||||
провайдер FSR 4 v07), AMD FidelityFX SDK (MIT), [LibAtrac9](https://github.com/Thealexbarney/LibAtrac9)
|
||||
(MIT), [Dear ImGui](https://github.com/ocornut/imgui) (MIT), шрифты DejaVu,
|
||||
[dxil-spirv](https://github.com/HansKristian-Work/dxil-spirv) (MIT, для сборки ассетов FSR 4.1.1).
|
||||
Патчи игры — Kyo, Lance McDonald, auser1337, illusion, emoose и другие участники сообщества
|
||||
(`patches/Bloodborne.xml`). Мост DLSS (`gpu/dlss_bridge`, MIT) и загрузчик взяты из
|
||||
[Windows-порта Supermedo](https://github.com/Supermedo/bloodborne_pc), адаптированного из
|
||||
[IFreemz/shadPS4-Bloodborne-DLSS-FSR](https://github.com/IFreemz/shadPS4-Bloodborne-DLSS-FSR).
|
||||
DLL и данные модели FSR 4 от AMD, а также SDK и библиотеки DLSS от NVIDIA здесь не
|
||||
распространяются.
|
||||
@@ -1,281 +1,622 @@
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
Version 2, June 1991
|
||||
Version 3, 29 June 2007
|
||||
|
||||
Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
|
||||
<https://fsf.org/>
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
Preamble
|
||||
|
||||
The licenses for most software are designed to take away your
|
||||
freedom to share and change it. By contrast, the GNU General Public
|
||||
License is intended to guarantee your freedom to share and change free
|
||||
software--to make sure the software is free for all its users. This
|
||||
General Public License applies to most of the Free Software
|
||||
Foundation's software and to any other program whose authors commit to
|
||||
using it. (Some other Free Software Foundation software is covered by
|
||||
the GNU Lesser General Public License instead.) You can apply it to
|
||||
The GNU General Public License is a free, copyleft license for
|
||||
software and other kinds of works.
|
||||
|
||||
The licenses for most software and other practical works are designed
|
||||
to take away your freedom to share and change the works. By contrast,
|
||||
the GNU General Public License is intended to guarantee your freedom to
|
||||
share and change all versions of a program--to make sure it remains free
|
||||
software for all its users. We, the Free Software Foundation, use the
|
||||
GNU General Public License for most of our software; it applies also to
|
||||
any other work released this way by its authors. You can apply it to
|
||||
your programs, too.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
have the freedom to distribute copies of free software (and charge for
|
||||
this service if you wish), that you receive source code or can get it
|
||||
if you want it, that you can change the software or use pieces of it
|
||||
in new free programs; and that you know you can do these things.
|
||||
them if you wish), that you receive source code or can get it if you
|
||||
want it, that you can change the software or use pieces of it in new
|
||||
free programs, and that you know you can do these things.
|
||||
|
||||
To protect your rights, we need to make restrictions that forbid
|
||||
anyone to deny you these rights or to ask you to surrender the rights.
|
||||
These restrictions translate to certain responsibilities for you if you
|
||||
distribute copies of the software, or if you modify it.
|
||||
To protect your rights, we need to prevent others from denying you
|
||||
these rights or asking you to surrender the rights. Therefore, you have
|
||||
certain responsibilities if you distribute copies of the software, or if
|
||||
you modify it: responsibilities to respect the freedom of others.
|
||||
|
||||
For example, if you distribute copies of such a program, whether
|
||||
gratis or for a fee, you must give the recipients all the rights that
|
||||
you have. You must make sure that they, too, receive or can get the
|
||||
source code. And you must show them these terms so they know their
|
||||
rights.
|
||||
gratis or for a fee, you must pass on to the recipients the same
|
||||
freedoms that you received. You must make sure that they, too, receive
|
||||
or can get the source code. And you must show them these terms so they
|
||||
know their rights.
|
||||
|
||||
We protect your rights with two steps: (1) copyright the software, and
|
||||
(2) offer you this license which gives you legal permission to copy,
|
||||
distribute and/or modify the software.
|
||||
Developers that use the GNU GPL protect your rights with two steps:
|
||||
(1) assert copyright on the software, and (2) offer you this License
|
||||
giving you legal permission to copy, distribute and/or modify it.
|
||||
|
||||
Also, for each author's protection and ours, we want to make certain
|
||||
that everyone understands that there is no warranty for this free
|
||||
software. If the software is modified by someone else and passed on, we
|
||||
want its recipients to know that what they have is not the original, so
|
||||
that any problems introduced by others will not reflect on the original
|
||||
authors' reputations.
|
||||
For the developers' and authors' protection, the GPL clearly explains
|
||||
that there is no warranty for this free software. For both users' and
|
||||
authors' sake, the GPL requires that modified versions be marked as
|
||||
changed, so that their problems will not be attributed erroneously to
|
||||
authors of previous versions.
|
||||
|
||||
Finally, any free program is threatened constantly by software
|
||||
patents. We wish to avoid the danger that redistributors of a free
|
||||
program will individually obtain patent licenses, in effect making the
|
||||
program proprietary. To prevent this, we have made it clear that any
|
||||
patent must be licensed for everyone's free use or not licensed at all.
|
||||
Some devices are designed to deny users access to install or run
|
||||
modified versions of the software inside them, although the manufacturer
|
||||
can do so. This is fundamentally incompatible with the aim of
|
||||
protecting users' freedom to change the software. The systematic
|
||||
pattern of such abuse occurs in the area of products for individuals to
|
||||
use, which is precisely where it is most unacceptable. Therefore, we
|
||||
have designed this version of the GPL to prohibit the practice for those
|
||||
products. If such problems arise substantially in other domains, we
|
||||
stand ready to extend this provision to those domains in future versions
|
||||
of the GPL, as needed to protect the freedom of users.
|
||||
|
||||
Finally, every program is threatened constantly by software patents.
|
||||
States should not allow patents to restrict development and use of
|
||||
software on general-purpose computers, but in those that do, we wish to
|
||||
avoid the special danger that patents applied to a free program could
|
||||
make it effectively proprietary. To prevent this, the GPL assures that
|
||||
patents cannot be used to render the program non-free.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
|
||||
TERMS AND CONDITIONS
|
||||
|
||||
0. This License applies to any program or other work which contains
|
||||
a notice placed by the copyright holder saying it may be distributed
|
||||
under the terms of this General Public License. The "Program", below,
|
||||
refers to any such program or work, and a "work based on the Program"
|
||||
means either the Program or any derivative work under copyright law:
|
||||
that is to say, a work containing the Program or a portion of it,
|
||||
either verbatim or with modifications and/or translated into another
|
||||
language. (Hereinafter, translation is included without limitation in
|
||||
the term "modification".) Each licensee is addressed as "you".
|
||||
0. Definitions.
|
||||
|
||||
Activities other than copying, distribution and modification are not
|
||||
covered by this License; they are outside its scope. The act of
|
||||
running the Program is not restricted, and the output from the Program
|
||||
is covered only if its contents constitute a work based on the
|
||||
Program (independent of having been made by running the Program).
|
||||
Whether that is true depends on what the Program does.
|
||||
"This License" refers to version 3 of the GNU General Public License.
|
||||
|
||||
1. You may copy and distribute verbatim copies of the Program's
|
||||
source code as you receive it, in any medium, provided that you
|
||||
conspicuously and appropriately publish on each copy an appropriate
|
||||
copyright notice and disclaimer of warranty; keep intact all the
|
||||
notices that refer to this License and to the absence of any warranty;
|
||||
and give any other recipients of the Program a copy of this License
|
||||
along with the Program.
|
||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||
works, such as semiconductor masks.
|
||||
|
||||
You may charge a fee for the physical act of transferring a copy, and
|
||||
you may at your option offer warranty protection in exchange for a fee.
|
||||
"The Program" refers to any copyrightable work licensed under this
|
||||
License. Each licensee is addressed as "you". "Licensees" and
|
||||
"recipients" may be individuals or organizations.
|
||||
|
||||
2. You may modify your copy or copies of the Program or any portion
|
||||
of it, thus forming a work based on the Program, and copy and
|
||||
distribute such modifications or work under the terms of Section 1
|
||||
above, provided that you also meet all of these conditions:
|
||||
To "modify" a work means to copy from or adapt all or part of the work
|
||||
in a fashion requiring copyright permission, other than the making of an
|
||||
exact copy. The resulting work is called a "modified version" of the
|
||||
earlier work or a work "based on" the earlier work.
|
||||
|
||||
a) You must cause the modified files to carry prominent notices
|
||||
stating that you changed the files and the date of any change.
|
||||
A "covered work" means either the unmodified Program or a work based
|
||||
on the Program.
|
||||
|
||||
b) You must cause any work that you distribute or publish, that in
|
||||
whole or in part contains or is derived from the Program or any
|
||||
part thereof, to be licensed as a whole at no charge to all third
|
||||
parties under the terms of this License.
|
||||
To "propagate" a work means to do anything with it that, without
|
||||
permission, would make you directly or secondarily liable for
|
||||
infringement under applicable copyright law, except executing it on a
|
||||
computer or modifying a private copy. Propagation includes copying,
|
||||
distribution (with or without modification), making available to the
|
||||
public, and in some countries other activities as well.
|
||||
|
||||
c) If the modified program normally reads commands interactively
|
||||
when run, you must cause it, when started running for such
|
||||
interactive use in the most ordinary way, to print or display an
|
||||
announcement including an appropriate copyright notice and a
|
||||
notice that there is no warranty (or else, saying that you provide
|
||||
a warranty) and that users may redistribute the program under
|
||||
these conditions, and telling the user how to view a copy of this
|
||||
License. (Exception: if the Program itself is interactive but
|
||||
does not normally print such an announcement, your work based on
|
||||
the Program is not required to print an announcement.)
|
||||
To "convey" a work means any kind of propagation that enables other
|
||||
parties to make or receive copies. Mere interaction with a user through
|
||||
a computer network, with no transfer of a copy, is not conveying.
|
||||
|
||||
These requirements apply to the modified work as a whole. If
|
||||
identifiable sections of that work are not derived from the Program,
|
||||
and can be reasonably considered independent and separate works in
|
||||
themselves, then this License, and its terms, do not apply to those
|
||||
sections when you distribute them as separate works. But when you
|
||||
distribute the same sections as part of a whole which is a work based
|
||||
on the Program, the distribution of the whole must be on the terms of
|
||||
this License, whose permissions for other licensees extend to the
|
||||
entire whole, and thus to each and every part regardless of who wrote it.
|
||||
An interactive user interface displays "Appropriate Legal Notices"
|
||||
to the extent that it includes a convenient and prominently visible
|
||||
feature that (1) displays an appropriate copyright notice, and (2)
|
||||
tells the user that there is no warranty for the work (except to the
|
||||
extent that warranties are provided), that licensees may convey the
|
||||
work under this License, and how to view a copy of this License. If
|
||||
the interface presents a list of user commands or options, such as a
|
||||
menu, a prominent item in the list meets this criterion.
|
||||
|
||||
Thus, it is not the intent of this section to claim rights or contest
|
||||
your rights to work written entirely by you; rather, the intent is to
|
||||
exercise the right to control the distribution of derivative or
|
||||
collective works based on the Program.
|
||||
1. Source Code.
|
||||
|
||||
In addition, mere aggregation of another work not based on the Program
|
||||
with the Program (or with a work based on the Program) on a volume of
|
||||
a storage or distribution medium does not bring the other work under
|
||||
the scope of this License.
|
||||
The "source code" for a work means the preferred form of the work
|
||||
for making modifications to it. "Object code" means any non-source
|
||||
form of a work.
|
||||
|
||||
3. You may copy and distribute the Program (or a work based on it,
|
||||
under Section 2) in object code or executable form under the terms of
|
||||
Sections 1 and 2 above provided that you also do one of the following:
|
||||
A "Standard Interface" means an interface that either is an official
|
||||
standard defined by a recognized standards body, or, in the case of
|
||||
interfaces specified for a particular programming language, one that
|
||||
is widely used among developers working in that language.
|
||||
|
||||
a) Accompany it with the complete corresponding machine-readable
|
||||
source code, which must be distributed under the terms of Sections
|
||||
1 and 2 above on a medium customarily used for software interchange; or,
|
||||
The "System Libraries" of an executable work include anything, other
|
||||
than the work as a whole, that (a) is included in the normal form of
|
||||
packaging a Major Component, but which is not part of that Major
|
||||
Component, and (b) serves only to enable use of the work with that
|
||||
Major Component, or to implement a Standard Interface for which an
|
||||
implementation is available to the public in source code form. A
|
||||
"Major Component", in this context, means a major essential component
|
||||
(kernel, window system, and so on) of the specific operating system
|
||||
(if any) on which the executable work runs, or a compiler used to
|
||||
produce the work, or an object code interpreter used to run it.
|
||||
|
||||
b) Accompany it with a written offer, valid for at least three
|
||||
years, to give any third party, for a charge no more than your
|
||||
cost of physically performing source distribution, a complete
|
||||
machine-readable copy of the corresponding source code, to be
|
||||
distributed under the terms of Sections 1 and 2 above on a medium
|
||||
customarily used for software interchange; or,
|
||||
The "Corresponding Source" for a work in object code form means all
|
||||
the source code needed to generate, install, and (for an executable
|
||||
work) run the object code and to modify the work, including scripts to
|
||||
control those activities. However, it does not include the work's
|
||||
System Libraries, or general-purpose tools or generally available free
|
||||
programs which are used unmodified in performing those activities but
|
||||
which are not part of the work. For example, Corresponding Source
|
||||
includes interface definition files associated with source files for
|
||||
the work, and the source code for shared libraries and dynamically
|
||||
linked subprograms that the work is specifically designed to require,
|
||||
such as by intimate data communication or control flow between those
|
||||
subprograms and other parts of the work.
|
||||
|
||||
c) Accompany it with the information you received as to the offer
|
||||
to distribute corresponding source code. (This alternative is
|
||||
allowed only for noncommercial distribution and only if you
|
||||
received the program in object code or executable form with such
|
||||
an offer, in accord with Subsection b above.)
|
||||
The Corresponding Source need not include anything that users
|
||||
can regenerate automatically from other parts of the Corresponding
|
||||
Source.
|
||||
|
||||
The source code for a work means the preferred form of the work for
|
||||
making modifications to it. For an executable work, complete source
|
||||
code means all the source code for all modules it contains, plus any
|
||||
associated interface definition files, plus the scripts used to
|
||||
control compilation and installation of the executable. However, as a
|
||||
special exception, the source code distributed need not include
|
||||
anything that is normally distributed (in either source or binary
|
||||
form) with the major components (compiler, kernel, and so on) of the
|
||||
operating system on which the executable runs, unless that component
|
||||
itself accompanies the executable.
|
||||
The Corresponding Source for a work in source code form is that
|
||||
same work.
|
||||
|
||||
If distribution of executable or object code is made by offering
|
||||
access to copy from a designated place, then offering equivalent
|
||||
access to copy the source code from the same place counts as
|
||||
distribution of the source code, even though third parties are not
|
||||
compelled to copy the source along with the object code.
|
||||
2. Basic Permissions.
|
||||
|
||||
4. You may not copy, modify, sublicense, or distribute the Program
|
||||
except as expressly provided under this License. Any attempt
|
||||
otherwise to copy, modify, sublicense or distribute the Program is
|
||||
void, and will automatically terminate your rights under this License.
|
||||
However, parties who have received copies, or rights, from you under
|
||||
this License will not have their licenses terminated so long as such
|
||||
parties remain in full compliance.
|
||||
All rights granted under this License are granted for the term of
|
||||
copyright on the Program, and are irrevocable provided the stated
|
||||
conditions are met. This License explicitly affirms your unlimited
|
||||
permission to run the unmodified Program. The output from running a
|
||||
covered work is covered by this License only if the output, given its
|
||||
content, constitutes a covered work. This License acknowledges your
|
||||
rights of fair use or other equivalent, as provided by copyright law.
|
||||
|
||||
5. You are not required to accept this License, since you have not
|
||||
signed it. However, nothing else grants you permission to modify or
|
||||
distribute the Program or its derivative works. These actions are
|
||||
prohibited by law if you do not accept this License. Therefore, by
|
||||
modifying or distributing the Program (or any work based on the
|
||||
Program), you indicate your acceptance of this License to do so, and
|
||||
all its terms and conditions for copying, distributing or modifying
|
||||
the Program or works based on it.
|
||||
You may make, run and propagate covered works that you do not
|
||||
convey, without conditions so long as your license otherwise remains
|
||||
in force. You may convey covered works to others for the sole purpose
|
||||
of having them make modifications exclusively for you, or provide you
|
||||
with facilities for running those works, provided that you comply with
|
||||
the terms of this License in conveying all material for which you do
|
||||
not control copyright. Those thus making or running the covered works
|
||||
for you must do so exclusively on your behalf, under your direction
|
||||
and control, on terms that prohibit them from making any copies of
|
||||
your copyrighted material outside their relationship with you.
|
||||
|
||||
6. Each time you redistribute the Program (or any work based on the
|
||||
Program), the recipient automatically receives a license from the
|
||||
original licensor to copy, distribute or modify the Program subject to
|
||||
these terms and conditions. You may not impose any further
|
||||
restrictions on the recipients' exercise of the rights granted herein.
|
||||
You are not responsible for enforcing compliance by third parties to
|
||||
Conveying under any other circumstances is permitted solely under
|
||||
the conditions stated below. Sublicensing is not allowed; section 10
|
||||
makes it unnecessary.
|
||||
|
||||
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||
|
||||
No covered work shall be deemed part of an effective technological
|
||||
measure under any applicable law fulfilling obligations under article
|
||||
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||
similar laws prohibiting or restricting circumvention of such
|
||||
measures.
|
||||
|
||||
When you convey a covered work, you waive any legal power to forbid
|
||||
circumvention of technological measures to the extent such circumvention
|
||||
is effected by exercising rights under this License with respect to
|
||||
the covered work, and you disclaim any intention to limit operation or
|
||||
modification of the work as a means of enforcing, against the work's
|
||||
users, your or third parties' legal rights to forbid circumvention of
|
||||
technological measures.
|
||||
|
||||
4. Conveying Verbatim Copies.
|
||||
|
||||
You may convey verbatim copies of the Program's source code as you
|
||||
receive it, in any medium, provided that you conspicuously and
|
||||
appropriately publish on each copy an appropriate copyright notice;
|
||||
keep intact all notices stating that this License and any
|
||||
non-permissive terms added in accord with section 7 apply to the code;
|
||||
keep intact all notices of the absence of any warranty; and give all
|
||||
recipients a copy of this License along with the Program.
|
||||
|
||||
You may charge any price or no price for each copy that you convey,
|
||||
and you may offer support or warranty protection for a fee.
|
||||
|
||||
5. Conveying Modified Source Versions.
|
||||
|
||||
You may convey a work based on the Program, or the modifications to
|
||||
produce it from the Program, in the form of source code under the
|
||||
terms of section 4, provided that you also meet all of these conditions:
|
||||
|
||||
a) The work must carry prominent notices stating that you modified
|
||||
it, and giving a relevant date.
|
||||
|
||||
b) The work must carry prominent notices stating that it is
|
||||
released under this License and any conditions added under section
|
||||
7. This requirement modifies the requirement in section 4 to
|
||||
"keep intact all notices".
|
||||
|
||||
c) You must license the entire work, as a whole, under this
|
||||
License to anyone who comes into possession of a copy. This
|
||||
License will therefore apply, along with any applicable section 7
|
||||
additional terms, to the whole of the work, and all its parts,
|
||||
regardless of how they are packaged. This License gives no
|
||||
permission to license the work in any other way, but it does not
|
||||
invalidate such permission if you have separately received it.
|
||||
|
||||
d) If the work has interactive user interfaces, each must display
|
||||
Appropriate Legal Notices; however, if the Program has interactive
|
||||
interfaces that do not display Appropriate Legal Notices, your
|
||||
work need not make them do so.
|
||||
|
||||
A compilation of a covered work with other separate and independent
|
||||
works, which are not by their nature extensions of the covered work,
|
||||
and which are not combined with it such as to form a larger program,
|
||||
in or on a volume of a storage or distribution medium, is called an
|
||||
"aggregate" if the compilation and its resulting copyright are not
|
||||
used to limit the access or legal rights of the compilation's users
|
||||
beyond what the individual works permit. Inclusion of a covered work
|
||||
in an aggregate does not cause this License to apply to the other
|
||||
parts of the aggregate.
|
||||
|
||||
6. Conveying Non-Source Forms.
|
||||
|
||||
You may convey a covered work in object code form under the terms
|
||||
of sections 4 and 5, provided that you also convey the
|
||||
machine-readable Corresponding Source under the terms of this License,
|
||||
in one of these ways:
|
||||
|
||||
a) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by the
|
||||
Corresponding Source fixed on a durable physical medium
|
||||
customarily used for software interchange.
|
||||
|
||||
b) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by a
|
||||
written offer, valid for at least three years and valid for as
|
||||
long as you offer spare parts or customer support for that product
|
||||
model, to give anyone who possesses the object code either (1) a
|
||||
copy of the Corresponding Source for all the software in the
|
||||
product that is covered by this License, on a durable physical
|
||||
medium customarily used for software interchange, for a price no
|
||||
more than your reasonable cost of physically performing this
|
||||
conveying of source, or (2) access to copy the
|
||||
Corresponding Source from a network server at no charge.
|
||||
|
||||
c) Convey individual copies of the object code with a copy of the
|
||||
written offer to provide the Corresponding Source. This
|
||||
alternative is allowed only occasionally and noncommercially, and
|
||||
only if you received the object code with such an offer, in accord
|
||||
with subsection 6b.
|
||||
|
||||
d) Convey the object code by offering access from a designated
|
||||
place (gratis or for a charge), and offer equivalent access to the
|
||||
Corresponding Source in the same way through the same place at no
|
||||
further charge. You need not require recipients to copy the
|
||||
Corresponding Source along with the object code. If the place to
|
||||
copy the object code is a network server, the Corresponding Source
|
||||
may be on a different server (operated by you or a third party)
|
||||
that supports equivalent copying facilities, provided you maintain
|
||||
clear directions next to the object code saying where to find the
|
||||
Corresponding Source. Regardless of what server hosts the
|
||||
Corresponding Source, you remain obligated to ensure that it is
|
||||
available for as long as needed to satisfy these requirements.
|
||||
|
||||
e) Convey the object code using peer-to-peer transmission, provided
|
||||
you inform other peers where the object code and Corresponding
|
||||
Source of the work are being offered to the general public at no
|
||||
charge under subsection 6d.
|
||||
|
||||
A separable portion of the object code, whose source code is excluded
|
||||
from the Corresponding Source as a System Library, need not be
|
||||
included in conveying the object code work.
|
||||
|
||||
A "User Product" is either (1) a "consumer product", which means any
|
||||
tangible personal property which is normally used for personal, family,
|
||||
or household purposes, or (2) anything designed or sold for incorporation
|
||||
into a dwelling. In determining whether a product is a consumer product,
|
||||
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||
product received by a particular user, "normally used" refers to a
|
||||
typical or common use of that class of product, regardless of the status
|
||||
of the particular user or of the way in which the particular user
|
||||
actually uses, or expects or is expected to use, the product. A product
|
||||
is a consumer product regardless of whether the product has substantial
|
||||
commercial, industrial or non-consumer uses, unless such uses represent
|
||||
the only significant mode of use of the product.
|
||||
|
||||
"Installation Information" for a User Product means any methods,
|
||||
procedures, authorization keys, or other information required to install
|
||||
and execute modified versions of a covered work in that User Product from
|
||||
a modified version of its Corresponding Source. The information must
|
||||
suffice to ensure that the continued functioning of the modified object
|
||||
code is in no case prevented or interfered with solely because
|
||||
modification has been made.
|
||||
|
||||
If you convey an object code work under this section in, or with, or
|
||||
specifically for use in, a User Product, and the conveying occurs as
|
||||
part of a transaction in which the right of possession and use of the
|
||||
User Product is transferred to the recipient in perpetuity or for a
|
||||
fixed term (regardless of how the transaction is characterized), the
|
||||
Corresponding Source conveyed under this section must be accompanied
|
||||
by the Installation Information. But this requirement does not apply
|
||||
if neither you nor any third party retains the ability to install
|
||||
modified object code on the User Product (for example, the work has
|
||||
been installed in ROM).
|
||||
|
||||
The requirement to provide Installation Information does not include a
|
||||
requirement to continue to provide support service, warranty, or updates
|
||||
for a work that has been modified or installed by the recipient, or for
|
||||
the User Product in which it has been modified or installed. Access to a
|
||||
network may be denied when the modification itself materially and
|
||||
adversely affects the operation of the network or violates the rules and
|
||||
protocols for communication across the network.
|
||||
|
||||
Corresponding Source conveyed, and Installation Information provided,
|
||||
in accord with this section must be in a format that is publicly
|
||||
documented (and with an implementation available to the public in
|
||||
source code form), and must require no special password or key for
|
||||
unpacking, reading or copying.
|
||||
|
||||
7. Additional Terms.
|
||||
|
||||
"Additional permissions" are terms that supplement the terms of this
|
||||
License by making exceptions from one or more of its conditions.
|
||||
Additional permissions that are applicable to the entire Program shall
|
||||
be treated as though they were included in this License, to the extent
|
||||
that they are valid under applicable law. If additional permissions
|
||||
apply only to part of the Program, that part may be used separately
|
||||
under those permissions, but the entire Program remains governed by
|
||||
this License without regard to the additional permissions.
|
||||
|
||||
When you convey a copy of a covered work, you may at your option
|
||||
remove any additional permissions from that copy, or from any part of
|
||||
it. (Additional permissions may be written to require their own
|
||||
removal in certain cases when you modify the work.) You may place
|
||||
additional permissions on material, added by you to a covered work,
|
||||
for which you have or can give appropriate copyright permission.
|
||||
|
||||
Notwithstanding any other provision of this License, for material you
|
||||
add to a covered work, you may (if authorized by the copyright holders of
|
||||
that material) supplement the terms of this License with terms:
|
||||
|
||||
a) Disclaiming warranty or limiting liability differently from the
|
||||
terms of sections 15 and 16 of this License; or
|
||||
|
||||
b) Requiring preservation of specified reasonable legal notices or
|
||||
author attributions in that material or in the Appropriate Legal
|
||||
Notices displayed by works containing it; or
|
||||
|
||||
c) Prohibiting misrepresentation of the origin of that material, or
|
||||
requiring that modified versions of such material be marked in
|
||||
reasonable ways as different from the original version; or
|
||||
|
||||
d) Limiting the use for publicity purposes of names of licensors or
|
||||
authors of the material; or
|
||||
|
||||
e) Declining to grant rights under trademark law for use of some
|
||||
trade names, trademarks, or service marks; or
|
||||
|
||||
f) Requiring indemnification of licensors and authors of that
|
||||
material by anyone who conveys the material (or modified versions of
|
||||
it) with contractual assumptions of liability to the recipient, for
|
||||
any liability that these contractual assumptions directly impose on
|
||||
those licensors and authors.
|
||||
|
||||
All other non-permissive additional terms are considered "further
|
||||
restrictions" within the meaning of section 10. If the Program as you
|
||||
received it, or any part of it, contains a notice stating that it is
|
||||
governed by this License along with a term that is a further
|
||||
restriction, you may remove that term. If a license document contains
|
||||
a further restriction but permits relicensing or conveying under this
|
||||
License, you may add to a covered work material governed by the terms
|
||||
of that license document, provided that the further restriction does
|
||||
not survive such relicensing or conveying.
|
||||
|
||||
If you add terms to a covered work in accord with this section, you
|
||||
must place, in the relevant source files, a statement of the
|
||||
additional terms that apply to those files, or a notice indicating
|
||||
where to find the applicable terms.
|
||||
|
||||
Additional terms, permissive or non-permissive, may be stated in the
|
||||
form of a separately written license, or stated as exceptions;
|
||||
the above requirements apply either way.
|
||||
|
||||
8. Termination.
|
||||
|
||||
You may not propagate or modify a covered work except as expressly
|
||||
provided under this License. Any attempt otherwise to propagate or
|
||||
modify it is void, and will automatically terminate your rights under
|
||||
this License (including any patent licenses granted under the third
|
||||
paragraph of section 11).
|
||||
|
||||
However, if you cease all violation of this License, then your
|
||||
license from a particular copyright holder is reinstated (a)
|
||||
provisionally, unless and until the copyright holder explicitly and
|
||||
finally terminates your license, and (b) permanently, if the copyright
|
||||
holder fails to notify you of the violation by some reasonable means
|
||||
prior to 60 days after the cessation.
|
||||
|
||||
Moreover, your license from a particular copyright holder is
|
||||
reinstated permanently if the copyright holder notifies you of the
|
||||
violation by some reasonable means, this is the first time you have
|
||||
received notice of violation of this License (for any work) from that
|
||||
copyright holder, and you cure the violation prior to 30 days after
|
||||
your receipt of the notice.
|
||||
|
||||
Termination of your rights under this section does not terminate the
|
||||
licenses of parties who have received copies or rights from you under
|
||||
this License. If your rights have been terminated and not permanently
|
||||
reinstated, you do not qualify to receive new licenses for the same
|
||||
material under section 10.
|
||||
|
||||
9. Acceptance Not Required for Having Copies.
|
||||
|
||||
You are not required to accept this License in order to receive or
|
||||
run a copy of the Program. Ancillary propagation of a covered work
|
||||
occurring solely as a consequence of using peer-to-peer transmission
|
||||
to receive a copy likewise does not require acceptance. However,
|
||||
nothing other than this License grants you permission to propagate or
|
||||
modify any covered work. These actions infringe copyright if you do
|
||||
not accept this License. Therefore, by modifying or propagating a
|
||||
covered work, you indicate your acceptance of this License to do so.
|
||||
|
||||
10. Automatic Licensing of Downstream Recipients.
|
||||
|
||||
Each time you convey a covered work, the recipient automatically
|
||||
receives a license from the original licensors, to run, modify and
|
||||
propagate that work, subject to this License. You are not responsible
|
||||
for enforcing compliance by third parties with this License.
|
||||
|
||||
An "entity transaction" is a transaction transferring control of an
|
||||
organization, or substantially all assets of one, or subdividing an
|
||||
organization, or merging organizations. If propagation of a covered
|
||||
work results from an entity transaction, each party to that
|
||||
transaction who receives a copy of the work also receives whatever
|
||||
licenses to the work the party's predecessor in interest had or could
|
||||
give under the previous paragraph, plus a right to possession of the
|
||||
Corresponding Source of the work from the predecessor in interest, if
|
||||
the predecessor has it or can get it with reasonable efforts.
|
||||
|
||||
You may not impose any further restrictions on the exercise of the
|
||||
rights granted or affirmed under this License. For example, you may
|
||||
not impose a license fee, royalty, or other charge for exercise of
|
||||
rights granted under this License, and you may not initiate litigation
|
||||
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||
any patent claim is infringed by making, using, selling, offering for
|
||||
sale, or importing the Program or any portion of it.
|
||||
|
||||
11. Patents.
|
||||
|
||||
A "contributor" is a copyright holder who authorizes use under this
|
||||
License of the Program or a work on which the Program is based. The
|
||||
work thus licensed is called the contributor's "contributor version".
|
||||
|
||||
A contributor's "essential patent claims" are all patent claims
|
||||
owned or controlled by the contributor, whether already acquired or
|
||||
hereafter acquired, that would be infringed by some manner, permitted
|
||||
by this License, of making, using, or selling its contributor version,
|
||||
but do not include claims that would be infringed only as a
|
||||
consequence of further modification of the contributor version. For
|
||||
purposes of this definition, "control" includes the right to grant
|
||||
patent sublicenses in a manner consistent with the requirements of
|
||||
this License.
|
||||
|
||||
7. If, as a consequence of a court judgment or allegation of patent
|
||||
infringement or for any other reason (not limited to patent issues),
|
||||
conditions are imposed on you (whether by court order, agreement or
|
||||
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||
patent license under the contributor's essential patent claims, to
|
||||
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||
propagate the contents of its contributor version.
|
||||
|
||||
In the following three paragraphs, a "patent license" is any express
|
||||
agreement or commitment, however denominated, not to enforce a patent
|
||||
(such as an express permission to practice a patent or covenant not to
|
||||
sue for patent infringement). To "grant" such a patent license to a
|
||||
party means to make such an agreement or commitment not to enforce a
|
||||
patent against the party.
|
||||
|
||||
If you convey a covered work, knowingly relying on a patent license,
|
||||
and the Corresponding Source of the work is not available for anyone
|
||||
to copy, free of charge and under the terms of this License, through a
|
||||
publicly available network server or other readily accessible means,
|
||||
then you must either (1) cause the Corresponding Source to be so
|
||||
available, or (2) arrange to deprive yourself of the benefit of the
|
||||
patent license for this particular work, or (3) arrange, in a manner
|
||||
consistent with the requirements of this License, to extend the patent
|
||||
license to downstream recipients. "Knowingly relying" means you have
|
||||
actual knowledge that, but for the patent license, your conveying the
|
||||
covered work in a country, or your recipient's use of the covered work
|
||||
in a country, would infringe one or more identifiable patents in that
|
||||
country that you have reason to believe are valid.
|
||||
|
||||
If, pursuant to or in connection with a single transaction or
|
||||
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||
covered work, and grant a patent license to some of the parties
|
||||
receiving the covered work authorizing them to use, propagate, modify
|
||||
or convey a specific copy of the covered work, then the patent license
|
||||
you grant is automatically extended to all recipients of the covered
|
||||
work and works based on it.
|
||||
|
||||
A patent license is "discriminatory" if it does not include within
|
||||
the scope of its coverage, prohibits the exercise of, or is
|
||||
conditioned on the non-exercise of one or more of the rights that are
|
||||
specifically granted under this License. You may not convey a covered
|
||||
work if you are a party to an arrangement with a third party that is
|
||||
in the business of distributing software, under which you make payment
|
||||
to the third party based on the extent of your activity of conveying
|
||||
the work, and under which the third party grants, to any of the
|
||||
parties who would receive the covered work from you, a discriminatory
|
||||
patent license (a) in connection with copies of the covered work
|
||||
conveyed by you (or copies made from those copies), or (b) primarily
|
||||
for and in connection with specific products or compilations that
|
||||
contain the covered work, unless you entered into that arrangement,
|
||||
or that patent license was granted, prior to 28 March 2007.
|
||||
|
||||
Nothing in this License shall be construed as excluding or limiting
|
||||
any implied license or other defenses to infringement that may
|
||||
otherwise be available to you under applicable patent law.
|
||||
|
||||
12. No Surrender of Others' Freedom.
|
||||
|
||||
If conditions are imposed on you (whether by court order, agreement or
|
||||
otherwise) that contradict the conditions of this License, they do not
|
||||
excuse you from the conditions of this License. If you cannot
|
||||
distribute so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you
|
||||
may not distribute the Program at all. For example, if a patent
|
||||
license would not permit royalty-free redistribution of the Program by
|
||||
all those who receive copies directly or indirectly through you, then
|
||||
the only way you could satisfy both it and this License would be to
|
||||
refrain entirely from distribution of the Program.
|
||||
excuse you from the conditions of this License. If you cannot convey a
|
||||
covered work so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you may
|
||||
not convey it at all. For example, if you agree to terms that obligate you
|
||||
to collect a royalty for further conveying from those to whom you convey
|
||||
the Program, the only way you could satisfy both those terms and this
|
||||
License would be to refrain entirely from conveying the Program.
|
||||
|
||||
If any portion of this section is held invalid or unenforceable under
|
||||
any particular circumstance, the balance of the section is intended to
|
||||
apply and the section as a whole is intended to apply in other
|
||||
circumstances.
|
||||
13. Use with the GNU Affero General Public License.
|
||||
|
||||
It is not the purpose of this section to induce you to infringe any
|
||||
patents or other property right claims or to contest validity of any
|
||||
such claims; this section has the sole purpose of protecting the
|
||||
integrity of the free software distribution system, which is
|
||||
implemented by public license practices. Many people have made
|
||||
generous contributions to the wide range of software distributed
|
||||
through that system in reliance on consistent application of that
|
||||
system; it is up to the author/donor to decide if he or she is willing
|
||||
to distribute software through any other system and a licensee cannot
|
||||
impose that choice.
|
||||
Notwithstanding any other provision of this License, you have
|
||||
permission to link or combine any covered work with a work licensed
|
||||
under version 3 of the GNU Affero General Public License into a single
|
||||
combined work, and to convey the resulting work. The terms of this
|
||||
License will continue to apply to the part which is the covered work,
|
||||
but the special requirements of the GNU Affero General Public License,
|
||||
section 13, concerning interaction through a network will apply to the
|
||||
combination as such.
|
||||
|
||||
This section is intended to make thoroughly clear what is believed to
|
||||
be a consequence of the rest of this License.
|
||||
14. Revised Versions of this License.
|
||||
|
||||
8. If the distribution and/or use of the Program is restricted in
|
||||
certain countries either by patents or by copyrighted interfaces, the
|
||||
original copyright holder who places the Program under this License
|
||||
may add an explicit geographical distribution limitation excluding
|
||||
those countries, so that distribution is permitted only in or among
|
||||
countries not thus excluded. In such case, this License incorporates
|
||||
the limitation as if written in the body of this License.
|
||||
|
||||
9. The Free Software Foundation may publish revised and/or new versions
|
||||
of the General Public License from time to time. Such new versions will
|
||||
The Free Software Foundation may publish revised and/or new versions of
|
||||
the GNU General Public License from time to time. Such new versions will
|
||||
be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the Program
|
||||
specifies a version number of this License which applies to it and "any
|
||||
later version", you have the option of following the terms and conditions
|
||||
either of that version or of any later version published by the Free
|
||||
Software Foundation. If the Program does not specify a version number of
|
||||
this License, you may choose any version ever published by the Free Software
|
||||
Foundation.
|
||||
Each version is given a distinguishing version number. If the
|
||||
Program specifies that a certain numbered version of the GNU General
|
||||
Public License "or any later version" applies to it, you have the
|
||||
option of following the terms and conditions either of that numbered
|
||||
version or of any later version published by the Free Software
|
||||
Foundation. If the Program does not specify a version number of the
|
||||
GNU General Public License, you may choose any version ever published
|
||||
by the Free Software Foundation.
|
||||
|
||||
10. If you wish to incorporate parts of the Program into other free
|
||||
programs whose distribution conditions are different, write to the author
|
||||
to ask for permission. For software which is copyrighted by the Free
|
||||
Software Foundation, write to the Free Software Foundation; we sometimes
|
||||
make exceptions for this. Our decision will be guided by the two goals
|
||||
of preserving the free status of all derivatives of our free software and
|
||||
of promoting the sharing and reuse of software generally.
|
||||
If the Program specifies that a proxy can decide which future
|
||||
versions of the GNU General Public License can be used, that proxy's
|
||||
public statement of acceptance of a version permanently authorizes you
|
||||
to choose that version for the Program.
|
||||
|
||||
NO WARRANTY
|
||||
Later license versions may give you additional or different
|
||||
permissions. However, no additional obligations are imposed on any
|
||||
author or copyright holder as a result of your choosing to follow a
|
||||
later version.
|
||||
|
||||
11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
|
||||
FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
|
||||
OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
|
||||
PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
|
||||
OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
|
||||
TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
|
||||
PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
|
||||
REPAIR OR CORRECTION.
|
||||
15. Disclaimer of Warranty.
|
||||
|
||||
12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
|
||||
REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
|
||||
OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
|
||||
TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
|
||||
YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
|
||||
PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
|
||||
POSSIBILITY OF SUCH DAMAGES.
|
||||
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
||||
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
||||
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||
|
||||
16. Limitation of Liability.
|
||||
|
||||
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
||||
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
||||
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
||||
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
||||
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
||||
SUCH DAMAGES.
|
||||
|
||||
17. Interpretation of Sections 15 and 16.
|
||||
|
||||
If the disclaimer of warranty and limitation of liability provided
|
||||
above cannot be given local legal effect according to their terms,
|
||||
reviewing courts shall apply local law that most closely approximates
|
||||
an absolute waiver of all civil liability in connection with the
|
||||
Program, unless a warranty or assumption of liability accompanies a
|
||||
copy of the Program in return for a fee.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
@@ -287,15 +628,15 @@ free software which everyone can redistribute and change under these terms.
|
||||
|
||||
To do so, attach the following notices to the program. It is safest
|
||||
to attach them to the start of each source file to most effectively
|
||||
convey the exclusion of warranty; and each file should have at least
|
||||
state the exclusion of warranty; and each file should have at least
|
||||
the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
<one line to give the program's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
This program is free software; you can redistribute it and/or modify
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation; either version 2 of the License, or
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
@@ -303,36 +644,31 @@ the "copyright" line and a pointer to where the full notice is found.
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License along
|
||||
with this program; if not, see <https://www.gnu.org/licenses/>.
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If the program is interactive, make it output a short notice like this
|
||||
when it starts in an interactive mode:
|
||||
If the program does terminal interaction, make it output a short
|
||||
notice like this when it starts in an interactive mode:
|
||||
|
||||
Gnomovision version 69, Copyright (C) year name of author
|
||||
Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||
<program> Copyright (C) <year> <name of author>
|
||||
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||
This is free software, and you are welcome to redistribute it
|
||||
under certain conditions; type `show c' for details.
|
||||
|
||||
The hypothetical commands `show w' and `show c' should show the appropriate
|
||||
parts of the General Public License. Of course, the commands you use may
|
||||
be called something other than `show w' and `show c'; they could even be
|
||||
mouse-clicks or menu items--whatever suits your program.
|
||||
parts of the General Public License. Of course, your program's commands
|
||||
might be different; for a GUI interface, you would use an "about box".
|
||||
|
||||
You should also get your employer (if you work as a programmer) or your
|
||||
school, if any, to sign a "copyright disclaimer" for the program, if
|
||||
necessary. Here is a sample; alter the names:
|
||||
You should also get your employer (if you work as a programmer) or school,
|
||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||
For more information on this, and how to apply and follow the GNU GPL, see
|
||||
<http://www.gnu.org/licenses/>.
|
||||
|
||||
Yoyodyne, Inc., hereby disclaims all copyright interest in the program
|
||||
`Gnomovision' (which makes passes at compilers) written by James Hacker.
|
||||
|
||||
<signature of Moe Ghoul>, 1 April 1989
|
||||
Moe Ghoul, President of Vice
|
||||
|
||||
This General Public License does not permit incorporating your program into
|
||||
proprietary programs. If your program is a subroutine library, you may
|
||||
consider it more useful to permit linking proprietary applications with the
|
||||
library. If this is what you want to do, use the GNU Lesser General
|
||||
Public License instead of this License.
|
||||
The GNU General Public License does not permit incorporating your program
|
||||
into proprietary programs. If your program is a subroutine library, you
|
||||
may consider it more useful to permit linking proprietary applications with
|
||||
the library. If this is what you want to do, use the GNU Lesser General
|
||||
Public License instead of this License. But first, please read
|
||||
<http://www.gnu.org/philosophy/why-not-lgpl.html>.
|
||||
@@ -0,0 +1,36 @@
|
||||
bbport — a native PC port of Bloodborne (CUSA03173, 1.09)
|
||||
https://github.com/deadinside28/bloodborne_pc
|
||||
|
||||
This copy is the Linux fork, https://github.com/ayozetr/bloodborne_linux. bbport's own files stay
|
||||
GPL-2.0-or-later, as below. The fork as a whole is distributed under the GNU General Public
|
||||
License, version 3 or (at your option) any later version (LICENSE has its text): it includes code
|
||||
under GPL-3.0-or-later (the online module's BBSC and The Hunter's Dream parts), and everything
|
||||
else in it allows that. Files from other projects keep their own notices.
|
||||
|
||||
Copyright 2026 bbport contributors (the authors of this repository's commits and merged pull
|
||||
requests). Licensed under the GNU General Public License, version 2 or (at your option) any later
|
||||
version (the fork's LICENSE has version 3's text). No game code or data is part of this project.
|
||||
|
||||
Files of this project carry, in their first lines,
|
||||
SPDX-FileCopyrightText: Copyright 2026 bbport contributors
|
||||
SPDX-License-Identifier: GPL-2.0-or-later
|
||||
(or the license and copyright holders of the code they came from). The GPL requires these notices
|
||||
to be kept intact in copies and in works based on them (GPL-2.0, sections 1 and 2): if you reuse
|
||||
bbport's code — the runtime in src/, the GPU layer in gpu/, the scripts, the launchers — keep
|
||||
them, keep this file, and say in your own documentation that the code comes from bbport, with a
|
||||
link to the repository above. Code reused from a fork of bbport is bbport's code too.
|
||||
|
||||
Code bbport is built on, with its own notices kept in its files:
|
||||
|
||||
- shadPS4 (https://github.com/shadps4-emu/shadPS4, GPL-2.0-or-later, Copyright shadPS4 Emulator
|
||||
Project): the GPU layer and the shader translator started from its video core and shader
|
||||
recompiler and were rewritten part by part (gpu/VENDOR.txt). The online module keeps shadPS4
|
||||
v0.20.0's network, NP and shadNet code (gpu/bbnet20/vendor/shadps4, gpu/bbnet20/SHADNET-PORT.md).
|
||||
- The online module's other parts (gpu/bbnet20: BBSC, The Hunter's Dream pool; GPL-3.0-or-later
|
||||
where their files say so) keep their authors' copyright (gpu/bbnet20/SHADNET-PORT.md).
|
||||
- The DLSS bridge (gpu/dlss_bridge, MIT), adapted from IFreemz's shadPS4-Bloodborne-DLSS-FSR
|
||||
(https://github.com/IFreemz/shadPS4-Bloodborne-DLSS-FSR).
|
||||
- AMD FidelityFX shaders and headers (MIT) in gpu/upscale/shaders/ffx, gpu/third_party and
|
||||
tools/fsr4cap/ffx, with AMD's notices.
|
||||
- The libraries in gpu/third_party (sirit, imgui, half, httplib, fsr-vulkan and others), each under
|
||||
its own license in its folder.
|
||||
@@ -2,68 +2,103 @@
|
||||
|
||||
[](https://github.com/ayozetr/bloodborne_linux/releases/latest)
|
||||
[](https://github.com/ayozetr/bloodborne_linux/releases/latest)
|
||||
[](LICENSE)
|
||||
[](LICENSE)
|
||||
[](#features)
|
||||
[](https://ko-fi.com/ayozetr)
|
||||
|
||||
The PlayStation 4 version of *Bloodborne* running natively on Linux. The game's own x86-64 code
|
||||
runs directly on the CPU and its graphics are translated to Vulkan. It is not an emulator.
|
||||
The PlayStation 4 version of *Bloodborne* running natively on Linux. The game's own code runs
|
||||
directly on the CPU and its graphics are translated to Vulkan: it is not an emulator.
|
||||
|
||||
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.
|
||||
This fork is built on [bbport](https://github.com/deadinside28/bloodborne_pc) by deadinside28. It
|
||||
adds improvements of its own, and others from the community, taken from pull requests and other
|
||||
projects ([credits](docs/CREDITS.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.
|
||||
> 1.09, from a retail release ([which ones](docs/COMPATIBILITY.md)). This project is not
|
||||
> affiliated with Sony Interactive Entertainment or FromSoftware.
|
||||
|
||||
**[The players' guide](https://ayozetr.github.io/bloodborne_linux/)**: installing, playing online and
|
||||
seamless co-op, the Steam Deck, and what to do when something goes wrong.
|
||||
|
||||
## Features
|
||||
|
||||
- **Online play (beta)** through [The Hunter's Dream](https://thehuntersdream.com): messages,
|
||||
bloodstains, ghosts, bells and summons, with bbhost's players and Windows players of bbport, or
|
||||
with your group on [a server 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, Russian and Simplified Chinese, 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, or any earlier release from 0.5-linux.1 on, to go back to one
|
||||
that worked better.
|
||||
- **Multiplayer**: online messages, bloodstains, ghosts, bells and summons, on
|
||||
[The Hunter's Dream](https://thehuntersdream.com) or a server of your own; **Seamless Co-op**
|
||||
(experimental), co-op that lasts through deaths, lamps and areas, for up to 5 hunters; and
|
||||
**local co-op** (split screen), 2 to 4 players on one PC, each with their own controller.
|
||||
- **In-game map** (Hunter's Atlas): a fullscreen map with where you are, lamps and items, and fast
|
||||
travel to the lamps you've found.
|
||||
- **Frame rate unlocked** (up to 120 FPS recommended; the game's movement can break above it)
|
||||
or capped at **30, 60 or 90**, and changed while you play from the in-game menu. Resolutions from
|
||||
**720p to 4K**, and any **aspect ratio** (4:3 to 32:9 ultrawide, [how](docs/TROUBLESHOOTING.md#the-picture-is-stretched-on-an-ultrawide-screen)).
|
||||
- **Upscaling:** AMD **FSR 3.1** and **FSR 4**, NVIDIA **DLSS** (GeForce RTX) and TAA, and
|
||||
**frame generation** (FSR 3.1, or DLSS on GeForce RTX 40 and newer).
|
||||
- **Any controller:** PlayStation, Xbox and Switch pads with their own **button prompts in the
|
||||
game**, and **keyboard and mouse**, with keys you can rebind (bbport 0.7's Dark Souls
|
||||
III-style layout, or the classic one of earlier Linux versions), movement keys that keep a
|
||||
sprint through turns (the last of two opposite keys wins; `keyboard_movement=0.7` in
|
||||
`bbport.ini` for bbport 0.7's ramped stick) and the mouse as a pointer in the game's menus.
|
||||
- **Exit to desktop** from the game's title and System menus.
|
||||
- **Cheats and gameplay tweaks:** never die, stealth, camera distance, ragdoll physics and more.
|
||||
- **Trophies and the platinum**, with a notice in the game like on the PS4.
|
||||
- **Add to Steam** in one click, and **Discord Rich Presence** with your area, your co-op
|
||||
partners and a button to this repository.
|
||||
- **Launcher and in-game menu** (Insert, or hold L3 + R3) in English, Spanish, French, German, Italian,
|
||||
Portuguese, Russian and Simplified Chinese.
|
||||
- **AppImage** ready for any distribution and the Steam Deck, which **updates itself** from the
|
||||
launcher (and can go back to an earlier release).
|
||||
- **Mods and community patches**, without touching the game files ([how](docs/MODS.en.md)).
|
||||
|
||||
## Requirements
|
||||
|
||||
- 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.
|
||||
- Linux x86-64 and a GPU with Vulkan 1.3: AMD or Intel with Mesa, or NVIDIA with NVIDIA's driver.
|
||||
- About 6 GB of free memory, 10 GB for 1440p or 4K.
|
||||
|
||||
## Getting started
|
||||
|
||||
### AppImage
|
||||
1. Download `Bloodborne-bbport-x86_64.AppImage` from the
|
||||
[latest release](https://github.com/ayozetr/bloodborne_linux/releases/latest).
|
||||
2. Make it executable and run it.
|
||||
3. Choose your game folder (the one with `eboot.bin`) and press **Play**.
|
||||
|
||||
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`.
|
||||
Saves and settings go to `~/.local/share/bbport`; **Setup → Saves** exports your saves into a
|
||||
.zip and imports them back, or from shadPS4, and can keep a copy of them from before each game
|
||||
(off by default). The Game of the Year edition already has The Old
|
||||
Hunters; with the base game, its DLC goes in **Setup → DLC folder** ([how](docs/COMPATIBILITY.md)).
|
||||
|
||||
### From source
|
||||
## Online play
|
||||
|
||||
In the launcher's **Multiplayer → Online** tab, turn on **Play online**, choose a server and press **Test**
|
||||
in the **Status** row (every line should get a check mark):
|
||||
|
||||
- **Seamless Hunt** (the default): the community server of Seamless Co-op (BBSC), with players on
|
||||
the Windows port. Make an account from the launcher with **Create** ([docs/ONLINE.md](docs/ONLINE.md#seamless-hunt)).
|
||||
- **The Hunter's Dream**: sign in with Discord. You play with bbhost's players
|
||||
([docs/ONLINE.md](docs/ONLINE.md#the-hunters-dream-with-bbhosts-players)).
|
||||
- **Your own server**: for your group, with Docker and Tailscale ([docs/SERVER.md](docs/SERVER.md)).
|
||||
- **Seamless Co-op (BBSC)**, in the same tab: every player needs it, and summons across areas need
|
||||
its game server, which your own server can run ([docs/ONLINE.md](docs/ONLINE.md#seamless-co-op-bbsc)).
|
||||
|
||||
With online play on, the game starts online by itself, as PLAY ONLINE on a PS4. If you turned off
|
||||
*Skip the “play online / offline” choice* and pick offline, choose **Log In** on the title screen later
|
||||
for online play; until then the co-op bells are greyed out.
|
||||
|
||||
Everyone in a session needs update 1.09. Turn cheats off when you play with others. Ports,
|
||||
firewalls and the rest: [docs/ONLINE.md](docs/ONLINE.md).
|
||||
|
||||
## Local co-op (experimental)
|
||||
|
||||
Several games on one PC, one per player, each in its part of the screen with its own controller
|
||||
(the launcher's **Multiplayer → Local** tab): choose the players (2 to 4), the layout and each player's
|
||||
controller, then press **Play together**. Player 1 plays with the launcher's settings and saves;
|
||||
players 2 to 4 get their own saves and settings in `~/.local/share/bbport/coop/player<N>` (copied
|
||||
from player 1's the first time, about 400 MB each), without online play or Discord. One player can
|
||||
use the keyboard and mouse. Only player 1 has sound unless you turn that off. The windows can go
|
||||
to a monitor of their own (**Monitor**), and **Copy player 1's saves** gives another player the
|
||||
same characters (their own saves are backed up first). The windows are placed through X11, which
|
||||
works in a Wayland session too (XWayland).
|
||||
|
||||
## Building from source
|
||||
|
||||
On Arch Linux (or CachyOS, Manjaro…):
|
||||
|
||||
@@ -76,120 +111,42 @@ 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
|
||||
python3 launcher/bbport_launcher.py
|
||||
```
|
||||
|
||||
With [Nix](https://nixos.org), `bash build.sh` gets every dependency from `shell.nix` by itself.
|
||||
With [Nix](https://nixos.org), `bash build.sh` gets every dependency by itself. DLSS needs
|
||||
`bash tools/build_dlss.sh` once; the AppImage is `bash packaging/appimage-sharun.sh`, which bundles
|
||||
the libraries of the PC it runs on. Releases are built with `bash packaging/appimage-in-arch.sh`
|
||||
(Docker, plain Arch Linux): on distributions built for newer processors (CachyOS's x86-64-v3
|
||||
repositories, for example) the AppImage would otherwise not start on CPUs without AVX2. Environment
|
||||
variables for the launcher's **Advanced → Developer → Extra variables**: [docs/ENVIRONMENT.md](docs/ENVIRONMENT.md).
|
||||
Problems: [docs/TROUBLESHOOTING.md](docs/TROUBLESHOOTING.md).
|
||||
|
||||
### Online play
|
||||
## On Windows
|
||||
|
||||
1. In the launcher, open **Online**, turn on **Play online** and choose the co-op server:
|
||||
- **The Hunter's Dream (with bbhost players)**: sign in with Discord, or create an account
|
||||
with just a name and keep the recovery code it shows (it brings the account back on
|
||||
another PC). [bbbridge](bbbridge/README.md) by MaskZeus runs with the game and talks to
|
||||
The Hunter's Dream the way bbhost does, so you play with bbhost's players and with Windows
|
||||
players of bbport on the same pool.
|
||||
- **Your own server**: your group's shadNet server and your account on it
|
||||
([docs/SERVER.md](docs/SERVER.md), Docker and Tailscale; tested with a Linux and a Windows
|
||||
player).
|
||||
2. **Test the connection**: every line should be green.
|
||||
3. Start the game and choose online at the title screen.
|
||||
|
||||
**Seamless co-op (experimental)**: turn on **Seamless co-op (BBSC)** in the Online page and the
|
||||
session lives through deaths, lamps, areas, bosses and the Hunter's Dream, with up to 5 hunters.
|
||||
It is [BBSC](https://github.com/Classmg22/BBSC) by CLS_AMG, built for Linux here; every player of
|
||||
the session needs it (on Windows, BBSC's package for the Windows port). Summons across areas need
|
||||
BBSC's game server ([docs/ONLINE.md](docs/ONLINE.md#seamless-co-op-bbsc)).
|
||||
|
||||
Everyone in a session needs game version 1.09 and the same pool. Turn cheats off when you play
|
||||
with others. shadPS4's public shadNet server no longer accepts bbport builds: it signs them out
|
||||
right after the login, and the launcher says so.
|
||||
|
||||
With your own server, 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).
|
||||
|
||||
### DLSS
|
||||
|
||||
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.
|
||||
|
||||
### Environment variables
|
||||
|
||||
The launcher's **Settings → Developer → Extra variables** field (`NAME=value`, space-separated)
|
||||
passes environment variables to the game, the same ones `run.sh` takes from a terminal. Every one
|
||||
the port reads, what it does and which win over which: [docs/ENVIRONMENT.md](docs/ENVIRONMENT.md).
|
||||
|
||||
### DLC
|
||||
|
||||
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.
|
||||
|
||||
### Add to Steam
|
||||
|
||||
**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.
|
||||
|
||||
### Button prompts
|
||||
|
||||
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.
|
||||
If you need a Windows version, I recommend
|
||||
[Supermedo's Windows port](https://github.com/Supermedo/bloodborne_pc) and
|
||||
[Awakenchan's LanternEngine](https://github.com/Awakenchan/LanternEngine).
|
||||
|
||||
## Support
|
||||
|
||||
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).
|
||||
This fork is built on the port deadinside28 created, but it has a lot of hours of work behind it,
|
||||
and above all hours and hours of testing things and hunting down fixes so it runs as well as it
|
||||
can. I always try to fix every report as soon as possible, because I know how much this game means
|
||||
to some people. If it's useful to you and you'd like to support it, you can on
|
||||
[Ko-fi](https://ko-fi.com/ayozetr).
|
||||
|
||||
## Credits
|
||||
|
||||
- [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.
|
||||
- [bbbridge](https://github.com/MaskZeus/bbbridge) by MaskZeus (GPL-3.0-or-later): The Hunter's
|
||||
Dream's co-op pool for bbport, built for Linux here ([bbbridge/](bbbridge/)).
|
||||
- [BBSC](https://github.com/Classmg22/BBSC) by CLS_AMG (GPL-2.0-or-later): seamless co-op, with
|
||||
Wozzardman's seamless summon; built for Linux here ([bbsc/](bbsc/)), with
|
||||
[xbyak](https://github.com/herumi/xbyak) and [fmt](https://github.com/fmtlib/fmt).
|
||||
- [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.
|
||||
- [ps4-pkg-extractor](https://github.com/paulomanrique/ps4-pkg-extractor) by paulomanrique
|
||||
(Unlicense), on [orbis-pkg-util](https://crates.io/crates/orbis-pkg-util): the launcher's
|
||||
*Install from PKG*.
|
||||
- Game patches by Kyo, Lance McDonald, auser1337, illusion, emoose and the Bloodborne community.
|
||||
|
||||
Full list of components and licenses: [BBPORT.md](BBPORT.md#credits-and-licenses).
|
||||
Built on [bbport](https://github.com/deadinside28/bloodborne_pc) by deadinside28, with
|
||||
[shadPS4](https://github.com/shadps4-emu/shadPS4)'s renderer, and with work from many people and
|
||||
projects: everyone is in [docs/CREDITS.md](docs/CREDITS.md). It includes code from
|
||||
Lantern Engine by Awaken (https://github.com/Awakenchan/LanternEngine). Components and their licenses:
|
||||
[BBPORT.md](BBPORT.md#credits-and-licenses).
|
||||
|
||||
## License
|
||||
|
||||
GNU GPL v2 or later ([LICENSE](LICENSE)). Third-party components keep their own licenses. NVIDIA
|
||||
GNU GPL v3 or later ([LICENSE](LICENSE)). Files taken from other projects keep their own notices:
|
||||
most are GPL-2.0-or-later (bbport, shadPS4), which allows them to be distributed under
|
||||
GPL-3.0-or-later. Third-party components keep their own licenses. NVIDIA
|
||||
and DLSS are trademarks of NVIDIA Corporation.
|
||||
@@ -11,6 +11,8 @@ Only what the mods drew is here, never the game's own files:
|
||||
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.
|
||||
The port draws its button prompts live (`gpu/shim/bbport_button_prompts.cpp`): `scripts/prompt_art.py`
|
||||
turns the 20 icons into the raw images it uses for the Xbox and Switch styles, and the keyboard's
|
||||
are its own. `scripts/gestures_menu.py` applies `gesturetop.json` to the player's own
|
||||
`menu/gesturetop.gfx` when the launcher's *Gestures menu without motion-control icons* is on.
|
||||
The Linux fork's `tools/extract_prompt_assets.py` rebuilt this folder from the mods' ZIPs.
|
||||
@@ -1,20 +0,0 @@
|
||||
# Your account (token + recovery code): never commit or share it.
|
||||
bbbridge_state.json
|
||||
bbbridge_state.json.*
|
||||
# Per-PC settings (the chosen bbport folder, ...)
|
||||
bbbridge_config.json
|
||||
|
||||
# Logs and trace captures (they can hold session ids)
|
||||
*.log
|
||||
captures/
|
||||
|
||||
# Build output
|
||||
build/
|
||||
bin/
|
||||
bbbridge.exe
|
||||
.vs/
|
||||
out/
|
||||
__pycache__/
|
||||
|
||||
# Release packages (tools\make_release.ps1)
|
||||
dist/
|
||||
@@ -1,40 +0,0 @@
|
||||
# Changelog
|
||||
|
||||
## 0.3.2 - 2026-10-12
|
||||
|
||||
- **Fix: "Failed to acquire Bloodborne server information" after closing
|
||||
bbbridge.** The file that sends the game's server traffic to bbbridge
|
||||
(`host_overrides.custom.json` in bbport's user folder) now exists only while
|
||||
bbbridge runs with the switch on The Hunter's Dream: it goes when bbbridge
|
||||
closes, when the bridge stops, or when bbport is set back to shadNet - also
|
||||
in the bbport launcher itself. One left behind by an older bbbridge
|
||||
(including the Python 0.2.0) is cleaned up the next time bbbridge starts.
|
||||
- Troubleshooting notes in the README.
|
||||
|
||||
## 0.3.1 - 2026-10-12
|
||||
|
||||
- **Online switch** in place of the "Route bbport through bbbridge" box:
|
||||
shadNet on the left, The Hunter's Dream (bbhost) on the right. A line under
|
||||
it says which pool and account PLAY uses.
|
||||
- **shadNet comes back whole**: switching to shadNet restores bbport's own
|
||||
server *and* shadNet account (name, password). The Hunter's Dream account is
|
||||
bbport's online id only while switched to it. PLAY follows the switch; on
|
||||
shadNet no Discord sign-in is needed.
|
||||
- **Set up by default**: once signed in with Discord, the switch goes to The
|
||||
Hunter's Dream and the live map patch is added; turning either off sticks.
|
||||
- **bbport folder**: shows where it was found; bbbridge.exe right next to
|
||||
Bloodborne.exe works too; asks when it had to guess, when a chosen folder is
|
||||
gone, or when bbport is not found.
|
||||
- **Clear errors**: PLAY and "bbport settings..." say what is missing (sign-in,
|
||||
game folder, shadNet account, launcher open, missing exe, Windows' error).
|
||||
"bbport settings..." also works before the launcher has any settings.
|
||||
- **Lighter**: the window draws at full rate only while used (idle CPU about a
|
||||
quarter of before).
|
||||
- **Steadier event poll**: it no longer stalls ~8 s about once a minute, so a
|
||||
summon reaches the game without that delay.
|
||||
- Verified live: joining a bbhost host's world as a guest (Small Resonant Bell).
|
||||
|
||||
## 0.3.0 - 2026-10-12
|
||||
|
||||
- First C++ release: one window for the bridge, Discord sign-in, routing bbport,
|
||||
the live map patch and starting the game.
|
||||
@@ -1,81 +0,0 @@
|
||||
cmake_minimum_required(VERSION 3.21)
|
||||
project(bbbridge VERSION 0.3.2 LANGUAGES CXX)
|
||||
|
||||
set(CMAKE_CXX_STANDARD 20)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
# One self-contained exe: the C++ runtime is linked in (no VC++ redistributable).
|
||||
set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CONFIG:Debug>:Debug>")
|
||||
|
||||
if(MSVC)
|
||||
add_compile_options(/utf-8 /permissive- /EHsc)
|
||||
add_compile_definitions(_WIN32_WINNT=0x0A00 WINVER=0x0A00 UNICODE _UNICODE _CRT_SECURE_NO_WARNINGS)
|
||||
endif()
|
||||
|
||||
# The bridge itself: everything but the front ends.
|
||||
add_library(bbbridge_core STATIC
|
||||
src/util.cpp
|
||||
src/proto3.cpp
|
||||
src/http.cpp
|
||||
src/bbhost_client.cpp
|
||||
src/p2p.cpp
|
||||
src/bridge.cpp
|
||||
)
|
||||
target_include_directories(bbbridge_core PUBLIC src third_party)
|
||||
target_sources(bbbridge_core PRIVATE src/account_link.cpp)
|
||||
if(WIN32)
|
||||
target_link_libraries(bbbridge_core PUBLIC ws2_32 winhttp bcrypt)
|
||||
# Pointing bbport for Windows at the bridge (its launcher's settings, host overrides).
|
||||
target_sources(bbbridge_core PRIVATE src/setup.cpp)
|
||||
else()
|
||||
# Linux: BSD sockets, libcurl for HTTPS, OpenSSL for SHA-256. The bbport launcher points the
|
||||
# game at the bridge itself, so there is no setup.cpp, and no window (bbbridge-cli only).
|
||||
find_package(Threads REQUIRED)
|
||||
find_package(CURL REQUIRED)
|
||||
find_package(OpenSSL REQUIRED COMPONENTS Crypto)
|
||||
target_link_libraries(bbbridge_core PUBLIC Threads::Threads CURL::libcurl OpenSSL::Crypto)
|
||||
target_compile_options(bbbridge_core PRIVATE -Wall -Wextra)
|
||||
endif()
|
||||
|
||||
if(WIN32)
|
||||
# Dear ImGui (third_party/imgui, v1.91.5) with the Win32 + DirectX 11 backends.
|
||||
add_library(imgui STATIC
|
||||
third_party/imgui/imgui.cpp
|
||||
third_party/imgui/imgui_draw.cpp
|
||||
third_party/imgui/imgui_tables.cpp
|
||||
third_party/imgui/imgui_widgets.cpp
|
||||
third_party/imgui/imgui_impl_win32.cpp
|
||||
third_party/imgui/imgui_impl_dx11.cpp
|
||||
)
|
||||
target_include_directories(imgui PUBLIC third_party/imgui)
|
||||
target_compile_definitions(imgui PUBLIC IMGUI_DISABLE_DEMO_WINDOWS)
|
||||
|
||||
|
||||
# The app: bbbridge.exe, next to this CMakeLists (inside bbport's folder).
|
||||
add_executable(bbbridge WIN32 src/app.cpp src/app.rc)
|
||||
target_link_libraries(bbbridge PRIVATE bbbridge_core imgui d3d11 dxgi)
|
||||
add_custom_command(TARGET bbbridge POST_BUILD
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_if_different $<TARGET_FILE:bbbridge> ${CMAKE_SOURCE_DIR}/bbbridge.exe)
|
||||
endif()
|
||||
|
||||
# Headless bridge: tests, servers, troubleshooting.
|
||||
add_executable(bbbridge-cli src/main_cli.cpp)
|
||||
target_link_libraries(bbbridge-cli PRIVATE bbbridge_core)
|
||||
|
||||
# Unit tests for the byte-level parts (framing, signaling, STUN, protobuf).
|
||||
add_executable(bbbridge-unit tests/unit.cpp)
|
||||
target_link_libraries(bbbridge-unit PRIVATE bbbridge_core)
|
||||
|
||||
# Warnings at /W4 for our code (not for third_party).
|
||||
if(MSVC)
|
||||
foreach(target bbbridge_core bbbridge bbbridge-cli bbbridge-unit)
|
||||
target_compile_options(${target} PRIVATE /W4)
|
||||
endforeach()
|
||||
endif()
|
||||
|
||||
# The CLI and the unit tests land in bin\ (on Windows; elsewhere they stay in the build folder).
|
||||
if(WIN32)
|
||||
foreach(target bbbridge-cli bbbridge-unit)
|
||||
add_custom_command(TARGET ${target} POST_BUILD
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_if_different $<TARGET_FILE:${target}> ${CMAKE_SOURCE_DIR}/bin/$<TARGET_FILE_NAME:${target}>)
|
||||
endforeach()
|
||||
endif()
|
||||
@@ -1,23 +0,0 @@
|
||||
{
|
||||
"version": 3,
|
||||
"configurePresets": [
|
||||
{
|
||||
"name": "x64-release",
|
||||
"displayName": "x64 Release",
|
||||
"generator": "Ninja",
|
||||
"binaryDir": "${sourceDir}/build/${presetName}",
|
||||
"architecture": { "value": "x64", "strategy": "external" },
|
||||
"cacheVariables": { "CMAKE_BUILD_TYPE": "Release", "CMAKE_CXX_COMPILER": "cl" }
|
||||
},
|
||||
{
|
||||
"name": "x64-debug",
|
||||
"displayName": "x64 Debug",
|
||||
"inherits": "x64-release",
|
||||
"cacheVariables": { "CMAKE_BUILD_TYPE": "Debug" }
|
||||
}
|
||||
],
|
||||
"buildPresets": [
|
||||
{ "name": "x64-release", "configurePreset": "x64-release" },
|
||||
{ "name": "x64-debug", "configurePreset": "x64-debug" }
|
||||
]
|
||||
}
|
||||
@@ -1,674 +0,0 @@
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
Version 3, 29 June 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
Preamble
|
||||
|
||||
The GNU General Public License is a free, copyleft license for
|
||||
software and other kinds of works.
|
||||
|
||||
The licenses for most software and other practical works are designed
|
||||
to take away your freedom to share and change the works. By contrast,
|
||||
the GNU General Public License is intended to guarantee your freedom to
|
||||
share and change all versions of a program--to make sure it remains free
|
||||
software for all its users. We, the Free Software Foundation, use the
|
||||
GNU General Public License for most of our software; it applies also to
|
||||
any other work released this way by its authors. You can apply it to
|
||||
your programs, too.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
have the freedom to distribute copies of free software (and charge for
|
||||
them if you wish), that you receive source code or can get it if you
|
||||
want it, that you can change the software or use pieces of it in new
|
||||
free programs, and that you know you can do these things.
|
||||
|
||||
To protect your rights, we need to prevent others from denying you
|
||||
these rights or asking you to surrender the rights. Therefore, you have
|
||||
certain responsibilities if you distribute copies of the software, or if
|
||||
you modify it: responsibilities to respect the freedom of others.
|
||||
|
||||
For example, if you distribute copies of such a program, whether
|
||||
gratis or for a fee, you must pass on to the recipients the same
|
||||
freedoms that you received. You must make sure that they, too, receive
|
||||
or can get the source code. And you must show them these terms so they
|
||||
know their rights.
|
||||
|
||||
Developers that use the GNU GPL protect your rights with two steps:
|
||||
(1) assert copyright on the software, and (2) offer you this License
|
||||
giving you legal permission to copy, distribute and/or modify it.
|
||||
|
||||
For the developers' and authors' protection, the GPL clearly explains
|
||||
that there is no warranty for this free software. For both users' and
|
||||
authors' sake, the GPL requires that modified versions be marked as
|
||||
changed, so that their problems will not be attributed erroneously to
|
||||
authors of previous versions.
|
||||
|
||||
Some devices are designed to deny users access to install or run
|
||||
modified versions of the software inside them, although the manufacturer
|
||||
can do so. This is fundamentally incompatible with the aim of
|
||||
protecting users' freedom to change the software. The systematic
|
||||
pattern of such abuse occurs in the area of products for individuals to
|
||||
use, which is precisely where it is most unacceptable. Therefore, we
|
||||
have designed this version of the GPL to prohibit the practice for those
|
||||
products. If such problems arise substantially in other domains, we
|
||||
stand ready to extend this provision to those domains in future versions
|
||||
of the GPL, as needed to protect the freedom of users.
|
||||
|
||||
Finally, every program is threatened constantly by software patents.
|
||||
States should not allow patents to restrict development and use of
|
||||
software on general-purpose computers, but in those that do, we wish to
|
||||
avoid the special danger that patents applied to a free program could
|
||||
make it effectively proprietary. To prevent this, the GPL assures that
|
||||
patents cannot be used to render the program non-free.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
|
||||
TERMS AND CONDITIONS
|
||||
|
||||
0. Definitions.
|
||||
|
||||
"This License" refers to version 3 of the GNU General Public License.
|
||||
|
||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||
works, such as semiconductor masks.
|
||||
|
||||
"The Program" refers to any copyrightable work licensed under this
|
||||
License. Each licensee is addressed as "you". "Licensees" and
|
||||
"recipients" may be individuals or organizations.
|
||||
|
||||
To "modify" a work means to copy from or adapt all or part of the work
|
||||
in a fashion requiring copyright permission, other than the making of an
|
||||
exact copy. The resulting work is called a "modified version" of the
|
||||
earlier work or a work "based on" the earlier work.
|
||||
|
||||
A "covered work" means either the unmodified Program or a work based
|
||||
on the Program.
|
||||
|
||||
To "propagate" a work means to do anything with it that, without
|
||||
permission, would make you directly or secondarily liable for
|
||||
infringement under applicable copyright law, except executing it on a
|
||||
computer or modifying a private copy. Propagation includes copying,
|
||||
distribution (with or without modification), making available to the
|
||||
public, and in some countries other activities as well.
|
||||
|
||||
To "convey" a work means any kind of propagation that enables other
|
||||
parties to make or receive copies. Mere interaction with a user through
|
||||
a computer network, with no transfer of a copy, is not conveying.
|
||||
|
||||
An interactive user interface displays "Appropriate Legal Notices"
|
||||
to the extent that it includes a convenient and prominently visible
|
||||
feature that (1) displays an appropriate copyright notice, and (2)
|
||||
tells the user that there is no warranty for the work (except to the
|
||||
extent that warranties are provided), that licensees may convey the
|
||||
work under this License, and how to view a copy of this License. If
|
||||
the interface presents a list of user commands or options, such as a
|
||||
menu, a prominent item in the list meets this criterion.
|
||||
|
||||
1. Source Code.
|
||||
|
||||
The "source code" for a work means the preferred form of the work
|
||||
for making modifications to it. "Object code" means any non-source
|
||||
form of a work.
|
||||
|
||||
A "Standard Interface" means an interface that either is an official
|
||||
standard defined by a recognized standards body, or, in the case of
|
||||
interfaces specified for a particular programming language, one that
|
||||
is widely used among developers working in that language.
|
||||
|
||||
The "System Libraries" of an executable work include anything, other
|
||||
than the work as a whole, that (a) is included in the normal form of
|
||||
packaging a Major Component, but which is not part of that Major
|
||||
Component, and (b) serves only to enable use of the work with that
|
||||
Major Component, or to implement a Standard Interface for which an
|
||||
implementation is available to the public in source code form. A
|
||||
"Major Component", in this context, means a major essential component
|
||||
(kernel, window system, and so on) of the specific operating system
|
||||
(if any) on which the executable work runs, or a compiler used to
|
||||
produce the work, or an object code interpreter used to run it.
|
||||
|
||||
The "Corresponding Source" for a work in object code form means all
|
||||
the source code needed to generate, install, and (for an executable
|
||||
work) run the object code and to modify the work, including scripts to
|
||||
control those activities. However, it does not include the work's
|
||||
System Libraries, or general-purpose tools or generally available free
|
||||
programs which are used unmodified in performing those activities but
|
||||
which are not part of the work. For example, Corresponding Source
|
||||
includes interface definition files associated with source files for
|
||||
the work, and the source code for shared libraries and dynamically
|
||||
linked subprograms that the work is specifically designed to require,
|
||||
such as by intimate data communication or control flow between those
|
||||
subprograms and other parts of the work.
|
||||
|
||||
The Corresponding Source need not include anything that users
|
||||
can regenerate automatically from other parts of the Corresponding
|
||||
Source.
|
||||
|
||||
The Corresponding Source for a work in source code form is that
|
||||
same work.
|
||||
|
||||
2. Basic Permissions.
|
||||
|
||||
All rights granted under this License are granted for the term of
|
||||
copyright on the Program, and are irrevocable provided the stated
|
||||
conditions are met. This License explicitly affirms your unlimited
|
||||
permission to run the unmodified Program. The output from running a
|
||||
covered work is covered by this License only if the output, given its
|
||||
content, constitutes a covered work. This License acknowledges your
|
||||
rights of fair use or other equivalent, as provided by copyright law.
|
||||
|
||||
You may make, run and propagate covered works that you do not
|
||||
convey, without conditions so long as your license otherwise remains
|
||||
in force. You may convey covered works to others for the sole purpose
|
||||
of having them make modifications exclusively for you, or provide you
|
||||
with facilities for running those works, provided that you comply with
|
||||
the terms of this License in conveying all material for which you do
|
||||
not control copyright. Those thus making or running the covered works
|
||||
for you must do so exclusively on your behalf, under your direction
|
||||
and control, on terms that prohibit them from making any copies of
|
||||
your copyrighted material outside their relationship with you.
|
||||
|
||||
Conveying under any other circumstances is permitted solely under
|
||||
the conditions stated below. Sublicensing is not allowed; section 10
|
||||
makes it unnecessary.
|
||||
|
||||
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||
|
||||
No covered work shall be deemed part of an effective technological
|
||||
measure under any applicable law fulfilling obligations under article
|
||||
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||
similar laws prohibiting or restricting circumvention of such
|
||||
measures.
|
||||
|
||||
When you convey a covered work, you waive any legal power to forbid
|
||||
circumvention of technological measures to the extent such circumvention
|
||||
is effected by exercising rights under this License with respect to
|
||||
the covered work, and you disclaim any intention to limit operation or
|
||||
modification of the work as a means of enforcing, against the work's
|
||||
users, your or third parties' legal rights to forbid circumvention of
|
||||
technological measures.
|
||||
|
||||
4. Conveying Verbatim Copies.
|
||||
|
||||
You may convey verbatim copies of the Program's source code as you
|
||||
receive it, in any medium, provided that you conspicuously and
|
||||
appropriately publish on each copy an appropriate copyright notice;
|
||||
keep intact all notices stating that this License and any
|
||||
non-permissive terms added in accord with section 7 apply to the code;
|
||||
keep intact all notices of the absence of any warranty; and give all
|
||||
recipients a copy of this License along with the Program.
|
||||
|
||||
You may charge any price or no price for each copy that you convey,
|
||||
and you may offer support or warranty protection for a fee.
|
||||
|
||||
5. Conveying Modified Source Versions.
|
||||
|
||||
You may convey a work based on the Program, or the modifications to
|
||||
produce it from the Program, in the form of source code under the
|
||||
terms of section 4, provided that you also meet all of these conditions:
|
||||
|
||||
a) The work must carry prominent notices stating that you modified
|
||||
it, and giving a relevant date.
|
||||
|
||||
b) The work must carry prominent notices stating that it is
|
||||
released under this License and any conditions added under section
|
||||
7. This requirement modifies the requirement in section 4 to
|
||||
"keep intact all notices".
|
||||
|
||||
c) You must license the entire work, as a whole, under this
|
||||
License to anyone who comes into possession of a copy. This
|
||||
License will therefore apply, along with any applicable section 7
|
||||
additional terms, to the whole of the work, and all its parts,
|
||||
regardless of how they are packaged. This License gives no
|
||||
permission to license the work in any other way, but it does not
|
||||
invalidate such permission if you have separately received it.
|
||||
|
||||
d) If the work has interactive user interfaces, each must display
|
||||
Appropriate Legal Notices; however, if the Program has interactive
|
||||
interfaces that do not display Appropriate Legal Notices, your
|
||||
work need not make them do so.
|
||||
|
||||
A compilation of a covered work with other separate and independent
|
||||
works, which are not by their nature extensions of the covered work,
|
||||
and which are not combined with it such as to form a larger program,
|
||||
in or on a volume of a storage or distribution medium, is called an
|
||||
"aggregate" if the compilation and its resulting copyright are not
|
||||
used to limit the access or legal rights of the compilation's users
|
||||
beyond what the individual works permit. Inclusion of a covered work
|
||||
in an aggregate does not cause this License to apply to the other
|
||||
parts of the aggregate.
|
||||
|
||||
6. Conveying Non-Source Forms.
|
||||
|
||||
You may convey a covered work in object code form under the terms
|
||||
of sections 4 and 5, provided that you also convey the
|
||||
machine-readable Corresponding Source under the terms of this License,
|
||||
in one of these ways:
|
||||
|
||||
a) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by the
|
||||
Corresponding Source fixed on a durable physical medium
|
||||
customarily used for software interchange.
|
||||
|
||||
b) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by a
|
||||
written offer, valid for at least three years and valid for as
|
||||
long as you offer spare parts or customer support for that product
|
||||
model, to give anyone who possesses the object code either (1) a
|
||||
copy of the Corresponding Source for all the software in the
|
||||
product that is covered by this License, on a durable physical
|
||||
medium customarily used for software interchange, for a price no
|
||||
more than your reasonable cost of physically performing this
|
||||
conveying of source, or (2) access to copy the
|
||||
Corresponding Source from a network server at no charge.
|
||||
|
||||
c) Convey individual copies of the object code with a copy of the
|
||||
written offer to provide the Corresponding Source. This
|
||||
alternative is allowed only occasionally and noncommercially, and
|
||||
only if you received the object code with such an offer, in accord
|
||||
with subsection 6b.
|
||||
|
||||
d) Convey the object code by offering access from a designated
|
||||
place (gratis or for a charge), and offer equivalent access to the
|
||||
Corresponding Source in the same way through the same place at no
|
||||
further charge. You need not require recipients to copy the
|
||||
Corresponding Source along with the object code. If the place to
|
||||
copy the object code is a network server, the Corresponding Source
|
||||
may be on a different server (operated by you or a third party)
|
||||
that supports equivalent copying facilities, provided you maintain
|
||||
clear directions next to the object code saying where to find the
|
||||
Corresponding Source. Regardless of what server hosts the
|
||||
Corresponding Source, you remain obligated to ensure that it is
|
||||
available for as long as needed to satisfy these requirements.
|
||||
|
||||
e) Convey the object code using peer-to-peer transmission, provided
|
||||
you inform other peers where the object code and Corresponding
|
||||
Source of the work are being offered to the general public at no
|
||||
charge under subsection 6d.
|
||||
|
||||
A separable portion of the object code, whose source code is excluded
|
||||
from the Corresponding Source as a System Library, need not be
|
||||
included in conveying the object code work.
|
||||
|
||||
A "User Product" is either (1) a "consumer product", which means any
|
||||
tangible personal property which is normally used for personal, family,
|
||||
or household purposes, or (2) anything designed or sold for incorporation
|
||||
into a dwelling. In determining whether a product is a consumer product,
|
||||
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||
product received by a particular user, "normally used" refers to a
|
||||
typical or common use of that class of product, regardless of the status
|
||||
of the particular user or of the way in which the particular user
|
||||
actually uses, or expects or is expected to use, the product. A product
|
||||
is a consumer product regardless of whether the product has substantial
|
||||
commercial, industrial or non-consumer uses, unless such uses represent
|
||||
the only significant mode of use of the product.
|
||||
|
||||
"Installation Information" for a User Product means any methods,
|
||||
procedures, authorization keys, or other information required to install
|
||||
and execute modified versions of a covered work in that User Product from
|
||||
a modified version of its Corresponding Source. The information must
|
||||
suffice to ensure that the continued functioning of the modified object
|
||||
code is in no case prevented or interfered with solely because
|
||||
modification has been made.
|
||||
|
||||
If you convey an object code work under this section in, or with, or
|
||||
specifically for use in, a User Product, and the conveying occurs as
|
||||
part of a transaction in which the right of possession and use of the
|
||||
User Product is transferred to the recipient in perpetuity or for a
|
||||
fixed term (regardless of how the transaction is characterized), the
|
||||
Corresponding Source conveyed under this section must be accompanied
|
||||
by the Installation Information. But this requirement does not apply
|
||||
if neither you nor any third party retains the ability to install
|
||||
modified object code on the User Product (for example, the work has
|
||||
been installed in ROM).
|
||||
|
||||
The requirement to provide Installation Information does not include a
|
||||
requirement to continue to provide support service, warranty, or updates
|
||||
for a work that has been modified or installed by the recipient, or for
|
||||
the User Product in which it has been modified or installed. Access to a
|
||||
network may be denied when the modification itself materially and
|
||||
adversely affects the operation of the network or violates the rules and
|
||||
protocols for communication across the network.
|
||||
|
||||
Corresponding Source conveyed, and Installation Information provided,
|
||||
in accord with this section must be in a format that is publicly
|
||||
documented (and with an implementation available to the public in
|
||||
source code form), and must require no special password or key for
|
||||
unpacking, reading or copying.
|
||||
|
||||
7. Additional Terms.
|
||||
|
||||
"Additional permissions" are terms that supplement the terms of this
|
||||
License by making exceptions from one or more of its conditions.
|
||||
Additional permissions that are applicable to the entire Program shall
|
||||
be treated as though they were included in this License, to the extent
|
||||
that they are valid under applicable law. If additional permissions
|
||||
apply only to part of the Program, that part may be used separately
|
||||
under those permissions, but the entire Program remains governed by
|
||||
this License without regard to the additional permissions.
|
||||
|
||||
When you convey a copy of a covered work, you may at your option
|
||||
remove any additional permissions from that copy, or from any part of
|
||||
it. (Additional permissions may be written to require their own
|
||||
removal in certain cases when you modify the work.) You may place
|
||||
additional permissions on material, added by you to a covered work,
|
||||
for which you have or can give appropriate copyright permission.
|
||||
|
||||
Notwithstanding any other provision of this License, for material you
|
||||
add to a covered work, you may (if authorized by the copyright holders of
|
||||
that material) supplement the terms of this License with terms:
|
||||
|
||||
a) Disclaiming warranty or limiting liability differently from the
|
||||
terms of sections 15 and 16 of this License; or
|
||||
|
||||
b) Requiring preservation of specified reasonable legal notices or
|
||||
author attributions in that material or in the Appropriate Legal
|
||||
Notices displayed by works containing it; or
|
||||
|
||||
c) Prohibiting misrepresentation of the origin of that material, or
|
||||
requiring that modified versions of such material be marked in
|
||||
reasonable ways as different from the original version; or
|
||||
|
||||
d) Limiting the use for publicity purposes of names of licensors or
|
||||
authors of the material; or
|
||||
|
||||
e) Declining to grant rights under trademark law for use of some
|
||||
trade names, trademarks, or service marks; or
|
||||
|
||||
f) Requiring indemnification of licensors and authors of that
|
||||
material by anyone who conveys the material (or modified versions of
|
||||
it) with contractual assumptions of liability to the recipient, for
|
||||
any liability that these contractual assumptions directly impose on
|
||||
those licensors and authors.
|
||||
|
||||
All other non-permissive additional terms are considered "further
|
||||
restrictions" within the meaning of section 10. If the Program as you
|
||||
received it, or any part of it, contains a notice stating that it is
|
||||
governed by this License along with a term that is a further
|
||||
restriction, you may remove that term. If a license document contains
|
||||
a further restriction but permits relicensing or conveying under this
|
||||
License, you may add to a covered work material governed by the terms
|
||||
of that license document, provided that the further restriction does
|
||||
not survive such relicensing or conveying.
|
||||
|
||||
If you add terms to a covered work in accord with this section, you
|
||||
must place, in the relevant source files, a statement of the
|
||||
additional terms that apply to those files, or a notice indicating
|
||||
where to find the applicable terms.
|
||||
|
||||
Additional terms, permissive or non-permissive, may be stated in the
|
||||
form of a separately written license, or stated as exceptions;
|
||||
the above requirements apply either way.
|
||||
|
||||
8. Termination.
|
||||
|
||||
You may not propagate or modify a covered work except as expressly
|
||||
provided under this License. Any attempt otherwise to propagate or
|
||||
modify it is void, and will automatically terminate your rights under
|
||||
this License (including any patent licenses granted under the third
|
||||
paragraph of section 11).
|
||||
|
||||
However, if you cease all violation of this License, then your
|
||||
license from a particular copyright holder is reinstated (a)
|
||||
provisionally, unless and until the copyright holder explicitly and
|
||||
finally terminates your license, and (b) permanently, if the copyright
|
||||
holder fails to notify you of the violation by some reasonable means
|
||||
prior to 60 days after the cessation.
|
||||
|
||||
Moreover, your license from a particular copyright holder is
|
||||
reinstated permanently if the copyright holder notifies you of the
|
||||
violation by some reasonable means, this is the first time you have
|
||||
received notice of violation of this License (for any work) from that
|
||||
copyright holder, and you cure the violation prior to 30 days after
|
||||
your receipt of the notice.
|
||||
|
||||
Termination of your rights under this section does not terminate the
|
||||
licenses of parties who have received copies or rights from you under
|
||||
this License. If your rights have been terminated and not permanently
|
||||
reinstated, you do not qualify to receive new licenses for the same
|
||||
material under section 10.
|
||||
|
||||
9. Acceptance Not Required for Having Copies.
|
||||
|
||||
You are not required to accept this License in order to receive or
|
||||
run a copy of the Program. Ancillary propagation of a covered work
|
||||
occurring solely as a consequence of using peer-to-peer transmission
|
||||
to receive a copy likewise does not require acceptance. However,
|
||||
nothing other than this License grants you permission to propagate or
|
||||
modify any covered work. These actions infringe copyright if you do
|
||||
not accept this License. Therefore, by modifying or propagating a
|
||||
covered work, you indicate your acceptance of this License to do so.
|
||||
|
||||
10. Automatic Licensing of Downstream Recipients.
|
||||
|
||||
Each time you convey a covered work, the recipient automatically
|
||||
receives a license from the original licensors, to run, modify and
|
||||
propagate that work, subject to this License. You are not responsible
|
||||
for enforcing compliance by third parties with this License.
|
||||
|
||||
An "entity transaction" is a transaction transferring control of an
|
||||
organization, or substantially all assets of one, or subdividing an
|
||||
organization, or merging organizations. If propagation of a covered
|
||||
work results from an entity transaction, each party to that
|
||||
transaction who receives a copy of the work also receives whatever
|
||||
licenses to the work the party's predecessor in interest had or could
|
||||
give under the previous paragraph, plus a right to possession of the
|
||||
Corresponding Source of the work from the predecessor in interest, if
|
||||
the predecessor has it or can get it with reasonable efforts.
|
||||
|
||||
You may not impose any further restrictions on the exercise of the
|
||||
rights granted or affirmed under this License. For example, you may
|
||||
not impose a license fee, royalty, or other charge for exercise of
|
||||
rights granted under this License, and you may not initiate litigation
|
||||
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||
any patent claim is infringed by making, using, selling, offering for
|
||||
sale, or importing the Program or any portion of it.
|
||||
|
||||
11. Patents.
|
||||
|
||||
A "contributor" is a copyright holder who authorizes use under this
|
||||
License of the Program or a work on which the Program is based. The
|
||||
work thus licensed is called the contributor's "contributor version".
|
||||
|
||||
A contributor's "essential patent claims" are all patent claims
|
||||
owned or controlled by the contributor, whether already acquired or
|
||||
hereafter acquired, that would be infringed by some manner, permitted
|
||||
by this License, of making, using, or selling its contributor version,
|
||||
but do not include claims that would be infringed only as a
|
||||
consequence of further modification of the contributor version. For
|
||||
purposes of this definition, "control" includes the right to grant
|
||||
patent sublicenses in a manner consistent with the requirements of
|
||||
this License.
|
||||
|
||||
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||
patent license under the contributor's essential patent claims, to
|
||||
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||
propagate the contents of its contributor version.
|
||||
|
||||
In the following three paragraphs, a "patent license" is any express
|
||||
agreement or commitment, however denominated, not to enforce a patent
|
||||
(such as an express permission to practice a patent or covenant not to
|
||||
sue for patent infringement). To "grant" such a patent license to a
|
||||
party means to make such an agreement or commitment not to enforce a
|
||||
patent against the party.
|
||||
|
||||
If you convey a covered work, knowingly relying on a patent license,
|
||||
and the Corresponding Source of the work is not available for anyone
|
||||
to copy, free of charge and under the terms of this License, through a
|
||||
publicly available network server or other readily accessible means,
|
||||
then you must either (1) cause the Corresponding Source to be so
|
||||
available, or (2) arrange to deprive yourself of the benefit of the
|
||||
patent license for this particular work, or (3) arrange, in a manner
|
||||
consistent with the requirements of this License, to extend the patent
|
||||
license to downstream recipients. "Knowingly relying" means you have
|
||||
actual knowledge that, but for the patent license, your conveying the
|
||||
covered work in a country, or your recipient's use of the covered work
|
||||
in a country, would infringe one or more identifiable patents in that
|
||||
country that you have reason to believe are valid.
|
||||
|
||||
If, pursuant to or in connection with a single transaction or
|
||||
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||
covered work, and grant a patent license to some of the parties
|
||||
receiving the covered work authorizing them to use, propagate, modify
|
||||
or convey a specific copy of the covered work, then the patent license
|
||||
you grant is automatically extended to all recipients of the covered
|
||||
work and works based on it.
|
||||
|
||||
A patent license is "discriminatory" if it does not include within
|
||||
the scope of its coverage, prohibits the exercise of, or is
|
||||
conditioned on the non-exercise of one or more of the rights that are
|
||||
specifically granted under this License. You may not convey a covered
|
||||
work if you are a party to an arrangement with a third party that is
|
||||
in the business of distributing software, under which you make payment
|
||||
to the third party based on the extent of your activity of conveying
|
||||
the work, and under which the third party grants, to any of the
|
||||
parties who would receive the covered work from you, a discriminatory
|
||||
patent license (a) in connection with copies of the covered work
|
||||
conveyed by you (or copies made from those copies), or (b) primarily
|
||||
for and in connection with specific products or compilations that
|
||||
contain the covered work, unless you entered into that arrangement,
|
||||
or that patent license was granted, prior to 28 March 2007.
|
||||
|
||||
Nothing in this License shall be construed as excluding or limiting
|
||||
any implied license or other defenses to infringement that may
|
||||
otherwise be available to you under applicable patent law.
|
||||
|
||||
12. No Surrender of Others' Freedom.
|
||||
|
||||
If conditions are imposed on you (whether by court order, agreement or
|
||||
otherwise) that contradict the conditions of this License, they do not
|
||||
excuse you from the conditions of this License. If you cannot convey a
|
||||
covered work so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you may
|
||||
not convey it at all. For example, if you agree to terms that obligate you
|
||||
to collect a royalty for further conveying from those to whom you convey
|
||||
the Program, the only way you could satisfy both those terms and this
|
||||
License would be to refrain entirely from conveying the Program.
|
||||
|
||||
13. Use with the GNU Affero General Public License.
|
||||
|
||||
Notwithstanding any other provision of this License, you have
|
||||
permission to link or combine any covered work with a work licensed
|
||||
under version 3 of the GNU Affero General Public License into a single
|
||||
combined work, and to convey the resulting work. The terms of this
|
||||
License will continue to apply to the part which is the covered work,
|
||||
but the special requirements of the GNU Affero General Public License,
|
||||
section 13, concerning interaction through a network will apply to the
|
||||
combination as such.
|
||||
|
||||
14. Revised Versions of this License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions of
|
||||
the GNU General Public License from time to time. Such new versions will
|
||||
be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the
|
||||
Program specifies that a certain numbered version of the GNU General
|
||||
Public License "or any later version" applies to it, you have the
|
||||
option of following the terms and conditions either of that numbered
|
||||
version or of any later version published by the Free Software
|
||||
Foundation. If the Program does not specify a version number of the
|
||||
GNU General Public License, you may choose any version ever published
|
||||
by the Free Software Foundation.
|
||||
|
||||
If the Program specifies that a proxy can decide which future
|
||||
versions of the GNU General Public License can be used, that proxy's
|
||||
public statement of acceptance of a version permanently authorizes you
|
||||
to choose that version for the Program.
|
||||
|
||||
Later license versions may give you additional or different
|
||||
permissions. However, no additional obligations are imposed on any
|
||||
author or copyright holder as a result of your choosing to follow a
|
||||
later version.
|
||||
|
||||
15. Disclaimer of Warranty.
|
||||
|
||||
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
||||
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
||||
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||
|
||||
16. Limitation of Liability.
|
||||
|
||||
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
||||
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
||||
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
||||
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
||||
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
||||
SUCH DAMAGES.
|
||||
|
||||
17. Interpretation of Sections 15 and 16.
|
||||
|
||||
If the disclaimer of warranty and limitation of liability provided
|
||||
above cannot be given local legal effect according to their terms,
|
||||
reviewing courts shall apply local law that most closely approximates
|
||||
an absolute waiver of all civil liability in connection with the
|
||||
Program, unless a warranty or assumption of liability accompanies a
|
||||
copy of the Program in return for a fee.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Programs
|
||||
|
||||
If you develop a new program, and you want it to be of the greatest
|
||||
possible use to the public, the best way to achieve this is to make it
|
||||
free software which everyone can redistribute and change under these terms.
|
||||
|
||||
To do so, attach the following notices to the program. It is safest
|
||||
to attach them to the start of each source file to most effectively
|
||||
state the exclusion of warranty; and each file should have at least
|
||||
the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
<one line to give the program's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If the program does terminal interaction, make it output a short
|
||||
notice like this when it starts in an interactive mode:
|
||||
|
||||
<program> Copyright (C) <year> <name of author>
|
||||
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||
This is free software, and you are welcome to redistribute it
|
||||
under certain conditions; type `show c' for details.
|
||||
|
||||
The hypothetical commands `show w' and `show c' should show the appropriate
|
||||
parts of the General Public License. Of course, your program's commands
|
||||
might be different; for a GUI interface, you would use an "about box".
|
||||
|
||||
You should also get your employer (if you work as a programmer) or school,
|
||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||
For more information on this, and how to apply and follow the GNU GPL, see
|
||||
<http://www.gnu.org/licenses/>.
|
||||
|
||||
The GNU General Public License does not permit incorporating your program
|
||||
into proprietary programs. If your program is a subroutine library, you
|
||||
may consider it more useful to permit linking proprietary applications with
|
||||
the library. If this is what you want to do, use the GNU Lesser General
|
||||
Public License instead of this License. But first, please read
|
||||
<http://www.gnu.org/philosophy/why-not-lgpl.html>.
|
||||
@@ -1,92 +0,0 @@
|
||||
# bbbridge (C++)
|
||||
|
||||
Play **co-op with bbhost players** on **The Hunter's Dream** while using
|
||||
**bbport** (Supermedo's `bloodborne_pc` for Windows, DLSS / FSR 3.1 / FSR 4).
|
||||
|
||||
bbbridge looks like a shadNet server to bbport and talks to The Hunter's Dream
|
||||
the way bbhost does: matching (rooms, bells, summons), the P2P traffic between
|
||||
the two clients (different packet framing, bbport's signaling handshake,
|
||||
The Hunter's Dream's STUN and relay), and the game's own server traffic
|
||||
(account token, play log for the live map). bbport itself is not modified.
|
||||
|
||||
## Layout
|
||||
|
||||
```
|
||||
E:\bloodborne\ bbport
|
||||
└─ bbbridge\ this folder
|
||||
├─ bbbridge.exe the app (built)
|
||||
├─ bbbridge_config.json settings (optional)
|
||||
├─ bbbridge_state.json your account: token + recovery code - never share it
|
||||
├─ bbbridge.log log of the last run (previous: bbbridge.prev.log)
|
||||
├─ src\ C++ sources
|
||||
├─ tests\ unit tests (C++) and end-to-end tests (Python)
|
||||
├─ third_party\ nlohmann/json 3.11.3, Dear ImGui 1.91.5
|
||||
└─ bin\ bbbridge-cli.exe (headless), bbbridge-unit.exe
|
||||
```
|
||||
<img width="798" height="718" alt="resim" src="https://github.com/user-attachments/assets/672a0038-4ead-4572-9293-b6c3936ed128" />
|
||||
|
||||
|
||||
## Using it
|
||||
|
||||
1. Put the `bbbridge` folder inside bbport's folder (next to `Bloodborne.exe`)
|
||||
and start `bbbridge.exe` (close the bbport launcher first). bbbridge uses
|
||||
the bbport it sits in; anywhere else it asks which bbport to use
|
||||
(**Change...** picks another one later).
|
||||
2. **Sign in with Discord**: the app shows a code and opens The Hunter's Dream;
|
||||
sign in there with your Discord account and approve the code. Your account
|
||||
there (the same one bbhost uses) becomes bbport's online id. No password is
|
||||
stored; the account's token goes to `bbbridge_state.json`.
|
||||
3. The **Online** switch goes to **The Hunter's Dream (bbhost)** by itself
|
||||
once you are signed in (and the live map patch is added; untick it if you
|
||||
like).
|
||||
4. Press **PLAY**. The app starts the bridge if needed and launches the game
|
||||
with bbport's saved settings. You can also start the game from the bbport
|
||||
launcher - as long as bbbridge is open. Graphics, mods and cheats stay in
|
||||
the bbport launcher (**bbport settings...**) or the in-game menu (Insert).
|
||||
|
||||
Move the **Online** switch to **shadNet** to go back to bbport's own shadNet
|
||||
co-op pool: bbport gets its own server and shadNet account back (the ones it
|
||||
had before; a shadNet account is registered at
|
||||
[shadnet.shadps4.net](https://shadnet.shadps4.net) and entered in the bbport
|
||||
launcher's *Online co-op*). PLAY follows the switch: right, the bbhost pool
|
||||
with your Discord sign-in; left, bbport's own shadNet - no sign-in needed.
|
||||
Running bbhost and bbport with the same account at the same time is not
|
||||
supported.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
- **"Failed to acquire Bloodborne server information" with bbbridge closed**:
|
||||
bbbridge 0.3.1 and older (and the Python 0.2.0) left
|
||||
`user\host_overrides.custom.json` in bbport's folder (bbport's user folder),
|
||||
which sends the game's server traffic to bbbridge even when it is not
|
||||
running. Start bbbridge 0.3.2 or newer once (it removes the file when it is
|
||||
not needed), or delete that file by hand. From 0.3.2 the file exists only
|
||||
while bbbridge runs with the switch on The Hunter's Dream.
|
||||
- **Nobody comes when I ring a bell**: a Small Resonant Bell is answered only
|
||||
by a host ringing the Beckoning Bell in the same area, near your level (and
|
||||
the other way round). The live map at
|
||||
[thehuntersdream.com/map](https://thehuntersdream.com/map) shows who is
|
||||
ringing which bell where.
|
||||
|
||||
## Building (Visual Studio 2022/2026)
|
||||
|
||||
Workload **Desktop development with C++** (MSVC, Windows SDK, CMake tools).
|
||||
|
||||
- Visual Studio: *File > Open > Folder...* this folder, pick the **x64 Release**
|
||||
preset, *Build > Build All*. `bbbridge.exe` is copied here, the CLI and the
|
||||
unit tests to `bin\`.
|
||||
- Command line (Developer PowerShell):
|
||||
`cmake --preset x64-release` then `cmake --build --preset x64-release`
|
||||
|
||||
## Tests
|
||||
|
||||
- `bin\bbbridge-unit.exe` - framing, signaling, STUN, protobuf, account id.
|
||||
- `python tests\e2e\test_e2e.py` - the bridge exe against a fake bbhost
|
||||
server, a fake STUN/relay server and fake game sockets (no internet).
|
||||
|
||||
## Credits and license
|
||||
|
||||
The protocol comes from **bbhost** and The Hunter's Dream by droogie (GPL-3.0);
|
||||
**bloodborne_pc / bbport** by Supermedo on deadinside28's port and shadPS4.
|
||||
bbbridge is GPL-3.0-or-later. nlohmann/json and Dear ImGui are MIT-licensed.
|
||||
Not affiliated with Sony, FromSoftware, Supermedo or The Hunter's Dream.
|
||||
@@ -1,51 +0,0 @@
|
||||
bbbridge includes the following third-party software.
|
||||
|
||||
------------------------------------------------------------------------------
|
||||
Dear ImGui 1.91.5 - https://github.com/ocornut/imgui
|
||||
------------------------------------------------------------------------------
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2014-2024 Omar Cornut
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
------------------------------------------------------------------------------
|
||||
JSON for Modern C++ 3.11.3 - https://github.com/nlohmann/json
|
||||
------------------------------------------------------------------------------
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2013-2023 Niels Lohmann <https://nlohmann.me>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -1,248 +0,0 @@
|
||||
#include "account_link.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <fstream>
|
||||
|
||||
#include "http.hpp"
|
||||
|
||||
namespace bb {
|
||||
|
||||
namespace {
|
||||
|
||||
// POST to the account server; the reply is read whatever the status (a limit
|
||||
// is a 429 whose Message the player should see), as bbhost's auth_call does.
|
||||
bool auth_call(const std::string& base, const std::string& path, const json& body, json& reply, std::string& error) {
|
||||
http::ClientRequest req;
|
||||
req.url = base + path;
|
||||
req.body = body.dump();
|
||||
req.headers = {{"Content-Type", "application/json"}, {"Accept", "application/json"},
|
||||
{"User-Agent", std::string("bbbridge/") + kVersion}};
|
||||
const http::ClientResponse res = http::send(req);
|
||||
if (!res.ok()) {
|
||||
error = res.error;
|
||||
return false;
|
||||
}
|
||||
reply = json::parse(res.body, nullptr, false);
|
||||
if (reply.is_discarded() || !reply.is_object() || !reply.contains("ResKind")) {
|
||||
error = fmt("%s has no account service (HTTP %d)", base.c_str(), res.status);
|
||||
return false;
|
||||
}
|
||||
if (jint(reply, "ResKind", 1) != 0) {
|
||||
error = jstr(reply, "Message", "refused");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
namespace {
|
||||
json read_state(const std::wstring& path) {
|
||||
std::ifstream in(fspath(path));
|
||||
if (!in) return json::object();
|
||||
json j = json::parse(in, nullptr, false);
|
||||
return j.is_object() ? j : json::object();
|
||||
}
|
||||
} // namespace
|
||||
|
||||
std::string account_kind(const std::wstring& state_file, const std::string& npid) {
|
||||
const json state = read_state(state_file);
|
||||
if (!state.contains("accounts") || !state["accounts"].contains(npid)) return "";
|
||||
const json& e = state["accounts"][npid];
|
||||
if (jstr(e, "token").empty() && jstr(e, "recovery_code").empty()) return "";
|
||||
return jstr(e, "linked") == "discord" ? "discord" : "bbbridge";
|
||||
}
|
||||
|
||||
std::string website_code(const std::string& auth_server, const std::wstring& state_file, const std::string& npid,
|
||||
std::string& error) {
|
||||
const json state = read_state(state_file);
|
||||
const std::string token =
|
||||
state.contains("accounts") && state["accounts"].contains(npid) ? jstr(state["accounts"][npid], "token") : "";
|
||||
if (token.empty()) {
|
||||
error = "no account for '" + npid + "' yet - play once (or sign in with Discord) first";
|
||||
return "";
|
||||
}
|
||||
json reply;
|
||||
if (!auth_call(auth_server, "/auth/web_code", {{"Token", token}}, reply, error)) return "";
|
||||
const std::string code = jstr(reply, "Code");
|
||||
if (code.empty()) error = "no code in the reply";
|
||||
return code;
|
||||
}
|
||||
|
||||
namespace {
|
||||
bool write_state(const std::wstring& path, const json& state) {
|
||||
const std::wstring tmp = path + L".tmp";
|
||||
{
|
||||
std::ofstream out(fspath(tmp), std::ios::trunc);
|
||||
if (!out) return false;
|
||||
out << state.dump(2);
|
||||
}
|
||||
return replace_file(tmp, path);
|
||||
}
|
||||
} // namespace
|
||||
|
||||
std::string recovery_code_of(const std::wstring& state_file, const std::string& name) {
|
||||
const json state = read_state(state_file);
|
||||
return state.contains("accounts") && state["accounts"].contains(name)
|
||||
? jstr(state["accounts"][name], "recovery_code")
|
||||
: "";
|
||||
}
|
||||
|
||||
std::string account_create_now(const std::string& auth_server, const std::wstring& state_file, const std::string& name) {
|
||||
json reply;
|
||||
std::string error;
|
||||
if (!auth_call(auth_server, "/auth/account/create", {{"Name", name}, {"DeviceName", device_name()}}, reply, error))
|
||||
return error;
|
||||
json state = read_state(state_file);
|
||||
if (!state["accounts"].is_object()) state["accounts"] = json::object();
|
||||
state["accounts"][name] = {{"bbhost_name", name}, {"recovery_code", jstr(reply, "RecoveryCode")},
|
||||
{"token", jstr(reply, "Token")}, {"online_id", jstr(reply, "OnlineId", name)}};
|
||||
if (!write_state(state_file, state)) return "could not write the account file";
|
||||
log("account: created '" + name + "' on this PC (recovery code stored in bbbridge_state.json)");
|
||||
return "";
|
||||
}
|
||||
|
||||
std::string account_recover_now(const std::string& auth_server, const std::wstring& state_file, const std::string& name,
|
||||
const std::string& recovery_code) {
|
||||
json reply;
|
||||
std::string error;
|
||||
if (!auth_call(auth_server, "/auth/recover", {{"Name", name}, {"RecoveryCode", recovery_code}}, reply, error))
|
||||
return error;
|
||||
json state = read_state(state_file);
|
||||
if (!state["accounts"].is_object()) state["accounts"] = json::object();
|
||||
const std::string fresh = jstr(reply, "RecoveryCode");
|
||||
state["accounts"][name] = {{"bbhost_name", name}, {"recovery_code", fresh.empty() ? recovery_code : fresh},
|
||||
{"token", jstr(reply, "Token")}, {"online_id", jstr(reply, "OnlineId", name)}};
|
||||
if (!write_state(state_file, state)) return "could not write the account file";
|
||||
log("account: recovered '" + name + "' (its new recovery code is in bbbridge_state.json)");
|
||||
return "";
|
||||
}
|
||||
|
||||
std::string account_sign_out(const std::string& auth_server, const std::wstring& state_file, const std::string& name) {
|
||||
json state = read_state(state_file);
|
||||
if (!state.contains("accounts") || !state["accounts"].contains(name)) return "no account for '" + name + "' here";
|
||||
json& e = state["accounts"][name];
|
||||
const std::string token = jstr(e, "token");
|
||||
if (!token.empty()) {
|
||||
json reply;
|
||||
std::string error;
|
||||
auth_call(auth_server, "/auth/logout", {{"Token", token}}, reply, error); // best effort, as bbhost
|
||||
}
|
||||
if (jstr(e, "recovery_code").empty()) state["accounts"].erase(name); // Discord: sign in again to come back
|
||||
else e.erase("token");
|
||||
if (!write_state(state_file, state)) return "could not write the account file";
|
||||
log("account: signed out of '" + name + "' on this PC");
|
||||
return "";
|
||||
}
|
||||
|
||||
std::string device_name() {
|
||||
char host[256] = {};
|
||||
#ifdef _WIN32
|
||||
size_t n = 0;
|
||||
getenv_s(&n, host, sizeof(host), "COMPUTERNAME");
|
||||
#else
|
||||
const size_t n = gethostname(host, sizeof(host) - 1) == 0 ? std::strlen(host) + 1 : 0;
|
||||
#endif
|
||||
return n > 1 ? "bbbridge on " + std::string(host).substr(0, 48) : "bbbridge";
|
||||
}
|
||||
|
||||
double DiscordLink::seconds_left() const {
|
||||
std::lock_guard<std::mutex> lk(mu_);
|
||||
return expires_at_ > 0 ? std::max(0.0, expires_at_ - now()) : 0.0;
|
||||
}
|
||||
|
||||
void DiscordLink::start() {
|
||||
cancel();
|
||||
cancel_ = false;
|
||||
{
|
||||
std::lock_guard<std::mutex> lk(mu_);
|
||||
user_code_.clear();
|
||||
uri_.clear();
|
||||
online_id_.clear();
|
||||
message_.clear();
|
||||
expires_at_ = 0;
|
||||
}
|
||||
state_ = State::Starting;
|
||||
worker_ = std::thread([this] { run(); });
|
||||
}
|
||||
|
||||
void DiscordLink::cancel() {
|
||||
cancel_ = true;
|
||||
if (worker_.joinable()) worker_.join();
|
||||
if (state_ == State::Starting || state_ == State::Waiting) state_ = State::Idle;
|
||||
}
|
||||
|
||||
void DiscordLink::fail(const std::string& why) {
|
||||
{
|
||||
std::lock_guard<std::mutex> lk(mu_);
|
||||
message_ = why;
|
||||
}
|
||||
log("discord sign-in: " + why);
|
||||
state_ = State::Failed;
|
||||
}
|
||||
|
||||
void DiscordLink::run() {
|
||||
json reply;
|
||||
std::string error;
|
||||
if (!auth_call(auth_, "/auth/device/start", {{"DeviceName", device_name()}}, reply, error)) return fail(error);
|
||||
const std::string code = jstr(reply, "DeviceCode");
|
||||
int interval = static_cast<int>(jint(reply, "Interval", 5));
|
||||
if (interval < 1) interval = 5;
|
||||
const int expires = static_cast<int>(jint(reply, "ExpiresIn", 600));
|
||||
{
|
||||
std::lock_guard<std::mutex> lk(mu_);
|
||||
user_code_ = jstr(reply, "UserCode");
|
||||
uri_ = jstr(reply, "VerificationUri");
|
||||
expires_at_ = now() + expires;
|
||||
if (code.empty() || user_code_.empty()) return fail("no code in the reply");
|
||||
}
|
||||
log("discord sign-in: approve code " + user_code() + " at " + uri());
|
||||
state_ = State::Waiting;
|
||||
while (!cancel_) {
|
||||
for (int i = 0; i < interval * 10 && !cancel_; ++i) Sleep(100);
|
||||
if (cancel_) return;
|
||||
if (seconds_left() <= 0) return fail("the code expired - start again");
|
||||
if (!auth_call(auth_, "/auth/device/poll", {{"DeviceCode", code}}, reply, error)) return fail(error);
|
||||
const std::string status = jstr(reply, "Status");
|
||||
if (status == "approved") {
|
||||
const std::string token = jstr(reply, "Token"), id = jstr(reply, "OnlineId");
|
||||
if (token.empty() || id.empty()) return fail("no token in the reply");
|
||||
if (!save(id, token)) return fail("could not write the account file");
|
||||
{
|
||||
std::lock_guard<std::mutex> lk(mu_);
|
||||
online_id_ = id;
|
||||
}
|
||||
log("discord sign-in: linked, signed in as '" + id + "'");
|
||||
state_ = State::Approved;
|
||||
return;
|
||||
}
|
||||
if (status != "pending" && !status.empty()) return fail("the server says: " + status);
|
||||
}
|
||||
}
|
||||
|
||||
bool DiscordLink::save(const std::string& online_id, const std::string& token) {
|
||||
json state = json::object();
|
||||
{
|
||||
std::ifstream in(fspath(state_file_));
|
||||
if (in) {
|
||||
state = json::parse(in, nullptr, false);
|
||||
if (!state.is_object()) state = json::object();
|
||||
}
|
||||
}
|
||||
if (!state["accounts"].is_object()) state["accounts"] = json::object();
|
||||
// Keyed by the account's name, which becomes the NPID: the game signs in to
|
||||
// the game server under it, and the token's account matches.
|
||||
state["accounts"][online_id] = {{"bbhost_name", online_id}, {"token", token}, {"online_id", online_id},
|
||||
{"linked", "discord"}};
|
||||
const std::wstring tmp = state_file_ + L".tmp";
|
||||
{
|
||||
std::ofstream out(fspath(tmp), std::ios::trunc);
|
||||
if (!out) return false;
|
||||
out << state.dump(2);
|
||||
}
|
||||
return replace_file(tmp, state_file_);
|
||||
}
|
||||
|
||||
} // namespace bb
|
||||
@@ -1,73 +0,0 @@
|
||||
// Sign in with Discord: the OAuth device flow bbhost uses (src/net/account.cpp).
|
||||
// POST /auth/device/start {DeviceName} -> DeviceCode, UserCode, VerificationUri, Interval, ExpiresIn
|
||||
// POST /auth/device/poll {DeviceCode} -> Status pending | approved (+ Token, OnlineId) | ...
|
||||
// The player approves the code on The Hunter's Dream with their Discord account;
|
||||
// the account and its token then go into bbbridge_state.json under its name, and
|
||||
// the bridge signs in with that token when the game connects with that NPID.
|
||||
#pragma once
|
||||
|
||||
#include <atomic>
|
||||
#include <functional>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
|
||||
#include "util.hpp"
|
||||
|
||||
namespace bb {
|
||||
|
||||
class DiscordLink {
|
||||
public:
|
||||
enum class State { Idle, Starting, Waiting, Approved, Failed };
|
||||
|
||||
DiscordLink(std::string auth_server, std::wstring state_file) : auth_(std::move(auth_server)), state_file_(std::move(state_file)) {}
|
||||
~DiscordLink() { cancel(); }
|
||||
|
||||
void start(); // runs in its own thread; read progress with the getters
|
||||
void cancel();
|
||||
|
||||
State state() const { return state_; }
|
||||
std::string user_code() const { std::lock_guard<std::mutex> lk(mu_); return user_code_; }
|
||||
std::string uri() const { std::lock_guard<std::mutex> lk(mu_); return uri_; }
|
||||
std::string online_id() const { std::lock_guard<std::mutex> lk(mu_); return online_id_; }
|
||||
std::string message() const { std::lock_guard<std::mutex> lk(mu_); return message_; }
|
||||
double seconds_left() const;
|
||||
|
||||
private:
|
||||
void run();
|
||||
void fail(const std::string& why);
|
||||
bool save(const std::string& online_id, const std::string& token);
|
||||
|
||||
std::string auth_;
|
||||
std::wstring state_file_;
|
||||
std::atomic<State> state_{State::Idle};
|
||||
std::atomic<bool> cancel_{false};
|
||||
mutable std::mutex mu_;
|
||||
std::string user_code_, uri_, online_id_, message_;
|
||||
double expires_at_ = 0;
|
||||
std::thread worker_;
|
||||
};
|
||||
|
||||
std::string device_name(); // "bbbridge on <COMPUTERNAME>"
|
||||
|
||||
// The website sign-in code for an account the bridge holds a token for
|
||||
// (thehuntersdream.com/account, "Sign in with a code from the game"):
|
||||
// POST /auth/web_code {Token} -> Code. Returns "" and sets error on failure.
|
||||
std::string website_code(const std::string& auth_server, const std::wstring& state_file, const std::string& npid,
|
||||
std::string& error);
|
||||
|
||||
// What the state file holds for this NPID: "discord", "bbbridge", or "" (none yet).
|
||||
std::string account_kind(const std::wstring& state_file, const std::string& npid);
|
||||
|
||||
// bbhost's other account buttons (src/net/account.cpp), on the same server:
|
||||
// create an account named `name` on this PC; recover one with its recovery code
|
||||
// (the server answers with a new code, which replaces the old one); sign out
|
||||
// (the token is ended on the server and removed here; a recovery code stays).
|
||||
// Each returns "" on success, else the reason.
|
||||
std::string account_create_now(const std::string& auth_server, const std::wstring& state_file, const std::string& name);
|
||||
std::string account_recover_now(const std::string& auth_server, const std::wstring& state_file, const std::string& name,
|
||||
const std::string& recovery_code);
|
||||
std::string account_sign_out(const std::string& auth_server, const std::wstring& state_file, const std::string& name);
|
||||
std::string recovery_code_of(const std::wstring& state_file, const std::string& name);
|
||||
|
||||
} // namespace bb
|
||||
@@ -1,976 +0,0 @@
|
||||
// bbbridge.exe: the bridge with a window. It sits in a folder inside bbport's
|
||||
// (E:\bloodborne\bbbridge\), keeps its config, account and log there, points
|
||||
// bbport at itself, and starts the game.
|
||||
#include "bridge.hpp" // first: winsock2.h before anything pulls in windows.h's winsock.h
|
||||
|
||||
#include <d3d11.h>
|
||||
#include <shellapi.h>
|
||||
#include <shlobj.h>
|
||||
#include <shobjidl.h>
|
||||
|
||||
#include <deque>
|
||||
#include <fstream>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
|
||||
#include "account_link.hpp"
|
||||
#include "imgui.h"
|
||||
#include "imgui_impl_dx11.h"
|
||||
#include "imgui_impl_win32.h"
|
||||
#include "setup.hpp"
|
||||
|
||||
#pragma comment(lib, "d3d11.lib")
|
||||
|
||||
extern IMGUI_IMPL_API LRESULT ImGui_ImplWin32_WndProcHandler(HWND, UINT, WPARAM, LPARAM);
|
||||
|
||||
namespace {
|
||||
|
||||
using namespace bb;
|
||||
|
||||
// ---- D3D11 boilerplate (from Dear ImGui's example_win32_directx11) ----
|
||||
ID3D11Device* g_device = nullptr;
|
||||
ID3D11DeviceContext* g_context = nullptr;
|
||||
IDXGISwapChain* g_swap = nullptr;
|
||||
ID3D11RenderTargetView* g_rtv = nullptr;
|
||||
UINT g_resize_w = 0, g_resize_h = 0;
|
||||
bool g_close_requested = false;
|
||||
|
||||
void create_rtv() {
|
||||
ID3D11Texture2D* back = nullptr;
|
||||
g_swap->GetBuffer(0, IID_PPV_ARGS(&back));
|
||||
g_device->CreateRenderTargetView(back, nullptr, &g_rtv);
|
||||
back->Release();
|
||||
}
|
||||
|
||||
void release_rtv() {
|
||||
if (g_rtv) g_rtv->Release();
|
||||
g_rtv = nullptr;
|
||||
}
|
||||
|
||||
bool create_device(HWND hwnd) {
|
||||
DXGI_SWAP_CHAIN_DESC sd{};
|
||||
sd.BufferCount = 2;
|
||||
sd.BufferDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;
|
||||
sd.BufferDesc.RefreshRate.Numerator = 60;
|
||||
sd.BufferDesc.RefreshRate.Denominator = 1;
|
||||
sd.Flags = DXGI_SWAP_CHAIN_FLAG_ALLOW_MODE_SWITCH;
|
||||
sd.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT;
|
||||
sd.OutputWindow = hwnd;
|
||||
sd.SampleDesc.Count = 1;
|
||||
sd.Windowed = TRUE;
|
||||
sd.SwapEffect = DXGI_SWAP_EFFECT_DISCARD;
|
||||
const D3D_FEATURE_LEVEL levels[] = {D3D_FEATURE_LEVEL_11_0, D3D_FEATURE_LEVEL_10_0};
|
||||
D3D_FEATURE_LEVEL got;
|
||||
HRESULT hr = D3D11CreateDeviceAndSwapChain(nullptr, D3D_DRIVER_TYPE_HARDWARE, nullptr, 0, levels, 2,
|
||||
D3D11_SDK_VERSION, &sd, &g_swap, &g_device, &got, &g_context);
|
||||
if (hr == DXGI_ERROR_UNSUPPORTED) // no GPU driver: the software rasterizer
|
||||
hr = D3D11CreateDeviceAndSwapChain(nullptr, D3D_DRIVER_TYPE_WARP, nullptr, 0, levels, 2, D3D11_SDK_VERSION,
|
||||
&sd, &g_swap, &g_device, &got, &g_context);
|
||||
if (FAILED(hr)) return false;
|
||||
create_rtv();
|
||||
return true;
|
||||
}
|
||||
|
||||
void destroy_device() {
|
||||
release_rtv();
|
||||
if (g_swap) g_swap->Release();
|
||||
if (g_context) g_context->Release();
|
||||
if (g_device) g_device->Release();
|
||||
}
|
||||
|
||||
LRESULT WINAPI wnd_proc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp) {
|
||||
if (ImGui_ImplWin32_WndProcHandler(hwnd, msg, wp, lp)) return TRUE;
|
||||
switch (msg) {
|
||||
case WM_SIZE:
|
||||
if (wp != SIZE_MINIMIZED) {
|
||||
g_resize_w = LOWORD(lp);
|
||||
g_resize_h = HIWORD(lp);
|
||||
}
|
||||
return 0;
|
||||
case WM_SYSCOMMAND:
|
||||
if ((wp & 0xfff0) == SC_KEYMENU) return 0;
|
||||
break;
|
||||
case WM_CLOSE:
|
||||
g_close_requested = true; // the app decides (the game may still be running)
|
||||
return 0;
|
||||
case WM_DESTROY:
|
||||
PostQuitMessage(0);
|
||||
return 0;
|
||||
}
|
||||
return DefWindowProcW(hwnd, msg, wp, lp);
|
||||
}
|
||||
|
||||
// ---- the app ----
|
||||
|
||||
fs::path exe_dir() {
|
||||
wchar_t buf[MAX_PATH];
|
||||
GetModuleFileNameW(nullptr, buf, MAX_PATH);
|
||||
return fs::path(buf).parent_path();
|
||||
}
|
||||
|
||||
// "" when started, else why not
|
||||
std::string launch(const fs::path& exe, const fs::path& cwd) {
|
||||
std::error_code ec;
|
||||
if (!fs::is_regular_file(exe, ec)) return narrow(exe.wstring()) + " is missing.";
|
||||
STARTUPINFOW si{};
|
||||
si.cb = sizeof(si);
|
||||
PROCESS_INFORMATION pi{};
|
||||
std::wstring cmd = L"\"" + exe.wstring() + L"\"";
|
||||
if (!CreateProcessW(exe.c_str(), cmd.data(), nullptr, nullptr, FALSE, 0, nullptr, cwd.c_str(), &si, &pi)) {
|
||||
const DWORD err = GetLastError();
|
||||
wchar_t* text = nullptr;
|
||||
FormatMessageW(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS,
|
||||
nullptr, err, 0, reinterpret_cast<wchar_t*>(&text), 0, nullptr);
|
||||
std::string why = text ? narrow(text) : fmt("error %lu", err);
|
||||
if (text) LocalFree(text);
|
||||
while (!why.empty() && (why.back() == '\n' || why.back() == '\r' || why.back() == '.')) why.pop_back();
|
||||
return "Could not start " + narrow(exe.wstring()) + ": " + why + ".";
|
||||
}
|
||||
CloseHandle(pi.hThread);
|
||||
CloseHandle(pi.hProcess);
|
||||
return "";
|
||||
}
|
||||
|
||||
// A two-way switch: "left" label, the switch, "right" label; *right is the
|
||||
// state. The switch and either label change it. True when changed.
|
||||
bool network_switch(const char* id, const char* left, const char* right, bool* on) {
|
||||
const ImVec4 lit(0.95f, 0.95f, 0.95f, 1), dim(0.5f, 0.5f, 0.5f, 1);
|
||||
const ImVec4 left_track(0.22f, 0.42f, 0.62f, 1), right_track(0.62f, 0.16f, 0.16f, 1);
|
||||
bool changed = false;
|
||||
ImGui::PushID(id);
|
||||
ImGui::AlignTextToFramePadding();
|
||||
ImGui::TextColored(*on ? dim : lit, "%s", left);
|
||||
if (ImGui::IsItemClicked() && *on) *on = false, changed = true;
|
||||
ImGui::SameLine();
|
||||
const float h = ImGui::GetFrameHeight(), w = h * 2.1f;
|
||||
const ImVec2 p = ImGui::GetCursorScreenPos();
|
||||
if (ImGui::InvisibleButton("switch", ImVec2(w, h))) *on = !*on, changed = true;
|
||||
const bool hovered = ImGui::IsItemHovered();
|
||||
// slide the knob over ~0.12 s
|
||||
ImGuiStorage* store = ImGui::GetStateStorage();
|
||||
const ImGuiID key = ImGui::GetID("t");
|
||||
float t = store->GetFloat(key, *on ? 1.0f : 0.0f);
|
||||
const float step = ImGui::GetIO().DeltaTime / 0.12f;
|
||||
t = *on ? (t + step > 1 ? 1 : t + step) : (t - step < 0 ? 0 : t - step);
|
||||
store->SetFloat(key, t);
|
||||
ImVec4 track(left_track.x + (right_track.x - left_track.x) * t, left_track.y + (right_track.y - left_track.y) * t,
|
||||
left_track.z + (right_track.z - left_track.z) * t, 1);
|
||||
if (hovered) track = ImVec4(track.x * 1.15f, track.y * 1.15f, track.z * 1.15f, 1);
|
||||
ImDrawList* dl = ImGui::GetWindowDrawList();
|
||||
dl->AddRectFilled(p, ImVec2(p.x + w, p.y + h), ImGui::GetColorU32(track), h * 0.5f);
|
||||
const float r = h * 0.5f - 3;
|
||||
dl->AddCircleFilled(ImVec2(p.x + r + 3 + t * (w - 2 * r - 6), p.y + h * 0.5f), r, ImGui::GetColorU32(lit));
|
||||
ImGui::SameLine();
|
||||
ImGui::TextColored(*on ? lit : dim, "%s", right);
|
||||
if (ImGui::IsItemClicked() && !*on) *on = true, changed = true;
|
||||
ImGui::PopID();
|
||||
return changed;
|
||||
}
|
||||
|
||||
std::optional<fs::path> pick_folder(HWND owner) {
|
||||
std::optional<fs::path> out;
|
||||
IFileOpenDialog* dlg = nullptr;
|
||||
if (FAILED(CoCreateInstance(CLSID_FileOpenDialog, nullptr, CLSCTX_ALL, IID_PPV_ARGS(&dlg)))) return out;
|
||||
DWORD opts = 0;
|
||||
dlg->GetOptions(&opts);
|
||||
dlg->SetOptions(opts | FOS_PICKFOLDERS);
|
||||
dlg->SetTitle(L"bbport folder (the one with Bloodborne.exe)");
|
||||
if (SUCCEEDED(dlg->Show(owner))) {
|
||||
IShellItem* item = nullptr;
|
||||
if (SUCCEEDED(dlg->GetResult(&item))) {
|
||||
PWSTR path = nullptr;
|
||||
if (SUCCEEDED(item->GetDisplayName(SIGDN_FILESYSPATH, &path))) {
|
||||
out = fs::path(path);
|
||||
CoTaskMemFree(path);
|
||||
}
|
||||
item->Release();
|
||||
}
|
||||
}
|
||||
dlg->Release();
|
||||
return out;
|
||||
}
|
||||
|
||||
class App {
|
||||
public:
|
||||
explicit App(HWND hwnd) : hwnd_(hwnd), dir_(exe_dir()) {
|
||||
set_log_file((dir_ / L"bbbridge.log").wstring());
|
||||
set_log_sink([this](const std::string& line) {
|
||||
std::lock_guard<std::mutex> lk(log_mu_);
|
||||
lines_.push_back(line);
|
||||
while (lines_.size() > 3000) lines_.pop_front();
|
||||
scroll_ = true;
|
||||
});
|
||||
load_config();
|
||||
reload_setup();
|
||||
log(fmt("bbbridge %s - bbport: %s (%s)", kVersion, bbport_dir_.empty() ? "-" : narrow(bbport_dir_.wstring()).c_str(),
|
||||
describe(bbport_source_).c_str()));
|
||||
if (!setup_error_.empty()) log(setup_error_);
|
||||
check_folder_on_start();
|
||||
if (setup_ && setup_->routed()) start_bridge(); // routed already: be ready when the game starts
|
||||
}
|
||||
|
||||
~App() {
|
||||
stop_bridge(); // and the game's server traffic goes back to bbport's own addresses
|
||||
}
|
||||
|
||||
void frame();
|
||||
bool wants_close() {
|
||||
if (!g_close_requested) return false;
|
||||
if (game_running_ && bridge_) {
|
||||
confirm_close_ = true;
|
||||
g_close_requested = false;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private:
|
||||
void load_config();
|
||||
void save_config();
|
||||
void try_setup();
|
||||
void reload_setup();
|
||||
void refresh();
|
||||
void check_folder_on_start();
|
||||
void choose_folder();
|
||||
void start_bridge();
|
||||
void stop_bridge();
|
||||
void play();
|
||||
void open_launcher();
|
||||
void fail(const std::string& message);
|
||||
void status_panel();
|
||||
void setup_panel();
|
||||
void account_row();
|
||||
void account_popups();
|
||||
void message_popups();
|
||||
void log_panel();
|
||||
void use_npid(const std::string& npid);
|
||||
void apply_defaults();
|
||||
void sync_overrides();
|
||||
bool live_map_wanted() const { return jint(config_, "live_map", 1) != 0; }
|
||||
bool route_wanted() const { return jint(config_, "route", 1) != 0; } // the box, as last ticked; bbhost by default
|
||||
bool signed_in() const { return !account_.empty() && !account_kind(state_path(), account_).empty(); }
|
||||
std::wstring state_path() const { return (dir_ / L"bbbridge_state.json").wstring(); }
|
||||
std::string auth_server() const { return jstr(config_, "auth_server", "https://thehuntersdream.com"); }
|
||||
|
||||
HWND hwnd_;
|
||||
fs::path dir_;
|
||||
json config_ = json::object();
|
||||
std::unique_ptr<Bridge> bridge_;
|
||||
std::optional<BbportSetup> setup_;
|
||||
std::string setup_error_;
|
||||
fs::path chosen_bbport_;
|
||||
fs::path bbport_dir_; // known even when setup_ is not (no launcher settings yet)
|
||||
BbportSource bbport_source_ = BbportSource::None;
|
||||
// The Hunter's Dream account (bbbridge_config.json "account"). It is bbport's
|
||||
// online id only while routed: unrouted, bbport keeps its own shadNet account.
|
||||
std::string account_;
|
||||
bool launcher_running_ = false, game_running_ = false, routed_ = false, patched_ = false;
|
||||
double last_refresh_ = 0;
|
||||
std::string error_, folder_question_;
|
||||
bool open_error_ = false, open_folder_ = false;
|
||||
bool confirm_close_ = false;
|
||||
std::unique_ptr<DiscordLink> link_;
|
||||
bool open_link_ = false, open_web_ = false;
|
||||
std::string web_code_, web_error_;
|
||||
std::mutex log_mu_;
|
||||
std::deque<std::string> lines_;
|
||||
bool scroll_ = false;
|
||||
};
|
||||
|
||||
void App::load_config() {
|
||||
std::ifstream in(dir_ / L"bbbridge_config.json");
|
||||
if (in) {
|
||||
json j = json::parse(in, nullptr, false);
|
||||
if (j.is_object()) config_ = j;
|
||||
}
|
||||
if (jhas(config_, "bbport_dir")) chosen_bbport_ = fs::path(widen(jstr(config_, "bbport_dir")));
|
||||
account_ = jstr(config_, "account");
|
||||
}
|
||||
|
||||
void App::save_config() {
|
||||
std::ofstream(dir_ / L"bbbridge_config.json", std::ios::trunc) << config_.dump(2) << "\n";
|
||||
}
|
||||
|
||||
void App::try_setup() {
|
||||
try {
|
||||
setup_.emplace(dir_, chosen_bbport_);
|
||||
setup_error_.clear();
|
||||
bbport_dir_ = setup_->bbport;
|
||||
bbport_source_ = setup_->source;
|
||||
// older bbbridge kept the account only as bbport's online id
|
||||
if (account_.empty() && !account_kind(state_path(), setup_->npid()).empty()) {
|
||||
account_ = setup_->npid();
|
||||
config_["account"] = account_;
|
||||
save_config();
|
||||
}
|
||||
} catch (const std::exception& e) {
|
||||
setup_.reset();
|
||||
setup_error_ = e.what();
|
||||
bbport_dir_ = find_bbport(dir_, chosen_bbport_, "", &bbport_source_); // still lets "bbport settings..." work
|
||||
}
|
||||
}
|
||||
|
||||
void App::reload_setup() {
|
||||
try_setup();
|
||||
refresh();
|
||||
}
|
||||
|
||||
// The first look: say where bbport is unless bbbridge sits inside it (the
|
||||
// normal install), and ask when it had to guess or a chosen folder is gone.
|
||||
void App::check_folder_on_start() {
|
||||
const std::string here = narrow(bbport_dir_.wstring());
|
||||
if (bbport_source_ == BbportSource::None) {
|
||||
folder_question_ = "bbport was not found.\n\nPut the bbbridge folder inside bbport's folder (the one with "
|
||||
"Bloodborne.exe), e.g. bloodborne\\bbbridge\\bbbridge.exe, or choose the bbport folder.";
|
||||
} else if (!chosen_bbport_.empty() && bbport_source_ != BbportSource::Chosen) {
|
||||
folder_question_ = "The bbport folder you chose before has no Bloodborne.exe any more:\n " +
|
||||
narrow(chosen_bbport_.wstring()) + "\n\nbbbridge found bbport here instead (" +
|
||||
describe(bbport_source_) + "):\n " + here + "\n\nUse this folder?";
|
||||
} else if (bbport_source_ == BbportSource::Launcher) {
|
||||
folder_question_ = "bbbridge is not inside a bbport folder. The bbport launcher points to this one:\n " + here +
|
||||
"\n\nUse this folder?";
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
open_folder_ = true;
|
||||
}
|
||||
|
||||
void App::choose_folder() {
|
||||
auto p = pick_folder(hwnd_);
|
||||
if (!p) return;
|
||||
if (!is_bbport_dir(*p)) {
|
||||
fail(narrow(p->wstring()) + " is not a bbport folder: it has no Bloodborne.exe or Play Bloodborne.exe.\n\n"
|
||||
"Choose the folder that has Bloodborne.exe in it.");
|
||||
return;
|
||||
}
|
||||
chosen_bbport_ = *p;
|
||||
config_["bbport_dir"] = narrow(p->wstring());
|
||||
save_config();
|
||||
reload_setup();
|
||||
log("bbport folder: " + narrow(p->wstring()));
|
||||
if (!setup_error_.empty()) fail(setup_error_);
|
||||
}
|
||||
|
||||
void App::fail(const std::string& message) {
|
||||
log("error: " + message);
|
||||
error_ = message;
|
||||
open_error_ = true;
|
||||
}
|
||||
|
||||
void App::refresh() {
|
||||
launcher_running_ = launcher_running();
|
||||
game_running_ = game_running();
|
||||
if (!setup_ && !launcher_running_ && bbport_source_ != BbportSource::None) try_setup(); // e.g. the launcher just made its settings
|
||||
if (setup_) {
|
||||
try {
|
||||
if (!launcher_running_) setup_->reload(); // the launcher may have saved new settings
|
||||
} catch (...) {
|
||||
}
|
||||
routed_ = setup_->routed();
|
||||
patched_ = setup_->patched();
|
||||
if (!jint(config_, "defaults_applied", 0) && !launcher_running_ && !game_running_ && signed_in())
|
||||
apply_defaults();
|
||||
sync_overrides();
|
||||
}
|
||||
last_refresh_ = now();
|
||||
}
|
||||
|
||||
// The game's server traffic goes to bbbridge (host_overrides.custom.json) exactly
|
||||
// while bbport is switched to The Hunter's Dream and the bridge runs: with
|
||||
// bbbridge closed, or bbport set back to shadNet in its own launcher, the file
|
||||
// would leave the game unable to reach its server.
|
||||
void App::sync_overrides() {
|
||||
if (!setup_) return;
|
||||
const bool want = setup_->routed() && bridge_; // live: PLAY routes and starts the bridge in one go
|
||||
if (want == setup_->overrides_written()) return;
|
||||
try {
|
||||
setup_->set_overrides(want);
|
||||
log(want ? "the game's server traffic goes through bbbridge"
|
||||
: "the game's server traffic goes to bbport's own addresses again");
|
||||
} catch (const std::exception& e) {
|
||||
log(std::string("host_overrides.custom.json: ") + e.what());
|
||||
}
|
||||
}
|
||||
|
||||
// Once, when there is an account to play with: route bbport through bbbridge
|
||||
// and add the live map patch, so starting from the bbport launcher works too
|
||||
// and nobody has to remember them. Switching them off later sticks.
|
||||
void App::apply_defaults() {
|
||||
try {
|
||||
if (!routed_) setup_->enable();
|
||||
if (setup_->npid() != account_) setup_->set_npid(account_);
|
||||
if (live_map_wanted() && !patched_) setup_->set_patch(true);
|
||||
} catch (const std::exception& e) {
|
||||
fail(std::string("Could not set bbport up: ") + e.what());
|
||||
return;
|
||||
}
|
||||
config_["defaults_applied"] = true;
|
||||
config_["route"] = true;
|
||||
save_config();
|
||||
routed_ = setup_->routed();
|
||||
patched_ = setup_->patched();
|
||||
log("bbport now uses bbbridge" + std::string(patched_ ? ", with the live map patch" : ""));
|
||||
if (routed_) start_bridge();
|
||||
}
|
||||
|
||||
void App::start_bridge() {
|
||||
if (bridge_) return;
|
||||
Config cfg = Config::from_json(config_);
|
||||
cfg.state_file = narrow((dir_ / L"bbbridge_state.json").wstring()); // the account, next to the exe
|
||||
auto b = std::make_unique<Bridge>(cfg);
|
||||
std::string error;
|
||||
if (!b->start(&error)) {
|
||||
fail("The bridge could not start: " + error);
|
||||
return;
|
||||
}
|
||||
bridge_ = std::move(b);
|
||||
sync_overrides();
|
||||
}
|
||||
|
||||
void App::stop_bridge() {
|
||||
if (!bridge_) return;
|
||||
bridge_->stop();
|
||||
bridge_.reset();
|
||||
log("bridge stopped");
|
||||
sync_overrides();
|
||||
}
|
||||
|
||||
void App::play() {
|
||||
refresh();
|
||||
if (!setup_) {
|
||||
fail(setup_error_);
|
||||
return;
|
||||
}
|
||||
const bool via_bridge = route_wanted();
|
||||
if (via_bridge && !signed_in()) {
|
||||
fail("Not signed in. Press 'Sign in with Discord...' first - The Hunter's Dream needs your own account.\n\n"
|
||||
"(To play in bbport's own shadNet pool instead, move the Online switch to shadNet.)");
|
||||
return;
|
||||
}
|
||||
if (launcher_running_) {
|
||||
fail("Close the bbport launcher first - it would undo bbbridge's settings when it saves.");
|
||||
return;
|
||||
}
|
||||
if (!setup_->game_ready()) {
|
||||
fail("bbport has no game folder yet. Press 'bbport settings...', choose your Bloodborne folder "
|
||||
"(CUSA03173, the one with eboot.bin), close the launcher, then press PLAY.");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
if (via_bridge) {
|
||||
if (!routed_) setup_->enable();
|
||||
if (setup_->npid() != account_) setup_->set_npid(account_);
|
||||
if (live_map_wanted() && !patched_) setup_->set_patch(true);
|
||||
} else if (routed_) {
|
||||
setup_->disable(); // e.g. the launcher was open when the switch moved
|
||||
}
|
||||
} catch (const std::exception& e) {
|
||||
fail(std::string("Could not set bbport up: ") + e.what());
|
||||
return;
|
||||
}
|
||||
if (!via_bridge && setup_->online() && !setup_->shadnet_account_set()) {
|
||||
fail("bbport's online play is on but it has no shadNet account.\n\nRegister one at "
|
||||
"https://shadnet.shadps4.net, then press 'bbport settings...' > Online co-op and enter the account "
|
||||
"name (not the email) and password. Or move the Online switch to The Hunter's Dream "
|
||||
"to play with bbhost players using your Discord sign-in.");
|
||||
return;
|
||||
}
|
||||
if (via_bridge) {
|
||||
start_bridge();
|
||||
if (!bridge_) return;
|
||||
}
|
||||
if (const std::string err = launch(setup_->play_exe(), setup_->bbport); !err.empty()) {
|
||||
fail(err);
|
||||
return;
|
||||
}
|
||||
log(fmt("starting the game (%s): %s", via_bridge ? "bbhost pool through bbbridge" : "bbport's own shadNet",
|
||||
narrow(setup_->play_exe().wstring()).c_str()));
|
||||
refresh();
|
||||
}
|
||||
|
||||
void App::open_launcher() {
|
||||
refresh();
|
||||
if (bbport_dir_.empty()) {
|
||||
fail("bbport was not found. Press 'Choose bbport folder...' and pick the folder with Bloodborne.exe.");
|
||||
return;
|
||||
}
|
||||
if (launcher_running_) {
|
||||
fail("The bbport launcher is already open.");
|
||||
return;
|
||||
}
|
||||
if (game_running_) {
|
||||
fail("The game is running - close it before changing bbport's settings.");
|
||||
return;
|
||||
}
|
||||
if (const std::string err = launch(bbport_dir_ / L"Bloodborne.exe", bbport_dir_); !err.empty()) fail(err);
|
||||
}
|
||||
|
||||
void App::status_panel() {
|
||||
const BridgeStatus st = bridge_ ? bridge_->status() : BridgeStatus{};
|
||||
const ImVec4 green(0.35f, 0.85f, 0.45f, 1), grey(0.6f, 0.6f, 0.6f, 1), amber(0.95f, 0.75f, 0.3f, 1);
|
||||
ImGui::TextColored(bridge_ ? green : grey, bridge_ ? " Bridge running" : " Bridge stopped");
|
||||
if (bridge_) {
|
||||
ImGui::SameLine();
|
||||
if (st.relay_port)
|
||||
ImGui::TextDisabled(" relay port %d%s", st.relay_port,
|
||||
st.mapped ? (" - seen as " + st.mapped->str()).c_str() : "");
|
||||
else
|
||||
ImGui::TextDisabled(" waiting for The Hunter's Dream's STUN answer");
|
||||
}
|
||||
if (bridge_) {
|
||||
if (st.logged_in) ImGui::Text("Signed in as %s", st.npid.c_str());
|
||||
else if (!st.npid.empty()) ImGui::TextColored(amber, "Signing in as %s ...", st.npid.c_str());
|
||||
else ImGui::TextDisabled("Waiting for the game to connect");
|
||||
if (st.room_id) ImGui::Text("Room %llu - %d member(s)", static_cast<unsigned long long>(st.room_id), st.room_members);
|
||||
if (!st.peers.empty() && ImGui::BeginTable("peers", 4, ImGuiTableFlags_BordersInnerH | ImGuiTableFlags_SizingStretchProp)) {
|
||||
ImGui::TableSetupColumn("Player");
|
||||
ImGui::TableSetupColumn("Member");
|
||||
ImGui::TableSetupColumn("Packets out");
|
||||
ImGui::TableSetupColumn("Packets in");
|
||||
ImGui::TableHeadersRow();
|
||||
for (const auto& p : st.peers) {
|
||||
ImGui::TableNextRow();
|
||||
ImGui::TableNextColumn();
|
||||
ImGui::TextUnformatted(p.online_id.c_str());
|
||||
ImGui::TableNextColumn();
|
||||
ImGui::Text("%d", p.member_id);
|
||||
ImGui::TableNextColumn();
|
||||
ImGui::Text("%llu", static_cast<unsigned long long>(p.to_peer));
|
||||
ImGui::TableNextColumn();
|
||||
ImGui::Text("%llu", static_cast<unsigned long long>(p.to_port));
|
||||
}
|
||||
ImGui::EndTable();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void App::setup_panel() {
|
||||
ImGui::SeparatorText("Setup");
|
||||
if (!bbport_dir_.empty()) {
|
||||
ImGui::Text("bbport:");
|
||||
ImGui::SameLine();
|
||||
ImGui::TextUnformatted(narrow(bbport_dir_.wstring()).c_str());
|
||||
ImGui::SameLine();
|
||||
ImGui::TextDisabled("(%s)", describe(bbport_source_).c_str());
|
||||
ImGui::SameLine();
|
||||
if (ImGui::SmallButton("Change...")) choose_folder();
|
||||
}
|
||||
if (!setup_) {
|
||||
ImGui::PushTextWrapPos(0);
|
||||
ImGui::TextColored(ImVec4(0.95f, 0.5f, 0.4f, 1), "%s", setup_error_.c_str());
|
||||
ImGui::PopTextWrapPos();
|
||||
if (bbport_dir_.empty() && ImGui::Button("Choose bbport folder...")) choose_folder();
|
||||
return;
|
||||
}
|
||||
if (!setup_->game_ready())
|
||||
ImGui::TextColored(ImVec4(0.95f, 0.75f, 0.3f, 1),
|
||||
"Choose your game folder (CUSA03173, with eboot.bin) in the bbport launcher first.");
|
||||
account_row();
|
||||
ImGui::BeginDisabled(launcher_running_);
|
||||
bool routed = routed_;
|
||||
ImGui::AlignTextToFramePadding();
|
||||
ImGui::TextUnformatted("Online:");
|
||||
ImGui::SameLine();
|
||||
if (network_switch("network", "shadNet", "The Hunter's Dream (bbhost)", &routed)) {
|
||||
try {
|
||||
if (routed) {
|
||||
setup_->enable();
|
||||
if (signed_in() && setup_->npid() != account_) setup_->set_npid(account_);
|
||||
start_bridge(); // the game's server traffic needs it running
|
||||
} else {
|
||||
setup_->disable(); // bbport's own shadNet server and account come back
|
||||
}
|
||||
config_["route"] = routed;
|
||||
save_config();
|
||||
log(routed ? "bbport now uses bbbridge" : "bbport back on its own shadNet server and account");
|
||||
} catch (const std::exception& e) {
|
||||
fail(e.what());
|
||||
}
|
||||
refresh();
|
||||
}
|
||||
if (ImGui::IsItemHovered())
|
||||
ImGui::SetTooltip("Right: bbbridge connects bbport to The Hunter's Dream (bbhost players, Discord sign-in).\n"
|
||||
"Left: bbport's own shadNet server and account, without bbbridge.");
|
||||
ImGui::Indent();
|
||||
if (routed_)
|
||||
ImGui::TextDisabled(signed_in() ? "Co-op with bbhost players through bbbridge, as '%s'."
|
||||
: "Co-op with bbhost players through bbbridge - sign in with Discord first.",
|
||||
account_.c_str());
|
||||
else if (!setup_->online())
|
||||
ImGui::TextDisabled("bbport's own settings - online play is off there.");
|
||||
else if (setup_->shadnet_account_set())
|
||||
ImGui::TextDisabled("bbport's own shadNet pool (%s), as '%s'.", setup_->server().c_str(),
|
||||
setup_->npid().c_str());
|
||||
else
|
||||
ImGui::TextColored(ImVec4(0.95f, 0.75f, 0.3f, 1),
|
||||
"shadNet needs a shadNet account (shadnet.shadps4.net), entered in\n"
|
||||
"bbport settings > Online co-op.");
|
||||
ImGui::Unindent();
|
||||
bool patched = patched_;
|
||||
if (ImGui::Checkbox("Live map patch (show me on thehuntersdream.com's map)", &patched)) {
|
||||
setup_->set_patch(patched);
|
||||
config_["live_map"] = patched; // PLAY keeps to it
|
||||
save_config();
|
||||
refresh();
|
||||
}
|
||||
ImGui::EndDisabled();
|
||||
if (launcher_running_)
|
||||
ImGui::TextColored(ImVec4(0.95f, 0.75f, 0.3f, 1), "The bbport launcher is open - close it to change these.");
|
||||
bool trace = jint(config_, "trace", 0) != 0;
|
||||
if (ImGui::Checkbox("Detailed log (for bug reports; applies when the bridge restarts)", &trace)) {
|
||||
config_["trace"] = trace;
|
||||
save_config();
|
||||
}
|
||||
}
|
||||
|
||||
void App::use_npid(const std::string& npid) {
|
||||
account_ = npid;
|
||||
config_["account"] = npid;
|
||||
save_config();
|
||||
// bbport's online id changes only while it plays through bbbridge (PLAY sets it too)
|
||||
if (!setup_ || !routed_ || launcher_running_) return;
|
||||
try {
|
||||
setup_->set_npid(npid);
|
||||
log(fmt("online id set to '%s'", npid.c_str()));
|
||||
} catch (const std::exception& e) {
|
||||
fail(e.what());
|
||||
}
|
||||
}
|
||||
|
||||
void App::account_row() {
|
||||
// Everyone signs in with their own Discord account on The Hunter's Dream;
|
||||
// the account's name is bbport's online id (NPID) while routed.
|
||||
const std::string kind = account_.empty() ? "" : account_kind(state_path(), account_);
|
||||
if (!kind.empty()) {
|
||||
ImGui::TextColored(ImVec4(0.55f, 0.65f, 1.0f, 1), "Signed in as '%s'%s", account_.c_str(),
|
||||
kind == "discord" ? " (Discord)" : " (bbbridge account)");
|
||||
ImGui::BeginDisabled(launcher_running_);
|
||||
if (ImGui::Button("Website sign-in code")) {
|
||||
web_code_ = website_code(auth_server(), state_path(), account_, web_error_);
|
||||
open_web_ = true;
|
||||
}
|
||||
if (ImGui::IsItemHovered())
|
||||
ImGui::SetTooltip("A code for thehuntersdream.com/account, 'Sign in with a code from the game'.");
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("Switch account...")) {
|
||||
link_ = std::make_unique<DiscordLink>(auth_server(), state_path());
|
||||
link_->start();
|
||||
open_link_ = true;
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("Sign out")) {
|
||||
const std::string err = account_sign_out(auth_server(), state_path(), account_);
|
||||
if (!err.empty()) {
|
||||
fail("Sign out failed: " + err);
|
||||
} else {
|
||||
account_.clear();
|
||||
config_.erase("account");
|
||||
save_config();
|
||||
}
|
||||
}
|
||||
ImGui::EndDisabled();
|
||||
return;
|
||||
}
|
||||
ImGui::TextColored(ImVec4(0.95f, 0.75f, 0.3f, 1), "Not signed in. Sign in with your Discord account to play online.");
|
||||
ImGui::BeginDisabled(launcher_running_);
|
||||
if (ImGui::Button("Sign in with Discord...", ImVec2(220, 0))) {
|
||||
link_ = std::make_unique<DiscordLink>(auth_server(), state_path());
|
||||
link_->start();
|
||||
open_link_ = true;
|
||||
}
|
||||
ImGui::EndDisabled();
|
||||
ImGui::SameLine();
|
||||
ImGui::TextDisabled("opens The Hunter's Dream: sign in there with Discord and approve the code");
|
||||
}
|
||||
|
||||
void App::account_popups() {
|
||||
if (open_link_) {
|
||||
ImGui::OpenPopup("Sign in with Discord");
|
||||
open_link_ = false;
|
||||
}
|
||||
if (ImGui::BeginPopupModal("Sign in with Discord", nullptr, ImGuiWindowFlags_AlwaysAutoResize)) {
|
||||
const DiscordLink::State st = link_ ? link_->state() : DiscordLink::State::Idle;
|
||||
if (st == DiscordLink::State::Starting) {
|
||||
ImGui::TextUnformatted("Asking The Hunter's Dream for a code ...");
|
||||
} else if (st == DiscordLink::State::Waiting) {
|
||||
ImGui::TextUnformatted("1. Open the sign-in page and sign in with Discord.");
|
||||
ImGui::TextUnformatted("2. Enter this code there:");
|
||||
ImGui::SetWindowFontScale(2.2f);
|
||||
ImGui::TextColored(ImVec4(0.95f, 0.85f, 0.5f, 1), " %s", link_->user_code().c_str());
|
||||
ImGui::SetWindowFontScale(1.0f);
|
||||
ImGui::TextDisabled("%s", link_->uri().c_str());
|
||||
if (ImGui::Button("Open sign-in page"))
|
||||
ShellExecuteW(nullptr, L"open", widen(link_->uri()).c_str(), nullptr, nullptr, SW_SHOWNORMAL);
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("Copy code")) ImGui::SetClipboardText(link_->user_code().c_str());
|
||||
ImGui::TextDisabled("Waiting for the approval ... (%d s left)", static_cast<int>(link_->seconds_left()));
|
||||
} else if (st == DiscordLink::State::Approved) {
|
||||
ImGui::Text("Signed in as '%s'.", link_->online_id().c_str());
|
||||
ImGui::TextUnformatted("bbport will use this account as its online id.");
|
||||
} else if (st == DiscordLink::State::Failed) {
|
||||
ImGui::TextColored(ImVec4(0.95f, 0.5f, 0.4f, 1), "Sign-in failed: %s", link_->message().c_str());
|
||||
}
|
||||
ImGui::Separator();
|
||||
if (st == DiscordLink::State::Approved) {
|
||||
if (ImGui::Button("Done")) {
|
||||
use_npid(link_->online_id());
|
||||
link_.reset();
|
||||
ImGui::CloseCurrentPopup();
|
||||
}
|
||||
} else {
|
||||
if (st == DiscordLink::State::Failed && ImGui::Button("Try again")) link_->start();
|
||||
if (st == DiscordLink::State::Failed) ImGui::SameLine();
|
||||
if (ImGui::Button("Cancel")) {
|
||||
link_.reset();
|
||||
ImGui::CloseCurrentPopup();
|
||||
}
|
||||
}
|
||||
ImGui::EndPopup();
|
||||
}
|
||||
|
||||
if (open_web_) {
|
||||
ImGui::OpenPopup("Website sign-in code");
|
||||
open_web_ = false;
|
||||
}
|
||||
if (ImGui::BeginPopupModal("Website sign-in code", nullptr, ImGuiWindowFlags_AlwaysAutoResize)) {
|
||||
if (web_code_.empty()) {
|
||||
ImGui::TextColored(ImVec4(0.95f, 0.5f, 0.4f, 1), "No code: %s", web_error_.c_str());
|
||||
} else {
|
||||
ImGui::Text("For thehuntersdream.com/account, 'Sign in with a code from the game' (account '%s'):",
|
||||
account_.c_str());
|
||||
ImGui::SetWindowFontScale(2.2f);
|
||||
ImGui::TextColored(ImVec4(0.95f, 0.85f, 0.5f, 1), " %s", web_code_.c_str());
|
||||
ImGui::SetWindowFontScale(1.0f);
|
||||
ImGui::TextDisabled("Valid for a few minutes.");
|
||||
if (ImGui::Button("Copy code")) ImGui::SetClipboardText(web_code_.c_str());
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("Open the account page"))
|
||||
ShellExecuteW(nullptr, L"open", widen(auth_server() + "/account").c_str(), nullptr, nullptr, SW_SHOWNORMAL);
|
||||
}
|
||||
ImGui::Separator();
|
||||
if (ImGui::Button("Close")) ImGui::CloseCurrentPopup();
|
||||
ImGui::EndPopup();
|
||||
}
|
||||
}
|
||||
|
||||
void App::message_popups() {
|
||||
const float wrap = ImGui::GetFontSize() * 34;
|
||||
if (open_folder_) {
|
||||
ImGui::OpenPopup("bbport folder");
|
||||
open_folder_ = false;
|
||||
}
|
||||
if (ImGui::BeginPopupModal("bbport folder", nullptr, ImGuiWindowFlags_AlwaysAutoResize)) {
|
||||
ImGui::PushTextWrapPos(wrap);
|
||||
ImGui::TextUnformatted(folder_question_.c_str());
|
||||
ImGui::PopTextWrapPos();
|
||||
ImGui::Separator();
|
||||
if (!bbport_dir_.empty() && ImGui::Button("Use this folder")) {
|
||||
if (bbport_source_ == BbportSource::ExeFolder) { // the normal place: nothing to remember
|
||||
chosen_bbport_.clear();
|
||||
config_.erase("bbport_dir");
|
||||
} else {
|
||||
chosen_bbport_ = bbport_dir_;
|
||||
config_["bbport_dir"] = narrow(bbport_dir_.wstring());
|
||||
}
|
||||
save_config();
|
||||
reload_setup();
|
||||
ImGui::CloseCurrentPopup();
|
||||
}
|
||||
if (!bbport_dir_.empty()) ImGui::SameLine();
|
||||
if (ImGui::Button("Choose another folder...")) {
|
||||
ImGui::CloseCurrentPopup();
|
||||
choose_folder();
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("Close")) ImGui::CloseCurrentPopup();
|
||||
ImGui::EndPopup();
|
||||
}
|
||||
|
||||
if (open_error_) {
|
||||
ImGui::OpenPopup("bbbridge###error");
|
||||
open_error_ = false;
|
||||
}
|
||||
if (ImGui::BeginPopupModal("bbbridge###error", nullptr, ImGuiWindowFlags_AlwaysAutoResize)) {
|
||||
ImGui::PushTextWrapPos(wrap);
|
||||
ImGui::TextColored(ImVec4(0.95f, 0.6f, 0.5f, 1), "%s", error_.c_str());
|
||||
ImGui::PopTextWrapPos();
|
||||
ImGui::Separator();
|
||||
if (ImGui::Button("OK", ImVec2(100, 0)) || ImGui::IsKeyPressed(ImGuiKey_Enter)) ImGui::CloseCurrentPopup();
|
||||
ImGui::EndPopup();
|
||||
}
|
||||
}
|
||||
|
||||
void App::log_panel() {
|
||||
ImGui::SeparatorText("Log");
|
||||
if (ImGui::SmallButton("Copy log")) {
|
||||
std::string all;
|
||||
{
|
||||
std::lock_guard<std::mutex> lk(log_mu_);
|
||||
for (const auto& l : lines_) all += l + "\n";
|
||||
}
|
||||
ImGui::SetClipboardText(all.c_str());
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (ImGui::SmallButton("Open folder")) ShellExecuteW(nullptr, L"open", dir_.c_str(), nullptr, nullptr, SW_SHOWNORMAL);
|
||||
ImGui::BeginChild("log", ImVec2(0, 0), ImGuiChildFlags_Borders, ImGuiWindowFlags_HorizontalScrollbar);
|
||||
{
|
||||
std::lock_guard<std::mutex> lk(log_mu_);
|
||||
ImGuiListClipper clip;
|
||||
clip.Begin(static_cast<int>(lines_.size()));
|
||||
while (clip.Step())
|
||||
for (int i = clip.DisplayStart; i < clip.DisplayEnd; ++i) ImGui::TextUnformatted(lines_[i].c_str());
|
||||
if (scroll_ && ImGui::GetScrollY() >= ImGui::GetScrollMaxY() - 40) ImGui::SetScrollHereY(1.0f);
|
||||
scroll_ = false;
|
||||
}
|
||||
ImGui::EndChild();
|
||||
}
|
||||
|
||||
void App::frame() {
|
||||
if (now() - last_refresh_ > 1.5) refresh();
|
||||
const ImGuiViewport* vp = ImGui::GetMainViewport();
|
||||
ImGui::SetNextWindowPos(vp->WorkPos);
|
||||
ImGui::SetNextWindowSize(vp->WorkSize);
|
||||
ImGui::Begin("bbbridge", nullptr,
|
||||
ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoSavedSettings |
|
||||
ImGuiWindowFlags_NoBringToFrontOnFocus);
|
||||
ImGui::Text("bbbridge %s", kVersion);
|
||||
ImGui::SameLine();
|
||||
ImGui::TextDisabled("- co-op with bbhost players on The Hunter's Dream");
|
||||
ImGui::Spacing();
|
||||
status_panel();
|
||||
ImGui::Spacing();
|
||||
setup_panel();
|
||||
ImGui::Spacing();
|
||||
|
||||
// PLAY and bbport settings stay clickable: when something is missing they say what.
|
||||
ImGui::BeginDisabled(game_running_);
|
||||
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.55f, 0.12f, 0.12f, 1));
|
||||
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.7f, 0.16f, 0.16f, 1));
|
||||
if (ImGui::Button(game_running_ ? " PLAYING " : " PLAY ", ImVec2(160, 40))) play();
|
||||
ImGui::PopStyleColor(2);
|
||||
ImGui::EndDisabled();
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button(bridge_ ? "Stop bridge" : "Start bridge", ImVec2(130, 40))) {
|
||||
if (bridge_) stop_bridge();
|
||||
else start_bridge();
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("bbport settings...", ImVec2(160, 40))) open_launcher();
|
||||
if (ImGui::IsItemHovered())
|
||||
ImGui::SetTooltip("Opens the bbport launcher for graphics, mods and cheats. Close it before pressing PLAY here.");
|
||||
ImGui::Spacing();
|
||||
log_panel();
|
||||
|
||||
account_popups();
|
||||
message_popups();
|
||||
if (confirm_close_) ImGui::OpenPopup("Close bbbridge?");
|
||||
if (ImGui::BeginPopupModal("Close bbbridge?", nullptr, ImGuiWindowFlags_AlwaysAutoResize)) {
|
||||
ImGui::TextUnformatted("The game is still running. Closing bbbridge takes it offline.");
|
||||
if (ImGui::Button("Close anyway")) {
|
||||
confirm_close_ = false;
|
||||
g_close_requested = true;
|
||||
game_running_ = false;
|
||||
ImGui::CloseCurrentPopup();
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("Cancel")) {
|
||||
confirm_close_ = false;
|
||||
ImGui::CloseCurrentPopup();
|
||||
}
|
||||
ImGui::EndPopup();
|
||||
}
|
||||
ImGui::End();
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int WINAPI wWinMain(HINSTANCE inst, HINSTANCE, PWSTR, int) {
|
||||
// One bbbridge at a time: a second would fight the first over the ports.
|
||||
HANDLE single = CreateMutexW(nullptr, TRUE, L"Local\\bbbridge-app");
|
||||
if (GetLastError() == ERROR_ALREADY_EXISTS) {
|
||||
if (HWND other = FindWindowW(L"bbbridge", nullptr)) SetForegroundWindow(other);
|
||||
return 0;
|
||||
}
|
||||
CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED);
|
||||
net_init();
|
||||
ImGui_ImplWin32_EnableDpiAwareness();
|
||||
WNDCLASSEXW wc{sizeof(wc), CS_CLASSDC, wnd_proc, 0, 0, inst, LoadIconW(inst, MAKEINTRESOURCEW(1)), nullptr,
|
||||
nullptr, nullptr, L"bbbridge", nullptr};
|
||||
RegisterClassExW(&wc);
|
||||
const float scale = ImGui_ImplWin32_GetDpiScaleForMonitor(MonitorFromPoint(POINT{0, 0}, MONITOR_DEFAULTTOPRIMARY));
|
||||
HWND hwnd = CreateWindowW(wc.lpszClassName, L"bbbridge", WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT,
|
||||
static_cast<int>(820 * scale), static_cast<int>(760 * scale), nullptr, nullptr, inst, nullptr);
|
||||
if (!create_device(hwnd)) {
|
||||
MessageBoxW(nullptr, L"Direct3D 11 is not available.", L"bbbridge", MB_ICONERROR);
|
||||
return 1;
|
||||
}
|
||||
ShowWindow(hwnd, SW_SHOWDEFAULT);
|
||||
UpdateWindow(hwnd);
|
||||
|
||||
IMGUI_CHECKVERSION();
|
||||
ImGui::CreateContext();
|
||||
ImGuiIO& io = ImGui::GetIO();
|
||||
io.IniFilename = nullptr;
|
||||
ImGui::StyleColorsDark();
|
||||
ImGuiStyle& style = ImGui::GetStyle();
|
||||
style.WindowPadding = ImVec2(14, 12);
|
||||
style.FrameRounding = 4;
|
||||
style.ScaleAllSizes(scale);
|
||||
static const ImWchar ranges[] = {0x0020, 0x00FF, 0x0100, 0x017F, 0x2013, 0x2026, 0}; // Latin-1 + Ext-A
|
||||
if (GetFileAttributesW(L"C:\\Windows\\Fonts\\segoeui.ttf") != INVALID_FILE_ATTRIBUTES)
|
||||
io.Fonts->AddFontFromFileTTF("C:\\Windows\\Fonts\\segoeui.ttf", 17.0f * scale, nullptr, ranges);
|
||||
ImGui_ImplWin32_Init(hwnd);
|
||||
ImGui_ImplDX11_Init(g_device, g_context);
|
||||
|
||||
{
|
||||
App app(hwnd);
|
||||
bool done = false;
|
||||
double last_input = now();
|
||||
while (!done) {
|
||||
// Full rate (vsync) only while the player is using the window; idle
|
||||
// ~10 frames a second (the log and status still move), minimized 4 -
|
||||
// the game is the one that should have the CPU and GPU.
|
||||
const bool minimized = IsIconic(hwnd) != 0;
|
||||
if (now() - last_input > 0.5)
|
||||
MsgWaitForMultipleObjects(0, nullptr, FALSE, minimized ? 250 : 100, QS_ALLINPUT);
|
||||
MSG msg;
|
||||
while (PeekMessageW(&msg, nullptr, 0, 0, PM_REMOVE)) {
|
||||
TranslateMessage(&msg);
|
||||
DispatchMessageW(&msg);
|
||||
if (msg.message == WM_QUIT) done = true;
|
||||
if (!minimized && msg.message != WM_TIMER && msg.message != WM_PAINT) last_input = now();
|
||||
}
|
||||
if (done) break;
|
||||
if (app.wants_close()) {
|
||||
DestroyWindow(hwnd);
|
||||
continue;
|
||||
}
|
||||
if (g_resize_w) {
|
||||
release_rtv();
|
||||
g_swap->ResizeBuffers(0, g_resize_w, g_resize_h, DXGI_FORMAT_UNKNOWN, 0);
|
||||
g_resize_w = g_resize_h = 0;
|
||||
create_rtv();
|
||||
}
|
||||
ImGui_ImplDX11_NewFrame();
|
||||
ImGui_ImplWin32_NewFrame();
|
||||
ImGui::NewFrame();
|
||||
app.frame();
|
||||
ImGui::Render();
|
||||
if (minimized) continue; // the app still runs (status, close); nothing to show
|
||||
const float clear[4] = {0.07f, 0.07f, 0.08f, 1};
|
||||
g_context->OMSetRenderTargets(1, &g_rtv, nullptr);
|
||||
g_context->ClearRenderTargetView(g_rtv, clear);
|
||||
ImGui_ImplDX11_RenderDrawData(ImGui::GetDrawData());
|
||||
g_swap->Present(1, 0);
|
||||
}
|
||||
} // the App stops the bridge here
|
||||
ImGui_ImplDX11_Shutdown();
|
||||
ImGui_ImplWin32_Shutdown();
|
||||
ImGui::DestroyContext();
|
||||
destroy_device();
|
||||
UnregisterClassW(wc.lpszClassName, wc.hInstance);
|
||||
CoUninitialize();
|
||||
if (single) CloseHandle(single);
|
||||
return 0;
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
#include <winver.h>
|
||||
|
||||
VS_VERSION_INFO VERSIONINFO
|
||||
FILEVERSION 0,3,2,0
|
||||
PRODUCTVERSION 0,3,2,0
|
||||
FILEFLAGSMASK 0x3fL
|
||||
FILEOS VOS_NT_WINDOWS32
|
||||
FILETYPE VFT_APP
|
||||
BEGIN
|
||||
BLOCK "StringFileInfo"
|
||||
BEGIN
|
||||
BLOCK "040904b0"
|
||||
BEGIN
|
||||
VALUE "FileDescription", "bbbridge - co-op with bbhost players for bbport"
|
||||
VALUE "FileVersion", "0.3.2"
|
||||
VALUE "ProductName", "bbbridge"
|
||||
VALUE "ProductVersion", "0.3.2"
|
||||
VALUE "LegalCopyright", "GPL-3.0-or-later"
|
||||
VALUE "OriginalFilename", "bbbridge.exe"
|
||||
END
|
||||
END
|
||||
BLOCK "VarFileInfo"
|
||||
BEGIN
|
||||
VALUE "Translation", 0x409, 1200
|
||||
END
|
||||
END
|
||||
@@ -1,141 +0,0 @@
|
||||
#include "bbhost_client.hpp"
|
||||
|
||||
#include "http.hpp"
|
||||
|
||||
namespace bb {
|
||||
|
||||
BBHostClient::Reply BBHostClient::post(const std::string& path, const json& body, int timeout_ms, http::Pool* pool) {
|
||||
http::ClientRequest req;
|
||||
req.method = "POST";
|
||||
req.url = base_ + path;
|
||||
req.body = body.dump();
|
||||
req.timeout_ms = timeout_ms;
|
||||
req.headers = {{"Content-Type", "application/json"}, {"Accept", "application/json"}, {"User-Agent", user_agent}};
|
||||
const std::string tok = token();
|
||||
if (!tok.empty()) req.headers.emplace_back("X-BB-Token", tok);
|
||||
if (!ruleset.empty()) req.headers.emplace_back("X-BBHost-Ruleset", ruleset);
|
||||
http::ClientResponse res = pool ? pool->send(req) : http::send(req);
|
||||
if (!res.ok()) throw BridgeError(path + ": " + res.error);
|
||||
Reply r;
|
||||
r.status = res.status;
|
||||
r.body = json::parse(res.body, nullptr, false);
|
||||
if (r.body.is_discarded()) r.body = json::object();
|
||||
return r;
|
||||
}
|
||||
|
||||
json BBHostClient::call(const std::string& path, const json& body, int timeout_ms) {
|
||||
Reply r = post(path, body, timeout_ms);
|
||||
if (r.status < 200 || r.status >= 300)
|
||||
throw BridgeError(fmt("%s: HTTP %d %s", path.c_str(), r.status, jstr(r.body, "Message").c_str()));
|
||||
if (!r.body.is_object() || !r.body.contains("ResKind"))
|
||||
throw BridgeError(fmt("%s: no account/matching service (HTTP %d)", path.c_str(), r.status));
|
||||
if (jint(r.body, "ResKind", -1) != 0)
|
||||
throw BridgeError(path + ": refused: " + jstr(r.body, "Message", "unknown"));
|
||||
return r.body;
|
||||
}
|
||||
|
||||
std::string BBHostClient::account_create(const std::string& name) {
|
||||
json r = call("/auth/account/create", {{"Name", name}, {"DeviceName", "bbbridge"}});
|
||||
set_identity(jstr(r, "Token"), jstr(r, "OnlineId", name));
|
||||
return jstr(r, "RecoveryCode");
|
||||
}
|
||||
|
||||
std::string BBHostClient::account_recover(const std::string& name, const std::string& code) {
|
||||
json r = call("/auth/recover", {{"Name", name}, {"RecoveryCode", code}});
|
||||
set_identity(jstr(r, "Token"), jstr(r, "OnlineId", name));
|
||||
return jstr(r, "RecoveryCode");
|
||||
}
|
||||
|
||||
std::optional<bool> BBHostClient::token_valid(const std::string& online_id) {
|
||||
try {
|
||||
Reply r = post("/np/signaling/resolve", {{"OnlineId", online_id}}, 6000);
|
||||
if (r.status == 401) return false;
|
||||
if (r.status >= 200 && r.status < 500) return true;
|
||||
} catch (const BridgeError&) {
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
json BBHostClient::context_start(const std::string& online_id, const Endpoint& signaling, const Endpoint& mapped) {
|
||||
return call("/mp/matching2/context_start", {{"OnlineId", online_id},
|
||||
{"SignalingAddr", signaling.ip},
|
||||
{"SignalingPort", signaling.port},
|
||||
{"MappedAddr", mapped.ip},
|
||||
{"MappedPort", mapped.port}});
|
||||
}
|
||||
|
||||
json BBHostClient::create_room(const std::string& online_id, const Endpoint& local, const Endpoint& mapped,
|
||||
int max_members, const json& extra) {
|
||||
json body = {{"OnlineId", online_id}, {"LocalAddr", local.ip}, {"LocalPort", local.port},
|
||||
{"PublicAddr", local.ip}, {"PublicPort", local.port}, {"MappedAddr", mapped.ip},
|
||||
{"MappedPort", mapped.port}, {"MaxMembers", max_members}};
|
||||
if (extra.is_object())
|
||||
for (auto it = extra.begin(); it != extra.end(); ++it) body[it.key()] = it.value();
|
||||
return call("/mp/matching2/create_room", body);
|
||||
}
|
||||
|
||||
json BBHostClient::join_room(const std::string& online_id, uint64_t room_id, const Endpoint& local,
|
||||
const Endpoint& mapped) {
|
||||
return call("/mp/matching2/join_room", {{"OnlineId", online_id}, {"RoomId", room_id},
|
||||
{"LocalAddr", local.ip}, {"LocalPort", local.port},
|
||||
{"PublicAddr", local.ip}, {"PublicPort", local.port},
|
||||
{"MappedAddr", mapped.ip}, {"MappedPort", mapped.port}});
|
||||
}
|
||||
|
||||
void BBHostClient::leave_room(const std::string& session_id, int member_id) {
|
||||
call("/mp/matching2/leave_room", {{"SessionId", session_id}, {"MemberId", member_id}});
|
||||
}
|
||||
|
||||
int BBHostClient::heartbeat(const std::string& session_id, int member_id) {
|
||||
try {
|
||||
json r = call("/mp/matching2/heartbeat", {{"SessionId", session_id}, {"MemberId", member_id}}, 4000);
|
||||
return static_cast<int>(jint(r, "InRoom", 1));
|
||||
} catch (const BridgeError&) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
void BBHostClient::kick_member(const std::string& session_id, int member_id, int kicker_id) {
|
||||
call("/mp/matching2/kick_member",
|
||||
{{"SessionId", session_id}, {"MemberId", member_id}, {"KickerMemberId", kicker_id}, {"OptData", ""}});
|
||||
}
|
||||
|
||||
void BBHostClient::signaling_update(const std::string& online_id, const Endpoint& mapped,
|
||||
const std::string& session_id, int member_id) {
|
||||
try {
|
||||
call("/mp/matching2/signaling_update", {{"OnlineId", online_id}, {"MappedAddr", mapped.ip},
|
||||
{"MappedPort", mapped.port}, {"SessionId", session_id},
|
||||
{"MemberId", member_id}});
|
||||
} catch (const BridgeError& e) {
|
||||
log(std::string("signaling_update failed: ") + e.what());
|
||||
}
|
||||
}
|
||||
|
||||
std::optional<Endpoint> BBHostClient::resolve_peer(const std::string& online_id) {
|
||||
try {
|
||||
json r = call("/np/signaling/resolve", {{"OnlineId", online_id}});
|
||||
Endpoint e{jstr(r, "Addr"), static_cast<int>(jint(r, "Port"))};
|
||||
if (e.valid()) return e;
|
||||
} catch (const BridgeError&) {
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
uint64_t BBHostClient::poll_events(const std::string& online_id, uint64_t cursor, json& event) {
|
||||
Reply r = post("/np/events/poll", {{"OnlineId", online_id}, {"Cursor", cursor}, {"Categories", {"matching2"}}},
|
||||
5000, &poll_pool_);
|
||||
if (r.status == 401) throw BridgeError("server refused our token (HTTP 401)");
|
||||
if (r.status < 200 || r.status >= 300) throw BridgeError(fmt("/np/events/poll: HTTP %d", r.status));
|
||||
event = jint(r.body, "HasEvent", 0) ? r.body : json();
|
||||
return static_cast<uint64_t>(jint(r.body, "NextCursor", static_cast<int64_t>(cursor)));
|
||||
}
|
||||
|
||||
void BBHostClient::ack_event(const std::string& online_id, uint64_t cursor) {
|
||||
try {
|
||||
post("/np/events/ack", {{"OnlineId", online_id}, {"Cursor", cursor}}, 4000);
|
||||
} catch (const BridgeError& e) {
|
||||
log(std::string("event ack failed: ") + e.what());
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace bb
|
||||
@@ -1,75 +0,0 @@
|
||||
// The Hunter's Dream (bbhost server) REST client. The contract is bbhost's own
|
||||
// client code (GPL-3.0): src/net/http.cpp (headers, ResKind), src/net/account.cpp,
|
||||
// src/net/session.cpp, src/hle/np_matching2.cpp.
|
||||
#pragma once
|
||||
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
|
||||
#include "http.hpp"
|
||||
#include "util.hpp"
|
||||
|
||||
namespace bb {
|
||||
|
||||
struct BridgeError : std::runtime_error {
|
||||
using std::runtime_error::runtime_error;
|
||||
};
|
||||
|
||||
class BBHostClient {
|
||||
public:
|
||||
explicit BBHostClient(std::string base) : base_(std::move(base)) {
|
||||
while (!base_.empty() && base_.back() == '/') base_.pop_back();
|
||||
}
|
||||
|
||||
std::string user_agent = "bbbridge";
|
||||
std::string ruleset = "vanilla";
|
||||
|
||||
std::string token() const { std::lock_guard<std::mutex> lk(mu_); return token_; }
|
||||
std::string online_id() const { std::lock_guard<std::mutex> lk(mu_); return online_id_; }
|
||||
void set_identity(const std::string& token, const std::string& online_id) {
|
||||
std::lock_guard<std::mutex> lk(mu_);
|
||||
token_ = token;
|
||||
online_id_ = online_id;
|
||||
}
|
||||
|
||||
// account; each returns the recovery code the server sent ("" if none)
|
||||
std::string account_create(const std::string& name);
|
||||
std::string account_recover(const std::string& name, const std::string& code);
|
||||
// true accepted, false refused (401), nullopt when it could not be asked
|
||||
std::optional<bool> token_valid(const std::string& online_id);
|
||||
|
||||
// matching
|
||||
json context_start(const std::string& online_id, const Endpoint& signaling, const Endpoint& mapped);
|
||||
json create_room(const std::string& online_id, const Endpoint& local, const Endpoint& mapped, int max_members,
|
||||
const json& extra);
|
||||
json join_room(const std::string& online_id, uint64_t room_id, const Endpoint& local, const Endpoint& mapped);
|
||||
void leave_room(const std::string& session_id, int member_id);
|
||||
int heartbeat(const std::string& session_id, int member_id); // InRoom, -1 when not asked
|
||||
void kick_member(const std::string& session_id, int member_id, int kicker_id);
|
||||
void signaling_update(const std::string& online_id, const Endpoint& mapped, const std::string& session_id,
|
||||
int member_id);
|
||||
std::optional<Endpoint> resolve_peer(const std::string& online_id);
|
||||
|
||||
// events: returns the next cursor; `event` is null when there was none
|
||||
uint64_t poll_events(const std::string& online_id, uint64_t cursor, json& event);
|
||||
void ack_event(const std::string& online_id, uint64_t cursor);
|
||||
|
||||
private:
|
||||
struct Reply {
|
||||
int status = 0;
|
||||
json body;
|
||||
};
|
||||
// throws on transport failure; pool: a connection of its own (else the shared one)
|
||||
Reply post(const std::string& path, const json& body, int timeout_ms = 8000, http::Pool* pool = nullptr);
|
||||
json call(const std::string& path, const json& body, int timeout_ms = 8000); // + ResKind == 0
|
||||
|
||||
http::Pool poll_pool_{45.0}; // the event poll, every 0.4 s: redialled before it is a minute old
|
||||
std::string base_;
|
||||
mutable std::mutex mu_;
|
||||
std::string token_;
|
||||
std::string online_id_;
|
||||
};
|
||||
|
||||
} // namespace bb
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1,195 +0,0 @@
|
||||
// bbbridge core: a local shadNet server for bbport that speaks The Hunter's
|
||||
// Dream's REST API (bbhost's protocol) behind it, plus the P2P gateway, the
|
||||
// WebAPI the game's summon calls go to, and the game-server proxy.
|
||||
#pragma once
|
||||
|
||||
#include <atomic>
|
||||
#include <condition_variable>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
#include "bbhost_client.hpp"
|
||||
#include "http.hpp"
|
||||
#include "p2p.hpp"
|
||||
#include "util.hpp"
|
||||
|
||||
namespace bb {
|
||||
|
||||
uint64_t account_id_for(const std::string& npid); // the PSN-style id bbport hands the game
|
||||
|
||||
struct Config {
|
||||
std::string bbhost_server = "https://thehuntersdream.com:18671";
|
||||
std::string auth_server = "https://thehuntersdream.com"; // Discord sign-in, website codes (bbhost's auth_server)
|
||||
std::string community_server = "https://thehuntersdream.com:18671";
|
||||
std::string bind_host = "127.0.0.1";
|
||||
int tcp_port = 31313, udp_port = 31314, webapi_port = 31315;
|
||||
bool p2p_gateway = true;
|
||||
int p2p_port = 0; // outer UDP port, 0 = any
|
||||
int port_p2p_port = 3658; // bbport's P2P UDP port (SHADPS4_P2P_PORT)
|
||||
std::string stun_server; // "host:port", "" = the bbhost host on 3478, "off"
|
||||
std::string local_addr; // LAN address to register, "" = detected
|
||||
double poll_interval = 0.4;
|
||||
std::string state_file = "bbbridge_state.json";
|
||||
bool trace = false;
|
||||
bool debug_endpoints = false; // /debug/* on the WebAPI (tests only)
|
||||
bool create_accounts = false; // make an account for an unknown NPID (tests); players sign in with Discord
|
||||
double stun_refresh = 15.0; // seconds between STUN refreshes (tests shorten it)
|
||||
std::map<int, std::string> game_proxy_routes = {{31316, "https://thehuntersdream.com:20443"},
|
||||
{31317, "https://thehuntersdream.com:18671"}};
|
||||
std::string user_agent = std::string("bbbridge/") + kVersion;
|
||||
|
||||
static Config from_json(const json& j);
|
||||
std::optional<std::pair<std::string, int>> stun_endpoint() const;
|
||||
};
|
||||
|
||||
class Bridge;
|
||||
|
||||
class TcpConn {
|
||||
public:
|
||||
explicit TcpConn(SOCKET s) : sock(s) {}
|
||||
bool send(const std::string& data);
|
||||
void close();
|
||||
SOCKET sock;
|
||||
std::atomic<bool> alive{true};
|
||||
|
||||
private:
|
||||
std::mutex send_mu_;
|
||||
};
|
||||
|
||||
struct RoomState {
|
||||
uint64_t room_id = 0;
|
||||
std::string session_id;
|
||||
int member_id = 0, owner_id = 0, max_slot = 5;
|
||||
std::map<int, json> members; // member_id -> {OnlineId, Addr, Port, LocalAddr, LocalPort, MappedAddr, MappedPort}
|
||||
void clear() { *this = RoomState{}; }
|
||||
};
|
||||
|
||||
class Session {
|
||||
public:
|
||||
Session(Bridge& bridge, std::shared_ptr<TcpConn> conn);
|
||||
~Session();
|
||||
std::pair<uint8_t, std::string> handle(uint16_t command, uint64_t packet_id, const std::string& payload);
|
||||
void shutdown();
|
||||
|
||||
bool logged_in() const { return logged_in_; }
|
||||
std::string npid() const;
|
||||
BBHostClient bb;
|
||||
std::atomic<bool> terminate_requested{false};
|
||||
std::shared_ptr<TcpConn> conn;
|
||||
|
||||
// for the bridge (mapped-address change, summon advertisement, app status)
|
||||
void on_mapped_change(const Endpoint& mapped);
|
||||
void set_advertisement(const json& adv);
|
||||
uint64_t room_id() const;
|
||||
int room_members() const;
|
||||
|
||||
private:
|
||||
using Reply = std::pair<uint8_t, std::string>;
|
||||
Reply on_login(const std::string& payload);
|
||||
Reply on_get_token();
|
||||
Reply on_context_start(const std::string& payload);
|
||||
Reply on_context_stop();
|
||||
Reply on_get_world_info_list();
|
||||
Reply on_create_room(const std::string& payload);
|
||||
Reply on_join_room(const std::string& payload);
|
||||
Reply on_leave_room(const std::string& payload);
|
||||
Reply on_request_signaling_infos(const std::string& payload);
|
||||
Reply on_room_id_echo(const std::string& payload); // SetRoomData{Internal,External}
|
||||
Reply on_kickout(const std::string& payload);
|
||||
|
||||
bool bbhost_account();
|
||||
std::pair<Endpoint, Endpoint> p2p_addresses();
|
||||
json host_extra_from_advertisement() const;
|
||||
static Endpoint member_endpoint(const json& member);
|
||||
Endpoint port_endpoint_for(const json& member);
|
||||
std::optional<Endpoint> port_p2p_endpoint() const;
|
||||
void forget_peers(std::optional<int> member_id = std::nullopt);
|
||||
void ensure_self_member();
|
||||
void absorb_members(const json& members);
|
||||
std::string build_details(int me_id, int owner_id);
|
||||
std::string member_data(const json& member, int owner_id);
|
||||
void leave_room_quietly();
|
||||
void send_room_event(uint32_t event, uint32_t cause, const json& member, int64_t error_code = 0);
|
||||
void start_pump();
|
||||
void pump_main();
|
||||
void handle_event(const json& event);
|
||||
|
||||
Bridge& bridge_;
|
||||
mutable std::recursive_mutex mu_; // the session's state; the TCP and pump threads share it
|
||||
std::string npid_, title_id_;
|
||||
uint64_t ctx_id_ = 0;
|
||||
std::atomic<bool> logged_in_{false};
|
||||
uint64_t account_id_ = 0;
|
||||
RoomState room_;
|
||||
json advertisement_;
|
||||
std::vector<json> pending_guest_invites_;
|
||||
std::thread pump_;
|
||||
std::atomic<bool> pump_running_{false};
|
||||
std::mutex join_mu_; // shutdown() can come from the TCP thread and Bridge::stop at once
|
||||
std::mutex pump_mu_;
|
||||
std::condition_variable pump_cv_;
|
||||
bool pump_stop_ = false;
|
||||
};
|
||||
|
||||
struct BridgeStatus {
|
||||
bool running = false;
|
||||
std::string npid;
|
||||
bool logged_in = false;
|
||||
uint64_t room_id = 0;
|
||||
int room_members = 0;
|
||||
int relay_port = 0;
|
||||
std::optional<Endpoint> mapped;
|
||||
std::vector<p2p::PeerInfo> peers;
|
||||
};
|
||||
|
||||
class Bridge {
|
||||
public:
|
||||
explicit Bridge(Config cfg) : cfg(std::move(cfg)) {}
|
||||
~Bridge() { stop(); }
|
||||
bool start(std::string* error);
|
||||
void stop();
|
||||
BridgeStatus status() const;
|
||||
|
||||
const Config cfg;
|
||||
std::unique_ptr<p2p::Gateway> gateway;
|
||||
|
||||
// shared with sessions
|
||||
json load_state();
|
||||
void save_state(const json& state);
|
||||
std::optional<Endpoint> registry_get(const std::string& npid) const;
|
||||
std::pair<std::string, std::string> identity() const; // (token, ruleset) of the logged-in player
|
||||
json last_advertisement() const;
|
||||
|
||||
private:
|
||||
void tcp_accept_loop();
|
||||
void tcp_serve(std::shared_ptr<TcpConn> conn);
|
||||
void stun_loop();
|
||||
http::ServerResponse webapi(const http::ServerRequest& req);
|
||||
http::ServerResponse summon(const http::ServerRequest& req);
|
||||
http::ServerResponse game_proxy(const std::string& upstream, const http::ServerRequest& req);
|
||||
void capture(const std::string& name, const std::string& request, int status, const std::string& response,
|
||||
const std::string& note);
|
||||
void on_mapped_change(const Endpoint& mapped);
|
||||
|
||||
SOCKET tcp_ = INVALID_SOCKET, stun_ = INVALID_SOCKET;
|
||||
std::thread tcp_thread_, stun_thread_;
|
||||
std::atomic<bool> running_{false};
|
||||
std::atomic<int> tcp_active_{0};
|
||||
http::Server webapi_server_;
|
||||
std::vector<std::unique_ptr<http::Server>> proxies_;
|
||||
|
||||
mutable std::mutex sessions_mu_;
|
||||
std::vector<std::shared_ptr<Session>> sessions_;
|
||||
mutable std::mutex registry_mu_;
|
||||
std::map<std::string, Endpoint> registry_; // npid -> the port's endpoint, from its STUN pings
|
||||
json last_advertisement_;
|
||||
std::mutex state_mu_;
|
||||
};
|
||||
|
||||
} // namespace bb
|
||||
@@ -1,437 +0,0 @@
|
||||
#include "http.hpp"
|
||||
|
||||
#ifdef _WIN32
|
||||
#include <winhttp.h>
|
||||
#else
|
||||
#include <curl/curl.h>
|
||||
#endif
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <sstream>
|
||||
|
||||
#ifdef _WIN32
|
||||
#pragma comment(lib, "winhttp.lib")
|
||||
#pragma comment(lib, "ws2_32.lib")
|
||||
#endif
|
||||
|
||||
namespace http {
|
||||
|
||||
namespace {
|
||||
|
||||
bool iequals(const std::string& a, const std::string& b) {
|
||||
return a.size() == b.size() &&
|
||||
std::equal(a.begin(), a.end(), b.begin(), [](char x, char y) {
|
||||
return std::tolower(static_cast<unsigned char>(x)) == std::tolower(static_cast<unsigned char>(y));
|
||||
});
|
||||
}
|
||||
|
||||
std::string trim(const std::string& s) {
|
||||
size_t a = 0, b = s.size();
|
||||
while (a < b && (s[a] == ' ' || s[a] == '\t')) ++a;
|
||||
while (b > a && (s[b - 1] == ' ' || s[b - 1] == '\t' || s[b - 1] == '\r')) --b;
|
||||
return s.substr(a, b - a);
|
||||
}
|
||||
|
||||
#ifdef _WIN32
|
||||
HINTERNET open_session() {
|
||||
return WinHttpOpen(L"bbbridge", WINHTTP_ACCESS_TYPE_AUTOMATIC_PROXY, WINHTTP_NO_PROXY_NAME,
|
||||
WINHTTP_NO_PROXY_BYPASS, 0);
|
||||
}
|
||||
|
||||
HINTERNET session() {
|
||||
static HINTERNET h = open_session();
|
||||
return h;
|
||||
}
|
||||
|
||||
ClientResponse send_on(HINTERNET session, const ClientRequest& req);
|
||||
|
||||
struct Handle {
|
||||
HINTERNET h = nullptr;
|
||||
~Handle() { if (h) WinHttpCloseHandle(h); }
|
||||
};
|
||||
#endif
|
||||
|
||||
const char* reason(int status) {
|
||||
switch (status) {
|
||||
case 200: return "OK";
|
||||
case 201: return "Created";
|
||||
case 204: return "No Content";
|
||||
case 400: return "Bad Request";
|
||||
case 401: return "Unauthorized";
|
||||
case 404: return "Not Found";
|
||||
case 500: return "Internal Server Error";
|
||||
case 502: return "Bad Gateway";
|
||||
default: return "Status";
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
std::string header_value(const Headers& headers, const std::string& name) {
|
||||
for (const auto& [k, v] : headers)
|
||||
if (iequals(k, name)) return v;
|
||||
return "";
|
||||
}
|
||||
|
||||
#ifdef _WIN32
|
||||
ClientResponse send(const ClientRequest& req) { return send_on(session(), req); }
|
||||
|
||||
Pool::~Pool() {
|
||||
if (session_) WinHttpCloseHandle(session_);
|
||||
}
|
||||
|
||||
ClientResponse Pool::send(const ClientRequest& req) {
|
||||
std::lock_guard<std::mutex> lk(mu_);
|
||||
if (session_ && bb::now() - opened_ >= max_age_) { // closing the session closes its connection
|
||||
WinHttpCloseHandle(session_);
|
||||
session_ = nullptr;
|
||||
}
|
||||
if (!session_) {
|
||||
session_ = open_session();
|
||||
opened_ = bb::now();
|
||||
}
|
||||
return send_on(session_, req);
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
ClientResponse send_on(HINTERNET session, const ClientRequest& req) {
|
||||
ClientResponse res;
|
||||
if (!session) {
|
||||
res.error = "WinHttpOpen failed";
|
||||
return res;
|
||||
}
|
||||
const std::wstring wurl = bb::widen(req.url);
|
||||
URL_COMPONENTS uc{};
|
||||
uc.dwStructSize = sizeof(uc);
|
||||
wchar_t host[256] = {}, path[4096] = {};
|
||||
uc.lpszHostName = host;
|
||||
uc.dwHostNameLength = 256;
|
||||
uc.lpszUrlPath = path;
|
||||
uc.dwUrlPathLength = 4096;
|
||||
wchar_t extra[4096] = {};
|
||||
uc.lpszExtraInfo = extra;
|
||||
uc.dwExtraInfoLength = 4096;
|
||||
if (!WinHttpCrackUrl(wurl.c_str(), 0, 0, &uc)) {
|
||||
res.error = "bad url " + req.url;
|
||||
return res;
|
||||
}
|
||||
const bool secure = uc.nScheme == INTERNET_SCHEME_HTTPS;
|
||||
Handle conn{WinHttpConnect(session, host, uc.nPort, 0)};
|
||||
if (!conn.h) {
|
||||
res.error = bb::fmt("connect failed (%lu)", GetLastError());
|
||||
return res;
|
||||
}
|
||||
const std::wstring object = std::wstring(path) + extra;
|
||||
Handle request{WinHttpOpenRequest(conn.h, bb::widen(req.method).c_str(), object.c_str(), nullptr,
|
||||
WINHTTP_NO_REFERER, WINHTTP_DEFAULT_ACCEPT_TYPES,
|
||||
secure ? WINHTTP_FLAG_SECURE : 0)};
|
||||
if (!request.h) {
|
||||
res.error = bb::fmt("open request failed (%lu)", GetLastError());
|
||||
return res;
|
||||
}
|
||||
DWORD policy = WINHTTP_OPTION_REDIRECT_POLICY_NEVER;
|
||||
WinHttpSetOption(request.h, WINHTTP_OPTION_REDIRECT_POLICY, &policy, sizeof(policy));
|
||||
WinHttpSetTimeouts(request.h, req.timeout_ms, req.timeout_ms, req.timeout_ms, req.timeout_ms);
|
||||
std::wstring headers;
|
||||
for (const auto& [k, v] : req.headers) headers += bb::widen(k + ": " + v + "\r\n");
|
||||
if (!headers.empty())
|
||||
WinHttpAddRequestHeaders(request.h, headers.c_str(), static_cast<DWORD>(-1L),
|
||||
WINHTTP_ADDREQ_FLAG_ADD | WINHTTP_ADDREQ_FLAG_REPLACE);
|
||||
const DWORD len = static_cast<DWORD>(req.body.size());
|
||||
if (!WinHttpSendRequest(request.h, WINHTTP_NO_ADDITIONAL_HEADERS, 0,
|
||||
len ? const_cast<char*>(req.body.data()) : WINHTTP_NO_REQUEST_DATA, len, len, 0) ||
|
||||
!WinHttpReceiveResponse(request.h, nullptr)) {
|
||||
const DWORD e = GetLastError();
|
||||
res.error = e == ERROR_WINHTTP_TIMEOUT ? "The read operation timed out" : bb::fmt("request failed (%lu)", e);
|
||||
return res;
|
||||
}
|
||||
DWORD status = 0, size = sizeof(status);
|
||||
WinHttpQueryHeaders(request.h, WINHTTP_QUERY_STATUS_CODE | WINHTTP_QUERY_FLAG_NUMBER,
|
||||
WINHTTP_HEADER_NAME_BY_INDEX, &status, &size, WINHTTP_NO_HEADER_INDEX);
|
||||
res.status = static_cast<int>(status);
|
||||
size = 0;
|
||||
WinHttpQueryHeaders(request.h, WINHTTP_QUERY_RAW_HEADERS_CRLF, WINHTTP_HEADER_NAME_BY_INDEX, nullptr,
|
||||
&size, WINHTTP_NO_HEADER_INDEX);
|
||||
if (size) {
|
||||
std::wstring raw(size / sizeof(wchar_t), L'\0');
|
||||
if (WinHttpQueryHeaders(request.h, WINHTTP_QUERY_RAW_HEADERS_CRLF, WINHTTP_HEADER_NAME_BY_INDEX,
|
||||
raw.data(), &size, WINHTTP_NO_HEADER_INDEX)) {
|
||||
std::istringstream lines(bb::narrow(raw));
|
||||
std::string line;
|
||||
bool first = true;
|
||||
while (std::getline(lines, line)) {
|
||||
if (first) { first = false; continue; } // status line
|
||||
const size_t c = line.find(':');
|
||||
if (c != std::string::npos) res.headers.emplace_back(trim(line.substr(0, c)), trim(line.substr(c + 1)));
|
||||
}
|
||||
}
|
||||
}
|
||||
while (true) {
|
||||
DWORD avail = 0;
|
||||
if (!WinHttpQueryDataAvailable(request.h, &avail)) {
|
||||
res.error = bb::fmt("read failed (%lu)", GetLastError());
|
||||
return res;
|
||||
}
|
||||
if (!avail) break;
|
||||
std::string chunk(avail, '\0');
|
||||
DWORD read = 0;
|
||||
if (!WinHttpReadData(request.h, chunk.data(), avail, &read)) {
|
||||
res.error = bb::fmt("read failed (%lu)", GetLastError());
|
||||
return res;
|
||||
}
|
||||
res.body.append(chunk.data(), read);
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
#else
|
||||
// Linux: libcurl, as WinHTTP above: certificates checked, no redirects followed, the request's
|
||||
// timeout for connecting and again for the answer, and a Pool keeping one connection alive.
|
||||
namespace {
|
||||
|
||||
// The system's CA bundle: libcurl's built-in path is the one of the distribution it was built
|
||||
// on, and the AppImage carries it to others.
|
||||
const char* ca_bundle() {
|
||||
static const char* found = [] {
|
||||
for (const char* p : {"/etc/ssl/certs/ca-certificates.crt", "/etc/pki/tls/certs/ca-bundle.crt",
|
||||
"/etc/ssl/ca-bundle.pem", "/etc/pki/tls/cacert.pem",
|
||||
"/etc/ssl/cert.pem"})
|
||||
if (access(p, R_OK) == 0) return p;
|
||||
return static_cast<const char*>(nullptr);
|
||||
}();
|
||||
return found;
|
||||
}
|
||||
|
||||
size_t on_body(char* data, size_t size, size_t count, void* user) {
|
||||
static_cast<std::string*>(user)->append(data, size * count);
|
||||
return size * count;
|
||||
}
|
||||
|
||||
size_t on_header(char* data, size_t size, size_t count, void* user) {
|
||||
auto* headers = static_cast<Headers*>(user);
|
||||
std::string line(data, size * count);
|
||||
while (!line.empty() && (line.back() == '\n' || line.back() == '\r')) line.pop_back(); // trim() keeps '\n'
|
||||
line = trim(line);
|
||||
if (line.rfind("HTTP/", 0) == 0) headers->clear(); // a new response (after a 100 Continue)
|
||||
else if (const size_t c = line.find(':'); c != std::string::npos)
|
||||
headers->emplace_back(trim(line.substr(0, c)), trim(line.substr(c + 1)));
|
||||
return size * count;
|
||||
}
|
||||
|
||||
ClientResponse send_on(CURL* curl, const ClientRequest& req) {
|
||||
ClientResponse res;
|
||||
if (!curl) {
|
||||
res.error = "curl_easy_init failed";
|
||||
return res;
|
||||
}
|
||||
curl_easy_reset(curl);
|
||||
curl_easy_setopt(curl, CURLOPT_URL, req.url.c_str());
|
||||
curl_easy_setopt(curl, CURLOPT_USERAGENT, "bbbridge");
|
||||
curl_easy_setopt(curl, CURLOPT_NOSIGNAL, 1L);
|
||||
curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 0L);
|
||||
if (const char* ca = ca_bundle()) curl_easy_setopt(curl, CURLOPT_CAINFO, ca);
|
||||
curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT_MS, static_cast<long>(req.timeout_ms));
|
||||
curl_easy_setopt(curl, CURLOPT_TIMEOUT_MS, static_cast<long>(req.timeout_ms) * 2);
|
||||
if (req.method == "GET") {
|
||||
curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L);
|
||||
} else if (req.method == "HEAD") {
|
||||
curl_easy_setopt(curl, CURLOPT_NOBODY, 1L);
|
||||
} else {
|
||||
curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, req.method.c_str());
|
||||
curl_easy_setopt(curl, CURLOPT_POSTFIELDS, req.body.data());
|
||||
curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, static_cast<long>(req.body.size()));
|
||||
}
|
||||
curl_slist* headers = curl_slist_append(nullptr, "Expect:"); // no 100-continue round trip
|
||||
for (const auto& [k, v] : req.headers) headers = curl_slist_append(headers, (k + ": " + v).c_str());
|
||||
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
|
||||
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, on_body);
|
||||
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &res.body);
|
||||
curl_easy_setopt(curl, CURLOPT_HEADERFUNCTION, on_header);
|
||||
curl_easy_setopt(curl, CURLOPT_HEADERDATA, &res.headers);
|
||||
const CURLcode code = curl_easy_perform(curl);
|
||||
curl_slist_free_all(headers);
|
||||
if (code != CURLE_OK) {
|
||||
res.error = code == CURLE_OPERATION_TIMEDOUT ? "The read operation timed out"
|
||||
: bb::fmt("request failed (%s)", curl_easy_strerror(code));
|
||||
res.body.clear();
|
||||
res.headers.clear();
|
||||
return res;
|
||||
}
|
||||
long status = 0;
|
||||
curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &status);
|
||||
res.status = static_cast<int>(status);
|
||||
return res;
|
||||
}
|
||||
|
||||
CURL* new_handle() {
|
||||
static const bool initialised = curl_global_init(CURL_GLOBAL_DEFAULT) == CURLE_OK;
|
||||
return initialised ? curl_easy_init() : nullptr;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
ClientResponse send(const ClientRequest& req) {
|
||||
// One connection per request, as WinHTTP's shared session gives no keep-alive guarantee.
|
||||
CURL* curl = new_handle();
|
||||
ClientResponse res = send_on(curl, req);
|
||||
if (curl) curl_easy_cleanup(curl);
|
||||
return res;
|
||||
}
|
||||
|
||||
Pool::~Pool() {
|
||||
if (session_) curl_easy_cleanup(static_cast<CURL*>(session_));
|
||||
}
|
||||
|
||||
ClientResponse Pool::send(const ClientRequest& req) {
|
||||
std::lock_guard<std::mutex> lk(mu_);
|
||||
if (session_ && bb::now() - opened_ >= max_age_) { // a new handle dials a new connection
|
||||
curl_easy_cleanup(static_cast<CURL*>(session_));
|
||||
session_ = nullptr;
|
||||
}
|
||||
if (!session_) {
|
||||
session_ = new_handle();
|
||||
opened_ = bb::now();
|
||||
}
|
||||
return send_on(static_cast<CURL*>(session_), req);
|
||||
}
|
||||
#endif
|
||||
|
||||
ServerResponse ServerResponse::json(const bb::json& j, int status) {
|
||||
ServerResponse r;
|
||||
r.status = status;
|
||||
r.body = j.dump();
|
||||
r.headers.emplace_back("Content-Type", "application/json");
|
||||
return r;
|
||||
}
|
||||
|
||||
// ---- server ----
|
||||
|
||||
bool Server::start(const std::string& host, int port, Handler handler, std::string* error) {
|
||||
bb::net_init();
|
||||
handler_ = std::move(handler);
|
||||
listen_ = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
|
||||
#ifdef _WIN32
|
||||
BOOL exclusive = TRUE;
|
||||
setsockopt(listen_, SOL_SOCKET, SO_EXCLUSIVEADDRUSE, reinterpret_cast<const char*>(&exclusive), sizeof(exclusive));
|
||||
#else
|
||||
// Linux keeps a port that just closed (TIME_WAIT) from being bound again without SO_REUSEADDR,
|
||||
// which Windows allows; it still never lets a second program listen on it.
|
||||
int reuse = 1;
|
||||
setsockopt(listen_, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse));
|
||||
#endif
|
||||
sockaddr_in addr = bb::to_sockaddr(bb::Endpoint{host, port});
|
||||
if (bind(listen_, reinterpret_cast<sockaddr*>(&addr), sizeof(addr)) != 0 || ::listen(listen_, 16) != 0) {
|
||||
if (error) *error = bb::fmt("cannot listen on %s:%d (error %d)", host.c_str(), port, WSAGetLastError());
|
||||
closesocket(listen_);
|
||||
listen_ = INVALID_SOCKET;
|
||||
return false;
|
||||
}
|
||||
socklen_t len = sizeof(addr);
|
||||
getsockname(listen_, reinterpret_cast<sockaddr*>(&addr), &len);
|
||||
port_ = ntohs(addr.sin_port);
|
||||
running_ = true;
|
||||
acceptor_ = std::thread([this] { accept_loop(); });
|
||||
return true;
|
||||
}
|
||||
|
||||
void Server::stop() {
|
||||
if (!running_.exchange(false)) return;
|
||||
closesocket(listen_);
|
||||
listen_ = INVALID_SOCKET;
|
||||
if (acceptor_.joinable()) acceptor_.join();
|
||||
{
|
||||
std::lock_guard<std::mutex> lk(mu_);
|
||||
for (SOCKET s : clients_) shutdown(s, SD_BOTH);
|
||||
}
|
||||
// Connection threads are detached (the game opens one per request); wait
|
||||
// for the ones still running, which a shutdown socket ends quickly.
|
||||
for (int i = 0; i < 300 && active_.load() > 0; ++i) Sleep(10);
|
||||
}
|
||||
|
||||
void Server::accept_loop() {
|
||||
while (running_) {
|
||||
SOCKET s = accept(listen_, nullptr, nullptr);
|
||||
if (s == INVALID_SOCKET) break;
|
||||
{
|
||||
std::lock_guard<std::mutex> lk(mu_);
|
||||
clients_.insert(s);
|
||||
}
|
||||
++active_;
|
||||
std::thread([this, s] {
|
||||
serve(s);
|
||||
--active_;
|
||||
}).detach();
|
||||
}
|
||||
}
|
||||
|
||||
void Server::serve(SOCKET s) {
|
||||
std::string buf;
|
||||
char chunk[8192];
|
||||
bool keep = true;
|
||||
while (keep && running_) {
|
||||
size_t end;
|
||||
while ((end = buf.find("\r\n\r\n")) == std::string::npos) {
|
||||
const int n = recv(s, chunk, sizeof(chunk), 0);
|
||||
if (n <= 0) { keep = false; break; }
|
||||
buf.append(chunk, n);
|
||||
if (buf.size() > (1 << 20)) { keep = false; break; }
|
||||
}
|
||||
if (!keep) break;
|
||||
ServerRequest req;
|
||||
std::istringstream head(buf.substr(0, end));
|
||||
std::string line, version;
|
||||
std::getline(head, line);
|
||||
{
|
||||
std::istringstream rl(line);
|
||||
rl >> req.method >> req.target >> version;
|
||||
}
|
||||
while (std::getline(head, line)) {
|
||||
const size_t c = line.find(':');
|
||||
if (c != std::string::npos) req.headers.emplace_back(trim(line.substr(0, c)), trim(line.substr(c + 1)));
|
||||
}
|
||||
buf.erase(0, end + 4);
|
||||
size_t want = 0;
|
||||
const std::string cl = header_value(req.headers, "Content-Length");
|
||||
if (!cl.empty()) {
|
||||
try { want = static_cast<size_t>(std::stoull(cl)); } catch (...) { want = 0; }
|
||||
}
|
||||
while (buf.size() < want) {
|
||||
const int n = recv(s, chunk, sizeof(chunk), 0);
|
||||
if (n <= 0) { keep = false; break; }
|
||||
buf.append(chunk, n);
|
||||
}
|
||||
if (!keep) break;
|
||||
req.body = buf.substr(0, want);
|
||||
buf.erase(0, want);
|
||||
if (iequals(header_value(req.headers, "Connection"), "close") || version == "HTTP/1.0") keep = false;
|
||||
|
||||
ServerResponse res;
|
||||
try {
|
||||
res = handler_(req);
|
||||
} catch (const std::exception& e) {
|
||||
res = ServerResponse::json({{"ResKind", 1}, {"Message", std::string("bridge error: ") + e.what()}}, 500);
|
||||
}
|
||||
std::string out = bb::fmt("HTTP/1.1 %d %s\r\n", res.status, reason(res.status));
|
||||
for (const auto& [k, v] : res.headers)
|
||||
if (!iequals(k, "Content-Length") && !iequals(k, "Connection")) out += k + ": " + v + "\r\n";
|
||||
out += "Content-Length: " + std::to_string(res.body.size()) + "\r\n";
|
||||
out += keep ? "Connection: keep-alive\r\n\r\n" : "Connection: close\r\n\r\n";
|
||||
if (req.method != "HEAD") out += res.body;
|
||||
size_t sent = 0;
|
||||
while (sent < out.size()) {
|
||||
const int n = ::send(s, out.data() + sent, static_cast<int>(out.size() - sent), 0);
|
||||
if (n <= 0) { keep = false; break; }
|
||||
sent += static_cast<size_t>(n);
|
||||
}
|
||||
}
|
||||
{
|
||||
std::lock_guard<std::mutex> lk(mu_);
|
||||
clients_.erase(s);
|
||||
}
|
||||
closesocket(s);
|
||||
}
|
||||
|
||||
} // namespace http
|
||||
@@ -1,101 +0,0 @@
|
||||
// HTTP: a WinHTTP client (certificates checked by Windows, no redirects
|
||||
// followed) and a small threaded HTTP/1.1 server for the bridge's local
|
||||
// listeners (WebAPI, game-server proxy).
|
||||
#pragma once
|
||||
|
||||
#include <atomic>
|
||||
#include <functional>
|
||||
#include <mutex>
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "util.hpp"
|
||||
|
||||
namespace http {
|
||||
|
||||
using Headers = std::vector<std::pair<std::string, std::string>>;
|
||||
|
||||
std::string header_value(const Headers& headers, const std::string& name); // case-insensitive, "" if absent
|
||||
|
||||
struct ClientRequest {
|
||||
std::string method = "POST";
|
||||
std::string url;
|
||||
Headers headers;
|
||||
std::string body;
|
||||
int timeout_ms = 8000;
|
||||
};
|
||||
|
||||
struct ClientResponse {
|
||||
int status = 0;
|
||||
Headers headers;
|
||||
std::string body;
|
||||
std::string error; // transport failure (no HTTP answer)
|
||||
bool ok() const { return error.empty(); }
|
||||
};
|
||||
|
||||
ClientResponse send(const ClientRequest& req);
|
||||
|
||||
// A WinHTTP session of its own for one caller's requests in a row: its kept-
|
||||
// alive connection is closed and dialled again once it is max_age_s old, before
|
||||
// something on the way drops it silently. (The Hunter's Dream's event poll, on
|
||||
// the shared pool, hung for its whole timeout about once a minute.)
|
||||
class Pool {
|
||||
public:
|
||||
explicit Pool(double max_age_s) : max_age_(max_age_s) {}
|
||||
~Pool();
|
||||
Pool(const Pool&) = delete;
|
||||
Pool& operator=(const Pool&) = delete;
|
||||
ClientResponse send(const ClientRequest& req);
|
||||
|
||||
private:
|
||||
std::mutex mu_;
|
||||
void* session_ = nullptr; // HINTERNET
|
||||
double opened_ = 0, max_age_;
|
||||
};
|
||||
|
||||
struct ServerRequest {
|
||||
std::string method;
|
||||
std::string target; // path with the query
|
||||
Headers headers;
|
||||
std::string body;
|
||||
std::string path() const { return target.substr(0, target.find('?')); }
|
||||
std::string query() const {
|
||||
const size_t q = target.find('?');
|
||||
return q == std::string::npos ? "" : target.substr(q + 1);
|
||||
}
|
||||
};
|
||||
|
||||
struct ServerResponse {
|
||||
int status = 200;
|
||||
Headers headers; // Content-Length is added
|
||||
std::string body;
|
||||
static ServerResponse json(const bb::json& j, int status = 200);
|
||||
};
|
||||
|
||||
using Handler = std::function<ServerResponse(const ServerRequest&)>;
|
||||
|
||||
class Server {
|
||||
public:
|
||||
~Server() { stop(); }
|
||||
bool start(const std::string& host, int port, Handler handler, std::string* error);
|
||||
void stop();
|
||||
int port() const { return port_; }
|
||||
|
||||
private:
|
||||
void accept_loop();
|
||||
void serve(SOCKET s);
|
||||
|
||||
SOCKET listen_ = INVALID_SOCKET;
|
||||
int port_ = 0;
|
||||
Handler handler_;
|
||||
std::thread acceptor_;
|
||||
std::atomic<bool> running_{false};
|
||||
std::mutex mu_;
|
||||
std::set<SOCKET> clients_;
|
||||
std::atomic<int> active_{0}; // detached connection threads still running
|
||||
};
|
||||
|
||||
} // namespace http
|
||||
@@ -1,152 +0,0 @@
|
||||
// bbbridge-cli: the bridge without a window (tests, servers, troubleshooting).
|
||||
// bbbridge-cli[.exe] [--config bbbridge_config.json] [--log bbbridge.log]
|
||||
#include <atomic>
|
||||
#include <cstdio>
|
||||
#include <fstream>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "account_link.hpp"
|
||||
#include "bridge.hpp"
|
||||
#ifdef _WIN32
|
||||
#include "setup.hpp"
|
||||
#else
|
||||
#include <csignal>
|
||||
#endif
|
||||
|
||||
namespace {
|
||||
std::atomic<bool> g_quit{false};
|
||||
#ifdef _WIN32
|
||||
BOOL WINAPI on_ctrl(DWORD) {
|
||||
g_quit = true;
|
||||
return TRUE;
|
||||
}
|
||||
#else
|
||||
void on_signal(int) { g_quit = true; }
|
||||
#endif
|
||||
|
||||
int run(const std::vector<std::wstring>& argv) {
|
||||
const int argc = static_cast<int>(argv.size());
|
||||
std::wstring config_path = L"bbbridge_config.json", log_path, web_code_npid;
|
||||
bool link = false;
|
||||
std::wstring route, bbport_dir;
|
||||
struct { std::wstring op, name, code; } account_op;
|
||||
for (int i = 1; i < argc; ++i) {
|
||||
const std::wstring a = argv[i];
|
||||
if (a == L"--config" && i + 1 < argc) config_path = argv[++i];
|
||||
else if (a == L"--log" && i + 1 < argc) log_path = argv[++i];
|
||||
else if (a == L"--link-discord") link = true;
|
||||
else if (a == L"--route" && i + 1 < argc) route = argv[++i];
|
||||
else if (a == L"--bbport" && i + 1 < argc) bbport_dir = argv[++i];
|
||||
else if (a == L"--web-code" && i + 1 < argc) web_code_npid = argv[++i];
|
||||
else if (a == L"--create" && i + 1 < argc) account_op = {L"create", argv[++i], L""};
|
||||
else if (a == L"--recover" && i + 2 < argc) {
|
||||
account_op = {L"recover", argv[i + 1], argv[i + 2]};
|
||||
i += 2;
|
||||
} else if (a == L"--sign-out" && i + 1 < argc) account_op = {L"sign-out", argv[++i], L""};
|
||||
else if (a == L"--version") {
|
||||
std::printf("bbbridge %s\n", bb::kVersion);
|
||||
return 0;
|
||||
} else {
|
||||
std::printf("usage: bbbridge-cli [--config file.json] [--log file.log] [--version]\n"
|
||||
" [--link-discord] [--web-code NPID] [--create NAME]\n"
|
||||
" [--recover NAME CODE] [--sign-out NAME]\n"
|
||||
" [--route on|off [--bbport DIR]]\n");
|
||||
return 2;
|
||||
}
|
||||
}
|
||||
if (!log_path.empty()) bb::set_log_file(log_path);
|
||||
bb::json j = bb::json::object();
|
||||
std::ifstream in(bb::fspath(config_path));
|
||||
if (in) {
|
||||
j = bb::json::parse(in, nullptr, false);
|
||||
if (j.is_discarded()) {
|
||||
std::printf("config %s is not valid JSON\n", bb::narrow(config_path).c_str());
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
const bb::Config cfg = bb::Config::from_json(j);
|
||||
if (link) { // sign in with Discord, store the account, exit
|
||||
bb::DiscordLink dl(cfg.auth_server, bb::widen(cfg.state_file));
|
||||
dl.start();
|
||||
bool shown = false;
|
||||
while (dl.state() == bb::DiscordLink::State::Starting || dl.state() == bb::DiscordLink::State::Waiting) {
|
||||
if (!shown && dl.state() == bb::DiscordLink::State::Waiting) {
|
||||
std::printf("Open %s and enter the code %s\n", dl.uri().c_str(), dl.user_code().c_str());
|
||||
std::fflush(stdout);
|
||||
shown = true;
|
||||
}
|
||||
Sleep(200);
|
||||
}
|
||||
if (dl.state() != bb::DiscordLink::State::Approved) {
|
||||
std::printf("failed: %s\n", dl.message().c_str());
|
||||
return 1;
|
||||
}
|
||||
std::printf("signed in as %s\n", dl.online_id().c_str());
|
||||
return 0;
|
||||
}
|
||||
if (!route.empty()) { // point bbport at the bridge (on) or back (off), as the app's checkbox
|
||||
#ifndef _WIN32
|
||||
std::printf("failed: --route is for bbport for Windows; on Linux the bbport launcher routes the game\n");
|
||||
return 1;
|
||||
#else
|
||||
try {
|
||||
wchar_t self[MAX_PATH];
|
||||
GetModuleFileNameW(nullptr, self, MAX_PATH);
|
||||
bb::BbportSetup setup(bb::fs::path(self).parent_path(), bbport_dir);
|
||||
if (route == L"on") setup.enable();
|
||||
else setup.disable();
|
||||
std::printf("ok: bbport %s, routed=%d\n", bb::narrow(setup.bbport.wstring()).c_str(), setup.routed() ? 1 : 0);
|
||||
return 0;
|
||||
} catch (const std::exception& e) {
|
||||
std::printf("failed: %s\n", e.what());
|
||||
return 1;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
if (!account_op.op.empty()) {
|
||||
const std::wstring state = bb::widen(cfg.state_file);
|
||||
const std::string name = bb::narrow(account_op.name);
|
||||
const std::string err =
|
||||
account_op.op == L"create" ? bb::account_create_now(cfg.auth_server, state, name)
|
||||
: account_op.op == L"recover" ? bb::account_recover_now(cfg.auth_server, state, name, bb::narrow(account_op.code))
|
||||
: bb::account_sign_out(cfg.auth_server, state, name);
|
||||
std::printf("%s\n", err.empty() ? "ok" : ("failed: " + err).c_str());
|
||||
return err.empty() ? 0 : 1;
|
||||
}
|
||||
if (!web_code_npid.empty()) {
|
||||
std::string error;
|
||||
const std::string code = bb::website_code(cfg.auth_server, bb::widen(cfg.state_file), bb::narrow(web_code_npid), error);
|
||||
if (code.empty()) {
|
||||
std::printf("failed: %s\n", error.c_str());
|
||||
return 1;
|
||||
}
|
||||
std::printf("%s\n", code.c_str());
|
||||
return 0;
|
||||
}
|
||||
bb::log(bb::fmt("bbbridge %s (cli)", bb::kVersion));
|
||||
bb::Bridge bridge(cfg);
|
||||
std::string error;
|
||||
if (!bridge.start(&error)) return 1;
|
||||
#ifdef _WIN32
|
||||
SetConsoleCtrlHandler(on_ctrl, TRUE);
|
||||
#else
|
||||
std::signal(SIGINT, on_signal);
|
||||
std::signal(SIGTERM, on_signal);
|
||||
#endif
|
||||
while (!g_quit) Sleep(200);
|
||||
bb::log("stopping");
|
||||
bridge.stop();
|
||||
return 0;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
#ifdef _WIN32
|
||||
int wmain(int argc, wchar_t** argv) { return run(std::vector<std::wstring>(argv, argv + argc)); }
|
||||
#else
|
||||
int main(int argc, char** argv) {
|
||||
std::vector<std::wstring> args;
|
||||
for (int i = 0; i < argc; ++i) args.push_back(bb::widen(argv[i]));
|
||||
return run(args);
|
||||
}
|
||||
#endif
|
||||
@@ -1,735 +0,0 @@
|
||||
#include "p2p.hpp"
|
||||
|
||||
#ifdef _WIN32
|
||||
#include <mstcpip.h>
|
||||
|
||||
#ifndef SIO_UDP_CONNRESET
|
||||
#define SIO_UDP_CONNRESET _WSAIOW(IOC_VENDOR, 12)
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <cstring>
|
||||
#include <random>
|
||||
|
||||
namespace bb::p2p {
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr uint16_t kPortSignalingVport = 0xFFFF;
|
||||
constexpr uint8_t kPs4Magic = 0xFF, kFlagP2p = 0x80, kFlagByteVports = 0x40, kFlagComid = 0x20, kFlagType = 0x03;
|
||||
constexpr uint8_t kRelayMagic = 0xFB;
|
||||
constexpr uint32_t kStunCookie = 0x2112A442;
|
||||
constexpr uint16_t kStunBindingRequest = 0x0001, kStunBindingResponse = 0x0101, kStunMapped = 0x0001,
|
||||
kStunXorMapped = 0x0020, kStunXorMappedOld = 0x8020, kBbhostHello = 0x8100,
|
||||
kBbhostRelay = 0x8101;
|
||||
const std::string kRelayVersion = "bbr1";
|
||||
const std::string kShad = "SHAD";
|
||||
|
||||
uint16_t be16(const std::string& s, size_t at) {
|
||||
return static_cast<uint16_t>((static_cast<uint8_t>(s[at]) << 8) | static_cast<uint8_t>(s[at + 1]));
|
||||
}
|
||||
uint32_t be32(const std::string& s, size_t at) {
|
||||
return (static_cast<uint32_t>(be16(s, at)) << 16) | be16(s, at + 2);
|
||||
}
|
||||
void put_be16(std::string& s, uint16_t v) {
|
||||
s.push_back(static_cast<char>(v >> 8));
|
||||
s.push_back(static_cast<char>(v & 0xFF));
|
||||
}
|
||||
template <typename T>
|
||||
void put_le(std::string& s, T v) {
|
||||
for (size_t i = 0; i < sizeof(T); ++i) s.push_back(static_cast<char>((static_cast<uint64_t>(v) >> (8 * i)) & 0xFF));
|
||||
}
|
||||
template <typename T>
|
||||
T get_le(const std::string& s, size_t at) {
|
||||
uint64_t v = 0;
|
||||
for (size_t i = 0; i < sizeof(T); ++i) v |= static_cast<uint64_t>(static_cast<uint8_t>(s[at + i])) << (8 * i);
|
||||
return static_cast<T>(v);
|
||||
}
|
||||
std::string id16(const std::string& name) {
|
||||
std::string out = name.substr(0, 16);
|
||||
out.resize(16, '\0');
|
||||
return out;
|
||||
}
|
||||
std::string ip_from_be32(uint32_t v) {
|
||||
return fmt("%u.%u.%u.%u", (v >> 24) & 0xFF, (v >> 16) & 0xFF, (v >> 8) & 0xFF, v & 0xFF);
|
||||
}
|
||||
uint64_t wall_micros() {
|
||||
using namespace std::chrono;
|
||||
return static_cast<uint64_t>(duration_cast<microseconds>(system_clock::now().time_since_epoch()).count());
|
||||
}
|
||||
void no_udp_connreset(SOCKET s) {
|
||||
#ifdef _WIN32
|
||||
// Without this, an ICMP "port unreachable" for an earlier send makes the
|
||||
// next recvfrom fail with WSAECONNRESET (10054).
|
||||
BOOL off = FALSE;
|
||||
DWORD ret = 0;
|
||||
WSAIoctl(s, SIO_UDP_CONNRESET, &off, sizeof(off), nullptr, 0, &ret, nullptr, nullptr);
|
||||
#else
|
||||
(void)s; // Linux reports ICMP errors only on connected UDP sockets; these are not
|
||||
#endif
|
||||
}
|
||||
SOCKET udp_bind(const std::string& ip, int port, Endpoint* bound) {
|
||||
SOCKET s = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
|
||||
if (s == INVALID_SOCKET) return s;
|
||||
sockaddr_in a = to_sockaddr(Endpoint{ip, port});
|
||||
if (bind(s, reinterpret_cast<sockaddr*>(&a), sizeof(a)) != 0) {
|
||||
closesocket(s);
|
||||
return INVALID_SOCKET;
|
||||
}
|
||||
no_udp_connreset(s);
|
||||
socklen_t len = sizeof(a);
|
||||
getsockname(s, reinterpret_cast<sockaddr*>(&a), &len);
|
||||
if (bound) *bound = from_sockaddr(a);
|
||||
return s;
|
||||
}
|
||||
void udp_send(SOCKET s, const std::string& data, const Endpoint& to) {
|
||||
sockaddr_in a = to_sockaddr(to);
|
||||
sendto(s, data.data(), static_cast<int>(data.size()), 0, reinterpret_cast<sockaddr*>(&a), sizeof(a));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
const std::string kProbe("\xfe" "bbhp\0\0\0", 8);
|
||||
|
||||
// ---- framing ----
|
||||
|
||||
Frame port_to_ps4(const std::string& frame, std::string& out) {
|
||||
if (frame.size() < 4) return Frame::None;
|
||||
const uint16_t src = be16(frame, 0), dst = be16(frame, 2);
|
||||
const std::string payload = frame.substr(4);
|
||||
if (dst == kPortSignalingVport) {
|
||||
out = payload;
|
||||
return Frame::Signaling;
|
||||
}
|
||||
out.clear();
|
||||
out.push_back(static_cast<char>(kPs4Magic));
|
||||
if (src < 256 && dst < 256) {
|
||||
out.push_back(static_cast<char>(kFlagP2p | kFlagByteVports | kFlagType));
|
||||
out.push_back(static_cast<char>(src));
|
||||
out.push_back(static_cast<char>(dst));
|
||||
} else {
|
||||
out.push_back(static_cast<char>(kFlagP2p | kFlagType));
|
||||
put_be16(out, src);
|
||||
put_be16(out, dst);
|
||||
}
|
||||
out += payload;
|
||||
return Frame::Game;
|
||||
}
|
||||
|
||||
bool ps4_to_port(const std::string& d, std::string& out) {
|
||||
if (d.size() < 4 || static_cast<uint8_t>(d[0]) != kPs4Magic) return false;
|
||||
const uint8_t flags = static_cast<uint8_t>(d[1]);
|
||||
size_t at = 2 + ((flags & kFlagComid) ? 4 : 0);
|
||||
uint16_t src, dst;
|
||||
if (flags & kFlagByteVports) {
|
||||
if (at + 2 > d.size()) return false;
|
||||
src = static_cast<uint8_t>(d[at]);
|
||||
dst = static_cast<uint8_t>(d[at + 1]);
|
||||
at += 2;
|
||||
} else {
|
||||
if (at + 4 > d.size()) return false;
|
||||
src = be16(d, at);
|
||||
dst = be16(d, at + 2);
|
||||
at += 4;
|
||||
}
|
||||
out.clear();
|
||||
put_be16(out, src);
|
||||
put_be16(out, dst);
|
||||
out += d.substr(at);
|
||||
return true;
|
||||
}
|
||||
|
||||
std::string port_signaling_frame(const std::string& payload) {
|
||||
std::string out;
|
||||
put_be16(out, kPortSignalingVport);
|
||||
put_be16(out, kPortSignalingVport);
|
||||
return out + payload;
|
||||
}
|
||||
|
||||
std::string relay_wrap(const std::string& token, int port, const std::string& datagram) {
|
||||
std::string out;
|
||||
out.push_back(static_cast<char>(kRelayMagic));
|
||||
out.push_back('R');
|
||||
out += token;
|
||||
put_be16(out, static_cast<uint16_t>(port));
|
||||
return out + datagram;
|
||||
}
|
||||
|
||||
bool relay_unwrap(const std::string& data, int& port, std::string& datagram) {
|
||||
if (data.size() < 4 || static_cast<uint8_t>(data[0]) != kRelayMagic || data[1] != 'r') return false;
|
||||
port = be16(data, 2);
|
||||
datagram = data.substr(4);
|
||||
return true;
|
||||
}
|
||||
|
||||
std::string stun_request(const std::string& token, std::string& txid) {
|
||||
static std::mt19937_64 rng{std::random_device{}()};
|
||||
txid.clear();
|
||||
txid.push_back(static_cast<char>(kStunCookie >> 24));
|
||||
txid.push_back(static_cast<char>((kStunCookie >> 16) & 0xFF));
|
||||
txid.push_back(static_cast<char>((kStunCookie >> 8) & 0xFF));
|
||||
txid.push_back(static_cast<char>(kStunCookie & 0xFF));
|
||||
for (int i = 0; i < 12; ++i) txid.push_back(static_cast<char>(rng() & 0xFF));
|
||||
std::string attr;
|
||||
put_be16(attr, kBbhostHello);
|
||||
put_be16(attr, 12);
|
||||
attr += kRelayVersion;
|
||||
attr += token.size() == 8 ? token : std::string(8, '\0');
|
||||
std::string out;
|
||||
put_be16(out, kStunBindingRequest);
|
||||
put_be16(out, static_cast<uint16_t>(attr.size()));
|
||||
return out + txid + attr;
|
||||
}
|
||||
|
||||
bool stun_parse(const std::string& d, const std::string& txid, StunAnswer& out) {
|
||||
if (d.size() < 20 || be16(d, 0) != kStunBindingResponse || d.compare(4, 16, txid) != 0) return false;
|
||||
const size_t length = be16(d, 2);
|
||||
if (20 + length > d.size()) return false;
|
||||
const uint32_t xor_addr = be32(d, 4);
|
||||
const uint16_t xor_port = be16(d, 4);
|
||||
std::optional<std::pair<uint32_t, uint16_t>> mapped, xored;
|
||||
out = StunAnswer{};
|
||||
size_t at = 20;
|
||||
const size_t end = 20 + length;
|
||||
while (at + 4 <= end) {
|
||||
const uint16_t type = be16(d, at), alen = be16(d, at + 2);
|
||||
if (at + 4 + alen > end) break;
|
||||
const std::string v = d.substr(at + 4, alen);
|
||||
if (type == kBbhostRelay && alen >= 20 && v.compare(0, 4, kRelayVersion) == 0) {
|
||||
out.relay = true;
|
||||
out.relay_token = v.substr(4, 8);
|
||||
out.relay_port = be16(v, 12);
|
||||
} else if (alen >= 8 && static_cast<uint8_t>(v[1]) == 0x01) {
|
||||
const uint16_t port = be16(v, 2);
|
||||
const uint32_t addr = be32(v, 4);
|
||||
if (type == kStunMapped) mapped = std::make_pair(addr, port);
|
||||
else if (type == kStunXorMapped || type == kStunXorMappedOld)
|
||||
xored = std::make_pair(addr ^ xor_addr, static_cast<uint16_t>(port ^ xor_port));
|
||||
}
|
||||
at += 4 + ((alen + 3u) & ~3u);
|
||||
}
|
||||
const auto chosen = mapped ? mapped : xored;
|
||||
if (!chosen || !chosen->first || !chosen->second) return false;
|
||||
out.mapped = Endpoint{ip_from_be32(chosen->first), chosen->second};
|
||||
return true;
|
||||
}
|
||||
|
||||
// ---- signaling ----
|
||||
|
||||
PeerSignaling::PeerSignaling(const std::string& peer_online_id, int peer_member, const std::string& port_online_id,
|
||||
int port_member, uint64_t room)
|
||||
: peer_id(id16(peer_online_id)),
|
||||
port_id(id16(port_online_id)),
|
||||
peer_member_id(peer_member > 0 ? peer_member : 0),
|
||||
port_member_id(port_member),
|
||||
room_id(room),
|
||||
conn_id(0x4242u + static_cast<uint32_t>(peer_member > 0 ? peer_member : 0)) {}
|
||||
|
||||
std::string PeerSignaling::handshake(uint8_t kind, uint32_t to_conn, uint64_t nonce) const {
|
||||
std::string p = kShad;
|
||||
p.push_back(static_cast<char>(T_HANDSHAKE));
|
||||
p.push_back(static_cast<char>(kind));
|
||||
p += peer_id + port_id;
|
||||
put_le<uint32_t>(p, conn_id);
|
||||
put_le<uint32_t>(p, to_conn);
|
||||
put_le<uint32_t>(p, 0); // mapped addr: 0 makes the port keep the inner socket's address
|
||||
put_le<uint16_t>(p, 0);
|
||||
put_le<uint16_t>(p, 0);
|
||||
put_le<uint64_t>(p, nonce);
|
||||
return p; // 62 bytes
|
||||
}
|
||||
|
||||
std::string PeerSignaling::control(uint8_t kind) const {
|
||||
std::string p = kShad;
|
||||
p.push_back(static_cast<char>(T_CONTROL));
|
||||
p.push_back(static_cast<char>(kind));
|
||||
p += peer_id + port_id;
|
||||
put_le<uint32_t>(p, conn_id);
|
||||
put_le<uint32_t>(p, 0);
|
||||
put_le<uint16_t>(p, 0);
|
||||
put_le<uint16_t>(p, 0);
|
||||
return p; // 50 bytes
|
||||
}
|
||||
|
||||
std::string PeerSignaling::m2(uint8_t kind, uint64_t nonce) const {
|
||||
std::string p = kShad;
|
||||
p.push_back(static_cast<char>(T_M2));
|
||||
p.push_back(static_cast<char>(kind));
|
||||
put_le<uint64_t>(p, room_id);
|
||||
put_le<uint16_t>(p, static_cast<uint16_t>(peer_member_id));
|
||||
put_le<uint16_t>(p, static_cast<uint16_t>(port_member_id));
|
||||
p += peer_id;
|
||||
put_le<uint32_t>(p, 0);
|
||||
put_le<uint16_t>(p, 0);
|
||||
put_le<uint16_t>(p, 0);
|
||||
put_le<uint64_t>(p, nonce);
|
||||
return p; // 50 bytes
|
||||
}
|
||||
|
||||
std::vector<std::string> PeerSignaling::handle(const std::string& p) {
|
||||
if (p.size() < 5 || p.compare(0, 4, kShad) != 0) return {};
|
||||
const uint8_t type = static_cast<uint8_t>(p[4]);
|
||||
if (type == T_ECHO_PING && p.size() == 17) {
|
||||
heard = true;
|
||||
std::string pong = kShad;
|
||||
pong.push_back(static_cast<char>(T_ECHO_PONG));
|
||||
pong += p.substr(5, 12); // conn u32 + timestamp u64, echoed
|
||||
return {pong};
|
||||
}
|
||||
if (type == T_HANDSHAKE && p.size() == 0x3E) {
|
||||
heard = true;
|
||||
const uint8_t kind = static_cast<uint8_t>(p[5]);
|
||||
const uint32_t from_conn = get_le<uint32_t>(p, 38);
|
||||
const uint64_t nonce = get_le<uint64_t>(p, 54);
|
||||
if (kind == HS_OFFER) return {handshake(HS_ACCEPT, from_conn, 0)};
|
||||
if (kind == HS_ACCEPT) return {handshake(HS_CHECK, from_conn, wall_micros())};
|
||||
if (kind == HS_CHECK) return {handshake(HS_CHECK_ACK, from_conn, nonce)};
|
||||
return {};
|
||||
}
|
||||
if (type == T_CONTROL && p.size() == 0x32) {
|
||||
heard = true;
|
||||
const uint8_t kind = static_cast<uint8_t>(p[5]);
|
||||
if (kind == CTL_ACTIVATION_REQUEST) return {control(CTL_ACTIVATION_ACK)};
|
||||
if (kind == CTL_ESTABLISHED && !sent_established) {
|
||||
sent_established = true;
|
||||
return {control(CTL_ESTABLISHED)};
|
||||
}
|
||||
return {};
|
||||
}
|
||||
if (type == T_M2 && p.size() == 0x32) {
|
||||
heard = true;
|
||||
const uint8_t kind = static_cast<uint8_t>(p[5]);
|
||||
const uint64_t room = get_le<uint64_t>(p, 6);
|
||||
const uint16_t from_member = get_le<uint16_t>(p, 14), to_member = get_le<uint16_t>(p, 16);
|
||||
const uint64_t nonce = get_le<uint64_t>(p, 42);
|
||||
if (room) room_id = room;
|
||||
// the port addresses the member it means: learn the ids from it
|
||||
if (to_member && !peer_member_id) peer_member_id = to_member;
|
||||
if (from_member && !port_member_id) port_member_id = from_member;
|
||||
if (kind == HS_OFFER) return {m2(HS_ACCEPT, 0)};
|
||||
if (kind == HS_ACCEPT) return {m2(HS_CHECK, 0)};
|
||||
if (kind == HS_CHECK) return {m2(HS_CHECK_ACK, nonce)};
|
||||
return {};
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
std::vector<std::string> PeerSignaling::opening() const {
|
||||
std::vector<std::string> out{control(CTL_ACTIVATION_REQUEST)};
|
||||
if (room_id && port_member_id && peer_member_id) out.push_back(m2(HS_OFFER, 0));
|
||||
return out;
|
||||
}
|
||||
|
||||
// ---- gateway ----
|
||||
|
||||
Gateway::Gateway(int outer_port, std::optional<std::pair<std::string, int>> stun_server, std::string lan_addr,
|
||||
std::function<void(const Endpoint&)> on_mapped_change)
|
||||
: outer_port_(outer_port),
|
||||
stun_server_(std::move(stun_server)),
|
||||
lan_addr_(std::move(lan_addr)),
|
||||
on_mapped_change_(std::move(on_mapped_change)) {}
|
||||
|
||||
Gateway::~Gateway() { stop(); }
|
||||
|
||||
bool Gateway::start(std::string* error) {
|
||||
net_init();
|
||||
Endpoint bound;
|
||||
outer_ = udp_bind("0.0.0.0", outer_port_, &bound);
|
||||
if (outer_ == INVALID_SOCKET) {
|
||||
if (error) *error = fmt("p2p: cannot bind udp :%d (error %d)", outer_port_, WSAGetLastError());
|
||||
return false;
|
||||
}
|
||||
outer_port_ = bound.port;
|
||||
stop_ = false;
|
||||
threads_.emplace_back([this] { outer_main(); });
|
||||
threads_.emplace_back([this] { inner_main(); });
|
||||
threads_.emplace_back([this] { tick_main(); });
|
||||
log(fmt("p2p: gateway on udp :%d (bbhost framing)", outer_port_));
|
||||
return true;
|
||||
}
|
||||
|
||||
void Gateway::stop() {
|
||||
if (stop_.exchange(true)) return;
|
||||
if (outer_ != INVALID_SOCKET) closesocket(outer_);
|
||||
outer_ = INVALID_SOCKET;
|
||||
for (auto& t : threads_)
|
||||
if (t.joinable()) t.join();
|
||||
threads_.clear();
|
||||
std::lock_guard<std::mutex> lk(mu_);
|
||||
for (auto& [id, link] : peers_) close_link(*link);
|
||||
peers_.clear();
|
||||
}
|
||||
|
||||
void Gateway::close_link(Link& link) {
|
||||
if (link.inner != INVALID_SOCKET) closesocket(link.inner);
|
||||
link.inner = INVALID_SOCKET;
|
||||
}
|
||||
|
||||
std::pair<Endpoint, Endpoint> Gateway::endpoints() const {
|
||||
std::lock_guard<std::mutex> lk(mu_);
|
||||
const Endpoint local{lan_addr_, outer_port_};
|
||||
return {local, mapped_ ? *mapped_ : local};
|
||||
}
|
||||
|
||||
std::optional<Endpoint> Gateway::mapped() const {
|
||||
std::lock_guard<std::mutex> lk(mu_);
|
||||
return mapped_;
|
||||
}
|
||||
|
||||
int Gateway::relay_port() const {
|
||||
std::lock_guard<std::mutex> lk(mu_);
|
||||
return relay_ ? relay_->port : 0;
|
||||
}
|
||||
|
||||
Endpoint Gateway::add_peer(int member_id, const std::string& online_id, const Endpoint& endpoint,
|
||||
const Endpoint& local_endpoint, const std::string& port_online_id, int port_member_id,
|
||||
uint64_t room_id, const std::optional<Endpoint>& port_endpoint) {
|
||||
LinkPtr link;
|
||||
{
|
||||
std::lock_guard<std::mutex> lk(mu_);
|
||||
// A peer the port asked for before it joined (a host signaling the
|
||||
// guest it summons) sits under a provisional key: keep its inner socket
|
||||
// - the port knows it there - and file it under the real id.
|
||||
for (auto it = peers_.begin(); it != peers_.end(); ++it) {
|
||||
if (!online_id.empty() && it->second->online_id == online_id && it->first != member_id) {
|
||||
LinkPtr other = it->second;
|
||||
peers_.erase(it);
|
||||
auto old = peers_.find(member_id);
|
||||
if (old != peers_.end()) {
|
||||
if (old->second != other) close_link(*old->second);
|
||||
peers_.erase(old);
|
||||
}
|
||||
other->member_id = member_id;
|
||||
other->signaling.peer_member_id = member_id > 0 ? member_id : 0;
|
||||
peers_[member_id] = other;
|
||||
break;
|
||||
}
|
||||
}
|
||||
auto it = peers_.find(member_id);
|
||||
if (it != peers_.end() && it->second->online_id == online_id) {
|
||||
Link& l = *it->second;
|
||||
if (endpoint.valid()) l.endpoint = endpoint;
|
||||
if (local_endpoint.valid()) l.local_endpoint = local_endpoint;
|
||||
if (port_endpoint && !l.port_endpoint) l.port_endpoint = port_endpoint;
|
||||
// a guest invite seeds the host before the port has joined: the room
|
||||
// and the port's member id arrive with the join
|
||||
if (port_member_id) l.signaling.port_member_id = port_member_id;
|
||||
if (room_id) l.signaling.room_id = room_id;
|
||||
return l.inner_endpoint;
|
||||
}
|
||||
if (it != peers_.end()) {
|
||||
close_link(*it->second);
|
||||
peers_.erase(it);
|
||||
}
|
||||
link = std::make_shared<Link>(PeerSignaling(online_id, member_id, port_online_id, port_member_id, room_id));
|
||||
link->member_id = member_id;
|
||||
link->online_id = online_id;
|
||||
link->endpoint = endpoint;
|
||||
link->local_endpoint = local_endpoint;
|
||||
link->port_endpoint = port_endpoint;
|
||||
link->created = now();
|
||||
link->inner = udp_bind("127.0.0.1", 0, &link->inner_endpoint);
|
||||
peers_[member_id] = link;
|
||||
}
|
||||
log(fmt("p2p: member %d (%s) at %s -> port sees 127.0.0.1:%d", member_id, online_id.c_str(),
|
||||
endpoint.valid() ? endpoint.str().c_str() : "?:0", link->inner_endpoint.port));
|
||||
return link->inner_endpoint;
|
||||
}
|
||||
|
||||
void Gateway::remove_peer(int member_id) {
|
||||
LinkPtr link;
|
||||
{
|
||||
std::lock_guard<std::mutex> lk(mu_);
|
||||
auto it = peers_.find(member_id);
|
||||
if (it == peers_.end()) return;
|
||||
link = it->second;
|
||||
peers_.erase(it);
|
||||
close_link(*link);
|
||||
}
|
||||
log(fmt("p2p: member %d removed (to peer %llu, to port %llu, signaling %llu)", member_id,
|
||||
static_cast<unsigned long long>(link->to_peer), static_cast<unsigned long long>(link->to_port),
|
||||
static_cast<unsigned long long>(link->signaling_count)));
|
||||
}
|
||||
|
||||
void Gateway::clear_peers() {
|
||||
std::vector<int> ids;
|
||||
{
|
||||
std::lock_guard<std::mutex> lk(mu_);
|
||||
for (const auto& [id, link] : peers_) ids.push_back(id);
|
||||
}
|
||||
for (int id : ids) remove_peer(id);
|
||||
}
|
||||
|
||||
void Gateway::remove_peers_except(const std::set<std::string>& online_ids) {
|
||||
std::vector<int> ids;
|
||||
{
|
||||
std::lock_guard<std::mutex> lk(mu_);
|
||||
for (const auto& [id, link] : peers_)
|
||||
if (!online_ids.count(link->online_id)) ids.push_back(id);
|
||||
}
|
||||
for (int id : ids) remove_peer(id);
|
||||
}
|
||||
|
||||
std::optional<Endpoint> Gateway::inner_endpoint_for(const std::string& online_id) const {
|
||||
std::lock_guard<std::mutex> lk(mu_);
|
||||
for (const auto& [id, link] : peers_)
|
||||
if (link->online_id == online_id) return link->inner_endpoint;
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::vector<PeerInfo> Gateway::peers() const {
|
||||
std::lock_guard<std::mutex> lk(mu_);
|
||||
std::vector<PeerInfo> out;
|
||||
for (const auto& [id, l] : peers_) {
|
||||
PeerInfo p;
|
||||
p.member_id = id;
|
||||
p.online_id = l->online_id;
|
||||
p.endpoint = l->endpoint;
|
||||
p.inner = l->inner_endpoint;
|
||||
p.peer_member_id = l->signaling.peer_member_id;
|
||||
p.port_member_id = l->signaling.port_member_id;
|
||||
p.room_id = l->signaling.room_id;
|
||||
p.to_peer = l->to_peer;
|
||||
p.to_port = l->to_port;
|
||||
p.signaling = l->signaling_count;
|
||||
out.push_back(p);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
bool Gateway::relay_target(const Endpoint& e) const {
|
||||
return relay_ && e.valid() && e.ip == relay_->server.ip && e.port != relay_->server.port;
|
||||
}
|
||||
|
||||
void Gateway::send_outer(const std::string& datagram, const Endpoint& to) {
|
||||
if (!to.valid()) return;
|
||||
std::optional<Relay> relay;
|
||||
{
|
||||
std::lock_guard<std::mutex> lk(mu_);
|
||||
relay = relay_;
|
||||
}
|
||||
if (relay && to.ip == relay->server.ip && to.port != relay->server.port)
|
||||
udp_send(outer_, relay_wrap(relay->token, to.port, datagram), relay->server);
|
||||
else
|
||||
udp_send(outer_, datagram, to);
|
||||
}
|
||||
|
||||
void Gateway::to_port(const LinkPtr& link, const std::string& frame) {
|
||||
SOCKET s;
|
||||
std::optional<Endpoint> dst;
|
||||
{
|
||||
std::lock_guard<std::mutex> lk(mu_);
|
||||
s = link->inner;
|
||||
dst = link->port_endpoint;
|
||||
}
|
||||
if (s != INVALID_SOCKET && dst) udp_send(s, frame, *dst);
|
||||
}
|
||||
|
||||
Gateway::LinkPtr Gateway::link_for(const Endpoint& src) {
|
||||
for (auto& [id, link] : peers_) {
|
||||
if (src == link->endpoint || src == link->local_endpoint || link->aliases.count(src)) {
|
||||
if (src != link->endpoint && link->aliases.count(src)) link->endpoint = src; // answer where it last spoke
|
||||
return link;
|
||||
}
|
||||
}
|
||||
// A NAT may hand the peer's datagrams a new port, and a relayed bbhost peer
|
||||
// can reach us from two relay ports (the one the room lists and the one
|
||||
// signaling resolved). With one peer from that address, take the new port
|
||||
// as another of its endpoints.
|
||||
LinkPtr only;
|
||||
int same = 0;
|
||||
for (auto& [id, link] : peers_)
|
||||
if (link->endpoint.valid() && link->endpoint.ip == src.ip) {
|
||||
only = link;
|
||||
++same;
|
||||
}
|
||||
if (same != 1) return nullptr;
|
||||
only->aliases.insert(only->endpoint);
|
||||
only->aliases.insert(src);
|
||||
log(fmt("p2p: member %d (%s) also at %s", only->member_id, only->online_id.c_str(), src.str().c_str()));
|
||||
only->endpoint = src;
|
||||
return only;
|
||||
}
|
||||
|
||||
void Gateway::outer_main() {
|
||||
std::string buf(65536, '\0');
|
||||
while (!stop_) {
|
||||
sockaddr_in from{};
|
||||
socklen_t flen = sizeof(from);
|
||||
const int n = recvfrom(outer_, buf.data(), static_cast<int>(buf.size()), 0,
|
||||
reinterpret_cast<sockaddr*>(&from), &flen);
|
||||
if (n < 0) {
|
||||
if (stop_) return;
|
||||
continue;
|
||||
}
|
||||
Endpoint src = from_sockaddr(from);
|
||||
std::string data(buf.data(), static_cast<size_t>(n));
|
||||
{
|
||||
std::lock_guard<std::mutex> lk(mu_);
|
||||
int port;
|
||||
std::string inner;
|
||||
if (relay_ && src == relay_->server && relay_unwrap(data, port, inner)) {
|
||||
src = Endpoint{relay_->server.ip, port};
|
||||
data = std::move(inner);
|
||||
}
|
||||
}
|
||||
if (data.size() >= 20 && data[0] == 0x01 && data[1] == 0x01) {
|
||||
on_stun(data);
|
||||
continue;
|
||||
}
|
||||
LinkPtr link;
|
||||
{
|
||||
std::lock_guard<std::mutex> lk(mu_);
|
||||
link = link_for(src);
|
||||
if (data.compare(0, 5, kProbe, 0, 5) == 0) {
|
||||
if (link) link->heard_peer = true;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
std::string frame;
|
||||
if (!link || !ps4_to_port(data, frame)) continue;
|
||||
{
|
||||
std::lock_guard<std::mutex> lk(mu_);
|
||||
link->heard_peer = true;
|
||||
++link->to_port;
|
||||
}
|
||||
to_port(link, frame);
|
||||
}
|
||||
}
|
||||
|
||||
void Gateway::inner_main() {
|
||||
std::string buf(65536, '\0');
|
||||
while (!stop_) {
|
||||
std::map<SOCKET, LinkPtr> links;
|
||||
{
|
||||
std::lock_guard<std::mutex> lk(mu_);
|
||||
for (auto& [id, link] : peers_)
|
||||
if (link->inner != INVALID_SOCKET) links[link->inner] = link;
|
||||
}
|
||||
if (links.empty()) {
|
||||
Sleep(50);
|
||||
continue;
|
||||
}
|
||||
fd_set rd;
|
||||
FD_ZERO(&rd);
|
||||
SOCKET highest = 0; // select()'s first argument: ignored on Windows, the highest fd + 1 elsewhere
|
||||
for (auto& [s, l] : links) {
|
||||
FD_SET(s, &rd);
|
||||
highest = std::max(highest, s);
|
||||
}
|
||||
timeval tv{0, 100000};
|
||||
if (select(static_cast<int>(highest) + 1, &rd, nullptr, nullptr, &tv) <= 0) continue; // a socket closed under us: rebuild
|
||||
for (auto& [s, link] : links) {
|
||||
if (!FD_ISSET(s, &rd)) continue;
|
||||
sockaddr_in from{};
|
||||
socklen_t flen = sizeof(from);
|
||||
const int n = recvfrom(s, buf.data(), static_cast<int>(buf.size()), 0,
|
||||
reinterpret_cast<sockaddr*>(&from), &flen);
|
||||
if (n < 0) continue;
|
||||
const std::string data(buf.data(), static_cast<size_t>(n));
|
||||
std::string out;
|
||||
const Frame kind = port_to_ps4(data, out);
|
||||
Endpoint peer;
|
||||
std::vector<std::string> replies;
|
||||
{
|
||||
std::lock_guard<std::mutex> lk(mu_);
|
||||
link->port_endpoint = from_sockaddr(from);
|
||||
if (kind == Frame::Game) {
|
||||
++link->to_peer;
|
||||
peer = link->endpoint;
|
||||
} else if (kind == Frame::Signaling) {
|
||||
++link->signaling_count;
|
||||
replies = link->signaling.handle(out);
|
||||
}
|
||||
}
|
||||
if (kind == Frame::Game) send_outer(out, peer);
|
||||
for (const auto& r : replies) to_port(link, port_signaling_frame(r));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Gateway::tick_main() {
|
||||
double last_stun = -1e9, last_punch = 0, last_opening = 0;
|
||||
while (!stop_) {
|
||||
Sleep(100);
|
||||
const double t = now();
|
||||
if (stun_server_ && t - last_stun >= stun_refresh) {
|
||||
last_stun = t;
|
||||
send_stun();
|
||||
}
|
||||
if (t - last_punch >= punch_interval) {
|
||||
last_punch = t;
|
||||
std::vector<Endpoint> targets;
|
||||
{
|
||||
std::lock_guard<std::mutex> lk(mu_);
|
||||
for (auto& [id, l] : peers_) {
|
||||
if (l->heard_peer || t - l->created > punch_for || relay_target(l->endpoint)) continue;
|
||||
if (l->endpoint.valid()) targets.push_back(l->endpoint);
|
||||
if (l->local_endpoint.valid()) targets.push_back(l->local_endpoint);
|
||||
}
|
||||
}
|
||||
for (const auto& e : targets) udp_send(outer_, kProbe, e);
|
||||
}
|
||||
if (t - last_opening >= opening_interval) {
|
||||
last_opening = t;
|
||||
std::vector<std::pair<LinkPtr, std::vector<std::string>>> openings;
|
||||
{
|
||||
std::lock_guard<std::mutex> lk(mu_);
|
||||
for (auto& [id, l] : peers_)
|
||||
if (!l->signaling.heard && t - l->created <= opening_for)
|
||||
openings.emplace_back(l, l->signaling.opening());
|
||||
}
|
||||
for (auto& [l, payloads] : openings)
|
||||
for (const auto& p : payloads) to_port(l, port_signaling_frame(p));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Gateway::send_stun() {
|
||||
const std::string ip = resolve_ipv4(stun_server_->first);
|
||||
if (ip.empty()) {
|
||||
log(fmt("p2p: cannot resolve STUN server %s", stun_server_->first.c_str()));
|
||||
return;
|
||||
}
|
||||
std::string request;
|
||||
Endpoint server{ip, stun_server_->second};
|
||||
{
|
||||
std::lock_guard<std::mutex> lk(mu_);
|
||||
const std::string token = relay_ && relay_->server.ip == ip ? relay_->token : "";
|
||||
request = stun_request(token, stun_txid_);
|
||||
stun_server_ep_ = server;
|
||||
}
|
||||
udp_send(outer_, request, server);
|
||||
}
|
||||
|
||||
void Gateway::on_stun(const std::string& data) {
|
||||
std::optional<Endpoint> changed;
|
||||
{
|
||||
std::lock_guard<std::mutex> lk(mu_);
|
||||
StunAnswer a;
|
||||
if (stun_txid_.empty() || !stun_parse(data, stun_txid_, a)) return;
|
||||
if (a.relay && stun_server_ep_.valid()) {
|
||||
if (!relay_ || relay_->port != a.relay_port) log(fmt("p2p: relay on, our port on it is %d", a.relay_port));
|
||||
relay_ = Relay{stun_server_ep_, a.relay_token, a.relay_port};
|
||||
} else if (relay_) {
|
||||
log("p2p: relay off");
|
||||
relay_.reset();
|
||||
}
|
||||
if (!mapped_ || *mapped_ != a.mapped) {
|
||||
log("p2p: STUN sees us as " + a.mapped.str());
|
||||
mapped_ = a.mapped;
|
||||
changed = a.mapped;
|
||||
}
|
||||
}
|
||||
if (changed && on_mapped_change_) {
|
||||
try {
|
||||
on_mapped_change_(*changed);
|
||||
} catch (const std::exception& e) {
|
||||
log(std::string("p2p: mapped-change callback failed: ") + e.what());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace bb::p2p
|
||||
@@ -1,163 +0,0 @@
|
||||
// P2P gateway: carries the game's UDP traffic between bbport and bbhost players.
|
||||
//
|
||||
// bbport (gpu/shadps4/core/libraries/network/p2p_sockets.cpp)
|
||||
// game datagram: [src vport u16 be][dst vport u16 be][payload]
|
||||
// dst vport 0xffff: internal "SHAD" packets - 0x05/0x06 echo (17 B),
|
||||
// 0x07 handshake (62 B), 0x10 control (50 B), 0x21 matching2 handshake (50 B)
|
||||
// bbhost (src/hle/net.cpp, src/net/stun.cpp)
|
||||
// game datagram: [0xff][flags][src][dst][payload] - flags 0xC3 with one-byte
|
||||
// vports (Bloodborne's 40 and 30), 0x83 with two-byte, +4 bytes with 0x20
|
||||
// hole-punch probe "\xfebbhp\0\0\0"; STUN Binding + BBHOST-HELLO (0x8100);
|
||||
// relay: to the STUN port [0xfb]['R'][token 8][port u16][datagram],
|
||||
// from it [0xfb]['r'][source port u16][datagram]
|
||||
//
|
||||
// One OUTER socket faces the internet and speaks bbhost's format; each remote
|
||||
// member gets an INNER socket on 127.0.0.1, which is where the port is told the
|
||||
// member lives. bbport's signaling handshakes are answered on the bbhost
|
||||
// peer's behalf (bbhost has none: its connection state comes from the server).
|
||||
#pragma once
|
||||
|
||||
#include <atomic>
|
||||
#include <functional>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "util.hpp"
|
||||
|
||||
namespace bb::p2p {
|
||||
|
||||
// ---- framing (pure, unit-tested) ----
|
||||
enum class Frame { None, Game, Signaling };
|
||||
Frame port_to_ps4(const std::string& frame, std::string& out); // Game: bbhost datagram; Signaling: payload
|
||||
bool ps4_to_port(const std::string& datagram, std::string& out);
|
||||
std::string port_signaling_frame(const std::string& payload);
|
||||
std::string relay_wrap(const std::string& token, int port, const std::string& datagram);
|
||||
bool relay_unwrap(const std::string& data, int& port, std::string& datagram);
|
||||
extern const std::string kProbe;
|
||||
|
||||
struct StunAnswer {
|
||||
Endpoint mapped;
|
||||
bool relay = false;
|
||||
std::string relay_token; // 8 bytes
|
||||
int relay_port = 0;
|
||||
};
|
||||
std::string stun_request(const std::string& token, std::string& txid); // token "" = zeros
|
||||
bool stun_parse(const std::string& data, const std::string& txid, StunAnswer& out);
|
||||
|
||||
// ---- the port's signaling, answered for one bbhost peer ----
|
||||
enum : uint8_t { T_ECHO_PING = 0x05, T_ECHO_PONG = 0x06, T_HANDSHAKE = 0x07, T_CONTROL = 0x10, T_M2 = 0x21 };
|
||||
enum : uint8_t { HS_OFFER = 1, HS_ACCEPT = 2, HS_CHECK = 3, HS_CHECK_ACK = 4 };
|
||||
enum : uint8_t { CTL_ACTIVATION_REQUEST = 1, CTL_ACTIVATION_ACK = 2, CTL_CLOSE = 3, CTL_ESTABLISHED = 4 };
|
||||
|
||||
class PeerSignaling {
|
||||
public:
|
||||
PeerSignaling(const std::string& peer_online_id, int peer_member_id, const std::string& port_online_id,
|
||||
int port_member_id, uint64_t room_id);
|
||||
std::vector<std::string> handle(const std::string& payload); // replies (payloads, no frame header)
|
||||
std::vector<std::string> opening() const; // what a real peer sends first
|
||||
|
||||
std::string peer_id, port_id; // 16 bytes each, NUL padded
|
||||
int peer_member_id = 0; // 0 until the peer is a room member
|
||||
int port_member_id = 0;
|
||||
uint64_t room_id = 0;
|
||||
uint32_t conn_id = 0;
|
||||
bool sent_established = false;
|
||||
bool heard = false; // any signaling from the port for this peer
|
||||
|
||||
private:
|
||||
std::string handshake(uint8_t kind, uint32_t to_conn, uint64_t nonce) const;
|
||||
std::string control(uint8_t kind) const;
|
||||
std::string m2(uint8_t kind, uint64_t nonce) const;
|
||||
};
|
||||
|
||||
// ---- the gateway ----
|
||||
struct PeerInfo { // a snapshot, for the app and the tests
|
||||
int member_id = 0;
|
||||
std::string online_id;
|
||||
Endpoint endpoint, inner;
|
||||
int peer_member_id = 0, port_member_id = 0;
|
||||
uint64_t room_id = 0;
|
||||
uint64_t to_peer = 0, to_port = 0, signaling = 0;
|
||||
};
|
||||
|
||||
class Gateway {
|
||||
public:
|
||||
Gateway(int outer_port, std::optional<std::pair<std::string, int>> stun_server, std::string lan_addr,
|
||||
std::function<void(const Endpoint&)> on_mapped_change);
|
||||
~Gateway();
|
||||
bool start(std::string* error);
|
||||
void stop();
|
||||
|
||||
std::pair<Endpoint, Endpoint> endpoints() const; // (local, mapped) as bbhost registers them
|
||||
std::optional<Endpoint> mapped() const;
|
||||
int relay_port() const;
|
||||
|
||||
// Returns the 127.0.0.1 endpoint the port should use for this member.
|
||||
Endpoint add_peer(int member_id, const std::string& online_id, const Endpoint& endpoint,
|
||||
const Endpoint& local_endpoint, const std::string& port_online_id, int port_member_id,
|
||||
uint64_t room_id, const std::optional<Endpoint>& port_endpoint);
|
||||
void remove_peer(int member_id);
|
||||
void clear_peers();
|
||||
void remove_peers_except(const std::set<std::string>& online_ids);
|
||||
std::optional<Endpoint> inner_endpoint_for(const std::string& online_id) const;
|
||||
std::vector<PeerInfo> peers() const;
|
||||
|
||||
double punch_interval = 0.5, punch_for = 20.0, opening_interval = 1.0, opening_for = 30.0,
|
||||
stun_refresh = 15.0;
|
||||
|
||||
private:
|
||||
struct Link {
|
||||
int member_id = 0;
|
||||
std::string online_id;
|
||||
Endpoint endpoint, local_endpoint;
|
||||
std::set<Endpoint> aliases;
|
||||
std::optional<Endpoint> port_endpoint;
|
||||
PeerSignaling signaling;
|
||||
SOCKET inner = INVALID_SOCKET;
|
||||
Endpoint inner_endpoint;
|
||||
bool heard_peer = false;
|
||||
double created = 0;
|
||||
uint64_t to_peer = 0, to_port = 0, signaling_count = 0;
|
||||
Link(PeerSignaling s) : signaling(std::move(s)) {}
|
||||
};
|
||||
using LinkPtr = std::shared_ptr<Link>;
|
||||
|
||||
void outer_main();
|
||||
void inner_main();
|
||||
void tick_main();
|
||||
void send_outer(const std::string& datagram, const Endpoint& to);
|
||||
void to_port(const LinkPtr& link, const std::string& frame);
|
||||
LinkPtr link_for(const Endpoint& src); // under mu_
|
||||
bool relay_target(const Endpoint& e) const; // under mu_
|
||||
void send_stun();
|
||||
void on_stun(const std::string& data);
|
||||
static void close_link(Link& link);
|
||||
|
||||
int outer_port_ = 0;
|
||||
std::optional<std::pair<std::string, int>> stun_server_;
|
||||
std::string lan_addr_;
|
||||
std::function<void(const Endpoint&)> on_mapped_change_;
|
||||
SOCKET outer_ = INVALID_SOCKET;
|
||||
mutable std::mutex mu_;
|
||||
std::map<int, LinkPtr> peers_;
|
||||
std::optional<Endpoint> mapped_;
|
||||
struct Relay {
|
||||
Endpoint server; // its STUN endpoint
|
||||
std::string token;
|
||||
int port = 0; // ours on it
|
||||
};
|
||||
std::optional<Relay> relay_;
|
||||
std::string stun_txid_;
|
||||
Endpoint stun_server_ep_;
|
||||
std::atomic<bool> stop_{false};
|
||||
std::vector<std::thread> threads_;
|
||||
};
|
||||
|
||||
} // namespace bb::p2p
|
||||
@@ -1,85 +0,0 @@
|
||||
#include "proto3.hpp"
|
||||
|
||||
#include <cstring>
|
||||
|
||||
namespace pb {
|
||||
|
||||
namespace {
|
||||
bool read_varint(const std::string& b, size_t& pos, uint64_t& out) {
|
||||
out = 0;
|
||||
int shift = 0;
|
||||
while (true) {
|
||||
if (pos >= b.size() || shift > 63) return false;
|
||||
const uint8_t c = static_cast<uint8_t>(b[pos++]);
|
||||
out |= static_cast<uint64_t>(c & 0x7F) << shift;
|
||||
if (!(c & 0x80)) return true;
|
||||
shift += 7;
|
||||
}
|
||||
}
|
||||
} // namespace
|
||||
|
||||
bool parse(const std::string& b, std::vector<Field>& out) {
|
||||
size_t pos = 0;
|
||||
while (pos < b.size()) {
|
||||
uint64_t tag = 0;
|
||||
if (!read_varint(b, pos, tag)) return false;
|
||||
Field f;
|
||||
f.number = static_cast<int>(tag >> 3);
|
||||
f.wire = static_cast<int>(tag & 7);
|
||||
switch (f.wire) {
|
||||
case 0:
|
||||
if (!read_varint(b, pos, f.value)) return false;
|
||||
break;
|
||||
case 2: {
|
||||
uint64_t len = 0;
|
||||
if (!read_varint(b, pos, len) || len > b.size() - pos) return false;
|
||||
f.data.assign(b, pos, static_cast<size_t>(len));
|
||||
pos += static_cast<size_t>(len);
|
||||
break;
|
||||
}
|
||||
case 5:
|
||||
if (b.size() - pos < 4) return false;
|
||||
{ uint32_t v; std::memcpy(&v, b.data() + pos, 4); f.value = v; }
|
||||
pos += 4;
|
||||
break;
|
||||
case 1:
|
||||
if (b.size() - pos < 8) return false;
|
||||
std::memcpy(&f.value, b.data() + pos, 8);
|
||||
pos += 8;
|
||||
break;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
out.push_back(std::move(f));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
uint64_t Message::u(int field, uint64_t def) const {
|
||||
uint64_t v = def;
|
||||
for (const auto& f : fields_)
|
||||
if (f.number == field && f.wire != 2) v = f.value;
|
||||
return v;
|
||||
}
|
||||
|
||||
std::string Message::s(int field) const {
|
||||
std::string v;
|
||||
for (const auto& f : fields_)
|
||||
if (f.number == field && f.wire == 2) v = f.data;
|
||||
return v;
|
||||
}
|
||||
|
||||
bool Message::has(int field) const {
|
||||
for (const auto& f : fields_)
|
||||
if (f.number == field) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
std::vector<std::string> Message::all(int field) const {
|
||||
std::vector<std::string> v;
|
||||
for (const auto& f : fields_)
|
||||
if (f.number == field && f.wire == 2) v.push_back(f.data);
|
||||
return v;
|
||||
}
|
||||
|
||||
} // namespace pb
|
||||
@@ -1,73 +0,0 @@
|
||||
// Minimal protobuf3 wire format: varint and length-delimited fields, which is
|
||||
// all the shadNet messages use. proto3 semantics: fields at their default value
|
||||
// are omitted on encode; decode keeps what is on the wire.
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace pb {
|
||||
|
||||
class Writer {
|
||||
public:
|
||||
void varint(uint64_t v) {
|
||||
while (v >= 0x80) {
|
||||
buf_.push_back(static_cast<char>((v & 0x7F) | 0x80));
|
||||
v >>= 7;
|
||||
}
|
||||
buf_.push_back(static_cast<char>(v));
|
||||
}
|
||||
void tag(int field, int wire) { varint((static_cast<uint64_t>(field) << 3) | static_cast<uint64_t>(wire)); }
|
||||
|
||||
Writer& u(int field, uint64_t v) { // uint / int (0 omitted)
|
||||
if (v) { tag(field, 0); varint(v); }
|
||||
return *this;
|
||||
}
|
||||
Writer& i(int field, int64_t v) { return u(field, static_cast<uint64_t>(v)); }
|
||||
Writer& flag(int field, bool v) { // bool (false omitted)
|
||||
if (v) { tag(field, 0); varint(1); }
|
||||
return *this;
|
||||
}
|
||||
Writer& s(int field, const std::string& v) { // string / bytes ("" omitted)
|
||||
if (!v.empty()) { tag(field, 2); varint(v.size()); buf_ += v; }
|
||||
return *this;
|
||||
}
|
||||
Writer& msg(int field, const std::string& encoded) { // embedded message, always written
|
||||
tag(field, 2);
|
||||
varint(encoded.size());
|
||||
buf_ += encoded;
|
||||
return *this;
|
||||
}
|
||||
const std::string& str() const { return buf_; }
|
||||
|
||||
private:
|
||||
std::string buf_;
|
||||
};
|
||||
|
||||
struct Field {
|
||||
int number = 0;
|
||||
int wire = 0;
|
||||
uint64_t value = 0; // varint / fixed
|
||||
std::string data; // length-delimited
|
||||
};
|
||||
|
||||
bool parse(const std::string& buf, std::vector<Field>& out);
|
||||
|
||||
// Read access by field number; scalars keep the last occurrence.
|
||||
class Message {
|
||||
public:
|
||||
Message() = default;
|
||||
explicit Message(const std::string& buf) { ok_ = parse(buf, fields_); }
|
||||
bool ok() const { return ok_; }
|
||||
uint64_t u(int field, uint64_t def = 0) const;
|
||||
std::string s(int field) const;
|
||||
bool has(int field) const;
|
||||
std::vector<std::string> all(int field) const;
|
||||
|
||||
private:
|
||||
std::vector<Field> fields_;
|
||||
bool ok_ = false;
|
||||
};
|
||||
|
||||
} // namespace pb
|
||||
@@ -1,262 +0,0 @@
|
||||
#include "setup.hpp"
|
||||
|
||||
#include <tlhelp32.h>
|
||||
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
|
||||
namespace bb {
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr const char* kBackup = ".before-bbbridge";
|
||||
constexpr const char* kMarker = "_bbbridge";
|
||||
constexpr const char* kP2pVar = "SHADPS4_P2P_PORT";
|
||||
constexpr int kP2pPort = 3658;
|
||||
constexpr const char* kBridgeServer = "127.0.0.1:31313";
|
||||
constexpr const char* kBridgeWebapi = "http://127.0.0.1:31315";
|
||||
constexpr const char* kPublicServer = "srv.shadps4.net:31313";
|
||||
constexpr const char* kPlaceholderPassword = "bbbridge"; // see enable()
|
||||
|
||||
// bbhost's play-log patch (bbhost src/engine/playlog.cpp) in bbport's patch
|
||||
// format: the float immediates of `mov dword [rip+disp], imm32` in
|
||||
// SprjPlaylogSystem's static initializer - 0x23F9B86 stores 300.0f (upload
|
||||
// period), 0x23F9B68 stores 1.5f (position period); the float is at +6.
|
||||
// Verified against eboot.bin 1.09.
|
||||
const char* kPatchXml = R"xml(<?xml version="1.0"?>
|
||||
<Patch>
|
||||
<TitleID>
|
||||
<ID>CUSA03173</ID>
|
||||
</TitleID>
|
||||
<Metadata Title="Bloodborne" Name="Live map: play log every 5 s, position every 0.5 s" Note="Uploads the play log every 5 s instead of 300 s and samples the position every 0.5 s instead of 1.5 s, as bbhost does, so the player shows on thehuntersdream.com's live map." Author="bbbridge (from bbhost)" PatchVer="1.0" AppVer="01.09" AppElf="eboot.bin" isEnabled="true">
|
||||
<PatchList>
|
||||
<Line Type="float32" Address="0x023F9B8C" Value="5.0"/>
|
||||
<Line Type="float32" Address="0x023F9B6E" Value="0.5"/>
|
||||
</PatchList>
|
||||
</Metadata>
|
||||
</Patch>
|
||||
)xml";
|
||||
|
||||
nlohmann::ordered_json overrides() {
|
||||
return {{kMarker, "Written by bbbridge: the game's server traffic goes through bbbridge. Move bbbridge's "
|
||||
"Online switch to shadNet to remove."},
|
||||
{"https://ss4.scej-network.jp:20443", "http://127.0.0.1:31316"},
|
||||
{"https://thehuntersdream.com:20443", "http://127.0.0.1:31316"},
|
||||
{"https://thehuntersdream.com:18671", "http://127.0.0.1:31317"},
|
||||
{"http://thehuntersdream.com:18671", "http://127.0.0.1:31317"},
|
||||
{"https://bb-playlog-test.s3.amazonaws.com:443", "http://127.0.0.1:31317"},
|
||||
{"https://bb-playlog-prod.s3.amazonaws.com:443", "http://127.0.0.1:31317"},
|
||||
{"https://thehuntersdream.com:18671/summon_messenger", "http://127.0.0.1:31315"},
|
||||
{"http://thehuntersdream.com:18671/summon_messenger", "http://127.0.0.1:31315"}};
|
||||
}
|
||||
|
||||
using ojson = nlohmann::ordered_json;
|
||||
|
||||
ojson read_json(const fs::path& p) {
|
||||
std::ifstream in(p);
|
||||
if (!in) return ojson();
|
||||
ojson j = ojson::parse(in, nullptr, false);
|
||||
return j.is_discarded() ? ojson() : j;
|
||||
}
|
||||
|
||||
void write_json(const fs::path& p, const ojson& j) {
|
||||
const fs::path tmp = p.wstring() + L".tmp";
|
||||
{
|
||||
std::ofstream out(tmp, std::ios::trunc);
|
||||
out << j.dump(2, ' ', false) << "\n";
|
||||
}
|
||||
MoveFileExW(tmp.c_str(), p.c_str(), MOVEFILE_REPLACE_EXISTING);
|
||||
}
|
||||
|
||||
void backup(const fs::path& p) {
|
||||
const fs::path target = p.wstring() + widen(kBackup);
|
||||
std::error_code ec;
|
||||
if (fs::exists(p, ec) && !fs::exists(target, ec)) fs::copy_file(p, target, ec);
|
||||
}
|
||||
|
||||
std::string merge_env(const std::string& extra, const std::string& key, const std::string* value) {
|
||||
std::istringstream in(extra);
|
||||
std::string item, out;
|
||||
while (in >> item)
|
||||
if (item.rfind(key + "=", 0) != 0) out += (out.empty() ? "" : " ") + item;
|
||||
if (value) out += (out.empty() ? "" : " ") + key + "=" + *value;
|
||||
return out;
|
||||
}
|
||||
|
||||
// Ours, or the Python bbbridge's (0.2.0: "_note": "bbbridge: ...").
|
||||
bool ours(const fs::path& overrides_file) {
|
||||
const ojson j = read_json(overrides_file);
|
||||
return j.is_object() && (j.contains(kMarker) || jstr(j, "_note").rfind("bbbridge:", 0) == 0);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool process_running(const wchar_t* image) {
|
||||
HANDLE snap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
|
||||
if (snap == INVALID_HANDLE_VALUE) return false;
|
||||
PROCESSENTRY32W e{};
|
||||
e.dwSize = sizeof(e);
|
||||
bool found = false;
|
||||
for (BOOL ok = Process32FirstW(snap, &e); ok && !found; ok = Process32NextW(snap, &e))
|
||||
found = _wcsicmp(e.szExeFile, image) == 0;
|
||||
CloseHandle(snap);
|
||||
return found;
|
||||
}
|
||||
|
||||
bool launcher_running() { return process_running(L"Bloodborne.exe"); }
|
||||
bool game_running() { return process_running(L"Play Bloodborne.exe") || process_running(L"bb-probe.exe"); }
|
||||
|
||||
bool is_bbport_dir(const fs::path& dir) {
|
||||
std::error_code ec;
|
||||
return fs::is_regular_file(dir / L"Bloodborne.exe", ec) || fs::is_regular_file(dir / L"Play Bloodborne.exe", ec);
|
||||
}
|
||||
|
||||
fs::path find_bbport(const fs::path& exe_dir, const fs::path& chosen, const std::string& game_dir,
|
||||
BbportSource* source) {
|
||||
const std::pair<fs::path, BbportSource> candidates[] = {
|
||||
{chosen, BbportSource::Chosen},
|
||||
{exe_dir.parent_path(), BbportSource::ExeFolder}, // bbbridge lives in a folder inside bbport's
|
||||
{exe_dir, BbportSource::ExeFolder}, // or right next to Bloodborne.exe
|
||||
{game_dir.empty() ? fs::path() : fs::path(widen(game_dir)).parent_path(), BbportSource::Launcher}};
|
||||
for (const auto& [dir, src] : candidates)
|
||||
if (!dir.empty() && is_bbport_dir(dir)) {
|
||||
if (source) *source = src;
|
||||
return dir;
|
||||
}
|
||||
if (source) *source = BbportSource::None;
|
||||
return {};
|
||||
}
|
||||
|
||||
std::string describe(BbportSource source) {
|
||||
switch (source) {
|
||||
case BbportSource::Chosen: return "the folder you chose";
|
||||
case BbportSource::ExeFolder: return "the folder bbbridge is in";
|
||||
case BbportSource::Launcher: return "found from the bbport launcher's game folder";
|
||||
default: return "not found";
|
||||
}
|
||||
}
|
||||
|
||||
BbportSetup::BbportSetup(const fs::path& exe_dir, const fs::path& bbport_dir) : exe_dir_(exe_dir), chosen_(bbport_dir) {
|
||||
reload();
|
||||
}
|
||||
|
||||
void BbportSetup::reload() {
|
||||
wchar_t appdata[MAX_PATH] = {};
|
||||
if (!GetEnvironmentVariableW(L"APPDATA", appdata, MAX_PATH)) throw SetupError("APPDATA is not set");
|
||||
settings_file.clear();
|
||||
for (const wchar_t* name : {L"bbport-launcher", L"bbport-launcher-coop"}) {
|
||||
const fs::path p = fs::path(appdata) / name / L"settings.json";
|
||||
std::error_code ec;
|
||||
if (fs::is_regular_file(p, ec)) {
|
||||
settings_file = p;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (settings_file.empty())
|
||||
throw SetupError("bbport launcher settings not found. Start the bbport launcher once and close it.");
|
||||
settings = read_json(settings_file);
|
||||
if (!settings.is_object()) throw SetupError("the bbport launcher's settings.json cannot be read");
|
||||
bbport = find_bbport(exe_dir_, chosen_, jstr(settings, "game_dir"), &source);
|
||||
if (bbport.empty()) throw SetupError("bbport folder not found - choose the folder with Bloodborne.exe.");
|
||||
const std::string user = jstr(settings, "user_dir"), patches = jstr(settings, "patches_dir");
|
||||
user_dir = user.empty() ? bbport / L"user" : fs::path(widen(user));
|
||||
patches_dir = patches.empty() ? bbport / L"patches" : fs::path(widen(patches));
|
||||
}
|
||||
|
||||
bool BbportSetup::routed() const {
|
||||
return jint(settings, "online", 0) && jstr(settings, "online_server") == kBridgeServer &&
|
||||
jstr(settings, "online_webapi") == kBridgeWebapi && !jstr(settings, "online_password").empty();
|
||||
}
|
||||
|
||||
bool BbportSetup::overrides_written() const { return ours(overrides_file()); }
|
||||
|
||||
// bbport's run.py uses host_overrides.custom.json, when there is one, instead of
|
||||
// its own addresses: ours sends the game's server traffic to bbbridge, so it may
|
||||
// only be there while bbbridge runs - else the game cannot reach its server
|
||||
// ("Failed to acquire Bloodborne server information").
|
||||
void BbportSetup::set_overrides(bool on) {
|
||||
std::error_code ec;
|
||||
if (on) {
|
||||
fs::create_directories(user_dir, ec);
|
||||
if (fs::exists(overrides_file(), ec) && !ours(overrides_file())) backup(overrides_file());
|
||||
write_json(overrides_file(), overrides());
|
||||
} else if (ours(overrides_file())) {
|
||||
fs::remove(overrides_file(), ec);
|
||||
const fs::path prev = overrides_file().wstring() + widen(kBackup);
|
||||
if (fs::exists(prev, ec)) fs::rename(prev, overrides_file(), ec);
|
||||
}
|
||||
}
|
||||
|
||||
bool BbportSetup::game_ready() const {
|
||||
const std::string dir = jstr(settings, "game_dir");
|
||||
std::error_code ec;
|
||||
return !dir.empty() && fs::is_regular_file(fs::path(widen(dir)) / L"eboot.bin", ec);
|
||||
}
|
||||
|
||||
bool BbportSetup::patched() const {
|
||||
std::error_code ec;
|
||||
return fs::is_regular_file(patch_file(), ec);
|
||||
}
|
||||
|
||||
void BbportSetup::save_settings() {
|
||||
backup(settings_file);
|
||||
write_json(settings_file, settings);
|
||||
}
|
||||
|
||||
void BbportSetup::set_npid(const std::string& npid) {
|
||||
settings["online_npid"] = npid;
|
||||
save_settings();
|
||||
}
|
||||
|
||||
bool BbportSetup::shadnet_account_set() const {
|
||||
const std::string pw = jstr(settings, "online_password");
|
||||
return !npid().empty() && !pw.empty() && pw != kPlaceholderPassword;
|
||||
}
|
||||
|
||||
void BbportSetup::enable() {
|
||||
// On bbport's own server now: keep that setup (server, shadNet account) for disable(),
|
||||
// also when the player changed it since the last time.
|
||||
if (jstr(settings, "online_server") != kBridgeServer)
|
||||
write_json(settings_file.wstring() + widen(kBackup), settings);
|
||||
settings["online"] = true;
|
||||
settings["online_server"] = kBridgeServer;
|
||||
settings["online_webapi"] = kBridgeWebapi;
|
||||
// bbport does not go online without a password (np_handler.cpp: "shadNet
|
||||
// enabled but credentials missing, skipping"). The bridge never reads it; a
|
||||
// player without a shadNet account just needs something there.
|
||||
if (jstr(settings, "online_password").empty()) settings["online_password"] = kPlaceholderPassword;
|
||||
const std::string port = std::to_string(kP2pPort);
|
||||
settings["extra_env"] = merge_env(jstr(settings, "extra_env"), kP2pVar, &port);
|
||||
save_settings();
|
||||
set_overrides(true);
|
||||
}
|
||||
|
||||
void BbportSetup::disable() {
|
||||
const ojson old = read_json(settings_file.wstring() + widen(kBackup));
|
||||
std::string server = jstr(old, "online_server", kPublicServer);
|
||||
if (server.empty() || server == kBridgeServer) server = kPublicServer;
|
||||
std::string webapi = jstr(old, "online_webapi", "");
|
||||
if (webapi == kBridgeWebapi) webapi.clear();
|
||||
settings["online_server"] = server;
|
||||
settings["online_webapi"] = webapi;
|
||||
if (jhas(old, "online_npid")) settings["online_npid"] = jstr(old, "online_npid"); // the shadNet account, not The Hunter's Dream's
|
||||
if (jstr(settings, "online_password") == kPlaceholderPassword) { // ours: back to what it was
|
||||
const std::string before = jstr(old, "online_password");
|
||||
settings["online_password"] = before == kPlaceholderPassword ? "" : before;
|
||||
}
|
||||
settings["extra_env"] = merge_env(jstr(settings, "extra_env"), kP2pVar, nullptr);
|
||||
save_settings();
|
||||
set_overrides(false);
|
||||
}
|
||||
|
||||
void BbportSetup::set_patch(bool on) {
|
||||
std::error_code ec;
|
||||
if (on) {
|
||||
fs::create_directories(patches_dir, ec);
|
||||
std::ofstream(patch_file(), std::ios::trunc) << kPatchXml;
|
||||
} else {
|
||||
fs::remove(patch_file(), ec);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace bb
|
||||
@@ -1,80 +0,0 @@
|
||||
// Points an installed bbport (bloodborne_pc for Windows) at the bridge, or back.
|
||||
// Turning it on keeps bbport's own shadNet setup (server, account) in
|
||||
// settings.json.before-bbbridge; turning it off puts that back.
|
||||
// "Routed" means (a backup of every file is kept as *.before-bbbridge):
|
||||
// 1. %APPDATA%\bbport-launcher\settings.json: online on; co-op server
|
||||
// 127.0.0.1:31313; WebAPI http://127.0.0.1:31315; extra variable
|
||||
// SHADPS4_P2P_PORT=3658 (the bridge must know the port's P2P UDP port).
|
||||
// 2. <bbport user dir>\host_overrides.custom.json: the game's own server
|
||||
// traffic goes through the bridge (the account token, and the play log to
|
||||
// The Hunter's Dream instead of FromSoftware's closed upload bucket). Only
|
||||
// while bbbridge runs: the app removes it when it closes.
|
||||
// 3. <bbport>\patches\bbbridge-live-map.xml (optional): play log every 5 s,
|
||||
// position every 0.5 s - bbhost's own patch - for the site's live map.
|
||||
// The bbport launcher must be closed while settings.json changes: it rewrites
|
||||
// the file when it saves.
|
||||
#pragma once
|
||||
|
||||
#include <filesystem>
|
||||
#include <string>
|
||||
|
||||
#include "util.hpp"
|
||||
|
||||
namespace bb {
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
struct SetupError : std::runtime_error {
|
||||
using std::runtime_error::runtime_error;
|
||||
};
|
||||
|
||||
bool process_running(const wchar_t* image);
|
||||
bool launcher_running(); // Bloodborne.exe
|
||||
bool game_running(); // Play Bloodborne.exe / bb-probe.exe
|
||||
bool is_bbport_dir(const fs::path& dir);
|
||||
|
||||
// Where bbport was found, in this order: the folder the player chose, the
|
||||
// folder bbbridge's own folder is in (the normal install), bbbridge's own folder
|
||||
// (bbbridge.exe next to Bloodborne.exe), the launcher's game folder.
|
||||
enum class BbportSource { None, Chosen, ExeFolder, Launcher };
|
||||
fs::path find_bbport(const fs::path& exe_dir, const fs::path& chosen, const std::string& game_dir,
|
||||
BbportSource* source);
|
||||
std::string describe(BbportSource source);
|
||||
|
||||
class BbportSetup {
|
||||
public:
|
||||
// bbport_dir: "" = the folder above this exe, or from the launcher's settings.
|
||||
explicit BbportSetup(const fs::path& exe_dir, const fs::path& bbport_dir = {});
|
||||
void reload();
|
||||
|
||||
fs::path bbport, settings_file, user_dir, patches_dir;
|
||||
BbportSource source = BbportSource::None;
|
||||
nlohmann::ordered_json settings; // the launcher's file, key order kept
|
||||
|
||||
std::string npid() const { return jstr(settings, "online_npid"); }
|
||||
std::string server() const { return jstr(settings, "online_server"); }
|
||||
bool online() const { return jint(settings, "online", 0) != 0; }
|
||||
bool shadnet_account_set() const; // a name and a real (not bbbridge's placeholder) password
|
||||
bool routed() const; // bbport's settings point at bbbridge
|
||||
bool overrides_written() const; // our host_overrides.custom.json is there
|
||||
void set_overrides(bool on); // only while bbbridge runs (see setup.cpp)
|
||||
bool patched() const;
|
||||
bool game_ready() const; // the launcher has a game folder with eboot.bin
|
||||
|
||||
// the caller makes sure the launcher is closed
|
||||
void set_npid(const std::string& npid);
|
||||
void enable();
|
||||
void disable();
|
||||
void set_patch(bool on);
|
||||
|
||||
fs::path play_exe() const { return bbport / L"Play Bloodborne.exe"; }
|
||||
fs::path launcher_exe() const { return bbport / L"Bloodborne.exe"; }
|
||||
|
||||
private:
|
||||
void save_settings();
|
||||
fs::path overrides_file() const { return user_dir / L"host_overrides.custom.json"; }
|
||||
fs::path patch_file() const { return patches_dir / L"bbbridge-live-map.xml"; }
|
||||
fs::path exe_dir_, chosen_;
|
||||
};
|
||||
|
||||
} // namespace bb
|
||||
@@ -1,103 +0,0 @@
|
||||
// shadNet wire protocol: 15-byte packet framing, command / error / notification
|
||||
// numbers, and the field numbers of the messages the bridge reads or writes.
|
||||
// Sources: shadnet-p2p's protocol.h and shadnet.proto, the port's client.cpp.
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
|
||||
namespace shadnet {
|
||||
|
||||
constexpr size_t kHeaderSize = 15;
|
||||
constexpr uint32_t kMaxPacketSize = 0x800000;
|
||||
constexpr uint32_t kProtocolVersion = 1;
|
||||
|
||||
enum PacketType : uint8_t { Request = 0, Reply = 1, Notification = 2, ServerInfo = 3 };
|
||||
|
||||
namespace cmd {
|
||||
constexpr uint16_t Login = 0, Terminate = 1, AddFriend = 8, RemoveFriend = 9, AddBlock = 10,
|
||||
RemoveBlock = 11, GetServerFeatures = 12, SetClientVersion = 13, GetToken = 39,
|
||||
SetAppearOffline = 40, ContextStart = 100, CreateRoom = 101, JoinRoom = 102, LeaveRoom = 103,
|
||||
SearchRoom = 104, RequestSignalingInfos = 105, ContextStop = 106,
|
||||
SetRoomDataInternal = 108, SetRoomDataExternal = 109, KickoutRoomMember = 110,
|
||||
GetWorldInfoList = 111;
|
||||
}
|
||||
|
||||
namespace err {
|
||||
constexpr uint8_t NoError = 0, Malformed = 1, Invalid = 2, InvalidInput = 3, LoginError = 5,
|
||||
LoginInvalidPassword = 8, RoomMissing = 14, NotFound = 26, Unsupported = 33;
|
||||
}
|
||||
|
||||
namespace notify {
|
||||
constexpr uint16_t RoomEvent = 10;
|
||||
}
|
||||
|
||||
namespace room_event {
|
||||
constexpr uint32_t MemberJoined = 0x1101, MemberLeft = 0x1102, KickedOut = 0x1103;
|
||||
}
|
||||
|
||||
namespace cause {
|
||||
constexpr uint32_t Leave = 1, Kickout = 2, ServerOperation = 4, MemberDisappeared = 5;
|
||||
}
|
||||
|
||||
constexpr uint32_t kOwnerFlag = 0x80000000; // SCE_NP_MATCHING2_ROOMMEMBER_FLAG_ATTR_OWNER
|
||||
|
||||
struct Header {
|
||||
uint8_t type = 0;
|
||||
uint16_t command = 0;
|
||||
uint32_t size = 0;
|
||||
uint64_t packet_id = 0;
|
||||
};
|
||||
|
||||
inline bool parse_header(const std::string& buf, Header& h) {
|
||||
if (buf.size() < kHeaderSize) return false;
|
||||
const char* p = buf.data();
|
||||
h.type = static_cast<uint8_t>(p[0]);
|
||||
std::memcpy(&h.command, p + 1, 2);
|
||||
std::memcpy(&h.size, p + 3, 4);
|
||||
std::memcpy(&h.packet_id, p + 7, 8);
|
||||
return true;
|
||||
}
|
||||
|
||||
inline std::string build_packet(uint8_t type, uint16_t command, uint64_t packet_id, const std::string& payload) {
|
||||
std::string out(kHeaderSize, '\0');
|
||||
const uint32_t size = static_cast<uint32_t>(kHeaderSize + payload.size());
|
||||
out[0] = static_cast<char>(type);
|
||||
std::memcpy(&out[1], &command, 2);
|
||||
std::memcpy(&out[3], &size, 4);
|
||||
std::memcpy(&out[7], &packet_id, 8);
|
||||
return out + payload;
|
||||
}
|
||||
|
||||
inline std::string build_server_info() {
|
||||
std::string v(4, '\0');
|
||||
std::memcpy(v.data(), &kProtocolVersion, 4);
|
||||
return build_packet(ServerInfo, 0, 0, v);
|
||||
}
|
||||
|
||||
inline std::string build_reply(uint16_t command, uint64_t packet_id, uint8_t error, const std::string& payload) {
|
||||
return build_packet(Reply, command, packet_id, std::string(1, static_cast<char>(error)) + payload);
|
||||
}
|
||||
|
||||
// Every proto payload is framed as a u32-LE length + the message.
|
||||
inline std::string proto_blob(const std::string& msg) {
|
||||
std::string out(4, '\0');
|
||||
const uint32_t n = static_cast<uint32_t>(msg.size());
|
||||
std::memcpy(out.data(), &n, 4);
|
||||
return out + msg;
|
||||
}
|
||||
|
||||
inline bool parse_proto_blob(const std::string& payload, std::string& msg) {
|
||||
if (payload.size() < 4) return false;
|
||||
uint32_t n;
|
||||
std::memcpy(&n, payload.data(), 4);
|
||||
msg = payload.substr(4, n);
|
||||
return true;
|
||||
}
|
||||
|
||||
inline std::string build_notification(uint16_t type, const std::string& msg) {
|
||||
return build_packet(Notification, type, 0, proto_blob(msg));
|
||||
}
|
||||
|
||||
} // namespace shadnet
|
||||
@@ -1,197 +0,0 @@
|
||||
#include "util.hpp"
|
||||
|
||||
#include <chrono>
|
||||
#include <cstdarg>
|
||||
#include <cstdio>
|
||||
#include <ctime>
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
|
||||
#ifndef _WIN32
|
||||
#include <csignal>
|
||||
#endif
|
||||
|
||||
namespace bb {
|
||||
|
||||
namespace {
|
||||
std::mutex g_log_mu;
|
||||
std::function<void(const std::string&)> g_sink;
|
||||
std::unique_ptr<std::ofstream> g_file;
|
||||
} // namespace
|
||||
|
||||
std::string fmt(const char* format, ...) {
|
||||
va_list args;
|
||||
va_start(args, format);
|
||||
va_list copy;
|
||||
va_copy(copy, args);
|
||||
const int n = std::vsnprintf(nullptr, 0, format, copy);
|
||||
va_end(copy);
|
||||
std::string out(n > 0 ? static_cast<size_t>(n) : 0, '\0');
|
||||
if (n > 0) std::vsnprintf(out.data(), out.size() + 1, format, args);
|
||||
va_end(args);
|
||||
return out;
|
||||
}
|
||||
|
||||
void log(const std::string& msg) {
|
||||
std::time_t t = std::time(nullptr);
|
||||
std::tm tm{};
|
||||
localtime_s(&tm, &t);
|
||||
char stamp[16];
|
||||
std::strftime(stamp, sizeof(stamp), "%H:%M:%S", &tm);
|
||||
const std::string line = std::string(stamp) + " [bbbridge] " + msg;
|
||||
std::lock_guard<std::mutex> lk(g_log_mu);
|
||||
std::fwrite(line.data(), 1, line.size(), stdout);
|
||||
std::fputc('\n', stdout);
|
||||
std::fflush(stdout);
|
||||
if (g_file && *g_file) {
|
||||
*g_file << line << '\n';
|
||||
g_file->flush();
|
||||
}
|
||||
if (g_sink) g_sink(line);
|
||||
}
|
||||
|
||||
void set_log_sink(std::function<void(const std::string&)> sink) {
|
||||
std::lock_guard<std::mutex> lk(g_log_mu);
|
||||
g_sink = std::move(sink);
|
||||
}
|
||||
|
||||
void set_log_file(const std::wstring& path) {
|
||||
std::lock_guard<std::mutex> lk(g_log_mu);
|
||||
std::wstring prev = path;
|
||||
const size_t dot = prev.rfind(L".log");
|
||||
prev = (dot == std::wstring::npos ? prev : prev.substr(0, dot)) + L".prev.log";
|
||||
replace_file(path, prev);
|
||||
g_file = std::make_unique<std::ofstream>(fspath(path), std::ios::out | std::ios::trunc);
|
||||
}
|
||||
|
||||
double now() {
|
||||
using namespace std::chrono;
|
||||
return duration_cast<duration<double>>(steady_clock::now().time_since_epoch()).count();
|
||||
}
|
||||
|
||||
bool net_init() {
|
||||
static bool ok = [] {
|
||||
#ifdef _WIN32
|
||||
WSADATA wsa;
|
||||
return WSAStartup(MAKEWORD(2, 2), &wsa) == 0;
|
||||
#else
|
||||
// A send to a peer that already closed raises SIGPIPE on Linux, which ends the process;
|
||||
// Windows returns an error, which the bridge handles.
|
||||
std::signal(SIGPIPE, SIG_IGN);
|
||||
return true;
|
||||
#endif
|
||||
}();
|
||||
return ok;
|
||||
}
|
||||
|
||||
sockaddr_in to_sockaddr(const Endpoint& e) {
|
||||
sockaddr_in a{};
|
||||
a.sin_family = AF_INET;
|
||||
a.sin_port = htons(static_cast<u_short>(e.port));
|
||||
inet_pton(AF_INET, e.ip.c_str(), &a.sin_addr);
|
||||
return a;
|
||||
}
|
||||
|
||||
Endpoint from_sockaddr(const sockaddr_in& a) {
|
||||
char ip[INET_ADDRSTRLEN] = {};
|
||||
inet_ntop(AF_INET, &a.sin_addr, ip, sizeof(ip));
|
||||
return Endpoint{ip, ntohs(a.sin_port)};
|
||||
}
|
||||
|
||||
std::string detect_lan_addr() {
|
||||
// The interface the OS would route to the internet through (no packet is sent).
|
||||
SOCKET s = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
|
||||
if (s == INVALID_SOCKET) return "127.0.0.1";
|
||||
sockaddr_in target = to_sockaddr(Endpoint{"8.8.8.8", 80});
|
||||
std::string out = "127.0.0.1";
|
||||
if (connect(s, reinterpret_cast<sockaddr*>(&target), sizeof(target)) == 0) {
|
||||
sockaddr_in local{};
|
||||
socklen_t len = sizeof(local);
|
||||
if (getsockname(s, reinterpret_cast<sockaddr*>(&local), &len) == 0) out = from_sockaddr(local).ip;
|
||||
}
|
||||
closesocket(s);
|
||||
return out;
|
||||
}
|
||||
|
||||
std::string resolve_ipv4(const std::string& host) {
|
||||
addrinfo hints{};
|
||||
hints.ai_family = AF_INET;
|
||||
addrinfo* res = nullptr;
|
||||
if (getaddrinfo(host.c_str(), nullptr, &hints, &res) != 0 || !res) return "";
|
||||
const std::string ip = from_sockaddr(*reinterpret_cast<sockaddr_in*>(res->ai_addr)).ip;
|
||||
freeaddrinfo(res);
|
||||
return ip;
|
||||
}
|
||||
|
||||
#ifdef _WIN32
|
||||
std::string narrow(const std::wstring& w) {
|
||||
if (w.empty()) return {};
|
||||
const int n = WideCharToMultiByte(CP_UTF8, 0, w.data(), static_cast<int>(w.size()), nullptr, 0, nullptr, nullptr);
|
||||
std::string out(static_cast<size_t>(n), '\0');
|
||||
WideCharToMultiByte(CP_UTF8, 0, w.data(), static_cast<int>(w.size()), out.data(), n, nullptr, nullptr);
|
||||
return out;
|
||||
}
|
||||
|
||||
std::wstring widen(const std::string& s) {
|
||||
if (s.empty()) return {};
|
||||
const int n = MultiByteToWideChar(CP_UTF8, 0, s.data(), static_cast<int>(s.size()), nullptr, 0);
|
||||
std::wstring out(static_cast<size_t>(n), L'\0');
|
||||
MultiByteToWideChar(CP_UTF8, 0, s.data(), static_cast<int>(s.size()), out.data(), n);
|
||||
return out;
|
||||
}
|
||||
|
||||
std::filesystem::path fspath(const std::wstring& w) { return std::filesystem::path(w); }
|
||||
|
||||
bool replace_file(const std::wstring& from, const std::wstring& to) {
|
||||
return MoveFileExW(from.c_str(), to.c_str(), MOVEFILE_REPLACE_EXISTING) != 0;
|
||||
}
|
||||
#else
|
||||
// wchar_t holds a whole code point (UTF-32) on Linux.
|
||||
std::string narrow(const std::wstring& w) {
|
||||
std::string out;
|
||||
for (const wchar_t wc : w) {
|
||||
const uint32_t c = static_cast<uint32_t>(wc);
|
||||
if (c < 0x80) {
|
||||
out += static_cast<char>(c);
|
||||
} else if (c < 0x800) {
|
||||
out += static_cast<char>(0xC0 | (c >> 6));
|
||||
out += static_cast<char>(0x80 | (c & 0x3F));
|
||||
} else if (c < 0x10000) {
|
||||
out += static_cast<char>(0xE0 | (c >> 12));
|
||||
out += static_cast<char>(0x80 | ((c >> 6) & 0x3F));
|
||||
out += static_cast<char>(0x80 | (c & 0x3F));
|
||||
} else {
|
||||
out += static_cast<char>(0xF0 | (c >> 18));
|
||||
out += static_cast<char>(0x80 | ((c >> 12) & 0x3F));
|
||||
out += static_cast<char>(0x80 | ((c >> 6) & 0x3F));
|
||||
out += static_cast<char>(0x80 | (c & 0x3F));
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
std::wstring widen(const std::string& s) {
|
||||
std::wstring out;
|
||||
for (size_t i = 0; i < s.size();) {
|
||||
const unsigned char b = static_cast<unsigned char>(s[i]);
|
||||
const int extra = b < 0x80 ? 0 : b < 0xE0 ? 1 : b < 0xF0 ? 2 : 3;
|
||||
uint32_t c = extra == 0 ? b : extra == 1 ? (b & 0x1F) : extra == 2 ? (b & 0x0F) : (b & 0x07);
|
||||
size_t j = 1;
|
||||
for (; j <= static_cast<size_t>(extra) && i + j < s.size(); ++j)
|
||||
c = (c << 6) | (static_cast<unsigned char>(s[i + j]) & 0x3F);
|
||||
out += static_cast<wchar_t>(c);
|
||||
i += j;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
std::filesystem::path fspath(const std::wstring& w) { return std::filesystem::path(narrow(w)); }
|
||||
|
||||
bool replace_file(const std::wstring& from, const std::wstring& to) {
|
||||
return std::rename(narrow(from).c_str(), narrow(to).c_str()) == 0;
|
||||
}
|
||||
#endif
|
||||
|
||||
} // namespace bb
|
||||
@@ -1,126 +0,0 @@
|
||||
// Shared helpers: logging, endpoints, Winsock, tolerant JSON field access.
|
||||
#pragma once
|
||||
|
||||
#ifdef _WIN32
|
||||
#ifndef WIN32_LEAN_AND_MEAN
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
#endif
|
||||
#ifndef NOMINMAX
|
||||
#define NOMINMAX
|
||||
#endif
|
||||
#include <winsock2.h>
|
||||
#include <ws2tcpip.h>
|
||||
#include <windows.h>
|
||||
#else
|
||||
// Linux: BSD sockets under Winsock's names, so the bridge's code is the same on both.
|
||||
#include <arpa/inet.h>
|
||||
#include <netdb.h>
|
||||
#include <netinet/in.h>
|
||||
#include <netinet/tcp.h>
|
||||
#include <sys/select.h>
|
||||
#include <sys/socket.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <cerrno>
|
||||
#include <chrono>
|
||||
#include <ctime>
|
||||
#include <thread>
|
||||
|
||||
using SOCKET = int;
|
||||
constexpr SOCKET INVALID_SOCKET = -1;
|
||||
constexpr int SD_BOTH = SHUT_RDWR;
|
||||
// Closing a socket does not wake a thread blocked in accept() or recvfrom() on it on Linux
|
||||
// (Windows does): shutdown() first, which does, also for listening and unconnected UDP sockets.
|
||||
inline int closesocket(SOCKET s) {
|
||||
::shutdown(s, SHUT_RDWR);
|
||||
return ::close(s);
|
||||
}
|
||||
inline int WSAGetLastError() { return errno; }
|
||||
inline void Sleep(unsigned ms) { std::this_thread::sleep_for(std::chrono::milliseconds(ms)); }
|
||||
inline void localtime_s(std::tm* out, const std::time_t* t) { localtime_r(t, out); }
|
||||
#endif
|
||||
|
||||
#include <cstdint>
|
||||
#include <filesystem>
|
||||
#include <functional>
|
||||
#include <string>
|
||||
|
||||
#include "nlohmann/json.hpp"
|
||||
|
||||
namespace bb {
|
||||
|
||||
using json = nlohmann::json;
|
||||
|
||||
constexpr const char* kVersion = "0.3.2";
|
||||
|
||||
// ---- logging: "HH:MM:SS [bbbridge] msg" to stdout, the log file and the app ----
|
||||
void log(const std::string& msg);
|
||||
std::string fmt(const char* format, ...);
|
||||
void set_log_sink(std::function<void(const std::string&)> sink); // the app's log panel
|
||||
void set_log_file(const std::wstring& path); // keeps the previous as *.prev.log
|
||||
|
||||
double now(); // seconds, monotonic
|
||||
|
||||
// ---- endpoints ----
|
||||
struct Endpoint {
|
||||
std::string ip;
|
||||
int port = 0;
|
||||
bool valid() const { return !ip.empty() && port > 0 && port <= 65535; }
|
||||
bool operator==(const Endpoint& o) const { return ip == o.ip && port == o.port; }
|
||||
bool operator!=(const Endpoint& o) const { return !(*this == o); }
|
||||
bool operator<(const Endpoint& o) const { return ip != o.ip ? ip < o.ip : port < o.port; }
|
||||
std::string str() const { return ip + ":" + std::to_string(port); }
|
||||
};
|
||||
|
||||
bool net_init();
|
||||
sockaddr_in to_sockaddr(const Endpoint& e);
|
||||
Endpoint from_sockaddr(const sockaddr_in& a);
|
||||
std::string detect_lan_addr();
|
||||
std::string resolve_ipv4(const std::string& host); // "" when it fails
|
||||
|
||||
// ---- JSON as Python's dict.get(): tolerant of missing keys, null, numbers as strings ----
|
||||
// Templates, so they serve json and ordered_json (settings files keep their key order).
|
||||
template <typename J>
|
||||
bool jhas(const J& j, const char* key) { // present and not null
|
||||
if (!j.is_object()) return false;
|
||||
auto it = j.find(key);
|
||||
return it != j.end() && !it->is_null();
|
||||
}
|
||||
|
||||
template <typename J>
|
||||
int64_t jint(const J& j, const char* key, int64_t def = 0) {
|
||||
if (!jhas(j, key)) return def;
|
||||
const auto& v = j.at(key);
|
||||
try {
|
||||
if (v.is_number_integer()) return v.template get<int64_t>();
|
||||
if (v.is_number_unsigned()) return static_cast<int64_t>(v.template get<uint64_t>());
|
||||
if (v.is_number_float()) return static_cast<int64_t>(v.template get<double>());
|
||||
if (v.is_boolean()) return v.template get<bool>() ? 1 : 0;
|
||||
if (v.is_string()) {
|
||||
const std::string s = v.template get<std::string>();
|
||||
return s.empty() ? def : std::stoll(s);
|
||||
}
|
||||
} catch (...) {
|
||||
}
|
||||
return def;
|
||||
}
|
||||
|
||||
template <typename J>
|
||||
std::string jstr(const J& j, const char* key, const std::string& def = "") {
|
||||
if (!jhas(j, key)) return def;
|
||||
const auto& v = j.at(key);
|
||||
if (v.is_string()) return v.template get<std::string>();
|
||||
if (v.is_number_integer()) return std::to_string(v.template get<int64_t>());
|
||||
if (v.is_number_unsigned()) return std::to_string(v.template get<uint64_t>());
|
||||
return v.dump();
|
||||
}
|
||||
|
||||
std::string narrow(const std::wstring& w);
|
||||
std::wstring widen(const std::string& s);
|
||||
// A file name kept as UTF-16/32 text (std::wstring) as a path to open: on Linux file names are bytes,
|
||||
// UTF-8 here.
|
||||
std::filesystem::path fspath(const std::wstring& w);
|
||||
// Replaces `to` with `from` in one step (MoveFileExW with MOVEFILE_REPLACE_EXISTING, rename()).
|
||||
bool replace_file(const std::wstring& from, const std::wstring& to);
|
||||
|
||||
} // namespace bb
|
||||
@@ -1,147 +0,0 @@
|
||||
"""Minimal protobuf3 wire-format codec (no dependencies).
|
||||
|
||||
Only what shadnet_proto.py needs: varint and length-delimited fields.
|
||||
proto3 semantics: fields at their default value are omitted on encode;
|
||||
decode returns only the fields present on the wire.
|
||||
|
||||
Schema entry: field_number -> (name, kind)
|
||||
kind: "varint" (int), "bool", "string", "bytes", or ("message", schema).
|
||||
A wire field may repeat; decoded values are then lists (never for our
|
||||
scalars in practice, but decode keeps the last occurrence for scalars and
|
||||
extends a list for messages, which is what the shadnet messages use).
|
||||
"""
|
||||
|
||||
import struct
|
||||
|
||||
|
||||
def encode_varint(value):
|
||||
if value < 0:
|
||||
# int32/int64 negatives are encoded as 10-byte two's complement
|
||||
value &= (1 << 64) - 1
|
||||
out = bytearray()
|
||||
while True:
|
||||
b = value & 0x7F
|
||||
value >>= 7
|
||||
if value:
|
||||
out.append(b | 0x80)
|
||||
else:
|
||||
out.append(b)
|
||||
return bytes(out)
|
||||
|
||||
|
||||
def decode_varint(buf, pos):
|
||||
result = 0
|
||||
shift = 0
|
||||
while True:
|
||||
if pos >= len(buf):
|
||||
raise ValueError("varint runs past end of buffer")
|
||||
b = buf[pos]
|
||||
pos += 1
|
||||
result |= (b & 0x7F) << shift
|
||||
if not (b & 0x80):
|
||||
return result, pos
|
||||
shift += 7
|
||||
if shift > 70:
|
||||
raise ValueError("varint too long")
|
||||
|
||||
|
||||
def encode_tag(field_number, wire_type):
|
||||
return encode_varint((field_number << 3) | wire_type)
|
||||
|
||||
|
||||
def encode_field(field_number, kind, value):
|
||||
"""Encode one field occurrence. Returns bytes."""
|
||||
if kind == "varint":
|
||||
if value in (0, None):
|
||||
return b""
|
||||
return encode_tag(field_number, 0) + encode_varint(int(value))
|
||||
if kind == "bool":
|
||||
if not value:
|
||||
return b""
|
||||
return encode_tag(field_number, 0) + encode_varint(1)
|
||||
if kind in ("string", "bytes"):
|
||||
if not value:
|
||||
return b""
|
||||
data = value.encode("utf-8") if kind == "string" else bytes(value)
|
||||
return encode_tag(field_number, 2) + encode_varint(len(data)) + data
|
||||
if isinstance(kind, tuple) and kind[0] == "message":
|
||||
if value is None:
|
||||
return b""
|
||||
data = encode_message(kind[1], value)
|
||||
return encode_tag(field_number, 2) + encode_varint(len(data)) + data
|
||||
raise ValueError("unknown kind %r" % (kind,))
|
||||
|
||||
|
||||
def encode_message(schema, values):
|
||||
"""values: {field_number: value}. Repeated messages take a list."""
|
||||
out = bytearray()
|
||||
for field_number in sorted(values):
|
||||
name, kind = schema[field_number]
|
||||
value = values[field_number]
|
||||
if isinstance(kind, tuple) and kind[0] == "repeated":
|
||||
_, inner = kind
|
||||
for item in (value or []):
|
||||
out += encode_field(field_number, inner, item)
|
||||
else:
|
||||
out += encode_field(field_number, kind, value)
|
||||
return bytes(out)
|
||||
|
||||
|
||||
def decode_raw_fields(buf):
|
||||
"""Yield (field_number, wire_type, value) for every field on the wire.
|
||||
Length-delimited values are bytes; varints are ints."""
|
||||
pos = 0
|
||||
n = len(buf)
|
||||
while pos < n:
|
||||
tag, pos = decode_varint(buf, pos)
|
||||
field_number = tag >> 3
|
||||
wire_type = tag & 7
|
||||
if wire_type == 0:
|
||||
value, pos = decode_varint(buf, pos)
|
||||
elif wire_type == 2:
|
||||
length, pos = decode_varint(buf, pos)
|
||||
value = buf[pos:pos + length]
|
||||
if len(value) != length:
|
||||
raise ValueError("length-delimited field runs past end")
|
||||
pos += length
|
||||
elif wire_type == 5: # fixed32
|
||||
value = struct.unpack_from("<I", buf, pos)[0]
|
||||
pos += 4
|
||||
elif wire_type == 1: # fixed64
|
||||
value = struct.unpack_from("<Q", buf, pos)[0]
|
||||
pos += 8
|
||||
else:
|
||||
raise ValueError("unsupported wire type %d" % wire_type)
|
||||
yield field_number, wire_type, value
|
||||
|
||||
|
||||
def decode_message(schema, buf):
|
||||
"""Decode into {name: value}. Scalars keep the last occurrence; repeated
|
||||
messages accumulate into lists; nested messages are decoded recursively.
|
||||
Unknown fields are kept under name "<field:N>" as raw bytes/int."""
|
||||
out = {}
|
||||
for field_number, wire_type, value in decode_raw_fields(buf):
|
||||
entry = schema.get(field_number)
|
||||
if entry is None:
|
||||
name = "<field:%d>" % field_number
|
||||
out[name] = value
|
||||
continue
|
||||
name, kind = entry
|
||||
if isinstance(kind, tuple) and kind[0] == "repeated":
|
||||
_, inner = kind
|
||||
if isinstance(inner, tuple):
|
||||
item = decode_message(inner[1], value)
|
||||
elif inner == "string":
|
||||
item = value.decode("utf-8", "replace")
|
||||
else:
|
||||
item = value
|
||||
out.setdefault(name, []).append(item)
|
||||
elif isinstance(kind, tuple) and kind[0] == "message":
|
||||
out[name] = decode_message(kind[1], value)
|
||||
elif kind == "string":
|
||||
out[name] = value.decode("utf-8", "replace") if isinstance(value, (bytes, bytearray)) else value
|
||||
elif kind == "bool":
|
||||
out[name] = bool(value)
|
||||
else: # varint
|
||||
out[name] = value
|
||||
return out
|
||||
@@ -1,395 +0,0 @@
|
||||
"""shadNet wire protocol: 15-byte packet framing + message schemas.
|
||||
|
||||
Sources (read from the actual code, not the stale docs):
|
||||
- Packet layout: shadnet-p2p-main/documentation/protocol.md
|
||||
- Command IDs: shadnet-p2p-main/src/protocol.h (== the port's client.h;
|
||||
matching commands live at 100-115, NOT 12-23)
|
||||
- Message fields: the port's gpu/shadps4/shadnet/shadnet.proto (subset used
|
||||
by the client) cross-checked against shadnet-p2p's fuller proto.
|
||||
"""
|
||||
|
||||
import struct
|
||||
|
||||
import proto3
|
||||
|
||||
HEADER_SIZE = 15
|
||||
MAX_PACKET_SIZE = 0x800000
|
||||
PROTOCOL_VERSION = 1
|
||||
|
||||
PACKET_REQUEST = 0
|
||||
PACKET_REPLY = 1
|
||||
PACKET_NOTIFICATION = 2
|
||||
PACKET_SERVER_INFO = 3
|
||||
|
||||
|
||||
class Command:
|
||||
Login = 0
|
||||
Terminate = 1
|
||||
Create = 2
|
||||
Delete = 3
|
||||
AddFriend = 8
|
||||
RemoveFriend = 9
|
||||
AddBlock = 10
|
||||
RemoveBlock = 11
|
||||
GetServerFeatures = 12
|
||||
SetClientVersion = 13
|
||||
GetToken = 39
|
||||
SetAppearOffline = 40
|
||||
ContextStart = 100
|
||||
CreateRoom = 101
|
||||
JoinRoom = 102
|
||||
LeaveRoom = 103
|
||||
SearchRoom = 104
|
||||
RequestSignalingInfos = 105
|
||||
ContextStop = 106
|
||||
SetUserInfo = 107
|
||||
SetRoomDataInternal = 108
|
||||
SetRoomDataExternal = 109
|
||||
KickoutRoomMember = 110
|
||||
GetWorldInfoList = 111
|
||||
GetRoomDataExternalList = 112
|
||||
GetUserInfoList = 113
|
||||
GetRoomMemberDataExternalList = 114
|
||||
SendRoomMessage = 115
|
||||
GetBoardInfos = 30
|
||||
RecordScore = 31
|
||||
RecordScoreData = 32
|
||||
GetScoreData = 33
|
||||
GetScoreRange = 34
|
||||
GetScoreFriends = 35
|
||||
GetScoreNpid = 36
|
||||
|
||||
|
||||
class Error:
|
||||
NoError = 0
|
||||
Malformed = 1
|
||||
Invalid = 2
|
||||
InvalidInput = 3
|
||||
TooSoon = 4
|
||||
LoginError = 5
|
||||
LoginAlreadyLoggedIn = 6
|
||||
LoginInvalidPassword = 8
|
||||
LoginInvalidToken = 9
|
||||
RoomMissing = 14
|
||||
RoomAlreadyJoined = 15
|
||||
RoomFull = 16
|
||||
Unauthorized = 23
|
||||
NotFound = 26
|
||||
Unsupported = 33
|
||||
|
||||
|
||||
class Notify:
|
||||
FriendQuery = 5
|
||||
FriendNew = 6
|
||||
FriendLost = 7
|
||||
FriendStatus = 8
|
||||
RoomEvent = 10
|
||||
RoomMessage = 11
|
||||
|
||||
|
||||
# Orbis room event ids (bbhost np_matching2.cpp + shadnet-p2p matching.md)
|
||||
class RoomEvent:
|
||||
MemberJoined = 0x1101
|
||||
MemberLeft = 0x1102
|
||||
KickedOut = 0x1103
|
||||
RoomDestroyed = 0x1104
|
||||
RoomDataUpdatedInternal = 0x1106
|
||||
|
||||
|
||||
class EventCause:
|
||||
Leave = 1
|
||||
Kickout = 2
|
||||
ServerOperation = 4
|
||||
MemberDisappeared = 5
|
||||
|
||||
|
||||
# ---- protobuf message schemas (field number -> (name, kind)) ----------------
|
||||
|
||||
MATCHING_BIN_ATTR = {
|
||||
1: ("attr_id", "varint"),
|
||||
2: ("data", "bytes"),
|
||||
3: ("update_date", "varint"),
|
||||
4: ("update_member_id", "varint"),
|
||||
}
|
||||
|
||||
MATCHING_INT_ATTR = {
|
||||
1: ("attr_id", "varint"),
|
||||
2: ("attr_value", "varint"),
|
||||
}
|
||||
|
||||
MATCHING_ROOM_GROUP = {
|
||||
1: ("group_id", "varint"),
|
||||
2: ("has_passwd", "bool"),
|
||||
3: ("has_label", "bool"),
|
||||
4: ("label", "bytes"),
|
||||
5: ("slot_count", "varint"),
|
||||
6: ("num_members", "varint"),
|
||||
}
|
||||
|
||||
MATCHING_ROOM_MEMBER_DATA = {
|
||||
1: ("npid", "string"),
|
||||
2: ("member_id", "varint"),
|
||||
3: ("team_id", "varint"),
|
||||
4: ("is_owner", "bool"),
|
||||
5: ("group", ("message", MATCHING_ROOM_GROUP)),
|
||||
6: ("bin_attrs_internal", ("repeated", ("message", MATCHING_BIN_ATTR))),
|
||||
7: ("join_date", "varint"),
|
||||
8: ("nat_type", "varint"),
|
||||
9: ("flag_attr", "varint"),
|
||||
10: ("addr", "string"),
|
||||
11: ("port", "varint"),
|
||||
12: ("group_id", "varint"),
|
||||
13: ("account_id", "varint"),
|
||||
14: ("platform", "varint"),
|
||||
}
|
||||
|
||||
MATCHING_ROOM_DATA_INTERNAL = {
|
||||
1: ("public_slots", "varint"),
|
||||
2: ("private_slots", "varint"),
|
||||
3: ("open_public_slots", "varint"),
|
||||
4: ("open_private_slots", "varint"),
|
||||
5: ("max_slot", "varint"),
|
||||
6: ("server_id", "varint"),
|
||||
7: ("world_id", "varint"),
|
||||
8: ("lobby_id", "varint"),
|
||||
9: ("room_id", "varint"),
|
||||
10: ("passwd_slot_mask", "varint"),
|
||||
11: ("joined_slot_mask", "varint"),
|
||||
12: ("groups", ("repeated", ("message", MATCHING_ROOM_GROUP))),
|
||||
13: ("flags", "varint"),
|
||||
14: ("bin_attrs_internal", ("repeated", ("message", MATCHING_BIN_ATTR))),
|
||||
}
|
||||
|
||||
CREATE_JOIN_ROOM_RESPONSE = {
|
||||
1: ("room_data", ("message", MATCHING_ROOM_DATA_INTERNAL)),
|
||||
2: ("members", ("repeated", ("message", MATCHING_ROOM_MEMBER_DATA))),
|
||||
3: ("me_member_id", "varint"),
|
||||
4: ("owner_member_id", "varint"),
|
||||
}
|
||||
|
||||
MATCHING_WORLD = {
|
||||
1: ("world_id", "varint"),
|
||||
2: ("lobbies_num", "varint"),
|
||||
3: ("max_lobby_members", "varint"),
|
||||
4: ("lobby_members_num", "varint"),
|
||||
5: ("rooms_num", "varint"),
|
||||
6: ("room_members_num", "varint"),
|
||||
}
|
||||
|
||||
LOGIN_REQUEST = {
|
||||
1: ("npid", "string"),
|
||||
2: ("password", "string"),
|
||||
3: ("token", "string"),
|
||||
4: ("title_id", "string"),
|
||||
5: ("title_name", "string"),
|
||||
6: ("appear_offline", "bool"),
|
||||
}
|
||||
|
||||
LOGIN_REPLY = {
|
||||
1: ("avatar_url", "string"),
|
||||
2: ("user_id", "varint"),
|
||||
3: ("friends", ("repeated", ("message", {
|
||||
1: ("npid", "string"),
|
||||
2: ("online", "bool"),
|
||||
3: ("presence", "bytes"),
|
||||
}))),
|
||||
4: ("friend_requests_sent", ("repeated", "string")),
|
||||
5: ("friend_requests_received", ("repeated", "string")),
|
||||
6: ("blocked", ("repeated", "string")),
|
||||
}
|
||||
|
||||
SERVER_FEATURES_REPLY = {1: ("matching2_enabled", "bool")}
|
||||
|
||||
GET_TOKEN_REPLY = {
|
||||
1: ("token", "string"),
|
||||
2: ("user_id", "varint"),
|
||||
3: ("npid", "string"),
|
||||
}
|
||||
|
||||
CONTEXT_START_REQUEST = {1: ("ctx_id", "varint")}
|
||||
CONTEXT_STOP_REQUEST = {1: ("ctx_id", "varint")}
|
||||
|
||||
CREATE_ROOM_REQUEST = {
|
||||
1: ("req_id", "varint"),
|
||||
2: ("max_slots", "varint"),
|
||||
3: ("team_id", "varint"),
|
||||
4: ("world_id", "varint"),
|
||||
5: ("lobby_id", "varint"),
|
||||
6: ("flags", "varint"),
|
||||
7: ("group_config_count", "varint"),
|
||||
8: ("allowed_user_count", "varint"),
|
||||
9: ("blocked_user_count", "varint"),
|
||||
10: ("internal_bin_attr_count", "varint"),
|
||||
11: ("external_search_int_attrs", ("repeated", ("message", MATCHING_INT_ATTR))),
|
||||
12: ("external_search_bin_attrs", ("repeated", ("message", MATCHING_BIN_ATTR))),
|
||||
13: ("external_bin_attrs", ("repeated", ("message", MATCHING_BIN_ATTR))),
|
||||
14: ("member_bin_attrs", ("repeated", ("message", MATCHING_BIN_ATTR))),
|
||||
15: ("join_group_label_present", "bool"),
|
||||
16: ("room_password_present", "bool"),
|
||||
17: ("sig_type", "varint"),
|
||||
18: ("sig_flag", "varint"),
|
||||
19: ("sig_main_member", "varint"),
|
||||
20: ("internal_bin_attrs", ("repeated", ("message", MATCHING_BIN_ATTR))),
|
||||
21: ("user_id_kind", "varint"),
|
||||
22: ("allowed_online_ids", ("repeated", "string")),
|
||||
23: ("blocked_online_ids", ("repeated", "string")),
|
||||
24: ("allowed_account_ids", ("repeated", "varint")),
|
||||
25: ("blocked_account_ids", ("repeated", "varint")),
|
||||
26: ("group_configs", ("repeated", ("message", {
|
||||
1: ("slot_count", "varint"),
|
||||
2: ("has_label", "bool"),
|
||||
3: ("label", "bytes"),
|
||||
4: ("has_passwd", "bool"),
|
||||
}))),
|
||||
27: ("room_password", "bytes"),
|
||||
28: ("has_passwd_slot_mask", "bool"),
|
||||
29: ("passwd_slot_mask", "varint"),
|
||||
30: ("join_group_label", "bytes"),
|
||||
}
|
||||
|
||||
CREATE_ROOM_REPLY = {
|
||||
1: ("room_id", "varint"),
|
||||
2: ("server_id", "varint"),
|
||||
3: ("world_id", "varint"),
|
||||
4: ("lobby_id", "varint"),
|
||||
5: ("member_id", "varint"),
|
||||
6: ("max_slots", "varint"),
|
||||
7: ("flags", "varint"),
|
||||
8: ("cur_members", "varint"),
|
||||
9: ("details", ("message", CREATE_JOIN_ROOM_RESPONSE)),
|
||||
}
|
||||
|
||||
JOIN_ROOM_REQUEST = {
|
||||
1: ("room_id", "varint"),
|
||||
2: ("req_id", "varint"),
|
||||
3: ("team_id", "varint"),
|
||||
4: ("join_flags", "varint"),
|
||||
5: ("blocked_user_count", "varint"),
|
||||
6: ("member_bin_attrs", ("repeated", ("message", MATCHING_BIN_ATTR))),
|
||||
7: ("room_password_present", "bool"),
|
||||
8: ("join_group_label_present", "bool"),
|
||||
9: ("user_id_kind", "varint"),
|
||||
10: ("blocked_online_ids", ("repeated", "string")),
|
||||
11: ("blocked_account_ids", ("repeated", "varint")),
|
||||
}
|
||||
|
||||
JOIN_ROOM_REPLY = {
|
||||
1: ("room_id", "varint"),
|
||||
2: ("member_id", "varint"),
|
||||
3: ("max_slots", "varint"),
|
||||
4: ("flags", "varint"),
|
||||
5: ("cur_members", "varint"),
|
||||
6: ("details", ("message", CREATE_JOIN_ROOM_RESPONSE)),
|
||||
}
|
||||
|
||||
LEAVE_ROOM_REQUEST = {1: ("room_id", "varint"), 2: ("req_id", "varint")}
|
||||
LEAVE_ROOM_REPLY = {1: ("room_id", "varint")}
|
||||
|
||||
REQUEST_SIGNALING_INFOS_REQUEST = {1: ("target_npid", "string")}
|
||||
REQUEST_SIGNALING_INFOS_REPLY = {
|
||||
1: ("target_npid", "string"),
|
||||
2: ("target_ip", "string"),
|
||||
3: ("target_port", "varint"),
|
||||
4: ("target_member_id", "varint"),
|
||||
}
|
||||
|
||||
SET_ROOM_DATA_INTERNAL_REQUEST = {
|
||||
1: ("req_id", "varint"),
|
||||
2: ("room_id", "varint"),
|
||||
3: ("flag_filter", "varint"),
|
||||
4: ("flag_attr", "varint"),
|
||||
5: ("bin_attrs", ("repeated", ("message", MATCHING_BIN_ATTR))),
|
||||
6: ("has_passwd_mask", "bool"),
|
||||
7: ("passwd_slot_mask", "varint"),
|
||||
}
|
||||
SET_ROOM_DATA_INTERNAL_REPLY = {1: ("room_id", "varint")}
|
||||
|
||||
SET_ROOM_DATA_EXTERNAL_REQUEST = {
|
||||
1: ("req_id", "varint"),
|
||||
2: ("room_id", "varint"),
|
||||
3: ("search_int_attrs", ("repeated", ("message", MATCHING_INT_ATTR))),
|
||||
4: ("search_bin_attrs", ("repeated", ("message", MATCHING_BIN_ATTR))),
|
||||
5: ("ext_bin_attrs", ("repeated", ("message", MATCHING_BIN_ATTR))),
|
||||
}
|
||||
SET_ROOM_DATA_EXTERNAL_REPLY = {1: ("room_id", "varint")}
|
||||
|
||||
KICKOUT_ROOM_MEMBER_REQUEST = {
|
||||
1: ("room_id", "varint"),
|
||||
2: ("req_id", "varint"),
|
||||
3: ("target_member_id", "varint"),
|
||||
4: ("block_kick_flag", "varint"),
|
||||
}
|
||||
KICKOUT_ROOM_MEMBER_REPLY = {1: ("room_id", "varint")}
|
||||
|
||||
SEARCH_ROOM_REPLY = {
|
||||
1: ("rooms", ("repeated", ("message", {
|
||||
1: ("max_slot", "varint"),
|
||||
2: ("cur_members", "varint"),
|
||||
3: ("flags", "varint"),
|
||||
4: ("server_id", "varint"),
|
||||
5: ("world_id", "varint"),
|
||||
6: ("lobby_id", "varint"),
|
||||
7: ("room_id", "varint"),
|
||||
}))),
|
||||
2: ("range_start", "varint"),
|
||||
3: ("range_total", "varint"),
|
||||
4: ("range_result", "varint"),
|
||||
}
|
||||
|
||||
GET_WORLD_INFO_LIST_REQUEST = {1: ("server_id", "varint")}
|
||||
GET_WORLD_INFO_LIST_REPLY = {1: ("worlds", ("repeated", ("message", MATCHING_WORLD)))}
|
||||
|
||||
FRIEND_COMMAND_REQUEST = {1: ("npid", "string")}
|
||||
|
||||
NOTIFY_ROOM_EVENT = {
|
||||
1: ("ctx_id", "varint"),
|
||||
2: ("room_id", "varint"),
|
||||
3: ("event", "varint"),
|
||||
4: ("event_cause", "varint"),
|
||||
5: ("error_code", "varint"),
|
||||
6: ("member", ("message", MATCHING_ROOM_MEMBER_DATA)),
|
||||
7: ("bin_attrs", ("repeated", ("message", MATCHING_BIN_ATTR))),
|
||||
8: ("flags", "varint"),
|
||||
9: ("has_passwd_mask", "bool"),
|
||||
10: ("passwd_slot_mask", "varint"),
|
||||
}
|
||||
|
||||
SET_APPEAR_OFFLINE_REQUEST = {1: ("appear_offline", "bool")}
|
||||
|
||||
|
||||
# ---- packet framing ---------------------------------------------------------
|
||||
|
||||
def build_packet(packet_type, command, packet_id, payload=b""):
|
||||
size = HEADER_SIZE + len(payload)
|
||||
return struct.pack("<BHIQ", packet_type, command, size, packet_id) + payload
|
||||
|
||||
|
||||
def build_server_info():
|
||||
return build_packet(PACKET_SERVER_INFO, 0, 0, struct.pack("<I", PROTOCOL_VERSION))
|
||||
|
||||
|
||||
def build_reply(command, packet_id, error, payload=b""):
|
||||
return build_packet(PACKET_REPLY, command, packet_id, bytes([error]) + payload)
|
||||
|
||||
|
||||
def build_notification(notification_type, payload):
|
||||
return build_packet(PACKET_NOTIFICATION, notification_type, 0, payload)
|
||||
|
||||
|
||||
def proto_blob(message_bytes):
|
||||
"""The u32-LE length prefix + message framing every proto payload uses."""
|
||||
return struct.pack("<I", len(message_bytes)) + message_bytes
|
||||
|
||||
|
||||
def parse_proto_blob(payload):
|
||||
if len(payload) < 4:
|
||||
return None
|
||||
(size,) = struct.unpack_from("<I", payload, 0)
|
||||
return payload[4:4 + size]
|
||||
|
||||
|
||||
def parse_header(buf):
|
||||
if len(buf) < HEADER_SIZE:
|
||||
return None
|
||||
packet_type, command, size, packet_id = struct.unpack_from("<BHIQ", buf, 0)
|
||||
return packet_type, command, size, packet_id
|
||||
@@ -1,778 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""End-to-end tests for the C++ bridge (bin/bbbridge-cli.exe).
|
||||
|
||||
The same checks the Python bridge passed (its test_offline.py and test_p2p.py),
|
||||
run against the C++ build over real sockets: a fake bbhost server, a fake
|
||||
STUN/relay server, a shadNet client, fake port and peer UDP sockets. The
|
||||
protobuf / shadNet codecs used to read the bridge's answers are the Python
|
||||
bridge's own (proto3.py, shadnet_proto.py) - an independent implementation.
|
||||
|
||||
python tests/e2e/test_e2e.py [path\\to\\bbbridge-cli.exe] (Linux: the path to bbbridge-cli)
|
||||
"""
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import socket
|
||||
import struct
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import threading
|
||||
import time
|
||||
import urllib.request
|
||||
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
||||
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
sys.path.insert(0, HERE)
|
||||
import proto3 # noqa: E402
|
||||
import shadnet_proto as sp # noqa: E402
|
||||
|
||||
EXE = sys.argv[1] if len(sys.argv) > 1 else os.path.join(HERE, "..", "..", "bin", "bbbridge-cli.exe")
|
||||
HOST = "127.0.0.1"
|
||||
NPID, HOST_NPID = "tester01", "hostplayer"
|
||||
TIMEOUT = 5.0
|
||||
PASSED, FAILED = [], []
|
||||
|
||||
|
||||
def check(name, cond, detail=""):
|
||||
(PASSED if cond else FAILED).append((name, detail))
|
||||
print(" [%s] %s%s" % ("PASS" if cond else "FAIL", name, "" if cond else " " + str(detail)))
|
||||
|
||||
|
||||
def account_id_for(npid):
|
||||
d = hashlib.sha256(("bbbridge:" + npid).encode()).digest()
|
||||
return (1 << 52) | (int.from_bytes(d[:8], "big") & ((1 << 48) - 1))
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- fake bbhost
|
||||
|
||||
class FakeBBHost(BaseHTTPRequestHandler):
|
||||
protocol_version = "HTTP/1.1"
|
||||
|
||||
def log_message(self, *a):
|
||||
pass
|
||||
|
||||
def _json(self, obj, status=200):
|
||||
body = json.dumps(obj).encode()
|
||||
self.send_response(status)
|
||||
self.send_header("Content-Type", "application/json")
|
||||
self.send_header("Content-Length", str(len(body)))
|
||||
self.end_headers()
|
||||
self.wfile.write(body)
|
||||
|
||||
def do_POST(self):
|
||||
n = int(self.headers.get("Content-Length", 0) or 0)
|
||||
try:
|
||||
body = json.loads(self.rfile.read(n).decode()) if n else {}
|
||||
except ValueError:
|
||||
body = {}
|
||||
path = self.path.split("?")[0]
|
||||
st = self.server.state
|
||||
with st["lock"]:
|
||||
st["calls"].append(path)
|
||||
emit = st["emit_event"]
|
||||
event = st.get("event")
|
||||
r = {"ResKind": 0}
|
||||
if path == "/auth/account/create":
|
||||
r.update(Token="tok-123", OnlineId=body.get("Name"), RecoveryCode="RC-42")
|
||||
elif path == "/auth/recover":
|
||||
r.update(Token="tok-456", OnlineId=body.get("Name"), RecoveryCode="RC-43")
|
||||
elif path == "/mp/matching2/create_room":
|
||||
r.update(RoomId=777, SessionId="sess-A", MemberId=1, OwnerMemberId=1, MaxMembers=5, Members=[])
|
||||
elif path == "/mp/matching2/join_room":
|
||||
r.update(SessionId="sess-B", MemberId=2, OwnerMemberId=1, MaxMembers=5, Members=[
|
||||
{"MemberId": 1, "OnlineId": HOST_NPID, "LocalAddr": "10.0.0.9", "LocalPort": 5551,
|
||||
"Addr": "9.9.9.8", "Port": 5552, "MappedAddr": "9.9.9.8", "MappedPort": 5552}])
|
||||
elif path == "/mp/matching2/heartbeat":
|
||||
r.update(InRoom=1)
|
||||
elif path == "/np/signaling/resolve":
|
||||
if self.headers.get("X-BB-Token") == "tok-expired":
|
||||
self._json({"ResKind": 1, "Message": "bad or expired token"}, 401)
|
||||
return
|
||||
r.update(Addr="9.9.9.9", Port=5555)
|
||||
elif path == "/auth/device/start":
|
||||
st["device_polls"] = 0
|
||||
r.update(DeviceCode="dev-1", UserCode="ABCD-EFGH", VerificationUri="http://example.test/link",
|
||||
Interval=1, ExpiresIn=60, Device=body.get("DeviceName"))
|
||||
elif path == "/auth/device/poll":
|
||||
st["device_polls"] = st.get("device_polls", 0) + 1
|
||||
if body.get("DeviceCode") == "dev-1" and st["device_polls"] >= 2:
|
||||
r.update(Status="approved", Token="tok-discord", OnlineId="discorduser")
|
||||
else:
|
||||
r.update(Status="pending")
|
||||
elif path == "/auth/web_code":
|
||||
if body.get("Token") != "tok-discord":
|
||||
self._json({"ResKind": 1, "Message": "unknown token"})
|
||||
return
|
||||
r.update(Code="WXYZ-1234")
|
||||
elif path == "/np/events/poll":
|
||||
r.update(NextCursor=body.get("Cursor", 0) + 1, HasEvent=bool(emit))
|
||||
if emit:
|
||||
r.update(event or {"Name": "room_member_joined", "MemberId": 2, "OnlineId": HOST_NPID,
|
||||
"RoomId": 777, "LocalAddr": "10.0.0.9", "LocalPort": 5551,
|
||||
"Addr": "9.9.9.8", "Port": 5552, "MappedAddr": "9.9.9.8",
|
||||
"MappedPort": 5552})
|
||||
elif path == "/summon_messenger/create":
|
||||
r.update(MessageId="SummonDataCreateResponse")
|
||||
st["summon_token"] = self.headers.get("X-BB-Token", "")
|
||||
elif path == "/gs/echo":
|
||||
r.update(Token=self.headers.get("X-BB-Token", ""), UA=self.headers.get("User-Agent", ""), Body=body)
|
||||
elif path not in ("/mp/matching2/context_start", "/mp/matching2/leave_room", "/auth/logout",
|
||||
"/mp/matching2/signaling_update", "/np/events/ack"):
|
||||
self._json({"ResKind": 1, "Message": "unknown path"}, 404)
|
||||
return
|
||||
self._json(r)
|
||||
|
||||
|
||||
class QuietServer(ThreadingHTTPServer):
|
||||
def handle_error(self, request, client_address):
|
||||
pass # a bridge we terminate drops its keep-alive connections; that is expected
|
||||
|
||||
|
||||
def fake_bbhost(port):
|
||||
srv = QuietServer((HOST, port), FakeBBHost)
|
||||
srv.state = {"lock": threading.Lock(), "calls": [], "emit_event": False, "event": None}
|
||||
threading.Thread(target=srv.serve_forever, daemon=True).start()
|
||||
return srv
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- bridge process
|
||||
|
||||
class ExeBridge:
|
||||
def __init__(self, cfg):
|
||||
self.dir = tempfile.mkdtemp(prefix="bbbridge-e2e-")
|
||||
self.cfg = dict(cfg, state_file=os.path.join(self.dir, "state.json"), debug_endpoints=True)
|
||||
path = os.path.join(self.dir, "config.json")
|
||||
with open(path, "w") as fh:
|
||||
json.dump(self.cfg, fh)
|
||||
self.log = os.path.join(self.dir, "bridge.log")
|
||||
self.proc = subprocess.Popen([EXE, "--config", path, "--log", self.log],
|
||||
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||
deadline = time.time() + 10
|
||||
while time.time() < deadline:
|
||||
try:
|
||||
urllib.request.urlopen("http://%s:%d/status" % (HOST, self.cfg["webapi_port"]), timeout=1).close()
|
||||
return
|
||||
except OSError:
|
||||
time.sleep(0.1)
|
||||
raise RuntimeError("bridge did not start; log: %s" % self.log)
|
||||
|
||||
def _url(self, path):
|
||||
return "http://%s:%d%s" % (HOST, self.cfg["webapi_port"], path)
|
||||
|
||||
def debug(self):
|
||||
with urllib.request.urlopen(self._url("/debug/state"), timeout=TIMEOUT) as r:
|
||||
return json.loads(r.read())
|
||||
|
||||
def post(self, path, obj):
|
||||
req = urllib.request.Request(self._url(path), data=json.dumps(obj).encode(), method="POST",
|
||||
headers={"Content-Type": "application/json"})
|
||||
with urllib.request.urlopen(req, timeout=TIMEOUT) as r:
|
||||
return json.loads(r.read())
|
||||
|
||||
def peer(self, online_id):
|
||||
return next((p for p in self.debug()["peers"] if p["online_id"] == online_id), None)
|
||||
|
||||
def stop(self):
|
||||
self.proc.terminate()
|
||||
self.proc.wait(10)
|
||||
|
||||
|
||||
class ShadNetClient:
|
||||
def __init__(self, port):
|
||||
self.sock = socket.create_connection((HOST, port), timeout=TIMEOUT)
|
||||
self.next_id = 1
|
||||
self.queue = []
|
||||
ptype, _, _, payload = self._read()
|
||||
assert ptype == sp.PACKET_SERVER_INFO and struct.unpack("<I", payload)[0] == sp.PROTOCOL_VERSION
|
||||
|
||||
def _exact(self, n):
|
||||
buf = b""
|
||||
while len(buf) < n:
|
||||
c = self.sock.recv(n - len(buf))
|
||||
if not c:
|
||||
raise ConnectionError("closed")
|
||||
buf += c
|
||||
return buf
|
||||
|
||||
def _read(self):
|
||||
ptype, cmd, size, pid = sp.parse_header(self._exact(sp.HEADER_SIZE))
|
||||
return ptype, cmd, pid, self._exact(size - sp.HEADER_SIZE) if size > sp.HEADER_SIZE else b""
|
||||
|
||||
def request(self, command, msg=None):
|
||||
payload = sp.proto_blob(msg) if msg is not None else b""
|
||||
self.sock.sendall(sp.build_packet(sp.PACKET_REQUEST, command, self.next_id, payload))
|
||||
self.next_id += 1
|
||||
deadline = time.time() + TIMEOUT * 2
|
||||
while time.time() < deadline:
|
||||
ptype, cmd, _, raw = self._read()
|
||||
if ptype == sp.PACKET_REPLY and cmd == command:
|
||||
return (raw[0] if raw else 255), sp.parse_proto_blob(raw[1:])
|
||||
self.queue.append((ptype, cmd, raw))
|
||||
raise TimeoutError(command)
|
||||
|
||||
def notification(self, kind, timeout=3.0):
|
||||
deadline = time.time() + timeout
|
||||
while time.time() < deadline:
|
||||
for i, (ptype, cmd, raw) in enumerate(self.queue):
|
||||
if ptype == sp.PACKET_NOTIFICATION and cmd == kind:
|
||||
return self.queue.pop(i)[2]
|
||||
try:
|
||||
self.sock.settimeout(0.5)
|
||||
ptype, cmd, _, raw = self._read()
|
||||
except (socket.timeout, TimeoutError):
|
||||
continue
|
||||
finally:
|
||||
self.sock.settimeout(TIMEOUT)
|
||||
self.queue.append((ptype, cmd, raw))
|
||||
return None
|
||||
|
||||
def close(self):
|
||||
self.sock.close()
|
||||
|
||||
|
||||
def stun_ping(npid, udp_port):
|
||||
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
s.settimeout(TIMEOUT)
|
||||
s.sendto(b"\x01" + npid.encode()[:16].ljust(16, b"\0") + socket.inet_aton(HOST), (HOST, udp_port))
|
||||
data, _ = s.recvfrom(64)
|
||||
port = s.getsockname()[1]
|
||||
s.close()
|
||||
return socket.inet_ntoa(data[:4]), struct.unpack(">H", data[4:6])[0], port
|
||||
|
||||
|
||||
def get_json(url):
|
||||
with urllib.request.urlopen(url, timeout=TIMEOUT) as r:
|
||||
return r.status, json.loads(r.read())
|
||||
|
||||
|
||||
def post_json(url, obj, headers=None):
|
||||
req = urllib.request.Request(url, data=json.dumps(obj).encode(), method="POST",
|
||||
headers=dict({"Content-Type": "application/json"}, **(headers or {})))
|
||||
with urllib.request.urlopen(req, timeout=TIMEOUT) as r:
|
||||
return json.loads(r.read())
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- A: server path
|
||||
|
||||
def part_a():
|
||||
print("-- A: server path (gateway off)")
|
||||
ports = {"tcp": 42313, "udp": 42314, "webapi": 42315, "bbhost": 42671, "gs": 42316}
|
||||
fake = fake_bbhost(ports["bbhost"])
|
||||
up = "http://%s:%d" % (HOST, ports["bbhost"])
|
||||
br = ExeBridge({"bbhost_server": up, "community_server": up, "bind_host": HOST,
|
||||
"tcp_port": ports["tcp"], "udp_port": ports["udp"], "webapi_port": ports["webapi"],
|
||||
"p2p_gateway": False, "local_addr": HOST, "poll_interval": 0.05,
|
||||
"game_proxy_routes": {str(ports["gs"]): up}, "create_accounts": True})
|
||||
c = None
|
||||
try:
|
||||
c = ShadNetClient(ports["tcp"])
|
||||
err, blob = c.request(sp.Command.Login, proto3.encode_message(sp.LOGIN_REQUEST, {1: NPID, 4: "CUSA03173"}))
|
||||
reply = proto3.decode_message(sp.LOGIN_REPLY, blob) if blob else {}
|
||||
check("login accepted with the per-player account id",
|
||||
err == 0 and reply.get("user_id") == account_id_for(NPID), (err, reply))
|
||||
check("bbhost account created", "/auth/account/create" in fake.state["calls"])
|
||||
err, blob = c.request(sp.Command.GetServerFeatures)
|
||||
check("matching2 enabled", err == 0 and proto3.decode_message(sp.SERVER_FEATURES_REPLY, blob).get("matching2_enabled") is True)
|
||||
err, _ = c.request(sp.Command.SetClientVersion)
|
||||
check("client version accepted", err == 0, err)
|
||||
err, blob = c.request(sp.Command.GetToken)
|
||||
tok = proto3.decode_message(sp.GET_TOKEN_REPLY, blob)
|
||||
check("token relayed", err == 0 and tok.get("token") == "tok-123" and tok.get("npid") == NPID, tok)
|
||||
|
||||
ip, port, local = stun_ping(NPID, ports["udp"])
|
||||
check("stun reply", ip == HOST and port == local, (ip, port, local))
|
||||
time.sleep(0.2)
|
||||
check("stun registry", br.debug()["registry"].get(NPID) == [HOST, local], br.debug()["registry"])
|
||||
# bbport sends its ping inside a P2P signaling frame (FF FF FF FF first): registered, no echo.
|
||||
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
s.settimeout(0.5)
|
||||
s.sendto(b"\xff\xff\xff\xff\x01" + b"framed".ljust(16, b"\0") + socket.inet_aton(HOST), (HOST, ports["udp"]))
|
||||
framed_port = s.getsockname()[1]
|
||||
try:
|
||||
s.recvfrom(64)
|
||||
echoed = True
|
||||
except socket.timeout:
|
||||
echoed = False
|
||||
s.close()
|
||||
check("framed stun ping registered without an echo",
|
||||
br.debug()["registry"].get("framed") == [HOST, framed_port] and not echoed, br.debug()["registry"])
|
||||
|
||||
err, _ = c.request(sp.Command.ContextStart, proto3.encode_message(sp.CONTEXT_START_REQUEST, {1: 7}))
|
||||
check("context_start", err == 0 and "/mp/matching2/context_start" in fake.state["calls"], err)
|
||||
|
||||
err, blob = c.request(sp.Command.CreateRoom, proto3.encode_message(sp.CREATE_ROOM_REQUEST, {1: 1, 2: 5}))
|
||||
reply = proto3.decode_message(sp.CREATE_ROOM_REPLY, blob) if blob else {}
|
||||
check("create_room accepted", err == 0 and reply.get("room_id") == 777 and reply.get("member_id") == 1
|
||||
and reply.get("max_slots") == 5, (err, reply))
|
||||
details = reply.get("details") or {}
|
||||
check("create_room details", details.get("me_member_id") == 1 and details.get("owner_member_id") == 1
|
||||
and (details.get("room_data") or {}).get("room_id") == 777, details)
|
||||
me = [m for m in details.get("members") or [] if m.get("member_id") == 1]
|
||||
check("create_room lists ourselves (port needs members.me/owner)",
|
||||
len(me) == 1 and me[0].get("npid") == NPID and me[0].get("is_owner") is True, details.get("members"))
|
||||
|
||||
with fake.state["lock"]:
|
||||
fake.state["emit_event"] = True
|
||||
raw = c.notification(sp.Notify.RoomEvent)
|
||||
with fake.state["lock"]:
|
||||
fake.state["emit_event"] = False
|
||||
ev = proto3.decode_message(sp.NOTIFY_ROOM_EVENT, sp.parse_proto_blob(raw)) if raw else {}
|
||||
check("room_member_joined notification", ev.get("event") == sp.RoomEvent.MemberJoined
|
||||
and ev.get("room_id") == 777 and (ev.get("member") or {}).get("npid") == HOST_NPID
|
||||
and (ev.get("member") or {}).get("member_id") == 2, ev)
|
||||
|
||||
err, blob = c.request(sp.Command.RequestSignalingInfos,
|
||||
proto3.encode_message(sp.REQUEST_SIGNALING_INFOS_REQUEST, {1: HOST_NPID}))
|
||||
sig = proto3.decode_message(sp.REQUEST_SIGNALING_INFOS_REPLY, blob) if blob else {}
|
||||
check("signaling resolve", err == 0 and sig.get("target_ip") == "9.9.9.9" and sig.get("target_port") == 5555, sig)
|
||||
|
||||
webapi = "http://%s:%d" % (HOST, ports["webapi"])
|
||||
check("webapi /status", get_json(webapi + "/status")[1].get("ok") is True)
|
||||
ok = True
|
||||
for name in ("friendList", "blockList"):
|
||||
status, body = get_json("%s/v1/users/%s/%s?fields=npId&limit=50" % (webapi, NPID, name))
|
||||
ok = ok and status == 200 and body.get(name) == [] and body.get("totalResults") == 0
|
||||
check("webapi friendList/blockList answer empty lists", ok)
|
||||
ad = {"SessionId": "sess-A", "UserId": 1, "AreaId": 3, "SummonData": "QUJD", "MatchingLevel": 44}
|
||||
r = post_json(webapi + "/summon_messenger/create", ad)
|
||||
check("summon create proxied", r.get("ResKind") == 0 and r.get("MessageId") == "SummonDataCreateResponse", r)
|
||||
check("summon proxy carries the account token", fake.state.get("summon_token") == "tok-123",
|
||||
fake.state.get("summon_token"))
|
||||
echo = post_json("http://%s:%d/gs/echo" % (HOST, ports["gs"]), {"x": 1}, {"User-Agent": "BloodborneGame"})
|
||||
check("game proxy forwards with token and own UA", echo.get("Token") == "tok-123" and echo.get("Body") == {"x": 1}
|
||||
and echo.get("UA", "").startswith("BloodborneGame bbbridge/"), echo)
|
||||
check("advertisement captured", (br.debug().get("last_advertisement") or {}).get("AreaId") == 3)
|
||||
|
||||
err, blob = c.request(sp.Command.LeaveRoom, proto3.encode_message(sp.LEAVE_ROOM_REQUEST, {1: 777, 2: 2}))
|
||||
check("leave_room accepted", err == 0 and proto3.decode_message(sp.LEAVE_ROOM_REPLY, blob).get("room_id") == 777
|
||||
and "/mp/matching2/leave_room" in fake.state["calls"])
|
||||
err, _ = c.request(sp.Command.Terminate)
|
||||
closed = False
|
||||
try:
|
||||
c.sock.settimeout(2)
|
||||
closed = not c.sock.recv(16)
|
||||
except OSError:
|
||||
closed = True
|
||||
check("terminate closes the connection", err == 0 and closed)
|
||||
|
||||
c2 = ShadNetClient(ports["tcp"])
|
||||
err, _ = c2.request(sp.Command.Login, proto3.encode_message(sp.LOGIN_REQUEST, {1: NPID}))
|
||||
_, blob = c2.request(sp.Command.GetToken)
|
||||
c2.close()
|
||||
check("second login uses the stored token, no recover",
|
||||
err == 0 and fake.state["calls"].count("/auth/recover") == 0
|
||||
and proto3.decode_message(sp.GET_TOKEN_REPLY, blob).get("token") == "tok-123")
|
||||
finally:
|
||||
if c:
|
||||
c.close()
|
||||
br.stop()
|
||||
fake.shutdown()
|
||||
fake.server_close()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- B: gateway
|
||||
|
||||
class FakeStun(threading.Thread):
|
||||
def __init__(self):
|
||||
super().__init__(daemon=True)
|
||||
self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
self.sock.bind((HOST, 0))
|
||||
self.sock.settimeout(0.2)
|
||||
self.port = self.sock.getsockname()[1]
|
||||
self.relay_port = 41000
|
||||
self.token = b"RELAYTOK"
|
||||
self.requests, self.relayed = [], []
|
||||
self.stop = False
|
||||
|
||||
def run(self):
|
||||
while not self.stop:
|
||||
try:
|
||||
data, src = self.sock.recvfrom(65536)
|
||||
except (socket.timeout, OSError):
|
||||
continue
|
||||
if data[:2] == b"\xfbR":
|
||||
self.relayed.append(data)
|
||||
continue
|
||||
if len(data) < 20 or struct.unpack(">H", data[:2])[0] != 1:
|
||||
continue
|
||||
self.requests.append(data)
|
||||
attrs = struct.pack(">HHBBH4s", 0x0001, 8, 0, 1, 5678, socket.inet_aton("1.2.3.4"))
|
||||
if self.relay_port and len(data) >= 36 and data[20:22] == b"\x81\x00":
|
||||
v = b"bbr1" + self.token + struct.pack(">H", self.relay_port) + socket.inet_aton(src[0]) + struct.pack(">H", src[1])
|
||||
attrs += struct.pack(">HH", 0x8101, len(v)) + v
|
||||
self.sock.sendto(struct.pack(">HH", 0x0101, len(attrs)) + data[4:20] + attrs, src)
|
||||
|
||||
|
||||
def udp(timeout=2.0):
|
||||
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
s.bind((HOST, 0))
|
||||
s.settimeout(timeout)
|
||||
return s
|
||||
|
||||
|
||||
def recv_until(sock, pred, timeout=2.0):
|
||||
deadline = time.time() + timeout
|
||||
while time.time() < deadline:
|
||||
sock.settimeout(max(0.05, deadline - time.time()))
|
||||
try:
|
||||
data, src = sock.recvfrom(65536)
|
||||
except socket.timeout:
|
||||
return None
|
||||
if pred(data):
|
||||
return data, src
|
||||
return None
|
||||
|
||||
|
||||
def handshake(kind, frm, to, from_conn=7, nonce=0):
|
||||
return b"SHAD" + bytes([0x07, kind]) + frm.encode().ljust(16, b"\0") + to.encode().ljust(16, b"\0") + \
|
||||
struct.pack("<IIIHHQ", from_conn, 0, 0, 0, 0, nonce)
|
||||
|
||||
|
||||
def m2(kind, room, frm_member, to_member, frm_id, nonce=0):
|
||||
return b"SHAD" + bytes([0x21, kind]) + struct.pack("<QHH", room, frm_member, to_member) + \
|
||||
frm_id.encode().ljust(16, b"\0") + struct.pack("<IHHQ", 0, 0, 0, nonce)
|
||||
|
||||
|
||||
def part_b():
|
||||
print("-- B: gateway over real sockets")
|
||||
stun = FakeStun()
|
||||
stun.start()
|
||||
ports = {"tcp": 43313, "udp": 43314, "webapi": 43315}
|
||||
br = ExeBridge({"bbhost_server": "http://%s:1" % HOST, "bind_host": HOST, "tcp_port": ports["tcp"],
|
||||
"udp_port": ports["udp"], "webapi_port": ports["webapi"], "local_addr": HOST,
|
||||
"stun_server": "%s:%d" % (HOST, stun.port), "stun_refresh": 0.3, "game_proxy_routes": {}})
|
||||
port_sock, peer_sock = udp(), udp()
|
||||
try:
|
||||
deadline = time.time() + 4
|
||||
while time.time() < deadline and not (br.debug().get("mapped") and br.debug().get("relay_port")):
|
||||
time.sleep(0.1)
|
||||
st = br.debug()
|
||||
check("STUN request carries BBHOST-HELLO", bool(stun.requests) and stun.requests[0][20:24] == b"\x81\x00\x00\x0c")
|
||||
check("STUN mapping learned", st.get("mapped") == ["1.2.3.4", 5678], st.get("mapped"))
|
||||
check("relay turned on", st.get("relay_port") == 41000, st.get("relay_port"))
|
||||
|
||||
stun.relay_port = 0 # direct part: the next refresh turns the relay off
|
||||
deadline = time.time() + 3
|
||||
while time.time() < deadline and br.debug().get("relay_port"):
|
||||
time.sleep(0.1)
|
||||
inner = tuple(br.post("/debug/add_peer", {
|
||||
"member_id": 2, "online_id": "hunter", "endpoint": list(peer_sock.getsockname()),
|
||||
"port_online_id": "porthost", "port_member_id": 1, "room_id": 777,
|
||||
"port_endpoint": list(port_sock.getsockname())})["inner"])
|
||||
check("inner endpoint on loopback", inner[0] == HOST and inner[1] > 0, inner)
|
||||
opening = recv_until(port_sock, lambda d: d[4:9] == b"SHAD\x10")
|
||||
check("peer opens with ActivationRequest to the port",
|
||||
opening is not None and opening[0][9] == 1 and opening[1] == inner, opening)
|
||||
|
||||
port_sock.sendto(struct.pack(">HH", 40, 30) + b"hello-peer", inner)
|
||||
got = recv_until(peer_sock, lambda d: d[:1] == b"\xff")
|
||||
check("port game data reaches the peer in bbhost framing",
|
||||
got is not None and got[0] == b"\xff\xc3\x28\x1ehello-peer", got)
|
||||
outer_port = got[1][1] if got else 0
|
||||
peer_sock.sendto(b"\xff\xc3\x1e\x28hello-port", (HOST, outer_port))
|
||||
got = recv_until(port_sock, lambda d: d.endswith(b"hello-port"))
|
||||
check("peer game data reaches the port in port framing",
|
||||
got is not None and got[0] == struct.pack(">HH", 30, 40) + b"hello-port" and got[1] == inner, got)
|
||||
peer_sock.sendto(b"\xfebbhp\0\0\0", (HOST, outer_port))
|
||||
got = recv_until(port_sock, lambda d: d[:4] != b"\xff\xff\xff\xff", timeout=0.4)
|
||||
check("hole-punch probe not delivered to the port", got is None, got)
|
||||
|
||||
sig = struct.pack(">HH", 0xFFFF, 0xFFFF)
|
||||
port_sock.sendto(sig + handshake(1, "porthost", "hunter"), inner)
|
||||
check("port's handshake Offer answered with Accept",
|
||||
recv_until(port_sock, lambda d: d[4:10] == b"SHAD\x07\x02") is not None)
|
||||
port_sock.sendto(sig + m2(3, 777, 1, 2, "porthost", nonce=42), inner)
|
||||
got = recv_until(port_sock, lambda d: d[4:10] == b"SHAD\x21\x04")
|
||||
check("port's matching2 Check answered with CheckAck",
|
||||
got is not None and struct.unpack("<Q", got[0][4 + 42:4 + 50])[0] == 42)
|
||||
|
||||
stun.relay_port = 41000
|
||||
deadline = time.time() + 3
|
||||
while time.time() < deadline and not br.debug().get("relay_port"):
|
||||
time.sleep(0.1)
|
||||
inner3 = tuple(br.post("/debug/add_peer", {
|
||||
"member_id": 3, "online_id": "relayed", "endpoint": [HOST, 41001], "port_online_id": "porthost",
|
||||
"port_member_id": 1, "room_id": 777, "port_endpoint": list(port_sock.getsockname())})["inner"])
|
||||
port_sock.sendto(struct.pack(">HH", 40, 30) + b"via-relay", inner3)
|
||||
deadline = time.time() + 2
|
||||
while time.time() < deadline and not stun.relayed:
|
||||
time.sleep(0.05)
|
||||
expect = b"\xfbR" + stun.token + struct.pack(">H", 41001) + b"\xff\xc3\x28\x1evia-relay"
|
||||
check("datagram for a relayed peer goes framed to the STUN port", stun.relayed[:1] == [expect], stun.relayed[:1])
|
||||
stun.sock.sendto(b"\xfbr" + struct.pack(">H", 41001) + b"\xff\xc3\x1e\x28back", (HOST, outer_port))
|
||||
got = recv_until(port_sock, lambda d: d.endswith(b"back"))
|
||||
check("relay delivery reaches the port from the right member", got is not None and got[1] == inner3, got)
|
||||
|
||||
br.post("/debug/remove_peer", {"member_id": 2})
|
||||
check("removed peer forgotten", br.peer("hunter") is None)
|
||||
finally:
|
||||
stun.stop = True
|
||||
br.stop()
|
||||
port_sock.close()
|
||||
peer_sock.close()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- C: bridge + gateway
|
||||
|
||||
def part_c():
|
||||
print("-- C: bridge flows with the gateway on")
|
||||
ports = {"tcp": 44313, "udp": 44314, "webapi": 44315, "bbhost": 44671}
|
||||
fake = fake_bbhost(ports["bbhost"])
|
||||
up = "http://%s:%d" % (HOST, ports["bbhost"])
|
||||
br = ExeBridge({"bbhost_server": up, "community_server": up, "bind_host": HOST, "tcp_port": ports["tcp"],
|
||||
"udp_port": ports["udp"], "webapi_port": ports["webapi"], "local_addr": HOST,
|
||||
"poll_interval": 0.05, "stun_server": "off", "game_proxy_routes": {}, "create_accounts": True})
|
||||
c = None
|
||||
try:
|
||||
c = ShadNetClient(ports["tcp"])
|
||||
c.request(sp.Command.Login, proto3.encode_message(sp.LOGIN_REQUEST, {1: NPID}))
|
||||
stun_ping(NPID, ports["udp"])
|
||||
c.request(sp.Command.ContextStart, proto3.encode_message(sp.CONTEXT_START_REQUEST, {1: 7}))
|
||||
with fake.state["lock"]:
|
||||
fake.state["event"] = {"Name": "guest_invite", "RoomId": 777, "HostOnlineId": HOST_NPID,
|
||||
"HostAddr": "9.9.9.8", "HostPort": 5552, "HostLocalAddr": "10.0.0.9",
|
||||
"HostLocalPort": 5551, "HostMappedAddr": "9.9.9.8", "HostMappedPort": 5552,
|
||||
"HostArea": 3, "HostLevel": 26}
|
||||
fake.state["emit_event"] = True
|
||||
deadline = time.time() + 3
|
||||
while time.time() < deadline and br.peer(HOST_NPID) is None:
|
||||
time.sleep(0.05)
|
||||
with fake.state["lock"]:
|
||||
fake.state["emit_event"] = False
|
||||
fake.state["event"] = None
|
||||
seeded = br.peer(HOST_NPID)
|
||||
check("guest_invite seeds the host in the gateway", seeded is not None)
|
||||
|
||||
err, blob = c.request(sp.Command.JoinRoom, proto3.encode_message(sp.JOIN_ROOM_REQUEST, {1: 777, 2: 1}))
|
||||
reply = proto3.decode_message(sp.JOIN_ROOM_REPLY, blob) if blob else {}
|
||||
host = next((m for m in (reply.get("details") or {}).get("members") or [] if m.get("npid") == HOST_NPID), {})
|
||||
link = br.peer(HOST_NPID)
|
||||
check("joining the invited room keeps the host's inner endpoint",
|
||||
seeded is not None and link is not None and link["inner"] == seeded["inner"], (seeded, link))
|
||||
check("the join fills in the port's member id for the handshakes",
|
||||
link is not None and link["port_member_id"] == 2 and link["room_id"] == 777, link)
|
||||
check("joined room member points at the gateway",
|
||||
err == 0 and link is not None and host.get("addr") == HOST and host.get("port") == link["inner"][1], host)
|
||||
err, blob = c.request(sp.Command.RequestSignalingInfos,
|
||||
proto3.encode_message(sp.REQUEST_SIGNALING_INFOS_REQUEST, {1: HOST_NPID}))
|
||||
sig = proto3.decode_message(sp.REQUEST_SIGNALING_INFOS_REPLY, blob) if blob else {}
|
||||
check("signaling infos point at the gateway",
|
||||
err == 0 and link is not None and sig.get("target_ip") == HOST and sig.get("target_port") == link["inner"][1], sig)
|
||||
|
||||
err, blob = c.request(sp.Command.RequestSignalingInfos,
|
||||
proto3.encode_message(sp.REQUEST_SIGNALING_INFOS_REQUEST, {1: "summonedguest"}))
|
||||
sig2 = proto3.decode_message(sp.REQUEST_SIGNALING_INFOS_REPLY, blob) if blob else {}
|
||||
early = br.peer("summonedguest")
|
||||
check("signaling infos for a non-member resolve through the server",
|
||||
err == 0 and early is not None and sig2.get("target_port") == early["inner"][1], (sig2, early))
|
||||
with fake.state["lock"]:
|
||||
fake.state["event"] = {"Name": "room_member_joined", "MemberId": 3, "RoomId": 777,
|
||||
"OnlineId": "summonedguest", "Addr": "9.9.9.9", "Port": 5555}
|
||||
fake.state["emit_event"] = True
|
||||
deadline = time.time() + 3
|
||||
while time.time() < deadline and (br.peer("summonedguest") or {}).get("member_id") != 3:
|
||||
time.sleep(0.05)
|
||||
with fake.state["lock"]:
|
||||
fake.state["emit_event"] = False
|
||||
fake.state["event"] = None
|
||||
joined = br.peer("summonedguest")
|
||||
check("the guest joining keeps the inner endpoint the port already uses",
|
||||
joined is not None and early is not None and joined["member_id"] == 3 and joined["inner"] == early["inner"]
|
||||
and joined["peer_member_id"] == 3 and all(p["member_id"] > 0 for p in br.debug()["peers"]), joined)
|
||||
|
||||
time.sleep(5.6)
|
||||
check("heartbeat inside the server's 15 s window", fake.state["calls"].count("/mp/matching2/heartbeat") >= 1)
|
||||
c.request(sp.Command.LeaveRoom, proto3.encode_message(sp.LEAVE_ROOM_REQUEST, {1: 777}))
|
||||
check("leaving the room drops the peers", br.debug()["peers"] == [], br.debug()["peers"])
|
||||
finally:
|
||||
if c:
|
||||
c.close()
|
||||
br.stop()
|
||||
fake.shutdown()
|
||||
fake.server_close()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- D: Discord sign-in
|
||||
|
||||
def part_d():
|
||||
print("-- D: sign in with Discord, website code, expired token")
|
||||
port = 45671
|
||||
fake = fake_bbhost(port)
|
||||
up = "http://%s:%d" % (HOST, port)
|
||||
d = tempfile.mkdtemp(prefix="bbbridge-e2e-")
|
||||
state = os.path.join(d, "state.json")
|
||||
cfg_path = os.path.join(d, "config.json")
|
||||
with open(cfg_path, "w") as fh:
|
||||
json.dump({"auth_server": up, "bbhost_server": up, "state_file": state}, fh)
|
||||
try:
|
||||
r = subprocess.run([EXE, "--config", cfg_path, "--link-discord"], capture_output=True, text=True, timeout=30)
|
||||
check("device code shown to the player", "ABCD-EFGH" in r.stdout and "http://example.test/link" in r.stdout,
|
||||
r.stdout)
|
||||
check("approval signs in", r.returncode == 0 and "signed in as discorduser" in r.stdout, r.stdout + r.stderr)
|
||||
with open(state) as fh:
|
||||
acc = json.load(fh)["accounts"].get("discorduser", {})
|
||||
check("Discord account stored under its name",
|
||||
acc.get("token") == "tok-discord" and acc.get("linked") == "discord" and not acc.get("recovery_code"), acc)
|
||||
r = subprocess.run([EXE, "--config", cfg_path, "--web-code", "discorduser"], capture_output=True, text=True,
|
||||
timeout=30)
|
||||
check("website sign-in code", r.returncode == 0 and r.stdout.strip() == "WXYZ-1234", r.stdout + r.stderr)
|
||||
|
||||
def cli(*args):
|
||||
return subprocess.run([EXE, "--config", cfg_path, *args], capture_output=True, text=True, timeout=30)
|
||||
|
||||
def accounts():
|
||||
with open(state) as fh:
|
||||
return json.load(fh)["accounts"]
|
||||
|
||||
r = cli("--create", "pcplayer")
|
||||
check("create account on this PC", r.returncode == 0 and accounts().get("pcplayer", {}).get("recovery_code") == "RC-42"
|
||||
and accounts()["pcplayer"].get("token") == "tok-123", r.stdout + str(accounts().get("pcplayer")))
|
||||
r = cli("--recover", "pcplayer", "RC-42")
|
||||
check("recover account keeps the server's new code", r.returncode == 0
|
||||
and accounts()["pcplayer"].get("recovery_code") == "RC-43" and accounts()["pcplayer"].get("token") == "tok-456",
|
||||
accounts().get("pcplayer"))
|
||||
r = cli("--sign-out", "pcplayer")
|
||||
check("sign out ends the token, keeps the recovery code", r.returncode == 0 and "/auth/logout" in fake.state["calls"]
|
||||
and "token" not in accounts()["pcplayer"] and accounts()["pcplayer"].get("recovery_code") == "RC-43",
|
||||
accounts().get("pcplayer"))
|
||||
r = cli("--sign-out", "discorduser")
|
||||
check("signing out of a Discord account forgets it here", r.returncode == 0 and "discorduser" not in accounts())
|
||||
acc = dict(acc) # the bridge part below uses the Discord account as linked earlier
|
||||
creates = fake.state["calls"].count("/auth/account/create") # the bridge must add none below
|
||||
|
||||
br = ExeBridge({"bbhost_server": up, "community_server": up, "auth_server": up, "bind_host": HOST,
|
||||
"tcp_port": 45313, "udp_port": 45314, "webapi_port": 45315, "p2p_gateway": False,
|
||||
"local_addr": HOST, "game_proxy_routes": {}})
|
||||
try:
|
||||
with open(br.cfg["state_file"], "w") as fh:
|
||||
json.dump({"accounts": {"discorduser": acc, "expired": {
|
||||
"bbhost_name": "expired", "token": "tok-expired", "online_id": "expired", "linked": "discord"}}}, fh)
|
||||
c = ShadNetClient(45313)
|
||||
err, _ = c.request(sp.Command.Login, proto3.encode_message(sp.LOGIN_REQUEST, {1: "discorduser"}))
|
||||
_, blob = c.request(sp.Command.GetToken)
|
||||
c.close()
|
||||
check("the bridge plays as the Discord account",
|
||||
err == 0 and proto3.decode_message(sp.GET_TOKEN_REPLY, blob).get("token") == "tok-discord"
|
||||
and fake.state["calls"].count("/auth/account/create") == creates)
|
||||
c = ShadNetClient(45313)
|
||||
err, _ = c.request(sp.Command.Login, proto3.encode_message(sp.LOGIN_REQUEST, {1: "expired"}))
|
||||
c.close()
|
||||
with open(br.log) as fh:
|
||||
said = "has expired" in fh.read()
|
||||
check("an expired Discord sign-in is refused, not replaced by a new account",
|
||||
err == sp.Error.LoginError and said and fake.state["calls"].count("/auth/account/create") == creates, err)
|
||||
c = ShadNetClient(45313)
|
||||
err, _ = c.request(sp.Command.Login, proto3.encode_message(sp.LOGIN_REQUEST, {1: "stranger"}))
|
||||
c.close()
|
||||
with open(br.log) as fh:
|
||||
said = "use 'Sign in with Discord' in bbbridge first" in fh.read()
|
||||
check("no account on this PC: sign in with Discord first, nothing created",
|
||||
err == sp.Error.LoginError and said and fake.state["calls"].count("/auth/account/create") == creates, err)
|
||||
finally:
|
||||
br.stop()
|
||||
finally:
|
||||
fake.shutdown()
|
||||
fake.server_close()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- E: bbport setup
|
||||
|
||||
def part_e():
|
||||
print("-- E: routing a fresh bbport (sandboxed APPDATA)")
|
||||
tmp = tempfile.mkdtemp(prefix="bbbridge-e2e-")
|
||||
appdata, bbport = os.path.join(tmp, "AppData"), os.path.join(tmp, "bloodborne")
|
||||
user = os.path.join(bbport, "user")
|
||||
os.makedirs(os.path.join(appdata, "bbport-launcher"))
|
||||
os.makedirs(user)
|
||||
open(os.path.join(bbport, "Bloodborne.exe"), "wb").close()
|
||||
mine = {"https://example.test:1": "http://127.0.0.1:1"}
|
||||
with open(os.path.join(user, "host_overrides.custom.json"), "w") as fh:
|
||||
json.dump(mine, fh)
|
||||
sfile = os.path.join(appdata, "bbport-launcher", "settings.json")
|
||||
fresh = {"ui_language": "", "game_dir": os.path.join(bbport, "CUSA03173"), "user_dir": "", "online": False,
|
||||
"online_server": "srv.shadps4.net:31313", "online_webapi": "", "online_npid": "", "online_password": "",
|
||||
"online_upnp": True, "extra_env": "BB_GPU_LOG=info SHADPS4_P2P_PORT=1"}
|
||||
with open(sfile, "w") as fh:
|
||||
json.dump(fresh, fh)
|
||||
env = dict(os.environ, APPDATA=appdata)
|
||||
|
||||
def route(on):
|
||||
return subprocess.run([EXE, "--route", "on" if on else "off", "--bbport", bbport], env=env,
|
||||
capture_output=True, text=True, timeout=30)
|
||||
|
||||
def settings():
|
||||
with open(sfile) as fh:
|
||||
return json.load(fh)
|
||||
|
||||
r = route(True)
|
||||
s = settings()
|
||||
check("route on: bbport points at the bridge", r.returncode == 0 and "routed=1" in r.stdout and s["online"] is True
|
||||
and s["online_server"] == "127.0.0.1:31313" and s["online_webapi"] == "http://127.0.0.1:31315", r.stdout + str(s))
|
||||
check("route on: an empty password gets a placeholder (bbport skips online without one)",
|
||||
s["online_password"] == "bbbridge", s["online_password"])
|
||||
check("route on: extra variables merged, P2P port pinned once",
|
||||
s["extra_env"].split() == ["BB_GPU_LOG=info", "SHADPS4_P2P_PORT=3658"], s["extra_env"])
|
||||
check("route on: npid untouched, key order kept", s["online_npid"] == "" and list(s)[:4] == list(fresh)[:4], list(s))
|
||||
ov = json.load(open(os.path.join(user, "host_overrides.custom.json")))
|
||||
check("route on: overrides written, the player's own backed up",
|
||||
"_bbbridge" in ov and json.load(open(os.path.join(user, "host_overrides.custom.json.before-bbbridge"))) == mine)
|
||||
r = route(False)
|
||||
s = settings()
|
||||
check("route off: back to shadNet, password and variables restored",
|
||||
r.returncode == 0 and s["online_server"] == "srv.shadps4.net:31313" and s["online_webapi"] == ""
|
||||
and s["online_password"] == "" and s["extra_env"] == "BB_GPU_LOG=info", s)
|
||||
check("route off: the player's own overrides restored",
|
||||
json.load(open(os.path.join(user, "host_overrides.custom.json"))) == mine)
|
||||
s["online_password"] = "secret"
|
||||
with open(sfile, "w") as fh:
|
||||
json.dump(s, fh)
|
||||
route(True)
|
||||
on_pw = settings()["online_password"]
|
||||
route(False)
|
||||
check("a real shadNet password is left alone", on_pw == "secret" and settings()["online_password"] == "secret",
|
||||
(on_pw, settings()["online_password"]))
|
||||
|
||||
def set_settings(**kv):
|
||||
s = settings()
|
||||
s.update(kv)
|
||||
with open(sfile, "w") as fh:
|
||||
json.dump(s, fh)
|
||||
|
||||
# the Python bbbridge's file is ours too: replaced, then removed - not kept as the player's
|
||||
ovfile = os.path.join(user, "host_overrides.custom.json")
|
||||
with open(ovfile, "w") as fh:
|
||||
json.dump({"_note": "bbbridge: the game's own server traffic goes through the bridge",
|
||||
"https://thehuntersdream.com:20443": "http://127.0.0.1:31316"}, fh)
|
||||
route(True)
|
||||
route(False)
|
||||
check("route off: an old (Python) bbbridge overrides file is removed, not restored",
|
||||
not os.path.exists(ovfile) and not os.path.exists(ovfile + ".before-bbbridge"),
|
||||
os.listdir(user))
|
||||
|
||||
# the app puts the Hunter's Dream account in while routed; off brings the shadNet one back
|
||||
for name, pw in (("hunter_sn", "secret"), ("renamed_sn", "secret2")): # also when changed in between
|
||||
set_settings(online_npid=name, online_password=pw)
|
||||
route(True)
|
||||
set_settings(online_npid="discord_name")
|
||||
route(False)
|
||||
s = settings()
|
||||
check("route off: the shadNet account comes back (%s)" % name,
|
||||
s["online_npid"] == name and s["online_password"] == pw and s["online_server"] == "srv.shadps4.net:31313",
|
||||
(s["online_npid"], s["online_server"]))
|
||||
|
||||
|
||||
def main():
|
||||
print("== bbbridge C++ end-to-end (%s) ==" % os.path.abspath(EXE))
|
||||
part_a()
|
||||
part_b()
|
||||
part_c()
|
||||
part_d()
|
||||
if os.name == "nt": # routing bbport for Windows (setup.cpp); on Linux the bbport launcher does it
|
||||
part_e()
|
||||
print("\n%d passed, %d failed" % (len(PASSED), len(FAILED)))
|
||||
sys.exit(1 if FAILED else 0)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,209 +0,0 @@
|
||||
// Byte-level unit tests: the same checks as the Python bridge's test_p2p.py
|
||||
// part 1, plus protobuf and the account id (values the Python bridge produced).
|
||||
#include <cstdio>
|
||||
#include <fstream>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "bridge.hpp"
|
||||
#include "p2p.hpp"
|
||||
#include "proto3.hpp"
|
||||
#ifdef _WIN32
|
||||
#include "setup.hpp"
|
||||
#endif
|
||||
#include "shadnet.hpp"
|
||||
|
||||
using namespace bb;
|
||||
using namespace bb::p2p;
|
||||
|
||||
static int g_pass = 0, g_fail = 0;
|
||||
|
||||
static void check(const char* name, bool ok, const std::string& detail = "") {
|
||||
if (ok) {
|
||||
++g_pass;
|
||||
std::printf(" [PASS] %s\n", name);
|
||||
} else {
|
||||
++g_fail;
|
||||
std::printf(" [FAIL] %s %s\n", name, detail.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
static std::string hex(const std::string& s) {
|
||||
std::string out;
|
||||
for (unsigned char c : s) out += fmt("%02x", c);
|
||||
return out;
|
||||
}
|
||||
|
||||
static std::string be16s(uint16_t v) { return std::string{static_cast<char>(v >> 8), static_cast<char>(v & 0xFF)}; }
|
||||
|
||||
static std::string handshake(uint8_t kind, const std::string& from, const std::string& to, uint32_t from_conn,
|
||||
uint64_t nonce) {
|
||||
std::string p = "SHAD";
|
||||
p.push_back(static_cast<char>(T_HANDSHAKE));
|
||||
p.push_back(static_cast<char>(kind));
|
||||
std::string f = from, t = to;
|
||||
f.resize(16, '\0');
|
||||
t.resize(16, '\0');
|
||||
p += f + t;
|
||||
for (int i = 0; i < 4; ++i) p.push_back(static_cast<char>((from_conn >> (8 * i)) & 0xFF));
|
||||
p += std::string(12, '\0');
|
||||
for (int i = 0; i < 8; ++i) p.push_back(static_cast<char>((nonce >> (8 * i)) & 0xFF));
|
||||
return p;
|
||||
}
|
||||
|
||||
static std::string m2(uint8_t kind, uint64_t room, uint16_t from_member, uint16_t to_member, const std::string& from,
|
||||
uint64_t nonce) {
|
||||
std::string p = "SHAD";
|
||||
p.push_back(static_cast<char>(T_M2));
|
||||
p.push_back(static_cast<char>(kind));
|
||||
for (int i = 0; i < 8; ++i) p.push_back(static_cast<char>((room >> (8 * i)) & 0xFF));
|
||||
p.push_back(static_cast<char>(from_member & 0xFF));
|
||||
p.push_back(static_cast<char>(from_member >> 8));
|
||||
p.push_back(static_cast<char>(to_member & 0xFF));
|
||||
p.push_back(static_cast<char>(to_member >> 8));
|
||||
std::string f = from;
|
||||
f.resize(16, '\0');
|
||||
p += f + std::string(8, '\0');
|
||||
for (int i = 0; i < 8; ++i) p.push_back(static_cast<char>((nonce >> (8 * i)) & 0xFF));
|
||||
return p;
|
||||
}
|
||||
|
||||
static std::string control(uint8_t kind, const std::string& from, const std::string& to) {
|
||||
std::string p = "SHAD";
|
||||
p.push_back(static_cast<char>(T_CONTROL));
|
||||
p.push_back(static_cast<char>(kind));
|
||||
std::string f = from, t = to;
|
||||
f.resize(16, '\0');
|
||||
t.resize(16, '\0');
|
||||
p += f + t + std::string(12, '\0');
|
||||
return p;
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
static T le(const std::string& s, size_t at) {
|
||||
uint64_t v = 0;
|
||||
for (size_t i = 0; i < sizeof(T); ++i) v |= static_cast<uint64_t>(static_cast<uint8_t>(s[at + i])) << (8 * i);
|
||||
return static_cast<T>(v);
|
||||
}
|
||||
|
||||
int main() {
|
||||
std::printf("== bbbridge unit ==\n");
|
||||
std::string out;
|
||||
|
||||
// framing
|
||||
check("port->bbhost one-byte vports",
|
||||
port_to_ps4(be16s(40) + be16s(30) + "game", out) == Frame::Game && out == std::string("\xff\xc3\x28\x1e") + "game",
|
||||
hex(out));
|
||||
check("bbhost->port one-byte vports",
|
||||
ps4_to_port(std::string("\xff\xc3\x1e\x28") + "data", out) && out == be16s(30) + be16s(40) + "data", hex(out));
|
||||
std::string two;
|
||||
port_to_ps4(be16s(3000) + be16s(40) + "x", two);
|
||||
check("two-byte vports round trip", ps4_to_port(two, out) && out == be16s(3000) + be16s(40) + "x", hex(two));
|
||||
check("comid flag skipped",
|
||||
ps4_to_port(std::string("\xff\xe3") + "ABCD" + "\x28\x1e" + "z", out) && out == be16s(40) + be16s(30) + "z");
|
||||
check("probe is not game data", !ps4_to_port(kProbe, out));
|
||||
check("port signaling channel detected",
|
||||
port_to_ps4(be16s(0xFFFF) + be16s(0xFFFF) + "SHAD\x05", out) == Frame::Signaling && out == "SHAD\x05");
|
||||
const std::string token = "T0K3N!!!";
|
||||
const std::string wrapped = relay_wrap(token, 41000, std::string("\xff\xc3\x28\x1e"));
|
||||
check("relay frame to server", wrapped.substr(0, 2) == "\xfbR" && wrapped.substr(2, 8) == token &&
|
||||
wrapped.substr(10, 2) == be16s(41000) && wrapped.size() == 16);
|
||||
int port = 0;
|
||||
check("relay frame from server",
|
||||
relay_unwrap(std::string("\xfbr") + be16s(41001) + "dg", port, out) && port == 41001 && out == "dg");
|
||||
|
||||
// STUN
|
||||
std::string txid;
|
||||
const std::string req = stun_request("", txid);
|
||||
check("stun request shape", req.substr(0, 4) == be16s(1) + be16s(16) && req.substr(4, 4) == "\x21\x12\xa4\x42" &&
|
||||
req.substr(20, 4) == std::string("\x81\x00\x00\x0c", 4) && req.substr(24, 4) == "bbr1");
|
||||
const uint32_t addr = (5u << 24) | (6u << 16) | (7u << 8) | 8u;
|
||||
const uint32_t xaddr = addr ^ ((static_cast<uint8_t>(txid[0]) << 24) | (static_cast<uint8_t>(txid[1]) << 16) |
|
||||
(static_cast<uint8_t>(txid[2]) << 8) | static_cast<uint8_t>(txid[3]));
|
||||
const uint16_t xport = static_cast<uint16_t>(4321 ^ ((static_cast<uint8_t>(txid[0]) << 8) | static_cast<uint8_t>(txid[1])));
|
||||
std::string attr = be16s(0x0020) + be16s(8) + std::string("\x00\x01", 2) + be16s(xport) + be16s(xaddr >> 16) +
|
||||
be16s(xaddr & 0xFFFF);
|
||||
const std::string resp = be16s(0x0101) + be16s(static_cast<uint16_t>(attr.size())) + txid + attr;
|
||||
StunAnswer a;
|
||||
check("stun xor-mapped parsed", stun_parse(resp, txid, a) && a.mapped.ip == "5.6.7.8" && a.mapped.port == 4321,
|
||||
a.mapped.str());
|
||||
check("stun wrong txid rejected", !stun_parse(resp, std::string(16, '\0'), a));
|
||||
|
||||
// signaling, answered for the bbhost peer
|
||||
PeerSignaling ps("hunter", 2, "porthost", 1, 777);
|
||||
auto r = ps.handle(handshake(HS_OFFER, "porthost", "hunter", 5, 0));
|
||||
check("Offer -> Accept from the peer",
|
||||
r.size() == 1 && r[0].size() == 62 && static_cast<uint8_t>(r[0][5]) == HS_ACCEPT &&
|
||||
r[0].compare(6, 6, "hunter") == 0 && r[0].compare(22, 8, "porthost") == 0 && le<uint32_t>(r[0], 42) == 5 &&
|
||||
le<uint32_t>(r[0], 46) == 0);
|
||||
r = ps.handle(handshake(HS_CHECK, "porthost", "hunter", 7, 123456));
|
||||
check("Check -> CheckAck echoes nonce",
|
||||
r.size() == 1 && static_cast<uint8_t>(r[0][5]) == HS_CHECK_ACK && le<uint64_t>(r[0], 54) == 123456);
|
||||
r = ps.handle(control(CTL_ACTIVATION_REQUEST, "porthost", "hunter"));
|
||||
check("ActivationRequest -> ActivationAck", r.size() == 1 && r[0].size() == 50 && static_cast<uint8_t>(r[0][5]) == CTL_ACTIVATION_ACK);
|
||||
const auto first = ps.handle(control(CTL_ESTABLISHED, "porthost", "hunter"));
|
||||
const auto second = ps.handle(control(CTL_ESTABLISHED, "porthost", "hunter"));
|
||||
check("Established answered once", first.size() == 1 && second.empty());
|
||||
r = ps.handle(m2(HS_OFFER, 777, 1, 2, "porthost", 0));
|
||||
check("matching2 Offer -> Accept, member ids swapped",
|
||||
r.size() == 1 && r[0].size() == 50 && static_cast<uint8_t>(r[0][5]) == HS_ACCEPT && le<uint64_t>(r[0], 6) == 777 &&
|
||||
le<uint16_t>(r[0], 14) == 2 && le<uint16_t>(r[0], 16) == 1 && r[0].compare(18, 6, "hunter") == 0);
|
||||
r = ps.handle(m2(HS_CHECK, 777, 1, 2, "porthost", 99));
|
||||
check("matching2 Check -> CheckAck", r.size() == 1 && static_cast<uint8_t>(r[0][5]) == HS_CHECK_ACK && le<uint64_t>(r[0], 42) == 99);
|
||||
PeerSignaling provisional("guest", -29999, "porthost", 1, 777);
|
||||
const auto opening = provisional.opening();
|
||||
check("a provisional (negative) key opens without matching2",
|
||||
opening.size() == 1 && static_cast<uint8_t>(opening[0][5]) == CTL_ACTIVATION_REQUEST &&
|
||||
provisional.conn_id == 0x4242);
|
||||
r = ps.handle(std::string("SHAD\x05", 5) + std::string("\x03\x00\x00\x00", 4) + std::string("\x2b\x02\x00\x00\x00\x00\x00\x00", 8));
|
||||
check("echo ping -> pong", r.size() == 1 && r[0].size() == 17 && static_cast<uint8_t>(r[0][4]) == T_ECHO_PONG &&
|
||||
le<uint32_t>(r[0], 5) == 3 && le<uint64_t>(r[0], 9) == 555);
|
||||
|
||||
// protobuf + shadNet framing
|
||||
pb::Writer w;
|
||||
w.s(1, "npid").u(2, 300).flag(4, true).u(5, 0).msg(6, "");
|
||||
pb::Message m(w.str());
|
||||
check("protobuf round trip", m.ok() && m.s(1) == "npid" && m.u(2) == 300 && m.u(4) == 1 && !m.has(5) && m.has(6));
|
||||
std::string blob;
|
||||
check("proto blob framing", shadnet::parse_proto_blob(shadnet::proto_blob("abc"), blob) && blob == "abc");
|
||||
shadnet::Header h;
|
||||
check("packet header", shadnet::parse_header(shadnet::build_reply(101, 7, 0, "xy"), h) && h.type == shadnet::Reply &&
|
||||
h.command == 101 && h.size == 18 && h.packet_id == 7);
|
||||
|
||||
// account id: the values the Python bridge gave these names
|
||||
check("account id matches the Python bridge (porthost)", account_id_for("porthost") == 4562636682122493ull,
|
||||
std::to_string(account_id_for("porthost")));
|
||||
check("account id matches the Python bridge (tester01)", account_id_for("tester01") == 4533768973479740ull,
|
||||
std::to_string(account_id_for("tester01")));
|
||||
|
||||
#ifdef _WIN32 // bbport for Windows' folder (setup.cpp); on Linux the launcher routes the game
|
||||
// where bbport is found: chosen > the folder bbbridge is in > the launcher's game folder
|
||||
{
|
||||
const fs::path root = fs::temp_directory_path() / L"bbbridge-unit-find";
|
||||
std::error_code ec;
|
||||
fs::remove_all(root, ec);
|
||||
const fs::path bbport_dir = root / L"bloodborne", other = root / L"other", games = root / L"games";
|
||||
for (const auto& d : {bbport_dir / L"bbbridge", other, games / L"CUSA03173"}) fs::create_directories(d);
|
||||
std::ofstream(bbport_dir / L"Bloodborne.exe") << "x";
|
||||
std::ofstream(games / L"Play Bloodborne.exe") << "x";
|
||||
const std::string game_dir = narrow((games / L"CUSA03173").wstring());
|
||||
BbportSource src{};
|
||||
check("bbport: the folder bbbridge is in",
|
||||
find_bbport(bbport_dir / L"bbbridge", {}, game_dir, &src) == bbport_dir && src == BbportSource::ExeFolder);
|
||||
check("bbport: bbbridge.exe right next to Bloodborne.exe",
|
||||
find_bbport(bbport_dir, {}, "", &src) == bbport_dir && src == BbportSource::ExeFolder);
|
||||
check("bbport: a chosen folder that is gone falls back",
|
||||
find_bbport(bbport_dir / L"bbbridge", other, game_dir, &src) == bbport_dir && src == BbportSource::ExeFolder);
|
||||
check("bbport: the chosen folder wins",
|
||||
find_bbport(bbport_dir / L"bbbridge", games, "", &src) == games && src == BbportSource::Chosen);
|
||||
check("bbport: from the launcher's game folder",
|
||||
find_bbport(other / L"bbbridge", {}, game_dir, &src) == games && src == BbportSource::Launcher);
|
||||
check("bbport: not found",
|
||||
find_bbport(other / L"bbbridge", {}, "", &src).empty() && src == BbportSource::None);
|
||||
fs::remove_all(root, ec);
|
||||
}
|
||||
#endif
|
||||
|
||||
std::printf("\n%d passed, %d failed\n", g_pass, g_fail);
|
||||
return g_fail ? 1 : 0;
|
||||
}
|
||||
-21
@@ -1,21 +0,0 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2014-2024 Omar Cornut
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
-138
@@ -1,138 +0,0 @@
|
||||
//-----------------------------------------------------------------------------
|
||||
// DEAR IMGUI COMPILE-TIME OPTIONS
|
||||
// Runtime options (clipboard callbacks, enabling various features, etc.) can generally be set via the ImGuiIO structure.
|
||||
// You can use ImGui::SetAllocatorFunctions() before calling ImGui::CreateContext() to rewire memory allocation functions.
|
||||
//-----------------------------------------------------------------------------
|
||||
// A) You may edit imconfig.h (and not overwrite it when updating Dear ImGui, or maintain a patch/rebased branch with your modifications to it)
|
||||
// B) or '#define IMGUI_USER_CONFIG "my_imgui_config.h"' in your project and then add directives in your own file without touching this template.
|
||||
//-----------------------------------------------------------------------------
|
||||
// You need to make sure that configuration settings are defined consistently _everywhere_ Dear ImGui is used, which include the imgui*.cpp
|
||||
// files but also _any_ of your code that uses Dear ImGui. This is because some compile-time options have an affect on data structures.
|
||||
// Defining those options in imconfig.h will ensure every compilation unit gets to see the same data structure layouts.
|
||||
// Call IMGUI_CHECKVERSION() from your .cpp file to verify that the data structures your files are using are matching the ones imgui.cpp is using.
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
#pragma once
|
||||
|
||||
//---- Define assertion handler. Defaults to calling assert().
|
||||
// If your macro uses multiple statements, make sure is enclosed in a 'do { .. } while (0)' block so it can be used as a single statement.
|
||||
//#define IM_ASSERT(_EXPR) MyAssert(_EXPR)
|
||||
//#define IM_ASSERT(_EXPR) ((void)(_EXPR)) // Disable asserts
|
||||
|
||||
//---- Define attributes of all API symbols declarations, e.g. for DLL under Windows
|
||||
// Using Dear ImGui via a shared library is not recommended, because of function call overhead and because we don't guarantee backward nor forward ABI compatibility.
|
||||
// - Windows DLL users: heaps and globals are not shared across DLL boundaries! You will need to call SetCurrentContext() + SetAllocatorFunctions()
|
||||
// for each static/DLL boundary you are calling from. Read "Context and Memory Allocators" section of imgui.cpp for more details.
|
||||
//#define IMGUI_API __declspec(dllexport) // MSVC Windows: DLL export
|
||||
//#define IMGUI_API __declspec(dllimport) // MSVC Windows: DLL import
|
||||
//#define IMGUI_API __attribute__((visibility("default"))) // GCC/Clang: override visibility when set is hidden
|
||||
|
||||
//---- Don't define obsolete functions/enums/behaviors. Consider enabling from time to time after updating to clean your code of obsolete function/names.
|
||||
//#define IMGUI_DISABLE_OBSOLETE_FUNCTIONS
|
||||
|
||||
//---- Disable all of Dear ImGui or don't implement standard windows/tools.
|
||||
// It is very strongly recommended to NOT disable the demo windows and debug tool during development. They are extremely useful in day to day work. Please read comments in imgui_demo.cpp.
|
||||
//#define IMGUI_DISABLE // Disable everything: all headers and source files will be empty.
|
||||
//#define IMGUI_DISABLE_DEMO_WINDOWS // Disable demo windows: ShowDemoWindow()/ShowStyleEditor() will be empty.
|
||||
//#define IMGUI_DISABLE_DEBUG_TOOLS // Disable metrics/debugger and other debug tools: ShowMetricsWindow(), ShowDebugLogWindow() and ShowIDStackToolWindow() will be empty.
|
||||
|
||||
//---- Don't implement some functions to reduce linkage requirements.
|
||||
//#define IMGUI_DISABLE_WIN32_DEFAULT_CLIPBOARD_FUNCTIONS // [Win32] Don't implement default clipboard handler. Won't use and link with OpenClipboard/GetClipboardData/CloseClipboard etc. (user32.lib/.a, kernel32.lib/.a)
|
||||
//#define IMGUI_ENABLE_WIN32_DEFAULT_IME_FUNCTIONS // [Win32] [Default with Visual Studio] Implement default IME handler (require imm32.lib/.a, auto-link for Visual Studio, -limm32 on command-line for MinGW)
|
||||
//#define IMGUI_DISABLE_WIN32_DEFAULT_IME_FUNCTIONS // [Win32] [Default with non-Visual Studio compilers] Don't implement default IME handler (won't require imm32.lib/.a)
|
||||
//#define IMGUI_DISABLE_WIN32_FUNCTIONS // [Win32] Won't use and link with any Win32 function (clipboard, IME).
|
||||
//#define IMGUI_ENABLE_OSX_DEFAULT_CLIPBOARD_FUNCTIONS // [OSX] Implement default OSX clipboard handler (need to link with '-framework ApplicationServices', this is why this is not the default).
|
||||
//#define IMGUI_DISABLE_DEFAULT_SHELL_FUNCTIONS // Don't implement default platform_io.Platform_OpenInShellFn() handler (Win32: ShellExecute(), require shell32.lib/.a, Mac/Linux: use system("")).
|
||||
//#define IMGUI_DISABLE_DEFAULT_FORMAT_FUNCTIONS // Don't implement ImFormatString/ImFormatStringV so you can implement them yourself (e.g. if you don't want to link with vsnprintf)
|
||||
//#define IMGUI_DISABLE_DEFAULT_MATH_FUNCTIONS // Don't implement ImFabs/ImSqrt/ImPow/ImFmod/ImCos/ImSin/ImAcos/ImAtan2 so you can implement them yourself.
|
||||
//#define IMGUI_DISABLE_FILE_FUNCTIONS // Don't implement ImFileOpen/ImFileClose/ImFileRead/ImFileWrite and ImFileHandle at all (replace them with dummies)
|
||||
//#define IMGUI_DISABLE_DEFAULT_FILE_FUNCTIONS // Don't implement ImFileOpen/ImFileClose/ImFileRead/ImFileWrite and ImFileHandle so you can implement them yourself if you don't want to link with fopen/fclose/fread/fwrite. This will also disable the LogToTTY() function.
|
||||
//#define IMGUI_DISABLE_DEFAULT_ALLOCATORS // Don't implement default allocators calling malloc()/free() to avoid linking with them. You will need to call ImGui::SetAllocatorFunctions().
|
||||
//#define IMGUI_DISABLE_SSE // Disable use of SSE intrinsics even if available
|
||||
|
||||
//---- Enable Test Engine / Automation features.
|
||||
//#define IMGUI_ENABLE_TEST_ENGINE // Enable imgui_test_engine hooks. Generally set automatically by include "imgui_te_config.h", see Test Engine for details.
|
||||
|
||||
//---- Include imgui_user.h at the end of imgui.h as a convenience
|
||||
// May be convenient for some users to only explicitly include vanilla imgui.h and have extra stuff included.
|
||||
//#define IMGUI_INCLUDE_IMGUI_USER_H
|
||||
//#define IMGUI_USER_H_FILENAME "my_folder/my_imgui_user.h"
|
||||
|
||||
//---- Pack colors to BGRA8 instead of RGBA8 (to avoid converting from one to another)
|
||||
//#define IMGUI_USE_BGRA_PACKED_COLOR
|
||||
|
||||
//---- Use 32-bit for ImWchar (default is 16-bit) to support Unicode planes 1-16. (e.g. point beyond 0xFFFF like emoticons, dingbats, symbols, shapes, ancient languages, etc...)
|
||||
//#define IMGUI_USE_WCHAR32
|
||||
|
||||
//---- Avoid multiple STB libraries implementations, or redefine path/filenames to prioritize another version
|
||||
// By default the embedded implementations are declared static and not available outside of Dear ImGui sources files.
|
||||
//#define IMGUI_STB_TRUETYPE_FILENAME "my_folder/stb_truetype.h"
|
||||
//#define IMGUI_STB_RECT_PACK_FILENAME "my_folder/stb_rect_pack.h"
|
||||
//#define IMGUI_STB_SPRINTF_FILENAME "my_folder/stb_sprintf.h" // only used if IMGUI_USE_STB_SPRINTF is defined.
|
||||
//#define IMGUI_DISABLE_STB_TRUETYPE_IMPLEMENTATION
|
||||
//#define IMGUI_DISABLE_STB_RECT_PACK_IMPLEMENTATION
|
||||
//#define IMGUI_DISABLE_STB_SPRINTF_IMPLEMENTATION // only disabled if IMGUI_USE_STB_SPRINTF is defined.
|
||||
|
||||
//---- Use stb_sprintf.h for a faster implementation of vsnprintf instead of the one from libc (unless IMGUI_DISABLE_DEFAULT_FORMAT_FUNCTIONS is defined)
|
||||
// Compatibility checks of arguments and formats done by clang and GCC will be disabled in order to support the extra formats provided by stb_sprintf.h.
|
||||
//#define IMGUI_USE_STB_SPRINTF
|
||||
|
||||
//---- Use FreeType to build and rasterize the font atlas (instead of stb_truetype which is embedded by default in Dear ImGui)
|
||||
// Requires FreeType headers to be available in the include path. Requires program to be compiled with 'misc/freetype/imgui_freetype.cpp' (in this repository) + the FreeType library (not provided).
|
||||
// On Windows you may use vcpkg with 'vcpkg install freetype --triplet=x64-windows' + 'vcpkg integrate install'.
|
||||
//#define IMGUI_ENABLE_FREETYPE
|
||||
|
||||
//---- Use FreeType + plutosvg or lunasvg to render OpenType SVG fonts (SVGinOT)
|
||||
// Only works in combination with IMGUI_ENABLE_FREETYPE.
|
||||
// - lunasvg is currently easier to acquire/install, as e.g. it is part of vcpkg.
|
||||
// - plutosvg will support more fonts and may load them faster. It currently requires to be built manually but it is fairly easy. See misc/freetype/README for instructions.
|
||||
// - Both require headers to be available in the include path + program to be linked with the library code (not provided).
|
||||
// - (note: lunasvg implementation is based on Freetype's rsvg-port.c which is licensed under CeCILL-C Free Software License Agreement)
|
||||
//#define IMGUI_ENABLE_FREETYPE_PLUTOSVG
|
||||
//#define IMGUI_ENABLE_FREETYPE_LUNASVG
|
||||
|
||||
//---- Use stb_truetype to build and rasterize the font atlas (default)
|
||||
// The only purpose of this define is if you want force compilation of the stb_truetype backend ALONG with the FreeType backend.
|
||||
//#define IMGUI_ENABLE_STB_TRUETYPE
|
||||
|
||||
//---- Define constructor and implicit cast operators to convert back<>forth between your math types and ImVec2/ImVec4.
|
||||
// This will be inlined as part of ImVec2 and ImVec4 class declarations.
|
||||
/*
|
||||
#define IM_VEC2_CLASS_EXTRA \
|
||||
constexpr ImVec2(const MyVec2& f) : x(f.x), y(f.y) {} \
|
||||
operator MyVec2() const { return MyVec2(x,y); }
|
||||
|
||||
#define IM_VEC4_CLASS_EXTRA \
|
||||
constexpr ImVec4(const MyVec4& f) : x(f.x), y(f.y), z(f.z), w(f.w) {} \
|
||||
operator MyVec4() const { return MyVec4(x,y,z,w); }
|
||||
*/
|
||||
//---- ...Or use Dear ImGui's own very basic math operators.
|
||||
//#define IMGUI_DEFINE_MATH_OPERATORS
|
||||
|
||||
//---- Use 32-bit vertex indices (default is 16-bit) is one way to allow large meshes with more than 64K vertices.
|
||||
// Your renderer backend will need to support it (most example renderer backends support both 16/32-bit indices).
|
||||
// Another way to allow large meshes while keeping 16-bit indices is to handle ImDrawCmd::VtxOffset in your renderer.
|
||||
// Read about ImGuiBackendFlags_RendererHasVtxOffset for details.
|
||||
//#define ImDrawIdx unsigned int
|
||||
|
||||
//---- Override ImDrawCallback signature (will need to modify renderer backends accordingly)
|
||||
//struct ImDrawList;
|
||||
//struct ImDrawCmd;
|
||||
//typedef void (*MyImDrawCallback)(const ImDrawList* draw_list, const ImDrawCmd* cmd, void* my_renderer_user_data);
|
||||
//#define ImDrawCallback MyImDrawCallback
|
||||
|
||||
//---- Debug Tools: Macro to break in Debugger (we provide a default implementation of this in the codebase)
|
||||
// (use 'Metrics->Tools->Item Picker' to pick widgets with the mouse and break into them for easy debugging.)
|
||||
//#define IM_DEBUG_BREAK IM_ASSERT(0)
|
||||
//#define IM_DEBUG_BREAK __debugbreak()
|
||||
|
||||
//---- Debug Tools: Enable slower asserts
|
||||
//#define IMGUI_DEBUG_PARANOID
|
||||
|
||||
//---- Tip: You can add extra functions within the ImGui:: namespace from anywhere (e.g. your own sources/header files)
|
||||
/*
|
||||
namespace ImGui
|
||||
{
|
||||
void MyFunction(const char* name, MyMatrix44* mtx);
|
||||
}
|
||||
*/
|
||||
Vendored
-16789
File diff suppressed because it is too large.
Load diff
Vendored
-3714
File diff suppressed because it is too large.
Load diff
-4674
File diff suppressed because it is too large.
Load diff
-618
@@ -1,618 +0,0 @@
|
||||
// dear imgui: Renderer Backend for DirectX11
|
||||
// This needs to be used along with a Platform Backend (e.g. Win32)
|
||||
|
||||
// Implemented features:
|
||||
// [X] Renderer: User texture binding. Use 'ID3D11ShaderResourceView*' as ImTextureID. Read the FAQ about ImTextureID!
|
||||
// [X] Renderer: Large meshes support (64k+ vertices) with 16-bit indices.
|
||||
// [X] Renderer: Expose selected render state for draw callbacks to use. Access in '(ImGui_ImplXXXX_RenderState*)GetPlatformIO().Renderer_RenderState'.
|
||||
|
||||
// You can use unmodified imgui_impl_* files in your project. See examples/ folder for examples of using this.
|
||||
// Prefer including the entire imgui/ repository into your project (either as a copy or as a submodule), and only build the backends you need.
|
||||
// Learn about Dear ImGui:
|
||||
// - FAQ https://dearimgui.com/faq
|
||||
// - Getting Started https://dearimgui.com/getting-started
|
||||
// - Documentation https://dearimgui.com/docs (same as your local docs/ folder).
|
||||
// - Introduction, links and more at the top of imgui.cpp
|
||||
|
||||
// CHANGELOG
|
||||
// (minor and older changes stripped away, please see git history for details)
|
||||
// 2024-10-07: DirectX11: Changed default texture sampler to Clamp instead of Repeat/Wrap.
|
||||
// 2024-10-07: DirectX11: Expose selected render state in ImGui_ImplDX11_RenderState, which you can access in 'void* platform_io.Renderer_RenderState' during draw callbacks.
|
||||
// 2022-10-11: Using 'nullptr' instead of 'NULL' as per our switch to C++11.
|
||||
// 2021-06-29: Reorganized backend to pull data from a single structure to facilitate usage with multiple-contexts (all g_XXXX access changed to bd->XXXX).
|
||||
// 2021-05-19: DirectX11: Replaced direct access to ImDrawCmd::TextureId with a call to ImDrawCmd::GetTexID(). (will become a requirement)
|
||||
// 2021-02-18: DirectX11: Change blending equation to preserve alpha in output buffer.
|
||||
// 2019-08-01: DirectX11: Fixed code querying the Geometry Shader state (would generally error with Debug layer enabled).
|
||||
// 2019-07-21: DirectX11: Backup, clear and restore Geometry Shader is any is bound when calling ImGui_ImplDX11_RenderDrawData. Clearing Hull/Domain/Compute shaders without backup/restore.
|
||||
// 2019-05-29: DirectX11: Added support for large mesh (64K+ vertices), enable ImGuiBackendFlags_RendererHasVtxOffset flag.
|
||||
// 2019-04-30: DirectX11: Added support for special ImDrawCallback_ResetRenderState callback to reset render state.
|
||||
// 2018-12-03: Misc: Added #pragma comment statement to automatically link with d3dcompiler.lib when using D3DCompile().
|
||||
// 2018-11-30: Misc: Setting up io.BackendRendererName so it can be displayed in the About Window.
|
||||
// 2018-08-01: DirectX11: Querying for IDXGIFactory instead of IDXGIFactory1 to increase compatibility.
|
||||
// 2018-07-13: DirectX11: Fixed unreleased resources in Init and Shutdown functions.
|
||||
// 2018-06-08: Misc: Extracted imgui_impl_dx11.cpp/.h away from the old combined DX11+Win32 example.
|
||||
// 2018-06-08: DirectX11: Use draw_data->DisplayPos and draw_data->DisplaySize to setup projection matrix and clipping rectangle.
|
||||
// 2018-02-16: Misc: Obsoleted the io.RenderDrawListsFn callback and exposed ImGui_ImplDX11_RenderDrawData() in the .h file so you can call it yourself.
|
||||
// 2018-02-06: Misc: Removed call to ImGui::Shutdown() which is not available from 1.60 WIP, user needs to call CreateContext/DestroyContext themselves.
|
||||
// 2016-05-07: DirectX11: Disabling depth-write.
|
||||
|
||||
#include "imgui.h"
|
||||
#ifndef IMGUI_DISABLE
|
||||
#include "imgui_impl_dx11.h"
|
||||
|
||||
// DirectX
|
||||
#include <stdio.h>
|
||||
#include <d3d11.h>
|
||||
#include <d3dcompiler.h>
|
||||
#ifdef _MSC_VER
|
||||
#pragma comment(lib, "d3dcompiler") // Automatically link with d3dcompiler.lib as we are using D3DCompile() below.
|
||||
#endif
|
||||
|
||||
// DirectX11 data
|
||||
struct ImGui_ImplDX11_Data
|
||||
{
|
||||
ID3D11Device* pd3dDevice;
|
||||
ID3D11DeviceContext* pd3dDeviceContext;
|
||||
IDXGIFactory* pFactory;
|
||||
ID3D11Buffer* pVB;
|
||||
ID3D11Buffer* pIB;
|
||||
ID3D11VertexShader* pVertexShader;
|
||||
ID3D11InputLayout* pInputLayout;
|
||||
ID3D11Buffer* pVertexConstantBuffer;
|
||||
ID3D11PixelShader* pPixelShader;
|
||||
ID3D11SamplerState* pFontSampler;
|
||||
ID3D11ShaderResourceView* pFontTextureView;
|
||||
ID3D11RasterizerState* pRasterizerState;
|
||||
ID3D11BlendState* pBlendState;
|
||||
ID3D11DepthStencilState* pDepthStencilState;
|
||||
int VertexBufferSize;
|
||||
int IndexBufferSize;
|
||||
|
||||
ImGui_ImplDX11_Data() { memset((void*)this, 0, sizeof(*this)); VertexBufferSize = 5000; IndexBufferSize = 10000; }
|
||||
};
|
||||
|
||||
struct VERTEX_CONSTANT_BUFFER_DX11
|
||||
{
|
||||
float mvp[4][4];
|
||||
};
|
||||
|
||||
// Backend data stored in io.BackendRendererUserData to allow support for multiple Dear ImGui contexts
|
||||
// It is STRONGLY preferred that you use docking branch with multi-viewports (== single Dear ImGui context + multiple windows) instead of multiple Dear ImGui contexts.
|
||||
static ImGui_ImplDX11_Data* ImGui_ImplDX11_GetBackendData()
|
||||
{
|
||||
return ImGui::GetCurrentContext() ? (ImGui_ImplDX11_Data*)ImGui::GetIO().BackendRendererUserData : nullptr;
|
||||
}
|
||||
|
||||
// Functions
|
||||
static void ImGui_ImplDX11_SetupRenderState(ImDrawData* draw_data, ID3D11DeviceContext* device_ctx)
|
||||
{
|
||||
ImGui_ImplDX11_Data* bd = ImGui_ImplDX11_GetBackendData();
|
||||
|
||||
// Setup viewport
|
||||
D3D11_VIEWPORT vp;
|
||||
memset(&vp, 0, sizeof(D3D11_VIEWPORT));
|
||||
vp.Width = draw_data->DisplaySize.x;
|
||||
vp.Height = draw_data->DisplaySize.y;
|
||||
vp.MinDepth = 0.0f;
|
||||
vp.MaxDepth = 1.0f;
|
||||
vp.TopLeftX = vp.TopLeftY = 0;
|
||||
device_ctx->RSSetViewports(1, &vp);
|
||||
|
||||
// Setup shader and vertex buffers
|
||||
unsigned int stride = sizeof(ImDrawVert);
|
||||
unsigned int offset = 0;
|
||||
device_ctx->IASetInputLayout(bd->pInputLayout);
|
||||
device_ctx->IASetVertexBuffers(0, 1, &bd->pVB, &stride, &offset);
|
||||
device_ctx->IASetIndexBuffer(bd->pIB, sizeof(ImDrawIdx) == 2 ? DXGI_FORMAT_R16_UINT : DXGI_FORMAT_R32_UINT, 0);
|
||||
device_ctx->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
|
||||
device_ctx->VSSetShader(bd->pVertexShader, nullptr, 0);
|
||||
device_ctx->VSSetConstantBuffers(0, 1, &bd->pVertexConstantBuffer);
|
||||
device_ctx->PSSetShader(bd->pPixelShader, nullptr, 0);
|
||||
device_ctx->PSSetSamplers(0, 1, &bd->pFontSampler);
|
||||
device_ctx->GSSetShader(nullptr, nullptr, 0);
|
||||
device_ctx->HSSetShader(nullptr, nullptr, 0); // In theory we should backup and restore this as well.. very infrequently used..
|
||||
device_ctx->DSSetShader(nullptr, nullptr, 0); // In theory we should backup and restore this as well.. very infrequently used..
|
||||
device_ctx->CSSetShader(nullptr, nullptr, 0); // In theory we should backup and restore this as well.. very infrequently used..
|
||||
|
||||
// Setup blend state
|
||||
const float blend_factor[4] = { 0.f, 0.f, 0.f, 0.f };
|
||||
device_ctx->OMSetBlendState(bd->pBlendState, blend_factor, 0xffffffff);
|
||||
device_ctx->OMSetDepthStencilState(bd->pDepthStencilState, 0);
|
||||
device_ctx->RSSetState(bd->pRasterizerState);
|
||||
}
|
||||
|
||||
// Render function
|
||||
void ImGui_ImplDX11_RenderDrawData(ImDrawData* draw_data)
|
||||
{
|
||||
// Avoid rendering when minimized
|
||||
if (draw_data->DisplaySize.x <= 0.0f || draw_data->DisplaySize.y <= 0.0f)
|
||||
return;
|
||||
|
||||
ImGui_ImplDX11_Data* bd = ImGui_ImplDX11_GetBackendData();
|
||||
ID3D11DeviceContext* device = bd->pd3dDeviceContext;
|
||||
|
||||
// Create and grow vertex/index buffers if needed
|
||||
if (!bd->pVB || bd->VertexBufferSize < draw_data->TotalVtxCount)
|
||||
{
|
||||
if (bd->pVB) { bd->pVB->Release(); bd->pVB = nullptr; }
|
||||
bd->VertexBufferSize = draw_data->TotalVtxCount + 5000;
|
||||
D3D11_BUFFER_DESC desc;
|
||||
memset(&desc, 0, sizeof(D3D11_BUFFER_DESC));
|
||||
desc.Usage = D3D11_USAGE_DYNAMIC;
|
||||
desc.ByteWidth = bd->VertexBufferSize * sizeof(ImDrawVert);
|
||||
desc.BindFlags = D3D11_BIND_VERTEX_BUFFER;
|
||||
desc.CPUAccessFlags = D3D11_CPU_ACCESS_WRITE;
|
||||
desc.MiscFlags = 0;
|
||||
if (bd->pd3dDevice->CreateBuffer(&desc, nullptr, &bd->pVB) < 0)
|
||||
return;
|
||||
}
|
||||
if (!bd->pIB || bd->IndexBufferSize < draw_data->TotalIdxCount)
|
||||
{
|
||||
if (bd->pIB) { bd->pIB->Release(); bd->pIB = nullptr; }
|
||||
bd->IndexBufferSize = draw_data->TotalIdxCount + 10000;
|
||||
D3D11_BUFFER_DESC desc;
|
||||
memset(&desc, 0, sizeof(D3D11_BUFFER_DESC));
|
||||
desc.Usage = D3D11_USAGE_DYNAMIC;
|
||||
desc.ByteWidth = bd->IndexBufferSize * sizeof(ImDrawIdx);
|
||||
desc.BindFlags = D3D11_BIND_INDEX_BUFFER;
|
||||
desc.CPUAccessFlags = D3D11_CPU_ACCESS_WRITE;
|
||||
if (bd->pd3dDevice->CreateBuffer(&desc, nullptr, &bd->pIB) < 0)
|
||||
return;
|
||||
}
|
||||
|
||||
// Upload vertex/index data into a single contiguous GPU buffer
|
||||
D3D11_MAPPED_SUBRESOURCE vtx_resource, idx_resource;
|
||||
if (device->Map(bd->pVB, 0, D3D11_MAP_WRITE_DISCARD, 0, &vtx_resource) != S_OK)
|
||||
return;
|
||||
if (device->Map(bd->pIB, 0, D3D11_MAP_WRITE_DISCARD, 0, &idx_resource) != S_OK)
|
||||
return;
|
||||
ImDrawVert* vtx_dst = (ImDrawVert*)vtx_resource.pData;
|
||||
ImDrawIdx* idx_dst = (ImDrawIdx*)idx_resource.pData;
|
||||
for (int n = 0; n < draw_data->CmdListsCount; n++)
|
||||
{
|
||||
const ImDrawList* draw_list = draw_data->CmdLists[n];
|
||||
memcpy(vtx_dst, draw_list->VtxBuffer.Data, draw_list->VtxBuffer.Size * sizeof(ImDrawVert));
|
||||
memcpy(idx_dst, draw_list->IdxBuffer.Data, draw_list->IdxBuffer.Size * sizeof(ImDrawIdx));
|
||||
vtx_dst += draw_list->VtxBuffer.Size;
|
||||
idx_dst += draw_list->IdxBuffer.Size;
|
||||
}
|
||||
device->Unmap(bd->pVB, 0);
|
||||
device->Unmap(bd->pIB, 0);
|
||||
|
||||
// Setup orthographic projection matrix into our constant buffer
|
||||
// Our visible imgui space lies from draw_data->DisplayPos (top left) to draw_data->DisplayPos+data_data->DisplaySize (bottom right). DisplayPos is (0,0) for single viewport apps.
|
||||
{
|
||||
D3D11_MAPPED_SUBRESOURCE mapped_resource;
|
||||
if (device->Map(bd->pVertexConstantBuffer, 0, D3D11_MAP_WRITE_DISCARD, 0, &mapped_resource) != S_OK)
|
||||
return;
|
||||
VERTEX_CONSTANT_BUFFER_DX11* constant_buffer = (VERTEX_CONSTANT_BUFFER_DX11*)mapped_resource.pData;
|
||||
float L = draw_data->DisplayPos.x;
|
||||
float R = draw_data->DisplayPos.x + draw_data->DisplaySize.x;
|
||||
float T = draw_data->DisplayPos.y;
|
||||
float B = draw_data->DisplayPos.y + draw_data->DisplaySize.y;
|
||||
float mvp[4][4] =
|
||||
{
|
||||
{ 2.0f/(R-L), 0.0f, 0.0f, 0.0f },
|
||||
{ 0.0f, 2.0f/(T-B), 0.0f, 0.0f },
|
||||
{ 0.0f, 0.0f, 0.5f, 0.0f },
|
||||
{ (R+L)/(L-R), (T+B)/(B-T), 0.5f, 1.0f },
|
||||
};
|
||||
memcpy(&constant_buffer->mvp, mvp, sizeof(mvp));
|
||||
device->Unmap(bd->pVertexConstantBuffer, 0);
|
||||
}
|
||||
|
||||
// Backup DX state that will be modified to restore it afterwards (unfortunately this is very ugly looking and verbose. Close your eyes!)
|
||||
struct BACKUP_DX11_STATE
|
||||
{
|
||||
UINT ScissorRectsCount, ViewportsCount;
|
||||
D3D11_RECT ScissorRects[D3D11_VIEWPORT_AND_SCISSORRECT_OBJECT_COUNT_PER_PIPELINE];
|
||||
D3D11_VIEWPORT Viewports[D3D11_VIEWPORT_AND_SCISSORRECT_OBJECT_COUNT_PER_PIPELINE];
|
||||
ID3D11RasterizerState* RS;
|
||||
ID3D11BlendState* BlendState;
|
||||
FLOAT BlendFactor[4];
|
||||
UINT SampleMask;
|
||||
UINT StencilRef;
|
||||
ID3D11DepthStencilState* DepthStencilState;
|
||||
ID3D11ShaderResourceView* PSShaderResource;
|
||||
ID3D11SamplerState* PSSampler;
|
||||
ID3D11PixelShader* PS;
|
||||
ID3D11VertexShader* VS;
|
||||
ID3D11GeometryShader* GS;
|
||||
UINT PSInstancesCount, VSInstancesCount, GSInstancesCount;
|
||||
ID3D11ClassInstance *PSInstances[256], *VSInstances[256], *GSInstances[256]; // 256 is max according to PSSetShader documentation
|
||||
D3D11_PRIMITIVE_TOPOLOGY PrimitiveTopology;
|
||||
ID3D11Buffer* IndexBuffer, *VertexBuffer, *VSConstantBuffer;
|
||||
UINT IndexBufferOffset, VertexBufferStride, VertexBufferOffset;
|
||||
DXGI_FORMAT IndexBufferFormat;
|
||||
ID3D11InputLayout* InputLayout;
|
||||
};
|
||||
BACKUP_DX11_STATE old = {};
|
||||
old.ScissorRectsCount = old.ViewportsCount = D3D11_VIEWPORT_AND_SCISSORRECT_OBJECT_COUNT_PER_PIPELINE;
|
||||
device->RSGetScissorRects(&old.ScissorRectsCount, old.ScissorRects);
|
||||
device->RSGetViewports(&old.ViewportsCount, old.Viewports);
|
||||
device->RSGetState(&old.RS);
|
||||
device->OMGetBlendState(&old.BlendState, old.BlendFactor, &old.SampleMask);
|
||||
device->OMGetDepthStencilState(&old.DepthStencilState, &old.StencilRef);
|
||||
device->PSGetShaderResources(0, 1, &old.PSShaderResource);
|
||||
device->PSGetSamplers(0, 1, &old.PSSampler);
|
||||
old.PSInstancesCount = old.VSInstancesCount = old.GSInstancesCount = 256;
|
||||
device->PSGetShader(&old.PS, old.PSInstances, &old.PSInstancesCount);
|
||||
device->VSGetShader(&old.VS, old.VSInstances, &old.VSInstancesCount);
|
||||
device->VSGetConstantBuffers(0, 1, &old.VSConstantBuffer);
|
||||
device->GSGetShader(&old.GS, old.GSInstances, &old.GSInstancesCount);
|
||||
|
||||
device->IAGetPrimitiveTopology(&old.PrimitiveTopology);
|
||||
device->IAGetIndexBuffer(&old.IndexBuffer, &old.IndexBufferFormat, &old.IndexBufferOffset);
|
||||
device->IAGetVertexBuffers(0, 1, &old.VertexBuffer, &old.VertexBufferStride, &old.VertexBufferOffset);
|
||||
device->IAGetInputLayout(&old.InputLayout);
|
||||
|
||||
// Setup desired DX state
|
||||
ImGui_ImplDX11_SetupRenderState(draw_data, device);
|
||||
|
||||
// Setup render state structure (for callbacks and custom texture bindings)
|
||||
ImGuiPlatformIO& platform_io = ImGui::GetPlatformIO();
|
||||
ImGui_ImplDX11_RenderState render_state;
|
||||
render_state.Device = bd->pd3dDevice;
|
||||
render_state.DeviceContext = bd->pd3dDeviceContext;
|
||||
render_state.SamplerDefault = bd->pFontSampler;
|
||||
platform_io.Renderer_RenderState = &render_state;
|
||||
|
||||
// Render command lists
|
||||
// (Because we merged all buffers into a single one, we maintain our own offset into them)
|
||||
int global_idx_offset = 0;
|
||||
int global_vtx_offset = 0;
|
||||
ImVec2 clip_off = draw_data->DisplayPos;
|
||||
for (int n = 0; n < draw_data->CmdListsCount; n++)
|
||||
{
|
||||
const ImDrawList* draw_list = draw_data->CmdLists[n];
|
||||
for (int cmd_i = 0; cmd_i < draw_list->CmdBuffer.Size; cmd_i++)
|
||||
{
|
||||
const ImDrawCmd* pcmd = &draw_list->CmdBuffer[cmd_i];
|
||||
if (pcmd->UserCallback != nullptr)
|
||||
{
|
||||
// User callback, registered via ImDrawList::AddCallback()
|
||||
// (ImDrawCallback_ResetRenderState is a special callback value used by the user to request the renderer to reset render state.)
|
||||
if (pcmd->UserCallback == ImDrawCallback_ResetRenderState)
|
||||
ImGui_ImplDX11_SetupRenderState(draw_data, device);
|
||||
else
|
||||
pcmd->UserCallback(draw_list, pcmd);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Project scissor/clipping rectangles into framebuffer space
|
||||
ImVec2 clip_min(pcmd->ClipRect.x - clip_off.x, pcmd->ClipRect.y - clip_off.y);
|
||||
ImVec2 clip_max(pcmd->ClipRect.z - clip_off.x, pcmd->ClipRect.w - clip_off.y);
|
||||
if (clip_max.x <= clip_min.x || clip_max.y <= clip_min.y)
|
||||
continue;
|
||||
|
||||
// Apply scissor/clipping rectangle
|
||||
const D3D11_RECT r = { (LONG)clip_min.x, (LONG)clip_min.y, (LONG)clip_max.x, (LONG)clip_max.y };
|
||||
device->RSSetScissorRects(1, &r);
|
||||
|
||||
// Bind texture, Draw
|
||||
ID3D11ShaderResourceView* texture_srv = (ID3D11ShaderResourceView*)pcmd->GetTexID();
|
||||
device->PSSetShaderResources(0, 1, &texture_srv);
|
||||
device->DrawIndexed(pcmd->ElemCount, pcmd->IdxOffset + global_idx_offset, pcmd->VtxOffset + global_vtx_offset);
|
||||
}
|
||||
}
|
||||
global_idx_offset += draw_list->IdxBuffer.Size;
|
||||
global_vtx_offset += draw_list->VtxBuffer.Size;
|
||||
}
|
||||
platform_io.Renderer_RenderState = NULL;
|
||||
|
||||
// Restore modified DX state
|
||||
device->RSSetScissorRects(old.ScissorRectsCount, old.ScissorRects);
|
||||
device->RSSetViewports(old.ViewportsCount, old.Viewports);
|
||||
device->RSSetState(old.RS); if (old.RS) old.RS->Release();
|
||||
device->OMSetBlendState(old.BlendState, old.BlendFactor, old.SampleMask); if (old.BlendState) old.BlendState->Release();
|
||||
device->OMSetDepthStencilState(old.DepthStencilState, old.StencilRef); if (old.DepthStencilState) old.DepthStencilState->Release();
|
||||
device->PSSetShaderResources(0, 1, &old.PSShaderResource); if (old.PSShaderResource) old.PSShaderResource->Release();
|
||||
device->PSSetSamplers(0, 1, &old.PSSampler); if (old.PSSampler) old.PSSampler->Release();
|
||||
device->PSSetShader(old.PS, old.PSInstances, old.PSInstancesCount); if (old.PS) old.PS->Release();
|
||||
for (UINT i = 0; i < old.PSInstancesCount; i++) if (old.PSInstances[i]) old.PSInstances[i]->Release();
|
||||
device->VSSetShader(old.VS, old.VSInstances, old.VSInstancesCount); if (old.VS) old.VS->Release();
|
||||
device->VSSetConstantBuffers(0, 1, &old.VSConstantBuffer); if (old.VSConstantBuffer) old.VSConstantBuffer->Release();
|
||||
device->GSSetShader(old.GS, old.GSInstances, old.GSInstancesCount); if (old.GS) old.GS->Release();
|
||||
for (UINT i = 0; i < old.VSInstancesCount; i++) if (old.VSInstances[i]) old.VSInstances[i]->Release();
|
||||
device->IASetPrimitiveTopology(old.PrimitiveTopology);
|
||||
device->IASetIndexBuffer(old.IndexBuffer, old.IndexBufferFormat, old.IndexBufferOffset); if (old.IndexBuffer) old.IndexBuffer->Release();
|
||||
device->IASetVertexBuffers(0, 1, &old.VertexBuffer, &old.VertexBufferStride, &old.VertexBufferOffset); if (old.VertexBuffer) old.VertexBuffer->Release();
|
||||
device->IASetInputLayout(old.InputLayout); if (old.InputLayout) old.InputLayout->Release();
|
||||
}
|
||||
|
||||
static void ImGui_ImplDX11_CreateFontsTexture()
|
||||
{
|
||||
// Build texture atlas
|
||||
ImGuiIO& io = ImGui::GetIO();
|
||||
ImGui_ImplDX11_Data* bd = ImGui_ImplDX11_GetBackendData();
|
||||
unsigned char* pixels;
|
||||
int width, height;
|
||||
io.Fonts->GetTexDataAsRGBA32(&pixels, &width, &height);
|
||||
|
||||
// Upload texture to graphics system
|
||||
{
|
||||
D3D11_TEXTURE2D_DESC desc;
|
||||
ZeroMemory(&desc, sizeof(desc));
|
||||
desc.Width = width;
|
||||
desc.Height = height;
|
||||
desc.MipLevels = 1;
|
||||
desc.ArraySize = 1;
|
||||
desc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;
|
||||
desc.SampleDesc.Count = 1;
|
||||
desc.Usage = D3D11_USAGE_DEFAULT;
|
||||
desc.BindFlags = D3D11_BIND_SHADER_RESOURCE;
|
||||
desc.CPUAccessFlags = 0;
|
||||
|
||||
ID3D11Texture2D* pTexture = nullptr;
|
||||
D3D11_SUBRESOURCE_DATA subResource;
|
||||
subResource.pSysMem = pixels;
|
||||
subResource.SysMemPitch = desc.Width * 4;
|
||||
subResource.SysMemSlicePitch = 0;
|
||||
bd->pd3dDevice->CreateTexture2D(&desc, &subResource, &pTexture);
|
||||
IM_ASSERT(pTexture != nullptr);
|
||||
|
||||
// Create texture view
|
||||
D3D11_SHADER_RESOURCE_VIEW_DESC srvDesc;
|
||||
ZeroMemory(&srvDesc, sizeof(srvDesc));
|
||||
srvDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;
|
||||
srvDesc.ViewDimension = D3D11_SRV_DIMENSION_TEXTURE2D;
|
||||
srvDesc.Texture2D.MipLevels = desc.MipLevels;
|
||||
srvDesc.Texture2D.MostDetailedMip = 0;
|
||||
bd->pd3dDevice->CreateShaderResourceView(pTexture, &srvDesc, &bd->pFontTextureView);
|
||||
pTexture->Release();
|
||||
}
|
||||
|
||||
// Store our identifier
|
||||
io.Fonts->SetTexID((ImTextureID)bd->pFontTextureView);
|
||||
|
||||
// Create texture sampler
|
||||
// (Bilinear sampling is required by default. Set 'io.Fonts->Flags |= ImFontAtlasFlags_NoBakedLines' or 'style.AntiAliasedLinesUseTex = false' to allow point/nearest sampling)
|
||||
{
|
||||
D3D11_SAMPLER_DESC desc;
|
||||
ZeroMemory(&desc, sizeof(desc));
|
||||
desc.Filter = D3D11_FILTER_MIN_MAG_MIP_LINEAR;
|
||||
desc.AddressU = D3D11_TEXTURE_ADDRESS_CLAMP;
|
||||
desc.AddressV = D3D11_TEXTURE_ADDRESS_CLAMP;
|
||||
desc.AddressW = D3D11_TEXTURE_ADDRESS_CLAMP;
|
||||
desc.MipLODBias = 0.f;
|
||||
desc.ComparisonFunc = D3D11_COMPARISON_ALWAYS;
|
||||
desc.MinLOD = 0.f;
|
||||
desc.MaxLOD = 0.f;
|
||||
bd->pd3dDevice->CreateSamplerState(&desc, &bd->pFontSampler);
|
||||
}
|
||||
}
|
||||
|
||||
bool ImGui_ImplDX11_CreateDeviceObjects()
|
||||
{
|
||||
ImGui_ImplDX11_Data* bd = ImGui_ImplDX11_GetBackendData();
|
||||
if (!bd->pd3dDevice)
|
||||
return false;
|
||||
if (bd->pFontSampler)
|
||||
ImGui_ImplDX11_InvalidateDeviceObjects();
|
||||
|
||||
// By using D3DCompile() from <d3dcompiler.h> / d3dcompiler.lib, we introduce a dependency to a given version of d3dcompiler_XX.dll (see D3DCOMPILER_DLL_A)
|
||||
// If you would like to use this DX11 sample code but remove this dependency you can:
|
||||
// 1) compile once, save the compiled shader blobs into a file or source code and pass them to CreateVertexShader()/CreatePixelShader() [preferred solution]
|
||||
// 2) use code to detect any version of the DLL and grab a pointer to D3DCompile from the DLL.
|
||||
// See https://github.com/ocornut/imgui/pull/638 for sources and details.
|
||||
|
||||
// Create the vertex shader
|
||||
{
|
||||
static const char* vertexShader =
|
||||
"cbuffer vertexBuffer : register(b0) \
|
||||
{\
|
||||
float4x4 ProjectionMatrix; \
|
||||
};\
|
||||
struct VS_INPUT\
|
||||
{\
|
||||
float2 pos : POSITION;\
|
||||
float4 col : COLOR0;\
|
||||
float2 uv : TEXCOORD0;\
|
||||
};\
|
||||
\
|
||||
struct PS_INPUT\
|
||||
{\
|
||||
float4 pos : SV_POSITION;\
|
||||
float4 col : COLOR0;\
|
||||
float2 uv : TEXCOORD0;\
|
||||
};\
|
||||
\
|
||||
PS_INPUT main(VS_INPUT input)\
|
||||
{\
|
||||
PS_INPUT output;\
|
||||
output.pos = mul( ProjectionMatrix, float4(input.pos.xy, 0.f, 1.f));\
|
||||
output.col = input.col;\
|
||||
output.uv = input.uv;\
|
||||
return output;\
|
||||
}";
|
||||
|
||||
ID3DBlob* vertexShaderBlob;
|
||||
if (FAILED(D3DCompile(vertexShader, strlen(vertexShader), nullptr, nullptr, nullptr, "main", "vs_4_0", 0, 0, &vertexShaderBlob, nullptr)))
|
||||
return false; // NB: Pass ID3DBlob* pErrorBlob to D3DCompile() to get error showing in (const char*)pErrorBlob->GetBufferPointer(). Make sure to Release() the blob!
|
||||
if (bd->pd3dDevice->CreateVertexShader(vertexShaderBlob->GetBufferPointer(), vertexShaderBlob->GetBufferSize(), nullptr, &bd->pVertexShader) != S_OK)
|
||||
{
|
||||
vertexShaderBlob->Release();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Create the input layout
|
||||
D3D11_INPUT_ELEMENT_DESC local_layout[] =
|
||||
{
|
||||
{ "POSITION", 0, DXGI_FORMAT_R32G32_FLOAT, 0, (UINT)offsetof(ImDrawVert, pos), D3D11_INPUT_PER_VERTEX_DATA, 0 },
|
||||
{ "TEXCOORD", 0, DXGI_FORMAT_R32G32_FLOAT, 0, (UINT)offsetof(ImDrawVert, uv), D3D11_INPUT_PER_VERTEX_DATA, 0 },
|
||||
{ "COLOR", 0, DXGI_FORMAT_R8G8B8A8_UNORM, 0, (UINT)offsetof(ImDrawVert, col), D3D11_INPUT_PER_VERTEX_DATA, 0 },
|
||||
};
|
||||
if (bd->pd3dDevice->CreateInputLayout(local_layout, 3, vertexShaderBlob->GetBufferPointer(), vertexShaderBlob->GetBufferSize(), &bd->pInputLayout) != S_OK)
|
||||
{
|
||||
vertexShaderBlob->Release();
|
||||
return false;
|
||||
}
|
||||
vertexShaderBlob->Release();
|
||||
|
||||
// Create the constant buffer
|
||||
{
|
||||
D3D11_BUFFER_DESC desc;
|
||||
desc.ByteWidth = sizeof(VERTEX_CONSTANT_BUFFER_DX11);
|
||||
desc.Usage = D3D11_USAGE_DYNAMIC;
|
||||
desc.BindFlags = D3D11_BIND_CONSTANT_BUFFER;
|
||||
desc.CPUAccessFlags = D3D11_CPU_ACCESS_WRITE;
|
||||
desc.MiscFlags = 0;
|
||||
bd->pd3dDevice->CreateBuffer(&desc, nullptr, &bd->pVertexConstantBuffer);
|
||||
}
|
||||
}
|
||||
|
||||
// Create the pixel shader
|
||||
{
|
||||
static const char* pixelShader =
|
||||
"struct PS_INPUT\
|
||||
{\
|
||||
float4 pos : SV_POSITION;\
|
||||
float4 col : COLOR0;\
|
||||
float2 uv : TEXCOORD0;\
|
||||
};\
|
||||
sampler sampler0;\
|
||||
Texture2D texture0;\
|
||||
\
|
||||
float4 main(PS_INPUT input) : SV_Target\
|
||||
{\
|
||||
float4 out_col = input.col * texture0.Sample(sampler0, input.uv); \
|
||||
return out_col; \
|
||||
}";
|
||||
|
||||
ID3DBlob* pixelShaderBlob;
|
||||
if (FAILED(D3DCompile(pixelShader, strlen(pixelShader), nullptr, nullptr, nullptr, "main", "ps_4_0", 0, 0, &pixelShaderBlob, nullptr)))
|
||||
return false; // NB: Pass ID3DBlob* pErrorBlob to D3DCompile() to get error showing in (const char*)pErrorBlob->GetBufferPointer(). Make sure to Release() the blob!
|
||||
if (bd->pd3dDevice->CreatePixelShader(pixelShaderBlob->GetBufferPointer(), pixelShaderBlob->GetBufferSize(), nullptr, &bd->pPixelShader) != S_OK)
|
||||
{
|
||||
pixelShaderBlob->Release();
|
||||
return false;
|
||||
}
|
||||
pixelShaderBlob->Release();
|
||||
}
|
||||
|
||||
// Create the blending setup
|
||||
{
|
||||
D3D11_BLEND_DESC desc;
|
||||
ZeroMemory(&desc, sizeof(desc));
|
||||
desc.AlphaToCoverageEnable = false;
|
||||
desc.RenderTarget[0].BlendEnable = true;
|
||||
desc.RenderTarget[0].SrcBlend = D3D11_BLEND_SRC_ALPHA;
|
||||
desc.RenderTarget[0].DestBlend = D3D11_BLEND_INV_SRC_ALPHA;
|
||||
desc.RenderTarget[0].BlendOp = D3D11_BLEND_OP_ADD;
|
||||
desc.RenderTarget[0].SrcBlendAlpha = D3D11_BLEND_ONE;
|
||||
desc.RenderTarget[0].DestBlendAlpha = D3D11_BLEND_INV_SRC_ALPHA;
|
||||
desc.RenderTarget[0].BlendOpAlpha = D3D11_BLEND_OP_ADD;
|
||||
desc.RenderTarget[0].RenderTargetWriteMask = D3D11_COLOR_WRITE_ENABLE_ALL;
|
||||
bd->pd3dDevice->CreateBlendState(&desc, &bd->pBlendState);
|
||||
}
|
||||
|
||||
// Create the rasterizer state
|
||||
{
|
||||
D3D11_RASTERIZER_DESC desc;
|
||||
ZeroMemory(&desc, sizeof(desc));
|
||||
desc.FillMode = D3D11_FILL_SOLID;
|
||||
desc.CullMode = D3D11_CULL_NONE;
|
||||
desc.ScissorEnable = true;
|
||||
desc.DepthClipEnable = true;
|
||||
bd->pd3dDevice->CreateRasterizerState(&desc, &bd->pRasterizerState);
|
||||
}
|
||||
|
||||
// Create depth-stencil State
|
||||
{
|
||||
D3D11_DEPTH_STENCIL_DESC desc;
|
||||
ZeroMemory(&desc, sizeof(desc));
|
||||
desc.DepthEnable = false;
|
||||
desc.DepthWriteMask = D3D11_DEPTH_WRITE_MASK_ALL;
|
||||
desc.DepthFunc = D3D11_COMPARISON_ALWAYS;
|
||||
desc.StencilEnable = false;
|
||||
desc.FrontFace.StencilFailOp = desc.FrontFace.StencilDepthFailOp = desc.FrontFace.StencilPassOp = D3D11_STENCIL_OP_KEEP;
|
||||
desc.FrontFace.StencilFunc = D3D11_COMPARISON_ALWAYS;
|
||||
desc.BackFace = desc.FrontFace;
|
||||
bd->pd3dDevice->CreateDepthStencilState(&desc, &bd->pDepthStencilState);
|
||||
}
|
||||
|
||||
ImGui_ImplDX11_CreateFontsTexture();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void ImGui_ImplDX11_InvalidateDeviceObjects()
|
||||
{
|
||||
ImGui_ImplDX11_Data* bd = ImGui_ImplDX11_GetBackendData();
|
||||
if (!bd->pd3dDevice)
|
||||
return;
|
||||
|
||||
if (bd->pFontSampler) { bd->pFontSampler->Release(); bd->pFontSampler = nullptr; }
|
||||
if (bd->pFontTextureView) { bd->pFontTextureView->Release(); bd->pFontTextureView = nullptr; ImGui::GetIO().Fonts->SetTexID(0); } // We copied data->pFontTextureView to io.Fonts->TexID so let's clear that as well.
|
||||
if (bd->pIB) { bd->pIB->Release(); bd->pIB = nullptr; }
|
||||
if (bd->pVB) { bd->pVB->Release(); bd->pVB = nullptr; }
|
||||
if (bd->pBlendState) { bd->pBlendState->Release(); bd->pBlendState = nullptr; }
|
||||
if (bd->pDepthStencilState) { bd->pDepthStencilState->Release(); bd->pDepthStencilState = nullptr; }
|
||||
if (bd->pRasterizerState) { bd->pRasterizerState->Release(); bd->pRasterizerState = nullptr; }
|
||||
if (bd->pPixelShader) { bd->pPixelShader->Release(); bd->pPixelShader = nullptr; }
|
||||
if (bd->pVertexConstantBuffer) { bd->pVertexConstantBuffer->Release(); bd->pVertexConstantBuffer = nullptr; }
|
||||
if (bd->pInputLayout) { bd->pInputLayout->Release(); bd->pInputLayout = nullptr; }
|
||||
if (bd->pVertexShader) { bd->pVertexShader->Release(); bd->pVertexShader = nullptr; }
|
||||
}
|
||||
|
||||
bool ImGui_ImplDX11_Init(ID3D11Device* device, ID3D11DeviceContext* device_context)
|
||||
{
|
||||
ImGuiIO& io = ImGui::GetIO();
|
||||
IMGUI_CHECKVERSION();
|
||||
IM_ASSERT(io.BackendRendererUserData == nullptr && "Already initialized a renderer backend!");
|
||||
|
||||
// Setup backend capabilities flags
|
||||
ImGui_ImplDX11_Data* bd = IM_NEW(ImGui_ImplDX11_Data)();
|
||||
io.BackendRendererUserData = (void*)bd;
|
||||
io.BackendRendererName = "imgui_impl_dx11";
|
||||
io.BackendFlags |= ImGuiBackendFlags_RendererHasVtxOffset; // We can honor the ImDrawCmd::VtxOffset field, allowing for large meshes.
|
||||
|
||||
// Get factory from device
|
||||
IDXGIDevice* pDXGIDevice = nullptr;
|
||||
IDXGIAdapter* pDXGIAdapter = nullptr;
|
||||
IDXGIFactory* pFactory = nullptr;
|
||||
|
||||
if (device->QueryInterface(IID_PPV_ARGS(&pDXGIDevice)) == S_OK)
|
||||
if (pDXGIDevice->GetParent(IID_PPV_ARGS(&pDXGIAdapter)) == S_OK)
|
||||
if (pDXGIAdapter->GetParent(IID_PPV_ARGS(&pFactory)) == S_OK)
|
||||
{
|
||||
bd->pd3dDevice = device;
|
||||
bd->pd3dDeviceContext = device_context;
|
||||
bd->pFactory = pFactory;
|
||||
}
|
||||
if (pDXGIDevice) pDXGIDevice->Release();
|
||||
if (pDXGIAdapter) pDXGIAdapter->Release();
|
||||
bd->pd3dDevice->AddRef();
|
||||
bd->pd3dDeviceContext->AddRef();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void ImGui_ImplDX11_Shutdown()
|
||||
{
|
||||
ImGui_ImplDX11_Data* bd = ImGui_ImplDX11_GetBackendData();
|
||||
IM_ASSERT(bd != nullptr && "No renderer backend to shutdown, or already shutdown?");
|
||||
ImGuiIO& io = ImGui::GetIO();
|
||||
|
||||
ImGui_ImplDX11_InvalidateDeviceObjects();
|
||||
if (bd->pFactory) { bd->pFactory->Release(); }
|
||||
if (bd->pd3dDevice) { bd->pd3dDevice->Release(); }
|
||||
if (bd->pd3dDeviceContext) { bd->pd3dDeviceContext->Release(); }
|
||||
io.BackendRendererName = nullptr;
|
||||
io.BackendRendererUserData = nullptr;
|
||||
io.BackendFlags &= ~ImGuiBackendFlags_RendererHasVtxOffset;
|
||||
IM_DELETE(bd);
|
||||
}
|
||||
|
||||
void ImGui_ImplDX11_NewFrame()
|
||||
{
|
||||
ImGui_ImplDX11_Data* bd = ImGui_ImplDX11_GetBackendData();
|
||||
IM_ASSERT(bd != nullptr && "Context or backend not initialized! Did you call ImGui_ImplDX11_Init()?");
|
||||
|
||||
if (!bd->pFontSampler)
|
||||
ImGui_ImplDX11_CreateDeviceObjects();
|
||||
}
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
#endif // #ifndef IMGUI_DISABLE
|
||||
-45
@@ -1,45 +0,0 @@
|
||||
// dear imgui: Renderer Backend for DirectX11
|
||||
// This needs to be used along with a Platform Backend (e.g. Win32)
|
||||
|
||||
// Implemented features:
|
||||
// [X] Renderer: User texture binding. Use 'ID3D11ShaderResourceView*' as ImTextureID. Read the FAQ about ImTextureID!
|
||||
// [X] Renderer: Large meshes support (64k+ vertices) with 16-bit indices.
|
||||
// [X] Renderer: Expose selected render state for draw callbacks to use. Access in '(ImGui_ImplXXXX_RenderState*)GetPlatformIO().Renderer_RenderState'.
|
||||
|
||||
// You can use unmodified imgui_impl_* files in your project. See examples/ folder for examples of using this.
|
||||
// Prefer including the entire imgui/ repository into your project (either as a copy or as a submodule), and only build the backends you need.
|
||||
// Learn about Dear ImGui:
|
||||
// - FAQ https://dearimgui.com/faq
|
||||
// - Getting Started https://dearimgui.com/getting-started
|
||||
// - Documentation https://dearimgui.com/docs (same as your local docs/ folder).
|
||||
// - Introduction, links and more at the top of imgui.cpp
|
||||
|
||||
#pragma once
|
||||
#include "imgui.h" // IMGUI_IMPL_API
|
||||
#ifndef IMGUI_DISABLE
|
||||
|
||||
struct ID3D11Device;
|
||||
struct ID3D11DeviceContext;
|
||||
struct ID3D11SamplerState;
|
||||
|
||||
// Follow "Getting Started" link and check examples/ folder to learn about using backends!
|
||||
IMGUI_IMPL_API bool ImGui_ImplDX11_Init(ID3D11Device* device, ID3D11DeviceContext* device_context);
|
||||
IMGUI_IMPL_API void ImGui_ImplDX11_Shutdown();
|
||||
IMGUI_IMPL_API void ImGui_ImplDX11_NewFrame();
|
||||
IMGUI_IMPL_API void ImGui_ImplDX11_RenderDrawData(ImDrawData* draw_data);
|
||||
|
||||
// Use if you want to reset your rendering device without losing Dear ImGui state.
|
||||
IMGUI_IMPL_API bool ImGui_ImplDX11_CreateDeviceObjects();
|
||||
IMGUI_IMPL_API void ImGui_ImplDX11_InvalidateDeviceObjects();
|
||||
|
||||
// [BETA] Selected render state data shared with callbacks.
|
||||
// This is temporarily stored in GetPlatformIO().Renderer_RenderState during the ImGui_ImplDX11_RenderDrawData() call.
|
||||
// (Please open an issue if you feel you need access to more data)
|
||||
struct ImGui_ImplDX11_RenderState
|
||||
{
|
||||
ID3D11Device* Device;
|
||||
ID3D11DeviceContext* DeviceContext;
|
||||
ID3D11SamplerState* SamplerDefault;
|
||||
};
|
||||
|
||||
#endif // #ifndef IMGUI_DISABLE
|
||||
-940
@@ -1,940 +0,0 @@
|
||||
// dear imgui: Platform Backend for Windows (standard windows API for 32-bits AND 64-bits applications)
|
||||
// This needs to be used along with a Renderer (e.g. DirectX11, OpenGL3, Vulkan..)
|
||||
|
||||
// Implemented features:
|
||||
// [X] Platform: Clipboard support (for Win32 this is actually part of core dear imgui)
|
||||
// [X] Platform: Mouse support. Can discriminate Mouse/TouchScreen/Pen.
|
||||
// [X] Platform: Keyboard support. Since 1.87 we are using the io.AddKeyEvent() function. Pass ImGuiKey values to all key functions e.g. ImGui::IsKeyPressed(ImGuiKey_Space). [Legacy VK_* values are obsolete since 1.87 and not supported since 1.91.5]
|
||||
// [X] Platform: Gamepad support. Enabled with 'io.ConfigFlags |= ImGuiConfigFlags_NavEnableGamepad'.
|
||||
// [X] Platform: Mouse cursor shape and visibility. Disable with 'io.ConfigFlags |= ImGuiConfigFlags_NoMouseCursorChange'.
|
||||
|
||||
// You can use unmodified imgui_impl_* files in your project. See examples/ folder for examples of using this.
|
||||
// Prefer including the entire imgui/ repository into your project (either as a copy or as a submodule), and only build the backends you need.
|
||||
// Learn about Dear ImGui:
|
||||
// - FAQ https://dearimgui.com/faq
|
||||
// - Getting Started https://dearimgui.com/getting-started
|
||||
// - Documentation https://dearimgui.com/docs (same as your local docs/ folder).
|
||||
// - Introduction, links and more at the top of imgui.cpp
|
||||
|
||||
// Configuration flags to add in your imconfig file:
|
||||
//#define IMGUI_IMPL_WIN32_DISABLE_GAMEPAD // Disable gamepad support. This was meaningful before <1.81 but we now load XInput dynamically so the option is now less relevant.
|
||||
|
||||
// CHANGELOG
|
||||
// (minor and older changes stripped away, please see git history for details)
|
||||
// 2024-07-08: Inputs: Fixed ImGuiMod_Super being mapped to VK_APPS instead of VK_LWIN||VK_RWIN. (#7768)
|
||||
// 2023-10-05: Inputs: Added support for extra ImGuiKey values: F13 to F24 function keys, app back/forward keys.
|
||||
// 2023-09-25: Inputs: Synthesize key-down event on key-up for VK_SNAPSHOT / ImGuiKey_PrintScreen as Windows doesn't emit it (same behavior as GLFW/SDL).
|
||||
// 2023-09-07: Inputs: Added support for keyboard codepage conversion for when application is compiled in MBCS mode and using a non-Unicode window.
|
||||
// 2023-04-19: Added ImGui_ImplWin32_InitForOpenGL() to facilitate combining raw Win32/Winapi with OpenGL. (#3218)
|
||||
// 2023-04-04: Inputs: Added support for io.AddMouseSourceEvent() to discriminate ImGuiMouseSource_Mouse/ImGuiMouseSource_TouchScreen/ImGuiMouseSource_Pen. (#2702)
|
||||
// 2023-02-15: Inputs: Use WM_NCMOUSEMOVE / WM_NCMOUSELEAVE to track mouse position over non-client area (e.g. OS decorations) when app is not focused. (#6045, #6162)
|
||||
// 2023-02-02: Inputs: Flipping WM_MOUSEHWHEEL (horizontal mouse-wheel) value to match other backends and offer consistent horizontal scrolling direction. (#4019, #6096, #1463)
|
||||
// 2022-10-11: Using 'nullptr' instead of 'NULL' as per our switch to C++11.
|
||||
// 2022-09-28: Inputs: Convert WM_CHAR values with MultiByteToWideChar() when window class was registered as MBCS (not Unicode).
|
||||
// 2022-09-26: Inputs: Renamed ImGuiKey_ModXXX introduced in 1.87 to ImGuiMod_XXX (old names still supported).
|
||||
// 2022-01-26: Inputs: replaced short-lived io.AddKeyModsEvent() (added two weeks ago) with io.AddKeyEvent() using ImGuiKey_ModXXX flags. Sorry for the confusion.
|
||||
// 2021-01-20: Inputs: calling new io.AddKeyAnalogEvent() for gamepad support, instead of writing directly to io.NavInputs[].
|
||||
// 2022-01-17: Inputs: calling new io.AddMousePosEvent(), io.AddMouseButtonEvent(), io.AddMouseWheelEvent() API (1.87+).
|
||||
// 2022-01-17: Inputs: always update key mods next and before a key event (not in NewFrame) to fix input queue with very low framerates.
|
||||
// 2022-01-12: Inputs: Update mouse inputs using WM_MOUSEMOVE/WM_MOUSELEAVE + fallback to provide it when focused but not hovered/captured. More standard and will allow us to pass it to future input queue API.
|
||||
// 2022-01-12: Inputs: Maintain our own copy of MouseButtonsDown mask instead of using ImGui::IsAnyMouseDown() which will be obsoleted.
|
||||
// 2022-01-10: Inputs: calling new io.AddKeyEvent(), io.AddKeyModsEvent() + io.SetKeyEventNativeData() API (1.87+). Support for full ImGuiKey range.
|
||||
// 2021-12-16: Inputs: Fill VK_LCONTROL/VK_RCONTROL/VK_LSHIFT/VK_RSHIFT/VK_LMENU/VK_RMENU for completeness.
|
||||
// 2021-08-17: Calling io.AddFocusEvent() on WM_SETFOCUS/WM_KILLFOCUS messages.
|
||||
// 2021-08-02: Inputs: Fixed keyboard modifiers being reported when host window doesn't have focus.
|
||||
// 2021-07-29: Inputs: MousePos is correctly reported when the host platform window is hovered but not focused (using TrackMouseEvent() to receive WM_MOUSELEAVE events).
|
||||
// 2021-06-29: Reorganized backend to pull data from a single structure to facilitate usage with multiple-contexts (all g_XXXX access changed to bd->XXXX).
|
||||
// 2021-06-08: Fixed ImGui_ImplWin32_EnableDpiAwareness() and ImGui_ImplWin32_GetDpiScaleForMonitor() to handle Windows 8.1/10 features without a manifest (per-monitor DPI, and properly calls SetProcessDpiAwareness() on 8.1).
|
||||
// 2021-03-23: Inputs: Clearing keyboard down array when losing focus (WM_KILLFOCUS).
|
||||
// 2021-02-18: Added ImGui_ImplWin32_EnableAlphaCompositing(). Non Visual Studio users will need to link with dwmapi.lib (MinGW/gcc: use -ldwmapi).
|
||||
// 2021-02-17: Fixed ImGui_ImplWin32_EnableDpiAwareness() attempting to get SetProcessDpiAwareness from shcore.dll on Windows 8 whereas it is only supported on Windows 8.1.
|
||||
// 2021-01-25: Inputs: Dynamically loading XInput DLL.
|
||||
// 2020-12-04: Misc: Fixed setting of io.DisplaySize to invalid/uninitialized data when after hwnd has been closed.
|
||||
// 2020-03-03: Inputs: Calling AddInputCharacterUTF16() to support surrogate pairs leading to codepoint >= 0x10000 (for more complete CJK inputs)
|
||||
// 2020-02-17: Added ImGui_ImplWin32_EnableDpiAwareness(), ImGui_ImplWin32_GetDpiScaleForHwnd(), ImGui_ImplWin32_GetDpiScaleForMonitor() helper functions.
|
||||
// 2020-01-14: Inputs: Added support for #define IMGUI_IMPL_WIN32_DISABLE_GAMEPAD/IMGUI_IMPL_WIN32_DISABLE_LINKING_XINPUT.
|
||||
// 2019-12-05: Inputs: Added support for ImGuiMouseCursor_NotAllowed mouse cursor.
|
||||
// 2019-05-11: Inputs: Don't filter value from WM_CHAR before calling AddInputCharacter().
|
||||
// 2019-01-17: Misc: Using GetForegroundWindow()+IsChild() instead of GetActiveWindow() to be compatible with windows created in a different thread or parent.
|
||||
// 2019-01-17: Inputs: Added support for mouse buttons 4 and 5 via WM_XBUTTON* messages.
|
||||
// 2019-01-15: Inputs: Added support for XInput gamepads (if ImGuiConfigFlags_NavEnableGamepad is set by user application).
|
||||
// 2018-11-30: Misc: Setting up io.BackendPlatformName so it can be displayed in the About Window.
|
||||
// 2018-06-29: Inputs: Added support for the ImGuiMouseCursor_Hand cursor.
|
||||
// 2018-06-10: Inputs: Fixed handling of mouse wheel messages to support fine position messages (typically sent by track-pads).
|
||||
// 2018-06-08: Misc: Extracted imgui_impl_win32.cpp/.h away from the old combined DX9/DX10/DX11/DX12 examples.
|
||||
// 2018-03-20: Misc: Setup io.BackendFlags ImGuiBackendFlags_HasMouseCursors and ImGuiBackendFlags_HasSetMousePos flags + honor ImGuiConfigFlags_NoMouseCursorChange flag.
|
||||
// 2018-02-20: Inputs: Added support for mouse cursors (ImGui::GetMouseCursor() value and WM_SETCURSOR message handling).
|
||||
// 2018-02-06: Inputs: Added mapping for ImGuiKey_Space.
|
||||
// 2018-02-06: Inputs: Honoring the io.WantSetMousePos by repositioning the mouse (when using navigation and ImGuiConfigFlags_NavMoveMouse is set).
|
||||
// 2018-02-06: Misc: Removed call to ImGui::Shutdown() which is not available from 1.60 WIP, user needs to call CreateContext/DestroyContext themselves.
|
||||
// 2018-01-20: Inputs: Added Horizontal Mouse Wheel support.
|
||||
// 2018-01-08: Inputs: Added mapping for ImGuiKey_Insert.
|
||||
// 2018-01-05: Inputs: Added WM_LBUTTONDBLCLK double-click handlers for window classes with the CS_DBLCLKS flag.
|
||||
// 2017-10-23: Inputs: Added WM_SYSKEYDOWN / WM_SYSKEYUP handlers so e.g. the VK_MENU key can be read.
|
||||
// 2017-10-23: Inputs: Using Win32 ::SetCapture/::GetCapture() to retrieve mouse positions outside the client area when dragging.
|
||||
// 2016-11-12: Inputs: Only call Win32 ::SetCursor(nullptr) when io.MouseDrawCursor is set.
|
||||
|
||||
#include "imgui.h"
|
||||
#ifndef IMGUI_DISABLE
|
||||
#include "imgui_impl_win32.h"
|
||||
#ifndef WIN32_LEAN_AND_MEAN
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
#endif
|
||||
#include <windows.h>
|
||||
#include <windowsx.h> // GET_X_LPARAM(), GET_Y_LPARAM()
|
||||
#include <tchar.h>
|
||||
#include <dwmapi.h>
|
||||
|
||||
// Using XInput for gamepad (will load DLL dynamically)
|
||||
#ifndef IMGUI_IMPL_WIN32_DISABLE_GAMEPAD
|
||||
#include <xinput.h>
|
||||
typedef DWORD(WINAPI* PFN_XInputGetCapabilities)(DWORD, DWORD, XINPUT_CAPABILITIES*);
|
||||
typedef DWORD(WINAPI* PFN_XInputGetState)(DWORD, XINPUT_STATE*);
|
||||
#endif
|
||||
|
||||
// Clang/GCC warnings with -Weverything
|
||||
#if defined(__clang__)
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Wcast-function-type" // warning: cast between incompatible function types (for loader)
|
||||
#endif
|
||||
#if defined(__GNUC__)
|
||||
#pragma GCC diagnostic push
|
||||
#pragma GCC diagnostic ignored "-Wpragmas" // warning: unknown option after '#pragma GCC diagnostic' kind
|
||||
#pragma GCC diagnostic ignored "-Wcast-function-type" // warning: cast between incompatible function types (for loader)
|
||||
#endif
|
||||
|
||||
struct ImGui_ImplWin32_Data
|
||||
{
|
||||
HWND hWnd;
|
||||
HWND MouseHwnd;
|
||||
int MouseTrackedArea; // 0: not tracked, 1: client area, 2: non-client area
|
||||
int MouseButtonsDown;
|
||||
INT64 Time;
|
||||
INT64 TicksPerSecond;
|
||||
ImGuiMouseCursor LastMouseCursor;
|
||||
UINT32 KeyboardCodePage;
|
||||
|
||||
#ifndef IMGUI_IMPL_WIN32_DISABLE_GAMEPAD
|
||||
bool HasGamepad;
|
||||
bool WantUpdateHasGamepad;
|
||||
HMODULE XInputDLL;
|
||||
PFN_XInputGetCapabilities XInputGetCapabilities;
|
||||
PFN_XInputGetState XInputGetState;
|
||||
#endif
|
||||
|
||||
ImGui_ImplWin32_Data() { memset((void*)this, 0, sizeof(*this)); }
|
||||
};
|
||||
|
||||
// Backend data stored in io.BackendPlatformUserData to allow support for multiple Dear ImGui contexts
|
||||
// It is STRONGLY preferred that you use docking branch with multi-viewports (== single Dear ImGui context + multiple windows) instead of multiple Dear ImGui contexts.
|
||||
// FIXME: multi-context support is not well tested and probably dysfunctional in this backend.
|
||||
// FIXME: some shared resources (mouse cursor shape, gamepad) are mishandled when using multi-context.
|
||||
static ImGui_ImplWin32_Data* ImGui_ImplWin32_GetBackendData()
|
||||
{
|
||||
return ImGui::GetCurrentContext() ? (ImGui_ImplWin32_Data*)ImGui::GetIO().BackendPlatformUserData : nullptr;
|
||||
}
|
||||
static ImGui_ImplWin32_Data* ImGui_ImplWin32_GetBackendData(ImGuiIO& io)
|
||||
{
|
||||
return (ImGui_ImplWin32_Data*)io.BackendPlatformUserData;
|
||||
}
|
||||
|
||||
// Functions
|
||||
static void ImGui_ImplWin32_UpdateKeyboardCodePage(ImGuiIO& io)
|
||||
{
|
||||
// Retrieve keyboard code page, required for handling of non-Unicode Windows.
|
||||
ImGui_ImplWin32_Data* bd = ImGui_ImplWin32_GetBackendData(io);
|
||||
HKL keyboard_layout = ::GetKeyboardLayout(0);
|
||||
LCID keyboard_lcid = MAKELCID(HIWORD(keyboard_layout), SORT_DEFAULT);
|
||||
if (::GetLocaleInfoA(keyboard_lcid, (LOCALE_RETURN_NUMBER | LOCALE_IDEFAULTANSICODEPAGE), (LPSTR)&bd->KeyboardCodePage, sizeof(bd->KeyboardCodePage)) == 0)
|
||||
bd->KeyboardCodePage = CP_ACP; // Fallback to default ANSI code page when fails.
|
||||
}
|
||||
|
||||
static bool ImGui_ImplWin32_InitEx(void* hwnd, bool platform_has_own_dc)
|
||||
{
|
||||
ImGuiIO& io = ImGui::GetIO();
|
||||
IMGUI_CHECKVERSION();
|
||||
IM_ASSERT(io.BackendPlatformUserData == nullptr && "Already initialized a platform backend!");
|
||||
|
||||
INT64 perf_frequency, perf_counter;
|
||||
if (!::QueryPerformanceFrequency((LARGE_INTEGER*)&perf_frequency))
|
||||
return false;
|
||||
if (!::QueryPerformanceCounter((LARGE_INTEGER*)&perf_counter))
|
||||
return false;
|
||||
|
||||
// Setup backend capabilities flags
|
||||
ImGui_ImplWin32_Data* bd = IM_NEW(ImGui_ImplWin32_Data)();
|
||||
io.BackendPlatformUserData = (void*)bd;
|
||||
io.BackendPlatformName = "imgui_impl_win32";
|
||||
io.BackendFlags |= ImGuiBackendFlags_HasMouseCursors; // We can honor GetMouseCursor() values (optional)
|
||||
io.BackendFlags |= ImGuiBackendFlags_HasSetMousePos; // We can honor io.WantSetMousePos requests (optional, rarely used)
|
||||
|
||||
bd->hWnd = (HWND)hwnd;
|
||||
bd->TicksPerSecond = perf_frequency;
|
||||
bd->Time = perf_counter;
|
||||
bd->LastMouseCursor = ImGuiMouseCursor_COUNT;
|
||||
ImGui_ImplWin32_UpdateKeyboardCodePage(io);
|
||||
|
||||
// Set platform dependent data in viewport
|
||||
ImGuiViewport* main_viewport = ImGui::GetMainViewport();
|
||||
main_viewport->PlatformHandle = main_viewport->PlatformHandleRaw = (void*)bd->hWnd;
|
||||
IM_UNUSED(platform_has_own_dc); // Used in 'docking' branch
|
||||
|
||||
// Dynamically load XInput library
|
||||
#ifndef IMGUI_IMPL_WIN32_DISABLE_GAMEPAD
|
||||
bd->WantUpdateHasGamepad = true;
|
||||
const char* xinput_dll_names[] =
|
||||
{
|
||||
"xinput1_4.dll", // Windows 8+
|
||||
"xinput1_3.dll", // DirectX SDK
|
||||
"xinput9_1_0.dll", // Windows Vista, Windows 7
|
||||
"xinput1_2.dll", // DirectX SDK
|
||||
"xinput1_1.dll" // DirectX SDK
|
||||
};
|
||||
for (int n = 0; n < IM_ARRAYSIZE(xinput_dll_names); n++)
|
||||
if (HMODULE dll = ::LoadLibraryA(xinput_dll_names[n]))
|
||||
{
|
||||
bd->XInputDLL = dll;
|
||||
bd->XInputGetCapabilities = (PFN_XInputGetCapabilities)::GetProcAddress(dll, "XInputGetCapabilities");
|
||||
bd->XInputGetState = (PFN_XInputGetState)::GetProcAddress(dll, "XInputGetState");
|
||||
break;
|
||||
}
|
||||
#endif // IMGUI_IMPL_WIN32_DISABLE_GAMEPAD
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
IMGUI_IMPL_API bool ImGui_ImplWin32_Init(void* hwnd)
|
||||
{
|
||||
return ImGui_ImplWin32_InitEx(hwnd, false);
|
||||
}
|
||||
|
||||
IMGUI_IMPL_API bool ImGui_ImplWin32_InitForOpenGL(void* hwnd)
|
||||
{
|
||||
// OpenGL needs CS_OWNDC
|
||||
return ImGui_ImplWin32_InitEx(hwnd, true);
|
||||
}
|
||||
|
||||
void ImGui_ImplWin32_Shutdown()
|
||||
{
|
||||
ImGui_ImplWin32_Data* bd = ImGui_ImplWin32_GetBackendData();
|
||||
IM_ASSERT(bd != nullptr && "No platform backend to shutdown, or already shutdown?");
|
||||
ImGuiIO& io = ImGui::GetIO();
|
||||
|
||||
// Unload XInput library
|
||||
#ifndef IMGUI_IMPL_WIN32_DISABLE_GAMEPAD
|
||||
if (bd->XInputDLL)
|
||||
::FreeLibrary(bd->XInputDLL);
|
||||
#endif // IMGUI_IMPL_WIN32_DISABLE_GAMEPAD
|
||||
|
||||
io.BackendPlatformName = nullptr;
|
||||
io.BackendPlatformUserData = nullptr;
|
||||
io.BackendFlags &= ~(ImGuiBackendFlags_HasMouseCursors | ImGuiBackendFlags_HasSetMousePos | ImGuiBackendFlags_HasGamepad);
|
||||
IM_DELETE(bd);
|
||||
}
|
||||
|
||||
static bool ImGui_ImplWin32_UpdateMouseCursor(ImGuiIO& io, ImGuiMouseCursor imgui_cursor)
|
||||
{
|
||||
if (io.ConfigFlags & ImGuiConfigFlags_NoMouseCursorChange)
|
||||
return false;
|
||||
|
||||
if (imgui_cursor == ImGuiMouseCursor_None || io.MouseDrawCursor)
|
||||
{
|
||||
// Hide OS mouse cursor if imgui is drawing it or if it wants no cursor
|
||||
::SetCursor(nullptr);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Show OS mouse cursor
|
||||
LPTSTR win32_cursor = IDC_ARROW;
|
||||
switch (imgui_cursor)
|
||||
{
|
||||
case ImGuiMouseCursor_Arrow: win32_cursor = IDC_ARROW; break;
|
||||
case ImGuiMouseCursor_TextInput: win32_cursor = IDC_IBEAM; break;
|
||||
case ImGuiMouseCursor_ResizeAll: win32_cursor = IDC_SIZEALL; break;
|
||||
case ImGuiMouseCursor_ResizeEW: win32_cursor = IDC_SIZEWE; break;
|
||||
case ImGuiMouseCursor_ResizeNS: win32_cursor = IDC_SIZENS; break;
|
||||
case ImGuiMouseCursor_ResizeNESW: win32_cursor = IDC_SIZENESW; break;
|
||||
case ImGuiMouseCursor_ResizeNWSE: win32_cursor = IDC_SIZENWSE; break;
|
||||
case ImGuiMouseCursor_Hand: win32_cursor = IDC_HAND; break;
|
||||
case ImGuiMouseCursor_NotAllowed: win32_cursor = IDC_NO; break;
|
||||
}
|
||||
::SetCursor(::LoadCursor(nullptr, win32_cursor));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool IsVkDown(int vk)
|
||||
{
|
||||
return (::GetKeyState(vk) & 0x8000) != 0;
|
||||
}
|
||||
|
||||
static void ImGui_ImplWin32_AddKeyEvent(ImGuiIO& io, ImGuiKey key, bool down, int native_keycode, int native_scancode = -1)
|
||||
{
|
||||
io.AddKeyEvent(key, down);
|
||||
io.SetKeyEventNativeData(key, native_keycode, native_scancode); // To support legacy indexing (<1.87 user code)
|
||||
IM_UNUSED(native_scancode);
|
||||
}
|
||||
|
||||
static void ImGui_ImplWin32_ProcessKeyEventsWorkarounds(ImGuiIO& io)
|
||||
{
|
||||
// Left & right Shift keys: when both are pressed together, Windows tend to not generate the WM_KEYUP event for the first released one.
|
||||
if (ImGui::IsKeyDown(ImGuiKey_LeftShift) && !IsVkDown(VK_LSHIFT))
|
||||
ImGui_ImplWin32_AddKeyEvent(io, ImGuiKey_LeftShift, false, VK_LSHIFT);
|
||||
if (ImGui::IsKeyDown(ImGuiKey_RightShift) && !IsVkDown(VK_RSHIFT))
|
||||
ImGui_ImplWin32_AddKeyEvent(io, ImGuiKey_RightShift, false, VK_RSHIFT);
|
||||
|
||||
// Sometimes WM_KEYUP for Win key is not passed down to the app (e.g. for Win+V on some setups, according to GLFW).
|
||||
if (ImGui::IsKeyDown(ImGuiKey_LeftSuper) && !IsVkDown(VK_LWIN))
|
||||
ImGui_ImplWin32_AddKeyEvent(io, ImGuiKey_LeftSuper, false, VK_LWIN);
|
||||
if (ImGui::IsKeyDown(ImGuiKey_RightSuper) && !IsVkDown(VK_RWIN))
|
||||
ImGui_ImplWin32_AddKeyEvent(io, ImGuiKey_RightSuper, false, VK_RWIN);
|
||||
}
|
||||
|
||||
static void ImGui_ImplWin32_UpdateKeyModifiers(ImGuiIO& io)
|
||||
{
|
||||
io.AddKeyEvent(ImGuiMod_Ctrl, IsVkDown(VK_CONTROL));
|
||||
io.AddKeyEvent(ImGuiMod_Shift, IsVkDown(VK_SHIFT));
|
||||
io.AddKeyEvent(ImGuiMod_Alt, IsVkDown(VK_MENU));
|
||||
io.AddKeyEvent(ImGuiMod_Super, IsVkDown(VK_LWIN) || IsVkDown(VK_RWIN));
|
||||
}
|
||||
|
||||
static void ImGui_ImplWin32_UpdateMouseData(ImGuiIO& io)
|
||||
{
|
||||
ImGui_ImplWin32_Data* bd = ImGui_ImplWin32_GetBackendData(io);
|
||||
IM_ASSERT(bd->hWnd != 0);
|
||||
|
||||
HWND focused_window = ::GetForegroundWindow();
|
||||
const bool is_app_focused = (focused_window == bd->hWnd);
|
||||
if (is_app_focused)
|
||||
{
|
||||
// (Optional) Set OS mouse position from Dear ImGui if requested (rarely used, only when io.ConfigNavMoveSetMousePos is enabled by user)
|
||||
if (io.WantSetMousePos)
|
||||
{
|
||||
POINT pos = { (int)io.MousePos.x, (int)io.MousePos.y };
|
||||
if (::ClientToScreen(bd->hWnd, &pos))
|
||||
::SetCursorPos(pos.x, pos.y);
|
||||
}
|
||||
|
||||
// (Optional) Fallback to provide mouse position when focused (WM_MOUSEMOVE already provides this when hovered or captured)
|
||||
// This also fills a short gap when clicking non-client area: WM_NCMOUSELEAVE -> modal OS move -> gap -> WM_NCMOUSEMOVE
|
||||
if (!io.WantSetMousePos && bd->MouseTrackedArea == 0)
|
||||
{
|
||||
POINT pos;
|
||||
if (::GetCursorPos(&pos) && ::ScreenToClient(bd->hWnd, &pos))
|
||||
io.AddMousePosEvent((float)pos.x, (float)pos.y);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Gamepad navigation mapping
|
||||
static void ImGui_ImplWin32_UpdateGamepads(ImGuiIO& io)
|
||||
{
|
||||
#ifndef IMGUI_IMPL_WIN32_DISABLE_GAMEPAD
|
||||
ImGui_ImplWin32_Data* bd = ImGui_ImplWin32_GetBackendData(io);
|
||||
//if ((io.ConfigFlags & ImGuiConfigFlags_NavEnableGamepad) == 0) // FIXME: Technically feeding gamepad shouldn't depend on this now that they are regular inputs.
|
||||
// return;
|
||||
|
||||
// Calling XInputGetState() every frame on disconnected gamepads is unfortunately too slow.
|
||||
// Instead we refresh gamepad availability by calling XInputGetCapabilities() _only_ after receiving WM_DEVICECHANGE.
|
||||
if (bd->WantUpdateHasGamepad)
|
||||
{
|
||||
XINPUT_CAPABILITIES caps = {};
|
||||
bd->HasGamepad = bd->XInputGetCapabilities ? (bd->XInputGetCapabilities(0, XINPUT_FLAG_GAMEPAD, &caps) == ERROR_SUCCESS) : false;
|
||||
bd->WantUpdateHasGamepad = false;
|
||||
}
|
||||
|
||||
io.BackendFlags &= ~ImGuiBackendFlags_HasGamepad;
|
||||
XINPUT_STATE xinput_state;
|
||||
XINPUT_GAMEPAD& gamepad = xinput_state.Gamepad;
|
||||
if (!bd->HasGamepad || bd->XInputGetState == nullptr || bd->XInputGetState(0, &xinput_state) != ERROR_SUCCESS)
|
||||
return;
|
||||
io.BackendFlags |= ImGuiBackendFlags_HasGamepad;
|
||||
|
||||
#define IM_SATURATE(V) (V < 0.0f ? 0.0f : V > 1.0f ? 1.0f : V)
|
||||
#define MAP_BUTTON(KEY_NO, BUTTON_ENUM) { io.AddKeyEvent(KEY_NO, (gamepad.wButtons & BUTTON_ENUM) != 0); }
|
||||
#define MAP_ANALOG(KEY_NO, VALUE, V0, V1) { float vn = (float)(VALUE - V0) / (float)(V1 - V0); io.AddKeyAnalogEvent(KEY_NO, vn > 0.10f, IM_SATURATE(vn)); }
|
||||
MAP_BUTTON(ImGuiKey_GamepadStart, XINPUT_GAMEPAD_START);
|
||||
MAP_BUTTON(ImGuiKey_GamepadBack, XINPUT_GAMEPAD_BACK);
|
||||
MAP_BUTTON(ImGuiKey_GamepadFaceLeft, XINPUT_GAMEPAD_X);
|
||||
MAP_BUTTON(ImGuiKey_GamepadFaceRight, XINPUT_GAMEPAD_B);
|
||||
MAP_BUTTON(ImGuiKey_GamepadFaceUp, XINPUT_GAMEPAD_Y);
|
||||
MAP_BUTTON(ImGuiKey_GamepadFaceDown, XINPUT_GAMEPAD_A);
|
||||
MAP_BUTTON(ImGuiKey_GamepadDpadLeft, XINPUT_GAMEPAD_DPAD_LEFT);
|
||||
MAP_BUTTON(ImGuiKey_GamepadDpadRight, XINPUT_GAMEPAD_DPAD_RIGHT);
|
||||
MAP_BUTTON(ImGuiKey_GamepadDpadUp, XINPUT_GAMEPAD_DPAD_UP);
|
||||
MAP_BUTTON(ImGuiKey_GamepadDpadDown, XINPUT_GAMEPAD_DPAD_DOWN);
|
||||
MAP_BUTTON(ImGuiKey_GamepadL1, XINPUT_GAMEPAD_LEFT_SHOULDER);
|
||||
MAP_BUTTON(ImGuiKey_GamepadR1, XINPUT_GAMEPAD_RIGHT_SHOULDER);
|
||||
MAP_ANALOG(ImGuiKey_GamepadL2, gamepad.bLeftTrigger, XINPUT_GAMEPAD_TRIGGER_THRESHOLD, 255);
|
||||
MAP_ANALOG(ImGuiKey_GamepadR2, gamepad.bRightTrigger, XINPUT_GAMEPAD_TRIGGER_THRESHOLD, 255);
|
||||
MAP_BUTTON(ImGuiKey_GamepadL3, XINPUT_GAMEPAD_LEFT_THUMB);
|
||||
MAP_BUTTON(ImGuiKey_GamepadR3, XINPUT_GAMEPAD_RIGHT_THUMB);
|
||||
MAP_ANALOG(ImGuiKey_GamepadLStickLeft, gamepad.sThumbLX, -XINPUT_GAMEPAD_LEFT_THUMB_DEADZONE, -32768);
|
||||
MAP_ANALOG(ImGuiKey_GamepadLStickRight, gamepad.sThumbLX, +XINPUT_GAMEPAD_LEFT_THUMB_DEADZONE, +32767);
|
||||
MAP_ANALOG(ImGuiKey_GamepadLStickUp, gamepad.sThumbLY, +XINPUT_GAMEPAD_LEFT_THUMB_DEADZONE, +32767);
|
||||
MAP_ANALOG(ImGuiKey_GamepadLStickDown, gamepad.sThumbLY, -XINPUT_GAMEPAD_LEFT_THUMB_DEADZONE, -32768);
|
||||
MAP_ANALOG(ImGuiKey_GamepadRStickLeft, gamepad.sThumbRX, -XINPUT_GAMEPAD_LEFT_THUMB_DEADZONE, -32768);
|
||||
MAP_ANALOG(ImGuiKey_GamepadRStickRight, gamepad.sThumbRX, +XINPUT_GAMEPAD_LEFT_THUMB_DEADZONE, +32767);
|
||||
MAP_ANALOG(ImGuiKey_GamepadRStickUp, gamepad.sThumbRY, +XINPUT_GAMEPAD_LEFT_THUMB_DEADZONE, +32767);
|
||||
MAP_ANALOG(ImGuiKey_GamepadRStickDown, gamepad.sThumbRY, -XINPUT_GAMEPAD_LEFT_THUMB_DEADZONE, -32768);
|
||||
#undef MAP_BUTTON
|
||||
#undef MAP_ANALOG
|
||||
#else // #ifndef IMGUI_IMPL_WIN32_DISABLE_GAMEPAD
|
||||
IM_UNUSED(io);
|
||||
#endif
|
||||
}
|
||||
|
||||
void ImGui_ImplWin32_NewFrame()
|
||||
{
|
||||
ImGui_ImplWin32_Data* bd = ImGui_ImplWin32_GetBackendData();
|
||||
IM_ASSERT(bd != nullptr && "Context or backend not initialized? Did you call ImGui_ImplWin32_Init()?");
|
||||
ImGuiIO& io = ImGui::GetIO();
|
||||
|
||||
// Setup display size (every frame to accommodate for window resizing)
|
||||
RECT rect = { 0, 0, 0, 0 };
|
||||
::GetClientRect(bd->hWnd, &rect);
|
||||
io.DisplaySize = ImVec2((float)(rect.right - rect.left), (float)(rect.bottom - rect.top));
|
||||
|
||||
// Setup time step
|
||||
INT64 current_time = 0;
|
||||
::QueryPerformanceCounter((LARGE_INTEGER*)¤t_time);
|
||||
io.DeltaTime = (float)(current_time - bd->Time) / bd->TicksPerSecond;
|
||||
bd->Time = current_time;
|
||||
|
||||
// Update OS mouse position
|
||||
ImGui_ImplWin32_UpdateMouseData(io);
|
||||
|
||||
// Process workarounds for known Windows key handling issues
|
||||
ImGui_ImplWin32_ProcessKeyEventsWorkarounds(io);
|
||||
|
||||
// Update OS mouse cursor with the cursor requested by imgui
|
||||
ImGuiMouseCursor mouse_cursor = io.MouseDrawCursor ? ImGuiMouseCursor_None : ImGui::GetMouseCursor();
|
||||
if (bd->LastMouseCursor != mouse_cursor)
|
||||
{
|
||||
bd->LastMouseCursor = mouse_cursor;
|
||||
ImGui_ImplWin32_UpdateMouseCursor(io, mouse_cursor);
|
||||
}
|
||||
|
||||
// Update game controllers (if enabled and available)
|
||||
ImGui_ImplWin32_UpdateGamepads(io);
|
||||
}
|
||||
|
||||
// Map VK_xxx to ImGuiKey_xxx.
|
||||
// Not static to allow third-party code to use that if they want to (but undocumented)
|
||||
ImGuiKey ImGui_ImplWin32_KeyEventToImGuiKey(WPARAM wParam, LPARAM lParam);
|
||||
ImGuiKey ImGui_ImplWin32_KeyEventToImGuiKey(WPARAM wParam, LPARAM lParam)
|
||||
{
|
||||
// There is no distinct VK_xxx for keypad enter, instead it is VK_RETURN + KF_EXTENDED.
|
||||
if ((wParam == VK_RETURN) && (HIWORD(lParam) & KF_EXTENDED))
|
||||
return ImGuiKey_KeypadEnter;
|
||||
|
||||
switch (wParam)
|
||||
{
|
||||
case VK_TAB: return ImGuiKey_Tab;
|
||||
case VK_LEFT: return ImGuiKey_LeftArrow;
|
||||
case VK_RIGHT: return ImGuiKey_RightArrow;
|
||||
case VK_UP: return ImGuiKey_UpArrow;
|
||||
case VK_DOWN: return ImGuiKey_DownArrow;
|
||||
case VK_PRIOR: return ImGuiKey_PageUp;
|
||||
case VK_NEXT: return ImGuiKey_PageDown;
|
||||
case VK_HOME: return ImGuiKey_Home;
|
||||
case VK_END: return ImGuiKey_End;
|
||||
case VK_INSERT: return ImGuiKey_Insert;
|
||||
case VK_DELETE: return ImGuiKey_Delete;
|
||||
case VK_BACK: return ImGuiKey_Backspace;
|
||||
case VK_SPACE: return ImGuiKey_Space;
|
||||
case VK_RETURN: return ImGuiKey_Enter;
|
||||
case VK_ESCAPE: return ImGuiKey_Escape;
|
||||
case VK_OEM_7: return ImGuiKey_Apostrophe;
|
||||
case VK_OEM_COMMA: return ImGuiKey_Comma;
|
||||
case VK_OEM_MINUS: return ImGuiKey_Minus;
|
||||
case VK_OEM_PERIOD: return ImGuiKey_Period;
|
||||
case VK_OEM_2: return ImGuiKey_Slash;
|
||||
case VK_OEM_1: return ImGuiKey_Semicolon;
|
||||
case VK_OEM_PLUS: return ImGuiKey_Equal;
|
||||
case VK_OEM_4: return ImGuiKey_LeftBracket;
|
||||
case VK_OEM_5: return ImGuiKey_Backslash;
|
||||
case VK_OEM_6: return ImGuiKey_RightBracket;
|
||||
case VK_OEM_3: return ImGuiKey_GraveAccent;
|
||||
case VK_CAPITAL: return ImGuiKey_CapsLock;
|
||||
case VK_SCROLL: return ImGuiKey_ScrollLock;
|
||||
case VK_NUMLOCK: return ImGuiKey_NumLock;
|
||||
case VK_SNAPSHOT: return ImGuiKey_PrintScreen;
|
||||
case VK_PAUSE: return ImGuiKey_Pause;
|
||||
case VK_NUMPAD0: return ImGuiKey_Keypad0;
|
||||
case VK_NUMPAD1: return ImGuiKey_Keypad1;
|
||||
case VK_NUMPAD2: return ImGuiKey_Keypad2;
|
||||
case VK_NUMPAD3: return ImGuiKey_Keypad3;
|
||||
case VK_NUMPAD4: return ImGuiKey_Keypad4;
|
||||
case VK_NUMPAD5: return ImGuiKey_Keypad5;
|
||||
case VK_NUMPAD6: return ImGuiKey_Keypad6;
|
||||
case VK_NUMPAD7: return ImGuiKey_Keypad7;
|
||||
case VK_NUMPAD8: return ImGuiKey_Keypad8;
|
||||
case VK_NUMPAD9: return ImGuiKey_Keypad9;
|
||||
case VK_DECIMAL: return ImGuiKey_KeypadDecimal;
|
||||
case VK_DIVIDE: return ImGuiKey_KeypadDivide;
|
||||
case VK_MULTIPLY: return ImGuiKey_KeypadMultiply;
|
||||
case VK_SUBTRACT: return ImGuiKey_KeypadSubtract;
|
||||
case VK_ADD: return ImGuiKey_KeypadAdd;
|
||||
case VK_LSHIFT: return ImGuiKey_LeftShift;
|
||||
case VK_LCONTROL: return ImGuiKey_LeftCtrl;
|
||||
case VK_LMENU: return ImGuiKey_LeftAlt;
|
||||
case VK_LWIN: return ImGuiKey_LeftSuper;
|
||||
case VK_RSHIFT: return ImGuiKey_RightShift;
|
||||
case VK_RCONTROL: return ImGuiKey_RightCtrl;
|
||||
case VK_RMENU: return ImGuiKey_RightAlt;
|
||||
case VK_RWIN: return ImGuiKey_RightSuper;
|
||||
case VK_APPS: return ImGuiKey_Menu;
|
||||
case '0': return ImGuiKey_0;
|
||||
case '1': return ImGuiKey_1;
|
||||
case '2': return ImGuiKey_2;
|
||||
case '3': return ImGuiKey_3;
|
||||
case '4': return ImGuiKey_4;
|
||||
case '5': return ImGuiKey_5;
|
||||
case '6': return ImGuiKey_6;
|
||||
case '7': return ImGuiKey_7;
|
||||
case '8': return ImGuiKey_8;
|
||||
case '9': return ImGuiKey_9;
|
||||
case 'A': return ImGuiKey_A;
|
||||
case 'B': return ImGuiKey_B;
|
||||
case 'C': return ImGuiKey_C;
|
||||
case 'D': return ImGuiKey_D;
|
||||
case 'E': return ImGuiKey_E;
|
||||
case 'F': return ImGuiKey_F;
|
||||
case 'G': return ImGuiKey_G;
|
||||
case 'H': return ImGuiKey_H;
|
||||
case 'I': return ImGuiKey_I;
|
||||
case 'J': return ImGuiKey_J;
|
||||
case 'K': return ImGuiKey_K;
|
||||
case 'L': return ImGuiKey_L;
|
||||
case 'M': return ImGuiKey_M;
|
||||
case 'N': return ImGuiKey_N;
|
||||
case 'O': return ImGuiKey_O;
|
||||
case 'P': return ImGuiKey_P;
|
||||
case 'Q': return ImGuiKey_Q;
|
||||
case 'R': return ImGuiKey_R;
|
||||
case 'S': return ImGuiKey_S;
|
||||
case 'T': return ImGuiKey_T;
|
||||
case 'U': return ImGuiKey_U;
|
||||
case 'V': return ImGuiKey_V;
|
||||
case 'W': return ImGuiKey_W;
|
||||
case 'X': return ImGuiKey_X;
|
||||
case 'Y': return ImGuiKey_Y;
|
||||
case 'Z': return ImGuiKey_Z;
|
||||
case VK_F1: return ImGuiKey_F1;
|
||||
case VK_F2: return ImGuiKey_F2;
|
||||
case VK_F3: return ImGuiKey_F3;
|
||||
case VK_F4: return ImGuiKey_F4;
|
||||
case VK_F5: return ImGuiKey_F5;
|
||||
case VK_F6: return ImGuiKey_F6;
|
||||
case VK_F7: return ImGuiKey_F7;
|
||||
case VK_F8: return ImGuiKey_F8;
|
||||
case VK_F9: return ImGuiKey_F9;
|
||||
case VK_F10: return ImGuiKey_F10;
|
||||
case VK_F11: return ImGuiKey_F11;
|
||||
case VK_F12: return ImGuiKey_F12;
|
||||
case VK_F13: return ImGuiKey_F13;
|
||||
case VK_F14: return ImGuiKey_F14;
|
||||
case VK_F15: return ImGuiKey_F15;
|
||||
case VK_F16: return ImGuiKey_F16;
|
||||
case VK_F17: return ImGuiKey_F17;
|
||||
case VK_F18: return ImGuiKey_F18;
|
||||
case VK_F19: return ImGuiKey_F19;
|
||||
case VK_F20: return ImGuiKey_F20;
|
||||
case VK_F21: return ImGuiKey_F21;
|
||||
case VK_F22: return ImGuiKey_F22;
|
||||
case VK_F23: return ImGuiKey_F23;
|
||||
case VK_F24: return ImGuiKey_F24;
|
||||
case VK_BROWSER_BACK: return ImGuiKey_AppBack;
|
||||
case VK_BROWSER_FORWARD: return ImGuiKey_AppForward;
|
||||
default: return ImGuiKey_None;
|
||||
}
|
||||
}
|
||||
|
||||
// Allow compilation with old Windows SDK. MinGW doesn't have default _WIN32_WINNT/WINVER versions.
|
||||
#ifndef WM_MOUSEHWHEEL
|
||||
#define WM_MOUSEHWHEEL 0x020E
|
||||
#endif
|
||||
#ifndef DBT_DEVNODES_CHANGED
|
||||
#define DBT_DEVNODES_CHANGED 0x0007
|
||||
#endif
|
||||
|
||||
// Helper to obtain the source of mouse messages.
|
||||
// See https://learn.microsoft.com/en-us/windows/win32/tablet/system-events-and-mouse-messages
|
||||
// Prefer to call this at the top of the message handler to avoid the possibility of other Win32 calls interfering with this.
|
||||
static ImGuiMouseSource ImGui_ImplWin32_GetMouseSourceFromMessageExtraInfo()
|
||||
{
|
||||
LPARAM extra_info = ::GetMessageExtraInfo();
|
||||
if ((extra_info & 0xFFFFFF80) == 0xFF515700)
|
||||
return ImGuiMouseSource_Pen;
|
||||
if ((extra_info & 0xFFFFFF80) == 0xFF515780)
|
||||
return ImGuiMouseSource_TouchScreen;
|
||||
return ImGuiMouseSource_Mouse;
|
||||
}
|
||||
|
||||
// Win32 message handler (process Win32 mouse/keyboard inputs, etc.)
|
||||
// Call from your application's message handler. Keep calling your message handler unless this function returns TRUE.
|
||||
// When implementing your own backend, you can read the io.WantCaptureMouse, io.WantCaptureKeyboard flags to tell if Dear ImGui wants to use your inputs.
|
||||
// - When io.WantCaptureMouse is true, do not dispatch mouse input data to your main application, or clear/overwrite your copy of the mouse data.
|
||||
// - When io.WantCaptureKeyboard is true, do not dispatch keyboard input data to your main application, or clear/overwrite your copy of the keyboard data.
|
||||
// Generally you may always pass all inputs to Dear ImGui, and hide them from your application based on those two flags.
|
||||
// PS: We treat DBLCLK messages as regular mouse down messages, so this code will work on windows classes that have the CS_DBLCLKS flag set. Our own example app code doesn't set this flag.
|
||||
|
||||
// Copy either line into your .cpp file to forward declare the function:
|
||||
extern IMGUI_IMPL_API LRESULT ImGui_ImplWin32_WndProcHandler(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam); // Use ImGui::GetCurrentContext()
|
||||
extern IMGUI_IMPL_API LRESULT ImGui_ImplWin32_WndProcHandlerEx(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam, ImGuiIO& io); // Doesn't use ImGui::GetCurrentContext()
|
||||
|
||||
IMGUI_IMPL_API LRESULT ImGui_ImplWin32_WndProcHandler(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
|
||||
{
|
||||
// Most backends don't have silent checks like this one, but we need it because WndProc are called early in CreateWindow().
|
||||
// We silently allow both context or just only backend data to be nullptr.
|
||||
if (ImGui::GetCurrentContext() == NULL)
|
||||
return 0;
|
||||
return ImGui_ImplWin32_WndProcHandlerEx(hwnd, msg, wParam, lParam, ImGui::GetIO());
|
||||
}
|
||||
|
||||
// This version is in theory thread-safe in the sense that no path should access ImGui::GetCurrentContext().
|
||||
IMGUI_IMPL_API LRESULT ImGui_ImplWin32_WndProcHandlerEx(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam, ImGuiIO& io)
|
||||
{
|
||||
ImGui_ImplWin32_Data* bd = ImGui_ImplWin32_GetBackendData(io);
|
||||
if (bd == NULL)
|
||||
return 0;
|
||||
switch (msg)
|
||||
{
|
||||
case WM_MOUSEMOVE:
|
||||
case WM_NCMOUSEMOVE:
|
||||
{
|
||||
// We need to call TrackMouseEvent in order to receive WM_MOUSELEAVE events
|
||||
ImGuiMouseSource mouse_source = ImGui_ImplWin32_GetMouseSourceFromMessageExtraInfo();
|
||||
const int area = (msg == WM_MOUSEMOVE) ? 1 : 2;
|
||||
bd->MouseHwnd = hwnd;
|
||||
if (bd->MouseTrackedArea != area)
|
||||
{
|
||||
TRACKMOUSEEVENT tme_cancel = { sizeof(tme_cancel), TME_CANCEL, hwnd, 0 };
|
||||
TRACKMOUSEEVENT tme_track = { sizeof(tme_track), (DWORD)((area == 2) ? (TME_LEAVE | TME_NONCLIENT) : TME_LEAVE), hwnd, 0 };
|
||||
if (bd->MouseTrackedArea != 0)
|
||||
::TrackMouseEvent(&tme_cancel);
|
||||
::TrackMouseEvent(&tme_track);
|
||||
bd->MouseTrackedArea = area;
|
||||
}
|
||||
POINT mouse_pos = { (LONG)GET_X_LPARAM(lParam), (LONG)GET_Y_LPARAM(lParam) };
|
||||
if (msg == WM_NCMOUSEMOVE && ::ScreenToClient(hwnd, &mouse_pos) == FALSE) // WM_NCMOUSEMOVE are provided in absolute coordinates.
|
||||
return 0;
|
||||
io.AddMouseSourceEvent(mouse_source);
|
||||
io.AddMousePosEvent((float)mouse_pos.x, (float)mouse_pos.y);
|
||||
return 0;
|
||||
}
|
||||
case WM_MOUSELEAVE:
|
||||
case WM_NCMOUSELEAVE:
|
||||
{
|
||||
const int area = (msg == WM_MOUSELEAVE) ? 1 : 2;
|
||||
if (bd->MouseTrackedArea == area)
|
||||
{
|
||||
if (bd->MouseHwnd == hwnd)
|
||||
bd->MouseHwnd = nullptr;
|
||||
bd->MouseTrackedArea = 0;
|
||||
io.AddMousePosEvent(-FLT_MAX, -FLT_MAX);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
case WM_DESTROY:
|
||||
if (bd->MouseHwnd == hwnd && bd->MouseTrackedArea != 0)
|
||||
{
|
||||
TRACKMOUSEEVENT tme_cancel = { sizeof(tme_cancel), TME_CANCEL, hwnd, 0 };
|
||||
::TrackMouseEvent(&tme_cancel);
|
||||
bd->MouseHwnd = nullptr;
|
||||
bd->MouseTrackedArea = 0;
|
||||
io.AddMousePosEvent(-FLT_MAX, -FLT_MAX);
|
||||
}
|
||||
return 0;
|
||||
case WM_LBUTTONDOWN: case WM_LBUTTONDBLCLK:
|
||||
case WM_RBUTTONDOWN: case WM_RBUTTONDBLCLK:
|
||||
case WM_MBUTTONDOWN: case WM_MBUTTONDBLCLK:
|
||||
case WM_XBUTTONDOWN: case WM_XBUTTONDBLCLK:
|
||||
{
|
||||
ImGuiMouseSource mouse_source = ImGui_ImplWin32_GetMouseSourceFromMessageExtraInfo();
|
||||
int button = 0;
|
||||
if (msg == WM_LBUTTONDOWN || msg == WM_LBUTTONDBLCLK) { button = 0; }
|
||||
if (msg == WM_RBUTTONDOWN || msg == WM_RBUTTONDBLCLK) { button = 1; }
|
||||
if (msg == WM_MBUTTONDOWN || msg == WM_MBUTTONDBLCLK) { button = 2; }
|
||||
if (msg == WM_XBUTTONDOWN || msg == WM_XBUTTONDBLCLK) { button = (GET_XBUTTON_WPARAM(wParam) == XBUTTON1) ? 3 : 4; }
|
||||
if (bd->MouseButtonsDown == 0 && ::GetCapture() == nullptr)
|
||||
::SetCapture(hwnd); // Allow us to read mouse coordinates when dragging mouse outside of our window bounds.
|
||||
bd->MouseButtonsDown |= 1 << button;
|
||||
io.AddMouseSourceEvent(mouse_source);
|
||||
io.AddMouseButtonEvent(button, true);
|
||||
return 0;
|
||||
}
|
||||
case WM_LBUTTONUP:
|
||||
case WM_RBUTTONUP:
|
||||
case WM_MBUTTONUP:
|
||||
case WM_XBUTTONUP:
|
||||
{
|
||||
ImGuiMouseSource mouse_source = ImGui_ImplWin32_GetMouseSourceFromMessageExtraInfo();
|
||||
int button = 0;
|
||||
if (msg == WM_LBUTTONUP) { button = 0; }
|
||||
if (msg == WM_RBUTTONUP) { button = 1; }
|
||||
if (msg == WM_MBUTTONUP) { button = 2; }
|
||||
if (msg == WM_XBUTTONUP) { button = (GET_XBUTTON_WPARAM(wParam) == XBUTTON1) ? 3 : 4; }
|
||||
bd->MouseButtonsDown &= ~(1 << button);
|
||||
if (bd->MouseButtonsDown == 0 && ::GetCapture() == hwnd)
|
||||
::ReleaseCapture();
|
||||
io.AddMouseSourceEvent(mouse_source);
|
||||
io.AddMouseButtonEvent(button, false);
|
||||
return 0;
|
||||
}
|
||||
case WM_MOUSEWHEEL:
|
||||
io.AddMouseWheelEvent(0.0f, (float)GET_WHEEL_DELTA_WPARAM(wParam) / (float)WHEEL_DELTA);
|
||||
return 0;
|
||||
case WM_MOUSEHWHEEL:
|
||||
io.AddMouseWheelEvent(-(float)GET_WHEEL_DELTA_WPARAM(wParam) / (float)WHEEL_DELTA, 0.0f);
|
||||
return 0;
|
||||
case WM_KEYDOWN:
|
||||
case WM_KEYUP:
|
||||
case WM_SYSKEYDOWN:
|
||||
case WM_SYSKEYUP:
|
||||
{
|
||||
const bool is_key_down = (msg == WM_KEYDOWN || msg == WM_SYSKEYDOWN);
|
||||
if (wParam < 256)
|
||||
{
|
||||
// Submit modifiers
|
||||
ImGui_ImplWin32_UpdateKeyModifiers(io);
|
||||
|
||||
// Obtain virtual key code and convert to ImGuiKey
|
||||
const ImGuiKey key = ImGui_ImplWin32_KeyEventToImGuiKey(wParam, lParam);
|
||||
const int vk = (int)wParam;
|
||||
const int scancode = (int)LOBYTE(HIWORD(lParam));
|
||||
|
||||
// Special behavior for VK_SNAPSHOT / ImGuiKey_PrintScreen as Windows doesn't emit the key down event.
|
||||
if (key == ImGuiKey_PrintScreen && !is_key_down)
|
||||
ImGui_ImplWin32_AddKeyEvent(io, key, true, vk, scancode);
|
||||
|
||||
// Submit key event
|
||||
if (key != ImGuiKey_None)
|
||||
ImGui_ImplWin32_AddKeyEvent(io, key, is_key_down, vk, scancode);
|
||||
|
||||
// Submit individual left/right modifier events
|
||||
if (vk == VK_SHIFT)
|
||||
{
|
||||
// Important: Shift keys tend to get stuck when pressed together, missing key-up events are corrected in ImGui_ImplWin32_ProcessKeyEventsWorkarounds()
|
||||
if (IsVkDown(VK_LSHIFT) == is_key_down) { ImGui_ImplWin32_AddKeyEvent(io, ImGuiKey_LeftShift, is_key_down, VK_LSHIFT, scancode); }
|
||||
if (IsVkDown(VK_RSHIFT) == is_key_down) { ImGui_ImplWin32_AddKeyEvent(io, ImGuiKey_RightShift, is_key_down, VK_RSHIFT, scancode); }
|
||||
}
|
||||
else if (vk == VK_CONTROL)
|
||||
{
|
||||
if (IsVkDown(VK_LCONTROL) == is_key_down) { ImGui_ImplWin32_AddKeyEvent(io, ImGuiKey_LeftCtrl, is_key_down, VK_LCONTROL, scancode); }
|
||||
if (IsVkDown(VK_RCONTROL) == is_key_down) { ImGui_ImplWin32_AddKeyEvent(io, ImGuiKey_RightCtrl, is_key_down, VK_RCONTROL, scancode); }
|
||||
}
|
||||
else if (vk == VK_MENU)
|
||||
{
|
||||
if (IsVkDown(VK_LMENU) == is_key_down) { ImGui_ImplWin32_AddKeyEvent(io, ImGuiKey_LeftAlt, is_key_down, VK_LMENU, scancode); }
|
||||
if (IsVkDown(VK_RMENU) == is_key_down) { ImGui_ImplWin32_AddKeyEvent(io, ImGuiKey_RightAlt, is_key_down, VK_RMENU, scancode); }
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
case WM_SETFOCUS:
|
||||
case WM_KILLFOCUS:
|
||||
io.AddFocusEvent(msg == WM_SETFOCUS);
|
||||
return 0;
|
||||
case WM_INPUTLANGCHANGE:
|
||||
ImGui_ImplWin32_UpdateKeyboardCodePage(io);
|
||||
return 0;
|
||||
case WM_CHAR:
|
||||
if (::IsWindowUnicode(hwnd))
|
||||
{
|
||||
// You can also use ToAscii()+GetKeyboardState() to retrieve characters.
|
||||
if (wParam > 0 && wParam < 0x10000)
|
||||
io.AddInputCharacterUTF16((unsigned short)wParam);
|
||||
}
|
||||
else
|
||||
{
|
||||
wchar_t wch = 0;
|
||||
::MultiByteToWideChar(bd->KeyboardCodePage, MB_PRECOMPOSED, (char*)&wParam, 1, &wch, 1);
|
||||
io.AddInputCharacter(wch);
|
||||
}
|
||||
return 0;
|
||||
case WM_SETCURSOR:
|
||||
// This is required to restore cursor when transitioning from e.g resize borders to client area.
|
||||
if (LOWORD(lParam) == HTCLIENT && ImGui_ImplWin32_UpdateMouseCursor(io, bd->LastMouseCursor))
|
||||
return 1;
|
||||
return 0;
|
||||
case WM_DEVICECHANGE:
|
||||
#ifndef IMGUI_IMPL_WIN32_DISABLE_GAMEPAD
|
||||
if ((UINT)wParam == DBT_DEVNODES_CHANGED)
|
||||
bd->WantUpdateHasGamepad = true;
|
||||
#endif
|
||||
return 0;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
//--------------------------------------------------------------------------------------------------------
|
||||
// DPI-related helpers (optional)
|
||||
//--------------------------------------------------------------------------------------------------------
|
||||
// - Use to enable DPI awareness without having to create an application manifest.
|
||||
// - Your own app may already do this via a manifest or explicit calls. This is mostly useful for our examples/ apps.
|
||||
// - In theory we could call simple functions from Windows SDK such as SetProcessDPIAware(), SetProcessDpiAwareness(), etc.
|
||||
// but most of the functions provided by Microsoft require Windows 8.1/10+ SDK at compile time and Windows 8/10+ at runtime,
|
||||
// neither we want to require the user to have. So we dynamically select and load those functions to avoid dependencies.
|
||||
//---------------------------------------------------------------------------------------------------------
|
||||
// This is the scheme successfully used by GLFW (from which we borrowed some of the code) and other apps aiming to be highly portable.
|
||||
// ImGui_ImplWin32_EnableDpiAwareness() is just a helper called by main.cpp, we don't call it automatically.
|
||||
// If you are trying to implement your own backend for your own engine, you may ignore that noise.
|
||||
//---------------------------------------------------------------------------------------------------------
|
||||
|
||||
// Perform our own check with RtlVerifyVersionInfo() instead of using functions from <VersionHelpers.h> as they
|
||||
// require a manifest to be functional for checks above 8.1. See https://github.com/ocornut/imgui/issues/4200
|
||||
static BOOL _IsWindowsVersionOrGreater(WORD major, WORD minor, WORD)
|
||||
{
|
||||
typedef LONG(WINAPI* PFN_RtlVerifyVersionInfo)(OSVERSIONINFOEXW*, ULONG, ULONGLONG);
|
||||
static PFN_RtlVerifyVersionInfo RtlVerifyVersionInfoFn = nullptr;
|
||||
if (RtlVerifyVersionInfoFn == nullptr)
|
||||
if (HMODULE ntdllModule = ::GetModuleHandleA("ntdll.dll"))
|
||||
RtlVerifyVersionInfoFn = (PFN_RtlVerifyVersionInfo)GetProcAddress(ntdllModule, "RtlVerifyVersionInfo");
|
||||
if (RtlVerifyVersionInfoFn == nullptr)
|
||||
return FALSE;
|
||||
|
||||
RTL_OSVERSIONINFOEXW versionInfo = { };
|
||||
ULONGLONG conditionMask = 0;
|
||||
versionInfo.dwOSVersionInfoSize = sizeof(RTL_OSVERSIONINFOEXW);
|
||||
versionInfo.dwMajorVersion = major;
|
||||
versionInfo.dwMinorVersion = minor;
|
||||
VER_SET_CONDITION(conditionMask, VER_MAJORVERSION, VER_GREATER_EQUAL);
|
||||
VER_SET_CONDITION(conditionMask, VER_MINORVERSION, VER_GREATER_EQUAL);
|
||||
return (RtlVerifyVersionInfoFn(&versionInfo, VER_MAJORVERSION | VER_MINORVERSION, conditionMask) == 0) ? TRUE : FALSE;
|
||||
}
|
||||
|
||||
#define _IsWindowsVistaOrGreater() _IsWindowsVersionOrGreater(HIBYTE(0x0600), LOBYTE(0x0600), 0) // _WIN32_WINNT_VISTA
|
||||
#define _IsWindows8OrGreater() _IsWindowsVersionOrGreater(HIBYTE(0x0602), LOBYTE(0x0602), 0) // _WIN32_WINNT_WIN8
|
||||
#define _IsWindows8Point1OrGreater() _IsWindowsVersionOrGreater(HIBYTE(0x0603), LOBYTE(0x0603), 0) // _WIN32_WINNT_WINBLUE
|
||||
#define _IsWindows10OrGreater() _IsWindowsVersionOrGreater(HIBYTE(0x0A00), LOBYTE(0x0A00), 0) // _WIN32_WINNT_WINTHRESHOLD / _WIN32_WINNT_WIN10
|
||||
|
||||
#ifndef DPI_ENUMS_DECLARED
|
||||
typedef enum { PROCESS_DPI_UNAWARE = 0, PROCESS_SYSTEM_DPI_AWARE = 1, PROCESS_PER_MONITOR_DPI_AWARE = 2 } PROCESS_DPI_AWARENESS;
|
||||
typedef enum { MDT_EFFECTIVE_DPI = 0, MDT_ANGULAR_DPI = 1, MDT_RAW_DPI = 2, MDT_DEFAULT = MDT_EFFECTIVE_DPI } MONITOR_DPI_TYPE;
|
||||
#endif
|
||||
#ifndef _DPI_AWARENESS_CONTEXTS_
|
||||
DECLARE_HANDLE(DPI_AWARENESS_CONTEXT);
|
||||
#define DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE (DPI_AWARENESS_CONTEXT)-3
|
||||
#endif
|
||||
#ifndef DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2
|
||||
#define DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2 (DPI_AWARENESS_CONTEXT)-4
|
||||
#endif
|
||||
typedef HRESULT(WINAPI* PFN_SetProcessDpiAwareness)(PROCESS_DPI_AWARENESS); // Shcore.lib + dll, Windows 8.1+
|
||||
typedef HRESULT(WINAPI* PFN_GetDpiForMonitor)(HMONITOR, MONITOR_DPI_TYPE, UINT*, UINT*); // Shcore.lib + dll, Windows 8.1+
|
||||
typedef DPI_AWARENESS_CONTEXT(WINAPI* PFN_SetThreadDpiAwarenessContext)(DPI_AWARENESS_CONTEXT); // User32.lib + dll, Windows 10 v1607+ (Creators Update)
|
||||
|
||||
// Helper function to enable DPI awareness without setting up a manifest
|
||||
void ImGui_ImplWin32_EnableDpiAwareness()
|
||||
{
|
||||
if (_IsWindows10OrGreater())
|
||||
{
|
||||
static HINSTANCE user32_dll = ::LoadLibraryA("user32.dll"); // Reference counted per-process
|
||||
if (PFN_SetThreadDpiAwarenessContext SetThreadDpiAwarenessContextFn = (PFN_SetThreadDpiAwarenessContext)::GetProcAddress(user32_dll, "SetThreadDpiAwarenessContext"))
|
||||
{
|
||||
SetThreadDpiAwarenessContextFn(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2);
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (_IsWindows8Point1OrGreater())
|
||||
{
|
||||
static HINSTANCE shcore_dll = ::LoadLibraryA("shcore.dll"); // Reference counted per-process
|
||||
if (PFN_SetProcessDpiAwareness SetProcessDpiAwarenessFn = (PFN_SetProcessDpiAwareness)::GetProcAddress(shcore_dll, "SetProcessDpiAwareness"))
|
||||
{
|
||||
SetProcessDpiAwarenessFn(PROCESS_PER_MONITOR_DPI_AWARE);
|
||||
return;
|
||||
}
|
||||
}
|
||||
#if _WIN32_WINNT >= 0x0600
|
||||
::SetProcessDPIAware();
|
||||
#endif
|
||||
}
|
||||
|
||||
#if defined(_MSC_VER) && !defined(NOGDI)
|
||||
#pragma comment(lib, "gdi32") // Link with gdi32.lib for GetDeviceCaps(). MinGW will require linking with '-lgdi32'
|
||||
#endif
|
||||
|
||||
float ImGui_ImplWin32_GetDpiScaleForMonitor(void* monitor)
|
||||
{
|
||||
UINT xdpi = 96, ydpi = 96;
|
||||
if (_IsWindows8Point1OrGreater())
|
||||
{
|
||||
static HINSTANCE shcore_dll = ::LoadLibraryA("shcore.dll"); // Reference counted per-process
|
||||
static PFN_GetDpiForMonitor GetDpiForMonitorFn = nullptr;
|
||||
if (GetDpiForMonitorFn == nullptr && shcore_dll != nullptr)
|
||||
GetDpiForMonitorFn = (PFN_GetDpiForMonitor)::GetProcAddress(shcore_dll, "GetDpiForMonitor");
|
||||
if (GetDpiForMonitorFn != nullptr)
|
||||
{
|
||||
GetDpiForMonitorFn((HMONITOR)monitor, MDT_EFFECTIVE_DPI, &xdpi, &ydpi);
|
||||
IM_ASSERT(xdpi == ydpi); // Please contact me if you hit this assert!
|
||||
return xdpi / 96.0f;
|
||||
}
|
||||
}
|
||||
#ifndef NOGDI
|
||||
const HDC dc = ::GetDC(nullptr);
|
||||
xdpi = ::GetDeviceCaps(dc, LOGPIXELSX);
|
||||
ydpi = ::GetDeviceCaps(dc, LOGPIXELSY);
|
||||
IM_ASSERT(xdpi == ydpi); // Please contact me if you hit this assert!
|
||||
::ReleaseDC(nullptr, dc);
|
||||
#endif
|
||||
return xdpi / 96.0f;
|
||||
}
|
||||
|
||||
float ImGui_ImplWin32_GetDpiScaleForHwnd(void* hwnd)
|
||||
{
|
||||
HMONITOR monitor = ::MonitorFromWindow((HWND)hwnd, MONITOR_DEFAULTTONEAREST);
|
||||
return ImGui_ImplWin32_GetDpiScaleForMonitor(monitor);
|
||||
}
|
||||
|
||||
//---------------------------------------------------------------------------------------------------------
|
||||
// Transparency related helpers (optional)
|
||||
//--------------------------------------------------------------------------------------------------------
|
||||
|
||||
#if defined(_MSC_VER)
|
||||
#pragma comment(lib, "dwmapi") // Link with dwmapi.lib. MinGW will require linking with '-ldwmapi'
|
||||
#endif
|
||||
|
||||
// [experimental]
|
||||
// Borrowed from GLFW's function updateFramebufferTransparency() in src/win32_window.c
|
||||
// (the Dwm* functions are Vista era functions but we are borrowing logic from GLFW)
|
||||
void ImGui_ImplWin32_EnableAlphaCompositing(void* hwnd)
|
||||
{
|
||||
if (!_IsWindowsVistaOrGreater())
|
||||
return;
|
||||
|
||||
BOOL composition;
|
||||
if (FAILED(::DwmIsCompositionEnabled(&composition)) || !composition)
|
||||
return;
|
||||
|
||||
BOOL opaque;
|
||||
DWORD color;
|
||||
if (_IsWindows8OrGreater() || (SUCCEEDED(::DwmGetColorizationColor(&color, &opaque)) && !opaque))
|
||||
{
|
||||
HRGN region = ::CreateRectRgn(0, 0, -1, -1);
|
||||
DWM_BLURBEHIND bb = {};
|
||||
bb.dwFlags = DWM_BB_ENABLE | DWM_BB_BLURREGION;
|
||||
bb.hRgnBlur = region;
|
||||
bb.fEnable = TRUE;
|
||||
::DwmEnableBlurBehindWindow((HWND)hwnd, &bb);
|
||||
::DeleteObject(region);
|
||||
}
|
||||
else
|
||||
{
|
||||
DWM_BLURBEHIND bb = {};
|
||||
bb.dwFlags = DWM_BB_ENABLE;
|
||||
::DwmEnableBlurBehindWindow((HWND)hwnd, &bb);
|
||||
}
|
||||
}
|
||||
|
||||
//---------------------------------------------------------------------------------------------------------
|
||||
|
||||
#if defined(__GNUC__)
|
||||
#pragma GCC diagnostic pop
|
||||
#endif
|
||||
#if defined(__clang__)
|
||||
#pragma clang diagnostic pop
|
||||
#endif
|
||||
|
||||
#endif // #ifndef IMGUI_DISABLE
|
||||
-53
@@ -1,53 +0,0 @@
|
||||
// dear imgui: Platform Backend for Windows (standard windows API for 32-bits AND 64-bits applications)
|
||||
// This needs to be used along with a Renderer (e.g. DirectX11, OpenGL3, Vulkan..)
|
||||
|
||||
// Implemented features:
|
||||
// [X] Platform: Clipboard support (for Win32 this is actually part of core dear imgui)
|
||||
// [X] Platform: Mouse support. Can discriminate Mouse/TouchScreen/Pen.
|
||||
// [X] Platform: Keyboard support. Since 1.87 we are using the io.AddKeyEvent() function. Pass ImGuiKey values to all key functions e.g. ImGui::IsKeyPressed(ImGuiKey_Space). [Legacy VK_* values are obsolete since 1.87 and not supported since 1.91.5]
|
||||
// [X] Platform: Gamepad support. Enabled with 'io.ConfigFlags |= ImGuiConfigFlags_NavEnableGamepad'.
|
||||
// [X] Platform: Mouse cursor shape and visibility. Disable with 'io.ConfigFlags |= ImGuiConfigFlags_NoMouseCursorChange'.
|
||||
|
||||
// You can use unmodified imgui_impl_* files in your project. See examples/ folder for examples of using this.
|
||||
// Prefer including the entire imgui/ repository into your project (either as a copy or as a submodule), and only build the backends you need.
|
||||
// Learn about Dear ImGui:
|
||||
// - FAQ https://dearimgui.com/faq
|
||||
// - Getting Started https://dearimgui.com/getting-started
|
||||
// - Documentation https://dearimgui.com/docs (same as your local docs/ folder).
|
||||
// - Introduction, links and more at the top of imgui.cpp
|
||||
|
||||
#pragma once
|
||||
#include "imgui.h" // IMGUI_IMPL_API
|
||||
#ifndef IMGUI_DISABLE
|
||||
|
||||
// Follow "Getting Started" link and check examples/ folder to learn about using backends!
|
||||
IMGUI_IMPL_API bool ImGui_ImplWin32_Init(void* hwnd);
|
||||
IMGUI_IMPL_API bool ImGui_ImplWin32_InitForOpenGL(void* hwnd);
|
||||
IMGUI_IMPL_API void ImGui_ImplWin32_Shutdown();
|
||||
IMGUI_IMPL_API void ImGui_ImplWin32_NewFrame();
|
||||
|
||||
// Win32 message handler your application need to call.
|
||||
// - Intentionally commented out in a '#if 0' block to avoid dragging dependencies on <windows.h> from this helper.
|
||||
// - You should COPY the line below into your .cpp code to forward declare the function and then you can call it.
|
||||
// - Call from your application's message handler. Keep calling your message handler unless this function returns TRUE.
|
||||
|
||||
#if 0
|
||||
extern IMGUI_IMPL_API LRESULT ImGui_ImplWin32_WndProcHandler(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam);
|
||||
#endif
|
||||
|
||||
// DPI-related helpers (optional)
|
||||
// - Use to enable DPI awareness without having to create an application manifest.
|
||||
// - Your own app may already do this via a manifest or explicit calls. This is mostly useful for our examples/ apps.
|
||||
// - In theory we could call simple functions from Windows SDK such as SetProcessDPIAware(), SetProcessDpiAwareness(), etc.
|
||||
// but most of the functions provided by Microsoft require Windows 8.1/10+ SDK at compile time and Windows 8/10+ at runtime,
|
||||
// neither we want to require the user to have. So we dynamically select and load those functions to avoid dependencies.
|
||||
IMGUI_IMPL_API void ImGui_ImplWin32_EnableDpiAwareness();
|
||||
IMGUI_IMPL_API float ImGui_ImplWin32_GetDpiScaleForHwnd(void* hwnd); // HWND hwnd
|
||||
IMGUI_IMPL_API float ImGui_ImplWin32_GetDpiScaleForMonitor(void* monitor); // HMONITOR monitor
|
||||
|
||||
// Transparency related helpers (optional) [experimental]
|
||||
// - Use to enable alpha compositing transparency with the desktop.
|
||||
// - Use together with e.g. clearing your framebuffer with zero-alpha.
|
||||
IMGUI_IMPL_API void ImGui_ImplWin32_EnableAlphaCompositing(void* hwnd); // HWND hwnd
|
||||
|
||||
#endif // #ifndef IMGUI_DISABLE
|
||||
-3578
File diff suppressed because it is too large.
Load diff
-4467
File diff suppressed because it is too large.
Load diff
-10310
File diff suppressed because it is too large.
Load diff
-627
@@ -1,627 +0,0 @@
|
||||
// [DEAR IMGUI]
|
||||
// This is a slightly modified version of stb_rect_pack.h 1.01.
|
||||
// Grep for [DEAR IMGUI] to find the changes.
|
||||
//
|
||||
// stb_rect_pack.h - v1.01 - public domain - rectangle packing
|
||||
// Sean Barrett 2014
|
||||
//
|
||||
// Useful for e.g. packing rectangular textures into an atlas.
|
||||
// Does not do rotation.
|
||||
//
|
||||
// Before #including,
|
||||
//
|
||||
// #define STB_RECT_PACK_IMPLEMENTATION
|
||||
//
|
||||
// in the file that you want to have the implementation.
|
||||
//
|
||||
// Not necessarily the awesomest packing method, but better than
|
||||
// the totally naive one in stb_truetype (which is primarily what
|
||||
// this is meant to replace).
|
||||
//
|
||||
// Has only had a few tests run, may have issues.
|
||||
//
|
||||
// More docs to come.
|
||||
//
|
||||
// No memory allocations; uses qsort() and assert() from stdlib.
|
||||
// Can override those by defining STBRP_SORT and STBRP_ASSERT.
|
||||
//
|
||||
// This library currently uses the Skyline Bottom-Left algorithm.
|
||||
//
|
||||
// Please note: better rectangle packers are welcome! Please
|
||||
// implement them to the same API, but with a different init
|
||||
// function.
|
||||
//
|
||||
// Credits
|
||||
//
|
||||
// Library
|
||||
// Sean Barrett
|
||||
// Minor features
|
||||
// Martins Mozeiko
|
||||
// github:IntellectualKitty
|
||||
//
|
||||
// Bugfixes / warning fixes
|
||||
// Jeremy Jaussaud
|
||||
// Fabian Giesen
|
||||
//
|
||||
// Version history:
|
||||
//
|
||||
// 1.01 (2021-07-11) always use large rect mode, expose STBRP__MAXVAL in public section
|
||||
// 1.00 (2019-02-25) avoid small space waste; gracefully fail too-wide rectangles
|
||||
// 0.99 (2019-02-07) warning fixes
|
||||
// 0.11 (2017-03-03) return packing success/fail result
|
||||
// 0.10 (2016-10-25) remove cast-away-const to avoid warnings
|
||||
// 0.09 (2016-08-27) fix compiler warnings
|
||||
// 0.08 (2015-09-13) really fix bug with empty rects (w=0 or h=0)
|
||||
// 0.07 (2015-09-13) fix bug with empty rects (w=0 or h=0)
|
||||
// 0.06 (2015-04-15) added STBRP_SORT to allow replacing qsort
|
||||
// 0.05: added STBRP_ASSERT to allow replacing assert
|
||||
// 0.04: fixed minor bug in STBRP_LARGE_RECTS support
|
||||
// 0.01: initial release
|
||||
//
|
||||
// LICENSE
|
||||
//
|
||||
// See end of file for license information.
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// INCLUDE SECTION
|
||||
//
|
||||
|
||||
#ifndef STB_INCLUDE_STB_RECT_PACK_H
|
||||
#define STB_INCLUDE_STB_RECT_PACK_H
|
||||
|
||||
#define STB_RECT_PACK_VERSION 1
|
||||
|
||||
#ifdef STBRP_STATIC
|
||||
#define STBRP_DEF static
|
||||
#else
|
||||
#define STBRP_DEF extern
|
||||
#endif
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
typedef struct stbrp_context stbrp_context;
|
||||
typedef struct stbrp_node stbrp_node;
|
||||
typedef struct stbrp_rect stbrp_rect;
|
||||
|
||||
typedef int stbrp_coord;
|
||||
|
||||
#define STBRP__MAXVAL 0x7fffffff
|
||||
// Mostly for internal use, but this is the maximum supported coordinate value.
|
||||
|
||||
STBRP_DEF int stbrp_pack_rects (stbrp_context *context, stbrp_rect *rects, int num_rects);
|
||||
// Assign packed locations to rectangles. The rectangles are of type
|
||||
// 'stbrp_rect' defined below, stored in the array 'rects', and there
|
||||
// are 'num_rects' many of them.
|
||||
//
|
||||
// Rectangles which are successfully packed have the 'was_packed' flag
|
||||
// set to a non-zero value and 'x' and 'y' store the minimum location
|
||||
// on each axis (i.e. bottom-left in cartesian coordinates, top-left
|
||||
// if you imagine y increasing downwards). Rectangles which do not fit
|
||||
// have the 'was_packed' flag set to 0.
|
||||
//
|
||||
// You should not try to access the 'rects' array from another thread
|
||||
// while this function is running, as the function temporarily reorders
|
||||
// the array while it executes.
|
||||
//
|
||||
// To pack into another rectangle, you need to call stbrp_init_target
|
||||
// again. To continue packing into the same rectangle, you can call
|
||||
// this function again. Calling this multiple times with multiple rect
|
||||
// arrays will probably produce worse packing results than calling it
|
||||
// a single time with the full rectangle array, but the option is
|
||||
// available.
|
||||
//
|
||||
// The function returns 1 if all of the rectangles were successfully
|
||||
// packed and 0 otherwise.
|
||||
|
||||
struct stbrp_rect
|
||||
{
|
||||
// reserved for your use:
|
||||
int id;
|
||||
|
||||
// input:
|
||||
stbrp_coord w, h;
|
||||
|
||||
// output:
|
||||
stbrp_coord x, y;
|
||||
int was_packed; // non-zero if valid packing
|
||||
|
||||
}; // 16 bytes, nominally
|
||||
|
||||
|
||||
STBRP_DEF void stbrp_init_target (stbrp_context *context, int width, int height, stbrp_node *nodes, int num_nodes);
|
||||
// Initialize a rectangle packer to:
|
||||
// pack a rectangle that is 'width' by 'height' in dimensions
|
||||
// using temporary storage provided by the array 'nodes', which is 'num_nodes' long
|
||||
//
|
||||
// You must call this function every time you start packing into a new target.
|
||||
//
|
||||
// There is no "shutdown" function. The 'nodes' memory must stay valid for
|
||||
// the following stbrp_pack_rects() call (or calls), but can be freed after
|
||||
// the call (or calls) finish.
|
||||
//
|
||||
// Note: to guarantee best results, either:
|
||||
// 1. make sure 'num_nodes' >= 'width'
|
||||
// or 2. call stbrp_allow_out_of_mem() defined below with 'allow_out_of_mem = 1'
|
||||
//
|
||||
// If you don't do either of the above things, widths will be quantized to multiples
|
||||
// of small integers to guarantee the algorithm doesn't run out of temporary storage.
|
||||
//
|
||||
// If you do #2, then the non-quantized algorithm will be used, but the algorithm
|
||||
// may run out of temporary storage and be unable to pack some rectangles.
|
||||
|
||||
STBRP_DEF void stbrp_setup_allow_out_of_mem (stbrp_context *context, int allow_out_of_mem);
|
||||
// Optionally call this function after init but before doing any packing to
|
||||
// change the handling of the out-of-temp-memory scenario, described above.
|
||||
// If you call init again, this will be reset to the default (false).
|
||||
|
||||
|
||||
STBRP_DEF void stbrp_setup_heuristic (stbrp_context *context, int heuristic);
|
||||
// Optionally select which packing heuristic the library should use. Different
|
||||
// heuristics will produce better/worse results for different data sets.
|
||||
// If you call init again, this will be reset to the default.
|
||||
|
||||
enum
|
||||
{
|
||||
STBRP_HEURISTIC_Skyline_default=0,
|
||||
STBRP_HEURISTIC_Skyline_BL_sortHeight = STBRP_HEURISTIC_Skyline_default,
|
||||
STBRP_HEURISTIC_Skyline_BF_sortHeight
|
||||
};
|
||||
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// the details of the following structures don't matter to you, but they must
|
||||
// be visible so you can handle the memory allocations for them
|
||||
|
||||
struct stbrp_node
|
||||
{
|
||||
stbrp_coord x,y;
|
||||
stbrp_node *next;
|
||||
};
|
||||
|
||||
struct stbrp_context
|
||||
{
|
||||
int width;
|
||||
int height;
|
||||
int align;
|
||||
int init_mode;
|
||||
int heuristic;
|
||||
int num_nodes;
|
||||
stbrp_node *active_head;
|
||||
stbrp_node *free_head;
|
||||
stbrp_node extra[2]; // we allocate two extra nodes so optimal user-node-count is 'width' not 'width+2'
|
||||
};
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// IMPLEMENTATION SECTION
|
||||
//
|
||||
|
||||
#ifdef STB_RECT_PACK_IMPLEMENTATION
|
||||
#ifndef STBRP_SORT
|
||||
#include <stdlib.h>
|
||||
#define STBRP_SORT qsort
|
||||
#endif
|
||||
|
||||
#ifndef STBRP_ASSERT
|
||||
#include <assert.h>
|
||||
#define STBRP_ASSERT assert
|
||||
#endif
|
||||
|
||||
#ifdef _MSC_VER
|
||||
#define STBRP__NOTUSED(v) (void)(v)
|
||||
#define STBRP__CDECL __cdecl
|
||||
#else
|
||||
#define STBRP__NOTUSED(v) (void)sizeof(v)
|
||||
#define STBRP__CDECL
|
||||
#endif
|
||||
|
||||
enum
|
||||
{
|
||||
STBRP__INIT_skyline = 1
|
||||
};
|
||||
|
||||
STBRP_DEF void stbrp_setup_heuristic(stbrp_context *context, int heuristic)
|
||||
{
|
||||
switch (context->init_mode) {
|
||||
case STBRP__INIT_skyline:
|
||||
STBRP_ASSERT(heuristic == STBRP_HEURISTIC_Skyline_BL_sortHeight || heuristic == STBRP_HEURISTIC_Skyline_BF_sortHeight);
|
||||
context->heuristic = heuristic;
|
||||
break;
|
||||
default:
|
||||
STBRP_ASSERT(0);
|
||||
}
|
||||
}
|
||||
|
||||
STBRP_DEF void stbrp_setup_allow_out_of_mem(stbrp_context *context, int allow_out_of_mem)
|
||||
{
|
||||
if (allow_out_of_mem)
|
||||
// if it's ok to run out of memory, then don't bother aligning them;
|
||||
// this gives better packing, but may fail due to OOM (even though
|
||||
// the rectangles easily fit). @TODO a smarter approach would be to only
|
||||
// quantize once we've hit OOM, then we could get rid of this parameter.
|
||||
context->align = 1;
|
||||
else {
|
||||
// if it's not ok to run out of memory, then quantize the widths
|
||||
// so that num_nodes is always enough nodes.
|
||||
//
|
||||
// I.e. num_nodes * align >= width
|
||||
// align >= width / num_nodes
|
||||
// align = ceil(width/num_nodes)
|
||||
|
||||
context->align = (context->width + context->num_nodes-1) / context->num_nodes;
|
||||
}
|
||||
}
|
||||
|
||||
STBRP_DEF void stbrp_init_target(stbrp_context *context, int width, int height, stbrp_node *nodes, int num_nodes)
|
||||
{
|
||||
int i;
|
||||
|
||||
for (i=0; i < num_nodes-1; ++i)
|
||||
nodes[i].next = &nodes[i+1];
|
||||
nodes[i].next = NULL;
|
||||
context->init_mode = STBRP__INIT_skyline;
|
||||
context->heuristic = STBRP_HEURISTIC_Skyline_default;
|
||||
context->free_head = &nodes[0];
|
||||
context->active_head = &context->extra[0];
|
||||
context->width = width;
|
||||
context->height = height;
|
||||
context->num_nodes = num_nodes;
|
||||
stbrp_setup_allow_out_of_mem(context, 0);
|
||||
|
||||
// node 0 is the full width, node 1 is the sentinel (lets us not store width explicitly)
|
||||
context->extra[0].x = 0;
|
||||
context->extra[0].y = 0;
|
||||
context->extra[0].next = &context->extra[1];
|
||||
context->extra[1].x = (stbrp_coord) width;
|
||||
context->extra[1].y = (1<<30);
|
||||
context->extra[1].next = NULL;
|
||||
}
|
||||
|
||||
// find minimum y position if it starts at x1
|
||||
static int stbrp__skyline_find_min_y(stbrp_context *c, stbrp_node *first, int x0, int width, int *pwaste)
|
||||
{
|
||||
stbrp_node *node = first;
|
||||
int x1 = x0 + width;
|
||||
int min_y, visited_width, waste_area;
|
||||
|
||||
STBRP__NOTUSED(c);
|
||||
|
||||
STBRP_ASSERT(first->x <= x0);
|
||||
|
||||
#if 0
|
||||
// skip in case we're past the node
|
||||
while (node->next->x <= x0)
|
||||
++node;
|
||||
#else
|
||||
STBRP_ASSERT(node->next->x > x0); // we ended up handling this in the caller for efficiency
|
||||
#endif
|
||||
|
||||
STBRP_ASSERT(node->x <= x0);
|
||||
|
||||
min_y = 0;
|
||||
waste_area = 0;
|
||||
visited_width = 0;
|
||||
while (node->x < x1) {
|
||||
if (node->y > min_y) {
|
||||
// raise min_y higher.
|
||||
// we've accounted for all waste up to min_y,
|
||||
// but we'll now add more waste for everything we've visted
|
||||
waste_area += visited_width * (node->y - min_y);
|
||||
min_y = node->y;
|
||||
// the first time through, visited_width might be reduced
|
||||
if (node->x < x0)
|
||||
visited_width += node->next->x - x0;
|
||||
else
|
||||
visited_width += node->next->x - node->x;
|
||||
} else {
|
||||
// add waste area
|
||||
int under_width = node->next->x - node->x;
|
||||
if (under_width + visited_width > width)
|
||||
under_width = width - visited_width;
|
||||
waste_area += under_width * (min_y - node->y);
|
||||
visited_width += under_width;
|
||||
}
|
||||
node = node->next;
|
||||
}
|
||||
|
||||
*pwaste = waste_area;
|
||||
return min_y;
|
||||
}
|
||||
|
||||
typedef struct
|
||||
{
|
||||
int x,y;
|
||||
stbrp_node **prev_link;
|
||||
} stbrp__findresult;
|
||||
|
||||
static stbrp__findresult stbrp__skyline_find_best_pos(stbrp_context *c, int width, int height)
|
||||
{
|
||||
int best_waste = (1<<30), best_x, best_y = (1 << 30);
|
||||
stbrp__findresult fr;
|
||||
stbrp_node **prev, *node, *tail, **best = NULL;
|
||||
|
||||
// align to multiple of c->align
|
||||
width = (width + c->align - 1);
|
||||
width -= width % c->align;
|
||||
STBRP_ASSERT(width % c->align == 0);
|
||||
|
||||
// if it can't possibly fit, bail immediately
|
||||
if (width > c->width || height > c->height) {
|
||||
fr.prev_link = NULL;
|
||||
fr.x = fr.y = 0;
|
||||
return fr;
|
||||
}
|
||||
|
||||
node = c->active_head;
|
||||
prev = &c->active_head;
|
||||
while (node->x + width <= c->width) {
|
||||
int y,waste;
|
||||
y = stbrp__skyline_find_min_y(c, node, node->x, width, &waste);
|
||||
if (c->heuristic == STBRP_HEURISTIC_Skyline_BL_sortHeight) { // actually just want to test BL
|
||||
// bottom left
|
||||
if (y < best_y) {
|
||||
best_y = y;
|
||||
best = prev;
|
||||
}
|
||||
} else {
|
||||
// best-fit
|
||||
if (y + height <= c->height) {
|
||||
// can only use it if it first vertically
|
||||
if (y < best_y || (y == best_y && waste < best_waste)) {
|
||||
best_y = y;
|
||||
best_waste = waste;
|
||||
best = prev;
|
||||
}
|
||||
}
|
||||
}
|
||||
prev = &node->next;
|
||||
node = node->next;
|
||||
}
|
||||
|
||||
best_x = (best == NULL) ? 0 : (*best)->x;
|
||||
|
||||
// if doing best-fit (BF), we also have to try aligning right edge to each node position
|
||||
//
|
||||
// e.g, if fitting
|
||||
//
|
||||
// ____________________
|
||||
// |____________________|
|
||||
//
|
||||
// into
|
||||
//
|
||||
// | |
|
||||
// | ____________|
|
||||
// |____________|
|
||||
//
|
||||
// then right-aligned reduces waste, but bottom-left BL is always chooses left-aligned
|
||||
//
|
||||
// This makes BF take about 2x the time
|
||||
|
||||
if (c->heuristic == STBRP_HEURISTIC_Skyline_BF_sortHeight) {
|
||||
tail = c->active_head;
|
||||
node = c->active_head;
|
||||
prev = &c->active_head;
|
||||
// find first node that's admissible
|
||||
while (tail->x < width)
|
||||
tail = tail->next;
|
||||
while (tail) {
|
||||
int xpos = tail->x - width;
|
||||
int y,waste;
|
||||
STBRP_ASSERT(xpos >= 0);
|
||||
// find the left position that matches this
|
||||
while (node->next->x <= xpos) {
|
||||
prev = &node->next;
|
||||
node = node->next;
|
||||
}
|
||||
STBRP_ASSERT(node->next->x > xpos && node->x <= xpos);
|
||||
y = stbrp__skyline_find_min_y(c, node, xpos, width, &waste);
|
||||
if (y + height <= c->height) {
|
||||
if (y <= best_y) {
|
||||
if (y < best_y || waste < best_waste || (waste==best_waste && xpos < best_x)) {
|
||||
best_x = xpos;
|
||||
//STBRP_ASSERT(y <= best_y); [DEAR IMGUI]
|
||||
best_y = y;
|
||||
best_waste = waste;
|
||||
best = prev;
|
||||
}
|
||||
}
|
||||
}
|
||||
tail = tail->next;
|
||||
}
|
||||
}
|
||||
|
||||
fr.prev_link = best;
|
||||
fr.x = best_x;
|
||||
fr.y = best_y;
|
||||
return fr;
|
||||
}
|
||||
|
||||
static stbrp__findresult stbrp__skyline_pack_rectangle(stbrp_context *context, int width, int height)
|
||||
{
|
||||
// find best position according to heuristic
|
||||
stbrp__findresult res = stbrp__skyline_find_best_pos(context, width, height);
|
||||
stbrp_node *node, *cur;
|
||||
|
||||
// bail if:
|
||||
// 1. it failed
|
||||
// 2. the best node doesn't fit (we don't always check this)
|
||||
// 3. we're out of memory
|
||||
if (res.prev_link == NULL || res.y + height > context->height || context->free_head == NULL) {
|
||||
res.prev_link = NULL;
|
||||
return res;
|
||||
}
|
||||
|
||||
// on success, create new node
|
||||
node = context->free_head;
|
||||
node->x = (stbrp_coord) res.x;
|
||||
node->y = (stbrp_coord) (res.y + height);
|
||||
|
||||
context->free_head = node->next;
|
||||
|
||||
// insert the new node into the right starting point, and
|
||||
// let 'cur' point to the remaining nodes needing to be
|
||||
// stiched back in
|
||||
|
||||
cur = *res.prev_link;
|
||||
if (cur->x < res.x) {
|
||||
// preserve the existing one, so start testing with the next one
|
||||
stbrp_node *next = cur->next;
|
||||
cur->next = node;
|
||||
cur = next;
|
||||
} else {
|
||||
*res.prev_link = node;
|
||||
}
|
||||
|
||||
// from here, traverse cur and free the nodes, until we get to one
|
||||
// that shouldn't be freed
|
||||
while (cur->next && cur->next->x <= res.x + width) {
|
||||
stbrp_node *next = cur->next;
|
||||
// move the current node to the free list
|
||||
cur->next = context->free_head;
|
||||
context->free_head = cur;
|
||||
cur = next;
|
||||
}
|
||||
|
||||
// stitch the list back in
|
||||
node->next = cur;
|
||||
|
||||
if (cur->x < res.x + width)
|
||||
cur->x = (stbrp_coord) (res.x + width);
|
||||
|
||||
#ifdef _DEBUG
|
||||
cur = context->active_head;
|
||||
while (cur->x < context->width) {
|
||||
STBRP_ASSERT(cur->x < cur->next->x);
|
||||
cur = cur->next;
|
||||
}
|
||||
STBRP_ASSERT(cur->next == NULL);
|
||||
|
||||
{
|
||||
int count=0;
|
||||
cur = context->active_head;
|
||||
while (cur) {
|
||||
cur = cur->next;
|
||||
++count;
|
||||
}
|
||||
cur = context->free_head;
|
||||
while (cur) {
|
||||
cur = cur->next;
|
||||
++count;
|
||||
}
|
||||
STBRP_ASSERT(count == context->num_nodes+2);
|
||||
}
|
||||
#endif
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
static int STBRP__CDECL rect_height_compare(const void *a, const void *b)
|
||||
{
|
||||
const stbrp_rect *p = (const stbrp_rect *) a;
|
||||
const stbrp_rect *q = (const stbrp_rect *) b;
|
||||
if (p->h > q->h)
|
||||
return -1;
|
||||
if (p->h < q->h)
|
||||
return 1;
|
||||
return (p->w > q->w) ? -1 : (p->w < q->w);
|
||||
}
|
||||
|
||||
static int STBRP__CDECL rect_original_order(const void *a, const void *b)
|
||||
{
|
||||
const stbrp_rect *p = (const stbrp_rect *) a;
|
||||
const stbrp_rect *q = (const stbrp_rect *) b;
|
||||
return (p->was_packed < q->was_packed) ? -1 : (p->was_packed > q->was_packed);
|
||||
}
|
||||
|
||||
STBRP_DEF int stbrp_pack_rects(stbrp_context *context, stbrp_rect *rects, int num_rects)
|
||||
{
|
||||
int i, all_rects_packed = 1;
|
||||
|
||||
// we use the 'was_packed' field internally to allow sorting/unsorting
|
||||
for (i=0; i < num_rects; ++i) {
|
||||
rects[i].was_packed = i;
|
||||
}
|
||||
|
||||
// sort according to heuristic
|
||||
STBRP_SORT(rects, num_rects, sizeof(rects[0]), rect_height_compare);
|
||||
|
||||
for (i=0; i < num_rects; ++i) {
|
||||
if (rects[i].w == 0 || rects[i].h == 0) {
|
||||
rects[i].x = rects[i].y = 0; // empty rect needs no space
|
||||
} else {
|
||||
stbrp__findresult fr = stbrp__skyline_pack_rectangle(context, rects[i].w, rects[i].h);
|
||||
if (fr.prev_link) {
|
||||
rects[i].x = (stbrp_coord) fr.x;
|
||||
rects[i].y = (stbrp_coord) fr.y;
|
||||
} else {
|
||||
rects[i].x = rects[i].y = STBRP__MAXVAL;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// unsort
|
||||
STBRP_SORT(rects, num_rects, sizeof(rects[0]), rect_original_order);
|
||||
|
||||
// set was_packed flags and all_rects_packed status
|
||||
for (i=0; i < num_rects; ++i) {
|
||||
rects[i].was_packed = !(rects[i].x == STBRP__MAXVAL && rects[i].y == STBRP__MAXVAL);
|
||||
if (!rects[i].was_packed)
|
||||
all_rects_packed = 0;
|
||||
}
|
||||
|
||||
// return the all_rects_packed status
|
||||
return all_rects_packed;
|
||||
}
|
||||
#endif
|
||||
|
||||
/*
|
||||
------------------------------------------------------------------------------
|
||||
This software is available under 2 licenses -- choose whichever you prefer.
|
||||
------------------------------------------------------------------------------
|
||||
ALTERNATIVE A - MIT License
|
||||
Copyright (c) 2017 Sean Barrett
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
this software and associated documentation files (the "Software"), to deal in
|
||||
the Software without restriction, including without limitation the rights to
|
||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||
of the Software, and to permit persons to whom the Software is furnished to do
|
||||
so, subject to the following conditions:
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
------------------------------------------------------------------------------
|
||||
ALTERNATIVE B - Public Domain (www.unlicense.org)
|
||||
This is free and unencumbered software released into the public domain.
|
||||
Anyone is free to copy, modify, publish, use, compile, sell, or distribute this
|
||||
software, either in source code form or as a compiled binary, for any purpose,
|
||||
commercial or non-commercial, and by any means.
|
||||
In jurisdictions that recognize copyright laws, the author or authors of this
|
||||
software dedicate any and all copyright interest in the software to the public
|
||||
domain. We make this dedication for the benefit of the public at large and to
|
||||
the detriment of our heirs and successors. We intend this dedication to be an
|
||||
overt act of relinquishment in perpetuity of all present and future rights to
|
||||
this software under copyright law.
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
|
||||
ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
------------------------------------------------------------------------------
|
||||
*/
|
||||
-1469
File diff suppressed because it is too large.
Load diff
-5085
File diff suppressed because it is too large.
Load diff
-24765
File diff suppressed because it is too large.
Load diff
@@ -1,26 +0,0 @@
|
||||
# Builds the release and packs what players download: dist\bbbridge-<version>-windows.zip
|
||||
# holding a "bbbridge" folder to drop into bbport's folder (next to Bloodborne.exe).
|
||||
# Never includes an account file, logs or build output.
|
||||
# powershell -ExecutionPolicy Bypass -File tools\make_release.ps1
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$root = Split-Path -Parent $PSScriptRoot
|
||||
$vc = 'C:\Program Files\Microsoft Visual Studio\18\Community\VC\Auxiliary\Build\vcvars64.bat'
|
||||
if (-not (Test-Path $vc)) {
|
||||
$vswhere = "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vswhere.exe"
|
||||
$vc = Join-Path (& $vswhere -latest -products * -property installationPath) 'VC\Auxiliary\Build\vcvars64.bat'
|
||||
}
|
||||
cmd /c "`"$vc`" >nul && cd /d `"$root`" && cmake --preset x64-release >nul && cmake --build --preset x64-release --target bbbridge"
|
||||
if ($LASTEXITCODE -ne 0) { throw 'build failed' }
|
||||
|
||||
$version = (Select-String -Path "$root\CMakeLists.txt" -Pattern 'project\(bbbridge VERSION ([0-9.]+)').Matches[0].Groups[1].Value
|
||||
$stage = Join-Path $root "dist\stage\bbbridge"
|
||||
Remove-Item (Join-Path $root 'dist\stage') -Recurse -Force -ErrorAction SilentlyContinue
|
||||
New-Item -ItemType Directory -Force $stage | Out-Null
|
||||
Copy-Item "$root\build\x64-release\bbbridge.exe", "$root\README.md", "$root\CHANGELOG.md", "$root\LICENSE", "$root\THIRD_PARTY_NOTICES.txt" $stage
|
||||
$zip = Join-Path $root "dist\bbbridge-$version-windows.zip"
|
||||
Remove-Item $zip -Force -ErrorAction SilentlyContinue
|
||||
Compress-Archive -Path $stage -DestinationPath $zip
|
||||
Remove-Item (Join-Path $root 'dist\stage') -Recurse -Force
|
||||
$hash = (Get-FileHash $zip -Algorithm SHA256).Hash.ToLower()
|
||||
"$hash $(Split-Path -Leaf $zip)" | Set-Content -Encoding ascii "$zip.sha256"
|
||||
Write-Output "$zip ($([math]::Round((Get-Item $zip).Length / 1MB, 2)) MB) sha256 $hash"
|
||||
@@ -1,25 +0,0 @@
|
||||
# bbport's changes to BBSC
|
||||
|
||||
This folder holds [BBSC](https://github.com/Classmg22/BBSC) 0.0.1 by CLS_AMG (GPL-2.0-or-later),
|
||||
as published at commit 315f76c, with the changes below made for the Linux port of bbport. Changed
|
||||
lines in BBSC's own files are marked `bbport:` in comments.
|
||||
|
||||
## 2026-10-15
|
||||
|
||||
- **Linux build.** `CMakeLists.txt` builds `libbbsc.so`, which bb-probe loads into the game.
|
||||
- New files `emulator/mod/linux/`:
|
||||
- `bbsc_mod_linux.cpp`, which replaces `bbsc_mod.cpp`;
|
||||
- `bbsc_hooks_linux.cpp`, which replaces the Windows hook code;
|
||||
- `bbsc_win32.h`, the Win32 and Winsock names the mod uses.
|
||||
- Linux branches (`#ifdef`) in `emulator/mod/bbsc_relay.cpp`, `bbsc_channel.cpp` and
|
||||
`bbsc_http.cpp`.
|
||||
- **Logs and captures.** BBSC keeps 5 of each, and stops a capture at 64 MB.
|
||||
- **Data folder.** BBSC's files go in the game's user folder, not the working directory.
|
||||
- **Memory search on Linux.** `emulator/src/core/bbsc_memscan.cpp` reads the readable mappings
|
||||
through `process_vm_readv`.
|
||||
- **F8 key.** The F8 unstuck key works on Linux through SDL (`emulator/src/core/bloodborne_re.cpp`).
|
||||
- **Saves.** `BBSC_SAVE_DIR` is set to bbport's save folder (`savedata/<user>/<title id>/SPRJ0005`).
|
||||
- **Translations.** BBSC's texts follow the game's language:
|
||||
- `emulator/src/core/bbsc_i18n.{h,cpp}` and `bbsc_translations.inc`, generated from
|
||||
`translations/*.json` by `translations/gen_translations.py`;
|
||||
- in `emulator/src/core/bloodborne_re.cpp`, BBSC's texts are passed through them.
|
||||
@@ -1,49 +0,0 @@
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
# bbport: libbbsc.so, BBSC (Bloodborne Seamless Co-op, CLS_AMG) for the Linux port. The mod's
|
||||
# own build (emulator/mod/build-mod.cmd) makes bbsc.dll for Windows; this one builds the same
|
||||
# game logic (emulator/src/core/bloodborne_re.cpp with BBSC_MOD) with the mod's network files and
|
||||
# the Linux counterparts of its platform files (emulator/mod/linux/). bb-probe loads it into the
|
||||
# game's process (BB_BBSC_LIB) and calls bbsc_start().
|
||||
cmake_minimum_required(VERSION 3.20)
|
||||
project(bbsc CXX)
|
||||
|
||||
set(CMAKE_CXX_STANDARD 23)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
set(CMAKE_POSITION_INDEPENDENT_CODE ON)
|
||||
if(NOT CMAKE_BUILD_TYPE)
|
||||
set(CMAKE_BUILD_TYPE Release)
|
||||
endif()
|
||||
|
||||
# Header-only, kept in third_party/ (xbyak 7.37.3, BSD-3-Clause; fmt 12.2.0, MIT) so the build does
|
||||
# not depend on what the system has.
|
||||
set(XBYAK_INCLUDE ${CMAKE_CURRENT_SOURCE_DIR}/third_party/xbyak)
|
||||
set(FMT_INCLUDE ${CMAKE_CURRENT_SOURCE_DIR}/third_party/fmt/include)
|
||||
|
||||
add_library(bbsc SHARED
|
||||
emulator/src/core/bloodborne_re.cpp
|
||||
emulator/src/core/bbsc_memscan.cpp
|
||||
emulator/src/core/bbsc_i18n.cpp
|
||||
emulator/mod/bbsc_relay.cpp
|
||||
emulator/mod/bbsc_channel.cpp
|
||||
emulator/mod/bbsc_http.cpp
|
||||
emulator/mod/linux/bbsc_mod_linux.cpp
|
||||
emulator/mod/linux/bbsc_hooks_linux.cpp)
|
||||
target_compile_definitions(bbsc PRIVATE BBSC_MOD FMT_HEADER_ONLY)
|
||||
target_include_directories(bbsc PRIVATE emulator/mod emulator/mod/shim emulator/src
|
||||
${XBYAK_INCLUDE} ${FMT_INCLUDE})
|
||||
# Only bbsc_start is for bb-probe; the game's runtime symbols it calls (runtime_memory_vma_info)
|
||||
# come from bb-probe itself, which exports them.
|
||||
set_target_properties(bbsc PROPERTIES CXX_VISIBILITY_PRESET hidden VISIBILITY_INLINES_HIDDEN ON)
|
||||
target_link_options(bbsc PRIVATE -Wl,-z,undefs)
|
||||
find_package(Threads REQUIRED)
|
||||
target_link_libraries(bbsc PRIVATE Threads::Threads)
|
||||
|
||||
# BBSC turns itself off without its license, notice and supporters list next to it (as the
|
||||
# Windows mod folder has them).
|
||||
add_custom_command(TARGET bbsc POST_BUILD
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_if_different ${CMAKE_CURRENT_SOURCE_DIR}/LICENSE
|
||||
$<TARGET_FILE_DIR:bbsc>/LICENSE.txt
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_if_different ${CMAKE_CURRENT_SOURCE_DIR}/NOTICE.md
|
||||
$<TARGET_FILE_DIR:bbsc>/NOTICE.txt
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_if_different ${CMAKE_CURRENT_SOURCE_DIR}/SUPPORTERS.txt
|
||||
$<TARGET_FILE_DIR:bbsc>/SUPPORTERS.txt)
|
||||
@@ -1,36 +0,0 @@
|
||||
BBSC - Bloodborne Seamless Co-op
|
||||
Credits
|
||||
|
||||
MADE BY
|
||||
CLS_AMG
|
||||
|
||||
TESTED BY
|
||||
fra98_ (framilano)
|
||||
Hardwater
|
||||
Onesadeye
|
||||
aboood888848
|
||||
Fahad
|
||||
Mob
|
||||
lelial
|
||||
J2mF
|
||||
Kukar3x
|
||||
|
||||
SPECIAL THANKS
|
||||
framilano (fra98_) - Bloodborne Nucleus Co-op Handler
|
||||
split-screen Bloodborne on one PC; BBSC runs in it
|
||||
https://github.com/framilano/BloodborneNucleusCoopHandler
|
||||
Fahad (fhody125) - No Dream For The Hunter
|
||||
the game server and ss.info groundwork BBSC's servers grew from
|
||||
|
||||
BUILT ON
|
||||
shadPS4 - the PS4 emulator (shadPS4 team and contributors, GPL-2.0)
|
||||
shadp2p by Wozzardman - the peer-to-peer online fork BBSC is built on
|
||||
|
||||
ONLINE
|
||||
shadNet team - shadNet, the official shadPS4 online service (sessions, matchmaking)
|
||||
The Hunter's Dream team - the public Bloodborne game server
|
||||
|
||||
BLOODBORNE
|
||||
Bloodborne is developed by FromSoftware and published by Sony Interactive Entertainment.
|
||||
BBSC is a free fan-made project. It is not affiliated with or endorsed by FromSoftware or Sony.
|
||||
You need your own copy of the game. No game files are included.
|
||||
-339
@@ -1,339 +0,0 @@
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
Version 2, June 1991
|
||||
|
||||
Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
|
||||
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
Preamble
|
||||
|
||||
The licenses for most software are designed to take away your
|
||||
freedom to share and change it. By contrast, the GNU General Public
|
||||
License is intended to guarantee your freedom to share and change free
|
||||
software--to make sure the software is free for all its users. This
|
||||
General Public License applies to most of the Free Software
|
||||
Foundation's software and to any other program whose authors commit to
|
||||
using it. (Some other Free Software Foundation software is covered by
|
||||
the GNU Lesser General Public License instead.) You can apply it to
|
||||
your programs, too.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
have the freedom to distribute copies of free software (and charge for
|
||||
this service if you wish), that you receive source code or can get it
|
||||
if you want it, that you can change the software or use pieces of it
|
||||
in new free programs; and that you know you can do these things.
|
||||
|
||||
To protect your rights, we need to make restrictions that forbid
|
||||
anyone to deny you these rights or to ask you to surrender the rights.
|
||||
These restrictions translate to certain responsibilities for you if you
|
||||
distribute copies of the software, or if you modify it.
|
||||
|
||||
For example, if you distribute copies of such a program, whether
|
||||
gratis or for a fee, you must give the recipients all the rights that
|
||||
you have. You must make sure that they, too, receive or can get the
|
||||
source code. And you must show them these terms so they know their
|
||||
rights.
|
||||
|
||||
We protect your rights with two steps: (1) copyright the software, and
|
||||
(2) offer you this license which gives you legal permission to copy,
|
||||
distribute and/or modify the software.
|
||||
|
||||
Also, for each author's protection and ours, we want to make certain
|
||||
that everyone understands that there is no warranty for this free
|
||||
software. If the software is modified by someone else and passed on, we
|
||||
want its recipients to know that what they have is not the original, so
|
||||
that any problems introduced by others will not reflect on the original
|
||||
authors' reputations.
|
||||
|
||||
Finally, any free program is threatened constantly by software
|
||||
patents. We wish to avoid the danger that redistributors of a free
|
||||
program will individually obtain patent licenses, in effect making the
|
||||
program proprietary. To prevent this, we have made it clear that any
|
||||
patent must be licensed for everyone's free use or not licensed at all.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
|
||||
|
||||
0. This License applies to any program or other work which contains
|
||||
a notice placed by the copyright holder saying it may be distributed
|
||||
under the terms of this General Public License. The "Program", below,
|
||||
refers to any such program or work, and a "work based on the Program"
|
||||
means either the Program or any derivative work under copyright law:
|
||||
that is to say, a work containing the Program or a portion of it,
|
||||
either verbatim or with modifications and/or translated into another
|
||||
language. (Hereinafter, translation is included without limitation in
|
||||
the term "modification".) Each licensee is addressed as "you".
|
||||
|
||||
Activities other than copying, distribution and modification are not
|
||||
covered by this License; they are outside its scope. The act of
|
||||
running the Program is not restricted, and the output from the Program
|
||||
is covered only if its contents constitute a work based on the
|
||||
Program (independent of having been made by running the Program).
|
||||
Whether that is true depends on what the Program does.
|
||||
|
||||
1. You may copy and distribute verbatim copies of the Program's
|
||||
source code as you receive it, in any medium, provided that you
|
||||
conspicuously and appropriately publish on each copy an appropriate
|
||||
copyright notice and disclaimer of warranty; keep intact all the
|
||||
notices that refer to this License and to the absence of any warranty;
|
||||
and give any other recipients of the Program a copy of this License
|
||||
along with the Program.
|
||||
|
||||
You may charge a fee for the physical act of transferring a copy, and
|
||||
you may at your option offer warranty protection in exchange for a fee.
|
||||
|
||||
2. You may modify your copy or copies of the Program or any portion
|
||||
of it, thus forming a work based on the Program, and copy and
|
||||
distribute such modifications or work under the terms of Section 1
|
||||
above, provided that you also meet all of these conditions:
|
||||
|
||||
a) You must cause the modified files to carry prominent notices
|
||||
stating that you changed the files and the date of any change.
|
||||
|
||||
b) You must cause any work that you distribute or publish, that in
|
||||
whole or in part contains or is derived from the Program or any
|
||||
part thereof, to be licensed as a whole at no charge to all third
|
||||
parties under the terms of this License.
|
||||
|
||||
c) If the modified program normally reads commands interactively
|
||||
when run, you must cause it, when started running for such
|
||||
interactive use in the most ordinary way, to print or display an
|
||||
announcement including an appropriate copyright notice and a
|
||||
notice that there is no warranty (or else, saying that you provide
|
||||
a warranty) and that users may redistribute the program under
|
||||
these conditions, and telling the user how to view a copy of this
|
||||
License. (Exception: if the Program itself is interactive but
|
||||
does not normally print such an announcement, your work based on
|
||||
the Program is not required to print an announcement.)
|
||||
|
||||
These requirements apply to the modified work as a whole. If
|
||||
identifiable sections of that work are not derived from the Program,
|
||||
and can be reasonably considered independent and separate works in
|
||||
themselves, then this License, and its terms, do not apply to those
|
||||
sections when you distribute them as separate works. But when you
|
||||
distribute the same sections as part of a whole which is a work based
|
||||
on the Program, the distribution of the whole must be on the terms of
|
||||
this License, whose permissions for other licensees extend to the
|
||||
entire whole, and thus to each and every part regardless of who wrote it.
|
||||
|
||||
Thus, it is not the intent of this section to claim rights or contest
|
||||
your rights to work written entirely by you; rather, the intent is to
|
||||
exercise the right to control the distribution of derivative or
|
||||
collective works based on the Program.
|
||||
|
||||
In addition, mere aggregation of another work not based on the Program
|
||||
with the Program (or with a work based on the Program) on a volume of
|
||||
a storage or distribution medium does not bring the other work under
|
||||
the scope of this License.
|
||||
|
||||
3. You may copy and distribute the Program (or a work based on it,
|
||||
under Section 2) in object code or executable form under the terms of
|
||||
Sections 1 and 2 above provided that you also do one of the following:
|
||||
|
||||
a) Accompany it with the complete corresponding machine-readable
|
||||
source code, which must be distributed under the terms of Sections
|
||||
1 and 2 above on a medium customarily used for software interchange; or,
|
||||
|
||||
b) Accompany it with a written offer, valid for at least three
|
||||
years, to give any third party, for a charge no more than your
|
||||
cost of physically performing source distribution, a complete
|
||||
machine-readable copy of the corresponding source code, to be
|
||||
distributed under the terms of Sections 1 and 2 above on a medium
|
||||
customarily used for software interchange; or,
|
||||
|
||||
c) Accompany it with the information you received as to the offer
|
||||
to distribute corresponding source code. (This alternative is
|
||||
allowed only for noncommercial distribution and only if you
|
||||
received the program in object code or executable form with such
|
||||
an offer, in accord with Subsection b above.)
|
||||
|
||||
The source code for a work means the preferred form of the work for
|
||||
making modifications to it. For an executable work, complete source
|
||||
code means all the source code for all modules it contains, plus any
|
||||
associated interface definition files, plus the scripts used to
|
||||
control compilation and installation of the executable. However, as a
|
||||
special exception, the source code distributed need not include
|
||||
anything that is normally distributed (in either source or binary
|
||||
form) with the major components (compiler, kernel, and so on) of the
|
||||
operating system on which the executable runs, unless that component
|
||||
itself accompanies the executable.
|
||||
|
||||
If distribution of executable or object code is made by offering
|
||||
access to copy from a designated place, then offering equivalent
|
||||
access to copy the source code from the same place counts as
|
||||
distribution of the source code, even though third parties are not
|
||||
compelled to copy the source along with the object code.
|
||||
|
||||
4. You may not copy, modify, sublicense, or distribute the Program
|
||||
except as expressly provided under this License. Any attempt
|
||||
otherwise to copy, modify, sublicense or distribute the Program is
|
||||
void, and will automatically terminate your rights under this License.
|
||||
However, parties who have received copies, or rights, from you under
|
||||
this License will not have their licenses terminated so long as such
|
||||
parties remain in full compliance.
|
||||
|
||||
5. You are not required to accept this License, since you have not
|
||||
signed it. However, nothing else grants you permission to modify or
|
||||
distribute the Program or its derivative works. These actions are
|
||||
prohibited by law if you do not accept this License. Therefore, by
|
||||
modifying or distributing the Program (or any work based on the
|
||||
Program), you indicate your acceptance of this License to do so, and
|
||||
all its terms and conditions for copying, distributing or modifying
|
||||
the Program or works based on it.
|
||||
|
||||
6. Each time you redistribute the Program (or any work based on the
|
||||
Program), the recipient automatically receives a license from the
|
||||
original licensor to copy, distribute or modify the Program subject to
|
||||
these terms and conditions. You may not impose any further
|
||||
restrictions on the recipients' exercise of the rights granted herein.
|
||||
You are not responsible for enforcing compliance by third parties to
|
||||
this License.
|
||||
|
||||
7. If, as a consequence of a court judgment or allegation of patent
|
||||
infringement or for any other reason (not limited to patent issues),
|
||||
conditions are imposed on you (whether by court order, agreement or
|
||||
otherwise) that contradict the conditions of this License, they do not
|
||||
excuse you from the conditions of this License. If you cannot
|
||||
distribute so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you
|
||||
may not distribute the Program at all. For example, if a patent
|
||||
license would not permit royalty-free redistribution of the Program by
|
||||
all those who receive copies directly or indirectly through you, then
|
||||
the only way you could satisfy both it and this License would be to
|
||||
refrain entirely from distribution of the Program.
|
||||
|
||||
If any portion of this section is held invalid or unenforceable under
|
||||
any particular circumstance, the balance of the section is intended to
|
||||
apply and the section as a whole is intended to apply in other
|
||||
circumstances.
|
||||
|
||||
It is not the purpose of this section to induce you to infringe any
|
||||
patents or other property right claims or to contest validity of any
|
||||
such claims; this section has the sole purpose of protecting the
|
||||
integrity of the free software distribution system, which is
|
||||
implemented by public license practices. Many people have made
|
||||
generous contributions to the wide range of software distributed
|
||||
through that system in reliance on consistent application of that
|
||||
system; it is up to the author/donor to decide if he or she is willing
|
||||
to distribute software through any other system and a licensee cannot
|
||||
impose that choice.
|
||||
|
||||
This section is intended to make thoroughly clear what is believed to
|
||||
be a consequence of the rest of this License.
|
||||
|
||||
8. If the distribution and/or use of the Program is restricted in
|
||||
certain countries either by patents or by copyrighted interfaces, the
|
||||
original copyright holder who places the Program under this License
|
||||
may add an explicit geographical distribution limitation excluding
|
||||
those countries, so that distribution is permitted only in or among
|
||||
countries not thus excluded. In such case, this License incorporates
|
||||
the limitation as if written in the body of this License.
|
||||
|
||||
9. The Free Software Foundation may publish revised and/or new versions
|
||||
of the General Public License from time to time. Such new versions will
|
||||
be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the Program
|
||||
specifies a version number of this License which applies to it and "any
|
||||
later version", you have the option of following the terms and conditions
|
||||
either of that version or of any later version published by the Free
|
||||
Software Foundation. If the Program does not specify a version number of
|
||||
this License, you may choose any version ever published by the Free Software
|
||||
Foundation.
|
||||
|
||||
10. If you wish to incorporate parts of the Program into other free
|
||||
programs whose distribution conditions are different, write to the author
|
||||
to ask for permission. For software which is copyrighted by the Free
|
||||
Software Foundation, write to the Free Software Foundation; we sometimes
|
||||
make exceptions for this. Our decision will be guided by the two goals
|
||||
of preserving the free status of all derivatives of our free software and
|
||||
of promoting the sharing and reuse of software generally.
|
||||
|
||||
NO WARRANTY
|
||||
|
||||
11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
|
||||
FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
|
||||
OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
|
||||
PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
|
||||
OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
|
||||
TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
|
||||
PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
|
||||
REPAIR OR CORRECTION.
|
||||
|
||||
12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
|
||||
REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
|
||||
OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
|
||||
TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
|
||||
YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
|
||||
PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
|
||||
POSSIBILITY OF SUCH DAMAGES.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Programs
|
||||
|
||||
If you develop a new program, and you want it to be of the greatest
|
||||
possible use to the public, the best way to achieve this is to make it
|
||||
free software which everyone can redistribute and change under these terms.
|
||||
|
||||
To do so, attach the following notices to the program. It is safest
|
||||
to attach them to the start of each source file to most effectively
|
||||
convey the exclusion of warranty; and each file should have at least
|
||||
the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
<one line to give the program's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
This program is free software; you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation; either version 2 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License along
|
||||
with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If the program is interactive, make it output a short notice like this
|
||||
when it starts in an interactive mode:
|
||||
|
||||
Gnomovision version 69, Copyright (C) year name of author
|
||||
Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||
This is free software, and you are welcome to redistribute it
|
||||
under certain conditions; type `show c' for details.
|
||||
|
||||
The hypothetical commands `show w' and `show c' should show the appropriate
|
||||
parts of the General Public License. Of course, the commands you use may
|
||||
be called something other than `show w' and `show c'; they could even be
|
||||
mouse-clicks or menu items--whatever suits your program.
|
||||
|
||||
You should also get your employer (if you work as a programmer) or your
|
||||
school, if any, to sign a "copyright disclaimer" for the program, if
|
||||
necessary. Here is a sample; alter the names:
|
||||
|
||||
Yoyodyne, Inc., hereby disclaims all copyright interest in the program
|
||||
`Gnomovision' (which makes passes at compilers) written by James Hacker.
|
||||
|
||||
<signature of Ty Coon>, 1 April 1989
|
||||
Ty Coon, President of Vice
|
||||
|
||||
This General Public License does not permit incorporating your program into
|
||||
proprietary programs. If your program is a subroutine library, you may
|
||||
consider it more useful to permit linking proprietary applications with the
|
||||
library. If this is what you want to do, use the GNU Lesser General
|
||||
Public License instead of this License.
|
||||
@@ -1,90 +0,0 @@
|
||||
# Bloodborne Seamless Co-op (BBSC)
|
||||
|
||||
Play the whole of Bloodborne together. BBSC is a mod for the [shadPS4](https://github.com/shadps4-emu/shadPS4)
|
||||
emulator that keeps a co-op session alive through deaths, lamps, area changes, bosses and the Hunter's
|
||||
Dream - up to five hunters, each keeping their own character.
|
||||
|
||||
**Downloads:** Nexus Mods (link coming with the release) · **Community:** [Discord](https://discord.gg/Df7BCmfWdt)
|
||||
|
||||
> 0.0.1 is the first public test. Back up your saves before playing.
|
||||
|
||||
## Features
|
||||
|
||||
- One co-op session that lasts through deaths, lamps, area changes, bosses and the Hunter's Dream
|
||||
- Up to 5 hunters: the host and 4 guests
|
||||
- Guests follow the host between areas and lamps and respawn with the group
|
||||
- Everyone keeps their own levels, echoes and items
|
||||
- Shared progress (optional): the host's boss kills and shortcuts are added to the guest's world
|
||||
- Guests get boss rewards and enemy drops; items already picked up do not come back
|
||||
- Enemy scaling by the number of hunters
|
||||
- A co-op kit with no Insight cost: Hunt Bell, Kin Bell, Bloodlust Bell, Parting Blank, Duel Bell (PvP toggle)
|
||||
- Clear co-op messages in place of the vanilla "beckoner / resonance" lines
|
||||
- Public server, self-hosted servers (Radmin VPN, Tailscale, LAN, VPS) and split screen (Nucleus Co-op)
|
||||
- Runs in shadPS4 and in the BBport Windows build
|
||||
|
||||
## Requirements
|
||||
|
||||
- Your own Bloodborne **CUSA03173, version 01.09** - no game files are included
|
||||
- Windows (the server also runs on Linux)
|
||||
|
||||
## Downloads
|
||||
|
||||
| File | For |
|
||||
|---|---|
|
||||
| `BBSC-<version>-Play.zip` | Players: launcher + emulator + BBSC, connects to the public server |
|
||||
| `BBSC-<version>-Server.zip` | Hosting your own server (Windows) |
|
||||
| `BBSC-<version>-Nucleus.zip` | Split screen with the [Bloodborne Nucleus Co-op Handler](https://github.com/framilano/BloodborneNucleusCoopHandler) |
|
||||
| `BBSC-<version>-BBport.zip` | Test build for the [BBport Windows build](https://github.com/Supermedo/bloodborne_pc) - same server as shadPS4 players |
|
||||
|
||||
## Quick start
|
||||
|
||||
1. Unzip the Play package and keep its files together.
|
||||
2. Run `PLAY BBSC.cmd`. Your shadPS4 settings, patches and saves are used as they are.
|
||||
3. Launcher > Settings > Accounts > Create Account on Server.
|
||||
4. Host rings the Hunt Bell, guests ring the Kin Bell.
|
||||
|
||||
## Repository layout
|
||||
|
||||
| Folder | Contents |
|
||||
|---|---|
|
||||
| `emulator/` | The shadPS4 fork (shadp2p based). BBSC game code: `src/core/bloodborne_re.cpp`; the mod DLL: `mod/` |
|
||||
| `launcher/` | The Qt launcher fork (Mods and Accounts tabs) |
|
||||
| `server/` | Host package scripts and the BBSC game server (`bbsc_gameserver.py`) |
|
||||
| `tools/` | Game data readers and helper tools |
|
||||
| `docs/` | Architecture, world sync, networking, known limitations, changelog |
|
||||
|
||||
## Building
|
||||
|
||||
- Emulator: `build/build-shadp2p.cmd` (Windows, Visual Studio 2022, CMake, Ninja)
|
||||
- Mod DLL: `emulator/mod/build-mod.cmd`
|
||||
- Game server: Python 3, no extra packages (`server/gameserver/bbsc_gameserver.py --shadnet <address>`)
|
||||
|
||||
## Reporting bugs
|
||||
|
||||
Post in `#bug-reports` on the [Discord](https://discord.gg/Df7BCmfWdt) with the version, host or guest,
|
||||
what happened and when, and both logs: the BBSC log (`mods/CUSA03173/bbsc/logs/`) and the emulator log
|
||||
(`user/log/shad_log.txt` or `%APPDATA%\shadPS4\log\`).
|
||||
|
||||
## Credits
|
||||
|
||||
- **CLS_AMG** - BBSC
|
||||
- **framilano (fra98_)** - Bloodborne Nucleus Co-op Handler; tester
|
||||
- **Wozzardman** - [shadp2p](https://github.com/Wozzardman/shadp2p) and shadnet-p2p, the online fork BBSC is built on
|
||||
- **Fahad (fhody125)** - No Dream For The Hunter, the game server groundwork (MIT)
|
||||
- **shadPS4 team and contributors** - the emulator; **shadNet team** - shadNet
|
||||
- **The Hunter's Dream team**
|
||||
- **deadinside28** (bbport) and **Supermedo** (its Windows build)
|
||||
- **Testers** - Hardwater, Onesadeye, aboood888848, Fahad, Mob, lelial, J2mF, Kukar3x
|
||||
- Supporters: see `SUPPORTERS.txt`
|
||||
|
||||
## Support
|
||||
|
||||
BBSC is free for everyone. If you want to support the time that goes into it:
|
||||
[Patreon](https://www.patreon.com/c/CLS_AMG) or [Buy Me a Coffee](https://buymeacoffee.com/j2mf).
|
||||
|
||||
## License
|
||||
|
||||
GPL-2.0-or-later, like shadPS4. See `LICENSE` and `NOTICE.md`. Third-party parts keep their own licenses.
|
||||
|
||||
Bloodborne is developed by FromSoftware and published by Sony Interactive Entertainment. BBSC is a free
|
||||
fan project and is not affiliated with or endorsed by either.
|
||||
@@ -1,24 +0,0 @@
|
||||
BBSC SUPPORTERS
|
||||
===============
|
||||
|
||||
BBSC is free for everyone, and every build and feature reaches everyone on the same day.
|
||||
These people support the time that goes into it. Thank you.
|
||||
|
||||
Support BBSC:
|
||||
- Patreon: https://www.patreon.com/c/CLS_AMG
|
||||
- Buy Me a Coffee: https://buymeacoffee.com/j2mf
|
||||
|
||||
|
||||
WORKSHOP
|
||||
--------
|
||||
(no one yet)
|
||||
|
||||
|
||||
HUNTER
|
||||
------
|
||||
(no one yet)
|
||||
|
||||
|
||||
LAMPLIGHT
|
||||
---------
|
||||
(no one yet)
|
||||
@@ -1,369 +0,0 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2026 CLS_AMG
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
// BBSC mod: the P2P channel - BBSC's own packets over the game's own player-to-player connection,
|
||||
// for servers that run no BBSC relay (the public ones: The Hunter's Dream + shadNet).
|
||||
//
|
||||
// The players of a session already exchange P2P datagrams (the game's socket, vport 40). The
|
||||
// emulator's P2P transport (official shadPS4: core/net/p2p_transport.cpp; the shadp2p fork:
|
||||
// network/p2p_sockets.cpp) carries a source and destination vport in every frame and hands each
|
||||
// frame to the socket bound to that vport, so a second socket on a vport of our own sees only
|
||||
// BBSC's frames and the game never sees them.
|
||||
//
|
||||
// - Peers: the game's import slots for sceNetSendto/sceNetRecvfrom are wrapped; every address
|
||||
// the game sends to or receives from on a P2P vport is a player of our session (kept while
|
||||
// the game talks to it, dropped 20 s after). sceNpSignalingGetConnectionStatus is wrapped too
|
||||
// (ACTIVE connections give the peer's address).
|
||||
// - Socket: our own DGRAM_P2P socket on ChannelVport, made through the emulator's sceNet
|
||||
// functions (the originals from the same slots), non-blocking, read by our own thread.
|
||||
// - Frames: the relay's frames unchanged (the "SHAD" 0x40 envelope with sender + session,
|
||||
// bbsc_relay.cpp), one datagram each; the receive side is the relay's Dispatch.
|
||||
//
|
||||
// Import slots, CUSA03173 01.09 (tools\eboot-imports.py, BBSC offsets):
|
||||
// 0x053E4B00 sceNetSocket 0x053E4B38 sceNetBind 0x053E4B68 sceNetSetsockopt
|
||||
// 0x053E4F48 sceNetSendto 0x053E4F58 sceNetRecvfrom
|
||||
// 0x053E52D0 sceNpSignalingGetConnectionStatus
|
||||
// Switch SHADPS4_BBSC_P2P_CHANNEL (Mods tab, on). Datagrams can be lost: every BBSC packet was
|
||||
// made for the emulator's UDP signaling first and is re-sent or repeated by its sender.
|
||||
|
||||
#ifdef _WIN32
|
||||
#include <windows.h>
|
||||
#else
|
||||
#include "linux/bbsc_win32.h" // bbport
|
||||
#endif
|
||||
|
||||
#include <atomic>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <map>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "common/logging/log.h"
|
||||
#include "common/types.h"
|
||||
|
||||
namespace BbscMod {
|
||||
u64 EbootAddress(); // bbsc_mod.cpp
|
||||
void GuardSlot(u64 slot_offset, u64 wrapper, void** original, const char* name);
|
||||
void RelayDispatch(const std::vector<u8>& wrapped); // bbsc_relay.cpp
|
||||
} // namespace BbscMod
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr u64 EbootSize = 0x056D4000ULL;
|
||||
constexpr u64 SlotSocket = 0x053E4B00;
|
||||
constexpr u64 SlotSocketClose = 0x053E4B08;
|
||||
constexpr u64 SlotBind = 0x053E4B38;
|
||||
constexpr u64 SlotSetsockopt = 0x053E4B68;
|
||||
constexpr u64 SlotSendto = 0x053E4F48;
|
||||
constexpr u64 SlotRecvfrom = 0x053E4F58;
|
||||
constexpr u64 SlotConnectionStatus = 0x053E52D0;
|
||||
|
||||
constexpr u16 ChannelVport = 3173; // below 32768 (official shadPS4 reserves the rest); game uses 40
|
||||
constexpr s32 OrbisAfInet = 2;
|
||||
constexpr s32 OrbisSockDgramP2P = 6;
|
||||
constexpr s32 OrbisSolSocket = 0xFFFF;
|
||||
constexpr s32 OrbisSoNbio = 0x1200;
|
||||
constexpr s32 OrbisMsgDontwait = 0x80;
|
||||
constexpr u32 SignalingConnActive = 2;
|
||||
constexpr s64 PeerKeepMs = 20'000;
|
||||
constexpr size_t MaxDatagram = 2048;
|
||||
|
||||
#pragma pack(push, 1)
|
||||
struct OrbisSockaddrIn {
|
||||
u8 sin_len;
|
||||
u8 sin_family;
|
||||
u16 sin_port; // network order (P2P datagrams: the peer's UDP port)
|
||||
u32 sin_addr; // network order
|
||||
u16 sin_vport; // network order
|
||||
char sin_zero[6];
|
||||
};
|
||||
#pragma pack(pop)
|
||||
static_assert(sizeof(OrbisSockaddrIn) == 16);
|
||||
|
||||
using SocketFn = s32 PS4_SYSV_ABI (*)(const char*, s32, s32, s32);
|
||||
using SocketCloseFn = s32 PS4_SYSV_ABI (*)(s32);
|
||||
using BindFn = s32 PS4_SYSV_ABI (*)(s32, const void*, u32);
|
||||
using SetsockoptFn = s32 PS4_SYSV_ABI (*)(s32, s32, s32, const void*, u32);
|
||||
using SendtoFn = s32 PS4_SYSV_ABI (*)(s32, const void*, u64, s32, const void*, u32);
|
||||
using RecvfromFn = s32 PS4_SYSV_ABI (*)(s32, void*, u64, s32, void*, u32*);
|
||||
using ConnectionStatusFn = s32 PS4_SYSV_ABI (*)(s32, s32, u32*, u32*, u16*);
|
||||
|
||||
SendtoFn original_sendto = nullptr;
|
||||
RecvfromFn original_recvfrom = nullptr;
|
||||
ConnectionStatusFn original_connection_status = nullptr;
|
||||
|
||||
struct Peer {
|
||||
s64 seen_ms = 0;
|
||||
bool logged = false;
|
||||
};
|
||||
|
||||
std::mutex peers_mutex;
|
||||
std::map<u64, Peer> peers; // key: addr << 16 | port (both network order)
|
||||
std::atomic<s32> channel_socket{-1};
|
||||
std::atomic<bool> enabled{false};
|
||||
std::atomic<u32> sent_count{0};
|
||||
std::atomic<u32> received_count{0};
|
||||
|
||||
s64 NowMs() {
|
||||
return static_cast<s64>(GetTickCount64());
|
||||
}
|
||||
|
||||
std::string AddressText(u32 addr, u16 port) {
|
||||
const auto* b = reinterpret_cast<const u8*>(&addr);
|
||||
return fmt::format("{}.{}.{}.x:{}", b[0], b[1], b[2], static_cast<u16>((port >> 8) | (port << 8)));
|
||||
}
|
||||
|
||||
void NotePeer(u32 addr, u16 port, const char* how) {
|
||||
if (addr == 0 || port == 0) {
|
||||
return;
|
||||
}
|
||||
const u64 key = (static_cast<u64>(addr) << 16) | port;
|
||||
std::scoped_lock lock{peers_mutex};
|
||||
Peer& peer = peers[key];
|
||||
const bool fresh = !peer.logged || NowMs() - peer.seen_ms > PeerKeepMs;
|
||||
peer.seen_ms = NowMs();
|
||||
if (fresh) {
|
||||
peer.logged = true;
|
||||
LOG_INFO(Mod, "P2P channel: player at {} ({})", AddressText(addr, port), how);
|
||||
}
|
||||
}
|
||||
|
||||
void NoteGameAddress(const void* addr, u32 len, const char* how) {
|
||||
if (!enabled || addr == nullptr || len < sizeof(OrbisSockaddrIn)) {
|
||||
return;
|
||||
}
|
||||
OrbisSockaddrIn in{};
|
||||
std::memcpy(&in, addr, sizeof(in));
|
||||
// A P2P address has a vport; plain UDP/TCP sockets of the game leave it 0.
|
||||
if (in.sin_family != OrbisAfInet || in.sin_vport == 0 ||
|
||||
in.sin_vport == static_cast<u16>((ChannelVport >> 8) | (ChannelVport << 8))) {
|
||||
return;
|
||||
}
|
||||
NotePeer(in.sin_addr, in.sin_port, how);
|
||||
}
|
||||
|
||||
s32 PS4_SYSV_ABI Sendto(s32 s, const void* buf, u64 len, s32 flags, const void* addr, u32 addrlen) {
|
||||
const s32 r = original_sendto(s, buf, len, flags, addr, addrlen);
|
||||
if (r > 0 && s != channel_socket) {
|
||||
NoteGameAddress(addr, addrlen, "game sends to it");
|
||||
}
|
||||
return r;
|
||||
}
|
||||
|
||||
s32 PS4_SYSV_ABI Recvfrom(s32 s, void* buf, u64 len, s32 flags, void* addr, u32* addrlen) {
|
||||
const s32 r = original_recvfrom(s, buf, len, flags, addr, addrlen);
|
||||
if (r > 0 && addrlen != nullptr && s != channel_socket) {
|
||||
NoteGameAddress(addr, *addrlen, "game hears from it");
|
||||
}
|
||||
return r;
|
||||
}
|
||||
|
||||
s32 PS4_SYSV_ABI ConnectionStatus(s32 ctx, s32 conn, u32* status, u32* addr, u16* port) {
|
||||
// The game may pass no address pointers: ask with our own, hand back what it asked for.
|
||||
u32 own_addr = 0;
|
||||
u16 own_port = 0;
|
||||
const s32 r = original_connection_status(ctx, conn, status, addr ? addr : &own_addr,
|
||||
port ? port : &own_port);
|
||||
if (r == 0 && status != nullptr && *status == SignalingConnActive) {
|
||||
NotePeer(addr ? *addr : own_addr, port ? *port : own_port, "signaling connection active");
|
||||
}
|
||||
return r;
|
||||
}
|
||||
|
||||
template <typename Fn>
|
||||
bool Swap(u64 slot_offset, Fn wrapper, Fn& original, const char* name) {
|
||||
auto* slot = reinterpret_cast<u64*>(BbscMod::EbootAddress() + slot_offset);
|
||||
for (int i = 0; i < 60'000; ++i) {
|
||||
const u64 value = *slot;
|
||||
if (value != 0 &&
|
||||
(value < BbscMod::EbootAddress() || value >= BbscMod::EbootAddress() + EbootSize)) {
|
||||
original = reinterpret_cast<Fn>(value);
|
||||
DWORD old = 0;
|
||||
VirtualProtect(slot, 8, PAGE_READWRITE, &old);
|
||||
*slot = reinterpret_cast<u64>(wrapper);
|
||||
DWORD ignored = 0;
|
||||
VirtualProtect(slot, 8, old, &ignored);
|
||||
BbscMod::GuardSlot(slot_offset, reinterpret_cast<u64>(wrapper),
|
||||
reinterpret_cast<void**>(&original), name);
|
||||
return true;
|
||||
}
|
||||
Sleep(1);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
template <typename Fn>
|
||||
Fn ReadSlot(u64 slot_offset) {
|
||||
const u64 value = *reinterpret_cast<const u64*>(BbscMod::EbootAddress() + slot_offset);
|
||||
if (value == 0 || (value >= BbscMod::EbootAddress() && value < BbscMod::EbootAddress() + EbootSize)) {
|
||||
return nullptr;
|
||||
}
|
||||
return reinterpret_cast<Fn>(value);
|
||||
}
|
||||
|
||||
// Made on the first peer: the emulator starts its P2P transport when the game needs it.
|
||||
// Made on the first peer: the emulator starts its P2P transport when the game needs it. The slots
|
||||
// are read here, not at start-up (review 2026-10-11): the official shadPS4 links the imports again
|
||||
// after BBSC starts, and these slots are not wrapped, so they always hold the emulator's current
|
||||
// functions.
|
||||
bool OpenSocket() {
|
||||
const auto open = ReadSlot<SocketFn>(SlotSocket);
|
||||
const auto bind = ReadSlot<BindFn>(SlotBind);
|
||||
const auto setsockopt = ReadSlot<SetsockoptFn>(SlotSetsockopt);
|
||||
const auto close = ReadSlot<SocketCloseFn>(SlotSocketClose);
|
||||
if (open == nullptr || bind == nullptr || setsockopt == nullptr) {
|
||||
LOG_WARNING(Mod, "P2P channel: the game's socket imports are not filled - retrying");
|
||||
return false;
|
||||
}
|
||||
const s32 s = open("BBSC channel", OrbisAfInet, OrbisSockDgramP2P, 0);
|
||||
if (s < 0) {
|
||||
LOG_WARNING(Mod, "P2P channel: sceNetSocket failed ({:#x}) - retrying", static_cast<u32>(s));
|
||||
return false;
|
||||
}
|
||||
OrbisSockaddrIn bind_to{};
|
||||
bind_to.sin_len = sizeof(bind_to);
|
||||
bind_to.sin_family = OrbisAfInet;
|
||||
bind_to.sin_vport = static_cast<u16>((ChannelVport >> 8) | (ChannelVport << 8));
|
||||
if (const s32 r = bind(s, &bind_to, sizeof(bind_to)); r < 0) {
|
||||
LOG_WARNING(Mod, "P2P channel: bind to vport {} failed ({:#x}) - retrying", ChannelVport,
|
||||
static_cast<u32>(r));
|
||||
if (close != nullptr) {
|
||||
close(s);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
const s32 on = 1;
|
||||
setsockopt(s, OrbisSolSocket, OrbisSoNbio, &on, sizeof(on));
|
||||
channel_socket = s;
|
||||
LOG_INFO(Mod, "P2P channel: socket {} on vport {} - BBSC packets go to the players of the "
|
||||
"session directly",
|
||||
s, ChannelVport);
|
||||
return true;
|
||||
}
|
||||
|
||||
DWORD WINAPI ChannelThread(void*) {
|
||||
std::vector<u8> buffer(MaxDatagram);
|
||||
s64 next_open_ms = 0;
|
||||
int failures = 0;
|
||||
s64 next_report_ms = NowMs() + 60'000;
|
||||
for (;;) {
|
||||
const s32 s = channel_socket;
|
||||
if (s < 0) {
|
||||
bool any = false;
|
||||
{
|
||||
std::scoped_lock lock{peers_mutex};
|
||||
any = !peers.empty();
|
||||
}
|
||||
if (any && NowMs() >= next_open_ms && failures < 10 && !OpenSocket()) {
|
||||
next_open_ms = NowMs() + 5'000;
|
||||
if (++failures == 10) {
|
||||
LOG_ERROR(Mod, "P2P channel: no socket after 10 tries - channel off for this run");
|
||||
}
|
||||
}
|
||||
Sleep(100);
|
||||
continue;
|
||||
}
|
||||
bool got_any = false;
|
||||
for (int i = 0; i < 64; ++i) {
|
||||
OrbisSockaddrIn from{};
|
||||
u32 from_len = sizeof(from);
|
||||
const s32 r = original_recvfrom(s, buffer.data(), buffer.size(), OrbisMsgDontwait,
|
||||
&from, &from_len);
|
||||
if (r <= 0) {
|
||||
break;
|
||||
}
|
||||
got_any = true;
|
||||
++received_count;
|
||||
NotePeer(from.sin_addr, from.sin_port, "BBSC packet from it");
|
||||
BbscMod::RelayDispatch(std::vector<u8>(buffer.begin(), buffer.begin() + r));
|
||||
}
|
||||
if (NowMs() >= next_report_ms) {
|
||||
next_report_ms = NowMs() + 60'000;
|
||||
const u32 sent = sent_count.exchange(0);
|
||||
const u32 received = received_count.exchange(0);
|
||||
if (sent != 0 || received != 0) {
|
||||
LOG_INFO(Mod, "P2P channel: last minute {} packet(s) sent, {} received", sent,
|
||||
received);
|
||||
}
|
||||
}
|
||||
Sleep(got_any ? 1 : 5);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
namespace BbscMod {
|
||||
|
||||
bool P2PChannelEnabled() {
|
||||
return enabled;
|
||||
}
|
||||
|
||||
// One relay frame (envelope included, no length prefix) to every live peer. Returns the peer count.
|
||||
u32 P2PChannelSend(const void* data, u32 size) {
|
||||
const s32 s = channel_socket;
|
||||
if (!enabled || s < 0 || size > MaxDatagram) {
|
||||
return 0;
|
||||
}
|
||||
std::vector<u64> live;
|
||||
{
|
||||
std::scoped_lock lock{peers_mutex};
|
||||
const s64 now = NowMs();
|
||||
for (auto it = peers.begin(); it != peers.end();) {
|
||||
if (now - it->second.seen_ms > PeerKeepMs) {
|
||||
it = peers.erase(it);
|
||||
} else {
|
||||
live.push_back(it->first);
|
||||
++it;
|
||||
}
|
||||
}
|
||||
}
|
||||
u32 count = 0;
|
||||
for (const u64 key : live) {
|
||||
OrbisSockaddrIn to{};
|
||||
to.sin_len = sizeof(to);
|
||||
to.sin_family = OrbisAfInet;
|
||||
to.sin_addr = static_cast<u32>(key >> 16);
|
||||
to.sin_port = static_cast<u16>(key);
|
||||
to.sin_vport = static_cast<u16>((ChannelVport >> 8) | (ChannelVport << 8));
|
||||
if (original_sendto(s, data, size, OrbisMsgDontwait, &to, sizeof(to)) > 0) {
|
||||
++count;
|
||||
}
|
||||
}
|
||||
sent_count += count;
|
||||
return count;
|
||||
}
|
||||
|
||||
u32 P2PChannelPeerCount() {
|
||||
std::scoped_lock lock{peers_mutex};
|
||||
const s64 now = NowMs();
|
||||
u32 count = 0;
|
||||
for (const auto& [key, peer] : peers) {
|
||||
count += now - peer.seen_ms <= PeerKeepMs ? 1 : 0;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
void StartP2PChannel() {
|
||||
if (const char* env = std::getenv("SHADPS4_BBSC_P2P_CHANNEL");
|
||||
env != nullptr && (env[0] == '0' || env[0] == 'f' || env[0] == 'F')) {
|
||||
LOG_INFO(Mod, "P2P channel: off (switch)");
|
||||
return;
|
||||
}
|
||||
if (!Swap<SendtoFn>(SlotSendto, &Sendto, original_sendto, "Sendto") ||
|
||||
!Swap<RecvfromFn>(SlotRecvfrom, &Recvfrom, original_recvfrom, "Recvfrom")) {
|
||||
LOG_ERROR(Mod, "P2P channel: an import slot was never filled - channel off");
|
||||
return;
|
||||
}
|
||||
Swap<ConnectionStatusFn>(SlotConnectionStatus, &ConnectionStatus, original_connection_status,
|
||||
"ConnectionStatus");
|
||||
enabled = true;
|
||||
if (HANDLE thread = CreateThread(nullptr, 0, ChannelThread, nullptr, 0, nullptr)) {
|
||||
CloseHandle(thread);
|
||||
}
|
||||
LOG_INFO(Mod, "P2P channel: ready (vport {}), waiting for the players of a session",
|
||||
ChannelVport);
|
||||
}
|
||||
|
||||
} // namespace BbscMod
|
||||
@@ -1,228 +0,0 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2026 CLS_AMG
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
// BBSC Discord status: "Playing BBSC - In Central Yharnam - Hosting (2 of 5)", or the boss during a
|
||||
// boss fight. Talks to the Discord app's local pipe (\\.\pipe\discord-ipc-N) itself - no Discord
|
||||
// library: a frame is {u32 opcode, u32 length, JSON}; opcode 0 = handshake with the application ID,
|
||||
// opcode 1 = a command (SET_ACTIVITY). The game state comes from bloodborne_re.cpp's game tick
|
||||
// (GetBbscPresence). Off with the Mods tab switch "Discord status" (SHADPS4_BBSC_DISCORD=0).
|
||||
// No Discord running = nothing happens; it tries again every 20 s.
|
||||
|
||||
#include <windows.h>
|
||||
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
|
||||
#include <fmt/format.h>
|
||||
|
||||
#include "core/bloodborne_re.h"
|
||||
|
||||
namespace BbscMod {
|
||||
void LogPlain(const char* text);
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr const char* DiscordApplicationId = "1554935855674691645"; // "BBSC" (CLS_AMG)
|
||||
|
||||
bool DiscordStatusEnabled() {
|
||||
const char* value = std::getenv("SHADPS4_BBSC_DISCORD");
|
||||
return value == nullptr || (std::string{value} != "0" && std::string{value} != "false");
|
||||
}
|
||||
|
||||
std::string Json(const std::string& text) {
|
||||
std::string out = "\"";
|
||||
for (const char c : text) {
|
||||
if (c == '"' || c == '\\') {
|
||||
out += '\\';
|
||||
out += c;
|
||||
} else if (static_cast<unsigned char>(c) < 0x20) {
|
||||
out += ' ';
|
||||
} else {
|
||||
out += c;
|
||||
}
|
||||
}
|
||||
return out + '"';
|
||||
}
|
||||
|
||||
class DiscordPipe {
|
||||
public:
|
||||
~DiscordPipe() {
|
||||
Close();
|
||||
}
|
||||
|
||||
bool Connect() {
|
||||
for (int i = 0; i < 10; ++i) {
|
||||
const std::wstring name = L"\\\\.\\pipe\\discord-ipc-" + std::to_wstring(i);
|
||||
pipe = CreateFileW(name.c_str(), GENERIC_READ | GENERIC_WRITE, 0, nullptr, OPEN_EXISTING,
|
||||
0, nullptr);
|
||||
if (pipe != INVALID_HANDLE_VALUE) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (pipe == INVALID_HANDLE_VALUE) {
|
||||
return false;
|
||||
}
|
||||
if (!Send(0, fmt::format("{{\"v\":1,\"client_id\":\"{}\"}}", DiscordApplicationId))) {
|
||||
Close();
|
||||
return false;
|
||||
}
|
||||
return Drain(true);
|
||||
}
|
||||
|
||||
bool Send(std::uint32_t opcode, const std::string& json) {
|
||||
if (pipe == INVALID_HANDLE_VALUE) {
|
||||
return false;
|
||||
}
|
||||
std::string frame(8, '\0');
|
||||
const auto length = static_cast<std::uint32_t>(json.size());
|
||||
std::memcpy(frame.data(), &opcode, 4);
|
||||
std::memcpy(frame.data() + 4, &length, 4);
|
||||
frame += json;
|
||||
DWORD written = 0;
|
||||
return WriteFile(pipe, frame.data(), static_cast<DWORD>(frame.size()), &written, nullptr) &&
|
||||
written == frame.size();
|
||||
}
|
||||
|
||||
// Reads whatever Discord sent (answers, READY) so the pipe never fills. wait = block for one
|
||||
// frame (the handshake answer). False = the pipe is gone or Discord closed it (opcode 2).
|
||||
bool Drain(bool wait) {
|
||||
for (;;) {
|
||||
DWORD available = 0;
|
||||
if (!PeekNamedPipe(pipe, nullptr, 0, nullptr, &available, nullptr)) {
|
||||
Close();
|
||||
return false;
|
||||
}
|
||||
if (available < 8 && !wait) {
|
||||
return true;
|
||||
}
|
||||
std::uint32_t header[2]{};
|
||||
DWORD got = 0;
|
||||
if (!ReadFile(pipe, header, 8, &got, nullptr) || got != 8) {
|
||||
Close();
|
||||
return false;
|
||||
}
|
||||
std::string body(header[1], '\0');
|
||||
if (header[1] != 0 &&
|
||||
(!ReadFile(pipe, body.data(), header[1], &got, nullptr) || got != header[1])) {
|
||||
Close();
|
||||
return false;
|
||||
}
|
||||
if (header[0] == 2) { // CLOSE: wrong application ID or Discord shutting down
|
||||
BbscMod::LogPlain(("Discord status: Discord closed the connection: " + body).c_str());
|
||||
Close();
|
||||
return false;
|
||||
}
|
||||
wait = false;
|
||||
}
|
||||
}
|
||||
|
||||
bool Connected() const {
|
||||
return pipe != INVALID_HANDLE_VALUE;
|
||||
}
|
||||
|
||||
void Close() {
|
||||
if (pipe != INVALID_HANDLE_VALUE) {
|
||||
CloseHandle(pipe);
|
||||
pipe = INVALID_HANDLE_VALUE;
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
HANDLE pipe = INVALID_HANDLE_VALUE;
|
||||
};
|
||||
|
||||
std::string ActivityJson(const Core::Bloodborne::BbscPresence& p, std::int64_t game_start_s) {
|
||||
std::string details;
|
||||
std::string state;
|
||||
std::string large_text = "Bloodborne Seamless Co-op";
|
||||
std::int64_t start = game_start_s;
|
||||
if (!p.in_game) {
|
||||
details = "At the title screen";
|
||||
} else if (!p.boss.empty()) {
|
||||
details = "Boss fight: " + p.boss;
|
||||
large_text = "In " + p.area;
|
||||
start = p.boss_since_s;
|
||||
} else {
|
||||
details = "In " + p.area;
|
||||
}
|
||||
if (p.in_game) {
|
||||
state = p.role == 1 ? "Hosting" : p.role == 2 ? "Co-op guest" : "Solo hunter";
|
||||
}
|
||||
std::string activity = "{\"details\":" + Json(details);
|
||||
if (!state.empty()) {
|
||||
activity += ",\"state\":" + Json(state);
|
||||
}
|
||||
activity += fmt::format(",\"timestamps\":{{\"start\":{}}}", start);
|
||||
activity += ",\"assets\":{\"large_image\":\"bbsc\",\"large_text\":" + Json(large_text) + "}";
|
||||
if (p.in_game && p.role != 0) {
|
||||
activity += fmt::format(",\"party\":{{\"id\":\"bbsc-{}\",\"size\":[{},{}]}}",
|
||||
GetCurrentProcessId(), p.hunters, p.max_hunters);
|
||||
}
|
||||
activity += "}";
|
||||
return activity;
|
||||
}
|
||||
|
||||
DWORD WINAPI DiscordThread(LPVOID) {
|
||||
const std::int64_t game_start_s = std::chrono::duration_cast<std::chrono::seconds>(
|
||||
std::chrono::system_clock::now().time_since_epoch())
|
||||
.count();
|
||||
DiscordPipe discord;
|
||||
std::string sent;
|
||||
std::uint64_t nonce = 0;
|
||||
auto next_try = std::chrono::steady_clock::now();
|
||||
bool logged_connect = false;
|
||||
for (;;) {
|
||||
std::this_thread::sleep_for(std::chrono::seconds(2));
|
||||
if (!discord.Connected()) {
|
||||
if (std::chrono::steady_clock::now() < next_try) {
|
||||
continue;
|
||||
}
|
||||
if (!discord.Connect()) {
|
||||
next_try = std::chrono::steady_clock::now() + std::chrono::seconds(20);
|
||||
continue;
|
||||
}
|
||||
if (!logged_connect) {
|
||||
BbscMod::LogPlain("Discord status: connected to Discord");
|
||||
logged_connect = true;
|
||||
}
|
||||
sent.clear();
|
||||
}
|
||||
if (!discord.Drain(false)) {
|
||||
continue;
|
||||
}
|
||||
const std::string activity = ActivityJson(Core::Bloodborne::GetBbscPresence(), game_start_s);
|
||||
if (activity == sent) {
|
||||
continue;
|
||||
}
|
||||
const std::string command = fmt::format(
|
||||
"{{\"cmd\":\"SET_ACTIVITY\",\"args\":{{\"pid\":{},\"activity\":{}}},\"nonce\":\"{}\"}}",
|
||||
GetCurrentProcessId(), activity, ++nonce);
|
||||
if (discord.Send(1, command)) {
|
||||
sent = activity;
|
||||
} else {
|
||||
discord.Close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
namespace BbscMod {
|
||||
|
||||
void StartDiscordStatus() {
|
||||
if (!DiscordStatusEnabled()) {
|
||||
LogPlain("Discord status: off (Mods tab)");
|
||||
return;
|
||||
}
|
||||
if (HANDLE thread = CreateThread(nullptr, 0, DiscordThread, nullptr, 0, nullptr)) {
|
||||
CloseHandle(thread);
|
||||
LogPlain("Discord status: on - shows when the Discord app is running");
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace BbscMod
|
||||
@@ -1,260 +0,0 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2026 CLS_AMG
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
// BBSC mod: Core::InstallGuestCodeHook for the DLL. The same trampoline as the emulator's
|
||||
// (core/cpu_patches.cpp): save every register and YMM into a GuestRegisterSnapshot, call the C++
|
||||
// hook with it, restore the (possibly changed) snapshot, run the copied instructions, jump back.
|
||||
//
|
||||
// Where the trampolines live: the emulator maps an 8 MB read/write/execute area right after every
|
||||
// module's image for its own patches (core/module.cpp, TrampolineSize) and fills it from the
|
||||
// start. The DLL takes the last 1 MB of the eboot's area - inside the 2 GB reach of a 5-byte jmp
|
||||
// from any game instruction, and checked to be mapped, executable and still all zero before use.
|
||||
// The image is wherever bbsc_mod.cpp found it (EbootAddress). If that area is not there or not
|
||||
// free (another emulator build), the DLL maps its own 1 MB within reach after (or before) the image.
|
||||
//
|
||||
// Not done here (the emulator checks them with its instruction decoder): that the copied bytes are
|
||||
// whole instructions without RIP-relative operands. Every site BBSC hooks is a fixed, byte-checked
|
||||
// instruction sequence that the emulator build has already installed the same way.
|
||||
|
||||
#include <windows.h>
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstring>
|
||||
#include <mutex>
|
||||
|
||||
#include <xbyak/xbyak.h>
|
||||
|
||||
#include "core/cpu_patches.h"
|
||||
|
||||
namespace BbscMod {
|
||||
void LogPlain(const char* text);
|
||||
u64 EbootAddress();
|
||||
}
|
||||
|
||||
namespace Core {
|
||||
namespace {
|
||||
|
||||
using namespace Xbyak::util;
|
||||
|
||||
constexpr u64 EbootAlignedSize = 0x056D4000ULL;
|
||||
constexpr u64 EmulatorTrampolineSize = 8ULL << 20;
|
||||
constexpr u64 OurSize = 1ULL << 20;
|
||||
constexpr u64 JumpReach = 0x7000'0000ULL; // inside a rel32 jump's 2 GB from anywhere in the image
|
||||
|
||||
std::mutex hook_mutex;
|
||||
Xbyak::CodeGenerator* generator = nullptr;
|
||||
bool area_checked = false;
|
||||
bool area_ok = false;
|
||||
|
||||
// The last 1 MB of the emulator's trampoline area after the image, when it is mapped RWX and unused.
|
||||
bool EmulatorAreaFree(u64 start) {
|
||||
MEMORY_BASIC_INFORMATION info{};
|
||||
if (VirtualQuery(reinterpret_cast<void*>(start), &info, sizeof(info)) == 0 ||
|
||||
info.State != MEM_COMMIT ||
|
||||
(info.Protect & (PAGE_EXECUTE_READWRITE | PAGE_EXECUTE_WRITECOPY)) == 0 ||
|
||||
reinterpret_cast<u64>(info.BaseAddress) + info.RegionSize < start + OurSize) {
|
||||
return false;
|
||||
}
|
||||
const auto* bytes = reinterpret_cast<const u8*>(start);
|
||||
for (u64 i = 0; i < OurSize; ++i) {
|
||||
if (bytes[i] != 0) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Our own 1 MB, 64 KB-aligned, within jump reach of the whole image.
|
||||
u64 MapOwnArea(u64 image) {
|
||||
const u64 image_end = image + EbootAlignedSize + EmulatorTrampolineSize;
|
||||
for (u64 at = (image_end + 0xFFFF) & ~0xFFFFULL; at < image + JumpReach; at += 0x10000) {
|
||||
if (void* got = VirtualAlloc(reinterpret_cast<void*>(at), OurSize, MEM_RESERVE | MEM_COMMIT,
|
||||
PAGE_EXECUTE_READWRITE)) {
|
||||
return reinterpret_cast<u64>(got);
|
||||
}
|
||||
}
|
||||
const u64 low = image > JumpReach - EbootAlignedSize ? image + EbootAlignedSize - JumpReach
|
||||
: 0x10000;
|
||||
for (u64 at = (image - OurSize) & ~0xFFFFULL; at >= low && at < image; at -= 0x10000) {
|
||||
if (void* got = VirtualAlloc(reinterpret_cast<void*>(at), OurSize, MEM_RESERVE | MEM_COMMIT,
|
||||
PAGE_EXECUTE_READWRITE)) {
|
||||
return reinterpret_cast<u64>(got);
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
bool CheckArea() {
|
||||
if (area_checked) {
|
||||
return area_ok;
|
||||
}
|
||||
area_checked = true;
|
||||
const u64 image = BbscMod::EbootAddress();
|
||||
if (image == 0) {
|
||||
BbscMod::LogPlain("Hook area: the game image was not found - no hooks");
|
||||
return false;
|
||||
}
|
||||
u64 start = image + EbootAlignedSize + EmulatorTrampolineSize - OurSize;
|
||||
if (!EmulatorAreaFree(start)) {
|
||||
start = MapOwnArea(image);
|
||||
if (start == 0) {
|
||||
BbscMod::LogPlain("Hook area: no free memory within reach of the game image - no hooks");
|
||||
return false;
|
||||
}
|
||||
BbscMod::LogPlain("Hook area: the emulator's area after the image is not usable - BBSC "
|
||||
"mapped its own");
|
||||
}
|
||||
generator = new Xbyak::CodeGenerator(OurSize, reinterpret_cast<void*>(start));
|
||||
area_ok = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool WriteJump(u8* site, const void* target, size_t length) {
|
||||
DWORD old = 0;
|
||||
if (!VirtualProtect(site, length, PAGE_EXECUTE_READWRITE, &old)) {
|
||||
return false;
|
||||
}
|
||||
std::array<u8, 32> bytes{};
|
||||
const s64 rel = reinterpret_cast<s64>(target) - reinterpret_cast<s64>(site + 5);
|
||||
bytes[0] = 0xE9;
|
||||
const s32 rel32 = static_cast<s32>(rel);
|
||||
std::memcpy(bytes.data() + 1, &rel32, 4);
|
||||
for (size_t i = 5; i < length; ++i) {
|
||||
bytes[i] = 0x90;
|
||||
}
|
||||
std::memcpy(site, bytes.data(), length);
|
||||
DWORD ignored = 0;
|
||||
VirtualProtect(site, length, old, &ignored);
|
||||
FlushInstructionCache(GetCurrentProcess(), site, length);
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool InstallGuestCodeHook(void* address, std::span<const u8> expected_instructions, u64 tag,
|
||||
GuestCodeHook hook, const u8* skip_flag) {
|
||||
constexpr size_t NearJumpBytes = 5;
|
||||
constexpr size_t GuestRedZoneBytes = 128;
|
||||
constexpr size_t SnapshotOffset = 0;
|
||||
constexpr size_t ScratchR11Offset = sizeof(GuestRegisterSnapshot);
|
||||
constexpr size_t ScratchRflagsOffset = ScratchR11Offset + sizeof(u64);
|
||||
constexpr size_t FrameReserve =
|
||||
((ScratchRflagsOffset + sizeof(u64) + GuestRedZoneBytes) + 15) & ~size_t{15};
|
||||
|
||||
auto* code = static_cast<u8*>(address);
|
||||
if (hook == nullptr || expected_instructions.size() < NearJumpBytes ||
|
||||
expected_instructions.size() > 32) {
|
||||
return false;
|
||||
}
|
||||
std::scoped_lock lock{hook_mutex};
|
||||
if (!CheckArea()) {
|
||||
return false;
|
||||
}
|
||||
if (std::memcmp(expected_instructions.data(), code, expected_instructions.size()) != 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
auto& trampoline = *generator;
|
||||
const size_t trampoline_offset = trampoline.getSize();
|
||||
const auto* trampoline_start = trampoline.getCurr();
|
||||
const auto snapshot = [](size_t field_offset) {
|
||||
return qword[rsp + SnapshotOffset + field_offset];
|
||||
};
|
||||
|
||||
try {
|
||||
trampoline.lea(rsp, ptr[rsp - FrameReserve]);
|
||||
trampoline.mov(qword[rsp + ScratchR11Offset], r11);
|
||||
trampoline.pushfq();
|
||||
trampoline.pop(r11);
|
||||
trampoline.mov(qword[rsp + ScratchRflagsOffset], r11);
|
||||
trampoline.mov(r11, rsp);
|
||||
trampoline.and_(rsp, -16);
|
||||
|
||||
trampoline.mov(snapshot(offsetof(GuestRegisterSnapshot, rax)), rax);
|
||||
trampoline.mov(snapshot(offsetof(GuestRegisterSnapshot, rbx)), rbx);
|
||||
trampoline.mov(snapshot(offsetof(GuestRegisterSnapshot, rcx)), rcx);
|
||||
trampoline.mov(snapshot(offsetof(GuestRegisterSnapshot, rdx)), rdx);
|
||||
trampoline.mov(snapshot(offsetof(GuestRegisterSnapshot, rsi)), rsi);
|
||||
trampoline.mov(snapshot(offsetof(GuestRegisterSnapshot, rdi)), rdi);
|
||||
trampoline.mov(snapshot(offsetof(GuestRegisterSnapshot, rbp)), rbp);
|
||||
trampoline.mov(snapshot(offsetof(GuestRegisterSnapshot, r8)), r8);
|
||||
trampoline.mov(snapshot(offsetof(GuestRegisterSnapshot, r9)), r9);
|
||||
trampoline.mov(snapshot(offsetof(GuestRegisterSnapshot, r10)), r10);
|
||||
trampoline.mov(snapshot(offsetof(GuestRegisterSnapshot, r12)), r12);
|
||||
trampoline.mov(snapshot(offsetof(GuestRegisterSnapshot, r13)), r13);
|
||||
trampoline.mov(snapshot(offsetof(GuestRegisterSnapshot, r14)), r14);
|
||||
trampoline.mov(snapshot(offsetof(GuestRegisterSnapshot, r15)), r15);
|
||||
trampoline.mov(rax, qword[r11 + ScratchR11Offset]);
|
||||
trampoline.mov(snapshot(offsetof(GuestRegisterSnapshot, r11)), rax);
|
||||
trampoline.mov(rax, qword[r11 + ScratchRflagsOffset]);
|
||||
trampoline.mov(snapshot(offsetof(GuestRegisterSnapshot, rflags)), rax);
|
||||
trampoline.lea(rax, ptr[r11 + FrameReserve]);
|
||||
trampoline.mov(snapshot(offsetof(GuestRegisterSnapshot, rsp)), rax);
|
||||
trampoline.stmxcsr(dword[rsp + SnapshotOffset + offsetof(GuestRegisterSnapshot, mxcsr)]);
|
||||
for (size_t index = 0; index < 16; ++index) {
|
||||
const size_t offset = SnapshotOffset + offsetof(GuestRegisterSnapshot, ymm) +
|
||||
index * sizeof(std::array<u8, 32>);
|
||||
trampoline.vmovdqu(yword[rsp + offset], Xbyak::Ymm(static_cast<int>(index)));
|
||||
}
|
||||
|
||||
// The hook is Windows code: 32 bytes of shadow space and the MS x64 arguments are not
|
||||
// what a PS4_SYSV_ABI function expects, so it is called the SysV way (rdi, rsi), exactly
|
||||
// as the emulator does. A 32-byte gap below the snapshot keeps either convention safe.
|
||||
trampoline.mov(r12, rsp);
|
||||
trampoline.sub(rsp, 32);
|
||||
trampoline.mov(rdi, tag);
|
||||
trampoline.lea(rsi, ptr[r12 + SnapshotOffset]);
|
||||
trampoline.mov(rax, reinterpret_cast<u64>(hook));
|
||||
trampoline.call(rax);
|
||||
trampoline.mov(rsp, r12);
|
||||
|
||||
for (size_t index = 0; index < 16; ++index) {
|
||||
const size_t offset = SnapshotOffset + offsetof(GuestRegisterSnapshot, ymm) +
|
||||
index * sizeof(std::array<u8, 32>);
|
||||
trampoline.vmovdqu(Xbyak::Ymm(static_cast<int>(index)), yword[rsp + offset]);
|
||||
}
|
||||
trampoline.ldmxcsr(dword[rsp + SnapshotOffset + offsetof(GuestRegisterSnapshot, mxcsr)]);
|
||||
trampoline.mov(rbx, snapshot(offsetof(GuestRegisterSnapshot, rbx)));
|
||||
trampoline.mov(rcx, snapshot(offsetof(GuestRegisterSnapshot, rcx)));
|
||||
trampoline.mov(rdx, snapshot(offsetof(GuestRegisterSnapshot, rdx)));
|
||||
trampoline.mov(rsi, snapshot(offsetof(GuestRegisterSnapshot, rsi)));
|
||||
trampoline.mov(rdi, snapshot(offsetof(GuestRegisterSnapshot, rdi)));
|
||||
trampoline.mov(rbp, snapshot(offsetof(GuestRegisterSnapshot, rbp)));
|
||||
trampoline.mov(r8, snapshot(offsetof(GuestRegisterSnapshot, r8)));
|
||||
trampoline.mov(r9, snapshot(offsetof(GuestRegisterSnapshot, r9)));
|
||||
trampoline.mov(r10, snapshot(offsetof(GuestRegisterSnapshot, r10)));
|
||||
trampoline.mov(r11, snapshot(offsetof(GuestRegisterSnapshot, r11)));
|
||||
trampoline.mov(r12, snapshot(offsetof(GuestRegisterSnapshot, r12)));
|
||||
trampoline.mov(r13, snapshot(offsetof(GuestRegisterSnapshot, r13)));
|
||||
trampoline.mov(r14, snapshot(offsetof(GuestRegisterSnapshot, r14)));
|
||||
trampoline.mov(r15, snapshot(offsetof(GuestRegisterSnapshot, r15)));
|
||||
trampoline.mov(rax, snapshot(offsetof(GuestRegisterSnapshot, rflags)));
|
||||
trampoline.push(rax);
|
||||
trampoline.popfq();
|
||||
trampoline.mov(rax, snapshot(offsetof(GuestRegisterSnapshot, rax)));
|
||||
trampoline.mov(rsp, snapshot(offsetof(GuestRegisterSnapshot, rsp)));
|
||||
Xbyak::Label run_original;
|
||||
if (skip_flag != nullptr) {
|
||||
// Function entry only: a plain ret skips the whole function (see cpu_patches.h).
|
||||
trampoline.push(rax);
|
||||
trampoline.mov(rax, reinterpret_cast<u64>(skip_flag));
|
||||
trampoline.cmp(byte[rax], 0);
|
||||
trampoline.mov(byte[rax], 0);
|
||||
trampoline.pop(rax);
|
||||
trampoline.je(run_original, Xbyak::CodeGenerator::LabelType::T_NEAR);
|
||||
trampoline.ret();
|
||||
}
|
||||
trampoline.L(run_original);
|
||||
trampoline.db(expected_instructions.data(), expected_instructions.size());
|
||||
trampoline.jmp(code + expected_instructions.size(), Xbyak::CodeGenerator::LabelType::T_NEAR);
|
||||
} catch (const Xbyak::Error& error) {
|
||||
trampoline.setSize(trampoline_offset);
|
||||
BbscMod::LogPlain(error.what());
|
||||
return false;
|
||||
}
|
||||
FlushInstructionCache(GetCurrentProcess(), trampoline_start,
|
||||
trampoline.getSize() - trampoline_offset);
|
||||
return WriteJump(code, trampoline_start, expected_instructions.size());
|
||||
}
|
||||
|
||||
} // namespace Core
|
||||
@@ -1,744 +0,0 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2026 CLS_AMG
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
// BBSC mod: the game's HTTP calls. In the emulator build, core/libraries/network/http.cpp
|
||||
// changes Bloodborne's summon traffic on its way out (Wozzardman's seamless route plus BBSC's
|
||||
// connect-first and stale-record cleanup). A mod cannot reach that code, so the DLL does it one
|
||||
// step earlier: it swaps the eboot's import slots (GOT entries the emulator filled at load) of
|
||||
// the libSceHttp functions the game calls for its own wrappers, which change the request and pass
|
||||
// it on to the emulator's function.
|
||||
//
|
||||
// Import slots, CUSA03173 01.09 (tools/gamedata/eboot_imports.py, BBSC offsets):
|
||||
// 0x053E5618 sceHttpCreateConnectionWithURL 0x053E5620 sceHttpCreateRequestWithURL
|
||||
// 0x053E55E0 sceHttpAddRequestHeader 0x053E5628 sceHttpSetRequestContentLength
|
||||
// 0x053E5670 sceHttpSendRequest 0x053E5648 sceHttpDeleteRequest
|
||||
// 0x053E5668 sceHttpReadData
|
||||
//
|
||||
// What the wrappers do:
|
||||
// - URLs: the entries of host_overrides.json (same file and format as the emulator's) are
|
||||
// applied here too, so a stock emulator reaches the BBSC game server and summon broker.
|
||||
// - /summon_messenger/*: header X-ShadPS4-Bloodborne-Seamless: 1 (the broker's seamless mode).
|
||||
// - summon search/request (a host looking for signs): the host's placement header, as the
|
||||
// emulator sent it, AND the same text to every other player through the BBSC relay - the game
|
||||
// does not import sceHttpGetAllResponseHeaders, so the guest cannot read it from the broker's
|
||||
// reply; the relay brings it instead (bbsc_relay.cpp -> BbscMod::OnHostPlacement).
|
||||
// - summon create (a guest's sign): the AreaId becomes the host's map once a host's placement
|
||||
// is known (connect-first), and the first seamless create of this run sends a forced delete
|
||||
// for the same SessionId/UserId first (stale broker record after a crash).
|
||||
// - a claim the broker delivered with a sign that never became a join, and a session that
|
||||
// ended, re-arm that delete: the next sign starts clean (see "Stuck claims" below).
|
||||
// Not done: the emulator's 3 s "fast sign" re-posts. They replaced the game's own reply; extra
|
||||
// posts from here would take the broker's claim themselves and the game would never join.
|
||||
|
||||
#ifdef _WIN32
|
||||
#include <windows.h>
|
||||
#include <winhttp.h>
|
||||
#else
|
||||
#include "linux/bbsc_win32.h" // bbport
|
||||
#endif
|
||||
#ifndef _WIN32
|
||||
#include "core/memory.h" // bbport: SessionState
|
||||
#endif
|
||||
|
||||
#include <array>
|
||||
#include <atomic>
|
||||
#include <cctype>
|
||||
#include <cstring>
|
||||
#include <fstream>
|
||||
#include <map>
|
||||
#include <thread>
|
||||
#include <mutex>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "common/logging/log.h"
|
||||
#include "common/types.h"
|
||||
#include "core/bloodborne_re.h"
|
||||
|
||||
namespace BbscMod {
|
||||
void RelaySendPlacement(const std::string& placement);
|
||||
u64 EbootAddress(); // bbsc_mod.cpp: where the game image is (0 until found)
|
||||
void GuardSlot(u64 slot_offset, u64 wrapper, void** original, const char* name); // bbsc_mod.cpp
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr u64 EbootSize = 0x056D4000ULL;
|
||||
constexpr u64 SlotCreateConnection = 0x053E5618;
|
||||
constexpr u64 SlotCreateRequest = 0x053E5620;
|
||||
constexpr u64 SlotAddHeader = 0x053E55E0;
|
||||
constexpr u64 SlotSetContentLength = 0x053E5628;
|
||||
constexpr u64 SlotSend = 0x053E5670;
|
||||
constexpr u64 SlotDelete = 0x053E5648;
|
||||
constexpr u64 SlotReadData = 0x053E5668;
|
||||
// A claim delivered with our sign is followed by a join (session state 4) within seconds when it
|
||||
// works; one that has not after this long is a dead claim.
|
||||
constexpr s64 ClaimJoinWaitMs = 20'000;
|
||||
constexpr s64 PlacementStaleMs = 20'000;
|
||||
|
||||
using CreateConnectionFn = int PS4_SYSV_ABI (*)(int, const char*, bool);
|
||||
using CreateRequestFn = int PS4_SYSV_ABI (*)(int, s32, const char*, u64);
|
||||
using AddHeaderFn = int PS4_SYSV_ABI (*)(int, const char*, const char*, s32);
|
||||
using SetContentLengthFn = int PS4_SYSV_ABI (*)(int, u64);
|
||||
using SendFn = int PS4_SYSV_ABI (*)(int, const void*, u64);
|
||||
using DeleteFn = int PS4_SYSV_ABI (*)(int);
|
||||
using ReadDataFn = int PS4_SYSV_ABI (*)(int, void*, u64);
|
||||
|
||||
CreateConnectionFn original_create_connection = nullptr;
|
||||
CreateRequestFn original_create_request = nullptr;
|
||||
AddHeaderFn original_add_header = nullptr;
|
||||
SetContentLengthFn original_set_content_length = nullptr;
|
||||
SendFn original_send = nullptr;
|
||||
DeleteFn original_delete = nullptr;
|
||||
ReadDataFn original_read_data = nullptr;
|
||||
|
||||
std::mutex state_mutex;
|
||||
std::map<int, std::string> request_urls; // reqId -> URL the request really goes to
|
||||
std::vector<std::pair<std::string, std::string>> overrides; // URL prefix -> replacement
|
||||
std::atomic<bool> stale_cleared{false};
|
||||
std::atomic<s64> placement_received_ms{0};
|
||||
// 5-player test 2026-10-12: every player polls /summon_messenger/get, waiting guests too, so every
|
||||
// guest sent its own placement as "a host looking for signs". The other waiting guests switched
|
||||
// their sign's map between the real host (Dream) and a guest in Central Yharnam, and the broker
|
||||
// hid every sign outside the searcher's map (Preparing) - nobody past the 3rd was summoned.
|
||||
// Only a player without a sign of its own (or a host, state 3) sends the placement now, and a
|
||||
// waiting guest follows one host until that host goes quiet.
|
||||
std::atomic<s64> own_sign_created_ms{0};
|
||||
std::atomic<s64> own_sign_deleted_ms{0};
|
||||
constexpr s64 OwnSignMs = 60'000; // the game posts its sign again about every 30 s
|
||||
std::atomic<u32> placement_sender{0};
|
||||
std::atomic<s64> claim_delivered_ms{0}; // our sign's reply carried a host's claim (0 = none open)
|
||||
std::atomic<bool> was_in_session{false};
|
||||
|
||||
s64 NowMs() {
|
||||
return static_cast<s64>(GetTickCount64());
|
||||
}
|
||||
|
||||
// ---- host_overrides.json ------------------------------------------------------------------------
|
||||
|
||||
#ifdef _WIN32
|
||||
std::string ReadFile(const std::wstring& path) {
|
||||
std::ifstream in(path, std::ios::binary);
|
||||
std::stringstream text;
|
||||
text << in.rdbuf();
|
||||
return text.str();
|
||||
}
|
||||
|
||||
// {"prefix": "replacement", ...} - flat string pairs, as JOIN-HOST writes them.
|
||||
void LoadOverrides() {
|
||||
wchar_t exe[MAX_PATH]{};
|
||||
GetModuleFileNameW(nullptr, exe, MAX_PATH);
|
||||
std::wstring dir{exe};
|
||||
const std::wstring exe_name = dir.substr(dir.find_last_of(L"\\/") + 1);
|
||||
dir = dir.substr(0, dir.find_last_of(L"\\/") + 1);
|
||||
// bbhost sends the game's URLs to its own server ([online] host) itself. shadPS4's
|
||||
// host_overrides.json on the same PC pointed them at another server (2026-10-12: game API on
|
||||
// the shadPS4 server, NP sessions on bbhost's - nothing matched). BBSC_URL_OVERRIDES=0 does the
|
||||
// same for any host.
|
||||
const char* switch_env = std::getenv("BBSC_URL_OVERRIDES");
|
||||
if (_wcsnicmp(exe_name.c_str(), L"bbhost", 6) == 0 || (switch_env && switch_env[0] == '0')) {
|
||||
LOG_INFO(Mod, "HTTP: URL overrides off ({}) - the host sends the game's URLs itself",
|
||||
switch_env && switch_env[0] == '0' ? "BBSC_URL_OVERRIDES=0" : "bbhost");
|
||||
return;
|
||||
}
|
||||
// The emulator's own rule: a "user" folder in the folder it was started in wins (portable).
|
||||
wchar_t cwd[MAX_PATH]{};
|
||||
GetCurrentDirectoryW(MAX_PATH, cwd);
|
||||
std::vector<std::wstring> candidates{std::wstring(cwd) + L"\\user\\host_overrides.json",
|
||||
dir + L"user\\host_overrides.json"};
|
||||
if (const wchar_t* appdata = _wgetenv(L"APPDATA")) {
|
||||
candidates.push_back(std::wstring(appdata) + L"\\shadPS4\\host_overrides.json");
|
||||
}
|
||||
for (const auto& path : candidates) {
|
||||
const std::string json = ReadFile(path);
|
||||
std::vector<std::string> strings;
|
||||
for (size_t at = json.find('"'); at != std::string::npos; at = json.find('"', at)) {
|
||||
const size_t end = json.find('"', at + 1);
|
||||
if (end == std::string::npos) {
|
||||
break;
|
||||
}
|
||||
strings.push_back(json.substr(at + 1, end - at - 1));
|
||||
at = end + 1;
|
||||
}
|
||||
for (size_t i = 0; i + 1 < strings.size(); i += 2) {
|
||||
overrides.emplace_back(strings[i], strings[i + 1]);
|
||||
}
|
||||
if (!overrides.empty()) {
|
||||
LOG_INFO(Mod, "HTTP: {} URL override(s) from host_overrides.json", overrides.size());
|
||||
return;
|
||||
}
|
||||
}
|
||||
LOG_INFO(Mod, "HTTP: no host_overrides.json - URLs unchanged");
|
||||
}
|
||||
#else
|
||||
// bbport: the online module applies the user folder's host_overrides itself, to every URL the game
|
||||
// sends; applying them here as well would rewrite them twice.
|
||||
void LoadOverrides() {
|
||||
LOG_INFO(Mod, "HTTP: URL overrides left to bbport's online module");
|
||||
}
|
||||
#endif
|
||||
|
||||
// 0.0.0.0 is a server's "listen on everything", never an address to connect to. A game server
|
||||
// started with it as its shadnet address (seen 2026-10-01 under Nucleus Co-op with
|
||||
// NoDreamForHunter's server) puts it into ss.info, and every summon sign call then fails with
|
||||
// "Could not establish connection". It can only have meant this PC.
|
||||
std::string FixAnyAddress(std::string text) {
|
||||
for (const char* scheme : {"http://0.0.0.0", "https://0.0.0.0"}) {
|
||||
const std::string prefix = scheme;
|
||||
if (text.rfind(prefix, 0) == 0 &&
|
||||
(text.size() == prefix.size() || text[prefix.size()] == ':' ||
|
||||
text[prefix.size()] == '/')) {
|
||||
static bool logged = false;
|
||||
if (!logged) {
|
||||
logged = true;
|
||||
LOG_WARNING(Mod, "HTTP: the server list sends the game to 0.0.0.0 ({}) - using "
|
||||
"127.0.0.1 instead. Start the game server with a real shadnet "
|
||||
"address.",
|
||||
text);
|
||||
}
|
||||
return text.replace(prefix.size() - 7, 7, "127.0.0.1");
|
||||
}
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
std::string Override(const char* url) {
|
||||
std::string text = FixAnyAddress(url != nullptr ? url : "");
|
||||
for (const auto& [prefix, replacement] : overrides) {
|
||||
if (text.rfind(prefix, 0) == 0) {
|
||||
return replacement + text.substr(prefix.size());
|
||||
}
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
// ---- Summon paths -------------------------------------------------------------------------------
|
||||
|
||||
bool Contains(const std::string& url, const char* part) {
|
||||
return url.find(part) != std::string::npos;
|
||||
}
|
||||
bool IsSummon(const std::string& url) {
|
||||
return Contains(url, "/summon_messenger/");
|
||||
}
|
||||
bool IsSummonCreate(const std::string& url) {
|
||||
return Contains(url, "/summon_messenger/create");
|
||||
}
|
||||
bool IsSummonDelete(const std::string& url) {
|
||||
return Contains(url, "/summon_messenger/delete");
|
||||
}
|
||||
|
||||
bool IsSummonSearch(const std::string& url) {
|
||||
return Contains(url, "/summon_messenger/request") || Contains(url, "/summon_messenger/get");
|
||||
}
|
||||
|
||||
// Public servers (2026-10-09): The Hunter's Dream (and the other community servers) let mods in as
|
||||
// long as the game's API calls are not changed. On them BBSC adds no headers, rewrites no sign and
|
||||
// sends no deletes - the seamless broker extras exist only on a BBSC host's shadnet-p2p server.
|
||||
bool IsPublicServer(const std::string& url) {
|
||||
static constexpr std::array<const char*, 4> Hosts{"thehuntersdream.com", "nodreamforthehunter.com",
|
||||
"shadps4.net", "scej-network.jp"};
|
||||
const size_t scheme = url.find("://");
|
||||
const size_t start = scheme == std::string::npos ? 0 : scheme + 3;
|
||||
const std::string host = url.substr(start, url.find_first_of(":/", start) - start);
|
||||
for (const char* known : Hosts) {
|
||||
const size_t len = std::strlen(known);
|
||||
if (host.size() >= len && host.compare(host.size() - len, len, known) == 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// "AreaId":<digits> -> the host's map, when connect-first has one. Returns true if changed.
|
||||
bool RewriteAreaId(std::string& body) {
|
||||
const auto host_map = Core::Bloodborne::GetBbscConnectFirstAreaId();
|
||||
if (!host_map) {
|
||||
return false;
|
||||
}
|
||||
const std::string key = "\"AreaId\":";
|
||||
const size_t at = body.find(key);
|
||||
if (at == std::string::npos) {
|
||||
return false;
|
||||
}
|
||||
const size_t begin = at + key.size();
|
||||
size_t end = begin;
|
||||
while (end < body.size() && body[end] >= '0' && body[end] <= '9') {
|
||||
++end;
|
||||
}
|
||||
const std::string original = body.substr(begin, end - begin);
|
||||
const std::string replacement = std::to_string(*host_map);
|
||||
if (end == begin || original == replacement) {
|
||||
return false;
|
||||
}
|
||||
body.replace(begin, end - begin, replacement);
|
||||
LOG_INFO(Mod, "HTTP: connect-first - sign AreaId {} -> {}", original, replacement);
|
||||
return true;
|
||||
}
|
||||
|
||||
std::string JsonString(const std::string& body, const char* key) {
|
||||
const std::string needle = std::string("\"") + key + "\":\"";
|
||||
const size_t at = body.find(needle);
|
||||
if (at == std::string::npos) {
|
||||
return {};
|
||||
}
|
||||
const size_t begin = at + needle.size();
|
||||
const size_t end = body.find('"', begin);
|
||||
return end == std::string::npos ? std::string{} : body.substr(begin, end - begin);
|
||||
}
|
||||
|
||||
std::string JsonNumber(const std::string& body, const char* key) {
|
||||
const std::string needle = std::string("\"") + key + "\":";
|
||||
const size_t at = body.find(needle);
|
||||
if (at == std::string::npos) {
|
||||
return {};
|
||||
}
|
||||
size_t end = at + needle.size();
|
||||
while (end < body.size() && (std::isdigit(static_cast<unsigned char>(body[end])) || body[end] == '-')) {
|
||||
++end;
|
||||
}
|
||||
return body.substr(at + needle.size(), end - at - needle.size());
|
||||
}
|
||||
|
||||
#ifdef _WIN32
|
||||
// One plain POST with WinHTTP (the stale-record delete). Returns the status code, 0 on failure.
|
||||
DWORD Post(const std::string& url, const std::string& body) {
|
||||
std::wstring wide(url.begin(), url.end());
|
||||
URL_COMPONENTS parts{sizeof(parts)};
|
||||
wchar_t host[256]{};
|
||||
wchar_t path[2048]{};
|
||||
parts.lpszHostName = host;
|
||||
parts.dwHostNameLength = 256;
|
||||
parts.lpszUrlPath = path;
|
||||
parts.dwUrlPathLength = 2048;
|
||||
if (!WinHttpCrackUrl(wide.c_str(), 0, 0, &parts)) {
|
||||
return 0;
|
||||
}
|
||||
DWORD status = 0;
|
||||
HINTERNET session = WinHttpOpen(L"BBSC", WINHTTP_ACCESS_TYPE_NO_PROXY, nullptr, nullptr, 0);
|
||||
HINTERNET connect = session ? WinHttpConnect(session, host, parts.nPort, 0) : nullptr;
|
||||
HINTERNET request =
|
||||
connect ? WinHttpOpenRequest(connect, L"POST", path, nullptr, nullptr, nullptr,
|
||||
parts.nScheme == INTERNET_SCHEME_HTTPS ? WINHTTP_FLAG_SECURE : 0)
|
||||
: nullptr;
|
||||
if (request != nullptr) {
|
||||
WinHttpSetTimeouts(request, 1000, 1000, 1000, 1500);
|
||||
const wchar_t* headers =
|
||||
L"Content-Type: application/json\r\nX-ShadPS4-Bloodborne-Seamless: 1\r\n";
|
||||
if (WinHttpSendRequest(request, headers, static_cast<DWORD>(-1),
|
||||
const_cast<char*>(body.data()), static_cast<DWORD>(body.size()),
|
||||
static_cast<DWORD>(body.size()), 0) &&
|
||||
WinHttpReceiveResponse(request, nullptr)) {
|
||||
DWORD size = sizeof(status);
|
||||
WinHttpQueryHeaders(request, WINHTTP_QUERY_STATUS_CODE | WINHTTP_QUERY_FLAG_NUMBER,
|
||||
nullptr, &status, &size, nullptr);
|
||||
}
|
||||
}
|
||||
for (HINTERNET h : {request, connect, session}) {
|
||||
if (h != nullptr) {
|
||||
WinHttpCloseHandle(h);
|
||||
}
|
||||
}
|
||||
return status;
|
||||
}
|
||||
#else
|
||||
// bbport: one plain HTTP POST over a socket (the stale-record delete goes to the summon broker,
|
||||
// shadNet's WebAPI, over http). Returns the status code, 0 on failure; https is not done here.
|
||||
DWORD Post(const std::string& url, const std::string& body) {
|
||||
constexpr std::string_view Scheme = "http://";
|
||||
if (url.rfind(Scheme, 0) != 0) {
|
||||
return 0;
|
||||
}
|
||||
const std::string rest = url.substr(Scheme.size());
|
||||
const size_t slash = rest.find('/');
|
||||
const std::string authority = rest.substr(0, slash);
|
||||
const std::string path = slash == std::string::npos ? "/" : rest.substr(slash);
|
||||
std::string host = authority, port = "80";
|
||||
if (const size_t colon = authority.rfind(':'); colon != std::string::npos) {
|
||||
host = authority.substr(0, colon);
|
||||
port = authority.substr(colon + 1);
|
||||
}
|
||||
addrinfo hints{};
|
||||
hints.ai_family = AF_INET;
|
||||
hints.ai_socktype = SOCK_STREAM;
|
||||
addrinfo* found = nullptr;
|
||||
if (getaddrinfo(host.c_str(), port.c_str(), &hints, &found) != 0 || found == nullptr) {
|
||||
return 0;
|
||||
}
|
||||
const SOCKET s = socket(found->ai_family, found->ai_socktype, found->ai_protocol);
|
||||
const timeval timeout{1, 500'000};
|
||||
if (s != INVALID_SOCKET) {
|
||||
setsockopt(s, SOL_SOCKET, SO_SNDTIMEO, &timeout, sizeof(timeout));
|
||||
setsockopt(s, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout));
|
||||
}
|
||||
const bool connected =
|
||||
s != INVALID_SOCKET && connect(s, found->ai_addr, found->ai_addrlen) == 0;
|
||||
freeaddrinfo(found);
|
||||
if (!connected) {
|
||||
if (s != INVALID_SOCKET) {
|
||||
closesocket(s);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
const std::string request = "POST " + path + " HTTP/1.0\r\nHost: " + authority +
|
||||
"\r\nContent-Type: application/json\r\n"
|
||||
"X-ShadPS4-Bloodborne-Seamless: 1\r\nContent-Length: " +
|
||||
std::to_string(body.size()) + "\r\nConnection: close\r\n\r\n" + body;
|
||||
DWORD status = 0;
|
||||
if (send(s, request.data(), request.size(), MSG_NOSIGNAL) == static_cast<ssize_t>(request.size())) {
|
||||
char reply[64]{};
|
||||
const ssize_t got = recv(s, reply, sizeof(reply) - 1, 0);
|
||||
// "HTTP/1.x NNN ..."
|
||||
if (got > 12 && std::strncmp(reply, "HTTP/", 5) == 0) {
|
||||
status = static_cast<DWORD>(std::atoi(reply + 9));
|
||||
}
|
||||
}
|
||||
closesocket(s);
|
||||
return status;
|
||||
}
|
||||
#endif
|
||||
|
||||
// The broker keeps Preparing/Claimed/Delivered records 15 minutes and Bloodborne reuses its
|
||||
// SessionId/UserId after a restart, so the first seamless sign of this run removes any old one.
|
||||
void ClearStaleRecord(const std::string& url, const std::string& body) {
|
||||
if (stale_cleared.exchange(true)) {
|
||||
return;
|
||||
}
|
||||
const std::string session = JsonString(body, "SessionId");
|
||||
const std::string user = JsonNumber(body, "UserId");
|
||||
if (session.empty() || user.empty()) {
|
||||
return;
|
||||
}
|
||||
std::string removal_url = url;
|
||||
removal_url.replace(removal_url.find("/summon_messenger/create"),
|
||||
std::strlen("/summon_messenger/create"), "/summon_messenger/delete");
|
||||
const std::string removal = "{\"MessageId\":\"SummonDataRemoveRequest\",\"SessionId\":\"" +
|
||||
session + "\",\"UserId\":" + user + ",\"Force\":true}";
|
||||
// It must finish before the new sign is created (the delete names the same session), so it
|
||||
// stays on this thread - the game's - with short timeouts (2026-10-08: up to 14 s before).
|
||||
const DWORD status = Post(removal_url, removal);
|
||||
LOG_INFO(Mod, "HTTP: stale summon cleanup for session {} user {}: status {}", session, user,
|
||||
status);
|
||||
}
|
||||
|
||||
// ---- Stuck claims (2026-10-06) -------------------------------------------------------------------
|
||||
// 4-player Nucleus test: one guest never joined and another waited minutes. In seamless mode the
|
||||
// broker keeps a claimed sign 15 minutes (Preparing/Claimed/Delivered) and, every time that guest
|
||||
// posts a sign again, hands back the OLD claim - from a host room that is gone or a join the game
|
||||
// already gave up on. The host's searches list the same record, and its new claims are refused as
|
||||
// conflicts. Nothing frees it but the 15-minute expiry or a forced delete, and the forced delete
|
||||
// only ran once per game start. Now it runs again (on the next sign) when:
|
||||
// - the broker delivered a claim and no join followed within ClaimJoinWaitMs, or
|
||||
// - we were in a session (joining, guest or host) and are back to posting signs.
|
||||
s32 SessionState() {
|
||||
const u64 manager = *reinterpret_cast<const u64*>(BbscMod::EbootAddress() + 0x05540290);
|
||||
#ifndef _WIN32
|
||||
// bbport: the game's memory map instead of VirtualQuery.
|
||||
if (manager >= 0x10000 && Core::Memory::Instance()->IsValidMapping(manager + 0x124, 4)) {
|
||||
return *reinterpret_cast<const s32*>(manager + 0x124);
|
||||
}
|
||||
return -1;
|
||||
#else
|
||||
MEMORY_BASIC_INFORMATION info{};
|
||||
constexpr DWORD Readable = PAGE_READONLY | PAGE_READWRITE | PAGE_WRITECOPY | PAGE_EXECUTE_READ |
|
||||
PAGE_EXECUTE_READWRITE | PAGE_EXECUTE_WRITECOPY;
|
||||
if (manager >= 0x10000 && VirtualQuery(reinterpret_cast<void*>(manager + 0x124), &info,
|
||||
sizeof(info)) != 0 &&
|
||||
info.State == MEM_COMMIT && (info.Protect & Readable) != 0 &&
|
||||
(info.Protect & (PAGE_GUARD | PAGE_NOACCESS)) == 0) {
|
||||
return *reinterpret_cast<const s32*>(manager + 0x124);
|
||||
}
|
||||
return -1;
|
||||
#endif
|
||||
}
|
||||
|
||||
void WatchSessionForStaleClaims() {
|
||||
const s32 state = SessionState();
|
||||
if (state == 3 || state == 4 || state == 6) {
|
||||
was_in_session = true;
|
||||
if (claim_delivered_ms.exchange(0) != 0) {
|
||||
LOG_INFO(Mod, "HTTP: the delivered claim became a join (session state {})", state);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (state != 0) {
|
||||
return;
|
||||
}
|
||||
if (was_in_session.exchange(false)) {
|
||||
stale_cleared = false;
|
||||
LOG_INFO(Mod, "HTTP: session over - the next sign first clears our old broker record");
|
||||
}
|
||||
const s64 delivered = claim_delivered_ms.load();
|
||||
if (delivered != 0 && NowMs() - delivered > ClaimJoinWaitMs) {
|
||||
claim_delivered_ms = 0;
|
||||
stale_cleared = false;
|
||||
LOG_WARNING(Mod, "HTTP: a host's claim was delivered {} s ago and never became a join - "
|
||||
"the next sign first clears it",
|
||||
(NowMs() - delivered) / 1000);
|
||||
}
|
||||
}
|
||||
|
||||
// ---- The wrappers -------------------------------------------------------------------------------
|
||||
|
||||
int PS4_SYSV_ABI CreateConnectionWithURL(int template_id, const char* url, bool keepalive) {
|
||||
const std::string target = Override(url);
|
||||
return original_create_connection(template_id, target.c_str(), keepalive);
|
||||
}
|
||||
|
||||
int PS4_SYSV_ABI CreateRequestWithURL(int connection, s32 method, const char* url, u64 length) {
|
||||
const std::string target = Override(url);
|
||||
const int request = original_create_request(connection, method, target.c_str(), length);
|
||||
if (request > 0) {
|
||||
std::scoped_lock lock{state_mutex};
|
||||
request_urls[request] = target;
|
||||
}
|
||||
return request;
|
||||
}
|
||||
|
||||
// A slot's value when it holds an emulator function (outside the eboot), else 0.
|
||||
u64 CurrentSlot(u64 slot_offset) {
|
||||
const u64 base = BbscMod::EbootAddress();
|
||||
const u64 value = *reinterpret_cast<const volatile u64*>(base + slot_offset);
|
||||
return value != 0 && (value < base || value >= base + EbootSize) ? value : 0;
|
||||
}
|
||||
|
||||
int PS4_SYSV_ABI SendRequest(int request, const void* data, u64 size) {
|
||||
std::string url;
|
||||
{
|
||||
std::scoped_lock lock{state_mutex};
|
||||
if (const auto it = request_urls.find(request); it != request_urls.end()) {
|
||||
url = it->second;
|
||||
}
|
||||
}
|
||||
WatchSessionForStaleClaims();
|
||||
if (!IsSummon(url)) {
|
||||
return original_send(request, data, size);
|
||||
}
|
||||
if (IsPublicServer(url)) {
|
||||
static std::atomic<bool> logged{false};
|
||||
if (!logged.exchange(true)) {
|
||||
LOG_INFO(Mod, "HTTP: summon requests go to a public server - sent unchanged (no "
|
||||
"seamless headers, connect-first or cleanup)");
|
||||
}
|
||||
return original_send(request, data, size);
|
||||
}
|
||||
// Not wrapped, so the slots always hold the emulator's current functions: read them again at
|
||||
// use (review 2026-10-11 - the official shadPS4 links the imports again after start-up).
|
||||
if (const u64 add = CurrentSlot(SlotAddHeader); add != 0) {
|
||||
original_add_header = reinterpret_cast<AddHeaderFn>(add);
|
||||
}
|
||||
if (const u64 length = CurrentSlot(SlotSetContentLength); length != 0) {
|
||||
original_set_content_length = reinterpret_cast<SetContentLengthFn>(length);
|
||||
}
|
||||
if (original_add_header == nullptr || original_set_content_length == nullptr) {
|
||||
return original_send(request, data, size); // (review 2026-10-08) the slots never filled
|
||||
}
|
||||
original_add_header(request, "X-ShadPS4-Bloodborne-Seamless", "1", 0);
|
||||
if (IsSummonDelete(url)) {
|
||||
own_sign_deleted_ms = NowMs();
|
||||
}
|
||||
if (IsSummonSearch(url)) {
|
||||
const s64 created = own_sign_created_ms.load();
|
||||
const bool own_sign_out = SessionState() != 3 && created != 0 &&
|
||||
created > own_sign_deleted_ms.load() &&
|
||||
NowMs() - created < OwnSignMs;
|
||||
if (own_sign_out) {
|
||||
static std::atomic<s64> last_logged{0};
|
||||
if (NowMs() - last_logged.load() > 60'000) {
|
||||
last_logged = NowMs();
|
||||
LOG_INFO(Mod, "HTTP: sign search while our own sign is out - no host placement "
|
||||
"sent (we are waiting, not hosting)");
|
||||
}
|
||||
} else if (const auto placement = Core::Bloodborne::GetSeamlessHostPlacementHeader()) {
|
||||
original_add_header(request, "X-ShadPS4-Bloodborne-Host-Placement", placement->c_str(),
|
||||
0);
|
||||
BbscMod::RelaySendPlacement(*placement);
|
||||
}
|
||||
return original_send(request, data, size);
|
||||
}
|
||||
if (IsSummonCreate(url)) {
|
||||
own_sign_created_ms = NowMs();
|
||||
}
|
||||
if (IsSummonCreate(url) && data != nullptr && size != 0) {
|
||||
std::string body(static_cast<const char*>(data), static_cast<size_t>(size));
|
||||
ClearStaleRecord(url, body);
|
||||
// A host's placement that has not been refreshed for a while is a host that stopped
|
||||
// looking: forget it, as the emulator did when a create reply carried none.
|
||||
const s64 received = placement_received_ms.load();
|
||||
if (received != 0 && NowMs() - received > PlacementStaleMs) {
|
||||
placement_received_ms = 0;
|
||||
placement_sender = 0;
|
||||
Core::Bloodborne::ClearSeamlessHostPlacementHeader();
|
||||
}
|
||||
if (RewriteAreaId(body)) {
|
||||
original_set_content_length(request, body.size());
|
||||
// The emulator copies the body during the call, so a local buffer is fine.
|
||||
return original_send(request, body.data(), body.size());
|
||||
}
|
||||
}
|
||||
return original_send(request, data, size);
|
||||
}
|
||||
|
||||
// A sign's reply is a bare {"ResKind":0,"MessageId":"SummonDataCreateResponse"} unless the broker
|
||||
// delivers a host's claim with it, which adds the claim's members (the host's UserId among them).
|
||||
int PS4_SYSV_ABI ReadData(int request, void* data, u64 size) {
|
||||
const int read = original_read_data(request, data, size);
|
||||
if (read > 0 && data != nullptr) {
|
||||
std::string url;
|
||||
{
|
||||
std::scoped_lock lock{state_mutex};
|
||||
if (const auto it = request_urls.find(request); it != request_urls.end()) {
|
||||
url = it->second;
|
||||
}
|
||||
}
|
||||
// Public servers (2026-10-10: a host's summon never reached the guest on The Hunter's
|
||||
// Dream): every summon reply that is not a bare create answer goes into the log.
|
||||
if (IsSummon(url) && IsPublicServer(url)) {
|
||||
const std::string_view reply(static_cast<const char*>(data), static_cast<size_t>(read));
|
||||
const size_t q = url.find('?');
|
||||
const size_t slash = url.rfind('/', q);
|
||||
const std::string what = url.substr(slash + 1, q == std::string::npos ? q : q - slash - 1);
|
||||
const bool bare = reply.find("\"SummonDataCreateResponse\"") != std::string_view::npos &&
|
||||
reply.size() <= 60;
|
||||
static std::atomic<int> bare_logged{0};
|
||||
if (what == "get") {
|
||||
// Sign lists hold other players' signs: only their size.
|
||||
LOG_INFO(Mod, "HTTP: summon reply (get, {} bytes)", read);
|
||||
} else if (!bare || bare_logged.fetch_add(1) < 2) {
|
||||
std::string text(reply.substr(0, 400));
|
||||
for (char& c : text) {
|
||||
if (static_cast<unsigned char>(c) < 0x20) {
|
||||
c = '.';
|
||||
}
|
||||
}
|
||||
LOG_INFO(Mod, "HTTP: summon reply ({}, {} bytes): {}", what, read, text);
|
||||
}
|
||||
}
|
||||
if (IsSummonCreate(url)) {
|
||||
const std::string_view reply(static_cast<const char*>(data), static_cast<size_t>(read));
|
||||
if (reply.find("\"UserId\"") != std::string_view::npos &&
|
||||
claim_delivered_ms.exchange(NowMs()) == 0) {
|
||||
LOG_INFO(Mod, "HTTP: a host's claim came with our sign ({} bytes) - waiting for the "
|
||||
"join",
|
||||
read);
|
||||
}
|
||||
}
|
||||
}
|
||||
return read;
|
||||
}
|
||||
|
||||
int PS4_SYSV_ABI DeleteRequest(int request) {
|
||||
{
|
||||
std::scoped_lock lock{state_mutex};
|
||||
request_urls.erase(request);
|
||||
}
|
||||
return original_delete(request);
|
||||
}
|
||||
|
||||
// Waits until the emulator has filled the slot (a host address, outside the eboot), then swaps
|
||||
// it. The original goes into `original` BEFORE the slot points at the wrapper (review 2026-10-08:
|
||||
// the game could call the wrapper before the original was stored). False if the slot never filled.
|
||||
template <typename Fn>
|
||||
bool Swap(u64 slot_offset, Fn wrapper, Fn& original, const char* name) {
|
||||
auto* slot = reinterpret_cast<u64*>(BbscMod::EbootAddress() + slot_offset);
|
||||
for (int i = 0; i < 60'000; ++i) {
|
||||
const u64 value = *slot;
|
||||
if (value != 0 && (value < BbscMod::EbootAddress() || value >= BbscMod::EbootAddress() + EbootSize)) {
|
||||
original = reinterpret_cast<Fn>(value);
|
||||
DWORD old = 0;
|
||||
VirtualProtect(slot, 8, PAGE_READWRITE, &old);
|
||||
*slot = reinterpret_cast<u64>(wrapper);
|
||||
DWORD ignored = 0;
|
||||
VirtualProtect(slot, 8, old, &ignored);
|
||||
BbscMod::GuardSlot(slot_offset, reinterpret_cast<u64>(wrapper),
|
||||
reinterpret_cast<void**>(&original), name);
|
||||
return true;
|
||||
}
|
||||
Sleep(1);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// The two slots read, not swapped: waits for them as Swap does (they are filled at the same time).
|
||||
u64 WaitForSlot(u64 slot_offset) {
|
||||
const auto* slot = reinterpret_cast<const u64*>(BbscMod::EbootAddress() + slot_offset);
|
||||
for (int i = 0; i < 60'000; ++i) {
|
||||
const u64 value = *slot;
|
||||
if (value != 0 && (value < BbscMod::EbootAddress() || value >= BbscMod::EbootAddress() + EbootSize)) {
|
||||
return value;
|
||||
}
|
||||
Sleep(1);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
namespace BbscMod {
|
||||
|
||||
// The relay asks before connecting (bbsc_relay.cpp): public servers run no BBSC relay.
|
||||
bool IsPublicServerAddress(const std::string& url_or_host) {
|
||||
return IsPublicServer(url_or_host);
|
||||
}
|
||||
|
||||
// A host's placement from the relay: what the broker's reply header carried in the emulator
|
||||
// build. Used only by a player who is not in a session yet (a guest waiting to be summoned).
|
||||
void OnHostPlacement(const std::string& placement, u32 sender) {
|
||||
const s32 state = SessionState();
|
||||
if (state == 3 || state == 4 || state == 6) {
|
||||
return; // hosting, joining or already a guest
|
||||
}
|
||||
// One host at a time: another sender's placement is taken only once ours has gone quiet.
|
||||
const u32 current = placement_sender.load();
|
||||
const s64 received = placement_received_ms.load();
|
||||
if (current != 0 && current != sender && received != 0 &&
|
||||
NowMs() - received <= PlacementStaleMs) {
|
||||
static std::atomic<s64> last_logged{0};
|
||||
if (NowMs() - last_logged.load() > 30'000) {
|
||||
last_logged = NowMs();
|
||||
LOG_INFO(Mod, "HTTP: placement from another player {:08x} ignored - following host "
|
||||
"{:08x}",
|
||||
sender, current);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (Core::Bloodborne::SetSeamlessHostPlacementHeader(placement)) {
|
||||
if (placement_sender.exchange(sender) != sender) {
|
||||
LOG_INFO(Mod, "HTTP: following host {:08x} for the sign's map", sender);
|
||||
}
|
||||
// "1,<map>,x,y,z,heading,area": the map is logged whenever it changes (2026-10-07: the
|
||||
// third player's sign stayed on a map the host had left; only the first one was logged).
|
||||
static std::string last_map;
|
||||
const size_t a = placement.find(',');
|
||||
const size_t b = a == std::string::npos ? a : placement.find(',', a + 1);
|
||||
const std::string map = b == std::string::npos ? std::string{} : placement.substr(a + 1, b - a - 1);
|
||||
if (placement_received_ms.exchange(NowMs()) == 0 || map != last_map) {
|
||||
LOG_INFO(Mod, "HTTP: a host is looking for signs - placement {}", placement);
|
||||
}
|
||||
last_map = map;
|
||||
}
|
||||
}
|
||||
|
||||
void InstallHttpWrappers() {
|
||||
LoadOverrides();
|
||||
original_add_header = reinterpret_cast<AddHeaderFn>(WaitForSlot(SlotAddHeader));
|
||||
original_set_content_length =
|
||||
reinterpret_cast<SetContentLengthFn>(WaitForSlot(SlotSetContentLength));
|
||||
Swap<CreateConnectionFn>(SlotCreateConnection, &CreateConnectionWithURL,
|
||||
original_create_connection, "CreateConnectionWithURL");
|
||||
Swap<CreateRequestFn>(SlotCreateRequest, &CreateRequestWithURL, original_create_request,
|
||||
"CreateRequestWithURL");
|
||||
Swap<SendFn>(SlotSend, &SendRequest, original_send, "SendRequest");
|
||||
Swap<DeleteFn>(SlotDelete, &DeleteRequest, original_delete, "DeleteRequest");
|
||||
Swap<ReadDataFn>(SlotReadData, &ReadData, original_read_data, "ReadData");
|
||||
const bool ok = original_create_connection && original_create_request && original_add_header &&
|
||||
original_set_content_length && original_send && original_delete &&
|
||||
original_read_data;
|
||||
LOG_INFO(Mod, "HTTP: game's HTTP calls {} (URL overrides, seamless summon, connect-first, stale "
|
||||
"cleanup)",
|
||||
ok ? "wrapped" : "NOT wrapped - an import slot was never filled");
|
||||
}
|
||||
|
||||
} // namespace BbscMod
|
||||
@@ -1,394 +0,0 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2026 CLS_AMG
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
// bbsc-launch.exe - starts shadPS4 with the BBSC mod DLL loaded into it.
|
||||
//
|
||||
// bbsc-launch.exe [--emu <shadPS4.exe>] [--dll <bbsc.dll>] [--window left|right] [--choose]
|
||||
// [--cwd <folder>] [shadPS4 arguments...]
|
||||
//
|
||||
// --cwd: the folder the emulator starts in (default: the emulator's own folder). shadPS4 uses the
|
||||
// "user" folder of that folder when there is one (portable), else %APPDATA%\shadPS4 - a portable
|
||||
// Qt launcher keeps "user" next to the launcher, not next to the emulator version it runs.
|
||||
//
|
||||
// Double-clicked with no arguments: the first time it asks (Windows file picker) for
|
||||
// Bloodborne's eboot.bin and, if there is no shadPS4.exe next to it, for the emulator; the
|
||||
// choices are kept in bbsc-launch.ini next to it. --choose asks again.
|
||||
//
|
||||
// Defaults: shadPS4.exe and bbsc.dll next to this launcher. Every other argument is passed to
|
||||
// shadPS4 unchanged (e.g. --game <path to eboot.bin>). The emulator is created suspended, the DLL
|
||||
// is loaded into it by a remote LoadLibraryW, and only then is the emulator let go - so the mod is
|
||||
// in place before the game's first instruction. The launcher waits for the emulator to close and
|
||||
// returns its exit code.
|
||||
|
||||
#include <windows.h>
|
||||
#include <commdlg.h>
|
||||
|
||||
#include <cstdio>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
|
||||
std::wstring HereFile(const wchar_t* name) {
|
||||
wchar_t path[MAX_PATH]{};
|
||||
GetModuleFileNameW(nullptr, path, MAX_PATH);
|
||||
std::wstring dir{path};
|
||||
const size_t slash = dir.find_last_of(L"\\/");
|
||||
return (slash == std::wstring::npos ? L"" : dir.substr(0, slash + 1)) + name;
|
||||
}
|
||||
|
||||
std::wstring Quote(const std::wstring& arg) {
|
||||
if (!arg.empty() && arg.find_first_of(L" \t\"") == std::wstring::npos) {
|
||||
return arg;
|
||||
}
|
||||
std::wstring out = L"\"";
|
||||
size_t backslashes = 0;
|
||||
for (const wchar_t c : arg) {
|
||||
if (c == L'\\') {
|
||||
++backslashes;
|
||||
} else if (c == L'"') {
|
||||
out.append(backslashes * 2 + 1, L'\\');
|
||||
out += c;
|
||||
backslashes = 0;
|
||||
continue;
|
||||
} else {
|
||||
backslashes = 0;
|
||||
}
|
||||
out += c;
|
||||
}
|
||||
out.append(backslashes * 2, L'\\');
|
||||
return out + L"\"";
|
||||
}
|
||||
|
||||
struct FindWindowArgs {
|
||||
DWORD pid;
|
||||
HWND found;
|
||||
};
|
||||
|
||||
BOOL CALLBACK FindProcessWindow(HWND window, LPARAM param) {
|
||||
auto* args = reinterpret_cast<FindWindowArgs*>(param);
|
||||
DWORD pid = 0;
|
||||
GetWindowThreadProcessId(window, &pid);
|
||||
if (pid == args->pid && IsWindowVisible(window) && GetWindow(window, GW_OWNER) == nullptr &&
|
||||
GetWindowTextLengthW(window) > 0) {
|
||||
args->found = window;
|
||||
return FALSE;
|
||||
}
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
// --window left|right: puts the emulator's window on that half of the main screen once it
|
||||
// appears, and keeps it there for the first seconds (the emulator sizes its window while starting).
|
||||
void PlaceWindow(DWORD pid, const std::wstring& side) {
|
||||
RECT work{};
|
||||
SystemParametersInfoW(SPI_GETWORKAREA, 0, &work, 0);
|
||||
// left / right: half the screen; tl / tr / bl / br: a quarter (4-window tests, 2026-10-07).
|
||||
const bool quarter = side == L"tl" || side == L"tr" || side == L"bl" || side == L"br";
|
||||
const int half = (work.right - work.left) / 2;
|
||||
const bool right = side == L"right" || side == L"tr" || side == L"br";
|
||||
const bool bottom = side == L"bl" || side == L"br";
|
||||
const int full_height = work.bottom - work.top;
|
||||
const int x = right ? work.left + half : work.left;
|
||||
const int y = bottom ? work.top + full_height / 2 : work.top;
|
||||
const int height = quarter ? full_height / 2 : full_height;
|
||||
DWORD placed_at = 0;
|
||||
for (int i = 0; i < 480; ++i) { // up to 2 minutes for the window to appear
|
||||
FindWindowArgs args{pid, nullptr};
|
||||
EnumWindows(FindProcessWindow, reinterpret_cast<LPARAM>(&args));
|
||||
if (args.found != nullptr) {
|
||||
RECT now{};
|
||||
GetWindowRect(args.found, &now);
|
||||
if (now.left != x || now.top != y || now.right - now.left != half ||
|
||||
now.bottom - now.top != height) {
|
||||
ShowWindow(args.found, SW_RESTORE);
|
||||
SetWindowPos(args.found, nullptr, x, y, half, height,
|
||||
SWP_NOZORDER | SWP_NOACTIVATE);
|
||||
}
|
||||
if (placed_at == 0) {
|
||||
placed_at = GetTickCount();
|
||||
} else if (GetTickCount() - placed_at > 15'000) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
Sleep(250);
|
||||
}
|
||||
}
|
||||
|
||||
DWORD WINAPI PlaceWindowThread(void* param) {
|
||||
auto* args = static_cast<std::pair<DWORD, std::wstring>*>(param);
|
||||
PlaceWindow(args->first, args->second);
|
||||
delete args;
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
// ---- Remembered choices: bbsc-launch.ini next to the launcher (game=..., emulator=...) ---------
|
||||
|
||||
std::wstring IniPath() {
|
||||
return HereFile(L"bbsc-launch.ini");
|
||||
}
|
||||
|
||||
std::wstring LoadSetting(const wchar_t* key) {
|
||||
wchar_t value[MAX_PATH * 2]{};
|
||||
GetPrivateProfileStringW(L"bbsc", key, L"", value, MAX_PATH * 2, IniPath().c_str());
|
||||
return value;
|
||||
}
|
||||
|
||||
void SaveSetting(const wchar_t* key, const std::wstring& value) {
|
||||
WritePrivateProfileStringW(L"bbsc", key, value.c_str(), IniPath().c_str());
|
||||
}
|
||||
|
||||
bool FileExists(const std::wstring& path) {
|
||||
const DWORD attributes = GetFileAttributesW(path.c_str());
|
||||
return !path.empty() && attributes != INVALID_FILE_ATTRIBUTES &&
|
||||
(attributes & FILE_ATTRIBUTE_DIRECTORY) == 0;
|
||||
}
|
||||
|
||||
void Message(const std::wstring& text, UINT icon = MB_ICONINFORMATION) {
|
||||
MessageBoxW(nullptr, text.c_str(), L"BBSC", MB_OK | icon | MB_SETFOREGROUND);
|
||||
}
|
||||
|
||||
// A Windows "open file" window. Empty if the player cancelled.
|
||||
std::wstring PickFile(const wchar_t* title, const wchar_t* filter) {
|
||||
wchar_t path[MAX_PATH]{};
|
||||
OPENFILENAMEW dialog{sizeof(dialog)};
|
||||
dialog.lpstrTitle = title;
|
||||
dialog.lpstrFilter = filter;
|
||||
dialog.lpstrFile = path;
|
||||
dialog.nMaxFile = MAX_PATH;
|
||||
dialog.Flags = OFN_FILEMUSTEXIST | OFN_PATHMUSTEXIST | OFN_NOCHANGEDIR;
|
||||
return GetOpenFileNameW(&dialog) ? std::wstring{path} : std::wstring{};
|
||||
}
|
||||
|
||||
// The folder of eboot.bin should hold sce_sys\param.sfo naming CUSA03173.
|
||||
bool IsBloodborne(const std::wstring& eboot) {
|
||||
const std::wstring sfo =
|
||||
eboot.substr(0, eboot.find_last_of(L"\\/") + 1) + L"sce_sys\\param.sfo";
|
||||
HANDLE file = CreateFileW(sfo.c_str(), GENERIC_READ, FILE_SHARE_READ, nullptr, OPEN_EXISTING, 0,
|
||||
nullptr);
|
||||
if (file == INVALID_HANDLE_VALUE) {
|
||||
return false;
|
||||
}
|
||||
std::string data(64 * 1024, '\0');
|
||||
DWORD read = 0;
|
||||
ReadFile(file, data.data(), static_cast<DWORD>(data.size()), &read, nullptr);
|
||||
CloseHandle(file);
|
||||
data.resize(read);
|
||||
return data.find("CUSA03173") != std::string::npos;
|
||||
}
|
||||
|
||||
std::wstring ChooseGame() {
|
||||
Message(L"Please pick your Bloodborne game file.\n\n"
|
||||
L"It is the file eboot.bin in your Bloodborne folder (the one named CUSA03173).\n"
|
||||
L"You only have to do this once.");
|
||||
for (;;) {
|
||||
const std::wstring picked =
|
||||
PickFile(L"BBSC - pick Bloodborne's eboot.bin", L"eboot.bin\0eboot.bin\0All files\0*.*\0");
|
||||
if (picked.empty()) {
|
||||
return {};
|
||||
}
|
||||
if (IsBloodborne(picked)) {
|
||||
SaveSetting(L"game", picked);
|
||||
return picked;
|
||||
}
|
||||
if (MessageBoxW(nullptr,
|
||||
L"That folder does not look like Bloodborne CUSA03173 (no sce_sys\\param.sfo "
|
||||
L"with CUSA03173 next to it).\n\nUse it anyway?",
|
||||
L"BBSC", MB_YESNO | MB_ICONWARNING | MB_SETFOREGROUND) == IDYES) {
|
||||
SaveSetting(L"game", picked);
|
||||
return picked;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::wstring ChooseEmulator() {
|
||||
Message(L"Please pick the shadPS4.exe you play BBSC with.\n\nYou only have to do this once.");
|
||||
const std::wstring picked =
|
||||
PickFile(L"BBSC - pick shadPS4.exe", L"shadPS4.exe\0*.exe\0All files\0*.*\0");
|
||||
if (!picked.empty()) {
|
||||
SaveSetting(L"emulator", picked);
|
||||
}
|
||||
return picked;
|
||||
}
|
||||
|
||||
void DefaultSwitch(const wchar_t* name, const wchar_t* value) {
|
||||
if (GetEnvironmentVariableW(name, nullptr, 0) == 0) {
|
||||
SetEnvironmentVariableW(name, value);
|
||||
}
|
||||
}
|
||||
|
||||
int Fail(const wchar_t* what, HANDLE process = nullptr) {
|
||||
const DWORD error = GetLastError();
|
||||
Message(std::wstring(what) + L"\n\n(Windows error " + std::to_wstring(error) + L")",
|
||||
MB_ICONERROR);
|
||||
if (process != nullptr) {
|
||||
TerminateProcess(process, 1);
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int wmain(int argc, wchar_t** argv) {
|
||||
std::wstring emulator;
|
||||
std::wstring dll = HereFile(L"bbsc.dll");
|
||||
std::wstring window_side;
|
||||
std::wstring start_dir;
|
||||
bool choose = false;
|
||||
bool no_mod = false; // --no-mod: start the emulator as it is (comparison runs)
|
||||
bool has_game = false;
|
||||
std::vector<std::wstring> passthrough;
|
||||
for (int i = 1; i < argc; ++i) {
|
||||
const std::wstring arg = argv[i];
|
||||
if (arg == L"--choose") {
|
||||
choose = true;
|
||||
} else if (arg == L"--no-mod") {
|
||||
no_mod = true;
|
||||
} else if (arg == L"--emu" && i + 1 < argc) {
|
||||
emulator = argv[++i];
|
||||
} else if (arg == L"--dll" && i + 1 < argc) {
|
||||
dll = argv[++i];
|
||||
} else if (arg == L"--cwd" && i + 1 < argc) {
|
||||
start_dir = argv[++i];
|
||||
} else if (arg == L"--window" && i + 1 < argc) {
|
||||
window_side = argv[++i];
|
||||
} else {
|
||||
has_game = has_game || arg == L"--game" || arg == L"-g";
|
||||
passthrough.push_back(arg);
|
||||
}
|
||||
}
|
||||
// The emulator: --emu, else shadPS4.exe next to the launcher, else the one picked before,
|
||||
// else ask. The game: --game, else the eboot.bin picked before, else ask.
|
||||
// BBSC_EMULATOR: another program to start instead (the bbport Windows build sets its game exe
|
||||
// through BB_PROBE, so bbsc-launch stands in for bb-probe.exe and starts it from here).
|
||||
if (emulator.empty()) {
|
||||
wchar_t from_env[MAX_PATH]{};
|
||||
if (GetEnvironmentVariableW(L"BBSC_EMULATOR", from_env, MAX_PATH) > 0) {
|
||||
emulator = from_env;
|
||||
}
|
||||
}
|
||||
// bbport (Windows): bb-probe.exe gets the prepared game image from bbport's run.py - no
|
||||
// --game, and it runs in the folder run.py started it in (paths relative to the port).
|
||||
const std::wstring emulator_name =
|
||||
emulator.substr(emulator.find_last_of(L"\\/") == std::wstring::npos
|
||||
? 0
|
||||
: emulator.find_last_of(L"\\/") + 1);
|
||||
const bool bbport = _wcsicmp(emulator_name.c_str(), L"bb-probe.exe") == 0;
|
||||
has_game = has_game || bbport;
|
||||
if (emulator.empty()) {
|
||||
emulator = HereFile(L"shadPS4.exe");
|
||||
if (!FileExists(emulator)) {
|
||||
emulator = choose ? std::wstring{} : LoadSetting(L"emulator");
|
||||
if (!FileExists(emulator)) {
|
||||
emulator = ChooseEmulator();
|
||||
if (emulator.empty()) {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Only shadPS4 takes --game; other hosts (bbport's bb-probe, bbhost) get their own arguments,
|
||||
// and keep our output handles (their launchers log what the game writes).
|
||||
bool foreign = false;
|
||||
{
|
||||
const size_t cut = emulator.find_last_of(L"\\/");
|
||||
const std::wstring name = emulator.substr(cut == std::wstring::npos ? 0 : cut + 1);
|
||||
foreign = _wcsnicmp(name.c_str(), L"shadps4", 7) != 0;
|
||||
has_game = has_game || foreign;
|
||||
}
|
||||
if (!has_game) {
|
||||
std::wstring game = choose ? std::wstring{} : LoadSetting(L"game");
|
||||
if (!FileExists(game)) {
|
||||
game = ChooseGame();
|
||||
if (game.empty()) {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
passthrough.push_back(L"--game");
|
||||
passthrough.push_back(game);
|
||||
}
|
||||
// The BBSC game build does all of this itself; with the mod it must not (the mod does it).
|
||||
// Set only if the player did not set them; a build without BBSC ignores them.
|
||||
if (!no_mod) {
|
||||
DefaultSwitch(L"SHADPS4_BBSC_PERSIST", L"0");
|
||||
DefaultSwitch(L"SHADPS4_BLOODBORNE_SEAMLESS_COOP", L"0");
|
||||
DefaultSwitch(L"SHADPS4_BLOODBORNE_RE_TRACE", L"0");
|
||||
}
|
||||
wchar_t full_dll[MAX_PATH]{};
|
||||
if (GetFullPathNameW(dll.c_str(), MAX_PATH, full_dll, nullptr) == 0 ||
|
||||
(!no_mod && GetFileAttributesW(full_dll) == INVALID_FILE_ATTRIBUTES)) {
|
||||
return Fail((L"mod DLL not found: " + dll).c_str());
|
||||
}
|
||||
if (GetFileAttributesW(emulator.c_str()) == INVALID_FILE_ATTRIBUTES) {
|
||||
return Fail((L"emulator not found: " + emulator).c_str());
|
||||
}
|
||||
|
||||
std::wstring command = Quote(emulator);
|
||||
for (const auto& arg : passthrough) {
|
||||
command += L" " + Quote(arg);
|
||||
}
|
||||
std::wstring working_dir = emulator;
|
||||
const size_t slash = working_dir.find_last_of(L"\\/");
|
||||
working_dir = slash == std::wstring::npos ? L"." : working_dir.substr(0, slash);
|
||||
if (!start_dir.empty()) {
|
||||
working_dir = start_dir;
|
||||
} else if (bbport) {
|
||||
wchar_t current[MAX_PATH]{};
|
||||
GetCurrentDirectoryW(MAX_PATH, current);
|
||||
working_dir = current;
|
||||
}
|
||||
|
||||
STARTUPINFOW startup{sizeof(startup)};
|
||||
if (foreign) {
|
||||
startup.dwFlags |= STARTF_USESTDHANDLES;
|
||||
startup.hStdInput = GetStdHandle(STD_INPUT_HANDLE);
|
||||
startup.hStdOutput = GetStdHandle(STD_OUTPUT_HANDLE);
|
||||
startup.hStdError = GetStdHandle(STD_ERROR_HANDLE);
|
||||
}
|
||||
PROCESS_INFORMATION info{};
|
||||
// bbport: its launcher sends bb-probe's output to user\last_run.log through our handles.
|
||||
if (!CreateProcessW(emulator.c_str(), command.data(), nullptr, nullptr, foreign ? TRUE : FALSE,
|
||||
CREATE_SUSPENDED,
|
||||
nullptr, working_dir.c_str(), &startup, &info)) {
|
||||
return Fail(L"could not start the emulator");
|
||||
}
|
||||
|
||||
// Load the DLL in the suspended emulator: its first thread runs the loader's process setup.
|
||||
if (!no_mod) {
|
||||
const size_t bytes = (wcslen(full_dll) + 1) * sizeof(wchar_t);
|
||||
void* remote = VirtualAllocEx(info.hProcess, nullptr, bytes, MEM_COMMIT | MEM_RESERVE,
|
||||
PAGE_READWRITE);
|
||||
if (remote == nullptr || !WriteProcessMemory(info.hProcess, remote, full_dll, bytes, nullptr)) {
|
||||
return Fail(L"could not pass the DLL path to the emulator", info.hProcess);
|
||||
}
|
||||
auto* load_library = reinterpret_cast<LPTHREAD_START_ROUTINE>(
|
||||
GetProcAddress(GetModuleHandleW(L"kernel32.dll"), "LoadLibraryW"));
|
||||
HANDLE loader = CreateRemoteThread(info.hProcess, nullptr, 0, load_library, remote, 0, nullptr);
|
||||
if (loader == nullptr) {
|
||||
return Fail(L"could not load the mod into the emulator", info.hProcess);
|
||||
}
|
||||
WaitForSingleObject(loader, 30'000);
|
||||
DWORD loaded = 0;
|
||||
GetExitCodeThread(loader, &loaded);
|
||||
CloseHandle(loader);
|
||||
VirtualFreeEx(info.hProcess, remote, 0, MEM_RELEASE);
|
||||
if (loaded == 0) {
|
||||
return Fail(L"the emulator refused the mod DLL (LoadLibraryW returned 0)", info.hProcess);
|
||||
}
|
||||
} // !no_mod
|
||||
ResumeThread(info.hThread);
|
||||
CloseHandle(info.hThread);
|
||||
if (window_side == L"left" || window_side == L"right" || window_side == L"tl" ||
|
||||
window_side == L"tr" || window_side == L"bl" || window_side == L"br") {
|
||||
auto* args = new std::pair<DWORD, std::wstring>(info.dwProcessId, window_side);
|
||||
if (HANDLE placer = CreateThread(nullptr, 0, PlaceWindowThread, args, 0, nullptr)) {
|
||||
CloseHandle(placer);
|
||||
}
|
||||
}
|
||||
WaitForSingleObject(info.hProcess, INFINITE);
|
||||
DWORD code = 0;
|
||||
GetExitCodeProcess(info.hProcess, &code);
|
||||
CloseHandle(info.hProcess);
|
||||
return static_cast<int>(code);
|
||||
}
|
||||
@@ -1,929 +0,0 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2026 CLS_AMG
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
// BBSC as a mod: a DLL that bbsc-launch.exe injects into shadPS4.exe before it starts.
|
||||
//
|
||||
// The DLL runs in the emulator's process, where the game's memory sits at its guest addresses
|
||||
// (eboot at 0x800000000 in the shadp2p fork; the official shadPS4 nightly of 2026-10-09 loads it
|
||||
// at 0x9B00000 - FindEbootBase looks for it), so it needs nothing from the emulator:
|
||||
// 1. wait until the eboot is in memory and every byte patch site reads back as the bytes that
|
||||
// must be there (all of them before writing any - another game or version is left alone);
|
||||
// 2. write the byte patches (core/bbsc_patch_tables.h, the same list as the emulator build);
|
||||
// 3. install every hook with the emulator build's own code, core/bloodborne_re.cpp, compiled
|
||||
// into this DLL with BBSC_MOD; its emulator calls are answered here and in bbsc_hooks.cpp
|
||||
// (hook trampolines) and bbsc_relay.cpp (BBSC's packets, through the game server);
|
||||
// 4. keep watching: a patch site the emulator wrote back is written again.
|
||||
// Session suspensions and the lamp role toggles come from bloodborne_re.cpp as in the emulator
|
||||
// build (SetPersistentSessionPatches / SetHostLampPatches below).
|
||||
//
|
||||
// Log: logs\bbsc-mod-<date>-<time>-<pid>.log next to the DLL (one per game window, so two
|
||||
// windows on one PC do not overwrite each other); capture files in captures\ next to it.
|
||||
|
||||
#include <windows.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <atomic>
|
||||
#include <cmath>
|
||||
#include <cstdarg>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <filesystem>
|
||||
#include <mutex>
|
||||
#include <vector>
|
||||
#include <string>
|
||||
|
||||
#include "common/logging/log.h"
|
||||
#include "common/memory_patcher.h"
|
||||
#include "common/path_util.h"
|
||||
#include "core/bbsc_cinematic.h"
|
||||
#include "core/bbsc_patch_tables.h"
|
||||
#include "core/bloodborne_re.h"
|
||||
#include "core/debugger.h"
|
||||
#include "core/libraries/np/np_matching2/np_matching2_mm.h"
|
||||
#include "core/memory.h"
|
||||
|
||||
using namespace Core::Bbsc::PatchTables;
|
||||
|
||||
// Public version (2026-10-11): 0.MINOR.PATCH while testing - PATCH for bug fixes, MINOR for new
|
||||
// features, 1.0.0 the finished release. Keep it equal to "version" in mod.json.
|
||||
#define BBSC_VERSION "0.0.1"
|
||||
|
||||
namespace BbscMod {
|
||||
void LogPlain(const char* text);
|
||||
u64 EbootAddress();
|
||||
void StartRelay();
|
||||
void StartP2PChannel(); // bbsc_channel.cpp
|
||||
bool GameServerIsPublic(std::string* host_out); // bbsc_relay.cpp
|
||||
void InstallHttpWrappers();
|
||||
void StartDiscordStatus();
|
||||
} // namespace BbscMod
|
||||
namespace Core::Bbsc {
|
||||
void InstallPadWrapper();
|
||||
} // namespace Core::Bbsc
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr u64 ForkEbootBase = 0x800000000ULL; // where the shadp2p fork puts it
|
||||
u64 EbootBase = 0; // found by FindEbootBase before anything else uses it
|
||||
constexpr u64 EbootSize = 0x056D4000ULL; // CUSA03173 01.09 image size
|
||||
constexpr DWORD WaitForImageMs = 180'000;
|
||||
constexpr DWORD SettleMs = 20; // let the emulator finish its own load-time patching
|
||||
constexpr DWORD WatchMs = 500;
|
||||
|
||||
HMODULE self_module = nullptr;
|
||||
std::filesystem::path mod_dir;
|
||||
FILE* log_file = nullptr;
|
||||
std::mutex log_mutex;
|
||||
|
||||
void OpenLog() {
|
||||
wchar_t path[MAX_PATH]{};
|
||||
GetModuleFileNameW(self_module, path, MAX_PATH);
|
||||
mod_dir = std::filesystem::path{path}.parent_path();
|
||||
SYSTEMTIME now;
|
||||
GetLocalTime(&now);
|
||||
wchar_t name[96]{};
|
||||
swprintf(name, 96, L"bbsc-mod-%04u%02u%02u-%02u%02u%02u-%lu.log", now.wYear, now.wMonth, now.wDay,
|
||||
now.wHour, now.wMinute, now.wSecond, GetCurrentProcessId());
|
||||
std::error_code ignored;
|
||||
std::filesystem::create_directories(mod_dir / L"logs", ignored);
|
||||
log_file = _wfopen((mod_dir / L"logs" / name).c_str(), L"w");
|
||||
}
|
||||
|
||||
// ---- Byte patches --------------------------------------------------------------------------------
|
||||
|
||||
// VirtualQuery is a kernel call that shares the address-space lock with the emulator's constant
|
||||
// page-protection changes (GPU memory tracking). bloodborne_re.cpp checks every memory read, so
|
||||
// thousands of these per frame took the game to ~1 fps (2026-09-30, two testers). The emulator
|
||||
// build answers the same checks from its own memory map; here each game thread remembers the last
|
||||
// few regions it asked about for a moment.
|
||||
struct CachedRegion {
|
||||
u64 start = 0;
|
||||
u64 end = 0;
|
||||
DWORD state = 0;
|
||||
DWORD protect = 0;
|
||||
u64 stamp_ms = 0;
|
||||
};
|
||||
constexpr u64 RegionCacheMs = 250;
|
||||
constexpr size_t RegionCacheSize = 16;
|
||||
|
||||
bool QueryRegion(u64 address, CachedRegion& out) {
|
||||
thread_local std::array<CachedRegion, RegionCacheSize> cache{};
|
||||
thread_local size_t next = 0;
|
||||
const u64 now = GetTickCount64();
|
||||
for (const CachedRegion& region : cache) {
|
||||
if (address >= region.start && address < region.end &&
|
||||
now - region.stamp_ms <= RegionCacheMs) {
|
||||
out = region;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
MEMORY_BASIC_INFORMATION info{};
|
||||
if (VirtualQuery(reinterpret_cast<void*>(address), &info, sizeof(info)) == 0) {
|
||||
return false;
|
||||
}
|
||||
CachedRegion& slot = cache[next];
|
||||
next = (next + 1) % RegionCacheSize;
|
||||
slot.start = reinterpret_cast<u64>(info.BaseAddress);
|
||||
slot.end = slot.start + info.RegionSize;
|
||||
slot.state = info.State;
|
||||
slot.protect = info.Protect;
|
||||
slot.stamp_ms = now;
|
||||
out = slot;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Readable(u64 address, size_t size) {
|
||||
u64 at = address;
|
||||
const u64 end = address + size;
|
||||
while (at < end) {
|
||||
CachedRegion region;
|
||||
if (!QueryRegion(at, region) || region.state != MEM_COMMIT ||
|
||||
(region.protect & (PAGE_NOACCESS | PAGE_GUARD)) != 0) {
|
||||
return false;
|
||||
}
|
||||
at = region.end;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool MatchesAt(u64 base, u64 offset, const u8* bytes, size_t size) {
|
||||
const u64 address = base + offset;
|
||||
return Readable(address, size) &&
|
||||
std::memcmp(reinterpret_cast<const void*>(address), bytes, size) == 0;
|
||||
}
|
||||
|
||||
bool Matches(u64 offset, const u8* bytes, size_t size) {
|
||||
const u64 address = EbootBase + offset;
|
||||
return Readable(address, size) &&
|
||||
std::memcmp(reinterpret_cast<const void*>(address), bytes, size) == 0;
|
||||
}
|
||||
|
||||
void Write(u64 offset, const u8* bytes, size_t size) {
|
||||
void* address = reinterpret_cast<void*>(EbootBase + offset);
|
||||
DWORD old = 0;
|
||||
if (!VirtualProtect(address, size, PAGE_EXECUTE_READWRITE, &old)) {
|
||||
return;
|
||||
}
|
||||
std::memcpy(address, bytes, size);
|
||||
DWORD ignored = 0;
|
||||
VirtualProtect(address, size, old, &ignored);
|
||||
FlushInstructionCache(GetCurrentProcess(), address, size);
|
||||
}
|
||||
|
||||
struct Site {
|
||||
const char* name;
|
||||
u64 offset;
|
||||
std::array<u8, 6> original{};
|
||||
std::array<u8, 6> patched{};
|
||||
size_t size;
|
||||
Group group;
|
||||
};
|
||||
|
||||
template <size_t N>
|
||||
std::array<u8, 6> Pad(const std::array<u8, N>& bytes) {
|
||||
std::array<u8, 6> out{};
|
||||
std::memcpy(out.data(), bytes.data(), N);
|
||||
return out;
|
||||
}
|
||||
|
||||
constexpr size_t SiteCount =
|
||||
Retargets.size() + EntryReturns.size() + ByteEdits.size() + WideEdits.size() + EntryJumps.size();
|
||||
std::array<Site, SiteCount> sites{};
|
||||
std::mutex patch_mutex;
|
||||
bool patches_ready = false;
|
||||
bool session_on = true; // session groups: off while a suspension runs
|
||||
bool role_on = false; // role groups (lamp): on while hosting
|
||||
|
||||
void BuildSites() {
|
||||
size_t i = 0;
|
||||
for (const auto& e : Retargets) {
|
||||
sites[i++] = {e.name, e.site, Pad(EncodeRel32(e.opcode, e.site, e.original)),
|
||||
Pad(EncodeRel32(e.opcode, e.site, e.target)), 5, e.group};
|
||||
}
|
||||
for (const auto& e : EntryReturns) {
|
||||
std::array<u8, 6> ret{};
|
||||
ret[0] = 0xC3;
|
||||
sites[i++] = {e.name, e.site, e.prologue, ret, 1, e.group};
|
||||
}
|
||||
for (const auto& e : ByteEdits) {
|
||||
sites[i++] = {e.name, e.site, Pad(e.original), Pad(e.replacement), 2, e.group};
|
||||
}
|
||||
for (const auto& e : WideEdits) {
|
||||
sites[i++] = {e.name, e.site, Pad(e.original), Pad(e.replacement), 4, e.group};
|
||||
}
|
||||
for (const auto& e : EntryJumps) {
|
||||
sites[i++] = {e.name, e.site, Pad(e.prologue), Pad(EncodeRel32(0xE9, e.site, e.target)), 5,
|
||||
e.group};
|
||||
}
|
||||
}
|
||||
|
||||
// The game image: where the fork puts it, else the start of any executable region whose first few
|
||||
// patch sites read as Bloodborne's (original or patched). AllSitesKnown then checks every site.
|
||||
u64 FindEbootBase() {
|
||||
const auto fits = [](u64 base) {
|
||||
int checked = 0;
|
||||
for (const Site& s : sites) {
|
||||
if (s.offset + s.size > EbootSize) {
|
||||
continue;
|
||||
}
|
||||
if (!MatchesAt(base, s.offset, s.original.data(), s.size) &&
|
||||
!MatchesAt(base, s.offset, s.patched.data(), s.size)) {
|
||||
return false;
|
||||
}
|
||||
if (++checked == 4) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return checked != 0;
|
||||
};
|
||||
if (fits(ForkEbootBase)) {
|
||||
return ForkEbootBase;
|
||||
}
|
||||
constexpr DWORD Executable = PAGE_EXECUTE | PAGE_EXECUTE_READ | PAGE_EXECUTE_READWRITE |
|
||||
PAGE_EXECUTE_WRITECOPY;
|
||||
MEMORY_BASIC_INFORMATION info{};
|
||||
u64 at = 0x10000;
|
||||
while (at < 0x7FFF'FFFF'0000ULL &&
|
||||
VirtualQuery(reinterpret_cast<void*>(at), &info, sizeof(info)) != 0) {
|
||||
const u64 base = reinterpret_cast<u64>(info.BaseAddress);
|
||||
if (info.State == MEM_COMMIT && (info.Protect & Executable) != 0 &&
|
||||
(info.Protect & PAGE_GUARD) == 0 && fits(base)) {
|
||||
return base;
|
||||
}
|
||||
at = base + info.RegionSize;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
bool AllSitesKnown(const Site** first_bad) {
|
||||
for (const Site& s : sites) {
|
||||
if (!GroupEnabled(s.group)) {
|
||||
continue;
|
||||
}
|
||||
if (s.offset + s.size > EbootSize ||
|
||||
(!Matches(s.offset, s.original.data(), s.size) &&
|
||||
!Matches(s.offset, s.patched.data(), s.size))) {
|
||||
*first_bad = &s;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Whether a site should be patched right now.
|
||||
bool Wanted(const Site& s) {
|
||||
if (!GroupEnabled(s.group)) {
|
||||
return false;
|
||||
}
|
||||
if (IsRoleGroup(s.group)) {
|
||||
return role_on;
|
||||
}
|
||||
if (IsSessionGroup(s.group)) {
|
||||
return session_on;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Brings every enabled site to its wanted form. Returns how many were written.
|
||||
int SyncSites() {
|
||||
int count = 0;
|
||||
for (const Site& s : sites) {
|
||||
if (!GroupEnabled(s.group)) {
|
||||
continue;
|
||||
}
|
||||
const auto& want = Wanted(s) ? s.patched : s.original;
|
||||
if (!Matches(s.offset, want.data(), s.size)) {
|
||||
Write(s.offset, want.data(), s.size);
|
||||
++count;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
// ---- Start-up ------------------------------------------------------------------------------------
|
||||
|
||||
// LICENSE.txt (the GPL) and NOTICE.txt (copyright + source link) ship in the mod folder so a copy
|
||||
// passed on keeps them. Missing or emptied -> BBSC stays off and says why.
|
||||
bool NoticesPresent() {
|
||||
auto holds = [](const wchar_t* name, const char* text) {
|
||||
FILE* file = _wfopen((mod_dir / name).c_str(), L"rb");
|
||||
if (file == nullptr) {
|
||||
return false;
|
||||
}
|
||||
std::string content(64 * 1024, '\0');
|
||||
content.resize(fread(content.data(), 1, content.size(), file));
|
||||
fclose(file);
|
||||
return content.find(text) != std::string::npos;
|
||||
};
|
||||
// SUPPORTERS.txt (2026-10-11, the user): the supporters list travels with BBSC like the
|
||||
// license; its names change freely, only the file and its header are required.
|
||||
return holds(L"LICENSE.txt", "GNU GENERAL PUBLIC LICENSE") &&
|
||||
holds(L"NOTICE.txt", "github.com/Classmg22/BBSC") &&
|
||||
holds(L"SUPPORTERS.txt", "BBSC SUPPORTERS");
|
||||
}
|
||||
|
||||
DWORD WINAPI Worker(void*) {
|
||||
OpenLog();
|
||||
wchar_t exe[MAX_PATH]{};
|
||||
GetModuleFileNameW(nullptr, exe, MAX_PATH);
|
||||
BbscMod::LogPlain("BBSC mod loaded - version " BBSC_VERSION);
|
||||
if (!NoticesPresent()) {
|
||||
LOG_ERROR(Mod, "LICENSE.txt / NOTICE.txt / SUPPORTERS.txt missing or changed in {} - BBSC "
|
||||
"is off",
|
||||
Common::FS::PathToUTF8String(mod_dir));
|
||||
MessageBoxW(nullptr,
|
||||
L"LICENSE.txt, NOTICE.txt or SUPPORTERS.txt is missing from the BBSC mod "
|
||||
L"folder (or was changed), so BBSC is off.\n\nThese files have to travel with "
|
||||
L"BBSC.\nDownload the full package again "
|
||||
L"from:\nhttps://github.com/Classmg22/BBSC",
|
||||
L"BBSC", MB_OK | MB_ICONERROR | MB_SETFOREGROUND);
|
||||
return 0;
|
||||
}
|
||||
LOG_INFO(Mod, "BBSC mod in pid {} ({})", GetCurrentProcessId(),
|
||||
Common::FS::PathToUTF8String(std::filesystem::path{exe}));
|
||||
// Public servers (2026-10-11, shadNet's author: The Hunter's Dream is exact to the official
|
||||
// server, custom summon behaviour does not work there). Wozzardman's seamless summon patches
|
||||
// (cross-map sign search, host-placement summon build) and the 5-hunter search patches change
|
||||
// what the game asks the summon server for, so they are off there whatever the switches say
|
||||
// (review 2026-10-11: the Mods tab writes its defaults, BBSC_MOD_SEAMLESS=1 among them, at
|
||||
// every start, so "a switch the player set wins" would have kept them on).
|
||||
{
|
||||
std::string server;
|
||||
if (BbscMod::GameServerIsPublic(&server)) {
|
||||
const auto force_off = [](const char* name) {
|
||||
_putenv_s(name, "0");
|
||||
SetEnvironmentVariableA(name, "0");
|
||||
};
|
||||
force_off("BBSC_MOD_SEAMLESS");
|
||||
force_off("SHADPS4_BBSC_FIVE_HUNTERS");
|
||||
LOG_INFO(Mod, "Public game server ({}): vanilla summons - seamless summon patches and the "
|
||||
"5-hunter search off (the session itself stays seamless)",
|
||||
server);
|
||||
}
|
||||
}
|
||||
BuildSites();
|
||||
|
||||
const DWORD started = GetTickCount();
|
||||
while ((EbootBase = FindEbootBase()) == 0) {
|
||||
if (GetTickCount() - started > WaitForImageMs) {
|
||||
LOG_ERROR(Mod, "No Bloodborne CUSA03173 01.09 image in memory after {} s - not this "
|
||||
"game or not this version; nothing was changed",
|
||||
WaitForImageMs / 1000);
|
||||
return 0;
|
||||
}
|
||||
Sleep(50);
|
||||
}
|
||||
LOG_INFO(Mod, "Game image found at {:#x}{}", EbootBase,
|
||||
EbootBase == ForkEbootBase ? "" : " (not the shadp2p fork's address)");
|
||||
// shadPS4 maps the game's code writable; other hosts (bbhost) keep it read-only, and the
|
||||
// shared hook code writes some code bytes directly. Make the image's code writable like shadPS4.
|
||||
{
|
||||
int unlocked = 0;
|
||||
MEMORY_BASIC_INFORMATION info{};
|
||||
u64 at = EbootBase;
|
||||
while (at < EbootBase + EbootSize &&
|
||||
VirtualQuery(reinterpret_cast<void*>(at), &info, sizeof(info)) != 0) {
|
||||
const u64 next = reinterpret_cast<u64>(info.BaseAddress) + info.RegionSize;
|
||||
if (info.State == MEM_COMMIT &&
|
||||
(info.Protect == PAGE_EXECUTE_READ || info.Protect == PAGE_EXECUTE)) {
|
||||
DWORD old = 0;
|
||||
const u64 end = std::min<u64>(next, EbootBase + EbootSize);
|
||||
if (VirtualProtect(info.BaseAddress, end - reinterpret_cast<u64>(info.BaseAddress),
|
||||
PAGE_EXECUTE_READWRITE, &old)) {
|
||||
++unlocked;
|
||||
}
|
||||
}
|
||||
at = next;
|
||||
}
|
||||
if (unlocked != 0) {
|
||||
LOG_INFO(Mod, "Game code made writable ({} region(s)) - this host maps it read-only",
|
||||
unlocked);
|
||||
}
|
||||
}
|
||||
const Site* bad = nullptr;
|
||||
while (!AllSitesKnown(&bad)) {
|
||||
if (GetTickCount() - started > WaitForImageMs) {
|
||||
LOG_ERROR(Mod,
|
||||
"No Bloodborne CUSA03173 01.09 image after {} s (first unknown site: {} at "
|
||||
"{:#x}) - not this game or not this version; nothing was changed",
|
||||
WaitForImageMs / 1000, bad ? bad->name : "?", bad ? bad->offset : 0);
|
||||
return 0;
|
||||
}
|
||||
Sleep(1);
|
||||
}
|
||||
Sleep(SettleMs);
|
||||
const DWORD verified = GetTickCount() - started;
|
||||
|
||||
MemoryPatcher::g_eboot_address = EbootBase;
|
||||
MemoryPatcher::g_eboot_image_size = EbootSize;
|
||||
MemoryPatcher::g_game_serial = "CUSA03173";
|
||||
|
||||
// Wozzardman's seamless patches first: one of them (0x019471B1) is also in BBSC's list, and it
|
||||
// checks for the original bytes.
|
||||
Core::Bloodborne::InstallSeamlessCoopPatches();
|
||||
|
||||
int written = 0;
|
||||
{
|
||||
std::scoped_lock lock{patch_mutex};
|
||||
patches_ready = true;
|
||||
written = SyncSites();
|
||||
}
|
||||
LOG_INFO(Mod, "Image verified after {} ms: {} byte patch sites written", verified, written);
|
||||
|
||||
Core::Bloodborne::InstallReverseEngineeringTrace();
|
||||
BbscMod::StartRelay();
|
||||
BbscMod::StartP2PChannel();
|
||||
BbscMod::InstallHttpWrappers();
|
||||
Core::Bbsc::InstallPadWrapper();
|
||||
BbscMod::StartDiscordStatus();
|
||||
LOG_INFO(Mod, "BBSC mod ready after {} ms", GetTickCount() - started);
|
||||
|
||||
for (;;) {
|
||||
Sleep(WatchMs);
|
||||
std::scoped_lock lock{patch_mutex};
|
||||
if (const int redone = SyncSites(); redone != 0) {
|
||||
LOG_WARNING(Mod, "{} patch site(s) had changed back - written again", redone);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
// ---- Logging -------------------------------------------------------------------------------------
|
||||
|
||||
namespace BbscMod {
|
||||
|
||||
void LogLine(char level, const char* category, const std::string& text) {
|
||||
std::scoped_lock lock{log_mutex};
|
||||
if (log_file == nullptr) {
|
||||
return;
|
||||
}
|
||||
SYSTEMTIME now;
|
||||
GetLocalTime(&now);
|
||||
std::fprintf(log_file, "[%02u:%02u:%02u.%03u] %c %s: %s\n", now.wHour, now.wMinute, now.wSecond,
|
||||
now.wMilliseconds, level, category, text.c_str());
|
||||
std::fflush(log_file);
|
||||
}
|
||||
|
||||
void LogPlain(const char* text) {
|
||||
LogLine('I', "Mod", text);
|
||||
}
|
||||
|
||||
u64 EbootAddress() {
|
||||
return EbootBase;
|
||||
}
|
||||
|
||||
// Import slots the DLL swapped for its wrappers (bbsc_http.cpp, bbsc_channel.cpp). The official
|
||||
// shadPS4 links the eboot's imports again when the game loads a library later (2026-10-10: no
|
||||
// wrapper ever ran on the nightly - the slots were written back to the emulator's functions), so a
|
||||
// thread puts the wrapper back whenever a slot no longer points at it, taking the new value as the
|
||||
// original.
|
||||
namespace {
|
||||
struct GuardedSlot {
|
||||
u64 slot_offset;
|
||||
u64 wrapper;
|
||||
void** original; // the wrapper's function pointer (an aligned 8-byte store)
|
||||
const char* name;
|
||||
int rewritten = 0;
|
||||
};
|
||||
std::mutex guarded_mutex;
|
||||
std::vector<GuardedSlot> guarded_slots;
|
||||
|
||||
DWORD WINAPI SlotGuardThread(void*) {
|
||||
for (;;) {
|
||||
Sleep(5);
|
||||
std::scoped_lock lock{guarded_mutex};
|
||||
for (auto& guarded : guarded_slots) {
|
||||
auto* slot = reinterpret_cast<volatile u64*>(EbootBase + guarded.slot_offset);
|
||||
const u64 value = *slot;
|
||||
if (value == guarded.wrapper || value == 0 ||
|
||||
(value >= EbootBase && value < EbootBase + EbootSize)) {
|
||||
continue;
|
||||
}
|
||||
*reinterpret_cast<void* volatile*>(guarded.original) = reinterpret_cast<void*>(value);
|
||||
DWORD old = 0;
|
||||
VirtualProtect(const_cast<u64*>(slot), 8, PAGE_READWRITE, &old);
|
||||
*slot = guarded.wrapper;
|
||||
DWORD ignored = 0;
|
||||
VirtualProtect(const_cast<u64*>(slot), 8, old, &ignored);
|
||||
if (++guarded.rewritten <= 3) {
|
||||
LOG_INFO(Mod, "Import {} was linked again by the emulator - wrapper put back ({}x)",
|
||||
guarded.name, guarded.rewritten);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} // namespace
|
||||
|
||||
void GuardSlot(u64 slot_offset, u64 wrapper, void** original, const char* name) {
|
||||
std::scoped_lock lock{guarded_mutex};
|
||||
if (guarded_slots.empty()) {
|
||||
if (HANDLE thread = CreateThread(nullptr, 0, SlotGuardThread, nullptr, 0, nullptr)) {
|
||||
CloseHandle(thread);
|
||||
}
|
||||
}
|
||||
guarded_slots.push_back({slot_offset, wrapper, original, name});
|
||||
}
|
||||
|
||||
} // namespace BbscMod
|
||||
|
||||
// ---- What bloodborne_re.cpp asks the emulator for ----------------------------------------------
|
||||
|
||||
namespace Core {
|
||||
|
||||
bool MemoryManager::IsValidMapping(u64 address, u64 size) {
|
||||
return Readable(address, size == 0 ? 1 : size);
|
||||
}
|
||||
|
||||
s32 MemoryManager::QueryProtection(u64 address, void** start, void** end, u32* prot) {
|
||||
CachedRegion info;
|
||||
if (!QueryRegion(address, info) || info.state != MEM_COMMIT) {
|
||||
return -1;
|
||||
}
|
||||
*start = reinterpret_cast<void*>(info.start);
|
||||
*end = reinterpret_cast<void*>(info.end);
|
||||
u32 bits = 0;
|
||||
switch (info.protect & 0xFF) {
|
||||
case PAGE_READONLY:
|
||||
bits = 1;
|
||||
break;
|
||||
case PAGE_READWRITE:
|
||||
case PAGE_WRITECOPY:
|
||||
bits = 3;
|
||||
break;
|
||||
case PAGE_EXECUTE_READ:
|
||||
bits = 5;
|
||||
break;
|
||||
case PAGE_EXECUTE_READWRITE:
|
||||
case PAGE_EXECUTE_WRITECOPY:
|
||||
bits = 7;
|
||||
break;
|
||||
case PAGE_EXECUTE:
|
||||
bits = 4;
|
||||
break;
|
||||
default:
|
||||
bits = 0;
|
||||
}
|
||||
if ((info.protect & PAGE_GUARD) != 0) {
|
||||
bits = 0;
|
||||
}
|
||||
*prot = bits;
|
||||
return 0;
|
||||
}
|
||||
|
||||
namespace Debugger {
|
||||
int GetCurrentPid() {
|
||||
return static_cast<int>(GetCurrentProcessId());
|
||||
}
|
||||
} // namespace Debugger
|
||||
|
||||
namespace Bbsc {
|
||||
|
||||
void InstallPersistentSessionPatches() {} // the DLL applies them itself (Worker)
|
||||
|
||||
void SetPersistentSessionPatches(bool enabled) {
|
||||
std::scoped_lock lock{patch_mutex};
|
||||
if (!patches_ready || session_on == enabled) {
|
||||
return;
|
||||
}
|
||||
session_on = enabled;
|
||||
const int count = SyncSites();
|
||||
LOG_INFO(Debug, "BBSC persistence {} ({} session sites; echoes, lamp, fog walls and deaths "
|
||||
"untouched)",
|
||||
enabled ? "re-applied" : "suspended", count);
|
||||
}
|
||||
|
||||
void SetHostLampPatches(bool hosting) {
|
||||
std::scoped_lock lock{patch_mutex};
|
||||
if (!patches_ready || role_on == hosting) {
|
||||
return;
|
||||
}
|
||||
role_on = hosting;
|
||||
const int count = SyncSites();
|
||||
LOG_INFO(Debug, "BBSC lamp: {} ({} sites)",
|
||||
hosting ? "prompt allowed (hosting a session)"
|
||||
: "vanilla gates (solo or guest - a guest never gets the lamp)",
|
||||
count);
|
||||
}
|
||||
|
||||
// The F1 camera. The emulator build feeds it from its SDL window events (bbsc_cinematic.cpp); a mod
|
||||
// sees no window events, so the keys are polled here instead, only while this process's window is
|
||||
// in front (Nucleus runs two). While it is on, CineWindowProc keeps the keys and the mouse from the
|
||||
// game (2026-10-12; before, they reached it too). bloodborne_re.cpp turns it on by itself when you
|
||||
// die in co-op and off when you respawn.
|
||||
// 2026-10-05 (the user: better mouse controls): the mouse looks around without a button held - the
|
||||
// cursor is hidden and kept in the middle of the game window - and the wheel sets the speed (the
|
||||
// game window is subclassed for WM_MOUSEWHEEL / WM_SETCURSOR while the camera is on; PageUp /
|
||||
// PageDown still work).
|
||||
std::mutex cine_mutex;
|
||||
CinematicInput cine_state;
|
||||
std::array<bool, 256> cine_was_down{};
|
||||
bool cine_looking = false;
|
||||
POINT cine_anchor{};
|
||||
HWND cine_window = nullptr; // the subclassed game window (SDL's "SDL_app")
|
||||
WNDPROC cine_window_proc = nullptr; // its own window procedure
|
||||
std::atomic<bool> cine_mouse_owned{false}; // the camera is on and has the mouse
|
||||
std::atomic<int> cine_wheel{0}; // wheel notches since the last TakeCinematicInput
|
||||
|
||||
LRESULT CALLBACK CineWindowProc(HWND window, UINT message, WPARAM wparam, LPARAM lparam) {
|
||||
if (cine_mouse_owned) {
|
||||
if (message == WM_MOUSEWHEEL) {
|
||||
const int delta = GET_WHEEL_DELTA_WPARAM(wparam);
|
||||
cine_wheel += delta > 0 ? 1 : (delta < 0 ? -1 : 0);
|
||||
return 0; // not for the game
|
||||
}
|
||||
if (message == WM_SETCURSOR && LOWORD(lparam) == HTCLIENT) {
|
||||
SetCursor(nullptr);
|
||||
return TRUE;
|
||||
}
|
||||
// 2026-10-12 (the user: "each button does a million things"): the camera's keys walked,
|
||||
// dodged and used items at the same time, and the mouse turned the game's camera. While
|
||||
// the camera is on, presses and movement stop here; the emulator (SDL) never sees them.
|
||||
// Releases still go through, so a key held when the camera came on is not left stuck.
|
||||
// The camera reads the keys itself (GetAsyncKeyState), so it is not affected. Alt+F4 passes.
|
||||
switch (message) {
|
||||
case WM_KEYDOWN:
|
||||
case WM_CHAR:
|
||||
case WM_DEADCHAR:
|
||||
case WM_SYSCHAR:
|
||||
case WM_LBUTTONDOWN:
|
||||
case WM_LBUTTONDBLCLK:
|
||||
case WM_RBUTTONDOWN:
|
||||
case WM_RBUTTONDBLCLK:
|
||||
case WM_MBUTTONDOWN:
|
||||
case WM_MBUTTONDBLCLK:
|
||||
case WM_XBUTTONDOWN:
|
||||
case WM_XBUTTONDBLCLK:
|
||||
case WM_MOUSEMOVE:
|
||||
return 0;
|
||||
case WM_SYSKEYDOWN:
|
||||
if (wparam != VK_F4) {
|
||||
return 0;
|
||||
}
|
||||
break;
|
||||
case WM_INPUT: {
|
||||
// Raw input (SDL's relative mouse): drop movement and presses, keep releases.
|
||||
RAWINPUT raw{};
|
||||
UINT size = sizeof(raw);
|
||||
if (GetRawInputData(reinterpret_cast<HRAWINPUT>(lparam), RID_INPUT, &raw, &size,
|
||||
sizeof(RAWINPUTHEADER)) != static_cast<UINT>(-1)) {
|
||||
bool release = false;
|
||||
if (raw.header.dwType == RIM_TYPEKEYBOARD) {
|
||||
release = (raw.data.keyboard.Flags & RI_KEY_BREAK) != 0;
|
||||
} else if (raw.header.dwType == RIM_TYPEMOUSE) {
|
||||
constexpr USHORT Ups = RI_MOUSE_BUTTON_1_UP | RI_MOUSE_BUTTON_2_UP |
|
||||
RI_MOUSE_BUTTON_3_UP | RI_MOUSE_BUTTON_4_UP |
|
||||
RI_MOUSE_BUTTON_5_UP;
|
||||
release = (raw.data.mouse.usButtonFlags & Ups) != 0;
|
||||
}
|
||||
if (!release) {
|
||||
return DefWindowProcW(window, message, wparam, lparam);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
return CallWindowProcW(cine_window_proc, window, message, wparam, lparam);
|
||||
}
|
||||
|
||||
// The game window when it is in front, else nullptr. Subclassed the first time it is seen.
|
||||
HWND CineGameWindowInFront() {
|
||||
HWND window = GetForegroundWindow();
|
||||
DWORD pid = 0;
|
||||
GetWindowThreadProcessId(window, &pid);
|
||||
if (window == nullptr || pid != GetCurrentProcessId()) {
|
||||
return nullptr;
|
||||
}
|
||||
char cls[32]{};
|
||||
GetClassNameA(window, cls, sizeof(cls));
|
||||
if (std::strcmp(cls, "SDL_app") != 0) {
|
||||
return nullptr; // e.g. the console
|
||||
}
|
||||
if (cine_window != window) {
|
||||
if (cine_window != nullptr && cine_window_proc != nullptr) {
|
||||
SetWindowLongPtrW(cine_window, GWLP_WNDPROC,
|
||||
reinterpret_cast<LONG_PTR>(cine_window_proc));
|
||||
}
|
||||
cine_window = window;
|
||||
cine_window_proc = reinterpret_cast<WNDPROC>(SetWindowLongPtrW(
|
||||
window, GWLP_WNDPROC, reinterpret_cast<LONG_PTR>(&CineWindowProc)));
|
||||
LOG_INFO(Debug, "BBSC cinematic camera: mouse wheel and cursor hooked on the game window");
|
||||
}
|
||||
return window;
|
||||
}
|
||||
|
||||
// The controller: the game's scePadReadState import slot (0x053E4EA0, tools/gamedata/
|
||||
// eboot_imports.py) is swapped for PadReadState below, which sees the pad data the emulator hands
|
||||
// the game - any controller the emulator supports. L3 + R3 together toggles the camera; while it is
|
||||
// on the sticks and buttons fly it and the game gets a pad at rest (the hunter does not move).
|
||||
constexpr u64 PadReadStateSlot = 0x053E4EA0;
|
||||
using PadReadStateFn = int PS4_SYSV_ABI (*)(s32 handle, u8* data);
|
||||
PadReadStateFn original_pad_read = nullptr;
|
||||
constexpr u32 PadL3 = 0x2, PadR3 = 0x4, PadUp = 0x10, PadRight = 0x20, PadDown = 0x40,
|
||||
PadLeft = 0x80, PadL1 = 0x400, PadR1 = 0x800, PadTriangle = 0x1000,
|
||||
PadCross = 0x4000, PadSquare = 0x8000;
|
||||
struct CinePad {
|
||||
u32 buttons = 0;
|
||||
u32 last_buttons = 0;
|
||||
float lx = 0.0f, ly = 0.0f, rx = 0.0f, ry = 0.0f, l2 = 0.0f, r2 = 0.0f;
|
||||
int speed_steps = 0, smooth_steps = 0;
|
||||
bool zoom_reset = false;
|
||||
};
|
||||
CinePad cine_pad;
|
||||
|
||||
float PadAxis(u8 value) {
|
||||
const float v = (static_cast<float>(value) - 128.0f) / 127.0f;
|
||||
constexpr float Dead = 0.15f;
|
||||
return std::abs(v) < Dead ? 0.0f : std::clamp((v - std::copysign(Dead, v)) / (1.0f - Dead), -1.0f, 1.0f);
|
||||
}
|
||||
|
||||
// OrbisPadData: +0 buttons (u32), +4 left stick x/y, +6 right stick x/y, +8 L2, +9 R2 (u8).
|
||||
int PS4_SYSV_ABI PadReadState(s32 handle, u8* data) {
|
||||
const int result = original_pad_read(handle, data);
|
||||
if (result != 0 || data == nullptr) {
|
||||
return result;
|
||||
}
|
||||
u32 buttons = 0;
|
||||
std::memcpy(&buttons, data, 4);
|
||||
std::scoped_lock lock{cine_mutex};
|
||||
const u32 pressed = buttons & ~cine_pad.last_buttons;
|
||||
cine_pad.last_buttons = buttons;
|
||||
if ((buttons & (PadL3 | PadR3)) == (PadL3 | PadR3) && (pressed & (PadL3 | PadR3)) != 0) {
|
||||
const bool enabled = !cine_state.enabled;
|
||||
cine_state = CinematicInput{};
|
||||
cine_state.enabled = enabled;
|
||||
LOG_INFO(Debug, "BBSC cinematic camera: {} (L3 + R3)", enabled ? "on" : "off");
|
||||
}
|
||||
if (!cine_state.enabled) {
|
||||
cine_pad = CinePad{.last_buttons = buttons};
|
||||
return result;
|
||||
}
|
||||
cine_pad.buttons = buttons;
|
||||
cine_pad.lx = PadAxis(data[4]);
|
||||
cine_pad.ly = PadAxis(data[5]);
|
||||
cine_pad.rx = PadAxis(data[6]);
|
||||
cine_pad.ry = PadAxis(data[7]);
|
||||
cine_pad.l2 = static_cast<float>(data[8]) / 255.0f;
|
||||
cine_pad.r2 = static_cast<float>(data[9]) / 255.0f;
|
||||
cine_pad.speed_steps += ((pressed & PadUp) ? 1 : 0) - ((pressed & PadDown) ? 1 : 0);
|
||||
cine_pad.smooth_steps += ((pressed & PadRight) ? 1 : 0) - ((pressed & PadLeft) ? 1 : 0);
|
||||
cine_pad.zoom_reset = cine_pad.zoom_reset || (pressed & PadTriangle) != 0;
|
||||
// The game sees a controller at rest (connected, nothing pressed).
|
||||
const u32 rest_buttons = buttons & 0x80000000u;
|
||||
std::memcpy(data, &rest_buttons, 4);
|
||||
data[4] = data[5] = data[6] = data[7] = 128;
|
||||
data[8] = data[9] = 0;
|
||||
return result;
|
||||
}
|
||||
|
||||
void InstallPadWrapper() {
|
||||
auto* slot = reinterpret_cast<u64*>(EbootBase + PadReadStateSlot);
|
||||
for (int i = 0; i < 60'000; ++i) {
|
||||
const u64 value = *slot;
|
||||
if (value != 0 && (value < EbootBase || value >= EbootBase + EbootSize)) {
|
||||
original_pad_read = reinterpret_cast<PadReadStateFn>(value);
|
||||
DWORD old = 0;
|
||||
VirtualProtect(slot, 8, PAGE_READWRITE, &old);
|
||||
*slot = reinterpret_cast<u64>(&PadReadState);
|
||||
DWORD ignored = 0;
|
||||
VirtualProtect(slot, 8, old, &ignored);
|
||||
LOG_INFO(Mod, "Pad: controller works the F1 camera too (L3 + R3)");
|
||||
return;
|
||||
}
|
||||
Sleep(1);
|
||||
}
|
||||
LOG_WARNING(Mod, "Pad: scePadReadState slot never filled - F1 camera keyboard only");
|
||||
}
|
||||
|
||||
bool CineKeyPressed(int key, bool down) {
|
||||
const bool pressed = down && !cine_was_down[key];
|
||||
cine_was_down[key] = down;
|
||||
return pressed;
|
||||
}
|
||||
|
||||
CinematicInput TakeCinematicInput() {
|
||||
std::scoped_lock lock{cine_mutex};
|
||||
DWORD pid = 0;
|
||||
GetWindowThreadProcessId(GetForegroundWindow(), &pid);
|
||||
const bool front = pid == GetCurrentProcessId();
|
||||
auto down = [front](int key) { return front && (GetAsyncKeyState(key) & 0x8000) != 0; };
|
||||
if (CineKeyPressed(VK_F1, down(VK_F1))) {
|
||||
const bool enabled = !cine_state.enabled;
|
||||
cine_state = CinematicInput{};
|
||||
cine_state.enabled = enabled;
|
||||
LOG_INFO(Debug, "BBSC cinematic camera: {} (F1)", enabled ? "on" : "off");
|
||||
}
|
||||
if (!cine_state.enabled) {
|
||||
cine_looking = false;
|
||||
cine_mouse_owned = false;
|
||||
cine_wheel = 0;
|
||||
return {};
|
||||
}
|
||||
CinematicInput& c = cine_state;
|
||||
const CinePad& p = cine_pad;
|
||||
c.forward = down('W');
|
||||
c.back = down('S');
|
||||
c.left = down('A');
|
||||
c.right = down('D');
|
||||
c.up = down('E') || down(VK_SPACE);
|
||||
c.down = down('Q') || down('C');
|
||||
c.fast = down(VK_SHIFT) || (p.buttons & PadCross) != 0;
|
||||
c.slow = down(VK_MENU) || (p.buttons & PadSquare) != 0;
|
||||
c.zoom_in = down('Z');
|
||||
c.zoom_out = down('X');
|
||||
c.zoom_reset = CineKeyPressed('V', down('V')) || p.zoom_reset;
|
||||
cine_pad.zoom_reset = false;
|
||||
c.roll_left = down(VK_OEM_COMMA);
|
||||
c.roll_right = down(VK_OEM_PERIOD);
|
||||
c.roll_reset = CineKeyPressed(VK_OEM_2, down(VK_OEM_2));
|
||||
c.look_up = down(VK_UP);
|
||||
c.look_down = down(VK_DOWN);
|
||||
c.look_left = down(VK_LEFT);
|
||||
c.look_right = down(VK_RIGHT);
|
||||
c.wheel = (CineKeyPressed(VK_PRIOR, down(VK_PRIOR)) ? 1 : 0) -
|
||||
(CineKeyPressed(VK_NEXT, down(VK_NEXT)) ? 1 : 0) + p.speed_steps +
|
||||
cine_wheel.exchange(0);
|
||||
c.smooth_steps = (CineKeyPressed(VK_OEM_6, down(VK_OEM_6)) ? 1 : 0) -
|
||||
(CineKeyPressed(VK_OEM_4, down(VK_OEM_4)) ? 1 : 0) + p.smooth_steps;
|
||||
cine_pad.speed_steps = 0;
|
||||
cine_pad.smooth_steps = 0;
|
||||
c.pad_move_x = p.lx;
|
||||
c.pad_move_y = -p.ly; // stick up = smaller y = forward
|
||||
c.pad_rise = ((p.buttons & PadR1) ? 1.0f : 0.0f) - ((p.buttons & PadL1) ? 1.0f : 0.0f);
|
||||
c.pad_look_x = p.rx;
|
||||
c.pad_look_y = -p.ry;
|
||||
c.pad_zoom = p.r2 - p.l2;
|
||||
// Mouse look, no button needed: the cursor's movement, put back in the middle of the game
|
||||
// window each frame (hidden there by CineWindowProc).
|
||||
c.mouse_dx = 0.0f;
|
||||
c.mouse_dy = 0.0f;
|
||||
POINT now{};
|
||||
RECT client{};
|
||||
HWND window = front ? CineGameWindowInFront() : nullptr;
|
||||
if (window != nullptr && GetClientRect(window, &client) && GetCursorPos(&now)) {
|
||||
POINT center{(client.left + client.right) / 2, (client.top + client.bottom) / 2};
|
||||
ClientToScreen(window, ¢er);
|
||||
if (cine_looking) {
|
||||
c.mouse_dx = static_cast<float>(now.x - cine_anchor.x);
|
||||
c.mouse_dy = static_cast<float>(now.y - cine_anchor.y);
|
||||
}
|
||||
cine_anchor = center;
|
||||
cine_looking = true;
|
||||
SetCursorPos(center.x, center.y);
|
||||
cine_mouse_owned = true;
|
||||
} else {
|
||||
cine_looking = false; // another window is in front: the mouse is the user's
|
||||
cine_mouse_owned = false;
|
||||
}
|
||||
return c;
|
||||
}
|
||||
|
||||
void CinematicForceOff() {
|
||||
std::scoped_lock lock{cine_mutex};
|
||||
cine_state = CinematicInput{};
|
||||
cine_pad = CinePad{.last_buttons = cine_pad.last_buttons};
|
||||
cine_looking = false;
|
||||
cine_mouse_owned = false;
|
||||
}
|
||||
|
||||
void CinematicForceOn() {
|
||||
std::scoped_lock lock{cine_mutex};
|
||||
if (cine_state.enabled) {
|
||||
return;
|
||||
}
|
||||
cine_state = CinematicInput{};
|
||||
cine_state.enabled = true;
|
||||
cine_pad = CinePad{.last_buttons = cine_pad.last_buttons};
|
||||
cine_looking = false;
|
||||
}
|
||||
|
||||
} // namespace Bbsc
|
||||
} // namespace Core
|
||||
|
||||
namespace Common::FS {
|
||||
std::filesystem::path GetUserPath(PathType type) {
|
||||
return type == PathType::CapturesDir ? mod_dir / "captures" : mod_dir;
|
||||
}
|
||||
} // namespace Common::FS
|
||||
|
||||
namespace Libraries::Np::NpMatching2 {
|
||||
int BbscReplayRoomMembers(bool) {
|
||||
return 0; // the emulator's room cache is out of reach for a mod
|
||||
}
|
||||
} // namespace Libraries::Np::NpMatching2
|
||||
|
||||
BOOL APIENTRY DllMain(HMODULE module, DWORD reason, LPVOID) {
|
||||
if (reason == DLL_PROCESS_ATTACH) {
|
||||
self_module = module;
|
||||
DisableThreadLibraryCalls(module);
|
||||
if (HANDLE thread = CreateThread(nullptr, 0, Worker, nullptr, 0, nullptr)) {
|
||||
CloseHandle(thread);
|
||||
}
|
||||
}
|
||||
return TRUE;
|
||||
}
|
||||
@@ -1,469 +0,0 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2026 CLS_AMG
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
// BBSC mod: the new home of BBSC's own packets (host position / follow, world flags, Dream state).
|
||||
// In the emulator build they rode on the emulator's P2P signaling. A mod cannot reach that, so
|
||||
// they go through the BBSC game server instead, which every host already runs and every player's
|
||||
// game already talks to: one TCP connection to its relay port (20444), frames of
|
||||
// [u32 little-endian length][payload], and the server hands every frame to every other player
|
||||
// connected to it. The payloads are the signaling packets unchanged ("SHAD" + type 0x20 follow,
|
||||
// 0x22 world flags), so the receiving code is the same.
|
||||
//
|
||||
// No relay (public servers, or ours not reachable): the same frames go over the game's own P2P
|
||||
// connection instead (bbsc_channel.cpp), and come back into Dispatch from there.
|
||||
//
|
||||
// Server address: BBSC_RELAY=host[:port], else the game server entry of host_overrides.json
|
||||
// (portable install: <emulator>\user\, else %APPDATA%\shadPS4\) - the address JOIN-HOST wrote.
|
||||
|
||||
#ifdef _WIN32
|
||||
#include <winsock2.h>
|
||||
#include <ws2tcpip.h>
|
||||
#include <windows.h>
|
||||
#else
|
||||
#include "linux/bbsc_win32.h" // bbport
|
||||
#endif
|
||||
|
||||
#include <atomic>
|
||||
#include <cstring>
|
||||
#include <fstream>
|
||||
#include <mutex>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "common/types.h"
|
||||
#include "core/bloodborne_re.h"
|
||||
#include "core/libraries/np/np_signaling/np_signaling_bbsc.h"
|
||||
#include "core/libraries/np/np_signaling/np_signaling_helpers.h"
|
||||
|
||||
namespace BbscMod {
|
||||
void LogPlain(const char* text);
|
||||
void OnHostPlacement(const std::string& placement, u32 sender);
|
||||
bool IsPublicServerAddress(const std::string& url_or_host); // bbsc_http.cpp
|
||||
u32 P2PChannelSend(const void* data, u32 size); // bbsc_channel.cpp
|
||||
u32 P2PChannelPeerCount();
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr u16 DefaultRelayPort = 20444;
|
||||
constexpr u8 FollowType = 0x20;
|
||||
constexpr u8 PlacementType = 0x30; // "SHAD" 0x30 + a host's placement text (bbsc_http.cpp)
|
||||
constexpr u8 LifeType = 0x23; // "SHAD" 0x23 + u32 player, u8 dead, u32 sequence
|
||||
constexpr u8 ScaleType = 0x24; // "SHAD" 0x24 + 5 x u16 the host's scaling percentages
|
||||
constexpr u8 PvpType = 0x25; // "SHAD" 0x25 + u8 kind (0 host state, 1 guest asks) + u8 on + u32
|
||||
constexpr u8 ProgressType = 0x26; // "SHAD" 0x26 + u8 count + count x u32 the host's boss flags
|
||||
constexpr u8 HostBossType = 0x27; // "SHAD" 0x27 + s32 the boss on the host's boss bar (-1 none)
|
||||
constexpr u8 PeersType = 0x7F; // server -> client: "N other players are connected", 1 byte count
|
||||
// Every frame since 2026-10-06: "SHAD" 0x40 + u32 sender + u32 session (the host's sender id, 0 = in
|
||||
// no session) + the packet above. The server sends each frame to everyone connected to it, so the
|
||||
// receiver keeps only its own session's packets (see "Relay sessions" in bloodborne_re.cpp).
|
||||
// Frames without it come from an older BBSC and are dropped.
|
||||
constexpr u8 EnvelopeType = 0x40;
|
||||
constexpr size_t EnvelopeSize = 13;
|
||||
|
||||
#pragma pack(push, 1)
|
||||
struct FollowPacket {
|
||||
u8 magic[4] = {'S', 'H', 'A', 'D'};
|
||||
u8 type = FollowType;
|
||||
u8 kind = 0;
|
||||
u32 packed_map = 0;
|
||||
float x = 0.0f;
|
||||
float y = 0.0f;
|
||||
float z = 0.0f;
|
||||
float heading = 0.0f;
|
||||
u32 sequence = 0;
|
||||
s32 lamp_point = -1;
|
||||
};
|
||||
#pragma pack(pop)
|
||||
static_assert(sizeof(FollowPacket) == 0x22);
|
||||
|
||||
std::mutex send_mutex;
|
||||
std::mutex dispatch_mutex; // the relay thread and the P2P channel's thread both deliver frames
|
||||
SOCKET relay = INVALID_SOCKET;
|
||||
std::atomic<int> peers{0};
|
||||
std::atomic<bool> started{false};
|
||||
u32 own_id = 0; // set once in StartRelay, never 0
|
||||
std::atomic<bool> logged_old_sender{false};
|
||||
|
||||
void Log(const std::string& text) {
|
||||
BbscMod::LogPlain(("Relay: " + text).c_str());
|
||||
}
|
||||
|
||||
#ifdef _WIN32
|
||||
std::string ReadFile(const std::wstring& path) {
|
||||
#else
|
||||
std::string ReadFile(const std::string& path) {
|
||||
#endif
|
||||
std::ifstream in(path, std::ios::binary);
|
||||
std::stringstream text;
|
||||
text << in.rdbuf();
|
||||
return text.str();
|
||||
}
|
||||
|
||||
// "http://26.1.2.3:20443" as the value of the ss4.scej-network.jp key -> "26.1.2.3".
|
||||
std::string HostFromOverrides(const std::string& json) {
|
||||
const size_t key = json.find("ss4.scej-network.jp");
|
||||
if (key == std::string::npos) {
|
||||
return {};
|
||||
}
|
||||
const size_t colon = json.find(':', json.find('"', key + 1) + 1);
|
||||
const size_t open = json.find('"', colon);
|
||||
const size_t close = json.find('"', open + 1);
|
||||
if (colon == std::string::npos || open == std::string::npos || close == std::string::npos) {
|
||||
return {};
|
||||
}
|
||||
std::string url = json.substr(open + 1, close - open - 1);
|
||||
if (const size_t scheme = url.find("://"); scheme != std::string::npos) {
|
||||
url = url.substr(scheme + 3);
|
||||
}
|
||||
return url.substr(0, url.find_first_of(":/"));
|
||||
}
|
||||
|
||||
bool FindServer(std::string& host, u16& port) {
|
||||
port = DefaultRelayPort;
|
||||
if (const char* env = std::getenv("BBSC_RELAY"); env != nullptr && env[0] != '\0') {
|
||||
std::string value = env;
|
||||
if (const size_t colon = value.find(':'); colon != std::string::npos) {
|
||||
port = static_cast<u16>(std::atoi(value.c_str() + colon + 1));
|
||||
value.resize(colon);
|
||||
}
|
||||
host = value;
|
||||
return true;
|
||||
}
|
||||
#ifdef _WIN32
|
||||
wchar_t exe[MAX_PATH]{};
|
||||
GetModuleFileNameW(nullptr, exe, MAX_PATH);
|
||||
std::wstring dir{exe};
|
||||
dir = dir.substr(0, dir.find_last_of(L"\\/") + 1);
|
||||
// The emulator's own rule: a "user" folder in the folder it was started in wins (portable).
|
||||
wchar_t cwd[MAX_PATH]{};
|
||||
GetCurrentDirectoryW(MAX_PATH, cwd);
|
||||
std::vector<std::wstring> candidates{std::wstring(cwd) + L"\\user\\host_overrides.json",
|
||||
dir + L"user\\host_overrides.json"};
|
||||
if (const wchar_t* appdata = _wgetenv(L"APPDATA")) {
|
||||
candidates.push_back(std::wstring(appdata) + L"\\shadPS4\\host_overrides.json");
|
||||
}
|
||||
#else
|
||||
// bbport: the overrides file run.sh hands the online module (SHADPS4_HTTP_HOST_OVERRIDES_JSON,
|
||||
// scripts/online.py), else the ones in the user folder.
|
||||
const char* user = std::getenv("BB_USER_DIR");
|
||||
const std::string dir = std::string{user != nullptr && *user != '\0' ? user : "user"} + "/";
|
||||
std::vector<std::string> candidates{dir + "host_overrides.custom.json", dir + "host_overrides.json"};
|
||||
if (const char* given = std::getenv("SHADPS4_HTTP_HOST_OVERRIDES_JSON"); given && *given) {
|
||||
candidates.insert(candidates.begin(), given);
|
||||
}
|
||||
#endif
|
||||
for (const auto& path : candidates) {
|
||||
host = HostFromOverrides(ReadFile(path));
|
||||
if (!host.empty()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool ReadAll(SOCKET s, void* data, int size) {
|
||||
auto* at = static_cast<char*>(data);
|
||||
while (size > 0) {
|
||||
const int got = recv(s, at, size, 0);
|
||||
if (got <= 0) {
|
||||
return false;
|
||||
}
|
||||
at += got;
|
||||
size -= got;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Our session as the relay sees it: hosting -> ourselves, a guest (or joining) -> our host.
|
||||
u32 SessionKey() {
|
||||
const s32 state = Core::Bloodborne::GetBbscRelaySessionState();
|
||||
if (state == 3) {
|
||||
return own_id;
|
||||
}
|
||||
if (state == 4 || state == 6) {
|
||||
return Core::Bloodborne::GetBbscRelayHost();
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Returns how many players it went to: the relay's count, or (no relay connected - public servers)
|
||||
// the players the P2P channel sent it to (bbsc_channel.cpp).
|
||||
u32 SendFrame(const void* data, u32 size) {
|
||||
std::scoped_lock lock{send_mutex};
|
||||
const u32 key = SessionKey();
|
||||
const u32 wrapped = static_cast<u32>(EnvelopeSize) + size;
|
||||
std::vector<char> frame(4 + wrapped);
|
||||
std::memcpy(frame.data(), &wrapped, 4);
|
||||
std::memcpy(frame.data() + 4, "SHAD", 4);
|
||||
frame[8] = static_cast<char>(EnvelopeType);
|
||||
std::memcpy(frame.data() + 9, &own_id, 4);
|
||||
std::memcpy(frame.data() + 13, &key, 4);
|
||||
std::memcpy(frame.data() + 4 + EnvelopeSize, data, size);
|
||||
if (relay == INVALID_SOCKET) {
|
||||
return BbscMod::P2PChannelSend(frame.data() + 4, wrapped);
|
||||
}
|
||||
int left = static_cast<int>(frame.size());
|
||||
const char* at = frame.data();
|
||||
while (left > 0) {
|
||||
const int sent = send(relay, at, left, 0);
|
||||
if (sent <= 0) {
|
||||
closesocket(relay);
|
||||
relay = INVALID_SOCKET;
|
||||
return BbscMod::P2PChannelSend(frame.data() + 4, wrapped);
|
||||
}
|
||||
at += sent;
|
||||
left -= sent;
|
||||
}
|
||||
return static_cast<u32>(peers.load());
|
||||
}
|
||||
|
||||
void Dispatch(const std::vector<u8>& wrapped) {
|
||||
if (wrapped.size() == 2 && wrapped[0] == PeersType) {
|
||||
peers = wrapped[1];
|
||||
return;
|
||||
}
|
||||
if (wrapped.size() < 5 || std::memcmp(wrapped.data(), "SHAD", 4) != 0) {
|
||||
return;
|
||||
}
|
||||
if (wrapped[4] != EnvelopeType || wrapped.size() < EnvelopeSize + 5) {
|
||||
if (!logged_old_sender.exchange(true)) {
|
||||
Log("a player on this server runs an older BBSC (packets without a sender) - their "
|
||||
"packets are ignored until they update");
|
||||
}
|
||||
return;
|
||||
}
|
||||
u32 sender = 0;
|
||||
u32 session = 0;
|
||||
std::memcpy(&sender, wrapped.data() + 5, 4);
|
||||
std::memcpy(&session, wrapped.data() + 9, 4);
|
||||
const std::vector<u8> frame(wrapped.begin() + EnvelopeSize, wrapped.end());
|
||||
if (sender == own_id || std::memcmp(frame.data(), "SHAD", 4) != 0) {
|
||||
return;
|
||||
}
|
||||
const s32 state = Core::Bloodborne::GetBbscRelaySessionState();
|
||||
const bool guest = state == 4 || state == 6;
|
||||
const bool sender_hosting = session != 0 && session == sender;
|
||||
// Host-only packets are taken from our host alone.
|
||||
const auto from_our_host = [&] {
|
||||
return guest && sender_hosting && sender == Core::Bloodborne::GetBbscRelayHost();
|
||||
};
|
||||
if (frame[4] == FollowType && frame.size() >= 0x1E) {
|
||||
FollowPacket follow{};
|
||||
std::memcpy(&follow, frame.data(), std::min(frame.size(), sizeof(follow)));
|
||||
if (sender_hosting) {
|
||||
Core::Bloodborne::NoteBbscRelayHost(sender, follow.packed_map, follow.x, follow.y,
|
||||
follow.z);
|
||||
}
|
||||
if (from_our_host()) {
|
||||
Core::Bloodborne::OnBbscHostFollowPacket(
|
||||
follow.packed_map, follow.x, follow.y, follow.z, follow.heading, follow.sequence,
|
||||
frame.size() >= sizeof(follow) ? follow.lamp_point : -1);
|
||||
}
|
||||
} else if (frame[4] == LifeType && frame.size() >= 14) {
|
||||
// Only the players of our own session (same host).
|
||||
if (session == 0 || session != SessionKey()) {
|
||||
return;
|
||||
}
|
||||
u32 player = 0;
|
||||
u32 sequence = 0;
|
||||
std::memcpy(&player, frame.data() + 5, 4);
|
||||
std::memcpy(&sequence, frame.data() + 10, 4);
|
||||
Core::Bloodborne::OnBbscLifePacket(player, frame[9], sequence);
|
||||
} else if (frame[4] == ScaleType && frame.size() >= 15) {
|
||||
if (from_our_host()) {
|
||||
std::uint16_t percent[5]{};
|
||||
std::memcpy(percent, frame.data() + 5, sizeof(percent));
|
||||
Core::Bloodborne::OnBbscScalePacket(percent);
|
||||
}
|
||||
} else if (frame[4] == PvpType && frame.size() >= 11) {
|
||||
// Kind 0 is the host's state (for its guests), kind 1 a guest asking its host.
|
||||
const bool ours = frame[5] == 0 ? from_our_host() : state == 3 && session == own_id;
|
||||
if (ours) {
|
||||
Core::Bloodborne::OnBbscPvpPacket(frame[5], frame[6]);
|
||||
}
|
||||
} else if (frame[4] == ProgressType && frame.size() >= 6) {
|
||||
// The host's boss flags for shared progress: u8 count + count x u32.
|
||||
const size_t count = std::min<size_t>(frame[5], (frame.size() - 6) / 4);
|
||||
if (from_our_host() && count != 0) {
|
||||
std::vector<std::uint32_t> flags(count);
|
||||
std::memcpy(flags.data(), frame.data() + 6, count * 4);
|
||||
Core::Bloodborne::OnBbscProgressPacket(flags.data(), flags.size());
|
||||
}
|
||||
} else if (frame[4] == HostBossType && frame.size() >= 9) {
|
||||
if (from_our_host()) {
|
||||
std::int32_t boss = -1;
|
||||
std::memcpy(&boss, frame.data() + 5, 4);
|
||||
Core::Bloodborne::OnBbscHostBossPacket(boss);
|
||||
}
|
||||
} else if (frame[4] == PlacementType) {
|
||||
// A host looking for signs: for players not in a session yet, any host on the server.
|
||||
BbscMod::OnHostPlacement(std::string(frame.begin() + 5, frame.end()), sender);
|
||||
} else if (frame[4] == Libraries::Np::NpSignaling::kSignalingBbscWorldFlagsType) {
|
||||
if (from_our_host()) {
|
||||
Core::Bloodborne::OnBbscWorldFlagsPacket(frame.data(), static_cast<u32>(frame.size()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DWORD WINAPI RelayThread(void*) {
|
||||
WSADATA wsa{};
|
||||
WSAStartup(MAKEWORD(2, 2), &wsa);
|
||||
std::string last_problem;
|
||||
for (;;) {
|
||||
std::string host;
|
||||
u16 port = 0;
|
||||
if (!FindServer(host, port)) {
|
||||
if (last_problem != "no server") {
|
||||
last_problem = "no server";
|
||||
Log("no game server address (host_overrides.json / BBSC_RELAY) - retrying");
|
||||
}
|
||||
Sleep(10'000);
|
||||
continue;
|
||||
}
|
||||
if (BbscMod::IsPublicServerAddress(host)) {
|
||||
// The Hunter's Dream / NoDream run no BBSC relay: BBSC packets take the P2P channel.
|
||||
if (last_problem != "public") {
|
||||
last_problem = "public";
|
||||
Log("public game server (" + host + ") - no relay there; BBSC packets go over "
|
||||
"the P2P channel");
|
||||
}
|
||||
Sleep(30'000);
|
||||
continue;
|
||||
}
|
||||
addrinfo hints{};
|
||||
hints.ai_family = AF_INET;
|
||||
hints.ai_socktype = SOCK_STREAM;
|
||||
addrinfo* found = nullptr;
|
||||
SOCKET s = INVALID_SOCKET;
|
||||
if (getaddrinfo(host.c_str(), std::to_string(port).c_str(), &hints, &found) == 0) {
|
||||
s = socket(found->ai_family, found->ai_socktype, found->ai_protocol);
|
||||
if (s != INVALID_SOCKET && connect(s, found->ai_addr, static_cast<int>(found->ai_addrlen)) != 0) {
|
||||
closesocket(s);
|
||||
s = INVALID_SOCKET;
|
||||
}
|
||||
freeaddrinfo(found);
|
||||
}
|
||||
if (s == INVALID_SOCKET) {
|
||||
const std::string problem = "cannot reach " + host + ":" + std::to_string(port);
|
||||
if (problem != last_problem) {
|
||||
last_problem = problem;
|
||||
Log(problem + " (game server not started, or an older one without the relay) - "
|
||||
"retrying every 5 s");
|
||||
}
|
||||
Sleep(5'000);
|
||||
continue;
|
||||
}
|
||||
BOOL on = TRUE;
|
||||
setsockopt(s, IPPROTO_TCP, TCP_NODELAY, reinterpret_cast<const char*>(&on), sizeof(on));
|
||||
// Frames are sent from the game thread (review 2026-10-08): a stalled server must not
|
||||
// freeze the game in send() - after 300 ms the frame fails and the link is rebuilt.
|
||||
const DWORD send_timeout_ms = 300;
|
||||
setsockopt(s, SOL_SOCKET, SO_SNDTIMEO, reinterpret_cast<const char*>(&send_timeout_ms),
|
||||
sizeof(send_timeout_ms));
|
||||
{
|
||||
std::scoped_lock lock{send_mutex};
|
||||
relay = s;
|
||||
}
|
||||
last_problem.clear();
|
||||
Log("connected to " + host + ":" + std::to_string(port));
|
||||
for (;;) {
|
||||
u32 size = 0;
|
||||
if (!ReadAll(s, &size, 4) || size > 64 * 1024) {
|
||||
break;
|
||||
}
|
||||
std::vector<u8> frame(size);
|
||||
if (size != 0 && !ReadAll(s, frame.data(), static_cast<int>(size))) {
|
||||
break;
|
||||
}
|
||||
std::scoped_lock dispatch_lock{dispatch_mutex};
|
||||
Dispatch(frame);
|
||||
}
|
||||
{
|
||||
std::scoped_lock lock{send_mutex};
|
||||
if (relay == s) {
|
||||
closesocket(relay);
|
||||
relay = INVALID_SOCKET;
|
||||
}
|
||||
}
|
||||
peers = 0;
|
||||
Log("connection lost - reconnecting");
|
||||
Sleep(2'000);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
namespace BbscMod {
|
||||
// Public game server (The Hunter's Dream...): it works like Sony's, so BBSC must summon the vanilla
|
||||
// way (bbsc_mod.cpp Worker). No host_overrides.json found = not public (review 2026-10-11): a
|
||||
// self-host install whose file BBSC does not look for must keep the seamless summons, and without
|
||||
// the file the game reaches no server at all anyway.
|
||||
bool GameServerIsPublic(std::string* host_out) {
|
||||
std::string host;
|
||||
u16 port = 0;
|
||||
if (!FindServer(host, port)) {
|
||||
if (host_out != nullptr) {
|
||||
*host_out = "none found";
|
||||
}
|
||||
return false;
|
||||
}
|
||||
if (host_out != nullptr) {
|
||||
*host_out = host;
|
||||
}
|
||||
return BbscMod::IsPublicServerAddress(host);
|
||||
}
|
||||
|
||||
// Frames the P2P channel received (same format as the relay's, without the length).
|
||||
void RelayDispatch(const std::vector<u8>& wrapped) {
|
||||
std::scoped_lock lock{dispatch_mutex};
|
||||
Dispatch(wrapped);
|
||||
}
|
||||
|
||||
void RelaySendPlacement(const std::string& placement) {
|
||||
std::string frame = "SHAD";
|
||||
frame.push_back(static_cast<char>(PlacementType));
|
||||
frame += placement;
|
||||
SendFrame(frame.data(), static_cast<u32>(frame.size()));
|
||||
}
|
||||
|
||||
void StartRelay() {
|
||||
if (!started.exchange(true)) {
|
||||
#ifdef _WIN32
|
||||
LARGE_INTEGER counter{};
|
||||
QueryPerformanceCounter(&counter);
|
||||
const u64 ticks = static_cast<u64>(counter.QuadPart);
|
||||
#else
|
||||
timespec now{};
|
||||
clock_gettime(CLOCK_MONOTONIC, &now);
|
||||
const u64 ticks = static_cast<u64>(now.tv_sec) * 1'000'000'000ull + static_cast<u64>(now.tv_nsec);
|
||||
#endif
|
||||
own_id = static_cast<u32>((ticks ^ GetCurrentProcessId()) * 2654435761ull) | 1u;
|
||||
if (HANDLE thread = CreateThread(nullptr, 0, RelayThread, nullptr, 0, nullptr)) {
|
||||
CloseHandle(thread);
|
||||
}
|
||||
}
|
||||
}
|
||||
} // namespace BbscMod
|
||||
|
||||
namespace Libraries::Np::NpSignaling::Helpers {
|
||||
|
||||
u32 SendBbscFollowToPeers(u32 packed_map, float x, float y, float z, float heading, u32 sequence,
|
||||
s32 lamp_point) {
|
||||
FollowPacket packet{};
|
||||
packet.packed_map = packed_map;
|
||||
packet.x = x;
|
||||
packet.y = y;
|
||||
packet.z = z;
|
||||
packet.heading = heading;
|
||||
packet.sequence = sequence;
|
||||
packet.lamp_point = lamp_point;
|
||||
return SendFrame(&packet, sizeof(packet));
|
||||
}
|
||||
|
||||
u32 SendBbscWorldFlagsToPeers(const void* packet, u32 size) {
|
||||
return SendFrame(packet, size);
|
||||
}
|
||||
|
||||
} // namespace Libraries::Np::NpSignaling::Helpers
|
||||
@@ -1,223 +0,0 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2026 CLS_AMG
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
// bbport: the Linux counterpart of emulator/mod/bbsc_hooks.cpp - Core::InstallGuestCodeHook for
|
||||
// libbbsc.so. The trampoline is the mod's, unchanged: save every register and YMM into a
|
||||
// GuestRegisterSnapshot, call the C++ hook with it, restore the (possibly changed) snapshot, run the
|
||||
// copied instructions, jump back. The trampolines live in 1 MB of our own, mapped within a rel32
|
||||
// jump's reach below the game's image (bbport has no emulator trampoline area after it; the image
|
||||
// grows upwards, and bbport's own hooks take pages below it the same way).
|
||||
|
||||
#include <sys/mman.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <cstring>
|
||||
#include <mutex>
|
||||
|
||||
#include <xbyak/xbyak.h>
|
||||
|
||||
#include "core/cpu_patches.h"
|
||||
|
||||
namespace BbscMod {
|
||||
void LogPlain(const char* text);
|
||||
u64 EbootAddress();
|
||||
}
|
||||
|
||||
namespace Core {
|
||||
namespace {
|
||||
|
||||
using namespace Xbyak::util;
|
||||
|
||||
constexpr u64 OurSize = 1ULL << 20;
|
||||
|
||||
std::mutex hook_mutex;
|
||||
Xbyak::CodeGenerator* generator = nullptr;
|
||||
bool area_checked = false;
|
||||
bool area_ok = false;
|
||||
|
||||
// 1 MB below the image, within jump reach of all of it, never over anything already mapped.
|
||||
u64 MapOwnArea(u64 image) {
|
||||
for (u64 k = 1; k <= 256; ++k) {
|
||||
const u64 hint = image - k * (8ULL << 20);
|
||||
void* got = mmap(reinterpret_cast<void*>(hint), OurSize, PROT_READ | PROT_WRITE | PROT_EXEC,
|
||||
MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED_NOREPLACE, -1, 0);
|
||||
if (got != MAP_FAILED) {
|
||||
return reinterpret_cast<u64>(got);
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
bool CheckArea() {
|
||||
if (area_checked) {
|
||||
return area_ok;
|
||||
}
|
||||
area_checked = true;
|
||||
const u64 image = BbscMod::EbootAddress();
|
||||
if (image == 0) {
|
||||
BbscMod::LogPlain("Hook area: the game image was not found - no hooks");
|
||||
return false;
|
||||
}
|
||||
const u64 start = MapOwnArea(image);
|
||||
if (start == 0) {
|
||||
BbscMod::LogPlain("Hook area: no free memory within reach of the game image - no hooks");
|
||||
return false;
|
||||
}
|
||||
generator = new Xbyak::CodeGenerator(OurSize, reinterpret_cast<void*>(start));
|
||||
area_ok = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
// The 5-byte jump (and NOPs to the end of the covered instructions): all but the first byte, then
|
||||
// the first, so a thread meeting the site decodes either the old instruction start or the jump.
|
||||
bool WriteJump(u8* site, const void* target, size_t length) {
|
||||
const auto page = static_cast<u64>(sysconf(_SC_PAGESIZE));
|
||||
void* first = reinterpret_cast<void*>(reinterpret_cast<u64>(site) & ~(page - 1));
|
||||
const size_t span = reinterpret_cast<u64>(site) + length - reinterpret_cast<u64>(first);
|
||||
if (mprotect(first, span, PROT_READ | PROT_WRITE | PROT_EXEC) != 0) {
|
||||
return false;
|
||||
}
|
||||
std::array<u8, 32> bytes{};
|
||||
const s64 rel = reinterpret_cast<s64>(target) - reinterpret_cast<s64>(site + 5);
|
||||
bytes[0] = 0xE9;
|
||||
const s32 rel32 = static_cast<s32>(rel);
|
||||
std::memcpy(bytes.data() + 1, &rel32, 4);
|
||||
for (size_t i = 5; i < length; ++i) {
|
||||
bytes[i] = 0x90;
|
||||
}
|
||||
for (size_t i = 1; i < length; ++i) {
|
||||
__atomic_store_n(site + i, bytes[i], __ATOMIC_SEQ_CST);
|
||||
}
|
||||
__atomic_store_n(site, bytes[0], __ATOMIC_SEQ_CST);
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool InstallGuestCodeHook(void* address, std::span<const u8> expected_instructions, u64 tag,
|
||||
GuestCodeHook hook, const u8* skip_flag) {
|
||||
constexpr size_t NearJumpBytes = 5;
|
||||
constexpr size_t GuestRedZoneBytes = 128;
|
||||
constexpr size_t SnapshotOffset = 0;
|
||||
constexpr size_t ScratchR11Offset = sizeof(GuestRegisterSnapshot);
|
||||
constexpr size_t ScratchRflagsOffset = ScratchR11Offset + sizeof(u64);
|
||||
constexpr size_t FrameReserve =
|
||||
((ScratchRflagsOffset + sizeof(u64) + GuestRedZoneBytes) + 15) & ~size_t{15};
|
||||
|
||||
auto* code = static_cast<u8*>(address);
|
||||
if (hook == nullptr || expected_instructions.size() < NearJumpBytes ||
|
||||
expected_instructions.size() > 32) {
|
||||
return false;
|
||||
}
|
||||
std::scoped_lock lock{hook_mutex};
|
||||
if (!CheckArea()) {
|
||||
return false;
|
||||
}
|
||||
if (std::memcmp(expected_instructions.data(), code, expected_instructions.size()) != 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
auto& trampoline = *generator;
|
||||
const size_t trampoline_offset = trampoline.getSize();
|
||||
const auto* trampoline_start = trampoline.getCurr();
|
||||
const auto snapshot = [](size_t field_offset) {
|
||||
return qword[rsp + SnapshotOffset + field_offset];
|
||||
};
|
||||
|
||||
try {
|
||||
trampoline.lea(rsp, ptr[rsp - FrameReserve]);
|
||||
trampoline.mov(qword[rsp + ScratchR11Offset], r11);
|
||||
trampoline.pushfq();
|
||||
trampoline.pop(r11);
|
||||
trampoline.mov(qword[rsp + ScratchRflagsOffset], r11);
|
||||
trampoline.mov(r11, rsp);
|
||||
trampoline.and_(rsp, -16);
|
||||
|
||||
trampoline.mov(snapshot(offsetof(GuestRegisterSnapshot, rax)), rax);
|
||||
trampoline.mov(snapshot(offsetof(GuestRegisterSnapshot, rbx)), rbx);
|
||||
trampoline.mov(snapshot(offsetof(GuestRegisterSnapshot, rcx)), rcx);
|
||||
trampoline.mov(snapshot(offsetof(GuestRegisterSnapshot, rdx)), rdx);
|
||||
trampoline.mov(snapshot(offsetof(GuestRegisterSnapshot, rsi)), rsi);
|
||||
trampoline.mov(snapshot(offsetof(GuestRegisterSnapshot, rdi)), rdi);
|
||||
trampoline.mov(snapshot(offsetof(GuestRegisterSnapshot, rbp)), rbp);
|
||||
trampoline.mov(snapshot(offsetof(GuestRegisterSnapshot, r8)), r8);
|
||||
trampoline.mov(snapshot(offsetof(GuestRegisterSnapshot, r9)), r9);
|
||||
trampoline.mov(snapshot(offsetof(GuestRegisterSnapshot, r10)), r10);
|
||||
trampoline.mov(snapshot(offsetof(GuestRegisterSnapshot, r12)), r12);
|
||||
trampoline.mov(snapshot(offsetof(GuestRegisterSnapshot, r13)), r13);
|
||||
trampoline.mov(snapshot(offsetof(GuestRegisterSnapshot, r14)), r14);
|
||||
trampoline.mov(snapshot(offsetof(GuestRegisterSnapshot, r15)), r15);
|
||||
trampoline.mov(rax, qword[r11 + ScratchR11Offset]);
|
||||
trampoline.mov(snapshot(offsetof(GuestRegisterSnapshot, r11)), rax);
|
||||
trampoline.mov(rax, qword[r11 + ScratchRflagsOffset]);
|
||||
trampoline.mov(snapshot(offsetof(GuestRegisterSnapshot, rflags)), rax);
|
||||
trampoline.lea(rax, ptr[r11 + FrameReserve]);
|
||||
trampoline.mov(snapshot(offsetof(GuestRegisterSnapshot, rsp)), rax);
|
||||
trampoline.stmxcsr(dword[rsp + SnapshotOffset + offsetof(GuestRegisterSnapshot, mxcsr)]);
|
||||
for (size_t index = 0; index < 16; ++index) {
|
||||
const size_t offset = SnapshotOffset + offsetof(GuestRegisterSnapshot, ymm) +
|
||||
index * sizeof(std::array<u8, 32>);
|
||||
trampoline.vmovdqu(yword[rsp + offset], Xbyak::Ymm(static_cast<int>(index)));
|
||||
}
|
||||
|
||||
// The hook is Windows code: 32 bytes of shadow space and the MS x64 arguments are not
|
||||
// what a PS4_SYSV_ABI function expects, so it is called the SysV way (rdi, rsi), exactly
|
||||
// as the emulator does. A 32-byte gap below the snapshot keeps either convention safe.
|
||||
trampoline.mov(r12, rsp);
|
||||
trampoline.sub(rsp, 32);
|
||||
trampoline.mov(rdi, tag);
|
||||
trampoline.lea(rsi, ptr[r12 + SnapshotOffset]);
|
||||
trampoline.mov(rax, reinterpret_cast<u64>(hook));
|
||||
trampoline.call(rax);
|
||||
trampoline.mov(rsp, r12);
|
||||
|
||||
for (size_t index = 0; index < 16; ++index) {
|
||||
const size_t offset = SnapshotOffset + offsetof(GuestRegisterSnapshot, ymm) +
|
||||
index * sizeof(std::array<u8, 32>);
|
||||
trampoline.vmovdqu(Xbyak::Ymm(static_cast<int>(index)), yword[rsp + offset]);
|
||||
}
|
||||
trampoline.ldmxcsr(dword[rsp + SnapshotOffset + offsetof(GuestRegisterSnapshot, mxcsr)]);
|
||||
trampoline.mov(rbx, snapshot(offsetof(GuestRegisterSnapshot, rbx)));
|
||||
trampoline.mov(rcx, snapshot(offsetof(GuestRegisterSnapshot, rcx)));
|
||||
trampoline.mov(rdx, snapshot(offsetof(GuestRegisterSnapshot, rdx)));
|
||||
trampoline.mov(rsi, snapshot(offsetof(GuestRegisterSnapshot, rsi)));
|
||||
trampoline.mov(rdi, snapshot(offsetof(GuestRegisterSnapshot, rdi)));
|
||||
trampoline.mov(rbp, snapshot(offsetof(GuestRegisterSnapshot, rbp)));
|
||||
trampoline.mov(r8, snapshot(offsetof(GuestRegisterSnapshot, r8)));
|
||||
trampoline.mov(r9, snapshot(offsetof(GuestRegisterSnapshot, r9)));
|
||||
trampoline.mov(r10, snapshot(offsetof(GuestRegisterSnapshot, r10)));
|
||||
trampoline.mov(r11, snapshot(offsetof(GuestRegisterSnapshot, r11)));
|
||||
trampoline.mov(r12, snapshot(offsetof(GuestRegisterSnapshot, r12)));
|
||||
trampoline.mov(r13, snapshot(offsetof(GuestRegisterSnapshot, r13)));
|
||||
trampoline.mov(r14, snapshot(offsetof(GuestRegisterSnapshot, r14)));
|
||||
trampoline.mov(r15, snapshot(offsetof(GuestRegisterSnapshot, r15)));
|
||||
trampoline.mov(rax, snapshot(offsetof(GuestRegisterSnapshot, rflags)));
|
||||
trampoline.push(rax);
|
||||
trampoline.popfq();
|
||||
trampoline.mov(rax, snapshot(offsetof(GuestRegisterSnapshot, rax)));
|
||||
trampoline.mov(rsp, snapshot(offsetof(GuestRegisterSnapshot, rsp)));
|
||||
Xbyak::Label run_original;
|
||||
if (skip_flag != nullptr) {
|
||||
// Function entry only: a plain ret skips the whole function (see cpu_patches.h).
|
||||
trampoline.push(rax);
|
||||
trampoline.mov(rax, reinterpret_cast<u64>(skip_flag));
|
||||
trampoline.cmp(byte[rax], 0);
|
||||
trampoline.mov(byte[rax], 0);
|
||||
trampoline.pop(rax);
|
||||
trampoline.je(run_original, Xbyak::CodeGenerator::LabelType::T_NEAR);
|
||||
trampoline.ret();
|
||||
}
|
||||
trampoline.L(run_original);
|
||||
trampoline.db(expected_instructions.data(), expected_instructions.size());
|
||||
trampoline.jmp(code + expected_instructions.size(), Xbyak::CodeGenerator::LabelType::T_NEAR);
|
||||
} catch (const Xbyak::Error& error) {
|
||||
trampoline.setSize(trampoline_offset);
|
||||
BbscMod::LogPlain(error.what());
|
||||
return false;
|
||||
}
|
||||
return WriteJump(code, trampoline_start, expected_instructions.size());
|
||||
}
|
||||
|
||||
} // namespace Core
|
||||
@@ -1,585 +0,0 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2026 CLS_AMG
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
// bbport: the Linux counterpart of emulator/mod/bbsc_mod.cpp - BBSC as a library (libbbsc.so)
|
||||
// that bb-probe loads into the game's process (BB_BBSC_LIB) instead of a DLL injected into
|
||||
// shadPS4. The steps are the DLL's, and the patch site logic below is its code unchanged:
|
||||
// 1. wait until every byte patch site reads back as the bytes that must be there (all of them
|
||||
// before writing any - another game or version is left alone);
|
||||
// 2. write the byte patches (core/bbsc_patch_tables.h);
|
||||
// 3. install every hook with core/bloodborne_re.cpp, compiled into this library with BBSC_MOD;
|
||||
// 4. keep watching: a patch site written back is written again.
|
||||
// What differs on Linux: the game is always at 0x800000000 (bbport, as the shadp2p fork); memory
|
||||
// questions are answered from bbport's own map of the game's memory (runtime_memory_vma_info, no
|
||||
// system call); the log goes to <user dir>/bbsc/logs/ and its warnings to the game's log; the F1 /
|
||||
// L3+R3 cinematic camera and BBSC's own Discord status are not in this build (bbport has its own
|
||||
// Discord status, and L3+R3 opens bbport's menu).
|
||||
|
||||
#include <dlfcn.h>
|
||||
#include <sys/mman.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <atomic>
|
||||
#include <cstdarg>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <ctime>
|
||||
#include <filesystem>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "linux/bbsc_win32.h"
|
||||
|
||||
#include "common/logging/log.h"
|
||||
#include "common/memory_patcher.h"
|
||||
#include "common/path_util.h"
|
||||
#include "core/bbsc_cinematic.h"
|
||||
#include "core/bbsc_patch_tables.h"
|
||||
#include "core/bloodborne_re.h"
|
||||
#include "core/debugger.h"
|
||||
#include "core/libraries/np/np_matching2/np_matching2_mm.h"
|
||||
#include "core/memory.h"
|
||||
|
||||
// bbport's runtime (bb-probe exports it): the game's memory map.
|
||||
extern "C" int runtime_memory_vma_info(uintptr_t address, int* prot, int* type, uintptr_t* end);
|
||||
|
||||
using namespace Core::Bbsc::PatchTables;
|
||||
|
||||
// Keep it equal to "version" in mod.json and to the Windows mod it plays with.
|
||||
#define BBSC_VERSION "0.0.1"
|
||||
|
||||
namespace BbscMod {
|
||||
void LogPlain(const char* text);
|
||||
u64 EbootAddress();
|
||||
void StartRelay();
|
||||
void StartP2PChannel(); // bbsc_channel.cpp
|
||||
bool GameServerIsPublic(std::string* host_out); // bbsc_relay.cpp
|
||||
void InstallHttpWrappers();
|
||||
} // namespace BbscMod
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr u64 ForkEbootBase = 0x800000000ULL; // bbport and the shadp2p fork
|
||||
u64 EbootBase = 0;
|
||||
constexpr u64 EbootSize = 0x056D4000ULL; // CUSA03173 01.09 image size
|
||||
// The image's code (its first PT_LOAD): made writable as shadPS4 maps it, since the shared hook
|
||||
// code writes some code bytes directly.
|
||||
constexpr u64 EbootCodeSize = 0x050DC000ULL;
|
||||
constexpr DWORD WaitForImageMs = 180'000;
|
||||
constexpr DWORD SettleMs = 20;
|
||||
constexpr DWORD WatchMs = 500;
|
||||
|
||||
std::filesystem::path mod_dir; // where libbbsc.so and its LICENSE/NOTICE/SUPPORTERS are
|
||||
std::filesystem::path data_dir; // <user dir>/bbsc: logs, captures
|
||||
FILE* log_file = nullptr;
|
||||
std::mutex log_mutex;
|
||||
|
||||
// The newest `keep` files of a folder with this extension stay; older ones (earlier game starts)
|
||||
// go. Each start writes a log and a capture of its own.
|
||||
void KeepNewest(const std::filesystem::path& folder, const char* extension, size_t keep) {
|
||||
std::error_code ignored;
|
||||
std::vector<std::filesystem::directory_entry> files;
|
||||
for (const auto& entry : std::filesystem::directory_iterator(folder, ignored)) {
|
||||
if (entry.is_regular_file(ignored) && entry.path().extension() == extension) {
|
||||
files.push_back(entry);
|
||||
}
|
||||
}
|
||||
if (files.size() <= keep) {
|
||||
return;
|
||||
}
|
||||
std::sort(files.begin(), files.end(), [](const auto& a, const auto& b) {
|
||||
std::error_code e;
|
||||
return a.last_write_time(e) > b.last_write_time(e);
|
||||
});
|
||||
for (size_t i = keep; i < files.size(); ++i) {
|
||||
std::filesystem::remove(files[i].path(), ignored);
|
||||
}
|
||||
}
|
||||
|
||||
void OpenLog() {
|
||||
Dl_info info{};
|
||||
if (dladdr(reinterpret_cast<void*>(&OpenLog), &info) != 0 && info.dli_fname != nullptr) {
|
||||
mod_dir = std::filesystem::path{info.dli_fname}.parent_path();
|
||||
}
|
||||
// The game's user folder (run.sh exports it as BB_GPU_USER_DIR). Not the working directory's
|
||||
// "user": in the AppImage that is inside its read-only mount, and BBSC installs none of its
|
||||
// hooks without a capture folder (InstallReverseEngineeringTrace) - with its death patch
|
||||
// still in, a death then never reloads. The temporary folder when the user folder is not
|
||||
// writable either.
|
||||
const char* user = std::getenv("BB_USER_DIR");
|
||||
if (user == nullptr || *user == '\0') {
|
||||
user = std::getenv("BB_GPU_USER_DIR");
|
||||
}
|
||||
const std::filesystem::path user_dir{user != nullptr && *user != '\0' ? user : "user"};
|
||||
data_dir = user_dir / "bbsc";
|
||||
std::error_code ignored;
|
||||
// BBSC looks for its saves in shadPS4's layout under the US id (user/home/<user>/savedata/
|
||||
// CUSA00207/SPRJ0005); bbport keeps them in user/savedata/<user>/<the game's id>/SPRJ0005 (EU
|
||||
// CUSA00900 and the others). Without them its kit is never given (it backs the saves up first)
|
||||
// and the own-world, loot and progress features have no save slot: point BBSC_SAVE_DIR, BBSC's
|
||||
// own setting for a save folder elsewhere, at the most recently written one.
|
||||
if (const char* set = std::getenv("BBSC_SAVE_DIR"); set == nullptr || *set == '\0') {
|
||||
std::filesystem::path newest;
|
||||
std::filesystem::file_time_type newest_time{};
|
||||
try { // a directory walk's ++ throws even with an error_code
|
||||
for (const auto& owner : std::filesystem::directory_iterator(user_dir / "savedata", ignored)) {
|
||||
for (const auto& title : std::filesystem::directory_iterator(owner.path(), ignored)) {
|
||||
const auto dir = title.path() / "SPRJ0005";
|
||||
const auto written = std::filesystem::last_write_time(dir, ignored);
|
||||
if (!ignored && (newest.empty() || written > newest_time)) {
|
||||
newest = dir;
|
||||
newest_time = written;
|
||||
}
|
||||
ignored.clear();
|
||||
}
|
||||
ignored.clear();
|
||||
}
|
||||
} catch (const std::exception&) {
|
||||
}
|
||||
ignored.clear();
|
||||
if (!newest.empty()) {
|
||||
setenv("BBSC_SAVE_DIR", newest.c_str(), 1);
|
||||
}
|
||||
}
|
||||
std::filesystem::create_directories(data_dir / "captures" / "bloodborne-re", ignored);
|
||||
if (ignored) {
|
||||
data_dir = std::filesystem::temp_directory_path(ignored) / "bbport-bbsc";
|
||||
std::filesystem::create_directories(data_dir / "captures" / "bloodborne-re", ignored);
|
||||
}
|
||||
std::filesystem::create_directories(data_dir / "logs", ignored);
|
||||
KeepNewest(data_dir / "logs", ".log", 4); // with this one, 5
|
||||
KeepNewest(data_dir / "captures" / "bloodborne-re", ".jsonl", 4);
|
||||
const std::time_t now = std::time(nullptr);
|
||||
std::tm local{};
|
||||
localtime_r(&now, &local);
|
||||
char name[96];
|
||||
std::snprintf(name, sizeof(name), "bbsc-mod-%04d%02d%02d-%02d%02d%02d-%d.log",
|
||||
local.tm_year + 1900, local.tm_mon + 1, local.tm_mday, local.tm_hour,
|
||||
local.tm_min, local.tm_sec, static_cast<int>(getpid()));
|
||||
log_file = std::fopen((data_dir / "logs" / name).c_str(), "w");
|
||||
std::printf("BBSC: log %s\n", (data_dir / "logs" / name).c_str());
|
||||
}
|
||||
|
||||
// ---- Memory --------------------------------------------------------------------------------------
|
||||
|
||||
// The game's own memory is in bbport's map; anything else (the image, host memory a game pointer
|
||||
// should never reach) is checked with mincore, which fails on an unmapped page.
|
||||
bool HostMapped(u64 address, size_t size) {
|
||||
const u64 page = static_cast<u64>(sysconf(_SC_PAGESIZE));
|
||||
const u64 first = address & ~(page - 1);
|
||||
const u64 last = (address + size - 1) & ~(page - 1);
|
||||
std::vector<unsigned char> pages((last - first) / page + 1);
|
||||
return mincore(reinterpret_cast<void*>(first), last - first + page, pages.data()) == 0;
|
||||
}
|
||||
|
||||
bool Readable(u64 address, size_t size) {
|
||||
if (address == 0 || address + size < address) {
|
||||
return false;
|
||||
}
|
||||
if (EbootBase != 0 && address >= EbootBase && address + size <= EbootBase + EbootSize) {
|
||||
return true;
|
||||
}
|
||||
u64 at = address;
|
||||
const u64 end = address + size;
|
||||
while (at < end) {
|
||||
int prot = 0, type = 0;
|
||||
uintptr_t region_end = 0;
|
||||
if (!runtime_memory_vma_info(at, &prot, &type, ®ion_end)) {
|
||||
return HostMapped(at, end - at);
|
||||
}
|
||||
if ((prot & 1) == 0) { // PS4 CPU read
|
||||
return false;
|
||||
}
|
||||
at = region_end;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool MatchesAt(u64 base, u64 offset, const u8* bytes, size_t size) {
|
||||
const u64 address = base + offset;
|
||||
return Readable(address, size) &&
|
||||
std::memcmp(reinterpret_cast<const void*>(address), bytes, size) == 0;
|
||||
}
|
||||
|
||||
bool Matches(u64 offset, const u8* bytes, size_t size) {
|
||||
const u64 address = EbootBase + offset;
|
||||
return Readable(address, size) &&
|
||||
std::memcmp(reinterpret_cast<const void*>(address), bytes, size) == 0;
|
||||
}
|
||||
|
||||
void Write(u64 offset, const u8* bytes, size_t size) {
|
||||
void* address = reinterpret_cast<void*>(EbootBase + offset);
|
||||
DWORD old = 0;
|
||||
if (!VirtualProtect(address, size, PAGE_EXECUTE_READWRITE, &old)) {
|
||||
return;
|
||||
}
|
||||
std::memcpy(address, bytes, size);
|
||||
}
|
||||
|
||||
// ---- Byte patches (bbsc_mod.cpp, unchanged) ------------------------------------------------------
|
||||
|
||||
struct Site {
|
||||
const char* name;
|
||||
u64 offset;
|
||||
std::array<u8, 6> original{};
|
||||
std::array<u8, 6> patched{};
|
||||
size_t size;
|
||||
Group group;
|
||||
};
|
||||
|
||||
template <size_t N>
|
||||
std::array<u8, 6> Pad(const std::array<u8, N>& bytes) {
|
||||
std::array<u8, 6> out{};
|
||||
std::memcpy(out.data(), bytes.data(), N);
|
||||
return out;
|
||||
}
|
||||
|
||||
constexpr size_t SiteCount =
|
||||
Retargets.size() + EntryReturns.size() + ByteEdits.size() + WideEdits.size() + EntryJumps.size();
|
||||
std::array<Site, SiteCount> sites{};
|
||||
std::mutex patch_mutex;
|
||||
bool patches_ready = false;
|
||||
bool session_on = true; // session groups: off while a suspension runs
|
||||
bool role_on = false; // role groups (lamp): on while hosting
|
||||
|
||||
void BuildSites() {
|
||||
size_t i = 0;
|
||||
for (const auto& e : Retargets) {
|
||||
sites[i++] = {e.name, e.site, Pad(EncodeRel32(e.opcode, e.site, e.original)),
|
||||
Pad(EncodeRel32(e.opcode, e.site, e.target)), 5, e.group};
|
||||
}
|
||||
for (const auto& e : EntryReturns) {
|
||||
std::array<u8, 6> ret{};
|
||||
ret[0] = 0xC3;
|
||||
sites[i++] = {e.name, e.site, e.prologue, ret, 1, e.group};
|
||||
}
|
||||
for (const auto& e : ByteEdits) {
|
||||
sites[i++] = {e.name, e.site, Pad(e.original), Pad(e.replacement), 2, e.group};
|
||||
}
|
||||
for (const auto& e : WideEdits) {
|
||||
sites[i++] = {e.name, e.site, Pad(e.original), Pad(e.replacement), 4, e.group};
|
||||
}
|
||||
for (const auto& e : EntryJumps) {
|
||||
sites[i++] = {e.name, e.site, Pad(e.prologue), Pad(EncodeRel32(0xE9, e.site, e.target)), 5,
|
||||
e.group};
|
||||
}
|
||||
}
|
||||
|
||||
bool AllSitesKnown(const Site** first_bad) {
|
||||
for (const Site& s : sites) {
|
||||
if (!GroupEnabled(s.group)) {
|
||||
continue;
|
||||
}
|
||||
if (s.offset + s.size > EbootSize ||
|
||||
(!Matches(s.offset, s.original.data(), s.size) &&
|
||||
!Matches(s.offset, s.patched.data(), s.size))) {
|
||||
*first_bad = &s;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Wanted(const Site& s) {
|
||||
if (!GroupEnabled(s.group)) {
|
||||
return false;
|
||||
}
|
||||
if (IsRoleGroup(s.group)) {
|
||||
return role_on;
|
||||
}
|
||||
if (IsSessionGroup(s.group)) {
|
||||
return session_on;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
int SyncSites() {
|
||||
int count = 0;
|
||||
for (const Site& s : sites) {
|
||||
if (!GroupEnabled(s.group)) {
|
||||
continue;
|
||||
}
|
||||
const auto& want = Wanted(s) ? s.patched : s.original;
|
||||
if (!Matches(s.offset, want.data(), s.size)) {
|
||||
Write(s.offset, want.data(), s.size);
|
||||
++count;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
// ---- Start-up ------------------------------------------------------------------------------------
|
||||
|
||||
// BBSC's license, notice and supporters travel with it (next to libbbsc.so); missing or changed,
|
||||
// BBSC stays off and says why, as the Windows mod does.
|
||||
bool NoticesPresent() {
|
||||
const auto holds = [](const char* name, const char* text) {
|
||||
FILE* file = std::fopen((mod_dir / name).c_str(), "rb");
|
||||
if (file == nullptr) {
|
||||
return false;
|
||||
}
|
||||
std::string content(64 * 1024, '\0');
|
||||
content.resize(std::fread(content.data(), 1, content.size(), file));
|
||||
std::fclose(file);
|
||||
return content.find(text) != std::string::npos;
|
||||
};
|
||||
return holds("LICENSE.txt", "GNU GENERAL PUBLIC LICENSE") &&
|
||||
holds("NOTICE.txt", "github.com/Classmg22/BBSC") &&
|
||||
holds("SUPPORTERS.txt", "BBSC SUPPORTERS");
|
||||
}
|
||||
|
||||
DWORD WINAPI Worker(void*) {
|
||||
OpenLog();
|
||||
BbscMod::LogPlain("BBSC mod loaded - version " BBSC_VERSION " (bbport, Linux)");
|
||||
if (!NoticesPresent()) {
|
||||
LOG_ERROR(Mod, "LICENSE.txt / NOTICE.txt / SUPPORTERS.txt missing or changed in {} - BBSC "
|
||||
"is off",
|
||||
mod_dir.string());
|
||||
return 0;
|
||||
}
|
||||
LOG_INFO(Mod, "BBSC mod in pid {}", static_cast<int>(getpid()));
|
||||
if (const char* saves = std::getenv("BBSC_SAVE_DIR"); saves != nullptr && *saves != '\0') {
|
||||
LOG_INFO(Mod, "Saves: {}", saves);
|
||||
} else {
|
||||
LOG_WARNING(Mod, "Saves: none found - no co-op kit, own world or shared progress");
|
||||
}
|
||||
{
|
||||
std::string server;
|
||||
if (BbscMod::GameServerIsPublic(&server)) {
|
||||
setenv("BBSC_MOD_SEAMLESS", "0", 1);
|
||||
setenv("SHADPS4_BBSC_FIVE_HUNTERS", "0", 1);
|
||||
LOG_INFO(Mod, "Public game server ({}): vanilla summons - seamless summon patches and the "
|
||||
"5-hunter search off (the session itself stays seamless)",
|
||||
server);
|
||||
}
|
||||
}
|
||||
BuildSites();
|
||||
|
||||
// bbport loads the game at the fork's address before any of its code runs.
|
||||
EbootBase = ForkEbootBase;
|
||||
const DWORD started = GetTickCount();
|
||||
if (mprotect(reinterpret_cast<void*>(EbootBase), EbootCodeSize,
|
||||
PROT_READ | PROT_WRITE | PROT_EXEC) == 0) {
|
||||
LOG_INFO(Mod, "Game code made writable, as shadPS4 maps it");
|
||||
}
|
||||
const Site* bad = nullptr;
|
||||
while (!AllSitesKnown(&bad)) {
|
||||
if (GetTickCount() - started > WaitForImageMs) {
|
||||
LOG_ERROR(Mod,
|
||||
"No Bloodborne CUSA03173 01.09 image after {} s (first unknown site: {} at "
|
||||
"{:#x}) - not this game or not this version; nothing was changed",
|
||||
WaitForImageMs / 1000, bad ? bad->name : "?", bad ? bad->offset : 0);
|
||||
return 0;
|
||||
}
|
||||
Sleep(1);
|
||||
}
|
||||
Sleep(SettleMs);
|
||||
const DWORD verified = GetTickCount() - started;
|
||||
|
||||
MemoryPatcher::g_eboot_address = EbootBase;
|
||||
MemoryPatcher::g_eboot_image_size = EbootSize;
|
||||
MemoryPatcher::g_game_serial = "CUSA03173";
|
||||
|
||||
// Wozzardman's seamless patches first: one of them (0x019471B1) is also in BBSC's list, and it
|
||||
// checks for the original bytes.
|
||||
Core::Bloodborne::InstallSeamlessCoopPatches();
|
||||
|
||||
int written = 0;
|
||||
{
|
||||
std::scoped_lock lock{patch_mutex};
|
||||
patches_ready = true;
|
||||
written = SyncSites();
|
||||
}
|
||||
LOG_INFO(Mod, "Image verified after {} ms: {} byte patch sites written", verified, written);
|
||||
|
||||
Core::Bloodborne::InstallReverseEngineeringTrace();
|
||||
BbscMod::StartRelay();
|
||||
BbscMod::StartP2PChannel();
|
||||
BbscMod::InstallHttpWrappers();
|
||||
LOG_INFO(Mod, "BBSC mod ready after {} ms", GetTickCount() - started);
|
||||
|
||||
for (;;) {
|
||||
Sleep(WatchMs);
|
||||
std::scoped_lock lock{patch_mutex};
|
||||
if (const int redone = SyncSites(); redone != 0) {
|
||||
LOG_WARNING(Mod, "{} patch site(s) had changed back - written again", redone);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
// ---- Logging -------------------------------------------------------------------------------------
|
||||
|
||||
namespace BbscMod {
|
||||
|
||||
void LogLine(char level, const char* category, const std::string& text) {
|
||||
std::scoped_lock lock{log_mutex};
|
||||
// The game's log gets the mod's own lines and every warning or error.
|
||||
if (level == 'W' || level == 'E' || std::strcmp(category, "Mod") == 0) {
|
||||
std::printf("BBSC: %c %s: %s\n", level, category, text.c_str());
|
||||
}
|
||||
if (log_file == nullptr) {
|
||||
return;
|
||||
}
|
||||
timespec now{};
|
||||
clock_gettime(CLOCK_REALTIME, &now);
|
||||
std::tm local{};
|
||||
localtime_r(&now.tv_sec, &local);
|
||||
std::fprintf(log_file, "[%02d:%02d:%02d.%03ld] %c %s: %s\n", local.tm_hour, local.tm_min,
|
||||
local.tm_sec, now.tv_nsec / 1'000'000, level, category, text.c_str());
|
||||
std::fflush(log_file);
|
||||
}
|
||||
|
||||
void LogPlain(const char* text) {
|
||||
LogLine('I', "Mod", text);
|
||||
}
|
||||
|
||||
u64 EbootAddress() {
|
||||
return EbootBase;
|
||||
}
|
||||
|
||||
// Import slots swapped for wrappers (bbsc_http.cpp, bbsc_channel.cpp): put back if anything links
|
||||
// them again (bbport links the imports once, before the game runs; kept as the Windows mod has it).
|
||||
namespace {
|
||||
struct GuardedSlot {
|
||||
u64 slot_offset;
|
||||
u64 wrapper;
|
||||
void** original;
|
||||
const char* name;
|
||||
int rewritten = 0;
|
||||
};
|
||||
std::mutex guarded_mutex;
|
||||
std::vector<GuardedSlot> guarded_slots;
|
||||
|
||||
DWORD WINAPI SlotGuardThread(void*) {
|
||||
for (;;) {
|
||||
Sleep(5);
|
||||
std::scoped_lock lock{guarded_mutex};
|
||||
for (auto& guarded : guarded_slots) {
|
||||
auto* slot = reinterpret_cast<volatile u64*>(EbootBase + guarded.slot_offset);
|
||||
const u64 value = *slot;
|
||||
if (value == guarded.wrapper || value == 0 ||
|
||||
(value >= EbootBase && value < EbootBase + EbootSize)) {
|
||||
continue;
|
||||
}
|
||||
*reinterpret_cast<void* volatile*>(guarded.original) = reinterpret_cast<void*>(value);
|
||||
DWORD old = 0;
|
||||
VirtualProtect(const_cast<u64*>(slot), 8, PAGE_READWRITE, &old);
|
||||
*slot = guarded.wrapper;
|
||||
if (++guarded.rewritten <= 3) {
|
||||
LOG_INFO(Mod, "Import {} was linked again - wrapper put back ({}x)", guarded.name,
|
||||
guarded.rewritten);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} // namespace
|
||||
|
||||
void GuardSlot(u64 slot_offset, u64 wrapper, void** original, const char* name) {
|
||||
std::scoped_lock lock{guarded_mutex};
|
||||
if (guarded_slots.empty()) {
|
||||
CreateThread(nullptr, 0, SlotGuardThread, nullptr, 0, nullptr);
|
||||
}
|
||||
guarded_slots.push_back({slot_offset, wrapper, original, name});
|
||||
}
|
||||
|
||||
void StartDiscordStatus() {} // bbport has its own Discord status
|
||||
|
||||
} // namespace BbscMod
|
||||
|
||||
// ---- What bloodborne_re.cpp asks the emulator for ----------------------------------------------
|
||||
|
||||
namespace Core {
|
||||
|
||||
bool MemoryManager::IsValidMapping(u64 address, u64 size) {
|
||||
return Readable(address, size == 0 ? 1 : size);
|
||||
}
|
||||
|
||||
// The PS4 protection bits (1 read, 2 write, 4 execute) of the region at address. bbport's map
|
||||
// gives where the region ends; its start is reported as the address's own page.
|
||||
s32 MemoryManager::QueryProtection(u64 address, void** start, void** end, u32* prot) {
|
||||
const u64 page = static_cast<u64>(sysconf(_SC_PAGESIZE));
|
||||
if (EbootBase != 0 && address >= EbootBase && address < EbootBase + EbootSize) {
|
||||
*start = reinterpret_cast<void*>(EbootBase);
|
||||
*end = reinterpret_cast<void*>(EbootBase + EbootSize);
|
||||
*prot = address < EbootBase + EbootCodeSize ? 7u : 3u;
|
||||
return 0;
|
||||
}
|
||||
int region_prot = 0, type = 0;
|
||||
uintptr_t region_end = 0;
|
||||
if (!runtime_memory_vma_info(address, ®ion_prot, &type, ®ion_end)) {
|
||||
return -1;
|
||||
}
|
||||
*start = reinterpret_cast<void*>(address & ~(page - 1));
|
||||
*end = reinterpret_cast<void*>(region_end);
|
||||
*prot = static_cast<u32>(region_prot) & 7u;
|
||||
return 0;
|
||||
}
|
||||
|
||||
namespace Debugger {
|
||||
int GetCurrentPid() {
|
||||
return static_cast<int>(getpid());
|
||||
}
|
||||
} // namespace Debugger
|
||||
|
||||
namespace Bbsc {
|
||||
|
||||
void InstallPersistentSessionPatches() {} // applied by Worker
|
||||
|
||||
void SetPersistentSessionPatches(bool enabled) {
|
||||
std::scoped_lock lock{patch_mutex};
|
||||
if (!patches_ready || session_on == enabled) {
|
||||
return;
|
||||
}
|
||||
session_on = enabled;
|
||||
const int count = SyncSites();
|
||||
LOG_INFO(Debug, "BBSC persistence {} ({} session sites; echoes, lamp, fog walls and deaths "
|
||||
"untouched)",
|
||||
enabled ? "re-applied" : "suspended", count);
|
||||
}
|
||||
|
||||
void SetHostLampPatches(bool hosting) {
|
||||
std::scoped_lock lock{patch_mutex};
|
||||
if (!patches_ready || role_on == hosting) {
|
||||
return;
|
||||
}
|
||||
role_on = hosting;
|
||||
const int count = SyncSites();
|
||||
LOG_INFO(Debug, "BBSC lamp: {} ({} sites)",
|
||||
hosting ? "prompt allowed (hosting a session)"
|
||||
: "vanilla gates (solo or guest - a guest never gets the lamp)",
|
||||
count);
|
||||
}
|
||||
|
||||
// The cinematic camera is not in the Linux build: it stays off.
|
||||
CinematicInput TakeCinematicInput() {
|
||||
return CinematicInput{};
|
||||
}
|
||||
void CinematicForceOff() {}
|
||||
void CinematicForceOn() {}
|
||||
|
||||
} // namespace Bbsc
|
||||
} // namespace Core
|
||||
|
||||
namespace Common::FS {
|
||||
std::filesystem::path GetUserPath(PathType type) {
|
||||
return type == PathType::CapturesDir ? data_dir / "captures" : data_dir;
|
||||
}
|
||||
} // namespace Common::FS
|
||||
|
||||
namespace Libraries::Np::NpMatching2 {
|
||||
int BbscReplayRoomMembers(bool) {
|
||||
return 0; // the online module's room cache is out of reach here
|
||||
}
|
||||
} // namespace Libraries::Np::NpMatching2
|
||||
|
||||
// bb-probe calls this once the game is loaded, linked and patched, before it runs any of it.
|
||||
extern "C" __attribute__((visibility("default"))) void bbsc_start(void) {
|
||||
CreateThread(nullptr, 0, Worker, nullptr, 0, nullptr);
|
||||
}
|
||||
@@ -1,116 +0,0 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
// bbport: the handful of Win32 and Winsock names the BBSC mod uses, on Linux. BSD sockets under
|
||||
// their Winsock names (as bbbridge's port), POSIX threads, mprotect, a monotonic clock. Only what
|
||||
// the mod's files call; anything that differs in meaning is handled in the file itself (#ifdef).
|
||||
#pragma once
|
||||
|
||||
#include <arpa/inet.h>
|
||||
#include <cerrno>
|
||||
#include <cstdint>
|
||||
#include <ctime>
|
||||
#include <netdb.h>
|
||||
#include <netinet/in.h>
|
||||
#include <netinet/tcp.h>
|
||||
#include <sys/mman.h>
|
||||
#include <sys/socket.h>
|
||||
#include <thread>
|
||||
#include <unistd.h>
|
||||
|
||||
using DWORD = std::uint32_t;
|
||||
using BOOL = int;
|
||||
using HANDLE = void*;
|
||||
using LPVOID = void*;
|
||||
using SOCKET = int;
|
||||
#ifndef TRUE
|
||||
#define TRUE 1
|
||||
#define FALSE 0
|
||||
#endif
|
||||
#define WINAPI
|
||||
#define MAX_PATH 4096
|
||||
#define INVALID_SOCKET (-1)
|
||||
#define SOCKET_ERROR (-1)
|
||||
#define INVALID_HANDLE_VALUE (reinterpret_cast<HANDLE>(-1))
|
||||
|
||||
// Memory protection: only the constants the mod passes.
|
||||
constexpr DWORD PAGE_NOACCESS = 0x01, PAGE_READONLY = 0x02, PAGE_READWRITE = 0x04,
|
||||
PAGE_EXECUTE = 0x10, PAGE_EXECUTE_READ = 0x20, PAGE_EXECUTE_READWRITE = 0x40;
|
||||
|
||||
inline int BbscPosixProt(DWORD protect) {
|
||||
switch (protect) {
|
||||
case PAGE_NOACCESS:
|
||||
return PROT_NONE;
|
||||
case PAGE_READONLY:
|
||||
return PROT_READ;
|
||||
case PAGE_READWRITE:
|
||||
return PROT_READ | PROT_WRITE;
|
||||
case PAGE_EXECUTE:
|
||||
case PAGE_EXECUTE_READ:
|
||||
return PROT_READ | PROT_EXEC;
|
||||
default:
|
||||
return PROT_READ | PROT_WRITE | PROT_EXEC;
|
||||
}
|
||||
}
|
||||
|
||||
// The pages under [address, address + size) get `protect`. The previous protection is not known
|
||||
// here: it is reported as read/write/execute, which is what restoring it then sets (the game's
|
||||
// image is made read/write/execute at start anyway, as shadPS4 maps it).
|
||||
inline BOOL VirtualProtect(void* address, std::size_t size, DWORD protect, DWORD* old) {
|
||||
const auto page = static_cast<std::uintptr_t>(sysconf(_SC_PAGESIZE));
|
||||
const auto start = reinterpret_cast<std::uintptr_t>(address) & ~(page - 1);
|
||||
const auto end = reinterpret_cast<std::uintptr_t>(address) + size;
|
||||
if (old != nullptr) {
|
||||
*old = PAGE_EXECUTE_READWRITE;
|
||||
}
|
||||
return mprotect(reinterpret_cast<void*>(start), end - start, BbscPosixProt(protect)) == 0;
|
||||
}
|
||||
|
||||
inline HANDLE GetCurrentProcess() {
|
||||
return nullptr;
|
||||
}
|
||||
// x86: instruction fetches see stores without a flush.
|
||||
inline BOOL FlushInstructionCache(HANDLE, const void*, std::size_t) {
|
||||
return TRUE;
|
||||
}
|
||||
inline DWORD GetCurrentProcessId() {
|
||||
return static_cast<DWORD>(getpid());
|
||||
}
|
||||
|
||||
inline std::uint64_t GetTickCount64() {
|
||||
timespec now{};
|
||||
clock_gettime(CLOCK_MONOTONIC, &now);
|
||||
return static_cast<std::uint64_t>(now.tv_sec) * 1000 + static_cast<std::uint64_t>(now.tv_nsec) / 1'000'000;
|
||||
}
|
||||
inline DWORD GetTickCount() {
|
||||
return static_cast<DWORD>(GetTickCount64());
|
||||
}
|
||||
inline void Sleep(DWORD ms) {
|
||||
timespec span{static_cast<time_t>(ms / 1000), static_cast<long>(ms % 1000) * 1'000'000};
|
||||
while (nanosleep(&span, &span) != 0 && errno == EINTR) {
|
||||
}
|
||||
}
|
||||
|
||||
// A detached thread running a DWORD WINAPI f(void*); the handle only says it started.
|
||||
inline HANDLE CreateThread(void*, std::size_t, DWORD (*start)(void*), void* argument, DWORD,
|
||||
DWORD*) {
|
||||
try {
|
||||
std::thread([start, argument] { start(argument); }).detach();
|
||||
} catch (...) {
|
||||
return nullptr;
|
||||
}
|
||||
return reinterpret_cast<HANDLE>(1);
|
||||
}
|
||||
inline BOOL CloseHandle(HANDLE) {
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
// Winsock.
|
||||
struct WSADATA {};
|
||||
#define MAKEWORD(a, b) static_cast<unsigned short>((a) | ((b) << 8))
|
||||
inline int WSAStartup(unsigned short, WSADATA*) {
|
||||
return 0;
|
||||
}
|
||||
// shutdown first: a thread blocked in recv on the socket wakes up (close alone leaves it there).
|
||||
inline int closesocket(SOCKET s) {
|
||||
shutdown(s, SHUT_RDWR);
|
||||
return close(s);
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
{
|
||||
"id": "bbsc",
|
||||
"name": "BBSC - Bloodborne Seamless Co-op",
|
||||
"version": "0.0.1",
|
||||
"author": "CLS_AMG; seamless summon by Wozzardman; server format by fhody125",
|
||||
"description": "One co-op session across areas, deaths, lamps, bosses and the Hunter's Dream. Bloodborne CUSA03173 v1.09 only. Warps and world sync go through the host's game server relay (port 20444). Off = the vanilla online game.",
|
||||
"games": ["CUSA03173"],
|
||||
"dll": "bbsc.dll",
|
||||
"enabled_by_default": true,
|
||||
"settings": [
|
||||
{"id": "seamless", "name": "Seamless summon", "description": "Summon without the vanilla signs and waits (Wozzardman)", "type": "bool", "default": true, "env": "BBSC_MOD_SEAMLESS"},
|
||||
{"id": "hooks", "name": "Co-op hooks", "description": "Warps, world sync, Hunter's Dream, messages... (most features below need this)", "type": "bool", "default": true, "env": "BBSC_MOD_HOOKS"},
|
||||
{"id": "connect_first", "name": "Cross-map summon", "description": "Join first, then one load into the host's map", "type": "bool", "default": true, "env": "SHADPS4_BBSC_CONNECT_FIRST"},
|
||||
{"id": "warp_together", "name": "Warp together", "description": "The guest follows the host through lamp warps", "type": "bool", "default": true, "env": "SHADPS4_BBSC_WARP_TOGETHER"},
|
||||
{"id": "world_sync", "name": "World sync", "description": "The guest sees the host's world state (doors, bosses, lifts)", "type": "bool", "default": true, "env": "SHADPS4_BBSC_WORLD_SYNC"},
|
||||
{"id": "dream", "name": "Hunter's Dream together", "description": "The guest uses the Doll, workshop and messengers", "type": "bool", "default": true, "env": "SHADPS4_BBSC_DREAM"},
|
||||
{"id": "no_fog_walls", "name": "No co-op fog walls", "description": "Leaving the area is allowed", "type": "bool", "default": true, "env": "SHADPS4_BBSC_NO_FOG_WALLS"},
|
||||
{"id": "lamp", "name": "Lamps in co-op", "description": "The host can use lamps during a session", "type": "bool", "default": true, "env": "SHADPS4_BBSC_LAMP"},
|
||||
{"id": "host_dead", "name": "Stay when the host dies", "description": "The guest keeps the session if the host dies", "type": "bool", "default": true, "env": "SHADPS4_BBSC_PERSIST_HOSTDEAD"},
|
||||
{"id": "spectate", "name": "Respawn in place", "description": "A dead player respawns / spectates instead of leaving", "type": "bool", "default": true, "env": "SHADPS4_BBSC_SPECTATE"},
|
||||
{"id": "full_echoes", "name": "Full echoes", "description": "A guest is paid the host's rate, not half", "type": "bool", "default": true, "env": "SHADPS4_BBSC_FULL_ECHOES"},
|
||||
{"id": "no_hp_penalty", "name": "No guest HP cut", "description": "Removes the summoned guest's 30% max HP cut", "type": "bool", "default": true, "env": "SHADPS4_BBSC_NO_HP_PENALTY"},
|
||||
{"id": "guest_items", "name": "Guest items", "description": "A guest picks up and keeps items", "type": "bool", "default": true, "env": "SHADPS4_BBSC_GUEST_ITEMS"},
|
||||
{"id": "solid_npcs", "name": "Solid NPCs", "description": "A guest sees NPCs solid, not as grey ghosts", "type": "bool", "default": true, "env": "SHADPS4_BBSC_SOLID_NPCS"},
|
||||
{"id": "death_penalty", "name": "Bloodstain on death", "description": "A kept death still drops a bloodstain and echoes", "type": "bool", "default": true, "env": "SHADPS4_BBSC_DEATH_PENALTY"},
|
||||
{"id": "free_bell", "name": "Free Beckoning Bell", "description": "The bell costs no Insight", "type": "bool", "default": true, "env": "SHADPS4_BBSC_FREE_BELL"},
|
||||
{"id": "five_hunters", "name": "Five hunters", "description": "The host may summon up to 4 guests", "type": "bool", "default": true, "env": "SHADPS4_BBSC_FIVE_HUNTERS"},
|
||||
{"id": "messages", "name": "Co-op texts", "description": "Texts that fit seamless play (no \"Returning to your world\")", "type": "bool", "default": true, "env": "SHADPS4_BBSC_MESSAGES"},
|
||||
{"id": "load_cover", "name": "Black fade on joins", "description": "The guest's loads happen behind a black fade", "type": "bool", "default": true, "env": "SHADPS4_BBSC_LOAD_COVER"},
|
||||
{"id": "floor_guard", "name": "Floor guard", "description": "No crash when a character's floor was unloaded", "type": "bool", "default": true, "env": "SHADPS4_BBSC_FLOOR_GUARD"},
|
||||
{"id": "scaling", "name": "Enemy scaling", "description": "Enemies and bosses get stronger for every guest (replaces the game's own boss buff)", "type": "bool", "default": true, "env": "SHADPS4_BBSC_SCALING"},
|
||||
{"id": "scale_npc_hunters", "name": "Scale NPC hunters", "description": "Henryk, Djura and the other hostile hunters also get the enemy scaling (off = vanilla)", "type": "bool", "default": false, "env": "BBSC_SCALE_NPC_HUNTERS"},
|
||||
{"id": "scale_enemy_hp", "name": "Enemy HP per guest (%)", "description": "Extra health for normal enemies, per guest", "type": "int", "default": 40, "min": 0, "max": 300, "env": "BBSC_SCALE_ENEMY_HP"},
|
||||
{"id": "scale_boss_hp", "name": "Boss HP per guest (%)", "description": "Extra health for bosses, per guest", "type": "int", "default": 100, "min": 0, "max": 300, "env": "BBSC_SCALE_BOSS_HP"},
|
||||
{"id": "scale_damage", "name": "Enemy damage per guest (%)", "description": "Extra damage for enemies and bosses, per guest", "type": "int", "default": 15, "min": 0, "max": 200, "env": "BBSC_SCALE_DAMAGE"},
|
||||
{"id": "scale_enemy_stagger", "name": "Enemy stagger resistance per guest (%)", "description": "Harder to stagger normal enemies, per guest", "type": "int", "default": 15, "min": 0, "max": 200, "env": "BBSC_SCALE_ENEMY_STAGGER"},
|
||||
{"id": "scale_boss_stagger", "name": "Boss stagger resistance per guest (%)", "description": "Harder to stagger bosses, per guest", "type": "int", "default": 20, "min": 0, "max": 200, "env": "BBSC_SCALE_BOSS_STAGGER"},
|
||||
{"id": "shared_progress", "name": "Shared progress", "description": "When a guest goes home, bosses, shortcuts, doors and lamps the host has done are added to the guest's own world (adds only, never removes; NPC quests and your character are not touched; saves backed up first)", "type": "bool", "default": true, "env": "SHADPS4_BBSC_SHARED_PROGRESS"},
|
||||
{"id": "discord", "name": "Discord status", "description": "Shows BBSC, your area, boss fights and the number of hunters on your Discord profile", "type": "bool", "default": true, "env": "SHADPS4_BBSC_DISCORD"},
|
||||
{"id": "p2p_channel", "name": "P2P channel", "description": "Sends BBSC's own messages (follow, world sync, scaling, leaving) straight to the other players over the game's own connection when there is no BBSC relay - needed on public servers (The Hunter's Dream)", "type": "bool", "default": true, "env": "SHADPS4_BBSC_P2P_CHANNEL"},
|
||||
{"id": "relay", "name": "Relay address", "description": "host:20444 - leave empty to use the game server address from host_overrides.json (JOIN-HOST sets it)", "type": "text", "default": "", "env": "BBSC_RELAY"}
|
||||
]
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
// BBSC mod: stand-in for the emulator's logging. Same macros, written to bbsc-mod.log.
|
||||
#pragma once
|
||||
#include <string>
|
||||
#include <fmt/format.h>
|
||||
|
||||
namespace BbscMod {
|
||||
void LogLine(char level, const char* category, const std::string& text);
|
||||
}
|
||||
|
||||
#define LOG_TRACE(cat, ...) ::BbscMod::LogLine('T', #cat, fmt::format(__VA_ARGS__))
|
||||
#define LOG_DEBUG(cat, ...) ::BbscMod::LogLine('D', #cat, fmt::format(__VA_ARGS__))
|
||||
#define LOG_INFO(cat, ...) ::BbscMod::LogLine('I', #cat, fmt::format(__VA_ARGS__))
|
||||
#define LOG_WARNING(cat, ...) ::BbscMod::LogLine('W', #cat, fmt::format(__VA_ARGS__))
|
||||
#define LOG_ERROR(cat, ...) ::BbscMod::LogLine('E', #cat, fmt::format(__VA_ARGS__))
|
||||
#define LOG_CRITICAL(cat, ...) ::BbscMod::LogLine('C', #cat, fmt::format(__VA_ARGS__))
|
||||
@@ -1,11 +0,0 @@
|
||||
// BBSC mod: the three values the emulator's MemoryPatcher knows about the game, set by the DLL
|
||||
// once it has verified the image (bbsc_mod.cpp).
|
||||
#pragma once
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
|
||||
namespace MemoryPatcher {
|
||||
inline std::uintptr_t g_eboot_address = 0;
|
||||
inline std::uint64_t g_eboot_image_size = 0;
|
||||
inline std::string g_game_serial;
|
||||
} // namespace MemoryPatcher
|
||||
@@ -1,13 +0,0 @@
|
||||
// BBSC mod: paths. The "user" folder is the DLL's own folder.
|
||||
#pragma once
|
||||
#include <filesystem>
|
||||
#include <string>
|
||||
|
||||
namespace Common::FS {
|
||||
enum class PathType { UserDir, LogDir, CapturesDir };
|
||||
std::filesystem::path GetUserPath(PathType type);
|
||||
inline std::string PathToUTF8String(const std::filesystem::path& path) {
|
||||
const auto text = path.u8string();
|
||||
return std::string(text.begin(), text.end());
|
||||
}
|
||||
} // namespace Common::FS
|
||||
@@ -1,5 +0,0 @@
|
||||
// BBSC mod: process id for the capture file name.
|
||||
#pragma once
|
||||
namespace Core::Debugger {
|
||||
int GetCurrentPid();
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
// BBSC mod: the game's param.sfo is not read; the DLL only runs on an image whose patch sites all
|
||||
// match CUSA03173 01.09, so that is what it reports.
|
||||
#pragma once
|
||||
#include <optional>
|
||||
#include <string_view>
|
||||
|
||||
class PSF {
|
||||
public:
|
||||
std::optional<std::string_view> GetString(std::string_view key) const {
|
||||
if (key == "APP_VER") {
|
||||
return "01.09";
|
||||
}
|
||||
if (key == "TITLE_ID") {
|
||||
return "CUSA03173";
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
};
|
||||
@@ -1,5 +0,0 @@
|
||||
// BBSC mod: the emulator's room-member replay is not reachable from a mod.
|
||||
#pragma once
|
||||
namespace Libraries::Np::NpMatching2 {
|
||||
int BbscReplayRoomMembers(bool announce_left);
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
// BBSC packets on the np_signaling socket that the Bloodborne code also builds and reads. Kept out
|
||||
// of np_signaling_state.h, which pulls in the platform headers (their `near`/`far` macros break
|
||||
// bloodborne_re.cpp).
|
||||
|
||||
#include <cstddef>
|
||||
|
||||
#include "common/types.h"
|
||||
|
||||
namespace Libraries::Np::NpSignaling {
|
||||
|
||||
inline constexpr u8 kSignalingBbscWorldFlagsType = 0x22;
|
||||
|
||||
// One chunk of a host world-flag snapshot: up to 8 event-flag groups (1000 flags, 125 bytes each)
|
||||
// of the host's current map block, in the fixed order of Core::Bloodborne's BbscWorldGroupKey.
|
||||
// ~1 KB, below the 1280-byte tunnel MTU (Tailscale) with the UDP and v-port headers.
|
||||
inline constexpr size_t kBbscWorldFlagGroupBytes = 125;
|
||||
inline constexpr size_t kBbscWorldFlagGroupsPerChunk = 8;
|
||||
|
||||
#pragma pack(push, 1)
|
||||
struct SignalingBbscWorldFlags {
|
||||
u8 magic[4] = {'S', 'H', 'A', 'D'};
|
||||
u8 type = kSignalingBbscWorldFlagsType;
|
||||
u8 version = 1;
|
||||
u32 packed_map = 0;
|
||||
u32 snapshot = 0; // the host's snapshot counter: chunks of one snapshot belong together
|
||||
u8 chunk = 0; // index of this chunk
|
||||
u8 chunk_count = 0; // chunks in the snapshot
|
||||
u8 present = 0; // bit n: group n of this chunk exists in the host's flag table
|
||||
u8 group_count = 0; // groups carried (the last chunk may carry fewer than 8)
|
||||
u8 groups[kBbscWorldFlagGroupsPerChunk][kBbscWorldFlagGroupBytes]{};
|
||||
};
|
||||
static_assert(sizeof(SignalingBbscWorldFlags) == 18 + 8 * 125);
|
||||
#pragma pack(pop)
|
||||
|
||||
} // namespace Libraries::Np::NpSignaling
|
||||
@@ -1,10 +0,0 @@
|
||||
// BBSC mod: BBSC's own packets go through the BBSC relay (bbsc_relay.cpp), not the emulator's
|
||||
// signaling. Same functions, same meaning.
|
||||
#pragma once
|
||||
#include "common/types.h"
|
||||
|
||||
namespace Libraries::Np::NpSignaling::Helpers {
|
||||
u32 SendBbscFollowToPeers(u32 packed_map, float x, float y, float z, float heading, u32 sequence,
|
||||
s32 lamp_point);
|
||||
u32 SendBbscWorldFlagsToPeers(const void* packet, u32 size);
|
||||
} // namespace Libraries::Np::NpSignaling::Helpers
|
||||
@@ -1,27 +0,0 @@
|
||||
// BBSC mod: stand-in for the emulator's memory manager. The game's memory sits at its guest
|
||||
// addresses inside the emulator's process, so Windows' VirtualQuery answers what the emulator's
|
||||
// own map would.
|
||||
#pragma once
|
||||
#include "common/enum.h"
|
||||
#include "common/singleton.h"
|
||||
#include "common/types.h"
|
||||
|
||||
namespace Core {
|
||||
|
||||
enum class MemoryProt : u32 {
|
||||
NoAccess = 0,
|
||||
CpuRead = 1,
|
||||
CpuWrite = 2,
|
||||
CpuReadWrite = 3,
|
||||
CpuExec = 4,
|
||||
};
|
||||
DECLARE_ENUM_FLAG_OPERATORS(MemoryProt)
|
||||
|
||||
class MemoryManager {
|
||||
public:
|
||||
bool IsValidMapping(u64 address, u64 size = 0);
|
||||
s32 QueryProtection(u64 address, void** start, void** end, u32* prot);
|
||||
};
|
||||
using Memory = Common::Singleton<MemoryManager>;
|
||||
|
||||
} // namespace Core
|
||||
@@ -1,161 +0,0 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <type_traits>
|
||||
#include "common/types.h"
|
||||
|
||||
#define DECLARE_ENUM_FLAG_OPERATORS(type) \
|
||||
[[nodiscard]] constexpr type operator|(type a, type b) noexcept { \
|
||||
using T = std::underlying_type_t<type>; \
|
||||
return static_cast<type>(static_cast<T>(a) | static_cast<T>(b)); \
|
||||
} \
|
||||
[[nodiscard]] constexpr type operator&(type a, type b) noexcept { \
|
||||
using T = std::underlying_type_t<type>; \
|
||||
return static_cast<type>(static_cast<T>(a) & static_cast<T>(b)); \
|
||||
} \
|
||||
[[nodiscard]] constexpr type operator^(type a, type b) noexcept { \
|
||||
using T = std::underlying_type_t<type>; \
|
||||
return static_cast<type>(static_cast<T>(a) ^ static_cast<T>(b)); \
|
||||
} \
|
||||
[[nodiscard]] constexpr type operator<<(type a, type b) noexcept { \
|
||||
using T = std::underlying_type_t<type>; \
|
||||
return static_cast<type>(static_cast<T>(a) << static_cast<T>(b)); \
|
||||
} \
|
||||
[[nodiscard]] constexpr type operator>>(type a, type b) noexcept { \
|
||||
using T = std::underlying_type_t<type>; \
|
||||
return static_cast<type>(static_cast<T>(a) >> static_cast<T>(b)); \
|
||||
} \
|
||||
constexpr type& operator|=(type& a, type b) noexcept { \
|
||||
a = a | b; \
|
||||
return a; \
|
||||
} \
|
||||
constexpr type& operator&=(type& a, type b) noexcept { \
|
||||
a = a & b; \
|
||||
return a; \
|
||||
} \
|
||||
constexpr type& operator^=(type& a, type b) noexcept { \
|
||||
a = a ^ b; \
|
||||
return a; \
|
||||
} \
|
||||
constexpr type& operator<<=(type& a, type b) noexcept { \
|
||||
a = a << b; \
|
||||
return a; \
|
||||
} \
|
||||
constexpr type& operator>>=(type& a, type b) noexcept { \
|
||||
a = a >> b; \
|
||||
return a; \
|
||||
} \
|
||||
[[nodiscard]] constexpr type operator~(type key) noexcept { \
|
||||
using T = std::underlying_type_t<type>; \
|
||||
return static_cast<type>(~static_cast<T>(key)); \
|
||||
} \
|
||||
[[nodiscard]] constexpr bool True(type key) noexcept { \
|
||||
using T = std::underlying_type_t<type>; \
|
||||
return static_cast<T>(key) != 0; \
|
||||
} \
|
||||
[[nodiscard]] constexpr bool False(type key) noexcept { \
|
||||
using T = std::underlying_type_t<type>; \
|
||||
return static_cast<T>(key) == 0; \
|
||||
}
|
||||
|
||||
namespace Common {
|
||||
|
||||
template <typename T>
|
||||
class Flags {
|
||||
public:
|
||||
using IntType = std::underlying_type_t<T>;
|
||||
|
||||
Flags() {}
|
||||
|
||||
Flags(IntType t) : m_bits(t) {}
|
||||
|
||||
template <typename... Tx>
|
||||
Flags(T f, Tx... fx) {
|
||||
this->set(f, fx...);
|
||||
}
|
||||
|
||||
template <typename... Tx>
|
||||
void set(Tx... fx) {
|
||||
m_bits |= bits(fx...);
|
||||
}
|
||||
|
||||
void set(Flags flags) {
|
||||
m_bits |= flags.m_bits;
|
||||
}
|
||||
|
||||
template <typename... Tx>
|
||||
void clr(Tx... fx) {
|
||||
m_bits &= ~bits(fx...);
|
||||
}
|
||||
|
||||
void clr(Flags flags) {
|
||||
m_bits &= ~flags.m_bits;
|
||||
}
|
||||
|
||||
template <typename... Tx>
|
||||
bool any(Tx... fx) const {
|
||||
return (m_bits & bits(fx...)) != 0;
|
||||
}
|
||||
|
||||
template <typename... Tx>
|
||||
bool all(Tx... fx) const {
|
||||
const IntType mask = bits(fx...);
|
||||
return (m_bits & mask) == mask;
|
||||
}
|
||||
|
||||
bool test(T f) const {
|
||||
return this->any(f);
|
||||
}
|
||||
|
||||
bool isClear() const {
|
||||
return m_bits == 0;
|
||||
}
|
||||
|
||||
void clrAll() {
|
||||
m_bits = 0;
|
||||
}
|
||||
|
||||
u32 raw() const {
|
||||
return m_bits;
|
||||
}
|
||||
|
||||
Flags operator&(const Flags& other) const {
|
||||
return Flags(m_bits & other.m_bits);
|
||||
}
|
||||
|
||||
Flags operator|(const Flags& other) const {
|
||||
return Flags(m_bits | other.m_bits);
|
||||
}
|
||||
|
||||
Flags operator^(const Flags& other) const {
|
||||
return Flags(m_bits ^ other.m_bits);
|
||||
}
|
||||
|
||||
bool operator==(const Flags& other) const {
|
||||
return m_bits == other.m_bits;
|
||||
}
|
||||
|
||||
bool operator!=(const Flags& other) const {
|
||||
return m_bits != other.m_bits;
|
||||
}
|
||||
|
||||
private:
|
||||
IntType m_bits = 0;
|
||||
|
||||
static IntType bit(T f) {
|
||||
return IntType(1) << static_cast<IntType>(f);
|
||||
}
|
||||
|
||||
template <typename... Tx>
|
||||
static IntType bits(T f, Tx... fx) {
|
||||
return bit(f) | bits(fx...);
|
||||
}
|
||||
|
||||
static IntType bits() {
|
||||
return 0;
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace Common
|
||||
@@ -1,28 +0,0 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <memory>
|
||||
|
||||
namespace Common {
|
||||
|
||||
template <class T>
|
||||
class Singleton {
|
||||
public:
|
||||
static T* Instance() {
|
||||
if (!m_instance) {
|
||||
m_instance = std::make_unique<T>();
|
||||
}
|
||||
return m_instance.get();
|
||||
}
|
||||
|
||||
protected:
|
||||
Singleton();
|
||||
~Singleton();
|
||||
|
||||
private:
|
||||
static inline std::unique_ptr<T> m_instance{};
|
||||
};
|
||||
|
||||
} // namespace Common
|
||||
@@ -1,39 +0,0 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
|
||||
using s8 = std::int8_t;
|
||||
using s16 = std::int16_t;
|
||||
using s32 = std::int32_t;
|
||||
using s64 = std::int64_t;
|
||||
|
||||
using u8 = std::uint8_t;
|
||||
using u16 = std::uint16_t;
|
||||
using u32 = std::uint32_t;
|
||||
using u64 = std::uint64_t;
|
||||
|
||||
using f32 = float;
|
||||
using f64 = double;
|
||||
|
||||
using u128 = std::array<std::uint64_t, 2>;
|
||||
static_assert(sizeof(u128) == 16, "u128 must be 128 bits wide");
|
||||
|
||||
using VAddr = uintptr_t;
|
||||
using PAddr = uintptr_t;
|
||||
|
||||
#define PS4_SYSV_ABI __attribute__((sysv_abi))
|
||||
|
||||
// UDLs for memory size values
|
||||
constexpr unsigned long long operator""_KB(unsigned long long x) {
|
||||
return 1024ULL * x;
|
||||
}
|
||||
constexpr unsigned long long operator""_MB(unsigned long long x) {
|
||||
return 1024_KB * x;
|
||||
}
|
||||
constexpr unsigned long long operator""_GB(unsigned long long x) {
|
||||
return 1024_MB * x;
|
||||
}
|
||||
@@ -1,142 +0,0 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2026 CLS_AMG
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#include "core/bbsc_memscan.h"
|
||||
|
||||
#ifdef _WIN32
|
||||
#include <windows.h>
|
||||
#else
|
||||
// bbport: the Linux search.
|
||||
#include <cinttypes>
|
||||
#include <cstdio>
|
||||
#include <sys/uio.h>
|
||||
#include <unistd.h>
|
||||
#endif
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
|
||||
namespace Core::Bbsc {
|
||||
|
||||
namespace {
|
||||
|
||||
#ifdef _WIN32
|
||||
// One region. The emulator changes page protection all the time (GPU tracking), so a page that was
|
||||
// readable a moment ago may fault now: the search runs under SEH and a fault just ends the region.
|
||||
void SearchRegion(const std::uint8_t* start, std::size_t size, const std::uint8_t* needle,
|
||||
std::size_t length, std::vector<std::uint64_t>& hits, std::size_t max_hits) {
|
||||
__try {
|
||||
const std::uint8_t* end = start + size;
|
||||
const std::uint8_t* at = start;
|
||||
while (hits.size() < max_hits && end - at >= static_cast<std::ptrdiff_t>(length)) {
|
||||
at = static_cast<const std::uint8_t*>(
|
||||
std::memchr(at, needle[0], static_cast<std::size_t>(end - at) - length + 1));
|
||||
if (at == nullptr) {
|
||||
break;
|
||||
}
|
||||
if (std::memcmp(at, needle, length) == 0) {
|
||||
hits.push_back(reinterpret_cast<std::uint64_t>(at));
|
||||
}
|
||||
++at;
|
||||
}
|
||||
} __except (EXCEPTION_EXECUTE_HANDLER) {
|
||||
}
|
||||
}
|
||||
#else
|
||||
// bbport: one region, copied in slices through process_vm_readv on this process: a page the GPU
|
||||
// side just protected, or a region unmapped meanwhile, only fails that read (as the SEH guard on
|
||||
// Windows) instead of faulting. Consecutive slices overlap by the needle's length.
|
||||
void SearchRegion(std::uint64_t start, std::uint64_t end, const std::uint8_t* needle,
|
||||
std::size_t length, std::vector<std::uint8_t>& buffer,
|
||||
std::vector<std::uint64_t>& hits, std::size_t max_hits) {
|
||||
const pid_t self = getpid();
|
||||
std::uint64_t at = start;
|
||||
while (at < end && hits.size() < max_hits) {
|
||||
const auto want =
|
||||
static_cast<std::size_t>(std::min<std::uint64_t>(buffer.size(), end - at));
|
||||
iovec local{buffer.data(), want};
|
||||
iovec remote{reinterpret_cast<void*>(at), want};
|
||||
const ssize_t got = process_vm_readv(self, &local, 1, &remote, 1, 0);
|
||||
if (got <= 0) {
|
||||
at = (at & ~std::uint64_t{0xFFF}) + 0x1000; // an unreadable page: the next one
|
||||
continue;
|
||||
}
|
||||
const auto size = static_cast<std::size_t>(got);
|
||||
const std::uint8_t* data = buffer.data();
|
||||
const std::uint8_t* scan = data;
|
||||
while (size >= length && hits.size() < max_hits) {
|
||||
if (scan > data + size - length) {
|
||||
break;
|
||||
}
|
||||
const auto left = static_cast<std::size_t>(data + size - length + 1 - scan);
|
||||
scan = static_cast<const std::uint8_t*>(std::memchr(scan, needle[0], left));
|
||||
if (scan == nullptr) {
|
||||
break;
|
||||
}
|
||||
if (std::memcmp(scan, needle, length) == 0) {
|
||||
hits.push_back(at + static_cast<std::uint64_t>(scan - data));
|
||||
}
|
||||
++scan;
|
||||
}
|
||||
at += at + size >= end || size < length ? size : size - length + 1;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
} // namespace
|
||||
|
||||
std::vector<std::uint64_t> FindInMemory(std::span<const std::uint8_t> needle, std::uint64_t lo,
|
||||
std::uint64_t hi, std::size_t max_hits) {
|
||||
std::vector<std::uint64_t> hits;
|
||||
#ifdef _WIN32
|
||||
if (needle.empty()) {
|
||||
return hits;
|
||||
}
|
||||
constexpr DWORD Readable = PAGE_READONLY | PAGE_READWRITE | PAGE_WRITECOPY | PAGE_EXECUTE_READ |
|
||||
PAGE_EXECUTE_READWRITE | PAGE_EXECUTE_WRITECOPY;
|
||||
std::uint64_t at = lo;
|
||||
while (at < hi && hits.size() < max_hits) {
|
||||
MEMORY_BASIC_INFORMATION info{};
|
||||
if (VirtualQuery(reinterpret_cast<void*>(at), &info, sizeof(info)) == 0) {
|
||||
break;
|
||||
}
|
||||
const auto start = reinterpret_cast<std::uint64_t>(info.BaseAddress);
|
||||
const std::uint64_t end = start + info.RegionSize;
|
||||
if (info.State == MEM_COMMIT && (info.Protect & Readable) != 0 &&
|
||||
(info.Protect & (PAGE_GUARD | PAGE_NOACCESS)) == 0) {
|
||||
SearchRegion(reinterpret_cast<const std::uint8_t*>(start), info.RegionSize,
|
||||
needle.data(), needle.size(), hits, max_hits);
|
||||
}
|
||||
if (end <= at) {
|
||||
break;
|
||||
}
|
||||
at = end;
|
||||
}
|
||||
#else
|
||||
if (needle.empty()) {
|
||||
return hits;
|
||||
}
|
||||
// The readable mappings in [lo, hi), from /proc/self/maps.
|
||||
FILE* maps = std::fopen("/proc/self/maps", "r");
|
||||
if (maps == nullptr) {
|
||||
return hits;
|
||||
}
|
||||
std::vector<std::uint8_t> buffer(std::size_t{1} << 20);
|
||||
char line[512];
|
||||
while (hits.size() < max_hits && std::fgets(line, sizeof(line), maps) != nullptr) {
|
||||
std::uint64_t start = 0;
|
||||
std::uint64_t end = 0;
|
||||
char prot[5] = {};
|
||||
if (std::sscanf(line, "%" SCNx64 "-%" SCNx64 " %4s", &start, &end, prot) != 3 ||
|
||||
prot[0] != 'r' || end <= lo || start >= hi) {
|
||||
continue;
|
||||
}
|
||||
SearchRegion(std::max(start, lo), std::min(end, hi), needle.data(), needle.size(), buffer,
|
||||
hits, max_hits);
|
||||
}
|
||||
std::fclose(maps);
|
||||
#endif
|
||||
return hits;
|
||||
}
|
||||
|
||||
} // namespace Core::Bbsc
|
||||
@@ -1,96 +0,0 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <array>
|
||||
#include <span>
|
||||
|
||||
#include "common/types.h"
|
||||
|
||||
// ============================================================================
|
||||
// Windows static guest red-zone protection
|
||||
// ============================================================================
|
||||
|
||||
enum class WindowsGuestRedZoneProtectionMode : u32 {
|
||||
Disabled,
|
||||
StaticPatching,
|
||||
};
|
||||
|
||||
namespace Core::WindowsGuestRedZoneProtection {
|
||||
|
||||
void SetActiveMode(WindowsGuestRedZoneProtectionMode mode) noexcept;
|
||||
WindowsGuestRedZoneProtectionMode GetActiveMode() noexcept;
|
||||
bool IsStaticPatchingEnabled() noexcept;
|
||||
|
||||
} // namespace Core::WindowsGuestRedZoneProtection
|
||||
|
||||
// ============================================================================
|
||||
// End Windows static guest red-zone protection
|
||||
// ============================================================================
|
||||
|
||||
namespace Core {
|
||||
|
||||
struct alignas(16) GuestRegisterSnapshot {
|
||||
u64 rax;
|
||||
u64 rbx;
|
||||
u64 rcx;
|
||||
u64 rdx;
|
||||
u64 rsi;
|
||||
u64 rdi;
|
||||
u64 rbp;
|
||||
u64 rsp;
|
||||
u64 r8;
|
||||
u64 r9;
|
||||
u64 r10;
|
||||
u64 r11;
|
||||
u64 r12;
|
||||
u64 r13;
|
||||
u64 r14;
|
||||
u64 r15;
|
||||
u64 rflags;
|
||||
u32 mxcsr;
|
||||
alignas(16) std::array<std::array<u8, 32>, 16> ymm;
|
||||
};
|
||||
|
||||
using GuestCodeHook = void PS4_SYSV_ABI (*)(u64 tag, const GuestRegisterSnapshot* registers);
|
||||
|
||||
// Windows static guest red-zone protection
|
||||
struct RedZonePatchResult {
|
||||
u64 function_count{};
|
||||
u64 instruction_count{};
|
||||
u64 red_zone_function_count{};
|
||||
u64 memory_instruction_count{};
|
||||
u64 short_memory_instruction_count{};
|
||||
u64 patched_memory_instruction_count{};
|
||||
u64 stack_dependent_memory_instruction_count{};
|
||||
u64 control_flow_memory_instruction_count{};
|
||||
u64 unrelocatable_memory_instruction_count{};
|
||||
u64 indirect_red_zone_function_count{};
|
||||
u64 cpu_patch_instruction_count{};
|
||||
u64 patched_cpu_patch_instruction_count{};
|
||||
u64 unsupported_cpu_patch_instruction_count{};
|
||||
};
|
||||
|
||||
/// Registers a module for patching, providing an area to generate trampoline code.
|
||||
void RegisterPatchModule(void* module_ptr, u64 module_size, void* trampoline_area_ptr,
|
||||
u64 trampoline_area_size);
|
||||
|
||||
/// Installs a byte-verified observer while preserving GPR, flags, MXCSR, and YMM state.
|
||||
/// The copied instruction span must contain whole instructions and must not use relative
|
||||
/// addressing.
|
||||
// skip_flag (optional): if the hook sets *skip_flag to non-zero, the trampoline clears it and
|
||||
// returns straight to the caller instead of running the hooked function (function entry only).
|
||||
bool InstallGuestCodeHook(void* address, std::span<const u8> expected_instructions, u64 tag,
|
||||
GuestCodeHook hook,
|
||||
const u8* skip_flag = nullptr);
|
||||
|
||||
/// Applies CPU patches that need to be done before beginning executions.
|
||||
void PrePatchInstructions(u64 segment_addr, u64 segment_size);
|
||||
|
||||
// Windows static guest red-zone protection
|
||||
/// Keeps Windows exception dispatch outside live guest red zones at faultable memory accesses.
|
||||
RedZonePatchResult PatchRedZoneMemoryInstructions(u64 segment_addr, u64 segment_size,
|
||||
std::span<const uintptr_t> function_starts);
|
||||
|
||||
} // namespace Core
|
||||
@@ -1,3 +0,0 @@
|
||||
@echo off
|
||||
rem Makes a server card (address + registration key) to send to your friends.
|
||||
powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0tools\generate-card.ps1"
|
||||
@@ -1,4 +0,0 @@
|
||||
@echo off
|
||||
rem For the player who is NOT hosting: connects your game to the host's servers.
|
||||
rem Drag the host's card (BBSC-card-....json) onto this file, or double-click and paste the address.
|
||||
powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0tools\join-host.ps1" -Address "%~1"
|
||||
@@ -1,3 +0,0 @@
|
||||
@echo off
|
||||
rem Creates an account on this PC's server (START-HOST first).
|
||||
powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0tools\new-account.ps1"
|
||||
@@ -1,62 +0,0 @@
|
||||
BBSC - HOST SERVER PACKAGE
|
||||
==========================
|
||||
Lets you host BBSC (Bloodborne Seamless Co-op) for your friends: the shadNet server (accounts,
|
||||
matchmaking, co-op summons) and the BBSC game server (messages, bloodstains, server list).
|
||||
Every player, you included, also needs the BBSC game package (BBSC-<build>.zip: launcher +
|
||||
emulator + BBSC mod) and their own copy of Bloodborne. No game files are included here.
|
||||
|
||||
WHAT YOU NEED
|
||||
- Windows 10/11
|
||||
- One network you and your friends are all on: Radmin VPN, Hamachi, Tailscale, ZeroTier,
|
||||
or the same house (same router / Wi-Fi).
|
||||
Not a privacy VPN (ProtonVPN, NordVPN...) - friends cannot reach you through those.
|
||||
|
||||
FIRST TIME (HOST)
|
||||
1. Unzip this folder somewhere permanent (e.g. C:\BBSC-Host). Do not run it from inside the zip.
|
||||
2. Double-click START-HOST.cmd
|
||||
- if this PC is on more than one network it asks which one your friends use
|
||||
(START-HOST-OTHER-NETWORK.cmd asks again later)
|
||||
- say Yes to the Windows prompt (installs the C++ runtime, opens the firewall for VPN
|
||||
addresses and your own local network only - nothing is opened to the internet;
|
||||
older BBSC firewall rules that only allowed Radmin are updated)
|
||||
- it creates your own empty account database and your own registration key
|
||||
- it points YOUR game at your servers too
|
||||
3. Double-click GENERATE-CARD.cmd. It asks a name, uses the same address as START-HOST
|
||||
(Enter) and makes cards\BBSC-card-<name>.json with the address and your registration key.
|
||||
Send that file to your friends in a DM - it holds your key, never post it publicly.
|
||||
4. Your account: BBSC launcher > Settings > Accounts, pick your card, type a name and
|
||||
password, Create Account on Server. (Or NEW-ACCOUNT.cmd, for yourself or a friend.)
|
||||
|
||||
YOUR FRIENDS
|
||||
1. Drag your card onto JOIN-HOST.cmd (in the BBSC game package). It connects their game to
|
||||
your servers and adds the card to their launcher.
|
||||
(No card? Double-click JOIN-HOST and paste the address START-HOST showed.)
|
||||
2. BBSC launcher > Settings > Accounts > pick the card > name + password >
|
||||
Create Account on Server. Accounts from other hosts do not exist on your server.
|
||||
|
||||
EVERY TIME (HOST)
|
||||
Double-click START-HOST.cmd and keep the two small server windows open while you play.
|
||||
Changed network (e.g. Radmin to Hamachi)? START-HOST-OTHER-NETWORK.cmd, then GENERATE-CARD
|
||||
again and send the new card.
|
||||
|
||||
playit.gg
|
||||
Account creation works through a playit tunnel, but co-op needs ports 31313-31315, 20443 and
|
||||
20444 with the same numbers on one address, which playit's free tunnels do not give. Use
|
||||
Radmin VPN, Hamachi or Tailscale for playing.
|
||||
|
||||
KEEP PRIVATE
|
||||
server\shadnet.cfg (your registration key), server\db\ (everyone's accounts),
|
||||
server\host-address.txt and cards\ (your key again) are yours.
|
||||
Never share them or zip them up for someone else - give out a fresh copy of this package.
|
||||
|
||||
LOGS
|
||||
server\logs\ - send these with a bug report.
|
||||
|
||||
CREDITS
|
||||
See CREDITS.txt. shadNet server: shadPS4 team / Wozzardman (shadnet-p2p). Server list format
|
||||
and game server groundwork: Fahad (fhody125), No Dream For The Hunter.
|
||||
|
||||
SOURCE CODE (GPL-2.0-or-later)
|
||||
shadnet.exe and shadnet-sample.exe are built from https://github.com/Wozzardman/shadnet-p2p
|
||||
(commit 5a849a1). The emulator BBSC runs on is built from https://github.com/Wozzardman/shadp2p
|
||||
with the BBSC changes. Source for the BBSC changes is available from CLS_AMG on request.
|
||||
@@ -1,3 +0,0 @@
|
||||
@echo off
|
||||
rem Like START-HOST, but asks again which network your friends use (e.g. switched from Radmin to Hamachi).
|
||||
powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0tools\start-host.ps1" -ChangeNetwork
|
||||
@@ -1,3 +0,0 @@
|
||||
@echo off
|
||||
rem Starts both BBSC servers on this PC (Radmin VPN, Hamachi, Tailscale, ZeroTier or home network).
|
||||
powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0tools\start-host.ps1"
|
||||
@@ -1,96 +0,0 @@
|
||||
# BBSC game server
|
||||
|
||||
What makes Bloodborne believe it is online, without depending on anybody else's machine.
|
||||
|
||||
> Need help with any of this, or with shadPS4 and Bloodborne on PC generally?
|
||||
> Join the **Bloodborne PC** Discord — it is the main community for running this game on PC.
|
||||
|
||||
## Why this exists
|
||||
|
||||
Before Bloodborne enables a single online feature it fetches:
|
||||
|
||||
```
|
||||
https://ss4.scej-network.jp:20443/bb-eu/ss.info
|
||||
```
|
||||
|
||||
That file is **base64-encoded XML**, and it is the **server list**. A `<gameurl2>` block names one
|
||||
URL per game API — login, notices, blood messages, ghosts, summoning, play logs — plus the timing
|
||||
and matching parameters the online systems run on. Everything the game does afterwards goes
|
||||
wherever that file says.
|
||||
|
||||
Private servers work by redirecting that one URL. Previously it pointed at `thehuntersdream.com`,
|
||||
so their `ss.info` decided where *our* co-op went. On 2026-09-21 their backend stopped responding
|
||||
(TCP connected, no HTTP reply, on both 18671 and 20443) and co-op stopped working for us with it.
|
||||
|
||||
Serving our own removes that dependency for good.
|
||||
|
||||
## What it does
|
||||
|
||||
| Endpoints | Where they go |
|
||||
| --- | --- |
|
||||
| the four `api_SummonData*` | the real shadnet server — this is where co-op actually happens |
|
||||
| everything else | this process, which answers empty-but-valid JSON so the game stops asking |
|
||||
|
||||
Blood messages, ghosts, notices and channels are **not implemented**. They get well-formed empty
|
||||
replies, which is enough to keep the game happy; this does not pretend to provide them.
|
||||
|
||||
Each player runs their own copy on `127.0.0.1`. Only the summon endpoints are shared, which is why
|
||||
the shadnet address is the one thing you have to set.
|
||||
|
||||
## Running it
|
||||
|
||||
```
|
||||
python bbsc_gameserver.py --shadnet 100.101.102.103
|
||||
```
|
||||
|
||||
Then in `%APPDATA%\shadPS4\host_overrides.json`:
|
||||
|
||||
```json
|
||||
{ "https://ss4.scej-network.jp:20443": "http://127.0.0.1:20443" }
|
||||
```
|
||||
|
||||
Standard library only — no Flask — so a stock Python runs it. Leave the window open while playing;
|
||||
it logs every request it answers, which is the fastest way to see whether the game is really
|
||||
talking to us.
|
||||
|
||||
| Option | Effect |
|
||||
| --- | --- |
|
||||
| `--shadnet ADDR` | **required** — the machine running `shadnet.exe` |
|
||||
| `--summon-interval N` | seconds between summon-sign checks (retail 35 / 30) |
|
||||
| `--level-range N` | co-op level window (retail ±20) |
|
||||
| `--host` / `--port` | where to listen (default `127.0.0.1:20443`) |
|
||||
| `--advertise ADDR` | address to write into `ss.info` when it differs from `--host` |
|
||||
| `--verbose` | log request headers and bodies |
|
||||
|
||||
Defaults change nothing except the addresses. Retuning is opt-in.
|
||||
|
||||
## Retail tuning values
|
||||
|
||||
Taken from the real `ss.info`, recorded here because they are not visible anywhere in the game and
|
||||
several of them explain behaviour we spent a long time blaming on the emulator.
|
||||
|
||||
| Tag | Retail | What it controls |
|
||||
| --- | --- | --- |
|
||||
| `SummonDataGetListInterval2` | 35 | seconds between checks for a summon sign |
|
||||
| `SummonDataCreateInterval2` | 30 | how often a host re-posts its sign |
|
||||
| `SummonDataGetListGuardTimer2` | 60 | guard timer on the sign list |
|
||||
| `SummonDataGetListGetMaxCount2` | 20 | most signs returned at once |
|
||||
| `SummonDataCoopMatchingLevelUpperAbs2` | 20 | co-op level window, upper |
|
||||
| `SummonDataCoopMatchingLevelLowerAbs2` | −20 | co-op level window, lower |
|
||||
| `SummonDataBloodMadHunterPercent2` | 5 | blood-mad hunter chance |
|
||||
| `ReloadServerStatusInfoInterval2` | 300 | how often the game re-reads `ss.info` |
|
||||
|
||||
**`SummonDataGetListInterval2` is what "quick summoning" means.** A sign can take over half a
|
||||
minute to appear on retail settings, and no amount of work inside the game could have changed it —
|
||||
the value lives in this file.
|
||||
|
||||
## Credit
|
||||
|
||||
The `ss.info` format, the endpoint list and the JSON response shapes come from
|
||||
[NoDreamForHunter](https://github.com/fhody125-web) by **fhody125**, MIT licensed — see
|
||||
`LICENSE.NoDreamForHunter`. He handed over the project and a real `ss.info` when thehuntersdream
|
||||
went down, which is the only reason any of this was possible. `ss_info_template.xml` is his file
|
||||
with the shadnet address left as a placeholder.
|
||||
|
||||
This script is a standalone reimplementation rather than a fork: no Flask dependency, and it can
|
||||
retune the summon parameters on the way out.
|
||||
@@ -1,5 +0,0 @@
|
||||
Welcome to BBSC - Bloodborne Seamless Co-op
|
||||
This is a test version (0.0.1): expect bugs, and back up your saves.
|
||||
Host: ring the Hunt Bell. Guest: ring the Kin Bell.
|
||||
Found a bug? Post it in #bug-reports on our Discord with your logs.
|
||||
Help, news and co-op partners: discord.gg/Df7BCmfWdt
|
||||
@@ -1,11 +0,0 @@
|
||||
#Syntax is communication_id|board_ids|rank_limit|update_mode|sort_mode|upload_num_limit|upload_size_limit
|
||||
#board_ids can be multiple board ids separated by a comma
|
||||
#update_mode values:
|
||||
#NORMAL_UPDATE
|
||||
#FORCE_UPDATE
|
||||
#sort_mode values:
|
||||
#DESCENDING_ORDER
|
||||
#ASCENDING_ORDER
|
||||
|
||||
#Sonic Mania
|
||||
NPWR12997_00|1,6|100|NORMAL_UPDATE|ASCENDING_ORDER|10|6000000
|
||||
@@ -1,14 +0,0 @@
|
||||
[General]
|
||||
Host=0.0.0.0
|
||||
UnsecuredPort=31313
|
||||
MatchingUdpPort=31314
|
||||
WebApiPort=31315
|
||||
StatsEnabled=false
|
||||
Matching2Enabled=true
|
||||
BloodborneSeamlessCoop=true
|
||||
StatsPort=31320
|
||||
StatsPath=stats
|
||||
StatsCacheLife=30
|
||||
EmailValidated=false
|
||||
AdminsList=
|
||||
RegistrationSecretKey=
|
||||
@@ -1,31 +0,0 @@
|
||||
# worlds.cfg — matchmaking world layout, per game.
|
||||
#
|
||||
# Two sections:
|
||||
#
|
||||
# [groups] maps a title id (CUSAxxxxx, no dash, as the client reports it from
|
||||
# param.sfo TITLE_ID) to a shared matching group. Titles in the same
|
||||
# group share one room pool / world list (regional or re-release
|
||||
# variants of the same game). A title id not listed here keys by its
|
||||
# own title id (isolated).
|
||||
#
|
||||
# [worlds] declares the worlds for a group (or for a bare title id used as its
|
||||
# own group). Format:
|
||||
# GROUP | WORLD_ID | SERVER_ID | LOBBIES_NUM | MAX_LOBBY_MEMBERS
|
||||
# Live room / member counts are filled in at request time.
|
||||
#
|
||||
# If this file is absent, GetWorldInfoList returns a single default world.
|
||||
|
||||
[groups]
|
||||
# Bloodborne — all regional / re-release SKUs share the "bloodborne" pool.
|
||||
CUSA00207 = bloodborne
|
||||
CUSA00208 = bloodborne
|
||||
CUSA00299 = bloodborne
|
||||
CUSA00900 = bloodborne
|
||||
CUSA01363 = bloodborne
|
||||
CUSA03014 = bloodborne
|
||||
CUSA03023 = bloodborne
|
||||
CUSA03173 = bloodborne
|
||||
CUSA03179 = bloodborne
|
||||
|
||||
[worlds]
|
||||
bloodborne | 1 | 1 | 0 | 0
|
||||
Loaded 100 of 1550 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user