Compare commits

...
149 Commits
Author SHA1 Message Date
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
244 changed files with 100487 additions and 2213 deletions

No files matched your search

+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.
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).
+9
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,10 @@ patches.json
/docs/ISSUE_ACTION_PLAN_*.md
# AMD FidelityFX DLLs (FSR 4.1.1 is built from the user's own): never in the repository
*.dll
# Game files and dumps: never in the repository
eboot.bin
param.sfo
*.pkg
*.dcx
*.7z
/CUSA*/
+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
+436
View File
@@ -0,0 +1,436 @@
THIS PROJECT IS NOT RELATED TO SHADPS4. ALL QUESTIONS RELATED TO THIS PROJECT SHOULD BE SENT TO THE DISCORD SERVER https://discord.gg/KYZRKk9CB, NOT TO THE SHADPS4 SERVER.
# bbport — a native Linux port of Bloodborne
> This is the README of the original project (deadinside28/bloodborne_pc), kept for its
> technical details. The fork's own README is [README.md](README.md).
**English** · [Русский](BBPORT.ru.md)
bbport is the counterpart of Wine + DXVK for a single game: *Bloodborne* for PlayStation 4
(CUSA03173, game version 1.09) on an x86-64 Linux PC. The game's original executable runs
directly on the PC:
- **as in Wine**, the game's x86-64 code runs on the CPU directly, and a runtime written for
this one game replaces the PS4 system libraries;
- **as in DXVK**, the game's graphics are translated to Vulkan — by a renderer derived from
[shadPS4](https://github.com/shadps4-emu/shadPS4) and heavily extended for this game, including
temporal upscaling with AMD FSR 3.1, FSR 4 and FSR 4.1.1;
- memory and the GPU's work are moving to the PC model: the launcher has an experimental
*New memory and translation model* mode; without it the game runs on the old
memory model, as in 0.3.
One step remains to the full Wine + DXVK model: make the new mode the default and remove the old
memory model (see below).
> **No game files are included.** You need your own dump of Bloodborne with update 1.09: any retail
> release (CUSA00900 in the US, CUSA03173 for the European Game of the Year edition, ...), as they
> share the same executable.
> This project is not affiliated with Sony Interactive Entertainment, FromSoftware or AMD.
**Status: experimental, playable.** The game boots, loads saves and plays (the Hunter's Dream
and several areas of Yharnam were played with it) with sound, gamepad and saving.
A full play-through has not been verified, and only one machine (Linux, AMD Radeon RX 7800 XT,
Mesa/RADV) has been tested thoroughly.
## Highlights
- **Native execution.** The eboot is converted offline into a flat memory image; PS4 libc and
libSceFios2 are linked into it as native code. No CPU emulation and no per-instruction
translation: the game code runs at full speed.
- **Two modes** (launcher → *Mode*):
- **Switch off (the default) — as in 0.3.** The old memory model: VRAM copies of the game's
memory, writes tracked through page protection. With every fix made since 0.3 (motion
vectors and the upscaler, flicker with DoF on, a damaged shader cache).
- **New memory and translation model — experimental.** The game's memory lives
in system RAM and the GPU reads it where it is, as a PC game's buffers; data it reads often
is kept in VRAM and given back when unused (textures after 20 s, buffers after 60 s). The
layer's memory module tracks CPU writes to mirrored blocks to keep those copies coherent;
it does not copy the whole GPU-visible memory. The PS4 command processor's work
is translated into Vulkan commands rather than emulated on the CPU: memory writes
(`WRITE_DATA`, DMA) are done by the GPU in command-stream order, and fences are written by the
GPU itself at the end of the pipeline, at the same point of the stream as the PS4's command
processor writes them. **It may crash**; hardware testing has been performed on an
RX 7800 XT, and performance depends on the scene and settings. On AMD the game's memory is kept
in a sparse buffer (the arena). **On NVIDIA, Intel and others** it goes through the layer's
memory module: the game's memory is bound in place and VRAM copies are separate buffers, with
no sparse rebinding. This avoids the sparse-binding path associated with reported freezes
on a GTX 1660 Ti. On NVIDIA the module runs on a tester's GTX 1660 Ti, its speed is still
being checked; not tested on Intel. Without the launcher: `BB_PC_MODEL=1`; to choose the memory
path: `BB_GUEST_MEMORY=host|dmabuf`; the memory module: `BB_LAYER_MEMORY=1/0`.
[0.5-pre4 changes](docs/CHANGES_0.5-pre4.md): no stutter of the memory module (NVIDIA) while areas
stream in, shader compilation in the background, VRAM on 4–6 GB cards, damaged game extractions
reported, the settings menu as in 0.3. [0.5-pre3](docs/CHANGES_0.5-pre3.md): the memory module on
NVIDIA — VRAM copies, one VRAM budget with the texture cache, watched blocks. [0.5-pre2](docs/CHANGES_0.5-pre2.md): the memory
module, native GPU copy shader, portable GPU completion labels, and shader-cache migration.
Unused textures are freed in both modes, so VRAM no longer grows with every area visited.
- **Unlocked frame rate.** Community patches (`patches/Bloodborne.xml`) make the simulation
use the real frame time; ~90 FPS at 4K with FSR 4 Balanced on an RX 7800 XT, ~150 FPS at
1440p with FSR 4 Quality. Also 30/60/90 FPS modes.
- **Temporal upscaling built for this game.** Bloodborne has no velocity buffer, so bbport
computes motion vectors itself: camera motion from depth and the scene matrices, and object
motion (characters, cloth, weapons) from the vertex positions of the previous frame. The
scene is jittered sub-pixel (Halton) and rendered at a reduced resolution; the upscaler fills
the output (720p for the Steam Deck, 1080p, 1440p or 2160p) and the UI is drawn natively at the output resolution.
- **FSR 3.1** (FireBurn/FSR-Vulkan).
- **FSR 4 (INT8, model v07)** on GPUs exposing the required Vulkan shader features —
RDNA2/3 included (see Requirements).
- **FSR 4.1.1**: AMD's 4.1.1 DLL is recorded once under vkd3d-proton and its passes are
replayed natively on Vulkan; the output is **bit-exact** with the DLL. Two variants, as in
the DLL: INT8 on any GPU with the required shader features, and FP8 matrices on RDNA4 (RX
9000; picked automatically). The assets are built on your machine from one DLL of your own
(4.1.x): the launcher's *FSR 4.1.1 from your own AMD DLL → Choose DLL…* button (2–5 minutes,
twice that on RDNA4; needs a recent Proton), or `tools/fsr4cap`.
- Faster than AMD's own shaders on RDNA3: the final passes of FSR 4 and 4.1.1 were rewritten
to store through workgroup memory (3.5× and 2.3× faster, bit-exact); FSR 4 costs ~4 ms at
4K on an RX 7800 XT instead of ~6 ms.
- **Multi-threaded GPU command processing.** The PS4 command stream is decoded on one thread
and draws are bound and recorded on another (two-stage pipeline), with a Vulkan recording
thread and helper threads for memory copies. Early on the single GPU thread capped the game
at ~26 FPS; now it runs at 90–150 FPS depending on resolution and scene.
- **Settings inside the game's own menu:** *System → Display / Game effects / Game patches*
(after *Screen/Sound*), built and drawn by the game itself — output resolution, upscaler,
preset, sharpness, FPS counter, model detail and the effect patches, in the game's language.
- **Overlay menu** (Insert or L3+R3), styled like the game's dialogs: everything above plus the
advanced settings, with gamepad, keyboard and mouse.
- **GTK4 launcher** and an **AppImage** for the Steam Deck.
## How it differs from shadPS4
| | shadPS4 | bbport |
|---|---|---|
| Scope | General PS4 emulator, many games | One game: Bloodborne v1.09 |
| Loading | Its own ELF loader and kernel emulation at run time | The eboot is converted offline (`scripts/`) into an image with PS4 libc/Fios2 linked in; a C loader maps it and jumps into the game (loader and runtime: ~5k lines) |
| Memory | The GPU's view of PS4 memory is kept in VRAM copies, synchronized through page-protection write tracking | By default the same model (as in 0.3); in the *New memory and translation model* mode: the game's memory in system RAM, used by the GPU in place, frequently read data in VRAM, freed when unused |
| Command processor | Emulated: memory writes, DMA and fences are done by the CPU while decoding | By default the same; in the new mode translated into Vulkan commands that the GPU runs in stream order, fences written after the work has really finished |
| System libraries | Broad HLE of the PS4 OS | A small runtime (`src/runtime_*.c`) that implements exactly what Bloodborne calls: memory, threads, sync, files, audio (incl. ATRAC9), pad, saves, AppContent |
| GPU | shadPS4 video core and shader recompiler | The same core (vendored, GPL) with ~200 marked changes (`bbport:`) plus new modules: two-stage draw pipeline, render-state and texture-set memoization, render-scale proxies, motion vectors, FSR 3.1/4/4.1.1, frame capture and GPU profiler |
| GPU thread | One thread processes the whole command stream (the bottleneck in Bloodborne) | Decode and draw recording run on separate threads; the work scales with the hardware threads (Steam Deck included) |
| Upscaling | — | Temporal (FSR 3.1, FSR 4, FSR 4.1.1) with the game's own motion vectors and jitter |
| Game patches | Patch files applied by the emulator | The same community patches, compiled at start (`scripts/patches.py`); render resolution, effects and FPS from the launcher |
Without shadPS4 there would be no bbport: its renderer and shader recompiler are the base of
the graphics side.
### Wine + DXVK for one game
- **CPU.** The PS4 CPU is x86-64, so the game's code runs directly on the PC's CPU, with no
emulation and no instruction translation.
- **System libraries.** As Wine replaces the Windows API, the bbport runtime (`src/runtime_*.c`)
implements exactly the PS4 OS functions Bloodborne calls: memory, threads, files, audio, pad,
saves.
- **Memory.** In the new mode, as in a PC game: the game's data in system RAM, VRAM used the way
a PC game uses it. The mode is still experimental.
- **Graphics.** The PS4 GPU's command stream (PM4) is a recording of the game's graphics API
calls: it is written by 99 functions of the statically linked libGnm, so decoding the stream
and translating the calls themselves come to the same thing. GCN shaders are translated to
SPIR-V. In the new mode the command processor's work (memory writes, DMA, fences) is
translated into Vulkan commands as well; fences are written by the GPU itself. Resource
descriptors (textures, buffers) are read by the translator on the CPU, as DXVK reads resource
bindings: a PS4 texture has to be converted from the PS4 tiling into a Vulkan image before the
draw runs, so the CPU must know its descriptor beforehand — translation, not emulation. The
descriptor tables (the constant engine's dumps) are read only by the translator, so the CPU
performs them. The game's occlusion queries (~60 a frame) are
Vulkan occlusion queries; predication, `COPY_DATA` and `COND_EXEC` are translated too (check:
`BB_PM4_SELFTEST=1`). Details:
[docs/EMULATION_REMOVAL_PLAN.ru.md](docs/EMULATION_REMOVAL_PLAN.ru.md), "Шаг 4" (Russian).
"Port" here means a build for this one game, not a rewrite of its source code, which the project
neither has nor includes.
## Requirements
- Linux x86-64, a Vulkan 1.3 GPU. Tested: AMD RX 7800 XT with Mesa 26 (RADV).
The *New memory and translation model* mode is not tested on NVIDIA yet.
FSR 4 / 4.1.1 require shader Float16, Int8/Int16, integer dot products, linear compute
derivatives and extended storage image formats; FSR 4.1.1 additionally requires
`VK_VALVE_shader_mixed_float_dot_product`. Unsupported choices fall back to FSR 3.1
before the first frame and are disabled in the in-game menu.
- Your decrypted game dump: the `CUSA03173` folder (eboot.bin, sce_module, ...), version 1.09.
The US release (`CUSA00900`) runs too: its 1.09 executable is the same as this European
Game of the Year edition's.
A dumped update is a separate folder: copy it over the base game, replacing files. The base
game alone (1.00) crashes at start (guest offset 0x20348b8); the launcher and `run.sh` check
the executable and say what is missing (`BB_SKIP_GAME_CHECK=1` skips the check). The other
retail releases (the US `CUSA00900`, the Asian `CUSA03023`, ...) run the same 1.09 executable. Some dumps ship it
with Lance McDonald's 60 fps patch baked in, which crashes when the gestures menu opens: the
check asks for a clean eboot.bin then (bbport has its own 60 fps).
- To build: GCC, CMake, Ninja, Python 3, glslang, SDL3, Vulkan headers and the libraries in
`shell.nix`. With [Nix](https://nixos.org) everything comes from `shell.nix` automatically.
## Build and run
```bash
git clone --recursive https://github.com/deadinside28/bloodborne_pc.git bbport && cd bbport
bash build.sh # builds out/bb-probe and out/gpu/libbbgpu.so
BB_GAME_DIR=/path/to/CUSA03173 bash run.sh
```
or the launcher (pick the game folder, settings, *Start*):
```bash
bash launcher/bb-launcher.sh # launcher/install-desktop.sh adds it to the app menu
```
By default the game folder is expected next to the repository (`../CUSA03173`). Saves and the
shader cache go to `user/` (the launcher lets you choose another folder); settings to
`bbport.ini`. A gamepad is used through SDL3 (the launcher's *Controls → Controller* picks one
when several are connected; `BB_GAMEPAD=<GUID or part of the name>`). The keyboard works too,
also next to a connected gamepad (the Steam Deck always has one); both are remapped in the
launcher (*Controls*). The character name is typed on the keyboard in a box over the game.
The touchpad: its left half (Tab, Back/Select) opens the gestures, the right half (Backspace)
the key items. There is no PSN: the *Play online / offline* screen is skipped and the game opens
its main menu offline (a game patch; the launcher's *Game effects → Skip the “play online / offline” choice*,
`BB_SKIP_NETWORK_CHOICE=0` shows it). With several monitors, *Screen → Monitor* in the launcher
(`BB_DISPLAY=<number or part of the name>`) picks the one for the game.
Add-ons: `BB_DLC_DIR=/path/to/dlc` (the launcher's *DLC folder*) holds the dumped add-ons of
the game, one folder each named by its entitlement label (`SPEXPANSIONDLC03/sce_sys/...`).
Their data ships with the 1.09 update, so a folder with only `sce_sys` is enough. `run.sh` links
it as `user/addcont/<title id>`, where the runtime finds add-ons (see **DLC** below); a real
folder already there is kept. The GOTY edition (CUSA03173) needs none.
**Resolution and preset changes:** for outputs other than 1080p (720p on the Steam Deck,
1440p, 4K) the whole game renders at the preset's resolution, set by a patch at start — the
fastest path. Changing the output or the preset in the in-game menu then needs *Apply and
restart the game*. The *Live resolution changes* setting (launcher, in-game menu,
`bbport.ini` `live_resolution=0|1|auto`; **off by default**) instead keeps the game at
1080p internally and scales its render targets at run time, so 720p/1080p/1440p/4K and the
presets switch without a restart. It costs more: the game then believes it renders 1080p
and draws more (e.g. ~8× more small lights), and some targets are copied between sizes —
use it on strong desktop GPUs only (`auto` turns it on for discrete GPUs with 8+ GB that are
not pre-Turing NVIDIA). 1080p output and TAA always use the live path.
**TAA:** a separate native-resolution temporal AA mode in the launcher and overlay,
switchable live without an FSR model. The saved FSR preset is restored when returning
to FSR. FSR Native AA adds reconstruction on top of full-resolution rendering and can
be slower than disabling AA. TAA also adds work compared with no temporal AA.
The RCAS switch and the 0–2 sharpness control also work with TAA. Sharpening runs after
temporal accumulation and leaves its history and HUD unchanged.
**Mods:** the launcher accepts separate loose-file mod folders (with `dvdroot_ps4/`, an extra
wrapper folder, or the game folders such as `chr/` directly; file name case does not matter),
with enable switches and load order. A sibling `CUSA03173-mods/` overlay also works.
The original game is preserved; later mods override conflicting files.
**Third-party patches:** shadPS4-format XML patch files in the data directory's `patches/`,
switched on and off in the launcher. See [mods and patches](docs/MODS.md).
**DLC:** put your add-on dumps in `user/addcont/<title id>/<entitlement label>/` (shadPS4's
layout, e.g. `user/addcont/CUSA03173/SPEXPANSIONDLC03` for The Old Hunters): each folder there is
reported to the game as an installed add-on.
**Launcher language:** Russian, English, Brazilian Portuguese, Spanish, Italian, French, German or Simplified Chinese (follows the system language by default).
**Game language:** the PS4 system language, *System language* in the launcher; by default it
follows the desktop's locale (Japanese, English, French, Spanish, German, Italian or Russian,
English for the rest). `run.sh` takes it as `BB_LANGUAGE` (PS4 code: 0 Japanese, 1 English,
2 French, 3 Spanish, 4 German, 5 Italian, 8 Russian; without it, the desktop's locale as above).
**Game region:** *Game region* in the launcher, `BB_REGION=eu|na` for `run.sh`: the regional text
of a language, such as Spanish from Spain (`eu`) or Latin American Spanish (`na`). It sets the
game's `USER_DEFINED_PARAM_1` (13 Europe, 2 the Americas) for the launch; by default the dump's
`param.sfo` decides.
**Free camera and game debug menu** (v1.09): enable the corresponding switches in the
launcher or in-game menu and restart. Free camera uses Lance McDonald's
[GoldHEN patch](https://github.com/GoldHEN/GoldHEN_Patch_Repository/blob/main/patches/xml/Bloodborne-Orbis.xml):
hold Cross and press L3 to cycle modes (keyboard: hold Space and press Z). It needs no fonts
and conflicts with *Enemy Control*.
For the game debug menu, install `DbgFont14h.ccm` and `DbgFont14h.tpf` from
[Debug Menu and XML Patch](https://www.nexusmods.com/bloodborne/mods/253) into the game's
`dvdroot_ps4/font/` first. Startup rejects missing or empty font files instead of launching
the unsafe patch. Open it with the left touchpad / Tab (with the debug menu on, the left half
no longer opens the gestures); Backspace is the right touchpad.
Touch coordinates are forwarded from SDL gamepads; Back/Select emulates a left click on
pads without a touch surface. The port's settings menu remains Insert / L3+R3.
GPU occlusion queries still use synthetic pixel counters (`PixelPipeStatDump`), and
`IT_SET_PREDICATION` is unimplemented. Free camera allows visual investigation; it does
not implement GPU occlusion culling.
**Upscaler assets** (not included; FSR 3.1 needs none):
```bash
bash tools/fetch_fsr4_assets.sh # FSR 4 v07 (MIT, built from AMD's source by Q2RTX)
# FSR 4.1.1, from your own AMD DLLs (e.g. OptiScaler's FSR4_LATEST), needs GE-Proton 10 or newer:
bash tools/fsr4cap/build_assets.sh <amd_fidelityfx_upscaler_dx12.dll>
```
**FSR 4.1.1 from your own DLL.** The easiest way is the launcher: *Upscaler → FSR 4.1.1 from your
own AMD DLL → Choose DLL…*, then pick `amd_fidelityfx_upscaler_dx12.dll` version 4.1.x: from
OptiScaler's `FSR4_LATEST` folder or from a game with FSR 4.1. That one file is enough: AMD's DLL
exports the FidelityFX API itself, so no loader is needed (only a DLL without these exports would
be recorded through `amd_fidelityfx_loader_dx12.dll`, and the launcher would ask for it). The DLL is checked at once,
without a long capture; these do not fit: AMD's official FSR 4.0.x (e.g. Pragmata's 4.0.3 —
AMD enables it on RDNA4 only, and under Proton it does not start on other GPUs) and community
builds (4.0.2b, 4.1.1b and similar OptiScaler INT8 builds: another model and pass count). Then
the DLL runs under Proton and is recorded at 20 sizes (progress in the row and in the *Log* tab;
on RDNA4 a second time, for the FP8 variant),
the passes are translated to SPIR-V, and FSR 4.1.1 is selected. The Proton build is picked
automatically — the first under which the DLL enables FSR 4.1: GE-Proton 10 or newer,
Proton-CachyOS, Proton Experimental (tested: GE-Proton 11, Proton-CachyOS 11, Experimental of
October 2026; Steam's Proton 11.0 and GE-Proton 9 do not). It runs in the Steam runtime it
requires (Steam installs it the first time any game runs with that Proton), or through
umu-launcher when installed. On NixOS umu-launcher comes from `nix-shell` (downloaded the first
time, ~1.7 GB); the AppImage runs the recording on the system itself, through the user's systemd
(`systemd-run --user`), since the system's `/nix` is out of its sight. Recording on the Steam Deck is not verified yet; if the DLL does not
enable FSR 4.1 there, build on a PC and copy the `fsr4_411` folder. The same from the command
line: the launcher's (and the AppImage's) `--build-fsr411 <DLL>`.
**DLSS (NVIDIA GeForce RTX).** NVIDIA's DLSS SDK is not included: check out
[github.com/NVIDIA/DLSS](https://github.com/NVIDIA/DLSS) (`include/` and `lib/Linux_x86_64` are
enough) and build with `DLSS_SDK_ROOT=/path/to/DLSS bash build.sh`. That also builds the bridge
`out/libbbport_dlss.so` (`gpu/dlss_bridge`, the only code that uses the SDK) and copies NVIDIA's
`libnvidia-ngx-dlss.so.<version>` next to `out/bb-probe`; the driver provides the rest. Then pick
*DLSS* in the launcher or the in-game menu: the preset sets the render size, Native AA is DLAA.
DLSS gets the inputs FSR 3.1 gets (scene color, depth, motion vectors, jitter). Without the
libraries, on other GPUs or with `BB_DLSS=0` it is listed as unavailable, and a DLSS setting falls
back to FSR 3.1.
On Linux, DLSS also needs NVIDIA's CUDA/UVM runtime. If device creation fails with the DLSS
extensions, the renderer retries without them and uses FSR 3.1. If `/dev/nvidia-uvm` is missing,
load the installed driver module with `sudo modprobe nvidia_uvm` and restart the game. Installing
the distribution's `nvidia-modprobe` package lets NVIDIA's libraries load the module when needed.
If `modprobe` reports `Key was rejected by service` with Secure Boot enabled, enroll the
module's signing key using the distribution's [MOK procedure](https://documentation.ubuntu.com/security/security-features/platform-protections/secure-boot/#mok-generation-and-signing-process).
The AppImage built with `DLSS_SDK_ROOT` carries the bridge and NVIDIA's library (NVIDIA's RTX SDK
license allows that in an application; its text is `NVIDIA-DLSS-LICENSE.txt` in the package). A
newer `libnvidia-ngx-dlss.so.<version>` (from the SDK's `lib/Linux_x86_64/rel`) is chosen in the
launcher: *Upscaler → DLSS: NVIDIA's library → Choose a file…*, as the FSR 4.1.1 DLL; it is
copied to `<user>/dlss/`, which the game prefers. A game's `nvngx_dlss.dll` is the Windows
library: NVIDIA's Linux driver does not load it.
From source the script takes its tools from the system (MinGW GCC, CMake, Ninja, Python 3,
SPIRV-Tools, Git) or from Nix; the AppImage has them prebuilt.
**FP8 on RDNA4.** When vkd3d-proton offers FP8 cooperative matrices (RDNA4), the DLL runs
another variant of its passes: other shaders and weights, other dispatch sizes. The build records
both, INT8 into `fsr4_411/` and FP8 into `fsr4_411/fp8/`, and the game picks FP8 itself when the
GPU has FP8 matrices (`VK_EXT_shader_float8`); the log says `Upscaler: FSR 4.1.1 replay, FP8 …`.
`BB_FSR411_VARIANT=int8` forces INT8. Checked on an RX 7800 XT through vkd3d-proton's FP16
emulation of FP8 (`BB_FSR4CAP_FP8=1` records it there too, `fp8emu/`): bit-exact with the DLL;
**not yet run on RDNA4 hardware.**
**AppImage** (Steam Deck): `bash build.sh && bash packaging/appimage.sh` →
`dist/Bloodborne-bbport-x86_64.AppImage`; data in `~/.local/share/bbport`, `--play` starts the
game without the launcher window (Game Mode). FSR 4.1.1 models are not packaged: build them with
the launcher's button (see above); they go to `~/.local/share/bbport/fsr4_411`
(`BB_PACKAGE_FSR411=1` bundles a local `fsr4_411` into an AppImage for your own devices). On the Steam Deck pick the 1280×720 output (the
game is 16:9; on the 1280×800 screen it gets thin bars).
**Adding the AppImage to Steam** (*Add a Non-Steam Game*) needs no options; the compatibility tool
does not matter. (Steam preloads its overlay into every non-Steam game; the AppImage removes it
before its own programs start, so the Steam overlay is not shown in the game.) Where Steam
runs games without FUSE (NixOS: Steam's FHS sandbox; the AppImage then exits with *Cannot mount
AppImage*), set the launch options to
```
TMPDIR=$HOME/.cache APPIMAGE_EXTRACT_AND_RUN=1 NO_CLEANUP=1 %command%
```
The AppImage then unpacks itself (~2 GB, `~/.cache/appimage_extracted_*`) on the first start
(~10 s) and reuses that copy afterwards; a new AppImage version gets a new copy, the old one can
be deleted. Without `TMPDIR` it would unpack into Steam's `/tmp`, which is in RAM there. Add
` --play` after `%command%` to skip the launcher.
**NVIDIA in the AppImage:** startup discovers the host's installed 64-bit NVIDIA Vulkan ICD
and exposes its vendor libraries alongside the bundled AMD/Intel drivers. This keeps the
NVIDIA userspace driver matched to the host kernel module. Standard Linux distributions keep
these libraries under `/usr/lib*`; on NixOS the AppImage's internal `/nix/store` may hide them.
In that case copy the NVIDIA libraries into an accessible directory and set
`BB_NVIDIA_LIB_DIR=/path/to/libraries` (the NVIDIA manifest must also be accessible).
Explicit `VK_DRIVER_FILES`/`VK_ICD_FILENAMES` overrides are preserved. Diagnose drivers inside
the package with:
```bash
./Bloodborne-bbport-x86_64.AppImage --vulkan-info 2>&1 | tee bbport-vulkan.log
```
A user reported successful startup with FSR 3 on a GTX 1060 6GB (Fedora 44, NVIDIA
580.178.04); selecting FSR 4 caused a black window. Use FSR 3 on this configuration.
MangoHud is bundled in the AppImage; enable its checkbox in the launcher. If MangoHud is also
installed system-wide, or Steam's performance overlay is on (Steam Deck game mode), only one
overlay is drawn (two drew doubled, offset text).
When running from source, install MangoHud separately. A diagnostic launch with
`VK_LOADER_LAYERS_DISABLE=~implicit~` also disables MangoHud.
Useful variables: `BB_FRAME_STATS=1` (frame statistics, including a `Memory:` line: VRAM, GTT,
RSS, images and guest blocks in VRAM), `BB_PC_MODEL=1` (the new memory and translation model,
0, the old model as in 0.3, is the default), `BB_ANISO=N` (anisotropic filtering of scene textures; 16 by
default, 0 = the game's own),
`BB_GC_IDLE_SECONDS=N` / `BB_VRAM_IDLE_SECONDS=N` (how long unused textures / buffers stay in VRAM;
20 / 60), `BB_BREADCRUMBS=0` (no GPU breadcrumbs; with them a GPU hang names the draw or dispatch
it is stuck in), `BB_GPU_PROFILE=1` (GPU time per
pass), `BB_FSR4_PROFILE=1` (GPU time per FSR 4 pass), `BB_UPSCALER=taa|fsr3|fsr4|fsr411|off|none`,
`BB_FRAMES_AHEAD=N` (how many frames the GPU command thread may run ahead of the GPU; 1 by default,
0 = unbounded), `BB_PRESENT_THREAD=0` (present on the vblank thread, as before),
`BB_LIVE_RES=1` (live resolution changes instead of the startup patch for outputs other than 1080p),
`BB_PAD_RECORD=file` / `BB_PAD_REPLAY=file` (record a route with F9, replay it in scripted tests),
`BB_GC_BUDGET_MB=N` (texture cache budget, as on integrated GPUs), `BB_VRAM_LIMIT_MB=N` (the VRAM
budget taken as at most N MiB: checks the behaviour of cards with little VRAM), `BB_ASYNC_PIPELINES=0`
(new pipelines compile at once and the game waits; by default in the background), `BB_PRESENT_DUMP_TRIGGER=file`
with `BB_PRESENT_DUMP_COUNT=N` (dump N consecutive presented frames),
`BB_FSR411_VARIANT=int8|fp8|fp8emu` (FSR 4.1.1 variant; by default FP8 where the GPU has FP8
matrices), `BB_READBACKS=0|1|2` (reads of GPU-written memory by the game: 1 by default, 2
precise and slow, 0 off).
More in [docs/](docs); recent changes: [docs/CHANGES_0.4.md](docs/CHANGES_0.4.md) (in Russian),
[docs/CHANGES_2026-10-06.md](docs/CHANGES_2026-10-06.md).
**Online play (shadNet).** A separate module, `out/gpu/libbbnet.so` (`gpu/bbnet`: shadPS4's network
and PSN libraries from shadp2p, a shadPS4 fork, with the shadNet client), built when protobuf,
nlohmann_json, OpenSSL, zlib and miniupnpc are there (`shell.nix`) and included in the AppImage.
The launcher's *Online* page turns it on (`BB_ONLINE=1`): messages, bloodstains and ghosts come
from The Hunter's Dream, bells and summons go through a shadNet server (`srv.shadps4.net:31313` by
default) with a shadNet account (Online ID and password). Offline the module is never loaded; a
build without it plays offline only.
## Repository layout
| Path | Contents |
|---|---|
| `src/` | Loader (`probe.c`) and the HLE runtime |
| `scripts/` | Offline preparation of the game image, module linking, patch compiler |
| `gpu/` | Renderer library: vendored shadPS4 video core with this port's changes (`gpu/VENDOR.txt`), shims, ImGui menu, FSR 4.1.1 runtime (`gpu/shadps4/video_core/renderer_vulkan/fsr411`) |
| `launcher/`, `packaging/` | GTK4 launcher; Nix package and AppImage |
| `patches/` | Community patches for Bloodborne |
| `tools/` | Developer tools: scripted runs, A/B toggles, FSR benchmark helpers, FSR 4 shader rewrites, `fsr4cap` (FSR 4.1.1 recording/extraction) |
| `tests/` | Loader, runtime, patch and renderer tests |
| `docs/` | Design notes and measurements ([upscaler](docs/upscaler.md), [parallel GPU](docs/parallel_gpu.md), [motion vectors](docs/motion_vectors.md), [roadmap](docs/ROADMAP.md)) |
Tests: `bash build.sh --test`, `python3 -m unittest discover -s tests`, and
`ninja -C out/gpu motion-history-test ui-composition-test scene-resolution-test motion-shader-test settings-test`.
## Roadmap
- The new memory and translation model on by default once it is stable and tested on NVIDIA;
the old model is removed after that.
- Textures as objects created at load time (less guessing by address, uploads outside the
frame).
- Shaders translated ahead of time, at install, not during play.
- More CPU parallelism in GPU command processing (split the draw-recording stage further),
scaling to all hardware threads — most important for the Steam Deck.
- Async compute for the upscaler (the frame is GPU-bound at 4K).
- XeSS (super resolution) and XeFG frame generation through a Wine helper sharing Vulkan
memory (a memory-bridge prototype is in `tools/bridge_helper`); inputs exposed so that OptiScaler-style mapping works.
- Frame generation (FSR 3.1 FG first), reactive and transparency masks for particles and fog.
- Fix the races in AMD's FSR 4.1.1 shaders at output widths that are not multiples of 64
(e.g. 1600×900), as already done for the left-edge race in FSR 4 v07 at 1080p.
- Steam Deck validation of the AppImage; HDR output.
## Credits and licenses
bbport is licensed under the **GNU GPL v2 or later** ([LICENSE](LICENSE)) — it contains code
from shadPS4 (GPL-2.0-or-later). Third-party components keep their licenses:
[shadPS4](https://github.com/shadps4-emu/shadPS4) video core and shader recompiler (GPL-2.0+),
[sirit](https://github.com/shadps4-emu/sirit), [half](https://half.sourceforge.net/),
[FSR-Vulkan](https://github.com/FireBurn/FSR-Vulkan) by FireBurn (MIT; FSR 3.1 on Vulkan and the
FSR 4 v07 provider), AMD FidelityFX SDK (MIT), [LibAtrac9](https://github.com/Thealexbarney/LibAtrac9)
(MIT), [Dear ImGui](https://github.com/ocornut/imgui) (MIT), DejaVu fonts,
[dxil-spirv](https://github.com/HansKristian-Work/dxil-spirv) (MIT, used to build the
FSR 4.1.1 assets). Game patches by Kyo, Lance McDonald, auser1337, illusion, emoose and other
community members (`patches/Bloodborne.xml`). The DLSS bridge (`gpu/dlss_bridge`, MIT) and
loader come from [Supermedo's Windows port](https://github.com/Supermedo/bloodborne_pc), adapted
from [IFreemz/shadPS4-Bloodborne-DLSS-FSR](https://github.com/IFreemz/shadPS4-Bloodborne-DLSS-FSR).
AMD's FSR 4 DLLs and model data and NVIDIA's DLSS SDK and libraries are not distributed here.
+12 -1
View File
@@ -2,7 +2,10 @@
# bbport — нативный порт Bloodborne для Linux
[English](README.md) · **Русский**
> README оригинального проекта (deadinside28/bloodborne_pc), сохранён ради технических
> подробностей. README этого форка — [README.md](README.md) (на английском).
[English](BBPORT.md) · **Русский**
bbport — это аналог Wine + DXVK, только для одной игры: *Bloodborne* для PlayStation 4
(CUSA03173, версия игры 1.09) на ПК с Linux x86-64. Оригинальный исполняемый файл игры
@@ -256,6 +259,14 @@ Experimental от октября 2026; Proton 11.0 из Steam и GE-Proton 9 —
векторы движения, jitter). Без библиотек, на других видеокартах или с `BB_DLSS=0` он недоступен,
а выбранный DLSS заменяется на FSR 3.1.
На Linux DLSS также нужен CUDA/UVM из драйвера NVIDIA. Если устройство не создаётся с
расширениями DLSS, рендерер повторяет попытку без них и использует FSR 3.1. Если отсутствует
`/dev/nvidia-uvm`, загрузи установленный модуль командой `sudo modprobe nvidia_uvm` и перезапусти
игру. Пакет `nvidia-modprobe` из репозитория дистрибутива позволяет библиотекам NVIDIA загружать
модуль по мере необходимости.
Если при включённом Secure Boot команда `modprobe` сообщает `Key was rejected by service`,
зарегистрируй ключ подписи модуля по [процедуре MOK своего дистрибутива](https://documentation.ubuntu.com/security/security-features/platform-protections/secure-boot/#mok-generation-and-signing-process).
AppImage, собранный с `DLSS_SDK_ROOT`, содержит мост и библиотеку NVIDIA (лицензия RTX SDK от NVIDIA
это разрешает в составе приложения; её текст — `NVIDIA-DLSS-LICENSE.txt` в пакете). Более новая
`libnvidia-ngx-dlss.so.<версия>` (из `lib/Linux_x86_64/rel` в SDK) выбирается в лаунчере:
+146 -375
View File
@@ -1,408 +1,179 @@
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-2.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 x86-64 code
runs directly on the CPU and its graphics are translated to Vulkan. It is not an emulator.
**English** · [Русский](README.ru.md)
This fork builds on [bbport](https://github.com/deadinside28/bloodborne_pc), the native Linux port
by deadinside28. It adds the features of [Supermedo's Windows port](https://github.com/Supermedo/bloodborne_pc)
(online play, DLSS, cheats) brought to Linux, plus a few of its own.
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, and which have been checked](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.
## Features
One step remains to the full Wine + DXVK model: the new model on NVIDIA as well, then removing the
old memory model (see below).
> **No game files are included.** You need your own dump of Bloodborne with update 1.09: any retail
> release (CUSA00900 in the US, CUSA03173 for the European Game of the Year edition, ...), as they
> share the same executable.
> This project is not affiliated with Sony Interactive Entertainment, FromSoftware or AMD.
**Status: experimental, playable.** The game boots, loads saves and plays (the Hunter's Dream
and several areas of Yharnam were played with it) with sound, gamepad and saving.
A full play-through has not been verified, and only one machine (Linux, AMD Radeon RX 7800 XT,
Mesa/RADV) has been tested thoroughly.
## Highlights
- **Native execution.** The eboot is converted offline into a flat memory image; PS4 libc and
libSceFios2 are linked into it as native code. No CPU emulation and no per-instruction
translation: the game code runs at full speed.
- **Two memory models** (launcher → *Mode → Memory and translation model*; *Auto*, the default,
picks by GPU):
- **The old memory model, as in 0.3 — the default on NVIDIA, Intel and others.** VRAM copies
of the game's memory, writes tracked through page protection. With every fix made since 0.3
(motion vectors and the upscaler, flicker with DoF on, a damaged shader cache).
- **The new memory and translation model — the default on AMD.** The game's memory lives
in system RAM and the GPU reads it where it is, as a PC game's buffers; data it reads often
is kept in VRAM and given back when unused (textures after 20 s, buffers after 60 s). The
layer's memory module tracks CPU writes to mirrored blocks to keep those copies coherent;
it does not copy the whole GPU-visible memory. The PS4 command processor's work
is translated into Vulkan commands rather than emulated on the CPU: memory writes
(`WRITE_DATA`, DMA) are done by the GPU in command-stream order, and fences are written by the
GPU itself at the end of the pipeline, at the same point of the stream as the PS4's command
processor writes them. On AMD the game's memory is kept in a sparse buffer (the arena); on
NVIDIA, Intel and others it goes through the layer's memory module: bound in place, VRAM
copies as separate buffers, no sparse rebinding (NVIDIA's sparse binding froze the GPU for
seconds); `BB_LAYER_MEMORY=1/0` chooses by hand. Tested on an RX 7800 XT. On NVIDIA it can be chosen by hand (*New*); testers' results differ by scene,
some scenes are slower than with the old model, so it is not the default there yet. Not
tested on Intel. Without the launcher: `BB_PC_MODEL=1/0` (unset: by GPU, as *Auto*).
[Changes in 0.5](docs/CHANGES_0.5.md); the prereleases: [pre4](docs/CHANGES_0.5-pre4.md),
[pre3](docs/CHANGES_0.5-pre3.md), [pre2](docs/CHANGES_0.5-pre2.md).
Unused textures are freed in both modes, so VRAM no longer grows with every area visited.
- **Unlocked frame rate.** Community patches (`patches/Bloodborne.xml`) make the simulation
use the real frame time; ~90 FPS at 4K with FSR 4 Balanced on an RX 7800 XT, ~150 FPS at
1440p with FSR 4 Quality. Also 30/60/90 FPS modes.
- **Temporal upscaling built for this game.** Bloodborne has no velocity buffer, so bbport
computes motion vectors itself: camera motion from depth and the scene matrices, and object
motion (characters, cloth, weapons) from the vertex positions of the previous frame. The
scene is jittered sub-pixel (Halton) and rendered at a reduced resolution; the upscaler fills
the output (720p for the Steam Deck, 1080p, 1440p or 2160p) and the UI is drawn natively at the output resolution.
- **FSR 3.1** (FireBurn/FSR-Vulkan).
- **FSR 4 (INT8, model v07)** on GPUs exposing the required Vulkan shader features —
RDNA2/3 included (see Requirements).
- **FSR 4.1.1**: AMD's 4.1.1 DLL is recorded once under vkd3d-proton and its passes are
replayed natively on Vulkan; the output is **bit-exact** with the DLL. Two variants, as in
the DLL: INT8 on any GPU with the required shader features, and FP8 matrices on RDNA4 (RX
9000; picked automatically). The assets are built on your machine from one DLL of your own
(4.1.x): the launcher's *FSR 4.1.1 from your own AMD DLL → Choose DLL…* button (2–5 minutes,
twice that on RDNA4; needs a recent Proton), or `tools/fsr4cap`.
- Faster than AMD's own shaders on RDNA3: the final passes of FSR 4 and 4.1.1 were rewritten
to store through workgroup memory (3.5× and 2.3× faster, bit-exact); FSR 4 costs ~4 ms at
4K on an RX 7800 XT instead of ~6 ms.
- **Multi-threaded GPU command processing.** The PS4 command stream is decoded on one thread
and draws are bound and recorded on another (two-stage pipeline), with a Vulkan recording
thread and helper threads for memory copies. Early on the single GPU thread capped the game
at ~26 FPS; now it runs at 90–150 FPS depending on resolution and scene.
- **Settings inside the game's own menu:** *System → Display / Effects* (after *Screen/Sound*),
built and drawn by the game itself — output resolution, upscaler, preset, sharpness, FPS
counter, model detail and the game's effects, in the game's language.
- **Overlay menu** (Insert or L3+R3), styled like the game's dialogs: everything above plus the
advanced settings, with gamepad, keyboard and mouse.
- **GTK4 launcher** and an **AppImage** for the Steam Deck.
## How it differs from shadPS4
| | shadPS4 | bbport |
|---|---|---|
| Scope | General PS4 emulator, many games | One game: Bloodborne v1.09 |
| Loading | Its own ELF loader and kernel emulation at run time | The eboot is converted offline (`scripts/`) into an image with PS4 libc/Fios2 linked in; a C loader maps it and jumps into the game (loader and runtime: ~5k lines) |
| Memory | The GPU's view of PS4 memory is kept in VRAM copies, synchronized through page-protection write tracking | By default the same model (as in 0.3); in the *New memory and translation model* mode: the game's memory in system RAM, used by the GPU in place, frequently read data in VRAM, freed when unused |
| Command processor | Emulated: memory writes, DMA and fences are done by the CPU while decoding | By default the same; in the new mode translated into Vulkan commands that the GPU runs in stream order, fences written after the work has really finished |
| System libraries | Broad HLE of the PS4 OS | A small runtime (`src/runtime_*.c`) that implements exactly what Bloodborne calls: memory, threads, sync, files, audio (incl. ATRAC9), pad, saves, AppContent |
| GPU | shadPS4 video core and shader recompiler | The same core (vendored, GPL) with ~200 marked changes (`bbport:`) plus new modules: two-stage draw pipeline, render-state and texture-set memoization, render-scale proxies, motion vectors, FSR 3.1/4/4.1.1, frame capture and GPU profiler |
| GPU thread | One thread processes the whole command stream (the bottleneck in Bloodborne) | Decode and draw recording run on separate threads; the work scales with the hardware threads (Steam Deck included) |
| Upscaling | — | Temporal (FSR 3.1, FSR 4, FSR 4.1.1) with the game's own motion vectors and jitter |
| Game patches | Patch files applied by the emulator | The same community patches, compiled at start (`scripts/patches.py`); render resolution, effects and FPS from the launcher |
Without shadPS4 there would be no bbport: its renderer and shader recompiler are the base of
the graphics side.
### Wine + DXVK for one game
- **CPU.** The PS4 CPU is x86-64, so the game's code runs directly on the PC's CPU, with no
emulation and no instruction translation.
- **System libraries.** As Wine replaces the Windows API, the bbport runtime (`src/runtime_*.c`)
implements exactly the PS4 OS functions Bloodborne calls: memory, threads, files, audio, pad,
saves.
- **Memory.** In the new mode, as in a PC game: the game's data in system RAM, VRAM used the way
a PC game uses it. The mode is still experimental.
- **Graphics.** The PS4 GPU's command stream (PM4) is a recording of the game's graphics API
calls: it is written by 99 functions of the statically linked libGnm, so decoding the stream
and translating the calls themselves come to the same thing. GCN shaders are translated to
SPIR-V. In the new mode the command processor's work (memory writes, DMA, fences) is
translated into Vulkan commands as well; fences are written by the GPU itself. Resource
descriptors (textures, buffers) are read by the translator on the CPU, as DXVK reads resource
bindings: a PS4 texture has to be converted from the PS4 tiling into a Vulkan image before the
draw runs, so the CPU must know its descriptor beforehand — translation, not emulation. The
descriptor tables (the constant engine's dumps) are read only by the translator, so the CPU
performs them. The game's occlusion queries (~60 a frame) are
Vulkan occlusion queries; predication, `COPY_DATA` and `COND_EXEC` are translated too (check:
`BB_PM4_SELFTEST=1`). Details:
[docs/EMULATION_REMOVAL_PLAN.ru.md](docs/EMULATION_REMOVAL_PLAN.ru.md), "Шаг 4" (Russian).
"Port" here means a build for this one game, not a rewrite of its source code, which the project
neither has nor includes.
- **Online play (beta)** through [The Hunter's Dream](https://thehuntersdream.com): messages,
bloodstains, ghosts, bells and summons, with bbhost's players and Windows players of bbport, or
with your group on [a server of your own](docs/SERVER.md).
- **Upscaling:** AMD FSR 3.1, FSR 4 and FSR 4.1.1, **NVIDIA DLSS** on GeForce RTX, and
native-resolution TAA.
- **Unlocked frame rate**, capped at your display's refresh rate up to 120 FPS (above that the
game's own movement timing breaks), or 30/60/90 FPS. Output from 720p to 4K.
- **Launcher** with every setting in one window, in English, Spanish, Italian, French, German,
Portuguese, Russian and Simplified Chinese, like the in-game menu (Insert or L3+R3).
- **Settings in the game's own System menu** as well (Display and Effects pages), in the game's
language.
- **Two memory models:** the new one, where the GPU works with the game's memory as in a PC game
(the default on AMD), and the one of 0.3 (the default elsewhere); the launcher switches them.
- **Xbox, Switch and keyboard button prompts**, picked by the controller in use, and a gestures
menu without the motion-control icons.
- **Cheats and gameplay tweaks:** never die, stealth, silent steps, enemy control, camera
distance, ragdoll physics and more.
- **Trophies:** a notice in the game, in the style of the PS4's, and a Trophies page in the
launcher with every trophy, from your own dump. Their names and descriptions need the PS4's
trophy key (shadPS4's TrophyKey), which you enter in the launcher; without it, only the icons.
- **Game effects** on and off, **mods** and community **patches**, loaded without changing the
game files ([how](docs/MODS.en.md)).
- **Add to Steam** in one click (native or Flatpak Steam), and **Discord Rich Presence** with the
area you are in and your co-op partners.
- **AppImage:** one file with everything inside, for any distribution and the Steam Deck. It
updates itself: the launcher's **About** page checks this repository's releases and installs
a new AppImage over the old one.
## Requirements
- Linux x86-64, a Vulkan 1.3 GPU. Tested: AMD RX 7800 XT with Mesa 26 (RADV).
The *New memory and translation model* mode is not tested on NVIDIA yet.
FSR 4 / 4.1.1 require shader Float16, Int8/Int16, integer dot products, linear compute
derivatives and extended storage image formats; FSR 4.1.1 additionally requires
`VK_VALVE_shader_mixed_float_dot_product`. Unsupported choices fall back to FSR 3.1
before the first frame and are disabled in the in-game menu.
- Your decrypted game dump: the `CUSA03173` folder (eboot.bin, sce_module, ...), version 1.09.
A dumped update is a separate folder: copy it over the base game, replacing files. The base
game alone (1.00) crashes at start (guest offset 0x20348b8); the launcher and `run.sh` check
the executable and say what is missing (`BB_SKIP_GAME_CHECK=1` skips the check). The other
retail releases (the US `CUSA00900`, the Asian `CUSA03023`, ...) run the same 1.09 executable. Some dumps ship it
with Lance McDonald's 60 fps patch baked in, which crashes when the gestures menu opens: the
check asks for a clean eboot.bin then (bbport has its own 60 fps).
- To build: GCC, CMake, Ninja, Python 3, glslang, SDL3, Vulkan headers and the libraries in
`shell.nix`. With [Nix](https://nixos.org) everything comes from `shell.nix` automatically.
- Linux x86-64 and a GPU with Vulkan 1.3: AMD or Intel with Mesa, or NVIDIA with NVIDIA's driver
(open kernel modules included). Mesa's NVK has not been tested.
- DLSS needs a GeForce RTX (20 series or newer) and NVIDIA's driver; it does not work with NVK.
- About 6 GB of free memory, 10 GB for 1440p or 4K output.
## Build and run
## Getting started
### AppImage
Build it with `bash build.sh && bash packaging/appimage-sharun.sh`, then run
`dist/Bloodborne-bbport-x86_64.AppImage`. Choose your game folder (the one with `eboot.bin`) and
press **Play**. Saves and settings go to `~/.local/share/bbport`.
### From source
On Arch Linux (or CachyOS, Manjaro…):
```bash
git clone --recursive https://github.com/deadinside28/bloodborne_pc.git bbport && cd bbport
bash build.sh # builds out/bb-probe and out/gpu/libbbgpu.so
BB_GAME_DIR=/path/to/CUSA03173 bash run.sh
sudo pacman -S --needed base-devel cmake ninja python python-gobject gtk4 libadwaita sdl3 \
vulkan-headers vulkan-icd-loader glslang spirv-headers spirv-cross ffmpeg boost fmt \
robin-map xxhash zydis miniz protobuf nlohmann-json openssl miniupnpc python-pillow ttf-dejavu
paru -S magic_enum vulkan-memory-allocator xbyak # from the AUR
git clone --recursive https://github.com/ayozetr/bloodborne_linux
cd bloodborne_linux
bash build.sh
python3 launcher/bbport_launcher.py # or launcher/install-desktop.sh for a menu entry
```
or the launcher (pick the game folder, settings, *Start*):
With [Nix](https://nixos.org), `bash build.sh` gets every dependency from `shell.nix` by itself.
```bash
bash launcher/bb-launcher.sh # launcher/install-desktop.sh adds it to the app menu
```
### Online play
By default the game folder is expected next to the repository (`../CUSA03173`). Saves and the
shader cache go to `user/` (the launcher lets you choose another folder); settings to
`bbport.ini`. A gamepad is used through SDL3 (the launcher's *Controls → Controller* picks one
when several are connected; `BB_GAMEPAD=<GUID or part of the name>`). The keyboard works too,
also next to a connected gamepad (the Steam Deck always has one); both are remapped in the
launcher (*Controls*). The character name is typed on the keyboard in a box over the game.
The touchpad: its left half (Tab, Back/Select) opens the gestures, the right half (Backspace)
the key items. There is no PSN: the *Play online / offline* screen is skipped and the game opens
its main menu offline (a game patch; the launcher's *Game effects → Skip the “play online / offline” choice*,
`BB_SKIP_NETWORK_CHOICE=0` shows it). With several monitors, *Screen → Monitor* in the launcher
(`BB_DISPLAY=<number or part of the name>`) picks the one for the game.
1. In the launcher, open **Online**, turn on **Play online** and choose the co-op server:
- **The Hunter's Dream (with bbhost players)**: sign in with Discord, or create an account
with just a name and keep the recovery code it shows (it brings the account back on
another PC). [bbbridge](bbbridge/README.md) by MaskZeus runs with the game and talks to
The Hunter's Dream the way bbhost does, so you play with bbhost's players and with Windows
players of bbport on the same pool.
- **Your own server**: your group's shadNet server and your account on it
([docs/SERVER.md](docs/SERVER.md), Docker and Tailscale; tested with a Linux and a Windows
player).
2. **Test the connection**: every line should be green.
3. Start the game and choose online at the title screen.
**Resolution and preset changes:** for outputs other than 1080p (720p on the Steam Deck,
1440p, 4K) the whole game renders at the preset's resolution, set by a patch at start — the
fastest path. Changing the output or the preset in the in-game menu then needs *Apply and
restart the game*. The *Live resolution changes* setting (launcher, in-game menu,
`bbport.ini` `live_resolution=0|1|auto`; **off by default**) instead keeps the game at
1080p internally and scales its render targets at run time, so 720p/1080p/1440p/4K and the
presets switch without a restart. It costs more: the game then believes it renders 1080p
and draws more (e.g. ~8× more small lights), and some targets are copied between sizes —
use it on strong desktop GPUs only (`auto` turns it on for discrete GPUs with 8+ GB that are
not pre-Turing NVIDIA). 1080p output and TAA always use the live path.
Everyone in a session needs game version 1.09 and the same pool. Turn cheats off when you play
with others. shadPS4's public shadNet server no longer accepts bbport builds: it signs them out
right after the login, and the launcher says so.
**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.
With your own server, co-op connects the players directly over UDP. UPnP opens the port on the router by itself; with
ufw or firewalld on, the launcher's **Online** page says so and gives the commands: pin the
**Co-op port** (for example 3658), open it (`sudo ufw allow 3658/udp`), and for UPnP let the
router's answers in (`sudo ufw allow from <router> proto udp`). [More in docs/ONLINE.md](docs/ONLINE.md).
**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.
### DLSS
**Launcher language:** Russian, English, Brazilian Portuguese or Simplified Chinese (follows the system language by default).
On a PC with a GeForce RTX, run `bash tools/build_dlss.sh` once after `build.sh`. It downloads
what it needs from the NVIDIA DLSS SDK and builds the bridge. Then choose **DLSS** as the
upscaler.
**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.
### DLC
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.
The Game of the Year edition (`CUSA03173`) already includes The Old Hunters. With the base game
(such as the US `CUSA00900`), dump your add-ons too: each one is a folder (`SPEXPANSIONDLC03`,
`SPDLCMESSENGER00`…) with only `sce_sys` inside, because their data ships with the 1.09 update.
Put them in one folder and pick it in the launcher (**Game → DLC folder**), or set
`BB_DLC_DIR=/path/to/dlc` for `run.sh`; the launcher shows how many it found. Each folder must
keep the add-on's name (its entitlement label, such as `SPEXPANSIONDLC03`). Copying them to
`user/addcont/<title id>/`, shadPS4's layout, works too.
**Upscaler assets** (not included; FSR 3.1 needs none):
### Add to Steam
```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>
```
**Game → Add to Steam** puts Bloodborne in your Steam library; it starts with the launcher's
settings. Steam has to be closed while the game is added, and the launcher offers to do that.
Flatpak Steam runs games inside its sandbox, which has neither the game nor your drivers, so
the shortcut starts the game outside it with `flatpak-spawn --host`. That needs the
`org.freedesktop.Flatpak` permission for Steam, which the launcher asks for once: it lets Steam
start programs on your system.
**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>`.
### Button prompts
**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 game shows PlayStation buttons. In the launcher, **Controls → Button prompts** switches them to
Xbox, Nintendo Switch or keyboard ones; *Automatic* follows the controller in use, and the
keyboard's when none is connected. Keyboard prompts show the keys bound in **Controls →
Keyboard**. **Gestures menu without motion-control icons** cleans up the gestures menu. The
prompts are put into your own game's menu files at each start, and nothing in the game folder
changes.
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.
## Support
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.
If this fork is useful to you and you would like to support it, you can on
[Ko-fi](https://ko-fi.com/ayozetr). The port itself is deadinside28's work (see below).
**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.**
## Credits
**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).
- [deadinside28/bloodborne_pc](https://github.com/deadinside28/bloodborne_pc): **bbport**, the
native Linux port this fork is built on, and almost all of the work behind it. Its README, with
the technical details, is in [BBPORT.md](BBPORT.md).
- [Supermedo/bloodborne_pc](https://github.com/Supermedo/bloodborne_pc): the Windows port, whose
online play, DLSS support, cheats page, Add to Steam and icon were brought to Linux here.
- [shadPS4](https://github.com/shadps4-emu/shadPS4): the renderer and shader recompiler.
- [shadp2p](https://github.com/Wozzardman/shadp2p) by Wozzardman: the network and PSN libraries
for online play. Servers by the [shadPS4](https://shadps4.net) team and
[The Hunter's Dream](https://thehuntersdream.com) by droogie and its team.
- [bbbridge](https://github.com/MaskZeus/bbbridge) by MaskZeus (GPL-3.0-or-later): The Hunter's
Dream's co-op pool for bbport, built for Linux here ([bbbridge/](bbbridge/)).
- [IFreemz/shadPS4-Bloodborne-DLSS-FSR](https://github.com/IFreemz/shadPS4-Bloodborne-DLSS-FSR):
the DLSS bridge, and the [NVIDIA DLSS SDK](https://github.com/NVIDIA/DLSS).
- [FSR-Vulkan](https://github.com/FireBurn/FSR-Vulkan) by FireBurn and the AMD FidelityFX SDK.
- **Dommo**: the Xbox and Switch button prompts (*DS3 Style - Modern Xbox prompts*, *DS3 Style -
Nintendo Switch prompts*) and the *Stripped down gesture menu*, from
[Nexus Mods (Bloodborne, mod 30)](https://www.nexusmods.com/bloodborne/mods/30), uploaded by
goomab. Only their drawn prompts are in this repository ([assets/menu_prompts](assets/menu_prompts)).
- **Abken**: the [KBM Icons Generator](https://www.nexusmods.com/bloodborne/mods/537), which showed
which textures keyboard prompts need (after the idea of
[mod 460](https://www.nexusmods.com/bloodborne/mods/460)); bbport draws its own.
- [quick-sharun](https://github.com/pkgforge-dev/Anylinux-AppImages) for the AppImage.
- [ps4-pkg-extractor](https://github.com/paulomanrique/ps4-pkg-extractor) by paulomanrique
(Unlicense), on [orbis-pkg-util](https://crates.io/crates/orbis-pkg-util): the launcher's
*Install from PKG*.
- Game patches by Kyo, Lance McDonald, auser1337, illusion, emoose and the Bloodborne community.
**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
Full list of components and licenses: [BBPORT.md](BBPORT.md#credits-and-licenses).
```
TMPDIR=$HOME/.cache APPIMAGE_EXTRACT_AND_RUN=1 NO_CLEANUP=1 %command%
```
## License
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 v2 or later ([LICENSE](LICENSE)). Third-party components keep their own licenses. NVIDIA
and DLSS are trademarks of NVIDIA Corporation.
+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 (tools\make_release.ps1)
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>.
+92
View File
@@ -0,0 +1,92 @@
# bbbridge (C++)
Play **co-op with bbhost players** on **The Hunter's Dream** while using
**bbport** (Supermedo's `bloodborne_pc` for Windows, DLSS / FSR 3.1 / FSR 4).
bbbridge looks like a shadNet server to bbport and talks to The Hunter's Dream
the way bbhost does: matching (rooms, bells, summons), the P2P traffic between
the two clients (different packet framing, bbport's signaling handshake,
The Hunter's Dream's STUN and relay), and the game's own server traffic
(account token, play log for the live map). bbport itself is not modified.
## Layout
```
E:\bloodborne\ bbport
└─ bbbridge\ this folder
├─ bbbridge.exe the app (built)
├─ bbbridge_config.json settings (optional)
├─ bbbridge_state.json your account: token + recovery code - never share it
├─ bbbridge.log log of the last run (previous: bbbridge.prev.log)
├─ src\ C++ sources
├─ tests\ unit tests (C++) and end-to-end tests (Python)
├─ third_party\ nlohmann/json 3.11.3, Dear ImGui 1.91.5
└─ bin\ bbbridge-cli.exe (headless), bbbridge-unit.exe
```
<img width="798" height="718" alt="resim" src="https://github.com/user-attachments/assets/672a0038-4ead-4572-9293-b6c3936ed128" />
## Using it
1. Put the `bbbridge` folder inside bbport's folder (next to `Bloodborne.exe`)
and start `bbbridge.exe` (close the bbport launcher first). bbbridge uses
the bbport it sits in; anywhere else it asks which bbport to use
(**Change...** picks another one later).
2. **Sign in with Discord**: the app shows a code and opens The Hunter's Dream;
sign in there with your Discord account and approve the code. Your account
there (the same one bbhost uses) becomes bbport's online id. No password is
stored; the account's token goes to `bbbridge_state.json`.
3. The **Online** switch goes to **The Hunter's Dream (bbhost)** by itself
once you are signed in (and the live map patch is added; untick it if you
like).
4. Press **PLAY**. The app starts the bridge if needed and launches the game
with bbport's saved settings. You can also start the game from the bbport
launcher - as long as bbbridge is open. Graphics, mods and cheats stay in
the bbport launcher (**bbport settings...**) or the in-game menu (Insert).
Move the **Online** switch to **shadNet** to go back to bbport's own shadNet
co-op pool: bbport gets its own server and shadNet account back (the ones it
had before; a shadNet account is registered at
[shadnet.shadps4.net](https://shadnet.shadps4.net) and entered in the bbport
launcher's *Online co-op*). PLAY follows the switch: right, the bbhost pool
with your Discord sign-in; left, bbport's own shadNet - no sign-in needed.
Running bbhost and bbport with the same account at the same time is not
supported.
## Troubleshooting
- **"Failed to acquire Bloodborne server information" with bbbridge closed**:
bbbridge 0.3.1 and older (and the Python 0.2.0) left
`user\host_overrides.custom.json` in bbport's folder (bbport's user folder),
which sends the game's server traffic to bbbridge even when it is not
running. Start bbbridge 0.3.2 or newer once (it removes the file when it is
not needed), or delete that file by hand. From 0.3.2 the file exists only
while bbbridge runs with the switch on The Hunter's Dream.
- **Nobody comes when I ring a bell**: a Small Resonant Bell is answered only
by a host ringing the Beckoning Bell in the same area, near your level (and
the other way round). The live map at
[thehuntersdream.com/map](https://thehuntersdream.com/map) shows who is
ringing which bell where.
## Building (Visual Studio 2022/2026)
Workload **Desktop development with C++** (MSVC, Windows SDK, CMake tools).
- Visual Studio: *File > Open > Folder...* this folder, pick the **x64 Release**
preset, *Build > Build All*. `bbbridge.exe` is copied here, the CLI and the
unit tests to `bin\`.
- Command line (Developer PowerShell):
`cmake --preset x64-release` then `cmake --build --preset x64-release`
## Tests
- `bin\bbbridge-unit.exe` - framing, signaling, STUN, protobuf, account id.
- `python tests\e2e\test_e2e.py` - the bridge exe against a fake bbhost
server, a fake STUN/relay server and fake game sockets (no internet).
## Credits and license
The protocol comes from **bbhost** and The Hunter's Dream by droogie (GPL-3.0);
**bloodborne_pc / bbport** by Supermedo on deadinside28's port and shadPS4.
bbbridge is GPL-3.0-or-later. nlohmann/json and Dear ImGui are MIT-licensed.
Not affiliated with Sony, FromSoftware, Supermedo or The Hunter's Dream.
+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, 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
+394
View File
@@ -0,0 +1,394 @@
"""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
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
+763
View File
@@ -0,0 +1,763 @@
#!/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, 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"])
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
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
Loaded 100 of 244 files, more files were not shown because too many files have changed in this diff. Show more