Compare commits

..
136 Commits
Author SHA1 Message Date
ayozetr 98750d0687 AppImage: give appimagetool the version
appimagetool writes the desktop entry's X-AppImage-Version itself, from
the VERSION environment variable, and replaced the one the build had
added with "UNKNOWN" (#49). The build now exports VERSION instead.
2026-11-08 01:21:34 +00:00
ayozetr 0b3dac11d7 Launcher: version 0.70-linux.3 2026-11-08 01:15:44 +00:00
ayozetr 5041586f9a AppImage: version and description for AppImage managers
Gear Lever showed the AppImage's version as "UNKNOWN" (#49): its desktop
entry had no X-AppImage-Version and there was no AppStream metainfo.
The build now writes both: the version is taken from the launcher's
VERSION (bbport_update.py), so it is kept in one place, and the
metainfo carries the name, summary, description, licence, links and
that version. Both pass desktop-file-validate and appstreamcli validate.
2026-11-08 01:15:38 +00:00
ayozetr e77bce746f Menu pointer: clicks reach the quick menu while the Atlas tile shows
The pointer left a click alone while the port's overlay was open, and
that check was also true whenever Hunter's Atlas's Map tile showed in
the game's quick menu. Clicks on Status, System, Inventory and the rest
did nothing there; only the tile's Map opened.

The pointer now steps aside only when the overlay or the open map takes
the input, and leaves a click on the tile, with its release, to the
tile. With BB_MENU_POINTER_FILE set, each click is traced: whether the
pointer took it, why not, and the button it pressed.
2026-11-08 01:15:30 +00:00
ayozetr d1dea1b8e4 Menu pointer: the right mouse button is L2 in the game's menus
In a game menu the right button went back, and the mouse's other
buttons did not reach the game at all, so with keyboard and mouse
nothing could press L2 or R2 there. The storage box switches to the
storage with them (#44), and its prompt shows the right button for it:
pressing it closed the box instead.

Only the left button is the pointer's now. The others keep their
bindings in menus as well (the right one L2, the side ones R2 and L1),
as the game's prompts show them; Escape goes back. The test file's
"right" action is "back".
2026-11-08 01:15:30 +00:00
ayozetr 6796fbbd13 Window: ask for fullscreen again when Wayland drops it at start
On Wayland a program cannot place its window. With several monitors
KWin could map the game on the one SDL reports as primary, move it to
the monitor chosen in the launcher and leave it there as a plain
window (#39); asked again, fullscreen holds, as KDE's own shortcut
showed. During the first 10 seconds the port now asks again, at most
three times and a second apart, and logs each try. After that the
desktop decides, so its own shortcuts keep working.

TROUBLESHOOTING.md: choosing the monitor, the retry and
SDL_VIDEO_DRIVER=x11 as the fallback.
2026-11-07 12:57:45 +00:00
ayozetr badd460277 Texture cache: name the images when an overlap resolve fails
The warning said only "Image overlap resolve failed". It now gives the
image the game asked for and the one dropped for it (address, size,
dimensions, format, levels and layers), and whether the dropped image
held what the GPU drew: then its contents are lost and the new image is
read from guest memory. Issue #25 had two of these warnings around a
garbled texture that went away after a reload.
2026-11-07 11:56:21 +00:00
ayozetr 1d0bbec978 Launcher: version 0.70-linux.2 2026-11-04 11:11:38 +00:00
ayozetrandMark Hall 843424de13 Guest memory: allow device-local host types on single-pool GPUs
On integrated Intel Arc/Xe with Mesa's ANV driver every memory type is
device-local and host-pointer imports are allowed only into those, so no
type was found and the game stopped at startup with exit code 23. A second
pass now accepts a device-local host-visible type when no system-memory
type is allowed; the search is unchanged on other GPUs.

From deadinside28/bloodborne_pc#429.

Co-Authored-By: Mark Hall <[email protected]>
2026-11-04 11:11:26 +00:00
ayozetr 21e1ce4224 Game menu: Exit to desktop saves first in the world
Exit to desktop closed the window at once, so whatever the game had not
saved yet was lost (#25). In the world the System menu's entry now runs the
game's own Quit Game: its question becomes "Quit the game and return to the
desktop?", the game saves and goes back to the title, and the runtime closes
it there (the intro logos or the title menu, once no save data is mounted).

Answering No leaves the quit armed for two minutes at most; the game's own
Quit Game disarms it. On the title screen the entry still closes the game
at once, as there is nothing to save there.
2026-11-04 11:11:26 +00:00
ayozetr 498a37d24b Runtime: run the game's movbe on processors without MOVBE
The game's code was built for the PS4's CPU and has 987 movbe. AMD's FX
processors of the Piledriver generation have every other instruction the
game uses but not this one, so the game stopped with SIGILL (exit code 132)
at the first one, while reading a file header (#39).

The SIGILL handler now decodes the movbe at the faulting address, does the
load or store with its bytes swapped and resumes after it. On a processor
with MOVBE this never runs. The CPU check no longer refuses to start
without MOVBE and says in the log that the port runs them; the log also
counts them every 10 s (a few thousand a second while loading, about ten a
second while playing).

BB_MOVBE_TRAP=file turns the movbe listed by tools/movbe_sites.py into ud2,
so the emulation can be tested on any processor. tests/test_movbe.c checks
the forms the game uses.
2026-11-04 11:11:26 +00:00
ayozetr e8c7eaaf9c Players' guide: brought up to 0.70
The guide still described 0.51 in a few places:

- FAQ: The Hunter's Dream has no seamless co-op in 0.70 (summons are the
  normal game's), and no bridge relays there; the ports answer now says
  what UPnP, BBSC's relay and Tailscale each cover. New answers on the
  in-game map (preparing it, opening it with the quick menu, M or R3)
  and on FSR 4.1.2 (AMD's installer or amdxcffx64.dll, plus the FSR 4.1.x
  DLL, and the model in the port's menu).
- Troubleshooting: DLSS failures as 0.70 logs them ("DLSS: unavailable
  (...)"), Frame Generation on RTX 40 and newer, without the 0.4-linux.1
  note and a log line that no longer exists.
- Getting a log: the first line as 0.70 writes it.
- Online: the BBSC switch is "Turn on BBSC", as the launcher says.
2026-11-03 21:54:23 +00:00
ayozetr 74b1a30870 README: what the unlocked frame rate allows
The features list said the unlocked frame rate went "up to 120 FPS". In
0.7 it goes up to 480 FPS, the most the game's loading screens take, and
120 is the most the game's movement is sure to keep up with (the
launcher warns above it). The line now says so, and that the in-game
menu changes the frame rate while playing.
2026-11-03 21:38:26 +00:00
ayozetr 62dbeefa6b AppImage build: the new image is renamed into dist/
packaging/appimage-in-arch.sh copied the new AppImage over
dist/Bloodborne-bbport-x86_64.AppImage. When the previous build was still
open (its launcher, or the game), that changed the file under its mount
and the running program could crash. The new image is now copied next to
it and renamed over it: the open one keeps running from the old file,
and the next start uses the new one.
2026-11-03 21:20:06 +00:00
7c6b25a560 Keyboard: the Linux fork's movement keys by default
bbport 0.7 turns the movement keys into a stick that ramps up over 70 ms,
turns at 1100 degrees a second and keeps a diagonal within a round gate,
and opposite keys held together cancel out. In the game that stops a
sprint when A and D (or W and S) overlap for a moment, and a diagonal
gives 90 of 127 on each axis while the game picks walk or run by the
larger one.

The movement keys now work as in 0.51-linux: the one of two opposite
keys pressed last wins, a diagonal stays on the square, the direction
turns toward the keys' at 900 degrees a second and at most 30 degrees a
read (a half turn going through forward), and a released key holds its
direction for 60 ms, so the game never sees a turn of 45 degrees or a
neutral read that would end a sprint (cassioKenji's measurements in
Supermedo/bloodborne_pc#74). bbport.ini keyboard_movement=0.7 gives back
0.7's stick; stick_socd=neutral and stick_circular=1 work as before. The
walk key and the camera keys are unchanged. The pad tests cover the keys'
direction, the turn, the release hold and the settings; the README says
it, and Jhony-Jhansen and cassioKenji are back in the credits and the
About page's thanks (in the seven languages).

Co-Authored-By: João Pedro Valadares <[email protected]>
Co-Authored-By: Supermedo <[email protected]>
2026-11-03 21:19:29 +00:00
ayozetr 43ffa68201 Launcher: About page layout
The About page lists this project, the players' guide, support, the
licence and the thanks first, and the related projects' links in one
expandable row after them, with its title in the seven languages.
2026-11-03 21:17:15 +00:00
ayozetr 0f90125d4a Discord: the repository fits in the art's hover text
Discord shows the large image's text in a narrow box that wraps at spaces
only. "github.com/ayozetr/bloodborne_linux" was one word wider than the
box and came out cut ("github.com/ayozetr/bloodbor"). The text is now
"Bloodborne on Linux · ayozetr/bloodborne_linux on GitHub": the
repository has a line of its own and the "Get the Linux port" button
still opens it. The test checks that no word is longer than the box.
2026-11-03 21:12:17 +00:00
ayozetr 02dc648c96 Build release AppImages in a plain Arch Linux container
packaging/appimage-sharun.sh bundles the system libraries of the PC it
runs on. Built on CachyOS, those come from its x86-64-v3 repositories:
libpython, GLib and GTK then carry AVX2/BMI2 instructions outside any
CPU check, and the AppImage died with SIGILL before showing anything on
processors without them (AMD FX and Phenom, Intel before Haswell; issue
#39).

packaging/appimage-in-arch.sh clones the current commit with its
submodules into out/arch-build/, builds the image from
packaging/appimage-arch.Dockerfile (archlinux:base-devel plus the build
and packaging dependencies, VulkanMemoryAllocator 3.5.0 and xbyak 7.43
headers from git), and runs build.sh, tools/build_dlss.sh and
packaging/appimage-sharun.sh inside it as the calling user. The DLSS SDK
cache and fsr4_shaders/ already on the PC are reused. The image keeps the translations Arch's Docker image leaves
out (GTK's and libadwaita's, X11's compose tables) and has the ALSA plugins for
PipeWire and PulseAudio, as a desktop does. Host networking is
used for both the image build and the run.

Checked on the result: libpython, GLib and libstdc++ have no BMI2
instructions; the ones left in glibc, GTK (CRoaring), FFmpeg, OpenSSL,
GMP and the codecs sit behind runtime CPU dispatch, as in Arch itself.
The README names the script as the way releases are built.
2026-11-03 02:01:38 +00:00
ayozetr c16b6d3d58 Discord Rich Presence: the repository in the image's tooltip and a button to it
Whoever sees the status in Discord can find the port: the large image's tooltip
names github.com/ayozetr/bloodborne_linux, and a "Get the Linux port" button
opens it. Discord shows the button to the others looking at the profile, not to
the player. Tested against Discord's length limits.
2026-11-01 23:44:35 +00:00
ayozetr b4f75ec15c Credits: what this build actually takes from each one
The credits and the launcher's thanks listed work that did not come over to 0.7:
features 0.7 replaces (the direct mouse camera, ultrawide and the HUD position,
Quit to Desktop, bbbridge, DLSS's groundwork - 0.7's DLSS is its own), the
earlier online module's shadNet fixes, keyboard movement and sprint, cloth, plants
and effects at high frame rates (pending a test against 0.7's), and renderer
fixes 0.7 does differently. Each entry now names what is in this tree; entries
with nothing left go (MaskZeus, Jhony-Jhansen, mcrib884, HatsyRei, pmwork-personal,
cassioKenji, jpramosjp, devilismyfriend, Obrikash, Vorshim92, AwesomeObserver,
gibs2193, zFenrys, and Abken's mod 537), in the credits and the launcher's thanks
in every language. Work that comes back later brings its credit back with it.
2026-11-01 23:38:23 +00:00
ayozetr c8373033ad Icon: a hunter before the crimson moon
A new icon for the launcher, the AppImage's desktop entry and the Steam shortcut:
the hunter's silhouette before a blood moon over Yharnam's spires, in a red rounded
frame. 256 px with its corners transparent (it was 128 px), stripped of metadata;
it reads at 24 px too. The old one (launcher/bloodborne.svg and its PNG) goes, and
so does it from Supermedo's credit, in the credits and the launcher's thanks in
every language. A Steam shortcut takes the new icon the next time Add to Steam is
used.
2026-11-01 23:33:10 +00:00
ayozetr 42e8b4c606 Gestures menu without the motion-control icons, as an option again
The Linux fork offered Dommo's stripped-down gestures menu (Nexus Mods,
Bloodborne mod 30) as a switch on the Controls page; it went with the fork's
prompt layer when 0.7's live prompts took over. It is back:
scripts/gestures_menu.py applies assets/menu_prompts/gesturetop.json to the
player's own menu/gesturetop.gfx (only to the file the edits were made for) as a
layer under the mods, when BB_GESTURES_NO_MOTION=1, the launcher's switch, keeps
the same setting it had. Checked against a retail 1.09 dump: the edits apply.
2026-11-01 23:31:27 +00:00
ayozetr afac1a4f39 Launcher: an own server at Seamless Hunt's address keeps its address
0.7's default shadNet address is Seamless Hunt's, and the Online page left the
own-server field empty when it held the default. The Linux fork's guide had
players enter Seamless Hunt as their own server, so for them the field showed
empty, and saving the page would have lost the server. With an own server
chosen the field now always shows its address.
2026-11-01 23:23:52 +00:00
ayozetr 4c1e2e3061 Launcher: BBSC's settings again, folded under its switch
The Linux fork's launcher had a page of BBSC's options; on 0.7 only the switch
was left, and the options could be changed through extra variables only. They
are back as an expander, "BBSC settings", under the switch: what the guest
shares with the host, five hunters, enemy scaling and its percentages per guest,
shared progress and the rest, 28 options from BBSC 0.0.2's list
(scripts/bbsc_options.json) with their names in the launcher's seven
languages. Only the options changed from BBSC's defaults are kept and passed to
the game (scripts/bbsc.py, through online.environment). Left out: BBSC's two
master switches (the BBSC switch is the one), its own Discord status and its P2P
channel, which 0.7's BBSC does not read.
2026-11-01 23:23:52 +00:00
ayozetr ab3088469a Docs: online and environment pages match 0.7's online module
- ONLINE.md quoted log lines of the earlier online module and said the co-op port
  is random by default. 0.7 uses UDP 3658, the PS4's own, unless another is set (the
  launcher's 0 keeps it), and logs its own lines for the router, the port, a sign-in
  and a server that refuses the build; the page quotes those now. The launcher's
  comment on the setting says the same.
- ENVIRONMENT.md: its list regenerated (BB_STEAM_INPUT_X11, BB_MOTION_LARGE and the
  others added since), and its header says which part is generated.
- The design notes (memory module, translator, parallel GPU commands) say they
  predate 0.7 and name switches that no longer exist.
2026-11-01 17:02:04 +00:00
ayozetr dbc374fcec run.sh: an explicit render size above 1080p gets the direct memory it needs
0.7 gives a scene above 1080p more direct memory on its scaled path only. A render
size set by hand (BB_RENDER_RES) skips that path while patches.py still grows the
graphics heaps for it, and the game stopped at start (exit 139); the Linux fork's
run.sh handled it. It does again, through the same large_scene_memory.
2026-11-01 17:00:38 +00:00
ayozetr 8e9dbc20e1 Game menu: 0.7's new rows in every language, and the test that checks them
The rows 0.7 adds to the game's own menu (the Display page with aspect ratio,
resolution and texture quality, FSR 4 through AMD's DLL, Exit to desktop, Bloom
among the effects) showed in English in Spanish, Italian, French, German,
Portuguese and Chinese. The Linux fork's test of the menu's translations, which
caught them, comes back with them.
2026-11-01 17:00:19 +00:00
ayozetr 027e834a79 Steam overlay in the sharun AppImage: found above the launcher, and passed on
0.7 loads Steam's overlay when its packaged entry (the Nix AppImage) names it in
BB_STEAM_OVERLAY. The Linux fork's AppImage is built with sharun, which has no such
entry and clears LD_PRELOAD in every program of the package, so the overlay never
loaded although the launcher's switch is on by default (#28). When Steam started the
game and nothing named it, run.sh reads Steam's 64-bit gameoverlayrenderer.so from
the nearest process above that still preloads it, as the fork's run.sh did; with
the overlay it asks sharun to pass LD_PRELOAD on (SHARUN_ALLOW_LD_PRELOAD=1) and, on
a Wayland desktop, opens the window through Xwayland, where Steam can draw it.
Tested with a simulated process tree; BB_STEAM_OVERLAY=0 still turns it off.
2026-11-01 16:59:04 +00:00
ayozetr 0219c34163 AppImage: the assets folder again, with the Xbox and Switch button art
scripts/prompt_art.py makes the live button prompts' Xbox and Switch icons from
assets/menu_prompts, but the AppImage no longer carried that folder: the prompts fell
back to letters without a word. It is copied again, as the Linux fork's AppImage did.
2026-11-01 16:58:17 +00:00
ayozetr f0306a62a1 Content: a dump without a region gets Europe (#9)
A dump whose param.sfo has no region (USER_DEFINED_PARAM_1 = 0) gave the game no
address for its server, and Log In failed with "Unable to obtain Bloodborne server
information". The Linux fork gave such a dump Europe; that went out with its Region
row, which 0.7's text and voice languages replace. It is back, without the row, and
tested.
2026-11-01 16:58:17 +00:00
ayozetr 5fd9e2ccb5 Repository: BBSC's text tables count as C++ in GitHub's language bar
The Linux fork marked BBSC's table of game texts as C++, since its <?name?>
placeholders made GitHub take it for PHP. The table moved to gpu/bbnet20/bbsc in
0.7; the attribute follows it, and covers its translations too.
2026-11-01 16:50:34 +00:00
ayozetr 311aeb36ff DLSS: tools/build_dlss.sh back, with Frame Generation, and the AppImage carries both
The Linux fork's releases ship DLSS ready to use: tools/build_dlss.sh fetches the
files of NVIDIA's DLSS SDK 310.9.1 once into ~/.cache/bbport, builds the bridge
and puts it with NVIDIA's library next to bb-probe, and the AppImage takes them.
The script had not come over to 0.7, so a build without it asked the player for
the library. It is back, and it also fetches the DLSS Frame Generation library,
which 0.7 uses on GeForce RTX 40 and newer; the AppImage now copies that one too.
2026-11-01 16:50:28 +00:00
ayozetr 3963108624 Steam Input: the window through Xwayland when Steam made a virtual controller (#12)
Steam sends its virtual controller's input only while it sees the game's window
focused, and on a Wayland desktop it only sees X11 windows: started from Steam
with Steam Input on, the controller did nothing. The Linux fork's run.sh opened
the window through Xwayland then, and that block did not come over to 0.7; it is
back (BB_STEAM_INPUT_X11=0 keeps Wayland). docs/TROUBLESHOOTING.md already
describes it.
2026-11-01 16:49:48 +00:00
ayozetr afef07c90f Version 0.70-linux.1, first in every log with bbport's
The launcher's version (launcher/bbport_update.py) moves to 0.70-linux.1, the
first release on bbport 0.7. run.sh prints it first in every log, with the build
and the bbport version it is built on: "bbport Linux 0.70-linux.1, build ...
(bbport 0.7)"; without the launcher's file, as before.
2026-11-01 15:31:37 +00:00
ayozetr aae3f0566f Credits: bbport 0.7's contributors, and the projects as 0.7 uses them
The projects table follows what this tree takes from each one now: the online
module is shadPS4 0.20's network libraries put together by Awaken, with
droogie's bbhost behind The Hunter's Dream backend, and BBSC lives in
gpu/bbnet20/bbsc with its game server in scripts/bbsc_gameserver; shadp2p and
bbbridge are no longer part of it (MaskZeus keeps the credit for bbbridge in
earlier releases). A new section names everyone bbport 0.7 took work from, with
a link to bbport's own list.
2026-11-01 15:30:55 +00:00
ayozetr dff4079ef6 Docs: BBPORT.md is bbport 0.7's README
BBPORT.md kept the original project's README from bbport 0.3. It is now 0.7's,
still marked as the original project's, with its links to documents this
repository does not carry pointing at deadinside28/bloodborne_pc.
2026-11-01 15:30:22 +00:00
ayozetr e0543751fb Docs: the environment variables from the port's own registry
docs/ENVIRONMENT.md described the 0.51 tree by hand, with file and line
references that no longer exist. Its list is now the one gpu/shim/bb_env_vars.def
gives (scripts/env_vars.py --doc), which tests/test_env_vars.py keeps complete;
how to set them and their precedence stay written by hand, brought up to date.
2026-11-01 15:30:22 +00:00
ayozetrandMikeTat1988 76ebf406f3 Hunter's Atlas: its map generator, running on Linux
The launcher's Map page (launcher/bbport_atlas_maps.py) prepares the
maps with Atlas's generator in tools/hunters-atlas, which this branch did
not have: Prepare had nothing to run. It is the Linux fork's copy of the
scripts of Atlas's source/generator/src at 4ee4a0ed (Atlas 1.3) by
MikeTat1988, with:

- art/: Atlas's source/art, its PNGs recompressed without loss (the same
  pixels, checked against their hashes by the launcher), and the fallback
  font with its licence;
- requirements.lock: exact versions with their hashes, installed by the
  launcher with uv into atlas-tools/ in the data folder;
- localize_names.py: the names of areas, lamps and pickups in the game's
  language;
- the Linux fix for parameter tables looked up by name (soulstruct gives
  Windows paths, which Path.name does not split on Linux).

The AppImage carries it next to the launcher, as in the Linux fork.
tests/test_atlas_maps.py covers the launcher's side (the maps' state,
the tools' reuse, the downloads' checks, the shipped art and lock) and
passes. scripts/env_vars.py skips BB_PATH, Soulstruct's game folder
setting in the generator, not an environment variable.

Co-Authored-By: MikeTat1988 <[email protected]>
2026-11-01 15:27:21 +00:00
ayozetrandMikeTat1988 c27d8fb36b Hunter's Atlas: the map keeps what it covers when the window size changes
The map's zoom is in pixels of the drawn map, so a window made larger or
smaller (a resize, fullscreen, a new resolution) showed more or less of
the area than before. The zoom now remembers the UI scale it was set for
and follows that scale in Draw; Fit's limits scale with it too. From
Hunter's Atlas 3.0, as the Linux fork has it.

Co-Authored-By: MikeTat1988 <[email protected]>
2026-11-01 15:25:56 +00:00
ayozetrandMikeTat1988 2719bce35f Hunter's Atlas: travel dialog selection and Alt+Tab
As in the Linux fork:
- The travel dialog marks its selected button and Left/Right choose
  between Travel and Cancel; the confirm button does what is selected.
  Only the button under the mouse was marked, so with a controller
  neither looked chosen.
- Losing the window's focus (Alt+Tab, local co-op) no longer closes the
  map; only what is held (sticks, zoom) is let go.
- After a travel the quick-menu tile stays hidden until the game's menu
  has closed, instead of showing on the loading screen.

Co-Authored-By: MikeTat1988 <[email protected]>
2026-11-01 15:24:19 +00:00
ayozetrandMikeTat1988 8cfcdc1f76 Hunter's Atlas: the Map tile follows the game's quick menu
Measured frame by frame against recordings of the game's own menu, as in
the Linux fork:
- the tile fades in and slides with the menu as it opens, instead of
  appearing 180 ms late and still;
- it stays until the menu leaves the screen, fades out with it when the
  menu is closed with Back, and is gone before a lamp travel's loading
  screen;
- losing the window's focus (Alt+Tab, local co-op) no longer hides it or
  drops its selection;
- back from the System page the game's menu manager reports 0 instead of
  2 while the menu is shown; 0 with the quick menu's list alive counts as
  shown (src/runtime_map.c already reads it so), so the tile no longer
  stays hidden until the menu is reopened;
- its "Map" label has the game's size, colour and weight, and the cover
  over the game's own label is only as wide as that label, opaque and
  faded at its edges, without hiding the divider line;
- Up from below the Map tile focuses it and Down from it reaches the tile
  below it, as for a native tile: the game knows of no tile above the
  fourth column, so Up landed on System and Down went below System (the
  game is moved to the tile below System and Right is pulsed until it
  gets there). tests/test_bbport_atlas_menu_state.cpp covers both ways.

BB_ATLAS_DEBUG=1 (docs/ENVIRONMENT.md) logs what the tile reads from the
quick menu whenever it changes, which is how the System case was found.

Co-Authored-By: MikeTat1988 <[email protected]>
2026-11-01 15:22:35 +00:00
ayozetrandMikeTat1988 a68b6a75c8 Hunter's Atlas: the map and its tile in the game's language
Atlas had its texts in English, with a Russian label on the tile only.
They now follow the port's other texts in the game, as in the Linux fork:

- the map and the quick-menu tile follow the game's language, like the
  port's pages in the game's System menu (BbGameMenu::Language, now
  exported: the game's own once its menu has looked up a text, else the
  port's menu language);
- the map's section of the port's menu (Insert) follows that menu's
  language;
- gpu/shim/bbport_atlas_i18n.h holds them in Russian, Spanish, Italian,
  French, German, Brazilian Portuguese and Simplified Chinese, with the
  game's own names where it has them (Farol, Fiole de sang, Озарение);
- the map's titles use the game's own map font in Russian too (it has
  Cyrillic); only Chinese takes the port's font, the map font having no
  CJK glyphs.

Identifiers stay in English (the travel popup's id); the text the footer
compares is translated on both sides. The names of areas and lamps come
from the generator. tests/test_atlas_i18n.cpp checks the columns and the
English fallback and runs in build.sh --test.

Co-Authored-By: MikeTat1988 <[email protected]>
2026-11-01 15:20:17 +00:00
ayozetrandMikeTat1988 2bdee3185a Hunter's Atlas: the map screen, its quick-menu tile and input
Second part of Hunter's Atlas by MikeTat1988 (GPL-2.0-or-later,
github.com/MikeTat1988/HuntersAtlas): the fullscreen map, drawn with the
overlay's ImGui, and the "Map" tile next to the game's quick menu, as the
Linux fork has them.

- gpu/shim/bbport_map*.cpp, bbport_atlas_*: the map screen (layers,
  floors, walls, lamps, markers, travel dialog), its art, features, tile
  and controller shortcut. Their BB_* variables are read through bb_env()
  and listed in bb_env_vars.def (BB_MAP_SOUND_GAIN and the map's test
  hooks are new there). The map packs are read with nlohmann/json, the
  system package the online module already builds with.
- The overlay draws and ticks them, routes events and the pad to them,
  loads the map's title font and has the map's section in its menu.
  MenuOpen() is also true while the map or the tile uses the pointer, so
  the window lets the mouse go and shows the system cursor over them, as
  for the menu; CapturesInput() also covers the open map. The L3+R3 hold
  that opens the menu is unchanged.
- runtime_pad/sample.c: with the map on, the pad is read even while the
  map holds it, Atlas takes what it uses (bbgpu_atlas_route_pad), the game
  gets neutral input meanwhile, and buttons still held afterwards stay
  hidden until released (sample_atlas). Scripted input, the injected
  mouse and the game modules' pad filter stay on that path.
- runtime_audio.c and runtime_menu_sound.c: the tile's cursor sound,
  decoded on first use from the player's own sound/sprj_main.fsb with
  LibAtrac9 and mixed into the main port at the level of the game's own
  quick-menu cursor (CURSOL_SELECT, BB_MAP_SOUND_GAIN), silent in the
  background like the game.

Everything is behind BB_ATLAS_ENABLED=1: off, the overlay calls none of
it and the pad keeps its current path. build.sh --test runs the pad with
the map on (test_pad_atlas.c), the tile's state machine, map styles,
floor interiors, the controller shortcut and the features on a synthetic
features.json; test_menu_sound.c needs the player's own sound bank and
stays a manual test.

Co-Authored-By: MikeTat1988 <[email protected]>
2026-11-01 15:17:04 +00:00
ayozetrandMikeTat1988 6621a1f0a7 Hunter's Atlas: the game-state readers and lamp travel
First part of Hunter's Atlas by MikeTat1988, a fullscreen in-game map
(GPL-2.0-or-later, github.com/MikeTat1988/HuntersAtlas), as the Linux fork
has it: the runtime pieces that read the game's state.

- src/runtime_map.c: the player's position, facing and area, the quick
  menu's state and which button confirms. Reads only; every guarded site
  is checked byte for byte first.
- src/runtime_atlas_state.c: event flags (read only) and the opt-in lamp
  travel (BB_MAP_TRAVEL_ENABLED=1), which hooks the game's frame pacer
  entry (0x2034770, not a site 0.7's frame rate hooks touch) and calls the
  game's own return-point and warp functions on the game thread.
- bb-probe binds them right after bbgpu_patch_image, and only with
  BB_ATLAS_ENABLED=1: with the map off, nothing of the game is touched.

Both test programs run in build.sh --test and pass. The map screen comes
next.

Co-Authored-By: MikeTat1988 <[email protected]>
2026-11-01 15:04:28 +00:00
ayozetr e2ac43b725 Local co-op: each player's window in its part of the screen, without the keyboard
The launcher's Local co-op page starts one game per player through X11
with BB_WINDOW_RECT, BB_KEYBOARD=0 for the controller players and
BB_PAD_BACKGROUND=1; 0.7 already took the chosen controller strictly
(BB_GAMEPAD_STRICT), kept it in the background and chose the monitor
(BB_DISPLAY). Now:

- BB_WINDOW_RECT=x,y,w,h (fractions of the display, read with a point
  whatever the locale) places a borderless window in that part of the
  screen, never fullscreen.
- BB_KEYBOARD=0: the game ignores the keyboard and the mouse, which reach
  whichever window has the focus; its player has a gamepad. The window
  drops their events as it does for Steam's gamepad-only mode.
- BB_PAD_BACKGROUND=1: an unfocused player's window keeps its sound
  (players 2-4 are silenced by BB_AUDIO=none instead).

From the Linux fork.
2026-11-01 15:02:38 +00:00
ayozetrandAwaken 614602cf36 Faults: a read or write fault stays within its page
A write or read fault on the last bytes of a mapping invalidated or read 8
bytes past its end: IsMapped failed and the fault went unhandled (the game
ended). Both now stay within the faulting page. From Awaken's
BloodBorneButPc engine (0.9.24), as the Linux fork has it; that change's
other half (a page whose tracking keeps faulting gets the guest's access
back) is not ported yet.

Co-Authored-By: Awaken <[email protected]>
2026-11-01 14:59:43 +00:00
ayozetrandAwaken f0068ecb34 VideoOut: a full flip queue no longer refuses an end-of-pipe flip
A full flip queue refused an end-of-pipe flip, which nobody can take back:
its caller asserted (exit 23). It is queued anyway and reported a few
times; a flip the game submits itself is still refused, as on the console.
The out-of-order flip IRQ half of this change was already in 0.7 (it waits
for the label and flips anyway). From Awaken's BloodBorneButPc engine
(0.9.24), as the Linux fork has it.

Co-Authored-By: Awaken <[email protected]>
2026-11-01 14:58:07 +00:00
ayozetrandAwaken 1f98a632b5 GPU: a garbage descriptor is reported and read as a known one, not exit 23
Several lookups ended the game (an assertion, exit 23) on a value outside
their tables, as a stale or garbage sharp brings: unknown tile, macro tile
and array modes and pipe configs, sample counts and bits per element out
of range, a mode that is not macro tiled (tiling.cpp), a data format with
no size (formats.cpp), a data/number format combination the surface table
lacks and an invalid sample count (gcn_to_vk.cpp). Each is now reported a
few times and replaced by a known value; at worst that surface renders
wrong. From Awaken's BloodBorneButPc engine (0.9.24), as the Linux fork
has it.

Co-Authored-By: Awaken <[email protected]>
2026-11-01 14:56:49 +00:00
ayozetr 4e0aa911d2 Runtime: large copy lists are recorded in slices
Loading an area on NVIDIA, a download of the buffer cache carried 178563
buffer copies (4.3 MB) into one deferred command. A command's data shares a
128 KiB recording chunk with it, so ReserveRecordData's check stopped the
game (exit 23). Buffer copies, guest chunk copies, buffer-to-image uploads
and image-to-buffer downloads are now recorded as one command per 64 KiB
of regions; UpdateBuffer already split its data. From the Linux fork.
2026-11-01 14:54:33 +00:00
ayozetrandSupermedo 312d119b5a VRAM full: allocate past the budget instead of stopping
On 6 GB cards the game went over the driver's VRAM budget and the next
image or buffer allocation stopped it ("image allocation" / "buffer
allocation", exit 23). An allocation the budget refuses is now made past
it, where the driver may keep it in system memory: slower, but the game
goes on, and the texture collector turns aggressive for a while. Logged
once per kind. Buffers that must be in VRAM or nothing (the mirrors) still
do without, as before.

From Supermedo/bloodborne_pc 44e7644, as the Linux fork has it; its other
half (dropping old GPU-written tiled images unwritten) is left out: the
Linux fork later had to write them back instead (#13, #14).

Co-Authored-By: Supermedo <[email protected]>
2026-11-01 14:53:03 +00:00
ayozetr 0d34da1577 Pipelines: skip the draws of a pipeline the driver cannot create
"Failed to create graphics pipeline: ErrorUnknown" (some AMD and older
drivers) stopped the game with code 23. The pipeline now stays empty and
is logged; its draws and dispatches are skipped, on the GPU thread and when
a background compile finishes, and it is not saved to the cache, so the
next start tries it again. A cached one the driver rejects still rebuilds
the cache, as before. After Supermedo/bloodborne_pc 188c1f6, as the Linux
fork has it.
2026-11-01 14:50:37 +00:00
ayozetrand0xCydral 28eac08195 Pipeline: skip a draw whose shader program address is not readable
A malformed command buffer can leave a shader program address that is not
mapped (0x4c0012d0000 during an area load), and reading the program there
ended the game. LooksLikeProgram, which every stage of every draw and
dispatch goes through, first checks that the program is readable and skips
the draw or dispatch otherwise; the last readable mapping is cached per
thread against the mapping table's generation. From 0xCydral's change in
the Linux fork (b99535b, after yumlevi/bloodborne_pc ce385b6 and
GoncaloLobo0/bloodborne_pc_win ef70ed2).

Co-Authored-By: 0xCydral <[email protected]>
2026-11-01 14:47:55 +00:00
ayozetrand0xCydral f0aa0fa2ae Draw preparation: a failed check or an exception ends the attempt, not the game
Draw-preparation workers specialize shaders ahead of the GPU thread from
descriptors the game may still be rewriting. A fault there already returned
to a recovery point and left the draw to the GPU thread, but a failed check
on such a torn descriptor stopped the port with exit 23, and a C++ exception
from the same data (a container over its capacity, an empty optional)
escaped the worker and ended the process through std::terminate.

A failed check while this thread's recovery point is armed now jumps there
like a fault (common/assert.cpp calls the pipeline cache's hook; anywhere
else the check still stops), and an exception is caught and treated the
same. From 0xCydral's changes in the Linux fork (917d2c0, df619c7).

Co-Authored-By: 0xCydral <[email protected]>
2026-11-01 14:45:22 +00:00
ayozetrandAwaken 983c89fa9a Shaders: the phi pass queues duplicate groups in a fixed order
The phi simplification pass queued its duplicate SCC groups in the hash
table's order, which is not the same from one run to the next, so the same
shader could compile to different SPIR-V each time. They are queued in the
SCCs' own order now (upstream shadPS4 #5364). From Awaken's Lantern Engine
(0.9.37 beta 5), as the Linux fork has it.

Co-Authored-By: Awaken <[email protected]>
2026-11-01 14:42:17 +00:00
ayozetr da3ff0ecb0 Shaders: per-mip image bindings counted the same everywhere a stage's bindings start
An image with a binding per mip level (DynamicIndex, on GPUs without
VK_AMD_shader_image_load_store_lod, such as NVIDIA's) takes one binding
number per level in the pipeline layout and in the SPIR-V, but
Info::AddBindings and the rasterizer counted one: a stage after it found
its resources at numbers the layout does not have. Both now count as the
layout does (Info::ImageBindingCount; the T# is read only for such
images). A stage whose start moves gets a new permutation, since
StageSpecialization compares the start, so saved pipelines stay valid.
From the Linux fork.
2026-11-01 14:41:16 +00:00
ayozetrandAwaken d698ad290f Shaders: a vertex shader is reused only for the same bindings
Two stage specializations with no guest sharps compared equal, but the
engine's own buffers (user data, clip planes and the like) carry no sharp
and are still bound from `start` on. A vertex shader with only those was
reused after a fragment shader with another binding count, so its bindings
did not match the pipeline layout: NVIDIA reads other descriptors, AMD's
driver can end the process. The shortcut now needs a stage with no
resources at all, and the resource counts must match.

The pipeline key version moves to 6: pipelines saved with such pairs are
rebuilt once (and shader recipe packs of version 5 no longer load; none
ships with the Linux build). Found by Awaken (Lantern Engine 0.9.37 beta 7).

Co-Authored-By: Awaken <[email protected]>
2026-11-01 14:39:25 +00:00
ayozetrand0xCydral e5fde7ed25 Log: a performance line every 30 seconds in normal play
Players' logs carried no frame rate, so a report of low FPS or stutter
came with nothing to measure. Without BB_FRAME_STATS the game now prints
one line per 30 seconds: game frames per second, the 1% low, the worst
frame and the shader/pipeline compiles of the window, for example
"Perf: 68.0 FPS over 30 s, 1% low 62.8 FPS, worst frame 22.1 ms, 0
shader/pipeline compiles". From 0xCydral's change in the Linux fork.

Co-Authored-By: 0xCydral <[email protected]>
2026-11-01 14:37:09 +00:00
ayozetr 7c4a740338 Presenter: say so when a frame or a swapchain image does not come for 10 s
The presenter's frame fence and the swapchain acquire were waited for
without a limit, so a GPU that never finished a frame, or a compositor
that kept every image, froze the game with nothing from them in the log
(the Linux fork's #5: frozen loading screens, GPU at 0 %, NVIDIA Xid 109).
They are now waited for in 10 s slices: past the first one the log says
which wait it is, once, with the GPU breadcrumbs for a frame, and again
when it does finish. The GPU watchdog already did this for submitted
work.
2026-11-01 14:35:48 +00:00
ayozetrand0xCydral 91f63ff553 Motion vectors: object motion for 16-64 KiB bone palettes (the large beasts)
Palettes of whole bones between 16 and 64 KiB (24576, 49152 bytes: the
large beasts in Central Yharnam) were left to camera vectors and smeared
under FSR. They are now LargeSkeleton: gated like small skeletons (history
only while the palette changes), since instance data of static props may
look the same. BB_MOTION_LARGE=0 restores the old rule. The motion history
test checks the classification and passes. From 0xCydral's change in the
Linux fork, after bmy/bbport-mac 80898cf.

Co-Authored-By: 0xCydral <[email protected]>
2026-11-01 14:34:24 +00:00
ayozetrand0xCydral 7331bb9410 Texture cache: the same hash range when an image is marked maybe dirty and when it is refreshed
MarkAsMaybeDirty hashed the whole image but RefreshImage only its first
8x8 texels, so the two never matched: every image a CPU write near it had
marked was uploaded again from the game's memory, over what the GPU had
written there (a black character preview, a missing loading-screen item
picture), and the collector's check before dropping such an image (the
earlier #30 change) could never clear it either. All three now use
MaybeDirtyHash, the whole image. From 0xCydral's change in the Linux fork,
after yumlevi/bloodborne_pc 43fcf95.

Co-Authored-By: 0xCydral <[email protected]>
2026-11-01 14:32:46 +00:00
ayozetrandartemkaVG 2399b7a708 Texture cache: a color image at the stencil address is not taken as the stencil
FindDepthTarget linked whatever image sat at the stencil address as the
depth target's stencil. When a color target reuses that memory, reads and
storage writes to it went to the depth buffer through an invalid view.
Only images with a stencil-compatible format (or none yet) are linked now.
From shadps4-emu/shadPS4#5118 by artemkaVG, whose NVIDIA GPU hang (Xid 109)
went away with it, as the Linux fork has it.

Co-Authored-By: artemkaVG <[email protected]>
2026-11-01 14:31:18 +00:00
ayozetrandemurphy04 cefd12304c AvPlayer: a video queue cleared while a frame is taken no longer crashes
GetVideoData peeks at the next frame's time and then pops it; a Clear()
from another thread in between (the player stopping, the game closing)
left nothing to pop, and the empty result was used. It now gives up when
nothing came back, as GetAudioData already does. From emurphy04's change
in the Linux fork.

Co-Authored-By: emurphy04 <[email protected]>
2026-11-01 14:31:18 +00:00
ayozetrand0xCydral e1239378d6 FSR 4: depth barriers name the depth image's own aspects
The vendored FSR 4 backend issued its layout barriers on every external
image with VK_IMAGE_ASPECT_COLOR_BIT, invalid for the depth image
(VUID-VkImageMemoryBarrier-image-03319 and subresourceRange-09601). The new
fsr-vulkan patch 0004 adds an optional aspectMask to
FfxFsr4VkExternalImageState (0 = color), and Register passes the depth's
aspects: depth and stencil, or depth alone for a format without stencil.
From 0xCydral's change in the Linux fork (its patch 0002).

Co-Authored-By: 0xCydral <[email protected]>
2026-11-01 14:29:47 +00:00
ayozetrand0xCydral 7f8cbf12a4 Vulkan: vulkan.hpp is included with NDEBUG everywhere, so the dispatcher's layout matches
vulkan.hpp adds fields to DispatchLoaderBase when NDEBUG is undefined, so
the layout of vk::detail::DispatchLoaderDynamic depends on it. libbbgpu is
built with NDEBUG while the renderer tests build without it (their asserts
must run); the inline init() copies the linker keeps could then read the
dispatcher at offsets another translation unit did not write. It is now
included with NDEBUG defined and assert() restored after. From 0xCydral's
change in the Linux fork; the camera motion test still passes.

Co-Authored-By: 0xCydral <[email protected]>
2026-11-01 14:28:19 +00:00
ayozetr 7f69b82128 FSR 4: the balanced model for Performance and Ultra Performance
The v07 performance and ultra performance models leave trails behind moving
characters in Bloodborne; the balanced one handles 2x and 3x cleanly. Those
presets now use it, as IFreemz's shadPS4-Bloodborne-DLSS-FSR (9b2766e) and
the Linux fork do; the render size still follows the preset.
2026-11-01 14:28:19 +00:00
ayozetrand0xCydral edfb811965 Indirect guard: an invalid limit keeps the default
An unparsable BB_INDIRECT_GUARD_MAX or BB_INDIRECT_GUARD_MAX_VERTICES gave
strtoul 0, so the shader skipped every indirect dispatch or draw without a
word. A value that is not a number from 1 to 4294967295 now keeps the
default and prints one warning line. From 0xCydral's change in the Linux
fork, applied to both limits.

Co-Authored-By: 0xCydral <[email protected]>
2026-11-01 14:26:13 +00:00
ayozetr 4a6177cf06 Monitor list: the monitor's pixels, not the scaled desktop size
On a scaled Wayland desktop SDL gives the desktop mode in points, so a
2560x1440 monitor at 115 % was listed as 2226x1252 (the launcher's display
choice). The size is now the points times the display's pixel density.
Unscaled monitors list as before.
2026-11-01 14:25:23 +00:00
ayozetrandMrsuss60 a238c6937b Texture cache: FindImage's memo no longer reads a deleted image's slot
FindImage remembers its last result per description, and when the
registrations have changed since, it checks whether the remembered image
still matches: by reading its slot, which by then may belong to an image
deleted meanwhile. The slot is read only while it is allocated. From
Mrsuss60's Windows port, as the Linux fork has it.

Co-Authored-By: Mrsuss60 <[email protected]>
2026-11-01 14:24:54 +00:00
ayozetrandAwaken 1441367174 Indexed indirect draws sync their whole index buffer
An indirect draw's index count is in GPU memory, and NUM_INDICES may be
another draw's smaller count, so indices past it could be read stale: a
possible cause of stretched models (the Linux fork's #25). The buffer's own
size (VGT_DMA_MAX_SIZE) is synced when it is larger, up to 4M indices and
only over mapped memory. From Awaken's Lantern Engine (940396b).

Co-Authored-By: Awaken <[email protected]>
2026-11-01 14:24:09 +00:00
ayozetrandAwaken 8c6aeeb1c5 Draws read their vertex inputs again when the prepared ones are stale (#25)
A draw-preparation worker reads a draw's vertex buffer descriptors (V#s)
ahead of the recording thread. When the game rewrites the table in between,
the worker's copy has stale addresses or strides and vertex fetch reads the
wrong data: a stretched shape across the screen, reported on an RTX 4060 in
the Hunter's Dream (the Linux fork's #25). The comparison with the V#s the
draw's packet carries was there, but only for statistics (BB_CE_STATS). A
draw whose prepared V#s differ now reads its inputs from the packet's, as
without a worker, and the log says how many did ("Vertex inputs: ...",
every 5 s at most). Found by Awaken (Lantern Engine 0.9.34).

Co-Authored-By: Awaken <[email protected]>
2026-11-01 14:22:34 +00:00
ayozetrand0xCydral ae6c8728f0 Camera motion: keep the camera vector where an object vector strays far from it
Object motion history that was not the object's gave still geometry huge
vectors: at one moment of the Hunter's Dream, bushes behind the workshop
moved 100-650 px a frame while the camera vectors around them were right,
and FSR and frame generation smeared them. The motion merge now keeps the
camera vector when the object vector differs from it by more than 5% of the
render width (at least 32 px). The camera motion test gains a far object
vector that is rejected and a 30 px one that is kept; both pass. From
0xCydral's change in the Linux fork.

Co-Authored-By: 0xCydral <[email protected]>
2026-11-01 14:21:08 +00:00
ayozetrandAwaken 5c8bf041d7 Camera motion: no NaN, Inf or wild vectors into the upscaler
A camera whose matrices are not finite is not used, and the shader zeroes
NaN/Inf motion and clamps it to four times the render size, so one bad
frame cannot poison the upscaler's history. From Awaken's Lantern Engine
0.9.38, as the Linux fork has it.

Co-Authored-By: Awaken <[email protected]>
2026-11-01 14:21:08 +00:00
ayozetr c93a1f915c Tests: the camera motion test builds and passes again
camera-motion-test (built on its own: cmake --build out/gpu --target
camera-motion-test; it needs a Vulkan GPU) no longer linked, the renderer
library having gained guest memory, heap growth, files and saves services
the test stubs lacked, and its object motion cases failed: the object
motion texture is now two 32-bit words a pixel (x and y as halves, then the
depth with the "written" flag in its top bit, as EmitFragmentMotion packs
it), and the test still wrote four floats. With the stubs added and the
test packing it the same way, its six cases pass again.
2026-11-01 14:21:07 +00:00
ayozetrandnick24051998 b2a3990de2 SRT walker: a null table pointer zeroes its subtree without a fault
A resource table the game leaves null in one draw faulted in the generated
walker, and the fault handler supplied zero for that load. Each such draw
cost a signal (Supermedo's port counted 2068 such loads across 37 shaders
in Ebrietas' area). The walker now checks each table pointer it loads and,
when it is null, zeroes the subtree's slots in the flat buffer itself. A
pointer that is not null but bad still faults and reads zero for that draw.
From nick24051998's change in Supermedo's port (257d6bbf), as the Linux
fork has it.

Co-Authored-By: nick24051998 <[email protected]>
2026-11-01 14:16:44 +00:00
ayozetr 4e715f48d5 Texture collector: no write-back over memory the game reused (#25, #32, #35)
When the collector frees a GPU-written image it writes it back into the
game's memory first. Write tracking does not see every change there: the
game's heap reusing the memory of an area left behind. The image still
looked clean, its write-back overwrote the heap's bookkeeping, and the game
crashed later in its allocator (0x208599f, 0x20859c7, 0x208596a) or stopped
with "memory block seems to be improper or freed already"; on the Steam
Deck at every second lamp travel (the Linux fork's #35). Keeping GPU-written
images longer (the previous change) makes their write-back older, and more
exposed to this.

The game's memory under an image is now hashed when the GPU first writes it
since its last upload (Image::NoteGpuBase, on every path that marks an image
GPU-written: draws, storage writes, copies, blits, resolves, clears and the
scene resolution's copies), and the write-back goes ahead only if it is
unchanged; otherwise the image is dropped. The pressure report counts them.
2026-11-01 14:15:02 +00:00
ayozetr 5b8ac8efa5 Texture collector: GPU-written images are kept until they are really unused (#30)
Two ways the collector lost what the GPU had drawn, which came back black
once the game used the image again (effects in the Hunter's Dream, the
player's armour, on 8 GB cards that are under pressure all session; the
Linux fork's #30):

- Linear GPU-written images were downloaded and freed under mere memory
  pressure. They now go only past the critical mark and unused for a
  minute. BB_GC_KEEP_GPU_WRITTEN=0 restores the old rule.
- An image "maybe touched by the CPU" (a page under it was written) counted
  as dirty, so what the GPU had written was dropped undownloaded. The
  suspicion is now settled by the same hash RefreshImage uses
  (TextureCache::MaybeDirtyHash) before the image is judged.
2026-11-01 14:12:12 +00:00
ayozetr d96d384538 Texture cache: a GPU-written linear image is downloaded with all its mip levels (#30)
DownloadImageMemory copied mip level 0 only. When the collector freed a
GPU-written linear image, its smaller levels never reached the game's
memory, and the image loaded again later had the game's old bytes there:
black wherever those levels were drawn, effects, particles and models seen
from afar (the Linux fork's #30). Every level of a linear image with all
its levels is now copied to its offset in the game's layout; tiled images
and ones without their largest levels keep level 0.
2026-11-01 14:12:12 +00:00
ayozetr 683b593f45 Online: no SIGPIPE when the shadNet server or BBSC's relay goes away mid-send
On Linux, send() on a TCP connection the other side has closed raises
SIGPIPE, whose default ends the process, and nothing in the game ignores
it: a shadNet server or BBSC relay that went away while the game was
sending closed the game instead of letting the worker reconnect. Both now
send with MSG_NOSIGNAL, as the Linux fork's BBSC did; the failure is then
an error the code already handles. vendor-changes.diff records the shadNet
client's part.
2026-11-01 14:08:43 +00:00
ayozetr a0ec0efaf3 Discord Rich Presence: the co-op partners, on shadNet and The Hunter's Dream
The presence helper (scripts/discord_presence.py) shows "Co-op with
<partners>" from BB_PRESENCE_DIR/coop, which the earlier online module
wrote and this one did not, so the status stayed at "Online". Both pools
now write the online IDs of the players they are connected with, only when
they change: on shadNet the worker, which already wakes every 500 ms, reads
the active signaling connections (vendor-changes.diff records it); on The
Hunter's Dream its signaling writes it when a connection is announced, ends
or is reset.
2026-11-01 14:07:26 +00:00
ayozetr 90ba06f7fc Own server: the shadNet and BBSC game server Docker images
docs/SERVER.md walks a group through hosting its own co-op server with
two Docker images that were not in this tree: tools/shadnet-server
(Wozzardman's shadnet-p2p on Debian 13, for Qt 6.8) and tools/bbsc-server
(BBSC's game server, now from scripts/bbsc_gameserver). The guide's paths
follow the game server's new place.
2026-11-01 14:03:32 +00:00
ayozetr 456532bfa7 BBSC game server: BBSC 0.0.2's, as the Linux fork has it
The game server next to the game (run.sh, online.py bbsc-local) was the
one from before BBSC 0.0.2, while the game's BBSC is 0.0.2 with its relay
client. It is now 0.0.2's: relay frames batched per client and routed per
session, and message and bloodstain replies kept under 64 KB, newest
first (larger ones made the game stop with "Dantelion2 Panic ...
Transaction failed"), among the rest. Its README is the Linux fork's. The
options run.sh passes are the same; tested with them: the server list and
a message area request are answered.
2026-11-01 14:03:32 +00:00
ayozetr 59da2f9a6f BBSC kit: the newest save folder, and the one the game will write on a first start
BBSC's save folder was the first SPRJ0005 the user folder listed, which
with saves of two regions could be the old one, and on a first start there
was none, so the co-op kit was never given. It is now the most recently
written one, and on a first start the folder the game will write
(savedata/1/<title id>/SPRJ0005): the runtime exports the saves' title id
as BB_SAVEDATA_TITLE.
2026-11-01 13:59:57 +00:00
ayozetr 47085f688c BBSC kit: its items keep their names with Seamless co-op off
BBSC gives every character five items of its own in goods slots the game
leaves unused and names them in memory while it runs; with Seamless co-op
off, or on The Hunter's Dream (no BBSC), they showed as "?GoodsName?". A
layer under the mods (scripts/bbsc_kit_texts.py) writes their name, BBSC's
caption and a one-line help saying they need BBSC into the game's item
texts in every language, only into empty slots, so no text of the game
changes. The names and captions come from BBSC's translations
(bbsc_kit_texts.json, --update remakes it). The FMG reader and writer join
fromsoft_files.py, with the archive helpers fov.py already uses.
2026-11-01 13:58:12 +00:00
ayozetr 10732fb349 BBSC: its texts in the language the game shows
BBSC writes its messages, item names and notices in English whatever the
game's language. They now follow the language the game shows its text in
(bbport.ini game_text_language, else BB_LANGUAGE) in Spanish, French,
German, Italian, Portuguese and Russian: bbsc/translations/*.json map BBSC's
English text to each language (texts naming one boss through their {boss}
form, numbers through {n} and {count}), gen_translations.py makes them into
core/bbsc_translations.inc, and core/bbsc_i18n.cpp looks them up by the
English text, so BBSC's own sources keep their strings. Every text BBSC
shows in this version has its translation.
2026-11-01 13:56:26 +00:00
ayozetr e4d879ef3c BBSC: enemy scaling and solid NPCs keep looking for the game's tables after 3 minutes
Both searches gave up 3 minutes after the start and never came back, so a
slow first start (shaders compiling on the title screen) left the session
without enemy scaling, or with the host world's NPCs see-through to a
guest. They now keep looking, every 5 s for those 3 minutes and every 15 s
after, as the co-op kit's own search already does.
2026-11-01 13:53:40 +00:00
ayozetr c8bfa7f54c BBSC: the F8 unstuck key on Linux
BBSC's F8 (put the player back on the ground when stuck inside another
player) only existed in its Windows mod: it reads the key with
GetAsyncKeyState and checks that the game's window is in front. On Linux
the key comes from SDL, which the game's window already uses, looked up at
run time, and the window is in front while SDL has one with keyboard focus.
docs/ONLINE.md already lists it.
2026-11-01 13:53:40 +00:00
ayozetr 61824c2b2a BBSC: its logs and captures kept to the newest five, and the kit's bells in its trace
Every game start wrote a new BBSC timeline and a new trace capture, kept
for good, and the capture grew about 30 MB an hour of play. Now the newest
five of each stay (older starts' files go when a new one opens), and a
capture stops at 64 MB.

The trace of the responder bell check kept only the vanilla Small Resonant
and Sinister Resonant Bells (205, 225), which the co-op kit retires; the
Kin and Bloodlust Bells that replace them (4121, 11201) are recorded too, so
a greyed-out Kin Bell leaves something to look at.
2026-11-01 13:53:40 +00:00
ayozetr a9dcce1eac shadNet: keep trying in the background when the server does not answer at start
A server that did not answer when the game started (still starting, a VPN
not up yet, a short outage) left the game offline for the whole run: the
start-up probe disabled shadNet for the session, and a sign-in that failed
was not retried. Both now schedule the background retries a dropped
connection gets (from 2 s, backing off to 30 s). A server that answers and
refuses (wrong name or password, an incompatible protocol) is not retried,
at start or after a drop.

NpHandler::Shutdown() is never called, so an exit() with the network worker
running aborted in std::thread's destructor and hid the exit code; the
destructor now stops the worker and lets it go.

vendor-changes.diff and SHADNET-PORT.md record both.
2026-11-01 13:49:54 +00:00
ayozetr 96bd219320 Port menu from the controller: how long L3+R3 is held, or off (#38)
menu_pad_hold_ms in bbport.ini (800 by default, as upstream's #304) sets how long L3+R3 is held
to open the in-game menu; 0 leaves both sticks to the game and only Insert opens it, for PvP,
where both get pressed by accident. Closing with L3+R3 is unchanged. The launcher's Controls page
has the choice (0.8 s, 2 s, off) in its seven languages.
2026-11-01 13:05:57 +00:00
ayozetr c0cde157e3 Launcher: the Linux fork's launcher on 0.7, with 0.7's new settings
The fork's own launcher (its sidebar, pages and seven languages) replaces
0.7's, wired to 0.7's engine:

- 0.7's settings as rows of its own: the GPU, the aspect ratio and the
  monitor's own output resolution, anti-aliasing, texture quality, GT-VBAO,
  bloom, frame generation with DLSS's frame generation library, FSR 4.1.2's
  models from AMD's driver, low latency (Reflex, Anti-Lag 2), the game's text
  and voice languages, the stick camera's speed and the mouse's horizontal
  invert.
- The button prompts are bbport.ini's button_prompts, drawn live by the game
  (Switch among them); the keyboard's layout is 0.7's, or the fork's classic
  one. Settings from earlier launchers move once: keyboard_layout=bbport
  becomes classic and 0.6 becomes 0.7's, and the prompts go to bbport.ini.
- The Online tab on 0.7's online module: The Hunter's Dream's Discord sign-in
  as 0.7 does it, BBSC as one switch, the P2P port's default 3658.
- The Steam overlay on by default (BB_STEAM_OVERLAY=0 when it is off).
- Gone with 0.7: the memory model and 0.3's synchronisation, the background
  pre-upload, the game region (the text language replaces it), live
  resolution changes and the HUD position (0.7 widens the view and keeps the
  HUD at the edges by itself), the direct mouse camera switch, the Quit to
  Desktop switch (0.7's Exit to desktop is always there), the gestures menu
  without motion icons for now, bbbridge and BBSC's options.
- Hunter's Atlas and local co-op keep their pages; their variables are
  registered.

Tests for the launcher's modules, its settings' migration and frame
generation's rows; the user docs follow.
2026-11-01 11:31:48 +00:00
ayozetr fcca90e6fa Online: the Linux fork's server choice, accounts and checks on 0.7's module
scripts/online.py keeps 0.7's pools (Seamless Hunt with BBSC, The Hunter's
Dream through the online module's own client) and gains what the fork's
launcher builds on: the kind of server (Seamless Hunt, an own one or The
Hunter's Dream) and one account for each, the game's variables for them
(environment; BB_P2P_PORT, BB_BBSC, BB_NET_BACKEND=thd with the Discord
sign-in's token), the connection test with shadNet's UDP ping and the
sign-in, account registration, the firewall's advice, an own server's game
server answering in plain http, and Seamless Hunt's bloodstains and death
visions left out unless BB_SEAMLESSHUNT_TOMBS=1 (they crash the game). The
bbbridge parts are gone: the online module talks to The Hunter's Dream itself.
2026-11-01 11:31:32 +00:00
ayozetr 0fd636c7f0 Button prompts: button_prompts=switch
The Switch's art and labels for any gamepad, as button_prompts=xbox gives the
Xbox's: the Linux fork's launcher offers it with the other choices.
2026-11-01 11:31:24 +00:00
ayozetr 4bac966821 Discord Rich Presence: the area the player is in
scripts/discord_presence.py shows "Playing Bloodborne" on the player's
Discord profile, with the area: the runtime writes the first map of each load
into BB_PRESENCE_DIR (src/runtime_file.c), and run.sh starts the helper next
to the game for each start; it ends with the game. BB_DISCORD=0 (the
launcher's Discord switch) turns it off. From the Linux fork.
2026-11-01 11:31:19 +00:00
ayozetr 32383ee5d6 Install from PKG: pkgextract in the build and the AppImage
scripts/pkg_install.py extracts the player's own PS4 packages (the game, the
1.09 update and the add-ons) into a game folder with pkgextract
(paulomanrique/ps4-pkg-extractor, Unlicense), which build.sh builds with cargo
at a reviewed revision and the AppImage carries (BB_PKGEXTRACT). Without
cargo the launcher asks for a game folder only. From the Linux fork.
2026-11-01 11:31:12 +00:00
ayozetr 8cf70f6492 Mods: the layouts mod sites use, and why a folder is not a mod
A mod found up to three wrapper folders deep (an archive extracted into a
folder of its name, notes without a suffix, previews and links beside it),
under any title ID, and the game's folders next to screenshots or optional
folders, which stay out of the game. unrecognized() names each folder of the
mods folder that is not a mod and why (empty, still an archive, several mods
in one, no game files), for the launcher's list. From the Linux fork; the
extra layouts are from Supermedo's port (dd2314e).
2026-11-01 11:31:03 +00:00
ayozetr 1bd6314b69 Button prompts: the Xbox and Switch art on 0.7's live prompts
0.7 draws the game's key guide icons for the device in use, live: an
Xbox or a Switch pad got letters on a plain badge. They now get the
Linux fork's art, Dommo's "DS3 Style" prompts (Nexus, Bloodborne mod
30; assets/menu_prompts, with its README's credit and terms):

- scripts/prompt_art.py (run.sh, at start) writes each KG_*.dds (BC7,
  32x32) as raw RGBA into out/prompt_art/<xbox|switch>, kept while newer
  than its source; BB_PROMPT_ART names the folder.
- BbPrompts::Update uses that art on a gamepad that is not a
  PlayStation's, scaled to the port's icon size: a single button's
  icon is the art of the SDL button its input is bound to, so A and B
  swapped on Nintendo-style pads come out right; the sticks, the d-pad
  clusters, the triggers and the right touchpad half use their own.
  runtime_pad_prompt_nintendo() tells the Switch's from the rest.
- Without the files (Pillow missing), or for a button with no art, the
  letters as before; PlayStation pads and the keyboard are unchanged.

The gestures menu without the motion-control icons (the fork's other
part of mod 30) is not brought yet.
2026-11-01 02:39:32 +00:00
ayozetr 39a2174f47 Trophies: the Linux fork's notices, list and chime in place of 0.7's
The fork keeps its own trophy system, which does more than 0.7's:

- scripts/trophies.py (run.sh, at every start) prepares the dump's list
  and icons in <user>/trophies/<NP communication id>: names in the
  game's language with the player's trophy key (BB_TROPHY_KEY), or
  PSN's English names for Bloodborne without one (0.7 shows only the
  trophy's number then), and which trophies count toward the platinum.
- The runtime records each trophy the first time (earned.tsv, with its
  time) and awards the platinum once every trophy that counts toward it
  is in: the base game's, not The Old Hunters'.
- The notice is a card in the top-left corner in the PS4's style with
  the trophy's icon (a cup in its grade's colour without one), and a
  short chime of its own (bbport.ini trophy_sound, a menu checkbox; from
  GerasimosKan's trophies, deadinside28/bloodborne_pc#127).
- BB_TROPHY_PREVIEW=<id> shows a trophy's notice at the first trophy
  call without recording it.

0.7's history file (user/trophies.jsonl) is still written for every
unlock. Its own notice and decryption (bbport_trophies.cpp) are no
longer called. tests/test_trophies.py checks the list, the runtime
tests the platinum without the add-on's trophies. Checked: run.sh
prepares the list and the icons for the game's dump.
2026-11-01 02:36:27 +00:00
ayozetrandEthan Murphy a5e89fd8ac Field of view setting
bbport.ini fov (degrees, 30 to 90; unset or 43 the game's): the game's
camera table (LockCamParam in gameparam.parambnd.dcx) with every
camera's field of view scaled by fov / 43, so the areas that set their
own keep their difference. It is a layer the port makes for the launch,
laid under the player's mods (a mod's own camera table wins): mods.py
takes such layers again (--ui-mods), kept with mods switched off and
left out of the save copies made when the set of mods changes. The
game reads the table at start, so a change needs a restart.

scripts/fromsoft_files.py holds the DCX and BND4 readers and writers it
uses (the Linux fork had them in its quit_menu.py). tests/test_fov.py
checks the table. Checked in game at 60 degrees: 72 cameras, the view
visibly wider.

From Supermedo's port (b75e811, Ethan Murphy), by way of the Linux fork;
the launcher's row comes with the launcher.

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

Reported by parsifal295; from the Linux fork.
2026-11-01 02:18:34 +00:00
ayozetr 570ee9b579 On-screen keyboard for the character name, typed with a controller
The game's text dialog (ImeDialog: the character's name) can only be
typed on a physical keyboard in 0.7, so in the Steam Deck's Game Mode or
with a controller on the sofa a character cannot be named. The dialog
now shows a keyboard: the d-pad or the left stick moves, Cross types,
Square deletes, Triangle is a space, L1/R1 switch caps, Options confirms
and Circle cancels. A physical keyboard types as before. A row with the
menu language's own letters (ñ and the accents of Spanish, Italian,
French and Portuguese, German's umlauts and ß) sits above the actions.

The keyboard is drawn in the dialog and its edits are queued to the
window thread, which owns the text and the window (TakeTextEdits). It
and the menu accept and go back on the game's cross and circle wherever
bbport.ini puts them (A and B swapped on Nintendo-style pads:
runtime_pad_button).

From SrShadowy's port (gpu/shim/bbport_keyboard.cpp), reworked in the
Linux fork; its texts are in the menu's languages.
2026-11-01 02:17:34 +00:00
ayozetr fc17a2d7f4 Cheats and gameplay tweaks in the in-game menu
Community patches for 1.09 already in patches/Bloodborne.xml, switched
from bbport.ini as the game effects are: never die, enemies do not see
or hear you, Rally that never fades, enemy control, no Rally, the
camera further away, no camera auto-rotation, running with less stick
tilt and Dark Souls ragdoll physics (cheat_* and tweak_* keys). They are
rows of the menu's effects list, translated in its languages, and apply
after a restart as the other startup patches do. Ported from the
Windows fork (Supermedo's port) by way of the Linux fork; its launcher
page comes with the launcher.
2026-11-01 02:14:46 +00:00
ayozetr afadddfaf9 In-game menu: 0.7's new texts in the fork's languages
The 75 menu texts 0.7 added (anti-aliasing, VBAO, texture quality, low
latency, frame generation, aspect ratio, camera speed, VRAM and frame
rate lines, the upscaler DLL choices, ...) in Spanish, Italian, French,
German, Brazilian Portuguese and Simplified Chinese, with the terms the
menu's other translations use. Every MenuText of the tree now has its
row; printf specifiers and ### ids are kept as in the English.
2026-11-01 02:13:02 +00:00
ayozetrandEthan Murphy 63be02c67f Mute in the background
bbport.ini mute_unfocused (off by default): the audio ports are silent
while the game window does not have focus. They keep their pace, so the
game's audio threads run on as before. A checkbox in the in-game menu
(translated in its languages); the launcher's switch comes with the
launcher.

Adapted from Supermedo's port (b75e811), by way of the Linux fork.

Co-Authored-By: Ethan Murphy <[email protected]>
2026-11-01 02:11:47 +00:00
ayozetr 3ed9330a6a In-game menu in Spanish, Italian, French, German, Brazilian Portuguese and Simplified Chinese
0.7's menu speaks English and Russian (BbSettings::MenuText(english,
russian)). The Linux fork's languages come back on top of it:

- MenuLanguage gets Spanish, Italian, French, German, Brazilian
  Portuguese and Simplified Chinese, with their bbport.ini codes
  (menu_language=es, it, fr, de, pt_BR, zh_CN) and own names in the
  Language selector. Before one is chosen, BB_MENU_LANGUAGE or the
  desktop's locale picks it (MenuLanguageFromCode).
- MenuText looks the English up in bbport_menu_i18n.h for those
  languages (MenuTranslation; the English when a text is missing), and
  MenuTextIn gives a text in any language.
- The pages the port adds to the game's own System menu follow the
  game's text language: bbport.ini game_text_language when it names one
  of these, else the language the game was given (BB_LANGUAGE or the
  locale); Cyrillic in the game's own text still means Russian. The
  game's font has no Chinese characters, so those pages stay in English
  there. The resolution row's "Auto (21:9)" is built in each language.
- Simplified Chinese: the menu's font has no Chinese characters, so the
  first time the menu opens or speaks Chinese a system font is merged
  into it: BB_CJK_FONT (listed in bb_env_vars.def), else fontconfig's
  choice for zh-cn; the log says when there is none.

The translations are the fork's (its main branch's table); 0.7's new
menu texts follow in the next commit.
2026-11-01 02:09:57 +00:00
ayozetr f47ef822ee Menu: sliders keep their changes with any compiler
Slider() passed ImGui::SliderFloat(..., &v, ...) and v to Store() in
one call: the order the arguments are evaluated in is the compiler's,
so Store could get v before the slider changed it, and a drag was lost.
The slider runs first now, as 0.7 already does for checkboxes. From the
Linux fork (542e9ada).
2026-11-01 02:05:35 +00:00
andrecabralgit 2824ea7ab2 Audio: the game at full level on stereo devices
The game opens 8-channel ports and SDL turned them into stereo with its 7.1
matrix, which scales the front pair by about 0.21 so a full 8-channel sum
stays under 1. The game's mix lives in the fronts, so on a stereo device it
played about 13 dB quieter than everything else, on PipeWire too. Ports now
open at the device's channel count when it has fewer, and the runtime folds
the 8 channels down itself with the fronts at unity (stereo, 2.1, quad, 5.1,
6.1). BB_AUDIO_GAIN (0-8) scales the whole output. BB_AUDIO_DUMP names the
channels it writes, the device's after a fold.

From Supermedo/bloodborne_pc#18 (d41e3da) and Supermedo's ff0e421.
2026-11-01 02:05:35 +00:00
ayozetr a48a131590 Keyboard: the Linux fork's classic layout as a choice
0.7's layout (Dark Souls III's on PC: Space the dodge, Left Shift the
run, F and Enter Cross, Escape the menu and the way back in it, IJKL the
camera) stays the default. keyboard_layout=classic in bbport.ini (the
fork's launcher, Controls) gives the keys the fork had up to
0.51-linux.20: WASD and the arrow keys the sticks, Space Cross, Left
Shift Circle, E Square, Q Triangle, 1 L1, 3 R1, R L2, F R2, Z L3, C R3,
Enter Options (also in a game menu, not the way back), Tab and Backspace
the touchpad halves, IJKL the d-pad, Left Alt walk, and the mouse
buttons as before. key.<input> lines still replace either layout's
keys. tests/test_pad.c checks both.

Also bb_env.h's note: env_vars.py prints the list, it no longer writes
docs/ENVIRONMENT.md.
2026-11-01 01:51:35 +00:00
ayozetrandAwaken 8cff10a374 Controller: the Linux fork's stick and button fixes
On 0.7's src/runtime_pad parts, each as on the fork's main branch:

- A stick at the end of its travel is never its neutral (its issue
  #20): an 8BitDo Pro 2 reported -32768 on all four axes right after
  reconnecting, which was taken as a biased neutral, and the camera and
  the player spun.
- A round dead zone: held to the stick's length and mapped from 0 at
  its edge, so a push close to one axis keeps its small part of the
  other one (BB_PAD_DEADZONE_RADIAL=0: per axis). The idea is from
  Lantern Engine.
- A round gate: a stick past it (a diagonal near the square's corner)
  is brought back onto it (pad_round_gate=0: as it comes).
- pad_camera_diagonal=1 evens out the camera stick's diagonals (its
  issue #7: the game's fast turn applies to them too); off by default.
- pad_swap_sticks, pad_invert_look_x and pad_invert_look_y (from
  Awaken's BloodBorneButPc).
- Back + d-pad left/right clicks the touchpad halves on pads without a
  touch surface, Back alone the left half on release (parsifal295's
  idea, as in Supermedo's port 1.7.3).
- Buttons still held when a menu closes stay hidden until released, so
  the game does not take them as a new press (hold_after_capture, from
  Supermedo's port by way of Awaken's).

tests/test_pad.c checks the neutral, the gate, the dead zone and the
stick options. The variables are listed in bb_env_vars.def.

Co-Authored-By: Awaken <[email protected]>
2026-11-01 01:47:47 +00:00
ayozetrandAwaken b902cbc5ff Controller: the one in use takes over, a light bar colour, and pads by device path
From the Linux fork, on 0.7's src/runtime_pad/gamepad.c:

- Several gamepads and none chosen (also one pad behind a 2.4 GHz
  receiver that shows as two): the one being used takes over, checked
  five times a second; used means a button held or an axis well away
  from where it rested when opened, so a wheel's pedals do not count.
  From Awaken's BloodBorneButPc.
- BB_PAD_LIGHTBAR=RRGGBB: every pad with a light bar (DualShock 4,
  DualSense) shows that colour; unset, SDL's own.
- BB_GAMEPAD may also be the pad's device path (/dev/input/eventN or
  hidraw), the only way to tell two pads of the same model apart; the
  log names each pad's path. BB_GAMEPAD_STRICT=1 (local co-op, one game
  per player) takes only the chosen pad, never another instead.

The variables are listed in bb_env_vars.def (next commit).

Co-Authored-By: Awaken <[email protected]>
2026-11-01 01:47:47 +00:00
ayozetrandethan cddb964ebd Crash guards: an online event without its data, from Supermedo's port
The game describes an online event and case 0 copies a string from its
data without checking it: a player on The Hunter's Dream crashed there
(read access to 0xa0 at guest offset 0x1eb56b7) right after the
server's ss.info request timed out. Case 0 with no data now goes the way
of an unknown event, "could not" (scripts/crash_guards.py;
BB_CRASH_GUARDS=0: off, listed in bb_env_vars.def). 0.7 does not touch
the site. patches.py checks the original bytes first (eboot_bytes) and
leaves the guard out, with a line, on other code.

Its code goes in the free tail of the code segment's last page (from
0x50D9700; the game's code ends at 0x50D96DC). The loader refused patch
writes past a segment's size ("bad patch entry"); it now takes them up
to the executable segment's 4 KiB page boundary, which is mapped with it
on every host and unused by the game (patchable(), from the Linux fork).

Checked: the guard's bytes read back from the running game's memory, and
the game plays. tests/test_crash_guards.py checks the code.

Co-Authored-By: ethan <[email protected]>
2026-11-01 01:21:32 +00:00
ayozetr cfc63a6766 Patches: start online at the title when online play is on
Skip Online/Offline Choice answers the title's dialog with PLAY
OFFLINE, so the game's own online session only started after Log In,
and the Kin and Bloodlust Bells stayed greyed out until then (the Linux
fork's issue #9). With online play on (BB_ONLINE=1), a variant now
answers PLAY ONLINE: the same dialog builder returns what the PLAY
ONLINE item returns (operator() at 0x1b49eb0), which differs from PLAY
OFFLINE's only in the online flag. Offline, 0.7's patch stays.
tests/test_patches.py checks both against the builder's items. From
the Linux fork.
2026-11-01 01:01:36 +00:00
ayozetrandAwaken 64f7962cac Patches: "1.09" is "01.09", and copies of Bloodborne.xml are not third-party
- A third-party patch file written for AppVer="1.09" is the game's
  01.09: it was skipped as written for another version.
- A copy of the built-in Bloodborne.xml in the patches folder (an older
  package's, any case) is not taken as a third-party file: it applied
  its frame-rate patches over the port's own.

tests/test_patches.py checks both. From Awaken's BloodBorneButPc, by
way of the Linux fork.

Co-Authored-By: Awaken <[email protected]>
2026-11-01 01:00:55 +00:00
ayozetr 1cb0fd24ed Loading and smoothness: three game patches from bbhost
From droogie/bbhost (patches/*.toml), each checked against the 1.09
executable's original bytes, noted beside it in Bloodborne.xml:

- Quick Re-entry (on): a load after a death or lamp travel ends when the
  area is ready, 2-3 s, not after the 12-second minimum.
- Fast Run-time Deflate (on): the player's recording, compressed every
  ~50 s on the main thread at deflate level 9 (~23 ms in one frame), at
  level 1.
- All Resource Post-Processors In Play (off, experimental): the third
  of the resource system's post-processor threads keeps running.

bbport.ini tweak_quick_reentry, tweak_fast_deflate and
tweak_post_processors (0/1) choose them; the first two are on when the
file does not name them. tests/test_patches.py checks it, and the tests
that count other patches turn them off. From the Linux fork; the
launcher's rows come with the launcher.
2026-11-01 01:00:20 +00:00
ayozetr b2a6130b42 DLC folder: the player's dumped add-ons linked where the runtime looks
0.7 reports <user>/addcont/<title id>/<label> as installed add-ons
(shadPS4's layout). BB_DLC_DIR, the Linux fork's launcher's DLC folder
(one folder per add-on, e.g. SPEXPANSIONDLC03), is now linked there by
run.sh for the game's title id; a real folder already in its place is
left alone. Listed in bb_env_vars.def. The launcher's row comes with the
launcher.
2026-11-01 00:52:55 +00:00
ayozetr ed9d812b85 Runtime: game language follows the desktop without the launcher too
The launcher passes BB_LANGUAGE, but run.sh started directly (scripts,
the terminal) left the game in English. Without BB_LANGUAGE the runtime
now maps the desktop's locale the way the Linux fork's launcher does:
Japanese, English, French, Spanish, German, Italian or Russian, English
otherwise. From the Linux fork.
2026-11-01 00:52:55 +00:00
ayozetrandAndiMajore bdd7994cca Debugging: BB_FAULT_WAIT=1 keeps a crashed process alive for gdb
A fatal fault in bb-probe or a GPU library assertion ended the process
at once, leaving a few lines in the log. With BB_FAULT_WAIT=1 the fault
handler and assert_fail_impl print how to attach, send a desktop
notification and wait instead of exiting; any process of the user may
ptrace it (PR_SET_PTRACER_ANY, set at start only then). Nothing changes
while the game runs. Linux only (fork, notify-send, ptrace).

tools/debug_attach.sh [pid] [--keep] attaches gdb in batch mode and
writes logs/crash-<time>-<pid>.txt: every thread's stack, the faulting
thread's registers and code (game code as a guest offset), and bt full
of an asserting thread (docs/DEBUGGING.md). The variables are listed in
bb_env_vars.def.

From deadinside28/bloodborne_pc#165, by way of the Linux fork.

Co-Authored-By: AndiMajore <[email protected]>
2026-11-01 00:51:22 +00:00
ayozetrandAwaken 4247abab8e Guest memory: the game's in/out arguments are written outside the region lock
Mapping, querying and batch calls read and wrote the guest's pointers
with the region table lock held. A write landing on a GPU-tracked page
faulted into page tracking, which waits for its own locks or a readback
by the GPU thread, while those threads waited for the region lock: a
hang. The arguments now go through locals, copied in before the lock and
out after: direct allocation, direct and flexible maps, virtual range
reservations, protection and virtual queries, direct memory type and
direct queries, and the batch map's entries. Still so in 0.7
(src/runtime_memory/kernel_api.c).

From Awaken's LanternEngine (7aa3d1b), by way of the Linux fork.
Checked: build.sh --test, and four minutes walking in game.

Co-Authored-By: Awaken <[email protected]>
2026-11-01 00:48:48 +00:00
ayozetr fa0d7232d9 Runtime: reservations of guest memory that fail are logged
Failed direct and flexible mappings already printed a line; a failed
direct allocation, virtual range reservation or low memory map returned
its error silently. A guest NULL read soon after one of them would then
look like a bug of its own. Each now prints what failed, its size, the
error and the direct memory in use; the first 16 only. The return values
are unchanged.

The idea comes from Mrsuss60's Windows port (4035ab4e); from the Linux
fork.
2026-11-01 00:39:07 +00:00
ayozetrandAwaken b2f516f0e0 Runtime: failed allocations in audio decoding and the save list are not fatal
- AJM: a job's joined input, sized by the game, is refused above 64 MiB
  or when malloc fails (the job reports an invalid parameter) instead of
  writing through NULL.
- Save list: a failed realloc stops the listing with what it has instead
  of losing the list and writing through NULL.

From Awaken's engine (bbhost-changes.patch, 0.9.24), by way of the Linux
fork.

Co-Authored-By: Awaken <[email protected]>
2026-11-01 00:39:07 +00:00
ayozetrand0xCydral 773368a150 Runtime: split flexible mappings keep their own pool offset
split_at moved the offset only for direct memory, so the right piece of
a split flexible mapping kept the left piece's. Unmapping it then freed
and zeroed the left piece's pages (still mapped) and leaked its own;
read/write backing used the wrong pages too. Still so in 0.7
(src/runtime_memory/vma.c).

From bmy/bbport-mac 9ffc78d, by way of 0xCydral's fork and the Linux
fork.

Co-Authored-By: 0xCydral <[email protected]>
2026-11-01 00:39:07 +00:00
ayozetr 0d091c9335 Runtime: the game's exit() ends the process like its _exit()
The game's own quit uses _exit(), but a title menu patch that adds Exit
Game calls exit(). That ran the game's static destructors while its
other threads kept running: they tore down Dantelion's mutexes under
them ("DLLightMutex.cpp(126) Mutex is not initialized"), and the game
faulted and ended with code 139 instead of 0. The host's atexit handlers
would also free the GPU's objects under the render thread. exit() now
reports, flushes the log and ends the process at once, as _exit() does;
saves are already on disk, written whole. From the Linux fork.
2026-11-01 00:39:07 +00:00
ayozetr 95c548e6f1 Runtime: the game's threads get the PS4's floating-point state
The PS4 runs its threads with flush-to-zero and denormals-are-zero set
in MXCSR (0x9fc0); a Linux thread starts with both clear (0x1f80), and
the runtime never set them. The game's own code then got different
results than on a PS4: very small values stayed as denormals instead of
becoming 0, which a physics step can drift on (the ghosts' cloth in
Cainhurst was reported glitching), and denormals are slow on x86.

The main thread gets it before the first guest code, and every thread
the game creates when it starts. The values are the ones dumped from
real hardware, as in shadPS4#3568 (StevenMiller123, found by red-prig).
From the Linux fork; tests/test_runtime.c checks it.
2026-11-01 00:39:07 +00:00
ayozetr 55eb72e933 Windows: upstream's launchers, packaging and tools left out of the Linux fork
The fork is Linux only and nobody here builds or tests the Windows
package, so its own files go: the WPF launcher (launcher/windows), the
Tkinter one (launcher/bbport_launcher_win.py), the Windows runner
(run.py), the zip's packaging (packaging/windows, packaging/bbport.ico),
FSR 4.1.x's Windows asset build (tools/fsr4cap/build_assets_win.py) and
the Windows benchmarks (tools/win-bench).

The engine's Windows code stays as upstream has it (gpu/platform/win32,
the _WIN32 parts and their CMake rules): leaving it alone keeps
upstream's fixes easy to bring in, and the code portable. Comments that
name the removed files say where that code came from.
2026-11-01 00:22:13 +00:00
ayozetr 40031dd622 Players' guide: its theme, hook and publishing script, published from main only
The fork's players' guide (GitHub Pages, https://ayozetr.github.io/bloodborne_linux/)
is built from docs/ by MkDocs Material: site/ holds the theme's
overrides, the hook that points links to files outside docs/ at them
on GitHub, and the pinned requirements; tools/publish_guide.sh builds it
(--strict) and pushes it to the gh-pages branch. _site/ is ignored.

The site is main's guide, so the script now publishes from main only: a
branch's pages, here 0.7's with links to files main does not have yet,
would replace it. BB_PUBLISH_BRANCH=1 publishes from another branch on
purpose (listed in bb_env_vars.def).

Checked: mkdocs build --strict passes on this branch.
2026-11-01 00:09:31 +00:00
ayozetr 1eadf67c8c Docs: the Linux fork's own, in place of upstream's
The repository's documentation is the fork's: README.md, BBPORT.md (the
original project's README, for its technical details), mkdocs.yml and
docs/ with the players' guide, the developers' pages and the design
notes from upstream the fork keeps in English. Upstream's own docs
(release notes, roadmap, session logs, its other notes, most of them in
Russian) and README.ru.md are not part of it; docs/README.md links to
them in upstream's repository, as on the fork's main branch. Pages that
describe 0.51's code are brought up to date as each part is brought onto
0.7.

docs/ENVIRONMENT.md stays the fork's guide, written by hand.
scripts/env_vars.py no longer writes over it: --doc prints the whole
list of gpu/shim/bb_env_vars.def as Markdown, and tests/test_env_vars.py
checks that list against the code, and that the printed list has every
variable.
2026-11-01 00:05:25 +00:00
ayozetr b8bb54a791 AppImage: its variables in the registry, without the parts not on 0.7
tests/test_env_vars.py checks every BB_* name the scripts use against
gpu/shim/bb_env_vars.def, and the game names any BB_* it does not know
as a typo:

- The host's Vulkan variables the AppImage puts aside under Steam's
  pressure-vessel are named one by one (BB_HOST_VK_DRIVER_FILES, ...)
  and listed as internal, so the log shows them without a warning.
- bbbridge left out: on 0.7 The Hunter's Dream is the online module's
  own pool. The PKG extractor comes back with the launcher's Install
  from PKG.
2026-11-01 00:01:51 +00:00
ayozetr f760e22ebf GitHub: the fork's issue templates and funding link
The bug report (version and build, how the game was installed, the GPU,
settings and the log), the feature request and the template chooser's
links, and the Ko-fi link, as on the fork's main branch. They name the
launcher's About and Log pages, which come with the launcher.
2026-10-31 23:59:43 +00:00
ayozetr c61dc69ae7 License: the Linux fork as a whole under GPL-3.0-or-later
As on the fork's main branch: the whole is distributed under
GPL-3.0-or-later, which everything in it allows (bbport and shadPS4 are
GPL-2.0-or-later, the online module's BBSC and The Hunter's Dream parts
GPL-3.0-or-later, the rest MIT, BSD or Zlib). LICENSE has the GPL-3.0
text. NOTICE keeps bbport's own notice whole (its files stay
GPL-2.0-or-later and its attribution request stands) and says so first.
2026-10-31 23:59:30 +00:00
ayozetr 05807030a2 Git: game files and local runs ignored; the patched FSR-Vulkan is not a change
- .gitignore: the mods folder and mods.last of runs from source, game
  files and dumps (eboot.bin, param.sfo, PKG, DCX, 7z, CUSA folders),
  and an exception for the launcher's icon past the *.png rule.
- .gitmodules: FSR-Vulkan's working tree holds build.sh's patches, so it
  is ignore = dirty: a build's commit (the log's first line, the
  AppImage's BUILD) no longer says "-modified" for them.
2026-10-31 23:59:02 +00:00
ayozetr 64660056b4 Logs: the build next to the version on the first line
Issue reports rarely say which build they ran, and a test build prints
the same version as the release it was made from. run.sh now prints the
build after VERSION: the commit, branch and date from the BUILD file
packaging/appimage-sharun.sh writes, or git's commit and branch in a
checkout ("bbport 0.7, build 0a1b2c3d main 2026-11-01"). From the Linux
fork.
2026-10-31 23:58:46 +00:00
ayozetr c2a45f36e6 Numbers with a decimal point in every language (AppImage)
The AppImage without Nix preloads sharun's anylinux.so, which checks the
host locale with setlocale(LC_ALL, "") and leaves it set. In Spanish,
German, Portuguese and other languages with a decimal comma the game's
process then wrote "72,9 FPS" in the log and "sharpness=0,30" in
bbport.ini, which strtof and atof read back as 0. bb-probe sets
LC_NUMERIC to C at the start of main, after the preload's check; the
rest of the user's locale stays. Checked with the AppImage under
es_ES.UTF-8: "479.7 FPS".
2026-10-31 23:58:27 +00:00
ayozetr 47b276b389 AppImage without Nix (packaging/appimage-sharun.sh)
The Linux fork's AppImage, brought onto 0.7: the port's build, its
scripts and the GTK4 launcher with every library they load (glibc,
Python with PyGObject, ssl and hashlib, GTK 4, libadwaita, SDL3, Mesa's
Vulkan drivers for AMD and Intel) collected from the build system by
quick-sharun at a fixed revision. NVIDIA's driver comes from the host.

On 0.7's layout: run.sh, VERSION, LICENSE, NOTICE and the shader
recipes when the build has them; the online module is
out/gpu/libbbnet20.so, handed over in BB_NET_MODULE (BBSC is inside it
now). The log's first line gets the build (BUILD).

- DLSS: with sharun /proc/self/exe is the dynamic loader, so the bridge
  and NVIDIA's libraries are looked for in BB_DLSS_DIR first
  (gpu/upscale/vk_dlss.cpp, listed in bb_env_vars.def).
- packaging/bbport-entry.c keeps 0.7's Steam overlay handling and the
  fork's pressure-vessel one (Steam's Game Mode, deadinside28#172).
- The launcher's icon (launcher/bloodborne.png, added past the
  .gitignore's *.png rule) in the desktop entry, also for
  launcher/install-desktop.sh.

Tested: the AppImage starts the game with --play in its own data and
config folders and plays at about 70 FPS on an RTX 3060.
2026-10-31 23:54:49 +00:00
ayozetr 71e6cc9592 FSR-Vulkan: the shader override and job mark hooks the port uses
0.7 sets ffxVkShaderOverride (gpu/upscale/ffx_shaders.cpp: the frame
generation passes built in this tree) and ffxVkJobMark
(gpu/upscale/fg_profile.cpp, BB_FG_PROFILE), but no patch to the FFX
Vulkan backend defined them, so a build from source failed to link
bb-probe (upstream #404). Patch 0003 defines both as null pointers and
calls them: the override where the pass's shader module is made (the
bindings stay the packaged blob's), the mark before each compute
dispatch.
2026-10-31 23:24:14 +00:00
+24 0fc2921c45 0.7: native Windows build with online and mods, one memory model, faster heavy views, cloth and shadow fixes
- Windows 10/11: native bb-probe.exe (llvm-mingw / MSYS2 CLANG64), WPF launcher in five
  languages; online module online\bbnet20.dll with both pools (Seamless Hunt + BBSC, The
  Hunter's Dream with Discord sign-in) and BBSC's local game server; mods and XML patches from
  the launcher (junctions and hard links without Developer Mode); DLSS / DLSS FG, FSR 4.1.1 and
  FSR 4.1.2 driver models, FSR 4 from AMD's SDK 2.4 DLLs; NVAPI power profile on NVIDIA.
- The old memory model of 0.3 removed; the game's graphics heaps grow on demand.
- Faster: occlusion queries counted only inside the game's open queries (164 -> 189 FPS in the
  heaviest view), ring dedup and copy-shader filter (less PCIe traffic), PGO retrained, Steam
  Deck draw preparation, FSR 3.1 FG shaders.
- Fixes: cloth fluttering above 30 FPS (#58), point-light shadow wedges, HUD item-icon grid,
  vertex explosions across heap pieces, FPS drop after a save switch, FG ghosts, crash
  0x2169edc (#336), FSR 3.1 protected memory (#85), Wayland HiDPI, launcher translations (#381).
- New: AA menu (FXAA2_GS, SMAA), FSR 1, GT-VBAO, AMD Anti-Lag 2, trophies.jsonl, shader
  recipes before play, live resolution by default.
- Packages: Windows zip, AppImage, and a Linux folder (tar.zst) with the online module open.
- Removed: the shadp2p online module, LEGO Dimensions toy pad and NGS2 code.
- Credits: docs/CONTRIBUTORS_0.7.md (pull requests since 0.4 and outside work).

Co-authored-by: Qiming-Liu <[email protected]>
Co-authored-by: AndiMajore <[email protected]>
Co-authored-by: adds39939 <[email protected]>
Co-authored-by: iannitram <[email protected]>
Co-authored-by: maiconjsv <[email protected]>
Co-authored-by: whp199 <[email protected]>
Co-authored-by: DominickCobb-rs <[email protected]>
Co-authored-by: Sarxzer <[email protected]>
Co-authored-by: wheat32 <[email protected]>
Co-authored-by: ArenaMMORPG <[email protected]>
Co-authored-by: SrShadowy <[email protected]>
Co-authored-by: Gustavo Bomfim <[email protected]>
Co-authored-by: Teefpyre <[email protected]>
Co-authored-by: SrDeTs <[email protected]>
Co-authored-by: IgorWarzocha <[email protected]>
Co-authored-by: itachi-re <[email protected]>
Co-authored-by: Bainianzzz <[email protected]>
Co-authored-by: hideoneric <[email protected]>
Co-authored-by: guitaripod <[email protected]>
Co-authored-by: unTanana <[email protected]>
Co-authored-by: andrR89 <[email protected]>
Co-authored-by: ayozetr <[email protected]>
Co-authored-by: JuanraCM <[email protected]>
Co-authored-by: SaeedShow <[email protected]>
Co-authored-by: msisovic <[email protected]>
Co-authored-by: axpico <[email protected]>
Co-authored-by: Aryxa <[email protected]>
Co-authored-by: Obrikash <[email protected]>
Co-authored-by: noelmiller <[email protected]>
Co-authored-by: AyrtonSanabio <[email protected]>
Co-authored-by: Supermedo <[email protected]>
Co-authored-by: Awaken <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 02:55:53 +03:00
deadinsideandClaude Opus 5.5 8185b88d80 0.6: online on shadPS4 0.20 (Seamless Hunt + BBSC, The Hunter's Dream), Laurence cinematic, flicker and load crash fixes
- Online module libbbnet20.so: shadPS4 v0.20 network/NP/shadNet code, BBSC ported to it,
  The Hunter's Dream backend (droogie/bbhost) with Discord sign-in; launcher pool choice;
  the shadp2p module stays as a fallback (BB_NET_GEN=19).
- Laurence's cinematic above 30 FPS: render-job slots given back; the render resource cache
  gets 512 MiB of port direct memory past the game's pool (the game's heap unchanged).
- Single shifted/quarter frames: push constants recorded after the pass begins (the occlusion
  results' compute dispatch overwrote them).
- Crashes: 0xa0020007 (#207, blur-kernel sample clamp), Bad PM4 at save load (FX init clear
  race, command buffers copied at submit), null V# tables (139).
- Light shafts on NVIDIA (#204, PR #210), #205, texture GC on 8 GB, PR #206/#224/#214/#211,
  FSR 4.1.1 reset after save change, SSAO forced back, SSR at start, present wait mask,
  Polaris labels on the CPU (#203), AUR GE-Proton (#186), 21 game languages (#249).
- Live: F3 30 FPS limit, effects without a restart, keyboard stick/run/dodge/jump, cursor,
  GPU choice in the launcher; more draw-preparation workers on 12+ thread CPUs.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-19 09:30:55 +03:00
deadinsideandClaude Opus 5.5 72e5c8ce22 0.6-pre1: new memory model on every GPU, seamless co-op, aspect ratios and frame rate by hooks, own renderer
- Auto memory model = the new one on every GPU (layer memory module, no sparse binding);
  the 0.3 model stays as a launcher option.
- Seamless co-op (BBSC) in the online module; Seamless Hunt servers by default; the shadNet
  client reports its version and stays offline when a server refuses it.
- Any aspect ratio live through game hooks (camera, HUD anchoring, UI fills, markers);
  frame rate through hooks instead of patches.
- Keyboard and mouse: native mouse camera, menu mouse pointer, Dark Souls III-like layout,
  chord bindings, button prompts for the device in use (HUD atlas included).
- Real occlusion queries; own command processor, GCN definitions, shader translator,
  renderer and caches instead of shadPS4 (byte-identical SPIR-V on the game's shaders).
- Fixes: NVIDIA start (#135 #147 #152), RIP=0 (#161 #177 #181), #149, #142, #162/#192,
  #178, #144/#165/#166, #134, #172, #186, #136, #145, lamp-travel loading hang (PR #193),
  FPS aging, write-trap race, exit crash, pipeline-cache stages, AppImage --play.
- Runtime tests follow the NID-only import rule.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-15 12:18:28 +03:00
1460 changed files with 215007 additions and 272484 deletions

No files matched your search

+2 -1
View File
@@ -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++
+1 -1
View File
@@ -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
View File
@@ -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/
+264 -170
View File
@@ -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.
+36
View File
@@ -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.
+22 -11
View File
@@ -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).
+1
View File
@@ -0,0 +1 @@
0.7
+5 -3
View File
@@ -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.
-20
View File
@@ -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/
-40
View File
@@ -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.
-81
View File
@@ -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()
-23
View File
@@ -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" }
]
}
-674
View File
@@ -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>.
-53
View File
@@ -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.
-51
View File
@@ -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.
-248
View File
@@ -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
-73
View File
@@ -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
-976
View File
@@ -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;
}
-26
View File
@@ -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
-141
View File
@@ -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
-75
View File
@@ -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
-195
View File
@@ -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
-437
View File
@@ -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
-101
View File
@@ -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
-152
View File
@@ -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
-735
View File
@@ -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
-163
View File
@@ -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
-85
View File
@@ -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
-73
View File
@@ -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
-262
View File
@@ -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
-80
View File
@@ -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
-103
View File
@@ -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
-197
View File
@@ -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
-126
View File
@@ -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
-147
View File
@@ -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
-395
View File
@@ -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
-778
View File
@@ -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()
-209
View File
@@ -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
View File
@@ -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
View File
@@ -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);
}
*/
File diff suppressed because it is too large. Load diff
-3714
View File
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
-618
View File
@@ -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
View File
@@ -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
View File
@@ -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*)&current_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
View File
@@ -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
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
-627
View File
@@ -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.
------------------------------------------------------------------------------
*/
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
-128
View File
@@ -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.
-49
View File
@@ -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)
-38
View File
@@ -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
View File
@@ -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.
-63
View File
@@ -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.
-21
View File
@@ -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)
-371
View File
@@ -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
-287
View File
@@ -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
-260
View File
@@ -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
-853
View File
@@ -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
-394
View File
@@ -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
-570
View File
@@ -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
-788
View File
@@ -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, &region_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, &region_prot, &type, &region_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);
}
-121
View File
@@ -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);
}
-44
View File
@@ -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
-13
View File
@@ -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
-5
View File
@@ -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
-27
View File
@@ -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
-161
View File
@@ -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
-28
View File
@@ -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
-39
View File
@@ -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;
}
-154
View File
@@ -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
-96
View File
@@ -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
-66
View File
@@ -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.
-5
View File
@@ -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
-11
View File
@@ -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
-14
View File
@@ -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=
-31
View File
@@ -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
-20
View File
@@ -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.
-1
View File
@@ -1 +0,0 @@
fmt 12.2.0 (https://github.com/fmtlib/fmt), MIT: LICENSE. Used header-only (FMT_HEADER_ONLY).
-218
View File
@@ -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_
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
-673
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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_
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