Compare commits

...
368 Commits
Author SHA1 Message Date
ayozetr 18205f83d1 Launcher: version 0.51-linux.19 2026-10-30 02:09:06 +00:00
ayozetr e10e2a8fb4 Credits: zFenrys and Aman9das; deadinside28's menu pointer and 0.6 keyboard layout
zFenrys asked for the mouse pointer in the game's menus (#31) and Aman9das
for bbport 0.6's keyboard layout (#33); both come from deadinside28's 0.6.
In docs/CREDITS.md and the launcher's About, in every language.
2026-10-30 01:41:26 +00:00
ayozetr 14c0d9b426 Docs: line references after the menu pointer and the 0.6 keyboard layout 2026-10-29 22:58:19 +00:00
ayozetranddeadinside b76c950a66 Keyboard: bbport 0.6's layout as a choice in Controls (#33)
Controls → Keyboard and mouse → Keyboard layout: this fork's keys (the
default, unchanged) or bbport 0.6's, Dark Souls III's on PC: Space to dodge,
Shift to sprint, Shift + Space to jump, F to interact, E to transform, Tab or
R for the quick item, Ctrl for L3, Escape for the menu and the way back in
the game's menus, IJKL for the camera, 1-4 for the d-pad, Shift + click for
R2. bbport.ini keyboard_layout=0.6; key.<input> lines still replace its
keys, and the launcher shows the chosen layout's.

From 0.6's runtime_pad.c:
- key bindings may be chords ("Left Shift+Mouse Left"): while the modifier is
  held, the key's plain bindings wait;
- sprint and dodge make Circle (a short press for the dodge, held for the
  run, let go and pressed again for the jump; BB_JUMP_GAP_MS and
  BB_JUMP_TAP_MS), and do nothing while a game menu is open;
- with 0.6's layout, Options' key is Circle while a game menu is open (the
  menu pointer's query), except for the press that opened it.

The keyboard prompts in the game's menus follow the layout (Circle is drawn as
Escape, a Shift chord with a Shift arrow). Unit tests cover the layout, the
chords, the dodge and jump timing and Escape in menus.

Co-Authored-By: deadinside <[email protected]>
2026-10-29 22:53:49 +00:00
ayozetranddeadinside cd8b014ab3 Menu pointer from bbport 0.6: the mouse points and clicks in the game's menus
The item under the mouse gets the game's cursor, a left click confirms (or
presses a slider's arrow), the right button goes back and the wheel moves up
and down, in the title menu, the inventory, the shops and the options (#31).
The module hooks the game's list update (0x1acff30, checked byte for byte) and
is unchanged from 0.6, except that Hunter's Atlas's map and tile keep the
mouse while they use it, as the port's own menu does.

Its parts that this fork lacked come with it:
- guest code hooks (bbport_guest_code_hooks), in a namespace of their own:
  libbbgpu's symbols are global and libbbsc.so has its own
  Core::InstallGuestCodeHook. A patched page gets back the protection it had.
- the window's event filters and pointer queries and the pad filter
  (bbgpu_add_*), and the shown frame's aspect (bbgpu_frame_aspect).

While a game menu is open the window lets the mouse go and shows the cursor,
also in fullscreen, and takes it back after if it had it (not after F1). It
is installed with the fixes of the game's own bugs, so it works with either
memory model. BB_MENU_POINTER=0 turns it off.

Co-Authored-By: deadinside <[email protected]>
2026-10-29 22:53:29 +00:00
ayozetr 74ed838daf Docs: line references after the changes above 2026-10-29 17:43:35 +00:00
ayozetr 976027ea53 Credits: iannitram, molnar-a and gibs2193; what Awaken, Mrsuss60 and Nahnahchi added since 0.51-linux.18
iannitram's FSR 3.1 memory-type fix, molnar-a's Steam Deck crash report
and test (#35), gibs2193's mouse camera report (#31); Awaken's camera
motion check, Mrsuss60's upscaler image changes and Nahnahchi's reports
of black effects, rainbow textures and memory-manager crashes. In
docs/CREDITS.md and the launcher's About, in every language.
2026-10-29 17:43:27 +00:00
ayozetranddeadinside 916bcd1ceb Image clears: one layer of a cube or array too
The point lights' shadow cube is cleared a face at a time. A clear of
one layer was not recognised, so the game's shader wrote only memory and
the face kept older shadows that could show as wedges of light. A clear
whose range is exactly one layer of a single-level array or cube image
now clears that layer, unless the image holds CPU writes not uploaded
yet (a clear drops that mark for the whole image). Seen here: 64x64
cubes, all six faces, in the Hunter's Dream. From upstream 0.7.

Co-Authored-By: deadinside <[email protected]>
2026-10-29 17:43:27 +00:00
ayozetrandiannitram 8ffbf6b5df FSR 3.1: never a protected memory type unless asked for
RADV lists device-local protected memory types after the plain ones on
some GPUs (Ryzen Z1 Extreme / PHOENIX: ROG Ally, Legion Go), and
findMemoryTypeIndex kept the last match, so FSR 3.1 put its resources
in protected memory the device never enabled: full-screen colour noise.
A third patch to FSR-Vulkan, from upstream bbport 0.7.

Co-Authored-By: iannitram <[email protected]>
2026-10-29 17:43:19 +00:00
ayozetrandAwaken 6530aa1641 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.

Co-Authored-By: Awaken <[email protected]>
2026-10-29 17:43:19 +00:00
ayozetranddeadinside 5cc279af74 Texel buffers: no miss remembered over an image not drawn yet (#1)
The memo of texel-buffer ranges with no image also took ranges where an
image exists but the GPU has not drawn into it yet. Once the GPU drew
into it nothing changed the registry generation, so the buffer kept
reading stale memory: the motion blur frozen after lamp travel until
the next one. A miss is remembered only where no image is there at
all. From upstream 0.7 (its #308).

Co-Authored-By: deadinside <[email protected]>
2026-10-29 17:43:19 +00:00
ayozetr 12d76b3862 Texture collector: the memory check for images written by copies and clears too (#29, #32, #35)
The game's memory under a GPU-written image was taken only where a draw
or a compute shader wrote it. Copies, blits, resolves and clears mark
an image GPU-written too, without it, so the collector wrote such an
image back unchecked; a session with the check on still crashed at
0x208599f (#29). Image::NoteGpuBase takes it now on every path that
marks an image GPU-written.
2026-10-29 17:30:55 +00:00
ayozetr 9507c9c392 Logs: the version and the build on the first line
Issue reports rarely say which version or test build they ran, and a
test build's About page showed the release it was built on. The
AppImage now carries a BUILD file (commit, branch, date) next to
run.sh; run.sh prints the launcher's version with it, or with git's
commit and branch in a checkout, as the first line of every log, and
the About page shows the build after the version. The issue template
and the log guide say so.
2026-10-29 15:43:54 +00:00
ayozetr 65fad4c52c Texture collector: no write-back over memory the game reused (#25, #32, #35)
Past the critical mark the collector writes an old GPU-written image back
into the game's memory before freeing it (#13, #14). Neither memory model sees
every change there: the game's heap reusing the memory of an area left behind
(write tracking misses it, and the PC model tracks none). 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". Cards with 8 GB or
less and the Steam Deck are past the critical mark often, so their players met
it: on the Deck at every second lamp travel (#35).

The game's memory under an image is now hashed when the GPU first writes it
since its last upload, and the write-back, tiled or linear, goes ahead only
if it is unchanged; otherwise the image is dropped, as BB_GC_TILE_BACK=0 does.
Reproduced on an RTX 3060 with BB_PC_MODEL=0 and BB_GC_BUDGET_MB=2500: a
crash at the first reload in 4 of 4 runs before, 18 reloads without one after.
With BB_PC_MODEL=1 the same images were written back over changed memory
(no crash there in 6 reloads), and are now dropped; the frames stay right.
2026-10-29 13:47:58 +00:00
ayozetr 28bb63315f Texture collector: no tiled write-back over memory the game reused (#25, #32)
Past the critical mark the collector tiles an old GPU-written image back into
the game's memory before freeing it (#13, #14). Write tracking misses some
changes 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". Cards with 8 GB or less are past the critical mark often, so their
players met it, mostly on NVIDIA's memory model.

The game's memory under an image is now hashed when the GPU first writes it
since its last upload, and the write-back goes ahead only if it is unchanged;
otherwise the image is dropped, as BB_GC_TILE_BACK=0 does. Reproduced on an RTX 3060
with BB_PC_MODEL=0 and BB_GC_BUDGET_MB=2500: a crash at the first reload in
4 of 4 runs before, 12 reloads without one after (about 450 write-backs in
each run, 8 or 9 of them refused).
2026-10-29 13:47:28 +00:00
ayozetr 8d18daeb96 Mouse: the direct camera by default, as in bbport 0.6 (#31)
With the mouse moving the right stick, the camera turned at the game's
stick speeds, which differ sideways and up and down, with the stick's
dead zone and acceleration on top. mouse_camera now defaults to direct;
mouse_camera=stick keeps the old way. Earlier launchers wrote
mouse_camera=stick into bbport.ini with the other keys, so the launcher
turns it to direct once.
2026-10-29 00:10:38 +00:00
ayozetrandMrsuss60 1cdb240933 Upscaling: three of Mrsuss60's image changes, off by default for comparisons
From Mrsuss60's Windows port (466e8e7d), each behind its own switch so the game looks exactly as
before until they are compared in game:
- BB_TAA_RESPONSIVE=1: TAA history weights 0.88/0.75 instead of 0.98/0.85, fewer trails.
- BB_STABLE_MOTION_HASH=1: object motion identifies a draw's vertex streams without their buffers'
  addresses, for streams rewritten into a ring buffer at a new address each frame.
- BB_DEPTH_MIN_FILTER=1: depth-only reduced-size targets are resampled by the shader, keeping the
  nearest of each 2x2 when downscaling.
The rest of that commit, YCoCg and variance clipping and the 3x3 filter on by default, is left
out: measured here on 2026-10-02 they did not help, and two made the picture worse. Docs line
references follow the code again, the binding count change before this one included.

Co-Authored-By: Mrsuss60 <[email protected]>
2026-10-27 19:42:31 +00:00
ayozetr 55f9db9941 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 the pipeline cache
stays valid. No such stage came up in a session in the Hunter's Dream; the counts there are
unchanged.
2026-10-27 19:23:59 +00:00
ayozetr 007c1e4b60 Shaders: images counted once per stage again when the next stage's bindings start
Counting every per-mip binding of a DynamicIndex image in Info::AddBindings (4e7c4a0d) agreed
with the pipeline layout, but the rasterizer still advances one per image when it writes the next
stage's descriptors: the next stage's shader would look for them where they are not written. That
mode is used on NVIDIA (no VK_AMD_shader_image_load_store_lod), so it goes back to how it was until
both sides change together. Pipeline key version 6 again, so caches are not rebuilt for nothing.
The phi pass ordering from the same commit stays.
2026-10-27 18:20:52 +00:00
ayozetr 1f4f56164c Launcher: version 0.51-linux.18 2026-10-27 18:14:14 +00:00
ayozetr df10280204 Texture collector: "maybe touched by the CPU" checked before a GPU-written image is dropped (#30)
An image whose page the CPU may have touched is only suspected dirty: RefreshImage compares a
hash and keeps what the GPU wrote when nothing changed. The collector treated the suspicion as
dirt and freed such images without downloading them, so they came back with the game's old bytes:
effects drawn by compute shaders turned black under memory pressure (#30). With a small VRAM
budget and three reloads, 63 GPU-written storage images were dropped this way; with the hash
checked first, 7, all of them really changed by the CPU.
2026-10-27 18:14:05 +00:00
ayozetr 42ec043c3e Texture cache: a GPU-written 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 and particles and models
seen from afar (#30). Every level is now copied to its offset in the game's layout, as
WriteBackTiledImage already does for tiled images. With the old eviction rule
(BB_GC_KEEP_GPU_WRITTEN=0), a small VRAM budget and seven reloads, the player's armour and weapon
now stay correct; before, they turned black.
2026-10-27 18:12:00 +00:00
ayozetr fe740c843d Texture collector: GPU-written linear images wait for the critical mark (#30)
Under memory pressure, below the critical mark, the collector downloaded the linear images the
GPU had written and freed them; loaded again they came back black. On 8 GB cards, under pressure
the whole session, effects in the Hunter's Dream turned black after a few reloads (#30).
Reproduced here with a small VRAM budget (BB_GC_BUDGET_MB=2500) and seven reloads: the player's
armour and weapon went black. Linear GPU-written images now follow the tiled ones' rule, leaving
only past the critical mark and unused for a minute; with that the same run stays correct.
BB_GC_KEEP_GPU_WRITTEN=0 restores the old behaviour.
2026-10-27 18:12:00 +00:00
ayozetr 804651e418 Players' guide: published to the gh-pages branch by a script
GitHub Pages now serves the gh-pages branch. tools/publish_guide.sh builds the guide (strict)
and commits it there, replacing the Actions workflow, which cannot run on this account for now.
2026-10-27 02:30:33 +00:00
ayozetr 0e03a5c29e Players' guide on GitHub Pages, built from docs/
A guide site for players (https://ayozetr.github.io/bloodborne_linux/), made with MkDocs Material
from docs/ itself, so the guide and the repository's documents never disagree. A theme of its
own, "Hunter's Dream" (moonlit blue-grey, IM Fell English headings, EB Garamond text, a moon and
mist on the home page), with no artwork or typeface from the game. New pages for players:
getting started, Steam and the Steam Deck, getting a log (Game Mode too) and a FAQ from the
questions asked in issues and on Reddit. A hook points links to files outside docs/ at GitHub.

Online play now starts with Seamless Hunt, the default server, and links read as words instead
of file names. The workflow builds the site (strict) and publishes it when docs/, mkdocs.yml or
site/ change on main. The README and the launcher's About page link to it.
2026-10-27 02:27:45 +00:00
619826c20b Small fixes: data folder parents, a deleted image's slot, the map's zoom on resize
- The GPU library's data folders are created with their parents: a home without ~/.local/share
  ended the process. From Awaken's LanternEngine.
- FindImage's cache no longer reads the slot of an image deleted since it was cached. From
  Mrsuss60's Windows port.
- Hunter's Atlas keeps what the map covers when the window size changes: the zoom follows the UI
  scale. From Hunter's Atlas 3.0.

Co-Authored-By: Awaken <[email protected]>
Co-Authored-By: Mrsuss60 <[email protected]>
Co-Authored-By: MikeTat1988 <[email protected]>
2026-10-25 16:55:54 +00:00
ayozetrandAwaken 4e7c4a0d47 Shaders: every per-mip image binding counted; the phi pass in a fixed order
Info::AddBindings counted one binding per image, while the SPIR-V emitter and the pipeline layout
give an image with a binding per mip level (DynamicIndex) one per level: the next stage's
bindings could start inside it and read the wrong resources. Pipeline key version 7, so saved
pipelines are rebuilt once.

The phi simplification pass queued SCC groups in the hash table's order, which depends on block
addresses, so the same shader could compile to different SPIR-V from one run to the next. They
are queued in the SCCs' order now (upstream shadPS4 #5364). Both from Awaken's LanternEngine
(0.9.37 beta 5 and 7).

Co-Authored-By: Awaken <[email protected]>
2026-10-25 16:55:46 +00:00
ayozetrandAwaken 25093ba8cc Guest memory: a block promoted to VRAM is uploaded whole
With the PC memory model, a block given a VRAM copy was only marked CPU-modified, so just the
ranges later bindings named were uploaded; the rest of its slot kept the bytes of the block that
had it before. Vertex buffers are bound without a size and shaders read through the page table,
so draws could read them: stretched polygons and flashing lights, as EnsureResident already says
for reused slots. The whole block is now uploaded at the next packet. From Awaken's LanternEngine
(0.9.37 beta 5).

Co-Authored-By: Awaken <[email protected]>
2026-10-25 16:55:46 +00:00
ayozetr a92a2dd921 Controller light bar colour, as a launcher option
A new row in Controls sets the colour of the light bar on a DualShock 4 or DualSense (off by
default: SDL's player colour). The launcher passes it as BB_PAD_LIGHTBAR=RRGGBB, and the runtime
sets it on every controller it opens, with a log line saying whether the controller took it.
Asked for in discussion #26. Docs line references follow the code again.
2026-10-25 16:36:04 +00:00
ayozetr 23ce6331d4 Launcher: version 0.51-linux.17 2026-10-25 15:13:52 +00:00
ayozetrandCLS_AMG a55d98a50c BBSC 0.0.2: spectator camera, bells anywhere, the host's night, fixes
CLS_AMG's BBSC 0.0.2 (4f11faf), merged three ways over our 0.0.1 base
(315f76c) with every Linux change kept; bbsc/CHANGES-bbport.md lists
what was taken and how each conflict went. The aimed placement and the
host lock that main already had now use upstream's names.

From 0.0.2: the camera that follows a partner while you are dead, the
kit's bells ringing anywhere (a new BBSC option in the launcher, in its
seven languages), the guest seeing the host's time of night, shorter
message and bloodstain lists against the "Panic 265" crash, the fixes
around boss kills, kicks and guest drops, the kit found in the game's
tables, the relay's write queue and a short timeline of each game in
bbsc/logs. New switches: SHADPS4_BBSC_AREA_LIST_CAP, _WORLD_SYNC_ALL,
_TIMELINE and _BELLS_ANYWHERE, all on as upstream (docs/ENVIRONMENT.md).

On Linux the spectator camera reads its keys through SDL, as F8 does,
and only while you are dead in co-op; the free filming camera stays out,
since every key and mouse button also reaches the game through the pad.
The relay sends with MSG_NOSIGNAL and sets its send timeout as a
timeval. BBSC's own Discord status is not used: bbport has its own.

The relay frames are upstream's, so Linux and Windows 0.0.2 players
share one protocol.

Co-Authored-By: CLS_AMG <[email protected]>
2026-10-25 15:12:58 +00:00
ayozetr a23e88394b License: GPL-3.0-or-later; credits and docs brought up to date
The port now includes code from Lantern Engine by Awaken
(https://github.com/Awakenchan/LanternEngine), whose engine is GPL-3.0-or-later, so the whole is
distributed under GPL-3.0-or-later. Everything else in it allows that: bbport and shadPS4 are
GPL-2.0-or-later, the rest MIT, BSD or Zlib. Files from other projects keep their own notices.
LICENSE has the GPL-3.0 text; the README, BBPORT.md, the launcher's About page and Hunter's
Atlas's notes say so, and the attribution Lantern Engine's NOTICE asks for is in the README,
BBPORT.md, the credits, the About page and the release notes template.

Credits: TacticalBill, holydingus, OPGZ2, kuday248 and BlazinKarp for their reports, with more
for n3vrs0ft, Nahnahchi and Awakenchan, in docs/CREDITS.md and the About page in every
language. Code line references in the docs follow the code again.
2026-10-25 15:09:59 +00:00
ayozetr 083ba1f085 Steam overlay, as an option, when the game is started from Steam (#28)
The AppImage's sharun clears LD_PRELOAD in every program it runs, so Steam's overlay never reached
the game. With the new launcher option (under Add to Steam, saved as soon as it changes) run.sh
reads Steam's 64-bit gameoverlayrenderer.so from the nearest process above that still has it and
hands only that library to the game. On a Wayland desktop the window then goes through Xwayland,
since Steam draws its overlay only on X11 windows. Off by default.
2026-10-25 15:02:24 +00:00
ayozetr d412c456cf Steam shortcut: start folder and icon outside the AppImage's mount
Added from the AppImage, the shortcut's start folder and icon were in the AppImage's mount under
/tmp, which is gone once it closes and changes on every start. The shortcut now starts in the
AppImage's folder, and its icon is a copy in the data folder.
2026-10-25 15:02:24 +00:00
ayozetrandAwaken 29a4d62feb 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 (#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 LanternEngine (940396b).

Co-Authored-By: Awaken <[email protected]>
2026-10-25 02:17:55 +00:00
ayozetrandAwaken 1b8c9de5b6 Draw preparation: a fault ahead of the GPU thread leaves the draw to it
A draw-preparation worker reading guest memory the game had protected or freed had its fault
handled like any other: a readback by the GPU command thread, waited for while the worker held
programs_mutex, which the GPU thread could be waiting for. Workers now jump to a recovery point
for the whole draw instead, and the GPU thread prepares that draw in order. GetProgram keeps
its own recovery point inside the lock and hands back to the outer one. From Awaken's
LanternEngine (7aa3d1b).

Co-Authored-By: Awaken <[email protected]>
2026-10-25 02:17:55 +00:00
ayozetrandAwaken 86d418b21b 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.
From Awaken's LanternEngine (7aa3d1b).

Co-Authored-By: Awaken <[email protected]>
2026-10-25 02:17:55 +00:00
ayozetrandAwaken fff6786e17 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, as
reported on an RTX 4060 in the Hunter's Dream. 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).

Not reproduced here: in the Hunter's Dream on an RTX 3060 with 8
workers, 0 of 4.4 million draws differed. Forcing 1 draw in 7 down the
new path: no errors, 60 FPS.

Found by Awaken (LanternEngine 0.9.34).

Co-Authored-By: Awaken <[email protected]>
2026-10-23 21:41:06 +01:00
ayozetr da01b97dbd Credits: thomasgarafano1's Hunter's Atlas report; GitHub's language bar
thomasgarafano1 found why Hunter's Atlas did not prepare the map with
their Game of the Year dump, down to the two lines, and tested the fix
(#24). Under Help and testing, in CREDITS.md and the launcher's About
section in its seven languages.

.gitattributes: GitHub counted bbsc_messages.inc, a C++ table of game
texts, as PHP because of the game's <?name?> placeholders; it is marked
as C++.
2026-10-23 01:56:01 +01:00
ayozetr 72f92a18cf Hunter's Atlas: any dump of the 1.09 executable prepares the map (#24)
The map generator took only one eboot.bin: it compared the file's SHA-256
with the US release dumped by one tool. A dump of the same 1.09
executable made by another tool (another SELF header around it), or of
another edition, stopped with "Unsupported executable fingerprint" (a
European Game of the Year edition dumped on the player's PS4).

Both checks now use the port's own one (scripts/game_check.py): the hash
of the executable's loaded image, the same for every dump and edition the
port runs. The features file keeps its format marker, which the game
checks, so maps already made still load.

Reported by @thomasgarafano1.
2026-10-23 01:55:25 +01:00
ayozetr e58b0a2a62 Docs: the European Game of the Year edition named as such
"The GOTY edition (CUSA03173)" read as if it were the only one. The title
IDs and regions in the docs were checked against the PlayStation Store and
ORBISPatches: they are right.
2026-10-23 01:39:38 +01:00
ayozetr 2d049d51c6 Docs: English only, up to date, and nothing left from other platforms
READMEs taken from other projects described their Windows packages:
- bbbridge/README.md, bbsc/README.md and bbsc/server/gameserver/README.md
  are rewritten for how this port builds and runs them; bbsc/server gets
  a README.md for hosting BBSC on Linux in place of the Windows host
  package's README.txt.
- BBSC's Windows host scripts (.cmd, tools/*.ps1) and bbbridge's
  make_release.ps1 are removed: nothing used them. SUPPORTERS.txt keeps
  its heading, which BBSC checks, without the donation links.

docs/ kept upstream bbport's files unchanged so that its updates merged;
the port has gone its own way since:
- Upstream's release notes, session logs, roadmap and dated experiments
  are removed; links to them point at upstream's copies.
- Every Markdown file is in English: BBPORT.ru.md and three dated
  Russian notes are removed, and the notes the code refers to
  (upscaler.md, TRANSLATOR_ARCHITECTURE, MEMORY_MODULE_PLAN) are
  translated.
- docs/README.md indexes what is left.

Checked against the code:
- ENVIRONMENT.md: file and line references follow the code again, plus
  Hunter's Atlas variables and other missing rows.
- BBPORT.md: the memory model's Auto default, occlusion counts, the debug
  menu's fonts and the clone URL.
- ONLINE.md and SERVER.md: Seamless Hunt as the default and the BBSC
  switch's name.
- parallel_gpu.md: the toggle bits.
- The launcher's comment on the memory module.
2026-10-23 01:26:44 +01:00
ayozetr 8161988bd9 Launcher: version 0.51-linux.16
The release with the crash at guest offset 0x2169edc fixed in all
likelihood (#23), the GPU freeze guard for NVIDIA (#19), the Language
entry back in the title's System menu, and the stick dead zone and local
co-op options.
2026-10-23 00:13:58 +01:00
ayozetr 8478fb95fb Credits: Awaken's occlusion and shader fixes, Nahnahchi's logs and tests
Awaken found why the occlusion counters were written ahead of the DMA that
clears them (#23) and why vertex shaders were reused with the wrong
bindings. Nahnahchi's logs and tests showed the GPU freeze in a boss fight
(#19) and the crash in Yahar'gul (#23). In CREDITS.md and the launcher's
About section in its seven languages.
2026-10-23 00:10:33 +01:00
ayozetr 81dd13e990 Merge branch 'fix/launcher-choice-align' into test/all-2026-10-22 2026-10-23 00:05:28 +01:00
ayozetr b9552c562e Launcher: a short chosen value sits against its arrow
The chosen value of a choice row is as wide as the list's longest one, so
it does not jump between choices, and was aligned to the left: a short one
such as "60" or "Principal" stood far from the arrow. It is aligned to
the right now; the open list keeps its items aligned to the left.
2026-10-23 00:05:28 +01:00
ayozetr 83bf796126 Merge branch 'fix/stage-specialization-reuse' into test/all-2026-10-22 2026-10-22 23:49:47 +01:00
ayozetr 717fa0ad83 Merge branch 'fix/zpass-in-order' into test/all-2026-10-22 2026-10-22 23:49:47 +01:00
ayozetr 22f9b6d769 Merge branch 'fix/quit-menu-language' into test/all-2026-10-22 2026-10-22 23:49:47 +01:00
ayozetr 7b8491fef8 Merge branch 'feature/allocation-failure-log' into test/all-2026-10-22 2026-10-22 23:49:47 +01:00
ayozetr 101e583bac Merge branch 'feature/guest-fault-call-chain' into test/all-2026-10-22 2026-10-22 23:49:47 +01:00
ayozetr f926e25676 Merge branch 'fix/issue19-indirect-draw-guard' into test/all-2026-10-22 2026-10-22 23:49:46 +01:00
ayozetr ba63ecfc4e Merge branch 'feature/launcher-polish' into test/all-2026-10-22 2026-10-22 23:49:46 +01:00
ayozetrandAwaken d8f3e3633e Occlusion counts written in stream order (#23)
With the occlusion queries not translated, the stand-in counter dump was
written at decode. The game zeroes a query's block with a DMA_DATA first,
which the draw pipe runs later on the recording thread: the zeroing landed
after the counts and the query never finished. The game makes new queries
in place of the ones it still waits for, and when it can make no more it
reads a null handle at guest +0x2169edc, the crash of issue #23.

The dump now runs in order with the draws, as a WRITE_DATA does.
BB_ZPASS_IN_ORDER=0 writes it at decode as before.

Found by Awaken (LanternEngine 0.9.35).

Co-Authored-By: Awaken <[email protected]>
2026-10-22 23:04:41 +01:00
ayozetrandAwaken 8d36a25b13 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.

Found by Awaken (LanternEngine 0.9.37 beta 7).

Co-Authored-By: Awaken <[email protected]>
2026-10-22 23:03:43 +01:00
ayozetrandethan f99498b900 Quit to Desktop: keep the Language entry in the title System menu
Quit to Desktop took the place of the System menu's Language entry, whose
handler held its code, and removed the entry outside the world, where the
game shows it (text and voice language). Players lost it.

The Language entry is the game's own again. In the world, where the game
builds it only to remove it, a jump at its start adds Quit to Desktop
instead, with its own text id (110006; Language keeps 110005). The new
code lives past the end of the code segment, in the rest of its last
4 KiB page: mapped executable with the segment and unused by the game.
The loader lets patches write there, and patches.py checks the segment
layout first.

From Supermedo's Windows port (c065c80e, by ethan).

Co-Authored-By: ethan <[email protected]>
2026-10-22 22:51:10 +01:00
ayozetr c0728a3dbc Log reservations of guest memory that fail
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 (issue #23) 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).
2026-10-22 22:34:34 +01:00
ayozetranddeadinside 936f51f3e7 Print the game's thread and call chain on a guest fault
A crash inside the game logged only the faulting offset (issue #23: a NULL
read at +0x2169edc). The fault handler now also prints the thread name and
up to 12 return addresses along the rbp chain, read with process_vm_readv so
a broken chain cannot fault again. It runs only when the process is already
terminating. From upstream 0.6 (72e5c8ce).

Co-Authored-By: deadinside <[email protected]>
2026-10-22 22:31:58 +01:00
ayozetr 25a8ca77e9 Check indirect draw arguments before the GPU runs them (#19)
Indirect draws take their vertex and instance counts from memory a
shader may have written, as indirect dispatches do. In #19 the GPU
stopped on one during a boss fight (NVIDIA Xid 109, context switch
timeout), as garbage counts would make it.

Before each indirect draw a compute shader copies its arguments to a
slot of our own, with the vertex (or index) and instance counts zeroed
when their product passes BB_INDIRECT_DRAW_GUARD_MAX (16M by default;
the largest the game was seen to use is 8600). Skipped draws are
counted in the log. BB_INDIRECT_DRAW_GUARD=0 turns it off.

The check cannot run inside a render pass, so an open one is paused
and continued afterwards, loading its attachments instead of clearing
them again. The game has a few indirect draws per frame at most, so
this costs nothing measurable; synchronisation validation reports
nothing on the new commands.
2026-10-22 02:00:25 +01:00
ayozetr 5768190fbd Local co-op: copy player 1's saves to another player
Players 2-4 have saves of their own, so playing the same characters
together meant copying files by hand. Local co-op now has Copy player
1's saves, to player 2, 3 or 4: their saves are first copied to their
savedata-backups (save_import.import_saves), with no game running.
2026-10-21 15:48:40 +01:00
ayozetr 62aa2ee587 Local co-op: a monitor of its own
The players' windows went to the game's monitor (Graphics → Monitor),
so playing together on a TV beside the desk meant changing the game's
monitor first. Local co-op now has its own Monitor row: as the game, or
any connected monitor, which all the players' windows then share.
2026-10-21 15:47:16 +01:00
ayozetr aab75135e6 Launcher: the sticks' dead zone in Controls
A worn stick that drifts moved the character or the camera on its own,
and the only fix was BB_PAD_DEADZONE in the extra variables. Controls
now has Stick dead zone (%), 0 to 50, 4 by default (the port's 5 of
127, which is then left to the port).
2026-10-21 15:46:02 +01:00
ayozetr 63e9cebb7a Local co-op: say so when there is no X server
The players' windows are placed through X11 (XWayland in a Wayland
session). Without an X server the games started and their windows could
not be placed. Play together now checks for one first and says how to
get it: XWayland in the session's settings, or an X11 session.
2026-10-21 15:45:02 +01:00
ayozetr e26cc5fd51 Launcher: a warning on FPS limits above 120
Above about 120 FPS the game's movement timing breaks: running and
rolling slow down. An FPS limit above 120 now shows a warning sign and
says so in the row. The idea is from Supermedo's launcher (51cac0e).
2026-10-21 15:44:14 +01:00
ayozetr f2dd58e118 Launcher: menu and debug patches out of Graphics
Skip intro, Quit to Desktop, the free camera and the debug menu change
the game's menus or open its debug tools, not the picture, yet they sat
under Graphics → Game effects. They are now in their own group, Mods →
Menus and debugging; the settings and bbport.ini keys stay the same.
2026-10-21 15:43:17 +01:00
ayozetr 119bce7969 Launcher: Online tab in the order it is used, and "Don't ask again"
The Online tab's groups follow the order they are set up in: the server
and its address, the account on it, seamless co-op, then the connection
details, with the title's online/offline choice at the end.

Play with an account set and "Play online" off asks whether to play
online; someone who plays offline on purpose can now tick "Don't ask
again". Turning "Play online" on brings the question back.
2026-10-21 15:37:50 +01:00
ayozetr de95d55c60 Trophies: an encrypted npbind.dat or trophy file in the dump
A Steam Deck dump of CUSA03173 has sce_sys/npbind.dat, but encrypted
(no NPBIND header, no NPWR id in it), so the Trophies page said the file
was not found (#22). For a Bloodborne release the port runs (param.sfo's
TITLE_ID), its own id NPWR05818_00 is used then; without the trophy key
that also gives PSN's English list. When the id is still unknown, the
page says the id is missing from npbind.dat rather than the file.

A trophy00.trp that is not a trophy file (encrypted or damaged the same
way) was taken for a wrong trophy key. It now says so, and to dump the
game again.
2026-10-21 14:53:03 +01:00
ayozetr a9690c592c Credits: n3vrs0ft's BBSC logs and simo1730's trophies report
n3vrs0ft's logs showed a BBSC guest following another host's world (#17),
and simo1730 reported the Trophies page not finding the dump's files
(#22). Both under Help and testing, in CREDITS.md and the launcher's
About section in its seven languages.
2026-10-21 12:25:04 +01:00
ayozetr ed36fd1aa0 Trophies: dump files found whatever their case, and the missing one named
A dump copied through Windows or extracted by some tools can have
SCE_SYS/TROPHY/TROPHY00.TRP or NPBIND.DAT, and on Linux the exact
lower-case path was not found, so the Trophies page said the dump had no
trophies (#22). Each name is now matched without regard to case, and
when the trophies still cannot be read the page says which file is
missing: sce_sys/trophy/trophy00.trp, or sce_sys/npbind.dat (the game's
NP communication id).
2026-10-21 12:20:43 +01:00
ayozetr 5023ce183c 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 size is now the
points times the display's pixel density.
2026-10-21 12:16:27 +01:00
ayozetr a5cff2a433 Launcher: version 0.51-linux.15
The release with the launcher in a sidebar, Seamless Hunt as the
default server, saves export, import and automatic backups, and
seamless co-op that meets players on BBSC 0.0.2.
2026-10-21 12:02:12 +01:00
ayozetr 279e978750 BBSC: the guest locks onto the host that rang its sign, as in 0.0.2
BBSC 0.0.2 (published 2026-10-21) ships the aimed placement this
branch reimplemented from its earlier DLL, with the same frame
("SHAD" 0x31 + "<TargetUserId>|<placement>") and the same 90 s, and
ignores the server-wide 0x30 that 0.0.1 sends. Windows 0.0.2 players
need it, so it goes in now with the part of 0.0.2 that uses it:

- a placement aimed at our sign notes its sender as the host that rang
  it, and while joining that host's packets are taken, so the join
  load gets its world flags;
- the guest locks onto that host when it is on the relay; otherwise the
  position lock is stricter (6 m, one host that close, 3 checks in a
  row), and "the only one hosting on the server" is gone, as it was a
  stranger whenever the real host was not on the relay.

The rest of 0.0.2 (spectator camera, bells anywhere, night time, the
fixes after boss fights) is left for a later update.
2026-10-21 12:02:00 +01:00
ayozetr 1c52215987 BBSC: a host's placement goes to the sign it rings
Issue #17: as a guest on Seamless Hunt, a Linux player's sign never
reached their friend, and sometimes a stranger summoned it despite the
password. BBSC 0.0.1 sent every host's placement to the whole server,
and a waiting guest followed the first host it heard from: with 12
hosts on the server in one session, it followed a stranger and its sign
was advertised on that stranger's map.

BBSC's own bbsc.dll of 2026-10-17 changed this, with no source
published yet. This is the same protocol, reimplemented from that
binary, so Linux and updated Windows players understand each other:

- 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>", plus the placement header to the
  broker;
- a guest takes only a placement sent to its own sign, and keeps it for
  90 s (20 s otherwise);
- placements sent to the whole server (0x30) are ignored, as the newer
  BBSC builds do; nothing is sent while polling for signs.

The frame layout matches the binary's sender and receiver. Not tested
with two players yet. Documented in bbsc/CHANGES-bbport.md.

Co-Authored-By: CLS_AMG <[email protected]>
(cherry picked from commit 729aaca178662c8671641eae949dded9edb279b8)
2026-10-21 12:02:00 +01:00
ayozetr e6d62a35d3 Launcher: optional automatic save backups, and a banner for a missing game
Setup → Saves can copy the saves into savedata-backups/auto-<date> before
each game start, keeping as many copies as chosen (5 by default); off by
default. Saves unchanged since the newest copy are not copied again, so
starting a few times without playing keeps the older ones. A copy is
restored like any folder, with Import → From a folder…, and the row menu
opens the backups folder.

Without a game folder (or without eboot.bin in it) Play stays grey; a
banner now says why on every page and leads to Setup, where the first
start opens.

The Journal's deferred scroll reads the current view: a rebuild right
after new log lines scrolled the destroyed one and aborted GTK.
2026-10-21 11:53:38 +01:00
ayozetr 9e8b45537a Launcher: saves export and import, folder rows in words
- Setup has a Saves group: the saves folder, Export (a .zip with the
  saves in use, in a folder named as a PS4 export) and Import, from a
  folder as before or from such a .zip. Only the save files are read
  out of an archive, by name, so no path in it can write elsewhere;
  the current saves are backed up first, as with a folder.
  save_import.py takes --export and a .zip source too.
- The game, DLC, saves, mods and patches folder rows had up to five
  icon buttons. They now have "Change..." and a "..." menu with the
  other actions in words (open in the file manager, back to the
  default folder, refresh), and the saves row says in its subtitle
  whether there are saves.
- "Mute in the background" moves from Display to Setup, next to the
  Discord status: how the game behaves on the desktop.
- Present mode and GPU data readbacks explain each choice under the
  row, and the readback choices are translated (Relaxed, Off, Precise).
2026-10-20 15:44:13 +01:00
ayozetr ada0be7b7c Launcher: Mods first in its section, loading under Advanced
The Mods section opened with the map, the cheats and the gameplay
tweaks, and its own Mods group came fourth. It now goes Mods,
Third-party patches, Map, Cheats, Gameplay tweaks. Loading and
smoothness (bbhost's quick loading and the stutter fixes) is about
performance, not mods: it moves to Advanced, after Performance.

Spanish says "Online" throughout, as the switch already did, instead
of mixing it with "en línea".
2026-10-20 15:30:44 +01:00
ayozetr 5899c2b37d Launcher: no freeze on Play, dialogs without a stutter
- Play froze the window for 0.2-0.5 s: the button prompts follow the
  controller, and the gamepads were looked at on the main loop. The
  look now runs in a thread (with_gamepads) and the game starts after
  it; Play is off meanwhile, so a double click starts one game. Local
  co-op and the Refresh buttons go the same way, and the window's lists
  are filled from the last look and again once a look in the
  background ends. The window now builds in 0.4 s (0.9 s before).
- Every dialog (alerts, confirmations, the thanks) opens and closes
  without its animation under the cairo renderer, where each frame
  redrew the whole window on the CPU (present_dialog).
- The Trophies page no longer remakes its rows and icons on every
  visit when nothing changed.
- The Log scrolls to the end once per burst of lines, not once per
  line.
- A language change waiting for work that ends just as the game starts
  waits for the game too.
2026-10-20 14:11:04 +01:00
ayozetr 57ea71d393 Launcher: translations, tooltips and smaller texts
- Simplified Chinese had 25 strings in English (cheats, gameplay
  tweaks, trophy grades, the own server's note): translated. 16 keys
  no code uses any more are gone from every language.
- The game folder row says why the game cannot start (no eboot.bin)
  in its subtitle, and "choose the game folder" when none is chosen,
  not only in a tooltip.
- Assign and Reset in Controls name their control in the tooltip, so
  a screen reader tells them apart; the trophies menu button has one.
- The guide to hosting a server shows with "Your own server" only.
  Failed test lines get an error mark, not a warning.
- Shorter "From the dump" region value (it was cut in Spanish), quoted
  account name after Create, the About group no longer repeats the
  section's title, and a few English wordings.
- Docs: the connection test has check and error marks, not green and
  red lines; BB_SKIP_NETWORK_CHOICE goes online when online play is
  on.
2026-10-20 14:07:05 +01:00
ayozetr ca41802094 Launcher: keep edits over a waited rebuild, own server checks
- A language change that waited for running work rebuilt the window
  from the saved settings: what was changed meanwhile, an account just
  signed in to The Hunter's Dream included, was lost. It is stored
  first now.
- With "Your own server" and no address, Create and Test used Seamless
  Hunt's server (and filed the account under the own server). They are
  off until there is an address.
- The Status row's "signed in" belonged to the name, password and kind
  only, so another own server's address still showed signed in. The
  server's address is part of it now.
- A rebuild on the Trophies page prepared the trophies twice at once:
  the two could clash on the temporary file and show a false "wrong
  key". It prepares once, the file is per thread, and only a decryption
  error counts as a wrong key. A result arriving while trophies are
  being marked no longer drops the checks.
2026-10-20 14:04:34 +01:00
ayozetr bf133c7153 Launcher: thanks dialog made once, shown without a stutter
The thanks dialog (some 60 rows) was made on every click and its
animation ran at about 20 frames a second: with the cairo renderer,
kept because GTK 4.24's GPU renderers still cut the top of capitals at
115 % scale, every frame redraws the whole window on the CPU. The
dialog is now made once while the window is idle (again after a
language change), and with cairo it opens and closes without its
animation; animations come back once it is gone.
2026-10-20 14:02:48 +01:00
ayozetr 866b8d410c Launcher: faster start, no pause when the game starts or stops
Profiled with real settings, building the window took 1.65 s and a
language change 1.62 s:

- The Hunter's Atlas row counted the maps' and tools' size on every
  update of Play (about 14 000 files, half a second), so the window
  also froze each time the game started or stopped. The size is now
  counted once per state of the folders; preparing or deleting the
  maps makes a new one.
- The gamepad list started bb-gpu-capabilities twice per build (0.3 s
  each). One look now serves the build; Refresh still looks again.
- The Trophies page decrypted the trophy names in Python on every
  visit (half a second in a thread, holding the interpreter). The same
  .trp, key, language and script are now decrypted once; the key is
  only part of a digest.

The window now builds in 0.9 s, a language change takes 0.5 s.
2026-10-20 13:26:09 +01:00
ayozetr 8a2d900587 Docs: launcher locations after the sidebar layout
The README, BBPORT (English and Russian), ENVIRONMENT, TROUBLESHOOTING,
ONLINE, SERVER, MODS, COMPATIBILITY, DEBUGGING, CREDITS and the Hunter's
Atlas README named pages the launcher no longer has (Settings, Game,
Mode, Screen, the Online and Local co-op pages, the Journal tab). They
now name the sidebar sections and groups: Graphics, Controls,
Multiplayer (Online and Local), Mods, Trophies, Setup, Advanced, Log
and About. The connection test is the Test button in the Status row,
and the README lists Seamless Hunt first, as the default server.
2026-10-20 13:21:30 +01:00
ayozetr 0ce9f40f88 Launcher: clearer Online tab and texts
- Online tab: one Test button, in the Status row, with the results
  right under it (cleared when the server changes). The intro says
  where messages and summons come from on the chosen server, the Play
  online switch says what it does in both states, and the game server
  rows show only where they apply (an own server's address, Seamless
  Hunt's bloodstains).
- Dropdowns keep their value readable (up to 16 characters) beside a
  long subtitle: the server choice read "Seamle...".
- Hunter's Atlas: Prepare is no longer the suggested action when maps
  from another folder are already in use.
- Setup: "System language" is now "Game language". Advanced drops the
  "Mode" title over its only row.
- English: "Couldn't" throughout, one "Applies when the game starts",
  the Log is no longer called the Journal. Spanish says "launcher"
  throughout.
2026-10-20 13:18:17 +01:00
ayozetr 7b04dffd8b Launcher: Play, co-op, sidebar and rebuild fixes
- Play and local co-op follow one rule (can_start): no game folder, a
  page that failed to build, an FSR 4.1.1 build or a map preparation
  keep them off. Before, finishing a map preparation or an FSR build
  turned Play back on without a game or in a half-built window, and
  co-op could start from one.
- The sidebar's page follows its selection, so the arrow keys work.
- A launcher language change keeps the section, the Multiplayer tab
  and, on a narrow window, the page shown. It waits until the game, a
  map preparation, an FSR build and the threaded tasks (PKG install,
  connection test, account creation, bbbridge sign-in) are done, so
  their buttons cannot be pressed twice on the new page.
- Refresh in the display, controller and co-op lists keeps the choice
  not saved yet.
- Trophies: only the latest refresh shows, a visit while marking keeps
  the checks, an unexpected error shows instead of an empty list, and
  the old count goes when no trophies are found.
- Players 2-4 of local co-op start only beside their own player 1.
- A failed section shows its own error, and a failed rebuild does not
  reuse the old page's "skip the online choice" group.
- Saving trophies, clearing the shader cache and deleting the maps say
  when a file could not be written or removed.
2026-10-20 13:13:58 +01:00
ayozetr 6eef9ed409 Online: Seamless Hunt by default, shadPS4's public server gone
srv.shadps4.net signs this port's builds out since 2026-10-12 (upstream
docs/BBSC.md), so it is no longer offered. Seamless Hunt is the default
server, and settings that chose the public server, or none yet, get it.

The public server's account is kept in online_accounts but not reused
on Seamless Hunt. An own server without an address now stops the game
from starting and shows in the Online status row. run.sh's default for
online play without the launcher is Seamless Hunt's server too.

The texts that sent players to the old Online page (the BBSC kit's help
line, the server refusal alert, the docs) now name Multiplayer.
2026-10-20 12:32:46 +01:00
ayozetr 2d243ff2ed Launcher: sections in a sidebar, Multiplayer with Online and Local
The pages were tabs in the header bar; with seven of them (nine now)
the names were cut on narrow windows and the everyday settings were
mixed with the ones set once. As asked in discussion #11, the launcher
now has a sidebar of sections, collapsed into a list on narrow windows
(the Steam Deck), with Play always in the header:

- Graphics: screen, upscaler, frame rate, game effects;
- Controls: the gamepad, then the keyboard and the mouse;
- Multiplayer: the Online and Local co-op pages as two tabs, Online
  starting with the account's status (off, no account, not checked,
  signed in or the reason it was not) and a Test button, and the
  play online / offline choice;
- Mods: the map, cheats, gameplay tweaks, mods, external patches,
  loading and smoothness;
- Trophies;
- Setup: the launcher's language, the game, saves and DLC folders,
  PKG install, language and region, Steam and Discord;
- Advanced: memory model, performance and developer options;
- Journal and About.

Local co-op also says which keys the keyboard player uses in menus.
Every setting keeps its row and its key; only where it is shown
changed. In the launcher's languages.
2026-10-20 12:10:48 +01:00
ayozetr 6029eced91 README: multiplayer with local co-op, the in-game map, support
The first feature is Multiplayer now: online play, Seamless Co-op and
local co-op (split screen). Hunter's Atlas gets its own line, and the
Support section says what goes into this fork beyond deadinside28's
port.
2026-10-20 02:06:03 +01:00
ayozetr a6bdb47739 Launcher: version 0.51-linux.14
The release with Hunter's Atlas (the in-game map, its maps prepared
from the launcher), local co-op, the controller that spun after
reconnecting (#20) and the settings the launcher wrote back (#21).
2026-10-20 01:08:06 +01:00
ayozetr d3632ba39a Launcher: Hunter's Atlas, its switch and its maps
Settings has a Map (Hunter's Atlas) group: the in-game map on or off
(off by default), lamp travel and items on the map, and the maps.

The maps are made from the player's own game files (they cannot be
shipped). Prepare (Remake once there are maps) runs
launcher/bbport_atlas_maps.py, with a progress bar of its steps and
Cancel:
- the first time it fetches uv 0.12.22 (its static musl build, checked
  against its SHA-256), CPython 3.12.15 and the generator's packages of
  tools/hunters-atlas/requirements.lock (exact versions with their
  hashes) into atlas-tools/ in the data folder: about 200 MB downloaded,
  about 600 MB on disk with the maps;
- it runs the generator, writes the names in the game's language and
  converts Atlas's art (tools/hunters-atlas/art: Atlas's source/art,
  its PNGs recompressed without loss by optipng, the same pixels,
  checked against their hashes) for the game;
- the maps are made in navigation-map.new and replace the old ones only
  once complete, keeping the map's settings.ini; a failed or cancelled
  run leaves the previous maps as they were, and the generator's work
  files are removed.
The bin button removes the maps and the tools. Nothing of it runs while
the game does, nor the game while the maps are made. uv and the
generator run without the package's Python and libraries.

On, the game gets BB_ATLAS_ENABLED=1 and the maps' paths; off or
without maps, BB_ATLAS_ENABLED=0 and none of the map's code runs. The
AppImage carries the generator and the art (tools/hunters-atlas).

Checked from scratch here: prepared in a new data folder, the maps are
byte for byte those already tested in the game; Remake from the
launcher reused the tools and kept settings.ini; Cancel during the
generation left the previous maps untouched. In the launcher's
languages.
2026-10-20 00:59:25 +01:00
ayozetr 50ec11bc74 Docs: Awakenchan's port is LanternEngine now
BloodBorneButPc was renamed LanternEngine: the credits link to its new
name, and the README recommends it for Windows next to Supermedo's port.
2026-10-20 00:27:30 +01:00
ayozetr be8478545b Credits: MikeTat1988's Hunter's Atlas
In docs/CREDITS.md (with Grimrukh's soulstruct, which its generator
runs with) and in the launcher's About page, in its languages.
2026-10-19 23:51:42 +01:00
ayozetr e0bbdbbfcc Hunter's Atlas: the game's own cursor sound
On a recording of the game's quick menu, every cursor move of the game
holds CURSOL_SELECT (0.0097 +- 0.0003 of the recording's scale, nothing
at other times) and never CURSOL_MOVE, which Atlas played; its timbre
was the difference the player heard. The map now plays CURSOL_SELECT
for its moves and confirmations, at the level measured against the
map's own sound in the same recording (gain 0.024).
2026-10-19 23:51:15 +01:00
ayozetr 3f1277f217 Hunter's Atlas: cursor sounds at the game's level
The map's cursor sounds were mixed at full level, several times louder
than the game's own menu cursor on a recording of both. They are mixed
at 0.07 now; BB_MAP_SOUND_GAIN (0-2) changes it.
2026-10-19 23:51:15 +01:00
ayozetr e00b4b6040 Hunter's Atlas: travel dialog selection and Alt+Tab
- 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 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.
2026-10-19 23:51:15 +01:00
ayozetr 311f09a1b8 Hunter's Atlas: the Map tile follows the game's quick menu
Measured frame by frame against recordings of the game's own menu:
- 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, so the tile stayed hidden until the menu
  was reopened; 0 with the quick menu's list alive counts as shown now;
- 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.

BB_ATLAS_DEBUG=1 logs what the map reads from the quick menu whenever it
changes, which is how the System case was found.
2026-10-19 23:51:15 +01:00
ayozetr f2ad0fdb25 Hunter's Atlas: Up and Down reach the Map tile like a native tile
The game knows of no tile above the fourth column, so Up from below the
Map tile landed on System, and Down from the Map tile went to the tile
below System. Up from below the Map tile now focuses it, and Down from
it reaches the tile below it (the game moves to the tile below System
and Right is pulsed until it gets there). Tested both ways, with System
and the tile below it keeping the game's own behavior.
2026-10-19 23:51:15 +01:00
ayozetr db9adde3f6 Hunter's Atlas: a test of the pad with the map on
tests/test_pad_atlas.c runs src/runtime_pad.c with BB_ATLAS_ENABLED=1
(tests/test_pad.c covers it off): buttons the tile or the map takes do
not reach the game, the game gets neutral input while the map is open,
and buttons and sticks still held when it closes stay hidden until
released, then reach the game when pressed again. Without the release
filter in sample_atlas the test fails at the closing step.
2026-10-19 23:51:15 +01:00
ayozetr dbeb093107 Hunter's Atlas: area, lamp and item names in the game's language
tools/hunters-atlas/localize_names.py runs after the generator, which
writes names in English and matches them by their English text (left
untouched). Each name of a pack that is one of the game's own English
texts (msg/engus, menu and item texts) is replaced by the text with the
same id in the chosen language folder, so the names are the game's own;
a name joined from several with " / " is translated part by part, and
one it cannot find stays in English. maps/generated-assets.json gets
the language and the new file hashes.

On the US release here: 778 names in Spanish (Sueño de cazador,
Distrito de la Catedral, Camposanto de Oedon, Vial de sangre), 790 in
Russian, 745 in French; the ones left as they were are the game's own
names in that language too (Byrgenwerth, Tonitrus, and Hemwick Charnel
Lane in French).
2026-10-19 23:51:15 +01:00
ayozetrandMikeTat1988 317c0d8a2c Hunter's Atlas: its map generator, running on Linux
The generator of Hunter's Atlas by MikeTat1988 (GPL-2.0-or-later), from
Atlas 1.3 (source/generator/src at 4ee4a0ed), in tools/hunters-atlas:
it reads the player's own game files and writes the map packs the
in-game map draws. Without Atlas's Windows packaging (PyInstaller,
generate_atlas_lite.py) and its copy of soulstruct (GPL-3.0-or-later),
which is installed from PyPI; requirements.txt keeps Atlas's pins.

One change for Linux: extract_verified_gates.py and
extract_subarea_names.py looked a parameter table up by the binder
entry's name, which soulstruct gives with the game's Windows path
(N:\SPRJ\...\ObjActParam.param); Path.name splits that only on
Windows, so the table was never found (StopIteration). The part after
the last \ or / is compared now. The files keep their CRLF endings.

Run on the US release here: 16 areas, 602 layers, 44 lamps, 48 ladders,
66 NPCs and 85 gates in about 4.5 minutes, the counts of Atlas's
Windows reference within 0.05 % of triangles and walls; this copy gives
byte-identical output to the generator run from Atlas's own sources.
README.md says how to run it.

Co-Authored-By: MikeTat1988 <[email protected]>
2026-10-19 23:51:15 +01:00
ayozetr 7a3ba74c81 Hunter's Atlas: Russian in the game's own map font
The map font the generator makes from the game's menu font keeps Latin
and Cyrillic glyphs (the extractor keeps U+0020-024F and U+0400-052F),
and every character of the map's Russian, Spanish, Italian, French,
German and Portuguese texts is in it, as checked against the font
generated here. Only Chinese still takes the port's font.
2026-10-19 23:51:15 +01:00
ayozetr 047895f8f9 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:

- 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, Озарение);
- in Russian and Chinese the map's titles use the port's font: the map
  font, made from the game's English menu font, has no such 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 still come from the generator in English. tests/test_atlas_i18n.cpp
checks the columns and the English fallback.
2026-10-19 23:51:15 +01:00
ayozetrandMikeTat1988 2f69cfaa37 Hunter's Atlas: the features test runs in build.sh too
test_atlas_features.cpp writes its own synthetic features.json into the
folder it is given, so it needs no game data after all (the last commit
said otherwise): it runs in build.sh --test with the other Atlas tests.
Only test_menu_sound.c needs the player's own sound bank; it decodes
sound/sprj_main.fsb of the US release here (3072 and 4096 frames).

Co-Authored-By: MikeTat1988 <[email protected]>
2026-10-19 23:51:15 +01:00
ayozetrandMikeTat1988 c0d40fe6d5 Hunter's Atlas: its tests
Hunter's Atlas's unit tests (MikeTat1988): the tile's state machine,
map styles, floor interiors and the controller shortcut run in
build.sh --test, without game data. test_atlas_features.cpp (a
generated features.json) and test_menu_sound.c (the game's own sound
bank) need the player's data, so they are kept for manual runs;
_putenv_s became setenv.

Co-Authored-By: MikeTat1988 <[email protected]>
2026-10-19 23:51:15 +01:00
ayozetrandMikeTat1988 e1e6c3e47c 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.

- gpu/shim/bbport_map*.cpp, bbport_atlas_*: the map screen (layers,
  floors, walls, lamps, markers, travel dialog), its art, features,
  tile and controller shortcut, as in Atlas. Its map packs are read
  with nlohmann/json, the copy bbbridge/third_party already carries.
- The overlay draws and ticks them, routes events and the pad to
  them, and loads the map's title font; the window lets the mouse go
  while the map or the tile uses it.
- runtime_pad.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).
- runtime_audio.c and runtime_menu_sound.c: the tile's cursor sounds,
  decoded on first use from the player's own sound/sprj_main.fsb with
  LibAtrac9, mixed into the main port, 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, unchanged.

Co-Authored-By: MikeTat1988 <[email protected]>
2026-10-19 23:51:15 +01:00
ayozetrandMikeTat1988 551b53ece0 Hunter's Atlas: the game-state readers and lamp travel on Linux
First part of Hunter's Atlas by MikeTat1988, a fullscreen in-game map
(GPL-2.0-or-later, github.com/MikeTat1988/HuntersAtlas): 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. On Linux the reads go through
  process_vm_readv, and /proc/self/mem where that fails (on AMD the
  game's memory is a GPU dma-buf); ReadProcessMemory stays for
  Windows.
- 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
  per-frame function and calls its own return-point and warp functions
  on the game thread. On Linux the lock is a pthread mutex and the
  hook is written while the loader still has the image writable, with
  no page protection change at run time.
- 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.

All nine sites checked match the 1.09 executable here. Atlas's tests
for both pieces run in build.sh --test, with a new case for travel on.
The map screen itself comes next.

Co-Authored-By: MikeTat1988 <[email protected]>
2026-10-19 23:51:04 +01:00
ayozetr ddd813258d Launcher: Local co-op page
A page to play local co-op without scripts: 2 to 4 players, the layout
for two (one above the other or side by side; three and four take the
quarters), each player's controller by device path (or the keyboard and
mouse), and sound for player 1 only. Play together starts player 1's
game with the launcher's settings, then the others 8 s apart; the
Journal marks their lines with [P2]..[P4], and they stop with player 1.

Players 2-4 get their own data folder (coop/player<N>: generated files,
saves, bbport.ini copied from player 1's the first time) and play
without an online account, Discord status or BBSC. The variables come
from bbport_coop.py, which has its own tests.
2026-10-19 23:51:00 +01:00
ayozetr 3b6eefc04b Local co-op: keep the sound while another player has the focus
With BB_PAD_BACKGROUND=1 one player's window is always out of focus,
so bbport.ini's mute_unfocused silenced player 1's game whenever
another player's window was active. It is ignored in local co-op;
players 2-4 are silenced with BB_AUDIO=none instead.
2026-10-19 23:51:00 +01:00
ayozetr b1d721ffb4 Gamepad list: the device path of each controller
bb-gpu-capabilities --gamepads prints each controller's device path as
a fourth column, and connected_gamepads(paths=True) returns it. Two
controllers of the same model share a GUID and a name; the path is what
tells them apart when each local co-op player gets one (BB_GAMEPAD
already takes a path).
2026-10-19 23:51:00 +01:00
ayozetr 5ece4d2aac Local co-op: a game per player, each with its own controller and window part
The groundwork for local co-op without PartyDeck: several games at once,
one per player, each with a HOME (or BB_DATA_DIR) of its own.

- BB_GAMEPAD also takes the device path, which tells two pads of the same
  model apart; the log's "gamepad connected" line shows it.
- BB_GAMEPAD_STRICT=1: only the chosen controller, never another
  player's (no fallback to the first one or the one in use).
- BB_KEYBOARD=0: a controller player's game ignores the keyboard and the
  mouse and never takes the mouse, which kept the other window from
  getting clicks and the focus.
- BB_PAD_BACKGROUND=1: the controller keeps driving its game while
  another player's window has the focus.
- BB_WINDOW_RECT=x,y,w,h: the window's part of the game's display, as
  fractions (the desktop's coordinates and scale differ from screen to
  screen), without borders; read whatever the locale. It needs X11:
  Wayland lets no window place itself.

Tried with two games on one PC: each in its half of the screen (2560x720,
32:9 with the ultrawide patch), the controller game took only its pad.
The keyboard game's input is the next thing to try.
2026-10-19 23:51:00 +01:00
ayozetr 737472975b Seamless co-op: name its kit items while it is off
BBSC names its five items (the Hunt, Kin, Bloodlust and Duel Bells and
the Parting Blank) in the game's text table in memory while it runs.
With Seamless co-op off they stayed in the inventory as "?GoodsName?".

A mod layer now writes their names and captions, BBSC's own in the
languages BBSC has them in (bbsc_kit_texts.json, made from
bbsc/translations), and a help line saying they need BBSC, into the
game's item texts. It fills only the slots the game leaves empty: on
the game's 22 item text archives every other text and file stays byte
for byte the same. With Seamless co-op on there is no layer and BBSC
shows its own texts as before. The layer is made once per game folder
(under a second) and is optional like the others.
2026-10-19 23:51:00 +01:00
ayozetr 9d3f43114f Launcher: keep bbport.ini changes made outside it (#21)
The launcher wrote every bbport.ini key it shows back to the file on
Play and on closing, with the values it had read when it opened. A
setting changed in the game's menu, or by hand, while the launcher was
open went back to the launcher's old value (resolution, upscaler
preset...), and keys it did not know were dropped.

Only the keys changed in the launcher since its page was built (or last
saved), the control bindings changed in it, and keys missing from the
file are written now; every other key keeps the file's current value.
Checked without a display: a file changed behind the launcher keeps its
values (main wrote them back), a launcher change is still saved, and a
new file still gets every key.
2026-10-19 23:51:00 +01:00
ayozetr 746d623518 Controller: a stick at the end of its travel is never its neutral
Issue #20: after the controller was disconnected and connected again
mid-game, the camera and the player spun towards the bottom right until
the game was restarted. Right after reconnecting, an 8BitDo Pro 2 (seen
as an Xbox One controller) reported -32768 on all four axes before its
first real report. The neutral measurement took that as a biased
neutral (the pattern of clone pads that rest at about +/-16380), so a
stick at rest then read as pushed all the way.

An axis at the end of its travel (30000 or more either way) is now
skipped like SDL's pre-report zeros, and is never taken as a biased
neutral; still there after the 3 s timeout, the neutral is 0 and the
log says so. A clone's real biased neutral is taken as before.

tests/test_pad.c feeds the reconnection pattern and then a stick at
rest (neutral 0, centred reading) and a clone's neutral; without the
fix the first check fails.
2026-10-19 23:51:00 +01:00
ayozetr 52c1aaf6fb Launcher: version 0.51-linux.13
The release with the fixes brought from upstream bbport 0.6, the
launcher that no longer stays blank and numbers with a decimal point
in every language.
2026-10-19 13:54:19 +01:00
ayozetr 6adcc2118f Numbers with a decimal point in every language (AppImage)
The AppImage 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 bbport.ini as "sharpness=0,30" and
"menu_pos=0,5000,0,3000" from the in-game menu (the launcher could not
read it and stayed blank: issue #18), and read "0.30" back as 0:
strtof and atof stop at the point. Builds run from the repository were
not affected; every AppImage log here has "59,6 FPS" where the
repository's have "60.0".

- bb-probe sets LC_NUMERIC to C at the start of main, after the
  preload's check: numbers use a point again, the rest of the user's
  locale stays.
- A single number written with a decimal comma by an earlier AppImage
  is read as written with a point (the game's settings and
  mouse_sensitivity), so those files keep their values.

tests/test_pad.c reads "1,5" and checks, under es_ES.UTF-8, that
strtof cuts "1.5" to 1 and that LC_NUMERIC=C reads it right; without
the comma handling it fails.
2026-10-19 13:54:06 +01:00
ayozetr 32d61dbcf5 Launcher: read a decimal comma in bbport.ini as a point
Earlier AppImages wrote bbport.ini from the in-game menu in the user's
language: "sharpness=0,30" in Spanish or German (see the bb-probe
locale fix). Such a single number now reads as written with a point,
so those files keep their values instead of falling back to the
default, and the next save writes them with a point.

Checked with sharpness=0,30 and mouse_sensitivity=1,50: the window
shows 0.30 and 1.50 and saves them as 0.30 and 1.50.
2026-10-19 13:54:06 +01:00
ayozetr baa18403db Launcher: a saved value it cannot read no longer leaves it blank
Issue #18: the launcher's window stayed blank for good, with an
unlabelled Play button. An error while building a page (a saved
setting of the wrong type: fps_limit "abc" in settings.json gives
"TypeError: Must be number, not str") stopped build() before any page
was added, and it happened again on every start.

- Numbers read from bbport.ini (fov, mouse_sensitivity, preset,
  sharpness) and settings.json (fps_limit, online_p2p_port, language)
  that are not numbers keep the default, with a line on stderr; the
  next save writes the default back.
- A page that still fails to build shows its error, with a Copy
  button, in its place; the other pages are built. Play is then off
  and nothing is saved (store() returns), so a half-built window never
  writes the user's files or starts the game. The error is in the
  Journal and on stderr too.

Checked with temporary settings: unreadable values open normally; a
forced failure in the settings, online or cheats page shows the error,
keeps both files byte for byte, and survives a rebuild (language
change). With copies of real settings, main and this write the same
files.
2026-10-19 13:54:06 +01:00
ayozetr 7e396e963f Guest hooks: install the game fixes once the image is relocated
The fixes for the game's own bugs were installed when the GPU started,
from the buffer cache. That is before the loader writes the image's
relocations and patches, and writing a jump left its code page
read-only and executable: the loader's next relocation in that page
(0xbbc40f, next to the blur kernel clamp) faulted at start (signal 11
in memcpy, from bb-probe's main).

They are now installed from bbgpu_patch_image, with the libGnm and
menu patches: the image is relocated and patched, still writable, and
no game thread runs yet. The jump is written as plain bytes and the
loader sets each segment's protection right after. The bytes checked
are those after relocation and patches, so a patch at a site leaves
that fix out. No BBSC hook is within 16 bytes of these sites.
2026-10-19 12:36:33 +01:00
ayozetr 7eb725dd94 Credits: axpico for waiting on evicted textures with full VRAM
axpico's upstream PR #206 has the game wait for the textures the
collector evicted instead of closing when VRAM is full: a line in the
launcher's About page in every language and in docs/CREDITS.md.
2026-10-19 11:36:21 +01:00
ayozetranddeadinside 3b65ad3f32 FSR 4.1.1 capture: GE-Proton from a package with another folder name
GE-Proton was found only in folders named GE-Proton*. A package can
name it otherwise (the AUR's proton-ge-custom installs to
/usr/share/steam/compatibilitytools.d/proton-ge-custom, upstream issue
#186): such a folder's `version` file names the build now. The list of
candidates here is the same as before.

From upstream bbport 0.6.

Co-Authored-By: deadinside <[email protected]>
2026-10-19 11:35:40 +01:00
ayozetranddeadinside 4bb9dc0853 Launcher: NVIDIA's libraries also from the host's library folders
The libraries NVIDIA's driver loads (libnvidia-*, libcuda) were looked
for only next to its ICD. Debian 13 puts the ICD in nvidia/current/ and
libnvidia-glsi, -tls and -glcore one level up, so the game fell back to
another GPU (upstream issue #205). The ICD's own folder still comes
first, then the host's library folders; the first file of each name
wins. Here (CachyOS) the 94 libraries linked are the same as before.

From upstream bbport 0.6.

Co-Authored-By: deadinside <[email protected]>
2026-10-19 11:35:40 +01:00
ayozetranddeadinside bb49ffe06f GPU: end-of-pipe labels by the CPU on Polaris
On AMD GCN 3/4 (GFX8: Polaris, RX 4xx/5xx), the game stalled with
end-of-pipe labels written by the GPU (upstream issue #203, an RX 580:
nothing submitted for 5 s after the first cinematic, or a guest fault
at character creation). There they are now written by a CPU thread
once the GPU is done, as BB_GPU_LABELS=0 did; BB_GPU_LABELS=1 forces the
GPU. RADV names the chip in the device name; other drivers are left as
they were. Not verified on such a GPU yet.

From upstream bbport 0.6.

Co-Authored-By: deadinside <[email protected]>
2026-10-19 11:35:40 +01:00
ayozetrandaxpico ddce44bbf7 Guest memory: wait for evicted textures before failing on full VRAM
With VRAM full, a copy of the game's memory that found no room stopped
the game ("Failed to allocate ... VRAM is full"), and a failed
system-memory fallback did too, through Vulkan::Check. Images the
texture collector evicted are destroyed only once the GPU is past them,
so their VRAM was still taken at that moment.

- A failed system-memory allocation now gives none, like a failed VRAM
  one, and the caller goes on to its other attempts.
- Before giving up, it waits for the submitted work (not the one being
  recorded), runs the pending deletions and tries again.

Only the paths that ended the game change. From upstream PR #206.

Co-Authored-By: axpico <[email protected]>
2026-10-19 11:35:40 +01:00
ayozetranddeadinside 0521a560d0 FSR 4.1.1: no white picture after loading another save
- On a reset (a scene change: another save loaded, a teleport), FSR
  4.1.1's temporal buffers are cleared and the previous exposure is
  dropped; what the last scene left there gave a white or broken
  picture.
- A WRITE_DATA longer than 8 bytes can initialize texture memory (the
  effects' private history clears): an image cached there is now
  uploaded again from the new contents. Label and fence writes keep
  their fast path.

From upstream bbport 0.6.

Co-Authored-By: deadinside <[email protected]>
2026-10-19 11:32:08 +01:00
ayozetranddeadinside 087f0516b1 Guest hooks: fixes for the game's own bugs, with either memory model
Hooks in the game's code were installed only with the PC memory model,
the default on AMD alone. Fixes for bugs of the game's own now have an
install step of their own (the profile's install_fixes), run with
either model before the game's threads start; the memory hooks are
unchanged. The idea comes from upstream PR #214 (whp199).

- Render job slots (BB_JOB_SLOT_FIX): a job that fails kept the slot it
  took from the game's pool. Above 30 FPS Laurence's cinematic fails
  enough of them to empty the pool, and the game then waits for a slot
  for good. The slot is now given back at the job's exit, and when its
  runner is destroyed or reset holding one.
- Blur kernel (BB_KERNEL_CLAMP): game data now and then gives a base
  sample count of 2 where the game asserts at least 3 (exception
  0xa0020007, upstream issue #207). It is raised to 3 at the entry.

Every site is checked byte for byte first (verified against the 1.09
executable); BB_GUEST_HOOKS=0 still turns everything off.

From upstream bbport 0.6.

Co-Authored-By: deadinside <[email protected]>
2026-10-19 11:32:08 +01:00
ayozetranddeadinside fa9e9bc6ff Shaders: a failed table read gives zero for that draw only
When the SRT walker read unmapped memory (a stale pointer in a shader
resource table), the fault handler patched the shared MOV into an XOR
for good. Draw-preparation workers run the same walker, so the patch
raced with other threads' handlers, and every later draw through that
walker read zero there even when the memory held valid data again.

The handler now sets the register to zero in the faulting thread's
context and resumes after the instruction; the code is never changed.
The first faults and then every power of two are logged.

From upstream bbport 0.6.

Co-Authored-By: deadinside <[email protected]>
2026-10-19 11:32:08 +01:00
ayozetranddeadinside a9fd9e429b GPU: submission waits cover the frame's first reader
The second wait of a submission waited only at the color attachment
output stage. A prepared frame is first read by the present blit (a
transfer) and by diagnostic dumps, so those reads could race the work
that produces the frame: single black or grey frames. Every wait now
covers all commands.

From upstream bbport 0.6.

Co-Authored-By: deadinside <[email protected]>
2026-10-19 11:32:08 +01:00
ayozetr 6d8abf14a7 Launcher: Copy and Export take the whole session's log
The Journal keeps only its last 5000 lines on screen, and Copy and
Export saved just those, so a long session lost its start (issue #17:
the failed co-op attempts were cut). Every line the Journal gets is
now also written to a temporary file for the session, and Copy and
Export take that. The screen keeps its limit. If the file cannot be
written, they take what the Journal shows, as before.
2026-10-19 00:00:14 +01:00
ayozetranddeadinside 6d409904ff FSR 4.1.1 capture: the FP8 passes on an RX 9000 after a spoofed id
When AMD's DLL did not offer FSR 4.1 to the GPU's id under the Proton
build, the capture asked again under an RX 7800 XT's id. On an RX 9000
the FP8 variant was then asked for under that id too, which gets no FP8
passes (deadinside28#186). On such a PC the FP8 variant is now asked
for under an RX 9070 XT's id, and the id is restored afterwards. Other
GPUs see no change.

From upstream bbport 0.6-pre1.

Co-Authored-By: deadinside <[email protected]>
2026-10-18 18:20:49 +01:00
ayozetranddeadinside 5fc3797c0e Memory model: blocks reloaded or unmapped start over (FPS aging)
With the layer memory module, a block the GPU once wrote in place stayed
"dynamic" (bound in place, read over the bus) for good. As areas loaded
over memory the last one's GPU buffers had used, such blocks piled up
(164 -> 1117 MiB after a few teleports upstream), and one in a range
kept the whole range in place: the frame rate fell the longer one
played, about 28 % after 17 minutes of lamp travel.

- A dynamic block the loaders then write whole may have a VRAM copy
  again; the GPU's writes there stay in place from then on, so the CPU's
  view is never left behind. Blocks the CPU writes often stay dynamic.
  BB_LAYER_RELOAD_RESET=0: as before.
- Blocks the game unmaps whole lose what kept them in place (CPU writes,
  GPU writes in place, failed watches); memory mapped there again
  starts as new. BB_LAYER_UNMAP_RESET=0: as before.

From upstream bbport 0.6-pre1.

Co-Authored-By: deadinside <[email protected]>
2026-10-18 18:20:15 +01:00
ayozetranddeadinside 5ad6bb0d00 Pipeline cache: stages loaded from disk and draws without a pixel shader
Three stale-state bugs around the pipeline cache:
- A pipeline with a geometry stage loaded from the disk cache got no
  fetch shader: the geometry stage, loaded after the vertex stage,
  overwrote it with none. Only a stage that has one sets it now, and a
  pipeline that fails to load leaves no stage behind for the next.
- Clip distance emulation's discard shader (NVIDIA) was never saved, so
  a pipeline without a pixel shader loaded from disk rebuilt it from
  whatever fragment stage the previous pipeline left. It is saved now
  (entries written before keep loading as they did).
- A draw without a pixel shader used the previous draw's fragment
  runtime info; it is now made from this draw's registers.

From upstream bbport 0.6-pre1.

Co-Authored-By: deadinside <[email protected]>
2026-10-18 18:18:56 +01:00
ayozetranddeadinside 47965c6a6b Memory model: unprotect a trapped block under the trap lock
The write trap handler took the block out of the armed set under the
lock, then unprotected it after releasing the lock. A trap armed again
for that block in between (LayerArmTraps, same lock) then lost its
protection: the game's writes went unnoticed and the block's VRAM copy
went stale, seen as "exploding" vertices. The block is now unprotected
before the lock is released.

From upstream bbport 0.6-pre1.

Co-Authored-By: deadinside <[email protected]>
2026-10-18 18:17:28 +01:00
ayozetranddeadinside f2efa0a227 GPU: an extension whose commands do not resolve counts as missing
Some drivers list and enable an extension but resolve its commands to
null (NVIDIA 595, Intel ANV): calling them jumped to address 0 (RIP=0
crashes, deadinside28#161, #177, #181). After the device is made, the
buffer marker, vertex input, dynamic state 3, memory export and host
memory import extensions are turned off when their commands are not
resolved, with a line in the log, and a missing push descriptor command
stops with a clear message instead. Drivers that resolve them all, as
most do, see no change.

From upstream bbport 0.6-pre1.

Co-Authored-By: deadinside <[email protected]>
2026-10-18 18:16:55 +01:00
ayozetranddeadinside e4729de4a3 Runtime: a clean stop when the game raises its own exception
When the game stopped itself (its debug exception), the runtime called
exit, which ran the static destructors while other threads (the online
module's) were still running, and the process ended with "terminate
called ..." instead of the report. It now flushes and calls _exit, and
the log says where it was called from and the game's return addresses
up the stack, to tell which check failed.

From upstream bbport 0.6-pre1.

Co-Authored-By: deadinside <[email protected]>
2026-10-18 18:16:17 +01:00
ayozetranddeadinside edbcdab361 run.sh: keep the AppImage's fd 3 when saving the log
With "Save the log" on, run.sh opened the log file on fd 3. The AppImage
runtime keeps its mount open on an inherited fd 3, so closing it could
unmount the image under the game ("Software caused connection abort"),
seen with --play. The log now goes to a free descriptor.

From upstream bbport 0.6-pre1.

Co-Authored-By: deadinside <[email protected]>
2026-10-18 18:15:43 +01:00
ayozetr 784638ed69 Credits: msisovic for the lamp travel loading hang
msisovic found and diagnosed why loading hung after a lamp travel (guest
thread handles above 40 bits): a line in the launcher's About page in
every language and in docs/CREDITS.md.
2026-10-18 18:15:14 +01:00
ayozetrandSupermedo 82b85856f8 Mods: more folder layouts load, and folders that do not say why
A mod folder whose layout was not recognised was left out of the list
without a word. Now accepted as well:
- notes without a suffix (README), previews (.webp, .gif, .pdf) and .url
  links beside the wrapper folder;
- up to three wrapper folders (Mod/Mod v2/dvdroot_ps4);
- the game's folders next to others (Screenshots, Optional), which, like
  notes beside them, stay out of the game.
Folders that are still not mods are listed under the mods in the
launcher with the reason (still an archive, several mods in one folder,
no game files, empty), in every language. Mods that loaded before load
the same files.

Adapted from Supermedo's port.

Co-Authored-By: Supermedo <[email protected]>
2026-10-18 18:14:23 +01:00
ayozetrandEthan Murphy 24248b0188 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 switch under Screen in the
launcher and a checkbox in the in-game menu, in every language.

Adapted from Supermedo's port.

Co-Authored-By: Ethan Murphy <[email protected]>
2026-10-18 18:12:24 +01:00
ayozetrandEthan Murphy a0b5d6e1aa Field of view setting
bbport.ini fov (30-90 degrees; unset or 43, the game's, changes nothing)
scales camFovY in every row of the game's camera table (LockCamParam in
gameparam.parambnd.dcx) by fov / 43, so areas with their own camera keep
their difference. scripts/fov.py writes the archive as a layer under the
mods (a mod's own camera table wins), kept between starts while the
setting and the game file match; run.sh adds it. A row under Screen in
the launcher, in every language.

Checked on the game's own file: its 72 cameras (43 and 48 degrees) are
scaled and the other 64 tables of the archive stay byte for byte.

Adapted from Supermedo's port.

Co-Authored-By: Ethan Murphy <[email protected]>
2026-10-18 18:10:07 +01:00
ayozetrandmsisovic fb6b45239f Threads: keep guest thread handles below 40 bits (lamp travel hang)
A guest thread's record is its ScePthread handle, and the game packs
handles into 40-bit fields: its hybrid lock masks the owner to 40 bits
and then compares it with the whole handle. Records came from calloc,
whose addresses are above 40 bits, so a thread could fail its own owner
check and sleep on its semaphore forever: lamp travel hung at a loading
screen now and then.

Records now come from low memory (below 40 bits, zeroed), carved out of
256 KiB pieces and never freed, as before; calloc stays the fallback,
with a warning. The new thread-handles test fails without the change.

Found and diagnosed by msisovic (deadinside28/bloodborne_pc PR #193),
as upstream bbport 0.6 takes it.

Co-Authored-By: msisovic <[email protected]>
2026-10-18 18:06:01 +01:00
ayozetrandSupermedo 6558ee508d GPU: the driver's fault report in the log on device lost
A lost device gave only the breadcrumbs (where they are available) and
no word from the driver. With VK_EXT_device_fault, enabled where the
driver offers it, the log now shows the driver's fault description, the
faulting addresses and the vendor's fault codes before the game stops,
also with the breadcrumbs off. For NVIDIA freezes such as #5.

Adapted from Supermedo's port.

Co-Authored-By: Supermedo <[email protected]>
2026-10-18 18:03:13 +01:00
ayozetrandEthan Murphy d5370741b2 Mouse camera: turn the camera in the game's camera update
The direct mouse camera wrote the camera's angles from the window thread
at moments unrelated to the game's frames, so a frame could get more or
less of the turn than the time it showed: a steady pan juddered by a
pixel now and then.

The window thread now only gathers the motion. The camera hook calls
into the port from the game's camera update, just before the game loads
the angles, and turns the camera there by the motion since the last
update. The stub skips the game's red zone and saves what the call may
change: the caller-saved registers, ymm0-15 whole (the game's code is
AVX) and the flags. Motion gathered while the camera is not updated (a
loading screen) is not saved up, and lock-on is checked every update.

Adapted for Linux from Supermedo's port.

Co-Authored-By: Ethan Murphy <[email protected]>
2026-10-18 18:00:04 +01:00
ayozetr eb75881360 Credits: HatsyRei for the keyboard walk key
The walk key comes from HatsyRei's work in Mrsuss60's port: a line in
the launcher's About page in every language and in docs/CREDITS.md.
2026-10-18 17:48:40 +01:00
ayozetrand0xCydral a2fd00134c Controls: after a menu, hide only the buttons still held
When the in-game menu or the text box closed, every button stayed hidden
until all were released, so that the one that closed it (the Cross that
accepted a name) was not taken as a new press. A button pressed after
the menu closed was ignored as well while another was still held.

Now only the buttons held through the menu are hidden, each until it is
let go; one pressed afterwards shows at once. The triggers' analog value
is hidden with their button, as before.

Adapted from 0xCydral's port.

Co-Authored-By: 0xCydral <[email protected]>
2026-10-18 17:47:44 +01:00
ayozetr 3f71604f25 GPU: free the command processor's finished indirect buffer tasks
Each indirect buffer runs in a coroutine that stays suspended when it
finishes (final_suspend), and nothing destroyed it: every indirect
buffer of every frame leaked its frame, ~600 bytes, so the process grew
for as long as the game ran. The graphics, constant and compute
indirect buffer tasks (and the self-test's) are now destroyed once
done. Only the top-level tasks are kept anywhere else, and those were
already destroyed.

From upstream shadPS4 (12c5978f), as Mrsuss60's port brought it.
2026-10-18 17:45:09 +01:00
ayozetrandHatsyRei e62439bb50 Keyboard: hold Left Alt to walk
The move keys always pushed the stick all the way, so the character
could only run. Held with them, the new walk input keeps the stick at
48 of 127, under the game's run threshold; a diagonal keeps its shape.
It is key.walk in bbport.ini (Left Alt by default) and a row in the
launcher's Controls page, keyboard only.

Adapted from HatsyRei's walk key in Mrsuss60's port.

Co-Authored-By: HatsyRei <[email protected]>
2026-10-18 17:43:22 +01:00
ayozetrandEthan Murphy 4e42c0d5fd Patches: an external FPS patch replaces the built-in preset
shadPS4's patch file has frame rate patches of its own ("120 FPS++",
"60FPS++ (no deltatime)") that write at the built-in preset's addresses
with other values. Chosen in the launcher, they were applied over the
preset and the two mixed.

An external patch named FPS that writes over the preset now replaces it
whole, and the log says so. Only one named FPS does: "Smaller Hunter"
writes there too and would otherwise leave the game at 30 FPS. Checked
with the real executable and shadPS4's file: Smaller Hunter keeps the
preset, 120 FPS++ replaces it, nothing chosen gives the same writes.

Adapted from Supermedo's port.

Co-Authored-By: Ethan Murphy <[email protected]>
2026-10-18 17:40:01 +01:00
ayozetrandEthan Murphy 7bacb0c2bd Texture cache: room for textures on cards below 8 GB
On dedicated GPUs below 8 GB the pressure mark was 40% of the budget
(2.4 GB on a 6 GB card), about what the game holds outside textures.
Such cards were under pressure all session, evicting textures unused
for 5 s and uploading them again when the camera turned back.

The pressure mark is now at least 60% of the budget there
(BB_GC_PRESSURE_PERCENT, 10-75), never above the critical mark, which
is unchanged. Cards of 8 GB or more and integrated GPUs keep their
marks. Adapted from Supermedo's port, where it cut evictions on a test
route from 13528 to 1398 with the same frame rate; not tested here on
such a card.

Co-Authored-By: Ethan Murphy <[email protected]>
2026-10-18 17:32:35 +01:00
ayozetr edc3774dc4 Credits: thank the people behind the latest reports
The reports and logs behind the fixes in 0.51-linux.11 and
0.51-linux.12 (#10, #12 to #16) go under Help and testing, in the
launcher's About page in every language and in docs/CREDITS.md.
2026-10-18 16:51:55 +01:00
ayozetr 1d5ddb7c78 Launcher: version 0.51-linux.12 2026-10-18 16:46:37 +01:00
ayozetr 72b69acf22 Write GPU-written tiled images back instead of dropping them
Past the critical memory mark the texture cache collector dropped
GPU-written tiled images that had not been used for a minute, without
writing them back. When the game came back to an area whose textures
it had rendered itself (the Hunter's Dream), it found that memory empty
and drew it black.

The collector now tiles those images back into the game's memory before
freeing them, the same way linear images were already written back.
Multisampled images are still dropped. BB_GC_TILE_BACK=0 restores the
old behaviour for diagnosis.

Fixes #13, #14.
2026-10-18 16:30:45 +01:00
ayozetr ca77ff8afd Steam Input: open the window through Xwayland on Wayland desktops
Started from Steam with Steam Input on, the game opened Steam's virtual
controller but got no input from it (#12). Steam sends that input only
to the window it sees focused, and on a Wayland desktop it sees only X11
windows, so the native Wayland window never counted as the game.

run.sh now sets SDL_VIDEO_DRIVER=x11 when Steam made a virtual
controller for the game (SteamVirtualGamepadInfo lists one), the desktop
is Wayland with Xwayland and SDL_VIDEO_DRIVER is not set already.
BB_STEAM_INPUT_X11=0 keeps Wayland. Anywhere else nothing changes.

TROUBLESHOOTING.md gives the launch options for older releases.
2026-10-18 12:47:19 +01:00
ayozetr 433f95dc69 Third-party patches: read shadPS4's Bloodborne.xml
Any file named Bloodborne.xml in the patches folder was skipped, so that
an older package's copy of the built-in file could not apply its
frame-rate patches over the port's. shadPS4's patch file has the same
name and was skipped too, without a word (#16).

Such a file is now read. Its patches named as built-in ones (any case)
are left out, and the rest start off until chosen in the launcher,
whatever the file says. Every past version of the built-in file still
adds nothing; shadPS4's file lists 30 patches, all off.
2026-10-18 12:19:13 +01:00
ayozetr 8464ac3f35 Quit to Desktop closes at the title again with online play on
Quit to Desktop from the System menu closes the game once the title
screen is built, which its code learns from the Play Online / Play
Offline label. Skip Online/Offline Choice: Online skips that dialog, so
the game signed in first and waited for the server's welcome message to
be dismissed before closing (#15).

The patch now calls the web browser import with bb:t before it answers
PLAY ONLINE, as the label does.
2026-10-18 12:13:15 +01:00
ayozetr 7b791c660b Launcher: version 0.51-linux.11 2026-10-18 00:39:38 +01:00
ayozetr bdb08e9898 Patches: start online at the title when online play is on
Skip Online/Offline Choice answered 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 (issue #9). With online
play on, 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, the
old patch stays. The launcher's switch and the README say so.
2026-10-18 00:39:13 +01:00
ayozetr 5deacbd471 Launcher: the game region also picks the game's server
The region decides the address of the game's server as well as the text
of regional languages: without one, Log In found no server (issue #9).
Its description in the launcher now says so.
2026-10-18 00:39:13 +01:00
ayozetr 99c49c4b6a Launcher: Seamless Hunt as a co-op server choice
Seamless Hunt, BBSC's community server with players on the Windows port,
took three addresses typed by hand, and a missing port or http:// broke
it (issue #9). It is now a choice next to the public server, your own
and The Hunter's Dream: its shadNet server, WebAPI and game server come
with it, it keeps its own account, and Create makes one there. README and
docs/ONLINE.md say how, and why its bloodstains are off.
2026-10-18 00:39:05 +01:00
ayozetr f0ad983381 Launcher: create an account on a private shadNet server
A private server such as Seamless Hunt had no way to make an account on
Linux: its tool is a Windows program. The account group gets a Create
button for your own server, which registers the name and password typed
above (shadNet's Create command, as shadnet-sample's register). A server
wants one email per account; one is made from the name, as Seamless
Hunt's tool does. The connection test and the new request share one
shadNet exchange (shadnet_request).
2026-10-18 00:24:44 +01:00
ayozetr da1d2ccfc5 Online: Seamless Hunt's bloodstains off by default
Seamless Hunt's bloodstains and death visions (tomb_messenger/message_area)
crash the game with "Transaction failed" on Windows and Linux alike, and
its own BBSC package sends those two requests nowhere by default (issue
#10). The overrides for that server now do the same, so they fail and the
game goes on without them; floor messages, ghosts and summon signs are
untouched. A switch under the game server in the launcher brings them
back.
2026-10-18 00:24:44 +01:00
ayozetr 15f15314cc Launcher: version 0.51-linux.10 2026-10-17 16:15:19 +01:00
ayozetr e5fc8e068a Content: a dump without a region gets Europe
A dump whose param.sfo has no region (USER_DEFINED_PARAM_1 = 0) left the
game without an address for its server: Log In on the title screen failed
with "Unable to obtain Bloodborne server information", whatever game
server was set (issue #9). With no region chosen in the launcher, such a
dump now gets Europe; both regions log in on The Hunter's Dream and
seamlesshunt.
2026-10-17 16:15:14 +01:00
ayozetr 97adb6d8c0 Overlay: the on-screen keyboard and menu follow A and B swapped
The on-screen keyboard typed on the south button and cancelled on the
east one, and the port's menu accepted and went back the same way,
whatever bbport.ini bound to cross and circle. With A and B swapped for
a Nintendo-style controller the game confirmed on A and the keyboard on
B. The runtime now tells the overlay which button cross and circle are
on (runtime_pad_button), and both follow it.
2026-10-17 16:15:14 +01:00
ayozetr 5323911239 Online: a game server typed without http:// keeps working
A game server address without a scheme was taken as https, and servers
that answer the game in plain http on port 20443 (seamlesshunt's) then
failed the TLS handshake. Such an address now gets whatever port 20443
speaks.
2026-10-17 15:59:07 +01:00
ayozetr d38059932a bbsc: the co-op kit comes on a first start with no saves
BBSC_SAVE_DIR was only set from an existing save folder, so a first start
had none and the kit was never given. The runtime now exports the saves'
title id (BB_SAVEDATA_TITLE), and the mod uses the folder the game will
write: savedata/1/<title id>/SPRJ0005.
2026-10-17 15:59:07 +01:00
ayozetr 3e7face701 bbsc: the bell trace records the kit's bells too
BBSC's trace of the responder bell check only kept the vanilla Small
Resonant and Sinister Resonant Bells (205, 225), which the kit retires:
the Kin and Bloodlust Bells (4121, 11201) that replace them were never
recorded, so a greyed-out Kin Bell (issue #9) left nothing to look at.
It now records them as well; that is how the Log In on the title screen
turned out to be the cause.
2026-10-17 11:40:13 +01:00
ayozetr 2c0c4c35dc README: online play may need Log In on the title screen
The game here starts without its own online session, although the port
tells it it is signed in: the session only starts after Log In on the
title screen, and until then the Kin and Bloodlust Bells are greyed out
(issue #9). Said where online play is set up.
2026-10-17 11:40:13 +01:00
ayozetr 7f0513cb32 bbsc: find the item tables in AMD's GPU memory
On AMD the port keeps the game's memory in a GPU dma-buf (the new
memory model), and process_vm_readv cannot read a driver's mapping:
BBSC's search for the item tables found nothing ("0 of 0 table(s)")
the whole session, so the co-op kit never came and the scaling had no
tables either. A slice that process_vm_readv cannot read is now read
again through /proc/self/mem, which goes through the driver's own
access.

Reported in #9 (holydingus, RX 9070 XT); BB_PC_MODEL=0, the older
memory model, brought the items there.
2026-10-17 02:55:08 +01:00
ayozetr 88b990746d Launcher: mark trophies earned on a PS4 or in another version of the port
A player who carries a save over from a PS4 already has some trophies,
and the game only records the ones it unlocks here. The Trophies page's
menu (⋮, "Mark trophies") shows a check per trophy; Save writes the
checked ones to earned.tsv with a third column, "manual", and takes back
the marks unchecked. The ones the game unlocked stay, and the platinum
follows the others as the runtime awards it. The game reads earned.tsv
when it starts, so it doesn't announce a marked trophy again; with the
game running the page doesn't edit it.

Requested in #8 (TacticalBill).
2026-10-16 23:49:42 +01:00
ayozetr caa9b4b17b 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 (the modules'
initializers), 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), which fixed similar issues there.
2026-10-16 10:56:00 +01:00
ayozetr f83bc7c567 Launcher: version 0.51-linux.9 2026-10-16 10:18:27 +01:00
ayozetr 5aa78796d2 bbsc: the co-op kit still comes when the character loads after 3 minutes
BBSC looks for the game's item tables to make its kit (Hunt Bell, Kin
Bell, Parting Blank, Duel Bell), and they only appear once a character
is loaded; its search gave up 3 minutes after the start. A player who
stayed longer on the title screen (a first start compiling shaders)
never got the kit, while the vanilla bells already said "No longer
used. Use the Hunt Bell". It now keeps looking, every 5 s for those 3
minutes and every 15 s after.

Reported on r/BloodbornePC (hcaoRRoach).
2026-10-16 09:53:30 +01:00
ayozetr 0c6f49a6f2 Launcher: 16:9 on the usual output sizes, icons and a clearer update check
- The usual output sizes, 720p to 4K, say (16:9), as the ultrawide ones
  say 21:9 or 32:9, in the launcher, the in-game menu and the game's
  System menu. The launcher's 720p drops "(Steam Deck)", which did not fit
  in the list; the row's subtitle already says the Steam Deck is 720p.
- The Updates group's rows have icons, like the rest of the About page.
- An update check that cannot reach GitHub (a connection not up yet when
  the launcher opens) tries again 5 s later, and then says there is no
  connection to GitHub instead of "[Errno -5] No address associated with
  hostname".
2026-10-16 01:17:26 +01:00
ayozetr 8feadda4fb Docs: credits for ultrawide, Quit to Desktop and the keyboard sprint 2026-10-16 00:18:00 +01:00
ayozetrandEthan Murphy 1497357b6b Menus: Quit to Desktop in the title and System menus
The PC builds' title-menu and System-menu entries that the console game hides
(Opinion Box, Language) are shown as Quit to Desktop (bbport.ini
quit_to_desktop, on by default).

- scripts/quit_menu.py makes a mod layer with the entries' texts in the
  game's menu text archives (22 languages; the "japanese" folder takes the
  Japanese texts) and six-row title and System menu movies. run.sh lays it
  after the button prompts and before the ultrawide HUD layout, which builds
  its menu movies on it, all under the mods. The layer is made once per game
  folder and reused while the game files and the scripts are the same.
- patches.py adds the menu code to patches.bin after checking the 01.09
  bytes it replaces. It is left out when the layer is missing (run.sh passes
  --quit-to-desktop 0), and when another enabled patch writes any of the same
  bytes: a third-party "Exit Game (title menu)" patch is kept then.
- The menus signal the runtime through sceSystemServiceLaunchWebBrowser
  (runtime_services.c). "bb:q" (title menu) closes the game. "bb:p" and
  "bb:d" (System menu, then Exit Game's confirmation, answered 1 so it shows
  the Quit to Desktop question) arm a quit after the game's own Exit Game, so
  the game saves first: it closes when a menu/logo_ file opens with no save
  data mounted, after the last save unmounts (runtime_savedata.c), or when
  the title screen is built ("bb:t"). An unanswered arm lapses after two
  minutes. Closing pushes SDL_EVENT_QUIT, the window's own close: the window
  thread ends the process with exit code 0.
- Optional at start: when the layer's files cannot be made, the game starts
  without the entries (quit_menu.py prints an empty layer, as hud_layout.py
  does).
- The switch is in the launcher (Settings, Game effects) and the overlay
  menu, in every language.

docs/TROUBLESHOOTING.md describes the entries, what the log says when they
are left out, the third-party patch and mods that replace the menu texts.
Tests: tests/test_quit_menu.py (code writes, original bytes, the texts in
the archives, the layer) and the browser messages and the quit they cause in
test_runtime.c.

From Supermedo/bloodborne_pc#70 (emurphy04), with Quit to Desktop optional
at start as in Supermedo's 9461312. Thanks: emurphy04, in 7 languages.

Co-authored-by: Ethan Murphy <[email protected]>
2026-10-16 00:17:51 +01:00
079392a447 Ultrawide: patch values computed for the output's aspect, a wider HUD canvas and the HUD position
Replaces the patch selection of 8d1db30, which applied Kyo's 21:9 or 32:9
patch from patches/Bloodborne.xml as it was: his 32:9 patches write a HUD
canvas and projection 3840 or 5120 wide against 1080 high (a squeezed HUD),
3840x1600 had no patch at all, and the HUD canvas they widen did not match the
1920x1080 the port drew the UI with.

- patches.py ultrawide_writes computes the values of Kyo's 21:9 patch for any
  output wider than 16:9: the camera's aspect ratio, the HUD canvas width (the
  `mov e?x, 2560` sites), the HUD projection width and the screen-to-HUD
  mapping of the lock-on and HP-bar markers. At 2560x1080 it is byte-identical
  to his patch. The HUD canvas is 1080 high and as wide as the output's aspect
  (hud_canvas: 2580x1080 for 3440x1440, 3840x1080 for 32:9); patches.py writes
  it as the UI size, and run.sh passes it to the renderer as BB_UI_RES.
- bbport.ini aspect_patch=0 still turns it off (the 16:9 picture stretched),
  and custom output_res sizes keep working, now with their own aspect too.
- UiComposition::Canvas replaces the fixed 1920x1080 in NativeViewport and
  Scale. A scene of exactly the canvas size (2560x1080 or 3840x1080 at Native
  AA) is drawn 4x2 pixels smaller, as 1920x1080 at 4K Performance already was:
  the UI composition tells UI draws apart by that size.
- Menu-only frames (title, loading) get black columns beside the 16:9 stage
  instead of the game's grey clear color; frames with a scene (upscaler off, a
  menu over the world) fill the whole width.
- Output resolutions 2560x1080, 3440x1440, 3840x1600, 5120x2160 (21:9) and
  3840x1080, 5120x1440 (32:9) in the launcher, the overlay menu and the game's
  Display page. They need the startup patch: run.sh never takes live
  resolution changes for them, the renderer does not switch to them live, and
  choosing one in the game restarts it.
- HUD position (bbport.ini hud_position, 0 centred in 16:9 .. 100 at the screen
  edges, the default; launcher and overlay menu): scripts/hud_layout.py, a
  pure-Python SWF/GFX editor, moves the HUD's corner groups in fe.gfx and widens
  full-screen menu backdrops in the menu movies; pictures (title art, loading
  screens) keep their shape. Its output is a layer laid after menu_prompts.py's
  (its gestures menu is built on theirs) and under the mods. It is optional:
  when its files cannot be written the game starts without it, the HUD
  centred.
- run.sh computes the output sizes before the menu layers now, so the HUD
  layer knows the canvas.

The README describes the ultrawide sizes; docs/ENVIRONMENT.md lists BB_UI_RES.
Tests: tests/test_ultrawide.py (patch values, canvas, HUD layout), the
canvas in test_ui_composition.cpp, custom ultrawide sizes and hud_position in
test_settings.cpp.

From Supermedo/bloodborne_pc#70 (emurphy04): ultrawide output and the HUD
layout, with the HUD layout optional at start as in Supermedo's 9461312.
Thanks: emurphy04, in 7 languages.

Co-authored-by: Ethan Murphy <[email protected]>
Co-authored-by: pmwork-personal <[email protected]>
2026-10-16 00:17:39 +01:00
ayozetrandSupermedo 523c88e08e Keyboard: a sprint is kept through direction changes
Measured by cassioKenji (Supermedo/bloodborne_pc#74): the game picks walk
or run by the larger stick axis, and ends a sprint when the stick turns 45
degrees or more between two reads. Our keyboard stick broke both: a
diagonal of the movement keys sat on a round gate (W+A was 90 of 127 on
each axis), and W -> W+A turned 45 degrees in one read.

The movement keys now give the square (the larger axis all the way at
every angle; stick_circular=1 brings the round gate back), the direction
turns toward the keys' at 900 degrees a second and at most 30 degrees a
read, a half turn (A <-> D) goes through forward, and a released key
keeps the last direction for 60 ms. Controller sticks are not touched.
After Supermedo's 4b930b1; cassioKenji in the About page.

Co-authored-by: Supermedo <[email protected]>
2026-10-16 00:17:17 +01:00
ayozetr 558fb3599a Online: The Hunter's Dream through bbbridge is marked unofficial
The Hunter's Dream's team has not approved bbbridge, and players of bbport
builds were asking their support for help (Supermedo hid the pool in his
Windows port for it, 9eeabba). The account group of the Online page now
says the connection is unofficial and that problems go to bbport on
GitHub, in the 7 languages; docs/ONLINE.md says the same.
2026-10-16 00:04:43 +01:00
ayozetr 43c5ee9a77 Docs: the README points to the credits and to a Windows version 2026-10-16 00:03:47 +01:00
ayozetr c7d012aa07 Docs: a shorter README, and every credit in docs/CREDITS.md
The README now shows at a glance what the port offers, how to start and
how to play online; building from source stays, the Add to Steam, button
prompt and DLSS sections go (the launcher explains them). The DLC steps
move to docs/COMPATIBILITY.md. docs/CREDITS.md lists every project this
fork took something from and everyone in the launcher's About page; the
About page's community patches entry links to it.
2026-10-16 00:00:12 +01:00
ayozetr 002f9c170f Launcher: version 0.51-linux.8 2026-10-15 23:25:15 +01:00
ayozetr 4e2d0824e2 Docs: BBSC's items and languages, and the camera on diagonals 2026-10-15 23:24:55 +01:00
ayozetr f6633ea393 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 the 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, and the
window is in front while SDL has one with keyboard focus.
2026-10-15 23:24:09 +01:00
ayozetr a22c380af1 Controls: an option to even out the camera stick's diagonals (issue #7)
Measured with the camera's yaw and pitch read while the right stick was
held at set positions: the game turns the camera in proportion to the
stick, diagonals included, up to ~82 degrees a second, and past ~96% of
the stick's reach switches to its fast turn, 174 degrees a second
sideways, whenever the stick is not near vertical (still at 60 degrees
from horizontal). A stick pushed all the way on a diagonal therefore
turned sideways at full speed, about twice the speed a moment before -
with any controller, in shadPS4 and on a PS4 alike.

Controls -> "Even camera speed on diagonals" (bbport.ini
pad_camera_diagonal=1) pulls a camera stick that far out between 20 and
80 degrees from horizontal in to 93%: diagonals turn at the proportional
speed and the fast turn stays straight sideways. Off by default: without
the fast turn on diagonals the camera feels heavier to players used to it.

BB_CAM_TRACE=<file> writes the camera's yaw and pitch as it turns, the
measurement this came from.

The round gate's comment no longer names it as the cause of #7.
2026-10-15 23:20:11 +01:00
ayozetr 80bfb4c5e8 bbsc: record bbport's changes to it, as its license asks 2026-10-15 22:49:51 +01:00
ayozetr 28246a9489 bbsc: its texts in the game's language
BBSC writes its messages, item names, descriptions and notices in English
whatever the game's language. They now follow 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), gen_translations.py makes them into
bbsc_translations.inc, and core/bbsc_i18n.cpp looks them up by the English
text, so BBSC's own sources keep their strings. The kit items use names of
their own in each language (Campana de cacería, Cloche de chasse, ...). The
boss name in the host's boss battle line stays BBSC's English one.
2026-10-15 22:47:28 +01:00
ayozetr 1a46cf5bcc Online: keep the session token out of the log
The HTTP library logged every request header with its value, the shadNet
session token (Authorization: Bearer ...) among them, three times per
WebAPI request. Players attach these logs to public bug reports. The
values of Authorization and Cookie are now logged as "(hidden)".
2026-10-15 22:41:42 +01:00
ayozetr c48221a3f3 bbsc: point BBSC at bbport's save folder
BBSC looks for saves in shadPS4's layout under the US title id
(home/<user>/savedata/CUSA00207/SPRJ0005); bbport keeps them in
savedata/<user>/<title id>/SPRJ0005. Without them BBSC gives no co-op kit
(it backs the saves up first: "no save folder found to back up") and its
own-world, loot and progress features have no save slot. BBSC_SAVE_DIR,
BBSC's own setting for a save folder elsewhere, is set to the most
recently written one, and the mod's log says which.
2026-10-15 22:36:39 +01:00
ayozetr b7b97895f2 bbsc: search the game's memory on Linux too
FindInMemory only had its Windows walk (VirtualQuery under SEH) and found
nothing on Linux, so BBSC never found the game's param tables: no co-op kit
(Hunt Bell, Kin Bell, Parting Blank, Duel Bell), no Duel Bell, no enemy
scaling per guest. The readable mappings come from /proc/self/maps and are
read in 1 MiB slices through process_vm_readv, so a page protected or
unmapped meanwhile fails that read instead of faulting.
2026-10-15 22:28:07 +01:00
ayozetr 21ba4491b9 bbsc: keep its files in the game's user folder, not the working directory
In the AppImage the working directory is its read-only mount: BBSC could not
create its capture folder, and without it installs none of its hooks. Its
death patch stayed in, so a death never reloaded and the player was left
waiting with camera control only. The folder is now BB_GPU_USER_DIR's
(run.sh exports it), or the temporary folder when that is not writable.
2026-10-15 22:12:29 +01:00
ayozetr d9599e4663 Docs: every environment variable the port reads
docs/ENVIRONMENT.md lists the 400 or so variables found in the code
(the game, the GPU library, the online module, BBSC, run.sh, the
launcher and the server images): what each does, its values and
default, and where it is read, by area, with the most useful ones
first. It also says how the launcher's Extra variables field is parsed
(space-separated NAME=value, no quoting) and which wins when the
launcher's own settings, the online settings, the extra variables and
run.sh set the same one, and lists the known quirks. The README points
to it.
2026-10-15 13:49:01 +01:00
ayozetr 4a45c725ee Swapchain: the launcher's FIFO Relaxed present mode is used
The launcher offers FIFO Relaxed (BB_PRESENT_MODE=FifoRelaxed), but the
swapchain knew only Mailbox, Fifo and Immediate: it logged "Unknown
present mode" and used Mailbox. A driver without FIFO Relaxed gets FIFO,
as for any mode it lacks.
2026-10-15 13:49:01 +01:00
ayozetr 71de6f9945 Launcher: Seamless Co-op by its name in Spanish
Spanish players know BBSC's kind of co-op as Seamless Co-op, as in other
games' mods; "cooperativo continuo" was nobody's word for it.
2026-10-15 13:38:33 +01:00
ayozetr 94999afb3d Server: BBSC's game server in Docker, shadNet's seamless mode by variable
tools/bbsc-server builds a small image of BBSC's game server
(bbsc/server/gameserver, Python only) taken from this repository: it
listens on BBSC_HOST (waiting for Tailscale, as the shadNet image does),
talks to the shadNet server at BBSC_SHADNET, keeps its messages,
bloodstains and logs in /data, and leaves the play-log uploads to Amazon
S3 off. BBSC_BBHOST=0 drops its bbhost-compatible service.

The shadNet image's entrypoint now honours
SHADNET_BLOODBORNE_SEAMLESS_COOP=1/0 (BloodborneSeamlessCoop in
shadnet.cfg), which docs/SERVER.md already told players to set. The
seamless co-op section of docs/SERVER.md walks through both.
2026-10-15 13:27:50 +01:00
ayozetr a445ff2e5a Online: BBSC's UDP relay only with BBSC on a server of its own
The online module now has BBSC's relay fallback, which by default sends
every peer's frames through UDP 3478 on the shadNet server's address as
well. Only BBSC's game server answers there: without BBSC, or on The
Hunter's Dream (bbbridge's gateway relays by itself), the launcher turns
it off (SHADPS4_P2P_RELAY=0), so their co-op goes direct only, as before.
2026-10-15 13:26:27 +01:00
ayozetrandCLS_AMG f605ea9be4 Online: BBSC's changes to the online module (relay, rejoin, NpLookup)
BBSC's build of the Windows port carries a patch to the shadp2p network
code (bbport-1.7.2-bbsc.patch, in its BBport release), and Linux players
must match it to play with BBSC's Windows players. Brought into the
online module (gpu/bbnet; Supermedo's gpu/shadps4):

- p2p_sockets.cpp, sockets.h: a peer that sends nothing back directly
  within 2 s is reached through the game server's UDP relay (port 3478
  on the shadNet server's address), by online id.
- np_signaling: a re-activation checks the peer, and a new peer
  connection id restarts the handshake (rejoining after a session ended).
- np_utility.cpp/.h (new): sceNpLookupNpId answered from the online id,
  which a third player's join needs; it was the offline stub.
- np_matching2: the peers' endpoints for the relay.
- bloodborne_re.h: BBSC's frames in signaling packets are left to BBSC.

The patch was made against Supermedo e5d04e2; most of it applied as it
is, np_signaling_state.cpp, sockets.h, bbnet_glue.cpp and the header by
hand (line endings and nearby differences).

Co-authored-by: CLS_AMG <[email protected]>
2026-10-15 13:25:33 +01:00
ayozetr 954d969f50 bbsc: keep its logs and captures from growing without end
Every game start wrote a new BBSC log and a new trace capture, kept for
good, and the capture grew about 30 MB an hour of play. Now only the
newest five of each stay (older starts' files go at start), and a capture
stops at 64 MB (Linux build only).
2026-10-15 13:22:45 +01:00
ayozetr d8ec9ed684 bbsc: seamless co-op in the build, the AppImage and the launcher
- build.sh builds libbbsc.so (bbsc/), with xbyak 7.37.3 (BSD-3-Clause)
  and fmt 12.2.0 (MIT) now kept header-only in bbsc/third_party.
- The AppImage carries it in share/bbport/bbsc/ with mod.json and the
  license, notice and supporters list BBSC checks for.
- run.sh sets BB_BBSC_LIB only with seamless co-op on (BB_BBSC=1) and
  online, so bb-probe loads BBSC then only.
- The launcher's Online page gains Seamless co-op (BBSC): the switch and
  BBSC's own options from mod.json (scripts/bbsc.py turns them into its
  variables; only what the player changed is kept), on any server, in 7
  languages. It takes the place of the old seamless option, which only
  routed summons to the server's WebAPI.
- BBSC's relay reads the overrides file run.sh hands the online module.
- Docs: README, docs/ONLINE.md (what works on which server) and
  docs/SERVER.md (BBSC's game server next to shadNet). Thanks: CLS_AMG
  (BBSC) and fhody125-web (NoDreamForHunter, the base of its game
  server), in 7 languages.
2026-10-15 13:21:56 +01:00
ayozetr efb652ad8a bbsc: build BBSC for Linux as libbbsc.so, loaded by bb-probe
BBSC's game logic (emulator/src/core/bloodborne_re.cpp, built with
BBSC_MOD as the Windows mod builds it) now builds on Linux into
libbbsc.so, with the mod's network files (relay, P2P channel, HTTP
wrappers) and Linux counterparts of its platform files:

- mod/linux/bbsc_mod_linux.cpp (for bbsc_mod.cpp): the same start-up -
  every byte patch site checked against the 1.09 bytes before any is
  written, Wozzardman's seamless patches, the hooks, a watcher that
  writes back a site that changed - with the game always at 0x800000000,
  memory questions answered from bbport's own map of the game's memory
  (runtime_memory_vma_info, no system call) and its log in
  <user>/bbsc/logs/, warnings also in the game's log. BBSC's cinematic
  camera (F1, L3+R3: bbport's menu chord) and its own Discord status are
  left out; bbport has its own.
- mod/linux/bbsc_hooks_linux.cpp (for bbsc_hooks.cpp): the same xbyak
  trampolines, in 1 MB mapped below the image.
- mod/linux/bbsc_win32.h: the Win32 and Winsock names the network files
  use, on POSIX; the few spots that differ are #ifdef'd in place (paths
  of host_overrides, a plain socket POST instead of WinHTTP, the game's
  memory map instead of VirtualQuery). On Linux the online module
  applies host_overrides itself, so BBSC's HTTP wrappers leave URLs as
  they are.

bb-probe loads it when BB_BBSC_LIB names it, once the game is linked and
patched and before any of its code runs. None of BBSC's 243 sites
overlaps a patch of patches/Bloodborne.xml or bbport's own hooks. Not
tried in the game yet.
2026-10-15 12:47:59 +01:00
ayozetr fb3f39ba8a bbsc: BBSC 0.0.1 by CLS_AMG (GPL-2.0-or-later), as published
Bloodborne Seamless Co-op from https://github.com/Classmg22/BBSC at
315f76c ("BBSC 0.0.1 - first public test"), unchanged: the game logic
(emulator/src/core/bloodborne_re.cpp and bbsc_*), the Windows mod that
runs it inside the game's process (emulator/mod/), the emulator headers
the mod build uses, and the host servers (server/). Its LICENSE, NOTICE,
CREDITS and SUPPORTERS come along: BBSC turns itself off without them.

The Linux port follows in the next commits.
2026-10-15 12:40:32 +01:00
ayozetr 4140b670bf Docs: what the public server and seamless co-op do today
docs/SERVER.md sold the seamless co-op option as moving the guest into the
host's area. In bbport it only sends the game's summon requests to a
shadnet-p2p server with BloodborneSeamlessCoop=true; shadp2p's host
placement is not ported, and it was never tried in a real session. The
section says so and points to BBSC, and the launcher's option says it is
not ready, in 7 languages.

The UDP line of Test the connection is no longer red with shadPS4's
public server (it answers in its own framing now), so TROUBLESHOOTING.md
no longer blames it for a red line; the public server does not accept
bbport builds at all. BBPORT.md (and its Russian version) no longer
gives srv.shadps4.net as the default without saying so.
2026-10-15 12:15:06 +01:00
ayozetrandJoao Ramos e7cbc43baa Online: the connection test pings the public server in its framing
Test the connection sent the UDP ping with the 4-byte vport header only, which
srv.shadps4.net drops, so its UDP line was always red. The ping now goes out in
both framings, as the game's does, and either reply counts.

Co-authored-by: Joao Ramos <[email protected]>
2026-10-15 12:13:19 +01:00
ayozetrandJoao Ramos f502965635 shadNet: co-op signaling in the public server's framing too
shadPS4's public server (srv.shadps4.net) only takes UDP framed as a PS4 P2P
datagram, FF 83 FF FF FF FF before the payload; the shadp2p servers take the
4-byte vport header we sent. The public server dropped our STUN pings, never
learned the players' addresses, and summons through it timed out (#121 blamed
the server). Packets to the shadNet server now go out in both framings, and
replies are taken in either.

SHADPS4_P2P_SAME_HOST=1 sends a peer at this network's own public address to
loopback, for two instances on one machine.

From Supermedo/bloodborne_pc#21 (004d2c3). Its UPnP part is left out: our
transport already maps its port (issue #121).

Co-authored-by: Joao Ramos <[email protected]>
2026-10-15 12:13:19 +01:00
ayozetr 8c7f4c53fa Launcher: thanks for the work in 0.51-linux.7
Supermedo's skipped draws with an empty vertex table, Awakenchan's fewer
stutters while areas load and the controller in use, and nr1971's reports
of the camera on diagonals and the crash on quitting, in 7 languages.
2026-10-15 12:04:47 +01:00
ayozetr d79bcb27a6 0.51-linux.7 2026-10-15 11:51:30 +01:00
ayozetr cb3252eb62 bbbridge: warn of a missing STUN ping only without the P2P gateway
bbport starts its co-op context before its first STUN ping, so every
session logged "no STUN ping seen ... P2P endpoint unknown" a moment
before the ping came. With the gateway (the default) that address is not
used: bbhost gets the gateway's endpoints, and the port's socket falls
back to the pinned port until the ping comes. The warning is kept for the
case without the gateway, where bbhost's address does come from it.
2026-10-15 11:51:30 +01:00
ayozetr 3f07ea173c Merge branch 'fix/guest-exit' 2026-10-15 11:50:53 +01:00
ayozetr 219be762f8 Merge branch 'feature/fault-skip-textures' 2026-10-15 11:50:53 +01:00
ayozetr 74e061091b Merge branch 'feature/mouse-camera' 2026-10-15 11:50:53 +01:00
ayozetr bd1b901698 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 (seen in the log of issue #7). 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.
2026-10-15 10:46:37 +01:00
ayozetrandAwaken ab6e5f4162 GPU: write faults on pages no image watches skip the texture cache
Every write fault on guest memory the GPU side protects went through
TextureCache::InvalidateMemory, which takes the texture cache's lock.
The draw recording thread holds that lock while it creates or uploads an
image, so a game thread writing its constants into a buffer's page (most
faults) waited for that: 0.3-0.6 s with memory short, a stutter of that
frame.

The texture cache now counts, per 4 KiB page and without its lock, the
tracked images that protect each page (watched_pages.h, kept with the
page manager's watchers: up before the pages are protected, down after
they are unprotected). A fault on a page no image watches only
invalidates the buffer cache. An image stops watching its pages only
once it is marked dirty or maybe dirty, so it is refreshed anyway.
BB_FAULT_SKIP_TEXTURES=0 takes the lock for every fault, as before.

From Awaken's BloodBorneButPc (WatchedPages, BB_FAULT_SKIP_TEXTURES).

Co-Authored-By: Awaken <[email protected]>
2026-10-15 02:24:21 +01:00
fd253355e5 Mouse: direct 1:1 camera (mouse_camera=direct)
Mouse look moves the right stick, so it inherits the game's stick dead
zone and turn curve: slow movements fall inside the dead zone, then the
camera jumps. Controls in the launcher gains Direct mouse camera
(mouse_camera=direct in bbport.ini, at mouse_sensitivity: 1 = 900 counts
per radian): the mouse then writes the camera's yaw and pitch itself,
the Monocular's too, like a PC game.

gpu/shim/bloodborne_cam.cpp hooks the camera update of game 1.09 at its
fixed address (the image is at 0x800000000, as in shadPS4, so no memory
scan): the update jumps to a stub that records the camera object, and
the game's own four writes of its angles become NOPs (the same four as
the Disable Camera Auto Rotation game patch, so NOPs there are accepted
and put back as they were). All five sites are checked against the 1.09
bytes first; other code is reported once and the mouse stays on the
stick. The hook is in while the window holds the mouse and out when it
lets it go (F1, the menu), so the stick turns the camera again. While
locked on, the motion goes to the stick as before, so a flick switches
targets. Motion is applied as the window reads it, with no sampler
thread.

From Supermedo/bloodborne_pc edd20a5 by Jhony-Jhansen, ported from
Windows; based on mcrib884's bbmouse camera (Supermedo/bloodborne_pc#3)
and shadlixps4's Bloodborne mouse camera. Thanks updated in 7 languages.

Co-Authored-By: João Pedro Valadares <[email protected]>
Co-Authored-By: mcrib884 <[email protected]>
2026-10-15 02:20:46 +01:00
ayozetrandAwaken fdd924b5e7 Pad: the controller in use takes over when none is chosen
With several controllers connected (or one behind a receiver that shows
as two) and none chosen in the launcher, the first one connected was the
only one read. Now the other ones are looked at five times a second, and
the first with a button held or an axis well away from where it rested
when it was opened takes over ("Runtime: gamepad in use"). Comparing
with where each axis rests keeps a wheel's pedals, which rest at one end
of their axis (issue #15), from taking over. A chosen controller is kept
as before. The launcher's Auto choice now says so.

From Awaken's BloodBorneButPc (follow_active_pad).

Co-Authored-By: Awaken <[email protected]>
2026-10-15 02:12:06 +01:00
ayozetrandAwaken a8f913a9c3 Launcher: install another released version, an earlier one too
The Updates group gains Another version: every release the launcher can
install (tagged <upstream>-linux.<n>, with an AppImage, not a pre-release
or a draft), newest first, the running one marked. Install downloads the
chosen one over the running AppImage like an update, after asking, and
says when it is older and how to come back (the same list, or Update).
Releases before 0.5-linux.1 are left out: their launcher has no updater,
so they could not come back. The list is fetched with the update check;
from a git checkout Install opens the release page instead.

The test reads this repository's real /releases reply, trimmed to the
fields the launcher uses.

Idea from Awaken's BloodBorneButPc (its version selector).

Co-Authored-By: Awaken <[email protected]>
2026-10-15 01:59:34 +01:00
ayozetrandAwaken 14ae3076ca Launcher: import saves from shadPS4, another port or a PS4 export
The saves folder row gains Import saves: a folder is searched for the
game's save folder (SPRJ0005, an export named after it, or any folder
with the system save userdata0010), the newest one is offered, and its
save files are copied in as user 1's saves under the game's save id
(INSTALL_DIR_SAVEDATA, else TITLE_ID, as src/probe.c). The saves already
there are copied to user/savedata-backups/<date>/ first, and the new ones
are staged next to them and swapped in, so a failed copy keeps them. A
console's USB copy (sdimg_*) is encrypted with its keys: the launcher
says to export it unencrypted (Apollo Save Tool, Save Wizard) instead.
It refuses while the game runs. scripts/save_import.py does the work and
also runs on its own.

From Awaken's BloodBorneButPc (SaveImport.cs).

Co-Authored-By: Awaken <[email protected]>
2026-10-15 01:44:32 +01:00
ayozetrandAwaken 27d5d3802f Pad: swap the sticks and invert the controller camera
Controls in the launcher gains three switches, kept in bbport.ini:
pad_swap_sticks moves with the right stick and looks with the left, and
pad_invert_look_x / pad_invert_look_y invert the controller's camera
stick. They apply to the controller after the round gate; keyboard and
mouse keep their own directions (mouse_invert_y). Inverting mirrors
around the centre (256 - v), so a stick at rest stays at rest.

From Awaken's BloodBorneButPc (bbport.ini options of the same names).

Co-Authored-By: Awaken <[email protected]>
2026-10-15 01:40:30 +01:00
ayozetr cc4999adc5 bbbridge: understand bbport's STUN ping and its client version
bbport sends its STUN ping inside a P2P signaling frame (virtual ports
FF FF, FF FF first), which the bridge dropped, so every session logged
"no STUN ping seen ... P2P endpoint unknown" and fell back to the pinned
co-op port. The framed ping now registers the port's P2P socket; it gets
no echo, which would hand the port 127.0.0.1 as its external address.

SetClientVersion (13), which bbport sends after logging in, was answered
Unsupported and logged as "SetClientVersion returned error=33"; the
bridge now accepts it. Both are covered by the end-to-end tests.
2026-10-15 01:38:07 +01:00
ayozetr 9b90958953 Fix translation-memory-test after the copy shader moved to the game profile
Upstream 0.5 replaced the BufferCopyShaderHash constant with the game
profile's buffer copy shader (Profile::buffer_copy_shader_hash), so the
test no longer built. It now sets the hash in the profile it emits with,
so the guest-store path it checks is the one the game uses.
2026-10-15 01:35:52 +01:00
cc28a796ae Pad: Back + d-pad left/right clicks the touchpad halves on Xbox pads
Pads without a touch surface had only Back, a click on the left half, so
the right half (key items, the right choice of some prompts) could not be
reached. While Back is bound to nothing but the touchpad (the default),
Back + d-pad left/right clicks that half and the d-pad press is not passed
on; Back alone clicks the left half for 150 ms once it is released. Pads
with a touchpad are unchanged.

Ported from Supermedo/bloodborne_pc a04e1e9 (Windows 1.7.3), on our
pad.<input> bindings instead of input.ini; the idea and timing are
parsifal295's.

Co-authored-by: Supermedo <[email protected]>
Co-authored-by: parsifal295 <[email protected]>
2026-10-15 01:11:15 +01:00
ayozetrandSupermedo 004c8d40ad Skip draws whose vertex buffer table pointer is null
Some draws leave the vertex buffer table pointer in user data null (four
"Patched SRT walker ... fault address 0x0" lines, then a read fault at 0
in ReadUdReg while specializing the vertex shader), reported at the
cutscene after Rom and in normal play. Such a table now reads as null
buffers and the draw, which has no vertex input to build, is skipped.

From Supermedo/bloodborne_pc 2c4ddeb (Windows 1.7.3).

Co-authored-by: Supermedo <[email protected]>
2026-10-15 01:10:14 +01:00
ayozetr 7191ac0ec7 0.51-linux.6 2026-10-15 00:25:28 +01:00
ayozetr 1cc27f4223 Launcher: what's new in this version, Install from PKG first, more thanks
- About → Updates: "What's new in this version", under the latest version,
  opens the installed version's release notes on GitHub.
- Settings → Game: Install from PKG comes before the game folder it fills.
- Thanks: paulomanrique (ps4-pkg-extractor, in the package for Install from
  PKG), Monochrome0907 (which effects freeze NVIDIA loading screens,
  deadinside28/bloodborne_pc#184) and nr1971's line names the DualShock 4
  camera report too (#7). 7 languages.
2026-10-15 00:25:15 +01:00
ayozetr e107d9e56f Merge branch 'feature/pkg-install'
# Conflicts:
#	build.sh
#	launcher/bbport_i18n.py
#	launcher/bbport_launcher.py
#	packaging/appimage-sharun.sh
2026-10-15 00:18:20 +01:00
ayozetr aedbe8779e Merge branch 'fix/awaken-crash-fixes'
# Conflicts:
#	launcher/bbport_i18n.py
#	launcher/bbport_launcher.py
2026-10-15 00:18:13 +01:00
ayozetr fab33687cf Merge branch 'fix/vram-over-budget' 2026-10-15 00:18:07 +01:00
ayozetr 49e6c535bb Merge branch 'fix/issue163-residency'
# Conflicts:
#	launcher/bbport_i18n.py
#	launcher/bbport_launcher.py
2026-10-15 00:18:07 +01:00
ayozetr 5531775af3 Merge branch 'fix/issue7-ds4-diagonal' 2026-10-15 00:18:00 +01:00
ayozetr 597ad6bad9 Merge branch 'feature/bbbridge-linux' 2026-10-15 00:18:00 +01:00
ayozetrandAwaken ea13d1c243 Patches: the 1 MiB GX command heaps on by default
Awaken's BloodBorneButPc enlarges the same two heaps (the imm32 at 0x2195c09
and 0x2195c46) to 1 MiB by default since 0.9.16, for a GXWorker crash in a
boss intro with Model LOD -2 at 2560x1440: the same NULL allocation as
deadinside28/bloodborne_pc#162 and #169. Their players run it, so the risk
noted when it came in (a parent heap that cannot give 1.5 MiB more) did not
show. On unless BB_GX_COMMAND_HEAP=0.

Co-Authored-By: Awaken <[email protected]>
2026-10-14 23:11:06 +01:00
ayozetrandAwaken 27f3c6d061 Patches: "1.09" is "01.09", and copies of Bloodborne.xml are not third-party
- Third-party patch files that write AppVer="1.09" were skipped; the
  version now compares by number.
- A copy of the built-in Bloodborne.xml in the patches folder (an older
  package's) was read as a third-party file and could apply its frame-rate
  patches over the port's own. A file of that name is skipped.

tests/test_patches.py covers both. From Awaken's BloodBorneButPc.

Co-Authored-By: Awaken <[email protected]>
2026-10-14 23:10:31 +01:00
ed7dbcf626 Pad: buttons still held when a menu closes are not new presses
The Cross that accepted a name in the text dialog, or the button that closed
the port's menu, was still down when the game took input back, and the game
took it as a new press. Buttons held then stay hidden until all are released.
tests/test_pad.c covers it.

From Supermedo/bloodborne_pc 1d34202 (hold_after_capture), as Awaken's
BloodBorneButPc carries it.

Co-Authored-By: Supermedo <[email protected]>
Co-Authored-By: Awaken <[email protected]>
2026-10-14 23:10:31 +01:00
ayozetr 189c03b25e Thanks: Awaken's line as on fix/awaken-crash-fixes
The VRAM work and the crash fixes in one line, so the two branches carry the
same row. 7 languages.
2026-10-14 23:08:01 +01:00
ayozetr a1ba326fdc Thanks: Awaken and AwesomeObserver
Awaken (Awakenchan, BloodBorneButPc): the VRAM work for 4 GB cards and the
crash fixes for flips, the swapchain, garbage descriptors and page faults.
AwesomeObserver (Evgeniy Doronin): large recorded command groups and the
swapchain after display changes. 7 languages.
2026-10-14 23:08:00 +01:00
cc30687384 Swapchain: display events recreate it instead of ending the game
Several display events were assertions or UNREACHABLE (exit 23): a swapchain
that cannot be created, a surface whose capabilities cannot be queried, a
present failing with anything but out-of-date or suboptimal, and an acquire
result outside the known ones. Each is now reported a few times ("Display:
...") and the swapchain recreated, frames skipped meanwhile; a lost surface
(a display or driver reset) is made again on the same window. A lost device
still stops.

- A suboptimal acquire owns an image and signals its semaphore: it is
  presented, then the swapchain recreated (dropping it left the semaphore
  signalled and never waited on).
- The indices start over with a new swapchain, which may have fewer images.
- The device waits for idle under Scheduler::submit_mutex, as the recording
  threads submit to the same queue; the presenter recreates after releasing
  that lock.

From Awaken's BloodBorneButPc engine (bbhost-changes.patch, 0.9.24), with
AwesomeObserver b3164c1 (Evgeniy Doronin); its HDR changes are not taken.

Co-Authored-By: Awaken <[email protected]>
Co-Authored-By: Evgeniy Doronin <[email protected]>
2026-10-14 23:07:35 +01:00
ayozetrandAwaken 3b826a415e Faults: within the page, a stuck tracked page gets its access back
- 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). It now stays within the faulting page.
- The same access faulting again and again within microseconds (the tracking
  claimed the fault but left the page protected) retried forever; after 64
  in a row, and for a fault nobody claims, the page gets the guest's own
  protection back (runtime_memory_restore_access) when the guest may access
  it. The GPU may then miss writes there until it protects the page again,
  which beats ending the game; instruction fetches and real faults stay
  unhandled. The log says when it happens.
- KernelServiceThread cleared its request count after run(): a request made
  during the run (a timer armed after run() found no work) was lost. It is
  taken before the run now.

From Awaken's BloodBorneButPc engine (bbhost-changes.patch, 0.9.24).

Co-Authored-By: Awaken <[email protected]>
2026-10-14 23:05:14 +01:00
084c7f7d6d Scheduler: a group of recorded data larger than a chunk spills, not exit 23
ReserveRecordData asserted that a command's data fits in a 128 KiB record
chunk: thousands of buffer copies or barriers in one group ended the game
(exit 23). RecordChunk::ReserveData keeps groups that fit inline and gives a
larger one a heap block owned by the chunk, freed when the chunk is recycled,
so no span outlives the commands that read it.

From AwesomeObserver/bloodborne_pc 54bd756 (Evgeniy Doronin), as Awaken's
BloodBorneButPc carries it.

Co-Authored-By: Evgeniy Doronin <[email protected]>
Co-Authored-By: Awaken <[email protected]>
2026-10-14 23:02:54 +01:00
ayozetrandAwaken 23728115f0 VideoOut: no exit 23 for an out-of-order flip IRQ or a full flip queue
- Flip released the buffer the flip replaced (its label back to 0) after the
  flip status and events: the game, rendering into that buffer next, could
  already have set its label to 1 again, and the reset cleared it. The next
  flip IRQ then found no label and stopped the game ("Out of order flip IRQ",
  exit 23). The buffer is released first now; frames are prepared at
  submission, so it is no longer read there.
- The flip IRQ waits up to 20 ms for a label still on its way and then flips
  anyway (at worst a frame shown twice, and the game gets its flip event).
- A full flip queue refused an EOP flip, which nobody could take back (an
  assertion, exit 23): it is queued anyway.

From Awaken's BloodBorneButPc engine (bbhost-changes.patch, 0.9.24).

Co-Authored-By: Awaken <[email protected]>
2026-10-14 23:01:24 +01:00
ayozetrandAwaken eb20620aaf 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 BloodBorneButPc engine (bbhost-changes.patch, 0.9.24).

Co-Authored-By: Awaken <[email protected]>
2026-10-14 23:00:05 +01:00
ayozetrandAwaken 037a24df0e GPU: a garbage descriptor is reported and read as a known one, not exit 23
Several lookups ended the game (UNREACHABLE or an assertion, exit 23) on a
value outside their tables, as a stale or garbage sharp brings: unknown tile,
macro tile, array modes and pipe configs, sample counts and bpp out of range
(tiling.cpp), a data/number format combination the surface table lacks and
an invalid sample count (liverpool_to_vk.cpp), anisotropy ratios 5-7
(resource.h), format names while logging (pixel_format.cpp). Each is now
reported a few times and replaced by a known value; at worst that surface
renders wrong. The CE/DE counter check compares across the u32 wrap.

From Awaken's BloodBorneButPc engine (bbhost-changes.patch, 0.9.24).

Co-Authored-By: Awaken <[email protected]>
2026-10-14 23:00:05 +01:00
ayozetrandAwaken d637e22258 Guest memory: unused VRAM copies leave under pressure, with the 0.3 model too
deadinside28#163: on NVIDIA and Intel the port runs the 0.3 memory model,
where every block of the game's memory a draw used kept its VRAM copy for
good, so a 4 GB card filled up while areas loaded and stopped (exit 23).
After Awaken's BloodBorneButPc 0.9.17:

- UpdateVramLevel, once a second: VRAM used against the driver's budget,
  tight from 85 % and full from 90 % (BB_VRAM_PRESSURE_PERCENT, 0 off); a
  failed VRAM allocation counts as full for 30 s. The log says each change.
- EvictIdleCopies (0.3 model): while tight, the copies unused for 30 s
  (10 s when full) leave VRAM; their page table entries are cleared and their
  slots take the next blocks. Kept: blocks with GPU-written data, blocks with
  a twin (their version turned this off with twins, which are on here by
  default), and all of them while shaders read the game's memory through the
  page table.
- The PC memory model's idle demotions and chunk evacuations get shorter under
  pressure; no new VRAM copies and no spare block while full.
- EnsureResident: evicted blocks come back with their whole 2 MiB group, and a
  reused slot is cleared before the uploads: it held the previous block's
  data, and draws reading past their range (vertex buffers bound without a
  size) got stretched polygons and flashing lights.

Their spill to system memory is not taken: on NVIDIA the sparse arena takes
VRAM only (an RTX 3060 reports memoryTypeBits 0x2). Not tried in play yet:
BB_VRAM_LIMIT_MB=3000 makes the budget small enough to see it work.

Thanks: Awakenchan, in 7 languages.

Co-Authored-By: Awaken <[email protected]>
2026-10-14 22:58:11 +01:00
ayozetrandMaskZeus 95878adbba Docs: The Hunter's Dream and your own server, as shadPS4's public one refuses bbport
The README, docs/ONLINE.md and docs/SERVER.md still sent players to
shadPS4's public shadNet server, which now signs bbport builds out after the
login. They now describe the two pools: The Hunter's Dream's through bbbridge
(sign in with Discord, or an account with a name and a recovery code; no
router port; the play log stays blocked, so no live map) and a server of your
own. ONLINE.md's log table has the refusal's line.

Credits: bbbridge by MaskZeus in the README and in About → Thanks (7
languages).

Co-Authored-By: MaskZeus <[email protected]>
2026-10-14 16:01:41 +01:00
Supermedo dac2621116 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 ("Failed allocating image"). 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. Logged once per
kind.

Past the critical mark, old GPU-written tiled images unused for a minute
(render targets of an area left behind) are freed too, unwritten, where
before they could never go. The pressure report counts them.

From Supermedo/bloodborne_pc@44e7644, adapted to this texture cache's
collector. Thanks entry updated.
2026-10-14 15:59:18 +01:00
ayozetr 5a88481947 Guest memory: VRAM full, empty copies go back to the driver at once
deadinside28#163 (a 4 GB card, NVIDIA): "Failed to allocate 1 MiB for guest
memory copies: VRAM is full" while an area loads, and the game stops. Chunks
whose blocks had all left VRAM were kept for 5 s before going back to the
driver, so during a load VRAM could be full of empty chunks.

When a chunk cannot be allocated (and there is no system memory to spill to),
AllocateResidency now frees the empty chunks the GPU is done with at once
(ReleaseEmptyResidencyChunks, the idle pass's loop with no wait), takes the
spare block being allocated in the background if it came, and tries again;
the log says how much it freed. Only then does it stop as before, saying how
much was freed. Evicting blocks still in use is not done there: their unbinds
have to reach the GPU before their memory can go.

BB_RESIDENCY_VRAM_LIMIT_MB=N (tests: VRAM "full" for residency on a large
card) now counts the chunks in VRAM, freed ones subtracted; it counted every
chunk ever allocated, so freeing never made room under it. Not tried in play
yet: BB_RESIDENCY_VRAM_LIMIT_MB=256 to try it.
2026-10-14 15:58:44 +01:00
ayozetr 636621403f README: credit ps4-pkg-extractor for Install from PKG
Tried with real packages (the US release: game 1.00, the 1.09 update and
three add-ons): installed in 3.5 minutes, game_check.py found the 1.09
executable and all 26837 .dcx files unpack.
2026-10-14 15:51:00 +01:00
ayozetr 35acaa9e59 Launcher: install the game from your own PS4 package files
Settings → Game → Install from PKG: choose the game's .pkg, its updates and
add-ons (any number, any order) and a folder. scripts/pkg_install.py tells
them apart by their header's content ID and their param.sfo (category gd, gp,
ac), not by file name: the game goes to <folder>/<title ID>, the updates are
merged into it oldest first, the add-ons go to <folder>/DLC/<label>, and the
launcher then points at both. game_check.py checks the result.

Nothing is replaced: a game folder that exists stops the install, each package
is extracted into a new folder under the destination (pkgextract's -f would
delete an existing one), and the space needed is checked first.

pkgextract (github.com/paulomanrique/ps4-pkg-extractor, Unlicense) is built by
build.sh with cargo at a reviewed revision and its Cargo.lock, and the AppImage
carries it; without it the row is hidden. tests/test_pkg_install.py covers it
with a stand-in extractor. 7 languages.
2026-10-14 15:47:45 +01:00
ayozetr a0148d4930 Patches: an experimental larger GX command heap (BB_GX_COMMAND_HEAP=1)
The game hands out its per-frame GX commands from two 256 KiB heaps (the
size is the imm32 of `mov $0x40000,%edx` at 0x2195c09 and 0x2195c46, before
each heap's init call). When a frame fills one, its allocator returns NULL
and several callers write through it unchecked: the "Guest fault at
0x2199310 (address 0x8)" of deadinside28/bloodborne_pc#162 and the host
fault in memcpy called from 0x21d5b40 of #169 are the same thing. ("Increased
Graphics Heap Sizes" changes other heaps, GFX_GraphicsPrivate/B.)

"Larger GX Command Heap (1 MiB)" makes both 1 MiB. Off by default and not
tried in play yet: their parent heap is not identified, and if it cannot
give the extra 1.5 MiB the game stops at start. BB_GX_COMMAND_HEAP=1 turns it
on. tests/test_patches.py checks the two writes and, with a build, the
instruction they land in.
2026-10-14 15:41:23 +01:00
ayozetr 7e7cedd4b0 Upscaler: one copy of the UI mask's depth handling
OnDraw had the block that sends a UI mask draw (no color target) into the
UI's depth twice: one from bmy's patch (d0318b1, via 0xCydral) and the same
from upstream 0.5. The first returns, so the second never ran; it is gone.
2026-10-14 15:40:32 +01:00
ayozetr ba79de106f Online: a The Hunter's Dream account without Discord
As bbhost's own account screen offers: an account held by this PC, created
with just a name (bbbridge-cli --create), and a recovery code to bring it back
on another PC (--recover NAME CODE). The Online page has both next to the
Discord sign-in; a new account's recovery code is shown once to be kept (it is
also in <user>/bbbridge/bbbridge_state.json). 7 languages.
2026-10-14 15:38:34 +01:00
ayozetrandSupermedo 8f67573bd3 Online: The Hunter's Dream's co-op pool through bbbridge
The public shadNet server no longer accepts bbport. The Online page now also
offers The Hunter's Dream, with bbhost's players: bbbridge stands in for the
shadNet server on this PC and talks to The Hunter's Dream as bbhost does.

- build.sh builds bbbridge-cli when libcurl and OpenSSL are there; the
  AppImage carries it with its licence (share/bbport/bbbridge)
- scripts/online.py: the "thd" server kind (the game signs in to the bridge
  at 127.0.0.1:31313 with the Discord account's online ID, P2P port 3658,
  no UPnP) and `online.py bridge`, which writes the bridge's config and the
  game's host overrides for it into <user>/bbbridge. The game's play log
  stays blocked, so The Hunter's Dream's live map is not fed.
- run.sh starts bbbridge-cli for the launch (or uses one already listening)
  and a watcher stops it once the game has ended
- launcher: the choice shows only when the build has the bridge; a Discord
  sign-in (bbbridge-cli --link-discord) shows the code and opens the page;
  the password, UPnP and port rows are hidden for it; 7 languages

After Supermedo/bloodborne_pc bb08740 (the same pool for bbport for Windows).

Co-Authored-By: Supermedo <[email protected]>
2026-10-14 15:35:53 +01:00
ayozetr 21e0edd40a bbbridge: the core, bbbridge-cli and the tests build and pass on Linux
The Windows build is unchanged; everything for Linux is under #ifdef _WIN32's
else branches:
- util.hpp: BSD sockets under Winsock's names (SOCKET, closesocket, Sleep,
  WSAGetLastError, localtime_s), and fspath()/replace_file() for the state
  and log files, whose names stay std::wstring (UTF-32 on Linux)
- closesocket() shuts the socket down first: on Linux closing it does not
  wake a thread blocked in accept() or recvfrom() on it, as it does on Windows
- net_init() ignores SIGPIPE (a send to a closed peer ended the process)
- listening sockets use SO_REUSEADDR on Linux, which otherwise refuses a port
  still in TIME_WAIT from the last run (Windows allows it)
- http.cpp: libcurl in place of WinHTTP, with the same behaviour (certificates
  checked against the system's CA bundle, no redirects, the request's timeout,
  a Pool keeping one connection alive until it is max_age old)
- SHA-256 from OpenSSL in place of bcrypt; gethostname() for the device name
- p2p.cpp: select() gets the highest descriptor + 1; no SIO_UDP_CONNRESET
- bbbridge-cli: main() and SIGINT/SIGTERM; --route (pointing bbport for Windows
  at the bridge, setup.cpp) is Windows-only, as the bbport launcher does that
  on Linux
- CMake: on Linux the core, bbbridge-cli and bbbridge-unit, with Threads, CURL
  and OpenSSL::Crypto; the window (Win32 + DirectX 11) stays Windows-only

bbbridge-unit: 24 passed. tests/e2e/test_e2e.py against the Linux
bbbridge-cli: 54 passed; its part E (routing bbport for Windows) runs on
Windows only.
2026-10-14 15:29:37 +01:00
ayozetrandMaskZeus a7e6d44076 bbbridge 0.3.2 by MaskZeus (GPL-3.0-or-later), as released
MaskZeus/bbbridge at 7cb7b68, unchanged: it stands in for the shadNet server
on this PC and talks to The Hunter's Dream the way bbhost does, so bbport
plays co-op with bbhost's players. Windows-only as released; the Linux port
of its core and command-line bridge follows.

Co-Authored-By: MaskZeus <[email protected]>
2026-10-14 15:23:19 +01:00
ayozetr 2b8d9e1fe1 Pad: a controller's sticks stay within a round gate
#7: with a DualShock 4 the camera turned two or three times faster at one
point of a diagonal, less so at 30 FPS; with an Xbox-style pad it does not.
The likely cause (not confirmed yet): a stick that reports a diagonal near the
corner of the square, up to ~1.41x the full deflection of one axis, which a
stick with a round gate never does. A stick past the gate is now brought back
onto it, keeping its direction; one axis all the way stays as it is. The log
says how far past the gate each stick went, once and again when further, so a
log tells whether a controller does this. pad_round_gate=0 in bbport.ini
passes the sticks on as they come.

tests/test_pad.c covers it (and now links libm).
2026-10-14 15:16:55 +01:00
ayozetrandSupermedo 1ccef67d2c Log: the memory in use once a minute
"Memory:" in the log every minute: VRAM, the texture cache, its images unused
for a minute and of them the GPU-written tiled ones (which the collector
cannot free), and the process's resident and anonymous memory. A log that ends
in a crash after an hour of play then shows what was growing (#1, #163).

After Supermedo/bloodborne_pc 5dc1f11; the process's memory comes from
/proc/self/status, as statm's first field is the address space, huge with the
game's reservations.

Co-Authored-By: Supermedo <[email protected]>
2026-10-14 13:51:32 +01:00
ayozetrandAndiMajore a643904df0 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 one line in the log. With BB_FAULT_WAIT=1 (launcher: extra
environment) 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.

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 describes it.

From deadinside28/bloodborne_pc#165.

Thanks: AndiMajore, in 7 languages.

Co-Authored-By: AndiMajore <[email protected]>
2026-10-14 13:49:52 +01:00
ayozetr a3e31c7227 Online: an exit() with the network worker running no longer aborts
NpHandler::Shutdown() is never called, so at exit() (a STOP with code 21 or
22, the online test) the worker thread was still joinable and std::thread's
destructor aborted the process, which also hid the real exit code. The
handler's destructor now tells the worker to stop and lets it go. The game's
normal quit uses _exit() and was not affected. tests/test_online.c passes again.
2026-10-14 13:48:24 +01:00
ayozetr 6b12b22376 Online: the CRLF line endings back in four files
2bcc093, 576a214 and b3c8b1d rewrote net_util.cpp, np_handler.cpp and
shadnet/client.cpp/.h with LF endings, so against upstream every line of
them showed as changed. Endings only; no code changes.
2026-10-14 13:48:24 +01:00
ayozetrandJoão Pedro Valadares a36201f796 Keyboard movement: a round diagonal, and the last of two opposite keys wins
- W+A gave the left stick the corner of a square, about 1.41x a thumbstick's
  reach. A diagonal of the movement keys is now full deflection within a round
  gate, as on a controller; stick_circular=0 in bbport.ini gives the corner.
- Opposite keys held together cancelled out, so switching A <-> D (both down
  for a moment) let go of the stick and ended a sprint. The one pressed last
  wins now, on both sticks; stick_socd=neutral gives the old behaviour.

tests/test_pad.c covers both. After João Pedro Valadares's keyboard movement in
Supermedo/bloodborne_pc (576514c, a147eb8), adapted to bbport.ini's controls;
its optional release delay and smooth turns (off by default there) are left out.

Thanks: Jhony-Jhansen, in 7 languages.

Co-Authored-By: João Pedro Valadares <[email protected]>
2026-10-14 13:48:15 +01:00
ayozetr 3c2033b5d0 Steam Deck Game Mode: pressure-vessel's Vulkan settings are not the user's
In Game Mode Steam runs a non-Steam game in pressure-vessel, which points
VK_DRIVER_FILES, VK_ICD_FILENAMES and the layer paths at its own manifests.
The Nix package took them for the user's own override and never registered
its Mesa: no VK_KHR_surface, no window. The AppImage (sharun) merged them
with its drivers, so pressure-vessel's proxy driver came along too.

Both entry points (packaging/bbport-entry.c and the AppImage's bin/bloodborne)
now drop the Vulkan variables that point into pressure-vessel, keep them as
BB_HOST_* for the log, and turn off the host's gamescope WSI layer with them.
A variable the user set elsewhere stays. Checked with the AppImage's own
loader: only the package's Mesa and the host's NVIDIA driver are listed.

From deadinside28/bloodborne_pc#172. Thanks: segasonicye (diagnosis and
workaround), in About → Thanks in 7 languages.
2026-10-14 13:44:23 +01:00
ayozetr d5c2611f72 0.51-linux.5 2026-10-14 13:20:52 +01:00
ayozetr 73f3bb512a Merge branch 'fix/issue5-loading-freeze' 2026-10-14 13:13:42 +01:00
ayozetr 5e86d0bde7 Thanks: Supermedo's line names the skipped pipelines too
49a37b4 (draws of a pipeline the driver cannot create are skipped) came from
Supermedo/bloodborne_pc 188c1f6; his line in About → Thanks now says so, in
7 languages.
2026-10-14 13:13:37 +01:00
ayozetr 30b3409674 Merge branch 'main' into fix/issue5-loading-freeze 2026-10-14 13:00:42 +01:00
ayozetr b3c8b1dadd Online: a server that refuses this build is said plainly, and not retried
The public shadNet server now refuses bbport's client version (ClientRejected,
"doesn't accept your shadPS4 build") and signs the game out. We then retried
the sign-in, which the server refuses with LoginError for a while, longer with
each try, and the player only saw the game's "not signed in" message.

- the client remembers the refusal; the handler signs that user out without
  reconnecting
- the game prints "Online: the shadNet server ... does not accept this build"
  with the server's message, and the launcher shows it in a dialog that points
  to the own-server option (7 languages)
2026-10-14 12:48:44 +01:00
ayozetr 697af8936f Merge branch 'main' into fix/issue5-loading-freeze
# Conflicts:
#	launcher/bbport_i18n.py
#	launcher/bbport_launcher.py
2026-10-14 12:21:14 +01:00
ayozetr 576a214b5f Online: the game's play log telemetry is not sent
With online play on, the game uploads FromSoftware's "Playlog" telemetry
(deaths, map changes, weapon switches, ...) to bb-playlog-prod or
bb-playlog-test on Amazon S3, hosts written in the game's own executable.
Their names no longer resolve, as with no network; the log says so once.
2026-10-14 12:16:06 +01:00
ayozetrandpmwork-personal 8d1db30f9e Ultrawide: widen the view for outputs wider than 16:9, and keep custom sizes
- An output_res wider than 16:9 (5120x2160, 3440x1440, ...) filled the screen
  but kept the game's 16:9 projection, so the picture was stretched. patches.py
  now takes the community resolution patch for that size (Kyo's 21:9 and 32:9
  ones in Bloodborne.xml, or one of the same aspect) and applies only its writes
  that widen the game's screen: not the scene and UI sizes or the heap patch,
  which bbport sets itself. bbport.ini aspect_patch=0 turns it off.
- The in-game menu and the launcher listed four sizes: a custom output_res
  became 3840x2160 with the first saved setting, and 1280x720 in the launcher.
  Both keep it now until another size is chosen; tests/test_settings.cpp
  covers the menu side.

From deadinside28/bloodborne_pc#183.

Thanks: pmwork-personal, in 7 languages.

Co-Authored-By: pmwork-personal <[email protected]>
2026-10-14 12:14:54 +01:00
ayozetrandAryxa 16a08695c3 Launcher: expose the host's libcuda.so.1 to the AppImage for DLSS on NVIDIA
With DLSS loaded, NGX makes NVIDIA's driver dlopen libcuda.so.1, but the
driver cache linked only libnvidia-*, libGLX_nvidia and libEGL_nvidia, and the
AppImage's glibc does not search the host's library folders: the load failed.
libcuda.so* joins the patterns; test_packaged_vulkan checks the link.

From deadinside28/bloodborne_pc#137.

Thanks: Aryxa, in 7 languages.

Co-Authored-By: Aryxa <[email protected]>
2026-10-14 12:11:52 +01:00
ayozetrandAyrtonSanabio b4d74c88f7 Menu language: --play passes the launcher's, and only a chosen one is saved
- play() never called set_language(), so --play (Steam Deck game mode) gave
  the in-game menu BB_MENU_LANGUAGE=ru whatever the launcher's language was.
- Save() wrote menu_language every time, so the language the menu merely
  started with went into bbport.ini with the first changed setting and stayed
  there. It is written now only when chosen in the menu or already in the file.
  tests/test_settings.cpp covers it.

From deadinside28/bloodborne_pc#153 and #154.

Thanks: AyrtonSanabio, in 7 languages.

Co-Authored-By: AyrtonSanabio <[email protected]>
2026-10-14 12:11:31 +01:00
ayozetr bb3a9d126d GPU breadcrumbs: off when the marker function is missing, instead of a crash
deadinside28#177: built against other Vulkan headers, the buffer marker
function stayed null although VK_AMD_buffer_marker was on, and the first
marker crashed. Breadcrumbs now check the function too and say they are off.
2026-10-14 12:10:36 +01:00
ayozetrandemurphy04 3d2b8b3067 AvPlayer: a queue cleared while a frame is taken no longer crashes
AvPlayerQueue::Pop checked the size outside the lock, so a Clear() from
another thread (the player stopping, the game closing) between the check and
front() read an empty queue, and GetVideoData/GetAudioData used the empty
result without checking it. The check is under the lock now and both callers
give up when nothing came back.

Thanks: emurphy04, in 7 languages.

Co-Authored-By: emurphy04 <[email protected]>
2026-10-14 12:10:04 +01:00
ayozetrandparsifal295 186a342750 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.

Thanks: parsifal295, in 7 languages.

Co-Authored-By: parsifal295 <[email protected]>
2026-10-14 12:09:42 +01:00
ayozetrandStefano Scalfari 2bcc093bd9 shadNet: report the client version after signing in
A shadNet server with clientversions.cfg rules signs out a client that never
sends SetClientVersion, then refuses its logins with LoginError for a while.
We sent nothing, so on the public server the game went offline a few seconds
after signing in and the next logins failed.

- send SetClientVersion (command 13) right after the login reply; the string
  is "shadPS4 bbport", BB_CLIENT_VERSION overrides it
- log the ClientRejected notification (19) with the server's message
- keep the SetClientVersion reply away from OnScoreReply

Thanks: Vorshim92, in 7 languages.

Co-Authored-By: Stefano Scalfari <[email protected]>
2026-10-14 12:08:54 +01:00
ayozetr 8937a721ca GPU: say so when a submission is not finished after 10 s
Semaphore::Wait waited without a limit, so a GPU stuck on a draw (the
presenter waiting for frames ahead, as in deadinside28#184) froze the game with
nothing in the log. It now waits in 10 s slices, prints once that the
submission is late with the GPU breadcrumbs, and says when it finishes after all.
2026-10-14 12:08:16 +01:00
ayozetr 4f3187843f Launcher: a failed update check says why instead of staying on "Checking"
When the check failed (offline, GitHub's rate limit) the result is the
error text, and the update row read its "tag" before seeing that, so it
raised and the row stayed on "Checking…". It now shows the error.
2026-10-13 22:22:55 +01:00
artemkaVG da5561d6c7 Texture cache: do not take a color image at the stencil address 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. Its author saw the NVIDIA GPU hang (Xid 109)
go away in Uncharted 2 with it; maybe related to #5. Thanks entry added.
2026-10-13 22:22:55 +01:00
ayozetr f9186dbb8d Presenter: say so when a frame or a swapchain image does not come for 10 s
The frame fences 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 in the log (#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.
2026-10-13 22:22:55 +01:00
Mrsuss60 1b93552dca Pad: the touchpad from the keyboard as a real touch, for the gestures and personal effects menus
Tab and Backspace (and an injected touchpad press) sent a touch with id 0
and no hold time. Each press now gets its own id (1..127) and the time held
since it went down, as the pad reports a touch.

From Mrsuss60/bloodborne_pc_windows_port@fb0d4db (#56). Thanks entry updated.
2026-10-13 22:17:40 +01:00
ayozetr d7a94e5dfc README: badges for downloads, latest release, license, platform and Ko-fi 2026-10-13 16:52:03 +01:00
ayozetr e2b3f73e71 Launcher: icons in About and Settings, whole tab names, uncut text
- About: GitHub's mark beside the three project links and a certificate
  beside the license, which now reads "GNU GPL v2 or later · No game files
  included."; that Bloodborne belongs to Sony Interactive Entertainment and
  FromSoftware is plain text under everything.
- Settings: Steam's and Discord's marks beside Add to Steam and the Discord
  status.
- The icons are the desktop theme's (github-, steam-, discord-symbolic); a
  theme without them gets the launcher's own (launcher/icons, from Simple
  Icons, CC0), not the installed app's colour icon.
- The tabs show their names whole (icon above the name), where they were cut
  off ("Ajus…", "Trof…") at the default width.
- Text is drawn with GTK's cairo renderer: its GPU renderers cut the top row
  of pixels off the letters at fractional display scales. GSK_RENDERER set by
  the user still wins; the game draws with its own Vulkan.
- The launcher language's title says "Launcher language" once in English.
2026-10-13 12:58:05 +01:00
ayozetr 1ce06532c2 Thanks: GerasimosKan's trophy chime too
His line in About → Thanks names the chime as well as the English trophy names.
2026-10-13 11:26:47 +01:00
ayozetr 49a37b4459 GPU: 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 already rebuilt the
cache.

After Supermedo/bloodborne_pc 188c1f6, adapted to the background compiles.
2026-10-13 11:26:21 +01:00
ayozetr 25cdef7482 Trophies: a short chime when one is earned
When a trophy's notice shows, a bell of our own plays: two notes, a third for
the platinum, synthesized on its own SDL stream beside the game's (no audio
from the console). "Sound when a trophy is earned" in the menu's Other section
turns it off (trophy_sound in bbport.ini); BB_AUDIO=none keeps it silent like
the game.

The chime is GerasimosKan's, from deadinside28/bloodborne_pc#127.
2026-10-13 11:26:21 +01:00
ayozetr 9413c82bfb Trophies: English names without the trophy key
Without the PS4's trophy key the lists in the dump stay encrypted, and the
notices and the Trophies page had only icons and numbers. For Bloodborne the
names, descriptions, grades and hidden flags now come from PSN's English list
(scripts/trophies.py BLOODBORNE_ENGLISH), with the platinum counting 1-33; with
the key they are still the game's own, in its language. Checked against the
dump's encrypted list.

The list is GerasimosKan's, from deadinside28/bloodborne_pc#127; About →
Thanks names him.
2026-10-13 11:26:16 +01:00
ayozetr f4a12043f6 Launcher: the shader cache's size, and a button to clear it
Settings → Performance shows how much the port's shader cache takes (the
shaders the GPU library translated, <user>/cache) and clears it after asking,
while the game is not running: the next start builds the shaders again, with
some stutter at first in each area. For a picture that looks wrong after an
update. Saves, settings and the drivers' own caches stay.
2026-10-13 11:15:23 +01:00
ayozetr 7e02e06139 FSR 4: the balanced model for Performance and Ultra Performance
The v07 performance and ultra performance models leave trails behind moving
characters in Bloodborne at 2x and 3x upscaling; the balanced model handles
those ratios cleanly. Both presets keep their render size and use it. Tested
at 1280x720 to 2560x1440 and the Ultra Performance ratio.

From IFreemz/shadPS4-Bloodborne-DLSS-FSR (9b2766e), whose line in About →
Thanks now says so.
2026-10-13 11:08:33 +01:00
ayozetr 7135aa9288 0.51-linux.4
The launcher's version for the About page's updates.
2026-10-13 10:40:20 +01:00
ayozetr 48de7625b5 AppImage: Mesa's Vulkan drivers for AMD and Intel again
quick-sharun collects Mesa's Vulkan drivers from the build host, and the host
had NVIDIA's driver only: no AMD or Intel driver went into the package, and
the game found no GPU on those cards ("Failed to enumerate physical devices:
ErrorInitializationFailed", #4), in every release built that way. The script
now stops when vulkan-radeon or vulkan-intel is missing on the host or from
the AppDir. sharun points Vulkan at the packaged drivers, plus the host's
NVIDIA driver when there is one.

Thanks: nr1971, who reported it.
2026-10-13 10:40:20 +01:00
ayozetr 1ca5a39b3a Launcher: a Thanks page in About
"None of this would be possible without them": everyone whose work is in
this fork, with what it brought, and those who helped it with reports and
tests. Each name opens its GitHub profile; the people credited on Nexus Mods
and the game patches' authors are listed after them. Someone whose work was
only looked at is not listed: whoever a commit brings something from is
added in that commit. In every launcher language.
2026-10-13 01:49:20 +01:00
ayozetr 34ca9d1340 0.51-linux.3
The launcher's version for the About page's updates.
2026-10-13 01:26:23 +01:00
ayozetr b18055557b On-screen keyboard: the letters of the menu's language
The keyboard had the English letters only, so a controller could not type
the ñ that a physical keyboard gives the name. A row with the menu
language's own letters now sits above the actions: Spanish ñ, the accented
vowels, ü, ¿ and ¡; the accents of Italian, French and Portuguese, ç among
them; German's umlauts and ß. English, Russian and Chinese keep four rows.
2026-10-13 01:22:04 +01:00
ayozetr ebb79d3774 Loading and smoothness: three game patches from bbhost
A new group in the launcher's Settings page, with patches from
droogie/bbhost (patches/*.toml), each checked against the 1.09
executable's original bytes, noted beside it:

- Quick loading after death (on): a load after a death or lamp travel
  ends when the area is ready, 2-3 s, not after the 12-second minimum
  the in-game step waits out. The multiplayer wait stays the game's.
- No stutter every ~50 s (on): the player's recording is deflated on the
  main thread at level 9, ~23 ms in one frame; level 1 does it in a
  fraction of that, and inflate reads either.
- Third resource preparation thread (off, experimental): the resource
  system's third post-processor keeps running in play.

The first two are on also when bbport.ini does not name them yet.
2026-10-13 01:22:04 +01:00
ayozetr 479e964d90 Overlay: a notice while shaders compile
"Compiling shaders…" in the bottom-left corner while the pipeline cache
compiles a shader or pipeline, kept 400 ms after the last one and faded
out over 400 ms more, so a burst does not flicker. A stutter at that
moment is the shader cache filling, once per place, not a fault worth
reporting. On by default; "Notice while shaders compile" in the menu's
Other section turns it off (shader_notice in bbport.ini).

From SrShadowy/bloodborne_pc (gpu/shim/bbport_shader_tracker), counted
in the existing CompileTimer.
2026-10-13 01:22:00 +01:00
ayozetr fb25a78b5d On-screen keyboard for the character name, typed with a controller
The game's text dialog (ImeDialog: the character's name) could only be
typed on a physical keyboard, so in the Steam Deck's Game Mode or with a
controller on the sofa a character could not 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.

From SrShadowy/bloodborne_pc (gpu/shim/bbport_keyboard.cpp), reworked:
the keyboard is drawn in the existing dialog and its edits are queued
to the window thread, which owns the text and the window. There, the
keyboard's mouse clicks changed the text and the window title from the
render thread. Its texts are in the menu's languages.
2026-10-13 01:22:00 +01:00
ayozetr 5bf966b85a Launcher: Test the connection signs in with the account too
It only checked that the servers answered, so a wrong name or password
showed up in the game as "not signed in to PlayStation Network". With
the account filled in, the test now signs in to the shadNet server as
the game does (ServerInfo, then a Login request) and leaves at once,
and says what the server answered: signed in, wrong name or password,
no such account, or already signed in (the game running with the same
account). srv.shadps4.net answers "wrong password" for an unknown name
as well, so the text names both.

Tests against a stand-in server check the request and each answer.
ONLINE.md says so, and that the game now keeps trying when the server
did not answer at start.
2026-10-13 01:07:23 +01:00
ayozetr e7f5b9f575 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,
Tailscale 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 dropped
connection was retried only by the worker a first successful sign-in
started. An unreachable server now schedules the same background retries,
with a backoff of up to 30 s, and the game signs in once it answers. A
sign-in that fails because the server did not answer is retried too.

Each user's latest attempt keeps how it ended. A server that answers and
refuses (wrong name or password, an incompatible protocol) is not retried,
at start or after a drop, since retrying cannot help.
2026-10-13 01:07:23 +01:00
Supermedo 51927fb018 SRT walker: leave speculative walks to their recovery point
Draw preparation workers and the recording thread's table check walk resource
tables ahead of the game and may read memory it has reused. Their faults went
to the SRT walker handler, which patched the walker for good: one stale read
made that load zero for the rest of the session. They now go back to their
recovery point, and the GPU thread walks the tables itself.

The patch is also written in an order that keeps every intermediate state
harmless for threads running that code: the nops from the end, then the ModRM
byte, the REX prefix and the xor opcode last, with atomic stores.

From Supermedo/bloodborne_pc 479f2f5, on top of 0xCydral's lock and retry.
2026-10-13 00:57:32 +01:00
0xCydral c5c983c248 fix(gpu): draw preparation steps over an invalid PM4 header like the GPU thread
The GPU thread steps over an invalid packet header (a type 0 header
Bloodborne leaves in a command buffer at an area load) instead of ending
the game, but the draw preparation scanner still stopped reading that
buffer there. Its register state then differed from the GPU thread's for
the rest of the session, so no prepared draw was used again and every
new pipeline was compiled on the GPU thread: on an RX 6700 XT draw
preparation fell from about 96% used to 0% after one such header, with
stutter. Both now skip by one rule (pm4_resync.h); pm4-resync-test
checks it on the bytes from that log.

From 0xCydral's ed19a84
2026-10-13 00:57:32 +01:00
0xCydral a80e1ed7d5 fix(shader): an exception in speculative draw preparation ends the attempt, not the game
Draw-preparation workers specialize shaders from game memory the game may
still be writing. A fault there already ends the attempt and leaves the
draw to the GPU thread; 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. One cold start in fourteen died that way
(exception 0x20474343, exit 3) and could not be reproduced. The
speculative specialization now treats an exception like a fault.
siglongjmp restores registers without unwinding, so a fault jump out of
the new try block is unaffected.

From 0xCydral's df619c7 (adapted: here only the specialization runs
ahead of the GPU thread)
2026-10-13 00:57:32 +01:00
0xCydral a07910eb89 fix: a failed check in speculative draw preparation abandons it instead of stopping the game
Draw-preparation workers specialize shaders ahead of the GPU thread from
descriptors the game may still be rewriting. Faults there already
returned to the worker's recovery point; a failed check stopped the port
with exit 23. It now leaves through the same recovery point and the GPU
thread prepares the draw in order. The SRT walker fault handler disarms
the recovery point while it holds its lock.

From 0xCydral's 917d2c0 (adapted: here the workers specialize programs
already translated, they do not translate new ones)
2026-10-13 00:57:32 +01:00
0xCydral 2e4a55f8bb fix(gpu): 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. The pipeline cache checks that a stage's 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 b99535b, itself from yumlevi/bloodborne_pc ce385b6 and
GoncaloLobo0/bloodborne_pc_win ef70ed2. Its other half, hashing textures
through the backing view, is left out: here a read-protected page holds
GPU-written data the fault reads back, and the backing view would hash
the stale copy.
2026-10-13 00:57:32 +01:00
0xCydral f1d897ea0d fix(shader): SRT walker fault handler is safe for concurrent draw-prep workers
Two draw-preparation workers can fault on the same SRT-walker store. The
second one decoded the already-patched instruction and tripped the MOV
ASSERT (exit 23). The handler now takes a mutex and returns early when the
instruction is already the xor patch. Registration of the handler goes
through call_once, and walkers loaded through RegisterWalkerCode register it
too: when every walker came from the pipeline cache, it was never
registered.

From Mrsuss60/bloodborne_pc_windows_port b7001bc (fault handler hunk only,
merged by hand)
2026-10-13 00:57:32 +01:00
0xCydral 9779cbbc70 fix(fsr4): depth barriers name the depth and stencil aspects
The vendored FSR 4 backend issued its layout barriers on every external
image with VK_IMAGE_ASPECT_COLOR_BIT, invalid for the D32_SFLOAT_S8_UINT
depth image (VUID-VkImageMemoryBarrier-image-03319 and
subresourceRange-09601, 20 each in a validation run).

New fsr-vulkan patch 0002 adds an optional aspectMask to
FfxFsr4VkExternalImageState (0 = color); Register passes DEPTH|STENCIL for
the depth image.
2026-10-13 00:57:32 +01:00
andrecabralgit e36717374e 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-10-13 00:48:43 +01:00
Jgamrot 542e9ada19 Menu: checkboxes and sliders keep their changes with any compiler
Store(value, v, ImGui::Checkbox(label, &v)) read v before the checkbox had
changed it on compilers that evaluate arguments left to right: C++ leaves the
order unspecified. GCC happens to call the widget first, so our builds worked;
clang lost every change. The widget now runs before Store.

From Supermedo/bloodborne_pc#19 (5c660f9).
2026-10-13 00:48:26 +01:00
ayozetr 9e629d2efd Launcher: one shadNet account for each kind of server
shadPS4's public server and a server of your own have their own accounts, but
the Online page kept a single online ID and password, so switching servers
sent one server's account to the other. The page now keeps one account for
each (online_accounts) and swaps them when the server changes. Settings from
before give their account to the server chosen then.
2026-10-12 17:43:38 +01:00
ayozetr 74584d57b5 Trophies: the platinum no longer waits for the add-on's trophies
On the PS4 the platinum needs the base game's trophies only; ours waited for
every other one, so without The Old Hunters' six (34-39) it never came. The
trophy list says which ones count: their pid names the platinum, and an
add-on's is -1. scripts/trophies.py keeps that as a sixth column of
trophies.tsv, and the runtime awards the platinum once those are earned. A
list without the column, from an older run, still needs every other trophy.
2026-10-12 13:05:43 +01:00
ayozetrandQiming-Liu 40b67fc784 Translations: native wording for the Simplified Chinese launcher
Takes Qiming-Liu's wording from deadinside28/bloodborne_pc#157 for the
21 strings where ours differed: 联机 and 账号 for online play and the
account, 「」 quotes, 检测 for the connection check, 自定义 for a custom
file, and a plain "{} / {}" for the DLL pass counter.

Co-authored-by: Qiming-Liu <[email protected]>
2026-10-12 13:01:22 +01:00
ayozetr 82923859db Launcher: swap A and B for Nintendo-style controllers
A switch under Controls binds cross to the gamepad's east button and
circle to the south one (pad.cross=b, pad.circle=a in bbport.ini), and
the Xbox and Switch prompts swap their cross and circle glyphs to match.

Closes #3
2026-10-12 10:20:59 +01:00
ayozetr 2ea5b61641 Issue templates: a bug report form, a feature request, and links to the guides
The bug report asks for the version, the system, the graphics settings, the online mode and the exported log, and that the dump is the reporter's own. Blank issues are off; the troubleshooting and online guides are linked instead.
2026-10-11 23:52:14 +01:00
ayozetr 0ca95492c4 In-game menu in Simplified Chinese
The overlay menu (Insert / L3+R3) and the trophy notice get Simplified
Chinese, the launcher's eighth language: a column in
bbport_menu_i18n.h, zh_CN in bbport.ini and BB_MENU_LANGUAGE, and
简体中文 in the menu's language list.

The menu's font has no Chinese characters. The first time the menu
opens or speaks Chinese, a system font is merged into it: BB_CJK_FONT,
else fontconfig's choice for zh-cn (Noto Sans CJK, WenQuanYi), checked
to cover zh-cn since fontconfig answers with some font regardless.
ImGui 1.92 rasterizes glyphs as they are drawn, so only the file is
loaded, not a texture of every character. Without such a font the log
says what to install.

The game's own System menu pages are drawn with the game's font, which
has no Chinese characters in the western releases: they stay in English
for Chinese.
2026-10-11 18:52:39 +01:00
ayozetr 4fd797f7e9 README: the launcher is in Simplified Chinese too 2026-10-11 18:32:18 +01:00
ayozetr 1ebe35c2e8 Translations: Simplified Chinese launcher complete, and the game menu's missing texts
The launcher's Simplified Chinese came from upstream and stopped with
the texts it had; the 87 added since (Online, Trophies, Discord, Steam,
DLC, the log export, Ko-fi) are translated now, and the test keeps it
complete like the other languages.

Nine texts of the in-game menu (Insert / L3+R3) showed in English in
Spanish, Italian, French, German and Portuguese: the DLSS, FSR 4.1.1
and FSR 4 hints, the free camera and debug menu hints, and the quit
buttons. Four of them were translated in an older wording that still
said FSR where the menu now says upscaler.

test_menu_i18n.py checks that every text of the overlay and the game
menu has a row in bbport_menu_i18n.h, with every language and the same
printf fields.
2026-10-11 18:31:06 +01:00
devilismyfriend 17e7741934 LRU cache: the early stop of ForEachItemBelow works
It compared std::invoke_result<...> (the trait, not its type) with bool,
so a callback returning true never stopped the walk: every texture cache
collection visited every old image, past its deletions and its 256-image
cap.

From yumlevi's "Texture cache: images it must keep no longer block
evictions" (d7a53a5); its other half, kept images not using up the
deletions, was already here.
2026-10-11 18:14:05 +01:00
ayozetr 97a60dd497 0.51-linux.2, and co-op on your own server tested with two players
The launcher's version for the About page's updates. docs/SERVER.md and the README say that a
private server has been tested with a Linux and a Windows player: summons, a boss fight together
and summoning again after the helper died.
2026-10-11 16:51:45 +01:00
0xCydral a3a8a3af6e fix(vulkan): include vulkan.hpp with NDEBUG so the dispatcher layout matches
vulkan.hpp adds two fields to DispatchLoaderBase when NDEBUG is undefined, which moves every
function pointer in vk::detail::DispatchLoaderDynamic by 16 bytes. libbbgpu is built with NDEBUG
while the renderer tests build with -UNDEBUG so their asserts run, so the two sides disagreed on
the layout. The linker keeps one inline copy of DispatchLoaderDynamic::init(Instance): the test
translation unit's copy read vkGetInstanceProcAddr at offset 88 while CreateInstance had stored it
at offset 72, and Instance construction called a null pointer (exit 139 on Windows).

vk_common.h now defines NDEBUG around the vulkan.hpp include and restores assert() afterwards,
so every translation unit sees the same layout while test asserts stay active.

(cherry picked from commit 03b383b34ccb74085d74baa1e3b87ca0b50013aa)
2026-10-11 16:50:59 +01:00
0xCydral f1091cc0b1 fix(gpu): 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 test route, 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; 85 px at 1706x960).

Measured on 24 frames at the same moment: object vectors over 100 px went
from 2.8% of the picture on the worst frames of two runs to none. The camera
motion test gains two cases, a far object vector that is rejected and a 30 px
one that is kept; that test does not run on Windows yet.

(cherry picked from commit a12911460872b37aba2bff4895903ad20aabf1e2)
2026-10-11 16:50:59 +01:00
0xCydral c496667284 feat(log): a performance line every 30 seconds in normal play
Players' last_run.log 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".

(cherry picked from commit 6c73bf375ea55972fd19bb5dcc254a92ab57ee54)
2026-10-11 16:50:59 +01:00
0xCydral a8b7a8eaa1 fix(gpu): keep the default indirect guard limit when BB_INDIRECT_GUARD_MAX is not a positive number
An unparsable value gave strtoul 0, so the shader skipped every indirect dispatch without a word. A value that is not a number from 1 to 4294967295 now keeps the default and prints one warning line.

(cherry picked from commit 62177377cf6c4a390d95d61b804505b077e7648c)
2026-10-11 16:50:59 +01:00
0xCydral 4d934b7529 fix(run.sh): give an explicit render size above 1080p the larger direct memory
run.sh raised BB_DMEM_MB to 9152 MiB only on the scaled path. An explicit BB_RENDER_RES skips that path, but patches.py still grows the graphics heap above 1080p, so the game asked for more memory than it had and stopped at start (exit 139). run.sh now does what run.py does since e5276cf: a WxH size above 1920x1080 by pixel count gets 9152 MiB unless BB_DMEM_MB is already set.

(cherry picked from commit 674b72e650f990dce906bc43446d024ab7109520)
2026-10-11 16:50:59 +01:00
0xCydral 442c2b66bd fix(gpu): resync the graphics command decoder at an invalid PM4 header
A cut-short SET_SH_REG leaves the next packets inside its body, and the
decoder then hit UNREACHABLE (Unimplemented PM4 type 0) at area loads. On
that path only, decoding now resumes at the next dword that can start a
type 3 packet, so the rest of the buffer and its fences still run. Each
resync logs the header dword and its offset; the buffer report stays
limited to the first 8.

From GoncaloLobo0/bloodborne_pc_win a334d26 and 9dc909d (adapted: crash path
only, no look-ahead on valid register packets)

(cherry picked from commit e0b464db61362d5c8b966ec645c592cf0a680667)
2026-10-11 16:50:59 +01:00
0xCydral d7a42fde94 fix(motion): object vectors for 16-64 KiB bone palettes; unknown SnormNz format is not fatal
Palettes of whole bones between 16 and 64 KiB (24576, 49152 bytes: the
large beasts in Central Yharnam) are now LargeSkeleton: gated like small
skeletons (history only while the palette changes). BB_MOTION_LARGE=0
restores the old rule (camera vectors only).

MapNumberConversion returns NumberConversion::None for a data format with
no SnormNz mapping instead of hitting UNREACHABLE (seen with data format 45,
a stale or unused descriptor).

From bmy/bbport-mac 80898cf and 5cf5684 (gpu part only)

(cherry picked from commit 4ff6b6292df844129332d75c6a1eb56eda14232b)
2026-10-11 16:50:59 +01:00
0xCydral d0318b1ab5 fix(upscaler): UI mask draws write the UI's output-size stencil
Scaleform masks are drawn with color writes off, so they reached the UI
composition without a color target and stayed in the game-size depth. The
next UI draw switched to the output-size UI depth, whose stencil
PrepareUiDepth clears, and the mask was gone: images cut out through a mask
(the loading screen's item picture) vanished at output sizes other than
1080p.

The frame generation HUD-less capture is unaffected: it runs in the
!Scaled() path before ui_phase, and the new branch only applies in ui_phase.

From bmy/bbport-mac e4f7d33

(cherry picked from commit 55b4a0d7247ec05dafc0d40643a2a146869c9531)
2026-10-11 16:50:59 +01:00
0xCydral 1bc4f37504 fix(texture-cache): hash the same guest range at marking and refresh
MarkAsMaybeDirty hashed the whole image but RefreshImage only its first 8x8
pixels, so the two never matched and stale guest memory replaced
GPU-written small images (black character preview, missing loading-screen
item picture). Both now use MaybeDirtyHash, and every upload records its
mip hash so an unchanged guest copy is not re-uploaded over GPU contents.

From yumlevi/bloodborne_pc 43fcf95

(cherry picked from commit 0d2dfd2f4ba3a13d5bee415769f5f0ff4f58fb2e)
2026-10-11 16:50:59 +01:00
0xCydral 44a6b7c3ce fix(shader): ReadUdReg on a null user-data table returns an empty sharp
An unbound descriptor table dereferenced a near-null pointer (access
violation at 0x8 on map load). Return a zeroed value so the existing
empty-sharp handling runs.

From Mrsuss60/bloodborne_pc_windows_port 8d8ac28 (info.h hunk only)

(cherry picked from commit 5e4513bad8b952bea95f6df76d94c97d5e827de8)
2026-10-11 16:50:59 +01:00
0xCydral efdf7b176e fix(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.

From bmy/bbport-mac 9ffc78d

(cherry picked from commit 389a1e5c3573460f73f96f255f5fbf7da43c981c)
2026-10-11 16:50:59 +01:00
ayozetr 4f7df3de5c Trophies: a notice in the game and a Trophies page
The game's trophies come from the player's own dump: scripts/trophies.py reads
sce_sys/trophy/trophy00.trp, keeps its icons, and decrypts the trophy lists with the PS4's
trophy key (shadPS4's TrophyKey, entered in the launcher), in the game's language. Without the
key, only the icons. AES is in Python, as the AppImage has no crypto package.

When the game unlocks a trophy, the runtime records it in earned.tsv, the first time only, and
the overlay shows a card in the top-left corner in the style of the PS4's: the trophy's icon,
"You have earned a trophy." and its name. The platinum follows once every other trophy is
earned. The launcher's Trophies page lists them all, earned first, with the date; hidden ones
stay hidden until earned. BB_TROPHY_PREVIEW=<id> shows a trophy's notice at the game's first
trophy call, to see how it looks.
2026-10-11 15:35:42 +01:00
ayozetr e3c51c48aa Discord Rich Presence: the area and the co-op partners
Discord shows the area the player is in, and under it "Co-op with <partners>", "Online" or
"Offline". The game writes both into BB_PRESENCE_DIR, which run.sh makes new for each start:

- The runtime takes the area from the MapStudio files the game opens, one per map it loads.
  The player's own map comes first, then the maps around it (Central Yharnam brings the
  Forbidden Woods), so a map opened 5 s or more after the previous one starts a new load and
  names the area. It changes at loading screens only.
- The online module writes the online IDs of the players it has an established connection
  with, when a connection is established or removed.

The area names are those of Smithbox's Bloodborne map aliases (MIT); the Chalice Dungeons are
one name.
2026-10-11 15:11:09 +01:00
ayozetr 33393185b9 Support on Ko-fi: the About page, the README and GitHub's Sponsor button
A "Support this project" row in the launcher's About page opens ko-fi.com/ayozetr, the
README has a Support section, and .github/FUNDING.yml shows the link in GitHub's Sponsor
button.
2026-10-11 14:51:58 +01:00
ayozetr c294942db4 Launcher: the game folder names the release that is in it
The row said "Game folder (CUSA03173)" and the check's tooltip "Bloodborne CUSA03173,
version 1.09" whatever the folder held, from when only the European Game of the Year edition
ran. Both now name the title ID in the folder's param.sfo (CUSA00900, ...), or none without a
game.
2026-10-11 14:33:28 +01:00
obrikash 6c6974a742 Fix Vulkan startup when optional DLSS device extensions fail 2026-10-11 13:34:39 +01:00
ayozetr 83246710d7 0.51-linux.1, and what each tool is for
The launcher's version for the About page's updates. tools/README.md tells the tools the port,
the launcher and the AppImage use from bbport's development tools, kept as they are so that
its updates merge cleanly.
2026-10-11 02:53:20 +01:00
ayozetr b71eb7cfee Online: your own co-op server
shadPS4's public server never answers on its UDP port, so summons through it cannot connect
(deadinside28/bloodborne_pc#121). The Online page now chooses between that server and your
own, a shadnet-p2p server for a group of friends: its address and WebAPI, and seamless co-op,
which the server has to have on as well. The co-op port is a setting of its own instead of
SHADPS4_P2P_PORT in the extra variables, and the firewall warning uses it and says that UPnP
opens the port on the router only.

Test the connection pings the shadNet server's UDP port the way the game does and shows the
public address the server sees, or that it does not answer. It uses a made-up online ID: the
server keeps one address per account, and the player's own would replace the address of a
game running at the same time.

tools/shadnet-server builds shadnet-p2p in Docker on Debian 13, for the Qt 6.8 it needs
(Ubuntu 24.04 has 6.4), and keeps the configuration and accounts in a volume. With the
server's Tailscale address, it waits for the address before starting: shadnet-p2p keeps
running without its listeners when the address is missing.

Checked against shadnet-p2p 5a849a1: login, the UDP ping, matchmaking and a summon sign
reaching its summon service. docs/SERVER.md is the guide; ONLINE.md and TROUBLESHOOTING.md
cover online play and the common problems.
2026-10-11 01:41:53 +01:00
ayozetr b6c25e1fb4 Merge upstream 0.51 (master)
The online module from upstream replaces this fork's copy: the same shadp2p libraries, now
in gpu/bbnet as libbbnet.so, loaded only for online play, and UPnP opens the co-op port on
the router. Kept from this fork: game region, firewall warning, About page and updates,
cheats, Discord, title-ID mod folders and the sharun AppImage, which now bundles
libbbnet.so (BB_NET_MODULE). DLSS: the launcher's choice of NVIDIA's library looks in
BB_DLSS_DIR, the AppImage's copy. The launcher's new texts are translated.
2026-10-11 00:28:06 +01:00
ayozetr 0e2b626358 Mods and patches: every retail release, and a guide in English
A mod wrapped in a title ID folder was found only as CUSA03173/dvdroot_ps4; one made with the
US release (CUSA00900/dvdroot_ps4) was not. Any title ID folder is accepted now. Third-party
patch files listed six of the nine retail releases: a file for the Japanese release or the US
and Japanese The Old Hunters editions was skipped. The list is now the game check's.

docs/MODS.en.md is the guide to mods and third-party patches in English, based on bbport's
docs/MODS.md and checked against the code; the README and docs/README.md link to it, and the
Russian original moves to the development notes.
2026-10-10 22:33:31 +01:00
ayozetr b060b9fa2b Document which game releases run, and which have been checked
docs/COMPATIBILITY.md lists the nine retail releases the launcher accepts, with their title and
content IDs (from deadinside28/bloodborne_pc#117), and which ones have had their 1.09
executable checked: the US release (CUSA00900) and the European Game of the Year edition
(CUSA03173). The others are accepted on the expectation that they share that executable, and
the image hash still decides. It also lists what was tested with this fork. The README's note
on game files links to it.

game_check.py said the Asian The Old Hunters Edition (CUSA03023) shares the executable; nobody
has checked it, so it now says what is known.

docs/README.md is an index of docs/: what is for players, what is this fork's, and bbport's
development notes by topic, which stay as they are so that upstream updates merge cleanly.
2026-10-10 22:26:45 +01:00
ayozetr 18a7dd7e39 AppImage: only what the port needs, and DLSS's files intact
quick-sharun's OpenGL and Vulkan deployment copies every libEGL*/libGLX* library and every ICD
manifest of the build host. Built on a PC with NVIDIA's driver, the AppImage carried that
driver's user-space libraries (one exact version, 615.71.09 in 0.4-linux.1) and manifests,
which load next to the player's own driver and cannot match another kernel module. They are
now removed before the AppImage is made; NVIDIA's driver always comes from the host, where
launcher/bbport_vulkan.py points Vulkan.

quick-sharun also strips and patches every library in the AppDir, DLSS's among them. NVIDIA's
DLSS library is signed: changed, NGX refused it (0xbad00012) and DLSS was unavailable in the
AppImage on an RTX 3060 that runs it from the repository. The DLSS files are now copied after
quick-sharun, as built.

DEPLOY_PYTHON copied the build host's whole site-packages (scipy, Qt bindings, matplotlib, ...:
431 MB). Only PyGObject, pycairo and Pillow stay, which is all the launcher imports. The
AppImage goes from 342 MB to 225 MB.
2026-10-10 21:38:02 +01:00
ayozetr 28a9233034 Launcher: About page, and updates from this fork's releases
A new About page shows the version and links this project, the original bbport by
deadinside28 and Supermedo's Windows port, and names the license (GNU GPL v2 or later, no
game files included).

It also checks GitHub for the latest release: at start, unless turned off, and with a Check
now button; a newer release at start is announced with a toast. From the AppImage, Update
downloads the release's AppImage next to the running one and renames it over it (a failed
download leaves the old one as it was); the running copy keeps its mount, and Restart starts
the new one once this launcher has exited (with the same application ID still held, the new
one would only raise the old window). From a git checkout, or an AppImage in a folder that is
not writable, the button opens the release page instead.

The version lives in launcher/bbport_update.py (0.5-linux.1); tags follow
<upstream version>-linux.<n>, and a release carries the AppImage as an asset.
2026-10-10 21:38:02 +01:00
ayozetr 9c056c437f Launcher: warn when a firewall blocks co-op
Co-op peers connect straight to each other over UDP, and UPnP's discovery replies come from the
router on random source ports. With ufw enabled or firewalld running, both are dropped, and
nothing in the launcher said why co-op could not connect.

The Online page now names the firewall and gives the commands: pin the P2P port with
SHADPS4_P2P_PORT=3658, open it, and with ufw let the router's answers in (the default gateway
read from /proc/net/route). Both are detected without root.
2026-10-10 21:38:02 +01:00
ayozetr 2683273a17 Launcher: game region
Bloodborne picks a language's regional text from USER_DEFINED_PARAM_1 in param.sfo: 13 Europe
(Spanish from Spain), 2 the Americas (Latin American Spanish). A US dump played in Spanish showed
the Latin American text, and the only way to change it was editing the dump's param.sfo.

The launcher's Game region (As in the game dump, Europe, the Americas) sets BB_REGION, and
content_profile.py writes the matching parameter for the launch; the dump stays as it is.
2026-10-10 21:38:02 +01:00
ayozetr 4d297b96bb Translate 0.5's new texts, and the game's System menu pages
The launcher's new texts (memory model, log export, monitors, the offline patch, damaged game
files) in Spanish, Italian, French, German and Brazilian Portuguese; Portuguese also gets the
52 texts it never had (controls, game check, frame limit, memory options, diagnostics).

The Display and Effects pages in the game's System menu were English or Russian only. They now
follow the game's language (BB_LANGUAGE, else the desktop's locale, as the runtime tells the
game), with the texts in bbport_menu_i18n.h. The README lists 0.5's new features.

tests/test_i18n.py now checks that every text the launcher translates exists in English,
Spanish, Italian, French, German and Portuguese, and that translations keep the placeholders:
the 18 texts 0.5 added had slipped through unnoticed.
2026-10-10 21:38:02 +01:00
ayozetr 917c4bdd20 Record large copy lists in slices
Loading an area on NVIDIA with 0.5, 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 assertion stopped the game. Buffer copies, guest chunk copies and
buffer-to-image uploads are now recorded as one command per 64 KiB of regions.
2026-10-10 21:38:02 +01:00
ayozetr 8e24cfdcf5 Merge main: the region fix and every retail release
main corrected the regions (CUSA00900 is the US release, CUSA03173 the EU GOTY) and accepts
every retail release; the 0.5 merge still had upstream's older texts. Conflicts take main's
side; the Chinese launcher text of the old message is replaced by the new one.
2026-10-10 21:38:02 +01:00
ayozetr cfede387ed Merge upstream 0.5 (master)
0.5 on top of 0.5-pre4: the memory model chosen by GPU (the new one on AMD, the 0.3 one
elsewhere, or either by hand in the launcher), the command decoder, game profiles, GPU
timestamps, less VRAM churn, the finished Display and Effects pages of the game's System menu,
NVIDIA EGL discovery and RADV nohiz for older AMD GPUs.

Conflicts: README.md keeps this fork's own; tools/gpu_capabilities.c's header now lists both
the gamepad type (button prompts) and upstream's new --device.
2026-10-10 17:37:45 +01:00
ayozetr f44390da19 Game check: every retail release of Bloodborne
The list of content IDs posted by h0kd33 in deadinside28/bloodborne_pc
PR #117 covers every Bloodborne release: the US, EU, UK, Japanese and
Asian ones and the four editions with The Old Hunters. A base game of
any of them without the update is now asked for 1.09 instead of being
called another edition; whether the executable runs is still decided by
its image hash. The message is reworded in every launcher language.
2026-10-10 16:02:32 +01:00
ayozetr c89f4d432c Fix the regions: CUSA00900 is the US release, CUSA03173 the EU GOTY
CUSA00900 was called the EU release and CUSA03173 the US one, the other
way round: their content IDs are UP9000-CUSA00900 (America) and
EP9000-CUSA03173_00-BLOODBORNE0000EU (Europe, Game of the Year edition).
Only names change: the README, BBPORT.md, the release template, the
game check's texts and the launcher's message in every language.
2026-10-10 15:21:24 +01:00
ayozetr 4fd1cd6ee6 Restore what the 0.5 merge dropped: menu languages, cheats, co-op setup
Resolving the merge, three files of the in-game menu were taken whole
from upstream to settle conflicts that were only about DLSS, and
probe.c to drop the old add-on profile. That also dropped this fork's
own changes in them: the menu's Language selector and its Spanish,
Italian, French, German and Portuguese texts, the cheats and gameplay
tweaks, and the bbnet_configure call that gives the co-op code the
game's identity. With online play on, the game stopped at start
(ElfInfo GameSerial assertion). Now merged line by line, with upstream's
version only in the DLSS hunks.
2026-10-10 15:12:23 +01:00
ayozetr cfe7af67a5 Add to Steam: Flatpak Steam
Flathub's Steam keeps its data in ~/.var/app/com.valvesoftware.Steam
and starts non-Steam games inside its sandbox, which has neither the
game folder nor the host's drivers and cannot mount an AppImage. The
launcher now finds it, closes and starts it through flatpak run, and
writes a shortcut that runs the game on the host with
flatpak-spawn --host. That needs Steam to talk to org.freedesktop.Flatpak:
the launcher explains what the permission allows and grants it
(flatpak override --user) only when the player agrees.

A flatpak-spawn shortcut is only replaced when it is this port's (same
name, runs --play), so other launchers' games in Steam are left alone.
2026-10-10 14:10:32 +01:00
ayozetr 124384eac1 Merge upstream 0.5-pre4 (testing)
bbport 0.5-pre1 to pre4: the memory module (now on NVIDIA too), native
GPU copies, background pipelines, settings in the game's System menu,
DLSS, DLC folders and the damaged-extraction check.

Where both sides did the same thing, upstream's version is kept:
- DLSS: upstream's bridge and loader (libraries next to bb-probe, built
  by build.sh with DLSS_SDK_ROOT). BB_DLSS_DIR still overrides the
  folder, for the AppImage; tools/build_dlss.sh now installs into out/.
- DLC: upstream's <user>/addcont/<title>/<label> reporting. The
  launcher's DLC folder (BB_DLC_DIR) is linked there by run.sh, a real
  folder in its place left alone; our content.bin add-on list is gone.
- The memory-model switch is no longer AMD-only, as upstream has it.

Kept from this fork: launcher and in-game menu languages (Simplified
Chinese joins them), the game check for the EU and Asian releases and
the 60 fps-patched eboot (as sent upstream in PR #117), button prompts
and the controller type in bb-gpu-capabilities --gamepads, which also
lists displays now. Upstream's README changes went into BBPORT.md.
2026-10-10 14:07:39 +01:00
ayozetr 5227432b1b Git: keep game files and the mods folder out, quiet FSR-Vulkan
The mods folder of runs from source (mods/) holds the user's modded game
files and was not ignored; neither were dumps and their parts
(eboot.bin, param.sfo, *.pkg, *.dcx, *.7z, CUSA* folders). The repository
never takes any of them.

FSR-Vulkan always showed as modified because build.sh applies
gpu/patches/fsr-vulkan to its working tree: it is now ignore = dirty.
2026-10-10 03:49:42 +01:00
ayozetr 81db68f552 Docs: release notes template, credit for the keyboard prompts
docs/RELEASE_TEMPLATE.md fixes the format of every release: tag and
title, requirements, download with its SHA-256, New / Improved / Fixed,
Upgrading, Known issues, and links to the changes and credits.

The credits now name Abken's KBM Icons Generator (Nexus, Bloodborne mod
537), which showed which textures keyboard prompts need; bbport draws
its own and uses nothing of it.
2026-10-10 03:48:43 +01:00
ayozetr f723f6a972 Button prompts: Xbox, Switch and keyboard, and a cleaner gestures menu
Controls get a Button prompts row (automatic, PlayStation, Xbox,
Nintendo Switch, keyboard) and a switch for the gestures menu without
the motion-control icons. Automatic follows the controller in use: bb-
gpu-capabilities --gamepads now also prints SDL's controller type, and
with no controller the keyboard's prompts are used.

The Xbox and Switch prompts and the gestures menu are Dommo's (Nexus,
Bloodborne mod 30, whose permissions allow reuse with credit). Only what
the mods drew is in assets/menu_prompts: the 20 icons as their BC7
blocks, the strip of big prompts from the UI atlas, and the edits to
gesturetop.gfx; tools/extract_prompt_assets.py rebuilds them from the
mods' ZIPs. Keyboard prompts are drawn with Pillow for the keys bound in
bbport.ini.

At start, scripts/menu_prompts.py puts them into the player's own
menu/common.tpf.dcx (scripts/menu_textures.py: DCX/TPF, PS4 tiling, a
small BC7 encoder) without touching any other texture, caches the
result, and mods.py lays it under the user's mods (--ui-mods). The
AppImage and the Nix package now carry Pillow and DejaVu Sans Bold. The
README also says the in-game menu speaks Portuguese, since c428122.
2026-10-10 03:38:39 +01:00
ayozetr 82a96e0d25 Support the EU release (CUSA00900)
The EU 1.09 executable is the same image as the US one: with a clean
eboot.bin the EU dump passes the check and plays, gestures menu
included. The check now takes CUSA00900 as a supported title, so an EU
base game without the update is told to merge 1.09 instead of being
called another edition. README and BBPORT.md name both releases; the
latter also notes that run.sh follows the desktop's locale without
BB_LANGUAGE (left out of the previous commit).
2026-10-10 02:39:04 +01:00
ayozetr 71717863d5 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 same way the launcher does: Japanese,
English, French, Spanish, German, Italian or Russian, English otherwise.
2026-10-10 02:37:02 +01:00
ayozetr c4281227da In-game menu: Brazilian Portuguese
The launcher already speaks pt_BR, but the overlay fell back to English
for it. Adds Português (Brasil) to the menu's Language selector and a
pt_BR column to the overlay translations; the launcher's pt_BR (and any
pt_* locale) now reaches the menu through BB_MENU_LANGUAGE.
2026-10-10 02:35:32 +01:00
ayozetr ee20c8b147 Game check: recognise the eboot with the 60 fps patch baked in
Some dumps (seen on EU CUSA00900) ship the 1.09 eboot.bin with Lance
McDonald's 60 fps patch already applied. Apart from that patch it is the
supported executable, but one of its edits inverts a branch in the menu
code (+0xf6d90d) and the game crashes as soon as the gestures menu opens.

The check now names this image and asks for a clean 1.09 eboot.bin,
instead of the misleading "other edition" message; bbport has its own
60 fps. Launcher message in en/es/it/fr/de.
2026-10-10 02:27:24 +01:00
ayozetr a39a0073de Launcher: game language follows the desktop by default
The game takes its text and voices from the PS4 system language, which
the launcher set to English unless changed. System language now starts
on "System": the desktop's locale picks Japanese, English, French,
Spanish, German, Italian or Russian, and English for anything else.
A language chosen in the launcher still wins.
2026-10-09 23:38:24 +01:00
ayozetr 09a1e4981a DLC folder: report dumped add-ons to the game
The base game (not the GOTY edition) learns which add-ons were bought
from sceAppContentGetAddcontInfoList, which always answered none. Their
data ships with the 1.09 update; a dumped add-on is just its sce_sys.

- content_profile.py --dlc DIR lists the add-on folders of this title
  (CATEGORY ac, CONTENT_ID of the same title id) into content.bin, now
  BBCONT02 with the entitlement labels when there are any.
- The runtime returns them as SceAppContentAddcontInfo entries (status
  "no extra data" for key-only folders, "installed" otherwise).
- run.sh passes BB_DLC_DIR; the launcher has a DLC folder row showing
  how many add-ons it found, translated in every launcher language.
2026-10-09 23:19:51 +01:00
ayozetr 992fc95ed1 New README for the fork
What the fork adds and how to play, build, play online and enable DLSS, with
credits to bbport (deadinside28) and the Windows port (Supermedo). The original
README moves to BBPORT.md (and its Russian version to BBPORT.ru.md) for its
technical details.
2026-10-09 21:06:24 +01:00
ayozetr 851712537b Discord Rich Presence
"Playing Bloodborne" on the player's Discord profile while the game runs, online
or offline, with the time played and the icon. scripts/discord_presence.py talks
to Discord's local IPC socket (also the Flatpak, Snap and Vesktop ones), keeps
looking for Discord when it is not running and ends with the game; run.sh starts
it unless BB_DISCORD=0, and the launcher has a switch for it.
launcher/bloodborne.svg: the icon redrawn as vector art, for the 1024 px assets
Discord needs.
2026-10-09 18:58:48 +01:00
ayozetr e289b722b5 DLSS Super Resolution for NVIDIA RTX on Linux
From the Windows port of Supermedo/bloodborne_pc (commit 9514b19; the bridge is
adapted from IFreemz/shadPS4-Bloodborne-DLSS-FSR): a new upscaler "dlss" that
gets the inputs FSR 3 gets, falls back to FSR 3.1 when DLSS fails, and changes
nothing without the DLSS files, on other GPUs or with BB_DLSS=0.

Linux: the bridge builds as libbbport_dlss.so against the SDK's libnvsdk_ngx.a
(only BbDlssGetApi exported) and is loaded with dlopen from BB_DLSS_DIR or the GPU
library's directory, next to NVIDIA's libnvidia-ngx-dlss.so.<version>.
tools/build_dlss.sh fetches the SDK files (v310.9.1, cached) and puts the bridge,
the library and its license into out/gpu; the AppImage ships them when present.
The launcher lists DLSS with a note on its files and the RTX requirement. Merged
with 0.4: FSR 4 keeps its reactive blend, Scheduler::WaitSubmittedWork.
2026-10-09 18:42:42 +01:00
ayozetr c7cae2a585 Launcher: online account checks before playing
As in the Windows launcher: with an account filled in and Play online off, ask
whether to play online or offline; an email address as the Online ID is refused
with a note (the NPID is the account name) and the Online page is shown.
2026-10-09 18:35:30 +01:00
ayozetr 7d53c5e9d6 AppImage: bundle Python's ssl and hashlib modules
quick-sharun leaves them out by default; the launcher's connection test and
scripts/online.py talk HTTPS to the co-op servers.
2026-10-09 18:32:08 +01:00
ayozetr 7dfbf49367 Online co-op on Linux
Online play through shadNet and The Hunter's Dream, from the Windows port of
Supermedo/bloodborne_pc (commit e076119), which built it from the network and PSN
libraries of shadp2p (github.com/Wozzardman/shadp2p): sign-in, matchmaking,
signaling, WebAPI and HTTPS. With BB_ONLINE=1 the game's network and PSN imports
go to these libraries instead of the offline stubs; trophies, voice and the store
dialogs stay in the runtime. It uses the same servers as the Windows version, so
players of both play together.

Linux changes: UPnP through the system's miniupnpc (the Windows port linked it
there only), no wepoll or Windows libraries; scripts/online.py replaces run.py's
host overrides (game server -> community server, summon signs -> the shadNet
WebAPI when it has the summon service) and is called by run.sh. Launcher: an
Online page (play online, game server, co-op server and WebAPI, shadNet account,
UPnP, connection test), translated; settings.json is now private (0600) since it
holds the shadNet password. tests/test_online.c checks the import routing.
2026-10-09 18:26:14 +01:00
ayozetr 52a6daf288 Add to Steam: replace this port's shortcut from another place
A shortcut named like ours with the same launch options (--play) is updated
rather than duplicated, so moving from a checkout to the AppImage keeps one
entry; other shortcuts with the same name (shadPS4) stay as they are.
2026-10-09 18:10:45 +01:00
ayozetr 912641c298 AppImage without Nix (packaging/appimage-sharun.sh)
Collects the build, the scripts, the GTK4 launcher and every library they load
(glibc, Python with PyGObject, GTK 4, libadwaita, SDL3, FFmpeg, Mesa's Vulkan
drivers) with quick-sharun from pkgforge-dev/Anylinux-AppImages, pinned and
checksummed. Downloads the FSR 4 assets when they are missing. Runs the launcher,
or the game alone with --play, from ~/.local/share/bbport.
2026-10-09 18:10:20 +01:00
ayozetr dca76e386f Add to Steam from the launcher
A non-Steam shortcut in every Steam account's shortcuts.vdf that runs the launcher
(or the AppImage) with --play. Steam is closed first when it runs and started again
afterwards. Under Steam, --play restores the system's LD_LIBRARY_PATH.
shortcuts.vdf handling and the icon come from the Windows launcher of
Supermedo/bloodborne_pc. bb-launcher.sh now passes its arguments through nix-shell,
and the desktop entry uses the icon.
2026-10-09 17:49:35 +01:00
ayozetr 0b465f8ee1 In-game menu in Spanish, Italian, French and German
The Language selector lists six languages; menu_language in bbport.ini and
BB_MENU_LANGUAGE take their codes (en, ru, es, it, fr, de), and the launcher's
language or the system locale picks one until the menu sets it. The translations
of the English texts are in gpu/shim/bbport_menu_i18n.h; missing ones stay English.
2026-10-09 17:45:56 +01:00
ayozetr 82782d8155 Launcher in Spanish, Italian, French and German
Every launcher string translated; the system locale picks them, and so does the
launcher language selector. Strings missing in a translation fall back to English.
2026-10-09 17:45:56 +01:00
ayozetr 89ea3deb70 Cheats and gameplay tweaks page in the launcher and in-game menu
Community patches for 1.09 already in patches/Bloodborne.xml (never die, stealth,
silent, Rally that never fades, enemy control, no Rally, camera distance, no camera
auto-rotation, easier running, ragdoll physics), switched from bbport.ini.
Ported from the Windows fork (Supermedo/bloodborne_pc).
2026-10-09 17:45:56 +01:00
524 changed files with 178495 additions and 8478 deletions

No files matched your search

+2
View File
@@ -0,0 +1,2 @@
# 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++
+1
View File
@@ -0,0 +1 @@
ko_fi: ayozetr
+84
View File
@@ -0,0 +1,84 @@
name: Bug report
description: The game or the launcher does something wrong, crashes or does not start.
labels: [bug]
body:
- type: markdown
attributes:
value: |
Thanks for reporting. Before you start, [docs/TROUBLESHOOTING.md](https://github.com/ayozetr/bloodborne_linux/blob/main/docs/TROUBLESHOOTING.md) covers the common problems.
Only your own dump of the game with the 1.09 update is supported. Do not ask for or link game files.
- type: input
id: version
attributes:
label: Version
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
- type: dropdown
id: install
attributes:
label: How you run it
options:
- AppImage
- From source (run.sh / the launcher in a checkout)
- Steam Deck (AppImage)
validations:
required: true
- type: input
id: system
attributes:
label: Distribution, GPU and driver
placeholder: CachyOS, RTX 3060, NVIDIA 580.95 / Fedora 43, RX 7800 XT, Mesa 25.2
validations:
required: true
- type: input
id: graphics
attributes:
label: Upscaler, preset and output resolution
description: From the launcher's **Settings** page or the in-game menu (Insert / L3+R3).
placeholder: FSR 3.1, Quality, 2560x1440
- type: textarea
id: what
attributes:
label: What happened
description: What you saw, and what you expected instead. Screenshots or a short video help.
validations:
required: true
- type: textarea
id: steps
attributes:
label: How to make it happen
description: Where in the game, and what you did right before. Say if it happens every time.
placeholder: |
1. Load the save at the Cathedral Ward lamp
2. Walk towards Old Yharnam
3. The game closes during the load
- type: dropdown
id: online
attributes:
label: Online
options:
- Offline
- Online, shadPS4's public server
- Online, my own server
- type: textarea
id: log
attributes:
label: Log
description: |
In the launcher's **Log** page, **Export log…** and drag the file here (or **Copy** and paste it).
Do not paste your shadNet password or your trophy key anywhere in the issue.
placeholder: Drag the exported log here.
validations:
required: true
- type: checkboxes
id: checks
attributes:
label: Checks
options:
- label: I use my own dump of Bloodborne with the 1.09 update.
required: true
- label: I searched the existing issues for the same problem.
required: true
+8
View File
@@ -0,0 +1,8 @@
blank_issues_enabled: false
contact_links:
- name: Troubleshooting
url: https://github.com/ayozetr/bloodborne_linux/blob/main/docs/TROUBLESHOOTING.md
about: The common problems and what to do, before opening an issue.
- name: Online play and your own server
url: https://github.com/ayozetr/bloodborne_linux/blob/main/docs/ONLINE.md
about: Accounts, the connection test, summons and the private server guide.
@@ -0,0 +1,21 @@
name: Feature request
description: Something the port or the launcher could do.
labels: [enhancement]
body:
- type: textarea
id: what
attributes:
label: What you would like
description: What it should do, and when you would use it.
validations:
required: true
- type: textarea
id: why
attributes:
label: Why
description: What it solves for you, or what you do today instead.
- type: textarea
id: elsewhere
attributes:
label: Seen elsewhere
description: Another port, fork or mod that already has it (a link helps a lot).
+17
View File
@@ -23,6 +23,8 @@ perf.data
/bloodborne_upscalers_*.md
mods.json
patches.json
# The mods folder of runs from source (the launcher's default, DATA_DIR/mods)
/mods/
# Local: game logs, messages and archives from users, internal plans
/logs/
/message*.txt
@@ -30,3 +32,18 @@ patches.json
/docs/ISSUE_ACTION_PLAN_*.md
# AMD FidelityFX DLLs (FSR 4.1.1 is built from the user's own): never in the repository
*.dll
# Game files and dumps: never in the repository
eboot.bin
param.sfo
*.pkg
*.dcx
*.7z
/CUSA*/
# 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 players' guide, built by mkdocs (mkdocs.yml)
_site/
+2
View File
@@ -1,6 +1,8 @@
[submodule "gpu/third_party/fsr-vulkan"]
path = gpu/third_party/fsr-vulkan
url = https://github.com/FireBurn/FSR-Vulkan.git
# build.sh applies gpu/patches/fsr-vulkan to its working tree
ignore = dirty
[submodule "gpu/third_party/imgui"]
path = gpu/third_party/imgui
url = https://github.com/ocornut/imgui.git
+455
View File
@@ -0,0 +1,455 @@
THIS PROJECT IS NOT RELATED TO SHADPS4. ALL QUESTIONS RELATED TO THIS PROJECT SHOULD BE SENT TO THE DISCORD SERVER https://discord.gg/KYZRKk9CB, NOT TO THE SHADPS4 SERVER.
# bbport — a native Linux port of Bloodborne
> This is the README of the original project (deadinside28/bloodborne_pc), kept for its
> technical details. The fork's own README is [README.md](README.md).
bbport is the counterpart of Wine + DXVK for a single game: *Bloodborne* for PlayStation 4
(CUSA03173, game version 1.09) on an x86-64 Linux PC. The game's original executable runs
directly on the PC:
- **as in Wine**, the game's x86-64 code runs on the CPU directly, and a runtime written for
this one game replaces the PS4 system libraries;
- **as in DXVK**, the game's graphics are translated to Vulkan — by a renderer derived from
[shadPS4](https://github.com/shadps4-emu/shadPS4) and heavily extended for this game, including
temporal upscaling with AMD FSR 3.1, FSR 4 and FSR 4.1.1;
- memory and the GPU's work are moving to the PC model: the *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).
> **No game files are included.** You need your own dump of Bloodborne with update 1.09: any retail
> release (CUSA00900 in the US, CUSA03173 for the European Game of the Year edition, ...), as they
> share the same executable.
> This project is not affiliated with Sony Interactive Entertainment, FromSoftware or AMD.
**Status: experimental, playable.** The game boots, loads saves and plays (the Hunter's Dream
and several areas of Yharnam were played with it) with sound, gamepad and saving.
A full play-through has not been verified, and only one machine (Linux, AMD Radeon RX 7800 XT,
Mesa/RADV) has been tested thoroughly.
## Highlights
- **Native execution.** The eboot is converted offline into a flat memory image; PS4 libc and
libSceFios2 are linked into it as native code. No CPU emulation and no per-instruction
translation: the game code runs at full speed.
- **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
in system RAM and the GPU reads it where it is, as a PC game's buffers; data it reads often
is kept in VRAM and given back when unused (textures after 20 s, buffers after 60 s). The
layer's memory module tracks CPU writes to mirrored blocks to keep those copies coherent;
it does not copy the whole GPU-visible memory. The PS4 command processor's work
is translated into Vulkan commands rather than emulated on the CPU: memory writes
(`WRITE_DATA`, DMA) are done by the GPU in command-stream order, and fences are written by the
GPU itself at the end of the pipeline, at the same point of the stream as the PS4's command
processor writes them. **It may crash**; hardware testing has been performed on an
RX 7800 XT, and performance depends on the scene and settings. On AMD the game's memory is kept
in a sparse buffer (the arena). **On NVIDIA, Intel and others** it goes through the layer's
memory module: the game's memory is bound in place and VRAM copies are separate buffers, with
no sparse rebinding. This avoids the sparse-binding path associated with reported freezes
on a GTX 1660 Ti. On NVIDIA the module runs on a tester's GTX 1660 Ti, its speed is still
being checked; not tested on Intel. Without the launcher: `BB_PC_MODEL=1`; to choose the memory
path: `BB_GUEST_MEMORY=host|dmabuf`; the memory module: `BB_LAYER_MEMORY=1/0`.
[0.5-pre4 changes](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.
Unused textures are freed in both modes, so VRAM no longer grows with every area visited.
- **Unlocked frame rate.** Community patches (`patches/Bloodborne.xml`) make the simulation
use the real frame time; ~90 FPS at 4K with FSR 4 Balanced on an RX 7800 XT, ~150 FPS at
1440p with FSR 4 Quality. Also 30/60/90 FPS modes.
- **Temporal upscaling built for this game.** Bloodborne has no velocity buffer, so bbport
computes motion vectors itself: camera motion from depth and the scene matrices, and object
motion (characters, cloth, weapons) from the vertex positions of the previous frame. The
scene is jittered sub-pixel (Halton) and rendered at a reduced resolution; the upscaler fills
the output (720p for the Steam Deck, 1080p, 1440p or 2160p) and the UI is drawn natively at the output resolution.
- **FSR 3.1** (FireBurn/FSR-Vulkan).
- **FSR 4 (INT8, model v07)** on GPUs exposing the required Vulkan shader features —
RDNA2/3 included (see Requirements).
- **FSR 4.1.1**: AMD's 4.1.1 DLL is recorded once under vkd3d-proton and its passes are
replayed natively on Vulkan; the output is **bit-exact** with the DLL. Two variants, as in
the DLL: INT8 on any GPU with the required shader features, and FP8 matrices on RDNA4 (RX
9000; picked automatically). The assets are built on your machine from one DLL of your own
(4.1.x): the launcher's *FSR 4.1.1 from your own AMD DLL → Choose DLL…* button (2–5 minutes,
twice that on RDNA4; needs a recent Proton), or `tools/fsr4cap`.
- Faster than AMD's own shaders on RDNA3: the final passes of FSR 4 and 4.1.1 were rewritten
to store through workgroup memory (3.5× and 2.3× faster, bit-exact); FSR 4 costs ~4 ms at
4K on an RX 7800 XT instead of ~6 ms.
- **Multi-threaded GPU command processing.** The PS4 command stream is decoded on one thread
and draws are bound and recorded on another (two-stage pipeline), with a Vulkan recording
thread and helper threads for memory copies. Early on the single GPU thread capped the game
at ~26 FPS; now it runs at 90–150 FPS depending on resolution and scene.
- **Settings inside the game's own menu:** *System → Display / Game effects / Game patches*
(after *Screen/Sound*), built and drawn by the game itself — output resolution, upscaler,
preset, sharpness, FPS counter, model detail and the effect patches, in the game's language.
- **Overlay menu** (Insert or L3+R3), styled like the game's dialogs: everything above plus the
advanced settings, with gamepad, keyboard and mouse.
- **GTK4 launcher** and an **AppImage** for the Steam Deck.
## How it differs from shadPS4
| | shadPS4 | bbport |
|---|---|---|
| Scope | General PS4 emulator, many games | One game: Bloodborne v1.09 |
| Loading | Its own ELF loader and kernel emulation at run time | The eboot is converted offline (`scripts/`) into an image with PS4 libc/Fios2 linked in; a C loader maps it and jumps into the game (loader and runtime: ~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 |
| 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 thread | One thread processes the whole command stream (the bottleneck in Bloodborne) | Decode and draw recording run on separate threads; the work scales with the hardware threads (Steam Deck included) |
| Upscaling | — | Temporal (FSR 3.1, FSR 4, FSR 4.1.1) with the game's own motion vectors and jitter |
| Game patches | Patch files applied by the emulator | The same community patches, compiled at start (`scripts/patches.py`); render resolution, effects and FPS from the launcher |
Without shadPS4 there would be no bbport: its renderer and shader recompiler are the base of
the graphics side.
### Wine + DXVK for one game
- **CPU.** The PS4 CPU is x86-64, so the game's code runs directly on the PC's CPU, with no
emulation and no instruction translation.
- **System libraries.** As Wine replaces the Windows API, the bbport runtime (`src/runtime_*.c`)
implements exactly the PS4 OS functions Bloodborne calls: memory, threads, files, audio, pad,
saves.
- **Memory.** In the new mode, as in a PC game: the game's data in system RAM, VRAM used the way
a PC game uses it. The mode is still experimental.
- **Graphics.** The PS4 GPU's command stream (PM4) is a recording of the game's graphics API
calls: it is written by 99 functions of the statically linked libGnm, so decoding the stream
and translating the calls themselves come to the same thing. GCN shaders are translated to
SPIR-V. In the new mode the command processor's work (memory writes, DMA, fences) is
translated into Vulkan commands as well; fences are written by the GPU itself. Resource
descriptors (textures, buffers) are read by the translator on the CPU, as DXVK reads resource
bindings: a PS4 texture has to be converted from the PS4 tiling into a Vulkan image before the
draw runs, so the CPU must know its descriptor beforehand — translation, not emulation. The
descriptor tables (the constant engine's dumps) are read only by the translator, so the CPU
performs them. The game's occlusion queries (~60 a frame) 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).
"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.
FSR 4 / 4.1.1 require shader Float16, Int8/Int16, integer dot products, linear compute
derivatives and extended storage image formats; FSR 4.1.1 additionally requires
`VK_VALVE_shader_mixed_float_dot_product`. Unsupported choices fall back to FSR 3.1
before the first frame and are disabled in the in-game menu.
- Your decrypted game dump: the `CUSA03173` folder (eboot.bin, sce_module, ...), version 1.09.
The US release (`CUSA00900`) runs too: its 1.09 executable is the same as this European
Game of the Year edition's.
A dumped update is a separate folder: copy it over the base game, replacing files. The base
game alone (1.00) crashes at start (guest offset 0x20348b8); the launcher and `run.sh` check
the executable and say what is missing (`BB_SKIP_GAME_CHECK=1` skips the check). The other
retail releases (the US `CUSA00900`, the Asian `CUSA03023`, ...) run the same 1.09 executable. Some dumps ship it
with Lance McDonald's 60 fps patch baked in, which crashes when the gestures menu opens: the
check asks for a clean eboot.bin then (bbport has its own 60 fps).
- To build: GCC, CMake, Ninja, Python 3, glslang, SDL3, Vulkan headers and the libraries in
`shell.nix`. With [Nix](https://nixos.org) everything comes from `shell.nix` automatically.
## Build and run
```bash
git clone --recursive https://github.com/ayozetr/bloodborne_linux.git && cd bloodborne_linux
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*):
```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,
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.
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
(`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.
**TAA:** a separate native-resolution temporal AA mode in the launcher and overlay,
switchable live without an FSR model. The saved FSR preset is restored when returning
to FSR. FSR Native AA adds reconstruction on top of full-resolution rendering and can
be slower than disabling AA. TAA also adds work compared with no temporal AA.
The RCAS switch and the 0–2 sharpness control also work with TAA. Sharpening runs after
temporal accumulation and leaves its history and HUD unchanged.
**Mods:** the launcher accepts separate loose-file mod folders (with `dvdroot_ps4/`, an extra
wrapper folder, or the game folders such as `chr/` directly; file name case does not matter),
with enable switches and load order. A sibling `CUSA03173-mods/` overlay also works.
The original game is preserved; later mods override conflicting files.
**Third-party patches:** shadPS4-format XML patch files in the data directory's `patches/`,
switched on and off in the launcher. See [mods and patches](docs/MODS.en.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.
**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
[GoldHEN patch](https://github.com/GoldHEN/GoldHEN_Patch_Repository/blob/main/patches/xml/Bloodborne-Orbis.xml):
hold Cross and press L3 to cycle modes (keyboard: hold Space and press Z). It needs no fonts
and conflicts with *Enemy Control*.
For the game debug menu, install 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
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.
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.
**Upscaler assets** (not included; FSR 3.1 needs none):
```bash
bash tools/fetch_fsr4_assets.sh # FSR 4 v07 (MIT, built from AMD's source by Q2RTX)
# FSR 4.1.1, from your own AMD DLLs (e.g. OptiScaler's FSR4_LATEST), needs GE-Proton 10 or newer:
bash tools/fsr4cap/build_assets.sh <amd_fidelityfx_upscaler_dx12.dll>
```
**FSR 4.1.1 from your own DLL.** The easiest way is the launcher: *Graphics → Upscaler → FSR 4.1.1 from your
own AMD DLL → Choose DLL…*, then pick `amd_fidelityfx_upscaler_dx12.dll` version 4.1.x: from
OptiScaler's `FSR4_LATEST` folder or from a game with FSR 4.1. That one file is enough: AMD's DLL
exports the FidelityFX API itself, so no loader is needed (only a DLL without these exports would
be recorded through `amd_fidelityfx_loader_dx12.dll`, and the launcher would ask for it). The DLL is checked at once,
without a long capture; these do not fit: AMD's official FSR 4.0.x (e.g. Pragmata's 4.0.3 —
AMD enables it on RDNA4 only, and under Proton it does not start on other GPUs) and community
builds (4.0.2b, 4.1.1b and similar OptiScaler INT8 builds: another model and pass count). Then
the DLL runs under Proton and is recorded at 20 sizes (progress in the row and in the *Log* section;
on RDNA4 a second time, for the FP8 variant),
the passes are translated to SPIR-V, and FSR 4.1.1 is selected. The Proton build is picked
automatically — the first under which the DLL enables FSR 4.1: GE-Proton 10 or newer,
Proton-CachyOS, Proton Experimental (tested: GE-Proton 11, Proton-CachyOS 11, Experimental of
October 2026; Steam's Proton 11.0 and GE-Proton 9 do not). It runs in the Steam runtime it
requires (Steam installs it the first time any game runs with that Proton), or through
umu-launcher when installed. On NixOS umu-launcher comes from `nix-shell` (downloaded the first
time, ~1.7 GB); the AppImage runs the recording on the system itself, through the user's systemd
(`systemd-run --user`), since the system's `/nix` is out of its sight. Recording on the Steam Deck is not verified yet; if the DLL does not
enable FSR 4.1 there, build on a PC and copy the `fsr4_411` folder. The same from the command
line: the launcher's (and the AppImage's) `--build-fsr411 <DLL>`. 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.
**DLSS (NVIDIA GeForce RTX).** NVIDIA's DLSS SDK is not included: check out
[github.com/NVIDIA/DLSS](https://github.com/NVIDIA/DLSS) (`include/` and `lib/Linux_x86_64` are
enough) and build with `DLSS_SDK_ROOT=/path/to/DLSS bash build.sh`. That also builds the bridge
`out/libbbport_dlss.so` (`gpu/dlss_bridge`, the only code that uses the SDK) and copies NVIDIA's
`libnvidia-ngx-dlss.so.<version>` next to `out/bb-probe`; the driver provides the rest. Then pick
*DLSS* in the launcher or the in-game menu: the preset sets the render size, Native AA is DLAA.
DLSS gets the inputs FSR 3.1 gets (scene color, depth, motion vectors, jitter). Without the
libraries, on other GPUs or with `BB_DLSS=0` it is listed as unavailable, and a DLSS setting falls
back to FSR 3.1.
On Linux, DLSS also needs NVIDIA's CUDA/UVM runtime. If device creation fails with the DLSS
extensions, the renderer retries without them and uses FSR 3.1. If `/dev/nvidia-uvm` is missing,
load the installed driver module with `sudo modprobe nvidia_uvm` and restart the game. Installing
the distribution's `nvidia-modprobe` package lets NVIDIA's libraries load the module when needed.
If `modprobe` reports `Key was rejected by service` with Secure Boot enabled, enroll the
module's signing key using the distribution's [MOK procedure](https://documentation.ubuntu.com/security/security-features/platform-protections/secure-boot/#mok-generation-and-signing-process).
The AppImage built with `DLSS_SDK_ROOT` carries the bridge and NVIDIA's library (NVIDIA's RTX SDK
license allows that in an application; its text is `NVIDIA-DLSS-LICENSE.txt` in the package). A
newer `libnvidia-ngx-dlss.so.<version>` (from the SDK's `lib/Linux_x86_64/rel`) is chosen in the
launcher: *Graphics → 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.
**FP8 on RDNA4.** When vkd3d-proton offers FP8 cooperative matrices (RDNA4), the DLL runs
another variant of its passes: other shaders and weights, other dispatch sizes. The build records
both, INT8 into `fsr4_411/` and FP8 into `fsr4_411/fp8/`, and the game picks FP8 itself when the
GPU has FP8 matrices (`VK_EXT_shader_float8`); the log says `Upscaler: FSR 4.1.1 replay, FP8 …`.
`BB_FSR411_VARIANT=int8` forces INT8. Checked on an RX 7800 XT through vkd3d-proton's FP16
emulation of FP8 (`BB_FSR4CAP_FP8=1` records it there too, `fp8emu/`): bit-exact with the DLL;
**not yet run on RDNA4 hardware.**
**AppImage** (Steam Deck): `bash build.sh && bash packaging/appimage.sh` →
`dist/Bloodborne-bbport-x86_64.AppImage`; data in `~/.local/share/bbport`, `--play` starts the
game without the launcher window (Game Mode). FSR 4.1.1 models are not packaged: build them with
the launcher's button (see above); they go to `~/.local/share/bbport/fsr4_411`
(`BB_PACKAGE_FSR411=1` bundles a local `fsr4_411` into an AppImage for your own devices). On the Steam Deck pick the 1280×720 output (the
game is 16:9; on the 1280×800 screen it gets thin bars).
**Adding the AppImage to Steam** (*Add a Non-Steam Game*) needs no options; the compatibility tool
does not matter. (Steam preloads its overlay into every non-Steam game; the AppImage removes it
before its own programs start, so the Steam overlay is not shown in the game.) Where Steam
runs games without FUSE (NixOS: Steam's FHS sandbox; the AppImage then exits with *Cannot mount
AppImage*), set the launch options to
```
TMPDIR=$HOME/.cache APPIMAGE_EXTRACT_AND_RUN=1 NO_CLEANUP=1 %command%
```
The AppImage then unpacks itself (~2 GB, `~/.cache/appimage_extracted_*`) on the first start
(~10 s) and reuses that copy afterwards; a new AppImage version gets a new copy, the old one can
be deleted. Without `TMPDIR` it would unpack into Steam's `/tmp`, which is in RAM there. Add
` --play` after `%command%` to skip the launcher.
**NVIDIA in the AppImage:** startup discovers the host's installed 64-bit NVIDIA Vulkan ICD
and exposes its vendor libraries alongside the bundled AMD/Intel drivers. This keeps the
NVIDIA userspace driver matched to the host kernel module. Standard Linux distributions keep
these libraries under `/usr/lib*`; on NixOS the AppImage's internal `/nix/store` may hide them.
In that case copy the NVIDIA libraries into an accessible directory and set
`BB_NVIDIA_LIB_DIR=/path/to/libraries` (the NVIDIA manifest must also be accessible).
Explicit `VK_DRIVER_FILES`/`VK_ICD_FILENAMES` overrides are preserved. Diagnose drivers inside
the package with:
```bash
./Bloodborne-bbport-x86_64.AppImage --vulkan-info 2>&1 | tee bbport-vulkan.log
```
A user reported successful startup with FSR 3 on a GTX 1060 6GB (Fedora 44, NVIDIA
580.178.04); selecting FSR 4 caused a black window. Use FSR 3 on this configuration.
MangoHud is bundled in the AppImage; enable its checkbox in the launcher. If MangoHud is also
installed system-wide, or Steam's performance overlay is on (Steam Deck game mode), only one
overlay is drawn (two drew doubled, offset text).
When running from source, install MangoHud separately. A diagnostic launch with
`VK_LOADER_LAYERS_DISABLE=~implicit~` also disables MangoHud.
Useful variables: `BB_FRAME_STATS=1` (frame statistics, including a `Memory:` line: VRAM, GTT,
RSS, images and guest blocks in VRAM), `BB_PC_MODEL=1/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
default, 0 = the game's own),
`BB_GC_IDLE_SECONDS=N` / `BB_VRAM_IDLE_SECONDS=N` (how long unused textures / buffers stay in VRAM;
20 / 60), `BB_BREADCRUMBS=0` (no GPU breadcrumbs; with them a GPU hang names the draw or dispatch
it is stuck in), `BB_GPU_PROFILE=1` (GPU time per
pass), `BB_FSR4_PROFILE=1` (GPU time per FSR 4 pass), `BB_UPSCALER=taa|fsr3|fsr4|fsr411|off|none`,
`BB_FRAMES_AHEAD=N` (how many frames the GPU command thread may run ahead of the GPU; 1 by default,
0 = unbounded), `BB_PRESENT_THREAD=0` (present on the vblank thread, as before),
`BB_LIVE_RES=1` (live resolution changes instead of the startup patch for outputs other than 1080p),
`BB_PAD_RECORD=file` / `BB_PAD_REPLAY=file` (record a route with F9, replay it in scripted tests),
`BB_GC_BUDGET_MB=N` (texture cache budget, as on integrated GPUs), `BB_VRAM_LIMIT_MB=N` (the VRAM
budget taken as at most N MiB: checks the behaviour of cards with little VRAM), `BB_ASYNC_PIPELINES=0`
(new pipelines compile at once and the game waits; by default in the background), `BB_PRESENT_DUMP_TRIGGER=file`
with `BB_PRESENT_DUMP_COUNT=N` (dump N consecutive presented frames),
`BB_FSR411_VARIANT=int8|fp8|fp8emu` (FSR 4.1.1 variant; by default FP8 where the GPU has FP8
matrices), `BB_READBACKS=0|1|2` (reads of GPU-written memory by the game: 1 by default, 2
precise and slow, 0 off).
More in [docs/](docs); what changed in each release: the
[releases page](https://github.com/ayozetr/bloodborne_linux/releases).
**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.
## Repository layout
| Path | Contents |
|---|---|
| `src/` | Loader (`probe.c`) and the HLE runtime |
| `scripts/` | Offline preparation of the game image, module linking, patch compiler |
| `gpu/` | Renderer library: vendored shadPS4 video core with this port's changes (`gpu/VENDOR.txt`), shims, ImGui menu, FSR 4.1.1 runtime (`gpu/shadps4/video_core/renderer_vulkan/fsr411`) |
| `launcher/`, `packaging/` | GTK4 launcher; Nix package and AppImage |
| `patches/` | Community patches for Bloodborne |
| `tools/` | Developer tools: scripted runs, A/B toggles, FSR benchmark helpers, FSR 4 shader rewrites, `fsr4cap` (FSR 4.1.1 recording/extraction) |
| `tests/` | Loader, runtime, patch and renderer tests |
| `docs/` | Design notes and measurements ([upscaler](docs/upscaler.md), [parallel GPU](docs/parallel_gpu.md), [motion vectors](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`.
## 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.
- Textures as objects created at load time (less guessing by address, uploads outside the
frame).
- Shaders translated ahead of time, at install, not during play.
- More CPU parallelism in GPU command processing (split the draw-recording stage further),
scaling to all hardware threads — most important for the Steam Deck.
- Async compute for the upscaler (the frame is GPU-bound at 4K).
- XeSS (super resolution) and XeFG frame generation through a Wine helper sharing Vulkan
memory (a memory-bridge prototype is in `tools/bridge_helper`); inputs exposed so that OptiScaler-style mapping works.
- Frame generation (FSR 3.1 FG first), reactive and transparency masks for particles and fog.
- Fix the races in AMD's FSR 4.1.1 shaders at output widths that are not multiples of 64
(e.g. 1600×900), as already done for the left-edge race in FSR 4 v07 at 1080p.
- Steam Deck validation of the AppImage; HDR output.
## Credits and licenses
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+),
[sirit](https://github.com/shadps4-emu/sirit), [half](https://half.sourceforge.net/),
[FSR-Vulkan](https://github.com/FireBurn/FSR-Vulkan) by FireBurn (MIT; FSR 3.1 on Vulkan and the
FSR 4 v07 provider), AMD FidelityFX SDK (MIT), [LibAtrac9](https://github.com/Thealexbarney/LibAtrac9)
(MIT), [Dear ImGui](https://github.com/ocornut/imgui) (MIT), DejaVu fonts,
[dxil-spirv](https://github.com/HansKristian-Work/dxil-spirv) (MIT, used to build the
FSR 4.1.1 assets). Game patches by Kyo, Lance McDonald, auser1337, illusion, emoose and other
community members (`patches/Bloodborne.xml`). The DLSS bridge (`gpu/dlss_bridge`, MIT) and
loader come from [Supermedo's Windows port](https://github.com/Supermedo/bloodborne_pc), adapted
from [IFreemz/shadPS4-Bloodborne-DLSS-FSR](https://github.com/IFreemz/shadPS4-Bloodborne-DLSS-FSR).
AMD's FSR 4 DLLs and model data and NVIDIA's DLSS SDK and libraries are not distributed here.
+584 -248
View File
@@ -1,281 +1,622 @@
GNU GENERAL PUBLIC LICENSE
Version 2, June 1991
Version 3, 29 June 2007
Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
<https://fsf.org/>
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The licenses for most software are designed to take away your
freedom to share and change it. By contrast, the GNU General Public
License is intended to guarantee your freedom to share and change free
software--to make sure the software is free for all its users. This
General Public License applies to most of the Free Software
Foundation's software and to any other program whose authors commit to
using it. (Some other Free Software Foundation software is covered by
the GNU Lesser General Public License instead.) You can apply it to
The GNU General Public License is a free, copyleft license for
software and other kinds of works.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
the GNU General Public License is intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users. We, the Free Software Foundation, use the
GNU General Public License for most of our software; it applies also to
any other work released this way by its authors. You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
this service if you wish), that you receive source code or can get it
if you want it, that you can change the software or use pieces of it
in new free programs; and that you know you can do these things.
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
To protect your rights, we need to make restrictions that forbid
anyone to deny you these rights or to ask you to surrender the rights.
These restrictions translate to certain responsibilities for you if you
distribute copies of the software, or if you modify it.
To protect your rights, we need to prevent others from denying you
these rights or asking you to surrender the rights. Therefore, you have
certain responsibilities if you distribute copies of the software, or if
you modify it: responsibilities to respect the freedom of others.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must give the recipients all the rights that
you have. You must make sure that they, too, receive or can get the
source code. And you must show them these terms so they know their
rights.
gratis or for a fee, you must pass on to the recipients the same
freedoms that you received. You must make sure that they, too, receive
or can get the source code. And you must show them these terms so they
know their rights.
We protect your rights with two steps: (1) copyright the software, and
(2) offer you this license which gives you legal permission to copy,
distribute and/or modify the software.
Developers that use the GNU GPL protect your rights with two steps:
(1) assert copyright on the software, and (2) offer you this License
giving you legal permission to copy, distribute and/or modify it.
Also, for each author's protection and ours, we want to make certain
that everyone understands that there is no warranty for this free
software. If the software is modified by someone else and passed on, we
want its recipients to know that what they have is not the original, so
that any problems introduced by others will not reflect on the original
authors' reputations.
For the developers' and authors' protection, the GPL clearly explains
that there is no warranty for this free software. For both users' and
authors' sake, the GPL requires that modified versions be marked as
changed, so that their problems will not be attributed erroneously to
authors of previous versions.
Finally, any free program is threatened constantly by software
patents. We wish to avoid the danger that redistributors of a free
program will individually obtain patent licenses, in effect making the
program proprietary. To prevent this, we have made it clear that any
patent must be licensed for everyone's free use or not licensed at all.
Some devices are designed to deny users access to install or run
modified versions of the software inside them, although the manufacturer
can do so. This is fundamentally incompatible with the aim of
protecting users' freedom to change the software. The systematic
pattern of such abuse occurs in the area of products for individuals to
use, which is precisely where it is most unacceptable. Therefore, we
have designed this version of the GPL to prohibit the practice for those
products. If such problems arise substantially in other domains, we
stand ready to extend this provision to those domains in future versions
of the GPL, as needed to protect the freedom of users.
Finally, every program is threatened constantly by software patents.
States should not allow patents to restrict development and use of
software on general-purpose computers, but in those that do, we wish to
avoid the special danger that patents applied to a free program could
make it effectively proprietary. To prevent this, the GPL assures that
patents cannot be used to render the program non-free.
The precise terms and conditions for copying, distribution and
modification follow.
GNU GENERAL PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
TERMS AND CONDITIONS
0. This License applies to any program or other work which contains
a notice placed by the copyright holder saying it may be distributed
under the terms of this General Public License. The "Program", below,
refers to any such program or work, and a "work based on the Program"
means either the Program or any derivative work under copyright law:
that is to say, a work containing the Program or a portion of it,
either verbatim or with modifications and/or translated into another
language. (Hereinafter, translation is included without limitation in
the term "modification".) Each licensee is addressed as "you".
0. Definitions.
Activities other than copying, distribution and modification are not
covered by this License; they are outside its scope. The act of
running the Program is not restricted, and the output from the Program
is covered only if its contents constitute a work based on the
Program (independent of having been made by running the Program).
Whether that is true depends on what the Program does.
"This License" refers to version 3 of the GNU General Public License.
1. You may copy and distribute verbatim copies of the Program's
source code as you receive it, in any medium, provided that you
conspicuously and appropriately publish on each copy an appropriate
copyright notice and disclaimer of warranty; keep intact all the
notices that refer to this License and to the absence of any warranty;
and give any other recipients of the Program a copy of this License
along with the Program.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
You may charge a fee for the physical act of transferring a copy, and
you may at your option offer warranty protection in exchange for a fee.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
2. You may modify your copy or copies of the Program or any portion
of it, thus forming a work based on the Program, and copy and
distribute such modifications or work under the terms of Section 1
above, provided that you also meet all of these conditions:
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
a) You must cause the modified files to carry prominent notices
stating that you changed the files and the date of any change.
A "covered work" means either the unmodified Program or a work based
on the Program.
b) You must cause any work that you distribute or publish, that in
whole or in part contains or is derived from the Program or any
part thereof, to be licensed as a whole at no charge to all third
parties under the terms of this License.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
c) If the modified program normally reads commands interactively
when run, you must cause it, when started running for such
interactive use in the most ordinary way, to print or display an
announcement including an appropriate copyright notice and a
notice that there is no warranty (or else, saying that you provide
a warranty) and that users may redistribute the program under
these conditions, and telling the user how to view a copy of this
License. (Exception: if the Program itself is interactive but
does not normally print such an announcement, your work based on
the Program is not required to print an announcement.)
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
These requirements apply to the modified work as a whole. If
identifiable sections of that work are not derived from the Program,
and can be reasonably considered independent and separate works in
themselves, then this License, and its terms, do not apply to those
sections when you distribute them as separate works. But when you
distribute the same sections as part of a whole which is a work based
on the Program, the distribution of the whole must be on the terms of
this License, whose permissions for other licensees extend to the
entire whole, and thus to each and every part regardless of who wrote it.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
Thus, it is not the intent of this section to claim rights or contest
your rights to work written entirely by you; rather, the intent is to
exercise the right to control the distribution of derivative or
collective works based on the Program.
1. Source Code.
In addition, mere aggregation of another work not based on the Program
with the Program (or with a work based on the Program) on a volume of
a storage or distribution medium does not bring the other work under
the scope of this License.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
3. You may copy and distribute the Program (or a work based on it,
under Section 2) in object code or executable form under the terms of
Sections 1 and 2 above provided that you also do one of the following:
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
a) Accompany it with the complete corresponding machine-readable
source code, which must be distributed under the terms of Sections
1 and 2 above on a medium customarily used for software interchange; or,
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
b) Accompany it with a written offer, valid for at least three
years, to give any third party, for a charge no more than your
cost of physically performing source distribution, a complete
machine-readable copy of the corresponding source code, to be
distributed under the terms of Sections 1 and 2 above on a medium
customarily used for software interchange; or,
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
c) Accompany it with the information you received as to the offer
to distribute corresponding source code. (This alternative is
allowed only for noncommercial distribution and only if you
received the program in object code or executable form with such
an offer, in accord with Subsection b above.)
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The source code for a work means the preferred form of the work for
making modifications to it. For an executable work, complete source
code means all the source code for all modules it contains, plus any
associated interface definition files, plus the scripts used to
control compilation and installation of the executable. However, as a
special exception, the source code distributed need not include
anything that is normally distributed (in either source or binary
form) with the major components (compiler, kernel, and so on) of the
operating system on which the executable runs, unless that component
itself accompanies the executable.
The Corresponding Source for a work in source code form is that
same work.
If distribution of executable or object code is made by offering
access to copy from a designated place, then offering equivalent
access to copy the source code from the same place counts as
distribution of the source code, even though third parties are not
compelled to copy the source along with the object code.
2. Basic Permissions.
4. You may not copy, modify, sublicense, or distribute the Program
except as expressly provided under this License. Any attempt
otherwise to copy, modify, sublicense or distribute the Program is
void, and will automatically terminate your rights under this License.
However, parties who have received copies, or rights, from you under
this License will not have their licenses terminated so long as such
parties remain in full compliance.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
5. You are not required to accept this License, since you have not
signed it. However, nothing else grants you permission to modify or
distribute the Program or its derivative works. These actions are
prohibited by law if you do not accept this License. Therefore, by
modifying or distributing the Program (or any work based on the
Program), you indicate your acceptance of this License to do so, and
all its terms and conditions for copying, distributing or modifying
the Program or works based on it.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
6. Each time you redistribute the Program (or any work based on the
Program), the recipient automatically receives a license from the
original licensor to copy, distribute or modify the Program subject to
these terms and conditions. You may not impose any further
restrictions on the recipients' exercise of the rights granted herein.
You are not responsible for enforcing compliance by third parties to
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
7. If, as a consequence of a court judgment or allegation of patent
infringement or for any other reason (not limited to patent issues),
conditions are imposed on you (whether by court order, agreement or
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot
distribute so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you
may not distribute the Program at all. For example, if a patent
license would not permit royalty-free redistribution of the Program by
all those who receive copies directly or indirectly through you, then
the only way you could satisfy both it and this License would be to
refrain entirely from distribution of the Program.
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
If any portion of this section is held invalid or unenforceable under
any particular circumstance, the balance of the section is intended to
apply and the section as a whole is intended to apply in other
circumstances.
13. Use with the GNU Affero General Public License.
It is not the purpose of this section to induce you to infringe any
patents or other property right claims or to contest validity of any
such claims; this section has the sole purpose of protecting the
integrity of the free software distribution system, which is
implemented by public license practices. Many people have made
generous contributions to the wide range of software distributed
through that system in reliance on consistent application of that
system; it is up to the author/donor to decide if he or she is willing
to distribute software through any other system and a licensee cannot
impose that choice.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU Affero General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the special requirements of the GNU Affero General Public License,
section 13, concerning interaction through a network will apply to the
combination as such.
This section is intended to make thoroughly clear what is believed to
be a consequence of the rest of this License.
14. Revised Versions of this License.
8. If the distribution and/or use of the Program is restricted in
certain countries either by patents or by copyrighted interfaces, the
original copyright holder who places the Program under this License
may add an explicit geographical distribution limitation excluding
those countries, so that distribution is permitted only in or among
countries not thus excluded. In such case, this License incorporates
the limitation as if written in the body of this License.
9. The Free Software Foundation may publish revised and/or new versions
of the General Public License from time to time. Such new versions will
The Free Software Foundation may publish revised and/or new versions of
the GNU General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the Program
specifies a version number of this License which applies to it and "any
later version", you have the option of following the terms and conditions
either of that version or of any later version published by the Free
Software Foundation. If the Program does not specify a version number of
this License, you may choose any version ever published by the Free Software
Foundation.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU General Public License, you may choose any version ever published
by the Free Software Foundation.
10. If you wish to incorporate parts of the Program into other free
programs whose distribution conditions are different, write to the author
to ask for permission. For software which is copyrighted by the Free
Software Foundation, write to the Free Software Foundation; we sometimes
make exceptions for this. Our decision will be guided by the two goals
of preserving the free status of all derivatives of our free software and
of promoting the sharing and reuse of software generally.
If the Program specifies that a proxy can decide which future
versions of the GNU General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
NO WARRANTY
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
REPAIR OR CORRECTION.
15. Disclaimer of Warranty.
12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
POSSIBILITY OF SUCH DAMAGES.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
@@ -287,15 +628,15 @@ free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
convey the exclusion of warranty; and each file should have at least
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software; you can redistribute it and/or modify
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
@@ -303,36 +644,31 @@ the "copyright" line and a pointer to where the full notice is found.
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program; if not, see <https://www.gnu.org/licenses/>.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If the program is interactive, make it output a short notice like this
when it starts in an interactive mode:
If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode:
Gnomovision version 69, Copyright (C) year name of author
Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
<program> Copyright (C) <year> <name of author>
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, the commands you use may
be called something other than `show w' and `show c'; they could even be
mouse-clicks or menu items--whatever suits your program.
parts of the General Public License. Of course, your program's commands
might be different; for a GUI interface, you would use an "about box".
You should also get your employer (if you work as a programmer) or your
school, if any, to sign a "copyright disclaimer" for the program, if
necessary. Here is a sample; alter the names:
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU GPL, see
<http://www.gnu.org/licenses/>.
Yoyodyne, Inc., hereby disclaims all copyright interest in the program
`Gnomovision' (which makes passes at compilers) written by James Hacker.
<signature of Moe Ghoul>, 1 April 1989
Moe Ghoul, President of Vice
This General Public License does not permit incorporating your program into
proprietary programs. If your program is a subroutine library, you may
consider it more useful to permit linking proprietary applications with the
library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License.
The GNU General Public License does not permit incorporating your program
into proprietary programs. If your program is a subroutine library, you
may consider it more useful to permit linking proprietary applications with
the library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License. But first, please read
<http://www.gnu.org/philosophy/why-not-lgpl.html>.
+122 -385
View File
@@ -1,408 +1,145 @@
THIS PROJECT IS NOT RELATED TO SHADPS4. ALL QUESTIONS RELATED TO THIS PROJECT SHOULD BE SENT TO THE DISCORD SERVER https://discord.gg/KYZRKk9CB, NOT TO THE SHADPS4 SERVER.
# Bloodborne for Linux
[![Total Download Count](https://img.shields.io/github/downloads/ayozetr/bloodborne_linux/total.svg?color=2ea44f)](https://github.com/ayozetr/bloodborne_linux/releases/latest)
[![Latest Release](https://img.shields.io/github/v/release/ayozetr/bloodborne_linux.svg?color=8a2be2)](https://github.com/ayozetr/bloodborne_linux/releases/latest)
[![License: GPL-3.0](https://img.shields.io/github/license/ayozetr/bloodborne_linux.svg?color=d73a49)](LICENSE)
[![Platform: Linux | Steam Deck](https://img.shields.io/badge/platform-Linux%20%7C%20Steam%20Deck-blue.svg)](#features)
[![Ko-fi](https://img.shields.io/badge/Ko--fi-support-FF5E5B.svg?logo=ko-fi&logoColor=white)](https://ko-fi.com/ayozetr)
# bbport — a native Linux port of Bloodborne
The PlayStation 4 version of *Bloodborne* running natively on Linux. The game's own code runs
directly on the CPU and its graphics are translated to Vulkan: it is not an emulator.
**English** · [Русский](README.ru.md)
This fork is built on [bbport](https://github.com/deadinside28/bloodborne_pc) by deadinside28. It
adds improvements of its own, and others from the community, taken from pull requests and other
projects ([credits](docs/CREDITS.md)).
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:
> **No game files are included.** You need your own decrypted dump of Bloodborne with update
> 1.09, from a retail release ([which ones](docs/COMPATIBILITY.md)). This project is not
> affiliated with Sony Interactive Entertainment or FromSoftware.
- **as in Wine**, the game's x86-64 code runs on the CPU directly, and a runtime written for
this one game replaces the PS4 system libraries;
- **as in DXVK**, the game's graphics are translated to Vulkan — by a renderer derived from
[shadPS4](https://github.com/shadps4-emu/shadPS4) and heavily extended for this game, including
temporal upscaling with AMD FSR 3.1, FSR 4 and FSR 4.1.1;
- memory and the GPU's work follow the PC model on AMD GPUs by default (the new memory and
translation model); NVIDIA and other GPUs keep the old memory model of 0.3 for now. The
launcher's *Memory model* switches either way.
**[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.
One step remains to the full Wine + DXVK model: the new model on NVIDIA as well, then removing the
old memory model (see below).
## Features
> **No game files are included.** You need your own dump of Bloodborne with update 1.09: any retail
> release (CUSA00900 in the US, CUSA03173 for the European Game of the Year edition, ...), as they
> share the same executable.
> This project is not affiliated with Sony Interactive Entertainment, FromSoftware or AMD.
**Status: experimental, playable.** The game boots, loads saves and plays (the Hunter's Dream
and several areas of Yharnam were played with it) with sound, gamepad and saving.
A full play-through has not been verified, and only one machine (Linux, AMD Radeon RX 7800 XT,
Mesa/RADV) has been tested thoroughly.
## Highlights
- **Native execution.** The eboot is converted offline into a flat memory image; PS4 libc and
libSceFios2 are linked into it as native code. No CPU emulation and no per-instruction
translation: the game code runs at full speed.
- **Two memory models** (launcher → *Mode → Memory and translation model*; *Auto*, the default,
picks by GPU):
- **The old memory model, as in 0.3 — the default on NVIDIA, Intel and others.** VRAM copies
of the game's memory, writes tracked through page protection. With every fix made since 0.3
(motion vectors and the upscaler, flicker with DoF on, a damaged shader cache).
- **The new memory and translation model — the default on AMD.** The game's memory lives
in system RAM and the GPU reads it where it is, as a PC game's buffers; data it reads often
is kept in VRAM and given back when unused (textures after 20 s, buffers after 60 s). The
layer's memory module tracks CPU writes to mirrored blocks to keep those copies coherent;
it does not copy the whole GPU-visible memory. The PS4 command processor's work
is translated into Vulkan commands rather than emulated on the CPU: memory writes
(`WRITE_DATA`, DMA) are done by the GPU in command-stream order, and fences are written by the
GPU itself at the end of the pipeline, at the same point of the stream as the PS4's command
processor writes them. On AMD the game's memory is kept in a sparse buffer (the arena); on
NVIDIA, Intel and others it goes through the layer's memory module: bound in place, VRAM
copies as separate buffers, no sparse rebinding (NVIDIA's sparse binding froze the GPU for
seconds); `BB_LAYER_MEMORY=1/0` chooses by hand. Tested on an RX 7800 XT. On NVIDIA it can be chosen by hand (*New*); testers' results differ by scene,
some scenes are slower than with the old model, so it is not the default there yet. Not
tested on Intel. Without the launcher: `BB_PC_MODEL=1/0` (unset: by GPU, as *Auto*).
[Changes in 0.5](docs/CHANGES_0.5.md); the prereleases: [pre4](docs/CHANGES_0.5-pre4.md),
[pre3](docs/CHANGES_0.5-pre3.md), [pre2](docs/CHANGES_0.5-pre2.md).
Unused textures are freed in both modes, so VRAM no longer grows with every area visited.
- **Unlocked frame rate.** Community patches (`patches/Bloodborne.xml`) make the simulation
use the real frame time; ~90 FPS at 4K with FSR 4 Balanced on an RX 7800 XT, ~150 FPS at
1440p with FSR 4 Quality. Also 30/60/90 FPS modes.
- **Temporal upscaling built for this game.** Bloodborne has no velocity buffer, so bbport
computes motion vectors itself: camera motion from depth and the scene matrices, and object
motion (characters, cloth, weapons) from the vertex positions of the previous frame. The
scene is jittered sub-pixel (Halton) and rendered at a reduced resolution; the upscaler fills
the output (720p for the Steam Deck, 1080p, 1440p or 2160p) and the UI is drawn natively at the output resolution.
- **FSR 3.1** (FireBurn/FSR-Vulkan).
- **FSR 4 (INT8, model v07)** on GPUs exposing the required Vulkan shader features —
RDNA2/3 included (see Requirements).
- **FSR 4.1.1**: AMD's 4.1.1 DLL is recorded once under vkd3d-proton and its passes are
replayed natively on Vulkan; the output is **bit-exact** with the DLL. Two variants, as in
the DLL: INT8 on any GPU with the required shader features, and FP8 matrices on RDNA4 (RX
9000; picked automatically). The assets are built on your machine from one DLL of your own
(4.1.x): the launcher's *FSR 4.1.1 from your own AMD DLL → Choose DLL…* button (2–5 minutes,
twice that on RDNA4; needs a recent Proton), or `tools/fsr4cap`.
- Faster than AMD's own shaders on RDNA3: the final passes of FSR 4 and 4.1.1 were rewritten
to store through workgroup memory (3.5× and 2.3× faster, bit-exact); FSR 4 costs ~4 ms at
4K on an RX 7800 XT instead of ~6 ms.
- **Multi-threaded GPU command processing.** The PS4 command stream is decoded on one thread
and draws are bound and recorded on another (two-stage pipeline), with a Vulkan recording
thread and helper threads for memory copies. Early on the single GPU thread capped the game
at ~26 FPS; now it runs at 90–150 FPS depending on resolution and scene.
- **Settings inside the game's own menu:** *System → Display / Effects* (after *Screen/Sound*),
built and drawn by the game itself — output resolution, upscaler, preset, sharpness, FPS
counter, model detail and the game's effects, in the game's language.
- **Overlay menu** (Insert or L3+R3), styled like the game's dialogs: everything above plus the
advanced settings, with gamepad, keyboard and mouse.
- **GTK4 launcher** and an **AppImage** for the Steam Deck.
## How it differs from shadPS4
| | shadPS4 | bbport |
|---|---|---|
| Scope | General PS4 emulator, many games | One game: Bloodborne v1.09 |
| Loading | Its own ELF loader and kernel emulation at run time | The eboot is converted offline (`scripts/`) into an image with PS4 libc/Fios2 linked in; a C loader maps it and jumps into the game (loader and runtime: ~5k lines) |
| Memory | The GPU's view of PS4 memory is kept in VRAM copies, synchronized through page-protection write tracking | By default the same model (as in 0.3); in the *New memory and translation model* mode: the game's memory in system RAM, used by the GPU in place, frequently read data in VRAM, freed when unused |
| Command processor | Emulated: memory writes, DMA and fences are done by the CPU while decoding | By default the same; in the new mode translated into Vulkan commands that the GPU runs in stream order, fences written after the work has really finished |
| System libraries | Broad HLE of the PS4 OS | A small runtime (`src/runtime_*.c`) that implements exactly what Bloodborne calls: memory, threads, sync, files, audio (incl. ATRAC9), pad, saves, AppContent |
| GPU | shadPS4 video core and shader recompiler | The same core (vendored, GPL) with ~200 marked changes (`bbport:`) plus new modules: two-stage draw pipeline, render-state and texture-set memoization, render-scale proxies, motion vectors, FSR 3.1/4/4.1.1, frame capture and GPU profiler |
| GPU thread | One thread processes the whole command stream (the bottleneck in Bloodborne) | Decode and draw recording run on separate threads; the work scales with the hardware threads (Steam Deck included) |
| Upscaling | — | Temporal (FSR 3.1, FSR 4, FSR 4.1.1) with the game's own motion vectors and jitter |
| Game patches | Patch files applied by the emulator | The same community patches, compiled at start (`scripts/patches.py`); render resolution, effects and FPS from the launcher |
Without shadPS4 there would be no bbport: its renderer and shader recompiler are the base of
the graphics side.
### Wine + DXVK for one game
- **CPU.** The PS4 CPU is x86-64, so the game's code runs directly on the PC's CPU, with no
emulation and no instruction translation.
- **System libraries.** As Wine replaces the Windows API, the bbport runtime (`src/runtime_*.c`)
implements exactly the PS4 OS functions Bloodborne calls: memory, threads, files, audio, pad,
saves.
- **Memory.** In the new mode, as in a PC game: the game's data in system RAM, VRAM used the way
a PC game uses it. The mode is still experimental.
- **Graphics.** The PS4 GPU's command stream (PM4) is a recording of the game's graphics API
calls: it is written by 99 functions of the statically linked libGnm, so decoding the stream
and translating the calls themselves come to the same thing. GCN shaders are translated to
SPIR-V. In the new mode the command processor's work (memory writes, DMA, fences) is
translated into Vulkan commands as well; fences are written by the GPU itself. Resource
descriptors (textures, buffers) are read by the translator on the CPU, as DXVK reads resource
bindings: a PS4 texture has to be converted from the PS4 tiling into a Vulkan image before the
draw runs, so the CPU must know its descriptor beforehand — translation, not emulation. The
descriptor tables (the constant engine's dumps) are read only by the translator, so the CPU
performs them. The game's occlusion queries (~60 a frame) are
Vulkan occlusion queries; predication, `COPY_DATA` and `COND_EXEC` are translated too (check:
`BB_PM4_SELFTEST=1`). Details:
[docs/EMULATION_REMOVAL_PLAN.ru.md](docs/EMULATION_REMOVAL_PLAN.ru.md), "Шаг 4" (Russian).
"Port" here means a build for this one game, not a rewrite of its source code, which the project
neither has nor includes.
- **Multiplayer**: online messages, bloodstains, ghosts, bells and summons, on
[The Hunter's Dream](https://thehuntersdream.com) or a server of your own; **Seamless Co-op**
(experimental), co-op that lasts through deaths, lamps and areas, for up to 5 hunters; and
**local co-op** (split screen), 2 to 4 players on one PC, each with their own controller.
- **In-game map** (Hunter's Atlas): a fullscreen map with where you are, lamps and items, and fast
travel to the lamps you've found.
- **Frame rate unlocked** (up to 120 FPS) 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.
- **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, keys you can rebind
(or bbport 0.6's Dark Souls III-style layout) and the mouse as a pointer in the game's menus.
- **Quit to Desktop** from the game's title and System menus (it saves first).
- **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,
Portuguese, Russian and Simplified Chinese.
- **AppImage** ready for any distribution and the Steam Deck, which **updates itself** from the
launcher (and can go back to an earlier release).
- **Mods and community patches**, without touching the game files ([how](docs/MODS.en.md)).
## Requirements
- Linux x86-64, a Vulkan 1.3 GPU. Tested: AMD RX 7800 XT with Mesa 26 (RADV).
The *New memory and translation model* mode is not tested on NVIDIA yet.
FSR 4 / 4.1.1 require shader Float16, Int8/Int16, integer dot products, linear compute
derivatives and extended storage image formats; FSR 4.1.1 additionally requires
`VK_VALVE_shader_mixed_float_dot_product`. Unsupported choices fall back to FSR 3.1
before the first frame and are disabled in the in-game menu.
- Your decrypted game dump: the `CUSA03173` folder (eboot.bin, sce_module, ...), version 1.09.
A dumped update is a separate folder: copy it over the base game, replacing files. The base
game alone (1.00) crashes at start (guest offset 0x20348b8); the launcher and `run.sh` check
the executable and say what is missing (`BB_SKIP_GAME_CHECK=1` skips the check). The other
retail releases (the US `CUSA00900`, the Asian `CUSA03023`, ...) run the same 1.09 executable. Some dumps ship it
with Lance McDonald's 60 fps patch baked in, which crashes when the gestures menu opens: the
check asks for a clean eboot.bin then (bbport has its own 60 fps).
- To build: GCC, CMake, Ninja, Python 3, glslang, SDL3, Vulkan headers and the libraries in
`shell.nix`. With [Nix](https://nixos.org) everything comes from `shell.nix` automatically.
- Linux x86-64 and a GPU with Vulkan 1.3: AMD or Intel with Mesa, or NVIDIA with NVIDIA's driver.
- About 6 GB of free memory, 10 GB for 1440p or 4K.
## Build and run
## Getting started
1. Download `Bloodborne-bbport-x86_64.AppImage` from the
[latest release](https://github.com/ayozetr/bloodborne_linux/releases/latest).
2. Make it executable and run it.
3. Choose your game folder (the one with `eboot.bin`) and press **Play**.
Saves and settings go to `~/.local/share/bbport`; **Setup → Saves** exports your saves into a
.zip and imports them back, or from shadPS4, and can keep a copy of them from before each game
(off by default). The Game of the Year edition already has The Old
Hunters; with the base game, its DLC goes in **Setup → DLC folder** ([how](docs/COMPATIBILITY.md)).
## Online play
In the launcher's **Multiplayer → Online** tab, turn on **Play online**, choose a server and press **Test**
in the **Status** row (every line should get a check mark):
- **Seamless Hunt** (the default): the community server of Seamless Co-op (BBSC), with players on
the Windows port. Make an account from the launcher with **Create** ([docs/ONLINE.md](docs/ONLINE.md#seamless-hunt)).
- **The Hunter's Dream**: sign in with Discord, 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.
- **Your own server**: for your group, with Docker and Tailscale ([docs/SERVER.md](docs/SERVER.md)).
- **Seamless Co-op (BBSC)**, in the same tab: every player needs it, and summons across areas need
its game server, which your own server can run ([docs/ONLINE.md](docs/ONLINE.md#seamless-co-op-bbsc)).
With online play on, the game starts online by itself, as PLAY ONLINE on a PS4. If you turned off
*Skip the “play online / offline” choice* and pick offline, choose **Log In** on the title screen later
for online play; until then the co-op bells are greyed out.
Everyone in a session needs update 1.09. Turn cheats off when you play with others. Ports,
firewalls and the rest: [docs/ONLINE.md](docs/ONLINE.md).
## Local co-op (experimental)
Several games on one PC, one per player, each in its part of the screen with its own controller
(the launcher's **Multiplayer → Local** tab): choose the players (2 to 4), the layout and each player's
controller, then press **Play together**. Player 1 plays with the launcher's settings and saves;
players 2 to 4 get their own saves and settings in `~/.local/share/bbport/coop/player<N>` (copied
from player 1's the first time, about 400 MB each), without online play or Discord. One player can
use the keyboard and mouse. Only player 1 has sound unless you turn that off. The windows can go
to a monitor of their own (**Monitor**), and **Copy player 1's saves** gives another player the
same characters (their own saves are backed up first). The windows are placed through X11, which
works in a Wayland session too (XWayland).
## Building from source
On Arch Linux (or CachyOS, Manjaro…):
```bash
git clone --recursive https://github.com/deadinside28/bloodborne_pc.git bbport && cd bbport
bash build.sh # builds out/bb-probe and out/gpu/libbbgpu.so
BB_GAME_DIR=/path/to/CUSA03173 bash run.sh
sudo pacman -S --needed base-devel cmake ninja python python-gobject gtk4 libadwaita sdl3 \
vulkan-headers vulkan-icd-loader glslang spirv-headers spirv-cross ffmpeg boost fmt \
robin-map xxhash zydis miniz protobuf nlohmann-json openssl miniupnpc python-pillow ttf-dejavu
paru -S magic_enum vulkan-memory-allocator xbyak # from the AUR
git clone --recursive https://github.com/ayozetr/bloodborne_linux
cd bloodborne_linux
bash build.sh
python3 launcher/bbport_launcher.py
```
or the launcher (pick the game folder, settings, *Start*):
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
variables for the launcher's **Advanced → Developer → Extra variables**: [docs/ENVIRONMENT.md](docs/ENVIRONMENT.md).
Problems: [docs/TROUBLESHOOTING.md](docs/TROUBLESHOOTING.md).
```bash
bash launcher/bb-launcher.sh # launcher/install-desktop.sh adds it to the app menu
```
## On Windows
By default the game folder is expected next to the repository (`../CUSA03173`). Saves and the
shader cache go to `user/` (the launcher lets you choose another folder); settings to
`bbport.ini`. A gamepad is used through SDL3 (the launcher's *Controls → Controller* picks one
when several are connected; `BB_GAMEPAD=<GUID or part of the name>`). The keyboard works too,
also next to a connected gamepad (the Steam Deck always has one); both are remapped in the
launcher (*Controls*). The character name is typed on the keyboard in a box over the game.
The touchpad: its left half (Tab, Back/Select) opens the gestures, the right half (Backspace)
the key items. There is no PSN: the *Play online / offline* screen is skipped and the game opens
its main menu offline (a game patch; the launcher's *Game effects → Skip the “play online / offline” choice*,
`BB_SKIP_NETWORK_CHOICE=0` shows it). With several monitors, *Screen → Monitor* in the launcher
(`BB_DISPLAY=<number or part of the name>`) picks the one for the game.
If you need a Windows version, I recommend
[Supermedo's Windows port](https://github.com/Supermedo/bloodborne_pc) and
[Awakenchan's LanternEngine](https://github.com/Awakenchan/LanternEngine).
**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.
## Support
**TAA:** a separate native-resolution temporal AA mode in the launcher and overlay,
switchable live without an FSR model. The saved FSR preset is restored when returning
to FSR. FSR Native AA adds reconstruction on top of full-resolution rendering and can
be slower than disabling AA. TAA also adds work compared with no temporal AA.
The RCAS switch and the 0–2 sharpness control also work with TAA. Sharpening runs after
temporal accumulation and leaves its history and HUD unchanged.
This fork is built on the port deadinside28 created, but it has a lot of hours of work behind it,
and above all hours and hours of testing things and hunting down fixes so it runs as well as it
can. I always try to fix every report as soon as possible, because I know how much this game means
to some people. If it's useful to you and you'd like to support it, you can on
[Ko-fi](https://ko-fi.com/ayozetr).
**Mods:** the launcher accepts separate loose-file mod folders (with `dvdroot_ps4/`, an extra
wrapper folder, or the game folders such as `chr/` directly; file name case does not matter),
with enable switches and load order. A sibling `CUSA03173-mods/` overlay also works.
The original game is preserved; later mods override conflicting files.
**Third-party patches:** shadPS4-format XML patch files in the data directory's `patches/`,
switched on and off in the launcher. See [mods and patches](docs/MODS.md).
**DLC:** put your add-on dumps in `user/addcont/<title id>/<entitlement label>/` (shadPS4's
layout, e.g. `user/addcont/CUSA03173/SPEXPANSIONDLC03` for The Old Hunters): each folder there is
reported to the game as an installed add-on.
## Credits
**Launcher language:** Russian, English, Brazilian Portuguese or Simplified Chinese (follows the system language by default).
Built on [bbport](https://github.com/deadinside28/bloodborne_pc) by deadinside28, with
[shadPS4](https://github.com/shadps4-emu/shadPS4)'s renderer, and with work from many people and
projects: everyone is in [docs/CREDITS.md](docs/CREDITS.md). It includes code from
Lantern Engine by Awaken (https://github.com/Awakenchan/LanternEngine). Components and their licenses:
[BBPORT.md](BBPORT.md#credits-and-licenses).
**Free camera and game debug menu** (v1.09): enable the corresponding switches in the
launcher or in-game menu and restart. Free camera uses Lance McDonald's
[GoldHEN patch](https://github.com/GoldHEN/GoldHEN_Patch_Repository/blob/main/patches/xml/Bloodborne-Orbis.xml):
hold Cross and press L3 to cycle modes (keyboard: hold Space and press Z). It needs no fonts
and conflicts with *Enemy Control*.
For the game debug menu, install `DbgFont14h.ccm` and `DbgFont14h.tpf` from
[Debug Menu and XML Patch](https://www.nexusmods.com/bloodborne/mods/253) into the game's
`dvdroot_ps4/font/` first. Startup rejects missing or empty font files instead of launching
the unsafe patch. Open it with the left touchpad / Tab (with the debug menu on, the left half
no longer opens the gestures); Backspace is the right touchpad.
Touch coordinates are forwarded from SDL gamepads; Back/Select emulates a left click on
pads without a touch surface. The port's settings menu remains Insert / L3+R3.
## License
GPU occlusion queries still use synthetic pixel counters (`PixelPipeStatDump`), and
`IT_SET_PREDICATION` is unimplemented. Free camera allows visual investigation; it does
not implement GPU occlusion culling.
**Upscaler assets** (not included; FSR 3.1 needs none):
```bash
bash tools/fetch_fsr4_assets.sh # FSR 4 v07 (MIT, built from AMD's source by Q2RTX)
# FSR 4.1.1, from your own AMD DLLs (e.g. OptiScaler's FSR4_LATEST), needs GE-Proton 10 or newer:
bash tools/fsr4cap/build_assets.sh <amd_fidelityfx_upscaler_dx12.dll>
```
**FSR 4.1.1 from your own DLL.** The easiest way is the launcher: *Upscaler → FSR 4.1.1 from your
own AMD DLL → Choose DLL…*, then pick `amd_fidelityfx_upscaler_dx12.dll` version 4.1.x: from
OptiScaler's `FSR4_LATEST` folder or from a game with FSR 4.1. That one file is enough: AMD's DLL
exports the FidelityFX API itself, so no loader is needed (only a DLL without these exports would
be recorded through `amd_fidelityfx_loader_dx12.dll`, and the launcher would ask for it). The DLL is checked at once,
without a long capture; these do not fit: AMD's official FSR 4.0.x (e.g. Pragmata's 4.0.3 —
AMD enables it on RDNA4 only, and under Proton it does not start on other GPUs) and community
builds (4.0.2b, 4.1.1b and similar OptiScaler INT8 builds: another model and pass count). Then
the DLL runs under Proton and is recorded at 20 sizes (progress in the row and in the *Log* tab;
on RDNA4 a second time, for the FP8 variant),
the passes are translated to SPIR-V, and FSR 4.1.1 is selected. The Proton build is picked
automatically — the first under which the DLL enables FSR 4.1: GE-Proton 10 or newer,
Proton-CachyOS, Proton Experimental (tested: GE-Proton 11, Proton-CachyOS 11, Experimental of
October 2026; Steam's Proton 11.0 and GE-Proton 9 do not). It runs in the Steam runtime it
requires (Steam installs it the first time any game runs with that Proton), or through
umu-launcher when installed. On NixOS umu-launcher comes from `nix-shell` (downloaded the first
time, ~1.7 GB); the AppImage runs the recording on the system itself, through the user's systemd
(`systemd-run --user`), since the system's `/nix` is out of its sight. Recording on the Steam Deck is not verified yet; if the DLL does not
enable FSR 4.1 there, build on a PC and copy the `fsr4_411` folder. The same from the command
line: the launcher's (and the AppImage's) `--build-fsr411 <DLL>`.
**DLSS (NVIDIA GeForce RTX).** NVIDIA's DLSS SDK is not included: check out
[github.com/NVIDIA/DLSS](https://github.com/NVIDIA/DLSS) (`include/` and `lib/Linux_x86_64` are
enough) and build with `DLSS_SDK_ROOT=/path/to/DLSS bash build.sh`. That also builds the bridge
`out/libbbport_dlss.so` (`gpu/dlss_bridge`, the only code that uses the SDK) and copies NVIDIA's
`libnvidia-ngx-dlss.so.<version>` next to `out/bb-probe`; the driver provides the rest. Then pick
*DLSS* in the launcher or the in-game menu: the preset sets the render size, Native AA is DLAA.
DLSS gets the inputs FSR 3.1 gets (scene color, depth, motion vectors, jitter). Without the
libraries, on other GPUs or with `BB_DLSS=0` it is listed as unavailable, and a DLSS setting falls
back to FSR 3.1.
The AppImage built with `DLSS_SDK_ROOT` carries the bridge and NVIDIA's library (NVIDIA's RTX SDK
license allows that in an application; its text is `NVIDIA-DLSS-LICENSE.txt` in the package). A
newer `libnvidia-ngx-dlss.so.<version>` (from the SDK's `lib/Linux_x86_64/rel`) is chosen in the
launcher: *Upscaler → DLSS: NVIDIA's library → Choose a file…*, as the FSR 4.1.1 DLL; it is
copied to `<user>/dlss/`, which the game prefers. A game's `nvngx_dlss.dll` is the Windows
library: NVIDIA's Linux driver does not load it.
From source the script takes its tools from the system (MinGW GCC, CMake, Ninja, Python 3,
SPIRV-Tools, Git) or from Nix; the AppImage has them prebuilt.
**FP8 on RDNA4.** When vkd3d-proton offers FP8 cooperative matrices (RDNA4), the DLL runs
another variant of its passes: other shaders and weights, other dispatch sizes. The build records
both, INT8 into `fsr4_411/` and FP8 into `fsr4_411/fp8/`, and the game picks FP8 itself when the
GPU has FP8 matrices (`VK_EXT_shader_float8`); the log says `Upscaler: FSR 4.1.1 replay, FP8 …`.
`BB_FSR411_VARIANT=int8` forces INT8. Checked on an RX 7800 XT through vkd3d-proton's FP16
emulation of FP8 (`BB_FSR4CAP_FP8=1` records it there too, `fp8emu/`): bit-exact with the DLL;
**not yet run on RDNA4 hardware.**
**AppImage** (Steam Deck): `bash build.sh && bash packaging/appimage.sh` →
`dist/Bloodborne-bbport-x86_64.AppImage`; data in `~/.local/share/bbport`, `--play` starts the
game without the launcher window (Game Mode). FSR 4.1.1 models are not packaged: build them with
the launcher's button (see above); they go to `~/.local/share/bbport/fsr4_411`
(`BB_PACKAGE_FSR411=1` bundles a local `fsr4_411` into an AppImage for your own devices). On the Steam Deck pick the 1280×720 output (the
game is 16:9; on the 1280×800 screen it gets thin bars).
**Adding the AppImage to Steam** (*Add a Non-Steam Game*) needs no options; the compatibility tool
does not matter. (Steam preloads its overlay into every non-Steam game; the AppImage removes it
before its own programs start, so the Steam overlay is not shown in the game.) Where Steam
runs games without FUSE (NixOS: Steam's FHS sandbox; the AppImage then exits with *Cannot mount
AppImage*), set the launch options to
```
TMPDIR=$HOME/.cache APPIMAGE_EXTRACT_AND_RUN=1 NO_CLEANUP=1 %command%
```
The AppImage then unpacks itself (~2 GB, `~/.cache/appimage_extracted_*`) on the first start
(~10 s) and reuses that copy afterwards; a new AppImage version gets a new copy, the old one can
be deleted. Without `TMPDIR` it would unpack into Steam's `/tmp`, which is in RAM there. Add
` --play` after `%command%` to skip the launcher.
**NVIDIA in the AppImage:** startup discovers the host's installed 64-bit NVIDIA Vulkan ICD
and exposes its vendor libraries alongside the bundled AMD/Intel drivers. This keeps the
NVIDIA userspace driver matched to the host kernel module. Standard Linux distributions keep
these libraries under `/usr/lib*`; on NixOS the AppImage's internal `/nix/store` may hide them.
In that case copy the NVIDIA libraries into an accessible directory and set
`BB_NVIDIA_LIB_DIR=/path/to/libraries` (the NVIDIA manifest must also be accessible).
With the host's NVIDIA driver the AppImage also passes the host's EGL vendor files, NVIDIA's
first (`__EGL_VENDOR_LIBRARY_FILENAMES`; without them NVIDIA's ICD gave no `vkCreateInstance` on
some hosts, #107; `BB_NVIDIA_EGL=0`: off). Explicit `VK_DRIVER_FILES`/`VK_ICD_FILENAMES` overrides are preserved. Diagnose drivers inside
the package with:
```bash
./Bloodborne-bbport-x86_64.AppImage --vulkan-info 2>&1 | tee bbport-vulkan.log
```
A user reported successful startup with FSR 3 on a GTX 1060 6GB (Fedora 44, NVIDIA
580.178.04); selecting FSR 4 caused a black window. Use FSR 3 on this configuration.
MangoHud is bundled in the AppImage; enable its checkbox in the launcher. If MangoHud is also
installed system-wide, or Steam's performance overlay is on (Steam Deck game mode), only one
overlay is drawn (two drew doubled, offset text).
When running from source, install MangoHud separately. A diagnostic launch with
`VK_LOADER_LAYERS_DISABLE=~implicit~` also disables MangoHud.
Useful variables: `BB_FRAME_STATS=1` (frame statistics, including a `Memory:` line: VRAM, GTT,
RSS, images and guest blocks in VRAM), `BB_PC_MODEL=1` (the new memory and translation model,
0, the old model as in 0.3, is the default), `BB_ANISO=N` (anisotropic filtering of scene textures; 16 by
default, 0 = the game's own),
`BB_GC_IDLE_SECONDS=N` / `BB_VRAM_IDLE_SECONDS=N` (how long unused textures / buffers stay in VRAM;
20 / 60), `BB_BREADCRUMBS=0` (no GPU breadcrumbs; with them a GPU hang names the draw or dispatch
it is stuck in), `BB_GPU_PROFILE=1` (GPU time per
pass), `BB_FSR4_PROFILE=1` (GPU time per FSR 4 pass), `BB_UPSCALER=taa|fsr3|fsr4|fsr411|off|none`,
`BB_FRAMES_AHEAD=N` (how many frames the GPU command thread may run ahead of the GPU; 1 by default,
0 = unbounded), `BB_PRESENT_THREAD=0` (present on the vblank thread, as before),
`BB_LIVE_RES=1` (live resolution changes instead of the startup patch for outputs other than 1080p),
`BB_PAD_RECORD=file` / `BB_PAD_REPLAY=file` (record a route with F9, replay it in scripted tests),
`BB_GC_BUDGET_MB=N` (texture cache budget, as on integrated GPUs), `BB_VRAM_LIMIT_MB=N` (the VRAM
budget taken as at most N MiB: checks the behaviour of cards with little VRAM), `BB_ASYNC_PIPELINES=0`
(new pipelines compile at once and the game waits; by default in the background), `BB_PRESENT_DUMP_TRIGGER=file`
with `BB_PRESENT_DUMP_COUNT=N` (dump N consecutive presented frames),
`BB_FSR411_VARIANT=int8|fp8|fp8emu` (FSR 4.1.1 variant; by default FP8 where the GPU has FP8
matrices), `BB_READBACKS=0|1|2` (reads of GPU-written memory by the game: 1 by default, 2
precise and slow, 0 off).
More in [docs/](docs); recent changes: [docs/CHANGES_0.4.md](docs/CHANGES_0.4.md) (in Russian),
[docs/CHANGES_2026-10-06.md](docs/CHANGES_2026-10-06.md).
**Online play (shadNet).** A separate module, `out/gpu/libbbnet.so` (`gpu/bbnet`: shadPS4's network
and PSN libraries from shadp2p, a shadPS4 fork, with the shadNet client), built when protobuf,
nlohmann_json, OpenSSL, zlib and miniupnpc are there (`shell.nix`) and included in the AppImage.
The launcher's *Online* page turns it on (`BB_ONLINE=1`): messages, bloodstains and ghosts come
from The Hunter's Dream, bells and summons go through a shadNet server (`srv.shadps4.net:31313` by
default) with a shadNet account (Online ID and password). Offline the module is never loaded; a
build without it plays offline only.
## Repository layout
| Path | Contents |
|---|---|
| `src/` | Loader (`probe.c`) and the HLE runtime |
| `scripts/` | Offline preparation of the game image, module linking, patch compiler |
| `gpu/` | Renderer library: vendored shadPS4 video core with this port's changes (`gpu/VENDOR.txt`), shims, ImGui menu, FSR 4.1.1 runtime (`gpu/shadps4/video_core/renderer_vulkan/fsr411`) |
| `launcher/`, `packaging/` | GTK4 launcher; Nix package and AppImage |
| `patches/` | Community patches for Bloodborne |
| `tools/` | Developer tools: scripted runs, A/B toggles, FSR benchmark helpers, FSR 4 shader rewrites, `fsr4cap` (FSR 4.1.1 recording/extraction) |
| `tests/` | Loader, runtime, patch and renderer tests |
| `docs/` | Design notes and measurements ([upscaler](docs/upscaler.md), [parallel GPU](docs/parallel_gpu.md), [motion vectors](docs/motion_vectors.md), [roadmap](docs/ROADMAP.md)) |
Tests: `bash build.sh --test`, `python3 -m unittest discover -s tests`, and
`ninja -C out/gpu motion-history-test ui-composition-test scene-resolution-test motion-shader-test settings-test`.
## Roadmap
- The new memory and translation model on by default on NVIDIA too, once it is as fast there;
the old model is removed after that.
- Textures as objects created at load time (less guessing by address, uploads outside the
frame).
- Shaders translated ahead of time, at install, not during play.
- More CPU parallelism in GPU command processing (split the draw-recording stage further),
scaling to all hardware threads — most important for the Steam Deck.
- Async compute for the upscaler (the frame is GPU-bound at 4K).
- XeSS (super resolution) and XeFG frame generation through a Wine helper sharing Vulkan
memory (a memory-bridge prototype is in `tools/bridge_helper`); inputs exposed so that OptiScaler-style mapping works.
- Frame generation (FSR 3.1 FG first), reactive and transparency masks for particles and fog.
- Fix the races in AMD's FSR 4.1.1 shaders at output widths that are not multiples of 64
(e.g. 1600×900), as already done for the left-edge race in FSR 4 v07 at 1080p.
- Steam Deck validation of the AppImage; HDR output.
## Credits and licenses
bbport is licensed under the **GNU GPL v2 or later** ([LICENSE](LICENSE)) — it contains code
from shadPS4 (GPL-2.0-or-later). Third-party components keep their licenses:
[shadPS4](https://github.com/shadps4-emu/shadPS4) video core and shader recompiler (GPL-2.0+),
[sirit](https://github.com/shadps4-emu/sirit), [half](https://half.sourceforge.net/),
[FSR-Vulkan](https://github.com/FireBurn/FSR-Vulkan) by FireBurn (MIT; FSR 3.1 on Vulkan and the
FSR 4 v07 provider), AMD FidelityFX SDK (MIT), [LibAtrac9](https://github.com/Thealexbarney/LibAtrac9)
(MIT), [Dear ImGui](https://github.com/ocornut/imgui) (MIT), DejaVu fonts,
[dxil-spirv](https://github.com/HansKristian-Work/dxil-spirv) (MIT, used to build the
FSR 4.1.1 assets). Game patches by Kyo, Lance McDonald, auser1337, illusion, emoose and other
community members (`patches/Bloodborne.xml`). The DLSS bridge (`gpu/dlss_bridge`, MIT) and
loader come from [Supermedo's Windows port](https://github.com/Supermedo/bloodborne_pc), adapted
from [IFreemz/shadPS4-Bloodborne-DLSS-FSR](https://github.com/IFreemz/shadPS4-Bloodborne-DLSS-FSR).
AMD's FSR 4 DLLs and model data and NVIDIA's DLSS SDK and libraries are not distributed here.
GNU GPL v3 or later ([LICENSE](LICENSE)). Files taken from other projects keep their own notices:
most are GPL-2.0-or-later (bbport, shadPS4), which allows them to be distributed under
GPL-3.0-or-later. Third-party components keep their own licenses. NVIDIA
and DLSS are trademarks of NVIDIA Corporation.
-399
View File
@@ -1,399 +0,0 @@
ДАННЫЙ ПРОЕКТ НЕ ИМЕЕТ ОТНОШЕНИЯ К SHADPS4. ВСЕ ВОПРОСЫ, СВЯЗАННЫЕ С ЭТИМ ПРОЕКТОМ - ПИШИТЕ НА ДИСКОРД СЕРВЕР https://discord.gg/KYZRKk9CB, А НЕ НА СЕРВЕР SHADPS4
# bbport — нативный порт Bloodborne для Linux
[English](README.md) · **Русский**
bbport — это аналог Wine + DXVK, только для одной игры: *Bloodborne* для PlayStation 4
(CUSA03173, версия игры 1.09) на ПК с Linux x86-64. Оригинальный исполняемый файл игры
запускается прямо на ПК:
- **как в Wine**, x86-64-код игры выполняется на процессоре напрямую, а системные библиотеки PS4
заменяет собственный рантайм, написанный под эту игру;
- **как в DXVK**, графика игры переводится в Vulkan — рендером на основе
[shadPS4](https://github.com/shadps4-emu/shadPS4), сильно доработанным под Bloodborne, вплоть
до временного апскейлинга AMD FSR 3.1, FSR 4 и FSR 4.1.1;
- память и работа видеочипа на AMD по умолчанию устроены как у игры для ПК (новая модель памяти
и трансляции); NVIDIA и другие видеокарты пока работают со старой моделью памяти, как в 0.3.
В лаунчере *Модель памяти* переключается в обе стороны.
До полной модели Wine + DXVK остался один шаг: новая модель и на NVIDIA, затем удаление старой
модели памяти (см. ниже).
> **Файлов игры в репозитории нет.** Нужен свой дамп Bloodborne с обновлением 1.09: любое издание
> из магазина (CUSA00900 в США, CUSA03173 — европейское Game of the Year, ...): у них один и тот же
> исполняемый файл.
> Проект не связан с Sony Interactive Entertainment, FromSoftware и AMD.
**Состояние: экспериментальный, играбельный.** Игра запускается, загружает сохранения и
проходится (Сон Охотника и несколько локаций Ярнама) со звуком,
геймпадом и сохранением. Полное прохождение не проверено; основательно проверена одна машина
(Linux, AMD Radeon RX 7800 XT, Mesa/RADV).
## Что сделано
- **Нативное исполнение.** eboot заранее превращается в плоский образ памяти, в который как
нативный код прилинкованы libc и libSceFios2 из PS4. Никакой эмуляции процессора и
трансляции инструкций: код игры работает на полной скорости.
- **Две модели памяти** (лаунчер → *Режим работы → Модель памяти и трансляции*; *Авто*, по
умолчанию, выбирает по видеокарте):
- **Старая модель памяти, как в 0.3, — по умолчанию на NVIDIA, Intel и других.** Копии памяти
игры в VRAM, записи отслеживаются защитой страниц. Со всеми исправлениями после 0.3
(векторы движения и апскейлер, мерцание при включённом DoF, повреждённый кэш шейдеров).
- **Новая модель памяти и трансляции — по умолчанию на AMD.** Память
игры находится в оперативной памяти, и GPU читает её на месте, как буферы игры для ПК; то,
что он читает часто, держится в видеопамяти и освобождается, когда перестаёт использоваться
(текстуры — через 20 с, буферы — через 60 с). Модуль памяти прослойки отслеживает записи CPU
в блоки с зеркалами для согласования данных; вся память, видимая GPU, не копируется.
Работа командного процессора PS4 переводится в команды Vulkan, а не
эмулируется на процессоре: записи в память (`WRITE_DATA`, DMA) выполняет видеочип в порядке
потока команд, метки (fences) пишет сам видеочип в конце конвейера, в той же точке потока,
что и командный процессор PS4.
На AMD память игры держится в sparse-буфере (арене); на NVIDIA, Intel и других — через модуль
памяти прослойки: привязывается на месте, копии в VRAM — отдельные буферы, без
sparse-перепривязок (на NVIDIA sparse binding держал видеочип секундами); `BB_LAYER_MEMORY=1/0`
— выбрать вручную. Проверено на RX 7800 XT. На NVIDIA её можно выбрать
вручную (*Новая*); у тестеров результат зависит от сцены, в части сцен медленнее старой
модели, поэтому там она пока не по умолчанию. На Intel не проверялась. Без лаунчера —
`BB_PC_MODEL=1/0` (не задано — по видеокарте, как *Авто*).
[Изменения в 0.5](docs/CHANGES_0.5.md); пререлизы: [pre4](docs/CHANGES_0.5-pre4.md),
[pre3](docs/CHANGES_0.5-pre3.md), [pre2](docs/CHANGES_0.5-pre2.md).
Неиспользуемые текстуры освобождаются в обоих режимах, поэтому видеопамять больше не растёт
с каждой локацией.
- **Разблокированная частота кадров.** Патчи сообщества (`patches/Bloodborne.xml`) переводят
симуляцию на реальное время кадра; ~90 FPS в 4K с FSR 4 Balanced на RX 7800 XT, ~150 FPS в
1440p с FSR 4 Quality. Есть режимы 30/60/90 FPS.
- **Временной апскейлинг под эту игру.** Буфера скоростей в Bloodborne нет, поэтому порт
считает векторы движения сам: движение камеры — по глубине и матрицам сцены, движение
объектов (персонажи, ткань, оружие) — по позициям вершин прошлого кадра. Сцена сдвигается
субпиксельным jitter (Halton) и рисуется в уменьшенном разрешении, апскейлер достраивает
вывод (720p для Steam Deck, 1080p, 1440p или 2160p), интерфейс рисуется сразу в выходном разрешении.
- **FSR 3.1** (FireBurn/FSR-Vulkan).
- **FSR 4 (INT8, модель v07)** на видеокартах с нужными возможностями шейдеров Vulkan,
включая RDNA2/3 (см. требования).
- **FSR 4.1.1**: DLL AMD 4.1.1 один раз записывается под vkd3d-proton, а её проходы
воспроизводятся нативно в Vulkan; результат **побитово совпадает** с DLL. Два варианта, как
в самой DLL: INT8 на любой видеокарте с нужными возможностями шейдеров и матрицы FP8 на
RDNA4 (RX 9000; выбирается сам). Ассеты собираются на твоей машине из одной твоей DLL
4.1.x: в лаунчере кнопка *FSR 4.1.1 из своей DLL AMD → Выбрать DLL…* (2–5 минут, на RDNA4
вдвое дольше; нужен свежий Proton), или `tools/fsr4cap`.
- Быстрее шейдеров самой AMD на RDNA3: финальные проходы FSR 4 и 4.1.1 переписаны с записью
через shared-память (в 3.5 и 2.3 раза быстрее, побитово так же); FSR 4 в 4K на RX 7800 XT
стоит ~4 мс вместо ~6.
- **Многопоточная обработка GPU-команд.** Поток команд PS4 разбирается в одном потоке, а
привязка ресурсов и запись draw — в другом (двухстадийный конвейер), плюс отдельный поток
записи команд Vulkan и вспомогательные потоки для копий памяти. В начале проекта единственный
поток GPU ограничивал игру ~26 FPS; сейчас 90–150 FPS в зависимости от разрешения и сцены.
- **Настройки в меню самой игры:** *Система → Изображение / Эффекты* (после *Экран/звук*) — их
строит и рисует сама игра: разрешение вывода, апскейлер, пресет, резкость, счётчик FPS,
детализация моделей и эффекты игры, на языке игры.
- **Меню поверх игры** (Insert или L3+R3) в стиле диалогов игры: всё то же и расширенные
настройки; геймпад, клавиатура и мышь.
- **Лаунчер на GTK4** и **AppImage** для Steam Deck.
## Чем отличается от shadPS4
| | shadPS4 | bbport |
|---|---|---|
| Охват | Универсальный эмулятор PS4, много игр | Одна игра: Bloodborne v1.09 |
| Загрузка | Свой ELF-загрузчик и эмуляция ядра во время работы | eboot заранее превращается (`scripts/`) в образ с прилинкованными libc/Fios2 PS4; загрузчик на C отображает его и передаёт управление игре (загрузчик и рантайм — ~5 тыс. строк) |
| Память | Вид памяти PS4 со стороны GPU хранится копиями в VRAM, синхронизация через отслеживание записей защитой страниц | По умолчанию та же модель (как в 0.3); в режиме *Новая модель памяти и трансляции*: память игры в оперативной памяти, GPU использует её на месте, часто читаемое — в VRAM, освобождается, когда не нужно |
| Командный процессор | Эмулируется: записи в память, DMA и метки выполняет процессор во время разбора команд | По умолчанию так же; в новом режиме переводится в команды Vulkan, их выполняет видеочип в порядке потока, метки — после реального завершения работы |
| Системные библиотеки | Широкий HLE ОС PS4 | Небольшой рантайм (`src/runtime_*.c`) ровно под то, что вызывает Bloodborne: память, потоки, синхронизация, файлы, звук (включая ATRAC9), геймпад, сохранения, AppContent |
| GPU | Видеоядро и рекомпилятор шейдеров shadPS4 | То же ядро (вложено, GPL) с ~200 помеченными правками (`bbport:`) и новыми модулями: двухстадийный конвейер draw, мемоизация состояния прохода и наборов текстур, уменьшенные цели сцены, векторы движения, FSR 3.1/4/4.1.1, захват кадра, профайлер GPU |
| Поток GPU | Один поток обрабатывает весь поток команд (узкое место в Bloodborne) | Разбор и запись draw идут в разных потоках; работа масштабируется под число потоков (в том числе Steam Deck) |
| Апскейлинг | — | Временной (FSR 3.1, FSR 4, FSR 4.1.1) на собственных векторах движения и jitter игры |
| Патчи игры | Патч-файлы применяет эмулятор | Те же патчи сообщества компилируются при запуске (`scripts/patches.py`); разрешение рендера, эффекты и FPS — из лаунчера |
Без shadPS4 bbport бы не было: его рендерер и рекомпилятор шейдеров — основа графической части.
### Wine + DXVK для одной игры
- **Процессор.** Процессор PS4 — x86-64, поэтому код игры выполняется прямо на процессоре ПК, без
эмуляции и перевода инструкций.
- **Системные библиотеки.** Как Wine подменяет Windows API, так рантайм bbport (`src/runtime_*.c`)
реализует ровно те функции ОС PS4, которые вызывает Bloodborne: память, потоки, файлы, звук,
геймпад, сохранения.
- **Память.** В новом режиме — как у игры для ПК: данные игры в оперативной памяти, видеопамять
используется так, как её использует игра для ПК. Пока режим экспериментальный.
- **Графика.** Поток команд видеочипа PS4 (PM4) — это записанные вызовы графического API игры:
его пишут 99 функций статически слинкованной libGnm, так что разбор потока и перевод самих
вызовов дают одно и то же. Шейдеры GCN переводятся в SPIR-V. В новом режиме работа командного
процессора (записи в память, DMA, метки) тоже переводится в команды Vulkan; метки пишет сам
видеочип. Дескрипторы ресурсов (текстур, буферов) транслятор читает на процессоре, как DXVK
читает привязки ресурсов: текстуру PS4 нужно перевести из раскладки PS4 в образ Vulkan до
отрисовки, поэтому процессор должен знать её дескриптор заранее — это трансляция, а не
эмуляция. Таблицы дескрипторов (дампы движка констант) читает только транслятор, поэтому их
выполняет процессор. Occlusion-запросы игры (~60 за кадр) — настоящие запросы Vulkan; предикация, `COPY_DATA`,
`COND_EXEC` тоже переводятся (проверка — `BB_PM4_SELFTEST=1`).
Подробно — [docs/EMULATION_REMOVAL_PLAN.ru.md](docs/EMULATION_REMOVAL_PLAN.ru.md), «Шаг 4».
«Порт» здесь — сборка под одну игру, а не переписанный исходный код: исходного кода игры у
проекта нет, и он его не содержит.
## Требования
- Linux x86-64, видеокарта с Vulkan 1.3. Проверено: AMD RX 7800 XT, Mesa 26 (RADV).
Режим *Новая модель памяти и трансляции* на NVIDIA пока не проверен.
FSR 4 / 4.1.1 требуют shader Float16, Int8/Int16, целочисленных dot-произведений,
linear compute derivatives и extended storage image formats; FSR 4.1.1 дополнительно
требует `VK_VALVE_shader_mixed_float_dot_product`. Неподдерживаемый вариант заменяется
на FSR 3.1 до первого кадра и недоступен в игровом меню.
- Свой расшифрованный дамп игры: папка `CUSA03173` (eboot.bin, sce_module, ...), версия 1.09.
Дамп обновления — отдельная папка: скопируйте её поверх базовой игры с заменой файлов. Одна
базовая игра (1.00) падает при запуске (guest offset 0x20348b8); лаунчер и `run.sh` проверяют
исполняемый файл и пишут, чего не хватает (`BB_SKIP_GAME_CHECK=1` отключает проверку).
Другие издания (американское `CUSA00900`, азиатское `CUSA03023`, ...) запускают тот же
исполняемый файл 1.09. В некоторых дампах в него уже вшит патч 60 FPS от Lance McDonald, из-за которого игра
падает при открытии меню жестов: проверка тогда просит чистый eboot.bin (у bbport свои 60 FPS).
- Для сборки: GCC, CMake, Ninja, Python 3, glslang, SDL3, заголовки Vulkan и библиотеки из
`shell.nix`. С [Nix](https://nixos.org) всё берётся из `shell.nix` автоматически.
## Сборка и запуск
```bash
git clone --recursive https://github.com/deadinside28/bloodborne_pc.git bbport && cd bbport
bash build.sh # собирает out/bb-probe и out/gpu/libbbgpu.so
BB_GAME_DIR=/путь/к/CUSA03173 bash run.sh
```
или через лаунчер (выбрать папку игры, настройки, «Запустить»):
```bash
bash launcher/bb-launcher.sh # launcher/install-desktop.sh добавит его в меню приложений
```
По умолчанию папка игры ищется рядом с репозиторием (`../CUSA03173`). Сохранения и кэш шейдеров
— в `user/` (в лаунчере можно выбрать другую папку), настройки — в `bbport.ini`. Геймпад через
SDL3 (если их несколько, нужный выбирается в лаунчере: *Управление → Контроллер*;
`BB_GAMEPAD=<GUID или часть имени>`). Клавиатура тоже работает, и при подключённом геймпаде
(на Steam Deck он есть всегда); обе раскладки меняются в лаунчере (*Управление*). Имя персонажа
вводится с клавиатуры в окне поверх игры. Тачпад: левая половина (Tab, Back/Select) открывает
жесты, правая (Backspace) — личные вещи. PSN нет: экран «Играть по сети / вне сети» пропускается,
игра сразу открывает главное меню вне сети (патч игры; в лаунчере *Эффекты игры → Пропускать
выбор «по сети / вне сети»*, `BB_SKIP_NETWORK_CHOICE=0` — показывать). При нескольких мониторах нужный выбирается в
лаунчере: *Экран → Монитор* (`BB_DISPLAY=<номер или часть имени>`).
**Смена разрешения и пресетов:** для вывода не 1080p (720p на Steam Deck, 1440p, 4K) вся игра
рисуется в разрешении пресета, которое задаёт патч при запуске, — это самый быстрый путь.
Смена вывода или пресета в игровом меню тогда требует «Применить и перезапустить игру».
Настройка «Смена разрешения на лету» (лаунчер, игровое меню, `bbport.ini`
`live_resolution=0|1|auto`; **по умолчанию выключена**) вместо этого оставляет игре 1080p и
масштабирует её цели на ходу: 720p/1080p/1440p/4K и пресеты меняются без перезапуска. Это
дороже: игра считает, что рисует 1080p, и рисует больше (например, мелких источников света
примерно в 8 раз больше), часть целей копируется между размерами — включай только на мощных
настольных видеокартах (`auto` включает её на дискретных GPU с 8+ ГБ, кроме NVIDIA старше
Turing). Вывод 1080p и TAA всегда идут живым путём.
**TAA:** отдельный пункт сглаживания в лаунчере и игровом меню, переключается на ходу.
Работает в разрешении вывода без модели FSR; пресет FSR сохраняется для обратного
переключения. Native AA у FSR выполняет реконструкцию поверх полного разрешения и
может быть тяжелее выключенного сглаживания. TAA тоже добавляет обработку кадра.
**Моды:** лаунчер подключает распакованные моды из отдельных папок (с `dvdroot_ps4/`, с лишней
папкой-обёрткой или сразу с папками игры вроде `chr/`; регистр имён не важен), с переключателями
и порядком загрузки. **Сторонние патчи:** XML-файлы в формате shadPS4 в папке `patches/` каталога
данных, включаются в лаунчере. Подробнее — [моды и патчи](docs/MODS.md).
**DLC:** дампы дополнений клади в `user/addcont/<title id>/<метка>/` (как в shadPS4, например
`user/addcont/CUSA03173/SPEXPANSIONDLC03` для The Old Hunters): каждая такая папка сообщается
игре как установленное дополнение.
**Язык лаунчера:** русский, английский, бразильский португальский или упрощённый китайский (по умолчанию — как в системе).
**Свободная камера и debug menu игры** (v1.09): включи соответствующие галочки в лаунчере
или игровом меню и перезапусти игру. Для камеры используется
[патч Lance McDonald из GoldHEN](https://github.com/GoldHEN/GoldHEN_Patch_Repository/blob/main/patches/xml/Bloodborne-Orbis.xml):
удерживай Cross и нажимай L3 для переключения режимов (клавиатура: Space + Z).
Камере шрифты не нужны; с патчем Enemy Control она несовместима.
Для debug menu сначала установи `DbgFont14h.ccm` и `DbgFont14h.tpf` из
[Debug Menu and XML Patch](https://www.nexusmods.com/bloodborne/mods/253) в
`dvdroot_ps4/font/` внутри папки игры. Если файлов нет или они пустые, запуск остановится
с объяснением. Меню открывается левым touchpad / Tab (с включённым debug menu левая половина
больше не открывает жесты); правый touchpad — Backspace.
Координаты касания передаются с SDL-геймпадов; Back/Select заменяет левый клик на геймпадах
без сенсорной поверхности. Меню настроек порта по-прежнему открывается Insert / L3+R3.
GPU-запросы occlusion пока возвращают искусственные счётчики пикселей
(`PixelPipeStatDump`), а `IT_SET_PREDICATION` не реализован. Свободная камера позволяет
проверять видимость, но не добавляет GPU occlusion culling.
**Ассеты апскейлеров** (в репозитории их нет; FSR 3.1 они не нужны):
```bash
bash tools/fetch_fsr4_assets.sh # FSR 4 v07 (MIT, собраны Q2RTX из исходников AMD)
# FSR 4.1.1 — из своих DLL AMD (например, FSR4_LATEST у OptiScaler), нужен GE-Proton 10 или новее:
bash tools/fsr4cap/build_assets.sh <amd_fidelityfx_upscaler_dx12.dll>
```
**FSR 4.1.1 из своей DLL.** Проще всего — в лаунчере: *Апскейлер → FSR 4.1.1 из своей DLL AMD →
Выбрать DLL…* и указать `amd_fidelityfx_upscaler_dx12.dll` версии 4.1.x: из папки `FSR4_LATEST`
OptiScaler или из игры с FSR 4.1. Этого одного файла достаточно: DLL AMD сама экспортирует
FidelityFX API, загрузчик не нужен (через `amd_fidelityfx_loader_dx12.dll` записывалась бы только
DLL без этих функций, и тогда лаунчер его спросит). DLL проверяется сразу, без долгой записи, и не подходят: официальная FSR 4.0.x
(например, 4.0.3 из Pragmata — AMD включает её только на RDNA4, под Proton на других видеокартах
она не запускается) и сборки сообщества (4.0.2b, 4.1.1b и подобные INT8-сборки OptiScaler:
другая модель и число проходов). Дальше DLL запускается под Proton и записывается в 20
разрешениях (ход виден в строке и во вкладке «Журнал»; на RDNA4 — второй раз, для варианта FP8), проходы переводятся в SPIR-V, и FSR 4.1.1
сразу выбирается. Proton выбирается сам — первый, под которым DLL включает FSR 4.1: GE-Proton 10
или новее, Proton-CachyOS, Proton Experimental (проверено: GE-Proton 11, Proton-CachyOS 11,
Experimental от октября 2026; Proton 11.0 из Steam и GE-Proton 9 — нет). Он запускается в
рантайме Steam, который требует (Steam ставит его при первом запуске любой игры с этим Proton),
или через umu-launcher, если он установлен. На NixOS umu-launcher берётся из `nix-shell` (в первый
раз скачивается, ~1,7 ГБ); AppImage запускает запись на самой системе, через systemd пользователя
(`systemd-run --user`), так как системный `/nix` ей не виден. Запись на Steam Deck пока не проверена; если DLL не включит
там FSR 4.1, собери на ПК и скопируй папку `fsr4_411`. То же из командной строки:
`--build-fsr411 <DLL>` у лаунчера (и у AppImage).
**DLSS (NVIDIA GeForce RTX).** SDK DLSS от NVIDIA не входит в репозиторий: скачай
[github.com/NVIDIA/DLSS](https://github.com/NVIDIA/DLSS) (достаточно `include/` и
`lib/Linux_x86_64`) и собери с `DLSS_SDK_ROOT=/путь/к/DLSS bash build.sh`. Тогда собирается и мост
`out/libbbport_dlss.so` (`gpu/dlss_bridge`, единственный код, использующий SDK), а
`libnvidia-ngx-dlss.so.<версия>` от NVIDIA копируется рядом с `out/bb-probe`; остальное даёт
драйвер. Затем выбери *DLSS* в лаунчере или во внутриигровом меню: пресет задаёт разрешение
рендера, Native AA — это DLAA. DLSS получает те же входы, что и FSR 3.1 (цвет сцены, глубину,
векторы движения, jitter). Без библиотек, на других видеокартах или с `BB_DLSS=0` он недоступен,
а выбранный DLSS заменяется на FSR 3.1.
AppImage, собранный с `DLSS_SDK_ROOT`, содержит мост и библиотеку NVIDIA (лицензия RTX SDK от NVIDIA
это разрешает в составе приложения; её текст — `NVIDIA-DLSS-LICENSE.txt` в пакете). Более новая
`libnvidia-ngx-dlss.so.<версия>` (из `lib/Linux_x86_64/rel` в SDK) выбирается в лаунчере:
*Апскейлер → DLSS: библиотека NVIDIA → Выбрать файл…*, как DLL для FSR 4.1.1; она копируется в
`<user>/dlss/`, и игра берёт её первой. `nvngx_dlss.dll` из игр — библиотека для Windows: драйвер
NVIDIA на Linux её не загружает.
Скрипт из исходников берёт инструменты из системы (MinGW GCC, CMake, Ninja, Python 3,
SPIRV-Tools, Git) или из Nix; в AppImage они уже собраны.
**FP8 на RDNA4.** Когда vkd3d-proton даёт кооперативные матрицы FP8 (RDNA4), DLL запускает
другой вариант своих проходов: другие шейдеры и веса, другие размеры диспатчей. Сборка
записывает оба: INT8 в `fsr4_411/`, FP8 в `fsr4_411/fp8/`, а игра сама выбирает FP8, если у
видеокарты есть матрицы FP8 (`VK_EXT_shader_float8`); в журнале — `Upscaler: FSR 4.1.1 replay,
FP8 …`. `BB_FSR411_VARIANT=int8` — принудительно INT8. Проверено на RX 7800 XT через эмуляцию FP8
на FP16 в vkd3d-proton (`BB_FSR4CAP_FP8=1` записывает его и там, `fp8emu/`): побитово как DLL;
**на самой RDNA4 ещё не запускалось.**
**AppImage** (Steam Deck): `bash build.sh && bash packaging/appimage.sh` →
`dist/Bloodborne-bbport-x86_64.AppImage`; данные в `~/.local/share/bbport`, `--play` запускает
игру без окна лаунчера (игровой режим). Модели FSR 4.1.1 в пакет не входят: собери их кнопкой
лаунчера (см. выше), они лягут в `~/.local/share/bbport/fsr4_411` (`BB_PACKAGE_FSR411=1` кладёт
локальную `fsr4_411` в AppImage для своих устройств). На Steam Deck выбирай вывод 1280×720 (игра 16:9, на
экране 1280×800 будут узкие полосы).
**Добавление AppImage в Steam** («Добавить стороннюю игру»): ничего настраивать не нужно,
инструмент совместимости не важен. (Steam подгружает свой оверлей во все сторонние игры; AppImage
убирает его до запуска своих программ, поэтому оверлея Steam в игре нет.) Если Steam запускает игры без FUSE (NixOS: FHS-песочница Steam; AppImage тогда завершается с
*Cannot mount AppImage*), в параметрах запуска укажи
```
TMPDIR=$HOME/.cache APPIMAGE_EXTRACT_AND_RUN=1 NO_CLEANUP=1 %command%
```
Тогда AppImage при первом запуске распакуется (~2 ГБ, `~/.cache/appimage_extracted_*`, ~10 с),
а дальше будет запускать эту копию; новая версия AppImage распакуется заново, старую копию можно
удалить. Без `TMPDIR` распаковка шла бы в `/tmp` Steam, а он там в оперативной памяти.
` --play` после `%command%` — запуск без лаунчера.
**NVIDIA в AppImage:** пакет автоматически ищет установленный на машине 64-битный NVIDIA
Vulkan-драйвер и подключает его вместе с комплектными AMD/Intel-драйверами. Используются
библиотеки NVIDIA с этой машины, чтобы их версия совпадала с модулем ядра. На обычных Linux
дистрибутивах они доступны в `/usr/lib*`; на NixOS внутренний `/nix/store` AppImage может скрывать
драйвер хоста. В этом случае библиотеки NVIDIA нужно скопировать в доступную папку и указать
`BB_NVIDIA_LIB_DIR=/путь/к/библиотекам` (манифест NVIDIA тоже должен быть доступен).
С драйвером NVIDIA хоста AppImage передаёт и файлы EGL-вендоров хоста, NVIDIA первым
(`__EGL_VENDOR_LIBRARY_FILENAMES`; без них ICD NVIDIA на некоторых системах не отдавал
`vkCreateInstance`, #107; `BB_NVIDIA_EGL=0` — выключить). Явные `VK_DRIVER_FILES`/`VK_ICD_FILENAMES` сохраняются. Проверка драйвера внутри пакета:
```bash
./Bloodborne-bbport-x86_64.AppImage --vulkan-info 2>&1 | tee bbport-vulkan.log
```
Пользователь подтвердил запуск с FSR 3 на GTX 1060 6GB (Fedora 44, NVIDIA 580.178.04);
выбор FSR 4 приводил к чёрному окну. На этой конфигурации используй FSR 3.
MangoHud включён в AppImage; включи его галочку в лаунчере. Если MangoHud установлен ещё и в
системе или включён оверлей производительности Steam (игровой режим Steam Deck), рисуется только
один оверлей (два давали двоящийся текст со сдвигом).
При запуске из исходников MangoHud нужно установить отдельно. Диагностический запуск
с `VK_LOADER_LAYERS_DISABLE=~implicit~` отключает и MangoHud.
Полезные переменные: `BB_FRAME_STATS=1` (статистика кадров, в том числе строка `Memory:` —
VRAM, GTT, RSS, текстуры и блоки памяти игры в VRAM), `BB_PC_MODEL=1` (новая модель памяти и трансляции;
по умолчанию 0 — старая модель, как в 0.3), `BB_ANISO=N` (анизотропная фильтрация текстур сцены; по умолчанию
16, 0 — как в игре),
`BB_GC_IDLE_SECONDS=N` / `BB_VRAM_IDLE_SECONDS=N` (сколько неиспользуемые текстуры / буферы
остаются в VRAM; 20 / 60), `BB_BREADCRUMBS=0` (без меток GPU; с ними зависание видеокарты
называет отрисовку или dispatch, на которой она встала), `BB_GPU_PROFILE=1` (время GPU по
проходам), `BB_FSR4_PROFILE=1` (время каждого прохода FSR 4), `BB_UPSCALER=taa|fsr3|fsr4|fsr411|off|none`,
`BB_FRAMES_AHEAD=N` (на сколько кадров поток команд может опережать GPU; по умолчанию 1, 0 — без
ограничения), `BB_PRESENT_THREAD=0` (показ в потоке vblank, как раньше), `BB_LIVE_RES=1` (смена
разрешения без перезапуска вместо патча при запуске для вывода не 1080p),
`BB_PAD_RECORD=файл` / `BB_PAD_REPLAY=файл` (запись маршрута по F9 и его повтор в тестах),
`BB_GC_BUDGET_MB=N` (бюджет кэша текстур, как на встроенных GPU), `BB_VRAM_LIMIT_MB=N` (бюджет
VRAM считается не больше N МиБ: проверка поведения карт с малой VRAM), `BB_ASYNC_PIPELINES=0`
(новые конвейеры компилируются сразу, игра ждёт; по умолчанию — в фоне), `BB_PRESENT_DUMP_TRIGGER=файл`
с `BB_PRESENT_DUMP_COUNT=N` (снимок N показанных кадров подряд),
`BB_FSR411_VARIANT=int8|fp8|fp8emu` (вариант FSR 4.1.1; по умолчанию FP8, где у видеокарты есть
матрицы FP8), `BB_READBACKS=0|1|2` (чтение игрой памяти, записанной GPU: по умолчанию 1, 2 —
точно и медленно, 0 — выключено).
Подробнее — в [docs/](docs); последние изменения — [docs/CHANGES_0.4.md](docs/CHANGES_0.4.md),
[docs/CHANGES_2026-10-06.md](docs/CHANGES_2026-10-06.md).
**Игра по сети (shadNet).** Отдельный модуль `out/gpu/libbbnet.so` (`gpu/bbnet`: сетевые и PSN-библиотеки
shadPS4 из shadp2p, форка shadPS4, с клиентом shadNet); собирается, когда есть protobuf, nlohmann_json,
OpenSSL, zlib и miniupnpc (`shell.nix`), входит в AppImage. Включается на странице лаунчера *Онлайн*
(`BB_ONLINE=1`): сообщения, пятна крови и призраки — с The Hunter's Dream, колокола и призывы — через
сервер shadNet (по умолчанию `srv.shadps4.net:31313`) с учётной записью shadNet (Online ID и пароль).
Офлайн модуль не загружается; сборка без него играет только офлайн.
## Устройство репозитория
| Путь | Содержимое |
|---|---|
| `src/` | Загрузчик (`probe.c`) и HLE-рантайм |
| `scripts/` | Подготовка образа игры, линковка модулей, компилятор патчей |
| `gpu/` | Библиотека рендерера: видеоядро shadPS4 с правками порта (`gpu/VENDOR.txt`), прослойки, меню ImGui, рантайм FSR 4.1.1 (`gpu/shadps4/video_core/renderer_vulkan/fsr411`) |
| `launcher/`, `packaging/` | Лаунчер GTK4; пакет Nix и AppImage |
| `patches/` | Патчи сообщества для Bloodborne |
| `tools/` | Утилиты разработчика: скриптовые запуски, A/B-переключатели, бенчмарк FSR, переписывание шейдеров FSR 4, `fsr4cap` (запись и извлечение FSR 4.1.1) |
| `tests/` | Тесты загрузчика, рантайма, патчей и рендерера |
| `docs/` | Заметки и замеры ([апскейлер](docs/upscaler.md), [многопоточный GPU](docs/parallel_gpu.md), [векторы движения](docs/motion_vectors.md), [план](docs/ROADMAP.md), [журнал разработки](docs/DEVELOPMENT_LOG.ru.md)) |
Тесты: `bash build.sh --test`, `python3 -m unittest discover -s tests` и
`ninja -C out/gpu motion-history-test ui-composition-test scene-resolution-test motion-shader-test settings-test`.
## Что будет дальше
- Новая модель памяти и трансляции по умолчанию и на NVIDIA, когда она станет там не медленнее;
после этого старая модель удаляется.
- Текстуры — объекты, которые создаются при загрузке (меньше угадывания по адресам, загрузка
вне кадра).
- Шейдеры переводятся заранее, при установке, а не во время игры.
- Больше параллельности CPU в обработке GPU-команд (дальнейшее разделение стадии записи draw)
с масштабированием на все потоки — важнее всего для Steam Deck.
- Async compute для апскейлера (в 4K кадр упирается в GPU).
- XeSS (суперразрешение) и генерация кадров XeFG через помощник под Wine с общей памятью Vulkan
(прототип моста памяти — `tools/bridge_helper`); входы апскейлера
в виде, совместимом с подходом OptiScaler.
- Генерация кадров (сначала FSR 3.1 FG), маски реактивности и прозрачности для частиц и тумана.
- Исправление гонок в шейдерах FSR 4.1.1 от AMD при ширине вывода, не кратной 64 (например,
1600×900) — так же, как уже исправлена гонка у левого края в FSR 4 v07 при 1080p.
- Проверка AppImage на Steam Deck; вывод HDR.
## Благодарности и лицензии
bbport распространяется по **GNU GPL v2 или новее** ([LICENSE](LICENSE)) — в нём код shadPS4
(GPL-2.0-or-later). Сторонние компоненты сохраняют свои лицензии:
видеоядро и рекомпилятор шейдеров [shadPS4](https://github.com/shadps4-emu/shadPS4) (GPL-2.0+),
[sirit](https://github.com/shadps4-emu/sirit), [half](https://half.sourceforge.net/),
[FSR-Vulkan](https://github.com/FireBurn/FSR-Vulkan) от FireBurn (MIT; FSR 3.1 на Vulkan и
провайдер FSR 4 v07), AMD FidelityFX SDK (MIT), [LibAtrac9](https://github.com/Thealexbarney/LibAtrac9)
(MIT), [Dear ImGui](https://github.com/ocornut/imgui) (MIT), шрифты DejaVu,
[dxil-spirv](https://github.com/HansKristian-Work/dxil-spirv) (MIT, для сборки ассетов FSR 4.1.1).
Патчи игры — Kyo, Lance McDonald, auser1337, illusion, emoose и другие участники сообщества
(`patches/Bloodborne.xml`). Мост DLSS (`gpu/dlss_bridge`, MIT) и загрузчик взяты из
[Windows-порта Supermedo](https://github.com/Supermedo/bloodborne_pc), адаптированного из
[IFreemz/shadPS4-Bloodborne-DLSS-FSR](https://github.com/IFreemz/shadPS4-Bloodborne-DLSS-FSR).
DLL и данные модели FSR 4 от AMD, а также SDK и библиотеки DLSS от NVIDIA здесь не
распространяются.
+16
View File
@@ -0,0 +1,16 @@
# Button prompts and gestures menu
By **Dommo**, from [Nexus Mods: Bloodborne, mod 30](https://www.nexusmods.com/bloodborne/mods/30)
(uploaded by goomab): *DS3 Style - Modern Xbox prompts*, *DS3 Style - Nintendo Switch prompts* and
*Stripped down gesture menu*. The mod page allows using its assets and uploading them elsewhere
with credit to the author, but not in anything sold.
Only what the mods drew is here, never the game's own files:
- `xbox/`, `switch/`: the 20 button icons (`KG_*.dds`, BC7 exactly as in the mods) and the strip
of big prompts from the shared UI atlas (`atlas.dds`, scaled down to the game's atlas size).
- `gesturetop.json`: the edits that turn the game's `menu/gesturetop.gfx` into the mod's.
`scripts/menu_prompts.py` puts them into the player's own `menu/common.tpf.dcx` and
`menu/gesturetop.gfx` at each start; `tools/extract_prompt_assets.py` rebuilds this folder from
the mods' ZIPs. Keyboard prompts are drawn by `scripts/menu_prompts.py` itself.
+91
View File
@@ -0,0 +1,91 @@
{
"original_sha256": "5d7bae1451d97296e88a6add62b1fd9d1425901acc5c907e90f52354bc0b624e",
"result_sha256": "c77f7398c02613bd3d1a9613ce773fa8b6b74143053c6092b52a3c8d959dd868",
"edits": [
[
4,
1,
"2f"
],
[
2548,
1,
"18"
],
[
2556,
1,
"8e"
],
[
2563,
0,
"c9"
],
[
2564,
0,
"0034bca0480238"
],
[
3189,
1,
"92"
],
[
3229,
1,
"92"
],
[
3242,
5,
"207b2673a5e0"
],
[
3268,
1,
"98"
],
[
3281,
5,
"2077b30a7b80"
],
[
3519,
1,
"41"
],
[
3529,
1,
"33"
],
[
3539,
5,
"c900004000207d11920000"
],
[
5795,
1,
"ab"
],
[
5817,
1,
"1a"
],
[
5826,
5,
"c900004000207a015deba0"
],
[
6076,
12,
""
]
]
}
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+20
View File
@@ -0,0 +1,20 @@
# 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
@@ -0,0 +1,40 @@
# 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
@@ -0,0 +1,81 @@
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
@@ -0,0 +1,23 @@
{
"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
@@ -0,0 +1,674 @@
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
@@ -0,0 +1,53 @@
# 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
@@ -0,0 +1,51 @@
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
@@ -0,0 +1,248 @@
#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
@@ -0,0 +1,73 @@
// 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
@@ -0,0 +1,976 @@
// 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
@@ -0,0 +1,26 @@
#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
@@ -0,0 +1,141 @@
#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
@@ -0,0 +1,75 @@
// 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
@@ -0,0 +1,195 @@
// 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
@@ -0,0 +1,437 @@
#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
@@ -0,0 +1,101 @@
// 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
@@ -0,0 +1,152 @@
// 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
@@ -0,0 +1,735 @@
#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
@@ -0,0 +1,163 @@
// 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
@@ -0,0 +1,85 @@
#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
@@ -0,0 +1,73 @@
// 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
@@ -0,0 +1,262 @@
#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
@@ -0,0 +1,80 @@
// 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
@@ -0,0 +1,103 @@
// 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
@@ -0,0 +1,197 @@
#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
@@ -0,0 +1,126 @@
// 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
@@ -0,0 +1,147 @@
"""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
@@ -0,0 +1,395 @@
"""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
@@ -0,0 +1,778 @@
#!/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
@@ -0,0 +1,209 @@
// 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
@@ -0,0 +1,21 @@
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
@@ -0,0 +1,138 @@
//-----------------------------------------------------------------------------
// 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
@@ -0,0 +1,618 @@
// 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
@@ -0,0 +1,45 @@
// 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
@@ -0,0 +1,940 @@
// 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
@@ -0,0 +1,53 @@
// 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
@@ -0,0 +1,627 @@
// [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
Loaded 100 of 524 files, more files were not shown because too many files have changed in this diff. Show more