mirror of
https://github.com/ayozetr/bloodborne_linux.git
synced 2026-11-08 01:08:25 -10:00
Compare commits
136
Commits
| 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
+2
-1
@@ -1,2 +1,3 @@
|
||||
# GitHub's language bar: this C++ table of game texts (its <?name?> placeholders) is not PHP.
|
||||
bbsc/emulator/src/core/bbsc_messages.inc linguist-language=C++
|
||||
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
-7
@@ -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,8 +41,7 @@ param.sfo
|
||||
*.dcx
|
||||
*.7z
|
||||
/CUSA*/
|
||||
# Hunter's Atlas's maps and their tools (launcher: Prepare maps)
|
||||
navigation-map*/
|
||||
atlas-tools/
|
||||
# Hunter's Atlas's own art (shipped; the launcher converts it for the game)
|
||||
!tools/hunters-atlas/art/*.png
|
||||
# The launcher's icon (shipped)
|
||||
!launcher/bloodborne.png
|
||||
# The players' guide, built by mkdocs (mkdocs.yml)
|
||||
_site/
|
||||
@@ -1,26 +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).
|
||||
> 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 *New memory and translation model* is
|
||||
used by default on AMD GPUs; other GPUs keep the old memory model, as in 0.3 (launcher →
|
||||
*Advanced → Memory and translation model*).
|
||||
|
||||
One step remains to the full Wine + DXVK model: make the new mode the default on every GPU 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
|
||||
@@ -29,20 +26,17 @@ remove the old 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.
|
||||
- **Three modes** (launcher → *Advanced → Memory and translation model*): *Auto* (the default:
|
||||
new on AMD, old on others), *New* and *Old (as in 0.3)*.
|
||||
- **Old (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.** 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;
|
||||
@@ -50,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](https://github.com/deadinside28/bloodborne_pc/blob/master/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](https://github.com/deadinside28/bloodborne_pc/blob/master/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](https://github.com/deadinside28/bloodborne_pc/blob/master/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
|
||||
@@ -83,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.
|
||||
@@ -90,29 +108,37 @@ 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
|
||||
|
||||
| | shadPS4 | bbport |
|
||||
|---|---|---|
|
||||
| Scope | General PS4 emulator, many games | One game: Bloodborne v1.09 |
|
||||
| Loading | Its own ELF loader and kernel emulation at run time | The eboot is converted offline (`scripts/`) into an image with PS4 libc/Fios2 linked in; a C loader maps it and jumps into the game (loader and runtime: ~8k lines) |
|
||||
| Memory | The GPU's view of PS4 memory is kept in VRAM copies, synchronized through page-protection write tracking | On AMD by default the *New memory and translation model*: the game's memory in system RAM, used by the GPU in place, frequently read data in VRAM, freed when unused; on other GPUs the same model as in 0.3 |
|
||||
| Command processor | Emulated: memory writes, DMA and fences are done by the CPU while decoding | With the old model the same; with the new one translated into Vulkan commands that the GPU runs in stream order, fences written after the work has really finished |
|
||||
| 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 | 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 ~500 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
|
||||
|
||||
@@ -121,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
|
||||
@@ -132,29 +158,23 @@ the graphics side.
|
||||
bindings: a PS4 texture has to be converted from the PS4 tiling into a Vulkan image before the
|
||||
draw runs, so the CPU must know its descriptor beforehand — translation, not emulation. The
|
||||
descriptor tables (the constant engine's dumps) are read only by the translator, so the CPU
|
||||
performs them. The game's occlusion queries (~60 a frame) can be
|
||||
Vulkan occlusion queries (`BB_OCCLUSION_QUERIES=1`, opt-in; by default a fixed "everything
|
||||
visible" sequence written in command-stream order, `BB_ZPASS_IN_ORDER=0` turns the ordering off);
|
||||
predication, `COPY_DATA` and `COND_EXEC` are translated too (check:
|
||||
`BB_PM4_SELFTEST=1`). Details: upstream bbport's
|
||||
[EMULATION_REMOVAL_PLAN.ru.md](https://github.com/deadinside28/bloodborne_pc/blob/master/docs/EMULATION_REMOVAL_PLAN.ru.md),
|
||||
step 4 (in Russian).
|
||||
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](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).
|
||||
On NVIDIA the new memory model is still being measured with testers, so *Auto* keeps the 0.3
|
||||
model there.
|
||||
- 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
|
||||
@@ -167,56 +187,36 @@ neither has nor includes.
|
||||
## Build and run
|
||||
|
||||
```bash
|
||||
git clone --recursive https://github.com/ayozetr/bloodborne_linux.git && cd bloodborne_linux
|
||||
git clone --recursive https://github.com/deadinside28/bloodborne_pc.git bbport && cd bbport
|
||||
bash build.sh # builds out/bb-probe and out/gpu/libbbgpu.so
|
||||
BB_GAME_DIR=/path/to/CUSA03173 bash run.sh
|
||||
```
|
||||
|
||||
or the launcher (pick the game folder, settings, *Play*):
|
||||
or the launcher (pick the game folder, settings, *Start*):
|
||||
|
||||
```bash
|
||||
bash launcher/bb-launcher.sh # launcher/install-desktop.sh adds it to the app menu
|
||||
```
|
||||
|
||||
By default the game folder is expected next to the repository (`../CUSA03173`). Saves and the
|
||||
shader cache go to `user/` (the launcher's *Setup → Saves* lets you choose another folder, exports
|
||||
the saves into a .zip, and imports them from such a .zip, shadPS4, another port or a PS4 save
|
||||
exported unencrypted, after backing up the current ones in `user/savedata-backups/`, and can
|
||||
copy them there before each game, keeping as many copies as you choose; `scripts/save_import.py`); settings to `bbport.ini`. A gamepad is used
|
||||
through SDL3 (the launcher's *Controls → Gamepad → 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*); holding Left Alt with the move keys walks instead of running (`key.walk`). The character name is typed on the keyboard in a box over the game.
|
||||
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). *Graphics → Display → Field of view* (`fov`, 30-90 degrees, the game's
|
||||
43) scales every camera of the game's camera table, as a layer under the mods (`scripts/fov.py`; a mod's own
|
||||
`gameparam.parambnd.dcx` wins). The *Play online / offline* screen is skipped: the game opens its main menu online when online
|
||||
play is on, offline otherwise (a game patch; the launcher's *Multiplayer → Online → Skip the “play online / offline” choice*,
|
||||
`BB_SKIP_NETWORK_CHOICE=0` shows it). With several monitors, *Graphics → Display → Monitor* in the launcher
|
||||
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 *Setup → 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 European Game of the Year 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
|
||||
@@ -228,42 +228,36 @@ 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.en.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, *Setup → Game 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, English: following the desktop's
|
||||
locale is the launcher's job).
|
||||
**Game region:** *Setup → 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 (*Mods → Menus and debugging*) or in the in-game menu and restart. Free camera uses Lance McDonald's
|
||||
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 the `adhoc` folder of
|
||||
[Debug Menu and XML Patch](https://www.nexusmods.com/bloodborne/mods/253)
|
||||
(`adhoc/font/DbgFont14h.ccm` and `.tpf`) into the game's `dvdroot_ps4/`, or as a mod, first.
|
||||
Without them (or with empty files) the debug menu patch is left out at start and the log says
|
||||
why. Open it with the left touchpad / Tab (with the debug menu on, the left half
|
||||
For the game debug menu, install `DbgFont14h.ccm` and `DbgFont14h.tpf` from
|
||||
[Debug Menu and XML Patch](https://www.nexusmods.com/bloodborne/mods/253) into the game's
|
||||
`dvdroot_ps4/font/` first. Startup rejects missing or empty font files instead of launching
|
||||
the unsafe patch. Open it with the left touchpad / Tab (with the debug menu on, the left half
|
||||
no longer opens the gestures); Backspace is the right touchpad.
|
||||
Touch coordinates are forwarded from SDL gamepads; Back/Select emulates a left click on
|
||||
pads without a touch surface, 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.
|
||||
|
||||
By default GPU occlusion queries return synthetic "visible" counters (`BB_OCCLUSION_QUERIES=1`
|
||||
counts real samples). Free camera allows visual investigation; it does not change 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):
|
||||
|
||||
@@ -273,7 +267,7 @@ bash tools/fetch_fsr4_assets.sh # FSR 4 v07 (MIT, built from AMD's source b
|
||||
bash tools/fsr4cap/build_assets.sh <amd_fidelityfx_upscaler_dx12.dll>
|
||||
```
|
||||
|
||||
**FSR 4.1.1 from your own DLL.** The easiest way is the launcher: *Graphics → Upscaler → FSR 4.1.1 from your
|
||||
**FSR 4.1.1 from your own DLL.** The easiest way is the launcher: *Upscaler → FSR 4.1.1 from your
|
||||
own AMD DLL → Choose DLL…*, then pick `amd_fidelityfx_upscaler_dx12.dll` version 4.1.x: from
|
||||
OptiScaler's `FSR4_LATEST` folder or from a game with FSR 4.1. That one file is enough: AMD's DLL
|
||||
exports the FidelityFX API itself, so no loader is needed (only a DLL without these exports would
|
||||
@@ -281,10 +275,11 @@ be recorded through `amd_fidelityfx_loader_dx12.dll`, and the launcher would ask
|
||||
without a long capture; these do not fit: AMD's official FSR 4.0.x (e.g. Pragmata's 4.0.3 —
|
||||
AMD enables it on RDNA4 only, and under Proton it does not start on other GPUs) and community
|
||||
builds (4.0.2b, 4.1.1b and similar OptiScaler INT8 builds: another model and pass count). Then
|
||||
the DLL runs under Proton and is recorded at 20 sizes (progress in the row and in the *Log* section;
|
||||
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
|
||||
@@ -292,9 +287,7 @@ umu-launcher when installed. On NixOS umu-launcher comes from `nix-shell` (downl
|
||||
time, ~1.7 GB); the AppImage runs the recording on the system itself, through the user's systemd
|
||||
(`systemd-run --user`), since the system's `/nix` is out of its sight. Recording on the Steam Deck is not verified yet; if the DLL does not
|
||||
enable FSR 4.1 there, build on a PC and copy the `fsr4_411` folder. The same from the command
|
||||
line: the launcher's (and the AppImage's) `--build-fsr411 <DLL>`. From source, `tools/fsr4cap/build_assets.sh` takes its tools from
|
||||
the system (MinGW GCC for the capture program run under Proton, CMake, Ninja, Python 3,
|
||||
SPIRV-Tools, Git) or from Nix; the AppImage has them prebuilt.
|
||||
line: the launcher's (and the AppImage's) `--build-fsr411 <DLL>`.
|
||||
|
||||
**DLSS (NVIDIA GeForce RTX).** NVIDIA's DLSS SDK is not included: check out
|
||||
[github.com/NVIDIA/DLSS](https://github.com/NVIDIA/DLSS) (`include/` and `lib/Linux_x86_64` are
|
||||
@@ -306,20 +299,41 @@ 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
|
||||
launcher: *Graphics → Upscaler → DLSS: NVIDIA's library → Choose a file…*, as the FSR 4.1.1 DLL; it is
|
||||
launcher: *Upscaler → DLSS: NVIDIA's library → Choose a file…*, as the FSR 4.1.1 DLL; it is
|
||||
copied to `<user>/dlss/`, which the game prefers. A game's `nvngx_dlss.dll` is the Windows
|
||||
library: NVIDIA's Linux driver does not load it.
|
||||
|
||||
**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.
|
||||
|
||||
**FP8 on RDNA4.** When vkd3d-proton offers FP8 cooperative matrices (RDNA4), the DLL runs
|
||||
another variant of its passes: other shaders and weights, other dispatch sizes. The build records
|
||||
both, INT8 into `fsr4_411/` and FP8 into `fsr4_411/fp8/`, and the game picks FP8 itself when the
|
||||
@@ -336,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
|
||||
|
||||
@@ -356,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
|
||||
@@ -373,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/0` (the new memory and translation
|
||||
model / the old one as in 0.3; unset: new on AMD, old elsewhere), `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
|
||||
@@ -390,19 +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); what changed in each release: the
|
||||
[releases page](https://github.com/ayozetr/bloodborne_linux/releases).
|
||||
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 *Multiplayer → Online* tab turns it on (`BB_ONLINE=1`): bells and summons go through
|
||||
a shadNet server with a shadNet account (Online ID and password), messages, bloodstains and ghosts
|
||||
through a game server. Seamless Hunt (the default) runs both; The Hunter's Dream's pool goes
|
||||
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
|
||||
|
||||
@@ -410,46 +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](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 on NVIDIA and Intel too (it already is on
|
||||
AMD) once tested there; 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
|
||||
|
||||
This fork is licensed under the **GNU GPL v3 or later** ([LICENSE](LICENSE)). bbport, which it is
|
||||
built on, is GPL-2.0-or-later and contains code from shadPS4 (GPL-2.0-or-later); the fork also
|
||||
includes code from Lantern Engine by Awaken (https://github.com/Awakenchan/LanternEngine), GPL-3.0-or-later. Third-party components keep their licenses:
|
||||
[shadPS4](https://github.com/shadps4-emu/shadPS4) video core and shader recompiler (GPL-2.0+),
|
||||
bbport is licensed under the **GNU GPL v2 or later** ([LICENSE](LICENSE)) — it contains code
|
||||
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.
|
||||
@@ -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.
|
||||
@@ -17,6 +17,9 @@ projects ([credits](docs/CREDITS.md)).
|
||||
> 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
|
||||
|
||||
- **Multiplayer**: online messages, bloodstains, ghosts, bells and summons, on
|
||||
@@ -25,16 +28,22 @@ projects ([credits](docs/CREDITS.md)).
|
||||
**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) or capped at **30, 60 or 90**. Resolutions from
|
||||
**720p to 4K**, and **ultrawide** (21:9, 32:9, [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.
|
||||
- **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 a direct 1:1 mouse camera and keys you can rebind.
|
||||
- **Quit to Desktop** from the game's title and System menus (it saves first).
|
||||
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 and co-op partners.
|
||||
- **Launcher and in-game menu** (L3 + R3 or Insert) in English, Spanish, French, German, Italian,
|
||||
- **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).
|
||||
@@ -64,9 +73,8 @@ 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, or create an account with just a name. You play
|
||||
with bbhost's players. This connection is unofficial: report problems here, not to The Hunter's
|
||||
Dream.
|
||||
- **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)).
|
||||
@@ -107,7 +115,10 @@ python3 launcher/bbport_launcher.py
|
||||
```
|
||||
|
||||
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`. Environment
|
||||
`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).
|
||||
|
||||
|
||||
@@ -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
|
||||
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,53 +0,0 @@
|
||||
# bbbridge
|
||||
|
||||
Co-op on **The Hunter's Dream** pool, with players of [bbhost](https://github.com/droogie/bbhost).
|
||||
|
||||
The game speaks shadNet, the online service shadPS4 uses; The Hunter's Dream speaks bbhost's
|
||||
protocol. bbbridge sits between them on your own computer: to the game it is a shadNet server, and
|
||||
to The Hunter's Dream it is one more bbhost client. It carries the matchmaking (rooms, bells,
|
||||
summons), the traffic between the two games (with The Hunter's Dream's STUN and relay, so no router
|
||||
port has to be opened) and the account. The game itself is not changed.
|
||||
|
||||
bbbridge is by [MaskZeus](https://github.com/MaskZeus/bbbridge). This folder holds its source, built
|
||||
for Linux.
|
||||
|
||||
## How the port uses it
|
||||
|
||||
You never start it yourself:
|
||||
|
||||
1. In the launcher's **Multiplayer → Online** tab, choose **The Hunter's Dream (with bbhost players)**.
|
||||
2. Sign in with Discord (the launcher shows a code and opens The Hunter's Dream's page), or make an
|
||||
account without Discord with **Create**, and keep the recovery code it shows.
|
||||
3. Press **Play**: `run.sh` starts `bbbridge-cli` for that game and stops it when the game closes.
|
||||
|
||||
The account and the bridge's log are in `user/bbbridge/`. More in
|
||||
[docs/ONLINE.md](../docs/ONLINE.md#the-hunters-dream-with-bbhosts-players).
|
||||
|
||||
> This connection is unofficial: The Hunter's Dream's team has not approved bbbridge and does not
|
||||
> support this port. Report problems in this repository's
|
||||
> [issues](https://github.com/ayozetr/bloodborne_linux/issues).
|
||||
|
||||
## Building and testing
|
||||
|
||||
`build.sh` builds it with the rest of the port, into `out/bbbridge/`. On its own:
|
||||
|
||||
```sh
|
||||
cmake -S bbbridge -B out/bbbridge -G Ninja -DCMAKE_BUILD_TYPE=Release
|
||||
ninja -C out/bbbridge bbbridge-cli bbbridge-unit
|
||||
out/bbbridge/bbbridge-unit # framing, signaling, STUN, protobuf
|
||||
python3 bbbridge/tests/e2e/test_e2e.py out/bbbridge/bbbridge-cli # against fake servers, offline
|
||||
```
|
||||
|
||||
| Folder | Contents |
|
||||
|---|---|
|
||||
| `src/` | The bridge (`bridge.cpp`, `p2p.cpp`, `bbhost_client.cpp`…) and `main_cli.cpp`, the program the port runs |
|
||||
| `tests/` | Unit tests (C++) and end-to-end tests (Python) |
|
||||
| `third_party/` | nlohmann/json and Dear ImGui (MIT) |
|
||||
|
||||
`src/app.cpp` is bbbridge's own window, which only builds on Windows; the port does not use it.
|
||||
|
||||
## License
|
||||
|
||||
GPL-3.0-or-later (`LICENSE`); nlohmann/json and Dear ImGui are MIT (`THIRD_PARTY_NOTICES.txt`).
|
||||
The protocol comes from bbhost and The Hunter's Dream by droogie. Not affiliated with Sony,
|
||||
FromSoftware 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,128 +0,0 @@
|
||||
# bbport's changes to BBSC
|
||||
|
||||
This folder holds [BBSC](https://github.com/Classmg22/BBSC) 0.0.2 by CLS_AMG (GPL-2.0-or-later),
|
||||
as published at commit 4f11faf (tag v0.0.2; 0.0.1 was 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`, and through `/proc/self/mem` where that fails: on AMD the game's
|
||||
memory is a GPU dma-buf, and the item tables were never found there (no co-op kit).
|
||||
- **F8 key.** The F8 unstuck key works on Linux through SDL (`emulator/src/core/bloodborne_re.cpp`).
|
||||
- **Bell trace.** The responder bell trace also records the kit's Kin and Bloodlust Bells (4121,
|
||||
11201), not only the vanilla bells they replace (`emulator/src/core/bloodborne_re.cpp`).
|
||||
- **The co-op kit, late.** The search for the item tables, which only appear once a character is
|
||||
loaded, no longer gives up after 3 minutes (`emulator/src/core/bloodborne_re.cpp`, StartBbscDuelBell).
|
||||
- **Saves.** `BBSC_SAVE_DIR` is set to bbport's save folder (`savedata/<user>/<title id>/SPRJ0005`).
|
||||
On a first start, with no saves yet, it is the folder the game will write (`BB_SAVEDATA_TITLE`,
|
||||
from bbport's runtime), so the kit still comes.
|
||||
- **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.
|
||||
|
||||
## 2026-10-19
|
||||
|
||||
- **A host's placement goes to the sign it rings** (bbport issue #17). BBSC 0.0.1 sent a host's
|
||||
placement to every player on the server, and a waiting guest followed the first host it heard
|
||||
from. On a busy server (12 hosts on Seamless Hunt in one session) a guest followed a stranger:
|
||||
its sign went to that stranger's map, its friend never found it, and a stranger could summon it
|
||||
despite the password. BBSC's own `bbsc.dll` of 2026-10-17 (no source published yet) changed
|
||||
the protocol; this is the same protocol, reimplemented from that binary
|
||||
(`emulator/mod/bbsc_http.cpp`, `emulator/mod/bbsc_relay.cpp`):
|
||||
- a guest keeps the `UserId` its sign is created under;
|
||||
- a host ringing a sign (`/summon_messenger/request` with a `TargetUserId`) sends its placement
|
||||
to that user only: relay frame `"SHAD"` 0x31 + `"<TargetUserId>|<placement>"`, and the
|
||||
placement header to the broker;
|
||||
- a guest takes only a placement sent to its own sign, and keeps it 90 s (20 s otherwise);
|
||||
- placements sent to the whole server (0x30) are ignored, as BBSC's newer builds do;
|
||||
- nothing is sent while polling for signs any more, so the "own sign out" check is gone.
|
||||
|
||||
## 2026-10-21
|
||||
|
||||
- **The guest locks onto the host that rang its sign** (BBSC 0.0.2, source published 2026-10-21,
|
||||
`emulator/src/core/bloodborne_re.cpp`, BbscRangHost / TickBbscRelayHostLock). The 2026-10-19
|
||||
placement is now the same as BBSC 0.0.2's on the wire, so Linux and Windows 0.0.2 players find
|
||||
each other. Taken from 0.0.2 on top of it:
|
||||
- a placement aimed at our sign notes its sender as the host that rang it (90 s);
|
||||
- while joining or a guest, that host's packets are taken before the lock is made, so the join
|
||||
load gets its world flags;
|
||||
- the lock takes that host when it is on the relay; else the position lock is stricter (6 m
|
||||
instead of 15, one host that close, 3 checks in a row), and "the only one hosting on the
|
||||
server" is gone: it was a stranger whenever the real host was not on the relay;
|
||||
- the hosts that rang are forgotten when the session ends.
|
||||
BBSC 0.0.2's timeline log was not taken then (it is now, see "BBSC 0.0.2" below).
|
||||
|
||||
## 2026-10-21: BBSC 0.0.2
|
||||
|
||||
The rest of BBSC 0.0.2 (commit 4f11faf), merged with BBSC's own changes from 0.0.1 kept as they
|
||||
are, and bbport's changes above kept on top of them.
|
||||
|
||||
- **Taken as published:** `CREDITS.txt`, `SUPPORTERS.txt`, `emulator/mod/mod.json` (version 0.0.2,
|
||||
the new "Bells anywhere" option, the Discord option's text), `emulator/mod/bbsc_channel.cpp`,
|
||||
`emulator/mod/bbsc_discord.cpp`, `emulator/mod/bbsc_mod.cpp` (both Windows only, not built
|
||||
here), `emulator/src/core/bbsc_cinematic.{h,cpp}` (not built here), `bbsc_patch_tables.h`,
|
||||
`bbsc_persistence.h` (the new default-on switches `SHADPS4_BBSC_AREA_LIST_CAP`,
|
||||
`SHADPS4_BBSC_WORLD_SYNC_ALL`, `SHADPS4_BBSC_TIMELINE`, `SHADPS4_BBSC_BELLS_ANYWHERE`), the
|
||||
shim's `np_signaling_bbsc.h` and `server/gameserver/`.
|
||||
- **Taken with bbport's changes kept:** `emulator/src/core/bloodborne_re.{h,cpp}` (the watch
|
||||
camera of a dead guest, the kit's bells anywhere, the host's time of night for the guest, the
|
||||
area list cap against "Panic 265", the boss, kick and drop fixes, the kit from the game's own
|
||||
tables, the frame rate and Discord data, the timeline), `emulator/mod/bbsc_http.cpp` and
|
||||
`emulator/mod/bbsc_relay.cpp` (the relay's write queue).
|
||||
- **The aimed placement and the host lock** (2026-10-19 and the first 2026-10-21 entry above) are
|
||||
now BBSC's own code, under its names: `OnAimedHostPlacement`, `RelaySendAimedPlacement`,
|
||||
`AimedPlacementType` (0x31), `own_user_id` / `placement_aimed`, and the claim told from the
|
||||
search by URL (`/summon_messenger/request` vs `/get`). The wire format is unchanged. Gone with
|
||||
them: bbport's own names (`OnDirectedPlacement`, `RelaySendPlacementTo`, `PlacementToType`) and
|
||||
its reading of a `UserId` / `TargetUserId` sent as a string.
|
||||
- **The co-op kit, late** (2026-10-15) stays: 0.0.2 first looks in the game's own tables for 3
|
||||
minutes; the search of every copy behind it keeps going after 3 minutes as before.
|
||||
- **Linux:**
|
||||
- `emulator/mod/linux/bbsc_mod_linux.cpp`: version 0.0.2; the import slot reads and writes
|
||||
0.0.2 calls `ReadSlotSafely` / `WriteSlotSafely` (through bbport's map of the game's memory);
|
||||
the timeline's start lines; the timeline file goes next to the log
|
||||
(`<user dir>/bbsc/logs/bbsc-timeline-*.txt`), and only the newest 5 are kept, as the logs.
|
||||
- **Watch camera:** the F1 free camera stays out of the Linux build (every key, button and the
|
||||
mouse also reach the game through bbport's pad, so flying it would move the hunter; L3+R3 is
|
||||
bbport's menu). The watch camera of 0.0.2 only comes on by itself while our hunter is dead in
|
||||
co-op, when those inputs do nothing in the game, so it is in: `bbsc_mod_linux.cpp` reads its
|
||||
keys through SDL like the F8 key, on the game thread and only while it is on: Tab / E / left
|
||||
click next hunter, Shift+Tab / Q previous, F fly freely and back; the controller's R1 / L1 and
|
||||
sticks; arrow keys and W / S. F1 turns it off for the rest of that death. The mouse's movement
|
||||
is not read (it is bbport's mouse look). Switches `SHADPS4_BBSC_CINEMATIC` and
|
||||
`SHADPS4_BBSC_SPECTATE` as on Windows.
|
||||
- `emulator/mod/bbsc_http.cpp`: 0.0.2's overrides file list (`HostOverridesFiles`) is Windows
|
||||
only; on Linux the online module still applies the overrides, and the relay keeps its own list.
|
||||
- `emulator/mod/bbsc_relay.cpp`: a send to a closed link fails instead of raising SIGPIPE
|
||||
(`MSG_NOSIGNAL`), and the writer's 10 s send timeout is set as a `timeval`.
|
||||
- `emulator/mod/linux/bbsc_win32.h`: `WSAGetLastError` and `SD_BOTH`, which the relay uses now.
|
||||
- **Translations:** the new texts ("An item was added to your items." and the watch camera's) in
|
||||
every language. `bbsc_i18n.cpp` also translates a text with numbers through its `{n}` /
|
||||
`{count}` form ("Watching hunter 1 of 2.").
|
||||
- **Not taken:**
|
||||
- BBSC's Discord status (`bbsc_discord.cpp`): bbport has its own; the build and boss timer data
|
||||
are gathered but not shown.
|
||||
- the files of BBSC's own emulator build (`emulator/src/core/bbsc_relay_client.cpp`, the
|
||||
emulator's network, signaling, pad and module files, `emulator/CMakeLists.txt`, the launcher's
|
||||
`emulator_settings.h`) and the patch for BBport's Windows build (`bbport/`): bbport builds
|
||||
none of them, and the game logic here does not need them.
|
||||
|
||||
## 2026-10-23
|
||||
|
||||
- **Documentation.** `README.md`, `server/README.md` (in place of `server/README.txt`) and
|
||||
`server/gameserver/README.md` describe BBSC as this port builds, runs and hosts it.
|
||||
- **Windows host package removed.** The `.cmd` scripts and `server/tools/*.ps1` set up BBSC's
|
||||
Windows server package; this port hosts with `tools/shadnet-server`, `tools/bbsc-server` and
|
||||
`docs/SERVER.md`.
|
||||
- **SUPPORTERS.txt** keeps its heading and list, without the donation links.
|
||||
@@ -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,38 +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,
|
||||
and the fixed Chalice Dungeon data (NoDreamForTheHunterBB, published by Mob)
|
||||
https://github.com/Mob100percent/NoDreamForTheHunterBB
|
||||
|
||||
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,63 +0,0 @@
|
||||
# BBSC: Bloodborne Seamless Co-op
|
||||
|
||||
[BBSC](https://github.com/Classmg22/BBSC) by CLS_AMG keeps a co-op session together through deaths,
|
||||
lamps, area changes, bosses and the Hunter's Dream, for up to five hunters who each keep their own
|
||||
character. This folder holds BBSC 0.0.2 built for this port: the port's runtime loads it into the
|
||||
game as `libbbsc.so`.
|
||||
|
||||
## What it does
|
||||
|
||||
- The session survives deaths, lamps, areas, bosses and trips to the Hunter's Dream; guests follow
|
||||
the host and come back with the group.
|
||||
- Up to five hunters: the host and four guests.
|
||||
- Everyone keeps their own levels, echoes and items; the host's boss kills and shortcuts can be
|
||||
shared with the guests' worlds.
|
||||
- Enemies get stronger for every guest.
|
||||
- A guest who dies watches a partner until the group comes back, sees the host's time of night,
|
||||
and can ring the co-op bells anywhere.
|
||||
- Its own bells, with no Insight cost: the **Hunt Bell** (call others), the **Kin Bell** (join
|
||||
someone), the **Bloodlust Bell** (invade), the **Duel Bell** (PvP on or off) and the **Parting
|
||||
Blank** (end the session).
|
||||
|
||||
## Using it
|
||||
|
||||
In the launcher's **Multiplayer → Online** tab, turn on BBSC in the **Seamless co-op (BBSC)** group;
|
||||
**BBSC settings** holds its options. Every player in the session needs BBSC, the same version. How much of it works
|
||||
depends on the server (Seamless Hunt, a group's own server, or The Hunter's Dream): see
|
||||
[docs/ONLINE.md](../docs/ONLINE.md#seamless-co-op-bbsc). To host a server for your group, see
|
||||
[server/](server/README.md).
|
||||
|
||||
## What is in this folder
|
||||
|
||||
| Path | Contents |
|
||||
|---|---|
|
||||
| `emulator/src/core/` | BBSC's game code (`bloodborne_re.cpp`: the session, the bells, world sync) |
|
||||
| `emulator/mod/` | The mod around it: relay, network channel, HTTP, settings (`mod.json`) |
|
||||
| `emulator/mod/linux/` | The Linux side, written for this port: start-up, hooks, the Win32 names the mod uses |
|
||||
| `server/` | Hosting a server: BBSC's game server and shadNet's settings |
|
||||
| `translations/` | BBSC's texts in the game's languages |
|
||||
| `third_party/` | fmt and xbyak |
|
||||
| `CHANGES-bbport.md` | Every change this port made to BBSC, by date |
|
||||
|
||||
`build.sh` builds it into `out/bbsc/libbbsc.so`; on its own:
|
||||
|
||||
```sh
|
||||
cmake -S bbsc -B out/bbsc -G Ninja -DCMAKE_BUILD_TYPE=Release
|
||||
ninja -C out/bbsc
|
||||
```
|
||||
|
||||
`LICENSE.txt`, `NOTICE.txt` and `SUPPORTERS.txt` are copied next to `libbbsc.so`, and BBSC stays off
|
||||
without them, as it does in its own builds.
|
||||
|
||||
## Reporting problems
|
||||
|
||||
In this repository's [issues](https://github.com/ayozetr/bloodborne_linux/issues), with the game's
|
||||
log from the launcher's **Log** page. Problems that also happen in BBSC's own builds belong in
|
||||
[BBSC's repository](https://github.com/Classmg22/BBSC).
|
||||
|
||||
## License
|
||||
|
||||
GPL-2.0-or-later (`LICENSE`, `NOTICE.md`); the third-party parts keep their own licenses. BBSC's
|
||||
credits are in `CREDITS.txt`, and this port's in [docs/CREDITS.md](../docs/CREDITS.md).
|
||||
Bloodborne is by FromSoftware and Sony Interactive Entertainment; BBSC is a fan project, not
|
||||
affiliated with either.
|
||||
@@ -1,21 +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.
|
||||
|
||||
|
||||
WORKSHOP
|
||||
--------
|
||||
aboood888848
|
||||
Prohorov Ilja
|
||||
|
||||
|
||||
HUNTER
|
||||
------
|
||||
.ζ͜长𐌏𐌕𐌏ᚱ𐌆
|
||||
|
||||
|
||||
LAMPLIGHT
|
||||
---------
|
||||
(no one yet)
|
||||
@@ -1,371 +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
|
||||
bool ReadSlotSafely(u64 address, u64* value); // bbsc_mod.cpp: no fault if unreadable
|
||||
bool WriteSlotSafely(u64 address, u64 value);
|
||||
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) {
|
||||
const u64 slot = BbscMod::EbootAddress() + slot_offset;
|
||||
for (int i = 0; i < 60'000; ++i) {
|
||||
u64 value = 0;
|
||||
if (BbscMod::ReadSlotSafely(slot, &value) && value != 0 &&
|
||||
(value < BbscMod::EbootAddress() || value >= BbscMod::EbootAddress() + EbootSize)) {
|
||||
original = reinterpret_cast<Fn>(value);
|
||||
if (!BbscMod::WriteSlotSafely(slot, reinterpret_cast<u64>(wrapper))) {
|
||||
Sleep(1);
|
||||
continue; // page not writable right now: try again
|
||||
}
|
||||
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) {
|
||||
u64 value = 0;
|
||||
if (!BbscMod::ReadSlotSafely(BbscMod::EbootAddress() + slot_offset, &value) || 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,287 +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 - Arcane build (2 of 5)", the
|
||||
// boss and a fight timer during a boss fight, "Dead - watching hunter 1 of 2" while spectating.
|
||||
// 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.
|
||||
// 2026-10-20: compiled into the emulator build too (before, only the mod showed it - the shadPS4
|
||||
// package had only shadPS4's own "Playing Bloodborne"); the emulator then leaves its own status off.
|
||||
|
||||
#include "core/bloodborne_re.h"
|
||||
|
||||
#ifdef _WIN32
|
||||
|
||||
#include <windows.h>
|
||||
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <thread>
|
||||
|
||||
#include <fmt/format.h>
|
||||
|
||||
#include "common/logging/log.h"
|
||||
#include "core/bbsc_persistence.h"
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr const char* DiscordApplicationId = "1554935855674691645"; // "BBSC" (CLS_AMG)
|
||||
|
||||
std::atomic<bool> discord_running{false};
|
||||
|
||||
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
|
||||
LOG_INFO(Debug, "Discord status: Discord closed the connection: {}", body);
|
||||
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()) {
|
||||
// The timer counts from when the boss bar appeared (Discord shows it as "mm:ss elapsed").
|
||||
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";
|
||||
if (p.dead) {
|
||||
if (p.spectating && p.watch_count > 0) {
|
||||
state += fmt::format(" - dead, watching hunter {} of {}", p.watching, p.watch_count);
|
||||
} else if (p.spectating) {
|
||||
state += " - dead, spectating";
|
||||
} else {
|
||||
state += " - dead";
|
||||
}
|
||||
} else if (!p.build.empty()) {
|
||||
state += " - " + p.build;
|
||||
}
|
||||
if (!p.build.empty() && (p.dead || !p.boss.empty())) {
|
||||
large_text += " - " + p.build;
|
||||
}
|
||||
}
|
||||
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) {
|
||||
LOG_INFO(Debug, "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 Core::Bbsc {
|
||||
|
||||
bool DiscordStatusRunning() {
|
||||
return discord_running.load();
|
||||
}
|
||||
|
||||
void StartDiscordStatus(std::string_view serial) {
|
||||
#ifndef BBSC_MOD
|
||||
// The emulator build: only for the game BBSC knows, and only while its own BBSC runs (the mod
|
||||
// shows the status itself when bbsc-launch switches these off).
|
||||
if (serial != "CUSA03173" || !SwitchEnabled("SHADPS4_BLOODBORNE_RE_TRACE") ||
|
||||
!SwitchEnabled("SHADPS4_BBSC_PERSIST")) {
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
if (!DiscordStatusEnabled()) {
|
||||
LOG_INFO(Debug, "Discord status: off (Mods tab)");
|
||||
return;
|
||||
}
|
||||
if (discord_running.exchange(true)) {
|
||||
return;
|
||||
}
|
||||
if (HANDLE thread = CreateThread(nullptr, 0, DiscordThread, nullptr, 0, nullptr)) {
|
||||
CloseHandle(thread);
|
||||
LOG_INFO(Debug, "Discord status: on - shows when the Discord app is running");
|
||||
} else {
|
||||
discord_running = false;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace Core::Bbsc
|
||||
|
||||
#else // not Windows: no Discord pipe
|
||||
|
||||
namespace Core::Bbsc {
|
||||
bool DiscordStatusRunning() {
|
||||
return false;
|
||||
}
|
||||
void StartDiscordStatus(std::string_view) {}
|
||||
} // namespace Core::Bbsc
|
||||
|
||||
#endif
|
||||
|
||||
#ifdef BBSC_MOD
|
||||
namespace BbscMod {
|
||||
void StartDiscordStatus() {
|
||||
Core::Bbsc::StartDiscordStatus("CUSA03173");
|
||||
}
|
||||
} // namespace BbscMod
|
||||
#endif
|
||||
@@ -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,853 +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 request (a host ringing one sign): the host's placement header, AND the same text
|
||||
// through the BBSC relay aimed at that sign's UserId only (TargetUserId of the claim) - 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::OnAimedHostPlacement).
|
||||
// 0.0.1 hotfix (2026-10-13): the search (/get) goes out unchanged. A search with a placement made the
|
||||
// broker lock every sign outside the host's map ("Preparing") for that host, and the 0.0.1
|
||||
// relay broadcast made waiting guests follow random hosts - on the public server nobody's
|
||||
// Kin Bell sign could be rung.
|
||||
// - 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 <cstdlib>
|
||||
#include <cstring>
|
||||
#include <filesystem>
|
||||
#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"
|
||||
|
||||
// One line in this run's BBSC timeline (bloodborne_re.cpp, "Timeline").
|
||||
#define BBSC_TIMELINE(kind, ...) Core::Bloodborne::BbscTimelineEvent(kind, fmt::format(__VA_ARGS__))
|
||||
|
||||
namespace BbscMod {
|
||||
void RelaySendAimedPlacement(s64 target_user, const std::string& placement);
|
||||
u64 EbootAddress(); // bbsc_mod.cpp: where the game image is (0 until found)
|
||||
bool ReadSlotSafely(u64 address, u64* value); // bbsc_mod.cpp: no fault if unreadable
|
||||
bool WriteSlotSafely(u64 address, u64 value);
|
||||
void GuardSlot(u64 slot_offset, u64 wrapper, void** original, const char* name); // bbsc_mod.cpp
|
||||
std::vector<std::wstring> HostOverridesFiles(); // below: the emulator's overrides file candidates
|
||||
}
|
||||
|
||||
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;
|
||||
// An aimed placement (0.0.1 hotfix) comes once per claim; the game re-posts its sign about every 30 s,
|
||||
// so it has to outlive that re-post.
|
||||
constexpr s64 AimedPlacementStaleMs = 90'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};
|
||||
std::atomic<s64> own_user_id{0}; // UserId of our own sign (from its /create body)
|
||||
std::atomic<bool> placement_aimed{false}; // the placement came aimed at our sign
|
||||
|
||||
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;
|
||||
}
|
||||
for (const auto& path : BbscMod::HostOverridesFiles()) {
|
||||
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 {}", overrides.size(),
|
||||
std::filesystem::path(path).string());
|
||||
BBSC_TIMELINE("START", "HTTP: {} URL override(s) from {}", overrides.size(),
|
||||
std::filesystem::path(path).string());
|
||||
return;
|
||||
}
|
||||
}
|
||||
LOG_INFO(Mod, "HTTP: no host_overrides.json - URLs unchanged");
|
||||
BBSC_TIMELINE("START", "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 IsSummonClaim(const std::string& url) {
|
||||
return Contains(url, "/summon_messenger/request");
|
||||
}
|
||||
bool IsSummonSearch(const std::string& url) {
|
||||
return 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);
|
||||
BBSC_TIMELINE("JOIN", "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);
|
||||
BBSC_TIMELINE("JOIN", "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");
|
||||
BBSC_TIMELINE("JOIN", "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);
|
||||
BBSC_TIMELINE("JOIN", "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();
|
||||
u64 value = 0;
|
||||
if (!BbscMod::ReadSlotSafely(base + slot_offset, &value)) {
|
||||
return 0;
|
||||
}
|
||||
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();
|
||||
// Fewer messages / bloodstains per list (Panic 265 at Gascoigne / Cathedral Ward, see
|
||||
// CapBbscAreaListRequest). Not on public servers: their API calls go out unchanged.
|
||||
if (Contains(url, "/message_area") && !IsPublicServer(url) && data != nullptr && size != 0) {
|
||||
if (const u64 length = CurrentSlot(SlotSetContentLength); length != 0) {
|
||||
original_set_content_length = reinterpret_cast<SetContentLengthFn>(length);
|
||||
}
|
||||
std::string body(static_cast<const char*>(data), static_cast<size_t>(size));
|
||||
const std::string changed = Core::Bloodborne::CapBbscAreaListRequest(url, body);
|
||||
if (!changed.empty() && original_set_content_length != nullptr) {
|
||||
original_set_content_length(request, body.size());
|
||||
LOG_INFO(Mod, "HTTP: {}", changed);
|
||||
// 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);
|
||||
}
|
||||
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)");
|
||||
BBSC_TIMELINE("NET", "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 (IsSummonClaim(url)) {
|
||||
// The host rang one sign: the broker gets our placement with the claim and only that
|
||||
// sign's player hears it over the relay (it moves its sign into our map, then the broker
|
||||
// delivers). Without a target nothing is sent - a claim with a placement nobody follows
|
||||
// would stay "Claimed" until it times out.
|
||||
const std::string body = data != nullptr && size != 0
|
||||
? std::string(static_cast<const char*>(data),
|
||||
static_cast<size_t>(size))
|
||||
: std::string{};
|
||||
const std::string target_text = JsonNumber(body, "TargetUserId");
|
||||
const s64 target = target_text.empty() ? 0 : std::atoll(target_text.c_str());
|
||||
const auto placement = Core::Bloodborne::GetSeamlessHostPlacementHeader();
|
||||
if (target > 0 && placement) {
|
||||
original_add_header(request, "X-ShadPS4-Bloodborne-Host-Placement", placement->c_str(),
|
||||
0);
|
||||
BbscMod::RelaySendAimedPlacement(target, *placement);
|
||||
LOG_INFO(Mod, "HTTP: ringing the sign of user {} - our placement {} sent to it", target,
|
||||
*placement);
|
||||
BBSC_TIMELINE("JOIN", "HTTP: ringing the sign of user {} - our placement {} sent to it", target,
|
||||
*placement);
|
||||
} else {
|
||||
LOG_INFO(Mod, "HTTP: ringing a sign - no placement sent (target user {}, placement {})",
|
||||
target_text.empty() ? std::string("unknown") : target_text,
|
||||
placement ? "known" : "none");
|
||||
BBSC_TIMELINE("JOIN", "HTTP: ringing a sign - no placement sent (target user {}, placement {})",
|
||||
target_text.empty() ? std::string("unknown") : target_text,
|
||||
placement ? "known" : "none");
|
||||
}
|
||||
return original_send(request, data, size);
|
||||
}
|
||||
if (IsSummonSearch(url)) {
|
||||
return original_send(request, data, size); // searches go out unchanged (0.0.1 hotfix)
|
||||
}
|
||||
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);
|
||||
if (const std::string user = JsonNumber(body, "UserId"); !user.empty()) {
|
||||
const s64 id = std::atoll(user.c_str());
|
||||
if (own_user_id.exchange(id) != id) {
|
||||
LOG_INFO(Mod, "HTTP: our sign's user id is {}", id);
|
||||
}
|
||||
}
|
||||
// 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();
|
||||
const s64 stale = placement_aimed.load() ? AimedPlacementStaleMs : PlacementStaleMs;
|
||||
if (received != 0 && NowMs() - received > stale) {
|
||||
placement_received_ms = 0;
|
||||
placement_aimed = false;
|
||||
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);
|
||||
BBSC_TIMELINE("JOIN", "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) {
|
||||
const u64 slot = BbscMod::EbootAddress() + slot_offset;
|
||||
for (int i = 0; i < 60'000; ++i) {
|
||||
u64 value = 0;
|
||||
if (BbscMod::ReadSlotSafely(slot, &value) && value != 0 &&
|
||||
(value < BbscMod::EbootAddress() || value >= BbscMod::EbootAddress() + EbootSize)) {
|
||||
original = reinterpret_cast<Fn>(value);
|
||||
if (!BbscMod::WriteSlotSafely(slot, reinterpret_cast<u64>(wrapper))) {
|
||||
Sleep(1);
|
||||
continue; // page not writable right now: try again
|
||||
}
|
||||
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 u64 slot = BbscMod::EbootAddress() + slot_offset;
|
||||
for (int i = 0; i < 60'000; ++i) {
|
||||
u64 value = 0;
|
||||
if (BbscMod::ReadSlotSafely(slot, &value) && value != 0 &&
|
||||
(value < BbscMod::EbootAddress() || value >= BbscMod::EbootAddress() + EbootSize)) {
|
||||
return value;
|
||||
}
|
||||
Sleep(1);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
namespace BbscMod {
|
||||
|
||||
#ifdef _WIN32
|
||||
// The emulator's overrides file, in the order the emulator looks for it:
|
||||
// 1. SHADPS4_HTTP_HOST_OVERRIDES_JSON - BBport (run.py) passes the file it uses this way: its own
|
||||
// user\host_overrides.custom.json when present, else a user\host_overrides.json it writes for
|
||||
// The Hunter's Dream. 0.0.1 read only the second -> BBport players with our server in the
|
||||
// custom file ran BBSC in public mode (vanilla summons, no relay) and could not join anyone
|
||||
// (2026-10-13 public reports).
|
||||
// 2. user\host_overrides.custom.json (BBport layout, if the variable is missing).
|
||||
// 3. user\host_overrides.json in the start folder / beside the exe (portable), then %APPDATA%.
|
||||
std::vector<std::wstring> HostOverridesFiles() {
|
||||
std::vector<std::wstring> files;
|
||||
if (const wchar_t* env = _wgetenv(L"SHADPS4_HTTP_HOST_OVERRIDES_JSON");
|
||||
env != nullptr && env[0] != L'\0') {
|
||||
files.emplace_back(env);
|
||||
}
|
||||
wchar_t exe[MAX_PATH]{};
|
||||
GetModuleFileNameW(nullptr, exe, MAX_PATH);
|
||||
std::wstring dir{exe};
|
||||
dir = dir.substr(0, dir.find_last_of(L"\\/") + 1);
|
||||
wchar_t cwd[MAX_PATH]{};
|
||||
GetCurrentDirectoryW(MAX_PATH, cwd);
|
||||
const std::wstring start = std::wstring(cwd) + L"\\";
|
||||
for (const std::wstring& base : {start, dir, dir + L"..\\"}) {
|
||||
files.push_back(base + L"user\\host_overrides.custom.json");
|
||||
}
|
||||
files.push_back(start + L"user\\host_overrides.json");
|
||||
files.push_back(dir + L"user\\host_overrides.json");
|
||||
if (const wchar_t* appdata = _wgetenv(L"APPDATA")) {
|
||||
files.push_back(std::wstring(appdata) + L"\\shadPS4\\host_overrides.json");
|
||||
}
|
||||
return files;
|
||||
}
|
||||
#endif
|
||||
|
||||
// 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) {
|
||||
// Older hosts sent their placement to everyone whenever they searched; waiting guests then
|
||||
// followed random hosts. Now we only follows a placement aimed at our own sign.
|
||||
static std::atomic<bool> logged{false};
|
||||
if (!logged.exchange(true)) {
|
||||
LOG_INFO(Mod, "HTTP: placement broadcast from {:08x} ignored (an old host - it must "
|
||||
"update BBSC): {}",
|
||||
sender, placement);
|
||||
BBSC_TIMELINE("JOIN", "HTTP: placement broadcast from {:08x} ignored (an old host - it must "
|
||||
"update BBSC): {}",
|
||||
sender, placement);
|
||||
}
|
||||
}
|
||||
|
||||
// A host rang our sign (0.0.1 hotfix): its placement, aimed at our sign's UserId. The next post of our
|
||||
// sign moves it into the host's map and the broker delivers the claim.
|
||||
void OnAimedHostPlacement(s64 target_user, const std::string& placement, u32 sender) {
|
||||
const s64 own = own_user_id.load();
|
||||
if (own == 0 || target_user != own) {
|
||||
return; // another player's sign was rung
|
||||
}
|
||||
const s32 state = SessionState();
|
||||
if (state == 3 || state == 4 || state == 6) {
|
||||
return; // hosting, joining or already a guest
|
||||
}
|
||||
// Its world flags are taken before the join load (bloodborne_re.cpp, "the host that rang").
|
||||
Core::Bloodborne::NoteBbscHostRangOurSign(sender);
|
||||
if (Core::Bloodborne::SetSeamlessHostPlacementHeader(placement)) {
|
||||
placement_sender = sender;
|
||||
placement_aimed = true;
|
||||
placement_received_ms = NowMs();
|
||||
LOG_INFO(Mod, "HTTP: host {:08x} rang our sign (user {}) - placement {}", sender, own,
|
||||
placement);
|
||||
BBSC_TIMELINE("JOIN", "HTTP: host {:08x} rang our sign (user {}) - placement {}", sender, own,
|
||||
placement);
|
||||
}
|
||||
}
|
||||
|
||||
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");
|
||||
BBSC_TIMELINE("START", "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);
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1,570 +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 <condition_variable>
|
||||
#include <cstdlib>
|
||||
#include <deque>
|
||||
#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);
|
||||
void OnAimedHostPlacement(s64 target_user, const std::string& placement, u32 sender);
|
||||
bool IsPublicServerAddress(const std::string& url_or_host); // bbsc_http.cpp
|
||||
std::vector<std::wstring> HostOverridesFiles(); // 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 (before the hotfix, ignored now)
|
||||
// "SHAD" 0x31 + "<target UserId>|<placement>": a host rang that sign (0.0.1 hotfix, bbsc_http.cpp)
|
||||
constexpr u8 AimedPlacementType = 0x31;
|
||||
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;
|
||||
// Frames wait here for the writer thread (2026-10-13): a public player's link dropped exactly 10 s
|
||||
// after every reconnect while hosting - the 1/s world flags filled the socket while the server
|
||||
// was stuck, and the game thread's 300 ms send timeout closed the link. Now the game thread only
|
||||
// queues; a full queue drops frames instead of the link. All under send_mutex.
|
||||
std::condition_variable send_cv;
|
||||
std::deque<std::vector<char>> send_queue;
|
||||
size_t send_queue_bytes = 0;
|
||||
constexpr size_t SendQueueLimit = 256 * 1024;
|
||||
SOCKET writing = INVALID_SOCKET; // the socket the writer is sending on right now
|
||||
u32 dropped_frames = 0; // since this connection began
|
||||
std::string lost_reason; // why the writer gave up on this connection
|
||||
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());
|
||||
Core::Bloodborne::BbscTimelineEvent("RELAY", text); // status changes only, see the callers
|
||||
}
|
||||
|
||||
#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
|
||||
for (const auto& path : BbscMod::HostOverridesFiles()) {
|
||||
#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);
|
||||
}
|
||||
for (const auto& path : candidates) {
|
||||
#endif
|
||||
host = HostFromOverrides(ReadFile(path));
|
||||
if (!host.empty()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
int last_read_result = 0; // 0 = the server closed the link, < 0 = recv error (relay thread only)
|
||||
|
||||
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) {
|
||||
last_read_result = got == 0 ? 0 : -WSAGetLastError();
|
||||
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);
|
||||
}
|
||||
if (send_queue_bytes + frame.size() > SendQueueLimit) {
|
||||
if (dropped_frames++ == 0) {
|
||||
Log("the server is not taking our frames (" + std::to_string(send_queue_bytes) +
|
||||
" bytes waiting) - dropping new ones until it does");
|
||||
}
|
||||
return static_cast<u32>(peers.load());
|
||||
}
|
||||
send_queue_bytes += frame.size();
|
||||
send_queue.push_back(std::move(frame));
|
||||
send_cv.notify_all();
|
||||
return static_cast<u32>(peers.load());
|
||||
}
|
||||
|
||||
std::string SocketError(const char* what) {
|
||||
return std::string(what) + " (WSA error " + std::to_string(WSAGetLastError()) + ")";
|
||||
}
|
||||
|
||||
// Sends the queued frames, one socket at a time. A failed send shuts the socket down; the relay
|
||||
// thread sees that in recv and reconnects.
|
||||
DWORD WINAPI WriterThread(void*) {
|
||||
for (;;) {
|
||||
std::vector<char> frame;
|
||||
SOCKET s = INVALID_SOCKET;
|
||||
{
|
||||
std::unique_lock lock{send_mutex};
|
||||
send_cv.wait(lock, [] { return !send_queue.empty(); });
|
||||
frame = std::move(send_queue.front());
|
||||
send_queue.pop_front();
|
||||
send_queue_bytes -= frame.size();
|
||||
s = relay;
|
||||
if (s == INVALID_SOCKET) {
|
||||
continue;
|
||||
}
|
||||
writing = s;
|
||||
}
|
||||
std::string problem;
|
||||
int left = static_cast<int>(frame.size());
|
||||
const char* at = frame.data();
|
||||
while (left > 0) {
|
||||
#ifdef _WIN32
|
||||
const int sent = send(s, at, left, 0);
|
||||
#else
|
||||
// bbport: a link the server closed fails the send instead of raising SIGPIPE, which
|
||||
// would end the game's process.
|
||||
const int sent = static_cast<int>(send(s, at, left, MSG_NOSIGNAL));
|
||||
#endif
|
||||
if (sent <= 0) {
|
||||
problem = SocketError("send failed");
|
||||
break;
|
||||
}
|
||||
at += sent;
|
||||
left -= sent;
|
||||
}
|
||||
std::scoped_lock lock{send_mutex};
|
||||
writing = INVALID_SOCKET;
|
||||
if (!problem.empty() && relay == s) {
|
||||
if (lost_reason.empty()) {
|
||||
lost_reason = problem;
|
||||
}
|
||||
shutdown(s, SD_BOTH);
|
||||
}
|
||||
send_cv.notify_all();
|
||||
}
|
||||
}
|
||||
|
||||
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] == AimedPlacementType) {
|
||||
const std::string text(frame.begin() + 5, frame.end());
|
||||
const size_t bar = text.find('|');
|
||||
if (bar != std::string::npos && bar != 0) {
|
||||
BbscMod::OnAimedHostPlacement(std::atoll(text.substr(0, bar).c_str()),
|
||||
text.substr(bar + 1), 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));
|
||||
setsockopt(s, SOL_SOCKET, SO_KEEPALIVE, reinterpret_cast<const char*>(&on), sizeof(on));
|
||||
// Only the writer thread sends now (2026-10-13), so a slow server no longer stalls the
|
||||
// game; a send stuck 10 s means the link is dead.
|
||||
#ifdef _WIN32
|
||||
const DWORD send_timeout_ms = 10'000;
|
||||
setsockopt(s, SOL_SOCKET, SO_SNDTIMEO, reinterpret_cast<const char*>(&send_timeout_ms),
|
||||
sizeof(send_timeout_ms));
|
||||
#else
|
||||
const timeval send_timeout{10, 0}; // bbport: SO_SNDTIMEO takes a timeval here
|
||||
setsockopt(s, SOL_SOCKET, SO_SNDTIMEO, &send_timeout, sizeof(send_timeout));
|
||||
#endif
|
||||
{
|
||||
std::scoped_lock lock{send_mutex};
|
||||
relay = s;
|
||||
lost_reason.clear();
|
||||
dropped_frames = 0;
|
||||
}
|
||||
last_problem.clear();
|
||||
Log("connected to " + host + ":" + std::to_string(port));
|
||||
const u64 connected_at = GetTickCount64();
|
||||
u64 frames_in = 0;
|
||||
std::string reason;
|
||||
for (;;) {
|
||||
u32 size = 0;
|
||||
if (!ReadAll(s, &size, 4)) {
|
||||
break;
|
||||
}
|
||||
if (size > 64 * 1024) {
|
||||
reason = "oversized frame from the server (" + std::to_string(size) + " bytes)";
|
||||
break;
|
||||
}
|
||||
std::vector<u8> frame(size);
|
||||
if (size != 0 && !ReadAll(s, frame.data(), static_cast<int>(size))) {
|
||||
break;
|
||||
}
|
||||
++frames_in;
|
||||
std::scoped_lock dispatch_lock{dispatch_mutex};
|
||||
Dispatch(frame);
|
||||
}
|
||||
u32 dropped = 0;
|
||||
{
|
||||
std::unique_lock lock{send_mutex};
|
||||
if (!lost_reason.empty()) {
|
||||
reason = lost_reason; // the writer gave up first
|
||||
}
|
||||
relay = INVALID_SOCKET;
|
||||
shutdown(s, SD_BOTH); // wakes a writer still in send() on it
|
||||
send_cv.wait(lock, [s] { return writing != s; });
|
||||
closesocket(s);
|
||||
send_queue.clear();
|
||||
send_queue_bytes = 0;
|
||||
dropped = dropped_frames;
|
||||
}
|
||||
if (reason.empty()) {
|
||||
reason = last_read_result == 0
|
||||
? std::string("the server closed the connection")
|
||||
: "read failed (WSA error " + std::to_string(-last_read_result) + ")";
|
||||
}
|
||||
peers = 0;
|
||||
Log("connection lost after " + std::to_string((GetTickCount64() - connected_at) / 1000) +
|
||||
" s, " + std::to_string(frames_in) + " frames in, " + std::to_string(dropped) +
|
||||
" dropped out: " + reason + " - 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 RelaySendAimedPlacement(s64 target_user, const std::string& placement) {
|
||||
std::string frame = "SHAD";
|
||||
frame.push_back(static_cast<char>(AimedPlacementType));
|
||||
frame += std::to_string(target_user) + "|" + 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);
|
||||
}
|
||||
if (HANDLE thread = CreateThread(nullptr, 0, WriterThread, 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,788 +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). Only BBSC 0.0.2's watch camera of a dead guest
|
||||
// is, with its keys read through SDL (see "Watch camera" below).
|
||||
|
||||
#include <dlfcn.h>
|
||||
#include <sys/mman.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <atomic>
|
||||
#include <cstdarg>
|
||||
#include <cmath>
|
||||
#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;
|
||||
|
||||
// One line in this run's BBSC timeline (bloodborne_re.cpp, "Timeline").
|
||||
#define BBSC_TIMELINE(kind, ...) Core::Bloodborne::BbscTimelineEvent(kind, fmt::format(__VA_ARGS__))
|
||||
|
||||
// Keep it equal to "version" in mod.json and to the Windows mod it plays with.
|
||||
#define BBSC_VERSION "0.0.2"
|
||||
|
||||
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();
|
||||
// bbport: a first start has no saves yet, and the kit was then never given: the folder
|
||||
// the game will write (user 1, src/runtime_services.c; BB_SAVEDATA_TITLE from the runtime).
|
||||
const char* title = std::getenv("BB_SAVEDATA_TITLE");
|
||||
if (newest.empty() && title != nullptr && *title != '\0' && std::strcmp(title, "UNKNOWN") != 0) {
|
||||
newest = user_dir / "savedata" / "1" / title / "SPRJ0005";
|
||||
}
|
||||
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 / "logs", ".txt", 4); // BBSC 0.0.2's timelines (bbsc-timeline-*.txt)
|
||||
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; built " __DATE__
|
||||
" " __TIME__ ")");
|
||||
if (!NoticesPresent()) {
|
||||
LOG_ERROR(Mod, "LICENSE.txt / NOTICE.txt / SUPPORTERS.txt missing or changed in {} - BBSC "
|
||||
"is off",
|
||||
mod_dir.string());
|
||||
BBSC_TIMELINE("START", "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);
|
||||
BBSC_TIMELINE("START", "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);
|
||||
BBSC_TIMELINE("START", "No Bloodborne CUSA03173 01.09 image after {} s - not this game "
|
||||
"or not this version; nothing was changed",
|
||||
WaitForImageMs / 1000);
|
||||
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);
|
||||
BBSC_TIMELINE("START", "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;
|
||||
}
|
||||
|
||||
// BBSC 0.0.2's safe import slot reads and writes (bbsc_http.cpp, bbsc_channel.cpp): on Windows a
|
||||
// slot page could be unreadable while the emulator linked the imports. Here the slot is only
|
||||
// touched while bbport's map says it is readable, as everything else this library reads.
|
||||
bool ReadSlotSafely(u64 address, u64* value) {
|
||||
if (!Readable(address, sizeof(u64))) {
|
||||
return false;
|
||||
}
|
||||
*value = *reinterpret_cast<const volatile u64*>(address);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool WriteSlotSafely(u64 address, u64 value) {
|
||||
if (!Readable(address, sizeof(u64))) {
|
||||
return false;
|
||||
}
|
||||
DWORD old = 0;
|
||||
if (!VirtualProtect(reinterpret_cast<void*>(address), 8, PAGE_READWRITE, &old)) {
|
||||
return false;
|
||||
}
|
||||
*reinterpret_cast<volatile u64*>(address) = value;
|
||||
return true;
|
||||
}
|
||||
|
||||
// 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) {
|
||||
const u64 slot = EbootBase + guarded.slot_offset;
|
||||
u64 value = 0;
|
||||
if (!ReadSlotSafely(slot, &value)) {
|
||||
continue;
|
||||
}
|
||||
if (value == guarded.wrapper || value == 0 ||
|
||||
(value >= EbootBase && value < EbootBase + EbootSize)) {
|
||||
continue;
|
||||
}
|
||||
*reinterpret_cast<void* volatile*>(guarded.original) = reinterpret_cast<void*>(value);
|
||||
if (!WriteSlotSafely(slot, guarded.wrapper)) {
|
||||
continue; // the next round tries again
|
||||
}
|
||||
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);
|
||||
}
|
||||
|
||||
// ---- Watch camera (BBSC 0.0.2) -------------------------------------------------------------------
|
||||
// The F1 free camera is not in the Linux build: the Windows mod reads the keyboard and mouse for it
|
||||
// and hands the game a controller at rest, while here every key and button also reaches the game
|
||||
// through bbport's own pad (src/runtime_pad.c: WASD move, E / Q / F / Tab are game buttons) and
|
||||
// L3+R3 opens bbport's menu - flying the camera would move the hunter too. BBSC 0.0.2's watch
|
||||
// camera is different: it only comes on by itself while our hunter is dead in co-op (bloodborne_re.cpp,
|
||||
// CinematicForceOn after the death has played out), when the game has nothing to do with those
|
||||
// keys. So that camera is here, read through SDL like the F8 key (the game's window already uses
|
||||
// it), on the game thread, and only while the camera is on:
|
||||
// Tab / E / left click next hunter, Shift+Tab / Q previous, F fly freely and back; the
|
||||
// controller's R1 / L1 and right stick (left stick: closer / farther); arrow keys turn, W / S
|
||||
// closer / farther; flying freely: WASD, Space / E up, C / Q down, Shift fast, Alt slow.
|
||||
// F1 turns it off for the rest of that death (it is bbport's "let the mouse go" key too).
|
||||
// Mouse movement is not read: SDL's relative mouse state is bbport's own mouse look.
|
||||
namespace {
|
||||
void* WatchSdl(const char* name) {
|
||||
return dlsym(RTLD_DEFAULT, name);
|
||||
}
|
||||
|
||||
// SDL3 scancodes and gamepad enums (SDL_scancode.h, SDL_gamepad.h).
|
||||
enum : int {
|
||||
ScA = 4, ScC = 6, ScD = 7, ScE = 8, ScF = 9, ScQ = 20, ScS = 22, ScW = 26, ScTab = 43,
|
||||
ScSpace = 44, ScF1 = 58, ScRight = 79, ScLeft = 80, ScDown = 81, ScUp = 82, ScLShift = 225,
|
||||
ScLAlt = 226, ScRShift = 229, ScRAlt = 230, ScCount = 231
|
||||
};
|
||||
constexpr int PadL1 = 9, PadR1 = 10; // SDL_GAMEPAD_BUTTON_LEFT / RIGHT_SHOULDER
|
||||
constexpr int AxisLY = 1, AxisRX = 2, AxisRY = 3; // SDL_GAMEPAD_AXIS_*
|
||||
|
||||
std::mutex watch_mutex;
|
||||
bool watch_on = false; // switched on by the game (a death in co-op), off by F1 or respawn
|
||||
bool watch_watching = false; // the game says it is watching a hunter
|
||||
std::array<bool, ScCount> watch_keys_before{};
|
||||
bool watch_click_before = false;
|
||||
bool watch_l1_before = false, watch_r1_before = false;
|
||||
|
||||
float WatchAxis(int value) {
|
||||
const float v = static_cast<float>(value) / 32767.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);
|
||||
}
|
||||
|
||||
// The first controller the game's runtime has open (SDL_GetGamepadFromID gives only open ones).
|
||||
void* WatchGamepad() {
|
||||
using List = unsigned* (*)(int*);
|
||||
using FromId = void* (*)(unsigned);
|
||||
using Free = void (*)(void*);
|
||||
static const auto list = reinterpret_cast<List>(WatchSdl("SDL_GetGamepads"));
|
||||
static const auto from_id = reinterpret_cast<FromId>(WatchSdl("SDL_GetGamepadFromID"));
|
||||
static const auto release = reinterpret_cast<Free>(WatchSdl("SDL_free"));
|
||||
if (list == nullptr || from_id == nullptr || release == nullptr) {
|
||||
return nullptr;
|
||||
}
|
||||
int count = 0;
|
||||
unsigned* ids = list(&count);
|
||||
void* pad = nullptr;
|
||||
for (int i = 0; ids != nullptr && i < count && pad == nullptr; ++i) {
|
||||
pad = from_id(ids[i]);
|
||||
}
|
||||
release(ids);
|
||||
return pad;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
CinematicInput TakeCinematicInput() {
|
||||
std::scoped_lock lock{watch_mutex};
|
||||
CinematicInput in{};
|
||||
if (!watch_on) {
|
||||
return in;
|
||||
}
|
||||
in.enabled = true;
|
||||
using KeyState = const bool* (*)(int*);
|
||||
using Focus = void* (*)();
|
||||
using MouseState = unsigned (*)(float*, float*);
|
||||
static const auto key_state = reinterpret_cast<KeyState>(WatchSdl("SDL_GetKeyboardState"));
|
||||
static const auto focus = reinterpret_cast<Focus>(WatchSdl("SDL_GetKeyboardFocus"));
|
||||
static const auto mouse_state = reinterpret_cast<MouseState>(WatchSdl("SDL_GetMouseState"));
|
||||
int count = 0;
|
||||
const bool* keys = key_state != nullptr && focus != nullptr && focus() != nullptr
|
||||
? key_state(&count)
|
||||
: nullptr;
|
||||
std::array<bool, ScCount> now{};
|
||||
for (int i = 0; keys != nullptr && i < ScCount && i < count; ++i) {
|
||||
now[i] = keys[i];
|
||||
}
|
||||
const auto pressed = [&](int key) { return now[key] && !watch_keys_before[key]; };
|
||||
if (pressed(ScF1)) {
|
||||
watch_on = false;
|
||||
watch_watching = false;
|
||||
watch_keys_before = now;
|
||||
LOG_INFO(Debug, "BBSC watch camera: off (F1)");
|
||||
return CinematicInput{};
|
||||
}
|
||||
const bool shift = now[ScLShift] || now[ScRShift];
|
||||
in.switch_target = (pressed(ScTab) ? (shift ? -1 : 1) : 0) + (pressed(ScE) ? 1 : 0) -
|
||||
(pressed(ScQ) ? 1 : 0);
|
||||
in.toggle_free = pressed(ScF);
|
||||
const bool click = keys != nullptr && mouse_state != nullptr &&
|
||||
(mouse_state(nullptr, nullptr) & 1u) != 0; // SDL_BUTTON_LMASK
|
||||
in.switch_target += click && !watch_click_before ? 1 : 0;
|
||||
watch_click_before = click;
|
||||
in.forward = now[ScW];
|
||||
in.back = now[ScS];
|
||||
in.left = now[ScA];
|
||||
in.right = now[ScD];
|
||||
in.up = now[ScSpace] || (now[ScE] && !watch_watching);
|
||||
in.down = now[ScC] || (now[ScQ] && !watch_watching);
|
||||
in.fast = shift;
|
||||
in.slow = now[ScLAlt] || now[ScRAlt];
|
||||
in.look_up = now[ScUp];
|
||||
in.look_down = now[ScDown];
|
||||
in.look_left = now[ScLeft];
|
||||
in.look_right = now[ScRight];
|
||||
watch_keys_before = now;
|
||||
using Button = bool (*)(void*, int);
|
||||
using Axis = short (*)(void*, int);
|
||||
static const auto button = reinterpret_cast<Button>(WatchSdl("SDL_GetGamepadButton"));
|
||||
static const auto axis = reinterpret_cast<Axis>(WatchSdl("SDL_GetGamepadAxis"));
|
||||
void* pad = button != nullptr && axis != nullptr ? WatchGamepad() : nullptr;
|
||||
if (pad != nullptr) {
|
||||
const bool l1 = button(pad, PadL1), r1 = button(pad, PadR1);
|
||||
if (watch_watching) {
|
||||
in.switch_target += (r1 && !watch_r1_before ? 1 : 0) - (l1 && !watch_l1_before ? 1 : 0);
|
||||
}
|
||||
watch_l1_before = l1;
|
||||
watch_r1_before = r1;
|
||||
in.pad_look_x = WatchAxis(axis(pad, AxisRX));
|
||||
in.pad_look_y = -WatchAxis(axis(pad, AxisRY));
|
||||
in.pad_move_y = -WatchAxis(axis(pad, AxisLY)); // stick up = closer
|
||||
}
|
||||
return in;
|
||||
}
|
||||
|
||||
void CinematicForceOff() {
|
||||
std::scoped_lock lock{watch_mutex};
|
||||
watch_on = false;
|
||||
watch_watching = false;
|
||||
}
|
||||
|
||||
void CinematicForceOn() {
|
||||
if (!Core::Bbsc::SwitchEnabled("SHADPS4_BBSC_CINEMATIC")) {
|
||||
return;
|
||||
}
|
||||
std::scoped_lock lock{watch_mutex};
|
||||
if (!watch_on) {
|
||||
// Keys already held (the attack that killed us) do not count as presses.
|
||||
watch_keys_before.fill(true);
|
||||
watch_click_before = watch_l1_before = watch_r1_before = true;
|
||||
}
|
||||
watch_on = true;
|
||||
}
|
||||
|
||||
void CinematicSetWatching(bool watching) {
|
||||
std::scoped_lock lock{watch_mutex};
|
||||
watch_watching = watching;
|
||||
}
|
||||
|
||||
void CinematicPadRead(unsigned, unsigned char, unsigned char, unsigned char, unsigned char) {}
|
||||
|
||||
} // 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,121 +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;
|
||||
}
|
||||
// BBSC 0.0.2's relay: the error of the last socket call, and shutdown's "both ways".
|
||||
inline int WSAGetLastError() {
|
||||
return errno;
|
||||
}
|
||||
#define SD_BOTH SHUT_RDWR
|
||||
// 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,44 +0,0 @@
|
||||
{
|
||||
"id": "bbsc",
|
||||
"name": "BBSC - Bloodborne Seamless Co-op",
|
||||
"version": "0.0.2",
|
||||
"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": "bells_anywhere", "name": "Bells anywhere", "description": "The kit's bells ring anywhere, the Hunter's Dream included (your role, Insight and being online still count)", "type": "bool", "default": true, "env": "SHADPS4_BBSC_BELLS_ANYWHERE"},
|
||||
{"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, your build, boss fights with a timer, when you are dead or spectating, 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,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,154 +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 <fcntl.h>
|
||||
#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.
|
||||
// process_vm_readv cannot read a driver's mapping (VM_IO / VM_PFNMAP): on AMD the game's memory is a
|
||||
// GPU dma-buf (gpu/shim/bbport_guest_memory.cpp), and the item tables were never found there
|
||||
// (issue #9). /proc/self/mem reads those through the driver's own access, so a failed slice is
|
||||
// read again that way.
|
||||
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, int mem,
|
||||
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};
|
||||
ssize_t got = process_vm_readv(self, &local, 1, &remote, 1, 0);
|
||||
if (got <= 0 && mem >= 0) {
|
||||
got = pread(mem, buffer.data(), want, static_cast<off_t>(at));
|
||||
}
|
||||
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);
|
||||
const int mem = open("/proc/self/mem", O_RDONLY | O_CLOEXEC);
|
||||
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,
|
||||
mem, hits, max_hits);
|
||||
}
|
||||
if (mem >= 0) {
|
||||
close(mem);
|
||||
}
|
||||
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,66 +0,0 @@
|
||||
# Hosting BBSC for your group
|
||||
|
||||
Seamless co-op with everything on (summons across areas, five hunters) needs two servers that
|
||||
your group shares:
|
||||
|
||||
| Server | What it does | Where it comes from |
|
||||
|---|---|---|
|
||||
| **shadNet** | Accounts, matchmaking, bells and summons | [shadnet-p2p](https://github.com/Wozzardman/shadnet-p2p) by Wozzardman |
|
||||
| **BBSC's game server** | The server list, messages, bloodstains and BBSC's relay | [`gameserver/`](gameserver/README.md), in this folder |
|
||||
|
||||
Both can run on one always-on computer (a home server, a VPS, or one of the players' PCs) that every
|
||||
player reaches through [Tailscale](https://tailscale.com), so no port is opened to the Internet.
|
||||
|
||||
## Quick start
|
||||
|
||||
The full guide, step by step, is [docs/SERVER.md](../../docs/SERVER.md). In short, on the server:
|
||||
|
||||
```sh
|
||||
# shadNet, in seamless mode
|
||||
docker build -t bbport-shadnet tools/shadnet-server
|
||||
docker run -d --name shadnet --restart unless-stopped --network host -v shadnet-data:/data \
|
||||
-e SHADNET_HOST=100.x.y.z -e SHADNET_REGISTRATION_KEY=a-long-code-of-your-own \
|
||||
-e SHADNET_BLOODBORNE_SEAMLESS_COOP=1 bbport-shadnet
|
||||
|
||||
# BBSC's game server, next to it
|
||||
docker build -t bbport-bbsc-server tools/bbsc-server
|
||||
docker run -d --name bbsc-server --restart unless-stopped --network host \
|
||||
-v bbsc-data:/data -e BBSC_HOST=100.x.y.z bbport-bbsc-server
|
||||
```
|
||||
|
||||
`100.x.y.z` is the server's Tailscale address (`tailscale ip -4`). Without Docker, the game server
|
||||
is one Python file with nothing to install:
|
||||
|
||||
```sh
|
||||
python3 bbsc/server/gameserver/bbsc_gameserver.py --shadnet 100.x.y.z --host 100.x.y.z
|
||||
```
|
||||
|
||||
Then every player, in the launcher's **Multiplayer → Online** tab:
|
||||
|
||||
1. chooses **Your own server** with the server's address, and makes an account on it;
|
||||
2. sets **Game server** to `http://100.x.y.z:20443`;
|
||||
3. turns on BBSC in the **Seamless co-op (BBSC)** group.
|
||||
|
||||
## Ports
|
||||
|
||||
| Port | Protocol | Server | Used for |
|
||||
|---|---|---|---|
|
||||
| 31313 | TCP | shadNet | Sign-in and matchmaking |
|
||||
| 31314 | UDP | shadNet | Finding each other's address (STUN) |
|
||||
| 31315 | TCP | shadNet | Its web API: summon signs |
|
||||
| 20443 | TCP | Game server | The server list, messages, bloodstains |
|
||||
| 20444 | TCP | Game server | BBSC's relay: warps, world sync, enemy scaling |
|
||||
| 18671 | TCP | Game server | Its bbhost-compatible service (`--no-bbhost` turns it off) |
|
||||
| 3478 | UDP | Game server | Relay for players who cannot reach each other directly |
|
||||
|
||||
## What is in this folder
|
||||
|
||||
| Path | Contents |
|
||||
|---|---|
|
||||
| `gameserver/` | BBSC's game server (`bbsc_gameserver.py`), its `ss.info` template and notice text |
|
||||
| `server/` | shadNet's settings as BBSC expects them (seamless co-op on, its ports, worlds and scoreboards) |
|
||||
|
||||
## Keep private
|
||||
|
||||
shadNet's registration key and its account database (the `shadnet-data` volume), and the game
|
||||
server's data (`bbsc-data`), belong to your group: do not share them or publish them.
|
||||
@@ -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
|
||||
Vendored
-20
@@ -1,20 +0,0 @@
|
||||
Copyright (c) 2012 - present, Victor Zverovich and {fmt} contributors
|
||||
|
||||
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.
|
||||
Vendored
-1
@@ -1 +0,0 @@
|
||||
fmt 12.2.0 (https://github.com/fmtlib/fmt), MIT: LICENSE. Used header-only (FMT_HEADER_ONLY).
|
||||
-218
@@ -1,218 +0,0 @@
|
||||
// Formatting library for C++ - dynamic argument lists
|
||||
//
|
||||
// Copyright (c) 2012 - present, Victor Zverovich and {fmt} contributors
|
||||
// All rights reserved.
|
||||
//
|
||||
// For the license information refer to format.h.
|
||||
|
||||
#ifndef FMT_ARGS_H_
|
||||
#define FMT_ARGS_H_
|
||||
|
||||
#ifndef FMT_MODULE
|
||||
# include <functional> // std::reference_wrapper
|
||||
# include <memory> // std::unique_ptr
|
||||
# include <vector>
|
||||
#endif
|
||||
|
||||
#include "format.h" // std_string_view
|
||||
|
||||
FMT_BEGIN_NAMESPACE
|
||||
namespace detail {
|
||||
|
||||
template <typename T> struct is_reference_wrapper : std::false_type {};
|
||||
template <typename T>
|
||||
struct is_reference_wrapper<std::reference_wrapper<T>> : std::true_type {};
|
||||
|
||||
template <typename T> auto unwrap(const T& v) -> const T& { return v; }
|
||||
template <typename T>
|
||||
auto unwrap(const std::reference_wrapper<T>& v) -> const T& {
|
||||
return static_cast<const T&>(v);
|
||||
}
|
||||
|
||||
// node is defined outside dynamic_arg_list to workaround a C2504 bug in MSVC
|
||||
// 2022 (v17.10.0).
|
||||
//
|
||||
// Workaround for clang's -Wweak-vtables. Unlike for regular classes, for
|
||||
// templates it doesn't complain about inability to deduce single translation
|
||||
// unit for placing vtable. So node is made a fake template.
|
||||
template <typename = void> struct node {
|
||||
virtual ~node() = default;
|
||||
std::unique_ptr<node<>> next;
|
||||
};
|
||||
|
||||
class dynamic_arg_list {
|
||||
template <typename T> struct typed_node : node<> {
|
||||
T value;
|
||||
|
||||
template <typename Arg>
|
||||
FMT_CONSTEXPR typed_node(const Arg& arg) : value(arg) {}
|
||||
|
||||
template <typename Char>
|
||||
FMT_CONSTEXPR typed_node(const basic_string_view<Char>& arg)
|
||||
: value(arg.data(), arg.size()) {}
|
||||
};
|
||||
|
||||
std::unique_ptr<node<>> head_;
|
||||
|
||||
public:
|
||||
template <typename T, typename Arg> auto push(const Arg& arg) -> const T& {
|
||||
auto new_node = std::unique_ptr<typed_node<T>>(new typed_node<T>(arg));
|
||||
auto& value = new_node->value;
|
||||
new_node->next = std::move(head_);
|
||||
head_ = std::move(new_node);
|
||||
return value;
|
||||
}
|
||||
};
|
||||
} // namespace detail
|
||||
|
||||
/**
|
||||
* A dynamic list of formatting arguments with storage.
|
||||
*
|
||||
* It can be implicitly converted into `fmt::basic_format_args` for passing
|
||||
* into type-erased formatting functions such as `fmt::vformat`.
|
||||
*/
|
||||
FMT_EXPORT template <typename Context> class dynamic_format_arg_store {
|
||||
private:
|
||||
using char_type = typename Context::char_type;
|
||||
|
||||
template <typename T> struct need_copy {
|
||||
static constexpr detail::type mapped_type =
|
||||
detail::mapped_type_constant<T, char_type>::value;
|
||||
|
||||
enum {
|
||||
value = !(detail::is_reference_wrapper<T>::value ||
|
||||
std::is_same<T, basic_string_view<char_type>>::value ||
|
||||
std::is_same<T, detail::std_string_view<char_type>>::value ||
|
||||
(mapped_type != detail::type::cstring_type &&
|
||||
mapped_type != detail::type::string_type &&
|
||||
mapped_type != detail::type::custom_type))
|
||||
};
|
||||
};
|
||||
|
||||
template <typename T>
|
||||
using stored_t = conditional_t<
|
||||
std::is_convertible<T, std::basic_string<char_type>>::value &&
|
||||
!detail::is_reference_wrapper<T>::value,
|
||||
std::basic_string<char_type>, T>;
|
||||
|
||||
// Storage of basic_format_arg must be contiguous.
|
||||
std::vector<basic_format_arg<Context>> data_;
|
||||
std::vector<detail::named_arg_info<char_type>> named_info_;
|
||||
|
||||
// Storage of arguments not fitting into basic_format_arg must grow
|
||||
// without relocation because items in data_ refer to it.
|
||||
detail::dynamic_arg_list dynamic_args_;
|
||||
|
||||
friend class basic_format_args<Context>;
|
||||
|
||||
auto data() const -> const basic_format_arg<Context>* {
|
||||
return named_info_.empty() ? data_.data() : data_.data() + 1;
|
||||
}
|
||||
|
||||
template <typename T> void emplace_arg(const T& arg) {
|
||||
data_.emplace_back(arg);
|
||||
}
|
||||
|
||||
template <typename T> void emplace_arg(const named_arg<T, char_type>& arg) {
|
||||
if (named_info_.empty())
|
||||
data_.insert(data_.begin(), basic_format_arg<Context>(nullptr, 0));
|
||||
data_.emplace_back(detail::unwrap(arg.value));
|
||||
auto pop_one = [](std::vector<basic_format_arg<Context>>* data) {
|
||||
data->pop_back();
|
||||
};
|
||||
std::unique_ptr<std::vector<basic_format_arg<Context>>, decltype(pop_one)>
|
||||
guard{&data_, pop_one};
|
||||
named_info_.push_back({arg.name, static_cast<int>(data_.size() - 2u)});
|
||||
data_[0] = {named_info_.data(), named_info_.size()};
|
||||
guard.release();
|
||||
}
|
||||
|
||||
public:
|
||||
constexpr dynamic_format_arg_store() = default;
|
||||
|
||||
operator basic_format_args<Context>() const {
|
||||
return basic_format_args<Context>(data(), static_cast<int>(data_.size()),
|
||||
!named_info_.empty());
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds an argument into the dynamic store for later passing to a formatting
|
||||
* function.
|
||||
*
|
||||
* Note that custom types and string types (but not string views) are copied
|
||||
* into the store dynamically allocating memory if necessary.
|
||||
*
|
||||
* **Example**:
|
||||
*
|
||||
* fmt::dynamic_format_arg_store<fmt::format_context> store;
|
||||
* store.push_back(42);
|
||||
* store.push_back("abc");
|
||||
* store.push_back(1.5f);
|
||||
* std::string result = fmt::vformat("{} and {} and {}", store);
|
||||
*/
|
||||
template <typename T> void push_back(const T& arg) {
|
||||
if FMT_CONSTEXPR20 (need_copy<T>::value)
|
||||
emplace_arg(dynamic_args_.push<stored_t<T>>(arg));
|
||||
else
|
||||
emplace_arg(detail::unwrap(arg));
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a reference to the argument into the dynamic store for later passing
|
||||
* to a formatting function.
|
||||
*
|
||||
* **Example**:
|
||||
*
|
||||
* fmt::dynamic_format_arg_store<fmt::format_context> store;
|
||||
* char band[] = "Rolling Stones";
|
||||
* store.push_back(std::cref(band));
|
||||
* band[9] = 'c'; // Changing str affects the output.
|
||||
* std::string result = fmt::vformat("{}", store);
|
||||
* // result == "Rolling Scones"
|
||||
*/
|
||||
template <typename T> void push_back(std::reference_wrapper<T> arg) {
|
||||
static_assert(
|
||||
need_copy<T>::value,
|
||||
"objects of built-in types and string views are always copied");
|
||||
emplace_arg(arg.get());
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds named argument into the dynamic store for later passing to a
|
||||
* formatting function. `std::reference_wrapper` is supported to avoid
|
||||
* copying of the argument. The name is always copied into the store.
|
||||
*/
|
||||
template <typename T> void push_back(const named_arg<T, char_type>& arg) {
|
||||
const char_type* arg_name =
|
||||
dynamic_args_.push<std::basic_string<char_type>>(arg.name).c_str();
|
||||
if FMT_CONSTEXPR20 (need_copy<T>::value) {
|
||||
emplace_arg(
|
||||
fmt::arg(arg_name, dynamic_args_.push<stored_t<T>>(arg.value)));
|
||||
} else {
|
||||
emplace_arg(fmt::arg(arg_name, arg.value));
|
||||
}
|
||||
}
|
||||
|
||||
/// Erase all elements from the store.
|
||||
void clear() {
|
||||
data_.clear();
|
||||
named_info_.clear();
|
||||
dynamic_args_ = {};
|
||||
}
|
||||
|
||||
/// Reserves space to store at least `new_cap` arguments including
|
||||
/// `new_cap_named` named arguments.
|
||||
void reserve(size_t new_cap, size_t new_cap_named) {
|
||||
FMT_ASSERT(new_cap >= new_cap_named,
|
||||
"set of arguments includes set of named arguments");
|
||||
data_.reserve(new_cap);
|
||||
named_info_.reserve(new_cap_named);
|
||||
}
|
||||
|
||||
/// Returns the number of elements in the store.
|
||||
auto size() const noexcept -> size_t { return data_.size(); }
|
||||
};
|
||||
|
||||
FMT_END_NAMESPACE
|
||||
|
||||
#endif // FMT_ARGS_H_
|
||||
-2944
File diff suppressed because it is too large.
Load diff
-2245
File diff suppressed because it is too large.
Load diff
-673
@@ -1,673 +0,0 @@
|
||||
// Formatting library for C++ - color support
|
||||
//
|
||||
// Copyright (c) 2018 - present, Victor Zverovich and {fmt} contributors
|
||||
// All rights reserved.
|
||||
//
|
||||
// For the license information refer to format.h.
|
||||
|
||||
#ifndef FMT_COLOR_H_
|
||||
#define FMT_COLOR_H_
|
||||
|
||||
#include "format.h"
|
||||
|
||||
FMT_BEGIN_NAMESPACE
|
||||
FMT_BEGIN_EXPORT
|
||||
|
||||
enum class color : uint32_t {
|
||||
alice_blue = 0xF0F8FF, // rgb(240,248,255)
|
||||
antique_white = 0xFAEBD7, // rgb(250,235,215)
|
||||
aqua = 0x00FFFF, // rgb(0,255,255)
|
||||
aquamarine = 0x7FFFD4, // rgb(127,255,212)
|
||||
azure = 0xF0FFFF, // rgb(240,255,255)
|
||||
beige = 0xF5F5DC, // rgb(245,245,220)
|
||||
bisque = 0xFFE4C4, // rgb(255,228,196)
|
||||
black = 0x000000, // rgb(0,0,0)
|
||||
blanched_almond = 0xFFEBCD, // rgb(255,235,205)
|
||||
blue = 0x0000FF, // rgb(0,0,255)
|
||||
blue_violet = 0x8A2BE2, // rgb(138,43,226)
|
||||
brown = 0xA52A2A, // rgb(165,42,42)
|
||||
burly_wood = 0xDEB887, // rgb(222,184,135)
|
||||
cadet_blue = 0x5F9EA0, // rgb(95,158,160)
|
||||
chartreuse = 0x7FFF00, // rgb(127,255,0)
|
||||
chocolate = 0xD2691E, // rgb(210,105,30)
|
||||
coral = 0xFF7F50, // rgb(255,127,80)
|
||||
cornflower_blue = 0x6495ED, // rgb(100,149,237)
|
||||
cornsilk = 0xFFF8DC, // rgb(255,248,220)
|
||||
crimson = 0xDC143C, // rgb(220,20,60)
|
||||
cyan = 0x00FFFF, // rgb(0,255,255)
|
||||
dark_blue = 0x00008B, // rgb(0,0,139)
|
||||
dark_cyan = 0x008B8B, // rgb(0,139,139)
|
||||
dark_golden_rod = 0xB8860B, // rgb(184,134,11)
|
||||
dark_gray = 0xA9A9A9, // rgb(169,169,169)
|
||||
dark_green = 0x006400, // rgb(0,100,0)
|
||||
dark_khaki = 0xBDB76B, // rgb(189,183,107)
|
||||
dark_magenta = 0x8B008B, // rgb(139,0,139)
|
||||
dark_olive_green = 0x556B2F, // rgb(85,107,47)
|
||||
dark_orange = 0xFF8C00, // rgb(255,140,0)
|
||||
dark_orchid = 0x9932CC, // rgb(153,50,204)
|
||||
dark_red = 0x8B0000, // rgb(139,0,0)
|
||||
dark_salmon = 0xE9967A, // rgb(233,150,122)
|
||||
dark_sea_green = 0x8FBC8F, // rgb(143,188,143)
|
||||
dark_slate_blue = 0x483D8B, // rgb(72,61,139)
|
||||
dark_slate_gray = 0x2F4F4F, // rgb(47,79,79)
|
||||
dark_turquoise = 0x00CED1, // rgb(0,206,209)
|
||||
dark_violet = 0x9400D3, // rgb(148,0,211)
|
||||
deep_pink = 0xFF1493, // rgb(255,20,147)
|
||||
deep_sky_blue = 0x00BFFF, // rgb(0,191,255)
|
||||
dim_gray = 0x696969, // rgb(105,105,105)
|
||||
dodger_blue = 0x1E90FF, // rgb(30,144,255)
|
||||
fire_brick = 0xB22222, // rgb(178,34,34)
|
||||
floral_white = 0xFFFAF0, // rgb(255,250,240)
|
||||
forest_green = 0x228B22, // rgb(34,139,34)
|
||||
fuchsia = 0xFF00FF, // rgb(255,0,255)
|
||||
gainsboro = 0xDCDCDC, // rgb(220,220,220)
|
||||
ghost_white = 0xF8F8FF, // rgb(248,248,255)
|
||||
gold = 0xFFD700, // rgb(255,215,0)
|
||||
golden_rod = 0xDAA520, // rgb(218,165,32)
|
||||
gray = 0x808080, // rgb(128,128,128)
|
||||
green = 0x008000, // rgb(0,128,0)
|
||||
green_yellow = 0xADFF2F, // rgb(173,255,47)
|
||||
honey_dew = 0xF0FFF0, // rgb(240,255,240)
|
||||
hot_pink = 0xFF69B4, // rgb(255,105,180)
|
||||
indian_red = 0xCD5C5C, // rgb(205,92,92)
|
||||
indigo = 0x4B0082, // rgb(75,0,130)
|
||||
ivory = 0xFFFFF0, // rgb(255,255,240)
|
||||
khaki = 0xF0E68C, // rgb(240,230,140)
|
||||
lavender = 0xE6E6FA, // rgb(230,230,250)
|
||||
lavender_blush = 0xFFF0F5, // rgb(255,240,245)
|
||||
lawn_green = 0x7CFC00, // rgb(124,252,0)
|
||||
lemon_chiffon = 0xFFFACD, // rgb(255,250,205)
|
||||
light_blue = 0xADD8E6, // rgb(173,216,230)
|
||||
light_coral = 0xF08080, // rgb(240,128,128)
|
||||
light_cyan = 0xE0FFFF, // rgb(224,255,255)
|
||||
light_golden_rod_yellow = 0xFAFAD2, // rgb(250,250,210)
|
||||
light_gray = 0xD3D3D3, // rgb(211,211,211)
|
||||
light_green = 0x90EE90, // rgb(144,238,144)
|
||||
light_pink = 0xFFB6C1, // rgb(255,182,193)
|
||||
light_salmon = 0xFFA07A, // rgb(255,160,122)
|
||||
light_sea_green = 0x20B2AA, // rgb(32,178,170)
|
||||
light_sky_blue = 0x87CEFA, // rgb(135,206,250)
|
||||
light_slate_gray = 0x778899, // rgb(119,136,153)
|
||||
light_steel_blue = 0xB0C4DE, // rgb(176,196,222)
|
||||
light_yellow = 0xFFFFE0, // rgb(255,255,224)
|
||||
lime = 0x00FF00, // rgb(0,255,0)
|
||||
lime_green = 0x32CD32, // rgb(50,205,50)
|
||||
linen = 0xFAF0E6, // rgb(250,240,230)
|
||||
magenta = 0xFF00FF, // rgb(255,0,255)
|
||||
maroon = 0x800000, // rgb(128,0,0)
|
||||
medium_aquamarine = 0x66CDAA, // rgb(102,205,170)
|
||||
medium_blue = 0x0000CD, // rgb(0,0,205)
|
||||
medium_orchid = 0xBA55D3, // rgb(186,85,211)
|
||||
medium_purple = 0x9370DB, // rgb(147,112,219)
|
||||
medium_sea_green = 0x3CB371, // rgb(60,179,113)
|
||||
medium_slate_blue = 0x7B68EE, // rgb(123,104,238)
|
||||
medium_spring_green = 0x00FA9A, // rgb(0,250,154)
|
||||
medium_turquoise = 0x48D1CC, // rgb(72,209,204)
|
||||
medium_violet_red = 0xC71585, // rgb(199,21,133)
|
||||
midnight_blue = 0x191970, // rgb(25,25,112)
|
||||
mint_cream = 0xF5FFFA, // rgb(245,255,250)
|
||||
misty_rose = 0xFFE4E1, // rgb(255,228,225)
|
||||
moccasin = 0xFFE4B5, // rgb(255,228,181)
|
||||
navajo_white = 0xFFDEAD, // rgb(255,222,173)
|
||||
navy = 0x000080, // rgb(0,0,128)
|
||||
old_lace = 0xFDF5E6, // rgb(253,245,230)
|
||||
olive = 0x808000, // rgb(128,128,0)
|
||||
olive_drab = 0x6B8E23, // rgb(107,142,35)
|
||||
orange = 0xFFA500, // rgb(255,165,0)
|
||||
orange_red = 0xFF4500, // rgb(255,69,0)
|
||||
orchid = 0xDA70D6, // rgb(218,112,214)
|
||||
pale_golden_rod = 0xEEE8AA, // rgb(238,232,170)
|
||||
pale_green = 0x98FB98, // rgb(152,251,152)
|
||||
pale_turquoise = 0xAFEEEE, // rgb(175,238,238)
|
||||
pale_violet_red = 0xDB7093, // rgb(219,112,147)
|
||||
papaya_whip = 0xFFEFD5, // rgb(255,239,213)
|
||||
peach_puff = 0xFFDAB9, // rgb(255,218,185)
|
||||
peru = 0xCD853F, // rgb(205,133,63)
|
||||
pink = 0xFFC0CB, // rgb(255,192,203)
|
||||
plum = 0xDDA0DD, // rgb(221,160,221)
|
||||
powder_blue = 0xB0E0E6, // rgb(176,224,230)
|
||||
purple = 0x800080, // rgb(128,0,128)
|
||||
rebecca_purple = 0x663399, // rgb(102,51,153)
|
||||
red = 0xFF0000, // rgb(255,0,0)
|
||||
rosy_brown = 0xBC8F8F, // rgb(188,143,143)
|
||||
royal_blue = 0x4169E1, // rgb(65,105,225)
|
||||
saddle_brown = 0x8B4513, // rgb(139,69,19)
|
||||
salmon = 0xFA8072, // rgb(250,128,114)
|
||||
sandy_brown = 0xF4A460, // rgb(244,164,96)
|
||||
sea_green = 0x2E8B57, // rgb(46,139,87)
|
||||
sea_shell = 0xFFF5EE, // rgb(255,245,238)
|
||||
sienna = 0xA0522D, // rgb(160,82,45)
|
||||
silver = 0xC0C0C0, // rgb(192,192,192)
|
||||
sky_blue = 0x87CEEB, // rgb(135,206,235)
|
||||
slate_blue = 0x6A5ACD, // rgb(106,90,205)
|
||||
slate_gray = 0x708090, // rgb(112,128,144)
|
||||
snow = 0xFFFAFA, // rgb(255,250,250)
|
||||
spring_green = 0x00FF7F, // rgb(0,255,127)
|
||||
steel_blue = 0x4682B4, // rgb(70,130,180)
|
||||
tan = 0xD2B48C, // rgb(210,180,140)
|
||||
teal = 0x008080, // rgb(0,128,128)
|
||||
thistle = 0xD8BFD8, // rgb(216,191,216)
|
||||
tomato = 0xFF6347, // rgb(255,99,71)
|
||||
turquoise = 0x40E0D0, // rgb(64,224,208)
|
||||
violet = 0xEE82EE, // rgb(238,130,238)
|
||||
wheat = 0xF5DEB3, // rgb(245,222,179)
|
||||
white = 0xFFFFFF, // rgb(255,255,255)
|
||||
white_smoke = 0xF5F5F5, // rgb(245,245,245)
|
||||
yellow = 0xFFFF00, // rgb(255,255,0)
|
||||
yellow_green = 0x9ACD32 // rgb(154,205,50)
|
||||
}; // enum class color
|
||||
|
||||
enum class terminal_color : uint8_t {
|
||||
black = 30,
|
||||
red,
|
||||
green,
|
||||
yellow,
|
||||
blue,
|
||||
magenta,
|
||||
cyan,
|
||||
white,
|
||||
bright_black = 90,
|
||||
bright_red,
|
||||
bright_green,
|
||||
bright_yellow,
|
||||
bright_blue,
|
||||
bright_magenta,
|
||||
bright_cyan,
|
||||
bright_white
|
||||
};
|
||||
|
||||
enum class emphasis : uint8_t {
|
||||
bold = 1,
|
||||
faint = 1 << 1,
|
||||
italic = 1 << 2,
|
||||
underline = 1 << 3,
|
||||
blink = 1 << 4,
|
||||
reverse = 1 << 5,
|
||||
conceal = 1 << 6,
|
||||
strikethrough = 1 << 7,
|
||||
};
|
||||
|
||||
// rgb is a struct for red, green and blue colors.
|
||||
// Using the name "rgb" makes some editors show the color in a tooltip.
|
||||
struct rgb {
|
||||
constexpr rgb() : r(0), g(0), b(0) {}
|
||||
constexpr rgb(uint8_t r_, uint8_t g_, uint8_t b_) : r(r_), g(g_), b(b_) {}
|
||||
constexpr rgb(uint32_t hex)
|
||||
: r((hex >> 16) & 0xFF), g((hex >> 8) & 0xFF), b(hex & 0xFF) {}
|
||||
constexpr rgb(color hex)
|
||||
: r((uint32_t(hex) >> 16) & 0xFF),
|
||||
g((uint32_t(hex) >> 8) & 0xFF),
|
||||
b(uint32_t(hex) & 0xFF) {}
|
||||
uint8_t r;
|
||||
uint8_t g;
|
||||
uint8_t b;
|
||||
};
|
||||
|
||||
namespace detail {
|
||||
|
||||
// A bit-packed variant of an RGB color, a terminal color, or unset color.
|
||||
// see text_style for the bit-packing scheme.
|
||||
struct color_type {
|
||||
constexpr color_type() noexcept = default;
|
||||
constexpr color_type(color rgb_color) noexcept
|
||||
: value_(static_cast<uint32_t>(rgb_color) | (1 << 24)) {}
|
||||
constexpr color_type(rgb rgb_color) noexcept
|
||||
: color_type(static_cast<color>(
|
||||
(static_cast<uint32_t>(rgb_color.r) << 16) |
|
||||
(static_cast<uint32_t>(rgb_color.g) << 8) | rgb_color.b)) {}
|
||||
constexpr color_type(terminal_color term_color) noexcept
|
||||
: value_(static_cast<uint32_t>(term_color) | (3 << 24)) {}
|
||||
|
||||
constexpr auto is_terminal_color() const noexcept -> bool {
|
||||
return (value_ & (1 << 25)) != 0;
|
||||
}
|
||||
|
||||
constexpr auto value() const noexcept -> uint32_t {
|
||||
return value_ & 0xFFFFFF;
|
||||
}
|
||||
|
||||
constexpr color_type(uint32_t value) noexcept : value_(value) {}
|
||||
|
||||
uint32_t value_ = 0;
|
||||
};
|
||||
} // namespace detail
|
||||
|
||||
/// A text style consisting of foreground and background colors and emphasis.
|
||||
class text_style {
|
||||
// The information is packed as follows:
|
||||
// ┌──┐
|
||||
// │ 0│─┐
|
||||
// │..│ ├── foreground color value
|
||||
// │23│─┘
|
||||
// ├──┤
|
||||
// │24│─┬── discriminator for the above value. 00 if unset, 01 if it's
|
||||
// │25│─┘ an RGB color, or 11 if it's a terminal color (10 is unused)
|
||||
// ├──┤
|
||||
// │26│──── overflow bit, always zero (see below)
|
||||
// ├──┤
|
||||
// │27│─┐
|
||||
// │..│ │
|
||||
// │50│ │
|
||||
// ├──┤ │
|
||||
// │51│ ├── background color (same format as the foreground color)
|
||||
// │52│ │
|
||||
// ├──┤ │
|
||||
// │53│─┘
|
||||
// ├──┤
|
||||
// │54│─┐
|
||||
// │..│ ├── emphases
|
||||
// │61│─┘
|
||||
// ├──┤
|
||||
// │62│─┬── unused
|
||||
// │63│─┘
|
||||
// └──┘
|
||||
// The overflow bits are there to make operator|= efficient.
|
||||
// When ORing, we must throw if, for either the foreground or background,
|
||||
// one style specifies a terminal color and the other specifies any color
|
||||
// (terminal or RGB); in other words, if one discriminator is 11 and the
|
||||
// other is 11 or 01.
|
||||
//
|
||||
// We do that check by adding the styles. Consider what adding does to each
|
||||
// possible pair of discriminators:
|
||||
// 00 + 00 = 000
|
||||
// 01 + 00 = 001
|
||||
// 11 + 00 = 011
|
||||
// 01 + 01 = 010
|
||||
// 11 + 01 = 100 (!!)
|
||||
// 11 + 11 = 110 (!!)
|
||||
// In the last two cases, the ones we want to catch, the third bit——the
|
||||
// overflow bit——is set. Bingo.
|
||||
//
|
||||
// We must take into account the possible carry bit from the bits
|
||||
// before the discriminator. The only potentially problematic case is
|
||||
// 11 + 00 = 011 (a carry bit would make it 100, not good!), but a carry
|
||||
// bit is impossible in that case, because 00 (unset color) means the
|
||||
// 24 bits that precede the discriminator are all zero.
|
||||
//
|
||||
// This test can be applied to both colors simultaneously.
|
||||
|
||||
public:
|
||||
FMT_CONSTEXPR text_style(emphasis em = emphasis()) noexcept
|
||||
: style_(static_cast<uint64_t>(em) << 54) {}
|
||||
|
||||
FMT_CONSTEXPR auto operator|=(text_style rhs) -> text_style& {
|
||||
if (((style_ + rhs.style_) & ((1ULL << 26) | (1ULL << 53))) != 0)
|
||||
report_error("can't OR a terminal color");
|
||||
style_ |= rhs.style_;
|
||||
return *this;
|
||||
}
|
||||
|
||||
friend FMT_CONSTEXPR auto operator|(text_style lhs, text_style rhs)
|
||||
-> text_style {
|
||||
return lhs |= rhs;
|
||||
}
|
||||
|
||||
FMT_CONSTEXPR auto operator==(text_style rhs) const noexcept -> bool {
|
||||
return style_ == rhs.style_;
|
||||
}
|
||||
|
||||
FMT_CONSTEXPR auto operator!=(text_style rhs) const noexcept -> bool {
|
||||
return !(*this == rhs);
|
||||
}
|
||||
|
||||
FMT_CONSTEXPR auto has_foreground() const noexcept -> bool {
|
||||
return (style_ & (1 << 24)) != 0;
|
||||
}
|
||||
FMT_CONSTEXPR auto has_background() const noexcept -> bool {
|
||||
return (style_ & (1ULL << 51)) != 0;
|
||||
}
|
||||
FMT_CONSTEXPR auto has_emphasis() const noexcept -> bool {
|
||||
return (style_ >> 54) != 0;
|
||||
}
|
||||
FMT_CONSTEXPR auto get_foreground() const noexcept -> detail::color_type {
|
||||
FMT_ASSERT(has_foreground(), "no foreground specified for this style");
|
||||
return style_ & 0x3FFFFFF;
|
||||
}
|
||||
FMT_CONSTEXPR auto get_background() const noexcept -> detail::color_type {
|
||||
FMT_ASSERT(has_background(), "no background specified for this style");
|
||||
return (style_ >> 27) & 0x3FFFFFF;
|
||||
}
|
||||
FMT_CONSTEXPR auto get_emphasis() const noexcept -> emphasis {
|
||||
FMT_ASSERT(has_emphasis(), "no emphasis specified for this style");
|
||||
return static_cast<emphasis>(style_ >> 54);
|
||||
}
|
||||
|
||||
private:
|
||||
FMT_CONSTEXPR text_style(uint64_t style) noexcept : style_(style) {}
|
||||
|
||||
friend FMT_CONSTEXPR auto fg(detail::color_type foreground) noexcept
|
||||
-> text_style;
|
||||
|
||||
friend FMT_CONSTEXPR auto bg(detail::color_type background) noexcept
|
||||
-> text_style;
|
||||
|
||||
uint64_t style_ = 0;
|
||||
};
|
||||
|
||||
/// Creates a text style from the foreground (text) color.
|
||||
FMT_CONSTEXPR inline auto fg(detail::color_type foreground) noexcept
|
||||
-> text_style {
|
||||
return foreground.value_;
|
||||
}
|
||||
|
||||
/// Creates a text style from the background color.
|
||||
FMT_CONSTEXPR inline auto bg(detail::color_type background) noexcept
|
||||
-> text_style {
|
||||
return static_cast<uint64_t>(background.value_) << 27;
|
||||
}
|
||||
|
||||
FMT_CONSTEXPR inline auto operator|(emphasis lhs, emphasis rhs) noexcept
|
||||
-> text_style {
|
||||
return text_style(lhs) | rhs;
|
||||
}
|
||||
|
||||
namespace detail {
|
||||
|
||||
template <typename Char> struct ansi_color_escape {
|
||||
FMT_CONSTEXPR ansi_color_escape(color_type text_color,
|
||||
const char* esc) noexcept {
|
||||
// If we have a terminal color, we need to output another escape code
|
||||
// sequence.
|
||||
if (text_color.is_terminal_color()) {
|
||||
bool is_background = esc == string_view("\x1b[48;2;");
|
||||
uint32_t value = text_color.value();
|
||||
// Background ASCII codes are the same as the foreground ones but with
|
||||
// 10 more.
|
||||
if (is_background) value += 10u;
|
||||
|
||||
buffer[size++] = static_cast<Char>('\x1b');
|
||||
buffer[size++] = static_cast<Char>('[');
|
||||
|
||||
if (value >= 100u) {
|
||||
buffer[size++] = static_cast<Char>('1');
|
||||
value %= 100u;
|
||||
}
|
||||
buffer[size++] = static_cast<Char>('0' + value / 10u);
|
||||
buffer[size++] = static_cast<Char>('0' + value % 10u);
|
||||
|
||||
buffer[size++] = static_cast<Char>('m');
|
||||
return;
|
||||
}
|
||||
|
||||
for (int i = 0; i < 7; i++) {
|
||||
buffer[i] = static_cast<Char>(esc[i]);
|
||||
}
|
||||
rgb color(text_color.value());
|
||||
to_esc(color.r, buffer + 7, ';');
|
||||
to_esc(color.g, buffer + 11, ';');
|
||||
to_esc(color.b, buffer + 15, 'm');
|
||||
size = 19;
|
||||
}
|
||||
FMT_CONSTEXPR ansi_color_escape(emphasis em) noexcept {
|
||||
uint8_t em_codes[num_emphases] = {};
|
||||
if (has_emphasis(em, emphasis::bold)) em_codes[0] = 1;
|
||||
if (has_emphasis(em, emphasis::faint)) em_codes[1] = 2;
|
||||
if (has_emphasis(em, emphasis::italic)) em_codes[2] = 3;
|
||||
if (has_emphasis(em, emphasis::underline)) em_codes[3] = 4;
|
||||
if (has_emphasis(em, emphasis::blink)) em_codes[4] = 5;
|
||||
if (has_emphasis(em, emphasis::reverse)) em_codes[5] = 7;
|
||||
if (has_emphasis(em, emphasis::conceal)) em_codes[6] = 8;
|
||||
if (has_emphasis(em, emphasis::strikethrough)) em_codes[7] = 9;
|
||||
|
||||
buffer[size++] = static_cast<Char>('\x1b');
|
||||
buffer[size++] = static_cast<Char>('[');
|
||||
|
||||
for (size_t i = 0; i < num_emphases; ++i) {
|
||||
if (!em_codes[i]) continue;
|
||||
buffer[size++] = static_cast<Char>('0' + em_codes[i]);
|
||||
buffer[size++] = static_cast<Char>(';');
|
||||
}
|
||||
|
||||
buffer[size - 1] = static_cast<Char>('m');
|
||||
}
|
||||
FMT_CONSTEXPR operator const Char*() const noexcept { return buffer; }
|
||||
|
||||
FMT_CONSTEXPR auto begin() const noexcept -> const Char* { return buffer; }
|
||||
FMT_CONSTEXPR auto end() const noexcept -> const Char* {
|
||||
return buffer + size;
|
||||
}
|
||||
|
||||
private:
|
||||
static constexpr size_t num_emphases = 8;
|
||||
Char buffer[7u + 4u * num_emphases] = {};
|
||||
size_t size = 0;
|
||||
|
||||
static FMT_CONSTEXPR void to_esc(uint8_t c, Char* out,
|
||||
char delimiter) noexcept {
|
||||
out[0] = static_cast<Char>('0' + c / 100);
|
||||
out[1] = static_cast<Char>('0' + c / 10 % 10);
|
||||
out[2] = static_cast<Char>('0' + c % 10);
|
||||
out[3] = static_cast<Char>(delimiter);
|
||||
}
|
||||
static FMT_CONSTEXPR auto has_emphasis(emphasis em, emphasis mask) noexcept
|
||||
-> bool {
|
||||
return static_cast<uint8_t>(em) & static_cast<uint8_t>(mask);
|
||||
}
|
||||
};
|
||||
|
||||
template <typename Char>
|
||||
FMT_CONSTEXPR auto make_foreground_color(color_type foreground) noexcept
|
||||
-> ansi_color_escape<Char> {
|
||||
return ansi_color_escape<Char>(foreground, "\x1b[38;2;");
|
||||
}
|
||||
|
||||
template <typename Char>
|
||||
FMT_CONSTEXPR auto make_background_color(color_type background) noexcept
|
||||
-> ansi_color_escape<Char> {
|
||||
return ansi_color_escape<Char>(background, "\x1b[48;2;");
|
||||
}
|
||||
|
||||
template <typename Char>
|
||||
FMT_CONSTEXPR auto make_emphasis(emphasis em) noexcept
|
||||
-> ansi_color_escape<Char> {
|
||||
return ansi_color_escape<Char>(em);
|
||||
}
|
||||
|
||||
template <typename Char> inline void reset_color(buffer<Char>& buffer) {
|
||||
auto reset_color = string_view("\x1b[0m");
|
||||
buffer.append(reset_color.begin(), reset_color.end());
|
||||
}
|
||||
|
||||
template <typename T> struct styled_arg : view {
|
||||
const T& value;
|
||||
text_style style;
|
||||
FMT_CONSTEXPR styled_arg(const T& v, text_style s) : value(v), style(s) {}
|
||||
};
|
||||
|
||||
template <typename Char>
|
||||
void vformat_to(buffer<Char>& buf, text_style ts, basic_string_view<Char> fmt,
|
||||
basic_format_args<buffered_context<Char>> args) {
|
||||
if (ts.has_emphasis()) {
|
||||
auto emphasis = make_emphasis<Char>(ts.get_emphasis());
|
||||
buf.append(emphasis.begin(), emphasis.end());
|
||||
}
|
||||
if (ts.has_foreground()) {
|
||||
auto foreground = make_foreground_color<Char>(ts.get_foreground());
|
||||
buf.append(foreground.begin(), foreground.end());
|
||||
}
|
||||
if (ts.has_background()) {
|
||||
auto background = make_background_color<Char>(ts.get_background());
|
||||
buf.append(background.begin(), background.end());
|
||||
}
|
||||
vformat_to(buf, fmt, args);
|
||||
if (ts != text_style()) reset_color<Char>(buf);
|
||||
}
|
||||
} // namespace detail
|
||||
|
||||
inline void vprint(FILE* f, text_style ts, string_view fmt, format_args args) {
|
||||
auto buf = memory_buffer();
|
||||
detail::vformat_to(buf, ts, fmt, args);
|
||||
print(f, FMT_STRING("{}"), string_view(buf.begin(), buf.size()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats a string and prints it to the specified file stream using ANSI
|
||||
* escape sequences to specify text formatting.
|
||||
*
|
||||
* **Example**:
|
||||
*
|
||||
* fmt::print(fmt::emphasis::bold | fg(fmt::color::red),
|
||||
* "Elapsed time: {0:.2f} seconds", 1.23);
|
||||
*/
|
||||
template <typename... T>
|
||||
void print(FILE* f, text_style ts, format_string<T...> fmt, T&&... args) {
|
||||
vprint(f, ts, fmt.str, vargs<T...>{{args...}});
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats a string and prints it to stdout using ANSI escape sequences to
|
||||
* specify text formatting.
|
||||
*
|
||||
* **Example**:
|
||||
*
|
||||
* fmt::print(fmt::emphasis::bold | fg(fmt::color::red),
|
||||
* "Elapsed time: {0:.2f} seconds", 1.23);
|
||||
*/
|
||||
template <typename... T>
|
||||
void print(text_style ts, format_string<T...> fmt, T&&... args) {
|
||||
return print(stdout, ts, fmt, std::forward<T>(args)...);
|
||||
}
|
||||
|
||||
inline void vprintln(FILE* f, text_style ts, string_view fmt,
|
||||
format_args args) {
|
||||
auto buf = memory_buffer();
|
||||
detail::vformat_to(buf, ts, fmt, args);
|
||||
buf.push_back('\n');
|
||||
print(f, FMT_STRING("{}"), string_view(buf.begin(), buf.size()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats a string and prints it to the specified file stream followed by a
|
||||
* newline, using ANSI escape sequences to specify text formatting.
|
||||
*
|
||||
* **Example**:
|
||||
*
|
||||
* fmt::println(fmt::emphasis::bold | fg(fmt::color::red),
|
||||
* "Elapsed time: {0:.2f} seconds", 1.23);
|
||||
*/
|
||||
template <typename... T>
|
||||
void println(FILE* f, text_style ts, format_string<T...> fmt, T&&... args) {
|
||||
vprintln(f, ts, fmt.str, vargs<T...>{{args...}});
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats a string and prints it to stdout followed by a newline, using ANSI
|
||||
* escape sequences to specify text formatting.
|
||||
*
|
||||
* **Example**:
|
||||
*
|
||||
* fmt::println(fmt::emphasis::bold | fg(fmt::color::red),
|
||||
* "Elapsed time: {0:.2f} seconds", 1.23);
|
||||
*/
|
||||
template <typename... T>
|
||||
void println(text_style ts, format_string<T...> fmt, T&&... args) {
|
||||
return println(stdout, ts, fmt, std::forward<T>(args)...);
|
||||
}
|
||||
|
||||
inline auto vformat(text_style ts, string_view fmt, format_args args)
|
||||
-> std::string {
|
||||
auto buf = memory_buffer();
|
||||
detail::vformat_to(buf, ts, fmt, args);
|
||||
return fmt::to_string(buf);
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats arguments and returns the result as a string using ANSI escape
|
||||
* sequences to specify text formatting.
|
||||
*
|
||||
* **Example**:
|
||||
*
|
||||
* ```
|
||||
* #include <fmt/color.h>
|
||||
* std::string message = fmt::format(fmt::emphasis::bold | fg(fmt::color::red),
|
||||
* "The answer is {}", 42);
|
||||
* ```
|
||||
*/
|
||||
template <typename... T>
|
||||
inline auto format(text_style ts, format_string<T...> fmt, T&&... args)
|
||||
-> std::string {
|
||||
return fmt::vformat(ts, fmt.str, vargs<T...>{{args...}});
|
||||
}
|
||||
|
||||
/// Formats a string with the given text_style and writes the output to `out`.
|
||||
template <typename OutputIt,
|
||||
FMT_ENABLE_IF(detail::is_output_iterator<OutputIt, char>::value)>
|
||||
auto vformat_to(OutputIt out, text_style ts, string_view fmt, format_args args)
|
||||
-> OutputIt {
|
||||
auto&& buf = detail::get_buffer<char>(out);
|
||||
detail::vformat_to(buf, ts, fmt, args);
|
||||
return detail::get_iterator(buf, out);
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats arguments with the given text style, writes the result to the output
|
||||
* iterator `out` and returns the iterator past the end of the output range.
|
||||
*
|
||||
* **Example**:
|
||||
*
|
||||
* std::vector<char> out;
|
||||
* fmt::format_to(std::back_inserter(out),
|
||||
* fmt::emphasis::bold | fg(fmt::color::red), "{}", 42);
|
||||
*/
|
||||
template <typename OutputIt, typename... T,
|
||||
FMT_ENABLE_IF(detail::is_output_iterator<OutputIt, char>::value)>
|
||||
inline auto format_to(OutputIt out, text_style ts, format_string<T...> fmt,
|
||||
T&&... args) -> OutputIt {
|
||||
return vformat_to(out, ts, fmt.str, vargs<T...>{{args...}});
|
||||
}
|
||||
|
||||
template <typename T, typename Char>
|
||||
struct formatter<detail::styled_arg<T>, Char> : formatter<T, Char> {
|
||||
template <typename FormatContext>
|
||||
FMT_CONSTEXPR auto format(const detail::styled_arg<T>& arg,
|
||||
FormatContext& ctx) const -> decltype(ctx.out()) {
|
||||
const auto& ts = arg.style;
|
||||
auto out = ctx.out();
|
||||
|
||||
bool has_style = false;
|
||||
if (ts.has_emphasis()) {
|
||||
has_style = true;
|
||||
auto emphasis = detail::make_emphasis<Char>(ts.get_emphasis());
|
||||
out = detail::copy<Char>(emphasis.begin(), emphasis.end(), out);
|
||||
}
|
||||
if (ts.has_foreground()) {
|
||||
has_style = true;
|
||||
auto foreground =
|
||||
detail::make_foreground_color<Char>(ts.get_foreground());
|
||||
out = detail::copy<Char>(foreground.begin(), foreground.end(), out);
|
||||
}
|
||||
if (ts.has_background()) {
|
||||
has_style = true;
|
||||
auto background =
|
||||
detail::make_background_color<Char>(ts.get_background());
|
||||
out = detail::copy<Char>(background.begin(), background.end(), out);
|
||||
}
|
||||
out = formatter<T, Char>::format(arg.value, ctx);
|
||||
if (has_style) {
|
||||
auto reset_color = string_view("\x1b[0m");
|
||||
out = detail::copy<Char>(reset_color.begin(), reset_color.end(), out);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns an argument that will be formatted using ANSI escape sequences,
|
||||
* to be used in a formatting function.
|
||||
*
|
||||
* **Example**:
|
||||
*
|
||||
* fmt::print("Elapsed time: {0:.2f} seconds",
|
||||
* fmt::styled(1.23, fmt::fg(fmt::color::green) |
|
||||
* fmt::bg(fmt::color::blue)));
|
||||
*/
|
||||
template <typename T>
|
||||
FMT_CONSTEXPR auto styled(const T& value, text_style ts)
|
||||
-> detail::styled_arg<remove_cvref_t<T>> {
|
||||
return detail::styled_arg<remove_cvref_t<T>>{value, ts};
|
||||
}
|
||||
|
||||
FMT_END_EXPORT
|
||||
FMT_END_NAMESPACE
|
||||
|
||||
#endif // FMT_COLOR_H_
|
||||
-601
@@ -1,601 +0,0 @@
|
||||
// Formatting library for C++ - experimental format string compilation
|
||||
//
|
||||
// Copyright (c) 2012 - present, Victor Zverovich and {fmt} contributors
|
||||
// All rights reserved.
|
||||
//
|
||||
// For the license information refer to format.h.
|
||||
|
||||
#ifndef FMT_COMPILE_H_
|
||||
#define FMT_COMPILE_H_
|
||||
|
||||
#ifndef FMT_MODULE
|
||||
# include <iterator> // std::back_inserter
|
||||
#endif
|
||||
|
||||
#include "format.h"
|
||||
|
||||
FMT_BEGIN_NAMESPACE
|
||||
FMT_BEGIN_EXPORT
|
||||
|
||||
// A compile-time string which is compiled into fast formatting code.
|
||||
class compiled_string {};
|
||||
|
||||
template <typename S>
|
||||
struct is_compiled_string : std::is_base_of<compiled_string, S> {};
|
||||
|
||||
/**
|
||||
* Converts a string literal `s` into a format string that will be parsed at
|
||||
* compile time and converted into efficient formatting code. Requires C++17
|
||||
* `constexpr if` compiler support.
|
||||
*
|
||||
* **Example**:
|
||||
*
|
||||
* // Converts 42 into std::string using the most efficient method and no
|
||||
* // runtime format string processing.
|
||||
* std::string s = fmt::format(FMT_COMPILE("{}"), 42);
|
||||
*/
|
||||
#if defined(__cpp_if_constexpr) && defined(__cpp_return_type_deduction)
|
||||
# define FMT_COMPILE(s) FMT_STRING_IMPL(s, fmt::compiled_string)
|
||||
#else
|
||||
# define FMT_COMPILE(s) FMT_STRING(s)
|
||||
#endif
|
||||
|
||||
/**
|
||||
* Converts a string literal into a format string that will be parsed at
|
||||
* compile time and converted into efficient formatting code. Requires support
|
||||
* for class types in constant template parameters (a C++20 feature).
|
||||
*
|
||||
* **Example**:
|
||||
*
|
||||
* // Converts 42 into std::string using the most efficient method and no
|
||||
* // runtime format string processing.
|
||||
* using namespace fmt::literals;
|
||||
* std::string s = fmt::format("{}"_cf, 42);
|
||||
*/
|
||||
#if FMT_USE_NONTYPE_TEMPLATE_ARGS
|
||||
inline namespace literals {
|
||||
template <detail::fixed_string Str> constexpr auto operator""_cf() {
|
||||
return FMT_COMPILE(Str.data);
|
||||
}
|
||||
} // namespace literals
|
||||
#endif
|
||||
|
||||
FMT_END_EXPORT
|
||||
|
||||
namespace detail {
|
||||
|
||||
template <typename T, typename... Tail>
|
||||
constexpr auto first(const T& value, const Tail&...) -> const T& {
|
||||
return value;
|
||||
}
|
||||
|
||||
#if defined(__cpp_if_constexpr) && defined(__cpp_return_type_deduction)
|
||||
template <typename... T> struct type_list {};
|
||||
|
||||
// Returns a reference to the argument at index N from [first, rest...].
|
||||
template <int N, typename T, typename... Args>
|
||||
constexpr auto get([[maybe_unused]] const T& first,
|
||||
[[maybe_unused]] const Args&... rest) -> const auto& {
|
||||
static_assert(N < 1 + sizeof...(Args), "index is out of bounds");
|
||||
if constexpr (N == 0)
|
||||
return first;
|
||||
else
|
||||
return detail::get<N - 1>(rest...);
|
||||
}
|
||||
|
||||
# if FMT_USE_NONTYPE_TEMPLATE_ARGS
|
||||
template <int N, typename T, typename... Args, typename Char>
|
||||
constexpr auto get_arg_index_by_name(basic_string_view<Char> name) -> int {
|
||||
if constexpr (is_static_named_arg<T>()) {
|
||||
if (name == T::name) return N;
|
||||
}
|
||||
if constexpr (sizeof...(Args) > 0)
|
||||
return get_arg_index_by_name<N + 1, Args...>(name);
|
||||
(void)name; // Workaround an MSVC bug about "unused" parameter.
|
||||
return -1;
|
||||
}
|
||||
# endif
|
||||
|
||||
template <typename... Args, typename Char>
|
||||
FMT_CONSTEXPR auto get_arg_index_by_name(basic_string_view<Char> name) -> int {
|
||||
# if FMT_USE_NONTYPE_TEMPLATE_ARGS
|
||||
if constexpr (sizeof...(Args) > 0)
|
||||
return get_arg_index_by_name<0, Args...>(name);
|
||||
# endif
|
||||
(void)name;
|
||||
return -1;
|
||||
}
|
||||
|
||||
template <typename Char, typename... Args>
|
||||
constexpr auto get_arg_index_by_name(basic_string_view<Char> name,
|
||||
type_list<Args...>) -> int {
|
||||
return get_arg_index_by_name<Args...>(name);
|
||||
}
|
||||
|
||||
template <int N, typename> struct get_type_impl;
|
||||
|
||||
template <int N, typename... Args> struct get_type_impl<N, type_list<Args...>> {
|
||||
using type =
|
||||
remove_cvref_t<decltype(detail::get<N>(std::declval<Args>()...))>;
|
||||
};
|
||||
|
||||
template <int N, typename T>
|
||||
using get_type = typename get_type_impl<N, T>::type;
|
||||
|
||||
template <typename T> struct is_compiled_format : std::false_type {};
|
||||
|
||||
template <typename Char> struct text {
|
||||
basic_string_view<Char> data;
|
||||
using char_type = Char;
|
||||
|
||||
template <typename OutputIt, typename... T>
|
||||
constexpr auto format(OutputIt out, const T&...) const -> OutputIt {
|
||||
return write<Char>(out, data);
|
||||
}
|
||||
};
|
||||
|
||||
template <typename Char>
|
||||
struct is_compiled_format<text<Char>> : std::true_type {};
|
||||
|
||||
template <typename Char>
|
||||
constexpr auto make_text(basic_string_view<Char> s, size_t pos, size_t size)
|
||||
-> text<Char> {
|
||||
return {{&s[pos], size}};
|
||||
}
|
||||
|
||||
template <typename Char> struct code_unit {
|
||||
Char value;
|
||||
using char_type = Char;
|
||||
|
||||
template <typename OutputIt, typename... T>
|
||||
constexpr auto format(OutputIt out, const T&...) const -> OutputIt {
|
||||
*out++ = value;
|
||||
return out;
|
||||
}
|
||||
};
|
||||
|
||||
// This ensures that the argument type is convertible to `const T&`.
|
||||
template <typename T, int N, typename... Args>
|
||||
constexpr auto get_arg_checked(const Args&... args) -> const T& {
|
||||
const auto& arg = detail::get<N>(args...);
|
||||
if constexpr (detail::is_named_arg<remove_cvref_t<decltype(arg)>>()) {
|
||||
return arg.value;
|
||||
} else {
|
||||
return arg;
|
||||
}
|
||||
}
|
||||
|
||||
template <typename Char>
|
||||
struct is_compiled_format<code_unit<Char>> : std::true_type {};
|
||||
|
||||
// A replacement field that refers to argument N.
|
||||
template <typename Char, typename V, int N> struct field {
|
||||
using char_type = Char;
|
||||
|
||||
template <typename OutputIt, typename... T>
|
||||
constexpr auto format(OutputIt out, const T&... args) const -> OutputIt {
|
||||
const V& arg = get_arg_checked<V, N>(args...);
|
||||
if constexpr (std::is_convertible<V, basic_string_view<Char>>::value) {
|
||||
auto s = basic_string_view<Char>(arg);
|
||||
return copy<Char>(s.begin(), s.end(), out);
|
||||
} else {
|
||||
return write<Char>(out, arg);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
template <typename Char, typename T, int N>
|
||||
struct is_compiled_format<field<Char, T, N>> : std::true_type {};
|
||||
|
||||
// A replacement field that refers to argument with name.
|
||||
template <typename Char> struct runtime_named_field {
|
||||
using char_type = Char;
|
||||
basic_string_view<Char> name;
|
||||
|
||||
template <typename OutputIt, typename T>
|
||||
constexpr static auto try_format_argument(
|
||||
OutputIt& out,
|
||||
// [[maybe_unused]] due to unused-but-set-parameter warning in GCC 7,8,9
|
||||
[[maybe_unused]] basic_string_view<Char> arg_name, const T& arg) -> bool {
|
||||
if constexpr (is_named_arg<typename std::remove_cv<T>::type>::value) {
|
||||
if (arg_name == arg.name) {
|
||||
out = write<Char>(out, arg.value);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
template <typename OutputIt, typename... T>
|
||||
constexpr auto format(OutputIt out, const T&... args) const -> OutputIt {
|
||||
bool found = (try_format_argument(out, name, args) || ...);
|
||||
if (!found) {
|
||||
FMT_THROW(format_error("argument with specified name is not found"));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
};
|
||||
|
||||
template <typename Char>
|
||||
struct is_compiled_format<runtime_named_field<Char>> : std::true_type {};
|
||||
|
||||
// A replacement field that refers to argument N and has format specifiers.
|
||||
template <typename Char, typename V, int N> struct spec_field {
|
||||
using char_type = Char;
|
||||
formatter<V, Char> fmt;
|
||||
|
||||
template <typename OutputIt, typename... T>
|
||||
constexpr FMT_INLINE auto format(OutputIt out, const T&... args) const
|
||||
-> OutputIt {
|
||||
const auto& vargs =
|
||||
fmt::make_format_args<basic_format_context<OutputIt, Char>>(args...);
|
||||
basic_format_context<OutputIt, Char> ctx(out, vargs);
|
||||
return fmt.format(get_arg_checked<V, N>(args...), ctx);
|
||||
}
|
||||
};
|
||||
|
||||
template <typename Char, typename T, int N>
|
||||
struct is_compiled_format<spec_field<Char, T, N>> : std::true_type {};
|
||||
|
||||
template <typename L, typename R> struct concat {
|
||||
L lhs;
|
||||
R rhs;
|
||||
using char_type = typename L::char_type;
|
||||
|
||||
template <typename OutputIt, typename... T>
|
||||
constexpr auto format(OutputIt out, const T&... args) const -> OutputIt {
|
||||
out = lhs.format(out, args...);
|
||||
return rhs.format(out, args...);
|
||||
}
|
||||
};
|
||||
|
||||
template <typename L, typename R>
|
||||
struct is_compiled_format<concat<L, R>> : std::true_type {};
|
||||
|
||||
template <typename L, typename R>
|
||||
constexpr auto make_concat(L lhs, R rhs) -> concat<L, R> {
|
||||
return {lhs, rhs};
|
||||
}
|
||||
|
||||
struct unknown_format {};
|
||||
|
||||
template <typename Char>
|
||||
constexpr auto parse_text(basic_string_view<Char> str, size_t pos) -> size_t {
|
||||
for (size_t size = str.size(); pos != size; ++pos) {
|
||||
if (str[pos] == '{' || str[pos] == '}') break;
|
||||
}
|
||||
return pos;
|
||||
}
|
||||
|
||||
template <typename Args, size_t POS, int ID, bool DYNAMIC_NAMES, typename S>
|
||||
constexpr auto compile_format_string(S fmt);
|
||||
|
||||
template <typename Args, size_t POS, int ID, bool DYNAMIC_NAMES, typename T,
|
||||
typename S>
|
||||
constexpr auto parse_tail(T head, S fmt) {
|
||||
if constexpr (POS != basic_string_view<typename S::char_type>(fmt).size()) {
|
||||
constexpr auto tail =
|
||||
compile_format_string<Args, POS, ID, DYNAMIC_NAMES>(fmt);
|
||||
if constexpr (std::is_same<remove_cvref_t<decltype(tail)>,
|
||||
unknown_format>())
|
||||
return tail;
|
||||
else
|
||||
return make_concat(head, tail);
|
||||
} else {
|
||||
return head;
|
||||
}
|
||||
}
|
||||
|
||||
template <typename T, typename Char> struct parse_specs_result {
|
||||
formatter<T, Char> fmt;
|
||||
size_t end;
|
||||
int next_arg_id;
|
||||
};
|
||||
|
||||
enum { manual_indexing_id = -1 };
|
||||
|
||||
template <typename T, typename Char>
|
||||
constexpr auto parse_specs(basic_string_view<Char> str, size_t pos,
|
||||
int next_arg_id) -> parse_specs_result<T, Char> {
|
||||
str.remove_prefix(pos);
|
||||
auto ctx =
|
||||
compile_parse_context<Char>(str, max_value<int>(), nullptr, next_arg_id);
|
||||
auto f = formatter<T, Char>();
|
||||
auto end = f.parse(ctx);
|
||||
return {f, pos + fmt::detail::to_unsigned(end - str.data()),
|
||||
next_arg_id == 0 ? manual_indexing_id : ctx.next_arg_id()};
|
||||
}
|
||||
|
||||
template <typename Char> struct arg_id_handler {
|
||||
arg_id_kind kind;
|
||||
arg_ref<Char> arg_id;
|
||||
|
||||
constexpr auto on_auto() -> int {
|
||||
FMT_ASSERT(false, "handler cannot be used with automatic indexing");
|
||||
return 0;
|
||||
}
|
||||
constexpr auto on_index(int id) -> int {
|
||||
kind = arg_id_kind::index;
|
||||
arg_id = arg_ref<Char>(id);
|
||||
return 0;
|
||||
}
|
||||
constexpr auto on_name(basic_string_view<Char> id) -> int {
|
||||
kind = arg_id_kind::name;
|
||||
arg_id = arg_ref<Char>(id);
|
||||
return 0;
|
||||
}
|
||||
};
|
||||
|
||||
template <typename Char> struct parse_arg_id_result {
|
||||
arg_id_kind kind;
|
||||
arg_ref<Char> arg_id;
|
||||
const Char* arg_id_end;
|
||||
};
|
||||
|
||||
template <int ID, typename Char>
|
||||
constexpr auto parse_arg_id(const Char* begin, const Char* end) {
|
||||
auto handler = arg_id_handler<Char>{arg_id_kind::none, arg_ref<Char>{}};
|
||||
auto arg_id_end = parse_arg_id(begin, end, handler);
|
||||
return parse_arg_id_result<Char>{handler.kind, handler.arg_id, arg_id_end};
|
||||
}
|
||||
|
||||
template <typename T, typename Enable = void> struct field_type {
|
||||
using type = remove_cvref_t<T>;
|
||||
};
|
||||
|
||||
template <typename T>
|
||||
struct field_type<T, enable_if_t<detail::is_named_arg<T>::value>> {
|
||||
using type = remove_cvref_t<decltype(T::value)>;
|
||||
};
|
||||
|
||||
template <typename T, typename Args, size_t END_POS, int ARG_INDEX, int NEXT_ID,
|
||||
bool DYNAMIC_NAMES, typename S>
|
||||
constexpr auto parse_replacement_field_then_tail(S fmt) {
|
||||
using char_type = typename S::char_type;
|
||||
constexpr auto str = basic_string_view<char_type>(fmt);
|
||||
constexpr char_type c = END_POS != str.size() ? str[END_POS] : char_type();
|
||||
if constexpr (c == '}') {
|
||||
return parse_tail<Args, END_POS + 1, NEXT_ID, DYNAMIC_NAMES>(
|
||||
field<char_type, typename field_type<T>::type, ARG_INDEX>(), fmt);
|
||||
} else if constexpr (c != ':') {
|
||||
FMT_THROW(format_error("expected ':'"));
|
||||
} else {
|
||||
constexpr auto result = parse_specs<typename field_type<T>::type>(
|
||||
str, END_POS + 1, NEXT_ID == manual_indexing_id ? 0 : NEXT_ID);
|
||||
if constexpr (result.end >= str.size() || str[result.end] != '}') {
|
||||
FMT_THROW(format_error("expected '}'"));
|
||||
return 0;
|
||||
} else {
|
||||
return parse_tail<Args, result.end + 1, result.next_arg_id,
|
||||
DYNAMIC_NAMES>(
|
||||
spec_field<char_type, typename field_type<T>::type, ARG_INDEX>{
|
||||
result.fmt},
|
||||
fmt);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Compiles a non-empty format string and returns the compiled representation
|
||||
// or unknown_format() on unrecognized input.
|
||||
template <typename Args, size_t POS, int ID, bool DYNAMIC_NAMES, typename S>
|
||||
constexpr auto compile_format_string(S fmt) {
|
||||
using char_type = typename S::char_type;
|
||||
constexpr auto str = basic_string_view<char_type>(fmt);
|
||||
if constexpr (str[POS] == '{') {
|
||||
if constexpr (POS + 1 == str.size())
|
||||
FMT_THROW(format_error("unmatched '{' in format string"));
|
||||
if constexpr (str[POS + 1] == '{') {
|
||||
return parse_tail<Args, POS + 2, ID, DYNAMIC_NAMES>(
|
||||
make_text(str, POS, 1), fmt);
|
||||
} else if constexpr (str[POS + 1] == '}' || str[POS + 1] == ':') {
|
||||
static_assert(ID != manual_indexing_id,
|
||||
"cannot switch from manual to automatic argument indexing");
|
||||
constexpr auto next_id =
|
||||
ID != manual_indexing_id ? ID + 1 : manual_indexing_id;
|
||||
return parse_replacement_field_then_tail<
|
||||
get_type<ID, Args>, Args, POS + 1, ID, next_id, DYNAMIC_NAMES>(fmt);
|
||||
} else {
|
||||
constexpr auto arg_id_result =
|
||||
parse_arg_id<ID>(str.data() + POS + 1, str.data() + str.size());
|
||||
constexpr auto arg_id_end_pos = arg_id_result.arg_id_end - str.data();
|
||||
constexpr char_type c =
|
||||
arg_id_end_pos != str.size() ? str[arg_id_end_pos] : char_type();
|
||||
static_assert(c == '}' || c == ':', "missing '}' in format string");
|
||||
if constexpr (arg_id_result.kind == arg_id_kind::index) {
|
||||
static_assert(
|
||||
ID == manual_indexing_id || ID == 0,
|
||||
"cannot switch from automatic to manual argument indexing");
|
||||
constexpr auto arg_index = arg_id_result.arg_id.index;
|
||||
return parse_replacement_field_then_tail<
|
||||
get_type<arg_index, Args>, Args, arg_id_end_pos, arg_index,
|
||||
manual_indexing_id, DYNAMIC_NAMES>(fmt);
|
||||
} else if constexpr (arg_id_result.kind == arg_id_kind::name) {
|
||||
constexpr auto arg_index =
|
||||
get_arg_index_by_name(arg_id_result.arg_id.name, Args{});
|
||||
|
||||
static_assert(arg_index >= 0 || DYNAMIC_NAMES,
|
||||
"named argument not found");
|
||||
|
||||
if constexpr (arg_index >= 0) {
|
||||
constexpr auto next_id =
|
||||
ID != manual_indexing_id ? ID + 1 : manual_indexing_id;
|
||||
return parse_replacement_field_then_tail<
|
||||
decltype(get_type<arg_index, Args>::value), Args, arg_id_end_pos,
|
||||
arg_index, next_id, DYNAMIC_NAMES>(fmt);
|
||||
} else if constexpr (c == '}') {
|
||||
return parse_tail<Args, arg_id_end_pos + 1, ID, DYNAMIC_NAMES>(
|
||||
runtime_named_field<char_type>{arg_id_result.arg_id.name}, fmt);
|
||||
} else if constexpr (c == ':') {
|
||||
return unknown_format(); // no type info for specs parsing
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if constexpr (str[POS] == '}') {
|
||||
if constexpr (POS + 1 == str.size())
|
||||
FMT_THROW(format_error("unmatched '}' in format string"));
|
||||
return parse_tail<Args, POS + 2, ID, DYNAMIC_NAMES>(make_text(str, POS, 1),
|
||||
fmt);
|
||||
} else {
|
||||
constexpr auto end = parse_text(str, POS + 1);
|
||||
if constexpr (end - POS > 1) {
|
||||
return parse_tail<Args, end, ID, DYNAMIC_NAMES>(
|
||||
make_text(str, POS, end - POS), fmt);
|
||||
} else {
|
||||
return parse_tail<Args, end, ID, DYNAMIC_NAMES>(
|
||||
code_unit<char_type>{str[POS]}, fmt);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
template <typename... Args, typename S,
|
||||
FMT_ENABLE_IF(is_compiled_string<S>::value)>
|
||||
constexpr auto compile(S fmt) {
|
||||
constexpr auto str = basic_string_view<typename S::char_type>(fmt);
|
||||
if constexpr (str.size() == 0) {
|
||||
return detail::make_text(str, 0, 0);
|
||||
} else {
|
||||
constexpr int num_static_named_args =
|
||||
detail::count_static_named_args<Args...>();
|
||||
constexpr auto result = detail::compile_format_string<
|
||||
detail::type_list<Args...>, 0, 0,
|
||||
num_static_named_args != detail::count_named_args<Args...>()>(fmt);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
#endif // defined(__cpp_if_constexpr) && defined(__cpp_return_type_deduction)
|
||||
} // namespace detail
|
||||
|
||||
FMT_BEGIN_EXPORT
|
||||
|
||||
#if defined(__cpp_if_constexpr) && defined(__cpp_return_type_deduction)
|
||||
|
||||
template <typename CompiledFormat, typename... T,
|
||||
typename Char = typename CompiledFormat::char_type,
|
||||
FMT_ENABLE_IF(detail::is_compiled_format<CompiledFormat>::value)>
|
||||
FMT_INLINE FMT_CONSTEXPR_STRING auto format(const CompiledFormat& cf,
|
||||
const T&... args)
|
||||
-> std::basic_string<Char> {
|
||||
auto s = std::basic_string<Char>();
|
||||
cf.format(std::back_inserter(s), args...);
|
||||
return s;
|
||||
}
|
||||
|
||||
template <typename OutputIt, typename CompiledFormat, typename... T,
|
||||
FMT_ENABLE_IF(detail::is_compiled_format<CompiledFormat>::value)>
|
||||
constexpr FMT_INLINE auto format_to(OutputIt out, const CompiledFormat& cf,
|
||||
const T&... args) -> OutputIt {
|
||||
return cf.format(out, args...);
|
||||
}
|
||||
|
||||
template <typename S, typename... T,
|
||||
FMT_ENABLE_IF(is_compiled_string<S>::value)>
|
||||
FMT_INLINE FMT_CONSTEXPR_STRING auto format(const S&, T&&... args)
|
||||
-> std::basic_string<typename S::char_type> {
|
||||
if constexpr (std::is_same<typename S::char_type, char>::value) {
|
||||
constexpr auto str = basic_string_view<typename S::char_type>(S());
|
||||
if constexpr (str.size() == 2 && str[0] == '{' && str[1] == '}') {
|
||||
const auto& first = detail::first(args...);
|
||||
if constexpr (detail::is_named_arg<
|
||||
remove_cvref_t<decltype(first)>>::value) {
|
||||
return fmt::to_string(first.value);
|
||||
} else {
|
||||
return fmt::to_string(first);
|
||||
}
|
||||
}
|
||||
}
|
||||
constexpr auto compiled = detail::compile<T...>(S());
|
||||
if constexpr (std::is_same<remove_cvref_t<decltype(compiled)>,
|
||||
detail::unknown_format>()) {
|
||||
return fmt::format(
|
||||
static_cast<basic_string_view<typename S::char_type>>(S()),
|
||||
std::forward<T>(args)...);
|
||||
} else {
|
||||
return fmt::format(compiled, std::forward<T>(args)...);
|
||||
}
|
||||
}
|
||||
|
||||
template <typename OutputIt, typename S, typename... T,
|
||||
FMT_ENABLE_IF(is_compiled_string<S>::value)>
|
||||
FMT_CONSTEXPR auto format_to(OutputIt out, const S&, T&&... args) -> OutputIt {
|
||||
constexpr auto compiled = detail::compile<T...>(S());
|
||||
if constexpr (std::is_same<remove_cvref_t<decltype(compiled)>,
|
||||
detail::unknown_format>()) {
|
||||
return fmt::format_to(
|
||||
out, static_cast<basic_string_view<typename S::char_type>>(S()),
|
||||
std::forward<T>(args)...);
|
||||
} else {
|
||||
return fmt::format_to(out, compiled, std::forward<T>(args)...);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
template <typename OutputIt, typename S, typename... T,
|
||||
FMT_ENABLE_IF(is_compiled_string<S>::value)>
|
||||
auto format_to_n(OutputIt out, size_t n, const S& fmt, T&&... args)
|
||||
-> format_to_n_result<OutputIt> {
|
||||
using traits = detail::fixed_buffer_traits;
|
||||
auto buf = detail::iterator_buffer<OutputIt, char, traits>(out, n);
|
||||
fmt::format_to(appender(buf), fmt, std::forward<T>(args)...);
|
||||
return {buf.out(), buf.count()};
|
||||
}
|
||||
|
||||
template <typename S, typename... T,
|
||||
FMT_ENABLE_IF(is_compiled_string<S>::value)>
|
||||
FMT_CONSTEXPR20 auto formatted_size(const S& fmt, T&&... args) -> size_t {
|
||||
auto buf = detail::counting_buffer<>();
|
||||
fmt::format_to(appender(buf), fmt, std::forward<T>(args)...);
|
||||
return buf.count();
|
||||
}
|
||||
|
||||
template <typename S, typename... T,
|
||||
FMT_ENABLE_IF(is_compiled_string<S>::value)>
|
||||
void print(std::FILE* f, const S& fmt, T&&... args) {
|
||||
auto buf = memory_buffer();
|
||||
fmt::format_to(appender(buf), fmt, std::forward<T>(args)...);
|
||||
detail::print(f, {buf.data(), buf.size()});
|
||||
}
|
||||
|
||||
template <typename S, typename... T,
|
||||
FMT_ENABLE_IF(is_compiled_string<S>::value)>
|
||||
void print(const S& fmt, T&&... args) {
|
||||
print(stdout, fmt, std::forward<T>(args)...);
|
||||
}
|
||||
|
||||
template <size_t N> class static_format_result {
|
||||
private:
|
||||
char data[N];
|
||||
|
||||
public:
|
||||
template <typename S, typename... T,
|
||||
FMT_ENABLE_IF(is_compiled_string<S>::value)>
|
||||
explicit FMT_CONSTEXPR static_format_result(const S& fmt, T&&... args) {
|
||||
*fmt::format_to(data, fmt, std::forward<T>(args)...) = '\0';
|
||||
}
|
||||
|
||||
FMT_CONSTEXPR auto str() const -> fmt::string_view { return {data, N - 1}; }
|
||||
FMT_CONSTEXPR auto c_str() const -> const char* { return data; }
|
||||
};
|
||||
|
||||
/**
|
||||
* Formats arguments according to the format string `fmt_str` and produces
|
||||
* a string of the exact required size at compile time. Both the format string
|
||||
* and the arguments must be compile-time expressions.
|
||||
*
|
||||
* The resulting string can be accessed as a C string via `c_str()` or as
|
||||
* a `fmt::string_view` via `str()`.
|
||||
*
|
||||
* **Example**:
|
||||
*
|
||||
* // Produces the static string "42" at compile time.
|
||||
* static constexpr auto result = FMT_STATIC_FORMAT("{}", 42);
|
||||
* const char* s = result.c_str();
|
||||
*/
|
||||
#define FMT_STATIC_FORMAT(fmt_str, ...) \
|
||||
fmt::static_format_result< \
|
||||
fmt::formatted_size(FMT_COMPILE(fmt_str), __VA_ARGS__) + 1>( \
|
||||
FMT_COMPILE(fmt_str), __VA_ARGS__)
|
||||
|
||||
FMT_END_EXPORT
|
||||
FMT_END_NAMESPACE
|
||||
|
||||
#endif // FMT_COMPILE_H_
|
||||
-14
@@ -1,14 +0,0 @@
|
||||
// Formatting library for C++ - core API
|
||||
//
|
||||
// Copyright (c) 2012 - present, Victor Zverovich and {fmt} contributors
|
||||
// All rights reserved.
|
||||
//
|
||||
// For the license information refer to format.h.
|
||||
|
||||
#include "base.h"
|
||||
|
||||
// Using fmt::format via fmt/core.h has been deprecated since version 11
|
||||
// and now requires an explicit opt in.
|
||||
#ifdef FMT_DEPRECATED_HEAVY_CORE
|
||||
# include "format.h"
|
||||
#endif
|
||||
-201
@@ -1,201 +0,0 @@
|
||||
// Formatting library for C++ - the C API
|
||||
//
|
||||
// Copyright (c) 2012 - present, Victor Zverovich and {fmt} contributors
|
||||
// All rights reserved.
|
||||
//
|
||||
// For the license information refer to format.h.
|
||||
|
||||
#ifndef FMT_C_H_
|
||||
#define FMT_C_H_
|
||||
|
||||
#include <stdbool.h> // bool
|
||||
#include <stddef.h> // size_t
|
||||
#include <stdio.h> // FILE
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
typedef enum {
|
||||
fmt_int = 1,
|
||||
fmt_uint,
|
||||
fmt_bool = 7,
|
||||
fmt_char,
|
||||
fmt_float,
|
||||
fmt_double,
|
||||
fmt_long_double,
|
||||
fmt_cstring,
|
||||
fmt_pointer = 14
|
||||
} fmt_type;
|
||||
|
||||
typedef struct {
|
||||
fmt_type type;
|
||||
union {
|
||||
long long int_value;
|
||||
unsigned long long uint_value; // Used for FMT_PTR and custom data
|
||||
bool bool_value;
|
||||
char char_value;
|
||||
float float_value;
|
||||
double double_value;
|
||||
long double long_double_value;
|
||||
const char* cstring;
|
||||
const void* pointer;
|
||||
} value;
|
||||
} fmt_arg;
|
||||
|
||||
enum { fmt_error = -1, fmt_error_invalid_arg = -2 };
|
||||
|
||||
int fmt_vformat(char* buffer, size_t size, const char* fmt, const fmt_arg* args,
|
||||
size_t num_args);
|
||||
int fmt_vprint(FILE* stream, const char* fmt, const fmt_arg* args,
|
||||
size_t num_args);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifndef __cplusplus
|
||||
|
||||
static inline fmt_arg fmt_from_int(long long x) {
|
||||
return (fmt_arg){.type = fmt_int, .value.int_value = x};
|
||||
}
|
||||
|
||||
static inline fmt_arg fmt_from_uint(unsigned long long x) {
|
||||
return (fmt_arg){.type = fmt_uint, .value.uint_value = x};
|
||||
}
|
||||
|
||||
static inline fmt_arg fmt_from_bool(bool x) {
|
||||
return (fmt_arg){.type = fmt_bool, .value.bool_value = x};
|
||||
}
|
||||
|
||||
static inline fmt_arg fmt_from_char(char x) {
|
||||
return (fmt_arg){.type = fmt_char, .value.char_value = x};
|
||||
}
|
||||
|
||||
static inline fmt_arg fmt_from_float(float x) {
|
||||
return (fmt_arg){.type = fmt_float, .value.float_value = x};
|
||||
}
|
||||
|
||||
static inline fmt_arg fmt_from_double(double x) {
|
||||
return (fmt_arg){.type = fmt_double, .value.double_value = x};
|
||||
}
|
||||
|
||||
static inline fmt_arg fmt_from_long_double(long double x) {
|
||||
return (fmt_arg){.type = fmt_long_double, .value.long_double_value = x};
|
||||
}
|
||||
|
||||
static inline fmt_arg fmt_from_str(const char* x) {
|
||||
return (fmt_arg){.type = fmt_cstring, .value.cstring = x};
|
||||
}
|
||||
|
||||
static inline fmt_arg fmt_from_ptr(const void* x) {
|
||||
return (fmt_arg){.type = fmt_pointer, .value.pointer = x};
|
||||
}
|
||||
|
||||
void fmt_unsupported_type(void);
|
||||
|
||||
# if !defined(_MSC_VER) || defined(__clang__)
|
||||
typedef signed char fmt_signed_char;
|
||||
# else
|
||||
typedef enum {} fmt_signed_char;
|
||||
# endif
|
||||
|
||||
// Require modern MSVC with conformant preprocessor.
|
||||
# if defined(_MSC_VER) && !defined(__clang__) && \
|
||||
(!defined(_MSVC_TRADITIONAL) || _MSVC_TRADITIONAL)
|
||||
# error "C API requires MSVC 2019+ with /Zc:preprocessor flag."
|
||||
# endif
|
||||
|
||||
# define FMT_MAKE_ARG(x) \
|
||||
_Generic((x), \
|
||||
fmt_signed_char: fmt_from_int, \
|
||||
unsigned char: fmt_from_uint, \
|
||||
short: fmt_from_int, \
|
||||
unsigned short: fmt_from_uint, \
|
||||
int: fmt_from_int, \
|
||||
unsigned int: fmt_from_uint, \
|
||||
long: fmt_from_int, \
|
||||
unsigned long: fmt_from_uint, \
|
||||
long long: fmt_from_int, \
|
||||
unsigned long long: fmt_from_uint, \
|
||||
bool: fmt_from_bool, \
|
||||
char: fmt_from_char, \
|
||||
float: fmt_from_float, \
|
||||
double: fmt_from_double, \
|
||||
long double: fmt_from_long_double, \
|
||||
char*: fmt_from_str, \
|
||||
const char*: fmt_from_str, \
|
||||
void*: fmt_from_ptr, \
|
||||
const void*: fmt_from_ptr, \
|
||||
default: fmt_unsupported_type)(x)
|
||||
|
||||
# define FMT_CAT(a, b) FMT_CAT_(a, b)
|
||||
# define FMT_CAT_(a, b) a##b
|
||||
|
||||
# define FMT_NARG_(_unused, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, \
|
||||
_12, _13, _14, _15, _16, N, ...) \
|
||||
N
|
||||
# define FMT_NARG(_unused, ...) \
|
||||
FMT_NARG_(, ##__VA_ARGS__, 16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, \
|
||||
3, 2, 1, 0)
|
||||
|
||||
# define FMT_MAP_0(...)
|
||||
# define FMT_MAP_1(f, a) f(a)
|
||||
# define FMT_MAP_2(f, a, b) f(a), f(b)
|
||||
# define FMT_MAP_3(f, a, b, c) f(a), f(b), f(c)
|
||||
# define FMT_MAP_4(f, a, b, c, d) f(a), f(b), f(c), f(d)
|
||||
# define FMT_MAP_5(f, a, b, c, d, e) f(a), f(b), f(c), f(d), f(e)
|
||||
# define FMT_MAP_6(f, a, b, c, d, e, g) f(a), f(b), f(c), f(d), f(e), f(g)
|
||||
# define FMT_MAP_7(f, a, b, c, d, e, g, h) \
|
||||
f(a), f(b), f(c), f(d), f(e), f(g), f(h)
|
||||
# define FMT_MAP_8(f, a, b, c, d, e, g, h, i) \
|
||||
f(a), f(b), f(c), f(d), f(e), f(g), f(h), f(i)
|
||||
# define FMT_MAP_9(f, a, b, c, d, e, g, h, i, j) \
|
||||
f(a), f(b), f(c), f(d), f(e), f(g), f(h), f(i), f(j)
|
||||
# define FMT_MAP_10(f, a, b, c, d, e, g, h, i, j, k) \
|
||||
f(a), f(b), f(c), f(d), f(e), f(g), f(h), f(i), f(j), f(k)
|
||||
# define FMT_MAP_11(f, a, b, c, d, e, g, h, i, j, k, l) \
|
||||
f(a), f(b), f(c), f(d), f(e), f(g), f(h), f(i), f(j), f(k), f(l)
|
||||
# define FMT_MAP_12(f, a, b, c, d, e, g, h, i, j, k, l, m) \
|
||||
f(a), f(b), f(c), f(d), f(e), f(g), f(h), f(i), f(j), f(k), f(l), f(m)
|
||||
# define FMT_MAP_13(f, a, b, c, d, e, g, h, i, j, k, l, m, n) \
|
||||
f(a), f(b), f(c), f(d), f(e), f(g), f(h), f(i), f(j), f(k), f(l), f(m), f(n)
|
||||
# define FMT_MAP_14(f, a, b, c, d, e, g, h, i, j, k, l, m, n, o) \
|
||||
f(a), f(b), f(c), f(d), f(e), f(g), f(h), f(i), f(j), f(k), f(l), f(m), \
|
||||
f(n), f(o)
|
||||
# define FMT_MAP_15(f, a, b, c, d, e, g, h, i, j, k, l, m, n, o, p) \
|
||||
f(a), f(b), f(c), f(d), f(e), f(g), f(h), f(i), f(j), f(k), f(l), f(m), \
|
||||
f(n), f(o), f(p)
|
||||
# define FMT_MAP_16(f, a, b, c, d, e, g, h, i, j, k, l, m, n, o, p, q) \
|
||||
f(a), f(b), f(c), f(d), f(e), f(g), f(h), f(i), f(j), f(k), f(l), f(m), \
|
||||
f(n), f(o), f(p), f(q)
|
||||
|
||||
# define FMT_MAP(f, ...) \
|
||||
FMT_CAT(FMT_MAP_, FMT_NARG(, ##__VA_ARGS__))(f, ##__VA_ARGS__)
|
||||
|
||||
// select between two expressions depending on whether __VA_ARGS__ is empty
|
||||
// expands to e if __VA_ARGS__ is empty and n otherwise
|
||||
# define FMT_VA_SELECT(e, n, ...) \
|
||||
FMT_NARG_(, ##__VA_ARGS__, n, n, n, n, n, n, n, n, n, n, n, n, n, n, n, n, \
|
||||
e)
|
||||
|
||||
# define FMT_MAKE_NULL(...) NULL
|
||||
# define FMT_MAKE_ARGLIST(...) \
|
||||
(fmt_arg[]) { FMT_MAP(FMT_MAKE_ARG, ##__VA_ARGS__) }
|
||||
# define FMT_EXPAND(v) v
|
||||
|
||||
# define FMT_FORMAT_ARGS(fmt, ...) \
|
||||
(fmt), \
|
||||
FMT_EXPAND(FMT_VA_SELECT(FMT_MAKE_NULL, FMT_MAKE_ARGLIST, \
|
||||
##__VA_ARGS__)(__VA_ARGS__)), \
|
||||
FMT_NARG(, ##__VA_ARGS__)
|
||||
|
||||
# define fmt_format(buffer, size, fmt, ...) \
|
||||
fmt_vformat((buffer), (size), FMT_FORMAT_ARGS((fmt), ##__VA_ARGS__))
|
||||
|
||||
# define fmt_print(stream, fmt, ...) \
|
||||
fmt_vprint((stream), FMT_FORMAT_ARGS((fmt), ##__VA_ARGS__))
|
||||
|
||||
#endif // __cplusplus
|
||||
|
||||
#endif // FMT_C_H_
|
||||
-1974
File diff suppressed because it is too large.
Load diff
Loaded 100 of 1460 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user